@nomos-arc/arc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +10 -0
- package/.nomos-config.json +5 -0
- package/CLAUDE.md +108 -0
- package/LICENSE +190 -0
- package/README.md +569 -0
- package/dist/cli.js +21120 -0
- package/docs/auth/googel_plan.yaml +1093 -0
- package/docs/auth/google_task.md +235 -0
- package/docs/auth/hardened_blueprint.yaml +1658 -0
- package/docs/auth/red_team_report.yaml +336 -0
- package/docs/auth/session_state.yaml +162 -0
- package/docs/certificate/cer_enhance_plan.md +605 -0
- package/docs/certificate/certificate_report.md +338 -0
- package/docs/dev_overview.md +419 -0
- package/docs/feature_assessment.md +156 -0
- package/docs/how_it_works.md +78 -0
- package/docs/infrastructure/map.md +867 -0
- package/docs/init/master_plan.md +3581 -0
- package/docs/init/red_team_report.md +215 -0
- package/docs/init/report_phase_1a.md +304 -0
- package/docs/integrity-gate/enhance_drift.md +703 -0
- package/docs/integrity-gate/overview.md +108 -0
- package/docs/management/manger-task.md +99 -0
- package/docs/management/scafffold.md +76 -0
- package/docs/map/ATOMIC_BLUEPRINT.md +1349 -0
- package/docs/map/RED_TEAM_REPORT.md +159 -0
- package/docs/map/map_task.md +147 -0
- package/docs/map/semantic_graph_task.md +792 -0
- package/docs/map/semantic_master_plan.md +705 -0
- package/docs/phase7/TEAM_RED.md +249 -0
- package/docs/phase7/plan.md +1682 -0
- package/docs/phase7/task.md +275 -0
- package/docs/prompts/USAGE.md +312 -0
- package/docs/prompts/architect.md +165 -0
- package/docs/prompts/executer.md +190 -0
- package/docs/prompts/hardener.md +190 -0
- package/docs/prompts/red_team.md +146 -0
- package/docs/verification/goveranance-overview.md +396 -0
- package/docs/verification/governance-overview.md +245 -0
- package/docs/verification/verification-arc-ar.md +560 -0
- package/docs/verification/verification-architecture.md +560 -0
- package/docs/very_next.md +52 -0
- package/docs/whitepaper.md +89 -0
- package/overview.md +1469 -0
- package/package.json +63 -0
- package/src/adapters/__tests__/git.test.ts +296 -0
- package/src/adapters/__tests__/stdio.test.ts +70 -0
- package/src/adapters/git.ts +226 -0
- package/src/adapters/pty.ts +159 -0
- package/src/adapters/stdio.ts +113 -0
- package/src/cli.ts +83 -0
- package/src/commands/apply.ts +47 -0
- package/src/commands/auth.ts +301 -0
- package/src/commands/certificate.ts +89 -0
- package/src/commands/discard.ts +24 -0
- package/src/commands/drift.ts +116 -0
- package/src/commands/index.ts +78 -0
- package/src/commands/init.ts +121 -0
- package/src/commands/list.ts +75 -0
- package/src/commands/map.ts +55 -0
- package/src/commands/plan.ts +30 -0
- package/src/commands/review.ts +58 -0
- package/src/commands/run.ts +63 -0
- package/src/commands/search.ts +147 -0
- package/src/commands/show.ts +63 -0
- package/src/commands/status.ts +59 -0
- package/src/core/__tests__/budget.test.ts +213 -0
- package/src/core/__tests__/certificate.test.ts +385 -0
- package/src/core/__tests__/config.test.ts +191 -0
- package/src/core/__tests__/preflight.test.ts +24 -0
- package/src/core/__tests__/prompt.test.ts +358 -0
- package/src/core/__tests__/review.test.ts +161 -0
- package/src/core/__tests__/state.test.ts +362 -0
- package/src/core/auth/__tests__/manager.test.ts +166 -0
- package/src/core/auth/__tests__/server.test.ts +220 -0
- package/src/core/auth/gcp-projects.ts +160 -0
- package/src/core/auth/manager.ts +114 -0
- package/src/core/auth/server.ts +141 -0
- package/src/core/budget.ts +119 -0
- package/src/core/certificate.ts +502 -0
- package/src/core/config.ts +212 -0
- package/src/core/errors.ts +54 -0
- package/src/core/factory.ts +49 -0
- package/src/core/graph/__tests__/builder.test.ts +272 -0
- package/src/core/graph/__tests__/contract-writer.test.ts +175 -0
- package/src/core/graph/__tests__/enricher.test.ts +299 -0
- package/src/core/graph/__tests__/parser.test.ts +200 -0
- package/src/core/graph/__tests__/pipeline.test.ts +202 -0
- package/src/core/graph/__tests__/renderer.test.ts +128 -0
- package/src/core/graph/__tests__/resolver.test.ts +185 -0
- package/src/core/graph/__tests__/scanner.test.ts +231 -0
- package/src/core/graph/__tests__/show.test.ts +134 -0
- package/src/core/graph/builder.ts +303 -0
- package/src/core/graph/constraints.ts +94 -0
- package/src/core/graph/contract-writer.ts +93 -0
- package/src/core/graph/drift/__tests__/classifier.test.ts +215 -0
- package/src/core/graph/drift/__tests__/comparator.test.ts +335 -0
- package/src/core/graph/drift/__tests__/drift.test.ts +453 -0
- package/src/core/graph/drift/__tests__/reporter.test.ts +203 -0
- package/src/core/graph/drift/classifier.ts +165 -0
- package/src/core/graph/drift/comparator.ts +205 -0
- package/src/core/graph/drift/reporter.ts +77 -0
- package/src/core/graph/enricher.ts +251 -0
- package/src/core/graph/grammar-paths.ts +30 -0
- package/src/core/graph/html-template.ts +493 -0
- package/src/core/graph/map-schema.ts +137 -0
- package/src/core/graph/parser.ts +336 -0
- package/src/core/graph/pipeline.ts +209 -0
- package/src/core/graph/renderer.ts +92 -0
- package/src/core/graph/resolver.ts +195 -0
- package/src/core/graph/scanner.ts +145 -0
- package/src/core/logger.ts +46 -0
- package/src/core/orchestrator.ts +792 -0
- package/src/core/plan-file-manager.ts +66 -0
- package/src/core/preflight.ts +64 -0
- package/src/core/prompt.ts +173 -0
- package/src/core/review.ts +95 -0
- package/src/core/state.ts +294 -0
- package/src/core/worktree-coordinator.ts +77 -0
- package/src/search/__tests__/chunk-extractor.test.ts +339 -0
- package/src/search/__tests__/embedder-auth.test.ts +124 -0
- package/src/search/__tests__/embedder.test.ts +267 -0
- package/src/search/__tests__/graph-enricher.test.ts +178 -0
- package/src/search/__tests__/indexer.test.ts +518 -0
- package/src/search/__tests__/integration.test.ts +649 -0
- package/src/search/__tests__/query-engine.test.ts +334 -0
- package/src/search/__tests__/similarity.test.ts +78 -0
- package/src/search/__tests__/vector-store.test.ts +281 -0
- package/src/search/chunk-extractor.ts +167 -0
- package/src/search/embedder.ts +209 -0
- package/src/search/graph-enricher.ts +95 -0
- package/src/search/indexer.ts +483 -0
- package/src/search/lexical-searcher.ts +190 -0
- package/src/search/query-engine.ts +225 -0
- package/src/search/vector-store.ts +311 -0
- package/src/types/index.ts +572 -0
- package/src/utils/__tests__/ansi.test.ts +54 -0
- package/src/utils/__tests__/frontmatter.test.ts +79 -0
- package/src/utils/__tests__/sanitize.test.ts +229 -0
- package/src/utils/ansi.ts +19 -0
- package/src/utils/context.ts +44 -0
- package/src/utils/frontmatter.ts +27 -0
- package/src/utils/sanitize.ts +78 -0
- package/test/e2e/lifecycle.test.ts +330 -0
- package/test/fixtures/mock-planner-hang.ts +5 -0
- package/test/fixtures/mock-planner.ts +26 -0
- package/test/fixtures/mock-reviewer-bad.ts +8 -0
- package/test/fixtures/mock-reviewer-retry.ts +34 -0
- package/test/fixtures/mock-reviewer.ts +18 -0
- package/test/fixtures/sample-project/src/circular-a.ts +6 -0
- package/test/fixtures/sample-project/src/circular-b.ts +6 -0
- package/test/fixtures/sample-project/src/config.ts +15 -0
- package/test/fixtures/sample-project/src/main.ts +19 -0
- package/test/fixtures/sample-project/src/services/product-service.ts +20 -0
- package/test/fixtures/sample-project/src/services/user-service.ts +18 -0
- package/test/fixtures/sample-project/src/types.ts +14 -0
- package/test/fixtures/sample-project/src/utils/index.ts +14 -0
- package/test/fixtures/sample-project/src/utils/validate.ts +12 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
# Atomic Remediation Blueprint — Semantic Graph (`arc map` / `arc show map`)
|
|
2
|
+
|
|
3
|
+
**Source Plan:** `docs/map/semantic_master_plan.md`
|
|
4
|
+
**Red Team Audit:** `docs/map/RED_TEAM_REPORT.md`
|
|
5
|
+
**Status:** Re-engineered. All 4 blockers neutralized, 8 ambiguities resolved, 6 resilience gaps closed.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Executive Summary of Fixes
|
|
10
|
+
|
|
11
|
+
| Red Team Finding | Resolution | Affected Steps |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **BLK-1**: tree-sitter native binary + esbuild = MODULE_NOT_FOUND | Switch to `web-tree-sitter` (WASM). Eliminates native `.node` binding entirely. WASM bundles cleanly with esbuild or loads from `node_modules` without path issues. Post-build verification step added. | 0.2, 2.1, 2.3, Final Check |
|
|
14
|
+
| **BLK-2**: No file-level locking on `project_map.json` | Use `proper-lockfile` on every read-modify-write cycle of `project_map.json`. Same package already used in `src/core/state.ts`. | 5.1j |
|
|
15
|
+
| **BLK-3**: `GraphBuilder.build()` mutates without clearing stale data | `build()` now resets `dependencies = []`, `dependents = []`, `depth = 0` on every `FileNode` before computing the graph. Explicit step added. | 3.2 |
|
|
16
|
+
| **BLK-4/AMB-1**: Kahn's algorithm direction misleading | Clarified: in-degree = `dependents.length` (files that import this node). Depth 0 = nobody imports this file. Direction documented inline in code and plan. | 3.2 |
|
|
17
|
+
| **AMB-2**: SemanticEnricher content source unspecified | Enricher reads file content from disk via `fs/promises.readFile()`. Content is NOT carried from `ScanResult`. Explicit re-read per file. | 4.1 |
|
|
18
|
+
| **AMB-3**: tree-sitter-typescript ESM vs CJS | Eliminated by switching to `web-tree-sitter` with `.wasm` grammar files. Uses `await Parser.init()` + `Language.load(wasmPath)`. No CJS/ESM ambiguity. | 2.1 |
|
|
19
|
+
| **AMB-4**: `ImportResolver` uses sync `fs.existsSync` | `resolve()` is now `async`. Uses `fs.access()` with a `Set<string>` cache populated from the known-files set. Zero sync filesystem calls. | 3.1 |
|
|
20
|
+
| **AMB-5**: ContractWriter assumes parent directory exists | Added `fs.mkdir(dir, { recursive: true })` before every `.semantic.md` write. Handles all edge cases. | 4.2 |
|
|
21
|
+
| **AMB-6**: Exit code 2 semantics | Changed partial-success exit code from `2` to `10`. Avoids Unix/Bash convention collision. | 5.2 |
|
|
22
|
+
| **AMB-7**: `readArchitecturalConstraints` path resolution | All `contextFiles` are normalized to project-root-relative paths using `path.relative(projectRoot, path.resolve(projectRoot, contextFile))` before map lookup. | 5.4b |
|
|
23
|
+
| **AMB-8**: HTML template JSON escaping — XSS surface | `JSON.stringify()` output is post-processed: replace `</` with `<\/` and `${` with `\\${`. Prevents `</script>` injection and template literal breakout. | 6.2a |
|
|
24
|
+
| **GAP-1**: No timeout on tree-sitter parse | Files exceeding 500KB are skipped with a warning before parsing. `web-tree-sitter` WASM parsing is inherently sandboxed but the size guard prevents memory exhaustion. | 2.1 |
|
|
25
|
+
| **GAP-2**: AI failure loses all scan/parse work | Map is written to disk with `semantic: null` BEFORE AI enrichment begins. AI enrichment then updates the map in a second write. Crash-safe. | 5.1 |
|
|
26
|
+
| **GAP-3**: No SIGINT/SIGTERM handler | Pipeline registers a `process.on('SIGINT')` handler that sets a cancellation flag. Enrichment loop checks the flag between batches and writes partial map on abort. | 5.1 |
|
|
27
|
+
| **GAP-4**: CDN dependency for visualization — offline failure | Cytoscape.js is bundled inline (~500KB minified) via a build-time fetch + embed. Fallback: `<noscript>` tag with a text-based file list. | 6.2a |
|
|
28
|
+
| **GAP-5**: `migrateProjectMap()` no forward compatibility | Added version ceiling check: if `schema_version > CURRENT_SCHEMA_VERSION`, throw `NomosError('graph_parse_error', 'Map was created by a newer version of arc. Please upgrade.')`. | 1.3 |
|
|
29
|
+
| **GAP-6**: Circular dependency — incomplete cycle reporting | Added Tarjan's SCC post-Kahn's to extract distinct cycles. Each cycle is logged with its full path. Nodes are grouped by SCC for accurate depth assignment. | 3.2 |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Pre-Flight Checklist
|
|
34
|
+
|
|
35
|
+
Every check must pass before execution begins. Failure on any check = STOP.
|
|
36
|
+
|
|
37
|
+
| # | Check | Command | Expected |
|
|
38
|
+
|---|-------|---------|----------|
|
|
39
|
+
| PF-1 | Node.js >= 20 | `node --version` | Output starts with `v20` or higher |
|
|
40
|
+
| PF-2 | Project root valid | `ls package.json src/cli.ts` | Both exist, exit code 0 |
|
|
41
|
+
| PF-3 | Clean dependency install | `npm install` | Exit code 0 |
|
|
42
|
+
| PF-4 | Existing tests pass | `npx vitest run` | 0 failures |
|
|
43
|
+
| PF-5 | `src/core/graph/` does not exist | `ls src/core/graph/ 2>&1` | "No such file or directory" |
|
|
44
|
+
| PF-6 | `proper-lockfile` already installed | `grep 'proper-lockfile' package.json` | Match found (used in `src/core/state.ts`) |
|
|
45
|
+
| PF-7 | Git working tree clean | `git status --porcelain` | Empty output (or acknowledged untracked) |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. Atomic Execution Sequence
|
|
50
|
+
|
|
51
|
+
### Phase 0: Prerequisites — Dependency & Configuration Setup
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
#### Step 0.1: Install runtime dependencies
|
|
56
|
+
|
|
57
|
+
**Pre-Condition:** PF-1 through PF-7 pass. `package.json` exists at project root.
|
|
58
|
+
|
|
59
|
+
**Action:**
|
|
60
|
+
```bash
|
|
61
|
+
npm install fast-glob gitignore-parser p-limit open cytoscape
|
|
62
|
+
```
|
|
63
|
+
Note: `cytoscape` is a runtime dependency used by `html-template.ts` to read `cytoscape.min.js` from `node_modules` and embed it inline in the generated HTML (see WATCH-2 in Step 6.2a). It is NOT bundled by esbuild.
|
|
64
|
+
|
|
65
|
+
**Validation:**
|
|
66
|
+
```bash
|
|
67
|
+
node -e "import('fast-glob').then(()=>console.log('ok'))" # → "ok"
|
|
68
|
+
node -e "import('p-limit').then(()=>console.log('ok'))" # → "ok"
|
|
69
|
+
grep '"fast-glob"' package.json # → match in dependencies
|
|
70
|
+
grep '"open"' package.json # → match in dependencies
|
|
71
|
+
grep '"cytoscape"' package.json # → match in dependencies (WATCH-2)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Rollback:** `npm uninstall fast-glob gitignore-parser p-limit open`
|
|
75
|
+
|
|
76
|
+
**Idempotency:** `npm install` is idempotent — re-running with already-installed packages is a no-op.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
#### Step 0.2: Install parsing & AI dependencies + update esbuild externals
|
|
81
|
+
|
|
82
|
+
**Pre-Condition:** Step 0.1 complete. `package.json` contains `fast-glob` in dependencies.
|
|
83
|
+
|
|
84
|
+
**Action:**
|
|
85
|
+
1. Install WASM-based parser (NOT native `tree-sitter`):
|
|
86
|
+
```bash
|
|
87
|
+
npm install web-tree-sitter @google/generative-ai
|
|
88
|
+
```
|
|
89
|
+
2. **[WATCH-1 PROTOCOL] WASM Grammar Acquisition — Deterministic 3-Tier Fallback:**
|
|
90
|
+
|
|
91
|
+
WASM grammar files are NOT shipped with `web-tree-sitter` itself. They must be obtained separately. The following tiers are tried **in order** — stop at the first that succeeds:
|
|
92
|
+
|
|
93
|
+
**Tier A — `tree-sitter-wasms` npm package (preferred):**
|
|
94
|
+
```bash
|
|
95
|
+
npm install tree-sitter-wasms
|
|
96
|
+
```
|
|
97
|
+
Verify the exact files exist:
|
|
98
|
+
```bash
|
|
99
|
+
ls node_modules/tree-sitter-wasms/out/tree-sitter-typescript.wasm # → exists
|
|
100
|
+
ls node_modules/tree-sitter-wasms/out/tree-sitter-tsx.wasm # → exists
|
|
101
|
+
```
|
|
102
|
+
If BOTH files exist → Tier A succeeds. Set grammar resolution paths:
|
|
103
|
+
```typescript
|
|
104
|
+
// In parser.ts — grammar paths resolved at runtime from node_modules
|
|
105
|
+
const GRAMMAR_BASE = path.join(
|
|
106
|
+
path.dirname(require.resolve('tree-sitter-wasms/package.json')),
|
|
107
|
+
'out'
|
|
108
|
+
);
|
|
109
|
+
const TS_WASM = path.join(GRAMMAR_BASE, 'tree-sitter-typescript.wasm');
|
|
110
|
+
const TSX_WASM = path.join(GRAMMAR_BASE, 'tree-sitter-tsx.wasm');
|
|
111
|
+
```
|
|
112
|
+
Note: `require.resolve` works in both ESM (via `createRequire`) and CJS. This avoids fragile relative paths and survives hoisted `node_modules`.
|
|
113
|
+
|
|
114
|
+
**Tier B — Official tree-sitter GitHub Release artifacts:**
|
|
115
|
+
If `tree-sitter-wasms` does not provide the files (package deprecated, renamed, or missing grammars):
|
|
116
|
+
```bash
|
|
117
|
+
mkdir -p src/core/graph/grammars
|
|
118
|
+
# Download pinned versions from tree-sitter-typescript releases
|
|
119
|
+
curl -L -o src/core/graph/grammars/tree-sitter-typescript.wasm \
|
|
120
|
+
"https://github.com/nicolo-ribaudo/tree-sitter-typescript/releases/download/v0.23.2/tree-sitter-typescript.wasm"
|
|
121
|
+
curl -L -o src/core/graph/grammars/tree-sitter-tsx.wasm \
|
|
122
|
+
"https://github.com/nicolo-ribaudo/tree-sitter-typescript/releases/download/v0.23.2/tree-sitter-tsx.wasm"
|
|
123
|
+
```
|
|
124
|
+
Verify:
|
|
125
|
+
```bash
|
|
126
|
+
file src/core/graph/grammars/tree-sitter-typescript.wasm # → "WebAssembly (wasm) binary module"
|
|
127
|
+
file src/core/graph/grammars/tree-sitter-tsx.wasm # → "WebAssembly (wasm) binary module"
|
|
128
|
+
```
|
|
129
|
+
Grammar paths in `parser.ts`:
|
|
130
|
+
```typescript
|
|
131
|
+
const GRAMMAR_BASE = path.join(path.dirname(fileURLToPath(import.meta.url)), 'grammars');
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Tier C — Build from source (last resort):**
|
|
135
|
+
```bash
|
|
136
|
+
npm install -g tree-sitter-cli
|
|
137
|
+
git clone --depth 1 https://github.com/nicolo-ribaudo/tree-sitter-typescript.git /tmp/ts-grammar
|
|
138
|
+
cd /tmp/ts-grammar/typescript && tree-sitter build --wasm
|
|
139
|
+
cd /tmp/ts-grammar/tsx && tree-sitter build --wasm
|
|
140
|
+
cp /tmp/ts-grammar/typescript/tree-sitter-typescript.wasm src/core/graph/grammars/
|
|
141
|
+
cp /tmp/ts-grammar/tsx/tree-sitter-tsx.wasm src/core/graph/grammars/
|
|
142
|
+
rm -rf /tmp/ts-grammar
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**CRITICAL GATE — Grammar Load Smoke Test:**
|
|
146
|
+
Regardless of which tier succeeded, run this Node.js script to confirm WASM grammars actually load:
|
|
147
|
+
```bash
|
|
148
|
+
node --input-type=module -e "
|
|
149
|
+
import Parser from 'web-tree-sitter';
|
|
150
|
+
await Parser.init();
|
|
151
|
+
const p = new Parser();
|
|
152
|
+
// Adjust path based on which tier was used
|
|
153
|
+
const lang = await Parser.Language.load('<path-to-tree-sitter-typescript.wasm>');
|
|
154
|
+
p.setLanguage(lang);
|
|
155
|
+
const tree = p.parse('const x: number = 1;');
|
|
156
|
+
console.log(tree.rootNode.type === 'program' ? 'GRAMMAR_OK' : 'GRAMMAR_FAIL');
|
|
157
|
+
"
|
|
158
|
+
```
|
|
159
|
+
Expected output: `GRAMMAR_OK`. If output is `GRAMMAR_FAIL` or the script throws → STOP. Do not proceed to Phase 2.
|
|
160
|
+
|
|
161
|
+
3. In `package.json`, update the `"build"` script to append:
|
|
162
|
+
```
|
|
163
|
+
--external:@google/generative-ai --external:open
|
|
164
|
+
```
|
|
165
|
+
Note: `web-tree-sitter` is pure JS/WASM — it does NOT need `--external`. This eliminates BLK-1 entirely.
|
|
166
|
+
|
|
167
|
+
4. **Grammar path abstraction** — To insulate `parser.ts` from which tier was used, create a one-line config constant:
|
|
168
|
+
```typescript
|
|
169
|
+
// src/core/graph/grammar-paths.ts (new file, ~10 lines)
|
|
170
|
+
import { createRequire } from 'node:module';
|
|
171
|
+
import path from 'node:path';
|
|
172
|
+
import { fileURLToPath } from 'node:url';
|
|
173
|
+
import fs from 'node:fs';
|
|
174
|
+
|
|
175
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
176
|
+
const require = createRequire(import.meta.url);
|
|
177
|
+
|
|
178
|
+
function resolveGrammar(name: string): string {
|
|
179
|
+
// Tier A: try tree-sitter-wasms
|
|
180
|
+
try {
|
|
181
|
+
const wasmsBase = path.join(
|
|
182
|
+
path.dirname(require.resolve('tree-sitter-wasms/package.json')),
|
|
183
|
+
'out'
|
|
184
|
+
);
|
|
185
|
+
const wasmPath = path.join(wasmsBase, name);
|
|
186
|
+
if (fs.existsSync(wasmPath)) return wasmPath;
|
|
187
|
+
} catch { /* package not installed */ }
|
|
188
|
+
|
|
189
|
+
// Tier B/C: local grammars directory
|
|
190
|
+
const localPath = path.join(__dirname, 'grammars', name);
|
|
191
|
+
if (fs.existsSync(localPath)) return localPath;
|
|
192
|
+
|
|
193
|
+
throw new Error(
|
|
194
|
+
`WASM grammar "${name}" not found. Run Step 0.2 grammar acquisition protocol.`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const TS_WASM_PATH = resolveGrammar('tree-sitter-typescript.wasm');
|
|
199
|
+
export const TSX_WASM_PATH = resolveGrammar('tree-sitter-tsx.wasm');
|
|
200
|
+
```
|
|
201
|
+
`parser.ts` imports `{ TS_WASM_PATH, TSX_WASM_PATH }` from `./grammar-paths.js` — single source of truth.
|
|
202
|
+
|
|
203
|
+
**Validation:**
|
|
204
|
+
```bash
|
|
205
|
+
node -e "import('web-tree-sitter').then(()=>console.log('ok'))" # → "ok"
|
|
206
|
+
grep 'external:@google/generative-ai' package.json # → match
|
|
207
|
+
grep 'external:open' package.json # → match
|
|
208
|
+
npm run build # → exit code 0
|
|
209
|
+
node dist/cli.js --help # → shows help (BLK-1 post-build check)
|
|
210
|
+
# WATCH-1 CRITICAL GATE:
|
|
211
|
+
node --input-type=module -e "import Parser from 'web-tree-sitter'; await Parser.init(); console.log('WASM_INIT_OK')"
|
|
212
|
+
# → WASM_INIT_OK
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Rollback:** Revert `package.json` to git HEAD (`git checkout package.json`), then `npm install`. Remove `src/core/graph/grammars/` if created. `npm uninstall tree-sitter-wasms` if installed.
|
|
216
|
+
|
|
217
|
+
**Idempotency:** `npm install` is idempotent. Grammar file resolution is deterministic — `resolveGrammar()` checks existence before returning. Build script append is guarded by checking if flag already exists.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
#### Step 0.3: Add new `NomosErrorCode` values
|
|
222
|
+
|
|
223
|
+
**Pre-Condition:** `src/core/errors.ts` exists. Contains `NomosErrorCode` union type.
|
|
224
|
+
|
|
225
|
+
**Action:** Add four new error code literals to the `NomosErrorCode` union type, immediately after `'certificate_invalid'`:
|
|
226
|
+
```typescript
|
|
227
|
+
| 'graph_map_not_found'
|
|
228
|
+
| 'graph_parse_error'
|
|
229
|
+
| 'graph_ai_key_missing'
|
|
230
|
+
| 'graph_write_failed'
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Validation:**
|
|
234
|
+
```bash
|
|
235
|
+
grep 'graph_map_not_found' src/core/errors.ts # → exactly 1 match
|
|
236
|
+
grep 'graph_write_failed' src/core/errors.ts # → exactly 1 match
|
|
237
|
+
npx tsc --noEmit # → exit code 0
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Rollback:** `git checkout src/core/errors.ts`
|
|
241
|
+
|
|
242
|
+
**Idempotency:** Check if `'graph_map_not_found'` already exists before inserting. If present, skip.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
#### Step 0.4: Update `.gitignore`
|
|
247
|
+
|
|
248
|
+
**Pre-Condition:** `.gitignore` exists at project root.
|
|
249
|
+
|
|
250
|
+
**Action:** Append (if not already present):
|
|
251
|
+
```
|
|
252
|
+
*.semantic.md
|
|
253
|
+
tasks-management/graph/
|
|
254
|
+
.project-map.lock
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Validation:**
|
|
258
|
+
```bash
|
|
259
|
+
grep '*.semantic.md' .gitignore # → exactly 1 match
|
|
260
|
+
grep 'tasks-management/graph/' .gitignore # → exactly 1 match
|
|
261
|
+
grep '.project-map.lock' .gitignore # → exactly 1 match (WATCH-3: lockfile artifact)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Rollback:** Remove appended lines manually.
|
|
265
|
+
|
|
266
|
+
**Idempotency:** `grep` check before appending. If lines exist, skip.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### Phase 1: Foundation — Types, Schema, and File Discovery
|
|
271
|
+
|
|
272
|
+
**Gate:** Phase 0 complete (all 4 steps verified).
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
#### Step 1.1: Define all Semantic Graph types
|
|
277
|
+
|
|
278
|
+
**Pre-Condition:** `src/types/index.ts` exists and compiles.
|
|
279
|
+
|
|
280
|
+
**Action:** Append after a `// ─── Semantic Graph Types ───` section comment:
|
|
281
|
+
- `export interface SymbolEntry { name: string; kind: 'class' | 'function' | 'method' | 'interface' | 'type' | 'enum' | 'variable' | 'export'; line: number; end_line: number | null; signature: string | null; exported: boolean; }`
|
|
282
|
+
- `export interface ImportEntry { source: string; resolved: string | null; symbols: string[]; is_external: boolean; }`
|
|
283
|
+
- `export interface SemanticInfo { overview: string; purpose: string; key_logic: string[]; usage_context: string[]; source_hash: string; enriched_at: string; model: string; }`
|
|
284
|
+
- `export interface FileNode { file: string; hash: string; language: string; symbols: SymbolEntry[]; imports: ImportEntry[]; dependents: string[]; dependencies: string[]; depth: number; last_parsed_at: string | null; semantic: SemanticInfo | null; }`
|
|
285
|
+
- `export interface ProjectMap { schema_version: 1; generated_at: string; root: string; files: Record<string, FileNode>; stats: { total_files: number; total_symbols: number; total_edges: number; core_modules: string[]; }; }`
|
|
286
|
+
|
|
287
|
+
**Validation:**
|
|
288
|
+
```bash
|
|
289
|
+
npx tsc --noEmit # → exit code 0
|
|
290
|
+
grep 'export interface FileNode' src/types/index.ts # → exactly 1 match
|
|
291
|
+
grep 'export interface ProjectMap' src/types/index.ts # → exactly 1 match
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Rollback:** Remove appended block.
|
|
295
|
+
|
|
296
|
+
**Idempotency:** Check if `interface FileNode` exists before appending.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
#### Step 1.2: Add `graph` section to NomosConfig and GraphConfigSchema
|
|
301
|
+
|
|
302
|
+
**Pre-Condition:** Step 1.1 complete. `src/types/index.ts` has `NomosConfig` interface. `src/core/config.ts` has `NomosConfigSchema`.
|
|
303
|
+
|
|
304
|
+
**Action:**
|
|
305
|
+
1. In `src/types/index.ts`, add `graph` property to `NomosConfig`:
|
|
306
|
+
```typescript
|
|
307
|
+
graph: {
|
|
308
|
+
exclude_patterns: string[];
|
|
309
|
+
ai_enrichment: boolean;
|
|
310
|
+
ai_model: string;
|
|
311
|
+
ai_concurrency: number;
|
|
312
|
+
ai_requests_per_minute: number;
|
|
313
|
+
max_file_chars: number;
|
|
314
|
+
core_modules_count: number;
|
|
315
|
+
output_dir: string;
|
|
316
|
+
};
|
|
317
|
+
```
|
|
318
|
+
2. In `src/core/config.ts`, define `GraphConfigSchema`:
|
|
319
|
+
```typescript
|
|
320
|
+
const GraphConfigSchema = z.object({
|
|
321
|
+
exclude_patterns: z.array(z.string()).default(['node_modules', 'dist', '*.test.*', '*.spec.*', '*.semantic.md']),
|
|
322
|
+
ai_enrichment: z.boolean().default(true),
|
|
323
|
+
ai_model: z.string().default('gemini-1.5-flash'),
|
|
324
|
+
ai_concurrency: z.number().int().positive().default(5),
|
|
325
|
+
ai_requests_per_minute: z.number().int().positive().default(14),
|
|
326
|
+
max_file_chars: z.number().positive().default(4000),
|
|
327
|
+
core_modules_count: z.number().int().positive().default(10),
|
|
328
|
+
output_dir: z.string().default('tasks-management/graph'),
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
3. Add to `NomosConfigSchema`: `graph: GraphConfigSchema.default(() => GraphConfigSchema.parse({}))`
|
|
332
|
+
|
|
333
|
+
**Validation:**
|
|
334
|
+
```bash
|
|
335
|
+
npx tsc --noEmit # → exit code 0
|
|
336
|
+
grep 'GraphConfigSchema' src/core/config.ts # → at least 2 matches
|
|
337
|
+
npx vitest run # → all existing tests pass
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Rollback:** `git checkout src/types/index.ts src/core/config.ts`
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
#### Step 1.3: Implement `ProjectMapSchema`, `migrateProjectMap()`, and `readProjectMap()`
|
|
345
|
+
|
|
346
|
+
**Pre-Condition:** Step 1.1 complete. Types available.
|
|
347
|
+
|
|
348
|
+
**Target File:** `src/core/graph/map-schema.ts` (new)
|
|
349
|
+
|
|
350
|
+
**Action:** Create with:
|
|
351
|
+
1. Zod schemas for `SymbolEntrySchema`, `ImportEntrySchema`, `SemanticInfoSchema`, `FileNodeSchema`, `ProjectMapSchema`.
|
|
352
|
+
2. `CURRENT_SCHEMA_VERSION = 1` constant (exported).
|
|
353
|
+
3. `migrations: Record<number, (raw: unknown) => unknown>` — empty.
|
|
354
|
+
4. `migrateProjectMap(raw: unknown): ProjectMap`:
|
|
355
|
+
- **[GAP-5 FIX]** First check: `if (raw.schema_version > CURRENT_SCHEMA_VERSION) throw new NomosError('graph_parse_error', 'Map was created by a newer version of arc. Please upgrade.')`
|
|
356
|
+
- Then apply migrations from current version to `CURRENT_SCHEMA_VERSION`.
|
|
357
|
+
- Validate with `ProjectMapSchema.parse()`.
|
|
358
|
+
5. `readProjectMap(mapPath: string): Promise<ProjectMap | null>`:
|
|
359
|
+
- Read JSON from `mapPath` using `fs/promises`.
|
|
360
|
+
- Return `null` on `ENOENT`.
|
|
361
|
+
- Throw `NomosError('graph_parse_error', ...)` on parse/validation failure.
|
|
362
|
+
|
|
363
|
+
**Validation:**
|
|
364
|
+
```bash
|
|
365
|
+
npx tsc --noEmit # → exit code 0
|
|
366
|
+
grep 'export const CURRENT_SCHEMA_VERSION' src/core/graph/map-schema.ts # → 1 match
|
|
367
|
+
grep 'Please upgrade' src/core/graph/map-schema.ts # → 1 match (GAP-5)
|
|
368
|
+
grep 'export function migrateProjectMap' src/core/graph/map-schema.ts # → 1 match
|
|
369
|
+
grep 'export async function readProjectMap' src/core/graph/map-schema.ts # → 1 match
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Rollback:** `rm src/core/graph/map-schema.ts`
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
#### Step 1.4: Implement `FileScanner`
|
|
377
|
+
|
|
378
|
+
**Pre-Condition:** Step 0.1 complete (`fast-glob`, `gitignore-parser` installed). Step 1.1 complete (types defined).
|
|
379
|
+
|
|
380
|
+
**Target File:** `src/core/graph/scanner.ts` (new)
|
|
381
|
+
|
|
382
|
+
**Action:** Create `FileScanner` class:
|
|
383
|
+
1. Constructor: `(projectRoot: string, config: NomosConfig['graph'], logger: Logger)`
|
|
384
|
+
2. Module-level `LANGUAGE_MAP` constant (exported): `.ts`, `.tsx`, `.mts`, `.cts`, `.d.ts`, `.js`, `.mjs`, `.cjs`, `.jsx`, `.py`, `.go`, `.rs`
|
|
385
|
+
3. `async scan(existingMap: ProjectMap | null, force: boolean, globPatterns?: string[]): Promise<ScanResult>`
|
|
386
|
+
- `ScanResult = { files: Map<string, { file: string; hash: string; language: string; content: string }>; carried: Map<string, FileNode> }`
|
|
387
|
+
- Read `.gitignore` via `gitignore-parser`. If missing, use empty ignore list.
|
|
388
|
+
- `fast-glob` with patterns `**/*`, applying gitignore + `config.exclude_patterns` as `ignore`.
|
|
389
|
+
- For each file: check extension in `LANGUAGE_MAP` (skip unknown). Compute `sha256:<hex>` via `node:crypto`. On `EACCES`/`ENOENT`, log warning, skip.
|
|
390
|
+
- **[GAP-1 FIX]** Skip files exceeding 500KB (`stat.size > 512000`) with warning: `[nomos:graph:warn] Skipping {file} — exceeds 500KB size limit`.
|
|
391
|
+
- Incremental: if `existingMap` and `!force`, compare hash. Unchanged → `carried`. Changed → `files`.
|
|
392
|
+
|
|
393
|
+
**Validation:**
|
|
394
|
+
```bash
|
|
395
|
+
npx tsc --noEmit # → exit code 0
|
|
396
|
+
grep 'export class FileScanner' src/core/graph/scanner.ts # → 1 match
|
|
397
|
+
grep '512000' src/core/graph/scanner.ts # → 1 match (GAP-1 size guard)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Rollback:** `rm src/core/graph/scanner.ts`
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
#### Step 1.5: Test `FileScanner`
|
|
405
|
+
|
|
406
|
+
**Pre-Condition:** Step 1.4 complete.
|
|
407
|
+
|
|
408
|
+
**Target File:** `src/core/graph/__tests__/scanner.test.ts` (new)
|
|
409
|
+
|
|
410
|
+
**Action:** vitest tests covering:
|
|
411
|
+
1. Finds `.ts` and `.js` files in a tmp directory.
|
|
412
|
+
2. Computes correct SHA-256 hashes.
|
|
413
|
+
3. Detects correct language from extension.
|
|
414
|
+
4. Excludes `node_modules` directory.
|
|
415
|
+
5. Handles unreadable file gracefully (no throw).
|
|
416
|
+
6. Skips files with unknown extensions (`.png`).
|
|
417
|
+
7. Carries forward unchanged `FileNode` when `force=false`.
|
|
418
|
+
8. Re-scans all files when `force=true` even if hashes match.
|
|
419
|
+
9. **[GAP-1]** Skips files exceeding 500KB.
|
|
420
|
+
|
|
421
|
+
**Validation:** `npx vitest run src/core/graph/__tests__/scanner.test.ts` — all pass.
|
|
422
|
+
|
|
423
|
+
**Rollback:** Delete test file and re-evaluate Step 1.4.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
### Phase 2: AST Parsing & Symbol Extraction
|
|
428
|
+
|
|
429
|
+
**Gate:** Phase 1 complete (Steps 1.1–1.5 verified).
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
#### Step 2.1: Implement `ASTParser` — WASM-based grammar + symbol extraction
|
|
434
|
+
|
|
435
|
+
**Pre-Condition:** Step 0.2 complete (`web-tree-sitter` installed, WASM grammars available).
|
|
436
|
+
|
|
437
|
+
**Target File:** `src/core/graph/parser.ts` (new)
|
|
438
|
+
|
|
439
|
+
**Action:** Create `ASTParser` class:
|
|
440
|
+
1. **[BLK-1 FIX + WATCH-1]** Constructor uses `web-tree-sitter` (WASM), NOT native `tree-sitter`. Grammar paths are resolved via the `grammar-paths.ts` abstraction layer (created in Step 0.2):
|
|
441
|
+
```typescript
|
|
442
|
+
import Parser from 'web-tree-sitter';
|
|
443
|
+
import { TS_WASM_PATH, TSX_WASM_PATH } from './grammar-paths.js';
|
|
444
|
+
|
|
445
|
+
export class ASTParser {
|
|
446
|
+
private parser: Parser | null = null;
|
|
447
|
+
private tsLang: Parser.Language | null = null;
|
|
448
|
+
private tsxLang: Parser.Language | null = null;
|
|
449
|
+
|
|
450
|
+
async init(): Promise<void> {
|
|
451
|
+
await Parser.init();
|
|
452
|
+
this.parser = new Parser();
|
|
453
|
+
// Grammar paths resolved by grammar-paths.ts (Tier A: node_modules, Tier B/C: local)
|
|
454
|
+
this.tsLang = await Parser.Language.load(TS_WASM_PATH);
|
|
455
|
+
this.tsxLang = await Parser.Language.load(TSX_WASM_PATH);
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
No `__dirname` hacks, no conditional path resolution in parser code. `grammar-paths.ts` handles all tier fallback logic.
|
|
459
|
+
2. `parse(filePath: string, content: string, language: string): ParseResult` where `ParseResult = { symbols: SymbolEntry[]; imports: ImportEntry[] }`.
|
|
460
|
+
3. **[AMB-3 FIX]** Grammar selection: No CJS/ESM ambiguity. `web-tree-sitter` loads `.wasm` files uniformly:
|
|
461
|
+
- `.tsx` → `this.tsxLang`
|
|
462
|
+
- All other TS variants → `this.tsLang`
|
|
463
|
+
- JS files → `this.tsLang` (TypeScript grammar parses JS superset)
|
|
464
|
+
- Unsupported languages → return empty `{ symbols: [], imports: [] }` with debug log.
|
|
465
|
+
4. Error-node threshold: count `ERROR` node chars. If `errorChars / totalChars > 0.20`, log warning, return empty result.
|
|
466
|
+
5. Symbol extraction via tree walk (unchanged from original plan):
|
|
467
|
+
- `function_declaration`, `arrow_function` → `kind: 'function'`
|
|
468
|
+
- `class_declaration` → `kind: 'class'`; child `method_definition` → `kind: 'method'`, `name: 'ClassName.methodName'`
|
|
469
|
+
- `interface_declaration` → `kind: 'interface'`
|
|
470
|
+
- `type_alias_declaration` → `kind: 'type'`
|
|
471
|
+
- `enum_declaration` → `kind: 'enum'`
|
|
472
|
+
- Top-level `lexical_declaration` with `export` parent → `kind: 'variable'`
|
|
473
|
+
6. Line extraction: `line = node.startPosition.row + 1`, `end_line = node.endPosition.row + 1`.
|
|
474
|
+
7. Signature capture per original plan.
|
|
475
|
+
|
|
476
|
+
**Validation:**
|
|
477
|
+
```bash
|
|
478
|
+
npx tsc --noEmit # → exit code 0
|
|
479
|
+
grep 'web-tree-sitter' src/core/graph/parser.ts # → at least 1 match
|
|
480
|
+
grep 'export class ASTParser' src/core/graph/parser.ts # → 1 match
|
|
481
|
+
grep 'async init' src/core/graph/parser.ts # → 1 match (WASM requires async init)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Rollback:** `rm src/core/graph/parser.ts`
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
#### Step 2.2: Implement import extraction in `ASTParser`
|
|
489
|
+
|
|
490
|
+
**Pre-Condition:** Step 2.1 complete.
|
|
491
|
+
|
|
492
|
+
**Target File:** `src/core/graph/parser.ts` (modify)
|
|
493
|
+
|
|
494
|
+
**Action:** In the `parse()` tree walk, add:
|
|
495
|
+
1. `import_statement` → extract `source` from string literal, `symbols` from named specifiers. Default/namespace → empty `symbols`.
|
|
496
|
+
2. `call_expression` with callee `require` → extract `source` from first argument string literal.
|
|
497
|
+
3. Create `ImportEntry` with `resolved: null`, `is_external: false` (resolver corrects later).
|
|
498
|
+
|
|
499
|
+
**Validation:**
|
|
500
|
+
```bash
|
|
501
|
+
npx tsc --noEmit # → exit code 0
|
|
502
|
+
grep 'import_statement' src/core/graph/parser.ts # → at least 1 match
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**Rollback:** Revert import extraction changes only (keep Step 2.1).
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
#### Step 2.3: Test `ASTParser`
|
|
510
|
+
|
|
511
|
+
**Pre-Condition:** Steps 2.1 + 2.2 complete.
|
|
512
|
+
|
|
513
|
+
**Target File:** `src/core/graph/__tests__/parser.test.ts` (new)
|
|
514
|
+
|
|
515
|
+
**Action:** vitest tests covering:
|
|
516
|
+
1. Function declarations (name, line, end_line, exported, signature).
|
|
517
|
+
2. Class + method extraction (`ClassName.methodName`).
|
|
518
|
+
3. Interface, type, enum declarations.
|
|
519
|
+
4. `export const` variable declarations.
|
|
520
|
+
5. TSX grammar selection for `.tsx` extension (test with JSX content).
|
|
521
|
+
6. Error-node threshold: malformed TS (> 20% errors) → empty arrays.
|
|
522
|
+
7. Import statements with named symbols.
|
|
523
|
+
8. `require()` calls.
|
|
524
|
+
9. `resolved` is always `null` at this stage.
|
|
525
|
+
|
|
526
|
+
**Important:** `ASTParser.init()` must be called in `beforeAll` (WASM init is async).
|
|
527
|
+
|
|
528
|
+
**Validation:** `npx vitest run src/core/graph/__tests__/parser.test.ts` — all pass.
|
|
529
|
+
|
|
530
|
+
**Rollback:** Fix failing tests. If WASM loading fails, verify Step 0.2 grammar file paths.
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
### Phase 3: Import Resolution & Dependency Graph
|
|
535
|
+
|
|
536
|
+
**Gate:** Phase 2 complete (Steps 2.1–2.3 verified).
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
#### Step 3.1: Implement `ImportResolver` (async)
|
|
541
|
+
|
|
542
|
+
**Pre-Condition:** Phase 2 complete.
|
|
543
|
+
|
|
544
|
+
**Target File:** `src/core/graph/resolver.ts` (new)
|
|
545
|
+
|
|
546
|
+
**Action:** Create `ImportResolver` class:
|
|
547
|
+
1. Constructor: `(projectRoot: string, logger: Logger)` — reads `tsconfig.json` to extract `compilerOptions.paths`. Missing/no-paths → empty alias map.
|
|
548
|
+
2. **[AMB-4 FIX]** `async resolve(importSource: string, importerFile: string, knownFiles: Set<string>): Promise<ResolveResult>` where `ResolveResult = { resolved: string | null; is_external: boolean }`.
|
|
549
|
+
3. Resolution logic (in order):
|
|
550
|
+
a. **External check:** Does not start with `.`, `/`, or matched tsconfig alias → `{ resolved: null, is_external: true }`.
|
|
551
|
+
b. **Tsconfig alias:** Match against `compilerOptions.paths` keys (supporting `*` wildcard). Substitute capture into first value pattern. Continue to relative resolution.
|
|
552
|
+
c. **Relative resolution:** Compute absolute path from importer's directory. Try extensions: `.ts`, `.tsx`, `.js`. Then `/index.ts`, `/index.tsx`, `/index.js`. Check each against `knownFiles` set. First match → `{ resolved: match, is_external: false }`.
|
|
553
|
+
d. **Path traversal guard:** If resolved absolute path falls outside `projectRoot` → `{ resolved: null, is_external: true }`. Log warning.
|
|
554
|
+
e. **Unresolvable:** Log `[nomos:graph:warn] Cannot resolve import '{source}' from '{importer}'` → `{ resolved: null, is_external: false }`.
|
|
555
|
+
4. **[AMB-4 FIX + WATCH-4] `knownFiles` Set Contract:**
|
|
556
|
+
|
|
557
|
+
The `knownFiles: Set<string>` parameter replaces ALL filesystem calls. This is O(1) per lookup vs O(1) amortized with syscall overhead. **Critical ordering invariant:**
|
|
558
|
+
|
|
559
|
+
> `knownFiles` MUST be populated from BOTH `ScanResult.files` (newly scanned) AND `ScanResult.carried` (unchanged from previous run) BEFORE any `resolve()` call.
|
|
560
|
+
|
|
561
|
+
The Set is constructed in `MapPipeline.run()` (Step 5.1) as follows:
|
|
562
|
+
```typescript
|
|
563
|
+
// Step 5.1g — build knownFiles BEFORE resolution
|
|
564
|
+
const knownFiles = new Set<string>();
|
|
565
|
+
for (const key of parsedFiles.keys()) knownFiles.add(key); // newly parsed
|
|
566
|
+
for (const key of carriedFiles.keys()) knownFiles.add(key); // unchanged from prev run
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Why both sources matter:** If only `parsedFiles` is included, carried-forward files (unchanged since last run) become invisible to the resolver. Import `./utils/hash` from a changed file would fail to resolve to `src/utils/hash.ts` if that file was unchanged — a silent, incremental-only bug.
|
|
570
|
+
|
|
571
|
+
**The resolver NEVER calls `fs.existsSync`, `fs.access`, `fs.stat`, or any filesystem API.** All existence checks use `knownFiles.has(candidatePath)`. The `async` signature exists solely for interface consistency — the implementation is synchronous in practice.
|
|
572
|
+
|
|
573
|
+
**Validation:**
|
|
574
|
+
```bash
|
|
575
|
+
npx tsc --noEmit # → exit code 0
|
|
576
|
+
grep 'export class ImportResolver' src/core/graph/resolver.ts # → 1 match
|
|
577
|
+
grep 'async resolve' src/core/graph/resolver.ts # → 1 match (AMB-4)
|
|
578
|
+
grep -c 'existsSync' src/core/graph/resolver.ts # → 0 matches (AMB-4 verified)
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Rollback:** `rm src/core/graph/resolver.ts`
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
#### Step 3.2: Implement `GraphBuilder`
|
|
586
|
+
|
|
587
|
+
**Pre-Condition:** Step 3.1 complete.
|
|
588
|
+
|
|
589
|
+
**Target File:** `src/core/graph/builder.ts` (new)
|
|
590
|
+
|
|
591
|
+
**Action:** Create `GraphBuilder` class:
|
|
592
|
+
1. Constructor: `(config: NomosConfig['graph'], logger: Logger)`
|
|
593
|
+
2. `build(fileNodes: Map<string, FileNode>): { total_files: number; total_symbols: number; total_edges: number; core_modules: string[] }` — mutates `FileNode` objects in-place.
|
|
594
|
+
3. Logic:
|
|
595
|
+
a. **[BLK-3 FIX — MANDATORY]** Reset ALL graph-computed fields before building:
|
|
596
|
+
```typescript
|
|
597
|
+
for (const node of fileNodes.values()) {
|
|
598
|
+
node.dependencies = [];
|
|
599
|
+
node.dependents = [];
|
|
600
|
+
node.depth = 0;
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
This prevents stale data accumulation across incremental runs.
|
|
604
|
+
b. **Adjacency list:** For each file, iterate `imports[]`. For `resolved !== null && !is_external`: add `resolved` to file's `dependencies` Set; add file to target's `dependents` Set. Deduplicate via `Set`, convert to array.
|
|
605
|
+
c. **[AMB-1/BLK-4 FIX] Kahn's BFS topological sort — DIRECTION CLARIFIED:**
|
|
606
|
+
```
|
|
607
|
+
DIRECTION: in-degree = number of files that IMPORT this node = dependents.length
|
|
608
|
+
Depth 0: nodes with in-degree 0 (no file imports them — entry points, leaf consumers)
|
|
609
|
+
Depth N: nodes that become available after all their importers are processed
|
|
610
|
+
Core utilities (types/index.ts, config.ts) get HIGHEST depth — most files depend on them
|
|
611
|
+
```
|
|
612
|
+
- Compute in-degree for each node: `dependents.length`
|
|
613
|
+
- Initialize queue with all nodes where `dependents.length === 0` → `depth = 0`
|
|
614
|
+
- BFS: dequeue node, for each entry in `node.dependencies`: decrement that target's in-degree. If in-degree reaches 0, enqueue with `depth = currentDepth + 1`.
|
|
615
|
+
d. **[GAP-6 FIX] Cycle detection with Tarjan's SCC:**
|
|
616
|
+
After Kahn's BFS, if unprocessed nodes remain (in-degree > 0), they contain cycles:
|
|
617
|
+
- Run **Tarjan's Strongly Connected Components** on the subgraph of remaining nodes.
|
|
618
|
+
- For each SCC (each is a distinct cycle):
|
|
619
|
+
- Find max depth among nodes that have edges INTO this SCC from already-processed nodes.
|
|
620
|
+
- Assign all SCC nodes `depth = maxDepth + 1`.
|
|
621
|
+
- Log the full cycle path: `[nomos:graph:warn] Circular dependency detected: {a.ts → b.ts → c.ts → a.ts}` (extracted from SCC traversal order).
|
|
622
|
+
e. **Stats:** `total_files = fileNodes.size`, `total_symbols = sum(symbols.length)`, `total_edges = count of non-external resolved imports`, `core_modules = top N by depth desc` (N = `config.core_modules_count`).
|
|
623
|
+
|
|
624
|
+
**Validation:**
|
|
625
|
+
```bash
|
|
626
|
+
npx tsc --noEmit # → exit code 0
|
|
627
|
+
grep 'export class GraphBuilder' src/core/graph/builder.ts # → 1 match
|
|
628
|
+
grep 'node.dependencies = \[\]' src/core/graph/builder.ts # → 1 match (BLK-3 reset)
|
|
629
|
+
grep 'node.dependents = \[\]' src/core/graph/builder.ts # → 1 match (BLK-3 reset)
|
|
630
|
+
grep -i 'tarjan' src/core/graph/builder.ts # → at least 1 match (GAP-6)
|
|
631
|
+
grep 'in-degree = dependents' src/core/graph/builder.ts # → 1 match (AMB-1 direction comment)
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
**Rollback:** `rm src/core/graph/builder.ts`
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
#### Step 3.3: Test `ImportResolver`
|
|
639
|
+
|
|
640
|
+
**Pre-Condition:** Step 3.1 complete.
|
|
641
|
+
|
|
642
|
+
**Target File:** `src/core/graph/__tests__/resolver.test.ts` (new)
|
|
643
|
+
|
|
644
|
+
**Action:** vitest tests:
|
|
645
|
+
1. `./utils/hash` → `src/utils/hash.ts` (extension append).
|
|
646
|
+
2. `./utils` → `src/utils/index.ts` (index resolution).
|
|
647
|
+
3. Tsconfig alias `@/utils` → `src/utils/index.ts`.
|
|
648
|
+
4. `zod` → external.
|
|
649
|
+
5. `commander` → external.
|
|
650
|
+
6. Path traversal `../../outside-project` → external with warning.
|
|
651
|
+
7. Unresolvable relative → `{ resolved: null, is_external: false }`.
|
|
652
|
+
8. Missing `tsconfig.json` → alias skipped gracefully.
|
|
653
|
+
9. **[AMB-4]** All resolution uses `knownFiles` Set, no filesystem calls.
|
|
654
|
+
10. **[WATCH-4]** Resolver finds a carried-forward file: `knownFiles` contains `src/utils/hash.ts` (from carried map, not freshly scanned) → `./utils/hash` resolves correctly. This confirms the Set includes both new and carried files.
|
|
655
|
+
|
|
656
|
+
**Validation:** `npx vitest run src/core/graph/__tests__/resolver.test.ts` — all pass.
|
|
657
|
+
|
|
658
|
+
**Rollback:** Fix tests or re-evaluate Step 3.1.
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
#### Step 3.4: Test `GraphBuilder`
|
|
663
|
+
|
|
664
|
+
**Pre-Condition:** Step 3.2 complete.
|
|
665
|
+
|
|
666
|
+
**Target File:** `src/core/graph/__tests__/builder.test.ts` (new)
|
|
667
|
+
|
|
668
|
+
**Action:** vitest tests:
|
|
669
|
+
1. Chain: A→B→C → depths: A=0, B=1, C=2.
|
|
670
|
+
2. Fan-in: A→C, B→C → C highest depth, A,B = 0.
|
|
671
|
+
3. Forward/reverse edges correctly populated.
|
|
672
|
+
4. 2-node cycle: A↔B → both `depth = max + 1`, warning logged.
|
|
673
|
+
5. 3-node cycle: A→B→C→A → all three get cycle depth, warning with full path.
|
|
674
|
+
6. **[BLK-3]** Incremental: run `build()` twice on same nodes with changed imports → second run does NOT carry stale edges.
|
|
675
|
+
7. **[GAP-6]** Disconnected cycles: A↔B and C↔D with no connection → distinct cycle warnings logged for each.
|
|
676
|
+
8. `core_modules` extraction: top N by depth.
|
|
677
|
+
9. Stats accuracy.
|
|
678
|
+
|
|
679
|
+
**Validation:** `npx vitest run src/core/graph/__tests__/builder.test.ts` — all pass.
|
|
680
|
+
|
|
681
|
+
**Rollback:** Fix tests or re-evaluate Step 3.2.
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
### Phase 4: AI Semantic Enrichment
|
|
686
|
+
|
|
687
|
+
**Gate:** Phase 3 complete (Steps 3.1–3.4 verified).
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
#### Step 4.1: Implement `SemanticEnricher`
|
|
692
|
+
|
|
693
|
+
**Pre-Condition:** Phase 3 complete. `@google/generative-ai` installed.
|
|
694
|
+
|
|
695
|
+
**Target File:** `src/core/graph/enricher.ts` (new)
|
|
696
|
+
|
|
697
|
+
**Action:** Create `SemanticEnricher` class:
|
|
698
|
+
1. Constructor: `(config: NomosConfig['graph'], logger: Logger)` — validates `GEMINI_API_KEY`. Missing + `ai_enrichment=true` → throw `NomosError('graph_ai_key_missing')`. Init `GoogleGenerativeAI` client + `p-limit(config.ai_concurrency)`.
|
|
699
|
+
2. `async enrich(fileNodes: Map<string, FileNode>, cancellationFlag: { cancelled: boolean }): Promise<number>` — returns count of failures. Mutates `FileNode.semantic`.
|
|
700
|
+
3. Logic per file:
|
|
701
|
+
a. Staleness: skip if `semantic !== null && semantic.source_hash === hash`.
|
|
702
|
+
b. **[AMB-2 FIX]** Content source: read file from disk via `fs/promises.readFile(path.join(projectRoot, fileNode.file), 'utf-8')`. Do NOT rely on `ScanResult.content` — it may be garbage-collected. Truncate at last complete line before `config.max_file_chars`.
|
|
703
|
+
c. Prompt: File, Language, Exports, Used by, content, JSON schema instruction (per task spec).
|
|
704
|
+
d. Gemini call with `responseSchema` for structured output.
|
|
705
|
+
e. Rate limiting: `p-limit` + minimum gap of `Math.ceil(60000 / config.ai_requests_per_minute)` ms.
|
|
706
|
+
f. Retry: 429/503 → 3 retries, exponential backoff (2s, 4s, 8s). After 3 failures → `semantic: null`, log error.
|
|
707
|
+
g. Zod validation on response. Failure → log truncated raw (200 chars), `semantic: null`.
|
|
708
|
+
h. Populate `SemanticInfo`.
|
|
709
|
+
i. **[GAP-3 FIX]** Between each batch, check `cancellationFlag.cancelled`. If true, stop processing and return current failure count.
|
|
710
|
+
|
|
711
|
+
**Validation:**
|
|
712
|
+
```bash
|
|
713
|
+
npx tsc --noEmit # → exit code 0
|
|
714
|
+
grep 'export class SemanticEnricher' src/core/graph/enricher.ts # → 1 match
|
|
715
|
+
grep 'readFile' src/core/graph/enricher.ts # → at least 1 match (AMB-2: reads from disk)
|
|
716
|
+
grep 'cancellationFlag' src/core/graph/enricher.ts # → at least 1 match (GAP-3)
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
**Rollback:** `rm src/core/graph/enricher.ts`
|
|
720
|
+
|
|
721
|
+
---
|
|
722
|
+
|
|
723
|
+
#### Step 4.2: Implement `ContractWriter`
|
|
724
|
+
|
|
725
|
+
**Pre-Condition:** Step 4.1 complete.
|
|
726
|
+
|
|
727
|
+
**Target File:** `src/core/graph/contract-writer.ts` (new)
|
|
728
|
+
|
|
729
|
+
**Action:** Create `ContractWriter` class:
|
|
730
|
+
1. Constructor: `(projectRoot: string, logger: Logger)`
|
|
731
|
+
2. `async writeContracts(fileNodes: Map<string, FileNode>): Promise<void>`
|
|
732
|
+
3. Logic per file:
|
|
733
|
+
a. Skip if `semantic === null`.
|
|
734
|
+
b. Output path: replace extension with `.semantic.md`.
|
|
735
|
+
c. Overwrite skip: if `.semantic.md` exists and contains current `source_hash`, skip.
|
|
736
|
+
d. **[AMB-5 FIX]** Before writing, ensure parent directory exists:
|
|
737
|
+
```typescript
|
|
738
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
739
|
+
```
|
|
740
|
+
e. Write markdown per template.
|
|
741
|
+
|
|
742
|
+
**Validation:**
|
|
743
|
+
```bash
|
|
744
|
+
npx tsc --noEmit # → exit code 0
|
|
745
|
+
grep 'export class ContractWriter' src/core/graph/contract-writer.ts # → 1 match
|
|
746
|
+
grep 'recursive: true' src/core/graph/contract-writer.ts # → at least 1 match (AMB-5)
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**Rollback:** `rm src/core/graph/contract-writer.ts`
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
#### Step 4.3: Test `SemanticEnricher`
|
|
754
|
+
|
|
755
|
+
**Pre-Condition:** Step 4.1 complete.
|
|
756
|
+
|
|
757
|
+
**Target File:** `src/core/graph/__tests__/enricher.test.ts` (new)
|
|
758
|
+
|
|
759
|
+
**Action:** vitest tests with `vi.mock('@google/generative-ai')`:
|
|
760
|
+
1. Enriches file with `semantic: null` → mock returns valid JSON → populated.
|
|
761
|
+
2. Staleness: matching `source_hash` → skipped.
|
|
762
|
+
3. Zod failure: mock returns `{ overview: 123 }` → `semantic: null`.
|
|
763
|
+
4. Retry on 429: mock throws twice then succeeds → populated.
|
|
764
|
+
5. Missing API key → constructor throws.
|
|
765
|
+
6. All retries exhausted → `semantic: null`.
|
|
766
|
+
7. **[GAP-3]** Cancellation flag set mid-enrichment → stops processing.
|
|
767
|
+
8. **[AMB-2]** Verifies file content is read from disk (mock `fs.readFile`).
|
|
768
|
+
|
|
769
|
+
**Validation:** `npx vitest run src/core/graph/__tests__/enricher.test.ts` — all pass.
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
#### Step 4.4: Test `ContractWriter`
|
|
774
|
+
|
|
775
|
+
**Pre-Condition:** Step 4.2 complete.
|
|
776
|
+
|
|
777
|
+
**Target File:** `src/core/graph/__tests__/contract-writer.test.ts` (new)
|
|
778
|
+
|
|
779
|
+
**Action:** vitest tests:
|
|
780
|
+
1. Writes `.semantic.md` with correct structure.
|
|
781
|
+
2. Skips when `source_hash` matches.
|
|
782
|
+
3. Overwrites when hash changed.
|
|
783
|
+
4. Skips files with `semantic: null`.
|
|
784
|
+
5. **[AMB-5]** Creates parent directory if missing.
|
|
785
|
+
|
|
786
|
+
**Validation:** `npx vitest run src/core/graph/__tests__/contract-writer.test.ts` — all pass.
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
### Phase 5: Command, Pipeline, and Integration
|
|
791
|
+
|
|
792
|
+
**Gate:** Phase 4 complete (Steps 4.1–4.4 verified).
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
#### Step 5.1: Implement `MapPipeline`
|
|
797
|
+
|
|
798
|
+
**Pre-Condition:** All Phase 1–4 components exist and pass tests.
|
|
799
|
+
|
|
800
|
+
**Target File:** `src/core/graph/pipeline.ts` (new)
|
|
801
|
+
|
|
802
|
+
**Action:** Create `MapPipeline` class:
|
|
803
|
+
1. Constructor: `(config: NomosConfig, projectRoot: string, logger: Logger)`
|
|
804
|
+
2. `async run(options: { noAi: boolean; force: boolean; patterns?: string[] }): Promise<{ map: ProjectMap; aiFailures: number }>`
|
|
805
|
+
3. Pipeline flow:
|
|
806
|
+
a. Read existing map via `readProjectMap()`.
|
|
807
|
+
b. `FileScanner.scan()` → `files` (need parsing) + `carried` (unchanged).
|
|
808
|
+
c. If both empty: log warning, return empty map.
|
|
809
|
+
d. **Init `ASTParser`** — call `await parser.init()` (WASM requires async initialization).
|
|
810
|
+
e. For each file in `files`: `ASTParser.parse()` → build `FileNode`.
|
|
811
|
+
f. Merge parsed + carried into `Map<string, FileNode>`.
|
|
812
|
+
g. **[WATCH-4 — CRITICAL ORDERING]** Build `knownFiles` Set BEFORE resolution:
|
|
813
|
+
```typescript
|
|
814
|
+
// Merge ALL known file paths — both newly parsed and carried forward
|
|
815
|
+
const allFiles = new Map<string, FileNode>(); // from step f
|
|
816
|
+
const knownFiles = new Set<string>(allFiles.keys());
|
|
817
|
+
// knownFiles now contains EVERY file the scanner saw, regardless of parse status
|
|
818
|
+
```
|
|
819
|
+
Then call `ImportResolver.resolve()` for every `ImportEntry`, passing `knownFiles`.
|
|
820
|
+
`resolve()` is async — use `Promise.all` with `p-limit` if needed.
|
|
821
|
+
**Invariant:** `knownFiles` is immutable after construction. Never add/remove entries during resolution.
|
|
822
|
+
h. `GraphBuilder.build()` → mutates graph fields, returns stats.
|
|
823
|
+
i. **[GAP-2 FIX]** Write map to disk BEFORE AI enrichment (with `semantic: null` for new files):
|
|
824
|
+
```typescript
|
|
825
|
+
const intermediateMap: ProjectMap = { schema_version: 1, generated_at, root, files, stats };
|
|
826
|
+
await this.writeMap(intermediateMap, mapPath);
|
|
827
|
+
```
|
|
828
|
+
j. If `!noAi`:
|
|
829
|
+
- **[GAP-3 FIX]** Set up cancellation:
|
|
830
|
+
```typescript
|
|
831
|
+
const cancellation = { cancelled: false };
|
|
832
|
+
const sigintHandler = () => { cancellation.cancelled = true; };
|
|
833
|
+
process.on('SIGINT', sigintHandler);
|
|
834
|
+
```
|
|
835
|
+
- Call `SemanticEnricher.enrich(fileNodes, cancellation)`.
|
|
836
|
+
- Call `ContractWriter.writeContracts(fileNodes)`.
|
|
837
|
+
- Remove SIGINT handler: `process.removeListener('SIGINT', sigintHandler)`.
|
|
838
|
+
k. **[BLK-2 FIX + WATCH-3] Atomic write with `proper-lockfile` — Safe on Non-Existent Files:**
|
|
839
|
+
|
|
840
|
+
**Problem:** `proper-lockfile` requires the target file to exist before it can acquire a lock. On first run, `project_map.json` does not exist. The original `.catch()` approach (create empty file then re-lock) has a TOCTOU race: two concurrent processes both see the file missing, both create it, and the second `lock()` call may fail or lock a file the first process is about to overwrite.
|
|
841
|
+
|
|
842
|
+
**Solution: Lock the DIRECTORY, not the file.** The output directory is created with `mkdir -p` and is guaranteed to exist before any write. A lockfile on the directory serializes all map writes regardless of whether the map file itself exists:
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
import lockfile from 'proper-lockfile';
|
|
846
|
+
|
|
847
|
+
private async writeMap(map: ProjectMap, mapPath: string): Promise<void> {
|
|
848
|
+
const outDir = path.dirname(mapPath);
|
|
849
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
850
|
+
|
|
851
|
+
// Lock the DIRECTORY — works even when project_map.json doesn't exist yet.
|
|
852
|
+
// proper-lockfile creates a .lock file (e.g., tasks-management/graph/.lock)
|
|
853
|
+
// which serializes concurrent arc map invocations.
|
|
854
|
+
const release = await lockfile.lock(outDir, {
|
|
855
|
+
lockfilePath: path.join(outDir, '.project-map.lock'),
|
|
856
|
+
realpath: false,
|
|
857
|
+
retries: { retries: 10, minTimeout: 200, maxTimeout: 2000 },
|
|
858
|
+
stale: 60000, // 60s stale lock — enrichment can be slow
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
try {
|
|
862
|
+
const tmpPath = `${mapPath}.tmp`;
|
|
863
|
+
await fs.writeFile(tmpPath, JSON.stringify(map, null, 2), 'utf-8');
|
|
864
|
+
const fd = await fs.open(tmpPath, 'r+');
|
|
865
|
+
try { await fd.sync(); } finally { await fd.close(); }
|
|
866
|
+
await fs.rename(tmpPath, mapPath);
|
|
867
|
+
} finally {
|
|
868
|
+
await release();
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
**Why directory locking is correct:**
|
|
874
|
+
- `outDir` is always created BEFORE lock acquisition → no existence check needed
|
|
875
|
+
- `lockfilePath` is explicit → no reliance on `proper-lockfile`'s default `.lock` naming
|
|
876
|
+
- `stale: 60000` is higher than file-lock (30s) because AI enrichment writes can take time
|
|
877
|
+
- The `.project-map.lock` file is an artifact — add to `.gitignore` in Step 0.4
|
|
878
|
+
- `rename()` is atomic on POSIX — combined with the directory lock, this is double-safe
|
|
879
|
+
|
|
880
|
+
**Additional `.gitignore` entry** (add in Step 0.4):
|
|
881
|
+
```
|
|
882
|
+
.project-map.lock
|
|
883
|
+
```
|
|
884
|
+
l. Count AI failures and return.
|
|
885
|
+
|
|
886
|
+
**Validation:**
|
|
887
|
+
```bash
|
|
888
|
+
npx tsc --noEmit # → exit code 0
|
|
889
|
+
grep 'export class MapPipeline' src/core/graph/pipeline.ts # → 1 match
|
|
890
|
+
grep 'proper-lockfile' src/core/graph/pipeline.ts # → at least 1 match (BLK-2)
|
|
891
|
+
grep 'SIGINT' src/core/graph/pipeline.ts # → at least 1 match (GAP-3)
|
|
892
|
+
grep 'writeMap.*intermediateMap' src/core/graph/pipeline.ts # → pattern present (GAP-2)
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
**Rollback:** `rm src/core/graph/pipeline.ts`
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
#### Step 5.2: Implement `arc map` command
|
|
900
|
+
|
|
901
|
+
**Pre-Condition:** Step 5.1 complete.
|
|
902
|
+
|
|
903
|
+
**Target File:** `src/commands/map.ts` (new)
|
|
904
|
+
|
|
905
|
+
**Action:** `registerMapCommand(program: Command): void`:
|
|
906
|
+
1. Options: `--no-ai`, `--force`, variadic `[patterns...]`.
|
|
907
|
+
2. Handler:
|
|
908
|
+
a. `loadConfig()`.
|
|
909
|
+
b. If `--no-ai` → `config.graph.ai_enrichment = false`.
|
|
910
|
+
c. `MapPipeline.run()`.
|
|
911
|
+
d. Print summary: `Mapped {N} files, enriched {M}, {K} skipped`.
|
|
912
|
+
e. **[AMB-6 FIX]** Exit codes: `0` = success, `1` = fatal error, `10` = partial AI failure (NOT `2`).
|
|
913
|
+
3. Progress on stderr.
|
|
914
|
+
|
|
915
|
+
**Validation:**
|
|
916
|
+
```bash
|
|
917
|
+
npx tsc --noEmit # → exit code 0
|
|
918
|
+
grep 'registerMapCommand' src/commands/map.ts # → 1 match
|
|
919
|
+
grep 'exit(10)' src/commands/map.ts # → 1 match (AMB-6: not exit(2))
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
**Rollback:** `rm src/commands/map.ts`
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
#### Step 5.3: Register `arc map` in CLI entry point
|
|
927
|
+
|
|
928
|
+
**Pre-Condition:** Step 5.2 complete.
|
|
929
|
+
|
|
930
|
+
**Target File:** `src/cli.ts` (modify)
|
|
931
|
+
|
|
932
|
+
**Action:**
|
|
933
|
+
1. Add: `import { registerMapCommand } from './commands/map.js';`
|
|
934
|
+
2. Add `registerMapCommand` to registration array.
|
|
935
|
+
|
|
936
|
+
**Validation:**
|
|
937
|
+
```bash
|
|
938
|
+
npx tsc --noEmit # → exit code 0
|
|
939
|
+
grep 'registerMapCommand' src/cli.ts # → 2 matches (import + usage)
|
|
940
|
+
npx tsx src/cli.ts map --help # → shows help
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
**Rollback:** `git checkout src/cli.ts`
|
|
944
|
+
|
|
945
|
+
---
|
|
946
|
+
|
|
947
|
+
#### Step 5.4a: Extend `PromptOptions` with `architecturalConstraints`
|
|
948
|
+
|
|
949
|
+
**Pre-Condition:** `src/types/index.ts` compiles.
|
|
950
|
+
|
|
951
|
+
**Action:** Add `architecturalConstraints: string | null;` to `PromptOptions` interface.
|
|
952
|
+
|
|
953
|
+
**Validation:** `grep 'architecturalConstraints' src/types/index.ts` — 1 match.
|
|
954
|
+
|
|
955
|
+
**Rollback:** Remove added line.
|
|
956
|
+
|
|
957
|
+
---
|
|
958
|
+
|
|
959
|
+
#### Step 5.4b: Implement `readArchitecturalConstraints`
|
|
960
|
+
|
|
961
|
+
**Pre-Condition:** Steps 1.3 and 5.4a complete.
|
|
962
|
+
|
|
963
|
+
**Target File:** `src/core/graph/constraints.ts` (new)
|
|
964
|
+
|
|
965
|
+
**Action:** Export `async function readArchitecturalConstraints(projectRoot: string, outputDir: string, contextFiles: string[]): Promise<string | null>`:
|
|
966
|
+
1. Read `project_map.json`. Missing → return `null`.
|
|
967
|
+
2. Parse with `migrateProjectMap()`.
|
|
968
|
+
3. **[AMB-7 FIX]** Normalize each `contextFile` before lookup:
|
|
969
|
+
```typescript
|
|
970
|
+
const normalizedPath = path.relative(projectRoot, path.resolve(projectRoot, contextFile))
|
|
971
|
+
.split(path.sep).join('/'); // ensure forward slashes
|
|
972
|
+
const fileNode = map.files[normalizedPath];
|
|
973
|
+
```
|
|
974
|
+
4. Collect dependents with non-null `semantic`, identify consumed symbols.
|
|
975
|
+
5. Build constraint string per spec format.
|
|
976
|
+
6. Return `null` if no constraints found.
|
|
977
|
+
|
|
978
|
+
**Validation:**
|
|
979
|
+
```bash
|
|
980
|
+
npx tsc --noEmit # → exit code 0
|
|
981
|
+
grep 'path.relative' src/core/graph/constraints.ts # → at least 1 match (AMB-7)
|
|
982
|
+
grep 'export async function readArchitecturalConstraints' src/core/graph/constraints.ts # → 1 match
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
**Rollback:** `rm src/core/graph/constraints.ts`
|
|
986
|
+
|
|
987
|
+
---
|
|
988
|
+
|
|
989
|
+
#### Step 5.4c: Wire constraints into `Orchestrator.plan()` and `assemblePrompt()`
|
|
990
|
+
|
|
991
|
+
**Pre-Condition:** Steps 5.4a + 5.4b complete.
|
|
992
|
+
|
|
993
|
+
**Target Files:** `src/core/orchestrator.ts`, `src/core/prompt.ts`
|
|
994
|
+
|
|
995
|
+
**Action:**
|
|
996
|
+
1. In `prompt.ts`: add `[ARCHITECTURAL CONSTRAINTS]` section when `options.architecturalConstraints !== null`.
|
|
997
|
+
2. In `orchestrator.ts`: import `readArchitecturalConstraints`, call before `assemblePrompt`, pass result.
|
|
998
|
+
3. Fix all `PromptOptions` construction sites to include `architecturalConstraints: null`.
|
|
999
|
+
|
|
1000
|
+
**Validation:**
|
|
1001
|
+
```bash
|
|
1002
|
+
npx tsc --noEmit # → exit code 0
|
|
1003
|
+
npx vitest run # → all tests pass
|
|
1004
|
+
grep 'ARCHITECTURAL CONSTRAINTS' src/core/prompt.ts # → 1 match
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
**Rollback:** Revert both files.
|
|
1008
|
+
|
|
1009
|
+
---
|
|
1010
|
+
|
|
1011
|
+
#### Step 5.5: Test `MapPipeline` end-to-end
|
|
1012
|
+
|
|
1013
|
+
**Pre-Condition:** Steps 5.1–5.4c complete.
|
|
1014
|
+
|
|
1015
|
+
**Sub-step 5.5a:** Create test fixture at `test/fixtures/sample-project/` with 8 TS files (per original plan).
|
|
1016
|
+
|
|
1017
|
+
**Sub-step 5.5b:** Write `src/core/graph/__tests__/pipeline.test.ts`:
|
|
1018
|
+
1. Full pipeline `noAi: true` → 8 files mapped.
|
|
1019
|
+
2. `src/types.ts` + `src/utils/index.ts` have highest depth.
|
|
1020
|
+
3. `src/main.ts` = depth 0.
|
|
1021
|
+
4. Circular pair detected.
|
|
1022
|
+
5. Incremental: second run carries all (0 re-parsed).
|
|
1023
|
+
6. `--force`: all re-parsed.
|
|
1024
|
+
7. `core_modules` correct.
|
|
1025
|
+
8. `stats.total_edges` correct.
|
|
1026
|
+
9. **[BLK-2]** Concurrent: launch two pipeline runs simultaneously → both complete without corruption (lockfile prevents race).
|
|
1027
|
+
10. **[GAP-2]** Kill-safe: verify intermediate map is on disk before enrichment begins.
|
|
1028
|
+
|
|
1029
|
+
**Validation:** `npx vitest run src/core/graph/__tests__/pipeline.test.ts` — all pass.
|
|
1030
|
+
|
|
1031
|
+
---
|
|
1032
|
+
|
|
1033
|
+
#### Step 5.6: Test Architectural Constraint injection
|
|
1034
|
+
|
|
1035
|
+
**Pre-Condition:** Step 5.4c complete.
|
|
1036
|
+
|
|
1037
|
+
**Target File:** `src/core/__tests__/prompt.test.ts` (modify)
|
|
1038
|
+
|
|
1039
|
+
**Action:**
|
|
1040
|
+
1. Non-null constraints → `[ARCHITECTURAL CONSTRAINTS]` present.
|
|
1041
|
+
2. Null → section absent.
|
|
1042
|
+
3. Integration: minimal `project_map.json` → constraint string correct.
|
|
1043
|
+
4. **[AMB-7]** Context file with relative path `../src/core/state.ts` → correctly normalized and found.
|
|
1044
|
+
5. Missing-from-map file → warning logged, skipped.
|
|
1045
|
+
|
|
1046
|
+
**Validation:** `npx vitest run src/core/__tests__/prompt.test.ts` — all pass.
|
|
1047
|
+
|
|
1048
|
+
---
|
|
1049
|
+
|
|
1050
|
+
### Phase 6: Visualization — `arc show map`
|
|
1051
|
+
|
|
1052
|
+
**Gate:** Phase 5 complete (Steps 5.1–5.6 verified).
|
|
1053
|
+
|
|
1054
|
+
---
|
|
1055
|
+
|
|
1056
|
+
#### Step 6.1: Implement `MapRenderer`
|
|
1057
|
+
|
|
1058
|
+
**Pre-Condition:** Phase 5 complete.
|
|
1059
|
+
|
|
1060
|
+
**Target File:** `src/core/graph/renderer.ts` (new)
|
|
1061
|
+
|
|
1062
|
+
**Action:** Per original plan. `render()` → Cytoscape nodes/edges. `computeColor()` → depth-based gradient.
|
|
1063
|
+
|
|
1064
|
+
**Validation:**
|
|
1065
|
+
```bash
|
|
1066
|
+
npx tsc --noEmit # → exit code 0
|
|
1067
|
+
grep 'export class MapRenderer' src/core/graph/renderer.ts # → 1 match
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
**Rollback:** `rm src/core/graph/renderer.ts`
|
|
1071
|
+
|
|
1072
|
+
---
|
|
1073
|
+
|
|
1074
|
+
#### Step 6.2a: Create HTML template — self-contained with inline Cytoscape
|
|
1075
|
+
|
|
1076
|
+
**Pre-Condition:** Step 6.1 complete.
|
|
1077
|
+
|
|
1078
|
+
**Target File:** `src/core/graph/html-template.ts` (new)
|
|
1079
|
+
|
|
1080
|
+
**Action:** `export function generateHtml(mapJson: string, cytoscapeNodes: string, cytoscapeEdges: string): string`:
|
|
1081
|
+
1. **[GAP-4 FIX + WATCH-2] Cytoscape.js bundled INLINE — Exact Mechanism:**
|
|
1082
|
+
|
|
1083
|
+
**Problem:** The original plan said "bundle inline" without specifying how. Cytoscape.js minified is ~500KB — too large for a string literal in source code (breaks linters, slows IDE indexing, bloats git diffs).
|
|
1084
|
+
|
|
1085
|
+
**Solution: Runtime read from `node_modules` at generation time.** The `generateHtml()` function reads `cytoscape.min.js` from disk when generating the HTML, NOT at module import time:
|
|
1086
|
+
|
|
1087
|
+
```typescript
|
|
1088
|
+
// src/core/graph/html-template.ts
|
|
1089
|
+
import fs from 'node:fs';
|
|
1090
|
+
import { createRequire } from 'node:module';
|
|
1091
|
+
|
|
1092
|
+
const require = createRequire(import.meta.url);
|
|
1093
|
+
|
|
1094
|
+
// Lazy-loaded, cached after first call
|
|
1095
|
+
let _cytoscapeSource: string | null = null;
|
|
1096
|
+
|
|
1097
|
+
function getCytoscapeSource(): string {
|
|
1098
|
+
if (_cytoscapeSource) return _cytoscapeSource;
|
|
1099
|
+
|
|
1100
|
+
// Resolve from node_modules — works regardless of hoisting
|
|
1101
|
+
const cytoscapePath = require.resolve('cytoscape/dist/cytoscape.min.js');
|
|
1102
|
+
_cytoscapeSource = fs.readFileSync(cytoscapePath, 'utf-8');
|
|
1103
|
+
return _cytoscapeSource;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export function generateHtml(
|
|
1107
|
+
mapJson: string,
|
|
1108
|
+
cytoscapeNodes: string,
|
|
1109
|
+
cytoscapeEdges: string,
|
|
1110
|
+
): string {
|
|
1111
|
+
const cytoscapeJs = getCytoscapeSource();
|
|
1112
|
+
return `<!DOCTYPE html>
|
|
1113
|
+
<html>
|
|
1114
|
+
<head><meta charset="utf-8"><title>nomos-arc Dependency Graph</title></head>
|
|
1115
|
+
<body>
|
|
1116
|
+
<div id="cy"></div>
|
|
1117
|
+
<noscript><pre>Dependency graph requires JavaScript. Files:\n${textFallback}</pre></noscript>
|
|
1118
|
+
<script>${cytoscapeJs}</script>
|
|
1119
|
+
<script>
|
|
1120
|
+
const PROJECT_MAP = ${safeJson};
|
|
1121
|
+
// ... Cytoscape init, layout, interactivity ...
|
|
1122
|
+
</script>
|
|
1123
|
+
</body>
|
|
1124
|
+
</html>`;
|
|
1125
|
+
}
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
**Why this approach:**
|
|
1129
|
+
- No build script needed — `cytoscape` is a regular npm dependency
|
|
1130
|
+
- No string literal in source — the 500KB is read at runtime only when `arc show map` runs
|
|
1131
|
+
- `require.resolve()` survives npm hoisting, monorepos, and `node_modules/.pnpm`
|
|
1132
|
+
- Lazy caching means the file is read once per process, not per call
|
|
1133
|
+
- The generated `index.html` is fully self-contained — works offline, no CDN
|
|
1134
|
+
|
|
1135
|
+
**Pre-Requisite Dependency:** Add `cytoscape` as a runtime dependency in Step 0.1:
|
|
1136
|
+
```bash
|
|
1137
|
+
npm install cytoscape
|
|
1138
|
+
```
|
|
1139
|
+
(This replaces the CDN dependency. ~500KB is added to `node_modules`, NOT to the esbuild bundle — `html-template.ts` reads it at runtime.)
|
|
1140
|
+
|
|
1141
|
+
**esbuild consideration:** Do NOT add `--external:cytoscape`. The `require.resolve()` call resolves a path at runtime — esbuild does not attempt to bundle it because it's inside a dynamic `readFileSync`, not a static `import`. If esbuild does attempt to bundle it, add `--external:cytoscape` to the build script.
|
|
1142
|
+
|
|
1143
|
+
- Add `<noscript>` fallback with plain-text file listing for offline/no-JS environments.
|
|
1144
|
+
2. **[AMB-8 FIX]** JSON embedding with XSS prevention:
|
|
1145
|
+
```typescript
|
|
1146
|
+
const safeJson = JSON.stringify(map)
|
|
1147
|
+
.replace(/<\//g, '<\\/') // prevent </script> injection
|
|
1148
|
+
.replace(/\$\{/g, '\\${'); // prevent template literal breakout
|
|
1149
|
+
// Embed as: const PROJECT_MAP = ${safeJson};
|
|
1150
|
+
```
|
|
1151
|
+
3. BFS layout, depth-based coloring, full-viewport CSS — per original plan.
|
|
1152
|
+
|
|
1153
|
+
**Validation:**
|
|
1154
|
+
```bash
|
|
1155
|
+
npx tsc --noEmit # → exit code 0
|
|
1156
|
+
grep 'cytoscape.min' src/core/graph/html-template.ts # → at least 1 match (GAP-4: inline)
|
|
1157
|
+
grep '<\\\\/' src/core/graph/html-template.ts # → at least 1 match (AMB-8: XSS fix)
|
|
1158
|
+
grep 'noscript' src/core/graph/html-template.ts # → at least 1 match (GAP-4: fallback)
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
**Rollback:** `rm src/core/graph/html-template.ts`
|
|
1162
|
+
|
|
1163
|
+
---
|
|
1164
|
+
|
|
1165
|
+
#### Step 6.2b: Add context card panel
|
|
1166
|
+
|
|
1167
|
+
Per original plan. No changes from Red Team findings.
|
|
1168
|
+
|
|
1169
|
+
---
|
|
1170
|
+
|
|
1171
|
+
#### Step 6.2c: Add ripple analysis
|
|
1172
|
+
|
|
1173
|
+
Per original plan. No changes from Red Team findings.
|
|
1174
|
+
|
|
1175
|
+
---
|
|
1176
|
+
|
|
1177
|
+
#### Step 6.2d: Add symbol search bar
|
|
1178
|
+
|
|
1179
|
+
Per original plan. No changes from Red Team findings.
|
|
1180
|
+
|
|
1181
|
+
---
|
|
1182
|
+
|
|
1183
|
+
#### Step 6.3: Implement `arc show map` command
|
|
1184
|
+
|
|
1185
|
+
**Pre-Condition:** Steps 6.1–6.2d complete.
|
|
1186
|
+
|
|
1187
|
+
**Target File:** `src/commands/show.ts` (new)
|
|
1188
|
+
|
|
1189
|
+
**Action:** Per original plan. `registerShowCommand(program: Command)`, read map, render, generate HTML, open browser.
|
|
1190
|
+
|
|
1191
|
+
**Validation:**
|
|
1192
|
+
```bash
|
|
1193
|
+
npx tsc --noEmit # → exit code 0
|
|
1194
|
+
grep 'registerShowCommand' src/commands/show.ts # → 1 match
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
**Rollback:** `rm src/commands/show.ts`
|
|
1198
|
+
|
|
1199
|
+
---
|
|
1200
|
+
|
|
1201
|
+
#### Step 6.4: Register `arc show map` in CLI
|
|
1202
|
+
|
|
1203
|
+
Per original plan. Add import + registration in `src/cli.ts`.
|
|
1204
|
+
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
#### Step 6.5: Test `MapRenderer`
|
|
1208
|
+
|
|
1209
|
+
Per original plan (8 test cases). No changes from Red Team findings.
|
|
1210
|
+
|
|
1211
|
+
---
|
|
1212
|
+
|
|
1213
|
+
#### Step 6.6: Test `arc show map` command
|
|
1214
|
+
|
|
1215
|
+
Per original plan (5 test cases). Add:
|
|
1216
|
+
6. **[GAP-4]** Generated HTML does NOT contain `cdnjs.cloudflare.com` (Cytoscape is inline).
|
|
1217
|
+
7. **[AMB-8]** Generated HTML with a file path containing `</script>` does not break the page.
|
|
1218
|
+
|
|
1219
|
+
---
|
|
1220
|
+
|
|
1221
|
+
## 4. Final System Integrity Check
|
|
1222
|
+
|
|
1223
|
+
### 4.1 Full Test Suite
|
|
1224
|
+
```bash
|
|
1225
|
+
npx vitest run
|
|
1226
|
+
```
|
|
1227
|
+
Expected: all tests pass — 0 failures. 9 new test files + all existing.
|
|
1228
|
+
|
|
1229
|
+
### 4.2 Type Check
|
|
1230
|
+
```bash
|
|
1231
|
+
npx tsc --noEmit
|
|
1232
|
+
```
|
|
1233
|
+
Expected: exit code 0, no errors.
|
|
1234
|
+
|
|
1235
|
+
### 4.3 Build Verification
|
|
1236
|
+
```bash
|
|
1237
|
+
npm run build
|
|
1238
|
+
```
|
|
1239
|
+
Expected: `dist/cli.js` produced. No `--external:tree-sitter` needed (WASM bundles natively).
|
|
1240
|
+
|
|
1241
|
+
### 4.4 Post-Build Runtime Verification [BLK-1 CRITICAL]
|
|
1242
|
+
```bash
|
|
1243
|
+
node dist/cli.js map --no-ai --help
|
|
1244
|
+
```
|
|
1245
|
+
Expected: shows help text. Proves WASM parser loads correctly from built artifact. **This was the exact failure mode BLK-1 identified.**
|
|
1246
|
+
|
|
1247
|
+
### 4.5 End-to-End Smoke Test (Structural)
|
|
1248
|
+
```bash
|
|
1249
|
+
npx tsx src/cli.ts map --no-ai
|
|
1250
|
+
```
|
|
1251
|
+
Expected: `project_map.json` at `tasks-management/graph/`. Summary printed. Exit code 0.
|
|
1252
|
+
```bash
|
|
1253
|
+
node -e "const m=JSON.parse(require('fs').readFileSync('tasks-management/graph/project_map.json','utf8')); console.log('files:', Object.keys(m.files).length, 'edges:', m.stats.total_edges, 'version:', m.schema_version)"
|
|
1254
|
+
```
|
|
1255
|
+
Expected: file count > 0, edge count > 0, version = 1.
|
|
1256
|
+
|
|
1257
|
+
### 4.6 Visualization Smoke Test
|
|
1258
|
+
```bash
|
|
1259
|
+
npx tsx src/cli.ts show map --no-open
|
|
1260
|
+
```
|
|
1261
|
+
Expected: `index.html` written. Exit code 0.
|
|
1262
|
+
```bash
|
|
1263
|
+
grep -c 'cytoscape' tasks-management/graph/index.html
|
|
1264
|
+
```
|
|
1265
|
+
Expected: > 0 matches (inline source, not CDN).
|
|
1266
|
+
|
|
1267
|
+
### 4.7 Incremental Run Verification
|
|
1268
|
+
```bash
|
|
1269
|
+
time npx tsx src/cli.ts map --no-ai # First run
|
|
1270
|
+
time npx tsx src/cli.ts map --no-ai # Second run
|
|
1271
|
+
```
|
|
1272
|
+
Expected: second run completes significantly faster (all files carried forward).
|
|
1273
|
+
|
|
1274
|
+
### 4.8 Concurrent Execution Safety [BLK-2]
|
|
1275
|
+
```bash
|
|
1276
|
+
npx tsx src/cli.ts map --no-ai & npx tsx src/cli.ts map --no-ai & wait
|
|
1277
|
+
```
|
|
1278
|
+
Expected: both complete without error. `project_map.json` is valid JSON. No corruption.
|
|
1279
|
+
|
|
1280
|
+
### 4.9 Forward Version Check [GAP-5]
|
|
1281
|
+
```bash
|
|
1282
|
+
node -e "const fs=require('fs'); const m=JSON.parse(fs.readFileSync('tasks-management/graph/project_map.json','utf8')); m.schema_version=99; fs.writeFileSync('tasks-management/graph/project_map.json',JSON.stringify(m))"
|
|
1283
|
+
npx tsx src/cli.ts map --no-ai 2>&1
|
|
1284
|
+
```
|
|
1285
|
+
Expected: error message containing "newer version" or "Please upgrade".
|
|
1286
|
+
|
|
1287
|
+
### 4.10 File Inventory Check
|
|
1288
|
+
|
|
1289
|
+
**New files (22 total):**
|
|
1290
|
+
```
|
|
1291
|
+
src/core/graph/map-schema.ts
|
|
1292
|
+
src/core/graph/scanner.ts
|
|
1293
|
+
src/core/graph/parser.ts
|
|
1294
|
+
src/core/graph/grammar-paths.ts (WATCH-1: tier-based WASM grammar resolution)
|
|
1295
|
+
src/core/graph/resolver.ts
|
|
1296
|
+
src/core/graph/builder.ts
|
|
1297
|
+
src/core/graph/enricher.ts
|
|
1298
|
+
src/core/graph/contract-writer.ts
|
|
1299
|
+
src/core/graph/constraints.ts
|
|
1300
|
+
src/core/graph/pipeline.ts
|
|
1301
|
+
src/core/graph/renderer.ts
|
|
1302
|
+
src/core/graph/html-template.ts
|
|
1303
|
+
src/core/graph/grammars/ (WASM grammar files — Tier B/C only, not needed if Tier A)
|
|
1304
|
+
src/core/graph/__tests__/scanner.test.ts
|
|
1305
|
+
src/core/graph/__tests__/parser.test.ts
|
|
1306
|
+
src/core/graph/__tests__/resolver.test.ts
|
|
1307
|
+
src/core/graph/__tests__/builder.test.ts
|
|
1308
|
+
src/core/graph/__tests__/enricher.test.ts
|
|
1309
|
+
src/core/graph/__tests__/contract-writer.test.ts
|
|
1310
|
+
src/core/graph/__tests__/pipeline.test.ts
|
|
1311
|
+
src/core/graph/__tests__/renderer.test.ts
|
|
1312
|
+
src/core/graph/__tests__/show.test.ts
|
|
1313
|
+
src/commands/map.ts
|
|
1314
|
+
src/commands/show.ts
|
|
1315
|
+
test/fixtures/sample-project/ (8 files)
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
**Modified files (6 total):**
|
|
1319
|
+
```
|
|
1320
|
+
package.json (new deps + esbuild externals)
|
|
1321
|
+
src/core/errors.ts (4 new error codes)
|
|
1322
|
+
src/types/index.ts (graph types + config + PromptOptions field)
|
|
1323
|
+
src/core/config.ts (GraphConfigSchema)
|
|
1324
|
+
src/core/orchestrator.ts (constraint injection)
|
|
1325
|
+
src/core/prompt.ts (architectural constraints section)
|
|
1326
|
+
src/cli.ts (2 new command registrations)
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
---
|
|
1330
|
+
|
|
1331
|
+
## 5. Trade-Off Register
|
|
1332
|
+
|
|
1333
|
+
| Decision | Chosen Option | Alternative | Rationale |
|
|
1334
|
+
|---|---|---|---|
|
|
1335
|
+
| `web-tree-sitter` over native `tree-sitter` | WASM | Native C++ N-API | Eliminates BLK-1 entirely. ~10-20% slower parsing but zero bundling complexity. Production stability > raw speed. |
|
|
1336
|
+
| `proper-lockfile` for map writes | File locking | Advisory lock via `flock` | Matches existing codebase pattern (`state.ts`). Cross-platform. Already a dependency. |
|
|
1337
|
+
| `knownFiles` Set over `fs.access()` | In-memory lookup | Async filesystem check | O(1) per lookup vs O(1) amortized with syscall overhead. Scanner already knows all files. Zero event-loop blocking. |
|
|
1338
|
+
| Inline Cytoscape.js over CDN | ~500KB larger HTML | CDN with offline fallback | Zero external dependencies. Works offline, on air-gapped networks, behind corporate proxies. HTML is generated once, not served repeatedly. |
|
|
1339
|
+
| Tarjan's SCC over "assign max+1 to remnants" | Full cycle extraction | Simpler but lossy | Distinct cycles get distinct log entries. Disconnected cycles get correct depth assignment. Marginal code complexity increase for significant diagnostic improvement. |
|
|
1340
|
+
| Exit code 10 over exit code 2 | Non-standard but safe | Exit code 2 (Bash convention) | Avoids CI/CD misinterpretation. Exit code 2 = "misuse of shell command" in Bash. Exit code 10 is unambiguous. |
|
|
1341
|
+
|
|
1342
|
+
### WATCH Resolutions (Implementation-Time Risks)
|
|
1343
|
+
|
|
1344
|
+
| WATCH | Risk | Resolution | Affected Steps |
|
|
1345
|
+
|---|---|---|---|
|
|
1346
|
+
| **WATCH-1** | `web-tree-sitter` WASM grammars not available via npm | 3-tier deterministic fallback (npm package → GitHub release → build from source) + `grammar-paths.ts` abstraction layer + CRITICAL GATE smoke test | 0.2, 2.1 |
|
|
1347
|
+
| **WATCH-2** | Cytoscape.js inline bundling mechanics unspecified | Runtime `readFileSync` from `node_modules/cytoscape/dist/cytoscape.min.js` via `require.resolve()`. Lazy-cached. No string literal in source, no build script. `cytoscape` added as npm dependency in Step 0.1 | 0.1, 6.2a |
|
|
1348
|
+
| **WATCH-3** | `proper-lockfile` fails on non-existent file (TOCTOU race) | Lock the output DIRECTORY instead of the file. Directory is guaranteed to exist after `mkdir -p`. Uses explicit `lockfilePath` to `.project-map.lock`. Lockfile added to `.gitignore` | 0.4, 5.1 |
|
|
1349
|
+
| **WATCH-4** | `knownFiles` Set must include carried files, not just newly parsed | Explicit construction from `allFiles.keys()` (merged parsed + carried) BEFORE any `resolve()` call. Ordering invariant documented in Step 5.1g. Dedicated test case in resolver tests | 3.1, 5.1, 3.3 |
|