@cleocode/nexus 2026.9.8 → 2026.9.10
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/dist/src/code/parser.d.ts +19 -1
- package/dist/src/code/parser.d.ts.map +1 -1
- package/dist/src/code/parser.js +61 -1
- package/dist/src/code/parser.js.map +1 -1
- package/dist/src/intelligence/impact.d.ts +9 -0
- package/dist/src/intelligence/impact.d.ts.map +1 -1
- package/dist/src/intelligence/impact.js +25 -13
- package/dist/src/intelligence/impact.js.map +1 -1
- package/dist/src/pipeline/call-processor.d.ts +4 -1
- package/dist/src/pipeline/call-processor.d.ts.map +1 -1
- package/dist/src/pipeline/call-processor.js +43 -3
- package/dist/src/pipeline/call-processor.js.map +1 -1
- package/dist/src/pipeline/community-processor.d.ts.map +1 -1
- package/dist/src/pipeline/community-processor.js +6 -27
- package/dist/src/pipeline/community-processor.js.map +1 -1
- package/dist/src/pipeline/extractors/typescript-extractor.d.ts +27 -35
- package/dist/src/pipeline/extractors/typescript-extractor.d.ts.map +1 -1
- package/dist/src/pipeline/extractors/typescript-extractor.js +100 -213
- package/dist/src/pipeline/extractors/typescript-extractor.js.map +1 -1
- package/dist/src/pipeline/filesystem-walker.d.ts +9 -5
- package/dist/src/pipeline/filesystem-walker.d.ts.map +1 -1
- package/dist/src/pipeline/filesystem-walker.js +154 -79
- package/dist/src/pipeline/filesystem-walker.js.map +1 -1
- package/dist/src/pipeline/heritage-processor.d.ts.map +1 -1
- package/dist/src/pipeline/heritage-processor.js +29 -4
- package/dist/src/pipeline/heritage-processor.js.map +1 -1
- package/dist/src/pipeline/import-processor.d.ts +28 -6
- package/dist/src/pipeline/import-processor.d.ts.map +1 -1
- package/dist/src/pipeline/import-processor.js +68 -45
- package/dist/src/pipeline/import-processor.js.map +1 -1
- package/dist/src/pipeline/index.d.ts +32 -20
- package/dist/src/pipeline/index.d.ts.map +1 -1
- package/dist/src/pipeline/index.js +173 -148
- package/dist/src/pipeline/index.js.map +1 -1
- package/dist/src/pipeline/knowledge-graph.d.ts +3 -1
- package/dist/src/pipeline/knowledge-graph.d.ts.map +1 -1
- package/dist/src/pipeline/knowledge-graph.js +36 -2
- package/dist/src/pipeline/knowledge-graph.js.map +1 -1
- package/dist/src/pipeline/lexical-scope.d.ts +60 -0
- package/dist/src/pipeline/lexical-scope.d.ts.map +1 -0
- package/dist/src/pipeline/lexical-scope.js +362 -0
- package/dist/src/pipeline/lexical-scope.js.map +1 -0
- package/dist/src/pipeline/parse-loop.d.ts +49 -2
- package/dist/src/pipeline/parse-loop.d.ts.map +1 -1
- package/dist/src/pipeline/parse-loop.js +346 -144
- package/dist/src/pipeline/parse-loop.js.map +1 -1
- package/dist/src/pipeline/processors/access-processor.d.ts +27 -14
- package/dist/src/pipeline/processors/access-processor.d.ts.map +1 -1
- package/dist/src/pipeline/processors/access-processor.js +106 -27
- package/dist/src/pipeline/processors/access-processor.js.map +1 -1
- package/dist/src/pipeline/structure-processor.d.ts.map +1 -1
- package/dist/src/pipeline/structure-processor.js +1 -0
- package/dist/src/pipeline/structure-processor.js.map +1 -1
- package/dist/src/pipeline/symbol-table.d.ts +41 -3
- package/dist/src/pipeline/symbol-table.d.ts.map +1 -1
- package/dist/src/pipeline/symbol-table.js +104 -2
- package/dist/src/pipeline/symbol-table.js.map +1 -1
- package/dist/src/pipeline/workers/parse-worker.d.ts +29 -92
- package/dist/src/pipeline/workers/parse-worker.d.ts.map +1 -1
- package/dist/src/pipeline/workers/parse-worker.js +49 -1110
- package/dist/src/pipeline/workers/parse-worker.js.map +1 -1
- package/dist/src/pipeline/workers/worker-pool.d.ts +6 -4
- package/dist/src/pipeline/workers/worker-pool.d.ts.map +1 -1
- package/dist/src/pipeline/workers/worker-pool.js +112 -22
- package/dist/src/pipeline/workers/worker-pool.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
|
@@ -31,15 +31,19 @@
|
|
|
31
31
|
* @task T540
|
|
32
32
|
* @module pipeline/parse-loop
|
|
33
33
|
*/
|
|
34
|
+
import { createHash } from 'node:crypto';
|
|
34
35
|
import fs from 'node:fs/promises';
|
|
36
|
+
import { basename, extname } from 'node:path';
|
|
35
37
|
import { fileURLToPath } from 'node:url';
|
|
36
38
|
import { confidenceLabelFromNumeric } from '@cleocode/contracts';
|
|
39
|
+
import { parseOriginalSource } from '../code/parser.js';
|
|
37
40
|
import { extractGo } from './extractors/go-extractor.js';
|
|
38
41
|
import { extractPython } from './extractors/python-extractor.js';
|
|
39
42
|
import { extractRust } from './extractors/rust-extractor.js';
|
|
40
43
|
import { extractTypeScript, } from './extractors/typescript-extractor.js';
|
|
41
|
-
import { buildBarrelExportMap,
|
|
44
|
+
import { buildBarrelExportMap, processExtractedImports } from './import-processor.js';
|
|
42
45
|
import { detectLanguageFromPath } from './language-detection.js';
|
|
46
|
+
import { buildLexicalScopeModel } from './lexical-scope.js';
|
|
43
47
|
import { extractAccesses } from './processors/access-processor.js';
|
|
44
48
|
import { createWorkerPool } from './workers/worker-pool.js';
|
|
45
49
|
// ---------------------------------------------------------------------------
|
|
@@ -124,32 +128,6 @@ function loadGrammar(langKey) {
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
// ---------------------------------------------------------------------------
|
|
127
|
-
// Content sanitization
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
/**
|
|
130
|
-
* Sanitize source content for tree-sitter parsing.
|
|
131
|
-
*
|
|
132
|
-
* tree-sitter 0.21.x Node.js bindings throw `Error: Invalid argument`
|
|
133
|
-
* when source strings contain non-ASCII Unicode characters (e.g. em-dash
|
|
134
|
-
* `—`, curly quotes, etc.) in comments. Since all structural elements that
|
|
135
|
-
* nexus cares about (function names, export specifiers, import paths) are
|
|
136
|
-
* ASCII, replacing non-ASCII characters with spaces is safe and preserves
|
|
137
|
-
* line/column positions for line number reporting.
|
|
138
|
-
*
|
|
139
|
-
* @param content - Raw file content read from disk
|
|
140
|
-
* @returns Content with non-ASCII characters replaced by spaces
|
|
141
|
-
*/
|
|
142
|
-
function sanitizeForParsing(content) {
|
|
143
|
-
// eslint-disable-next-line no-control-regex
|
|
144
|
-
return content.replace(/[^\x00-\x7F]/g, ' ');
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Hard limit for tree-sitter 0.21.x: native code uses a 15-bit byte-offset
|
|
148
|
-
* counter that overflows at 32768 characters. Files larger than this use
|
|
149
|
-
* regex-based fallback extraction.
|
|
150
|
-
*/
|
|
151
|
-
const TREE_SITTER_MAX_CHARS = 32_767;
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
131
|
// Language key mapping
|
|
154
132
|
// ---------------------------------------------------------------------------
|
|
155
133
|
/**
|
|
@@ -175,16 +153,20 @@ function grammarKeyForLanguage(language) {
|
|
|
175
153
|
* @param language - Canonical language name from `detectLanguageFromPath`
|
|
176
154
|
* @param rootNode - Parsed tree-sitter AST root node
|
|
177
155
|
* @param filePath - File path relative to repo root
|
|
156
|
+
* @param sourceGeneration - SHA-256 of original source bytes, distinct from publication identity.
|
|
157
|
+
* @param publicationGeneration - Preallocated graph publication identity, when publishing.
|
|
178
158
|
* @returns Uniform extraction result
|
|
179
159
|
*/
|
|
180
|
-
function runExtractor(language, rootNode, filePath) {
|
|
181
|
-
// biome-ignore lint/suspicious/noExplicitAny: tree-sitter rootNode has no shared TS type
|
|
160
|
+
function runExtractor(language, rootNode, filePath, sourceGeneration, publicationGeneration) {
|
|
182
161
|
const node = rootNode;
|
|
162
|
+
const model = language === 'typescript' || language === 'javascript'
|
|
163
|
+
? buildLexicalScopeModel(rootNode, filePath, sourceGeneration, language, publicationGeneration)
|
|
164
|
+
: undefined;
|
|
183
165
|
let result;
|
|
184
166
|
switch (language) {
|
|
185
167
|
case 'typescript':
|
|
186
168
|
case 'javascript':
|
|
187
|
-
result = extractTypeScript(node, filePath, language);
|
|
169
|
+
result = extractTypeScript(node, filePath, language, model);
|
|
188
170
|
break;
|
|
189
171
|
case 'python':
|
|
190
172
|
result = extractPython(node, filePath);
|
|
@@ -201,9 +183,39 @@ function runExtractor(language, rootNode, filePath) {
|
|
|
201
183
|
// Run access extraction on the parsed AST (Phase 3f — T1837).
|
|
202
184
|
// Supports all languages with member_expression / attribute / field_expression /
|
|
203
185
|
// selector_expression AST node types (TS, JS, Python, Go, Rust).
|
|
204
|
-
result.accesses = extractAccesses(node, filePath);
|
|
186
|
+
result.accesses = extractAccesses(node, filePath, model);
|
|
205
187
|
return result;
|
|
206
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Extract a file through the same native parser and language capabilities in either realm.
|
|
191
|
+
* @param source - Original Unicode source text.
|
|
192
|
+
* @param filePath - Repository-relative source path used for symbol identities.
|
|
193
|
+
* @param limits - Native source-byte and synchronous parsing deadline limits.
|
|
194
|
+
* @param publicationGeneration - Optional immutable publication identity passed by the pipeline.
|
|
195
|
+
* @returns Declarations, references, imports, heritage and access evidence.
|
|
196
|
+
* @remarks Static extraction does not establish complete runtime-call discovery.
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* const extracted = extractOriginalSource('export const 文 = 1;', 'source.ts');
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export function extractOriginalSource(source, filePath, limits, publicationGeneration) {
|
|
203
|
+
const language = detectLanguageFromPath(filePath);
|
|
204
|
+
const grammarKey = language
|
|
205
|
+
? filePath.endsWith('.tsx')
|
|
206
|
+
? 'tsx'
|
|
207
|
+
: grammarKeyForLanguage(language)
|
|
208
|
+
: null;
|
|
209
|
+
const parser = getParser();
|
|
210
|
+
const grammar = grammarKey ? loadGrammar(grammarKey) : null;
|
|
211
|
+
if (!language || !grammarKey)
|
|
212
|
+
throw new Error('Unsupported source language');
|
|
213
|
+
if (!parser || !grammar)
|
|
214
|
+
throw new Error(`Parser or grammar unavailable: ${grammarKey}`);
|
|
215
|
+
parser.setLanguage(grammar);
|
|
216
|
+
const tree = parseOriginalSource(parser, source, limits);
|
|
217
|
+
return runExtractor(language, tree.rootNode, filePath, createHash('sha256').update(source).digest('hex'), publicationGeneration);
|
|
218
|
+
}
|
|
207
219
|
// ---------------------------------------------------------------------------
|
|
208
220
|
// SymbolTable registration helpers
|
|
209
221
|
// ---------------------------------------------------------------------------
|
|
@@ -289,33 +301,199 @@ function emitDefinesEdges(fileNodeId, symbols, graph) {
|
|
|
289
301
|
});
|
|
290
302
|
}
|
|
291
303
|
}
|
|
304
|
+
const EXECUTABLE_CAPABILITIES = [
|
|
305
|
+
'file-evidence',
|
|
306
|
+
'declarations',
|
|
307
|
+
'imports',
|
|
308
|
+
'call-references',
|
|
309
|
+
'access-references',
|
|
310
|
+
'type-heritage',
|
|
311
|
+
];
|
|
312
|
+
const SQL_CAPABILITIES = [
|
|
313
|
+
'file-evidence',
|
|
314
|
+
'sql-schema-objects',
|
|
315
|
+
'sql-migrations',
|
|
316
|
+
'sql-triggers',
|
|
317
|
+
'sql-constraints',
|
|
318
|
+
'sql-literal-references',
|
|
319
|
+
'sql-dynamic-references',
|
|
320
|
+
];
|
|
321
|
+
const PARSEABLE_LANGUAGES = new Set(['typescript', 'javascript', 'python', 'go', 'rust']);
|
|
322
|
+
const EXECUTABLE_LANGUAGES = new Set([
|
|
323
|
+
...PARSEABLE_LANGUAGES,
|
|
324
|
+
'java',
|
|
325
|
+
'kotlin',
|
|
326
|
+
'c',
|
|
327
|
+
'cpp',
|
|
328
|
+
'csharp',
|
|
329
|
+
'php',
|
|
330
|
+
'ruby',
|
|
331
|
+
'swift',
|
|
332
|
+
'dart',
|
|
333
|
+
'vue',
|
|
334
|
+
'shell',
|
|
335
|
+
'cobol',
|
|
336
|
+
]);
|
|
337
|
+
/** Build a role observation from independently stated requested capabilities. */
|
|
338
|
+
function roleCoverage(role, classification, evidence) {
|
|
339
|
+
return {
|
|
340
|
+
role,
|
|
341
|
+
classification,
|
|
342
|
+
requested: role === 'sql'
|
|
343
|
+
? [...SQL_CAPABILITIES]
|
|
344
|
+
: role === 'executable' || role === 'unknown'
|
|
345
|
+
? [...EXECUTABLE_CAPABILITIES]
|
|
346
|
+
: ['file-evidence', ...(evidence ? [evidence] : [])],
|
|
347
|
+
completed: ['file-evidence', ...(evidence ? [evidence] : [])],
|
|
348
|
+
limitations: role === 'sql'
|
|
349
|
+
? [
|
|
350
|
+
'SQL extraction is unsupported; schema and literal references remain unassessed.',
|
|
351
|
+
'Dynamic SQL references remain unresolved.',
|
|
352
|
+
]
|
|
353
|
+
: role === 'executable' || role === 'unknown'
|
|
354
|
+
? ['Static extraction cannot establish complete runtime-call discovery.']
|
|
355
|
+
: [],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/** Identify known binary resources from both their extension and observed bytes. */
|
|
359
|
+
function hasResourceSignature(extension, bytes) {
|
|
360
|
+
const prefix = bytes.subarray(0, 12);
|
|
361
|
+
if (extension === '.png')
|
|
362
|
+
return prefix.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex'));
|
|
363
|
+
if (extension === '.jpg' || extension === '.jpeg')
|
|
364
|
+
return prefix.subarray(0, 3).equals(Buffer.from('ffd8ff', 'hex'));
|
|
365
|
+
if (extension === '.gif')
|
|
366
|
+
return /^GIF8[79]a/.test(prefix.toString('ascii'));
|
|
367
|
+
if (extension === '.webp')
|
|
368
|
+
return (prefix.subarray(0, 4).toString('ascii') === 'RIFF' &&
|
|
369
|
+
prefix.subarray(8).toString('ascii') === 'WEBP');
|
|
370
|
+
if (extension === '.woff')
|
|
371
|
+
return prefix.subarray(0, 4).toString('ascii') === 'wOFF';
|
|
372
|
+
if (extension === '.woff2')
|
|
373
|
+
return prefix.subarray(0, 4).toString('ascii') === 'wOF2';
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
/** Classify only observed role evidence; unsupported or ambiguous code remains a gap. */
|
|
377
|
+
async function classifyFileCapabilities(file, repoPath, signal) {
|
|
378
|
+
const path = file.path;
|
|
379
|
+
const extension = extname(path).toLowerCase();
|
|
380
|
+
const language = detectLanguageFromPath(path);
|
|
381
|
+
const bytes = await fs.readFile(path.startsWith('/') ? path : `${repoPath}/${path}`, { signal });
|
|
382
|
+
if (file.contentHash && createHash('sha256').update(bytes).digest('hex') !== file.contentHash)
|
|
383
|
+
throw new Error('Source changed between scanning and capability classification');
|
|
384
|
+
const report = (capabilities) => ({
|
|
385
|
+
path,
|
|
386
|
+
status: capabilities.requested.every((capability) => capabilities.completed.includes(capability))
|
|
387
|
+
? 'analyzed'
|
|
388
|
+
: 'unsupported',
|
|
389
|
+
...(capabilities.role === 'sql'
|
|
390
|
+
? { reason: 'No SQL schema or reference extractor is available' }
|
|
391
|
+
: capabilities.role === 'executable' || capabilities.role === 'unknown'
|
|
392
|
+
? {
|
|
393
|
+
reason: 'Unsupported or unclassified executable capabilities require a symbol extractor',
|
|
394
|
+
}
|
|
395
|
+
: {}),
|
|
396
|
+
capabilities,
|
|
397
|
+
});
|
|
398
|
+
if (hasResourceSignature(extension, bytes))
|
|
399
|
+
return report(roleCoverage('asset', {
|
|
400
|
+
basis: 'path-and-content',
|
|
401
|
+
reason: `Resource extension ${extension} agrees with its observed binary signature`,
|
|
402
|
+
}, 'resource-evidence'));
|
|
403
|
+
const content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
404
|
+
if (/^\uFEFF?#!/.test(content))
|
|
405
|
+
return report(roleCoverage('executable', {
|
|
406
|
+
basis: 'content',
|
|
407
|
+
reason: 'Observed executable shebang; an asset extension cannot override it',
|
|
408
|
+
}));
|
|
409
|
+
if (EXECUTABLE_LANGUAGES.has(language ?? '') ||
|
|
410
|
+
['.svelte', '.mdx', '.lua', '.pl', '.r', '.ps1', '.bat', '.cmd'].includes(extension) ||
|
|
411
|
+
/^(?:Makefile|Dockerfile)$/.test(basename(path)))
|
|
412
|
+
return report(roleCoverage('executable', {
|
|
413
|
+
basis: 'path',
|
|
414
|
+
reason: `Recognized executable language or filename: ${path}`,
|
|
415
|
+
}));
|
|
416
|
+
if (extension === '.sql')
|
|
417
|
+
return report(roleCoverage('sql', {
|
|
418
|
+
basis: 'path',
|
|
419
|
+
reason: 'SQL source extension; no schema or reference extractor is implemented',
|
|
420
|
+
}));
|
|
421
|
+
if (extension === '.md' || extension === '.markdown')
|
|
422
|
+
return report(roleCoverage('documentation', {
|
|
423
|
+
basis: 'path-and-content',
|
|
424
|
+
reason: 'Readable documentary Markdown; embedded examples are not production callers',
|
|
425
|
+
}, 'documentary-evidence'));
|
|
426
|
+
if (extension === '.json') {
|
|
427
|
+
let value;
|
|
428
|
+
try {
|
|
429
|
+
value = JSON.parse(content);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
return {
|
|
433
|
+
path,
|
|
434
|
+
status: 'failed',
|
|
435
|
+
reason: `JSON evidence is malformed: ${error instanceof Error ? error.message : String(error)}`,
|
|
436
|
+
capabilities: {
|
|
437
|
+
...roleCoverage('data', {
|
|
438
|
+
basis: 'path',
|
|
439
|
+
reason: 'JSON extension with invalid content; role-specific analysis did not complete',
|
|
440
|
+
}, 'data-evidence'),
|
|
441
|
+
completed: ['file-evidence'],
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
446
|
+
if ('$schema' in value &&
|
|
447
|
+
typeof value.$schema === 'string' &&
|
|
448
|
+
/^https?:\/\/json-schema\.org\/(?:draft-0[467]|draft\/20\d\d-\d\d)\/schema#?$/.test(value.$schema))
|
|
449
|
+
return report(roleCoverage('schema', {
|
|
450
|
+
basis: 'content',
|
|
451
|
+
reason: `Valid JSON declares the JSON Schema dialect ${value.$schema}`,
|
|
452
|
+
}, 'schema-evidence'));
|
|
453
|
+
if (basename(path) === 'package-lock.json' &&
|
|
454
|
+
'lockfileVersion' in value &&
|
|
455
|
+
typeof value.lockfileVersion === 'number' &&
|
|
456
|
+
'packages' in value &&
|
|
457
|
+
value.packages !== null &&
|
|
458
|
+
typeof value.packages === 'object' &&
|
|
459
|
+
!Array.isArray(value.packages))
|
|
460
|
+
return report(roleCoverage('generated-data', {
|
|
461
|
+
basis: 'path-and-content',
|
|
462
|
+
reason: 'npm lockfile path with numeric lockfileVersion and packages map; generated data evidence only',
|
|
463
|
+
}, 'data-evidence'));
|
|
464
|
+
if (['package.json', 'tsconfig.json', 'jsconfig.json', 'composer.json', 'deno.json'].includes(basename(path)) ||
|
|
465
|
+
/(?:^|\/)\.vscode\/(?:settings|extensions|launch|tasks)\.json$/.test(path))
|
|
466
|
+
return report(roleCoverage('configuration', {
|
|
467
|
+
basis: 'path-and-content',
|
|
468
|
+
reason: `Recognized configuration path ${path} contains a valid JSON object; it is not executed`,
|
|
469
|
+
}, 'configuration-evidence'));
|
|
470
|
+
}
|
|
471
|
+
return report(roleCoverage('data', {
|
|
472
|
+
basis: 'content',
|
|
473
|
+
reason: 'Valid ordinary JSON without positive configuration, schema or generated-data provenance',
|
|
474
|
+
}, 'data-evidence'));
|
|
475
|
+
}
|
|
476
|
+
if (extension === '.snap') {
|
|
477
|
+
const header = /^\/\/ (?:Vitest|Jest) Snapshot v1,[^\r\n]*\r?\n/.exec(content);
|
|
478
|
+
const literal = String.raw `\x60(?:\\[\s\S]|[^\x60\\$]|\$(?!\{))*\x60`;
|
|
479
|
+
if (header &&
|
|
480
|
+
new RegExp(String.raw `^(?:\s*exports\[${literal}\]\s*=\s*${literal};\s*)+$`).test(content.slice(header[0].length)))
|
|
481
|
+
return report(roleCoverage('generated-data', {
|
|
482
|
+
basis: 'path-and-content',
|
|
483
|
+
reason: 'Recognized generated snapshot header and literal-only snapshot assignments',
|
|
484
|
+
}, 'data-evidence'));
|
|
485
|
+
}
|
|
486
|
+
return report(roleCoverage('unknown', {
|
|
487
|
+
basis: 'unknown',
|
|
488
|
+
reason: 'No positive role evidence; potentially executable content remains unassessed',
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
292
491
|
// ---------------------------------------------------------------------------
|
|
293
492
|
// Main parse loop
|
|
294
493
|
// ---------------------------------------------------------------------------
|
|
295
494
|
// ---------------------------------------------------------------------------
|
|
296
495
|
// Parallel parse path helpers (Wave H — T540)
|
|
297
496
|
// ---------------------------------------------------------------------------
|
|
298
|
-
/**
|
|
299
|
-
* Map a `WorkerParsedSymbol` to a `GraphNode` for consumption by the
|
|
300
|
-
* knowledge graph. The worker's extraction mirrors the sequential extractor
|
|
301
|
-
* in structure so this conversion is 1:1.
|
|
302
|
-
*/
|
|
303
|
-
function workerSymbolToGraphNode(sym) {
|
|
304
|
-
return {
|
|
305
|
-
id: sym.id,
|
|
306
|
-
kind: sym.kind,
|
|
307
|
-
name: sym.name,
|
|
308
|
-
filePath: sym.filePath,
|
|
309
|
-
startLine: sym.startLine,
|
|
310
|
-
endLine: sym.endLine,
|
|
311
|
-
language: sym.language,
|
|
312
|
-
exported: sym.exported,
|
|
313
|
-
parameters: sym.parameters,
|
|
314
|
-
returnType: sym.returnType,
|
|
315
|
-
docSummary: sym.docSummary,
|
|
316
|
-
parent: sym.parent,
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
497
|
/**
|
|
320
498
|
* Run the parse loop using a worker pool for parallel parsing.
|
|
321
499
|
*
|
|
@@ -350,20 +528,26 @@ async function runParallelParseLoop(parseableFiles, graph, symbolTable, importCt
|
|
|
350
528
|
const file = parseableFiles[i];
|
|
351
529
|
try {
|
|
352
530
|
const absPath = file.path.startsWith('/') ? file.path : `${repoPath}/${file.path}`;
|
|
353
|
-
|
|
354
|
-
|
|
531
|
+
options.parserLimits?.signal?.throwIfAborted();
|
|
532
|
+
const bytes = await fs.readFile(absPath);
|
|
533
|
+
if (file.contentHash &&
|
|
534
|
+
createHash('sha256').update(bytes).digest('hex') !== file.contentHash) {
|
|
535
|
+
throw new Error('Source changed between scanning and parsing');
|
|
536
|
+
}
|
|
537
|
+
const { signal: _signal, ...limits } = options.parserLimits ?? {};
|
|
538
|
+
workerInputs.push({
|
|
539
|
+
path: file.path,
|
|
540
|
+
content: bytes.toString('utf8'),
|
|
541
|
+
limits,
|
|
542
|
+
publicationGeneration: options.publicationGeneration,
|
|
543
|
+
});
|
|
355
544
|
}
|
|
356
|
-
catch {
|
|
357
|
-
|
|
545
|
+
catch (error) {
|
|
546
|
+
options.parserLimits?.signal?.throwIfAborted();
|
|
547
|
+
throw new Error(`Parser input unavailable: ${file.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
358
548
|
}
|
|
359
549
|
}
|
|
360
|
-
|
|
361
|
-
try {
|
|
362
|
-
pool = createWorkerPool(workerUrl);
|
|
363
|
-
}
|
|
364
|
-
catch {
|
|
365
|
-
return null; // Worker script unavailable — fall back to sequential
|
|
366
|
-
}
|
|
550
|
+
const pool = createWorkerPool(workerUrl, undefined, options.parserLimits, options.parserExecution);
|
|
367
551
|
let filesProcessedSoFar = 0;
|
|
368
552
|
let workerResults;
|
|
369
553
|
try {
|
|
@@ -375,12 +559,6 @@ async function runParallelParseLoop(parseableFiles, graph, symbolTable, importCt
|
|
|
375
559
|
}
|
|
376
560
|
});
|
|
377
561
|
}
|
|
378
|
-
catch (err) {
|
|
379
|
-
await pool.terminate().catch(() => undefined);
|
|
380
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
381
|
-
process.stderr.write(`[nexus] Worker pool failed (${msg}), falling back to sequential.\n`);
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
562
|
finally {
|
|
385
563
|
await pool.terminate().catch(() => undefined);
|
|
386
564
|
}
|
|
@@ -390,16 +568,19 @@ async function runParallelParseLoop(parseableFiles, graph, symbolTable, importCt
|
|
|
390
568
|
const allParallelReExports = [];
|
|
391
569
|
const allHeritage = [];
|
|
392
570
|
const allCalls = [];
|
|
393
|
-
//
|
|
394
|
-
// only. Workers do not transport ASTs, so parallel mode does not accumulate
|
|
395
|
-
// accesses via this path — the sequential fallback handles small repos and
|
|
396
|
-
// fixture tests. A future wave can add worker support for access extraction.
|
|
571
|
+
// Workers invoke the same extractor, including access records.
|
|
397
572
|
const allParallelAccesses = [];
|
|
398
573
|
for (const workerResult of workerResults) {
|
|
574
|
+
for (const report of workerResult.reports)
|
|
575
|
+
options.onFileReport?.(report);
|
|
576
|
+
const failed = workerResult.reports.filter((report) => report.status !== 'analyzed');
|
|
577
|
+
if (failed.length > 0)
|
|
578
|
+
throw new Error(`Parser worker incomplete: ${failed.map((report) => `${report.path}: ${report.reason}`).join('; ')}`);
|
|
579
|
+
allParallelAccesses.push(...workerResult.accesses);
|
|
399
580
|
// Register symbols into SymbolTable and add graph nodes
|
|
400
581
|
const fileGraphNodes = new Map();
|
|
401
582
|
for (const sym of workerResult.symbols) {
|
|
402
|
-
const graphNode =
|
|
583
|
+
const graphNode = sym;
|
|
403
584
|
registerInSymbolTable([graphNode], symbolTable);
|
|
404
585
|
graph.addNode(graphNode);
|
|
405
586
|
// Group by filePath for defines edge emission below
|
|
@@ -413,40 +594,10 @@ async function runParallelParseLoop(parseableFiles, graph, symbolTable, importCt
|
|
|
413
594
|
for (const [filePath, nodes] of fileGraphNodes) {
|
|
414
595
|
emitDefinesEdges(filePath, nodes, graph);
|
|
415
596
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
rawImportPath: imp.rawImportPath,
|
|
421
|
-
namedBindings: imp.namedBindings,
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
// Collect re-exports for barrel map construction (T617)
|
|
425
|
-
if (workerResult.reExports) {
|
|
426
|
-
for (const re of workerResult.reExports) {
|
|
427
|
-
allParallelReExports.push(re);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
// Map heritage — worker uses same field names as sequential path
|
|
431
|
-
for (const h of workerResult.heritage) {
|
|
432
|
-
allHeritage.push({
|
|
433
|
-
filePath: h.filePath,
|
|
434
|
-
typeName: h.typeName,
|
|
435
|
-
typeNodeId: h.typeNodeId,
|
|
436
|
-
kind: h.kind,
|
|
437
|
-
parentName: h.parentName,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
// Map calls from worker format to pipeline format
|
|
441
|
-
for (const c of workerResult.calls) {
|
|
442
|
-
allCalls.push({
|
|
443
|
-
filePath: c.filePath,
|
|
444
|
-
sourceId: c.sourceId,
|
|
445
|
-
calledName: c.calledName,
|
|
446
|
-
callForm: c.callForm,
|
|
447
|
-
receiverName: c.receiverName,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
597
|
+
allExtractedImports.push(...workerResult.imports);
|
|
598
|
+
allParallelReExports.push(...workerResult.reExports);
|
|
599
|
+
allHeritage.push(...workerResult.heritage);
|
|
600
|
+
allCalls.push(...workerResult.calls);
|
|
450
601
|
}
|
|
451
602
|
if (onProgress && total > 0) {
|
|
452
603
|
const lastFile = parseableFiles[total - 1];
|
|
@@ -475,7 +626,7 @@ async function runParallelParseLoop(parseableFiles, graph, symbolTable, importCt
|
|
|
475
626
|
process.stderr.write(`[parse-loop-debug] internal.ts in workerInputs = ${internalInput ? 'YES: ' + internalInput.path : 'NO'}\n`);
|
|
476
627
|
// Check if internal.ts had symbols or imports
|
|
477
628
|
for (const wr of workerResults) {
|
|
478
|
-
const hasInternalSymbol = wr.symbols.some((s) => s.filePath
|
|
629
|
+
const hasInternalSymbol = wr.symbols.some((s) => s.filePath?.includes('core/src/internal'));
|
|
479
630
|
const hasInternalImport = wr.imports.some((i) => i.filePath.includes('core/src/internal'));
|
|
480
631
|
const hasInternalReExport = wr.reExports.some((r) => r.filePath.includes('core/src/internal'));
|
|
481
632
|
if (hasInternalSymbol || hasInternalImport || hasInternalReExport) {
|
|
@@ -522,12 +673,58 @@ async function runParallelParseLoop(parseableFiles, graph, symbolTable, importCt
|
|
|
522
673
|
*/
|
|
523
674
|
export async function runParseLoop(files, graph, symbolTable, importCtx, repoPath, options = {}) {
|
|
524
675
|
const { tsconfigPaths = null, namedImportMap = new Map(), onProgress } = options;
|
|
525
|
-
//
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
676
|
+
// Attach one capability contract to sequential and worker-returned reports.
|
|
677
|
+
const callerReports = options.onFileReport;
|
|
678
|
+
const classifications = new Map();
|
|
679
|
+
const emitReport = (report) => {
|
|
680
|
+
const coverage = classifications.get(report.path);
|
|
681
|
+
if (!coverage)
|
|
682
|
+
throw new Error(`Missing file capability classification: ${report.path}`);
|
|
683
|
+
callerReports?.({
|
|
684
|
+
...report,
|
|
685
|
+
capabilities: {
|
|
686
|
+
...coverage,
|
|
687
|
+
requested: [...coverage.requested],
|
|
688
|
+
completed: report.status === 'analyzed' && coverage.role === 'executable'
|
|
689
|
+
? [...coverage.requested]
|
|
690
|
+
: [...coverage.completed],
|
|
691
|
+
limitations: [...coverage.limitations],
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
options = { ...options, onFileReport: emitReport };
|
|
696
|
+
const parseableFiles = [];
|
|
697
|
+
for (const file of files) {
|
|
698
|
+
options.parserLimits?.signal?.throwIfAborted();
|
|
699
|
+
let report;
|
|
700
|
+
try {
|
|
701
|
+
report = await classifyFileCapabilities(file, repoPath, options.parserLimits?.signal);
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
options.parserLimits?.signal?.throwIfAborted();
|
|
705
|
+
report = {
|
|
706
|
+
path: file.path,
|
|
707
|
+
status: 'failed',
|
|
708
|
+
reason: `read: ${error instanceof Error ? error.message : String(error)}`,
|
|
709
|
+
capabilities: {
|
|
710
|
+
...roleCoverage('unknown', {
|
|
711
|
+
basis: 'unknown',
|
|
712
|
+
reason: 'Role evidence unavailable because the source read failed',
|
|
713
|
+
}),
|
|
714
|
+
completed: [],
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
if (!report.capabilities)
|
|
719
|
+
throw new Error(`Missing file capability assessment: ${file.path}`);
|
|
720
|
+
classifications.set(file.path, report.capabilities);
|
|
721
|
+
if (report.status !== 'failed' &&
|
|
722
|
+
report.capabilities.role === 'executable' &&
|
|
723
|
+
PARSEABLE_LANGUAGES.has(detectLanguageFromPath(file.path) ?? ''))
|
|
724
|
+
parseableFiles.push(file);
|
|
725
|
+
else
|
|
726
|
+
emitReport(report);
|
|
727
|
+
}
|
|
531
728
|
const total = parseableFiles.length;
|
|
532
729
|
if (total === 0)
|
|
533
730
|
return {
|
|
@@ -538,21 +735,25 @@ export async function runParseLoop(files, graph, symbolTable, importCtx, repoPat
|
|
|
538
735
|
};
|
|
539
736
|
// Wave H: Check thresholds for parallel dispatch
|
|
540
737
|
const totalBytes = files.reduce((acc, f) => acc + f.size, 0);
|
|
541
|
-
const useWorkers =
|
|
542
|
-
|
|
738
|
+
const useWorkers = Boolean(options.parserExecution) ||
|
|
739
|
+
total >= WORKER_FILE_THRESHOLD ||
|
|
740
|
+
totalBytes >= WORKER_BYTE_THRESHOLD;
|
|
741
|
+
if (useWorkers && (options.parserExecution || !callerReports)) {
|
|
543
742
|
process.stderr.write(`[nexus] Parallel parse: ${total} files, ${Math.round(totalBytes / 1024)}KB total — spawning worker pool\n`);
|
|
544
743
|
try {
|
|
545
|
-
const parallelResult = await runParallelParseLoop(parseableFiles, graph, symbolTable, importCtx, repoPath, { tsconfigPaths, namedImportMap, onProgress });
|
|
744
|
+
const parallelResult = await runParallelParseLoop(parseableFiles, graph, symbolTable, importCtx, repoPath, { ...options, tsconfigPaths, namedImportMap, onProgress });
|
|
546
745
|
if (parallelResult !== null) {
|
|
547
746
|
process.stderr.write('[nexus] Parallel parse complete.\n');
|
|
548
747
|
return parallelResult;
|
|
549
748
|
}
|
|
749
|
+
if (options.parserExecution)
|
|
750
|
+
throw new Error('Isolated parser executable unavailable');
|
|
550
751
|
// Worker unavailable — fall through to sequential
|
|
551
752
|
process.stderr.write('[nexus] Worker script not found — using sequential parse.\n');
|
|
552
753
|
}
|
|
553
754
|
catch (err) {
|
|
554
755
|
const msg = err instanceof Error ? err.message : String(err);
|
|
555
|
-
|
|
756
|
+
throw new Error(`Parallel parser failed; generation not published: ${msg}`);
|
|
556
757
|
}
|
|
557
758
|
}
|
|
558
759
|
const allExtractedImports = [];
|
|
@@ -562,6 +763,13 @@ export async function runParseLoop(files, graph, symbolTable, importCtx, repoPat
|
|
|
562
763
|
const allAccesses = [];
|
|
563
764
|
const parser = getParser();
|
|
564
765
|
if (!parser) {
|
|
766
|
+
for (const file of parseableFiles) {
|
|
767
|
+
options.onFileReport?.({
|
|
768
|
+
path: file.path,
|
|
769
|
+
status: 'failed',
|
|
770
|
+
reason: 'tree-sitter native module unavailable',
|
|
771
|
+
});
|
|
772
|
+
}
|
|
565
773
|
process.stderr.write('[nexus] WARNING: tree-sitter native module not available — parse loop skipped.\n');
|
|
566
774
|
return {
|
|
567
775
|
allHeritage: [],
|
|
@@ -572,6 +780,7 @@ export async function runParseLoop(files, graph, symbolTable, importCtx, repoPat
|
|
|
572
780
|
}
|
|
573
781
|
let filesProcessed = 0;
|
|
574
782
|
for (const file of parseableFiles) {
|
|
783
|
+
options.parserLimits?.signal?.throwIfAborted();
|
|
575
784
|
filesProcessed++;
|
|
576
785
|
// Progress reporting (every file for small repos, every 10 for larger ones)
|
|
577
786
|
if (onProgress && (total <= 100 || filesProcessed % 10 === 0 || filesProcessed === total)) {
|
|
@@ -592,6 +801,11 @@ export async function runParseLoop(files, graph, symbolTable, importCtx, repoPat
|
|
|
592
801
|
// Load grammar
|
|
593
802
|
const grammar = loadGrammar(grammarKey);
|
|
594
803
|
if (!grammar) {
|
|
804
|
+
options.onFileReport?.({
|
|
805
|
+
path: file.path,
|
|
806
|
+
status: 'failed',
|
|
807
|
+
reason: `No grammar for ${lang}`,
|
|
808
|
+
});
|
|
595
809
|
process.stderr.write(`[nexus] SKIP: no grammar for ${lang} (file: ${file.path})\n`);
|
|
596
810
|
continue;
|
|
597
811
|
}
|
|
@@ -599,57 +813,45 @@ export async function runParseLoop(files, graph, symbolTable, importCtx, repoPat
|
|
|
599
813
|
let source;
|
|
600
814
|
try {
|
|
601
815
|
const absPath = file.path.startsWith('/') ? file.path : `${repoPath}/${file.path}`;
|
|
602
|
-
|
|
816
|
+
const bytes = await fs.readFile(absPath);
|
|
817
|
+
if (file.contentHash &&
|
|
818
|
+
createHash('sha256').update(bytes).digest('hex') !== file.contentHash) {
|
|
819
|
+
throw new Error('Source changed between scanning and parsing');
|
|
820
|
+
}
|
|
821
|
+
source = bytes.toString('utf-8');
|
|
603
822
|
}
|
|
604
823
|
catch (err) {
|
|
605
824
|
const msg = err instanceof Error ? err.message : String(err);
|
|
825
|
+
options.onFileReport?.({ path: file.path, status: 'failed', reason: `read: ${msg}` });
|
|
606
826
|
process.stderr.write(`[nexus] SKIP read error: ${file.path}: ${msg}\n`);
|
|
607
827
|
continue;
|
|
608
828
|
}
|
|
609
|
-
// tree-sitter 0.21.x has a hard limit of 32767 chars per file (15-bit offset counter).
|
|
610
|
-
// Files larger than this use a regex fallback for imports and re-exports only.
|
|
611
|
-
const sanitized = sanitizeForParsing(source);
|
|
612
|
-
if (sanitized.length > TREE_SITTER_MAX_CHARS) {
|
|
613
|
-
// Regex-based fallback: extract re-exports and imports for barrel tracing.
|
|
614
|
-
// Symbol definitions, heritage, and calls are skipped for oversized files.
|
|
615
|
-
try {
|
|
616
|
-
const regexReExports = extractReExportsViaRegex(source, file.path);
|
|
617
|
-
for (const re of regexReExports) {
|
|
618
|
-
allReExports.push(re);
|
|
619
|
-
}
|
|
620
|
-
const regexImports = extractImportsViaRegex(source, file.path);
|
|
621
|
-
for (const imp of regexImports) {
|
|
622
|
-
allExtractedImports.push(imp);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
catch (err) {
|
|
626
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
627
|
-
process.stderr.write(`[nexus] SKIP regex fallback error: ${file.path}: ${msg}\n`);
|
|
628
|
-
}
|
|
629
|
-
continue;
|
|
630
|
-
}
|
|
631
829
|
// Parse with tree-sitter
|
|
632
830
|
let rootNode;
|
|
633
831
|
try {
|
|
634
832
|
parser.setLanguage(grammar);
|
|
635
|
-
const tree = parser.
|
|
833
|
+
const tree = parseOriginalSource(parser, source, options.parserLimits);
|
|
636
834
|
rootNode = tree.rootNode;
|
|
637
835
|
}
|
|
638
836
|
catch (err) {
|
|
639
837
|
const msg = err instanceof Error ? err.message : String(err);
|
|
838
|
+
options.onFileReport?.({ path: file.path, status: 'failed', reason: `parse: ${msg}` });
|
|
839
|
+
options.parserLimits?.signal?.throwIfAborted();
|
|
640
840
|
process.stderr.write(`[nexus] SKIP parse error: ${file.path}: ${msg}\n`);
|
|
641
841
|
continue;
|
|
642
842
|
}
|
|
643
843
|
// Extract definitions, imports, heritage, calls, and re-exports — dispatch by language
|
|
644
844
|
let extracted;
|
|
645
845
|
try {
|
|
646
|
-
extracted = runExtractor(lang, rootNode, file.path);
|
|
846
|
+
extracted = runExtractor(lang, rootNode, file.path, createHash('sha256').update(source).digest('hex'), options.publicationGeneration);
|
|
647
847
|
}
|
|
648
848
|
catch (err) {
|
|
649
849
|
const msg = err instanceof Error ? err.message : String(err);
|
|
850
|
+
options.onFileReport?.({ path: file.path, status: 'failed', reason: `extract: ${msg}` });
|
|
650
851
|
process.stderr.write(`[nexus] SKIP extract error: ${file.path}: ${msg}\n`);
|
|
651
852
|
continue;
|
|
652
853
|
}
|
|
854
|
+
options.onFileReport?.({ path: file.path, status: 'analyzed' });
|
|
653
855
|
// Register in SymbolTable
|
|
654
856
|
registerInSymbolTable(extracted.definitions, symbolTable);
|
|
655
857
|
// Add nodes to graph
|