markfluence never deletes, so an attachment it uploaded stays on the page after the page stops referencing it. There is no way to clean those up except by hand in the Confluence UI. Add attachment-prune PAGE to remove them.
What makes an attachment prunable
Two conditions, both required:
- Unreferenced. The page body does not name it.
cmd/export/export.go already computes this as referencedNames(storage), matching ri:filename anywhere in the raw storage rather than only inside ac:image — an attachment link target or a reference inside a passthrough macro counts as referenced.
- Managed by markfluence.
client.AttachmentMeta.Managed is true only for attachments carrying the markfluence: comment prefix. This half is not optional: an attachment someone added in the Confluence UI is unreferenced by definition if it is not embedded, so pruning on "unreferenced" alone would delete a PDF a human attached for reference. That is a S5 violation.
Report the checksum, do not branch on it
Every managed attachment records a SHA-256 in its comment, which makes it possible to say whether deleting an orphan loses content: if its checksum matches an attachment the page still references, the bytes survive under another name. If it matches nothing, this is the only copy on the page.
That distinction should be reported, not encoded as different behaviour. Branching on it guesses; showing it lets the operator decide.
$ markfluence attachment-prune docs/guides/index.md --dry-run
orphan assets%2Fbrand.png content also attached as ..%2Fassets%2Fbrand.png
orphan old-diagram.png only copy on this page
keep handbook.pdf not managed by markfluence
Worth noting that the second case is usually still safe: a managed attachment came from a local file, so republishing reproduces it. The exception is when that file has since been deleted from the repo — which is exactly when the checksum will not match anything.
Guarantees
This command exists in the shape it does because of docs/guarantees.md:
- S4 — removal is a command's stated purpose or it does not happen. This is why pruning must not be a step inside
update. Publishing that quietly deleted things would be removal as a side effect.
- S5 — markfluence removes only what markfluence created. The
Managed gate.
- S6 — a command that removes says what it will remove first and honours
--dry-run.
S4-S6 are currently Vacuous: nothing in markfluence removes anything. This would be the first command to exercise them, so their status changes when it lands.
Shape
attachment-prune PAGE, matching its three siblings — singular noun, noun-first, so attachment-<TAB> still completes as a group. PAGE goes through internal/pageref like the others, so it accepts a page id, a Confluence URL, or a .md file with a page_id — pointing at the markdown file whose images just changed is the obvious way to invoke this.
Single page to start, like attachment-list. Batch pruning across a space has a much larger blast radius and can follow if it is wanted.
Prerequisites
- A delete method in the client. There is none:
http.MethodDelete appears only in send's idempotent-methods list.
referencedNames has to move somewhere shared. It is local to cmd/export today, and export and prune must not each have their own scanner — same rule as the traversal clamp in internal/attachfile.
- A schema entry: the
command enum plus an if/then branch constraining results.items and summary, or internal/schematest fails.
Relationship to the file-organization work
The root-relative naming model in _plans/025_file-organization.md removes the most common source of orphans: today, moving a markdown file renames every attachment it references, because the name encodes the path relative to the referencing file. Under that model moving a page changes nothing, so nothing is stranded.
This command handles what is left over: moving or renaming an asset (whose identity does follow its location, by design), removing an image from a page, and orphans that already exist from before the model lands. It is a cleanup tool for the residual rather than a fix for the naming problem.
markfluence never deletes, so an attachment it uploaded stays on the page after the page stops referencing it. There is no way to clean those up except by hand in the Confluence UI. Add
attachment-prune PAGEto remove them.What makes an attachment prunable
Two conditions, both required:
cmd/export/export.goalready computes this asreferencedNames(storage), matchingri:filenameanywhere in the raw storage rather than only insideac:image— an attachment link target or a reference inside a passthrough macro counts as referenced.client.AttachmentMeta.Managedis true only for attachments carrying themarkfluence:comment prefix. This half is not optional: an attachment someone added in the Confluence UI is unreferenced by definition if it is not embedded, so pruning on "unreferenced" alone would delete a PDF a human attached for reference. That is a S5 violation.Report the checksum, do not branch on it
Every managed attachment records a SHA-256 in its comment, which makes it possible to say whether deleting an orphan loses content: if its checksum matches an attachment the page still references, the bytes survive under another name. If it matches nothing, this is the only copy on the page.
That distinction should be reported, not encoded as different behaviour. Branching on it guesses; showing it lets the operator decide.
Worth noting that the second case is usually still safe: a managed attachment came from a local file, so republishing reproduces it. The exception is when that file has since been deleted from the repo — which is exactly when the checksum will not match anything.
Guarantees
This command exists in the shape it does because of docs/guarantees.md:
update. Publishing that quietly deleted things would be removal as a side effect.Managedgate.--dry-run.S4-S6 are currently Vacuous: nothing in markfluence removes anything. This would be the first command to exercise them, so their status changes when it lands.
Shape
attachment-prune PAGE, matching its three siblings — singular noun, noun-first, soattachment-<TAB>still completes as a group. PAGE goes throughinternal/pagereflike the others, so it accepts a page id, a Confluence URL, or a.mdfile with apage_id— pointing at the markdown file whose images just changed is the obvious way to invoke this.Single page to start, like
attachment-list. Batch pruning across a space has a much larger blast radius and can follow if it is wanted.Prerequisites
http.MethodDeleteappears only insend's idempotent-methods list.referencedNameshas to move somewhere shared. It is local tocmd/exporttoday, and export and prune must not each have their own scanner — same rule as the traversal clamp ininternal/attachfile.commandenum plus anif/thenbranch constrainingresults.itemsandsummary, orinternal/schematestfails.Relationship to the file-organization work
The root-relative naming model in
_plans/025_file-organization.mdremoves the most common source of orphans: today, moving a markdown file renames every attachment it references, because the name encodes the path relative to the referencing file. Under that model moving a page changes nothing, so nothing is stranded.This command handles what is left over: moving or renaming an asset (whose identity does follow its location, by design), removing an image from a page, and orphans that already exist from before the model lands. It is a cleanup tool for the residual rather than a fix for the naming problem.