@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,336 @@
|
|
|
1
|
+
import Parser from 'web-tree-sitter';
|
|
2
|
+
import { TS_WASM_PATH, TSX_WASM_PATH } from './grammar-paths.js';
|
|
3
|
+
import type { SymbolEntry, ImportEntry } from '../../types/index.js';
|
|
4
|
+
|
|
5
|
+
export interface ParseResult {
|
|
6
|
+
symbols: SymbolEntry[];
|
|
7
|
+
imports: ImportEntry[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ASTParser {
|
|
11
|
+
private parser: Parser | null = null;
|
|
12
|
+
private tsLang: Parser.Language | null = null;
|
|
13
|
+
private tsxLang: Parser.Language | null = null;
|
|
14
|
+
|
|
15
|
+
// [BLK-1 FIX + WATCH-1] WASM-based init — no native .node bindings
|
|
16
|
+
async init(): Promise<void> {
|
|
17
|
+
await Parser.init();
|
|
18
|
+
this.parser = new Parser();
|
|
19
|
+
// Grammar paths resolved by grammar-paths.ts (Tier A: node_modules, Tier B/C: local)
|
|
20
|
+
this.tsLang = await Parser.Language.load(TS_WASM_PATH);
|
|
21
|
+
this.tsxLang = await Parser.Language.load(TSX_WASM_PATH);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// [AMB-3 FIX] No CJS/ESM ambiguity — web-tree-sitter loads .wasm files uniformly
|
|
25
|
+
private selectLanguage(language: string): Parser.Language | null {
|
|
26
|
+
switch (language) {
|
|
27
|
+
case 'tsx':
|
|
28
|
+
return this.tsxLang;
|
|
29
|
+
case 'typescript':
|
|
30
|
+
case 'javascript':
|
|
31
|
+
return this.tsLang;
|
|
32
|
+
default:
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
parse(filePath: string, content: string, language: string): ParseResult {
|
|
38
|
+
if (!this.parser || !this.tsLang || !this.tsxLang) {
|
|
39
|
+
throw new Error('ASTParser not initialized. Call init() first.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const lang = this.selectLanguage(language);
|
|
43
|
+
if (!lang) {
|
|
44
|
+
// Unsupported language — return empty result with debug log
|
|
45
|
+
return { symbols: [], imports: [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.parser.setLanguage(lang);
|
|
49
|
+
const tree = this.parser.parse(content);
|
|
50
|
+
|
|
51
|
+
// Error-node threshold: if > 20% chars are ERROR nodes, return empty
|
|
52
|
+
const errorChars = this._countErrorChars(tree.rootNode);
|
|
53
|
+
if (content.length > 0 && errorChars / content.length > 0.20) {
|
|
54
|
+
return { symbols: [], imports: [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const symbols: SymbolEntry[] = [];
|
|
58
|
+
const imports: ImportEntry[] = [];
|
|
59
|
+
|
|
60
|
+
this._walk(tree.rootNode, content, symbols, imports, null, false);
|
|
61
|
+
|
|
62
|
+
return { symbols, imports };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Symbol & Import Extraction ─────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
private _walk(
|
|
68
|
+
node: Parser.SyntaxNode,
|
|
69
|
+
source: string,
|
|
70
|
+
symbols: SymbolEntry[],
|
|
71
|
+
imports: ImportEntry[],
|
|
72
|
+
currentClass: string | null,
|
|
73
|
+
parentIsExport: boolean,
|
|
74
|
+
): void {
|
|
75
|
+
const type = node.type;
|
|
76
|
+
|
|
77
|
+
switch (type) {
|
|
78
|
+
// ── Imports (Step 2.2) ──────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
case 'import_statement': {
|
|
81
|
+
const entry = this._extractImportStatement(node);
|
|
82
|
+
if (entry) imports.push(entry);
|
|
83
|
+
return; // no need to descend into import statements
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'call_expression': {
|
|
87
|
+
const entry = this._extractRequireCall(node);
|
|
88
|
+
if (entry) imports.push(entry);
|
|
89
|
+
// still descend — require() may appear in nested expressions
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Symbols ─────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
case 'function_declaration': {
|
|
96
|
+
const sym = this._extractFunction(node, source, parentIsExport);
|
|
97
|
+
if (sym) symbols.push(sym);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'lexical_declaration':
|
|
102
|
+
case 'variable_declaration': {
|
|
103
|
+
// Top-level `export const foo = () => ...` — check for arrow function
|
|
104
|
+
const isExported = parentIsExport || this._hasExportAncestor(node);
|
|
105
|
+
for (const child of node.children) {
|
|
106
|
+
if (child.type === 'variable_declarator') {
|
|
107
|
+
const nameNode = child.childForFieldName('name');
|
|
108
|
+
const valueNode = child.childForFieldName('value');
|
|
109
|
+
if (nameNode && valueNode && valueNode.type === 'arrow_function') {
|
|
110
|
+
symbols.push({
|
|
111
|
+
name: nameNode.text,
|
|
112
|
+
kind: 'function',
|
|
113
|
+
line: valueNode.startPosition.row + 1,
|
|
114
|
+
end_line: valueNode.endPosition.row + 1,
|
|
115
|
+
signature: this._arrowSignature(nameNode.text, valueNode, source),
|
|
116
|
+
exported: isExported,
|
|
117
|
+
});
|
|
118
|
+
} else if (nameNode && isExported) {
|
|
119
|
+
symbols.push({
|
|
120
|
+
name: nameNode.text,
|
|
121
|
+
kind: 'variable',
|
|
122
|
+
line: nameNode.startPosition.row + 1,
|
|
123
|
+
end_line: nameNode.endPosition.row + 1,
|
|
124
|
+
signature: null,
|
|
125
|
+
exported: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Also descend into value for nested call_expressions (e.g. require())
|
|
129
|
+
if (valueNode && valueNode.type !== 'arrow_function') {
|
|
130
|
+
this._walk(valueNode, source, symbols, imports, currentClass, parentIsExport);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'class_declaration': {
|
|
138
|
+
const nameNode = node.childForFieldName('name');
|
|
139
|
+
if (nameNode) {
|
|
140
|
+
const isExported = parentIsExport || this._hasExportAncestor(node);
|
|
141
|
+
symbols.push({
|
|
142
|
+
name: nameNode.text,
|
|
143
|
+
kind: 'class',
|
|
144
|
+
line: node.startPosition.row + 1,
|
|
145
|
+
end_line: node.endPosition.row + 1,
|
|
146
|
+
signature: `class ${nameNode.text}`,
|
|
147
|
+
exported: isExported,
|
|
148
|
+
});
|
|
149
|
+
// Descend into class body for methods, passing className
|
|
150
|
+
const body = node.childForFieldName('body');
|
|
151
|
+
if (body) {
|
|
152
|
+
for (const child of body.children) {
|
|
153
|
+
this._walk(child, source, symbols, imports, nameNode.text, false);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case 'method_definition': {
|
|
161
|
+
const nameNode = node.childForFieldName('name');
|
|
162
|
+
if (nameNode && currentClass) {
|
|
163
|
+
const params = node.childForFieldName('parameters');
|
|
164
|
+
symbols.push({
|
|
165
|
+
name: `${currentClass}.${nameNode.text}`,
|
|
166
|
+
kind: 'method',
|
|
167
|
+
line: node.startPosition.row + 1,
|
|
168
|
+
end_line: node.endPosition.row + 1,
|
|
169
|
+
signature: `${nameNode.text}(${params ? params.text : ''})`,
|
|
170
|
+
exported: false,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
case 'interface_declaration': {
|
|
177
|
+
const nameNode = node.childForFieldName('name');
|
|
178
|
+
if (nameNode) {
|
|
179
|
+
symbols.push({
|
|
180
|
+
name: nameNode.text,
|
|
181
|
+
kind: 'interface',
|
|
182
|
+
line: node.startPosition.row + 1,
|
|
183
|
+
end_line: node.endPosition.row + 1,
|
|
184
|
+
signature: `interface ${nameNode.text}`,
|
|
185
|
+
exported: parentIsExport || this._hasExportAncestor(node),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'type_alias_declaration': {
|
|
192
|
+
const nameNode = node.childForFieldName('name');
|
|
193
|
+
if (nameNode) {
|
|
194
|
+
symbols.push({
|
|
195
|
+
name: nameNode.text,
|
|
196
|
+
kind: 'type',
|
|
197
|
+
line: node.startPosition.row + 1,
|
|
198
|
+
end_line: node.endPosition.row + 1,
|
|
199
|
+
signature: `type ${nameNode.text}`,
|
|
200
|
+
exported: parentIsExport || this._hasExportAncestor(node),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
case 'enum_declaration': {
|
|
207
|
+
const nameNode = node.childForFieldName('name');
|
|
208
|
+
if (nameNode) {
|
|
209
|
+
symbols.push({
|
|
210
|
+
name: nameNode.text,
|
|
211
|
+
kind: 'enum',
|
|
212
|
+
line: node.startPosition.row + 1,
|
|
213
|
+
end_line: node.endPosition.row + 1,
|
|
214
|
+
signature: `enum ${nameNode.text}`,
|
|
215
|
+
exported: parentIsExport || this._hasExportAncestor(node),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'export_statement': {
|
|
222
|
+
// Recurse into export children with parentIsExport=true
|
|
223
|
+
for (const child of node.children) {
|
|
224
|
+
this._walk(child, source, symbols, imports, currentClass, true);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Default: recurse into children
|
|
231
|
+
for (const child of node.children) {
|
|
232
|
+
this._walk(child, source, symbols, imports, currentClass, parentIsExport);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Import Extraction (Step 2.2) ────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
private _extractImportStatement(node: Parser.SyntaxNode): ImportEntry | null {
|
|
239
|
+
// import_statement: `import { A, B } from './foo'` or `import foo from './foo'`
|
|
240
|
+
let source: string | null = null;
|
|
241
|
+
const symbolNames: string[] = [];
|
|
242
|
+
|
|
243
|
+
for (const child of node.children) {
|
|
244
|
+
if (child.type === 'string') {
|
|
245
|
+
source = child.text.replace(/^['"]|['"]$/g, '');
|
|
246
|
+
} else if (child.type === 'import_clause') {
|
|
247
|
+
for (const clause of child.children) {
|
|
248
|
+
if (clause.type === 'named_imports') {
|
|
249
|
+
for (const spec of clause.children) {
|
|
250
|
+
if (spec.type === 'import_specifier') {
|
|
251
|
+
const nameNode = spec.childForFieldName('name') ?? spec.children[0];
|
|
252
|
+
if (nameNode) symbolNames.push(nameNode.text);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// namespace import `* as foo` and default import: leave symbols empty
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!source) return null;
|
|
262
|
+
return {
|
|
263
|
+
source,
|
|
264
|
+
resolved: null,
|
|
265
|
+
symbols: symbolNames,
|
|
266
|
+
is_external: false, // resolver corrects this later
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private _extractRequireCall(node: Parser.SyntaxNode): ImportEntry | null {
|
|
271
|
+
// call_expression: `require('./foo')`
|
|
272
|
+
const calleeNode = node.childForFieldName('function');
|
|
273
|
+
if (!calleeNode || calleeNode.text !== 'require') return null;
|
|
274
|
+
|
|
275
|
+
const argsNode = node.childForFieldName('arguments');
|
|
276
|
+
if (!argsNode) return null;
|
|
277
|
+
|
|
278
|
+
for (const arg of argsNode.children) {
|
|
279
|
+
if (arg.type === 'string') {
|
|
280
|
+
const source = arg.text.replace(/^['"]|['"]$/g, '');
|
|
281
|
+
return {
|
|
282
|
+
source,
|
|
283
|
+
resolved: null,
|
|
284
|
+
symbols: [],
|
|
285
|
+
is_external: false,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
private _extractFunction(
|
|
295
|
+
node: Parser.SyntaxNode,
|
|
296
|
+
source: string,
|
|
297
|
+
parentIsExport: boolean,
|
|
298
|
+
): SymbolEntry | null {
|
|
299
|
+
const nameNode = node.childForFieldName('name');
|
|
300
|
+
if (!nameNode) return null;
|
|
301
|
+
const params = node.childForFieldName('parameters');
|
|
302
|
+
const returnType = node.childForFieldName('return_type');
|
|
303
|
+
const sig = `function ${nameNode.text}(${params ? params.text : ''})${returnType ? ': ' + returnType.text : ''}`;
|
|
304
|
+
return {
|
|
305
|
+
name: nameNode.text,
|
|
306
|
+
kind: 'function',
|
|
307
|
+
line: node.startPosition.row + 1,
|
|
308
|
+
end_line: node.endPosition.row + 1,
|
|
309
|
+
signature: sig,
|
|
310
|
+
exported: parentIsExport || this._hasExportAncestor(node),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private _arrowSignature(name: string, node: Parser.SyntaxNode, source: string): string {
|
|
315
|
+
const params = node.childForFieldName('parameters');
|
|
316
|
+
return `const ${name} = (${params ? params.text.replace(/^\(|\)$/g, '') : ''}) =>`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private _hasExportAncestor(node: Parser.SyntaxNode): boolean {
|
|
320
|
+
let cur: Parser.SyntaxNode | null = node.parent;
|
|
321
|
+
while (cur) {
|
|
322
|
+
if (cur.type === 'export_statement') return true;
|
|
323
|
+
cur = cur.parent;
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private _countErrorChars(node: Parser.SyntaxNode): number {
|
|
329
|
+
if (node.type === 'ERROR') return node.text.length;
|
|
330
|
+
let total = 0;
|
|
331
|
+
for (const child of node.children) {
|
|
332
|
+
total += this._countErrorChars(child);
|
|
333
|
+
}
|
|
334
|
+
return total;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import lockfile from 'proper-lockfile';
|
|
4
|
+
import pLimit from 'p-limit';
|
|
5
|
+
import type { NomosConfig, FileNode, ProjectMap } from '../../types/index.js';
|
|
6
|
+
import { readProjectMap } from './map-schema.js';
|
|
7
|
+
import { FileScanner } from './scanner.js';
|
|
8
|
+
import { ASTParser } from './parser.js';
|
|
9
|
+
import { ImportResolver } from './resolver.js';
|
|
10
|
+
import { GraphBuilder } from './builder.js';
|
|
11
|
+
import { SemanticEnricher } from './enricher.js';
|
|
12
|
+
import { AuthManager } from '../auth/manager.js';
|
|
13
|
+
import { ContractWriter } from './contract-writer.js';
|
|
14
|
+
|
|
15
|
+
// ─── Logger Interface ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
interface Logger {
|
|
18
|
+
info(msg: string): void;
|
|
19
|
+
warn(msg: string): void;
|
|
20
|
+
error(msg: string): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── PipelineResult ───────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface PipelineResult {
|
|
26
|
+
map: ProjectMap;
|
|
27
|
+
aiFailures: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── MapPipeline ──────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export class MapPipeline {
|
|
33
|
+
private readonly scanner: FileScanner;
|
|
34
|
+
private readonly builder: GraphBuilder;
|
|
35
|
+
private readonly resolver: ImportResolver;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly config: NomosConfig,
|
|
39
|
+
private readonly projectRoot: string,
|
|
40
|
+
private readonly logger: Logger,
|
|
41
|
+
private readonly authManager?: AuthManager | null,
|
|
42
|
+
) {
|
|
43
|
+
this.scanner = new FileScanner(projectRoot, config.graph, logger);
|
|
44
|
+
this.builder = new GraphBuilder(config.graph, logger);
|
|
45
|
+
this.resolver = new ImportResolver(projectRoot, logger);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async run(options: {
|
|
49
|
+
noAi: boolean;
|
|
50
|
+
force: boolean;
|
|
51
|
+
patterns?: string[];
|
|
52
|
+
}): Promise<PipelineResult> {
|
|
53
|
+
const outputDir = path.resolve(this.projectRoot, this.config.graph.output_dir);
|
|
54
|
+
const mapPath = path.join(outputDir, 'project_map.json');
|
|
55
|
+
|
|
56
|
+
// ── a. Read existing map ───────────────────────────────────────────────────
|
|
57
|
+
const existingMap = await readProjectMap(mapPath);
|
|
58
|
+
|
|
59
|
+
// ── b. Scan ───────────────────────────────────────────────────────────────
|
|
60
|
+
const scanResult = await this.scanner.scan(existingMap, options.force, options.patterns);
|
|
61
|
+
const { files: newFiles, carried } = scanResult;
|
|
62
|
+
|
|
63
|
+
// ── c. Empty check ────────────────────────────────────────────────────────
|
|
64
|
+
if (newFiles.size === 0 && carried.size === 0) {
|
|
65
|
+
this.logger.warn('[nomos:graph:warn] No files found to map.');
|
|
66
|
+
const emptyMap: ProjectMap = {
|
|
67
|
+
schema_version: 1,
|
|
68
|
+
generated_at: new Date().toISOString(),
|
|
69
|
+
root: this.projectRoot,
|
|
70
|
+
files: {},
|
|
71
|
+
stats: { total_files: 0, total_symbols: 0, total_edges: 0, core_modules: [], structural_only: 0, semantically_enriched: 0, indexed: 0 },
|
|
72
|
+
};
|
|
73
|
+
return { map: emptyMap, aiFailures: 0 };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── d. Init ASTParser (WASM requires async initialization) ────────────────
|
|
77
|
+
const parser = new ASTParser();
|
|
78
|
+
await parser.init();
|
|
79
|
+
|
|
80
|
+
// ── e. Parse all new/changed files ────────────────────────────────────────
|
|
81
|
+
const parsedNodes = new Map<string, FileNode>();
|
|
82
|
+
for (const [relPath, entry] of newFiles) {
|
|
83
|
+
const parseResult = parser.parse(entry.file, entry.content, entry.language);
|
|
84
|
+
const node: FileNode = {
|
|
85
|
+
file: relPath,
|
|
86
|
+
hash: entry.hash,
|
|
87
|
+
language: entry.language,
|
|
88
|
+
symbols: parseResult.symbols,
|
|
89
|
+
imports: parseResult.imports,
|
|
90
|
+
dependents: [],
|
|
91
|
+
dependencies: [],
|
|
92
|
+
depth: 0,
|
|
93
|
+
last_parsed_at: new Date().toISOString(),
|
|
94
|
+
semantic: null,
|
|
95
|
+
enrichment_status: 'structural',
|
|
96
|
+
};
|
|
97
|
+
// Carry over semantic + enrichment_status if hash hasn't changed (incremental)
|
|
98
|
+
const existing = existingMap?.files[relPath];
|
|
99
|
+
if (existing?.semantic && existing.semantic.source_hash === entry.hash) {
|
|
100
|
+
node.semantic = existing.semantic;
|
|
101
|
+
node.enrichment_status = existing.enrichment_status ?? 'semantic';
|
|
102
|
+
}
|
|
103
|
+
parsedNodes.set(relPath, node);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── f. Merge parsed + carried (backfill enrichment_status for old maps) ──
|
|
107
|
+
const allFiles = new Map<string, FileNode>();
|
|
108
|
+
for (const [k, v] of carried) {
|
|
109
|
+
if (!v.enrichment_status) {
|
|
110
|
+
v.enrichment_status = v.semantic ? 'semantic' : 'structural';
|
|
111
|
+
}
|
|
112
|
+
allFiles.set(k, v);
|
|
113
|
+
}
|
|
114
|
+
for (const [k, v] of parsedNodes) allFiles.set(k, v);
|
|
115
|
+
|
|
116
|
+
// ── g. [WATCH-4] Build knownFiles BEFORE resolution ───────────────────────
|
|
117
|
+
const knownFiles = new Set<string>(allFiles.keys());
|
|
118
|
+
|
|
119
|
+
const resolveLimit = pLimit(8);
|
|
120
|
+
await Promise.all(
|
|
121
|
+
[...allFiles.values()].map(node =>
|
|
122
|
+
resolveLimit(async () => {
|
|
123
|
+
for (const imp of node.imports) {
|
|
124
|
+
const result = await this.resolver.resolve(imp.source, node.file, knownFiles);
|
|
125
|
+
imp.resolved = result.resolved;
|
|
126
|
+
imp.is_external = result.is_external;
|
|
127
|
+
}
|
|
128
|
+
}),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// ── h. Build graph ────────────────────────────────────────────────────────
|
|
133
|
+
const stats = this.builder.build(allFiles);
|
|
134
|
+
|
|
135
|
+
// ── i. [GAP-2 FIX] Write intermediate map BEFORE AI enrichment ────────────
|
|
136
|
+
const intermediateMap: ProjectMap = {
|
|
137
|
+
schema_version: 1,
|
|
138
|
+
generated_at: new Date().toISOString(),
|
|
139
|
+
root: this.projectRoot,
|
|
140
|
+
files: Object.fromEntries(allFiles),
|
|
141
|
+
stats,
|
|
142
|
+
};
|
|
143
|
+
await this.writeMap(intermediateMap, mapPath);
|
|
144
|
+
|
|
145
|
+
let aiFailures = 0;
|
|
146
|
+
|
|
147
|
+
// ── j. AI enrichment ──────────────────────────────────────────────────────
|
|
148
|
+
if (!options.noAi && this.config.graph.ai_enrichment) {
|
|
149
|
+
// [GAP-3 FIX] Set up cancellation flag
|
|
150
|
+
const cancellation = { cancelled: false };
|
|
151
|
+
const sigintHandler = () => { cancellation.cancelled = true; };
|
|
152
|
+
process.on('SIGINT', sigintHandler);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const enricher = await SemanticEnricher.create(
|
|
156
|
+
this.projectRoot, this.config.graph, this.logger, this.authManager,
|
|
157
|
+
);
|
|
158
|
+
aiFailures = await enricher.enrich(allFiles, cancellation);
|
|
159
|
+
|
|
160
|
+
const contractWriter = new ContractWriter(this.projectRoot, this.logger);
|
|
161
|
+
await contractWriter.writeContracts(allFiles);
|
|
162
|
+
} finally {
|
|
163
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Write final map with enriched semantic data
|
|
167
|
+
const finalMap: ProjectMap = {
|
|
168
|
+
schema_version: 1,
|
|
169
|
+
generated_at: new Date().toISOString(),
|
|
170
|
+
root: this.projectRoot,
|
|
171
|
+
files: Object.fromEntries(allFiles),
|
|
172
|
+
stats,
|
|
173
|
+
};
|
|
174
|
+
await this.writeMap(finalMap, mapPath);
|
|
175
|
+
|
|
176
|
+
return { map: finalMap, aiFailures };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── l. Return result ──────────────────────────────────────────────────────
|
|
180
|
+
return { map: intermediateMap, aiFailures };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── [BLK-2 FIX + WATCH-3] Atomic write with directory-level lock ────────────
|
|
184
|
+
|
|
185
|
+
private async writeMap(map: ProjectMap, mapPath: string): Promise<void> {
|
|
186
|
+
const outDir = path.dirname(mapPath);
|
|
187
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
188
|
+
|
|
189
|
+
// Lock the DIRECTORY — works even when project_map.json doesn't exist yet.
|
|
190
|
+
// proper-lockfile creates a .lock file (e.g., tasks-management/graph/.project-map.lock)
|
|
191
|
+
// which serializes concurrent arc map invocations.
|
|
192
|
+
const release = await lockfile.lock(outDir, {
|
|
193
|
+
lockfilePath: path.join(outDir, '.project-map.lock'),
|
|
194
|
+
realpath: false,
|
|
195
|
+
retries: { retries: 10, minTimeout: 200, maxTimeout: 2000 },
|
|
196
|
+
stale: 60000, // 60s stale lock — enrichment writes can be slow
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const tmpPath = `${mapPath}.tmp`;
|
|
201
|
+
await fs.writeFile(tmpPath, JSON.stringify(map, null, 2), 'utf-8');
|
|
202
|
+
const fd = await fs.open(tmpPath, 'r+');
|
|
203
|
+
try { await fd.sync(); } finally { await fd.close(); }
|
|
204
|
+
await fs.rename(tmpPath, mapPath);
|
|
205
|
+
} finally {
|
|
206
|
+
await release();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ProjectMap, FileNode } from '../../types/index.js';
|
|
2
|
+
|
|
3
|
+
// ─── Cytoscape Data Types ─────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface CytoscapeNode {
|
|
6
|
+
data: {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
color: string;
|
|
10
|
+
} & FileNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CytoscapeEdge {
|
|
14
|
+
data: {
|
|
15
|
+
id: string;
|
|
16
|
+
source: string;
|
|
17
|
+
target: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── MapRenderer ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export class MapRenderer {
|
|
24
|
+
/**
|
|
25
|
+
* Transforms a ProjectMap into Cytoscape.js-compatible nodes and edges.
|
|
26
|
+
*/
|
|
27
|
+
render(map: ProjectMap): { nodes: CytoscapeNode[]; edges: CytoscapeEdge[] } {
|
|
28
|
+
const files = Object.values(map.files);
|
|
29
|
+
const maxDepth = files.reduce((max, f) => Math.max(max, f.depth), 0);
|
|
30
|
+
|
|
31
|
+
const nodes: CytoscapeNode[] = files.map((node) => ({
|
|
32
|
+
data: {
|
|
33
|
+
...node,
|
|
34
|
+
id: node.file,
|
|
35
|
+
label: node.file.split('/').pop() ?? node.file,
|
|
36
|
+
color: this.computeColor(node.depth, maxDepth),
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const edges: CytoscapeEdge[] = [];
|
|
41
|
+
for (const node of files) {
|
|
42
|
+
for (const dep of node.dependencies) {
|
|
43
|
+
edges.push({
|
|
44
|
+
data: {
|
|
45
|
+
id: `${node.file}→${dep}`,
|
|
46
|
+
source: node.file,
|
|
47
|
+
target: dep,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { nodes, edges };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Maps a depth value to a hex color along a three-stop gradient:
|
|
58
|
+
* depth 0 → #a8d8ea (cool blue)
|
|
59
|
+
* mid depth → #f9c784 (warm amber)
|
|
60
|
+
* max depth → #c0392b (deep red)
|
|
61
|
+
*/
|
|
62
|
+
computeColor(depth: number, maxDepth: number): string {
|
|
63
|
+
if (maxDepth === 0) return '#a8d8ea';
|
|
64
|
+
|
|
65
|
+
const t = Math.min(depth / maxDepth, 1); // 0..1
|
|
66
|
+
|
|
67
|
+
// Three color stops
|
|
68
|
+
const stops = [
|
|
69
|
+
{ r: 0xa8, g: 0xd8, b: 0xea }, // #a8d8ea — blue
|
|
70
|
+
{ r: 0xf9, g: 0xc7, b: 0x84 }, // #f9c784 — amber
|
|
71
|
+
{ r: 0xc0, g: 0x39, b: 0x2b }, // #c0392b — red
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
let r: number, g: number, b: number;
|
|
75
|
+
|
|
76
|
+
if (t <= 0.5) {
|
|
77
|
+
// Interpolate between blue and amber
|
|
78
|
+
const u = t / 0.5;
|
|
79
|
+
r = Math.round(stops[0].r + u * (stops[1].r - stops[0].r));
|
|
80
|
+
g = Math.round(stops[0].g + u * (stops[1].g - stops[0].g));
|
|
81
|
+
b = Math.round(stops[0].b + u * (stops[1].b - stops[0].b));
|
|
82
|
+
} else {
|
|
83
|
+
// Interpolate between amber and red
|
|
84
|
+
const u = (t - 0.5) / 0.5;
|
|
85
|
+
r = Math.round(stops[1].r + u * (stops[2].r - stops[1].r));
|
|
86
|
+
g = Math.round(stops[1].g + u * (stops[2].g - stops[1].g));
|
|
87
|
+
b = Math.round(stops[1].b + u * (stops[2].b - stops[1].b));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
91
|
+
}
|
|
92
|
+
}
|