Skip to content

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.Stat with a per-Run file-exists cache — still two syscalls per new target in a run; the enumeration map is zero additional syscalls.

Consequences.

  • The relPathOf map built during the walk must mirror resolveConceptRelPath's "direct form wins when both exist" rule; the oracle test pins this.
  • The conformance cache stores unfiltered findings; uiVisibleFindings must 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_status calls 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 next graphView() call).
  • The graph cache tracks concept files only, so the key also stamps (path, size, mtime) of everything else under the KB root except .git and concept bodies — map descriptors and indexes, assets, glossary.yaml, paths.yaml, hooks, templates. Without it a map_update would 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.