docir — design documents Graph

Documents / Release notes / rel-ae6dffde6f50

0.23.0 — the section you read is the section you can write back

The read/write round trip on one section stopped duplicating its heading, and --remove-section became the exit for a body that already has one.

rel-ae6dffde6f50release_notepublished#cli#docs
View as Markdown◉ View in graph

Reading a section and writing it back is the round trip the section read path exists to enable — docir context ranks a document on one of its sections, docir get --section reads it, docir update --replace-section writes it back — and it corrupted the document. The read returns the heading line; the write supplies its own. So the obvious sequence spelled the heading twice, and from there nothing could take the second one out.

Upgrade notes

  • --replace-section and --append-section now refuse text that opens with the heading they are writing under. A script that pipes docir get --section X straight into docir update <id> --replace-section X will start failing, with an error naming what to pass instead: drop the heading line, the flag writes it. This is exactly the round trip that was producing duplicates silently, so a failure here means the script was corrupting documents.
  • docir update <id> --remove-section "<heading>" is new, and takes no --body — passing one is refused rather than ignored.
  • Nothing else changes. A body that does not repeat a heading writes exactly as before.

🐛 A section read could be written straight back as a duplicate heading

docir get <id> --section "Decision" returns the heading line and the text under it. docir update <id> --replace-section "Decision" keeps the document's own heading line and replaces only what is beneath it. The two agree on where a section ends and differ by that one line — and nothing said so. The docir get help called it "the same span --replace-section would overwrite".

So an agent that read a section, edited it and wrote it back produced two ## Decision lines, reported as success. The second attempt was worse: --replace-section matches the first heading, so it wrote into the now-empty first section and left the real content stranded under the phantom one. The text the caller meant to replace survived untouched.

Nothing caught it. docir check sees neither malformed frontmatter nor a graph problem, and docir lint --deep's ambiguous-heading advisory arrives after the fact and names no way out. --replace-section keeps the heading line by contract and --append-section adds a sibling, so the only exit was docir update <id> --replace-body --force — the riskiest edit mode, the one with the divergence guard, the one the agent guide ranks last. The safest body edit reached a state only the riskiest one could leave.

Both writing modes now refuse, at Tier 0, replacement text whose first line is a heading repeating the one being written under. Deliberately narrow: a section may open with a sub-heading of its own, prose may name the heading further down, and a fence may quote it — all three stay legal and are pinned as tests. Stripping the line instead was rejected for the reason the same shortcut was rejected once before: it guesses at an intent the caller stated.

🎯 docir update <id> --remove-section "<heading>"

The repair path a duplicated heading never had. It deletes a heading and everything under it, on the CLI and as docir_update(remove_section=) over MCP.

It is a body edit mode like the others: at most one per call, and resolved against the body as it is on disk, so it composes with an out-of-band change and never needs the divergence guard. It uses the same end boundary as docir get --section — a nested subsection goes with its parent, a heading quoted in a fenced block is not one — and it carries no #-marker guard, because a body that already spells ## ## Resolution has to be nameable to be repairable.

A repeated heading resolves to the first, here as everywhere, so removing the second of two is the same command run twice:

bash
docir lint --deep                              # names the documents that have one
docir update adr-3f9a2b1c7d4e --remove-section "Notes"

A --body passed alongside it is refused, not ignored: --remove-section X --body "..." reads as "delete this text from X" and would delete the whole section while consuming nothing. That check sits at the dispatcher, so one rule covers the CLI and MCP, and it fires only when no writing mode would consume the text — naming two modes still answers "only one body edit mode" rather than blaming the argument doing its job.

🔗 Full Changelog

See CHANGELOG.md

To amend: Re-verify: