diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md index 35c0b112..7e2688ba 100644 --- a/understand-anything-plugin/skills/understand/SKILL.md +++ b/understand-anything-plugin/skills/understand/SKILL.md @@ -49,76 +49,9 @@ Determine whether to run a full analysis or incremental update. - Verify the resolved path exists and is a directory (run `test -d `). If it does not exist or is not a directory, report an error to the user and **STOP**. - Set `PROJECT_ROOT` to the resolved absolute path. - If no directory path argument is found, set `PROJECT_ROOT` to the current working directory. - - **Worktree redirect.** If `PROJECT_ROOT` is 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 comparing `git rev-parse --git-dir` against `git 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-dir` is the main repo root. + - **Worktree redirect.** If `PROJECT_ROOT` is 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). See `recipes/worktree-redirect.md` for the detection bash snippet. Set `UNDERSTAND_NO_WORKTREE_REDIRECT=1` if you intentionally want a per-worktree graph (rare). - ```bash - 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 - fi - ``` - - Set `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: - - ```bash - 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:-}" - echo " - $HOME/.understand-anything-plugin" - echo " - ${SELF_RELATIVE:-}" - echo " - ${COPILOT_SELF_RELATIVE:-}" - 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 - fi - ``` - - If `pnpm` is missing, report to the user: "Install Node.js ≥ 22 and pnpm ≥ 10, then re-run `/understand`." +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. See `recipes/plugin-root-resolution.md` for the full bash snippet that resolves `PLUGIN_ROOT` across install locations (CLAUDE_PLUGIN_ROOT, universal symlinks, copilot, codex, opencode, pi, clone-based) and runs the one-time `pnpm install + pnpm --filter @understand-anything/core build`. If `pnpm` is missing, report: "Install Node.js ≥ 22 and pnpm ≥ 10, then re-run `/understand`." 2. Get the current git commit hash: ```bash @@ -197,31 +130,7 @@ 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: - - Run the following Node.js one-liner in `$PROJECT_ROOT` (reads `.gitignore` and deduplicates against built-in defaults): - ```bash - node -e " - const fs = require('fs'); - const path = require('path'); - const root = process.cwd(); - const defaults = ['node_modules/','node_modules','.git/','vendor/','venv/','.venv/','__pycache__/','dist/','dist','build/','build','out/','coverage/','coverage','.next/','.cache/','.turbo/','target/','obj/','*.lock','package-lock.json','yarn.lock','pnpm-lock.yaml','*.png','*.jpg','*.jpeg','*.gif','*.svg','*.ico','*.woff','*.woff2','*.ttf','*.eot','*.mp3','*.mp4','*.pdf','*.zip','*.tar','*.gz','*.min.js','*.min.css','*.map','*.generated.*','.idea/','.vscode/','LICENSE','.gitignore','.editorconfig','.prettierrc','.eslintrc*','*.log']; - const norm = p => p.replace(/\/+$/, ''); - const defaultSet = new Set(defaults.map(norm)); - const header = '# .understandignore — patterns for files/dirs to exclude from analysis\n# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)\n# Lines below are suggestions — uncomment to activate.\n# Use ! prefix to force-include something excluded by defaults.\n#\n# Built-in defaults (always excluded unless negated):\n# node_modules/, .git/, dist/, build/, obj/, *.lock, *.min.js, etc.\n#\n'; - let body = ''; - const gitignorePath = path.join(root, '.gitignore'); - if (fs.existsSync(gitignorePath)) { - const gi = fs.readFileSync(gitignorePath, 'utf-8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).filter(p => !defaultSet.has(norm(p))); - if (gi.length) { body += '# --- From .gitignore (uncomment to exclude) ---\n\n' + gi.map(p => '# ' + p).join('\n') + '\n\n'; } - } - const dirs = ['__tests__','test','tests','fixtures','testdata','docs','examples','scripts','migrations','.storybook']; - const found = dirs.filter(d => fs.existsSync(path.join(root, d))); - if (found.length) { body += '# --- Detected directories (uncomment to exclude) ---\n\n' + found.map(d => '# ' + d + '/').join('\n') + '\n\n'; } - body += '# --- Test file patterns (uncomment to exclude) ---\n\n# *.test.*\n# *.spec.*\n# *.snap\n'; - const outDir = path.join(root, '.understand-anything'); - if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); - fs.writeFileSync(path.join(outDir, '.understandignore'), header + body); - " - ``` + - Run the Node.js one-liner from `recipes/understandignore-generator.md` in `$PROJECT_ROOT`. It reads `.gitignore`, deduplicates against built-in defaults, detects common test/docs/scripts directories, and writes the starter `.understand-anything/.understandignore`. - 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.** @@ -362,29 +271,7 @@ 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: -```bash -git diff ..HEAD --name-only > $PROJECT_ROOT/.understand-anything/tmp/changed-files.txt -``` - -Run compute-batches with `--changed-files`: -```bash -node /compute-batches.mjs $PROJECT_ROOT \ - --changed-files=$PROJECT_ROOT/.understand-anything/tmp/changed-files.txt -``` - -This 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: - ```bash - python /merge-batch-graphs.py $PROJECT_ROOT - ``` +See `recipes/incremental-update.md` for the full procedure. In short: write changed files to `tmp/changed-files.txt`, re-run `compute-batches.mjs` with `--changed-files`, dispatch file-analyzers using the same template as the full path, then prune nodes/edges referencing changed files into `batch-existing.json` and re-run `merge-batch-graphs.py`. --- @@ -470,20 +357,7 @@ After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermedi 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:`. 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: - -```json -[ - { - "id": "layer:", - "name": "", - "description": "", - "nodeIds": ["file:src/App.tsx", "config:tsconfig.json", "document:README.md"] - } -] -``` - -All four fields (`id`, `name`, `description`, `nodeIds`) are required. +Each element of the final `layers` array MUST have the `layers[*]` shape documented in `reference/output-shapes.md` (required fields: `id`, `name`, `description`, `nodeIds`). **For incremental updates:** Always re-run architecture analysis on the full merged node set, since layer assignments may shift when files change. @@ -548,26 +422,7 @@ After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermedi 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: - -```json -[ - { - "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. +Each element of the final `tour` array MUST have the `tour[*]` shape documented in `reference/output-shapes.md` (required fields: `order`, `title`, `description`, `nodeIds`; preserve optional `languageLesson` when present). --- @@ -575,25 +430,7 @@ Required fields: `order`, `title`, `description`, `nodeIds`. Preserve optional ` Report to the user: `[Phase 6/7] Validating knowledge graph...` -Assemble the full KnowledgeGraph JSON object: - -```json -{ - "version": "1.0.0", - "project": { - "name": "", - "languages": [""], - "frameworks": [""], - "description": "", - "analyzedAt": "", - "gitCommitHash": "" - }, - "nodes": [], - "edges": [], - "layers": [], - "tour": [] -} -``` +Assemble the full KnowledgeGraph JSON object — see the assembled-graph shape in `reference/output-shapes.md`. 1. Before writing the assembled graph, validate that: - `layers` is an array of objects with these required fields: `id`, `name`, `description`, `nodeIds` @@ -612,109 +449,21 @@ Assemble the full KnowledgeGraph JSON object: #### Default path (no `--review`): inline deterministic validation -Write the following Node.js script to `$PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.cjs`: - -```javascript -#!/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); } -``` +Write the Node.js validator from `recipes/inline-validator.md` to `$PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.cjs`, then execute it: -Execute it: ```bash 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. +The script validates required node fields (id, type, name, summary, tags), checks edge endpoints exist, ensures every file-level node is in exactly one layer, flags orphan nodes as warnings, and writes `{issues, warnings, stats}` to the output path. If it 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 whose `filePath` doesn'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_ROOT` -> Read the file and validate it for completeness and correctness. -> Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/review.json` +If `--review` IS in `$ARGUMENTS`, dispatch the LLM graph-reviewer subagent. See `recipes/llm-review-path.md` for the full additional-context block (Phase 1 scan inventory, accumulated warnings, cross-validation instructions) and the dispatch prompt. The reviewer writes its report to `$PROJECT_ROOT/.understand-anything/intermediate/review.json` — the same path the default path uses. --- @@ -739,28 +488,7 @@ 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: - ```bash - cat > $PROJECT_ROOT/.understand-anything/intermediate/fingerprint-input.json <], - "gitCommitHash": "" - } - EOF - ``` - - Then invoke the bundled script (located next to this SKILL.md): - ```bash - node /build-fingerprints.mjs \ - $PROJECT_ROOT/.understand-anything/intermediate/fingerprint-input.json - ``` - - The 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`).** +2. **Generate structural fingerprints baseline.** Run the procedure in `recipes/fingerprints-baseline.md`. It writes `intermediate/fingerprint-input.json` and invokes `build-fingerprints.mjs`. **Must succeed before `meta.json` is written** (issue #152) — abort Phase 7 if the script exits non-zero or stdout does not include `Fingerprints baseline:`. 3. Write metadata to `$PROJECT_ROOT/.understand-anything/meta.json` (only after step 2 succeeded): ```json @@ -772,18 +500,7 @@ Report to the user: `[Phase 7/7] Saving knowledge graph...` } ``` -4. Clean up intermediate files, **preserving `scan-result.json`** so future incremental runs can skip Phase 1 SCAN (see issue #293): - ```bash - # 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. - INTER="$PROJECT_ROOT/.understand-anything/intermediate" - if [ -d "$INTER" ]; then - find "$INTER" -mindepth 1 -maxdepth 1 -not -name 'scan-result.json' -exec rm -rf {} + - fi - rm -rf $PROJECT_ROOT/.understand-anything/tmp - ``` +4. Clean up intermediate files, **preserving `scan-result.json`** so future incremental runs can skip Phase 1 SCAN (see issue #293). See `recipes/intermediate-cleanup.md` for the cleanup bash snippet. 5. Report a summary to the user containing: - Project name and description @@ -813,41 +530,4 @@ Report to the user: `[Phase 7/7] Saving knowledge graph...` ## Reference: KnowledgeGraph Schema -### Node Types (13 total) -| Type | Description | ID Convention | -|---|---|---| -| `file` | Source code file | `file:` | -| `function` | Function or method | `function::` | -| `class` | Class, interface, or type | `class::` | -| `module` | Logical module or package | `module:` | -| `concept` | Abstract concept or pattern | `concept:` | -| `config` | Configuration file (YAML, JSON, TOML, env) | `config:` | -| `document` | Documentation file (Markdown, RST, TXT) | `document:` | -| `service` | Deployable service definition (Dockerfile, K8s) | `service:` | -| `table` | Database table or migration | `table::` | -| `endpoint` | API endpoint or route definition | `endpoint::` | -| `pipeline` | CI/CD pipeline configuration | `pipeline:` | -| `schema` | Schema definition (GraphQL, Protobuf, Prisma) | `schema:` | -| `resource` | Infrastructure resource (Terraform, CloudFormation) | `resource:` | - -### 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) | +See `reference/knowledge-graph-schema.md` for the full tables — node types (13), edge types (26 across structural/behavioral/data flow/dependencies/semantic/infrastructure/schema categories), and edge weight conventions. diff --git a/understand-anything-plugin/skills/understand/recipes/fingerprints-baseline.md b/understand-anything-plugin/skills/understand/recipes/fingerprints-baseline.md new file mode 100644 index 00000000..188f4ce7 --- /dev/null +++ b/understand-anything-plugin/skills/understand/recipes/fingerprints-baseline.md @@ -0,0 +1,26 @@ +# Recipe: Phase 7 Structural Fingerprints Baseline + +> Builds the fingerprint baseline that future auto-update runs compare against. Referenced from `SKILL.md` Phase 7 step 2 (Generate structural fingerprints baseline). + +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: +```bash +cat > $PROJECT_ROOT/.understand-anything/intermediate/fingerprint-input.json <], + "gitCommitHash": "" +} +EOF +``` + +Then invoke the bundled script (located next to this SKILL.md): +```bash +node /build-fingerprints.mjs \ + $PROJECT_ROOT/.understand-anything/intermediate/fingerprint-input.json +``` + +The 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 write `meta.json`.** diff --git a/understand-anything-plugin/skills/understand/recipes/incremental-update.md b/understand-anything-plugin/skills/understand/recipes/incremental-update.md new file mode 100644 index 00000000..25f5b446 --- /dev/null +++ b/understand-anything-plugin/skills/understand/recipes/incremental-update.md @@ -0,0 +1,27 @@ +# Recipe: Phase 2 Incremental Update Path + +> The "Incremental update path" branch of Phase 2 — how to re-batch only changed files, prune the prior graph, and re-merge. Referenced from `SKILL.md` Phase 2 → "Incremental update path". + +Write the changed-files list (one path per line) to a temp file: +```bash +git diff ..HEAD --name-only > $PROJECT_ROOT/.understand-anything/tmp/changed-files.txt +``` + +Run compute-batches with `--changed-files`: +```bash +node /compute-batches.mjs $PROJECT_ROOT \ + --changed-files=$PROJECT_ROOT/.understand-anything/tmp/changed-files.txt +``` + +This 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: + ```bash + python /merge-batch-graphs.py $PROJECT_ROOT + ``` diff --git a/understand-anything-plugin/skills/understand/recipes/inline-validator.md b/understand-anything-plugin/skills/understand/recipes/inline-validator.md new file mode 100644 index 00000000..3e66c90e --- /dev/null +++ b/understand-anything-plugin/skills/understand/recipes/inline-validator.md @@ -0,0 +1,80 @@ +# Recipe: Inline Deterministic Graph Validator + +> The Node.js script written to `$PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.cjs` for the default (non-`--review`) Phase 6 path. Referenced from `SKILL.md` Phase 6 → "Default path (no `--review`): inline deterministic validation". + +Write the following Node.js script to `$PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.cjs`: + +```javascript +#!/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: +```bash +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. diff --git a/understand-anything-plugin/skills/understand/recipes/intermediate-cleanup.md b/understand-anything-plugin/skills/understand/recipes/intermediate-cleanup.md new file mode 100644 index 00000000..6bb25401 --- /dev/null +++ b/understand-anything-plugin/skills/understand/recipes/intermediate-cleanup.md @@ -0,0 +1,17 @@ +# Recipe: Phase 7 Intermediate Cleanup + +> Cleans up `.understand-anything/intermediate/` while preserving `scan-result.json`. Referenced from `SKILL.md` Phase 7 step 4. + +Clean up intermediate files, **preserving `scan-result.json`** so future incremental runs can skip Phase 1 SCAN (see issue #293): + +```bash +# 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. +INTER="$PROJECT_ROOT/.understand-anything/intermediate" +if [ -d "$INTER" ]; then + find "$INTER" -mindepth 1 -maxdepth 1 -not -name 'scan-result.json' -exec rm -rf {} + +fi +rm -rf $PROJECT_ROOT/.understand-anything/tmp +``` diff --git a/understand-anything-plugin/skills/understand/recipes/llm-review-path.md b/understand-anything-plugin/skills/understand/recipes/llm-review-path.md new file mode 100644 index 00000000..cba6f818 --- /dev/null +++ b/understand-anything-plugin/skills/understand/recipes/llm-review-path.md @@ -0,0 +1,26 @@ +# Recipe: Phase 6 `--review` Path (LLM Graph Reviewer) + +> The alternative Phase 6 validation branch when `--review` is in `$ARGUMENTS`. Dispatches the LLM `graph-reviewer` subagent instead of the inline deterministic validator. Referenced from `SKILL.md` Phase 6 → "`--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 whose `filePath` doesn'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_ROOT` +> Read the file and validate it for completeness and correctness. +> Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/review.json` diff --git a/understand-anything-plugin/skills/understand/recipes/plugin-root-resolution.md b/understand-anything-plugin/skills/understand/recipes/plugin-root-resolution.md new file mode 100644 index 00000000..dc215ac6 --- /dev/null +++ b/understand-anything-plugin/skills/understand/recipes/plugin-root-resolution.md @@ -0,0 +1,51 @@ +# Recipe: Plugin Root Resolution and Build Check + +> Resolves the `understand-anything` plugin root across install locations, then ensures `@understand-anything/core` is built. Referenced from `SKILL.md` Phase 0 step 1.5 (Ensure the plugin is built). + +**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: + +```bash +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:-}" + echo " - $HOME/.understand-anything-plugin" + echo " - ${SELF_RELATIVE:-}" + echo " - ${COPILOT_SELF_RELATIVE:-}" + 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 +fi +``` + +If `pnpm` is missing, report to the user: "Install Node.js ≥ 22 and pnpm ≥ 10, then re-run `/understand`." diff --git a/understand-anything-plugin/skills/understand/recipes/understandignore-generator.md b/understand-anything-plugin/skills/understand/recipes/understandignore-generator.md new file mode 100644 index 00000000..d5670af7 --- /dev/null +++ b/understand-anything-plugin/skills/understand/recipes/understandignore-generator.md @@ -0,0 +1,30 @@ +# Recipe: Generate `.understandignore` Starter File + +> One-shot Node.js script that synthesizes a starter `.understandignore` from `.gitignore` and detected directories. Referenced from `SKILL.md` Phase 0.5 step 2 (If `.understandignore` does not exist, generate a starter file). + +Run the following Node.js one-liner in `$PROJECT_ROOT` (reads `.gitignore` and deduplicates against built-in defaults): + +```bash +node -e " +const fs = require('fs'); +const path = require('path'); +const root = process.cwd(); +const defaults = ['node_modules/','node_modules','.git/','vendor/','venv/','.venv/','__pycache__/','dist/','dist','build/','build','out/','coverage/','coverage','.next/','.cache/','.turbo/','target/','obj/','*.lock','package-lock.json','yarn.lock','pnpm-lock.yaml','*.png','*.jpg','*.jpeg','*.gif','*.svg','*.ico','*.woff','*.woff2','*.ttf','*.eot','*.mp3','*.mp4','*.pdf','*.zip','*.tar','*.gz','*.min.js','*.min.css','*.map','*.generated.*','.idea/','.vscode/','LICENSE','.gitignore','.editorconfig','.prettierrc','.eslintrc*','*.log']; +const norm = p => p.replace(/\/+$/, ''); +const defaultSet = new Set(defaults.map(norm)); +const header = '# .understandignore — patterns for files/dirs to exclude from analysis\n# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)\n# Lines below are suggestions — uncomment to activate.\n# Use ! prefix to force-include something excluded by defaults.\n#\n# Built-in defaults (always excluded unless negated):\n# node_modules/, .git/, dist/, build/, obj/, *.lock, *.min.js, etc.\n#\n'; +let body = ''; +const gitignorePath = path.join(root, '.gitignore'); +if (fs.existsSync(gitignorePath)) { + const gi = fs.readFileSync(gitignorePath, 'utf-8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).filter(p => !defaultSet.has(norm(p))); + if (gi.length) { body += '# --- From .gitignore (uncomment to exclude) ---\n\n' + gi.map(p => '# ' + p).join('\n') + '\n\n'; } +} +const dirs = ['__tests__','test','tests','fixtures','testdata','docs','examples','scripts','migrations','.storybook']; +const found = dirs.filter(d => fs.existsSync(path.join(root, d))); +if (found.length) { body += '# --- Detected directories (uncomment to exclude) ---\n\n' + found.map(d => '# ' + d + '/').join('\n') + '\n\n'; } +body += '# --- Test file patterns (uncomment to exclude) ---\n\n# *.test.*\n# *.spec.*\n# *.snap\n'; +const outDir = path.join(root, '.understand-anything'); +if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); +fs.writeFileSync(path.join(outDir, '.understandignore'), header + body); +" +``` diff --git a/understand-anything-plugin/skills/understand/recipes/worktree-redirect.md b/understand-anything-plugin/skills/understand/recipes/worktree-redirect.md new file mode 100644 index 00000000..67bbcb1e --- /dev/null +++ b/understand-anything-plugin/skills/understand/recipes/worktree-redirect.md @@ -0,0 +1,25 @@ +# Recipe: Worktree Redirect + +> Detects whether `PROJECT_ROOT` is a git worktree and redirects output to the main repo root. Referenced from `SKILL.md` Phase 0 step 1 (Resolve `PROJECT_ROOT`). + +If `PROJECT_ROOT` is 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 comparing `git rev-parse --git-dir` against `git 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-dir` is the main repo root. + +```bash +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 +fi +``` + +Set `UNDERSTAND_NO_WORKTREE_REDIRECT=1` if you intentionally want a per-worktree graph (rare — most users want the redirect). diff --git a/understand-anything-plugin/skills/understand/reference/knowledge-graph-schema.md b/understand-anything-plugin/skills/understand/reference/knowledge-graph-schema.md new file mode 100644 index 00000000..7fafb79f --- /dev/null +++ b/understand-anything-plugin/skills/understand/reference/knowledge-graph-schema.md @@ -0,0 +1,45 @@ +# Reference: KnowledgeGraph Schema + +> Authoritative node-type, edge-type, and edge-weight tables for the assembled `knowledge-graph.json`. Referenced from `SKILL.md` "Reference: KnowledgeGraph Schema". + +## Node Types (13 total) + +| Type | Description | ID Convention | +|---|---|---| +| `file` | Source code file | `file:` | +| `function` | Function or method | `function::` | +| `class` | Class, interface, or type | `class::` | +| `module` | Logical module or package | `module:` | +| `concept` | Abstract concept or pattern | `concept:` | +| `config` | Configuration file (YAML, JSON, TOML, env) | `config:` | +| `document` | Documentation file (Markdown, RST, TXT) | `document:` | +| `service` | Deployable service definition (Dockerfile, K8s) | `service:` | +| `table` | Database table or migration | `table::` | +| `endpoint` | API endpoint or route definition | `endpoint::` | +| `pipeline` | CI/CD pipeline configuration | `pipeline:` | +| `schema` | Schema definition (GraphQL, Protobuf, Prisma) | `schema:` | +| `resource` | Infrastructure resource (Terraform, CloudFormation) | `resource:` | + +## 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) | diff --git a/understand-anything-plugin/skills/understand/reference/output-shapes.md b/understand-anything-plugin/skills/understand/reference/output-shapes.md new file mode 100644 index 00000000..98de1956 --- /dev/null +++ b/understand-anything-plugin/skills/understand/reference/output-shapes.md @@ -0,0 +1,61 @@ +# Reference: Phase Output Shapes + +> Canonical JSON shapes for `layers[*]`, `tour[*]`, and the assembled `KnowledgeGraph` object that the workflow normalizes to. Referenced from `SKILL.md` Phase 4, Phase 5, and Phase 6. + +## `layers[*]` shape (Phase 4) + +Each element of the final `layers` array MUST have this shape. All four fields (`id`, `name`, `description`, `nodeIds`) are required. + +```json +[ + { + "id": "layer:", + "name": "", + "description": "", + "nodeIds": ["file:src/App.tsx", "config:tsconfig.json", "document:README.md"] + } +] +``` + +## `tour[*]` shape (Phase 5) + +Each element of the final `tour` array MUST have this shape. Required fields: `order`, `title`, `description`, `nodeIds`. Preserve optional `languageLesson` when present. + +```json +[ + { + "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"] + } +] +``` + +## Assembled `KnowledgeGraph` shape (Phase 6) + +The full top-level shape written to `knowledge-graph.json`: + +```json +{ + "version": "1.0.0", + "project": { + "name": "", + "languages": [""], + "frameworks": [""], + "description": "", + "analyzedAt": "", + "gitCommitHash": "" + }, + "nodes": [], + "edges": [], + "layers": [], + "tour": [] +} +``` diff --git a/understand-anything-plugin/src/__tests__/worktree-redirect.test.mjs b/understand-anything-plugin/src/__tests__/worktree-redirect.test.mjs index f0ffcf31..71521bbf 100644 --- a/understand-anything-plugin/src/__tests__/worktree-redirect.test.mjs +++ b/understand-anything-plugin/src/__tests__/worktree-redirect.test.mjs @@ -1,8 +1,8 @@ // Validates the worktree-redirect bash snippet embedded in -// `skills/understand/SKILL.md` Phase 0 step 1 and -// `skills/understand-domain/SKILL.md` Phase 0. +// `skills/understand/recipes/worktree-redirect.md` (referenced from `skills/understand/SKILL.md` +// Phase 0 step 1) and `skills/understand-domain/SKILL.md` Phase 0. // -// If you edit the snippet in either SKILL.md, mirror the change to RESOLVE_SNIPPET +// If you edit the snippet in either location, mirror the change to RESOLVE_SNIPPET // below — there is no shared script to source (per-skill convention in this repo). import { describe, it, expect, beforeAll, afterAll } from "vitest";