Files
zeekay 82120531a1 fix(debrand): read the TypeScript import graph before dropping a product
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.
2026-08-01 22:17:05 -07:00

46 lines
1.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()