15 KiB
Auto-Update Knowledge Graph (Internal — Hook-Triggered)
Incrementally update the knowledge graph using deterministic structural fingerprinting to minimize token usage. This prompt is triggered automatically by the post-commit hook when autoUpdate is enabled. It is NOT a user-facing skill.
Key principle: Spend zero LLM tokens when changes are cosmetic (formatting, internal logic). Only invoke LLM agents when structural changes (new/removed functions, classes, imports, exports) are detected.
Phase 0 — Pre-flight (Zero Token Cost)
-
Set
PROJECT_ROOTto the current working directory. -
Check that
$PROJECT_ROOT/.understand-anything/knowledge-graph.jsonexists.- If not: report "No existing knowledge graph found. Run
/understandfirst to create one." and STOP.
- If not: report "No existing knowledge graph found. Run
-
Check that
$PROJECT_ROOT/.understand-anything/meta.jsonexists and readgitCommitHash.- If not: report "No analysis metadata found. Run
/understandto create a baseline." and STOP.
- If not: report "No analysis metadata found. Run
-
Get current commit hash:
git rev-parse HEAD -
If commit hashes match and
--forceis NOT in$ARGUMENTS: report "Knowledge graph is already up to date." and STOP. -
Get changed files:
git diff <lastCommitHash>..HEAD --name-onlyIf no files changed: update
meta.jsonwith the new commit hash and STOP. -
Filter to source files only (
.ts,.tsx,.js,.jsx,.py,.go,.rs,.java,.rb,.cpp,.c,.h,.cs,.swift,.kt,.php). If no source files changed: updatemeta.jsonwith the new commit hash, report "Only non-source files changed. Metadata updated." and STOP. -
Create intermediate directory:
mkdir -p $PROJECT_ROOT/.understand-anything/intermediate -
Apply
.understandignoreexclusions (same semantics as/understandStep 2.5 inagents/project-scanner.md).Without this step, files in user-excluded paths (migrations, vendored code, tests) are counted as structural changes and can spuriously escalate the action to
FULL_UPDATEeven when the real change set is tiny.-
If neither
$PROJECT_ROOT/.understand-anything/.understandignorenor$PROJECT_ROOT/.understandignoreexists, the step 7 extension filter is sufficient — skip to Phase 1. -
Write the step 7 file list to
$PROJECT_ROOT/.understand-anything/intermediate/changed-files-pre.jsonas a JSON array of relative paths. -
Resolve
$PLUGIN_ROOT:- Use
$CLAUDE_PLUGIN_ROOTif set (Claude Code's hook context sets this). - Otherwise try
$HOME/.understand-anything-plugin. - Validate the chosen candidate by checking
$candidate/packages/core/dist/ignore-filter.jsexists. - If neither resolves: report "Cannot locate plugin install at
$CLAUDE_PLUGIN_ROOTor$HOME/.understand-anything-plugin; auto-update aborted. Run/understandto re-baseline." and STOP. Do not silently skip — silent skip reproduces issue #153.
- Use
-
Write
$PROJECT_ROOT/.understand-anything/intermediate/ignore-filter.mjs:import { readFileSync, writeFileSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; import path from 'node:path'; const PROJECT_ROOT = process.cwd(); const PLUGIN_ROOT = process.argv[2]; const inputPath = process.argv[3]; const modUrl = pathToFileURL( path.join(PLUGIN_ROOT, 'packages/core/dist/ignore-filter.js'), ).href; const { createIgnoreFilter } = await import(modUrl); const filter = createIgnoreFilter(PROJECT_ROOT); const input = JSON.parse(readFileSync(inputPath, 'utf-8')); const kept = input.filter((p) => !filter.isIgnored(p)); const removed = input.length - kept.length; writeFileSync( path.join(PROJECT_ROOT, '.understand-anything/intermediate/changed-files.json'), JSON.stringify({ kept, removed, total: input.length }, null, 2), ); console.log(`.understandignore: kept ${kept.length}/${input.length} (removed ${removed})`); -
Run it:
node $PROJECT_ROOT/.understand-anything/intermediate/ignore-filter.mjs \ "$PLUGIN_ROOT" \ $PROJECT_ROOT/.understand-anything/intermediate/changed-files-pre.json -
Read
$PROJECT_ROOT/.understand-anything/intermediate/changed-files.json. Pass thekeptarray as the input file list for Phase 1's fingerprint-check script. -
If
kept.length === 0: updatemeta.jsonwith the new commit hash, report "All changed source files are in ignored paths. Metadata updated." and STOP.
-
Phase 1 — Structural Fingerprint Check (Zero LLM Tokens)
This phase runs a deterministic Node.js script that compares file structures against stored fingerprints. It costs zero LLM tokens — only the script execution cost.
- Write and execute a Node.js script (
$PROJECT_ROOT/.understand-anything/intermediate/fingerprint-check.mjs):
// The script should:
// 1. Read fingerprints.json from .understand-anything/fingerprints.json
// 2. For each changed source file:
// a. Read the file content
// b. Compute SHA-256 content hash
// c. If content hash matches stored hash → NONE (skip)
// d. Extract structural elements via regex:
// - Functions: match patterns like `function NAME(`, `const NAME = (`, `export function NAME(`
// - Classes: match `class NAME`, `export class NAME`
// - Imports: match `import ... from '...'`, `import '...'`
// - Exports: match `export { ... }`, `export default`, `export function`, `export class`, `export const`
// e. Compare extracted elements against stored fingerprint
// f. Classify as NONE, COSMETIC, or STRUCTURAL
// 3. For new files (not in fingerprints.json): classify as STRUCTURAL
// 4. For deleted files (in fingerprints.json but not on disk): classify as STRUCTURAL
// 5. Determine overall decision:
// - All NONE/COSMETIC → action: "SKIP"
// - Some STRUCTURAL, ≤10 files, same directories → action: "PARTIAL_UPDATE"
// - New/deleted directories or >10 structural files → action: "ARCHITECTURE_UPDATE"
// - >30 structural files or >50% of graph → action: "FULL_UPDATE"
// 6. Write result to .understand-anything/intermediate/change-analysis.json
The output JSON should have this shape:
{
"action": "SKIP | PARTIAL_UPDATE | ARCHITECTURE_UPDATE | FULL_UPDATE",
"filesToReanalyze": ["src/new-feature.ts"],
"rerunArchitecture": false,
"rerunTour": false,
"reason": "1 file has structural changes (new function added)",
"fileChanges": [
{ "filePath": "src/utils.ts", "changeLevel": "COSMETIC", "details": ["internal logic changed"] },
{ "filePath": "src/new-feature.ts", "changeLevel": "STRUCTURAL", "details": ["new function: handleRequest"] }
]
}
-
Read
.understand-anything/intermediate/change-analysis.json. -
Decision gate:
Action What to do SKIPUpdate meta.jsonwith new commit hash. Report: "No structural changes detected. Graph metadata updated. Zero tokens spent." STOP.FULL_UPDATEReport: "Major structural changes detected (reason). Recommend running /understand --fullfor a complete rebuild." STOP.PARTIAL_UPDATEProceed to Phase 2 with filesToReanalyzeARCHITECTURE_UPDATEProceed to Phase 2 with filesToReanalyze, flag architecture re-run
Phase 2 — Targeted Re-Analysis (Minimal Token Cost)
Only re-analyze files with structural changes. This is the only phase that costs LLM tokens.
-
Read the existing knowledge graph from
$PROJECT_ROOT/.understand-anything/knowledge-graph.json. -
Batch the files from
filesToReanalyze(from Phase 1). Use a single batch if ≤10 files, otherwise batch into groups of 5-10. -
For each batch, dispatch a subagent using the
file-analyzeragent definition (atagents/file-analyzer.md). Append:Additional context from main session:
Project:
<projectName from existing graph>—<projectDescription>Frameworks detected:<frameworks from existing graph>Languages:<languages from existing graph>IMPORTANT: This is an incremental update. Only the files listed below have structural changes. Analyze them thoroughly but do not invent nodes for files not in this batch.
Fill in batch-specific parameters:
Analyze these source files and produce GraphNode and GraphEdge objects. Project root:
$PROJECT_ROOTProject:<projectName>Languages:<languages>Batch index:1Write output to:$PROJECT_ROOT/.understand-anything/intermediate/batch-1.jsonAll project files (for import resolution):
<file list from existing graph nodes>Files to analyze in this batch:
<path>(<sizeLines>lines) ...
-
After batch(es) complete, read each
batch-<N>.jsonand merge results. -
Merge with existing graph:
- Remove old nodes whose
filePathmatches any file infilesToReanalyzeor in the deleted files list - Remove old edges whose
sourceortargetreferences a removed node - Add new nodes and edges from the fresh analysis
- Deduplicate nodes by ID (keep latest), edges by
source + target + type - Remove any edge with dangling
sourceortargetreferences
- Remove old nodes whose
Phase 3 — Conditional Architecture/Tour + Save
3a. Architecture update (only if rerunArchitecture === true)
If the change analysis flagged ARCHITECTURE_UPDATE:
-
Dispatch a subagent using the
architecture-analyzeragent definition (atagents/architecture-analyzer.md), passing the full merged node set and import edges. Include previous layer definitions for naming consistency:Previous layer definitions (for naming consistency):
[previous layers from existing graph]Maintain the same layer names and IDs where possible. Only add/remove layers if the file structure has materially changed.
-
After completion, read and normalize layers (same normalization as
/understandPhase 4). -
Optionally re-run tour builder if layers changed significantly.
3b. Lite layer update (if rerunArchitecture === false)
If only a partial update:
- For new files: assign them to the most likely existing layer based on directory path matching
- For deleted files: remove their IDs from layer
nodeIdsarrays - Remove any layer that ends up with zero nodeIds
3c. Lite validation
Perform lightweight validation (no graph-reviewer agent):
- Remove any edge with dangling
sourceortarget - Remove any layer
nodeIdsentry that doesn't exist in the node set - Ensure every file node appears in exactly one layer (add to a catch-all layer if missing)
3d. Save
-
Write the final knowledge graph to
$PROJECT_ROOT/.understand-anything/knowledge-graph.json. -
Write updated metadata to
$PROJECT_ROOT/.understand-anything/meta.json:{ "lastAnalyzedAt": "<ISO 8601 timestamp>", "gitCommitHash": "<current commit hash>", "version": "1.0.0", "analyzedFiles": <total file count in graph> } -
Update fingerprints (LOAD-PATCH-SAVE, not OVERWRITE).
The most common failure mode here: writing only the freshly-computed batch entries to
fingerprints.json, discarding every other file's fingerprint. The next auto-update then sees all those files as new (no stored fingerprint), classifies them as STRUCTURAL, and escalates to FULL_UPDATE permanently (issue #152). The script must LOAD ALL existing entries, PATCH only the re-analyzed ones, and SAVE the full dict back.Write and execute a Node.js script in this exact ordering:
import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { createHash } from 'node:crypto'; import path from 'node:path'; const fpPath = path.join(PROJECT_ROOT, '.understand-anything', 'fingerprints.json'); const existedAndNonEmpty = existsSync(fpPath) && readFileSync(fpPath, 'utf-8').trim().length > 0; // 1. LOAD ALL existing entries (NEVER skip — preserves un-analyzed files) const all = existedAndNonEmpty ? JSON.parse(readFileSync(fpPath, 'utf-8')) : {}; const before = Object.keys(all).length; // 2. PATCH (file still exists) or REMOVE (file deleted) for each re-analyzed path. // `filesToReanalyze` may include paths that were deleted in this commit — // handle both branches inline rather than expecting a separate deleted list. for (const filePath of filesToReanalyze) { const fullPath = path.join(PROJECT_ROOT, filePath); if (!existsSync(fullPath)) { delete all[filePath]; continue; } const content = readFileSync(fullPath, 'utf-8'); const contentHash = createHash('sha256').update(content).digest('hex'); // Extract functions, classes, imports, exports via the same regex as Phase 1. all[filePath] = { contentHash, functions, classes, imports, exports }; } // 3. GUARD against silent load failure: if fingerprints.json existed and was // non-empty but `before` came out as 0, refuse to overwrite — something // went wrong reading the file and writing now would clobber every entry. if (existedAndNonEmpty && before === 0) { throw new Error('fingerprints.json existed and was non-empty but loaded as {} — refusing to overwrite'); } // 4. SAVE ALL entries back (full dict — not just the patched subset) writeFileSync(fpPath, JSON.stringify(all, null, 2)); console.log(`Fingerprints: ${before} → ${Object.keys(all).length}`);The
existedAndNonEmpty && before === 0guard catches the silent-load-failure case before it corrupts the store. If the count shrinks from N to a small number that matches the batch size, the LOAD step was skipped — abort the write rather than persist the wrong dict. -
Clean up intermediate files:
rm -rf $PROJECT_ROOT/.understand-anything/intermediate -
Report a summary:
- Files checked: N (total changed)
- Structural changes found: N files
- Cosmetic-only changes: N files (skipped)
- Nodes updated: N
- Action taken: PARTIAL_UPDATE / ARCHITECTURE_UPDATE
- Path to output:
$PROJECT_ROOT/.understand-anything/knowledge-graph.json
Error Handling
- If the fingerprint check script fails: fall back to treating all changed files as STRUCTURAL (conservative approach).
- If
fingerprints.jsondoesn't exist: treat all changed files as STRUCTURAL and regenerate fingerprints after the update. - If a subagent dispatch fails: retry once. If it fails again, save partial results and report the error.
- ALWAYS save partial results — a partially updated graph is better than no update.
Notes
- This skill reuses the same
file-analyzerandarchitecture-analyzeragent definitions as/understand— no separate agent prompts needed. - The fingerprint comparison in Phase 1 uses regex-based extraction (not tree-sitter) because it runs as a temporary Node.js script and doesn't need full AST accuracy — just signature-level detection.
- The authoritative fingerprints stored in
fingerprints.jsonare generated by/understandPhase 7 using the corefingerprint.tsmodule (which uses tree-sitter for precise extraction).