harness check: report media references (image params, /media/ links) that resolve to no file #300

Closed
opened 2026-10-15 11:06:34 +02:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Summary

harness check reports every media reference whose file does not exist — a library reference (media/<folder>/<name>, or /media/<folder>/<name> in a link or in prose) with no file under assets/media/, and a bundle-local image reference with no such file in the page's bundle — so a deleted or renamed asset, or one committed by hand without its file, surfaces before the live site shows a broken image or a 404. The one rule deciding whether a library reference names a file gets one owner that the check, GET /api/link/resolve, GET /api/media/info, the media operations and the publish pre-flight all call.

Current behavior

No media reference is checked. Reproduced on a scaffolded site (2026-10-16): a leaf-bundle page content/about/index.md with an img row holding image="media/photos/missing.jpg", another holding image="missing-local.jpg", a markdown link and an inline anchor to /media/docs/gone.pdf in a text body, and a front-matter value brochure: /media/docs/gone.pdf gets no finding from harness check. The link check skips them on purpose (linkrefs.Classify yields Media for the /media/… form, and the bare media/… form is not root-relative). A missing library file does not fail the Hugo build either: the theme emits the raw reference as src (ce/img.html), so scaffold/site/AGENTS.md and the verify-site skill ask for a manual commit check instead.

"Does this library reference name an existing file" is evaluated four times today, in the server only: handleLinkResolve's media branch (internal/server/linktargets.go), handleMediaInfo (internal/server/media.go) and mediaops.resolveFile (internal/mediaops/paths.go) each call media.ResolvePath and stat the result; the publish pre-flight (checkExportedMediaRefs, internal/server/publishset.go) maps the ref with media.LibrarySitePath and stats it under the exported site.

Desired behavior

  • harness check walks every page and translation file under content/, draft pages included, and reports a warning in area content on the file holding the reference, once per distinct reference per file, for:
    • a library reference — any value linkrefs.Destinations yields that media.StoredLibraryRef recognises (prose links, string shortcode params at any depth, front-matter strings; the bare media/… form and the /media/… URL form alike) — whose file does not exist under assets/media/;
    • a bundle-local reference — the value of an image-typed param (by the element's loaded schema) that is a bare filename (media.IsBundleRef) — that is not a published file of the page's bundle, by the rule linkresolve already applies to a bundle sub-path link (a leaf bundle's files at any depth, a branch bundle's directly in its directory, never a .md file). A plain page (foo.md) has no bundle, so every bundle-local reference on it is dangling. A translation file's bundle is its source page's.
  • The message names the reference and what is missing, in the style of the link findings (links to <dest>, but …): no such file in the library; no such file in the bundle of <page permalink>; the page is not a bundle.
  • Existence is all that is checked: no type sniffing, no dimension reading. A PDF behind a link counts exactly like an image. A /media/… destination StoredLibraryRef rejects (/media/press/, a trailing slash, no extension, a query) draws nothing, as today; an empty value draws nothing.
  • One owner for "does this library reference name an existing file of this media root": a function in internal/media (name and signature the implementer's) that takes the media root and the reference and answers found / not found / invalid (the invalid answer stays ResolvePath's coded library.invalidRef). The check, handleLinkResolve's media branch, handleMediaInfo, mediaops.resolveFile and the publish pre-flight (with the exported site's media root) call it; no ResolvePath-plus-stat or LibrarySitePath-plus-stat pair remains outside it. Wire answers and status codes do not change.
  • The bundle-file rule is called, not re-derived: linkresolve exports what the check needs to ask "is this a published file of this page's bundle" for a page it already holds, or the check resolves <permalink>/<name> through Resolve; either way bundleDir and bundleFile stay the single evaluation.
  • The package doc of internal/checkcmd lists the check. docs/harness.md "Check" gains a Media. paragraph beside Links. (the warnings list names it too). scaffold/site/AGENTS.md "Verifying a change" names it, and its "Library media publishes with the pages that use it" and "Image values and uploads" bullets say harness check reports the dangling reference (today they say only that hugo will not catch it). The verify-site skill step 1 names the warning and what to do (restore or re-upload the file, or fix the reference), and step 5's "commit content together with the assets/media/ files" step points at the finding; the migrate-site skill's "after each section, harness check lists …" sentence and its section 6 (Media) include it; the edit-content skill's closing paragraph on verification signals includes it.

Decisions

Settled in triage on 2026-10-16; implement, do not reopen.

  • Library references are found by the one scan and the one shape rule that exist — linkrefs.Destinations and media.StoredLibraryRef — not by field type, so a link param, a front-matter value and a prose link are all covered and no second notion of "library reference" appears. Prose links are therefore checked although mediaops.PageRefs does not count them today; making rename, move, delete and publish-carry agree is #310, not this issue.
  • Bundle-local references are found by field type only: a bare filename is not recognisable by shape (IsBundleRef accepts any string without a slash), and image is the one field type whose value the theme resolves against the bundle (ce/img-res.html).
  • One owner in media for library-file existence — CLAUDE.md "Every rule has exactly one owner"; four evaluations exist and the check would be a fifth. The publish pre-flight joins the callers: it answers the same question about another root.
  • Bundle existence reuses linkresolve's rule, because a bundle-local image reference on a page is the sub-path case of a link to that page. No third evaluation of "which files does this bundle publish" (the server's pageBundleDir is a second one already and is out of scope).
  • Warning, not error — the site builds; a stale reference is "probably not what was meant" (docs/harness.md's severity rule), as for a dangling link.
  • Drafts and translations are checked like any file, on the file holding the reference, as the link check does.
  • One issue — the owner move, the check, its callers and the docs and skills land together.
  • No ADR — a check is reversible.

Key interfaces

  • linkrefs.Destinations, linkrefs.Classify, linkrefs.SplitTarget — the scan and the reading of a candidate; never a second scan or regex.
  • media.StoredLibraryRef, media.IsBundleRef, media.ResolvePath, media.LibrarySitePath, media.LibraryDir — the reference shapes and the file locations the new owner composes.
  • linkresolve.Resolver (bundleDir, bundleFile) — the bundle-file rule.
  • elemops.WalkRoles with the loaded element.Schema set and element.TypeImage — how the check reaches image-typed params; checkcmd already loads the schemas and walks every content file (content(), linkable, links() in internal/checkcmd/check.go).
  • checkcmd.Finding, checker.warnf, AreaContent — the finding shape; Counts and the --json shape stay as they are.
  • Callers to convert: handleLinkResolve (internal/server/linktargets.go), handleMediaInfo (internal/server/media.go), mediaops.resolveFile (internal/mediaops/paths.go), checkExportedMediaRefs (internal/server/publishset.go).

Acceptance criteria

  • The reproduction site gets five content warnings on content/about/index.md, one per distinct reference (the two spellings of /media/docs/gone.pdf in prose count once), each naming the reference and what is missing; with the files added under assets/media/ and the page's bundle, the report is clean; harness check exits 0 either way.
  • A library reference to an existing file, a bundle-local image reference to a file in a leaf bundle's subdirectory, a /media/press/ page link, a text param reading media/x, an empty image param, a .md file in a bundle and a relative value produce no finding.
  • A bundle-local image reference on a plain page, and one naming a file in a branch bundle's subdirectory, are reported.
  • A dangling reference in a draft page and in a translation file is reported on that file; a translation's bundle-local reference resolves against the source page's bundle.
  • GET /api/link/resolve (/media/…), GET /api/media/info, the media rename, move and delete operations and the publish pre-flight answer exactly as before (their tests pass unchanged), through the one owner; no ResolvePath-plus-stat or LibrarySitePath-plus-stat pair remains outside it, and no comment says a function mirrors or must stay in sync with another.
  • The documents and skills named under Desired behavior say what the engine now does; the drift tests (go test ./docs/ ./scaffold/ ./internal/element/ ./cmd/harness/ ./internal/server/) pass.

How to verify

  • go test ./internal/checkcmd/ ./internal/media/ ./internal/mediaops/ ./internal/linkresolve/ ./internal/server/ ./docs/ ./scaffold/ ./internal/element/ ./cmd/harness/ and go vet ./... (building ./... needs web/dist; the package tests do not).
  • The reproduction as a test in internal/checkcmd beside TestDanglingLinks: scaffoldSite, write content/about/index.md with the five references above, Run(Options{SiteDir: dir}); before the change the report is empty, after it holds the five warnings.
  • harness check --site <site> on a site after a library file was deleted by hand prints the reference on every page using it.

Assumptions to check

  • That linkrefs.Destinations yields an image param's value whole (it yields every string param), so the library-ref scan needs no second walk — the typed walk is only for bundle refs.
  • That Hugo resolves .Resources.GetMatch for a branch bundle only against files directly in its directory, as linkresolve.bundleFile assumes for sub-path links — check against the pinned Hugo before reporting a branch bundle's nested file as missing.
  • That the publish pre-flight's exported site has its library at assets/media/ under the export root, so the owner can take that root; regularFileAt is the place to read.
  • That elemops.WalkRoles visits every shortcode the check cares about, items rows included (img is an items child of image and text-media).

Out of scope

  • Prose /media/… links in the media operations (rename rewrite, delete refusal with links to the referencing places, publish-carry) — #310.
  • Consolidating the server's pageBundleDir onto linkresolve's bundle rule.
  • Checking the media type, dimensions or size of an existing file.
  • A thumbnail or bundle-listing change; any change to the --json shape of harness check beyond the new finding rows.
  • Links of any other kind (#195, landed in #301).

Blocked by

None - can start immediately

Original report

This was generated by AI during triage.

Summary

harness check verifies no media reference today: neither an image param (media/<folder>/<name> or a bare bundle filename) nor a /media/… link in prose, a link param or front matter is resolved against the library or the page bundle. A missing library file does not fail the Hugo build either (the verify-site skill says so and asks for a manual commit check), so a reference to a deleted or renamed asset surfaces only on the live site.

Context

Filed during the grilling of #195 (dangling page links in harness check), where /media/… links were deliberately left out: the rule "does this media reference exist" has its own owner (media.ResolvePath plus a stat, as /api/link/resolve and /api/media/info evaluate it) and is a separate check from page resolution. Open design points before this is agent-ready:

  • which references count: image params by field type, /media/… links of every shape linkrefs.Destinations yields, bundle-local filenames;
  • one owner for "does this media reference resolve", callable from internal/checkcmd and internal/server without a second implementation (CLAUDE.md "Every rule has exactly one owner");
  • severity (a warning, presumably, like the page-link finding) and the documents and skills to update: docs/harness.md "Check", scaffold/site/AGENTS.md "Verifying a change", the verify-site skill (its "commit content together with the assets/media/ files" step could then point at the finding) and migrate-site.
  • #195 — page-link check; its owner move is the pattern to follow.
> *This was generated by AI during triage.* ## Summary `harness check` reports every media reference whose file does not exist — a library reference (`media/<folder>/<name>`, or `/media/<folder>/<name>` in a link or in prose) with no file under `assets/media/`, and a bundle-local image reference with no such file in the page's bundle — so a deleted or renamed asset, or one committed by hand without its file, surfaces before the live site shows a broken image or a 404. The one rule deciding whether a library reference names a file gets one owner that the check, `GET /api/link/resolve`, `GET /api/media/info`, the media operations and the publish pre-flight all call. ## Current behavior No media reference is checked. Reproduced on a scaffolded site (2026-10-16): a leaf-bundle page `content/about/index.md` with an `img` row holding `image="media/photos/missing.jpg"`, another holding `image="missing-local.jpg"`, a markdown link and an inline anchor to `/media/docs/gone.pdf` in a text body, and a front-matter value `brochure: /media/docs/gone.pdf` gets **no finding** from `harness check`. The link check skips them on purpose (`linkrefs.Classify` yields `Media` for the `/media/…` form, and the bare `media/…` form is not root-relative). A missing library file does not fail the Hugo build either: the theme emits the raw reference as `src` (`ce/img.html`), so `scaffold/site/AGENTS.md` and the `verify-site` skill ask for a manual commit check instead. "Does this library reference name an existing file" is evaluated four times today, in the server only: `handleLinkResolve`'s media branch (`internal/server/linktargets.go`), `handleMediaInfo` (`internal/server/media.go`) and `mediaops.resolveFile` (`internal/mediaops/paths.go`) each call `media.ResolvePath` and stat the result; the publish pre-flight (`checkExportedMediaRefs`, `internal/server/publishset.go`) maps the ref with `media.LibrarySitePath` and stats it under the exported site. ## Desired behavior - `harness check` walks every page and translation file under `content/`, draft pages included, and reports a **warning** in area `content` on the file holding the reference, once per distinct reference per file, for: - a **library reference** — any value `linkrefs.Destinations` yields that `media.StoredLibraryRef` recognises (prose links, string shortcode params at any depth, front-matter strings; the bare `media/…` form and the `/media/…` URL form alike) — whose file does not exist under `assets/media/`; - a **bundle-local reference** — the value of an `image`-typed param (by the element's loaded schema) that is a bare filename (`media.IsBundleRef`) — that is not a published file of the page's bundle, by the rule `linkresolve` already applies to a bundle sub-path link (a leaf bundle's files at any depth, a branch bundle's directly in its directory, never a `.md` file). A plain page (`foo.md`) has no bundle, so every bundle-local reference on it is dangling. A translation file's bundle is its source page's. - The message names the reference and what is missing, in the style of the link findings (`links to <dest>, but …`): no such file in the library; no such file in the bundle of `<page permalink>`; the page is not a bundle. - Existence is all that is checked: no type sniffing, no dimension reading. A PDF behind a link counts exactly like an image. A `/media/…` destination `StoredLibraryRef` rejects (`/media/press/`, a trailing slash, no extension, a query) draws nothing, as today; an empty value draws nothing. - One owner for "does this library reference name an existing file of this media root": a function in `internal/media` (name and signature the implementer's) that takes the media root and the reference and answers found / not found / invalid (the invalid answer stays `ResolvePath`'s coded `library.invalidRef`). The check, `handleLinkResolve`'s media branch, `handleMediaInfo`, `mediaops.resolveFile` and the publish pre-flight (with the exported site's media root) call it; no `ResolvePath`-plus-stat or `LibrarySitePath`-plus-stat pair remains outside it. Wire answers and status codes do not change. - The bundle-file rule is called, not re-derived: `linkresolve` exports what the check needs to ask "is this a published file of this page's bundle" for a page it already holds, or the check resolves `<permalink>/<name>` through `Resolve`; either way `bundleDir` and `bundleFile` stay the single evaluation. - The package doc of `internal/checkcmd` lists the check. `docs/harness.md` "Check" gains a **Media.** paragraph beside **Links.** (the warnings list names it too). `scaffold/site/AGENTS.md` "Verifying a change" names it, and its "Library media publishes with the pages that use it" and "Image values and uploads" bullets say `harness check` reports the dangling reference (today they say only that `hugo` will not catch it). The `verify-site` skill step 1 names the warning and what to do (restore or re-upload the file, or fix the reference), and step 5's "commit content together with the `assets/media/` files" step points at the finding; the `migrate-site` skill's "after each section, `harness check` lists …" sentence and its section 6 (Media) include it; the `edit-content` skill's closing paragraph on verification signals includes it. ## Decisions Settled in triage on 2026-10-16; implement, do not reopen. - Library references are found by the one scan and the one shape rule that exist — `linkrefs.Destinations` and `media.StoredLibraryRef` — not by field type, so a link param, a front-matter value and a prose link are all covered and no second notion of "library reference" appears. Prose links are therefore checked although `mediaops.PageRefs` does not count them today; making rename, move, delete and publish-carry agree is #310, not this issue. - Bundle-local references are found by field type only: a bare filename is not recognisable by shape (`IsBundleRef` accepts any string without a slash), and `image` is the one field type whose value the theme resolves against the bundle (`ce/img-res.html`). - One owner in `media` for library-file existence — CLAUDE.md "Every rule has exactly one owner"; four evaluations exist and the check would be a fifth. The publish pre-flight joins the callers: it answers the same question about another root. - Bundle existence reuses `linkresolve`'s rule, because a bundle-local image reference on a page is the sub-path case of a link to that page. No third evaluation of "which files does this bundle publish" (the server's `pageBundleDir` is a second one already and is out of scope). - Warning, not error — the site builds; a stale reference is "probably not what was meant" (`docs/harness.md`'s severity rule), as for a dangling link. - Drafts and translations are checked like any file, on the file holding the reference, as the link check does. - One issue — the owner move, the check, its callers and the docs and skills land together. - No ADR — a check is reversible. ## Key interfaces - `linkrefs.Destinations`, `linkrefs.Classify`, `linkrefs.SplitTarget` — the scan and the reading of a candidate; never a second scan or regex. - `media.StoredLibraryRef`, `media.IsBundleRef`, `media.ResolvePath`, `media.LibrarySitePath`, `media.LibraryDir` — the reference shapes and the file locations the new owner composes. - `linkresolve.Resolver` (`bundleDir`, `bundleFile`) — the bundle-file rule. - `elemops.WalkRoles` with the loaded `element.Schema` set and `element.TypeImage` — how the check reaches image-typed params; `checkcmd` already loads the schemas and walks every content file (`content()`, `linkable`, `links()` in `internal/checkcmd/check.go`). - `checkcmd.Finding`, `checker.warnf`, `AreaContent` — the finding shape; `Counts` and the `--json` shape stay as they are. - Callers to convert: `handleLinkResolve` (`internal/server/linktargets.go`), `handleMediaInfo` (`internal/server/media.go`), `mediaops.resolveFile` (`internal/mediaops/paths.go`), `checkExportedMediaRefs` (`internal/server/publishset.go`). ## Acceptance criteria - [ ] The reproduction site gets five `content` warnings on `content/about/index.md`, one per distinct reference (the two spellings of `/media/docs/gone.pdf` in prose count once), each naming the reference and what is missing; with the files added under `assets/media/` and the page's bundle, the report is clean; `harness check` exits 0 either way. - [ ] A library reference to an existing file, a bundle-local image reference to a file in a leaf bundle's subdirectory, a `/media/press/` page link, a text param reading `media/x`, an empty image param, a `.md` file in a bundle and a relative value produce no finding. - [ ] A bundle-local image reference on a plain page, and one naming a file in a branch bundle's subdirectory, are reported. - [ ] A dangling reference in a draft page and in a translation file is reported on that file; a translation's bundle-local reference resolves against the source page's bundle. - [ ] `GET /api/link/resolve` (`/media/…`), `GET /api/media/info`, the media rename, move and delete operations and the publish pre-flight answer exactly as before (their tests pass unchanged), through the one owner; no `ResolvePath`-plus-stat or `LibrarySitePath`-plus-stat pair remains outside it, and no comment says a function mirrors or must stay in sync with another. - [ ] The documents and skills named under Desired behavior say what the engine now does; the drift tests (`go test ./docs/ ./scaffold/ ./internal/element/ ./cmd/harness/ ./internal/server/`) pass. ## How to verify - `go test ./internal/checkcmd/ ./internal/media/ ./internal/mediaops/ ./internal/linkresolve/ ./internal/server/ ./docs/ ./scaffold/ ./internal/element/ ./cmd/harness/` and `go vet ./...` (building `./...` needs `web/dist`; the package tests do not). - The reproduction as a test in `internal/checkcmd` beside `TestDanglingLinks`: `scaffoldSite`, write `content/about/index.md` with the five references above, `Run(Options{SiteDir: dir})`; before the change the report is empty, after it holds the five warnings. - `harness check --site <site>` on a site after a library file was deleted by hand prints the reference on every page using it. ## Assumptions to check - That `linkrefs.Destinations` yields an `image` param's value whole (it yields every string param), so the library-ref scan needs no second walk — the typed walk is only for bundle refs. - That Hugo resolves `.Resources.GetMatch` for a branch bundle only against files directly in its directory, as `linkresolve.bundleFile` assumes for sub-path links — check against the pinned Hugo before reporting a branch bundle's nested file as missing. - That the publish pre-flight's exported site has its library at `assets/media/` under the export root, so the owner can take that root; `regularFileAt` is the place to read. - That `elemops.WalkRoles` visits every shortcode the check cares about, items rows included (`img` is an items child of `image` and `text-media`). ## Out of scope - Prose `/media/…` links in the media operations (rename rewrite, delete refusal with links to the referencing places, publish-carry) — #310. - Consolidating the server's `pageBundleDir` onto `linkresolve`'s bundle rule. - Checking the media type, dimensions or size of an existing file. - A thumbnail or bundle-listing change; any change to the `--json` shape of `harness check` beyond the new finding rows. - Links of any other kind (#195, landed in #301). ## Blocked by None - can start immediately ## Original report > *This was generated by AI during triage.* ### Summary `harness check` verifies no media reference today: neither an image param (`media/<folder>/<name>` or a bare bundle filename) nor a `/media/…` link in prose, a link param or front matter is resolved against the library or the page bundle. A missing library file does not fail the Hugo build either (the `verify-site` skill says so and asks for a manual commit check), so a reference to a deleted or renamed asset surfaces only on the live site. ### Context Filed during the grilling of #195 (dangling page links in `harness check`), where `/media/…` links were deliberately left out: the rule "does this media reference exist" has its own owner (`media.ResolvePath` plus a stat, as `/api/link/resolve` and `/api/media/info` evaluate it) and is a separate check from page resolution. Open design points before this is agent-ready: - which references count: image params by field type, `/media/…` links of every shape `linkrefs.Destinations` yields, bundle-local filenames; - one owner for "does this media reference resolve", callable from `internal/checkcmd` and `internal/server` without a second implementation (CLAUDE.md "Every rule has exactly one owner"); - severity (a warning, presumably, like the page-link finding) and the documents and skills to update: `docs/harness.md` "Check", `scaffold/site/AGENTS.md` "Verifying a change", the `verify-site` skill (its "commit content together with the `assets/media/` files" step could then point at the finding) and `migrate-site`. ### Related - #195 — page-link check; its owner move is the pattern to follow.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#300
No description provided.