D316 — Lint covers the KB's artifacts, not only its concepts¶
Decision. A whole-KB lint checks what a KB ships beside its concepts: skills
(skill_invalid, skill_warning from skill.Validate; skill_broken_ref;
skill_git_command; skill_missing_perimeter), every artifact text file
(legacy_tool_name, cross_kb_path), the KB itself (missing_instructions), the
junk files git tracks (junk_file, and junk_asset in place of orphan_asset),
and sops pipelines in skills and concepts (sops_format_mismatch,
sops_missing_file). Two optional instructions.md frontmatter keys feed it:
perimeter and legacy_paths. legacy_path and legacy_tool_name carry mechanical
fixes (replace_prefix, strip_tool_prefix) that kb_repair applies. kb create
writes data/.gitignore with the shared junk patterns, the Atlas Artifacts panel
shows the findings per artifact, and a materialized KB skill gets a generated
SOURCE.env naming its KB.
Why. Lint saw concepts and hooks (D284) and nothing else, so a skill that no
client could catalogue, a skill calling pre-D288 tool names, or a .pyc committed by
an agent stayed invisible until something broke on a client. orphan_asset's one
advice ("cite it") was followed literally on a .pyc. Every check is a regex or a
stat, warning or info, so no gate turns red on an existing KB; the cost is false
positives, which is why code is scanned and prose is not, the cross-KB roots are
injected rather than guessed, and the perimeter and legacy-path checks only run when
the KB declares the key.
Alternatives rejected.
- A separate artifact_lint tool: two lint surfaces, two caches, and the Observatory,
kb_status and the gate would each need to learn it; one lint with artifact paths
marked as such (Finding.Artifact) costs one field.
- Run unchanged and only the lint tool passing sibling roots (RunWithOptions):
the cached findings behind kb_status, the Atlas and the gate would disagree with
the lint tool on the same KB. The roots live on kb.KB.SiblingRoots, set at
mount; RunWithOptions stays for an explicit caller.
- Run the concept-side sops and legacy-path checks in the unscoped block: they would
ignore scope and lint_ignore, which the plan promised them; they run per concept.
- One legacy_path finding per concept with every prefix in its message: a Fix has
one field/to, so the repair would have to re-read instructions.md. One finding
per declared prefix found (never per occurrence) keeps the fix self-contained; the
repair groups a concept's prefixes and applies them in one pass, longest first.
- Resolve skill_broken_ref paths against the KB root only: scripts/run.sh in a
skill conventionally means the skill's own directory, so both are tried.
- Scan Python relative imports for broken references: a relative import resolves
against the importing script, which a SKILL.md body does not name.
- CARTOGRAPHER_KB_ROOT exported by the bootstrap hook: Claude-only
(CLAUDE_ENV_FILE); a file beside the skill works for every client. Exported by
the service: a user daemon cannot inject variables into client processes.
- CARTOGRAPHER_KB_ROOT always: a client fed over HTTP has no copy of the KB, and the
server's path would point at nothing on it; the line is written only when the root
is on the materializing machine.
- A root .gitignore: D62 removed it and stands; data/.gitignore travels with the
content, written only when kb create creates the KB so an existing KB gets no
untracked file.
- kb_repair legacy_tool_name without allow_artifact_write: it would be a second
way to write a skill that the per-KB opt-in (D71) does not gate.
Consequences. A caller that maps a finding path back to a concept must honour
Finding.Artifact (findingConcept in the UI API, planRepair, lint.Review);
an artifact finding is shown only to a whole-KB reader. kb.JunkPatterns is the one
junk list: kb create, junk_file, junk_asset and the orphan_asset exclusion
read it. SOURCE.env is a generated file like hermes' SOURCE.md: part of the
skill's materialized hash and lockfile entry, and a KB skill shipping a file of that
name is not materialized; a skill materialized earlier gets it on its next change.
The legacyToolNameRe relies on no current tool name containing __, and the
cross-KB match on a root boundary, so kb never matches kb-b.