D350 — An opt-in write gate refuses, and rolls back, a write that introduces findings¶
Superseded by D907: the gate is fixed behaviour in every git KB, refuses an introduced finding of any check marked
AvoidableAtWriteat any severity, and nothing accepts one (write_gateandaccept_findingsare gone).
Decision. A KB may set kbs[].write_gate to error or warning (default off). gitWrap then judges every concept_* write and supersede after the handler, the generated-index regeneration and any repair-on-write (D349), before the commit: a finding at or above the floor that the concepts it touched did not already have refuses the call, the working tree goes back to HEAD, and nothing is committed. accept_findings: [{check, reason}] accepts every introduced finding of a check for that call; each match leaves an Accepted-Finding: <check>: <reason> trailer that concept_history and changes_since show. This amends D275 and D289: "writes report findings and never fail" now holds only for a KB that does not opt in.
Why. Reporting alone left a trail of new findings (an invalid_field_value error committed by a move into archive/, duplicate_link and bare_link_list from subagents) for someone else to clean up. The gate sits in gitWrap because it already holds the per-KB lock, sees the final tree and has not committed: one tool-agnostic enforcement point, with the working-tree diff saying what changed whatever the tool. "Introduced" is set-difference by (path, check, message) against the same concepts at HEAD (a moved concept under its old path), so a repair of a drifted KB is never blocked by what was already wrong. The baseline costs a stash and a second lint only when the write left something at or above the floor. The cost: a second identical message on one page counts as pre-existing (set semantics), and an opted-in KB pays about 200 bytes of tools/list per gated tool (accept_findings is in the schema only then, so a gate-off KB is byte-identical and D285's budget is untouched).
Alternatives rejected. Per-tool gating: eleven tools write concepts and a new one would silently be ungated. Lint surfacing accepted findings, as lint_ignore does: lint is a pure function of the working tree (D294), and reading commit trailers there would break that; the history readers carry them instead. Per-finding accept: forces the agent to echo paths and messages, while the reason is about the kind of exception. Gating _map.md, index, log, asset and artifact changes: a contract change is a bulk event whose fallout is lint's and the doctor's job. Failing closed on a git problem during the baseline: the write is intact then, so it proceeds as it does with the gate off.
Consequences. The gate needs auto-commit on a git repository (capability inactive otherwise, behaving as off). It skips a call whose tree was not clean before the handler (rolling back would destroy foreign changes) or whose repository has no commit yet, with one stderr line. A refusal leaves files, indexes and history exactly as before; search and the link graph reconcile by themselves. A check added to writeLintFindings or ScopedCheck later is gateable with no change here.