D907 — The lint cannot be escaped: a write never introduces an avoidable finding, and nothing hides one¶
Decision. No setting, key or call switches a lint check off, loosens it or hides a finding.
- The write gate is fixed behaviour. In every KB that is a git repository, after the server has applied the
safe mechanical fixes to what the call wrote (always, in the write's own commit), a
concept_*write (butconcept_transfer, which commits two KBs itself, D760) orsupersedeis refused and rolled back when it introduces a finding of a check markedAvoidableAtWrite, at any severity. The refusal lists each finding with its fix hint. A finding the touched concepts already had never refuses a write. Nothing accepts a finding:accept_findingsand theAccepted-Findingtrailer go (old trailers stay readable inconcept_historyandchanges_since). - Avoidable means one call could have avoided it, with
concept_batchatomic: create the link target or drop the link, list the page in the index or link it from its parent, link instead of copying, write a checkbox instead of a TODO, split the page now, fix the field, value, title or template. A check is not avoidable when the finding comes from time (stale_open,stale_claim,artifact_unused), from growth (map_oversize,facet_sprawl,missing_value_contract,map_without_templates,island,cut_concept,map_misfit), from a legacy shape (legacy_archive_descriptor,contract_legacy_key), from a step another tool takes (source_uncited,orphan_asset,unused_placeholder,imported_draft), from the KB's initial state (missing_instructions,missing_registry) or from what the server regenerates itself (index_stale). What the write path refuses before lint (missing_frontmatter,unparseable_frontmatter,missing_type,concept_too_deep, D356) needs no gate. A check that is not avoidable says why indocs/data-plane.md, and a test fails on one that does not, so a new check is classified on purpose. - Every exception goes:
lint_ignore(concept and_map.md),lint_acceptininstructions.md, thelint_ignore_invalidcheck, the suppression inventory,CheckAcceptability, and the contract keys that turned a check off or moved its threshold:require_template,template_sections,require_index_entry,machine_path_allow_prefixes,title_max_length,repeated_fact_min,hotspot_in_degree,hotspot_bytes,oversize_bytes,oversize_concepts, andstale_after: 0/harvest_after: 0.ontology_modeis derived fromconcept_types;required_fieldsandfield_valueshave one shape each, keyed by type with*for every type. - Old keys are realigned, never honoured. The new check
contract_legacy_key(warning, auto-repair safe, fixrewrite_contract) reports a removed key in a_map.mdorlint_acceptininstructions.md; its fix drops the key, derives the mode and merges the flat shapes into the*entry, so the unattended repair realigns a KB in one commit.lint_ignorein a concept is anonstandard_fieldthe repair drops. - Loosening a contract is allowed, with a reason. A
map_updatethat drops a required or forbidden field, a field's allowed values, the type list or the templates, or widens one, is refused without the write'sreason; with one it commits with theReasontrailer andlog.mdrecordsloosened: … (reason: …), which the Atlas Activity shows. - A
kb_reviewitem closes on page state, never on a dismissal. Each kind closes when the pages it names are in the state it asks for:duplicate_candidatemerged or linked both ways (D854);zombie_workthe work closed or its origin link retargeted;promotion_candidatea link intopromote_to;glossary_gapthe term inglossary.yamlor a glossary map;repeated_factthe line kept in fewer than three concepts;read_hotspota smaller body or fewer inbound links;scattered_worka link intowork_mapor the items closed;harvest_candidatethe entry archived;status_reclassifythe status changed or listed open;map_namingtitles that share one scheme and are not too long;template_proposalthe map declaring templates its pages follow;lint_judgementthe check no longer reporting. A test writes each closing state and requires the item to go; a new kind needs one. A reason no page state could settle is not a review item: the title a folder does not echo (D315) is onlymap_create/map_update's write-time warning now, because a title in another language than its folder may be right as it stands.
Supersedes D313 (acceptability), D332 (lint_accept), D834 (the suppression inventory) and D350 (the opt-in gate
and accept_findings). Amends D306 (a finding is fixed, no longer accepted), D159 (the per-concept opt-out and
the allow prefixes; its size thresholds stay), D349 (repair on write has no opt-out), D371 (require_template:
false is gone; a check that cannot run still says why) and D315 (the folder clause of map_naming).
Deviations from the plan (#907). The plan made a review reason no page state can settle "doctor advice shown
once per session"; the one such reason found, the folder clause of map_naming, already had a better home in
the write-time warning of map_create/map_update, which reaches the writer while the title is being chosen,
so no new advice channel was built. Putting the folder reason back as an item is a one-line change in
poorMapTitles, should a page state for it appear. The plan also removed the server keys in the same change; it
lands in two halves instead, the behaviour first and the keys after #906's settings registry (see Consequences).
Why. Each opt-out was added for a good local reason, and together they let a long-lived KB be configured almost at random: a Health panel with no findings could mean the pages are right or that something was set aside, and nobody could tell which. A false positive silenced on one page stays wrong on every other KB; reported as a defect, the check is fixed for all of them. Refusing at write time is the cheapest point to prevent a finding: the writer has the page open, and one batch can fix it. The cost is that writes are refused more often, most visibly a new page that its map's curated index does not list yet, and that a map whose rule is wrong must change the rule, in the open, rather than step around it.
Alternatives rejected.
- Keep the gate as a setting and turn it on by default: a switch is an escape, and the KBs that most need the
gate are the old ones that would keep it off.
- Gate by severity (refuse errors and warnings, pass info): time- and growth-based warnings would block writes
that did not cause them, while avoidable info findings would still pile up. Avoidability is the property that
matters to the writer.
- Keep lint_ignore with a mandatory reason and audit it (D834): the inventory grew into a second list to
review, and an accepted finding still hid the page from every later reader.
- Store review dismissals outside the pages: state outside the KB is invisible in git, diverges between
replicas, and outlives the page change that should reopen the item.
- Keep the folder clause of map_naming with a "title is fine" key: an exception by another name.
Consequences.
- A new lint check states AvoidableAtWrite deliberately; a new kb_review kind comes with its closing page
state and a closer in the trap test.
- Tests and tools that write a page on its own into a curated map must list it in the index (or link it from
its parent) in the same call, or use a generated index.
- A future knob that would turn a check off, raise its threshold per KB or hide a finding contradicts this
decision; a threshold that is wrong is changed as a constant, for every KB.
- The gate's refusal for a caller who cannot see the whole KB is judged as for anyone, but reports a whole-graph finding with the check and the visible page only and no fix, and an artifact finding not at all: the refusal names no page the caller cannot read. A successful write's response follows the same rule through the same helper (callerView), including concept_move's lists of rewritten pages.
- A delete plus an add that git does not report as a rename is treated as a move only when the two pages share frontmatter title and type (and the file name is unique among the call's deletions and additions); otherwise the added page is judged as new.
- (Part A, superseded by Part B below) The heartbeat always ran contract_legacy_key and the drop of a lint_ignore field whatever auto_repair listed, while the list still governed the other checks and doctor_auto_interval: 0 still turned the heartbeat off. Part B removes both keys: every safe repair runs daily on every KB.
- A page keeps a field its map's contract names: nonstandard_field does not report, and the safe repair does not rename, a synonym that the page's map requires or lists in field_values. A contract requiring updated therefore never conflicts with the repair, and a page write never edits _map.md.
- Part B: the keys are gone (#907). write_gate, auto_repair, repair_on_write, doctor_auto_interval,
doctor_interval, doctor_mode and doctor_budget leave the KB's settings.yaml (the schema, kb.Policy,
config.KBSpec, the capabilities of kb_status) and are registered as removed in kb.SettingsRetired and
config.Retired (kbs[].…): a file that still carries one is realigned by a server commit and reports what
replaced it. What each drove is now fixed, at the value it had by default:
- the unattended repairs run every AutoRepairSafe check (lint.AutoRepairChecks, the list that was
config.DefaultAutoRepair, moved to lint.AutoRepairOrder with its dependency order), on write, in the
background and under kb repair --apply; the heartbeat no longer limits nonstandard_field to dropping
lint_ignore, because the list is never narrower than the full set;
- the heartbeat runs at a constant interval, kb.AutoRepairIntervalDays = 1 (the old doctor_auto_interval
default), at every start and after a pull as before; doctor_auto_interval: 0 no longer turns it off, which
closes the stated deviation of Part A;
- the doctor suggestion (conformance.doctor_suggested, next_doctor, the nudge, the Atlas Health panel) is
due kb.DoctorSuggestionDays = 14 days after the last session (the old doctor_interval default); it cannot
be disabled, and doctor_mode, which already did nothing, goes without a replacement;
- doctor_budget is a parameter of the kb-doctor session, not of the KB: nothing on the server read it except
the kb_status capability the skill quoted, so no tool argument is added. The skill (3.3) says "10, or the
number the operator names when asking for the session" and passes it as kb_review's limit.
kb_status capabilities keep only secrets and mount. The background log (auto-repair-log.jsonl) lists only
the checks that changed or skipped something, since every safe check now runs and the rest would be noise. The
refusal text still starts with write_gate:: it names the gate, not a key. The test fixtures of the package that
relied on a KB with doctor_interval 0 (no nudge) get the nudge now, as every real server did.