
Understand
- 2k installs
- 77.5k repo stars
- Updated July 30, 2026
- lum1104/understand-anything
understand is an agent skill for Analyze a codebase to produce an interactive knowledge graph for understanding architecture, components, and relationshi
About
The understand skill Analyze a codebase to produce an interactive knowledge graph for understanding architecture, components, and relationships It covers $ARGUMENTS may contain:. Key workflows include --full - Force a full rebuild, ignoring any existing graph. Developers invoke understand when the task matches the triggers and reference files in SKILL.md for grounded, stepwise execution. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation.
- $ARGUMENTS may contain:
- --full - Force a full rebuild, ignoring any existing graph
- --auto-update - Enable automatic graph updates on commit writes autoUpdate: true to .understand-anything/config.json
- --no-auto-update - Disable automatic graph updates writes autoUpdate: false to .understand-anything/config.json
- --review - Run full LLM graph-reviewer instead of inline deterministic validation
Understand by the numbers
- 1,986 all-time installs (skills.sh)
- +48 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #393 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
understand capabilities & compatibility
- Capabilities
- $arguments may contain: · full force a full rebuild, ignoring any exis · auto update enable automatic graph updates o · no auto update disable automatic graph updat · review run full llm graph reviewer instead o
- Use cases
- documentation
What understand says it does
description: Analyze a codebase to produce an interactive knowledge graph for understanding architecture, components, and relationships
npx skills add https://github.com/lum1104/understand-anything --skill understandAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2k |
|---|---|
| repo stars | ★ 77.5k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 30, 2026 |
| Repository | lum1104/understand-anything ↗ |
What problem does understand solve for developers using the documented workflows?
Analyze a codebase to produce an interactive knowledge graph for understanding architecture, components, and relationships
Who is it for?
Developers working with understand patterns described in the skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill documented scope.
When should I use this skill?
Use when Analyze a codebase to produce an interactive knowledge graph for understanding architecture, components, and relationships
What you get
Actionable understand guidance grounded in SKILL.md workflows and reference files.
- .understand-anything/fingerprints.json
- Structural fingerprint baseline
Files
/understand
Analyze the current codebase and produce a knowledge-graph.json file in .understand-anything/. This file powers the interactive dashboard for exploring the project's architecture.
Options
$ARGUMENTSmay contain:--full— Force a full rebuild, ignoring any existing graph--auto-update— Enable automatic graph updates on commit (writesautoUpdate: trueto.understand-anything/config.json)--no-auto-update— Disable automatic graph updates (writesautoUpdate: falseto.understand-anything/config.json)--review— Run full LLM graph-reviewer instead of inline deterministic validation--language <lang>— Generate all textual content (summaries, descriptions, tags, titles, languageNotes, languageLesson) in the specified language. Accepts ISO 639-1 codes (zh,ja,ko,en,es,fr,de, etc.) or friendly names (chinese,japanese,korean,english,spanish, etc.). Locale variants supported:zh-TW,zh-HK, etc. Defaults toen(English). Stores preference in.understand-anything/config.jsonfor consistency across incremental updates.- A directory path (e.g.
/path/to/repoor../other-project) — Analyze the given directory instead of the current working directory
---
Progress Reporting
Throughout execution, report progress to the user at each phase transition and during batch processing. This keeps users informed on large codebases where analysis can take a long time.
- Phase transitions: At the start of each phase, print a status line:
[Phase N/7] <phase name>...>
Example: [Phase 2/7] Analyzing files (12 batches)...- Batch progress: During Phase 2, report each batch with its index and total:
Analyzing batch X/N (files: foo.ts, bar.ts, ...)(list up to 3 filenames, then...if more)
- Phase completion: When a phase finishes, briefly confirm:
Phase N complete. <one-line summary of result>>
Example: Phase 1 complete. Found 247 files across 3 languages.---
Phase 0 — Pre-flight
Determine whether to run a full analysis or incremental update.
1. Resolve `PROJECT_ROOT`:
- Parse
$ARGUMENTSfor a non-flag token (any argument that does not start with--). If found, treat it as the target directory path. - If the path is relative, resolve it against the current working directory.
- Verify the resolved path exists and is a directory (run
test -d <path>). If it does not exist or is not a directory, report an error to the user and STOP. - Set
PROJECT_ROOTto the resolved absolute path. - If no directory path argument is found, set
PROJECT_ROOTto the current working directory. - Worktree redirect. If
PROJECT_ROOTis inside a git worktree (not the main checkout), redirect output to the main repository root. Worktrees managed by Claude Code are ephemeral —.understand-anything/written there is destroyed when the session ends, taking the knowledge graph with it (issue #133). Detect a worktree by comparinggit rev-parse --git-diragainstgit rev-parse --git-common-dir; in a normal checkout or submodule they resolve to the same path, in a worktree they differ and the parent of--git-common-diris the main repo root.
COMMON_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-common-dir 2>/dev/null)
GIT_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-dir 2>/dev/null)
if [ -n "$COMMON_DIR" ] && [ -n "$GIT_DIR" ]; then
COMMON_ABS=$(cd "$PROJECT_ROOT" && cd "$COMMON_DIR" 2>/dev/null && pwd -P)
GIT_ABS=$(cd "$PROJECT_ROOT" && cd "$GIT_DIR" 2>/dev/null && pwd -P)
if [ -n "$COMMON_ABS" ] && [ "$COMMON_ABS" != "$GIT_ABS" ]; then
MAIN_ROOT=$(dirname "$COMMON_ABS")
if [ -d "$MAIN_ROOT" ] && [ "${UNDERSTAND_NO_WORKTREE_REDIRECT:-0}" != "1" ]; then
echo "[understand] Detected git worktree at $PROJECT_ROOT"
echo "[understand] Redirecting output to main repo root: $MAIN_ROOT"
echo "[understand] (Set UNDERSTAND_NO_WORKTREE_REDIRECT=1 to keep PROJECT_ROOT as the worktree.)"
PROJECT_ROOT="$MAIN_ROOT"
fi
fi
fiSet UNDERSTAND_NO_WORKTREE_REDIRECT=1 if you intentionally want a per-worktree graph (rare — most users want the redirect). 1.5. Ensure the plugin is built. Later phases invoke Node scripts that import @understand-anything/core. On a fresh install packages/core/dist/ does not exist yet — build once.
Important: do not assume the plugin root is simply two directories above the skill path string. In many installations ~/.agents/skills/understand is a symlink into the real plugin checkout. Prefer runtime-provided plugin roots first (for Claude), then fall back to universal symlinks, skill symlink resolution, and common clone-based install paths.
Resolve the plugin root like this:
SKILL_REAL=$(realpath ~/.agents/skills/understand 2>/dev/null || readlink -f ~/.agents/skills/understand 2>/dev/null || echo "")
SELF_RELATIVE=$([ -n "$SKILL_REAL" ] && cd "$SKILL_REAL/../.." 2>/dev/null && pwd || echo "")
COPILOT_SKILL_REAL=$(realpath ~/.copilot/skills/understand 2>/dev/null || readlink -f ~/.copilot/skills/understand 2>/dev/null || echo "")
COPILOT_SELF_RELATIVE=$([ -n "$COPILOT_SKILL_REAL" ] && cd "$COPILOT_SKILL_REAL/../.." 2>/dev/null && pwd || echo "")
PLUGIN_ROOT=""
for candidate in \
"${CLAUDE_PLUGIN_ROOT}" \
"$HOME/.understand-anything-plugin" \
"$SELF_RELATIVE" \
"$COPILOT_SELF_RELATIVE" \
"$HOME/.codex/understand-anything/understand-anything-plugin" \
"$HOME/.opencode/understand-anything/understand-anything-plugin" \
"$HOME/.pi/understand-anything/understand-anything-plugin" \
"$HOME/understand-anything/understand-anything-plugin"; do
if [ -n "$candidate" ] && [ -f "$candidate/package.json" ] && [ -f "$candidate/pnpm-workspace.yaml" ]; then
PLUGIN_ROOT="$candidate"
break
fi
done
if [ -z "$PLUGIN_ROOT" ]; then
echo "Error: Cannot find the understand-anything plugin root."
echo "Checked:"
echo " - ${CLAUDE_PLUGIN_ROOT:-<unset CLAUDE_PLUGIN_ROOT>}"
echo " - $HOME/.understand-anything-plugin"
echo " - ${SELF_RELATIVE:-<unresolved path derived from ~/.agents/skills/understand>}"
echo " - ${COPILOT_SELF_RELATIVE:-<unresolved path derived from ~/.copilot/skills/understand>}"
echo " - $HOME/.codex/understand-anything/understand-anything-plugin"
echo " - $HOME/.opencode/understand-anything/understand-anything-plugin"
echo " - $HOME/.pi/understand-anything/understand-anything-plugin"
echo " - $HOME/understand-anything/understand-anything-plugin"
echo "Make sure the plugin is installed correctly."
exit 1
fi
if [ ! -f "$PLUGIN_ROOT/packages/core/dist/index.js" ]; then
cd "$PLUGIN_ROOT" && (pnpm install --frozen-lockfile 2>/dev/null || pnpm install) && pnpm --filter @understand-anything/core build
fiIf pnpm is missing, report to the user: "Install Node.js ≥ 22 and pnpm ≥ 10, then re-run /understand."
2. Get the current git commit hash:
git rev-parse HEAD3. Create the intermediate and temp output directories:
mkdir -p $PROJECT_ROOT/.understand-anything/intermediate
mkdir -p $PROJECT_ROOT/.understand-anything/tmp3.1. Purge stale trash dirs. Phase 7 cleanup mvs scratch dirs into .trash-<timestamp>/ rather than rm -rfing them directly (see issue #301), so that destructive-action gates on hardened hosts don't trip on just-created paths. Reclaim the space here once the trash is older than 7 days — by this point any freshness-window check has long since stopped caring about those dirs:
find $PROJECT_ROOT/.understand-anything/ -maxdepth 1 -type d -name '.trash-*' -mtime +7 -exec rm -rf {} + 2>/dev/null || true3.5. Auto-update configuration:
- If
--auto-updateis in$ARGUMENTS: write{"autoUpdate": true}to$PROJECT_ROOT/.understand-anything/config.json - If
--no-auto-updateis in$ARGUMENTS: write{"autoUpdate": false}to$PROJECT_ROOT/.understand-anything/config.json - These flags only set the config — analysis proceeds normally regardless.
3.6. Language configuration:
- Parse
$ARGUMENTSfor--language <lang>flag. If found, extract the language code. - Language code normalization: Map friendly names to ISO codes:
chinese→zh,japanese→ja,korean→ko,english→en,spanish→es,french→fr,german→de,portuguese→pt,russian→ru,arabic→ar, etc.- Locale variants:
zh-TW,zh-HK,zh-CN,pt-BR, etc. are preserved as-is. - If
--languageis NOT specified: - Stored preference wins. If
$PROJECT_ROOT/.understand-anything/config.jsonhas anoutputLanguagefield, set$OUTPUT_LANGUAGEto it and skip the rest. - Otherwise detect (first run only). Infer the predominant language of the user's conversation as an ISO 639-1 code (
$DETECTED_LANG). If it isenor cannot be confidently determined, set$OUTPUT_LANGUAGE=enand proceed silently — no prompt (English users see no change). - If `$DETECTED_LANG` ≠ `en`, confirm once before analyzing: tell the user you detected
<language>and ask whether to generate all content in it; they press Enter/"yes" to accept, or type another language code/name to override (normalize via the friendly-name map above). If running non-interactively (no reply possible), skip the wait, use$DETECTED_LANG, and print a one-line notice instead of blocking. - Persist the resolved
$OUTPUT_LANGUAGE(includingen) intoconfig.jsonso it never re-prompts for this project. - If
--languageIS specified: - Update
$PROJECT_ROOT/.understand-anything/config.jsonwith the new language: merge{"outputLanguage": "<lang>"}into existing config. - Store as
$OUTPUT_LANGUAGEfor use throughout all phases. - Language directive template: Store as
$LANGUAGE_DIRECTIVE:
> **Language directive**: Generate all textual content (summaries, descriptions, tags, titles, languageNotes, languageLesson) in **{language}**. Maintain technical accuracy while using natural, native-level phrasing in the target language. Keep technical terms in English when no standard translation exists (e.g., "middleware", "hook", "barrel").4. Check for subdomain knowledge graphs to merge: List all *knowledge-graph*.json files in $PROJECT_ROOT/.understand-anything/ excluding knowledge-graph.json itself (e.g. frontend-knowledge-graph.json, backend-knowledge-graph.json). If any subdomain graphs exist, run the merge script bundled with this skill (located next to this SKILL.md file — use the skill directory path, not the project root):
python <SKILL_DIR>/merge-subdomain-graphs.py $PROJECT_ROOTThe script discovers subdomain graphs, loads the existing knowledge-graph.json as a base (if present), and merges everything into knowledge-graph.json (deduplicating nodes and edges). Report the merge summary to the user, then continue with the merged graph.
5. Check if $PROJECT_ROOT/.understand-anything/knowledge-graph.json exists. If it does, read it. 6. Check if $PROJECT_ROOT/.understand-anything/meta.json exists. If it does, read it to get gitCommitHash. 7. Decision logic:
| Condition | Action |
|---|---|
--full flag in $ARGUMENTS | Full analysis (all phases) |
| No existing graph or meta | Full analysis (all phases) |
--review flag + existing graph + unchanged commit hash | Skip to Phase 6 (review-only — reuse existing assembled graph) |
| Existing graph + unchanged commit hash | Ask the user: "The graph is up to date at this commit. Would you like to: (a) run a full rebuild (--full), (b) run the LLM graph reviewer (--review), or (c) do nothing?" Then follow their choice. If they pick (c), STOP. |
| Existing graph + changed files | Incremental update (re-analyze changed files only) |
Review-only path: Copy the existing knowledge-graph.json to $PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json, then jump directly to Phase 6 step 3.
For incremental updates, get the changed file list:
git diff <lastCommitHash>..HEAD --name-onlyIf this returns no files, report "Graph is up to date" and STOP.
8. Collect project context for subagent injection:
- Read
README.md(orREADME.rst,readme.md) from$PROJECT_ROOTif it exists. Store as$README_CONTENT(first 3000 characters). - Read the primary package manifest (
package.json,pyproject.toml,Cargo.toml,go.mod,pom.xml) if it exists. Store as$MANIFEST_CONTENT. - Capture the top-level directory tree:
find $PROJECT_ROOT -maxdepth 2 -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' | head -100Store as $DIR_TREE.
- Detect the project entry point by checking for common patterns (in order):
src/index.ts,src/main.ts,src/App.tsx,index.js,main.py,manage.py,app.py,wsgi.py,asgi.py,run.py,__main__.py,main.go,cmd/*/main.go,src/main.rs,src/lib.rs,src/main/java/**/Application.java,Program.cs,config.ru,index.php. Store first match as$ENTRY_POINT.
---
Phase 0.5 — Ignore Configuration
Set up and verify the .understandignore file before scanning.
1. Check if $PROJECT_ROOT/.understand-anything/.understandignore exists. 2. If it does NOT exist, generate a starter file by invoking the bundled script (delegates to generateStarterIgnoreFile in @understand-anything/core, which reads .gitignore, deduplicates against built-in defaults, and emits language-grouped test-file suggestions). Pass $PLUGIN_ROOT via the env so the script doesn't have to re-derive it from its own path (which breaks for copied skill installs):
PLUGIN_ROOT="$PLUGIN_ROOT" node <SKILL_DIR>/generate-ignore.mjs $PROJECT_ROOT- Report to the user:
Generated .understand-anything/.understandignore with suggested exclusions based on your project structure. Please review it and uncomment any patterns you'd like to exclude from analysis. When ready, confirm to continue.- Wait for user confirmation before proceeding.
3. If it already exists, report:
Found .understand-anything/.understandignore. Review it if needed, then confirm to continue.- Wait for user confirmation before proceeding.
4. After confirmation, proceed to Phase 1.
---
Phase 1 — SCAN (Full analysis only)
Report to the user: [Phase 1/7] Scanning project files...
Dispatch a subagent using the project-scanner agent definition (at agents/project-scanner.md). Append the following additional context:
Additional context from main session:
>
Project README (first 3000 chars):
```
$README_CONTENT
```
>
Package manifest:
```
$MANIFEST_CONTENT
```
>
Use this context to produce more accurate project name, description, and framework detection. The README and manifest are authoritative — prefer their information over heuristics.
>
$LANGUAGE_DIRECTIVE
Pass these parameters in the dispatch prompt:
Scan this project directory to discover all project files (including non-code files like configs, docs, infrastructure), detect languages and frameworks.
Project root: $PROJECT_ROOTWrite output to: $PROJECT_ROOT/.understand-anything/intermediate/scan-result.jsonAfter the subagent completes, read $PROJECT_ROOT/.understand-anything/intermediate/scan-result.json to get:
- Project name, description
- Languages, frameworks
- File list with line counts and
fileCategoryper file (code,config,docs,infra,data,script,markup) - Complexity estimate
- Import map (
importMap): pre-resolved project-internal imports per file (non-code files have empty arrays)
Store importMap in memory as $IMPORT_MAP for use in Phase 2 batch construction. Store the file list as $FILE_LIST with fileCategory metadata for use in Phase 2 batch construction.
Gate check: If >100 files, inform the user and suggest scoping with a subdirectory argument. Proceed only if user confirms or add guidance that this may take a while.
If the scan result includes filteredByIgnore > 0, report:
Excluded {filteredByIgnore} files via .understandignore.---
Phase 1.5 — BATCH
Report: [Phase 1.5/7] Computing semantic batches...
Run the bundled batching script:
node <SKILL_DIR>/compute-batches.mjs $PROJECT_ROOTReads .understand-anything/intermediate/scan-result.json, writes .understand-anything/intermediate/batches.json.
Capture stderr. Append any line starting with Warning: to $PHASE_WARNINGS for the final report.
If the script exits non-zero, the failure is hard — relay the full stderr to the user as a Phase 1.5 failure. Do not attempt to recover; the script's internal fallback (count-based) already handles recoverable issues. A non-zero exit means a fundamental problem (missing input file, malformed JSON, etc.).
---
Phase 2 — ANALYZE
Full analysis path
Load .understand-anything/intermediate/batches.json (produced by Phase 1.5). Iterate the batches[] array.
Report: [Phase 2/7] Analyzing files — <totalFiles> files in <totalBatches> batches (up to 5 concurrent)...
For each batch, dispatch a subagent using the file-analyzer agent definition (at agents/file-analyzer.md). Run up to 5 subagents concurrently. Append the following additional context:
Additional context from main session:
>
Project:<projectName>—<projectDescription>
Languages: <languages from Phase 1>>
$LANGUAGE_DIRECTIVE
Dispatch prompt template (fill in batch-specific values from batches.json[i]):
Analyze these files and produce GraphNode and GraphEdge objects.
Project root: $PROJECT_ROOTProject: <projectName>Languages: <languages>Batch: <batchIndex>/<totalBatches>Skill directory (for bundled scripts): <SKILL_DIR>Output: write to$PROJECT_ROOT/.understand-anything/intermediate/batch-<batchIndex>.json(single-file mode) ORbatch-<batchIndex>-part-<k>.json(split mode, per Step B of your output protocol).
>
Pre-resolved import data for this batch (use directly — do NOT re-resolve imports from source):
```json
<batchImportData JSON from batches.json[i].batchImportData>
```
>
Cross-batch neighbors with their exported symbols (confidence boost for cross-batch edges):
```json
<neighborMap JSON from batches.json[i].neighborMap>
```
>
Files to analyze in this batch (every entry MUST be passed through tobatchFileswith all four fields —path,language,sizeLines,fileCategory):
1.<path>(<sizeLines> lines, language:<language>, fileCategory:<fileCategory>)
2.<path>(<sizeLines> lines, language:<language>, fileCategory:<fileCategory>)
...
Output naming is per-batchIndex — no fusion. If you fuse multiple small batches into a single file-analyzer dispatch for token efficiency, the dispatched agent must STILL write one output file per original batchIndex using batch-<batchIndex>.json or batch-<batchIndex>-part-<k>.json. The merge script's regex (batch-(\d+)(?:-part-(\d+))?\.json) silently drops any other naming (e.g., batch-fused-8-13.json, batch-8-13.json), losing every node and edge in that file. After each dispatch returns, verify each batchIndex in the dispatched input has a corresponding batch-<batchIndex>.json (or batch-<batchIndex>-part-*.json) on disk before proceeding to the next dispatch.
After ALL batches complete, report to the user: Phase 2 complete. All <totalBatches> batches analyzed.
Run the merge-and-normalize script bundled with this skill (located next to this SKILL.md file — use the skill directory path, not the project root):
python <SKILL_DIR>/merge-batch-graphs.py $PROJECT_ROOTThis script reads all batch-*.json files (including batch-<i>-part-<k>.json produced by file-analyzers that split their output) from $PROJECT_ROOT/.understand-anything/intermediate/, then in one pass:
- Combines all nodes and edges across batches
- Normalizes node IDs (strips double prefixes, project-name prefixes, adds missing prefixes)
- Normalizes complexity values (
low→simple,medium→moderate,high→complex, etc.) - Rewrites edge references to match corrected node IDs
- Deduplicates nodes by ID (keeps last occurrence) and edges by
(source, target, type) - Drops dangling edges referencing missing nodes
- Logs all corrections and dropped items to stderr
The merge script also runs a tested_by linker that canonicalizes test-coverage edges in two passes. Pass 1 walks LLM-emitted tested_by edges and flips inverted ones in place; semantically broken edges (test↔test, prod↔prod, orphan endpoints) are dropped. Pass 2 supplements with path-convention pairings. Production nodes that end up sourcing any tested_by edge get a "tested" tag. All resulting edges run production → test.
Output: $PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json
Include the script's warnings in $PHASE_WARNINGS for the reviewer.
Incremental update path
Write the changed-files list (one path per line) to a temp file:
git diff <lastCommitHash>..HEAD --name-only > $PROJECT_ROOT/.understand-anything/tmp/changed-files.txtRun compute-batches with --changed-files:
node <SKILL_DIR>/compute-batches.mjs $PROJECT_ROOT \
--changed-files=$PROJECT_ROOT/.understand-anything/tmp/changed-files.txtThis produces a batches.json that contains only batches with changed files, but neighborMap entries still reference unchanged files (with their full-graph batchIndex) so cross-batch edges remain emittable.
Then dispatch file-analyzer subagents per the same template as the full path.
After batches complete: 1. Remove old nodes whose filePath matches any changed file from the existing graph 2. Remove old edges whose source or target references a removed node 3. Write the pruned existing nodes/edges as batch-existing.json in the intermediate directory 4. Run the same merge script — it will combine batch-existing.json with the fresh batch-*.json files:
python <SKILL_DIR>/merge-batch-graphs.py $PROJECT_ROOT---
Phase 3 — ASSEMBLE REVIEW
Report to the user: [Phase 3/7] Reviewing assembled graph...
Dispatch a subagent using the assemble-reviewer agent definition (at agents/assemble-reviewer.md).
Pass these parameters in the dispatch prompt:
Review the assembled graph at $PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json.Project root: $PROJECT_ROOTBatch files are at: $PROJECT_ROOT/.understand-anything/intermediate/batch-*.jsonWrite review output to: $PROJECT_ROOT/.understand-anything/intermediate/assemble-review.json>
Merge script report:
```
<paste the full stderr output from merge-batch-graphs.py>
```
>
Import map for cross-batch edge verification:
```json
$IMPORT_MAP
```
After the subagent completes, read $PROJECT_ROOT/.understand-anything/intermediate/assemble-review.json and add any notes to $PHASE_WARNINGS.
---
Phase 4 — ARCHITECTURE
Report to the user: [Phase 4/7] Identifying architectural layers...
Build the combined prompt template: 1. Use the architecture-analyzer agent definition (at agents/architecture-analyzer.md). 2. Language context injection: For each language detected in Phase 1 (e.g., python, markdown, dockerfile, yaml, sql, terraform, graphql, protobuf, shell, html, css), read the file at ./languages/<language-id>.md (e.g., ./languages/python.md, ./languages/dockerfile.md) and append its content after the base template under a ## Language Context header. If the file does not exist for a detected language, skip it silently and continue. These files are in the languages/ subdirectory next to this SKILL.md file. Include non-code language snippets — they provide edge patterns and summary styles for non-code files. 3. Framework addendum injection: For each framework detected in Phase 1 (e.g., Django), read the file at ./frameworks/<framework-id-lowercase>.md (e.g., ./frameworks/django.md) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the frameworks/ subdirectory next to this SKILL.md file. 4. Output locale injection: If $OUTPUT_LANGUAGE is NOT en (English), read the locale guidance file at ./locales/<language-code>.md (e.g., ./locales/zh.md, ./locales/ja.md, ./locales/ko.md) and append its content after the framework addendums under a ## Output Language Guidelines header. This provides language-specific guidance for tag naming conventions, summary style, and layer name translations. If the locale file does not exist for the specified language, skip silently — the $LANGUAGE_DIRECTIVE still applies. These files are in the locales/ subdirectory next to this SKILL.md file.
Append the language/framework context and the following additional context to the agent's prompt:
Additional context from main session:
>
Frameworks detected: <frameworks from Phase 1>>
Directory tree (top 2 levels):
```
$DIR_TREE
```
>
Use the directory tree, language context, and framework addendums (appended above) to inform layer assignments. Directory structure is strong evidence for layer boundaries. Non-code files (config, docs, infrastructure, data) should be assigned to appropriate layers — see the prompt template for guidance.
>
$LANGUAGE_DIRECTIVE
Pass these parameters in the dispatch prompt:
Analyze this codebase's structure to identify architectural layers.
Project root: $PROJECT_ROOTWrite output to: $PROJECT_ROOT/.understand-anything/intermediate/layers.jsonProject:<projectName>—<projectDescription>
>
File nodes (all node types — includes code files, config, document, service, pipeline, table, schema, resource, endpoint):
```json
[list of {id, type, name, filePath, summary, tags} for ALL file-level nodes — omit complexity, languageNotes]
```
>
Import edges:
```json
[list of edges with type "imports"]
```
>
All edges (for cross-category analysis — includes configures, documents, deploys, triggers, etc.):
```json
[list of ALL edges — include all edge types]
```
After the subagent completes, read $PROJECT_ROOT/.understand-anything/intermediate/layers.json and normalize it into a final layers array. Apply these steps in order:
1. Unwrap envelope: If the file contains { "layers": [...] } instead of a plain array, extract the inner array. (The prompt requests a plain array, but LLMs may still produce an envelope.) 2. Rename legacy fields: If any layer object has a nodes field instead of nodeIds, rename nodes → nodeIds. If nodes entries are objects with an id field rather than plain strings, extract just the id values into nodeIds. 3. Synthesize missing IDs: If any layer is missing an id, generate one as layer:<kebab-case-name>. 4. Convert file paths: If nodeIds entries are raw file paths without a known prefix (file:, config:, document:, service:, pipeline:, table:, schema:, resource:, endpoint:), convert them to file:<relative-path>. 5. Drop dangling refs: Remove any nodeIds entries that do not exist in the merged node set.
Each element of the final layers array MUST have this shape:
[
{
"id": "layer:<kebab-case-name>",
"name": "<layer name>",
"description": "<what belongs in this layer>",
"nodeIds": ["file:src/App.tsx", "config:tsconfig.json", "document:README.md"]
}
]All four fields (id, name, description, nodeIds) are required.
For incremental updates: Always re-run architecture analysis on the full merged node set, since layer assignments may shift when files change.
Context for incremental updates: When re-running architecture analysis, also inject the previous layer definitions:
Previous layer definitions (for naming consistency):
```json
[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.
---
Phase 5 — TOUR
Report to the user: [Phase 5/7] Building guided tour...
Dispatch a subagent using the tour-builder agent definition (at agents/tour-builder.md). Append the following additional context:
Additional context from main session:
>
Project README (first 3000 chars):
```
$README_CONTENT
```
>
Project entry point: $ENTRY_POINT>
Use the README to align the tour narrative with the project's own documentation. Start the tour from the entry point if one was detected. The tour should tell the same story the README tells, but through the lens of actual code structure.
>
$LANGUAGE_DIRECTIVE
Pass these parameters in the dispatch prompt:
Create a guided learning tour for this codebase.
Project root: $PROJECT_ROOTWrite output to: $PROJECT_ROOT/.understand-anything/intermediate/tour.jsonProject:<projectName>—<projectDescription>
Languages: <languages>>
Nodes (all file-level nodes — includes code files, config, document, service, pipeline, table, schema, resource, endpoint):
```json
[list of {id, name, filePath, summary, type} for ALL file-level nodes — do NOT include function or class nodes]
```
>
Layers:
```json
[list of {id, name, description} for each layer — omit nodeIds]
```
>
Edges (all types — includes imports, calls, configures, documents, deploys, triggers, etc.):
```json
[list of ALL edges — include all edge types for complete graph topology analysis]
```
After the subagent completes, read $PROJECT_ROOT/.understand-anything/intermediate/tour.json and normalize it into a final tour array. Apply these steps in order:
1. Unwrap envelope: If the file contains { "steps": [...] } instead of a plain array, extract the inner array. (The prompt requests a plain array, but LLMs may still produce an envelope.) 2. Rename legacy fields: If any step has nodesToInspect instead of nodeIds, rename it → nodeIds. If any step has whyItMatters instead of description, rename it → description. 3. Convert file paths: If nodeIds entries are raw file paths without a known prefix (file:, config:, document:, service:, pipeline:, table:, schema:, resource:, endpoint:), convert them to file:<relative-path>. 4. Drop dangling refs: Remove any nodeIds entries that do not exist in the merged node set. 5. Sort by order before saving.
Each element of the final tour array MUST have this shape:
[
{
"order": 1,
"title": "Project Overview",
"description": "Start with the README to understand the project's purpose and architecture.",
"nodeIds": ["document:README.md"]
},
{
"order": 2,
"title": "Application Entry Point",
"description": "This step explains how the frontend boots and mounts.",
"nodeIds": ["file:src/main.tsx", "file:src/App.tsx"]
}
]Required fields: order, title, description, nodeIds. Preserve optional languageLesson when present.
---
Phase 6 — REVIEW
Report to the user: [Phase 6/7] Validating knowledge graph...
Assemble the full KnowledgeGraph JSON object:
{
"version": "1.0.0",
"project": {
"name": "<projectName>",
"languages": ["<languages>"],
"frameworks": ["<frameworks>"],
"description": "<projectDescription>",
"analyzedAt": "<ISO 8601 timestamp>",
"gitCommitHash": "<commit hash from Phase 0>"
},
"nodes": [<all nodes from assembled-graph.json after Phase 3 review>],
"edges": [<all edges from assembled-graph.json after Phase 3 review>],
"layers": [<layers from Phase 4>],
"tour": [<steps from Phase 5>]
}1. Before writing the assembled graph, validate that:
layersis an array of objects with these required fields:id,name,description,nodeIdstouris an array of objects with these required fields:order,title,description,nodeIdstour[*].languageLessonis allowed as an optional string field- Every
layers[*].nodeIdsentry exists in the merged node set - Every
tour[*].nodeIdsentry exists in the merged node set
If validation fails, automatically normalize and rewrite the graph into this shape before saving. If the graph still fails final validation after the normalization pass, save it with warnings but mark dashboard auto-launch as skipped.
2. Write the assembled graph to $PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json.
3. Check `$ARGUMENTS` for `--review` flag. Then run the appropriate validation path:
---
Default path (no --review): inline deterministic validation
Write the following Node.js script to $PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.cjs:
#!/usr/bin/env node
const fs = require('fs');
const graphPath = process.argv[2];
const outputPath = process.argv[3];
try {
const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8'));
const issues = [], warnings = [];
if (!Array.isArray(graph.nodes)) { issues.push('graph.nodes is missing or not an array'); graph.nodes = []; }
if (!Array.isArray(graph.edges)) { issues.push('graph.edges is missing or not an array'); graph.edges = []; }
const nodeIds = new Set();
const seen = new Map();
graph.nodes.forEach((n, i) => {
if (!n.id) { issues.push(`Node[${i}] missing id`); return; }
if (!n.type) issues.push(`Node[${i}] '${n.id}' missing type`);
if (!n.name) issues.push(`Node[${i}] '${n.id}' missing name`);
if (!n.summary) issues.push(`Node[${i}] '${n.id}' missing summary`);
if (!n.tags || !n.tags.length) issues.push(`Node[${i}] '${n.id}' missing tags`);
if (seen.has(n.id)) issues.push(`Duplicate node ID '${n.id}' at indices ${seen.get(n.id)} and ${i}`);
else seen.set(n.id, i);
nodeIds.add(n.id);
});
graph.edges.forEach((e, i) => {
if (!nodeIds.has(e.source)) issues.push(`Edge[${i}] source '${e.source}' not found`);
if (!nodeIds.has(e.target)) issues.push(`Edge[${i}] target '${e.target}' not found`);
});
const fileLevelTypes = new Set(['file', 'config', 'document', 'service', 'pipeline', 'table', 'schema', 'resource', 'endpoint']);
const fileNodes = graph.nodes.filter(n => fileLevelTypes.has(n.type)).map(n => n.id);
const assigned = new Map();
if (!Array.isArray(graph.layers)) { if (graph.layers) warnings.push('graph.layers is not an array'); graph.layers = []; }
if (!Array.isArray(graph.tour)) { if (graph.tour) warnings.push('graph.tour is not an array'); graph.tour = []; }
graph.layers.forEach(layer => {
(layer.nodeIds || []).forEach(id => {
if (!nodeIds.has(id)) issues.push(`Layer '${layer.id}' refs missing node '${id}'`);
if (assigned.has(id)) issues.push(`Node '${id}' appears in multiple layers`);
assigned.set(id, layer.id);
});
});
fileNodes.forEach(id => {
if (!assigned.has(id)) issues.push(`File node '${id}' not in any layer`);
});
graph.tour.forEach((step, i) => {
(step.nodeIds || []).forEach(id => {
if (!nodeIds.has(id)) issues.push(`Tour step[${i}] refs missing node '${id}'`);
});
});
const withEdges = new Set([
...graph.edges.map(e => e.source),
...graph.edges.map(e => e.target)
]);
graph.nodes.forEach(n => {
if (!withEdges.has(n.id)) warnings.push(`Node '${n.id}' has no edges (orphan)`);
});
const stats = {
totalNodes: graph.nodes.length,
totalEdges: graph.edges.length,
totalLayers: graph.layers.length,
tourSteps: graph.tour.length,
nodeTypes: graph.nodes.reduce((a, n) => { a[n.type] = (a[n.type]||0)+1; return a; }, {}),
edgeTypes: graph.edges.reduce((a, e) => { a[e.type] = (a[e.type]||0)+1; return a; }, {})
};
fs.writeFileSync(outputPath, JSON.stringify({ issues, warnings, stats }, null, 2));
process.exit(0);
} catch (err) { process.stderr.write(err.message + '\n'); process.exit(1); }Execute it:
node $PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.cjs \
"$PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json" \
"$PROJECT_ROOT/.understand-anything/intermediate/review.json"If the script exits non-zero, read stderr, fix the script, and retry once.
---
--review path: full LLM reviewer
If --review IS in $ARGUMENTS, dispatch the LLM graph-reviewer subagent as follows:
Dispatch a subagent using the graph-reviewer agent definition (at agents/graph-reviewer.md). Append the following additional context:
Additional context from main session:
>
Phase 1 scan results (file inventory):
```json
[list of {path, sizeLines} from scan-result.json]
```
>
Phase warnings/errors accumulated during analysis:
- [list any batch failures, skipped files, or warnings from Phases 2-5]
>
Cross-validate: every file in the scan inventory should have a corresponding node in the graph (node types may vary:file:,config:,document:,service:,pipeline:,table:,schema:,resource:,endpoint:). Flag any missing files. Also flag any graph nodes whosefilePathdoesn't appear in the scan inventory.
Pass these parameters in the dispatch prompt:
Validate the knowledge graph at $PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json.Project root: $PROJECT_ROOTRead the file and validate it for completeness and correctness.
Write output to: $PROJECT_ROOT/.understand-anything/intermediate/review.json---
4. Read $PROJECT_ROOT/.understand-anything/intermediate/review.json.
5. If `issues` array is non-empty:
- Review the
issueslist - Apply automated fixes where possible:
- Remove edges with dangling references
- Fill missing required fields with sensible defaults (e.g., empty
tags->["untagged"], emptysummary->"No summary available") - Remove nodes with invalid types
- Re-run the final graph validation after automated fixes
- If critical issues remain after one fix attempt, save the graph anyway but include the warnings in the final report and mark dashboard auto-launch as skipped
6. If `issues` array is empty: Proceed to Phase 7.
---
Phase 7 — SAVE
Report to the user: [Phase 7/7] Saving knowledge graph...
1. Write the final knowledge graph to $PROJECT_ROOT/.understand-anything/knowledge-graph.json.
2. Generate structural fingerprints baseline. This creates the basis for future automatic incremental updates and must succeed before `meta.json` is written — otherwise auto-update sees a fresh commit hash with no fingerprints to compare against, classifies every file as STRUCTURAL, and escalates to FULL_UPDATE on every subsequent commit (issue #152).
Write the input file:
cat > $PROJECT_ROOT/.understand-anything/intermediate/fingerprint-input.json <<EOF
{
"projectRoot": "$PROJECT_ROOT",
"sourceFilePaths": [<all source file paths from Phase 1, as JSON array>],
"gitCommitHash": "<current commit hash>"
}
EOFThen invoke the bundled script (located next to this SKILL.md):
node <SKILL_DIR>/build-fingerprints.mjs \
$PROJECT_ROOT/.understand-anything/intermediate/fingerprint-input.jsonThe script uses TreeSitterPlugin + PluginRegistry exactly like extract-structure.mjs, so the baseline matches the comparison logic used during auto-updates.
If the script exits non-zero or stdout does not include `Fingerprints baseline:`, abort Phase 7 and report the error. Do NOT proceed to step 3 (writing `meta.json`).
3. Write metadata to $PROJECT_ROOT/.understand-anything/meta.json (only after step 2 succeeded):
{
"lastAnalyzedAt": "<ISO 8601 timestamp>",
"gitCommitHash": "<commit hash>",
"version": "1.0.0",
"analyzedFiles": <number of files analyzed>
}4. Clean up intermediate files, preserving `scan-result.json` so future incremental runs can skip Phase 1 SCAN (see issue #293). We mv scratch dirs into a timestamped .trash-* instead of rm -rfing them directly — this avoids tripping destructive-action gates on hardened hosts (e.g. freshness-window checks) that flag deleting directories created moments earlier (see issue #301). The delayed-purge step in Phase 0 reclaims the space once the trash is older than 7 days.
# Preserve scan-result.json — Phase 1's deterministic file inventory.
# Future incremental runs (Phase 2 compute-batches.mjs --changed-files=…)
# need this inventory; without it, Phase 1 must re-dispatch and pay ~157k
# tokens / ~158s per incremental run.
TRASH="$PROJECT_ROOT/.understand-anything/.trash-$(date +%s)"
mkdir -p "$TRASH"
INTER="$PROJECT_ROOT/.understand-anything/intermediate"
if [ -d "$INTER" ]; then
# Move every entry except scan-result.json into the trash dir.
find "$INTER" -mindepth 1 -maxdepth 1 -not -name 'scan-result.json' -exec mv {} "$TRASH/" \; 2>/dev/null || true
fi
mv "$PROJECT_ROOT/.understand-anything/tmp" "$TRASH/" 2>/dev/null || true5. Report a summary to the user containing:
- Project name and description
- Files analyzed / total files (with breakdown by fileCategory: code, config, docs, infra, data, script, markup)
- Nodes created (broken down by type: file, function, class, config, document, service, table, endpoint, pipeline, schema, resource)
- Edges created (broken down by type)
- Layers identified (with names)
- Tour steps generated (count)
- Any warnings from the reviewer
- Path to the output file:
$PROJECT_ROOT/.understand-anything/knowledge-graph.json
6. Only automatically launch the dashboard by invoking the /understand-dashboard skill if final graph validation passed after normalization/review fixes. If final validation did not pass, report that the graph was saved with warnings and dashboard launch was skipped.
---
Error Handling
- If any subagent dispatch fails, retry once with the same prompt plus additional context about the failure.
- Track all warnings and errors from each phase in a
$PHASE_WARNINGSlist. When using--review, pass this list to the graph-reviewer in Phase 6. On the default path, include accumulated warnings in the Phase 7 final report. - If it fails a second time, skip that phase and continue with partial results.
- ALWAYS save partial results — a partial graph is better than no graph.
- Report any skipped phases or errors in the final summary so the user knows what happened.
- NEVER silently drop errors. Every failure must be visible in the final report.
---
Reference: KnowledgeGraph Schema
Node Types (13 total)
| Type | Description | ID Convention |
|---|---|---|
file | Source code file | file:<relative-path> |
function | Function or method | function:<relative-path>:<name> |
class | Class, interface, or type | class:<relative-path>:<name> |
module | Logical module or package | module:<name> |
concept | Abstract concept or pattern | concept:<name> |
config | Configuration file (YAML, JSON, TOML, env) | config:<relative-path> |
document | Documentation file (Markdown, RST, TXT) | document:<relative-path> |
service | Deployable service definition (Dockerfile, K8s) | service:<relative-path> |
table | Database table or migration | table:<relative-path>:<table-name> |
endpoint | API endpoint or route definition | endpoint:<relative-path>:<endpoint-name> |
pipeline | CI/CD pipeline configuration | pipeline:<relative-path> |
schema | Schema definition (GraphQL, Protobuf, Prisma) | schema:<relative-path> |
resource | Infrastructure resource (Terraform, CloudFormation) | resource:<relative-path> |
Edge Types (26 total)
| Category | Types |
|---|---|
| Structural | imports, exports, contains, inherits, implements |
| Behavioral | calls, subscribes, publishes, middleware |
| Data flow | reads_from, writes_to, transforms, validates |
| Dependencies | depends_on, tested_by, configures |
| Semantic | related, similar_to |
| Infrastructure | deploys, serves, provisions, triggers |
| Schema/Data | migrates, documents, routes, defines_schema |
Edge Weight Conventions
| Edge Type | Weight |
|---|---|
contains | 1.0 |
inherits, implements | 0.9 |
calls, exports, defines_schema | 0.8 |
imports, deploys, migrates | 0.7 |
depends_on, configures, triggers | 0.6 |
tested_by, documents, provisions, serves, routes | 0.5 |
| All others | 0.5 (default) |
#!/usr/bin/env node
/**
* build-fingerprints.mjs
*
* Builds the structural-fingerprint baseline used by auto-update's
* incremental change detection. Runs once per /understand full rebuild
* (Phase 7 step 2.5), generating .understand-anything/fingerprints.json.
*
* Replaces the LLM-written fingerprint script that previously sat in
* SKILL.md as a code example — that example had the wrong signature
* for buildFingerprintStore() and never successfully produced a baseline,
* which silently broke auto-update for every install (see issue #152).
*
* Usage:
* node build-fingerprints.mjs <input.json>
*
* Input JSON:
* { projectRoot: string, sourceFilePaths: string[], gitCommitHash: string }
*
* Writes: <projectRoot>/.understand-anything/fingerprints.json
* Exit code: 0 on success (including 0 files analyzed); non-zero on error.
*/
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { readFileSync } from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
// skills/understand/ -> plugin root is two dirs up
const pluginRoot = resolve(__dirname, '../..');
const require = createRequire(resolve(pluginRoot, 'package.json'));
// ---------------------------------------------------------------------------
// Resolve @understand-anything/core (matches extract-structure.mjs).
// pathToFileURL() is required for Windows: dynamic import() of a raw
// "C:\..." path throws ERR_UNSUPPORTED_ESM_URL_SCHEME.
// ---------------------------------------------------------------------------
let core;
try {
core = await import(pathToFileURL(require.resolve('@understand-anything/core')).href);
} catch {
core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href);
}
const {
TreeSitterPlugin,
PluginRegistry,
builtinLanguageConfigs,
registerAllParsers,
buildFingerprintStore,
saveFingerprints,
} = core;
async function main() {
const [, , inputPath] = process.argv;
if (!inputPath) {
process.stderr.write('Usage: node build-fingerprints.mjs <input.json>\n');
process.exit(1);
}
const { projectRoot, sourceFilePaths, gitCommitHash } = JSON.parse(
readFileSync(inputPath, 'utf-8'),
);
if (!projectRoot || !Array.isArray(sourceFilePaths) || typeof gitCommitHash !== 'string') {
throw new Error(
'Invalid input: requires { projectRoot: string, sourceFilePaths: string[], gitCommitHash: string }',
);
}
// Create tree-sitter plugin with all configs that have WASM grammars,
// mirroring extract-structure.mjs so the baseline matches the comparison
// logic used during auto-updates.
const tsConfigs = builtinLanguageConfigs.filter((c) => c.treeSitter);
const tsPlugin = new TreeSitterPlugin(tsConfigs);
await tsPlugin.init();
const registry = new PluginRegistry();
registry.register(tsPlugin);
registerAllParsers(registry);
const store = buildFingerprintStore(projectRoot, sourceFilePaths, registry, gitCommitHash);
saveFingerprints(projectRoot, store);
const fileCount = Object.keys(store.files).length;
process.stdout.write(`Fingerprints baseline: ${fileCount} files\n`);
}
await main();
#!/usr/bin/env node
/**
* compute-batches.mjs — Phase 1.5 of /understand
*
* Reads scan-result.json, runs Louvain community detection on the import
* graph, and writes batches.json containing batches + neighborMap.
*
* Usage:
* node compute-batches.mjs <project-root> [--changed-files=<path>]
*
* Input: <project-root>/.understand-anything/intermediate/scan-result.json
* Output: <project-root>/.understand-anything/intermediate/batches.json
*/
import { readFileSync, writeFileSync, existsSync, realpathSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
/**
* Chunk size for parallel file I/O. Bounded so a 15k-file repo doesn't try
* to open every descriptor at once (would hit `EMFILE`) while still keeping
* libuv's worker-thread pool saturated. Empirically chosen to keep memory
* around tens of MB even when the average file is ~10 KB.
*/
const IO_PARALLELISM = 64;
const __filename = fileURLToPath(import.meta.url);
const PLUGIN_ROOT = resolve(dirname(__filename), '../..');
const require = createRequire(resolve(PLUGIN_ROOT, 'package.json'));
let core;
try {
core = await import(pathToFileURL(require.resolve('@understand-anything/core')).href);
} catch {
core = await import(pathToFileURL(resolve(PLUGIN_ROOT, 'packages/core/dist/index.js')).href);
}
const { TreeSitterPlugin, PluginRegistry, builtinLanguageConfigs, registerAllParsers } = core;
import Graph from 'graphology';
import louvain from 'graphology-communities-louvain';
/**
* For each code file, returns its top-level exported symbol names (functions,
* classes, exported consts). Per-file errors are swallowed into [] with a
* visible warning so a single bad file does not abort batching.
*
* Returns Map<path, string[]>.
*/
async function extractExports(projectRoot, codeFiles) {
let registry;
try {
const tsConfigs = builtinLanguageConfigs.filter(c => c.treeSitter);
const tsPlugin = new TreeSitterPlugin(tsConfigs);
await tsPlugin.init();
registry = new PluginRegistry();
registry.register(tsPlugin);
registerAllParsers(registry);
} catch (err) {
process.stderr.write(
`Warning: compute-batches: tree-sitter init failed (${err.message}) ` +
`— all symbols=[] in neighborMap — cross-batch edges limited to file-level\n`,
);
return new Map(codeFiles.map(f => [f.path, []]));
}
const exportsByPath = new Map();
// I/O is parallelised in bounded chunks (libuv worker threads handle the
// disk reads concurrently) while the actual tree-sitter parse stays on
// the main thread, since web-tree-sitter is single-threaded WASM. For a
// 15k-file iOS repo (#226), the sequential `readFileSync` loop dominated;
// letting reads pipeline drops wall time roughly proportional to the
// share of the loop spent waiting on disk.
for (let start = 0; start < codeFiles.length; start += IO_PARALLELISM) {
const slice = codeFiles.slice(start, start + IO_PARALLELISM);
// Read every file in the slice concurrently. Errors per file are
// captured in-place so a single bad file does not abort the chunk.
const reads = await Promise.all(
slice.map(async (file) => {
const abs = join(projectRoot, file.path);
try {
const content = await readFile(abs, 'utf-8');
return { file, content, readError: null };
} catch (err) {
return { file, content: null, readError: err };
}
}),
);
// Serialise the CPU-bound tree-sitter work and the stderr warning emits
// so log order remains identical to the previous sequential loop. This
// also keeps existing fixture-comparison tests stable.
for (const { file, content, readError } of reads) {
if (readError) {
process.stderr.write(
`Warning: compute-batches: exports extraction failed for ${file.path} ` +
`(read error: ${readError.message}) — symbols=[] in neighborMap — ` +
`cross-batch edges to this file limited to file-level\n`,
);
exportsByPath.set(file.path, []);
continue;
}
try {
const analysis = registry.analyzeFile(file.path, content);
const names = (analysis?.exports || []).map(e => e.name).filter(Boolean);
exportsByPath.set(file.path, names);
} catch (err) {
process.stderr.write(
`Warning: compute-batches: exports extraction failed for ${file.path} ` +
`(analyze error: ${err.message}) — symbols=[] in neighborMap — ` +
`cross-batch edges to this file limited to file-level\n`,
);
exportsByPath.set(file.path, []);
}
}
}
return exportsByPath;
}
/**
* Build batches for non-code files per Groups A-E in the design spec.
* Returns Array<{ files: FileMeta[], mergeable: boolean }> — caller assigns
* batchIndex. `mergeable=false` for semantic Groups A-D (Dockerfile clusters,
* .github/workflows, .gitlab-ci/.circleci, SQL migrations) preserves their
* boundary intent across the merge-small pass; Group E (catch-all parent-dir
* grouping) is `mergeable=true` so its tiny singletons can be pooled.
*/
function buildNonCodeBatches(nonCodeFiles) {
const byPath = new Map(nonCodeFiles.map(f => [f.path, f]));
const consumed = new Set();
const groups = [];
const dirOf = p => p.includes('/') ? p.slice(0, p.lastIndexOf('/')) : '';
const baseOf = p => p.includes('/') ? p.slice(p.lastIndexOf('/') + 1) : p;
// Group A: per-directory Dockerfile clusters.
const dirsWithDockerfile = new Set(
[...byPath.keys()]
.filter(p => baseOf(p) === 'Dockerfile')
.map(dirOf),
);
for (const dir of [...dirsWithDockerfile].sort()) {
const inDir = [...byPath.keys()].filter(p => dirOf(p) === dir);
const cluster = inDir.filter(p => {
const b = baseOf(p);
return b === 'Dockerfile'
|| b === '.dockerignore'
|| b.startsWith('docker-compose.');
});
if (cluster.length) {
groups.push({ files: cluster.map(p => byPath.get(p)), mergeable: false });
cluster.forEach(p => consumed.add(p));
}
}
// Group B: .github/workflows/*
const ghWorkflows = [...byPath.keys()].filter(
p => p.startsWith('.github/workflows/') && (p.endsWith('.yml') || p.endsWith('.yaml')),
).filter(p => !consumed.has(p));
if (ghWorkflows.length) {
groups.push({ files: ghWorkflows.map(p => byPath.get(p)), mergeable: false });
ghWorkflows.forEach(p => consumed.add(p));
}
// Group C: .gitlab-ci.yml + .circleci/*
const ciFiles = [...byPath.keys()].filter(
p => (p === '.gitlab-ci.yml' || p.startsWith('.circleci/'))
&& !consumed.has(p),
);
if (ciFiles.length) {
groups.push({ files: ciFiles.map(p => byPath.get(p)), mergeable: false });
ciFiles.forEach(p => consumed.add(p));
}
// Group D: SQL migrations per migrations/ or migration/ directory.
// Defensive consumed.has check: no upstream group consumes SQL today, but
// future Group additions could; keep the check for forward-compat.
const migrationDirs = new Set(
[...byPath.keys()]
.filter(p => p.endsWith('.sql'))
.map(dirOf)
.filter(d => /(^|\/)migrations?$/.test(d)),
);
for (const dir of migrationDirs) {
const sqls = [...byPath.keys()]
.filter(p => dirOf(p) === dir && p.endsWith('.sql') && !consumed.has(p))
.sort();
if (sqls.length) {
groups.push({ files: sqls.map(p => byPath.get(p)), mergeable: false });
sqls.forEach(p => consumed.add(p));
}
}
// Group E: all remaining grouped by immediate parent dir, max 20 per batch
const remainingByDir = new Map();
for (const p of [...byPath.keys()].sort()) {
if (consumed.has(p)) continue;
const dir = dirOf(p);
if (!remainingByDir.has(dir)) remainingByDir.set(dir, []);
remainingByDir.get(dir).push(p);
}
// Per design spec: max files per parent-dir batch for Group E.
const MAX_E = 20;
for (const [, paths] of remainingByDir) {
for (let i = 0; i < paths.length; i += MAX_E) {
const slice = paths.slice(i, i + MAX_E);
groups.push({ files: slice.map(p => byPath.get(p)), mergeable: true });
}
}
return groups;
}
/**
* Build a lookup map from file path → batchIndex across all batches (code +
* non-code). Used to resolve cross-batch neighbor references in neighborMap.
*/
function buildBatchOfMap(allBatches) {
const m = new Map();
for (const b of allBatches) {
for (const f of b.files) m.set(f.path, b.batchIndex);
}
return m;
}
/**
* Returns Map<path, communityId> via Louvain. May throw — caller must catch
* and fall back if it does. Honors UA_COMPUTE_BATCHES_FORCE_LOUVAIN_THROW=1
* to allow tests to exercise the fallback path.
*/
function runLouvain(codeFiles, importMap) {
if (process.env.UA_COMPUTE_BATCHES_FORCE_LOUVAIN_THROW === '1') {
throw new Error('forced throw via UA_COMPUTE_BATCHES_FORCE_LOUVAIN_THROW');
}
const g = new Graph({ type: 'undirected', allowSelfLoops: false });
for (const f of codeFiles) g.addNode(f.path);
for (const [src, targets] of Object.entries(importMap)) {
if (!g.hasNode(src)) continue;
for (const tgt of targets) {
if (!g.hasNode(tgt) || src === tgt || g.hasEdge(src, tgt)) continue;
g.addEdge(src, tgt);
}
}
const cs = louvain(g); // { nodeId: communityId }
return new Map(Object.entries(cs));
}
/**
* Returns Map<path, communityId> via alphabetical chunking of `batchSize`
* files per batch. Deterministic, used as fallback when Louvain fails.
*/
function countBasedAssignment(codeFiles, batchSize = 12) {
const out = new Map();
const sorted = [...codeFiles].map(f => f.path).sort();
for (let i = 0; i < sorted.length; i++) {
out.set(sorted[i], `count_${Math.floor(i / batchSize)}`);
}
return out;
}
/**
* Pool small mergeable batches into "misc" batches to reduce dispatch overhead.
* Preserves semantic groupings (non-code Groups A-D, marked `mergeable=false`)
* regardless of size; only merges code Louvain singletons / orphans and
* Group E parent-dir batches that fall below MIN_BATCH_SIZE.
*
* On a 314-file microservices-demo run, vanilla Louvain produced 87 singleton
* communities → 87 dispatch tasks of size 1. This pass collapses them into
* ceil(N / MAX_MERGE_TARGET) misc batches, drastically cutting orchestration
* overhead while leaving the high-modularity communities untouched.
*
* Returns the rewritten batch list with reassigned batchIndex (1-based,
* keepers first preserving their relative order, misc batches appended).
*/
function mergeSmallBatches(bareBatches) {
// MIN_BATCH_SIZE=3: below this, file-analyzer dispatch overhead (subagent
// spin-up, prompt setup) dwarfs the per-file analysis cost — not worth a
// standalone batch.
const MIN_BATCH_SIZE = 3;
// MAX_MERGE_TARGET=25: stays below MAX_COMMUNITY_SIZE=35 so the misc-batch
// agent retains headroom for neighborMap context without overflowing.
const MAX_MERGE_TARGET = 25;
const keepers = [];
const smallMergeable = [];
for (const b of bareBatches) {
if (b.mergeable && b.files.length < MIN_BATCH_SIZE) {
smallMergeable.push(b);
} else {
keepers.push(b);
}
}
if (smallMergeable.length === 0) {
// Nothing to merge — strip mergeable flag and renumber for cleanliness.
return keepers.map((b, i) => ({
batchIndex: i + 1,
files: b.files,
}));
}
// Pool and sort deterministically by path so repeated runs match byte-for-byte.
const pooledFiles = smallMergeable
.flatMap(b => b.files)
.sort((a, b) => a.path.localeCompare(b.path));
const miscBatches = [];
for (let i = 0; i < pooledFiles.length; i += MAX_MERGE_TARGET) {
miscBatches.push({ files: pooledFiles.slice(i, i + MAX_MERGE_TARGET) });
}
// Use `Info:` rather than `Warning:` — singleton consolidation is a
// routine optimization, not a fallback/degrade path. Per
// [[feedback_visible_warnings]] only fallbacks should bubble as Warning:
// to the Phase 7 final report. Real warnings would get drowned out if
// every normal Louvain run with singletons (i.e. almost every run) added
// a Warning: line.
process.stderr.write(
`Info: compute-batches: merged ${smallMergeable.length} small batches ` +
`(${pooledFiles.length} files) into ${miscBatches.length} misc batches ` +
`— singletons and orphans consolidated\n`,
);
const final = [...keepers, ...miscBatches];
return final.map((b, i) => ({
batchIndex: i + 1,
files: b.files,
}));
}
// ── Main: load → Louvain (or count-fallback) → enrich → write batches.json ─
async function main() {
const projectRoot = process.argv[2];
if (!projectRoot) {
process.stderr.write('Usage: node compute-batches.mjs <project-root> [--changed-files=<path>]\n');
process.exit(1);
}
let changedFiles = null;
for (const arg of process.argv.slice(3)) {
const m = arg.match(/^--changed-files=(.+)$/);
if (m) {
const p = m[1];
let content;
try {
content = readFileSync(p, 'utf-8');
} catch (err) {
process.stderr.write(
`Error: compute-batches: --changed-files path not readable: ${p} (${err.message})\n`,
);
process.exit(1);
}
const lines = content
.split('\n')
.map(s => s.trim())
.filter(Boolean);
changedFiles = new Set(lines);
}
}
const scanPath = join(projectRoot, '.understand-anything', 'intermediate', 'scan-result.json');
if (!existsSync(scanPath)) {
process.stderr.write(`Error: scan-result.json not found at ${scanPath}\n`);
process.exit(1);
}
const scan = JSON.parse(readFileSync(scanPath, 'utf-8'));
const files = scan.files || [];
const codeFiles = files.filter(f => f.fileCategory === 'code');
const nonCodeFiles = files.filter(f => f.fileCategory !== 'code');
const importMap = scan.importMap || {};
process.stderr.write(`Loaded ${files.length} files (${codeFiles.length} code).\n`);
const exportsByPath = await extractExports(projectRoot, codeFiles);
let algorithm = 'louvain';
let perFileCommunity;
try {
perFileCommunity = runLouvain(codeFiles, importMap);
} catch (err) {
process.stderr.write(
`Warning: compute-batches: Louvain failed (${err.message}) ` +
`— falling back to count-based grouping (12 files/batch) ` +
`— module semantic boundaries lost\n`,
);
perFileCommunity = countBasedAssignment(codeFiles, 12);
algorithm = 'count-fallback';
}
// Group files by community id
const filesByCommunity = new Map();
for (const [path, cid] of perFileCommunity) {
if (!filesByCommunity.has(cid)) filesByCommunity.set(cid, []);
filesByCommunity.get(cid).push(path);
}
// Size enforcement only on louvain output. count-fallback already chunked.
const MAX_COMMUNITY_SIZE = 35;
const splitCommunities = new Map();
let nextSyntheticId = 0;
if (algorithm === 'louvain') {
for (const [cid, paths] of filesByCommunity) {
if (paths.length <= MAX_COMMUNITY_SIZE) {
splitCommunities.set(cid, paths);
continue;
}
process.stderr.write(
`Warning: compute-batches: community size ${paths.length} > max ${MAX_COMMUNITY_SIZE} ` +
`— splitting via alphabetical chunking — modularity may decrease\n`,
);
const sorted = [...paths].sort();
const parts = Math.ceil(paths.length / MAX_COMMUNITY_SIZE);
const perPart = Math.ceil(paths.length / parts);
for (let i = 0; i < parts; i++) {
const slice = sorted.slice(i * perPart, (i + 1) * perPart);
const synthId = `__split_${cid}_${nextSyntheticId++}`;
splitCommunities.set(synthId, slice);
}
}
} else {
for (const [cid, paths] of filesByCommunity) splitCommunities.set(cid, paths);
}
// Sort communities by size desc, then by min-path asc for determinism
const sortedCommunities = [...splitCommunities.entries()]
.sort((a, b) => {
if (b[1].length !== a[1].length) return b[1].length - a[1].length;
const minA = [...a[1]].sort()[0];
const minB = [...b[1]].sort()[0];
return minA.localeCompare(minB);
});
// Build per-batch file list with full file metadata from scan
const fileMetaByPath = new Map(files.map(f => [f.path, f]));
// Safe: every path in a community is a graph node, and graph nodes are a
// subset of files (see addNode loop above). fileMetaByPath.get() can
// never return undefined here.
// First-pass: assemble bare batches (no batchImportData/neighborMap yet).
// All Louvain communities are mergeable=true so the merge-small pass can
// collapse singletons / 2-file orphans. Non-code groups carry per-group
// mergeable flags from buildNonCodeBatches (false for semantic Groups A-D,
// true for Group E catch-all).
const codeBatchObjsBare = sortedCommunities.map(([, paths], idx) => ({
batchIndex: idx + 1,
files: paths.sort().map(p => fileMetaByPath.get(p)),
mergeable: true,
}));
const nonCodeGroups = buildNonCodeBatches(nonCodeFiles);
const nonCodeBatchObjsBare = nonCodeGroups.map((g, i) => ({
batchIndex: codeBatchObjsBare.length + i + 1,
files: g.files,
mergeable: g.mergeable,
}));
const bareBatches = [...codeBatchObjsBare, ...nonCodeBatchObjsBare];
const mergedBareBatches = mergeSmallBatches(bareBatches);
const batchOf = buildBatchOfMap(mergedBareBatches);
// Build reverse import map: target → [sources that import target]
const reverseImportMap = new Map();
for (const [src, targets] of Object.entries(importMap)) {
for (const tgt of targets) {
if (!reverseImportMap.has(tgt)) reverseImportMap.set(tgt, []);
reverseImportMap.get(tgt).push(src);
}
}
// Compute neighbor degree (number of import relations) per path, used for
// truncation when neighborMap[file] has > MAX_NEIGHBORS entries.
const NEIGHBOR_DEGREE = new Map();
for (const f of codeFiles) {
const outDeg = (importMap[f.path] || []).length;
const inDeg = (reverseImportMap.get(f.path) || []).length;
NEIGHBOR_DEGREE.set(f.path, outDeg + inDeg);
}
const MAX_NEIGHBORS = 50;
// Second-pass: enrich each batch with batchImportData + neighborMap
const batches = mergedBareBatches.map(b => {
const batchPaths = new Set(b.files.map(f => f.path));
const batchImportData = {};
const neighborMap = {};
for (const f of b.files) {
batchImportData[f.path] = (importMap[f.path] || []).slice();
// 1-hop neighbors: imports out + imported-by in, excluding same batch.
// Note on truncation: we measure "popularity" by total raw 1-hop neighbor
// count (rawCount), not kept.length. A widely-imported hub like a logger
// module may have N>50 inbound imports but, after Louvain + size
// enforcement, only some land in other batches — kept.length can be < 50
// while the file is still a high-degree hub whose missing relationships
// matter for downstream cross-batch edge confidence. Warning on rawCount
// surfaces this; truncation on kept ensures the JSON stays bounded.
const outNeighbors = importMap[f.path] || [];
const inNeighbors = reverseImportMap.get(f.path) || [];
const all = new Set([...outNeighbors, ...inNeighbors]);
const rawCount = all.size;
const filtered = [...all].filter(p => batchOf.has(p) && !batchPaths.has(p));
let kept = filtered.map(p => ({
path: p,
batchIndex: batchOf.get(p),
symbols: exportsByPath.get(p) || [],
}));
if (rawCount > MAX_NEIGHBORS) {
kept.sort((a, b2) => (NEIGHBOR_DEGREE.get(b2.path) || 0)
- (NEIGHBOR_DEGREE.get(a.path) || 0)
|| a.path.localeCompare(b2.path)); // deterministic tiebreak
const beforeSlice = kept.length;
kept = kept.slice(0, MAX_NEIGHBORS);
process.stderr.write(
`Warning: compute-batches: neighborMap for ${f.path} has high 1-hop degree ${rawCount} ` +
`— exceeds soft cap of ${MAX_NEIGHBORS} — keeping top ${kept.length} cross-batch entries ` +
`(${beforeSlice - kept.length} dropped by degree sort)\n`,
);
}
if (kept.length) neighborMap[f.path] = kept;
}
return { batchIndex: b.batchIndex, files: b.files, batchImportData, neighborMap };
});
let finalBatches = batches;
if (changedFiles) {
finalBatches = batches.filter(b => b.files.some(f => changedFiles.has(f.path)));
// batchIndex on filtered batches retains the full-graph assignment
// (the design says neighborMap should still reference unchanged files'
// full-graph batchIndex). No renumbering.
}
// Note: under --changed-files mode, totalFiles is the FULL project file
// count (unchanged from the input scan) while totalBatches reflects only
// the filtered set written to disk. batchIndex values on the kept batches
// preserve the full-graph assignment so neighborMap references resolve.
const output = {
schemaVersion: 1,
algorithm,
totalFiles: scan.files.length,
totalBatches: finalBatches.length,
exportsByPath: Object.fromEntries(exportsByPath),
batches: finalBatches,
};
const outPath = join(projectRoot, '.understand-anything', 'intermediate', 'batches.json');
writeFileSync(outPath, JSON.stringify(output, null, 2), 'utf-8');
const batchSizes = finalBatches.map(b => b.files.length);
const maxSize = batchSizes.length ? Math.max(...batchSizes) : 0;
const minSize = batchSizes.length ? Math.min(...batchSizes) : 0;
process.stderr.write(
`Wrote ${finalBatches.length} batches (sizes: max=${maxSize}, min=${minSize}) to ${outPath}\n`,
);
}
// ---------------------------------------------------------------------------
// Run only when executed directly as a CLI; importing the module (e.g. from
// tests) must not trigger main().
//
// Canonicalize both sides through realpathSync. Node ESM resolves
// import.meta.url through symlinks but pathToFileURL(process.argv[1]) preserves
// them, so a raw equality check silently no-ops when the script is invoked via
// a symlinked plugin install path (the default in Claude Code / Copilot CLI
// caches). See GitHub issue #162.
// ---------------------------------------------------------------------------
function isCliEntry() {
if (!process.argv[1]) return false;
try {
const modulePath = realpathSync(fileURLToPath(import.meta.url));
const argvPath = realpathSync(process.argv[1]);
return modulePath === argvPath;
} catch {
return false;
}
}
if (isCliEntry()) {
try {
await main();
} catch (err) {
process.stderr.write(`compute-batches.mjs failed: ${err.message}\n${err.stack}\n`);
process.exit(1);
}
}
#!/usr/bin/env node
/**
* extract-structure.mjs
*
* Deterministic structural extraction script for the file-analyzer agent.
* Uses PluginRegistry (TreeSitterPlugin + non-code parsers) from @understand-anything/core
* to replace the LLM-generated throwaway regex scripts in Phase 1.
*
* Usage:
* node extract-structure.mjs <input.json> <output.json>
*
* Input JSON:
* { projectRoot, batchFiles: [{path, language, sizeLines, fileCategory}], batchImportData }
*
* Output JSON:
* { scriptCompleted, filesAnalyzed, filesSkipped, results: [...] }
*/
import { createRequire } from 'node:module';
import { dirname, resolve, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
// skills/understand/ -> plugin root is two dirs up
const pluginRoot = resolve(__dirname, '../..');
const require = createRequire(resolve(pluginRoot, 'package.json'));
// ---------------------------------------------------------------------------
// Resolve @understand-anything/core
//
// Node ESM dynamic import() requires a file:// URL on Windows; passing a raw
// absolute path like "C:\..." throws ERR_UNSUPPORTED_ESM_URL_SCHEME because the
// loader parses "C:" as a URL scheme. Wrap both resolutions in pathToFileURL().
// ---------------------------------------------------------------------------
let core;
try {
core = await import(pathToFileURL(require.resolve('@understand-anything/core')).href);
} catch {
// Fallback: direct path for installed plugin cache layouts
core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href);
}
const { TreeSitterPlugin, PluginRegistry, builtinLanguageConfigs, registerAllParsers } = core;
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
const [,, inputPath, outputPath] = process.argv;
if (!inputPath || !outputPath) {
process.stderr.write('Usage: node extract-structure.mjs <input.json> <output.json>\n');
process.exit(1);
}
// Read input
const inputRaw = readFileSync(inputPath, 'utf-8');
const input = JSON.parse(inputRaw);
const { projectRoot, batchFiles, batchImportData } = input;
if (!projectRoot || !Array.isArray(batchFiles)) {
throw new Error('Invalid input: must contain projectRoot and batchFiles array');
}
// Create tree-sitter plugin with all configs that have WASM grammars
const tsConfigs = builtinLanguageConfigs.filter(c => c.treeSitter);
const tsPlugin = new TreeSitterPlugin(tsConfigs);
await tsPlugin.init();
// Create registry and register tree-sitter + all non-code parsers
const registry = new PluginRegistry();
registry.register(tsPlugin);
registerAllParsers(registry);
const results = [];
const filesSkipped = [];
for (const file of batchFiles) {
const absolutePath = join(projectRoot, file.path);
// Read file content
let content;
try {
content = readFileSync(absolutePath, 'utf-8');
} catch {
filesSkipped.push(file.path);
continue;
}
// Line counts. POSIX text files end in a trailing newline, which makes
// `split('\n')` produce one extra empty element. Match `wc -l` semantics
// (used by the project scanner for `sizeLines`) so the two counts agree.
const lines = content.split('\n');
const totalLines = content.endsWith('\n') ? Math.max(0, lines.length - 1) : lines.length;
const nonEmptyLines = lines.filter(l => l.trim().length > 0).length;
// Structural analysis via registry
let analysis = null;
try {
analysis = registry.analyzeFile(file.path, content);
} catch {
// If analysis throws, treat as degraded — still include basic metrics
}
// Call graph extraction (code files only)
let callGraph = null;
if (file.fileCategory === 'code' || file.fileCategory === 'script') {
try {
const cg = registry.extractCallGraph(file.path, content);
if (cg && cg.length > 0) {
callGraph = cg.map(entry => ({
caller: entry.caller,
callee: entry.callee,
lineNumber: entry.lineNumber,
}));
}
} catch {
// Call graph extraction failed — non-fatal
}
}
// Build result object
const result = buildResult(file, totalLines, nonEmptyLines, analysis, callGraph, batchImportData);
results.push(result);
}
// Write output
const output = {
scriptCompleted: true,
filesAnalyzed: results.length,
filesSkipped,
results,
};
writeFileSync(outputPath, JSON.stringify(output, null, 2), 'utf-8');
if (!existsSync(outputPath)) {
throw new Error(`output file missing after write: ${outputPath}`);
}
}
// ---------------------------------------------------------------------------
// Result builder: maps StructuralAnalysis to the expected output schema.
// Exported for unit tests; pure function, no I/O.
// ---------------------------------------------------------------------------
export function buildResult(file, totalLines, nonEmptyLines, analysis, callGraph, batchImportData) {
const base = {
path: file.path,
language: file.language,
fileCategory: file.fileCategory,
totalLines,
nonEmptyLines,
};
if (!analysis) {
// No parser matched — return basic metrics only
base.metrics = {};
return base;
}
// Functions (code files)
if (analysis.functions && analysis.functions.length > 0) {
base.functions = analysis.functions.map(fn => ({
name: fn.name,
startLine: fn.lineRange[0],
endLine: fn.lineRange[1],
params: fn.params || [],
}));
}
// Classes (code files)
if (analysis.classes && analysis.classes.length > 0) {
base.classes = analysis.classes.map(cls => ({
name: cls.name,
startLine: cls.lineRange[0],
endLine: cls.lineRange[1],
methods: cls.methods || [],
properties: cls.properties || [],
}));
}
// Exports (code files)
if (analysis.exports && analysis.exports.length > 0) {
base.exports = analysis.exports.map(exp => ({
name: exp.name,
line: exp.lineNumber,
isDefault: exp.isDefault === true,
}));
}
// Non-code structural data: pass through directly
if (analysis.sections && analysis.sections.length > 0) {
base.sections = analysis.sections.map(s => ({
heading: s.name,
level: s.level,
line: s.lineRange[0],
}));
}
if (analysis.definitions && analysis.definitions.length > 0) {
base.definitions = analysis.definitions.map(d => ({
name: d.name,
kind: d.kind,
fields: d.fields || [],
startLine: d.lineRange[0],
endLine: d.lineRange[1],
}));
}
if (analysis.services && analysis.services.length > 0) {
base.services = analysis.services.map(s => ({
name: s.name,
image: s.image,
ports: s.ports || [],
...(s.lineRange ? { startLine: s.lineRange[0], endLine: s.lineRange[1] } : {}),
}));
}
if (analysis.endpoints && analysis.endpoints.length > 0) {
base.endpoints = analysis.endpoints.map(e => ({
method: e.method,
path: e.path,
startLine: e.lineRange[0],
endLine: e.lineRange[1],
}));
}
if (analysis.steps && analysis.steps.length > 0) {
base.steps = analysis.steps.map(s => ({
name: s.name,
startLine: s.lineRange[0],
endLine: s.lineRange[1],
}));
}
if (analysis.resources && analysis.resources.length > 0) {
base.resources = analysis.resources.map(r => ({
name: r.name,
kind: r.kind,
startLine: r.lineRange[0],
endLine: r.lineRange[1],
}));
}
// Call graph
if (callGraph && callGraph.length > 0) {
base.callGraph = callGraph;
}
// Metrics
const metrics = {};
// Import count from batchImportData (pre-resolved by project scanner).
// Empty arrays are truthy, so explicitly check length so we fall back to the
// parser's own import list when the scanner could not resolve any imports
// (e.g. Python absolute imports the scanner doesn't follow).
//
// The fallback counts only relative-style imports (those starting with `.`)
// so the metric stays *internal-import* in semantics rather than mixing in
// every external package import seen by the parser. Resolved external imports
// can never produce edges anyway, so counting them would be misleading.
const importPaths = batchImportData?.[file.path];
if (importPaths && importPaths.length > 0) {
metrics.importCount = importPaths.length;
} else if (analysis.imports) {
const internal = analysis.imports.filter(imp => {
const src = imp?.source ?? '';
return src.startsWith('.');
});
metrics.importCount = internal.length;
}
if (analysis.exports) {
metrics.exportCount = analysis.exports.length;
}
if (analysis.functions) {
metrics.functionCount = analysis.functions.length;
}
if (analysis.classes) {
metrics.classCount = analysis.classes.length;
}
if (analysis.sections) {
metrics.sectionCount = analysis.sections.length;
}
if (analysis.definitions) {
metrics.definitionCount = analysis.definitions.length;
}
if (analysis.services) {
metrics.serviceCount = analysis.services.length;
}
if (analysis.endpoints) {
metrics.endpointCount = analysis.endpoints.length;
}
if (analysis.steps) {
metrics.stepCount = analysis.steps.length;
}
if (analysis.resources) {
metrics.resourceCount = analysis.resources.length;
}
base.metrics = metrics;
return base;
}
// ---------------------------------------------------------------------------
// Run only when executed directly as a CLI; importing the module (e.g. from
// tests) must not trigger main().
//
// Canonicalize both sides through realpathSync. Node ESM resolves
// import.meta.url through symlinks but pathToFileURL(process.argv[1]) preserves
// them, so a raw equality check silently no-ops when the script is invoked via
// a symlinked plugin install path (the default in Claude Code / Copilot CLI
// caches). See GitHub issue #162.
// ---------------------------------------------------------------------------
function isCliEntry() {
if (!process.argv[1]) return false;
try {
const modulePath = realpathSync(fileURLToPath(import.meta.url));
const argvPath = realpathSync(process.argv[1]);
return modulePath === argvPath;
} catch {
return false;
}
}
if (isCliEntry()) {
try {
await main();
} catch (err) {
process.stderr.write(`extract-structure.mjs failed: ${err.message}\n${err.stack}\n`);
process.exit(1);
}
}
Django Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when Django is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
Django Project Structure
When analyzing a Django project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
manage.py | CLI entry point for dev server, migrations, management commands | entry-point, config |
*/settings.py, */settings/*.py | Project-wide configuration (DB, installed apps, middleware) | config |
*/urls.py | URL routing — maps URL patterns to views | api-handler, routing |
*/views.py, */views/*.py | Request handlers (function-based or class-based views) | api-handler, controller |
*/models.py, */models/*.py | ORM models — map to database tables | data-model |
*/serializers.py | DRF serializers — convert models to/from JSON | serialization, api-handler |
*/forms.py | Django forms — validation and rendering logic | validation, ui |
*/admin.py | Admin site registrations — exposes models in Django admin | config |
*/signals.py | Signal handlers — cross-cutting side effects on model events | event-handler |
*/tasks.py | Celery async task definitions | service, event-handler |
*/middleware.py, */middleware/*.py | Request/response middleware classes | middleware |
*/permissions.py | DRF permission classes | middleware, validation |
*/filters.py | DRF filter backends | utility |
*/migrations/*.py | Auto-generated schema migrations — do not summarize individually | config |
*/templates/**/*.html | Django HTML templates | ui |
*/templatetags/*.py | Custom template filters and tags | utility |
*/management/commands/*.py | Custom management commands (./manage.py mycommand) | config, entry-point |
wsgi.py, asgi.py | WSGI/ASGI server adapter — production entry point | config, entry-point |
*/apps.py | App configuration and startup hooks (AppConfig) | config |
*/tests.py, */tests/*.py | Unit and integration tests | test |
Edge Patterns to Look For
URL routing graph — Create calls edges from urls.py nodes to their corresponding view nodes when path() or re_path() maps a URL pattern to a view function or class. These edges represent the HTTP routing chain.
Signal wiring — When signals.py uses post_save.connect(handler, sender=Model) or @receiver(post_save, sender=Model), create subscribes edges from the signal handler function to the model class. Create publishes edges from the model to the signal handler to show the trigger direction.
ORM relationships — When models.py defines ForeignKey, OneToOneField, or ManyToManyField, create depends_on edges between the model classes with a description indicating the relationship type and cardinality.
Serializer-to-model binding — When a DRF serializer has model = MyModel in its Meta class, create a depends_on edge from the serializer to the model.
View-to-serializer binding — When a DRF ViewSet or APIView references a serializer class, create a depends_on edge from the view to the serializer.
Architectural Layers for Django
Assign nodes to these layers when detected:
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:api | API Layer | views.py, serializers.py, urls.py, DRF ViewSets and APIViews |
layer:data | Data Layer | models.py, migrations/, database utility files |
layer:service | Service Layer | signals.py, tasks.py, custom managers, service modules |
layer:ui | UI Layer | templates/, forms.py, templatetags/ |
layer:middleware | Middleware Layer | middleware.py, permissions.py, authentication backends |
layer:config | Config Layer | settings.py, urls.py (root), wsgi.py, asgi.py, apps.py, manage.py |
layer:test | Test Layer | tests.py, tests/ directory, conftest.py |
Notable Patterns to Capture in languageLesson
- Fat models vs. thin views: Django encourages business logic in model methods, keeping views thin HTTP adapters
- Django ORM lazy evaluation: QuerySets are not evaluated until iterated — chain filters without DB hits
- Class-based views (CBVs): Mixins like
LoginRequiredMixin,PermissionRequiredMixincompose behavior through multiple inheritance - Signal anti-patterns: Signals create invisible coupling; a signal in
signals.pymay be triggered by asave()call anywhere in the codebase - App isolation: Each Django app (
INSTALLED_APPS) should be self-contained with its own models, views, urls, and migrations
Express Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when Express is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
Express Project Structure
When analyzing an Express project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
app.js, app.ts | Application entry point — creates Express app, mounts middleware and routes | entry-point, config |
server.js, server.ts, index.js, index.ts | Server bootstrap — starts HTTP listener, may import app | entry-point, config |
routes/*.js, routes/*.ts | Route definitions — map HTTP methods and paths to handlers | api-handler, routing |
controllers/*.js, controllers/*.ts | Request handlers — process requests, orchestrate services, return responses | api-handler, service |
models/*.js, models/*.ts | Data models — Mongoose schemas, Sequelize models, or plain data definitions | data-model |
middleware/*.js, middleware/*.ts | Middleware functions — authentication, logging, validation, error handling | middleware |
services/*.js, services/*.ts | Business logic — domain operations decoupled from HTTP layer | service |
db/*.js, db/*.ts, database/*.js | Database connection and configuration | data-model, config |
config/*.js, config/*.ts | Application configuration — environment variables, feature flags | config |
validators/*.js, validators/*.ts | Request validation schemas (Joi, Zod, express-validator) | validation, utility |
utils/*.js, utils/*.ts | Shared utility functions | utility |
tests/*.js, test/*.js, __tests__/*.js | Unit and integration tests | test |
Edge Patterns to Look For
Route mounting — When app.use('/api/users', usersRouter) mounts a router, create depends_on edges from the main app to the router module. These edges represent the HTTP routing tree.
Middleware chain — When app.use(cors()), app.use(authMiddleware), or router.use(validate) registers middleware, create middleware edges from the app or router to the middleware function. Order matters — middleware executes in registration order.
Controller-to-service calls — When a controller imports and calls a service function, create depends_on edges from the controller to the service. This represents the separation between HTTP handling and business logic.
Model relationships — When models reference each other (Mongoose ref, Sequelize associations), create depends_on edges between model files with descriptions indicating the relationship type.
Architectural Layers for Express
Assign nodes to these layers when detected:
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:api | API Layer | routes/, controllers/, request validators |
layer:data | Data Layer | models/, db/, migration files, seeders |
layer:service | Service Layer | services/, business logic modules |
layer:middleware | Middleware Layer | middleware/, error handlers, authentication, logging |
layer:config | Config Layer | app.js, config/, environment setup, server.js |
layer:utility | Utility Layer | utils/, helpers/, shared pure functions |
layer:test | Test Layer | tests/, __tests__/, *.test.js, *.spec.js |
Notable Patterns to Capture in languageLesson
- Middleware chain (req, res, next): Express processes requests through a pipeline of middleware functions — each receives the request, response, and a
next()callback to pass control forward - Error-handling middleware (4 params): Middleware with signature
(err, req, res, next)catches errors — must be registered after all routes to act as a global error handler - Router modularity:
express.Router()creates modular, mountable route handlers that can be composed into the main app at different path prefixes - MVC pattern: Express apps commonly separate concerns into Models (data), Views (response formatting), and Controllers (request handling)
- Body parsing and validation: Request body parsing (
express.json(),express.urlencoded()) and validation (Joi, Zod, express-validator) are middleware concerns applied before route handlers
FastAPI Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when FastAPI is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
FastAPI Project Structure
When analyzing a FastAPI project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
main.py, app.py | Application factory — creates and configures the FastAPI() instance | entry-point, config |
*/routers/*.py, */api/*.py | APIRouter modules — group related endpoints by domain | api-handler, routing |
*/schemas.py, */schemas/*.py | Pydantic request/response models | type-definition, serialization |
*/models.py, */models/*.py | SQLAlchemy ORM models or other DB models | data-model |
*/dependencies.py, */deps.py | Depends() provider functions — shared logic injected into routes | service, middleware |
*/crud.py, */repository.py | Database access layer — CRUD operations | data-model, service |
*/database.py, */db.py | DB engine, session factory, connection management | config, data-model |
*/config.py, */settings.py | pydantic-settings / BaseSettings config classes | config |
*/middleware.py | Starlette middleware classes | middleware |
*/exceptions.py | Custom exception classes and exception handlers | utility |
*/security.py, */auth.py | Auth utilities — JWT decoding, password hashing, OAuth helpers | service, middleware |
*/tasks.py | Background tasks or Celery task definitions | service, event-handler |
*/tests/*.py, test_*.py | pytest test files | test |
conftest.py | pytest fixtures and test configuration | test, config |
Edge Patterns to Look For
Router inclusion chain — When app.include_router(some_router, prefix="/api") appears in main.py or a router aggregator, create imports + depends_on edges from the main app file to each router module. This builds the URL hierarchy graph.
Dependency injection tree — When a route function or another Depends() provider imports and calls Depends(some_function), create depends_on edges from the caller to the dependency provider. Trace these chains — they often span multiple files (e.g., route → auth dependency → DB session dependency).
Pydantic model inheritance — When a schema class inherits from another (e.g., class UserCreate(UserBase)), create inherits edges between the schema class nodes.
ORM model relationships — When SQLAlchemy models use relationship(), ForeignKey, create depends_on edges between the model classes.
CRUD-to-model binding — When a crud.py function takes a model type as an argument or directly references a model class, create depends_on edges from the CRUD file to the model file.
Architectural Layers for FastAPI
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:api | API Layer | Router files, endpoint functions with @router.get/post/... decorators |
layer:types | Types Layer | Pydantic schema files, request/response models |
layer:service | Service Layer | dependencies.py, crud.py, business logic modules |
layer:data | Data Layer | ORM models, database.py, migrations |
layer:config | Config Layer | main.py / app.py factory, settings.py, config.py |
layer:middleware | Middleware Layer | middleware.py, security.py, auth.py, exception handlers |
layer:test | Test Layer | tests/, conftest.py |
Notable Patterns to Capture in languageLesson
- Dependency injection as composition: FastAPI's
Depends()is a first-class DI system — a route can declare any number of dependencies, each of which can have their own dependencies, forming a tree resolved at request time - Pydantic for validation: Request bodies, query params, and path params are automatically validated by Pydantic — invalid input raises
422 Unprocessable Entitybefore your code runs - Async endpoints:
async defroutes run in the event loop;defroutes run in a threadpool — mixing them incorrectly can cause performance issues - Path operation order: FastAPI matches routes in declaration order; a catch-all route before a specific one will shadow it
Flask Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when Flask is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
Flask Project Structure
When analyzing a Flask project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
app.py, __init__.py (in app package) | Application factory (create_app()) or direct Flask(__name__) instance | entry-point, config |
run.py, wsgi.py | Production/dev server entry point | entry-point, config |
*/views.py, */routes.py | Route handler functions with @app.route or @blueprint.route | api-handler, routing |
*/blueprints/*.py, */api/*.py | Blueprint modules — group routes by feature | api-handler, routing |
*/models.py | SQLAlchemy models or other ORM models | data-model |
*/forms.py | WTForms form classes | validation, ui |
*/schemas.py | Marshmallow serialization schemas | serialization, type-definition |
*/config.py | Config classes (DevelopmentConfig, ProductionConfig) | config |
*/extensions.py | Flask extension initialization (db = SQLAlchemy(), login_manager = LoginManager()) | config, singleton |
*/decorators.py | Custom route decorators (auth guards, rate limiting) | middleware, utility |
*/utils.py, */helpers.py | Shared utility functions | utility |
*/templates/**/*.html | Jinja2 templates | ui |
*/static/ | CSS, JS, and asset files | assets |
*/tests/*.py, test_*.py | pytest or unittest test files | test |
Edge Patterns to Look For
Blueprint registration — When app.register_blueprint(bp, url_prefix='/api') appears in the application factory, create depends_on edges from the app factory to each blueprint module.
Extension coupling — When a view imports from extensions.py (e.g., from .extensions import db, login_manager), create imports edges to show which views depend on which extensions.
Before/after request hooks — When @app.before_request or @blueprint.before_request decorates a function, create middleware edges from those functions to the app/blueprint they attach to.
Architectural Layers for Flask
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:api | API Layer | Blueprint route files, view functions |
layer:data | Data Layer | models.py, database migration files |
layer:service | Service Layer | Business logic modules, schemas.py, service classes |
layer:ui | UI Layer | templates/, forms.py, static/ |
layer:config | Config Layer | app.py factory, config.py, extensions.py |
layer:middleware | Middleware Layer | decorators.py, before/after request hooks |
layer:test | Test Layer | Test files, conftest.py |
Notable Patterns to Capture in languageLesson
- Application factory pattern:
create_app()functions allow multiple app instances (e.g., for testing) and delay extension initialization — avoids circular imports - Blueprint modularity: Blueprints group related routes, templates, and static files; they are registered on the app with a URL prefix, making them independently testable
- Flask extension protocol: Extensions follow
init_app(app)for lazy initialization — the extension object is created globally but bound to an app instance later
Gin (Go) Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when Gin is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
Gin Project Structure
When analyzing a Gin project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
main.go | Application entry point — initializes the Gin engine, registers routes, starts the server | entry-point, config |
cmd/*.go, cmd/**/*.go | CLI entry points — multiple binaries in a multi-command project | entry-point, config |
handlers/*.go, handler/*.go | HTTP handlers — process requests with gin.Context | api-handler |
controllers/*.go, controller/*.go | Controllers — alternative naming for HTTP handlers | api-handler |
routes/*.go, router/*.go | Route definitions — register endpoints and route groups | routing, config |
models/*.go, model/*.go | Data models — struct definitions mapped to database tables | data-model |
middleware/*.go | Middleware functions — authentication, logging, CORS, rate limiting | middleware |
services/*.go, service/*.go | Business logic — domain operations decoupled from HTTP layer | service |
repository/*.go, repo/*.go | Data access layer — database queries and persistence logic | data-model, service |
config/*.go, config.go | Application configuration — environment loading, struct-based config | config |
dto/*.go | Data transfer objects — request and response structs | type-definition |
utils/*.go, pkg/*.go | Shared utility packages | utility |
*_test.go | Unit and integration tests | test |
Edge Patterns to Look For
Route group registration — When r.Group("/api") creates a route group and registers handlers, create configures edges from the route definition file to each handler. Route groups organize endpoints by prefix and shared middleware.
Handler-to-service calls — When a handler function calls a service method, create depends_on edges from the handler to the service. This represents the separation between HTTP handling and business logic.
Service-to-repository calls — When a service calls a repository method for data access, create depends_on edges from the service to the repository. This represents the data access abstraction.
Middleware chaining — When r.Use(middleware) or a route group applies middleware, create middleware edges from the router or group to the middleware function. Middleware executes in registration order.
Architectural Layers for Gin
Assign nodes to these layers when detected:
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:api | API Layer | handlers/, controllers/, HTTP handler functions |
layer:data | Data Layer | models/, repository/, database access, migrations |
layer:service | Service Layer | services/, business logic |
layer:middleware | Middleware Layer | middleware/, authentication, logging, rate limiting |
layer:config | Config Layer | main.go, routes/, config/, environment setup |
layer:utility | Utility Layer | utils/, pkg/, shared helper packages |
layer:test | Test Layer | *_test.go, test fixtures, test helpers |
Notable Patterns to Capture in languageLesson
- Handler functions with gin.Context: Every Gin handler receives a
*gin.Contextparameter — it provides request parsing (c.Bind,c.Param,c.Query), response writing (c.JSON,c.HTML), and control flow (c.Abort,c.Next) - Middleware chain with c.Next(): Middleware calls
c.Next()to pass control to the next handler in the chain — code beforec.Next()runs pre-handler, code after runs post-handler - Route grouping for modular APIs:
r.Group("/v1")creates modular sub-routers that can have their own middleware stack — enables versioning and access control at the group level - Dependency injection via constructors (no framework DI): Go has no DI framework — dependencies are passed as constructor parameters (e.g.,
NewUserHandler(userService)) and stored as struct fields - Interface-driven design for testability: Services and repositories are defined as interfaces — handlers depend on the interface, enabling mock implementations in tests
- Error handling with gin.Error: Gin collects errors via
c.Error(err)— middleware can inspectc.Errorsafter handler execution to implement centralized error logging and response formatting
Next.js Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when Next.js is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
Next.js Project Structure
When analyzing a Next.js project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
app/layout.tsx | Root layout — wraps all pages, defines HTML shell and global providers | entry-point, config, ui |
app/page.tsx | Root page component — renders at / | ui, routing |
app/**/page.tsx | Route page components — file path determines URL | ui, routing |
app/**/layout.tsx | Nested layouts — wrap child routes with shared UI | ui, config |
app/**/loading.tsx | Loading UI — shown as Suspense fallback during route transitions | ui |
app/**/error.tsx | Error boundary — catches errors in the route segment | ui |
app/**/not-found.tsx | 404 UI — shown when notFound() is called | ui |
app/api/**/route.ts | API route handlers — serverless endpoint functions (GET, POST, etc.) | api-handler |
middleware.ts | Edge middleware — intercepts requests before they reach routes | middleware |
lib/*.ts, lib/**/*.ts | Shared server-side utilities, data access, and business logic | service |
components/*.tsx, components/**/*.tsx | Reusable UI components | ui |
next.config.js, next.config.mjs, next.config.ts | Next.js configuration — redirects, rewrites, env, webpack overrides | config |
actions/*.ts, app/**/actions.ts | Server Actions — server-side mutation functions callable from client | service, api-handler |
Edge Patterns to Look For
Layout nesting — When app/foo/layout.tsx wraps app/foo/page.tsx and app/foo/bar/page.tsx, create contains edges from the layout to the pages it wraps. Layouts compose via the file-system hierarchy.
API route handlers — When a route.ts file exports named functions (GET, POST, PUT, DELETE), create edges from consuming components or server actions to the route handler based on fetch calls.
Server/Client component boundary — Files with "use client" directive at the top are Client Components. All other components in the app/ directory are Server Components by default. Create depends_on edges that cross this boundary and note the boundary in the edge description.
Parallel routes — When app/@slot/page.tsx patterns appear, create contains edges from the parent layout to each parallel slot. These render simultaneously in the same layout.
Route groups — Directories wrapped in parentheses (group) organize routes without affecting the URL path. Note these in node descriptions.
Architectural Layers for Next.js
Assign nodes to these layers when detected:
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:ui | UI Layer | app/**/page.tsx, app/**/layout.tsx, components/, loading/error boundaries |
layer:api | API Layer | app/api/**/route.ts, API route handlers |
layer:service | Service Layer | lib/, server actions, data-fetching utilities |
layer:middleware | Middleware Layer | middleware.ts, edge functions |
layer:config | Config Layer | next.config.*, root layout, tailwind.config.*, environment setup |
layer:test | Test Layer | __tests__/, *.test.tsx, *.spec.tsx, e2e/ |
Notable Patterns to Capture in languageLesson
- Server Components by default: Components in the
app/directory are Server Components — no JavaScript is sent to the client unless"use client"is declared - Server Actions for mutations: Functions marked with
"use server"can be called directly from client components, replacing traditional API routes for form submissions and mutations - App Router file conventions: Special files (
page,layout,loading,error,not-found,route) define behavior by naming convention within the file-system router - ISR and static generation:
generateStaticParamspre-renders pages at build time; revalidation strategies control cache freshness - Parallel and intercepting routes:
@slotdirectories enable parallel rendering;(.)prefix directories enable route interception for modal patterns
Ruby on Rails Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when Rails is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
Rails Project Structure
When analyzing a Ruby on Rails project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
config.ru | Rack entry point — boots the Rails application for the web server | entry-point |
config/application.rb | Application configuration — sets up Rails, loads gems, configures middleware | entry-point, config |
app/controllers/*_controller.rb | Controllers — handle HTTP requests, orchestrate models, render responses | api-handler |
app/controllers/concerns/*.rb | Controller concerns — shared controller behavior via mixins | middleware, utility |
app/models/*.rb | ActiveRecord models — map to database tables, contain validations and associations | data-model |
app/models/concerns/*.rb | Model concerns — shared model behavior via mixins | utility |
app/views/**/*.erb, app/views/**/*.haml | View templates — HTML rendering with embedded Ruby | ui |
app/helpers/*_helper.rb | View helpers — utility methods available in templates | utility |
app/mailers/*_mailer.rb | Action Mailer classes — send email notifications | service |
app/jobs/*_job.rb | Active Job classes — background job processing | service |
app/channels/*_channel.rb | Action Cable channels — WebSocket communication | service |
app/serializers/*_serializer.rb | API serializers — JSON response formatting (ActiveModelSerializers, Blueprinter) | api-handler, utility |
app/services/*.rb | Service objects — encapsulate complex business logic | service |
db/migrate/*.rb | Database migrations — schema changes versioned by timestamp | config, data-model |
db/schema.rb, db/structure.sql | Generated schema snapshot — current database structure | data-model, config |
config/routes.rb | Route definitions — maps URLs to controller actions | routing, config |
config/initializers/*.rb | Initializers — run once at boot to configure gems and services | config |
lib/**/*.rb | Library code — custom classes, Rake tasks, extensions | utility, service |
spec/**/*_spec.rb, test/**/*_test.rb | RSpec or Minitest test files | test |
Edge Patterns to Look For
Route-to-controller mapping — When config/routes.rb defines resources :users or get '/foo', to: 'bar#baz', create configures edges from the routes file to the corresponding controller. RESTful resources generate a full set of action mappings.
ActiveRecord associations — When models define has_many, belongs_to, has_one, or has_and_belongs_to_many, create depends_on edges between model files with descriptions indicating the association type and direction.
Controller-to-model — When a controller calls model methods (User.find, @post.save), create depends_on edges from the controller to the model. Controllers are the primary consumers of model data.
Callbacks — When models or controllers use before_action, after_save, before_validation, or similar callbacks, note these as middleware-like edges. Callbacks create implicit execution paths that are not visible from the call site.
Architectural Layers for Rails
Assign nodes to these layers when detected:
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:api | API Layer | app/controllers/, app/serializers/, API-specific controllers |
layer:data | Data Layer | app/models/, db/migrate/, db/schema.rb |
layer:ui | UI Layer | app/views/, app/helpers/, app/assets/, app/javascript/ |
layer:service | Service Layer | app/mailers/, app/jobs/, app/channels/, app/services/, lib/ |
layer:config | Config Layer | config/routes.rb, config/initializers/, config/application.rb, config.ru |
layer:middleware | Middleware Layer | app/middleware/, controller concerns, Rack middleware |
layer:test | Test Layer | spec/, test/, *.spec.rb, *_test.rb |
Notable Patterns to Capture in languageLesson
- Convention over configuration: Rails derives routing, table names, and file locations from naming conventions —
UsersControllermaps tousers_controller.rb, handles/users, and queries theuserstable - ActiveRecord pattern: Models are database wrappers — each model class maps to a table, instances map to rows, and attributes map to columns with automatic type coercion
- Concerns for shared behavior:
ActiveSupport::Concernmodules are mixins included in models or controllers to share validations, scopes, callbacks, and methods across classes - Strong parameters for mass-assignment protection:
params.require(:user).permit(:name, :email)whitelists attributes — controllers must explicitly declare which fields can be set from user input - RESTful resource routing:
resources :postsgenerates seven standard CRUD routes — Rails strongly encourages RESTful design where each controller maps to a resource - Callbacks and observers:
before_save,after_create, and similar callbacks inject logic into the object lifecycle — they create invisible execution paths that can be difficult to trace
React Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when React is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
React Project Structure
When analyzing a React project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
src/App.tsx | Root application component — mounts providers, router, and top-level layout | entry-point, ui |
components/*.tsx, components/**/*.tsx | Reusable UI components | ui |
hooks/*.ts, hooks/*.tsx | Custom React hooks — encapsulate reusable stateful logic | service, utility |
contexts/*.tsx, context/*.tsx | React Context providers and consumers — shared state across component tree | service, state |
pages/*.tsx, views/*.tsx | Page-level components mapped to routes | ui, routing |
utils/*.ts, helpers/*.ts | Pure utility functions — formatting, validation, transformations | utility |
types/*.ts, types/*.d.ts | TypeScript type definitions and interfaces | type-definition |
services/*.ts, api/*.ts | API client functions and data-fetching logic | service |
store/*.ts, slices/*.ts | State management (Redux, Zustand, etc.) | service, state |
constants/*.ts | Application-wide constants and enums | config |
__tests__/*.tsx, *.test.tsx, *.spec.tsx | Unit and integration tests | test |
Edge Patterns to Look For
Component composition — When a parent component renders a child component in its JSX return, create contains edges from the parent to the child. These edges represent the component tree hierarchy.
Hook usage — When a component or hook imports and calls a custom hook (useX), create depends_on edges from the consumer to the hook module. Hooks are the primary mechanism for shared logic in React.
Context provider/consumer — When a Context provider wraps components, create publishes edges from the provider to the context definition. When components call useContext or use a custom context hook, create subscribes edges from the consumer to the context.
Props drilling chains — When props are passed through multiple component layers without being used, create depends_on edges along the chain to surface the coupling depth.
Architectural Layers for React
Assign nodes to these layers when detected:
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:ui | UI Layer | components/, pages/, views/, layout components |
layer:service | Service Layer | hooks/, contexts/, services/, api/, store/ |
layer:types | Types Layer | types/, shared TypeScript interfaces and type definitions |
layer:utility | Utility Layer | utils/, helpers/, pure functions |
layer:config | Config Layer | App.tsx, router configuration, provider setup, constants |
layer:test | Test Layer | __tests__/, *.test.tsx, *.spec.tsx |
Notable Patterns to Capture in languageLesson
- Component composition over inheritance: React favors composing components via props and children rather than class inheritance hierarchies
- Custom hooks for reusable logic: Hooks prefixed with
useextract stateful logic into shareable modules without changing the component tree - React.memo for performance: Components wrapped in
React.memoskip re-renders when props are unchanged — indicates performance-sensitive paths - Controlled vs. uncontrolled components: Controlled components derive state from props; uncontrolled components manage internal state via refs
- Render props pattern: Components that accept a function as children or a render prop to delegate rendering decisions to the consumer
Spring Boot Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when Spring Boot is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
Spring Boot Project Structure
When analyzing a Spring Boot project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
*Application.java, *Application.kt | Application entry point — @SpringBootApplication class with main() method | entry-point, config |
*Controller.java, *RestController.java | REST controllers — handle HTTP requests, delegate to services | api-handler |
*Service.java | Service interfaces — define business operation contracts | service |
*ServiceImpl.java | Service implementations — contain business logic | service |
*Repository.java | Spring Data repositories — data access interfaces extending JpaRepository/CrudRepository | data-model |
*Entity.java | JPA entities — map to database tables via @Entity annotation | data-model |
*DTO.java, *Request.java, *Response.java | Data transfer objects — request/response payloads | type-definition |
*Config.java, *Configuration.java | Configuration classes — @Configuration beans, security config, web config | config |
*Filter.java | Servlet filters — intercept requests before they reach controllers | middleware |
*Interceptor.java | Handler interceptors — pre/post processing around controller methods | middleware |
*Advice.java, *ExceptionHandler.java | Controller advice — global exception handling and response wrapping | middleware |
*Mapper.java | Object mappers — convert between entities and DTOs (MapStruct, ModelMapper) | utility |
application.yml, application.properties | Application configuration — profiles, datasource, server settings | config |
*Test.java, *Tests.java, *IT.java | Unit tests, integration tests | test |
Edge Patterns to Look For
@Autowired injection — When a class injects a dependency via @Autowired, constructor injection, or @Inject, create depends_on edges from the consumer to the injected bean. Constructor injection is preferred and most common in modern Spring.
Controller-Service-Repository chain — The canonical call chain is @RestController -> @Service -> @Repository. Create depends_on edges along this chain to show the layered architecture.
@Entity relationships — When entities define @OneToMany, @ManyToOne, @OneToOne, or @ManyToMany annotations, create depends_on edges between entity classes with descriptions indicating the relationship type and direction.
@Configuration bean definitions — When a @Configuration class defines @Bean methods, create configures edges from the configuration class to the types it produces. These beans become available for injection throughout the application.
Architectural Layers for Spring Boot
Assign nodes to these layers when detected:
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:api | API Layer | *Controller.java, REST endpoints, API documentation |
layer:service | Service Layer | *Service.java, *ServiceImpl.java, business logic |
layer:data | Data Layer | *Repository.java, *Entity.java, JPA mappings, database migrations |
layer:types | Types Layer | *DTO.java, *Request.java, *Response.java, shared value objects |
layer:config | Config Layer | *Configuration.java, application.yml, security config, *Application.java |
layer:middleware | Middleware Layer | *Filter.java, *Interceptor.java, *Advice.java, security filters |
layer:test | Test Layer | *Test.java, *Tests.java, *IT.java, test configuration |
Notable Patterns to Capture in languageLesson
- Dependency injection via constructor injection: Spring favors constructor injection over field injection (
@Autowiredon fields) — it makes dependencies explicit, supports immutability, and simplifies testing - Layered architecture (Controller -> Service -> Repository): Spring Boot applications follow a strict layered pattern where controllers handle HTTP, services contain business logic, and repositories manage persistence
- Spring Security filter chain: Security is implemented as a chain of servlet filters —
SecurityFilterChainbeans configure authentication, authorization, CORS, and CSRF protection - JPA entity lifecycle: Entities transition through states (transient, managed, detached, removed) — understanding this lifecycle is essential for tracing data flow through the persistence layer
- AOP for cross-cutting concerns:
@Aspectclasses with@Before,@After, and@Aroundadvice inject behavior at join points — used for logging, transactions (@Transactional), and caching (@Cacheable)
Vue Framework Addendum
Injected into file-analyzer and architecture-analyzer prompts when Vue is detected.
Do NOT use as a standalone prompt — always appended to the base prompt template.
Vue Project Structure
When analyzing a Vue project, apply these additional conventions on top of the base analysis rules.
Canonical File Roles
| File / Pattern | Role | Tags |
|---|---|---|
src/App.vue | Root application component — mounts the top-level layout and router view | entry-point, ui |
src/main.ts, src/main.js | Application bootstrap — creates Vue app instance, registers plugins, mounts to DOM | entry-point, config |
components/*.vue, components/**/*.vue | Reusable UI components | ui |
views/*.vue, pages/*.vue | Page-level components mapped to routes | ui, routing |
composables/*.ts, composables/*.js | Composable functions — reusable stateful logic using Composition API | service, utility |
store/*.ts, stores/*.ts | State management modules (Pinia stores or Vuex modules) | service, state |
router/*.ts, router/index.ts | Vue Router configuration — route definitions, navigation guards | config, routing |
plugins/*.ts, plugins/*.js | Vue plugin registrations — extend app functionality (i18n, auth, etc.) | config |
utils/*.ts, helpers/*.ts | Pure utility functions | utility |
types/*.ts, types/*.d.ts | TypeScript type definitions and interfaces | type-definition |
api/*.ts, services/*.ts | API client functions and data-fetching logic | service |
directives/*.ts | Custom Vue directives | utility |
tests/*.spec.ts, __tests__/*.spec.ts | Unit and integration tests | test |
Edge Patterns to Look For
Component parent-child — When a parent component uses a child component in its <template>, create contains edges from the parent to the child. Template refs and slot usage further indicate composition relationships.
Composable usage — When a component or composable imports and calls a useX function, create depends_on edges from the consumer to the composable module. Composables are the primary mechanism for shared stateful logic.
Store actions/getters — When components or composables import and use a Pinia store (useXStore()), create depends_on edges from the consumer to the store. Store-to-store dependencies should also be captured.
Router view mapping — When router/index.ts maps paths to view components, create configures edges from the router to each view component. Navigation guards add middleware-like edges.
Plugin registration — When main.ts calls app.use(plugin), create configures edges from the bootstrap file to each plugin.
Architectural Layers for Vue
Assign nodes to these layers when detected:
| Layer ID | Layer Name | What Goes Here |
|---|---|---|
layer:ui | UI Layer | components/, views/, pages/, layout components |
layer:service | Service Layer | composables/, store/, stores/, api/, services/ |
layer:config | Config Layer | router/, plugins/, main.ts, App.vue, configuration files |
layer:utility | Utility Layer | utils/, helpers/, directives/, pure functions |
layer:test | Test Layer | tests/, __tests__/, *.spec.ts |
Notable Patterns to Capture in languageLesson
- Composition API over Options API: Modern Vue favors
setup()and<script setup>with composables, replacing the Options API's data/methods/computed separation - Pinia for state management: Pinia stores provide type-safe, modular state with actions and getters — each store is independently defined and can depend on other stores
- Vue Router with navigation guards:
beforeEach,beforeEnter, andafterEachguards act as middleware for route transitions — used for authentication and data prefetching - Single-file components (.vue): Each
.vuefile encapsulates template, script, and style in a single file — the<script setup>syntax is the recommended concise form - Reactive refs and computed properties:
ref()andreactive()create reactive state;computed()derives values that auto-update — understanding reactivity is key to tracing data flow - Provide/inject for deep dependency passing:
provide()andinject()pass values down the component tree without prop drilling — creates implicit dependencies that should be captured as edges
#!/usr/bin/env node
/**
* generate-ignore.mjs
*
* Writes a starter `.understand-anything/.understandignore` for the target
* project by delegating to `generateStarterIgnoreFile` in
* `@understand-anything/core`. Invoked from SKILL.md Phase 0.5; replaces the
* inline `node -e "…"` block that previously duplicated the generator logic.
*
* Usage:
* node generate-ignore.mjs <projectRoot>
*
* Behaviour:
* - Exits 0 with a stderr notice if the target file already exists.
* - Creates `<projectRoot>/.understand-anything/` if missing.
* - Emits a one-line stderr summary on success.
*
* Mirrors the @understand-anything/core resolution dance used by
* scan-project.mjs: workspace-linked package first, plugin-cache dist fallback.
*
* Plugin root resolution: prefer $PLUGIN_ROOT from the environment (set by
* SKILL.md Phase 0 via its multi-candidate search) over the
* `resolve(__dirname, '../..')` heuristic. The relative path breaks when
* `skills/understand/` is copied into a runtime skills directory whose
* parent is not the plugin checkout.
*/
import { createRequire } from 'node:module';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
function resolvePluginRoot() {
const envRoot = process.env.PLUGIN_ROOT;
if (envRoot && existsSync(join(envRoot, 'package.json'))) {
return envRoot;
}
return resolve(__dirname, '../..');
}
const pluginRoot = resolvePluginRoot();
const require = createRequire(resolve(pluginRoot, 'package.json'));
let core;
try {
core = await import(pathToFileURL(require.resolve('@understand-anything/core')).href);
} catch {
core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href);
}
const { generateStarterIgnoreFile } = core;
const projectRoot = resolve(process.argv[2] ?? process.cwd());
const outDir = join(projectRoot, '.understand-anything');
const outPath = join(outDir, '.understandignore');
if (existsSync(outPath)) {
console.error(`generate-ignore: ${outPath} already exists — skipping`);
process.exit(0);
}
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
writeFileSync(outPath, generateStarterIgnoreFile(projectRoot));
console.error(`generate-ignore: wrote ${outPath}`);
Related skills
Forks & variants (1)
Understand has 1 known copy in the catalog totaling 846 installs. They canonicalize to this original listing.
- egonex-ai - 846 installs
How it compares
Use understand for persistent structural fingerprints when agents need incremental sync instead of full-repository rescans each session.
FAQ
Who is understand for?
Developers and software engineers working with understand patterns described in the skill documentation.
When should I use understand?
When Analyze a codebase to produce an interactive knowledge graph for understanding architecture, components, and relationships.
Is understand safe to install?
Review the Security Audits panel on this page before installing in production.