Skip to content

D312 — a write answers with the structural findings it caused, from a scoped lint

Amended by D1099: the first-write sentence below holds for every page but the only live one of its archive or of the KB.

Decision. concept_write, concept_new, concept_patch, concept_batch, concept_move, supersede and index_patch return, beside the frontmatter findings of D289, the body, link and light graph findings of lint.ScopedCheck: broken_link, duplicate_link, bare_link_list, reciprocal_link_item, unknown_placeholder, forbidden_term, orphan, broken_relation, link_to_retired and index_incomplete. ScopedCheck reads the cached link graph and the written concepts' own files, not the KB: about 10 ms on 1,000 concepts (BenchmarkScopedCheck), against 180 ms for a whole-KB lint. gate_check with changed_ids and no scope runs the same scoped lint over exactly those concepts and says so with lint_scope; an empty changed_ids stays the whole-KB gate (D318).

Why. An agent writing 60 pages in three days left 10 findings it never saw: a move into archive/ retires a concept its linkers still cite, a new journal entry is an orphan missing from a curated index. They surfaced only at the next lint, by which time nobody remembered the write. Findings are cheap to fix at the write and expensive afterwards.

Alternatives rejected. - A full lint.Run after each write: 180 ms on 1,000 concepts, paid on every write, for findings that are mostly someone else's. - Reporting every finding on the neighbours: a write would answer for the KB's whole backlog. A neighbour is reported only for what the write introduced: a page linking an ID that is gone, a retired concept the written one links. - link_to_retired on the linking page, as the plan wrote it: D313 reports it once, on the retired concept, so the scoped check reports it there, for the retired concept written or linked. - Running open_marker and closed_with_open_items in ScopedCheck: they are frontmatter-driven decay checks CheckConcept already returns, so a write response had them before D312.

Consequences. cut_concept, island, map_misfit, map_oversize, facet_sprawl, missing_value_contract and source_uncited need the whole KB and stay with lint and gate_check: a clean write response does not mean a clean KB. The neighbour expansion is capped at 200 per direction (scopedNeighbourCap) so a hub does not turn a write into a full lint; past it, the remaining linkers wait for the next lint. ScopedCheck shares linkFindings, orphanFinding and structure.linkToRetired with runChecks, so the two cannot report different findings for the same body. A first write of an unlinked concept now answers orphan, which is true and is what the agent should fix. The tools/list budget (D285) is tight: the description additions are one clause each, the detail is in docs/control-plane.md. A benchmark ages its fixture past the graph cache's racy window first (settleGraph), or it measures re-reading files written a moment ago.