Media operations ignore library links in body prose: rename/move must rewrite them, delete must refuse and link to the referencing places #310
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#310
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
A library reference inside body prose — a markdown link or inline anchor to
/media/<folder>/<name>in a text body or a rich-text param — counts as a reference everywhere the media operations care: a library rename or move rewrites it, a delete is refused while it exists, and a publish carries the file. The delete refusal, and the dialog's pre-check list, name each referencing place as a link the editor opens, not a bare file path.Current behavior
mediaopsrecognises a library reference only as a whole shortcode-parameter string or front-matter value (media.StoredLibraryRef, baremedia/…or/media/…form); "body prose never counts" (internal/mediaops/doc.go,refs.go,delete.go). Yet the link picker's File tab stores/media/…into links, including links in rich text, and the theme renders them (ce/href.html). Consequences, all on the same page:POST /api/media/rename,/move,/folder/rename,/folder/moverewrite the params and front matter of every referencing page and leave a prose link pointing at the old path — a 404 on the live site.POST /api/media/deleteand/folder/deleterefuse only when a param or front-matter value references the file; a file linked only from prose is deleted and the link dangles.checkExportedMediaRefs,internal/server/publishset.go) builds its carried set frommediaops.PageRefs, so a file linked only from prose is not carried tomainwith its page, and the pre-flight does not notice.DeleteFileDialogandDeleteMediaConfirm,web/src/components) lists the referencing pages as plain content paths (<li>{path}</li>); the user has to find the page and the element by hand. The pre-check list (GET /api/media/refs) is a substring scan (media.ReferenceScan), not the engine's answer, so it can disagree with the refusal.The prose link positions are already owned:
linkrefsscans them (Destinations:](,](<andhref="anchors, in Text nodes and inside string values) and rewrites them for page renames (Rewrites,RewritePage,Pair).linkrefssits belowmediain the import graph and already owns the/media/spelling (linkrefs.MediaPrefix,linkrefs.Media).Desired behavior
linkrefs.Destinations' positions) in the body or in a string value.mediaops.PageRefs,ReferencingPages, the delete refusals, the folder-delete plan and the rename/move rewrites all use that one notion. A bare textual mention of a path that is not at a link position stays a non-reference, aslinkrefsalready rules for page links.RenameFile,MoveFile,RenameFolderandMoveFolderrewrite a prose link to the file (or to a file under the folder) to its new reference, in the same operation and under the same honest-refusal contract the parameter rewrite has (a page that cannot be parsed or re-serialised aborts the operation before anything moves). The rewrite of a link position islinkrefs' rewrite, called with media pairs — not a second prose rewriter inmediaops. Whetherlinkrefs.Pairgrows an exact-file form ormediaopsalways passes folder-prefix pairs plus a file pair is the implementer's call; the trailing-slash prefix contract oflinkrefsmust not be weakened for page links.409 media.referencedbody.409body ofPOST /api/media/delete, thekeepentries ofPOST /api/media/folder/delete(and its plan) and the answer ofGET /api/media/refsname each referencing place so the client can open it: the page (its content path and language, as the editor's routes address a page) and, when the reference is in a shortcode parameter, the element's uid, so the client can useelementEditRoute/viewElementRoute(web/src/routing.ts) and land on the element. A prose reference and a front-matter reference link to the page. The delete dialogs render each place as a link; the dialog's pre-check list comes from the engine's notion of reference (the same answer the refusal gives), so what it lists is exactly what blocks the delete.checkExportedMediaRefssees prose references through the samePageRefs, so a page linking a PDF only from its text carries the PDF and is refused when the PDF is missing onmain.docs/api.mdrecords the new shapes ofPOST /api/media/delete(409 body),POST /api/media/folder/delete,GET /api/media/refs, and the rename/move routes' "rewrites every page that references it" sentences name prose links.docs/architecture.md"Media gate" / publish narrative says prose links count.scaffold/site/AGENTS.md's "Library media publishes with the pages that use it" bullet names prose links among the reference shapes (/media/<folder>/<name>in a link value, in front matter or in a text body). Themediaopsandlinkrefspackage docs say which rule each owns. Theedit-contentskill's media paragraph, if it lists where a library reference may appear, includes prose links.Decisions
Settled by the maintainer on 2026-10-16 (issue #300's triage); implement, do not reopen.
linkrefs.mediaopscalls it; it does not add a second prose scanner or rewriter (CLAUDE.md "Every rule has exactly one owner"). The whole-value notion (StoredLibraryRefover params and front matter) staysmediaops' as today.harness checkwarning and already reads prose throughlinkrefs.Destinations; this one makes the operations agree with it. Land in either order.Key interfaces
linkrefs.Destinations,linkrefs.Rewrites,linkrefs.RewritePage,linkrefs.Pair,linkrefs.Classify,linkrefs.MediaPrefix— the prose scan and rewrite.media.StoredLibraryRef,mediaops.PageRefs,mediaops.ReferencingPages,scanReferences,scanRewrites,rewriteParams,frontMatterChanges,*mediaops.ReferencedError,mediaops.DeletePlan/KeepEntry— the engine side to extend.handleMediaDelete,handleMediaFolderDelete,handleMediaRefs,mediaReferencedResponse(internal/server/media.go);checkExportedMediaRefs(internal/server/publishset.go).web/src/api/media.ts(deleteLibraryMedia,fetchMediaRefs),DeleteFileDialoginweb/src/components/MediaModule.tsx,web/src/components/DeleteMediaConfirm.tsx,elementEditRoute/viewElementRouteinweb/src/routing.ts.Acceptance criteria
[brochure](/media/docs/a.pdf)and nothing else referencing the file: renaminga.pdftob.pdfrewrites the link to/media/docs/b.pdf; moving it todocs/old/rewrites it to/media/docs/old/a.pdf; renaming or moving thedocsfolder rewrites it accordingly. The same for an inline<a href="/media/docs/a.pdf">and for a link inside a rich-text param. A page mentioningmedia/docs/a.pdfas plain words is untouched.409 media.referenced, the page named; the folder-delete plan keeps it with the page named; after the link is removed, both succeed.GET /api/media/refscarry, per referencing place, what the client needs to open the page in its language and, for a parameter reference, the element; the delete dialogs render each place as a link that opens the editor there, and the pre-check list and the refusal agree.main, the pre-flight refuses withpublish.mediaMissingnaming it.mediaops; no comment says a function mirrors or must stay in sync with another.docs/api.md,docs/architecture.md,scaffold/site/AGENTS.mdand the package docs say what the engine now does; the route drift test ininternal/serverandgo test ./docs/ ./scaffold/ ./internal/element/pass; the web client's tests (vitest) pass.How to verify
go test ./internal/mediaops/ ./internal/linkrefs/ ./internal/media/ ./internal/server/ ./docs/ ./scaffold/ ./internal/element/,go vet ./...; inweb/:npm testandnpm run build(the server package tests do not needweb/dist).mediaopstests beside the existing rename/move/delete tests (renamefile_test.go,movefile_test.go,delete_test.go): before the change the prose link is left as is and the delete succeeds.Assumptions to check
/media/…for a File-tab choice and that the stored form never carries a query or fragment (media.StoredLibraryRefrejects those) — readweb/src/components/SetLinkForm.tsxand the rte link code underweb/src/rte/.linkrefs.Destinationsyields prose destinations inside string params (rich-text params) as the package doc says, so no new position is needed.langplus page path plus uid) for a reference found in a translation.GET /api/media/refshas no other caller relying on the substring semantics (grepfetchMediaRefsandReferenceScan); ifmedia.ReferenceScanloses its last caller, remove it.Out of scope
harness checkwarning for a missing media file — #300.linkrefs).Blocked by
None - can start immediately
Related
harness checkreports media references that resolve to no file; its triage found this gap.