Skip to content

D309 — Writes never corrupt frontmatter or drop graph edges

Decision. The OKF serialiser only emits what its own parser reads back, and the write path verifies it: a scalar holding any flow-list delimiter or quote (' included) is quoted; overwriting or removing a key also drops the indented lines its old value left behind; blank lines before the first key are dropped; and prepareWriteConcept parses the serialised frontmatter and rejects the write if it does not parse. reciprocal_link_item counts a back-link only when it sits in the target's text, not in the target's own links section, and kb_repair never drops both items of a mutual pair in one run. concept_patch/concept_batch accept an unset array of keys to remove, and concept_read returns the parsed frontmatter next to frontmatter_raw.

Why. Each of these left a production KB in a state no tool could repair cleanly. An unquoted dall'operatore in a flow list opened a single-quoted string that never closed, so the file stopped parsing; a provenance: text followed by - item lines kept the items as unrecognised lines that Serialize printed after the new value; two pages listing each other only in their links sections were both flagged, and repairing both findings removed the edge from the graph — the one thing D301 promised backlinks would keep. Some MCP clients strip null from the arguments, so the D88 removal path was unreachable from them; an agent re-parsing frontmatter_raw by hand introduced its own quoting errors. The cost: values with an apostrophe are now quoted, so files show a one-time diff on their next write, and every write pays one extra frontmatter parse.

Alternatives rejected. - Fixing the parser to accept unquoted ' inside a flow element: the parser's quote rules are what lets ] and , live inside an element; the serialiser is the side that must adapt to them. - Pruning orphaned continuation lines at parse time: it would silently rewrite files a read never meant to change; doing it in Set/Delete limits it to the key the caller is replacing. - Removing every blank line after the key, as first planned: that also collapsed blank lines a person used to group keys; only the run up to the last indented line goes. - Keeping only the repair-side guard for mutual pairs: the lint would still report findings no repair applies; the lint fix is primary, the guard is defence-in-depth. - Replacing null with unset: null is the documented contract (D88) and works from most clients; unset is an alternative, not a replacement.

Consequences. serializeScalar's character set and the quote handling of splitFlowList/collectFlowList are one contract: a change to either needs the round-trip tests in internal/okf to stay green (a \" inside a quoted element did not round-trip until collectFlowList learned the escape). The round-trip guard turns any future serialisation regression into a refused write instead of a corrupt file. unset is a tool parameter, so a frontmatter key named unset is now a tool_param_field. A bundled kb-doctor note tells agents on an older server to dry-run reciprocal_link_item first.