Enrich an ontology node (DOC-164 slice 5+)
Drive the agentic-code-ontology pipeline for one or more substrate nodes. Each invocation produces (or refreshes) Markdown sidecars at lineage/{repo}/understanding/{slug}.md, written by the file-analyser subagent with the live-URL-only doc rule and Gate 9 code-anchor mandate.
This skill is the maintainer-facing entry point for the per-node enrichment layer per adrs/drafts/agentic-code-ontology.md revision 2.
Prerequisite
The substrate scaffold (DOC-164 slices 1-4) must have run against the target repo. lineage/{repo}/nodes.jsonl must exist. If it doesn't, run /scan scanners/code-lineage/{axis}.md first or invoke the substrate's lineage-extractor scan {repo} --full.
Argument forms
| Form | Behaviour |
|---|---|
/enrich <path> |
Single-file enrichment. Resolves <path> (relative to a repo) to its substrate node(s) and enriches each. If the path matches multiple nodes (e.g. a controller file with multiple methods), enrich the file-level node first, then the maintainer can drill into method nodes via /enrich --node <id>. |
/enrich --node <node-id> |
Single-node enrichment by node ID (verbatim from nodes.jsonl's id field). |
/enrich --touched |
Per-PR incremental. Reads lineage/{repo}/manifest.yaml's enrichment.last_enriched_commit; computes git diff against repo HEAD; enriches every node whose source file appears in the diff (1-hop fanout NOT YET — slice 5 is single-file granularity). |
/enrich --batch <axis> |
Backfill mode. Picks the next N (default 5) unenriched-or-stale nodes from a given axis (ui_shell, ui_routes, controllers, openapi_tags, config_prefixes). Useful for working through the backlog one axis at a time. |
/enrich --max-nodes N |
Cap how many enrichments this session runs. Default 5 (fits comfortably in a session's context budget; tune empirically). Caps any of the above forms. |
Protocol
1. Orient
Read these (skip if already loaded this session):
CLAUDE.md— the workspace's quality baradrs/drafts/agentic-code-ontology.md— the layered ADR + the per-node sidecar schema.claude/agents/file-analyser.md— the subagent system prompt you'll invoke (so you know what input shape it expects)
2. Resolve the worklist
Build a list of (node_id, path, kind, axis) tuples for this invocation:
- For
<path>form:jq -c 'select(.path == "<path>" or (.path | startswith("<path>:")))' lineage/{repo}/nodes.jsonl. Pick the file-level match first; if multiple symbol-level nodes also match, list them but don't enrich them this invocation (one node at a time). - For
--node <id>:jq -c 'select(.id == "<id>")' lineage/{repo}/nodes.jsonl. Exactly one match expected; error if 0 or >1. - For
--touched: readlineage/{repo}/manifest.yaml'senrichment.last_enriched_commit(orlast_scan_commitif no enrichment block yet).git -C ../{repo} diff --name-only <commit>..HEAD→ list of touched files. For each touched file,jqfor nodes whosepathstarts with that file. Cap at--max-nodes. - For
--batch <axis>:jq -c 'select(.axis == "<axis>")' lineage/{repo}/nodes.jsonl→ all nodes; filter out those with an existinglineage/{repo}/understanding/{slug}.mdwhoseenriched_at_commitmatches current HEAD; pick the first--max-nodes.
If the worklist is empty, report "Nothing to enrich (all nodes current)" and exit.
3. Resolve the sidecar slug for each node
Slug rule (matches the ADR): node_id.replace(' ', '__').replace('/', '_').replace(':', '__').replace('.', '_'). Filesystem-safe, reversible, greppable.
Sidecar path: lineage/{repo}/understanding/{slug}.md.
4. Resolve scaffold edges per node (1-hop)
For each node in the worklist, read lineage/{repo}/edges.jsonl and collect:
imports: edges where this node issrcand type=importsimported-by: edges where this node isdstand type=importsexposes: src→dst with type=exposesconfigures: src→dst with type=configuresmounts: src→dst with type=mounts
This goes into the file-analyser's prompt as SCAFFOLD_EDGES.
4.5. Cache check (slice 6+)
Before spawning file-analyser for a node, check whether the sidecar is already current:
- If
lineage/{repo}/understanding/{slug}.mddoes NOT exist → CACHE MISS, proceed to spawn. - If the sidecar exists, parse its frontmatter:
enriched_at_commit— substrate commit at which the sidecar was authoredprompt_version— the file-analyser prompt version that authored it- (planned:
extracted_at_commit,model,scaffold_hashfor finer cache keys)
- Compare to the current substrate state (manifest's
last_scan_commit) AND the current file-analyser prompt version (read the frontmatter of.claude/agents/file-analyser.md— typicallyfile-analyser/0.1.0for slice 5/6). - Skip the spawn IF:
enriched_at_commit == last_scan_commit(substrate hasn't moved)prompt_version == current file-analyser version(prompt unchanged)- The node's source file hasn't changed since
enriched_at_commit(usegit -C ../{repo} log --oneline <enriched_at_commit>..HEAD -- <source_path>— empty output = unchanged)
- Force re-enrichment when the maintainer passes
--no-cacheor--force. Default is cache-on; this is the cheap/fast path.
Cache hits are reported as <node-id>: cached (no change since <commit>). Cache misses fall through to the spawn step.
The cache invariant per the ADR: an enrichment is reused iff (node_id, scaffold_hash_of_file, prompt_version, model) matches. MVP slice-6 collapses this to (node_id, source_unchanged_since_enriched_at_commit, prompt_version) — the source-unchanged check is a proxy for scaffold_hash. Future slices may add explicit content-hash keys if false-cache-misses surface.
5. Spawn the file-analyser per cache-missed node
Invoke the file-analyser subagent via the Agent tool, one node at a time (parallel-spawn is fine for independent nodes — they don't share state). Construct the prompt as:
NODE_ID: <id>
NODE_KIND: <kind>
AXIS: <axis>
PATH: <path>
REPO: <repo>
REPO_ROOT_ABS: <absolute, e.g. /home/.../work/odd/{repo}>
SCAFFOLD_EDGES:
imports: [...]
imported-by: [...]
exposes: [...]
configures: [...]
mounts: [...]
NODE_METADATA: <verbatim from nodes.jsonl's metadata block>
SIDECAR_TARGET: <absolute path>
WORKSPACE_ROOT_ABS: <absolute, e.g. /home/.../work/odd/odd-team>
EXISTING_SIDECAR: <if a sidecar exists, the verbatim content; else "(none)">
The subagent's tool surface is fixed by the .claude/agents/file-analyser.md frontmatter (Read, Grep, Glob, WebFetch, Write). It writes the sidecar via Write to SIDECAR_TARGET and replies with Wrote: <path> + Confidence: ....
6. Validate the resulting sidecar
For each sidecar produced:
- Run the validator:
python -m lineage_extractor.cli validate-sidecar <sidecar-path>(Bash). The validator parses YAML frontmatter + named Markdown sections and verifies required fields are present. - If validation fails, log the failure to
lineage/{repo}/enrichment.logand surface to the maintainer. Do NOT auto-retry in slice 5 — single-shot per invocation; the maintainer iterates the prompt if quality is wrong.
7. Update the manifest
After all nodes in the worklist complete (success or failure):
- Read
lineage/{repo}/manifest.yaml. - Add or update the
enrichmentblock:schema_version: 0.1.0subagent_versions.file-analyser: file-analyser/0.1.0last_enriched_commit: <repo's HEAD commit>(only advance if every node in the worklist succeeded)enriched_node_count: <count of unique sidecars under understanding/>total_node_count: <count of unique node IDs in nodes.jsonl>enrichment_coverage: <pct>last_session_id: <Claude Code session id, if available; else timestamp>
- Append to
lineage/{repo}/enrichment.log: one line per node —<timestamp>\t<session_id>\t<node_id>\t<sidecar_path>\t<confidence>\t<doc_urls_fetched>.
8. Report
Concise output:
- Worklist size: N
- Successful enrichments: M
- Failed enrichments: K (list them with the validator's reason)
- Coverage now: X% (Y/Z nodes)
- Suggested next:
/enrich --batch <axis>for the next N unenriched nodes, or/enrich <specific-path>for ad-hoc work.
Rules
- Live-URL-only doc rule is non-negotiable. If a sidecar has a
documents.declared_docsentry withoutlast_verified_status, validation fails. The file-analyser prompt enforces this; the validator double-checks. - One sidecar per node. Don't enrich neighbour nodes in the same invocation — they get their own
/enrichcall when the maintainer chooses. - Don't auto-fix the source code. The file-analyser's tool surface excludes
Edit. If enrichment surfaces a bug, log it as a backlog item viaplaybooks/follow-up-on-disk.md; don't patch the code. - Preserve
## Maintainer notes. If a sidecar already exists, pass its content asEXISTING_SIDECARso the file-analyser preserves the maintainer notes block. - Skip auto-advance if any node failed.
last_enriched_commitonly advances when every node in the worklist passed validation. Partial success leaves the mark where it was. - Cap at
--max-nodes. Token budget per session is the constraint. Default 5; the maintainer can raise if files are small.
Failure modes to surface (not auto-fix)
- Validator rejects sidecar (missing required field, fabricated file:line, banned phrase) → log + report; maintainer iterates the prompt.
- WebFetch fails (404 doc URL) → the sidecar records
last_verified_status: 404; the doc-gap-finder reducer (slice 7) will surface these as DOC-NNN candidates. - Source file hash differs from substrate's
nodes.jsonl(substrate is stale relative to current HEAD) → report; recommendlineage-extractor scan {repo} --fullfirst.
Cross-references
- Subagent:
.claude/agents/file-analyser.md - Validator:
lineage/_extractor/src/lineage_extractor/validators.py - ADR:
adrs/drafts/agentic-code-ontology.mdrevision 2 - Substrate ADR:
adrs/drafts/code-lineage-substrate.mdrevision 2 - Probe set:
lineage/PROBES.md(extended for semantic claims peradrs/drafts/research/agentic-code-ontology/PROBES.md)