Skip to content

feat(listing): define how a description image resolves after ingest - #47

Merged
justinmerrell merged 2 commits into
mainfrom
feat/listing-description-image-resolution
Aug 18, 2026
Merged

feat(listing): define how a description image resolves after ingest#47
justinmerrell merged 2 commits into
mainfrom
feat/listing-description-image-resolution

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

What changes

Listing §5 gains §5.1 Resolution after ingest, which names the mapping an ingesting consumer already holds — the published media set — and obliges it to resolve a description image through the same mapping it applies to icon and screenshots[].file (LIST-MEDIA-004). The mapping is keyed on the whole media path rather than on the basename, which is what lets LIST-MEDIA-003 keep stopping at the screenshot gallery and lets §4.1 keep exempting a description image from it; §5's rationale for that rule said "published assets are addressed by basename" and now describes the gallery it actually governs. A consumer that cannot resolve an image MUST omit it rather than substitute a remote URL — carried in §10 as well, because there it is a security rule rather than a resolution one. Three conformance cases pin the half of this the corpus can observe: a description image against the filesystem, which §4.1 has required since ADR 0004 and nothing tested.

Why

Closes #44.

§4.1 holds a description image to §5's media-path grammar and also binds the renderer. Both are right, and together they left no third spelling. icon and screenshots[].file are fields, so an ingesting storefront reads them, uploads the bytes, and rewrites each to a CDN URL. A description image is inside a Markdown blob — nothing enumerates it, so nothing rewrites it, and media/overview.png resolves against an item root that exists in the authoring repository and nowhere downstream. A conforming image was un-renderable; a renderable one was ERR_IMAGE_NOT_LOCAL. Left unstated, the resolution gets invented once per consumer and in private, which is the objection ADR 0004 opens with.

This is issue option 1. Option 3 turned out to already hold in code — mediaPaths() has always collected well-formed description images — but no fixture reached it: all five description cases are case.yaml cases, which get no itemRoot, so checkMediaOnDisk never ran against one. LIST-MEDIA-001/002 looked covered only because cases 004 and 005 exercise them through a screenshot and an icon. That gap is closed here.

No ADR. This closes a gap inside ADR 0004 §2's image rule rather than deciding new policy, and that section's table reads true afterwards — the same test e5a3f85 (#45) applied.

Compatibility

  • Additive — new optional field. A document valid before is still valid.

No previously valid document is affected: §5.1 constrains consumers, not documents. LIST-MEDIA-004 carries no diagnostic and no schema change; schemas/src/, schemas/dist/, and catalog.json are all untouched.

Checklist

  • task check passes locally (ci:test, ci:lint, check:types all exit 0)
  • schemas/dist/ regenerated with task bundle and committed (never edited by hand) — no schema change; check:drift confirms the bundles are byte-identical
  • Conformance fixtures added for every behavioural change, each citing a clause
  • Normative prose updated in the affected spec.md — schema descriptions are explanatory, not normative
  • Commit messages are Conventional and correctly scoped (the scope drives release-please)
  • Commits are DCO signed off (git commit -s)

Fixtures

Case Shape Outcome
semantic/013-description-image-absent description names media/diagram.png; the item does not ship it ERR_MEDIA_NOT_FOUND @ /spec/description
semantic/014-description-image-escaping-the-item escapes the root by symlink, declared in metadata.json per ADR 0002 §3 ERR_PATH_ESCAPE @ /spec/description
semantic/015-description-image-resolves names a file the item ships, the same one backing its screenshot passes

These pass on arrival. They are a corpus backfill rather than the usual red-then-green sequence, because the behaviour was already implemented and only the fixture was missing — worth saying plainly rather than implying CONTRIBUTING rule 2 was exercised. 013 was checked against a vacuous pass by shipping media/diagram.png into its tree and confirming the runner then reports it as validating cleanly.

The one rule with no case behind it

LIST-MEDIA-004 constrains an implementation's output, so no fixture can observe it — the limit ADR 0004 already recorded for §4.1's renderer clause. It goes in the runner's UNPINNED list with that reason rather than looking covered, and §9 records it as a debt of a kind that rejecting nothing will close. Removing the UNPINNED entry was verified to fail check:conformance with LIST-MEDIA-004 is declared … but no case cites it, so the ID is genuinely indexed.

Note for musher-dev/platform

The storefront's current behaviour — rendering alt text in place of the img — is conforming under §5.1 and stays conforming. What the section adds is that once the platform resolves description images through its existing media mapping, it MUST NOT fall back to a remote URL for one it cannot resolve.

🤖 Generated with Claude Code

§4.1 holds a description image to §5's media-path grammar, so a remote
image is unspellable, and it binds the renderer as well as the author. Both
are right. Together they left no third spelling: `icon` and
`screenshots[].file` are fields, so an ingesting storefront reads them,
uploads the bytes, and rewrites each to whatever it serves them from — but a
description image is inside a Markdown blob. Nothing enumerates it, so
nothing rewrites it, and `media/overview.png` resolves against an item root
that exists in the authoring repository and nowhere downstream. A conforming
image was un-renderable; a renderable one was ERR_IMAGE_NOT_LOCAL.

§5.1 names the mapping every such consumer already holds — the published
media set — and says a description image is entitled to it. The mapping is
keyed on the whole media path rather than on the basename, which is what
lets LIST-MEDIA-003 keep stopping at the screenshot gallery and lets §4.1
keep exempting a description image from it. §5's rationale for that rule
said "published assets are addressed by basename", which would have
contradicted the key chosen here; it now describes the gallery it governs.

The clause a consumer will feel is the last one: an image that will not
resolve is omitted, never made remote. Substituting a reachable URL for a
broken one is the *helpful* failure mode, which is why it needs saying — a
storefront doing it is repairing a page, not attacking anyone, and it
reopens the beacon §4.1 closed on a page the author no longer controls.
§10 carries it too, because that is where it is a security rule rather than
a resolution one.

LIST-MEDIA-004 constrains an implementation's output, so no fixture can
observe it — the limit ADR 0004 already recorded for §4.1's renderer clause.
It goes in the runner's UNPINNED list with that reason rather than looking
covered, and §9 records it as a debt of a kind that rejecting nothing will
close.

No ADR: this closes a gap inside ADR 0004 §2's image rule rather than
deciding new policy, and that section's table reads true afterwards.

No previously valid document is affected — §5.1 constrains consumers, not
documents.

Refs #44

Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
§4.1 has said since ADR 0004 that §5's semantic rules reach a description
image — that it MUST resolve to a file that exists and MUST lie inside the
item root, reported at /spec/description — and tools/src/semantic.ts has
implemented it: mediaPaths() collects every well-formed description image
destination alongside icon and screenshots[].file.

Nothing tested it. All five description cases are case.yaml cases, which get
no itemRoot, so checkMediaOnDisk never ran against one. LIST-MEDIA-001 and
LIST-MEDIA-002 looked covered because cases 004 and 005 exercise them
through a screenshot and an icon; coverage is per-requirement, not per
pointer, so the corpus was green over a rule it never reached.

Three tree cases close that:

  013  a description image the item does not ship  → ERR_MEDIA_NOT_FOUND
  014  one escaping the root by symlink            → ERR_PATH_ESCAPE
  015  one the item ships                          → passes

all reported at /spec/description. 015 references the same file as its
screenshot on purpose: one asset named twice is one entry in the published
media set, and basename uniqueness has nothing to say about it.

These pass on arrival. They are a corpus backfill rather than the usual
red-then-green sequence, because the behaviour was already implemented and
only the fixture was missing. 013 was checked against a vacuous pass by
shipping media/diagram.png into its tree and confirming the runner then
reports it as validating cleanly.

docs/traceability.md is regenerated: LIST-MEDIA-001/002 and LIST-MD-003 gain
the new cases, and LIST-MEDIA-004 appears with no case, as UNPINNED says it
should.

Refs #44

Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
@justinmerrell
justinmerrell merged commit 9c8238a into main Aug 18, 2026
6 checks passed
@justinmerrell
justinmerrell deleted the feat/listing-description-image-resolution branch August 18, 2026 10:28
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.

listing §4.1: a conforming description image has no defined resolution for a storefront that ingests items

1 participant