@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,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`)
|