D294 — lint and kb_status stay fast on real KBs¶
Decision. lint.Run resolves broken-link existence and concept-to-file
mapping from the set of concepts it already enumerated in its walk, instead of
calling resolveConceptRelPath (two os.Stat) and ReadConcept (read + parse)
per link. kb_status caches the whole-KB lint findings on the graph-cache
generation plus a stat-only stamp of every non-concept input lint reads, and lastDoctorDate on the log file's mtime+size; the per-caller
visibility filter (D226) is always applied outside the cache.
Why. D290 made kb_status run a whole-KB lint on every call. On a
~740-concept work KB with ~5,000 links, kb_status went from 0.22 s to 2.2 s —
10× slower and 10× over D290's own 200 ms threshold. CPU profile: 49 % of
lint.Run was in resolveConceptRelPath (os.Stat) and ReadConcept
(existence test via full file read), all answerable from the walk lint already
did.
Alternatives rejected.
- Timer-based cache — a fixed TTL does not invalidate on writes and shows stale data after a quick concept_write + kb_status cycle. The graph generation already carries the right semantics: a write bumps it, a read does not.
- Per-link
os.Statwith a per-Run file-exists cache — still two syscalls per new target in a run; the enumeration map is zero additional syscalls.
Consequences.
- The
relPathOfmap built during the walk must mirrorresolveConceptRelPath's "direct form wins when both exist" rule; the oracle test pins this. - The conformance cache stores unfiltered findings;
uiVisibleFindingsmust always apply the visibility filter on every call so a restricted caller never sees hidden concepts from a warm cache (D226). - A write between two
kb_statuscalls invalidates the cache because it bumps the graph generation; an out-of-band file edit does the same (the graph cache re-validates on the nextgraphView()call). - The graph cache tracks concept files only, so the key also stamps (path,
size, mtime) of everything else under the KB root except
.gitand concept bodies — map descriptors and indexes, assets,glossary.yaml,paths.yaml, hooks, templates. Without it amap_updatewould serve stale findings. - Findings computed while a write landed are returned but not cached: the key is re-read after the lint and must equal the one read before.
- The cached slice is never nil, so "cached, clean" is not mistaken for "not computed".
- Amends D290's "computed on every call" to "computed once per generation, then cached". The cache key is the generation, not a timer.