| D-M3-06-1 |
2026-10-20 |
files.scan keeps its read pass; for clean and skipped its final transaction leaves a JPEG, PNG, GIF or WebP upload_state = 'pending' and inserts files.thumbnail with InsertTx; MP4 and QuickTime files become complete and get the job; other types and blocked or failed files get none. The image becomes complete only in T1 of the job, which stores its cleaned original, and only T1 or the failure transaction publishes files.FileFinalized for it. image/apng (the detected name of an animated PNG) takes the PNG filter; the container types are video/mp4, video/quicktime, video/x-m4v, audio/mp4 and audio/x-m4a as the pinned detection names them. Place: domain.OriginalPlanFor, domain.HasContainerMetadata, app.ScanPass.write, 12 2 and 3.4. |
12 3.4 set complete before the processing of 7, a window in which the original with location data was deliverable and final for a held agent invocation; 12 7.1 and SEC-UP-10 say images are free of metadata as soon as they are stored. Without the APNG name its text chunks would reach the store unfiltered. |
| D-M3-06-2 |
2026-10-20 |
The JPEG filter keeps every marker T.81 needs to decode (SOI, DQT, DHT, DAC, DRI, DNL, DHP, EXP, SOF0 to SOF15 except JPG, SOS with its data and RST0 to RST7, EOI), the first APP0 JFIF rewritten with the same version, units and densities but without its embedded thumbnail, every APP2 ICC_PROFILE part in input order, APP14 Adobe, and one new APP1 with only "Orientation"; JFXX, other APP0, every other APPn, COM, JPGn, reserved markers and every byte after the first EOI fall away. Output order: SOI, APP0, APP1, then the other kept segments in input order. Stray bytes between segments are dropped like fill bytes; a second SOI is a broken structure. Place: adapters/imaging/jpeg.go, 12 7.1. |
An embedded thumbnail can show what a crop removed, the reason EXIF thumbnails fall away in ADR 0047; the decoder needs exactly the T.81 markers. |
| D-M3-06-3 |
2026-10-20 |
The orientation comes from the first APP1 Exif\0\0; an unreadable block or a value outside 1 to 8 yields 1 and does not reject the image. The new APP1 is big-endian with IFD0 holding only "Orientation" and no sub-IFD, written only for the values 2 to 8. Place: adapters/imaging/exif.go. |
The block falls away anyway; value 1 needs no tag. |
| D-M3-06-4 |
2026-10-20 |
A PNG with a CRC mismatch, a chunk beyond the input, IHDR not first or no IEND is a broken structure; bytes after IEND fall away; eXIf falls away like every unlisted chunk and its orientation is not applied to the thumbnails. Place: adapters/imaging/png.go, 12 7.1. |
Broken structure in the sense of 12 7.1; original and thumbnails show the same pixels. |
| D-M3-06-5 |
2026-10-20 |
A still WebP within file.image.max_pixels is decoded, its EXIF orientation applied to the pixels, and stored as PNG (not opaque) or JPEG with file.image.jpeg_quality, with its ICC profile unchanged when the profile fits the output (an RGB profile; otherwise none) and no orientation tag; mime_type becomes image/png or image/jpeg, original_name keeps its extension. A GIF or still WebP above the limit or failing to decode, a malformed GIF or WebP, and every animated WebP stay unchanged as revision 1: T1 sets complete without a new object and without thumbnails. Place: adapters/imaging/codec.go, app.ImageProcessor.keepOriginal, 12 7.1 and 7.2. |
12 6.1: the extension decides nothing. 12 7.2 does not say what happens to the original of such a file (spec gap); keeping it is the only choice without libvips. |
| D-M3-06-6 |
2026-10-20 |
Thumbnails: the first frame of an animated GIF and the default image of an APNG; all three variants for every decodable image, also when they end up the same size; PNG when the decoded image is not opaque, otherwise JPEG with file.thumbnail.jpeg_quality; resampler x/image/draw.CatmullRom (scaling before the turn of the orientation); the colour profile of the original only when its data colour space fits the thumbnail (RGB for colour, GRAY for gray), so CMYK input carries none; a PNG profile larger than the APP2 capacity of a JPEG means no thumbnails. Place: adapters/imaging/render.go, 12 7.3. |
12 7.1: the app shows medium in the chat and large in full screen, so both always exist; RK-41 names CMYK as a fidelity risk; a profile of another colour space is wrong for the output. |
| D-M3-06-7 |
2026-10-20 |
A JPEG or PNG the filter refuses ends upload_state = 'failed' with file_upload_sessions.failure_reason = 'image_malformed', file.updated and files.FileFinalized, without an audit entry; orig-1 and any orig-2 are deleted after the commit. Place: app.ImageProcessor.failMalformed. |
A code without content next to checksum_mismatch and size_mismatch of 12 3.4; 05 audits file.upload only on a malware finding. |
| D-M3-06-8 |
2026-10-20 |
Width, height and duration of an MP4 or QuickTime file are written in T3 only where a value is new, with one file.updated. Place: app.ImageProcessor.containerPhase. |
The event covers every change of the File payload (12 2). |
| D-M3-06-9 |
2026-10-20 |
files.width and files.height of images and videos are display sizes after the EXIF orientation or the tkhd matrix of the first video track (swapped for 90 and 270 degrees); the duration is mvhd duration over timescale in milliseconds, else mehd; any box error leaves the fields absent and never blocks the file. Place: adapters/isobmff, 12 7.3. |
Clients lay out the original from them, and thumbnails have the same aspect; the values are display hints, not checks. |
| D-M3-06-10 |
2026-10-20 |
File.thumbnails lists the rows of file_thumbnails in the order small, medium, large, poster with variant, mime_type, width and height; an empty list while there are none. getFile and the payload of file.updated use the same mapping. Place: app.FileView. |
One order for every reader. |
| D-M3-06-11 |
2026-10-20 |
Cross-check thumbnail_sizes_ordered: file.thumbnail.small_px below file.thumbnail.medium_px below file.thumbnail.large_px; the error names the keys only. file.image.jpeg_quality and file.thumbnail.jpeg_quality have the bounds 1 to 100 of Go's jpeg.Options. file.image.libvips_enabled and file.poster_max_bytes are not registered. Place: internal/wire. |
Three equal variants would be pointless; the unbuilt keys would promise behaviour that does not exist. |
| D-M3-06-12 |
2026-10-20 |
Every processed original is revision 2 (orig-2), also when the filter changed no byte; files.object_key points to it in T1, revision 1 is deleted after the commit and no object is overwritten. domain.ParseOriginalKey (M3-01) already returns the revision, so no OriginalRevision function was added. Place: app.ImageProcessor, 12 4.4. |
One code path; the revision tells a re-encoded GIF from an unchanged one, the marker M3-05 uses for inline delivery. |
| D-M3-06-13 |
2026-10-20 |
Build: first built on an integration branch, then landed on main as its own branch with the commits of this ticket (D-M3-06-16). Names that differ from the brief: the ports live in app/imageports.go and the use case in app/images.go (ImageProcessor.ProcessImage); the per-process limiter is in the use case, not the job adapter; domain.Variant already existed (M3-01); HandlerOptions.Timeout already existed (M3-04). |
The brief allows the names of the merged code. |
| D-M3-06-14 |
2026-10-20 |
Retries: a JPEG is read three times (structure scan, filter into SHA-256 and counter, filter into Put(orig-2)), PNG twice; the second filter pass fails before its last byte when size or SHA-256 differ, so the store keeps nothing and River retries. A run that finds a complete file deletes the original revisions below its own and a run that finds a failed JPEG or PNG deletes its originals again, so a failed Delete after a commit is repaired by the retry. |
12 3.2 and 4.2 keep the server free of spool files; 12 12 deletes failed objects at once. |
| D-M3-06-15 |
2026-10-20 |
Red team round 1: a WebP whose VP8X chunk follows the bitstream is a broken structure; the scan refuses VP8X after an image chunk or twice. Re-review fixes: the GIF block scan reads every extension exactly like image/gif (plain text with 13 fixed bytes, graphic control with 6, application identifier of any length including 0, unknown labels malformed), and a losing concurrent scan keeps the original of an image the winner left pending. Place: adapters/imaging/webp.go, gif.go, app/scan.go. |
x/image/webp decodes the first bitstream at its own size and never reads a later VP8X, and image/gif read frames the old scan did not count; either let an image above file.image.max_pixels be decoded (SEC-UP-4). |
| D-M3-06-16 |
2026-10-22 |
Landing on main: the table file_thumbnails comes from the migration 00056_files_thumbnails.sql of M3-05 (same columns, keys and checks), so this ticket adds no migration; its grant keeps UPDATE for bothy_app, which the job never uses (it only inserts with ON CONFLICT DO NOTHING and deletes). The two declarations of app.ThumbnailRecord are one type (Variant as domain.Variant, plus the FileID of the download read), and the scan uses domain.IsDeliverable of M3-05. The download tests of M3-05 upload a valid PNG, wait until upload_state leaves pending and for the three variants before they replace one, and ask a ticket of the missing variant poster. the generated sqlc models and the configuration reference are generated again, the spec text is written into the English chapters (12 2, 3.4, 3.5, 7.1 to 7.3, 02 12.3 and the sequence diagram seq-7), and the expected output of bothy config print lists the eight new keys. The waits of the tests of this ticket use testkit (Until, Receive) instead of own deadlines and sleeps: the concurrency tests hold the runs that took a slot until the test saw them enter (the integration test also until the other job runs in River), and the run-twice test compares after the job is completed in River instead of after a fixed pause. |
The migration numbers must stay consecutive; tests must not depend on the speed of the machine. |