Skip to content

D289 — lint checks a KB against the current standard, not only against its own contracts

Decision. Lint gains conformance checks, all warning or info and deterministic. nonstandard_field (warning, suppressible) reports a frontmatter key that is a known synonym of a standard field (timestamp, provenance, status, description, tags, review_after, superseded_by) and carries the fix rename_field <synonym> → <standard>; when both are present it reports the clash with no fix. tool_param_field (warning, not suppressible) reports a key named like a parameter of a concept-write tool, with the fix drop_field. missing_value_contract (info, one per map, on _map.md) reports a scalar field carried by at least 5 concepts with at most 8 distinct values and no field_values, and prints the contract line to add. A Finding may carry a machine-readable Fix {kind, field, to}, returned as fix by lint and gate_check and omitted when empty. The frontmatter-driven checks are one function shared by Run and lint.CheckConcept, and concept_write, concept_new, concept_patch and concept_batch return findings for what they wrote. One write-time rejection is added: concept_write, concept_new and concept_batch refuse a frontmatter key named like a tool parameter.

Why. Lint compared a KB with the contracts it declared for itself, so a KB created on an older release lint-cleaned while its timestamp lived under a local synonym that concept_list, kb_status and the graph cache never read. An agent that passed if_match inside frontmatter had it persisted as a field with nothing to say so. A write also returned nothing about the violation it had just made. Machine-readable fixes let a repair tool apply the mechanical ones without parsing messages.

Alternatives rejected. A per-KB field_aliases contract: every reader of a standard field, now and in every future feature, would have to consult it, and one forgotten reader is a silent bug; a rename converges the content once and leaves the readers unchanged. A write gate on the conformance checks: same stance as D275, it would block bulk repairs of drifted KBs, so writes report and do not fail. The one exception, a tool-parameter key, is never intentional. Merging both-present values automatically: a judgement, so no fix. Suggesting vocabularies with a fix: declaring a vocabulary is a judgement too.

Consequences. StandardFieldSynonyms and ToolParamFields (in internal/lint/conformance.go) are the tables; every synonym is listed in docs/data-plane.md, and a test in internal/mcpserver fails when a write-tool schema property is missing from ToolParamFields. id is in that list: a search of the code and docs found no frontmatter meaning for it (only concept ids and unrelated query parameters). reason is not: it is no parameter of these four tools and is a plausible legitimate field. concept_patch does take frontmatter (#321), so it applies the rejection to non-null values only, since a null removes the key and is the repair. gate_check still fails only on errors, so no gate turns red. This amends D275: write-time feedback now exists, but the write is never blocked by a contract finding. A timestamp synonym is flagged only when its value is date-shaped (YYYY-MM-DD or RFC 3339): data or date holding other text is a legitimate field, and the rename fix would let a repair tool corrupt it silently. A synonym a map names in required_fields is still reported, with a note; rewriting the contract is the repair tool's job (plan issue #464).