mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
drop_closure exists to refuse a drop that is really a refactor: if code we keep still imports a product, deleting it is a ModuleNotFoundError waiting for the next boot. It only ever read Python, so half the dependency graph was invisible to it and five products whose TypeScript the core still imports were dropped anyway — 21 files left importing 25 specifiers that resolve to nothing, including lib/api.ts, which 760 files import, and seven lazy scene imports in productScenes.tsx. Django booted past it and the bundle did not, which is why the Python-only rule looked like it was working. The closure now reads both module systems through one table of (extensions, pattern) pairs, so a third one is a row rather than a branch. The TypeScript pattern is anchored on the import keyword rather than on the path, because jest's testPathIgnorePatterns and oxlint's ignore list quote a product path precisely because it is NOT built; matching those would hold desktop, the 4,797-file product the drop is mostly for. integrations, wizard, platform_features, visual_review and ai_gateway come back with their Django apps and workspace entries, since the closure now spares them. desktop, field_notes, ingestion and support stay dropped.
46 lines
1.9 KiB
Python
46 lines
1.9 KiB
Python
"""
|
||
Canonical image hashing for upload integrity verification.
|
||
|
||
Mirrors the CLI's pipeline (PNG → RGBA pixel buffer → BLAKE3) closely enough
|
||
to verify upload integrity. The CLI uses sharp's
|
||
``toColorspace('srgb').ensureAlpha().raw()``; for inputs without an embedded
|
||
ICC profile (the case for every screenshot Playwright/Storybook produce in
|
||
CI), Pillow's ``convert("RGBA")`` produces byte-identical pixel buffers.
|
||
|
||
Known limitation: PNGs with an embedded non-sRGB ICC profile (e.g. Display P3
|
||
macOS screenshots) hash differently between sharp's libvips engine and any
|
||
Pillow pipeline. Both engines transform but disagree at the byte level. We
|
||
accept this — those uploads will fail integrity verification with a clear
|
||
error rather than silently approve mismatched bytes. If real-world uploads
|
||
start carrying ICC profiles, the fix is to align both ends on a profile-strip
|
||
pipeline rather than chase engine parity.
|
||
"""
|
||
|
||
import io
|
||
|
||
from blake3 import blake3
|
||
from PIL import Image
|
||
|
||
# Cap decoded pixel count to bound memory. A 7000×7000 RGBA image is ~196 MB —
|
||
# plenty for screenshots, well under Pillow's default DecompressionBombWarning.
|
||
# Pillow raises DecompressionBombError above this (instead of just warning).
|
||
_MAX_PIXELS = 50_000_000
|
||
Image.MAX_IMAGE_PIXELS = _MAX_PIXELS
|
||
|
||
# Reject the PNG before decode if it's larger than this. Compressed PNGs above
|
||
# 64 MiB are almost certainly bogus for a screenshot upload.
|
||
_MAX_PNG_BYTES = 64 * 1024 * 1024
|
||
|
||
|
||
class ImageTooLargeError(Exception):
|
||
"""Uploaded image exceeds the size limit allowed for hashing."""
|
||
|
||
|
||
def hash_image(png_bytes: bytes) -> str:
|
||
"""Decode PNG to RGBA pixels and return the BLAKE3 hex digest."""
|
||
if len(png_bytes) > _MAX_PNG_BYTES:
|
||
raise ImageTooLargeError(f"PNG is {len(png_bytes)} bytes, exceeds {_MAX_PNG_BYTES} limit")
|
||
img = Image.open(io.BytesIO(png_bytes))
|
||
img.load()
|
||
return blake3(img.convert("RGBA").tobytes()).hexdigest()
|