harness check: report media references (image params, /media/ links) that resolve to no file #300
Labels
No labels
bug
enhancement
needs-triage
ready-for-agent
ready-for-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle#300
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
harness checkreports 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 underassets/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.mdwith animgrow holdingimage="media/photos/missing.jpg", another holdingimage="missing-local.jpg", a markdown link and an inline anchor to/media/docs/gone.pdfin a text body, and a front-matter valuebrochure: /media/docs/gone.pdfgets no finding fromharness check. The link check skips them on purpose (linkrefs.ClassifyyieldsMediafor the/media/…form, and the baremedia/…form is not root-relative). A missing library file does not fail the Hugo build either: the theme emits the raw reference assrc(ce/img.html), soscaffold/site/AGENTS.mdand theverify-siteskill 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) andmediaops.resolveFile(internal/mediaops/paths.go) each callmedia.ResolvePathand stat the result; the publish pre-flight (checkExportedMediaRefs,internal/server/publishset.go) maps the ref withmedia.LibrarySitePathand stats it under the exported site.Desired behavior
harness checkwalks every page and translation file undercontent/, draft pages included, and reports a warning in areacontenton the file holding the reference, once per distinct reference per file, for:linkrefs.Destinationsyields thatmedia.StoredLibraryRefrecognises (prose links, string shortcode params at any depth, front-matter strings; the baremedia/…form and the/media/…URL form alike) — whose file does not exist underassets/media/;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 rulelinkresolvealready 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.mdfile). 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.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./media/…destinationStoredLibraryRefrejects (/media/press/, a trailing slash, no extension, a query) draws nothing, as today; an empty value draws nothing.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 staysResolvePath's codedlibrary.invalidRef). The check,handleLinkResolve's media branch,handleMediaInfo,mediaops.resolveFileand the publish pre-flight (with the exported site's media root) call it; noResolvePath-plus-stat orLibrarySitePath-plus-stat pair remains outside it. Wire answers and status codes do not change.linkresolveexports 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>throughResolve; either waybundleDirandbundleFilestay the single evaluation.internal/checkcmdlists 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 sayharness checkreports the dangling reference (today they say only thathugowill not catch it). Theverify-siteskill 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 theassets/media/files" step points at the finding; themigrate-siteskill's "after each section,harness checklists …" sentence and its section 6 (Media) include it; theedit-contentskill's closing paragraph on verification signals includes it.Decisions
Settled in triage on 2026-10-16; implement, do not reopen.
linkrefs.Destinationsandmedia.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 althoughmediaops.PageRefsdoes not count them today; making rename, move, delete and publish-carry agree is #310, not this issue.IsBundleRefaccepts any string without a slash), andimageis the one field type whose value the theme resolves against the bundle (ce/img-res.html).mediafor 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.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'spageBundleDiris a second one already and is out of scope).docs/harness.md's severity rule), as for a dangling link.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.WalkRoleswith the loadedelement.Schemaset andelement.TypeImage— how the check reaches image-typed params;checkcmdalready loads the schemas and walks every content file (content(),linkable,links()ininternal/checkcmd/check.go).checkcmd.Finding,checker.warnf,AreaContent— the finding shape;Countsand the--jsonshape stay as they are.handleLinkResolve(internal/server/linktargets.go),handleMediaInfo(internal/server/media.go),mediaops.resolveFile(internal/mediaops/paths.go),checkExportedMediaRefs(internal/server/publishset.go).Acceptance criteria
contentwarnings oncontent/about/index.md, one per distinct reference (the two spellings of/media/docs/gone.pdfin prose count once), each naming the reference and what is missing; with the files added underassets/media/and the page's bundle, the report is clean;harness checkexits 0 either way./media/press/page link, a text param readingmedia/x, an empty image param, a.mdfile in a bundle and a relative value produce no finding.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; noResolvePath-plus-stat orLibrarySitePath-plus-stat pair remains outside it, and no comment says a function mirrors or must stay in sync with another.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/andgo vet ./...(building./...needsweb/dist; the package tests do not).internal/checkcmdbesideTestDanglingLinks:scaffoldSite, writecontent/about/index.mdwith 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
linkrefs.Destinationsyields animageparam'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..Resources.GetMatchfor a branch bundle only against files directly in its directory, aslinkresolve.bundleFileassumes for sub-path links — check against the pinned Hugo before reporting a branch bundle's nested file as missing.assets/media/under the export root, so the owner can take that root;regularFileAtis the place to read.elemops.WalkRolesvisits every shortcode the check cares about, items rows included (imgis an items child ofimageandtext-media).Out of scope
/media/…links in the media operations (rename rewrite, delete refusal with links to the referencing places, publish-carry) — #310.pageBundleDirontolinkresolve's bundle rule.--jsonshape ofharness checkbeyond the new finding rows.Blocked by
None - can start immediately
Original report
Summary
harness checkverifies 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 (theverify-siteskill 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.ResolvePathplus a stat, as/api/link/resolveand/api/media/infoevaluate it) and is a separate check from page resolution. Open design points before this is agent-ready:/media/…links of every shapelinkrefs.Destinationsyields, bundle-local filenames;internal/checkcmdandinternal/serverwithout a second implementation (CLAUDE.md "Every rule has exactly one owner");docs/harness.md"Check",scaffold/site/AGENTS.md"Verifying a change", theverify-siteskill (its "commit content together with theassets/media/files" step could then point at the finding) andmigrate-site.Related