Skip to content

fix(photos): the ingest regenerates semantics.json, the one derivation it skipped - #662

Merged
oddharsh merged 2 commits into
mainfrom
fix/photos-semantics-ingest
Aug 29, 2026
Merged

oddharsh merged 2 commits into
mainfrom
fix/photos-semantics-ingest

Conversation

@oddharsh

Copy link
Copy Markdown
Owner

The reported defect no longer exists, and I checked before changing anything

The task said public/images/semantics.json is stale by 7 stems (XT500084, XT500088, XT500132, XT500138, XT500144, XT500183, XT500189). Re-measured on origin/main at f7ed4916:

hashes 165 semantics 165
missing from semantics: []
extra in semantics: []
XT500084 PRESENT  XT500088 PRESENT  XT500132 PRESENT  XT500138 PRESENT
XT500144 PRESENT  XT500183 PRESENT  XT500189 PRESENT

All seven carry records. #609 (b7526da8) refilled them a week ago, so no backfill is owed and nothing here needs an owner-run step or a credential.

What it is, and what a missing stem costs

Live data, read by the Worker. queryPhotos (src/worker/photos.ts:388) loads images/semantics.json and ranks its terms as the expansion field at weight 3, under alt at 5 and film at 6. It widens what the photo search matches past the one-sentence caption and the EXIF, so a stem missing from it stays findable one tier down. The failure is a search result that is quietly worse, never an error. Not dead data, and not deletable.

Three of the four asks were already done, by #609

ask state
1. backfill the 7 stems done, 165/165
3. assert every published stem carries a record done, and better than asked
4. declare it in config/derivations.json with an input digest done, id: images/semantics, set-mode inputs, covers
2. wire it into the ingest still open, and this PR

I am deliberately NOT adding the assertion to check-photo-pipeline.ts. covers in config/derivations.json already asserts exactly it: every input key projected by the set regex must have an entry in the output, computed live rather than recorded, which the declaration's own $comment calls "the one verdict --lock cannot clear". derive:check runs in validate on every PR at ci.yml:107, immediately after photos:check at 97.

A second copy would contradict a rule that file states explicitly, in the block listing what is deliberately left out:

That is stronger than a digest, and a second check over the same artifact is a second thing to keep in agreement.

Control: the existing check has teeth

Deleted XT500144's record and ran the gate.

$ bun run derive:check
  STALE images/semantics
      outputs: public/images/semantics.json
      NO ENTRY for (1): XT500144
      regenerate: node tools/photos/gen-photo-semantics.ts   # add --vision for the model tier
derive: 5 fresh, 1 stale, 6 unverifiable, 0 undeclared
a stale artifact describes bytes this site no longer serves. regenerate it, then --lock.
error: script "derive:check" exited with code 1

Exit 1, the stem by name, the fix command. photos:check on the same tree exited 0 and said 165 photos … complete EXIF + histogram + alt-text metadata, which is the blindness the task describes, and it is covered one gate over rather than absent. Reverted; semantics.json diffs byte-identical to origin/main.

What was actually still broken

add-photos.sh runs five of the six stem-keyed generators and never ran the sixth:

derivation reached by
images/hashes hash-thumbnails.sh
images/fingerprints hash-thumbnails.sh:150
images/histograms extract-photo-metadata.sh:274 + the zenc bake
images/alt the gen-alt-text.py block
images/semantics nothing

#609 fixed the producer and left the hole. It found 158 of 165, traced it to gen-photo-semantics.ts joining a www/ the 2026-08-18 split deleted (gotcha 40), repaired the joins and re-ran it. Nothing then made the ingest call it, so the next photo add reopens the gap. That is caught now instead of silent, and it arrives as a red derive:check on somebody's PR with a command to go find, which the ingest should have run itself.

The change

One node call, after the captioner and before the validator, plus its argument in comments and the phase-6 header line.

Ordering is a real constraint rather than tidiness: the derived tier folds alt[stem] into terms at gen-photo-semantics.ts:206, so captioning second leaves a new stem carrying camera vocabulary and no subject.

It is mandatory rather than tolerated, which is where it differs from the gen-alt-text.py block above it. That one is wrapped in || echo "captions incomplete" because a 429 against the free neuron budget is expected. This tier reads metadata.json, hashes.json and alt.json, needs no credential and no network, so it cannot be rate-limited and set -e should stop the run. --vision stays opt-in and is not passed.

Control: the wiring refills

Removed XT500183 and XT500189 (163 stems), then ran the committed line verbatim with the script's own variable, which also proves the path expression resolves:

$ grep -n 'gen-photo-semantics.ts"$' tools/photos/add-photos.sh
637:node "$PROJECT_DIR/tools/photos/gen-photo-semantics.ts"

$ PROJECT_DIR=<worktree> node "$PROJECT_DIR/tools/photos/gen-photo-semantics.ts"
wrote public/images/semantics.json — 165 stems, 0 with vision terms
exit=0

$ diff -q <origin/main copy> public/images/semantics.json
REFILLED byte-identical to origin/main

Separately, running it against the untouched library is a no-op: 165 stems, git diff empty, byte-identical, 95ms. So the step adds no diff to an add that changes nothing.

Gates, from this worktree after bun install --frozen-lockfile

gate result
bun run photos:check 0 — 165 photos, 660 hashed tiers, index in bijection
bun run derive:check 0 — 6 fresh, 0 stale, 6 unverifiable, 0 undeclared, census 36 of 36
bun run build 0 — 55 documents, 112 CSP hashes, 160 dcz deltas
bun run lint 0
bun run typecheck 0 — baseline 501 owed, unchanged, 300 source files, all held
bun test 0 — 520 pass, 0 fail, 72 files
bun run tools:check 0 (zenc absent locally, advisory presence tier, predates this branch)
bun run pages:check 0
bash -n tools/photos/add-photos.sh 0

Nothing lands red. The library is complete on main and stays complete.

…n it skipped

add-photos.sh runs five of the six stem-keyed generators and never ran the
sixth. hashes and fingerprints go through hash-thumbnails.sh (the second at
line 150), histograms through extract-photo-metadata.sh line 274 plus the zenc
bake, alt text through the gen-alt-text.py block. images/semantics had no
caller at all, which is how seven photos drifted out of it for a week.

#609 fixed the producer and not the hole. Its `covers` verdict found
semantics.json at 158 of 165, traced it to gen-photo-semantics.ts joining a
www/ the 2026-08-18 split deleted (gotcha 40), repaired the joins and re-ran
it. Nothing then made the ingest call it, so the next photo add reopens the
gap and lands on derive:check as a red PR with a command to go find.

It goes after the captioner because the derived tier folds alt[stem] into
`terms` (gen-photo-semantics.ts:206). Captioning second leaves a new stem with
camera vocabulary and no subject, and that failure is quiet by construction:
queryPhotos still finds the photo through the alt field at weight 5, one tier
above expansion at 3, so the search result is worse rather than absent.

Cheap and mandatory rather than tolerated. The derived tier reads
metadata.json, hashes.json and alt.json, so unlike gen-alt-text.py it cannot be
rate-limited and needs no credential; `set -e` stops the run instead of
degrading it. Measured here: 95ms over 165 stems, and re-running the whole
library is byte-identical, so the step adds no diff on an add that changes
nothing. --vision stays opt-in and is not passed.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
aadhar-sh d8ecf5c Aug 29 2026, 01:38 PM

@github-actions github-actions Bot added area: photos The photo pipeline and everything it emits type: fix Corrects behaviour that was wrong labels Aug 29, 2026
@github-actions

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Wire-size diff vs merge base

4e067bc3 → a3333234

Brotli q11, the quality the edge serves. Every number here is deterministic:
an unchanged file produces no row. Advisory — this check fails on nothing.

Client assets

No change. 25 files, 86.41 KiB Brotli total.

Pages

No change. 55 files, 581.17 KiB Brotli total.

Worker bundle

No change. 240.38 KiB gzip.

Dictionary deltas (pd/)

No change. 160 deltas, 513.08 KiB total.

@oddharsh
oddharsh enabled auto-merge (squash) August 29, 2026 13:38
@oddharsh
oddharsh merged commit 4b622b5 into main Aug 29, 2026
6 checks passed
@oddharsh
oddharsh deleted the fix/photos-semantics-ingest branch August 29, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: photos The photo pipeline and everything it emits type: fix Corrects behaviour that was wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant