D159 — Per-concept lint opt-out, home-anchored allow prefixes, related size thresholds¶
Status: implemented (2026-08-28). Amends D124 and D107. Closes #180.
Context. Four findings an author could not legitimately clear, or a remedy the product itself made unavailable.
- Lint fired on documentation about lint. A concept documenting a
machine_pathfalse positive raisedmachine_path; one documenting the Mermaid/POSIX link collision raisedbroken_link. A KB's own "known false positives" page could not be written without generating the findings it described, and there was no per-concept escape: the only allowlist was per-map and covered path prefixes only. - The
machine_pathallowlist rejected home-anchored paths.normalizeAllowPrefixaccepted only POSIX-absolute or Windows-drive-absolute prefixes, so~/.ssh/config,~/.m2/settings.xmland friends stayed flagged forever — twelve of 73 initial findings in the field. They are conventional paths:~/.ssh/configmeans "your ssh config" on every machine, exactly the reasoningmachinePathRe's own comment already applies to/etcand/var. - The oversize remedy was structurally impossible for a satellite.
concept_oversizeadvisedconcept_expand, which requires exactly two segments, while the write path caps depth at three. The two largest concepts in the reporting KB were satellites, so the advice named an operation the product refuses. - Two unrelated numbers decided "too big".
conceptOversizeThreshold = 30000advised a split;conceptReadSizeGuard = 60000switchedconcept_readto an outline. A concept between them was simultaneously "too big, split it" and "fine, here it is whole", and the lint constant's own comment claimed it "mirrors" the read guard — which is false, 30000 does not mirror 60000.
Decision.
- The opt-out is per-concept frontmatter,
lint_ignore: [check, …]. Not per-map: the cases are individual pages, and a map-wide suppression would hide the same check on fifty concepts nobody inspected. Implemented as oneemitclosure at the top of the per-concept loop rather than a condition at each site. error-severity checks cannot be suppressed and route aroundemiton purpose:missing_required_fieldandexpanded_ambiguousare contract violations, not judgements, and allowing an opt-out would let a KB declare its own contract void.- Directory-level checks are not suppressible either, and that includes
orphan_asset: it belongs to an expanded concept's asset set and is reported in the directory pass, so there is no single concept frontmatter that owns it. Listing it as suppressible would have been a promise the implementation does not keep. - An unsuppressible or unknown name in
lint_ignoreis itself a finding (lint_ignore_invalid), with the reason: a typo that silently suppresses nothing is worse than no opt-out. A bare string is accepted as a one-element list, since the parser distinguishes the two and a bare string is the obvious authoring mistake. ~/-anchored prefixes are accepted, matched literally, with no home expansion anywhere: expanding~would make the contract's meaning depend on the reader's home directory, the opposite of what an allowlist for portable paths is for.~user/…is rejected because the detector never produces that form, so such a prefix could never match.matchesAllowedPrefixneeded no change — it already compares at segment boundaries — which a test asserts rather than assumes.- No shipped default list of well-known tool paths. The field report offers it as an alternative;
a built-in list is a policy every KB inherits without asking, and it would silently unflag
~/.ssh/id_rsain a KB that wants exactly that flagged. - The oversize suggestion is depth-aware, and at maximum depth names what is available.
- The two thresholds are related, not merged. They answer different questions — should this be
split for a human, can this be returned whole to a model — and merging them would couple lint
policy to a client's token budget.
conceptOversizeThresholdis nowokf.ConceptReadSizeGuard / 2, in one expression, and the message names both bounds. The constant moved tointernal/okfbecause two packages need it with no import cycle.
Consequences. Additive: one new frontmatter key, one widened allowlist form, two reworded messages. The numeric value is unchanged (60000/2 = 30000), deliberately: the fix is coherence, not a new policy, and a threshold change would belong to its own decision. A package-level test asserts the two constants cannot drift apart again.