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.
@@ -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
- return node.children().slice(0, 4).map((child) => child.text()).join(' ');
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 /^\s*static\b/u.test(node.text()) ? 'internal' : 'public';
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('#') || /\b(?:private|protected)\s+/u.test(directText(node))) return 'internal';
210
- const classNode = ancestor(node, ['class_declaration', 'class_expression']);
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
- return !nestedFunction && !name.startsWith('_') ? 'public' : 'internal';
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(content);
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) : new Set();
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
- if (file.path.split('/').some((segment) => EXCLUDED_DIRECTORIES.has(segment))) return false;
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
+ }
@@ -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 do not infer ownership from folders.'] : []),
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',