genesis-compiler 1.7.8 → 1.8.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/README.md +56 -9
- package/docs/assurance-model.md +4 -2
- package/docs/templates.md +11 -0
- package/package.json +5 -3
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/prompts/describe.txt +5 -0
- package/prompts/program.txt +9 -1
- package/skills/genesis-program/SKILL.md +41 -4
- package/skills/genesis-project/SKILL.md +2 -2
- package/src/index/architecture-observations.js +167 -0
- package/src/index/architecture.js +227 -0
- package/src/index/code-index.js +66 -6
- package/src/index/code-indexers/ast-grep.js +223 -19
- package/src/index/code-indexers/npm-packages.js +200 -0
- package/src/index/context.js +1 -1
- package/src/index/program.js +28 -3
- package/src/index/project-inspection.js +26 -1
- package/src/index/session-context.js +1 -1
- package/src/index/source-explanation.js +31 -0
- package/src/index/stack-package-discovery.js +25 -0
- package/src/index/stack-ui-discovery.js +38 -0
- package/src/index/stack.js +6 -0
- package/src/index/subsystems.js +16 -3
- package/src/index/utils.js +2 -2
- package/src/index.js +8 -1
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { asDiagnostic } from '../errors.js';
|
|
6
|
+
import { sha256 } from '../utils.js';
|
|
6
7
|
import { runProcess } from '../process.js';
|
|
7
8
|
import { parserAbortSignal, prepareCodeParsers } from './parsers.js';
|
|
8
9
|
|
|
@@ -52,6 +53,136 @@ const DEFINITIONS = Object.freeze({
|
|
|
52
53
|
},
|
|
53
54
|
});
|
|
54
55
|
const CALLABLE_KINDS = new Set(Object.values(DEFINITIONS).flatMap(({ kinds }) => kinds));
|
|
56
|
+
const DECLARATION_KINDS = {
|
|
57
|
+
javascript: ['class_declaration', 'class', 'interface_declaration', 'type_alias_declaration', 'enum_declaration', 'variable_declarator', 'field_definition', 'public_field_definition'],
|
|
58
|
+
python: ['class_definition', 'assignment'], java: ['class_declaration', 'interface_declaration', 'enum_declaration', 'field_declaration', 'constant_declaration'],
|
|
59
|
+
csharp: ['class_declaration', 'interface_declaration', 'struct_declaration', 'enum_declaration', 'field_declaration', 'property_declaration'],
|
|
60
|
+
cpp: ['class_specifier', 'struct_specifier', 'enum_specifier', 'declaration', 'field_declaration'], php: ['class_declaration', 'interface_declaration', 'trait_declaration', 'const_declaration', 'property_declaration'],
|
|
61
|
+
go: ['type_spec', 'const_spec', 'var_spec'], rust: ['struct_item', 'enum_item', 'trait_item', 'type_item', 'const_item', 'static_item'],
|
|
62
|
+
ruby: ['class', 'module', 'assignment'], kotlin: ['class_declaration', 'object_declaration', 'property_declaration'], shell: [],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function nodeRange(node, lineOffset = 0, columnOffset = 0) {
|
|
66
|
+
const range = node.range();
|
|
67
|
+
let start = range.start;
|
|
68
|
+
// Rust attributes are siblings of the item whose semantics they govern.
|
|
69
|
+
if (node.kind().endsWith('_item')) {
|
|
70
|
+
const siblings = node.parent()?.children() || [];
|
|
71
|
+
for (let index = siblings.findIndex(child => child.id() === node.id()) - 1; index >= 0; index--) {
|
|
72
|
+
const previous = siblings[index];
|
|
73
|
+
if (previous.kind() === 'attribute_item') start = previous.range().start;
|
|
74
|
+
else if (previous.kind() !== 'line_comment' && previous.kind() !== 'block_comment') break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// napi ranges use UTF-16 columns, matching the editor; do not convert them as bytes.
|
|
78
|
+
const position = point => ({
|
|
79
|
+
line: point.line + lineOffset + 1,
|
|
80
|
+
column: point.column + 1 + (point.line === 0 ? columnOffset : 0),
|
|
81
|
+
});
|
|
82
|
+
return { start: position(start), end: position(range.end) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function declarationNode(node) {
|
|
86
|
+
if (['arrow_function', 'function_expression'].includes(node.kind())) {
|
|
87
|
+
if (['field_definition', 'public_field_definition'].includes(node.parent()?.kind())) return node.parent();
|
|
88
|
+
return ancestor(node, ['lexical_declaration', 'variable_declaration']) || node;
|
|
89
|
+
}
|
|
90
|
+
if (['const_spec', 'var_spec', 'type_spec'].includes(node.kind())) return ancestor(node, ['const_declaration', 'var_declaration', 'type_declaration']) || node;
|
|
91
|
+
return node.parent()?.kind() === 'decorated_definition' ? node.parent() : node;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function declarationsInTree(root, language, source, lineOffset, columnOffset, pythonExports = null) {
|
|
95
|
+
const exportedNames = language === 'javascript' ? javascriptExportedNames(source) : pythonExports?.names;
|
|
96
|
+
const result = [];
|
|
97
|
+
for (const kind of DECLARATION_KINDS[language] || []) {
|
|
98
|
+
let nodes;
|
|
99
|
+
try { nodes = root.findAll({ rule: { kind } }); } catch { continue; }
|
|
100
|
+
for (const node of nodes) {
|
|
101
|
+
let declaredNodes = [node];
|
|
102
|
+
if (language === 'java' && ['field_declaration', 'constant_declaration'].includes(kind)) declaredNodes = node.children().filter(child => child.kind() === 'variable_declarator');
|
|
103
|
+
if (language === 'csharp' && kind === 'field_declaration') declaredNodes = node.children().find(child => child.kind() === 'variable_declaration')?.children().filter(child => child.kind() === 'variable_declarator') || [];
|
|
104
|
+
if (language === 'php' && ['const_declaration', 'property_declaration'].includes(kind)) declaredNodes = node.children().filter(child => ['const_element', 'property_element'].includes(child.kind()));
|
|
105
|
+
if (language === 'go' && ['const_spec', 'var_spec'].includes(kind)) declaredNodes = node.children().filter(child => child.kind() === 'identifier');
|
|
106
|
+
if (language === 'ruby' && kind === 'assignment' && safeField(node, 'left')?.kind() !== 'constant') continue;
|
|
107
|
+
if (language === 'python' && kind === 'assignment' && (safeField(node, 'left')?.text() === '__all__' || node.ancestors().some(parent => parent.kind() === 'function_definition'))) continue;
|
|
108
|
+
if (['variable_declarator', 'field_definition', 'public_field_definition'].includes(kind) && ['arrow_function', 'function_expression'].includes(safeField(node, 'value')?.kind())) continue;
|
|
109
|
+
const declaration = kind === 'variable_declarator' ? node.parent() : declarationNode(node);
|
|
110
|
+
const range = nodeRange(declaration, lineOffset, columnOffset);
|
|
111
|
+
for (const namedNode of declaredNodes) {
|
|
112
|
+
const name = namedNode.kind() === 'identifier' ? namedNode.text() : declaredName(namedNode);
|
|
113
|
+
if (!name) continue;
|
|
114
|
+
const prototype = language === 'cpp' && safeField(node, 'declarator')?.kind() === 'function_declarator' && ['identifier', 'field_identifier'].includes(safeField(safeField(node, 'declarator'), 'declarator')?.kind());
|
|
115
|
+
const value = ['assignment', 'variable_declarator', 'field_definition', 'public_field_definition', 'const_spec', 'var_spec', 'const_item', 'static_item', 'field_declaration', 'property_declaration', 'const_declaration', 'constant_declaration', 'declaration'].includes(kind);
|
|
116
|
+
const scope = language === 'rust' ? node.children().find(child => child.kind() === 'visibility_modifier')?.text() || 'private' : null;
|
|
117
|
+
result.push({ name, qualifiedName: [containerName(node, language), name].filter(Boolean).join(language === 'php' ? '::' : '.'), kind: prototype ? 'function' : kind.includes('class') || kind === 'class' ? 'class' : kind.includes('interface') ? 'interface' : value ? 'value' : 'type', visibility: visibility(kind === 'variable_declarator' ? node.parent() : node, name, language, exportedNames), ...(scope ? { visibilityScope: scope === 'pub' ? 'public' : scope.replace(/^pub\((.*)\)$/u, '$1') } : {}), ...range, line: range.start.line, column: range.start.column });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// These are syntax observations. Resolution belongs to the native index, and
|
|
125
|
+
// unsupported/dynamic targets remain explicit rather than guessed.
|
|
126
|
+
function dependencyReferences(root, language, source, lineOffset, columnOffset) {
|
|
127
|
+
const kinds = {
|
|
128
|
+
javascript: ['import_statement', 'export_statement', 'call_expression'],
|
|
129
|
+
python: ['import_statement', 'import_from_statement'], cpp: ['preproc_include'],
|
|
130
|
+
java: ['import_declaration'], csharp: ['using_directive'], go: ['import_spec'],
|
|
131
|
+
rust: ['use_declaration', 'mod_item'], php: ['namespace_use_declaration', 'include_expression', 'require_expression'],
|
|
132
|
+
ruby: ['call'], kotlin: ['import_header'], shell: ['command'],
|
|
133
|
+
}[language] || [];
|
|
134
|
+
const references = [];
|
|
135
|
+
for (const kind of kinds) {
|
|
136
|
+
let nodes;
|
|
137
|
+
try { nodes = root.findAll({ rule: { kind } }); } catch { continue; }
|
|
138
|
+
for (const node of nodes) {
|
|
139
|
+
const text = node.text();
|
|
140
|
+
let target;
|
|
141
|
+
let relationKind = 'import';
|
|
142
|
+
let typeOnly = false;
|
|
143
|
+
let exports;
|
|
144
|
+
if (language === 'javascript') {
|
|
145
|
+
if (kind === 'call_expression' && !/^(?:require|import)\s*\(/u.test(text)) continue;
|
|
146
|
+
if (kind === 'export_statement' && !/\bfrom\s+['"]/u.test(text)) continue;
|
|
147
|
+
target = text.match(/(?:\bfrom\s*|^(?:require|import)\s*\(\s*|^import\s*)['"]([^'"]+)['"]/u)?.[1];
|
|
148
|
+
if (kind === 'export_statement') relationKind = 're-export';
|
|
149
|
+
const clause = node.children().find(child => ['import_clause', 'export_clause'].includes(child.kind()));
|
|
150
|
+
const specifiers = clause?.findAll({ rule: { kind: kind === 'export_statement' ? 'export_specifier' : 'import_specifier' } }) || [];
|
|
151
|
+
if (kind === 'export_statement') {
|
|
152
|
+
exports = specifiers.map(specifier => ({ name: safeField(specifier, 'name')?.text(), alias: safeField(specifier, 'alias')?.text() || safeField(specifier, 'name')?.text() })).filter(entry => entry.name && entry.alias);
|
|
153
|
+
// Namespace exports are values, not the individual operations inside
|
|
154
|
+
// them. Leave those unresolved rather than inventing signatures.
|
|
155
|
+
if (!clause && node.children().some(child => child.kind() === '*') && !node.children().some(child => child.kind() === 'namespace_export')) exports = [{ name: '*', alias: '*' }];
|
|
156
|
+
}
|
|
157
|
+
typeOnly = node.children().some(child => child.kind() === 'type') ||
|
|
158
|
+
(specifiers.length > 0 && specifiers.every(specifier => specifier.children().some(child => child.kind() === 'type')) && !clause.children().some(child => ['identifier', 'namespace_import'].includes(child.kind()))) ||
|
|
159
|
+
(kind === 'call_expression' && /^import\s*\(/u.test(text) && node.ancestors().some(parent => ['type_alias_declaration', 'type_annotation', 'type_query'].includes(parent.kind())));
|
|
160
|
+
if (typeOnly && kind !== 'export_statement') relationKind = 'type-import';
|
|
161
|
+
} else if (language === 'python') {
|
|
162
|
+
const from = text.match(/^from\s+([.\w]+)\s+import/u);
|
|
163
|
+
if (from) target = from[1];
|
|
164
|
+
else target = text.match(/^import\s+([\w.]+)/u)?.[1];
|
|
165
|
+
} else if (language === 'cpp') {
|
|
166
|
+
target = text.match(/#\s*include\s*["<]([^">]+)[">]/u)?.[1]; relationKind = 'include';
|
|
167
|
+
} else if (language === 'go') target = text.match(/["`]([^"`]+)["`]/u)?.[1];
|
|
168
|
+
else if (language === 'ruby') {
|
|
169
|
+
if (!/^require(?:_relative)?\b/u.test(text)) continue;
|
|
170
|
+
target = text.match(/['"]([^'"]+)['"]/u)?.[1];
|
|
171
|
+
if (/^require_relative\b/u.test(text) && target && !target.startsWith('.')) target = `./${target}`;
|
|
172
|
+
} else if (language === 'shell') {
|
|
173
|
+
if (!/^(?:source|\.)\s/u.test(text)) continue;
|
|
174
|
+
target = text.match(/^(?:source|\.)\s+(?:['"]([^'"]+)['"]|([^\s;$]+))/u)?.slice(1).find(Boolean);
|
|
175
|
+
if (target?.includes('$')) target = null;
|
|
176
|
+
} else if (language === 'java' || language === 'kotlin') target = text.match(/^import\s+(?:static\s+)?([\w.*]+)/u)?.[1];
|
|
177
|
+
else if (language === 'csharp') target = text.match(/^using\s+(?:static\s+)?([\w.]+)\s*;/u)?.[1];
|
|
178
|
+
else if (language === 'rust') target = text.match(/^(?:pub\s+)?(?:use|mod)\s+([\w:]+)/u)?.[1];
|
|
179
|
+
else if (language === 'php') target = text.match(/['"]([^'"]+)['"]/u)?.[1] || text.match(/^use\s+([\w\\]+)/u)?.[1];
|
|
180
|
+
const range = nodeRange(node, lineOffset, columnOffset);
|
|
181
|
+
references.push({ specifier: target || null, kind: relationKind, typeOnly, ...(exports ? { exports } : {}), ...range, resolution: target ? 'pending' : 'unresolved', reason: target ? '' : 'Dynamic or unsupported dependency expression.' });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return references;
|
|
185
|
+
}
|
|
55
186
|
|
|
56
187
|
function safeField(node, name) {
|
|
57
188
|
try { return node.field(name); } catch { return null; }
|
|
@@ -71,8 +202,13 @@ function firstDescendant(node, kinds) {
|
|
|
71
202
|
function declaredName(node) {
|
|
72
203
|
const direct = safeField(node, 'name');
|
|
73
204
|
if (direct) return direct.text().trim();
|
|
205
|
+
if (node.kind() === 'field_definition') return safeField(node, 'property')?.text() || '';
|
|
206
|
+
if (node.kind() === 'assignment') return ['identifier', 'constant'].includes(safeField(node, 'left')?.kind()) ? safeField(node, 'left').text() : '';
|
|
207
|
+
if (node.kind() === 'property_declaration') return node.children().find(child => child.kind() === 'variable_declaration')?.children().find(child => child.kind() === 'simple_identifier')?.text() || '';
|
|
74
208
|
const parent = node.parent();
|
|
209
|
+
if (parent?.kind() === 'export_statement' && parent.children().some(child => child.kind() === 'default')) return 'default';
|
|
75
210
|
if (['arrow_function', 'function_expression'].includes(node.kind())) {
|
|
211
|
+
if (['field_definition', 'public_field_definition'].includes(parent?.kind())) return (safeField(parent, 'property') || safeField(parent, 'name'))?.text() || '';
|
|
76
212
|
if (parent?.kind() === 'variable_declarator') {
|
|
77
213
|
return safeField(parent, 'name')?.text().trim() || parent.child(0)?.text().trim() || '';
|
|
78
214
|
}
|
|
@@ -141,7 +277,7 @@ function containerName(node, language) {
|
|
|
141
277
|
const receiver = safeField(node, 'receiver')?.text().trim();
|
|
142
278
|
if (receiver) return receiver.replace(/^\(|\)$/gu, '').replace(/^\w+\s+\*?/u, '').trim();
|
|
143
279
|
}
|
|
144
|
-
const container = node.ancestors().find((candidate) => CONTAINER_KINDS.has(candidate.kind()));
|
|
280
|
+
const container = node.ancestors().find((candidate) => CONTAINER_KINDS.has(candidate.kind()) && !(language === 'python' && candidate.kind() === 'module'));
|
|
145
281
|
if (!container) {
|
|
146
282
|
const parentFunction = node.ancestors().find((candidate) => DEFINITIONS.javascript.kinds.includes(candidate.kind())
|
|
147
283
|
|| candidate.kind() === 'function_definition');
|
|
@@ -152,6 +288,7 @@ function containerName(node, language) {
|
|
|
152
288
|
|| container.text().match(/^\s*impl(?:<[^>]+>)?\s+([^\s{]+)/u)?.[1]
|
|
153
289
|
|| null;
|
|
154
290
|
}
|
|
291
|
+
if (language === 'javascript' && container.parent()?.kind() === 'export_statement' && container.parent().children().some(child => child.kind() === 'default')) return declaredName(container);
|
|
155
292
|
return safeField(container, 'name')?.text().trim()
|
|
156
293
|
|| container.children().find((child) => [
|
|
157
294
|
'constant', 'identifier', 'name', 'simple_identifier', 'type_identifier',
|
|
@@ -160,12 +297,22 @@ function containerName(node, language) {
|
|
|
160
297
|
}
|
|
161
298
|
|
|
162
299
|
function directText(node) {
|
|
163
|
-
|
|
300
|
+
// Access/storage modifiers are syntax, not words in bodies or annotations.
|
|
301
|
+
const keywords = new Set(['public', 'private', 'protected', 'internal', 'static']);
|
|
302
|
+
const relevant = child => child.kind().includes('modifier') || child.kind() === 'storage_class_specifier' || keywords.has(child.kind());
|
|
303
|
+
const pending = node.children().filter(relevant); const words = [];
|
|
304
|
+
while (pending.length) {
|
|
305
|
+
const child = pending.pop();
|
|
306
|
+
if (keywords.has(child.text())) words.push(child.text());
|
|
307
|
+
else pending.push(...child.children().filter(relevant));
|
|
308
|
+
}
|
|
309
|
+
return words.join(' ');
|
|
164
310
|
}
|
|
165
311
|
|
|
166
312
|
function cPlusPlusVisibility(node) {
|
|
313
|
+
if (node.ancestors().some(parent => parent.kind() === 'namespace_definition' && !safeField(parent, 'name'))) return 'internal';
|
|
167
314
|
const classBody = ancestor(node, ['field_declaration_list']);
|
|
168
|
-
if (!classBody) return
|
|
315
|
+
if (!classBody) return /\bstatic\b/u.test(directText(node)) ? 'internal' : 'public';
|
|
169
316
|
const classNode = ancestor(node, ['class_specifier', 'struct_specifier', 'union_specifier']);
|
|
170
317
|
let current = /^\s*class\b/u.test(classNode?.text() || '') ? 'private' : 'public';
|
|
171
318
|
for (const sibling of classBody.children()) {
|
|
@@ -205,16 +352,41 @@ function javascriptExportedNames(source) {
|
|
|
205
352
|
return names;
|
|
206
353
|
}
|
|
207
354
|
|
|
355
|
+
function pythonExportedNames(root) {
|
|
356
|
+
let names = null; let partial = false;
|
|
357
|
+
const nodes = root.findAll({ rule: { any: [{ kind: 'assignment' }, { kind: 'augmented_assignment' }, { kind: 'call' }] } });
|
|
358
|
+
for (const node of nodes.sort((a, b) => a.range().start.index - b.range().start.index)) {
|
|
359
|
+
const parents = node.ancestors();
|
|
360
|
+
if (parents.some(parent => ['function_definition', 'class_definition', 'lambda'].includes(parent.kind()))) continue;
|
|
361
|
+
const call = node.kind() === 'call';
|
|
362
|
+
if (call ? !node.findAll({ rule: { kind: 'identifier' } }).some(identifier => identifier.text() === '__all__') : safeField(node, 'left')?.text() !== '__all__') continue;
|
|
363
|
+
const right = safeField(node, 'right');
|
|
364
|
+
const literals = right && ['list', 'tuple'].includes(right.kind()) ? right.children().filter(child => !['[', ']', '(', ')', ',', 'comment'].includes(child.kind())) : null;
|
|
365
|
+
const values = literals?.map(child => child.kind() === 'string' ? child.text().match(/^(?:r|u)?(['"])([^'"\\\r\n]*)\1$/iu)?.[2] : undefined);
|
|
366
|
+
const addition = node.kind() === 'augmented_assignment';
|
|
367
|
+
if (call || parents.some(parent => !['module', 'expression_statement'].includes(parent.kind())) || !values || values.some(value => value === undefined) || (addition && (!names || safeField(node, 'operator')?.text() !== '+='))) {
|
|
368
|
+
names = null; partial = true;
|
|
369
|
+
} else {
|
|
370
|
+
names = new Set([...(addition ? names : []), ...values]); partial = false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { names, partial };
|
|
374
|
+
}
|
|
375
|
+
|
|
208
376
|
function javascriptVisibility(node, name, exportedNames) {
|
|
209
|
-
if (name.startsWith('#') ||
|
|
210
|
-
|
|
377
|
+
if (name.startsWith('#') || node.children().some(child => child.kind() === 'accessibility_modifier' && ['private', 'protected'].includes(child.text()))) return 'internal';
|
|
378
|
+
if (['field_definition', 'public_field_definition'].includes(node.parent()?.kind()) && node.parent().children().some(child => child.kind() === 'accessibility_modifier' && ['private', 'protected'].includes(child.text()))) return 'internal';
|
|
379
|
+
// An exported class exposes its members, not locals inside those members.
|
|
380
|
+
// An export-name match also cannot expose a shadowing nested declaration.
|
|
381
|
+
if (node.ancestors().some(parent => DEFINITIONS.javascript.kinds.includes(parent.kind()))) return 'internal';
|
|
382
|
+
const classNode = ancestor(node, ['class_declaration', 'class_expression', 'class']);
|
|
211
383
|
if (classNode) {
|
|
212
384
|
const className = declaredName(classNode);
|
|
213
385
|
const exported = classNode.parent()?.kind() === 'export_statement' || exportedNames.has(className);
|
|
214
386
|
return exported ? 'public' : 'internal';
|
|
215
387
|
}
|
|
216
388
|
const declaration = ['arrow_function', 'function_expression'].includes(node.kind())
|
|
217
|
-
? ancestor(node, ['lexical_declaration', 'variable_declaration'])
|
|
389
|
+
? ancestor(node, ['lexical_declaration', 'variable_declaration']) || node
|
|
218
390
|
: node;
|
|
219
391
|
return declaration?.parent()?.kind() === 'export_statement' || exportedNames.has(name)
|
|
220
392
|
? 'public'
|
|
@@ -223,27 +395,30 @@ function javascriptVisibility(node, name, exportedNames) {
|
|
|
223
395
|
|
|
224
396
|
function visibility(node, name, language, exportedNames) {
|
|
225
397
|
const head = directText(node);
|
|
398
|
+
if (node.ancestors().some(parent => CALLABLE_KINDS.has(parent.kind()) || ['lambda_expression', 'closure_expression', 'lambda_literal', 'func_literal', 'lambda', 'anonymous_method_expression'].includes(parent.kind()))) return 'internal';
|
|
226
399
|
if (language === 'javascript') return javascriptVisibility(node, name, exportedNames);
|
|
227
400
|
if (language === 'python') {
|
|
228
401
|
const nestedFunction = node.ancestors().some((candidate) => candidate.kind() === 'function_definition');
|
|
229
|
-
|
|
402
|
+
if (nestedFunction) return 'internal';
|
|
403
|
+
const classMember = node.ancestors().some(parent => parent.kind() === 'class_definition');
|
|
404
|
+
return (exportedNames && !classMember ? exportedNames.has(name) : !name.startsWith('_')) ? 'public' : 'internal';
|
|
230
405
|
}
|
|
231
|
-
if (['java', 'csharp'].includes(language)) return /\bpublic\b/u.test(head) ? 'public' : 'internal';
|
|
406
|
+
if (['java', 'csharp'].includes(language)) return /\bpublic\b/u.test(head) || (!/\b(?:private|protected|internal)\b/u.test(head) && ancestor(node, ['interface_declaration'])) ? 'public' : 'internal';
|
|
232
407
|
if (language === 'cpp') return cPlusPlusVisibility(node);
|
|
233
408
|
if (language === 'php') return /\b(?:private|protected)\b/u.test(head) ? 'internal' : 'public';
|
|
234
409
|
if (language === 'go') return /^\p{Lu}/u.test(name) ? 'public' : 'internal';
|
|
235
410
|
if (language === 'rust') return /^\s*pub(?:\([^)]*\))?\s+/u.test(node.text()) ? 'public' : 'internal';
|
|
236
|
-
if (language === 'ruby') return rubyVisibility(node);
|
|
411
|
+
if (language === 'ruby') return node.kind() === 'assignment' && safeField(node, 'left')?.kind() === 'constant' ? 'public' : rubyVisibility(node);
|
|
237
412
|
if (language === 'kotlin') return /\b(?:private|protected|internal)\b/u.test(head) ? 'internal' : 'public';
|
|
238
413
|
if (language === 'shell') return name.startsWith('_') ? 'internal' : 'public';
|
|
239
414
|
return 'internal';
|
|
240
415
|
}
|
|
241
416
|
|
|
242
|
-
function callableKind(node, name) {
|
|
417
|
+
function callableKind(node, name, language) {
|
|
243
418
|
if (node.kind().includes('constructor') || ['__construct', '__init__', 'constructor'].includes(name)) return 'constructor';
|
|
244
419
|
if (node.kind().includes('method')) return 'method';
|
|
245
420
|
const owner = node.ancestors().find((candidate) => (
|
|
246
|
-
CONTAINER_KINDS.has(candidate.kind()) || CALLABLE_KINDS.has(candidate.kind())
|
|
421
|
+
!(language === 'python' && candidate.kind() === 'module') && (CONTAINER_KINDS.has(candidate.kind()) || CALLABLE_KINDS.has(candidate.kind()))
|
|
247
422
|
));
|
|
248
423
|
return owner && CONTAINER_KINDS.has(owner.kind()) ? 'method' : 'function';
|
|
249
424
|
}
|
|
@@ -256,20 +431,21 @@ function scriptUnits(source, filePath, definition) {
|
|
|
256
431
|
const units = [];
|
|
257
432
|
for (const match of source.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/giu)) {
|
|
258
433
|
const content = match[2] || '';
|
|
259
|
-
const contentIndex = (match.index || 0) + match[0].indexOf(
|
|
434
|
+
const contentIndex = (match.index || 0) + match[0].indexOf('>') + 1;
|
|
260
435
|
const typescript = /\blang\s*=\s*["']tsx?["']/iu.test(match[1] || '');
|
|
261
436
|
const jsx = /\blang\s*=\s*["'](?:jsx|tsx)["']/iu.test(match[1] || '');
|
|
262
437
|
units.push({
|
|
263
438
|
source: content,
|
|
264
439
|
lineOffset: source.slice(0, contentIndex).split('\n').length - 1,
|
|
440
|
+
columnOffset: contentIndex - (source.lastIndexOf('\n', contentIndex - 1) + 1),
|
|
265
441
|
parser: jsx ? 'Tsx' : typescript ? 'TypeScript' : 'JavaScript',
|
|
266
442
|
});
|
|
267
443
|
}
|
|
268
444
|
return units;
|
|
269
445
|
}
|
|
270
446
|
|
|
271
|
-
function functionsInTree(root, definition, { language, lineOffset, source }) {
|
|
272
|
-
const exportedNames = language === 'javascript' ? javascriptExportedNames(source) :
|
|
447
|
+
function functionsInTree(root, definition, { language, lineOffset, columnOffset, source, pythonExports = null }) {
|
|
448
|
+
const exportedNames = language === 'javascript' ? javascriptExportedNames(source) : pythonExports?.names;
|
|
273
449
|
const nodes = new Map();
|
|
274
450
|
for (const kind of definition.kinds) {
|
|
275
451
|
for (const node of root.findAll({ rule: { kind } })) nodes.set(node.id(), node);
|
|
@@ -280,16 +456,22 @@ function functionsInTree(root, definition, { language, lineOffset, source }) {
|
|
|
280
456
|
if (!name) return null;
|
|
281
457
|
const container = containerName(node, language);
|
|
282
458
|
const location = node.range().start;
|
|
459
|
+
const range = nodeRange(declarationNode(node), lineOffset, columnOffset);
|
|
283
460
|
const parameters = splitParameters(parameterText(node));
|
|
284
461
|
const resolvedVisibility = visibility(node, name, language, exportedNames);
|
|
462
|
+
const scope = language === 'rust' ? node.children().find(child => child.kind() === 'visibility_modifier')?.text() || 'private' : null;
|
|
285
463
|
return {
|
|
286
464
|
name,
|
|
287
465
|
qualifiedName: [container, name].filter(Boolean).join(language === 'php' ? '::' : '.'),
|
|
288
|
-
kind: callableKind(node, name),
|
|
466
|
+
kind: callableKind(node, name, language),
|
|
289
467
|
visibility: resolvedVisibility,
|
|
468
|
+
...(language === 'javascript' && declarationNode(node).parent()?.kind() === 'export_statement' && declarationNode(node).parent().children().some(child => child.kind() === 'default') ? { exportName: 'default' } : {}),
|
|
469
|
+
...(scope ? { visibilityScope: scope === 'pub' ? 'public' : scope.replace(/^pub\((.*)\)$/u, '$1') } : {}),
|
|
290
470
|
container,
|
|
291
471
|
line: location.line + lineOffset + 1,
|
|
292
|
-
column: location.column + 1,
|
|
472
|
+
column: location.column + 1 + (location.line === 0 ? columnOffset || 0 : 0),
|
|
473
|
+
start: range.start,
|
|
474
|
+
end: range.end,
|
|
293
475
|
parameters,
|
|
294
476
|
async: /^\s*async\b/u.test(node.text()),
|
|
295
477
|
generator: /\bfunction\s*\*|\byield\b/u.test(node.text()),
|
|
@@ -304,7 +486,9 @@ function hasSupportedExtension(filePath, definition) {
|
|
|
304
486
|
}
|
|
305
487
|
|
|
306
488
|
function isCandidate(file, definition) {
|
|
307
|
-
|
|
489
|
+
// Only package discovery can supply a finite, declared public entry. Normal
|
|
490
|
+
// project indexing retains its original generated/dependency exclusions.
|
|
491
|
+
if (!file.publicEntry && file.path.split('/').some((segment) => EXCLUDED_DIRECTORIES.has(segment))) return false;
|
|
308
492
|
return hasSupportedExtension(file.path, definition)
|
|
309
493
|
|| (definition.shebang && file.mode === 0o100755 && path.posix.extname(file.path) === '');
|
|
310
494
|
}
|
|
@@ -351,8 +535,14 @@ function createIndexer(id) {
|
|
|
351
535
|
});
|
|
352
536
|
continue;
|
|
353
537
|
}
|
|
538
|
+
if (sha256(source) !== file.hash) {
|
|
539
|
+
diagnostics.push({ code: 'CODE_INDEX_SOURCE_CHANGED', path: file.path, message: 'The source changed during indexing or is not valid UTF-8. Refresh analysis against a stable source snapshot.' });
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
354
542
|
if (!hasSupportedExtension(file.path, definition) && !definition.shebang?.test(source)) continue;
|
|
355
543
|
const functions = [];
|
|
544
|
+
const symbols = [];
|
|
545
|
+
const dependencies = [];
|
|
356
546
|
for (const unit of scriptUnits(source, file.path, definition)) {
|
|
357
547
|
try {
|
|
358
548
|
if (parserErrors[unit.parser]) {
|
|
@@ -361,11 +551,17 @@ function createIndexer(id) {
|
|
|
361
551
|
}
|
|
362
552
|
const parsed = await parseAsync(unit.parser, unit.source);
|
|
363
553
|
const root = parsed.root();
|
|
554
|
+
const pythonExports = id === 'python' ? pythonExportedNames(root) : null;
|
|
555
|
+
if (pythonExports?.partial) diagnostics.push({ code: 'CODE_INDEX_PYTHON_EXPORTS_PARTIAL', path: file.path, message: 'Python __all__ is dynamic or conditional. Public naming is shown; runtime exports remain unresolved.' });
|
|
364
556
|
functions.push(...functionsInTree(root, definition, {
|
|
365
557
|
language: id,
|
|
366
558
|
lineOffset: unit.lineOffset,
|
|
559
|
+
columnOffset: unit.columnOffset,
|
|
367
560
|
source: unit.source,
|
|
561
|
+
pythonExports,
|
|
368
562
|
}));
|
|
563
|
+
symbols.push(...declarationsInTree(root, id, unit.source, unit.lineOffset, unit.columnOffset, pythonExports));
|
|
564
|
+
dependencies.push(...dependencyReferences(root, id, unit.source, unit.lineOffset, unit.columnOffset));
|
|
369
565
|
diagnostics.push(...syntaxDiagnostics(root, file.path, unit.lineOffset));
|
|
370
566
|
} catch (error) {
|
|
371
567
|
if (error.code === 'PROCESS_ABORTED') throw error;
|
|
@@ -376,6 +572,12 @@ function createIndexer(id) {
|
|
|
376
572
|
});
|
|
377
573
|
}
|
|
378
574
|
}
|
|
575
|
+
// Vue SFCs expose a component as their default public interface even
|
|
576
|
+
// when <script setup> has no explicit JavaScript export declaration.
|
|
577
|
+
if (id === 'javascript' && file.path.endsWith('.vue') && source.length) {
|
|
578
|
+
const lines = source.split('\n');
|
|
579
|
+
symbols.push({ name: 'Component', qualifiedName: 'default component', exportName: 'default', kind: 'component', visibility: 'public', container: null, line: 1, column: 1, start: { line: 1, column: 1 }, end: { line: lines.length, column: lines.at(-1).length + 1 } });
|
|
580
|
+
}
|
|
379
581
|
indexedFiles.push({
|
|
380
582
|
path: file.path,
|
|
381
583
|
language: id,
|
|
@@ -385,6 +587,8 @@ function createIndexer(id) {
|
|
|
385
587
|
lines: source.length === 0 ? 0 : source.split('\n').length - (source.endsWith('\n') ? 1 : 0),
|
|
386
588
|
hash: file.hash,
|
|
387
589
|
functions,
|
|
590
|
+
symbols,
|
|
591
|
+
dependencies,
|
|
388
592
|
});
|
|
389
593
|
}
|
|
390
594
|
return { files: indexedFiles, diagnostics };
|
|
@@ -398,7 +602,7 @@ export const astGrepCodeIndexers = new Map(
|
|
|
398
602
|
|
|
399
603
|
// ast-grep permits one registration per process. A finite indexing child loads
|
|
400
604
|
// this request's languages once and releases every native parser when it exits.
|
|
401
|
-
export async function extractCodeIndexes({ indexers, files, projectRoot }) {
|
|
605
|
+
export async function extractCodeIndexes({ indexers, files, projectRoot, signal }) {
|
|
402
606
|
const selected = indexers.filter(id => astGrepCodeIndexers.has(id));
|
|
403
607
|
const diagnostics = indexers.filter(id => !astGrepCodeIndexers.has(id)).map(id => ({
|
|
404
608
|
code: 'CODE_INDEXER_UNAVAILABLE', message: `No installed code indexer exists for ${id}.`,
|
|
@@ -412,7 +616,7 @@ export async function extractCodeIndexes({ indexers, files, projectRoot }) {
|
|
|
412
616
|
try {
|
|
413
617
|
const result = await runProcess(process.execPath, [fileURLToPath(new URL('./worker.js', import.meta.url))], {
|
|
414
618
|
input: JSON.stringify({ indexers: selected, files, projectRoot, registrations, errors }),
|
|
415
|
-
signal: parserAbortSignal(), timeoutMs: 180_000, code: 'CODE_INDEXER_FAILED',
|
|
619
|
+
signal: signal || parserAbortSignal(), timeoutMs: 180_000, code: 'CODE_INDEXER_FAILED',
|
|
416
620
|
});
|
|
417
621
|
contributions = JSON.parse(result.stdout.toString('utf8'));
|
|
418
622
|
} catch (error) {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { minimatch } from 'minimatch';
|
|
4
|
+
import { extractCodeIndexes } from './ast-grep.js';
|
|
5
|
+
import { sha256 } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
const canonical = value => typeof value === 'string' && value.length > 0 && value !== '.' && !path.posix.isAbsolute(value) && path.posix.normalize(value) === value && !/[\\\0]/u.test(value) && !value.split('/').includes('..');
|
|
8
|
+
const packageName = value => typeof value === 'string' && /^(?:@[a-zA-Z0-9._-]+\/)?[a-zA-Z0-9._-]+$/u.test(value) && !value.split('/').some(part => part === '.' || part === '..');
|
|
9
|
+
|
|
10
|
+
// npm owns manifest and export semantics. Read direct declarations and their
|
|
11
|
+
// public entry files only; never walk dependency trees or enumerate internals.
|
|
12
|
+
export async function inspectNpmPackages({ projectRoot, discovery, files, indexedFiles = [], signal }) {
|
|
13
|
+
const known = new Map(files.map(file => [file.path, file]));
|
|
14
|
+
const packages = []; const inputs = []; const diagnostics = [];
|
|
15
|
+
const manifests = new Map(); const workspaceScopes = []; const byRoot = new Map();
|
|
16
|
+
async function read(relative, { indexed = false } = {}) {
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
if (!canonical(relative)) return null;
|
|
19
|
+
try {
|
|
20
|
+
for (let index = 0, parts = relative.split('/'); index < parts.length; index++) if ((await lstat(path.join(projectRoot, ...parts.slice(0, index + 1)))).isSymbolicLink()) return null;
|
|
21
|
+
const bytes = await readFile(path.join(projectRoot, relative), { signal });
|
|
22
|
+
if (bytes.length > 4 * 1024 * 1024) return null;
|
|
23
|
+
if (!Buffer.from(bytes.toString('utf8'), 'utf8').equals(bytes)) return null;
|
|
24
|
+
const hash = sha256(bytes);
|
|
25
|
+
if (indexed && known.get(relative)?.hash !== hash) throw new Error(`Package declaration changed: ${relative}`);
|
|
26
|
+
if (!indexed) inputs.push({ path: relative, hash });
|
|
27
|
+
return { path: relative, hash, source: bytes.toString('utf8') };
|
|
28
|
+
} catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null; throw error; }
|
|
29
|
+
}
|
|
30
|
+
// Workspace ownership comes from each declared workspace root, including
|
|
31
|
+
// manifests in packages which do not repeat their parent's workspaces field.
|
|
32
|
+
for (const manifest of discovery.manifests) {
|
|
33
|
+
if (!known.has(manifest)) continue;
|
|
34
|
+
const input = await read(manifest, { indexed: true });
|
|
35
|
+
if (!input) { diagnostics.push({ code: 'NPM_MANIFEST_UNAVAILABLE', path: manifest, message: 'The declared dependency manifest cannot be read safely.' }); continue; }
|
|
36
|
+
let document;
|
|
37
|
+
try { document = JSON.parse(input.source); if (!document || typeof document !== 'object' || Array.isArray(document)) throw new TypeError('Manifest must be an object'); } catch { diagnostics.push({ code: 'NPM_MANIFEST_INVALID', path: manifest, message: 'The direct dependency manifest is invalid JSON.' }); continue; }
|
|
38
|
+
manifests.set(manifest, { input, document });
|
|
39
|
+
const declaration = Array.isArray(document.workspaces?.packages) ? document.workspaces.packages : document.workspaces;
|
|
40
|
+
const patterns = []; const exclusions = [];
|
|
41
|
+
for (const declared of Array.isArray(declaration) ? declaration : []) {
|
|
42
|
+
if (typeof declared !== 'string') continue;
|
|
43
|
+
const prefix = declared.match(/^!+/u)?.[0] || '';
|
|
44
|
+
const pattern = declared.slice(prefix.length).replace(/^\.\//u, '').replace(/\/$/u, '');
|
|
45
|
+
if (!canonical(pattern)) continue;
|
|
46
|
+
if (prefix.length % 2) exclusions.push(pattern);
|
|
47
|
+
else {
|
|
48
|
+
// npm lets a later positive pattern override preceding exclusions.
|
|
49
|
+
for (let index = exclusions.length - 1; index >= 0; index--) if (minimatch(pattern, exclusions[index])) exclusions.splice(index, 1);
|
|
50
|
+
patterns.push(pattern);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!patterns.length) continue;
|
|
54
|
+
const workspaces = new Map();
|
|
55
|
+
for (const candidate of known.keys()) {
|
|
56
|
+
if (!candidate.endsWith('/package.json')) continue;
|
|
57
|
+
const directory = path.posix.dirname(candidate);
|
|
58
|
+
const relative = path.posix.relative(path.posix.dirname(manifest), directory);
|
|
59
|
+
if (!patterns.some(pattern => minimatch(relative, pattern)) || exclusions.some(pattern => minimatch(relative, pattern, { dot: true }))) continue;
|
|
60
|
+
const workspace = await read(candidate, { indexed: true });
|
|
61
|
+
let definition; try { definition = workspace && JSON.parse(workspace.source); } catch { continue; }
|
|
62
|
+
if (!definition || !packageName(definition.name)) continue;
|
|
63
|
+
if (workspaces.has(definition.name)) { workspaces.set(definition.name, null); diagnostics.push({ code: 'NPM_WORKSPACE_AMBIGUOUS', path: candidate, message: `${definition.name}: multiple declared workspace roots; ownership is unresolved.` }); }
|
|
64
|
+
else workspaces.set(definition.name, directory);
|
|
65
|
+
}
|
|
66
|
+
workspaceScopes.push({ directory: path.posix.dirname(manifest), workspaces });
|
|
67
|
+
}
|
|
68
|
+
const entries = [];
|
|
69
|
+
for (const [manifest, { input, document }] of manifests) {
|
|
70
|
+
const parent = path.posix.dirname(manifest);
|
|
71
|
+
const scope = workspaceScopes.filter(entry => entry.directory === '.' || parent === entry.directory || parent.startsWith(`${entry.directory}/`)).sort((a, b) => b.directory.length - a.directory.length)[0];
|
|
72
|
+
const declarations = new Map();
|
|
73
|
+
for (const kind of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) for (const [name, requested] of Object.entries(document[kind] || {})) {
|
|
74
|
+
if (!packageName(name) || typeof requested !== 'string') continue;
|
|
75
|
+
if (!declarations.has(name)) declarations.set(name, { requested, kinds: [] });
|
|
76
|
+
declarations.get(name).kinds.push(kind);
|
|
77
|
+
}
|
|
78
|
+
for (const [name, declaration] of declarations) {
|
|
79
|
+
let root = path.posix.join(parent, discovery.packageRoot, name);
|
|
80
|
+
const ownedRoot = declaration.requested.startsWith('file:') ? path.posix.normalize(path.posix.join(parent, declaration.requested.slice(5))) : scope?.workspaces.get(name);
|
|
81
|
+
const ownedManifest = ownedRoot ? path.posix.join(ownedRoot, 'package.json') : null;
|
|
82
|
+
const owned = ownedManifest && canonical(ownedManifest) && known.has(ownedManifest);
|
|
83
|
+
let metadata = owned ? await read(ownedManifest, { indexed: true }) : null;
|
|
84
|
+
if (owned) root = ownedRoot;
|
|
85
|
+
else {
|
|
86
|
+
// npm hoists installations. Resolve the nearest safe installation inside
|
|
87
|
+
// this project; custom Stack package roots retain their exact meaning.
|
|
88
|
+
let directory = parent;
|
|
89
|
+
while (true) {
|
|
90
|
+
const candidate = path.posix.join(directory, discovery.packageRoot, name);
|
|
91
|
+
metadata = await read(`${candidate}/package.json`);
|
|
92
|
+
if (metadata) { root = candidate; break; }
|
|
93
|
+
if (directory === '.' || discovery.packageRoot !== 'node_modules') break;
|
|
94
|
+
directory = path.posix.dirname(directory);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const identity = `${root}:${name}`;
|
|
98
|
+
const existing = byRoot.get(identity);
|
|
99
|
+
const declared = { manifest, requested: declaration.requested, dependencyKinds: declaration.kinds };
|
|
100
|
+
if (existing) {
|
|
101
|
+
existing.declarations.push(declared); existing.evidence.push({ path: input.path, hash: input.hash });
|
|
102
|
+
existing.dependencyKinds = [...new Set([...existing.dependencyKinds, ...declaration.kinds])];
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let definition;
|
|
106
|
+
try { definition = metadata ? JSON.parse(metadata.source) : null; } catch { definition = null; }
|
|
107
|
+
const record = { id: `package:npm:${identity}`, manager: 'npm', name, root, opaque: !owned, requested: declaration.requested, version: typeof definition?.version === 'string' ? definition.version : null, dependencyKinds: declaration.kinds, manifest, declarations: [declared], evidence: [{ path: input.path, hash: input.hash }], entryPoints: [], symbolCoverage: 'unsupported', metadata: metadata ? { path: metadata.path, hash: metadata.hash } : null };
|
|
108
|
+
packages.push(record); byRoot.set(identity, record);
|
|
109
|
+
if (!definition) { diagnostics.push({ code: 'NPM_INTERFACE_UNAVAILABLE', path: manifest, message: `${name}: installed public-interface metadata is unavailable.` }); continue; }
|
|
110
|
+
function targets(value) {
|
|
111
|
+
const result = []; const pending = [{ value, conditions: [] }];
|
|
112
|
+
while (pending.length && result.length < 512) {
|
|
113
|
+
signal?.throwIfAborted();
|
|
114
|
+
const entry = pending.pop();
|
|
115
|
+
if (typeof entry.value === 'string') result.push({ target: entry.value, conditions: entry.conditions });
|
|
116
|
+
else if (entry.conditions.length < 32 && entry.value && typeof entry.value === 'object') {
|
|
117
|
+
const children = Object.entries(entry.value);
|
|
118
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
119
|
+
const [condition, child] = children[index];
|
|
120
|
+
pending.push({ value: child, conditions: [...entry.conditions, Array.isArray(entry.value) ? `fallback:${condition}` : condition] });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (pending.length) diagnostics.push({ code: 'NPM_EXPORTS_PARTIAL', path: metadata.path, message: `${name}: the public export declaration exceeds inspection limits.` });
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
const exported = definition.exports;
|
|
128
|
+
const definitions = exported == null ? [ ['.', { default: definition.main || './index.js', ...(definition.types || definition.typings ? { types: definition.types || definition.typings } : {}) }] ]
|
|
129
|
+
: typeof exported === 'object' && !Array.isArray(exported) && Object.keys(exported).some(key => key.startsWith('.')) ? Object.entries(exported) : [['.', exported]];
|
|
130
|
+
for (const [importPath, value] of definitions) for (const entry of targets(value)) {
|
|
131
|
+
if (importPath !== '.' && !importPath.startsWith('./')) continue;
|
|
132
|
+
const relative = entry.target.replace(/^\.\//u, '');
|
|
133
|
+
const publicPath = path.posix.join(record.root, relative);
|
|
134
|
+
const point = { importPath, target: entry.target, conditions: entry.conditions, file: canonical(relative) && canonical(publicPath) ? publicPath : null, symbols: [], coverage: 'unsupported' };
|
|
135
|
+
record.entryPoints.push(point);
|
|
136
|
+
if (entry.target.includes('*') || !point.file || !/\.(?:[cm]?js|jsx|tsx?|vue)$/u.test(point.file)) continue;
|
|
137
|
+
const file = await read(point.file, { indexed: Boolean(known.has(point.file)) });
|
|
138
|
+
if (file) entries.push({ record, point, file });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const projectIndexes = new Map(indexedFiles.map(file => [file.path, file]));
|
|
143
|
+
const candidates = [...new Map(entries.filter(entry => projectIndexes.get(entry.file.path)?.hash !== entry.file.hash).map(entry => [entry.file.path, { path: entry.file.path, hash: entry.file.hash, mode: 0o100644, publicEntry: true }])).values()];
|
|
144
|
+
const result = candidates.length ? await extractCodeIndexes({ indexers: ['javascript'], files: candidates, projectRoot, signal }) : { contributions: [], diagnostics: [] };
|
|
145
|
+
diagnostics.push(...result.diagnostics);
|
|
146
|
+
const indexed = new Map([...projectIndexes, ...result.contributions.flatMap(contribution => contribution.files.map(file => [file.path, file]))]);
|
|
147
|
+
function publicSurface(filePath, visited = new Set()) {
|
|
148
|
+
const contribution = indexed.get(filePath);
|
|
149
|
+
if (!contribution || visited.has(filePath) || visited.size >= 32) return [];
|
|
150
|
+
const branch = new Set([...visited, filePath]);
|
|
151
|
+
const surface = [...(contribution.functions || []), ...(contribution.symbols || [])].filter(symbol => symbol.visibility === 'public').map(symbol => ({ ...symbol, sourceFile: filePath, sourceHash: contribution.hash, sourceName: symbol.name, sourceQualifiedName: symbol.qualifiedName }));
|
|
152
|
+
// Follow only already indexed project source. External packages remain
|
|
153
|
+
// opaque: neither their internals nor their transitive dependencies are read.
|
|
154
|
+
if (known.has(filePath)) for (const reference of contribution.dependencies || []) {
|
|
155
|
+
if (reference.kind !== 're-export' || !reference.specifier?.startsWith('.') || !reference.exports?.length) continue;
|
|
156
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(filePath), reference.specifier));
|
|
157
|
+
const target = [base, ...['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue'].map(extension => base + extension), ...['.js', '.ts', '.tsx', '.jsx'].map(extension => base + '/index' + extension)].find(candidate => known.has(candidate) && indexed.has(candidate));
|
|
158
|
+
if (!target) continue;
|
|
159
|
+
const available = publicSurface(target, branch);
|
|
160
|
+
for (const exposure of reference.exports) for (const symbol of available) {
|
|
161
|
+
const exported = symbol.exportName || symbol.name;
|
|
162
|
+
if (exposure.name === '*' ? exported === 'default' : exposure.name !== exported) continue;
|
|
163
|
+
const name = exposure.name === '*' ? symbol.name : exposure.alias;
|
|
164
|
+
surface.push({ ...symbol, name, qualifiedName: name, exportName: name });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const names = new Map();
|
|
168
|
+
for (const symbol of surface) {
|
|
169
|
+
const name = symbol.exportName || symbol.qualifiedName || symbol.name;
|
|
170
|
+
const previous = names.get(name);
|
|
171
|
+
if (!names.has(name)) names.set(name, [symbol]);
|
|
172
|
+
else if (!previous || previous[0].sourceFile !== symbol.sourceFile) names.set(name, null);
|
|
173
|
+
else if (!previous.some(entry => entry.start?.line === symbol.start?.line && entry.start?.column === symbol.start?.column)) previous.push(symbol);
|
|
174
|
+
}
|
|
175
|
+
return [...names.values()].filter(Boolean).flat();
|
|
176
|
+
}
|
|
177
|
+
for (const { record, point, file } of entries) {
|
|
178
|
+
const contribution = indexed.get(file.path);
|
|
179
|
+
if (!contribution) continue;
|
|
180
|
+
point.hash = file.hash; point.coverage = 'partial'; record.symbolCoverage = 'partial';
|
|
181
|
+
point.symbols = publicSurface(file.path);
|
|
182
|
+
}
|
|
183
|
+
return { packages, inputs: [...new Map(inputs.map(input => [input.path, input])).values()], diagnostics };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Metadata and entry files live outside Git's inventory. Recheck those exact
|
|
187
|
+
// inputs before publishing, including ancestor links; a race retains the index.
|
|
188
|
+
export async function recheckNpmInputs(projectRoot, inputs, signal) {
|
|
189
|
+
for (const input of inputs) {
|
|
190
|
+
signal?.throwIfAborted();
|
|
191
|
+
try {
|
|
192
|
+
const parts = input.path.split('/');
|
|
193
|
+
for (let index = 0; index < parts.length; index++) if ((await lstat(path.join(projectRoot, ...parts.slice(0, index + 1)))).isSymbolicLink()) throw new Error('Symbolic link');
|
|
194
|
+
if (sha256(await readFile(path.join(projectRoot, input.path), { signal })) !== input.hash) throw new Error('Content changed');
|
|
195
|
+
} catch (error) {
|
|
196
|
+
signal?.throwIfAborted();
|
|
197
|
+
throw new Error(`Public package interface changed during architecture analysis: ${input.path}`, { cause: error });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/index/context.js
CHANGED
|
@@ -113,7 +113,7 @@ export async function contextForProjectPaths({ paths, projectRoot, stackPackages
|
|
|
113
113
|
'',
|
|
114
114
|
'## Relevant subsystems',
|
|
115
115
|
'',
|
|
116
|
-
...(['missing', 'empty'].includes(program.subsystemMap?.status) ? ['The subsystem map needs authoring. Before implementation, create genesis/subsystems.md from source, schema, and existing Program; preserve existing explanations and
|
|
116
|
+
...(['missing', 'empty'].includes(program.subsystemMap?.status) ? ['The subsystem map needs authoring. Before implementation, create genesis/subsystems.md from source, schema, and existing Program; preserve existing explanations and require explicit directory roots and Program membership.'] : []),
|
|
117
117
|
...((program.subsystemMap?.subsystems || []).filter((entry) => targets.includes('') || modules.some((module) => module.subsystem === entry.id)).map((entry) => JSON.stringify(entry, null, 2))),
|
|
118
118
|
'',
|
|
119
119
|
'## Relevant Program',
|