@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.
Files changed (160) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/.nomos-config.json +5 -0
  3. package/CLAUDE.md +108 -0
  4. package/LICENSE +190 -0
  5. package/README.md +569 -0
  6. package/dist/cli.js +21120 -0
  7. package/docs/auth/googel_plan.yaml +1093 -0
  8. package/docs/auth/google_task.md +235 -0
  9. package/docs/auth/hardened_blueprint.yaml +1658 -0
  10. package/docs/auth/red_team_report.yaml +336 -0
  11. package/docs/auth/session_state.yaml +162 -0
  12. package/docs/certificate/cer_enhance_plan.md +605 -0
  13. package/docs/certificate/certificate_report.md +338 -0
  14. package/docs/dev_overview.md +419 -0
  15. package/docs/feature_assessment.md +156 -0
  16. package/docs/how_it_works.md +78 -0
  17. package/docs/infrastructure/map.md +867 -0
  18. package/docs/init/master_plan.md +3581 -0
  19. package/docs/init/red_team_report.md +215 -0
  20. package/docs/init/report_phase_1a.md +304 -0
  21. package/docs/integrity-gate/enhance_drift.md +703 -0
  22. package/docs/integrity-gate/overview.md +108 -0
  23. package/docs/management/manger-task.md +99 -0
  24. package/docs/management/scafffold.md +76 -0
  25. package/docs/map/ATOMIC_BLUEPRINT.md +1349 -0
  26. package/docs/map/RED_TEAM_REPORT.md +159 -0
  27. package/docs/map/map_task.md +147 -0
  28. package/docs/map/semantic_graph_task.md +792 -0
  29. package/docs/map/semantic_master_plan.md +705 -0
  30. package/docs/phase7/TEAM_RED.md +249 -0
  31. package/docs/phase7/plan.md +1682 -0
  32. package/docs/phase7/task.md +275 -0
  33. package/docs/prompts/USAGE.md +312 -0
  34. package/docs/prompts/architect.md +165 -0
  35. package/docs/prompts/executer.md +190 -0
  36. package/docs/prompts/hardener.md +190 -0
  37. package/docs/prompts/red_team.md +146 -0
  38. package/docs/verification/goveranance-overview.md +396 -0
  39. package/docs/verification/governance-overview.md +245 -0
  40. package/docs/verification/verification-arc-ar.md +560 -0
  41. package/docs/verification/verification-architecture.md +560 -0
  42. package/docs/very_next.md +52 -0
  43. package/docs/whitepaper.md +89 -0
  44. package/overview.md +1469 -0
  45. package/package.json +63 -0
  46. package/src/adapters/__tests__/git.test.ts +296 -0
  47. package/src/adapters/__tests__/stdio.test.ts +70 -0
  48. package/src/adapters/git.ts +226 -0
  49. package/src/adapters/pty.ts +159 -0
  50. package/src/adapters/stdio.ts +113 -0
  51. package/src/cli.ts +83 -0
  52. package/src/commands/apply.ts +47 -0
  53. package/src/commands/auth.ts +301 -0
  54. package/src/commands/certificate.ts +89 -0
  55. package/src/commands/discard.ts +24 -0
  56. package/src/commands/drift.ts +116 -0
  57. package/src/commands/index.ts +78 -0
  58. package/src/commands/init.ts +121 -0
  59. package/src/commands/list.ts +75 -0
  60. package/src/commands/map.ts +55 -0
  61. package/src/commands/plan.ts +30 -0
  62. package/src/commands/review.ts +58 -0
  63. package/src/commands/run.ts +63 -0
  64. package/src/commands/search.ts +147 -0
  65. package/src/commands/show.ts +63 -0
  66. package/src/commands/status.ts +59 -0
  67. package/src/core/__tests__/budget.test.ts +213 -0
  68. package/src/core/__tests__/certificate.test.ts +385 -0
  69. package/src/core/__tests__/config.test.ts +191 -0
  70. package/src/core/__tests__/preflight.test.ts +24 -0
  71. package/src/core/__tests__/prompt.test.ts +358 -0
  72. package/src/core/__tests__/review.test.ts +161 -0
  73. package/src/core/__tests__/state.test.ts +362 -0
  74. package/src/core/auth/__tests__/manager.test.ts +166 -0
  75. package/src/core/auth/__tests__/server.test.ts +220 -0
  76. package/src/core/auth/gcp-projects.ts +160 -0
  77. package/src/core/auth/manager.ts +114 -0
  78. package/src/core/auth/server.ts +141 -0
  79. package/src/core/budget.ts +119 -0
  80. package/src/core/certificate.ts +502 -0
  81. package/src/core/config.ts +212 -0
  82. package/src/core/errors.ts +54 -0
  83. package/src/core/factory.ts +49 -0
  84. package/src/core/graph/__tests__/builder.test.ts +272 -0
  85. package/src/core/graph/__tests__/contract-writer.test.ts +175 -0
  86. package/src/core/graph/__tests__/enricher.test.ts +299 -0
  87. package/src/core/graph/__tests__/parser.test.ts +200 -0
  88. package/src/core/graph/__tests__/pipeline.test.ts +202 -0
  89. package/src/core/graph/__tests__/renderer.test.ts +128 -0
  90. package/src/core/graph/__tests__/resolver.test.ts +185 -0
  91. package/src/core/graph/__tests__/scanner.test.ts +231 -0
  92. package/src/core/graph/__tests__/show.test.ts +134 -0
  93. package/src/core/graph/builder.ts +303 -0
  94. package/src/core/graph/constraints.ts +94 -0
  95. package/src/core/graph/contract-writer.ts +93 -0
  96. package/src/core/graph/drift/__tests__/classifier.test.ts +215 -0
  97. package/src/core/graph/drift/__tests__/comparator.test.ts +335 -0
  98. package/src/core/graph/drift/__tests__/drift.test.ts +453 -0
  99. package/src/core/graph/drift/__tests__/reporter.test.ts +203 -0
  100. package/src/core/graph/drift/classifier.ts +165 -0
  101. package/src/core/graph/drift/comparator.ts +205 -0
  102. package/src/core/graph/drift/reporter.ts +77 -0
  103. package/src/core/graph/enricher.ts +251 -0
  104. package/src/core/graph/grammar-paths.ts +30 -0
  105. package/src/core/graph/html-template.ts +493 -0
  106. package/src/core/graph/map-schema.ts +137 -0
  107. package/src/core/graph/parser.ts +336 -0
  108. package/src/core/graph/pipeline.ts +209 -0
  109. package/src/core/graph/renderer.ts +92 -0
  110. package/src/core/graph/resolver.ts +195 -0
  111. package/src/core/graph/scanner.ts +145 -0
  112. package/src/core/logger.ts +46 -0
  113. package/src/core/orchestrator.ts +792 -0
  114. package/src/core/plan-file-manager.ts +66 -0
  115. package/src/core/preflight.ts +64 -0
  116. package/src/core/prompt.ts +173 -0
  117. package/src/core/review.ts +95 -0
  118. package/src/core/state.ts +294 -0
  119. package/src/core/worktree-coordinator.ts +77 -0
  120. package/src/search/__tests__/chunk-extractor.test.ts +339 -0
  121. package/src/search/__tests__/embedder-auth.test.ts +124 -0
  122. package/src/search/__tests__/embedder.test.ts +267 -0
  123. package/src/search/__tests__/graph-enricher.test.ts +178 -0
  124. package/src/search/__tests__/indexer.test.ts +518 -0
  125. package/src/search/__tests__/integration.test.ts +649 -0
  126. package/src/search/__tests__/query-engine.test.ts +334 -0
  127. package/src/search/__tests__/similarity.test.ts +78 -0
  128. package/src/search/__tests__/vector-store.test.ts +281 -0
  129. package/src/search/chunk-extractor.ts +167 -0
  130. package/src/search/embedder.ts +209 -0
  131. package/src/search/graph-enricher.ts +95 -0
  132. package/src/search/indexer.ts +483 -0
  133. package/src/search/lexical-searcher.ts +190 -0
  134. package/src/search/query-engine.ts +225 -0
  135. package/src/search/vector-store.ts +311 -0
  136. package/src/types/index.ts +572 -0
  137. package/src/utils/__tests__/ansi.test.ts +54 -0
  138. package/src/utils/__tests__/frontmatter.test.ts +79 -0
  139. package/src/utils/__tests__/sanitize.test.ts +229 -0
  140. package/src/utils/ansi.ts +19 -0
  141. package/src/utils/context.ts +44 -0
  142. package/src/utils/frontmatter.ts +27 -0
  143. package/src/utils/sanitize.ts +78 -0
  144. package/test/e2e/lifecycle.test.ts +330 -0
  145. package/test/fixtures/mock-planner-hang.ts +5 -0
  146. package/test/fixtures/mock-planner.ts +26 -0
  147. package/test/fixtures/mock-reviewer-bad.ts +8 -0
  148. package/test/fixtures/mock-reviewer-retry.ts +34 -0
  149. package/test/fixtures/mock-reviewer.ts +18 -0
  150. package/test/fixtures/sample-project/src/circular-a.ts +6 -0
  151. package/test/fixtures/sample-project/src/circular-b.ts +6 -0
  152. package/test/fixtures/sample-project/src/config.ts +15 -0
  153. package/test/fixtures/sample-project/src/main.ts +19 -0
  154. package/test/fixtures/sample-project/src/services/product-service.ts +20 -0
  155. package/test/fixtures/sample-project/src/services/user-service.ts +18 -0
  156. package/test/fixtures/sample-project/src/types.ts +14 -0
  157. package/test/fixtures/sample-project/src/utils/index.ts +14 -0
  158. package/test/fixtures/sample-project/src/utils/validate.ts +12 -0
  159. package/tsconfig.json +20 -0
  160. package/vitest.config.ts +12 -0
@@ -0,0 +1,792 @@
1
+ # nomos-arc Semantic Graph — `arc map`
2
+
3
+ ## Architectural Master Plan
4
+
5
+ Transform a raw codebase into a **Self-Aware Knowledge Base** by decoupling **Static Analysis** (fast, free) from **AI Enrichment** (cheap, batched). The result: `project_map.json` — a single artifact that both humans and AI agents consume.
6
+
7
+ ---
8
+
9
+ ## Design Principles
10
+
11
+ 1. **JSON is the source of truth** — consistent with the rest of nomos-arc (state, config, etc.). `.semantic.md` files are rendered views only; they are never parsed back by any code path.
12
+ 2. **Incremental by default** — SHA-256 file hashes mean we only re-analyze what changed.
13
+ 3. **AI is optional** — Phase 1-2 produce a fully usable structural map without spending a single AI token.
14
+ 4. **Fits existing patterns** — new command in `src/commands/map.ts`, core logic in `src/core/graph/`, types in `src/types/index.ts`.
15
+ 5. **Execution Rules compliance** — follows all 14 rules from the master plan: ESM imports, no shell injection, JSON source of truth, schema versioning with migration infrastructure, atomic writes.
16
+
17
+ ---
18
+
19
+ ## Data Model
20
+
21
+ ```typescript
22
+ // ─── Semantic Graph Types ────────────────────────────────────────────────────
23
+
24
+ interface FileNode {
25
+ file: string; // relative path from project root (e.g. "src/core/state.ts")
26
+ hash: string; // "sha256:<hex>" — skip re-analysis when unchanged
27
+ language: string; // "typescript" | "javascript" | "python" | etc.
28
+ symbols: SymbolEntry[]; // classes, functions, exports extracted by parser
29
+ imports: ImportEntry[]; // resolved import references
30
+ dependents: string[]; // relative paths (matching FileNode keys) that import THIS file
31
+ dependencies: string[]; // relative paths (matching FileNode keys) that THIS file imports
32
+ depth: number; // fan-in depth: 0 = nobody imports this file (entry point/leaf),
33
+ // higher = more files depend on it (core module, high risk to modify)
34
+ last_parsed_at: string | null; // ISO 8601 — when this node was last actively parsed
35
+ // null for carry-over nodes in incremental runs
36
+ semantic: SemanticInfo | null; // null until AI enrichment runs
37
+ }
38
+
39
+ interface SymbolEntry {
40
+ name: string; // "UserService", "handleAuth", etc.
41
+ // for class methods: "ClassName.methodName"
42
+ kind: 'class' | 'function' | 'method' | 'interface' | 'type' | 'enum' | 'variable' | 'export';
43
+ line: number; // start line (1-indexed)
44
+ end_line: number | null; // end line (1-indexed), null if parser cannot determine
45
+ signature: string | null; // "async handleAuth(req: Request): Promise<Response>"
46
+ exported: boolean;
47
+ }
48
+
49
+ interface ImportEntry {
50
+ source: string; // raw import path ("./utils/hash", "zod", etc.)
51
+ resolved: string | null; // canonical relative path from project root WITH extension
52
+ // e.g. "src/utils/hash.ts" — always matches a FileNode key
53
+ // null for external packages or unresolvable paths
54
+ symbols: string[]; // named imports ["z", "ZodSchema"]
55
+ is_external: boolean;
56
+ }
57
+
58
+ interface SemanticInfo {
59
+ overview: string; // one-paragraph summary
60
+ purpose: string; // business problem it solves
61
+ key_logic: string[]; // step-by-step algorithmic breakdown
62
+ usage_context: string[]; // how dependents use this file (from dependency data)
63
+ source_hash: string; // "sha256:<hex>" of the file content at enrichment time
64
+ // used for staleness check: re-enrich only when hash changes
65
+ enriched_at: string; // ISO 8601 — when AI last analyzed this
66
+ model: string; // which model produced this ("gemini-1.5-flash", etc.)
67
+ }
68
+
69
+ interface ProjectMap {
70
+ schema_version: 1; // starts at 1; run migrateProjectMap() on read if older
71
+ generated_at: string; // ISO 8601
72
+ root: string; // absolute path of the project root — RUNTIME ONLY
73
+ // not meaningful across machines; do not rely on for portability
74
+ files: Record<string, FileNode>; // keyed by relative file path (matches FileNode.file)
75
+ stats: {
76
+ total_files: number;
77
+ total_symbols: number;
78
+ total_edges: number; // count of internal (non-external) resolved dependency links only
79
+ core_modules: string[]; // top N files by depth — N = config.graph.core_modules_count
80
+ };
81
+ }
82
+ ```
83
+
84
+ ### Depth Computation
85
+
86
+ `depth` is computed by **BFS topological sort on the dependency graph** where an edge A → B means "A imports B":
87
+
88
+ - In-degree of a node = number of files that import it (`dependents.length`)
89
+ - **Depth 0**: nodes with in-degree 0 — entry points that no other file imports (e.g. `src/cli.ts`, `src/commands/plan.ts`)
90
+ - **Depth N**: nodes that become reachable only after all their importers are processed
91
+ - Core utilities (`src/types/index.ts`, `src/core/config.ts`) have the highest depth — many files depend on them
92
+
93
+ Algorithm: Kahn's BFS. If cycles are detected, assign the highest depth among nodes entering the cycle + 1, log a warning, and continue.
94
+
95
+ ### ProjectMap Schema & Migrations
96
+
97
+ Following Execution Rule 11, a Zod schema and migration infrastructure must exist from day one:
98
+
99
+ ```typescript
100
+ // In src/core/graph/map-schema.ts
101
+ import { z } from 'zod';
102
+
103
+ const SymbolEntrySchema = z.object({ ... });
104
+ const ImportEntrySchema = z.object({ ... });
105
+ const SemanticInfoSchema = z.object({ ... });
106
+ const FileNodeSchema = z.object({ ... });
107
+
108
+ export const ProjectMapSchema = z.object({
109
+ schema_version: z.literal(1),
110
+ generated_at: z.string(),
111
+ root: z.string(),
112
+ files: z.record(z.string(), FileNodeSchema),
113
+ stats: z.object({
114
+ total_files: z.number(),
115
+ total_symbols: z.number(),
116
+ total_edges: z.number(),
117
+ core_modules: z.array(z.string()),
118
+ }),
119
+ });
120
+
121
+ // Migration map — empty now, infrastructure required from day one
122
+ const migrations: Record<number, (raw: unknown) => unknown> = {};
123
+
124
+ export function migrateProjectMap(raw: unknown): ProjectMap {
125
+ let current = raw as { schema_version?: number };
126
+ const version = current.schema_version ?? 0;
127
+ for (let v = version; v < 1; v++) {
128
+ const migrateFn = migrations[v];
129
+ if (migrateFn) current = migrateFn(current) as typeof current;
130
+ }
131
+ return ProjectMapSchema.parse(current) as ProjectMap;
132
+ }
133
+ ```
134
+
135
+ ---
136
+
137
+ ## NomosConfig Extension
138
+
139
+ Add to `NomosConfig` in `src/types/index.ts` and `GraphConfigSchema` in `src/core/config.ts`:
140
+
141
+ ```typescript
142
+ // In NomosConfig interface (types/index.ts)
143
+ graph: {
144
+ exclude_patterns: string[]; // glob patterns to exclude from scanning
145
+ ai_enrichment: boolean; // default: true — set false for --no-ai behavior
146
+ ai_model: string; // default: "gemini-1.5-flash"
147
+ ai_concurrency: number; // default: 5 (max concurrent Gemini requests)
148
+ ai_requests_per_minute: number; // default: 14 (Gemini free tier: 15 RPM, leave headroom)
149
+ max_file_chars: number; // default: 4000 — truncate at last complete line before limit
150
+ core_modules_count: number; // default: 10 — top N files in stats.core_modules
151
+ output_dir: string; // default: "tasks-management/graph"
152
+ };
153
+ ```
154
+
155
+ ```typescript
156
+ // In config.ts — GraphConfigSchema
157
+ const GraphConfigSchema = z.object({
158
+ exclude_patterns: z.array(z.string()).default(['node_modules', 'dist', '*.test.*', '*.spec.*', '*.semantic.md']),
159
+ ai_enrichment: z.boolean().default(true),
160
+ ai_model: z.string().default('gemini-1.5-flash'),
161
+ ai_concurrency: z.number().int().positive().default(5),
162
+ ai_requests_per_minute: z.number().int().positive().default(14),
163
+ max_file_chars: z.number().positive().default(4000),
164
+ core_modules_count: z.number().int().positive().default(10),
165
+ output_dir: z.string().default('tasks-management/graph'),
166
+ });
167
+ // Add to NomosConfigSchema: graph: GraphConfigSchema.default(() => GraphConfigSchema.parse({}))
168
+ ```
169
+
170
+ ---
171
+
172
+ ## New Error Codes
173
+
174
+ Add to `NomosErrorCode` in `src/core/errors.ts`:
175
+
176
+ ```typescript
177
+ | 'graph_map_not_found' // project_map.json missing when arc show map runs
178
+ | 'graph_parse_error' // tree-sitter or file read failed for a file
179
+ | 'graph_ai_key_missing' // GEMINI_API_KEY not set when ai_enrichment is true
180
+ | 'graph_write_failed' // project_map.json atomic write failed
181
+ ```
182
+
183
+ ---
184
+
185
+ ## File Extension → Language Map
186
+
187
+ ```typescript
188
+ const LANGUAGE_MAP: Record<string, string> = {
189
+ '.ts': 'typescript',
190
+ '.tsx': 'typescript', // uses tsx grammar in tree-sitter-typescript
191
+ '.mts': 'typescript',
192
+ '.cts': 'typescript',
193
+ '.d.ts':'typescript', // declaration files — symbols extracted but no imports
194
+ '.js': 'javascript',
195
+ '.mjs': 'javascript',
196
+ '.cjs': 'javascript',
197
+ '.jsx': 'javascript',
198
+ '.py': 'python',
199
+ '.go': 'go',
200
+ '.rs': 'rust',
201
+ };
202
+ // Files with unrecognized extensions are skipped (not an error).
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Pipeline Overview
208
+
209
+ ```
210
+ arc map [--no-ai] [--force] [glob patterns...]
211
+ |
212
+ v
213
+ Phase 1: File Discovery & Hashing
214
+ | fast-glob + gitignore-parser + node:crypto
215
+ v
216
+ Phase 2: AST Parsing & Symbol Extraction
217
+ | tree-sitter (ts/tsx/js) — external to esbuild bundle
218
+ v
219
+ Phase 3: Import Resolution & Dependency Graph
220
+ | custom resolver + Kahn's BFS topological sort
221
+ v
222
+ Phase 4: AI Semantic Enrichment (skippable with --no-ai)
223
+ | Gemini 1.5 Flash — p-limit(5), 4s inter-request gap, 3x retry
224
+ v
225
+ Phase 5: Output
226
+ | Atomic write project_map.json + .semantic.md files
227
+ v
228
+ Phase 6: Visualization (arc show map)
229
+ | Generate standalone index.html with Cytoscape.js
230
+ v
231
+ Done — used by `arc plan` for Architectural Constraint injection
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Phase 1: File Discovery & Hashing
237
+
238
+ **Goal:** Find all source files and compute their SHA-256 hashes for incremental updates.
239
+
240
+ **Input:** Project root + optional glob filters.
241
+ **Output:** List of `{ file, hash, language }` entries.
242
+
243
+ **How it works:**
244
+ 1. **First-run detection:** If `project_map.json` does not yet exist, treat all files as new (no hash matches — full parse required).
245
+ 2. **Gitignore parsing:** `fast-glob` does NOT natively respect `.gitignore`. Use the `gitignore-parser` package to read `.gitignore` at the project root, convert its patterns, and pass them to `fast-glob`'s `ignore` option alongside `config.graph.exclude_patterns`.
246
+ 3. **Hash each file:** Compute `sha256:<hex>` using `node:crypto`. If a file cannot be read (`EACCES`, `ENOENT`), log `[nomos:graph:warn] Cannot read {file} — skipping` and continue (do not throw).
247
+ 4. **Incremental skip:** Compare each file's hash against the existing `project_map.json`. If the hash matches AND `--force` is not set, carry the existing `FileNode` forward unchanged (mark `last_parsed_at` unchanged).
248
+ 5. **Language detection:** Use `LANGUAGE_MAP` above. Files with unknown extensions are skipped silently.
249
+
250
+ **New dependencies:** `fast-glob`, `gitignore-parser`
251
+
252
+ ---
253
+
254
+ ## Phase 2: AST Parsing & Symbol Extraction
255
+
256
+ **Goal:** Extract the structural skeleton — every class, function, interface, type, and export — without AI.
257
+
258
+ **Input:** File content + language.
259
+ **Output:** `symbols: SymbolEntry[]` + `imports: ImportEntry[]` per file.
260
+
261
+ **How it works:**
262
+ 1. **Grammar selection:** `tree-sitter-typescript` ships two grammars. Use `tsx` grammar for `.tsx` files; use `typescript` grammar for all other TypeScript variants (`.ts`, `.mts`, `.cts`).
263
+ 2. **Error node handling:** If the parsed tree contains error nodes covering > 20% of the file (by character count), log `[nomos:graph:warn] Parse errors in {file} — symbols may be incomplete` and emit `symbols: []` and `imports: []` for that file. Do not fail the pipeline.
264
+ 3. **Symbol extraction:** Walk the tree to extract:
265
+ - Top-level function/arrow-function declarations (name, line, end_line, signature, exported)
266
+ - Class declarations (name, line, end_line, exported) + their method declarations as `kind: 'method'` with `name: 'ClassName.methodName'`
267
+ - Interface/type/enum declarations
268
+ - Top-level `export const` / `export let` variable declarations
269
+ 4. **Signature capture:** For functions, capture the full text span from the name through the closing `)` of the parameter list plus return type annotation. For class methods, include the `async` keyword and access modifier if present.
270
+ 5. **Import extraction:** During the same AST walk, extract all `import` declarations and `require()` calls. Populate `ImportEntry.source` with the raw module specifier; leave `resolved` as `null` for now (Phase 3 fills it).
271
+
272
+ **esbuild note:** `tree-sitter` uses N-API native bindings (`.node` files) that cannot be bundled by esbuild. The `package.json` build script must include:
273
+ ```
274
+ --external:tree-sitter --external:tree-sitter-typescript
275
+ ```
276
+
277
+ **New dependencies:** `tree-sitter`, `tree-sitter-typescript`
278
+
279
+ ---
280
+
281
+ ## Phase 3: Import Resolution & Dependency Graph
282
+
283
+ **Goal:** Map every `import`/`require` to its resolved file, then build a directed dependency graph.
284
+
285
+ **Input:** `imports: ImportEntry[]` per file.
286
+ **Output:** `dependencies[]`, `dependents[]`, `depth` per file + `stats.core_modules`.
287
+
288
+ **How it works:**
289
+
290
+ 1. **Path resolution algorithm** (tried in this order for relative imports):
291
+ - Append `.ts`, `.tsx`, `.js` in that order and check existence
292
+ - Append `/index.ts`, `/index.tsx`, `/index.js` in that order
293
+ - If none resolve, set `resolved: null` (missing file — log warning, do not fail)
294
+
295
+ 2. **tsconfig alias resolution:**
296
+ - Read only `tsconfig.json` at the project root (no project references in Phase 1)
297
+ - Parse `compilerOptions.paths` — a `Record<string, string[]>` where keys may contain `*` wildcards
298
+ - Match import source against each key pattern in declaration order; on match, substitute the `*` capture into the corresponding value pattern, then apply the relative path resolution above
299
+ - Example: `"@/*": ["src/*"]` — import `@/core/state` → try `src/core/state.ts` etc.
300
+ - If `tsconfig.json` does not exist or has no `paths`, skip alias resolution
301
+
302
+ 3. **External packages:** Any import that does not start with `.`, `/`, or a matched tsconfig alias is `is_external: true`, `resolved: null`.
303
+
304
+ 4. **Path traversal guard:** If a resolved absolute path falls outside `projectRoot`, treat as external (`is_external: true`, `resolved: null`). Log `[nomos:graph:warn] Import escapes project root: {source}`.
305
+
306
+ 5. **Build adjacency list:**
307
+ - Forward edges: `A` imports `B` → `A.dependencies.push(B.file)`
308
+ - Reverse edges: → `B.dependents.push(A.file)`
309
+ - All values are relative paths matching `FileNode` keys
310
+
311
+ 6. **Topological sort (Kahn's BFS):**
312
+ - Compute in-degree for each node (`dependents.length`)
313
+ - BFS from all depth-0 nodes (in-degree 0 = nobody imports this file)
314
+ - Assign `depth` = BFS layer
315
+ - Extract `stats.core_modules` = top `config.graph.core_modules_count` files by depth
316
+
317
+ 7. **Cycle detection:** If Kahn's queue empties before all nodes are processed, remaining nodes form cycles. Assign them `depth = max_depth_of_cycle_entering_nodes + 1`. Log:
318
+ ```
319
+ [nomos:graph:warn] Circular dependency detected: src/a.ts → src/b.ts → src/a.ts
320
+ ```
321
+
322
+ 8. **Partial glob runs:** When `arc map "src/core/**"` is used, only matched files are scanned and their `dependencies[]` are updated. `dependents[]` back-references from unscanned files will be incomplete. The stats section will reflect only scanned files. Run `arc map` without patterns for a complete graph.
323
+
324
+ **New dependencies:** None (custom implementation).
325
+
326
+ ---
327
+
328
+ ## Phase 4: AI Semantic Enrichment
329
+
330
+ **Goal:** Use AI to explain the *why* and *how* of each file — skip with `--no-ai`.
331
+
332
+ **Input:** File content + `dependents[]` list + `symbols[]`.
333
+ **Output:** `semantic: SemanticInfo` per file + `.semantic.md` files on disk.
334
+
335
+ **How it works:**
336
+
337
+ 1. **GEMINI_API_KEY validation:** At `SemanticEnricher` construction time (before any API calls), check `process.env.GEMINI_API_KEY`. If absent and `config.graph.ai_enrichment` is true, throw `NomosError('graph_ai_key_missing', 'GEMINI_API_KEY environment variable is not set.')`.
338
+
339
+ 2. **Staleness filter:** Enrich a file only when:
340
+ - `FileNode.semantic === null`, OR
341
+ - `FileNode.hash !== FileNode.semantic.source_hash` (file changed since last enrichment)
342
+
343
+ 3. **Prompt per file** (content truncated at last complete line boundary before `config.graph.max_file_chars` characters):
344
+ ```
345
+ File: {relative_path}
346
+ Language: {language}
347
+ Exports: {symbol names and signatures}
348
+ Used by: {dependent file list}
349
+ ---
350
+ {file content}
351
+ ---
352
+ Respond in JSON matching this schema exactly:
353
+ { "overview": string, "purpose": string, "key_logic": string[], "usage_context": string[] }
354
+ ```
355
+
356
+ 4. **Gemini structured output:** Pass `responseSchema` to `generationConfig`:
357
+ ```typescript
358
+ const responseSchema = {
359
+ type: 'object',
360
+ properties: {
361
+ overview: { type: 'string' },
362
+ purpose: { type: 'string' },
363
+ key_logic: { type: 'array', items: { type: 'string' } },
364
+ usage_context: { type: 'array', items: { type: 'string' } },
365
+ },
366
+ required: ['overview', 'purpose', 'key_logic', 'usage_context'],
367
+ };
368
+ ```
369
+
370
+ 5. **Rate limiting:** Gemini 1.5 Flash free tier allows 15 RPM. Use `p-limit` with concurrency = `config.graph.ai_concurrency` (default 5) and a minimum 4-second gap between requests (4s × 15 = 60s window).
371
+
372
+ 6. **Retry policy:** On HTTP 429 or 503, retry up to 3 times with exponential backoff: 2s, 4s, 8s. After 3 failures, set `semantic: null` and log `[nomos:graph:error] AI enrichment failed for {file} after 3 retries — skipping`.
373
+
374
+ 7. **Zod validation:** If the response parses but fails Zod validation, log the raw response (truncated to 200 chars) and set `semantic: null`. Do not throw.
375
+
376
+ 8. **Populate SemanticInfo:**
377
+ ```typescript
378
+ {
379
+ overview, purpose, key_logic, usage_context,
380
+ source_hash: fileNode.hash, // snapshot the current hash
381
+ enriched_at: new Date().toISOString(),
382
+ model: config.graph.ai_model,
383
+ }
384
+ ```
385
+
386
+ 9. **Write `.semantic.md` Semantic Contract** next to the source file:
387
+ - `src/core/state.ts` → write `src/core/state.semantic.md`
388
+ - If the file already exists AND `source_hash` matches the current hash, skip writing (no change)
389
+ - Template:
390
+ ```markdown
391
+ # state.ts — Semantic Contract
392
+ > Auto-generated by `arc map` — do not edit manually.
393
+
394
+ ## Overview
395
+ {overview}
396
+
397
+ ## Purpose
398
+ {purpose}
399
+
400
+ ## Key Logic
401
+ {key_logic as numbered list}
402
+
403
+ ## Usage Context
404
+ Used by: {dependent files}
405
+ {usage_context as bullet list}
406
+
407
+ ---
408
+ *Enriched at: {enriched_at} | Model: {model}*
409
+ ```
410
+
411
+ 10. **Graceful degradation:** A failed file never blocks the pipeline. The map is written with `semantic: null` for failed files.
412
+
413
+ **New dependencies:** `@google/generative-ai`, `p-limit`
414
+
415
+ **esbuild note:** Add `--external:@google/generative-ai` to the build script (ESM/CJS compatibility).
416
+
417
+ ---
418
+
419
+ ## Phase 5: Output & Consumption
420
+
421
+ **Goal:** Write `project_map.json` atomically and inject Architectural Constraints into `arc plan`.
422
+
423
+ **Output artifacts:**
424
+ - `{config.graph.output_dir}/project_map.json` — full map, JSON, source of truth.
425
+ - `*.semantic.md` files next to each source file (written in Phase 4).
426
+
427
+ **Atomic write for `project_map.json`:**
428
+ Follow the same `tmp → fsync → rename` pattern used by `StateManager.write()` in `src/core/state.ts`:
429
+ ```typescript
430
+ const outDir = path.join(projectRoot, config.graph.output_dir);
431
+ await fs.mkdir(outDir, { recursive: true });
432
+ const mapPath = path.join(outDir, 'project_map.json');
433
+ const tmpPath = `${mapPath}.tmp`;
434
+ await fs.writeFile(tmpPath, JSON.stringify(map, null, 2), 'utf-8');
435
+ const fd = await fs.open(tmpPath, 'r+');
436
+ try { await fd.sync(); } finally { await fd.close(); }
437
+ await fs.rename(tmpPath, mapPath);
438
+ ```
439
+ If the write fails, throw `NomosError('graph_write_failed', ...)`.
440
+
441
+ **Architectural Constraint injection into `arc plan`:**
442
+
443
+ The injection happens in THREE layers:
444
+
445
+ **Layer 5.4a — Extend `PromptOptions`** (`src/types/index.ts`):
446
+ ```typescript
447
+ export interface PromptOptions {
448
+ // ... existing fields ...
449
+ architecturalConstraints: string | null; // null when no project_map.json exists
450
+ }
451
+ ```
452
+
453
+ **Layer 5.4b — Implement `readArchitecturalConstraints`** (new utility in `src/core/graph/constraints.ts`):
454
+ ```typescript
455
+ export async function readArchitecturalConstraints(
456
+ projectRoot: string,
457
+ outputDir: string,
458
+ contextFiles: string[],
459
+ ): Promise<string | null>
460
+ ```
461
+ - Reads `project_map.json` from `path.join(projectRoot, outputDir, 'project_map.json')`
462
+ - If file does not exist, returns `null` (no constraints injected — not an error)
463
+ - For each `contextFile`, look it up in `ProjectMap.files`
464
+ - If absent from map: log `[nomos:graph:warn] {file} not in project map — run arc map to update` and skip it
465
+ - For each found `FileNode`, collect `dependents[]` that have non-null `semantic`, and identify the specific `symbols[]` they consume
466
+ - Build the constraint string (see format below)
467
+ - Returns `null` if no constraints were found
468
+
469
+ **Layer 5.4c — Wire into `Orchestrator.plan()`** (`src/core/orchestrator.ts`):
470
+ - Before calling `assemblePrompt(...)`, call `readArchitecturalConstraints(projectRoot, config.graph.output_dir, contextFiles)`
471
+ - Pass the result into `PromptOptions.architecturalConstraints`
472
+
473
+ **Constraint section format in `assemblePrompt`** (`src/core/prompt.ts`):
474
+ ```typescript
475
+ if (options.architecturalConstraints !== null) {
476
+ sections.push(
477
+ `[ARCHITECTURAL CONSTRAINTS]\n` +
478
+ `The following contracts MUST NOT be broken. Changing the listed symbol signatures ` +
479
+ `or return types will break dependent modules.\n\n` +
480
+ options.architecturalConstraints
481
+ );
482
+ }
483
+ ```
484
+
485
+ **Constraint string example:**
486
+ ```
487
+ - src/core/state.ts → symbol: readState(taskId: string): TaskState
488
+ Consumed by: orchestrator.ts, status.ts
489
+ Contract: "Return type must remain TaskState — orchestrator destructures .version and .status directly"
490
+
491
+ - src/adapters/git.ts → symbol: commitToShadowBranch(files: string[]): Promise<void>
492
+ Consumed by: apply.ts, worktree-coordinator.ts
493
+ Contract: "Parameter must remain string[] — callers build the list explicitly"
494
+ ```
495
+
496
+ ---
497
+
498
+ ## Phase 6: Visualization — `arc show map`
499
+
500
+ **Goal:** Generate a standalone interactive HTML graph visualizing the dependency hierarchy.
501
+
502
+ **Input:** `project_map.json`
503
+ **Output:** `{config.graph.output_dir}/index.html` — opens in browser, zero server needed.
504
+
505
+ **Missing map behavior:** If `project_map.json` does not exist, exit with code 1:
506
+ ```
507
+ [nomos:error] No project map found. Run: arc map first.
508
+ ```
509
+
510
+ **How it works:**
511
+ 1. **Read `project_map.json`**, run `migrateProjectMap()` on load. Transform into Cytoscape.js graph format:
512
+ - Each `FileNode` → **node** (label = filename, data = full FileNode)
513
+ - Each dependency link → **directed edge** (source → target)
514
+ 2. **Layout:** Cytoscape **Breadth-First (BFS) layout** — roots at top (core modules), leaves at bottom.
515
+ 3. **Depth-based coloring:**
516
+ - Map each file's `depth` to a color gradient:
517
+ - **Depth 0 (leaves/entry points):** `#a8d8ea` — cool blue (low risk)
518
+ - **Mid depth:** `#f9c784` — warm amber (moderate risk)
519
+ - **Max depth (core):** `#c0392b` — deep red (high risk — modify carefully)
520
+ - Visual danger signal: darker = more files depend on this.
521
+ 4. **Visual Ripple Analysis:** On node click, propagate a highlight across the graph:
522
+ - **Red (`#e74c3c`):** direct dependents (`dependents[]`, depth +1) — files that immediately break
523
+ - **Yellow (`#f39c12`):** transitive dependents (dependents of dependents) — indirect blast radius
524
+ - All other nodes dim to `#bdc3c7` (grey), making the blast radius instantly visible
525
+ - Click empty canvas to reset all highlights to default depth coloring
526
+ 5. **Interactive Context Cards:** Clicking a node expands a fixed side panel (right, 320px) showing:
527
+ - Filename + language badge
528
+ - `semantic.overview` and `semantic.purpose` (or "Run arc map --ai to enrich" if `semantic === null`)
529
+ - `dependents[]` list as clickable items that re-center the graph on that node
530
+ - `symbols[]` list with kind badges and signatures
531
+ 6. **Symbol Search & Path Tracing:** A search input in the top bar:
532
+ - Accepts a symbol name (e.g. `readState`) or file path fragment (e.g. `state`)
533
+ - Matches against `FileNode.symbols[].name` and `FileNode.file`
534
+ - Highlights all matched nodes in **green** and dims the rest
535
+ - Traces the full dependency path: for each match, also highlights the chain of importers
536
+ - Results update live (client-side filter against embedded JSON — no server needed)
537
+ - Press Escape or clear the input to reset
538
+ 7. **Generate standalone HTML:**
539
+ - Cytoscape.js pinned CDN: `https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js`
540
+ - Embed `project_map.json` data inline as `const PROJECT_MAP = { ... };`
541
+ - One self-contained `index.html` — no build step, no server
542
+
543
+ **Browser open:** Use the `open` npm package (not `child_process.exec` — shell injection risk). Call `open(htmlPath)` only when `--no-open` flag is absent.
544
+
545
+ **esbuild note:** Add `--external:open` to the build script.
546
+
547
+ **`arc show map` commander registration:**
548
+ `arc show map` is a nested subcommand. Register a parent `show` command group first:
549
+ ```typescript
550
+ // In src/commands/show.ts
551
+ export function registerShowCommand(program: Command): void {
552
+ const show = program.command('show').description('Display visual outputs');
553
+ show
554
+ .command('map')
555
+ .description('Open the interactive dependency graph in your browser')
556
+ .option('--no-open', 'Generate index.html without opening browser')
557
+ .action(async (opts) => { /* ... */ });
558
+ }
559
+ ```
560
+
561
+ **New dependencies:** `open` (runtime), `@types/cytoscape` (devDependency for template type safety)
562
+
563
+ ---
564
+
565
+ ## Atomic Implementation Plan
566
+
567
+ Each task is a single, testable unit of work. Tasks within a phase can be parallelized.
568
+
569
+ ### Phase 0: Prerequisites (do before any graph code)
570
+
571
+ | # | Task | Deliverables | Depends On |
572
+ |---|------|-------------|------------|
573
+ | 0.1 | Add `fast-glob`, `gitignore-parser`, `p-limit`, `open` to `package.json` | `package.json` | — |
574
+ | 0.2 | Add `tree-sitter`, `tree-sitter-typescript`, `@google/generative-ai` to `package.json` and update esbuild script with `--external:tree-sitter --external:tree-sitter-typescript --external:@google/generative-ai --external:open` | `package.json` | 0.1 |
575
+ | 0.3 | Add new `NomosErrorCode` values (`graph_map_not_found`, `graph_parse_error`, `graph_ai_key_missing`, `graph_write_failed`) to `src/core/errors.ts` | `src/core/errors.ts` | — |
576
+ | 0.4 | Update `.gitignore` — add `*.semantic.md` and `tasks-management/graph/` entries, document the VCS policy | `.gitignore` | — |
577
+
578
+ ### Phase 1: Foundation (File Discovery)
579
+
580
+ | # | Task | Deliverables | Depends On |
581
+ |---|------|-------------|------------|
582
+ | 1.1 | Define all Semantic Graph types in `src/types/index.ts` (`FileNode`, `SymbolEntry`, `ImportEntry`, `SemanticInfo`, `ProjectMap`) | `src/types/index.ts` | 0.3 |
583
+ | 1.2 | Add `graph` section to `NomosConfig` interface and `GraphConfigSchema` Zod schema with all defaults | `src/types/index.ts`, `src/core/config.ts` | 1.1 |
584
+ | 1.3 | Implement `ProjectMapSchema`, `migrateProjectMap()`, and `readProjectMap()` (atomic read + migration) | `src/core/graph/map-schema.ts` | 1.1 |
585
+ | 1.4 | Implement `FileScanner` — gitignore-aware glob + hash + language detection + incremental skip logic | `src/core/graph/scanner.ts` | 0.1, 1.1 |
586
+ | 1.5 | Test `FileScanner` — finds files, computes hashes, detects languages, excludes node_modules, handles unreadable files, skips unchanged hashes, respects --force | `src/core/graph/__tests__/scanner.test.ts` | 1.4 |
587
+
588
+ ### Phase 2: Parsing (Symbol Extraction)
589
+
590
+ | # | Task | Deliverables | Depends On |
591
+ |---|------|-------------|------------|
592
+ | 2.1 | Implement `ASTParser` — grammar selection (ts vs tsx), error-node threshold check, symbol extraction with `end_line` and method names | `src/core/graph/parser.ts` | 0.2, 1.1 |
593
+ | 2.2 | Implement import extraction in `ASTParser` — populate `ImportEntry.source` and `symbols[]`, leave `resolved: null` | `src/core/graph/parser.ts` | 2.1 |
594
+ | 2.3 | Test `ASTParser` — symbols + imports from sample TS files, tsx grammar selection, error-node fallback, class method extraction | `src/core/graph/__tests__/parser.test.ts` | 2.1, 2.2 |
595
+
596
+ ### Phase 3: Graph (Dependencies & Topology)
597
+
598
+ | # | Task | Deliverables | Depends On |
599
+ |---|------|-------------|------------|
600
+ | 3.1 | Implement `ImportResolver` — relative path resolution (extensions + index), tsconfig alias resolution, external detection, path traversal guard | `src/core/graph/resolver.ts` | 1.1 |
601
+ | 3.2 | Implement `GraphBuilder` — adjacency list, Kahn's BFS depth, cycle detection with warning, `stats.core_modules` | `src/core/graph/builder.ts` | 1.1, 3.1 |
602
+ | 3.3 | Test `ImportResolver` — relative paths, index resolution, tsconfig aliases with wildcards, externals, path traversal, unresolvable paths | `src/core/graph/__tests__/resolver.test.ts` | 3.1 |
603
+ | 3.4 | Test `GraphBuilder` — forward/reverse edges, depth calc, 2-node cycle, 3-node cycle, `core_modules` extraction | `src/core/graph/__tests__/builder.test.ts` | 3.2 |
604
+
605
+ ### Phase 4: AI Enrichment + Semantic Contracts
606
+
607
+ | # | Task | Deliverables | Depends On |
608
+ |---|------|-------------|------------|
609
+ | 4.1 | Implement `SemanticEnricher` — GEMINI_API_KEY validation, staleness check via `source_hash`, prompt builder with line-boundary truncation, Gemini structured output call, Zod validation, retry policy (3x backoff on 429/503), p-limit rate limiter | `src/core/graph/enricher.ts` | 0.1, 0.2, 1.1, 1.2, 2.1, 3.2 |
610
+ | 4.2 | Implement `ContractWriter` — renders `SemanticInfo` into `.semantic.md` using source_hash for overwrite skip | `src/core/graph/contract-writer.ts` | 1.1 |
611
+ | 4.3 | Test `SemanticEnricher` — mock `@google/generative-ai` with `vi.mock()`, validate staleness check, Zod failure fallback, retry on 429, key missing error | `src/core/graph/__tests__/enricher.test.ts` | 4.1 |
612
+ | 4.4 | Test `ContractWriter` — generates correct Markdown structure, skips overwrite when source_hash unchanged, overwrites when hash changed | `src/core/graph/__tests__/contract-writer.test.ts` | 4.2 |
613
+
614
+ ### Phase 5: Command & Integration
615
+
616
+ | # | Task | Deliverables | Depends On |
617
+ |---|------|-------------|------------|
618
+ | 5.1 | Implement `MapPipeline` — constructor `(config: NomosConfig, projectRoot: string, logger: Logger)`, chains scanner → parser → resolver → builder → enricher → contract-writer, creates `tasks-management/graph/` dir, atomic write of `project_map.json` | `src/core/graph/pipeline.ts` | 1.4, 2.1, 3.1, 3.2, 4.1, 4.2 |
619
+ | 5.2 | Implement `arc map` command — wires `MapPipeline` via `createOrchestrator()` factory, handles `--no-ai` (sets `config.graph.ai_enrichment = false`), `--force`, and glob args; exits 0 on success, 1 on fatal error, 2 on partial AI failure | `src/commands/map.ts` | 5.1 |
620
+ | 5.3 | Register `arc map` in CLI entry point | `src/cli.ts` | 5.2 |
621
+ | 5.4a | Extend `PromptOptions` with `architecturalConstraints: string \| null` | `src/types/index.ts` | 1.1 |
622
+ | 5.4b | Implement `readArchitecturalConstraints(projectRoot, outputDir, contextFiles): Promise<string \| null>` — reads map, skips missing files with warning, builds constraint string with symbol signatures | `src/core/graph/constraints.ts` | 1.3, 5.4a |
623
+ | 5.4c | Wire `readArchitecturalConstraints` into `Orchestrator.plan()` before `assemblePrompt`; add `[ARCHITECTURAL CONSTRAINTS]` section to `assemblePrompt` in `prompt.ts` | `src/core/orchestrator.ts`, `src/core/prompt.ts` | 5.4a, 5.4b |
624
+ | 5.5 | Test `MapPipeline` end-to-end — scan + parse + resolve + build on `test/fixtures/sample-project/` fixture; incremental run skips unchanged files; `--force` re-parses all | `src/core/graph/__tests__/pipeline.test.ts` | 5.1 |
625
+ | 5.6 | Test Architectural Constraint injection — write minimal `project_map.json` to tmpdir, assert `[ARCHITECTURAL CONSTRAINTS]` section appears in assembled prompt with correct symbol signatures; assert missing-from-map files log a warning | `src/core/__tests__/prompt.test.ts` | 5.4c |
626
+
627
+ ### Phase 6: Visualization (`arc show map`)
628
+
629
+ | # | Task | Deliverables | Depends On |
630
+ |---|------|-------------|------------|
631
+ | 6.1 | Implement `MapRenderer` — transforms `ProjectMap` into Cytoscape.js node/edge JSON, computes depth-to-hex-color mapping using the pinned gradient | `src/core/graph/renderer.ts` | 1.1 |
632
+ | 6.2a | Create HTML template — static shell, CDN Cytoscape.js (pinned 3.28.1), BFS layout, depth coloring via inline `<style>` | `src/core/graph/html-template.ts` | 6.1 |
633
+ | 6.2b | Add click-to-expand context card panel to HTML template — side panel with overview, purpose, dependents (clickable), symbols list | `src/core/graph/html-template.ts` | 6.2a |
634
+ | 6.2c | Add ripple analysis to HTML template — on node click: highlight direct dependents red, transitive yellow, dim rest; click canvas to reset | `src/core/graph/html-template.ts` | 6.2b |
635
+ | 6.2d | Add symbol search bar to HTML template — live filter matching `FileNode.symbols[].name` and `FileNode.file`, highlight matches in green + trace dependency path | `src/core/graph/html-template.ts` | 6.2c |
636
+ | 6.3 | Implement `arc show map` command — reads map via `readProjectMap()`, calls `MapRenderer`, writes `index.html` to `output_dir`, opens with `open` package (unless `--no-open`) | `src/commands/show.ts` | 6.1, 6.2d |
637
+ | 6.4 | Register `arc show map` under a `show` parent command group in CLI entry point | `src/cli.ts` | 6.3 |
638
+ | 6.5 | Test `MapRenderer` — correct node/edge counts, color gradient mapping at depth 0 / mid / max, context card data fields, empty-map edge case | `src/core/graph/__tests__/renderer.test.ts` | 6.1 |
639
+ | 6.6 | Test `arc show map` command — map missing → exit 1 with correct message; map present → `index.html` written to correct path; `--no-open` suppresses browser launch | `src/core/graph/__tests__/show.test.ts` | 6.3 |
640
+
641
+ ---
642
+
643
+ ## Test Fixture — `test/fixtures/sample-project/`
644
+
645
+ All graph tests share a minimal fixture project with known relationships. Create these files:
646
+
647
+ ```
648
+ test/fixtures/sample-project/
649
+ tsconfig.json # { "compilerOptions": { "paths": { "@/utils": ["src/utils/index.ts"] } } }
650
+ src/
651
+ types.ts # exports: interface User, type UserId
652
+ utils/
653
+ index.ts # exports: function hash(s: string): string — imports nothing
654
+ format.ts # exports: function formatUser(u: User): string — imports ../types, @/utils
655
+ service.ts # exports: class UserService — imports ./types, ./utils/index
656
+ main.ts # imports ./service — no exports (entry point)
657
+ circular-a.ts # imports ./circular-b
658
+ circular-b.ts # imports ./circular-a
659
+ ```
660
+
661
+ Expected graph properties:
662
+ - `src/types.ts` and `src/utils/index.ts` are the highest-depth nodes (most imported)
663
+ - `src/main.ts` is depth 0 (nobody imports it)
664
+ - `circular-a.ts` and `circular-b.ts` form a 2-node cycle
665
+
666
+ ---
667
+
668
+ ## Dependency Summary
669
+
670
+ | Package | Purpose | Phase | esbuild |
671
+ |---------|---------|-------|---------|
672
+ | `fast-glob` | File discovery | 1 | bundled |
673
+ | `gitignore-parser` | Parse `.gitignore` for fast-glob ignore list | 1 | bundled |
674
+ | `tree-sitter` | AST parsing engine | 2 | `--external` |
675
+ | `tree-sitter-typescript` | TS/TSX grammar | 2 | `--external` |
676
+ | `@google/generative-ai` | Gemini 1.5 Flash SDK | 4 | `--external` |
677
+ | `p-limit` | Concurrency limiter for AI requests | 4 | bundled |
678
+ | `open` | Cross-platform browser open | 6 | `--external` |
679
+ | `zod` (existing) | Validate AI responses + ProjectMapSchema | 1, 4 | bundled |
680
+ | `node:crypto` (built-in) | SHA-256 hashing | 1 | built-in |
681
+ | `cytoscape` (CDN in HTML) | Interactive graph visualization | 6 | CDN only |
682
+
683
+ ---
684
+
685
+ ## File Structure
686
+
687
+ ```
688
+ src/core/graph/
689
+ map-schema.ts # ProjectMapSchema, migrateProjectMap(), readProjectMap()
690
+ scanner.ts # Phase 1 — gitignore-aware file discovery + hashing
691
+ parser.ts # Phase 2 — tree-sitter AST + symbol + import extraction
692
+ resolver.ts # Phase 3 — import path resolution (relative, alias, external)
693
+ builder.ts # Phase 3 — dependency graph + Kahn's BFS topological sort
694
+ enricher.ts # Phase 4 — Gemini AI semantic enrichment with retry + rate limit
695
+ contract-writer.ts # Phase 4 — writes .semantic.md files next to source
696
+ constraints.ts # Phase 5 — reads project_map.json, builds constraint strings
697
+ pipeline.ts # Phase 5 — chains all phases (was: orchestrator.ts — renamed to avoid clash)
698
+ renderer.ts # Phase 6 — transforms ProjectMap to Cytoscape.js format
699
+ html-template.ts # Phase 6 — standalone HTML with Cytoscape.js + all interactive features
700
+ __tests__/
701
+ scanner.test.ts
702
+ parser.test.ts
703
+ resolver.test.ts
704
+ builder.test.ts
705
+ enricher.test.ts
706
+ contract-writer.test.ts
707
+ pipeline.test.ts
708
+ renderer.test.ts
709
+ show.test.ts
710
+
711
+ src/commands/map.ts # CLI command: arc map
712
+ src/commands/show.ts # CLI command group: arc show [map]
713
+ ```
714
+
715
+ ---
716
+
717
+ ## CLI Interface
718
+
719
+ ```bash
720
+ # Full map with AI enrichment (requires GEMINI_API_KEY env var)
721
+ arc map
722
+
723
+ # Structural only — no AI tokens spent (sets config.graph.ai_enrichment = false)
724
+ arc map --no-ai
725
+
726
+ # Force re-analysis of all files (ignore hashes)
727
+ arc map --force
728
+
729
+ # Map only specific directories/patterns
730
+ arc map "src/core/**" "src/commands/**"
731
+
732
+ # Open the interactive visual graph in the browser
733
+ arc show map
734
+
735
+ # Generate HTML only, don't open browser
736
+ arc show map --no-open
737
+
738
+ # Output locations:
739
+ # -> {config.graph.output_dir}/project_map.json (default: tasks-management/graph/)
740
+ # -> {config.graph.output_dir}/index.html
741
+ # -> src/**/*.semantic.md (one per source file, written during AI enrichment)
742
+ ```
743
+
744
+ ### Exit Codes
745
+
746
+ | Code | Meaning |
747
+ |------|---------|
748
+ | `0` | Success — map written, all operations completed |
749
+ | `1` | Fatal error — config not found, I/O failure, key missing |
750
+ | `2` | Partial failure — map written but some AI enrichment failed |
751
+ | `130` | Interrupted — SIGINT (follows existing CLI convention) |
752
+
753
+ ### stdout vs stderr
754
+
755
+ - **Progress output** (files scanned, AI per-file status) → **stderr** (so stdout is pipeable)
756
+ - **Final summary line** (e.g. `Mapped 32 files, enriched 28, 4 skipped`) → **stdout**
757
+ - Zero files matched by glob patterns → `[nomos:graph:warn] No files matched — is the pattern correct?` on stderr, exit 0
758
+
759
+ ---
760
+
761
+ ## Execution Rules Compliance Notes
762
+
763
+ | Rule | Compliance |
764
+ |------|-----------|
765
+ | Rule 3 — JSON source of truth | `.semantic.md` files are write-only rendered views. All programmatic reads use `project_map.json`. No code path reads `.semantic.md`. |
766
+ | Rule 6 — ESM | All imports use `.js` extensions. `html-template.ts` (not `.html.ts`) avoids `.html.js` import weirdness. |
767
+ | Rule 10 — Shell injection | `arc show map` uses the `open` package or `spawn(cmd, [path], { shell: false })`. Never `exec(\`open ${path}\`)`.|
768
+ | Rule 11 — Schema versioning | `ProjectMap.schema_version` starts at 1. `migrateProjectMap()` exists in `map-schema.ts` with empty but present `migrations` map. |
769
+ | Rule 14 — No wildcard git adds | `arc map` and `arc show map` write only to `tasks-management/graph/` and `*.semantic.md`. These paths are gitignored (task 0.4). No git operations in the map pipeline. |
770
+
771
+ ---
772
+
773
+ ## Success Criteria
774
+
775
+ - [ ] `arc map --no-ai` completes in under 5 seconds on this project (~30 files)
776
+ - [ ] `project_map.json` accurately reflects all imports, exports, and depth values
777
+ - [ ] Incremental run (no file changes) skips all parsing and completes in < 1 second
778
+ - [ ] `project_map.json` is written atomically (tmp → fsync → rename)
779
+ - [ ] `--force` causes all files to be re-parsed regardless of hash
780
+ - [ ] AI enrichment writes `.semantic.md` files next to each source file
781
+ - [ ] `.semantic.md` files are human-readable and contain all four pillars (Overview, Purpose, Key Logic, Usage Context)
782
+ - [ ] `arc map` exits 2 (not 1) when some AI calls fail but the map is still written
783
+ - [ ] `arc plan` prompt includes `[ARCHITECTURAL CONSTRAINTS]` section with symbol signatures when `project_map.json` exists
784
+ - [ ] Missing context files log a warning and are gracefully skipped (not a crash)
785
+ - [ ] AI enrichment gracefully handles API failures without blocking the map (sets `semantic: null`)
786
+ - [ ] `arc show map` generates a standalone `index.html` with Cytoscape.js 3.28.1 BFS layout
787
+ - [ ] Graph nodes are colored by topological depth (red = core/high-risk, blue = leaf/low-risk)
788
+ - [ ] Clicking a node shows ripple analysis (direct dependents red, transitive yellow, rest dimmed)
789
+ - [ ] Symbol search highlights matching nodes and traces their dependency path
790
+ - [ ] Clicking a node shows its semantic overview, dependents list, and exported symbols
791
+ - [ ] `arc show map` exits 1 with a clear message when `project_map.json` is missing
792
+ - [ ] All 9 test files pass (`scanner`, `parser`, `resolver`, `builder`, `enricher`, `contract-writer`, `pipeline`, `renderer`, `show`)