Skip to content

feat: canonical upload metadata vocabulary#361

Merged
Zach Dunn (zachdunn) merged 7 commits into
mainfrom
claude/upload-metadata-fields-209639
Jul 21, 2026
Merged

feat: canonical upload metadata vocabulary#361
Zach Dunn (zachdunn) merged 7 commits into
mainfrom
claude/upload-metadata-fields-209639

Conversation

@zachdunn

Copy link
Copy Markdown
Member

Gives uploads a small canonical metadata vocabulary — mostly derived — so that
uploads find path=/settings state=after still works months after the shot was
taken and the filename stopped meaning anything.

The vocabulary

Ten keys. Eight are derived; two are worth your keystrokes.

Key Source Example
url auto — screenshot target https://app.example/settings
path auto — pathname, query stripped /settings
env auto — local only local
theme auto — only when forced dark
viewport auto — capture opts or EXIF dims+DPI 1280x800@2x
device auto — image EXIF Apple iPhone 16 Pro
software auto — image EXIF Figma
captured auto — image EXIF 2026-07-20T20:35:39
state you--state before | after
app you--app web | ios

screenshot knows its own target, so it stamps the first five. put/attach
read each image's EXIF before the optimizer strips it and promote an
allowlist. --state is the highest-value thing a human or agent can add, since
before/after is the dominant pattern in PR screenshots and nothing can infer it.

env is only ever local. Inferring prod from "not localhost" would mislabel
every staging and preview URL, and wrong metadata is worse than none.

Two behavior changes worth reading before merging

EXIF that was previously discarded now becomes public. device and
software render on the public /f/ page. This is deliberate and allowlisted —
all GPS tags, body/lens serial numbers, Artist/Copyright/OwnerName and
free-form user comments are never promoted, and that denial is tested directly.
But it is a real escalation and shouldn't be merged without a nod.

Derived metadata makes re-uploads destructive. apps/api/src/files-core.ts
full-replaces a key's metadata whenever any is sent. Derived keys count, so a
re-upload that derives something now replaces a set that was previously left
untouched. This matches the pre-existing gh.* tier rather than being new in
kind, and it's pinned by a regression test. --no-auto is the opt-out.

Notes on the shape

  • The MCP schema was teaching the wrong keys. METADATA_DESCRIPTION
    suggested page and resolution — two of the exact near-miss spellings the
    CLI now warns about. Every agent using the MCP server had been told to use
    them. That's plausibly a real source of the inconsistency this fixes.
  • --no-git deliberately does not disable the derived tier, though an
    earlier draft of the design said it should. Implementing it that way, a test
    helper reached for --no-git — the natural choice outside a repo — and
    silently got zero metadata. --no-git means "don't shell out to git" and says
    nothing about a viewport. gh.* still requires git, checked separately.
  • Near-miss keys (route=, page=) warn and suggest the canonical spelling but
    are never rewritten — a wrong guess is worse than a nag.
  • Derived keys are dropped first under the 24-key cap; caller keys are never
    dropped, and a full budget never fails an upload.

Verification

2134 tests pass across the monorepo; typecheck and lint clean. Beyond unit
tests, the built binary was pointed at a local listener to confirm the real
wire format — a 1624×1154 @144dpi PNG produces viewport=812x577@2x alongside
state and app as X-Uploads-Meta-* headers. exif-reader's output shape
was verified against real EXIF bytes rather than assumed, and pinned with a
round-trip test, since the hand-built fixtures would have passed either way.

The final commit is a cleanup pass that closed two gaps found by review:
uploads attach --state/--app was parsed, documented, and recommended by the
skill but never read — a silent no-op; and EXIF promotion was wired into one
caller rather than the shared upload tail, so MCP and attach derived nothing
while the tool description already claimed otherwise. Both now go through
uploadPreparedImage.

Not included

Title/description metadata was designed and then cut as not-yet-needed, so the
"nicer PR comments" payoff isn't here. A follow-up issue for surfacing path
and state in the managed 📎 comment is drafted and still needs filing.

…XIF facts

Three pure modules that later tasks wire in:
- metadata-vocab: the ten canonical keys, the state enum with near-miss
  suggestions, and a capped merge that drops derived keys before explicit ones.
- capture-facts: url/path/env/theme/viewport from a screenshot target. env is
  only ever local; inferring prod from 'not localhost' would mislabel staging.
- image-facts: an EXIF allowlist (viewport/device/software/captured). GPS,
  serial numbers and personal-name tags are excluded by construction.

Adds exif-reader; sharp exposes the raw EXIF buffer but does not parse it.
Both are sugar for the matching --meta keys; the value is --help
discoverability and fail-fast validation of --state. A near-miss key like
route= or page= now warns and suggests the canonical spelling, but is never
rewritten — a wrong guess is worse than a nag.
screenshot now derives url/path/env/theme/viewport from what it captured.

Adds derivedMetaEnabled() as the single switch for the derived tier, shared
with gh.* auto-resolution. It intentionally ignores --no-git: that flag means
'don't shell out to git', which says nothing about a viewport or a URL path,
and gating on it silently produced zero metadata for captures outside a repo.
gh.* still requires git, checked separately at its own call site.
METADATA_DESCRIPTION suggested 'page' and 'resolution', two of the exact
near-miss spellings the CLI now warns about — the schema in front of every
agent call was teaching the wrong vocabulary. It now names the canonical keys
and points at path as the one to search by.

state is enum-constrained in the schema and re-validated in the handler, since
a schema is a hint rather than an enforcement boundary.
put now reads each file's EXIF before the optimizer strips it and promotes the
allowlist into queryable metadata. Per-file by construction, so one image's
camera never labels another's upload.

Resolves the spec's open question against apps/api/src/files-core.ts:313: any
metadata sent on a put fully replaces that key's stored set. Derived keys count,
so a re-upload that derives anything now replaces where it previously preserved
— matching the existing gh.* tier. Pinned with a regression test and called out
in the changeset; --no-auto is the opt-out.

Also retires the page=/app= examples from CLI help, which taught a spelling the
CLI now warns about.
The new flags had been inserted between --meta and its wrapped explanation,
so the re-upload note read as if it described --app.
Cleanup pass over the vocabulary change. Two capability gaps it closes:

- `uploads attach --state/--app` parsed the flags, documented them in
  ATTACH_HELP and completions, and the github-screenshots skill recommended
  them — but nothing ever read them. The flags were a silent no-op.
- EXIF promotion was wired into one caller (uploadPuts' loop) rather than the
  shared bytes tail, so MCP put/screenshot and CLI attach derived nothing,
  while the shared MCP metadata description already claimed uploads.sh derives
  these "automatically where it can". Moved into uploadPreparedImage and
  uploadAttachmentBatch; MCP screenshot now derives capture facts too.

Dedup: metadataArgWithCanonical replaces three near-identical MCP merge
blocks; dropUnsafeMetaValues and warnNearMissMeta replace copies in
capture-facts/image-facts and runPut/runScreenshot/runAttach; MCP state
validation now delegates to validateStateValue, so MCP callers get the same
near-miss suggestions the CLI gives instead of a generic enum list.

safeCaptureFacts moves to capture-facts.ts and is shared by both surfaces.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (2)
  • coderabbit:review
  • review
🚫 Excluded labels (none allowed) (1)
  • wip

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 946ea706-6808-4f5f-8840-34085730e751

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/upload-metadata-fields-209639

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-auth 18773f6 Commit Preview URL

Branch Preview URL
Jul 21 2026, 09:31 PM

@cloudflare-workers-and-pages

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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-api 18773f6 Commit Preview URL

Branch Preview URL
Jul 21 2026, 09:31 PM

@cloudflare-workers-and-pages

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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-web 18773f6 Commit Preview URL

Branch Preview URL
Jul 21 2026, 09:31 PM

@zachdunn
Zach Dunn (zachdunn) merged commit 963ea12 into main Jul 21, 2026
6 checks passed
@zachdunn
Zach Dunn (zachdunn) deleted the claude/upload-metadata-fields-209639 branch July 21, 2026 21:48
Zach Dunn (zachdunn) added a commit that referenced this pull request Jul 21, 2026
…d docs (#364)

Follow-ups to #361, found by auditing the surfaces it did not touch.

The hosted MCP worker had its own hardcoded copy of the metadata tool
description, so it still told every connected agent to use 'page' and
'resolution' — the exact near-miss spellings the CLI now warns about, and the
thing #361 set out to fix. It now composes the shared METADATA_DESCRIPTION.
Its put tool also gains state/app; it cannot derive the EXIF keys (no image
decoding in the Workers runtime), so those two are its whole canonical surface.

The public docs said 'EXIF metadata is stripped'. That is now only half the
story: EXIF still leaves the uploaded bytes, but an allowlist is promoted into
metadata that renders publicly. Reworded so the privacy claim matches
behavior, with the denials and the --no-auto opt-out spelled out.

Also fixes a pre-existing missing space before the Reference link (the Astro
whitespace collapse, live since #254) in the paragraph next door.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant