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.
@@ -0,0 +1,227 @@
1
+ import path from 'node:path';
2
+ import { connectArchitectureObservations } from './architecture-observations.js';
3
+ import { buildCityPresentation } from './city-presentation.js';
4
+ import { GenesisError } from './errors.js';
5
+ import { sha256, stableJson } from './utils.js';
6
+
7
+ const owns = (root, file) => root === '.' || file.startsWith(`${root}/`);
8
+
9
+ function resolveReference(file, reference, filesByPath, packages) {
10
+ const specifier = reference.specifier;
11
+ if (!specifier) return { ...reference, resolution: 'unresolved' };
12
+ let target;
13
+ if (file.language === 'python') {
14
+ const dots = specifier.match(/^\.+/u)?.[0].length || 0;
15
+ const parts = specifier.slice(dots).split('.').join('/');
16
+ const base = dots ? path.posix.join(path.posix.dirname(file.path), ...Array(dots - 1).fill('..'), parts) : parts;
17
+ target = [base + '.py', base + '/__init__.py'].find(candidate => filesByPath.has(candidate));
18
+ } else if (specifier.startsWith('.') || file.language === 'cpp' || file.language === 'shell') {
19
+ const base = path.posix.normalize(path.posix.join(path.posix.dirname(file.path), specifier));
20
+ if (base !== '..' && !base.startsWith('../')) {
21
+ target = [base, ...['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.py', '.rb', '.sh'].map(extension => base + extension), ...['.js', '.ts', '.tsx', '.jsx'].map(extension => base + '/index' + extension)].find(candidate => filesByPath.has(candidate));
22
+ }
23
+ }
24
+ if (!target && file.language === 'javascript' && !specifier.startsWith('.') && !specifier.startsWith('/')) {
25
+ const candidates = packages.flatMap(entry => (entry.declarations || [{ manifest: entry.manifest }]).filter(declaration => owns(path.posix.dirname(declaration.manifest), file.path) && (specifier === entry.name || specifier.startsWith(`${entry.name}/`))).map(declaration => ({ entry, manifest: declaration.manifest }))).sort((a, b) => b.manifest.length - a.manifest.length);
26
+ const dependency = candidates[0]?.entry;
27
+ if (dependency) {
28
+ const importPath = specifier === dependency.name ? '.' : `.${specifier.slice(dependency.name.length)}`;
29
+ const publicFiles = [...new Set(dependency.entryPoints.filter(entry => entry.importPath === importPath).map(entry => entry.file).filter(value => filesByPath.has(value)))];
30
+ return { ...reference, targetPackageId: dependency.id, targetFileId: !dependency.opaque && publicFiles.length === 1 ? filesByPath.get(publicFiles[0]).id : null, importPath, resolution: 'resolved', reason: dependency.opaque ? 'Opaque package boundary; internal dependencies are not indexed.' : 'Project-owned package boundary; conditions can require runtime resolution.' };
31
+ }
32
+ }
33
+ return { ...reference, targetFileId: target ? filesByPath.get(target).id : null, resolution: target ? 'resolved' : 'unresolved', reason: target ? '' : 'Target requires external package, alias, build or language resolution context.' };
34
+ }
35
+
36
+ // Membership is one root per subsystem, regardless of language or UI role.
37
+ // A ready authored map is the sole owner list. Stack campuses only aid an
38
+ // unexplained project's initial inventory; they never split an authored owner.
39
+ export function buildArchitectureFacts({ machine, subsystemMap, program = null, uiDiscovery, packageDiscovery, packages: discoveredPackages = [], stackHash, diagnostics = [], observations = null, previous = null, inventory = [], cityRegions = [] }) {
40
+ const indexedPaths = new Set(machine.buildings.map(file => file.path));
41
+ const sourceFiles = [...machine.buildings, ...inventory.filter(file => !indexedPaths.has(file.path)).map(file => ({ ...file, id: `file:${file.path}`, title: path.posix.basename(file.path), language: 'unknown', analysis: 'unsupported', functionIds: [], dependencies: [] }))].sort((a, b) => a.path.localeCompare(b.path));
42
+ const presentation = inventory.length ? buildCityPresentation({ files: sourceFiles, districts: [], regions: cityRegions }) : { regions: machine.presentationRegions, campuses: machine.presentationCampuses };
43
+ const authored = new Map((subsystemMap?.subsystems || []).filter(entry => entry.sourceRoot).map(entry => [entry.sourceRoot, entry]));
44
+ const regions = new Map(presentation.regions.map(entry => [entry.id, entry]));
45
+ const roots = new Map((subsystemMap?.status === 'valid' ? [] : presentation.campuses).map(campus => {
46
+ const root = campus.path || '.';
47
+ return [root, { root, provenance: regions.get(campus.regionId)?.fallback ? 'fallback' : 'stack' }];
48
+ }));
49
+ for (const [root, entry] of authored) roots.set(root, { root, provenance: 'authored', authored: entry });
50
+ const packages = [...new Map(discoveredPackages.map(entry => [entry.id, { ...entry }])).values()];
51
+ for (const entry of packages.filter(entry => !entry.opaque)) if (subsystemMap?.status !== 'valid' && !roots.has(entry.root)) roots.set(entry.root, { root: entry.root, provenance: 'package' });
52
+ if (!roots.size) roots.set('.', { root: '.', provenance: 'fallback' });
53
+ const subsystems = [...roots.values()].map(entry => {
54
+ const authoredEntry = authored.get(entry.root);
55
+ return { id: `directory:${entry.root}`, root: entry.root, title: authoredEntry?.title || (entry.root === '.' ? 'Project' : path.posix.basename(entry.root)), description: authoredEntry?.description || '', authoredId: authoredEntry?.id || null, program: authoredEntry?.program || [], dataOwned: authoredEntry?.dataOwned || [], dataUsed: authoredEntry?.dataUsed || [], provenance: entry.provenance, fileIds: [], parentId: null };
56
+ }).sort((a, b) => b.root.length - a.root.length || a.root.localeCompare(b.root));
57
+ const authoritative = subsystems.filter(entry => entry.provenance !== 'fallback');
58
+ const fallback = subsystems.filter(entry => entry.provenance === 'fallback');
59
+ const files = sourceFiles.map(file => {
60
+ let owner = authoritative.find(entry => owns(entry.root, file.path)) || fallback.find(entry => owns(entry.root, file.path));
61
+ if (!owner) {
62
+ if (subsystemMap?.status === 'valid') throw new GenesisError('SUBSYSTEM_SOURCE_UNOWNED', `No declared subsystem owns ${file.path}. Add its directory to genesis/subsystems.md or declare a project-root owner.`);
63
+ owner = subsystems.find(entry => entry.root === '.');
64
+ if (!owner) { owner = { id: 'directory:.', root: '.', title: 'Project', description: '', authoredId: null, provenance: 'fallback', fileIds: [], parentId: null }; subsystems.push(owner); }
65
+ }
66
+ owner.fileIds.push(file.id);
67
+ return { ...file, subsystemId: owner.id };
68
+ });
69
+ const visibleSubsystems = subsystems.filter(entry => entry.fileIds.length || entry.provenance === 'authored');
70
+ for (const entry of packages) {
71
+ if (entry.opaque) {
72
+ entry.subsystemId = entry.id;
73
+ visibleSubsystems.push({ id: entry.id, root: entry.root, title: entry.name, kind: 'package', packageId: entry.id, opaque: true, provenance: 'package', fileIds: [], parentId: null, description: 'External public boundary. Internal files and dependencies are not indexed.' });
74
+ } else entry.subsystemId = visibleSubsystems.find(owner => owner.root === entry.root || owns(owner.root, entry.root))?.id;
75
+ }
76
+ for (const entry of visibleSubsystems) entry.parentId = visibleSubsystems.find(candidate => candidate.provenance !== 'fallback' && candidate.id !== entry.id && owns(candidate.root, entry.root))?.id || null;
77
+ const filesByPath = new Map(files.map(file => [file.path, file]));
78
+ const relations = files.flatMap(file => (file.dependencies || []).map((reference, index) => ({
79
+ id: `dependency:${file.id}:${index}`, fromFileId: file.id, ...resolveReference(file, reference, filesByPath, packages), evidence: { path: file.path, hash: file.hash, start: reference.start, end: reference.end },
80
+ })));
81
+ const symbols = [...machine.functions, ...(machine.symbols || [])].map(symbol => ({ ...symbol, start: symbol.start || { line: symbol.line, column: symbol.column }, rangeAvailable: Boolean(symbol.end) }));
82
+ const sourceHash = sha256(stableJson(files.map(file => ({ path: file.path, hash: file.hash, mode: file.mode }))));
83
+ let ui = observations ? connectArchitectureObservations(observations, files, packages) : null;
84
+ if (!observations && previous?.ui) {
85
+ // Source-only indexing must not erase unchanged consumer observations.
86
+ // A changed snapshot requires its inspector; never carry guesses forward.
87
+ if (previous.sourceHash === sourceHash && previous.stackHash === stackHash) {
88
+ try { ui = connectArchitectureObservations(previous.ui, files, packages); }
89
+ catch (error) {
90
+ if (error.code !== 'ARCHITECTURE_OBSERVATIONS_INVALID') throw error;
91
+ }
92
+ }
93
+ if (!ui) diagnostics.push({ code: 'ARCHITECTURE_UI_STALE', message: 'UI observations need refresh against the current source and Stack.' });
94
+ }
95
+ const symbolsByFile = new Map();
96
+ for (const symbol of symbols) if (symbol.visibility === 'public') {
97
+ if (!symbolsByFile.has(symbol.fileId)) symbolsByFile.set(symbol.fileId, []);
98
+ symbolsByFile.get(symbol.fileId).push(symbol);
99
+ }
100
+ for (const use of ui?.imports || []) {
101
+ if (use.resolution !== 'resolved') continue;
102
+ const from = filesByPath.get(use.fromFile);
103
+ const target = filesByPath.get(use.targetFile);
104
+ if (!from || !target) continue;
105
+ const importedSymbolIds = (symbolsByFile.get(target.id) || []).filter(symbol => use.names.includes(symbol.exportName || symbol.name)).map(symbol => symbol.id);
106
+ const evidence = use.evidence.find(entry => entry.path === from.path);
107
+ const matching = relations.find(relation => relation.fromFileId === from.id && relation.specifier === use.specifier
108
+ && relation.start.line === evidence.start.line && relation.start.column === evidence.start.column);
109
+ const resolved = { targetFileId: target.id, resolution: 'resolved', reason: 'Resolved by the configured source inspector.', importedNames: use.names, importedSymbolIds, typeOnly: use.typeOnly };
110
+ if (matching) Object.assign(matching, resolved);
111
+ else relations.push({ id: `dependency:${use.id}`, fromFileId: from.id, kind: 'import', specifier: use.specifier,
112
+ start: evidence.start, end: evidence.end, evidence, ...resolved });
113
+ }
114
+ const packageIdentity = sha256(stableJson(packages));
115
+ const programFacts = program ? { status: program.status, hash: program.programHash, diagnostics: program.diagnostics,
116
+ modules: program.buildings.map(module => ({ ...module, subsystemId: visibleSubsystems.find(owner => owner.authoredId === module.subsystem)?.id || null })) } : null;
117
+ const snapshotId = sha256(stableJson({ sourceHash, stackHash, subsystemIdentity: subsystemMap?.identity || null, programHash: program?.programHash, packages, observations: ui }));
118
+ return {
119
+ schema: 'genesis.architecture.v1', snapshotId, codeHash: machine.codeHash, sourceHash, stackHash,
120
+ subsystems: visibleSubsystems.sort((a, b) => a.root.localeCompare(b.root)), files, symbols, relations,
121
+ ui, packages, packageIdentity, program: programFacts,
122
+ uiDiscovery: uiDiscovery || { status: 'unconfigured', discoveries: [] },
123
+ packageDiscovery: packageDiscovery || { status: 'unconfigured', discoveries: [] },
124
+ capabilities: { symbols: 'partial', dependencies: 'partial', pages: ui?.capabilities.pages || (uiDiscovery?.status === 'configured' && !uiDiscovery.discoveries.length ? 'complete' : 'unconfigured'), runtimeCalls: 'unsupported' },
125
+ diagnostics,
126
+ };
127
+ }
128
+
129
+ export function validateArchitectureFacts(snapshot) {
130
+ const check = (condition, message) => { if (!condition) throw new GenesisError('ARCHITECTURE_INVALID', message); };
131
+ check(snapshot?.schema === 'genesis.architecture.v1' && typeof snapshot.snapshotId === 'string' && snapshot.snapshotId.length > 0, 'Invalid architecture schema or snapshot identity.');
132
+ const records = name => {
133
+ check(Array.isArray(snapshot[name]), `${name} must be an array.`);
134
+ const map = new Map();
135
+ for (const record of snapshot[name]) {
136
+ check(record && typeof record.id === 'string' && record.id.length > 0 && !map.has(record.id), `Invalid or duplicate ${name} id.`);
137
+ map.set(record.id, record);
138
+ }
139
+ return map;
140
+ };
141
+ const subsystems = records('subsystems'); const files = records('files'); const symbols = records('symbols'); records('relations');
142
+ const packages = snapshot.packages === undefined ? new Map() : records('packages');
143
+ if (snapshot.program) {
144
+ check(typeof snapshot.program.hash === 'string' && Array.isArray(snapshot.program.modules), 'Invalid Program facts.');
145
+ const moduleIds = new Set();
146
+ for (const module of snapshot.program.modules) {
147
+ const owner = subsystems.get(module.subsystemId);
148
+ check(typeof module.id === 'string' && !moduleIds.has(module.id), 'Invalid or duplicate Program operation.');
149
+ moduleIds.add(module.id);
150
+ check(owner && owner.authoredId === module.subsystem && owner.program?.includes(module.path), 'Program operation requires its one declared subsystem owner.');
151
+ for (const field of ['path', 'title', 'description', 'publicContract', 'implementationMap']) check(typeof module[field] === 'string', 'Invalid Program explanation.');
152
+ check(Array.isArray(module.sources) && module.sources.every(source => typeof source === 'string' && source !== '.' && path.posix.normalize(source) === source && !path.posix.isAbsolute(source) && !/[\\\0]/u.test(source) && !source.split('/').includes('..')), 'Invalid Program source references.');
153
+ // Previously saved v1 indexes remain readable until an explicit re-scan.
154
+ // New authored modules must supply their anchor in inspectProgram.
155
+ if (module.mainSource !== undefined) {
156
+ check(typeof module.mainSource === 'string' && module.sources.includes(module.mainSource), 'Program main source must be a cited source.');
157
+ const mainOwner = [...subsystems.values()].filter(entry => owns(entry.root, module.mainSource)).sort((a, b) => (b.root === '.' ? 0 : b.root.length) - (a.root === '.' ? 0 : a.root.length))[0];
158
+ check(mainOwner?.id === owner.id, 'Program main source must belong to its declared subsystem.');
159
+ }
160
+ }
161
+ }
162
+ const roots = new Set();
163
+ for (const subsystem of subsystems.values()) {
164
+ check(typeof subsystem.root === 'string' && subsystem.root.length > 0 && path.posix.normalize(subsystem.root) === subsystem.root && !path.posix.isAbsolute(subsystem.root) && !/[\\\0]/u.test(subsystem.root) && !subsystem.root.split('/').includes('..') && !roots.has(subsystem.root), 'Subsystems require unique canonical directory roots.');
165
+ roots.add(subsystem.root);
166
+ check(Array.isArray(subsystem.fileIds) && new Set(subsystem.fileIds).size === subsystem.fileIds.length, 'Invalid subsystem file membership.');
167
+ for (const id of subsystem.fileIds) check(files.get(id)?.subsystemId === subsystem.id, 'Conflicting subsystem ownership.');
168
+ if (subsystem.parentId != null) {
169
+ const parent = subsystems.get(subsystem.parentId);
170
+ check(parent && parent.id !== subsystem.id && owns(parent.root, subsystem.root), 'Subsystem parent must be an enclosing directory owner.');
171
+ }
172
+ }
173
+ const paths = new Set();
174
+ for (const file of files.values()) {
175
+ check(typeof file.path === 'string' && file.path.length > 0 && file.path !== '.' && path.posix.normalize(file.path) === file.path && !path.posix.isAbsolute(file.path) && !/[\\\0]/u.test(file.path) && !file.path.split('/').includes('..') && !paths.has(file.path), 'Invalid or duplicate file path.');
176
+ paths.add(file.path);
177
+ const owner = subsystems.get(file.subsystemId);
178
+ check(owner && owns(owner.root, file.path) && owner.fileIds.includes(file.id), 'File has no matching directory owner.');
179
+ check(typeof file.hash === 'string' && file.hash.length > 0, 'File requires a content hash.');
180
+ }
181
+ function range(start, end, file, label) {
182
+ const point = value => value && Number.isSafeInteger(value.line) && value.line > 0 && Number.isSafeInteger(value.column) && value.column > 0;
183
+ check(point(start) && point(end) && (end.line > start.line || (end.line === start.line && end.column > start.column)), `${label} has an impossible source range.`);
184
+ check(!Number.isInteger(file.lines) || (start.line <= file.lines && (end.line <= file.lines || (end.line === file.lines + 1 && end.column === 1))), `${label} range exceeds its source.`);
185
+ }
186
+ for (const symbol of snapshot.symbols) {
187
+ check(files.has(symbol.fileId) && files.get(symbol.fileId).path === symbol.path, 'Symbol references an unknown file.');
188
+ check(typeof symbol.name === 'string' && typeof symbol.visibility === 'string', 'Invalid public symbol.');
189
+ check(symbol.visibilityScope === undefined || (typeof symbol.visibilityScope === 'string' && symbol.visibilityScope.length > 0), 'Invalid symbol visibility scope.');
190
+ check(typeof symbol.rangeAvailable === 'boolean' && symbol.rangeAvailable === Boolean(symbol.end), 'Symbol range availability disagrees with its declaration.');
191
+ if (symbol.rangeAvailable) range(symbol.start, symbol.end, files.get(symbol.fileId), 'Symbol');
192
+ }
193
+ for (const dependency of packages.values()) {
194
+ const owner = subsystems.get(dependency.subsystemId);
195
+ check(owner && (owner.root === dependency.root || (!dependency.opaque && owns(owner.root, dependency.root))) && typeof dependency.opaque === 'boolean' && Boolean(owner.opaque) === dependency.opaque, 'Package requires its directory boundary owner.');
196
+ check(typeof dependency.name === 'string' && dependency.name.length > 0 && typeof dependency.manager === 'string' && files.has(`file:${dependency.manifest}`), 'Package requires a project declaration.');
197
+ check(Array.isArray(dependency.evidence) && dependency.evidence.some(entry => files.get(`file:${entry.path}`)?.hash === entry.hash && entry.path === dependency.manifest), 'Package declaration evidence is stale.');
198
+ if (dependency.declarations !== undefined) check(Array.isArray(dependency.declarations) && dependency.declarations.length > 0 && dependency.declarations.every(declaration => typeof declaration.requested === 'string' && Array.isArray(declaration.dependencyKinds) && dependency.evidence.some(entry => entry.path === declaration.manifest && files.get(`file:${entry.path}`)?.hash === entry.hash)), 'Package declarations require current project evidence.');
199
+ check(['complete', 'partial', 'unsupported'].includes(dependency.symbolCoverage) && Array.isArray(dependency.entryPoints), 'Invalid package interface coverage.');
200
+ for (const entry of dependency.entryPoints) {
201
+ check(typeof entry.importPath === 'string' && typeof entry.target === 'string' && Array.isArray(entry.conditions) && entry.conditions.every(condition => typeof condition === 'string') && Array.isArray(entry.symbols) && ['complete', 'partial', 'unsupported'].includes(entry.coverage), 'Invalid package entry point.');
202
+ if (entry.file != null) check(typeof entry.file === 'string' && path.posix.normalize(entry.file) === entry.file && !path.posix.isAbsolute(entry.file) && !/[\\\0]/u.test(entry.file) && !entry.file.split('/').includes('..') && owns(dependency.root, entry.file), 'Package entry must stay inside its directory root.');
203
+ for (const symbol of entry.symbols) {
204
+ check(typeof symbol.name === 'string' && symbol.visibility === 'public' && typeof entry.hash === 'string' && entry.hash.length > 0, 'Public package symbol requires hashed entry evidence.');
205
+ if (symbol.sourceFile !== undefined) check(dependency.opaque ? symbol.sourceFile === entry.file && symbol.sourceHash === entry.hash : files.get(`file:${symbol.sourceFile}`)?.hash === symbol.sourceHash && typeof symbol.sourceHash === 'string', 'Public declaration source must match current indexed evidence.');
206
+ if (symbol.end) range(symbol.start, symbol.end, {}, 'Package symbol');
207
+ }
208
+ }
209
+ }
210
+ for (const relation of snapshot.relations) {
211
+ check(files.has(relation.fromFileId) && ['resolved', 'unresolved', 'ambiguous'].includes(relation.resolution), 'Invalid dependency source or resolution.');
212
+ check(relation.typeOnly === undefined || typeof relation.typeOnly === 'boolean', 'Dependency type-only classification must be boolean when provided.');
213
+ if (relation.resolution === 'resolved') check(files.has(relation.targetFileId) || packages.has(relation.targetPackageId), 'Dependency references an unknown target.');
214
+ if (relation.targetPackageId != null) check(packages.has(relation.targetPackageId), 'Dependency references an unknown package.');
215
+ if (relation.importedNames !== undefined) check(Array.isArray(relation.importedNames) && relation.importedNames.every(name => typeof name === 'string' && name.length > 0), 'Imported names must be explicit.');
216
+ if (relation.importedSymbolIds !== undefined) check(Array.isArray(relation.importedSymbolIds) && relation.importedSymbolIds.every(id => symbols.get(id)?.fileId === relation.targetFileId && symbols.get(id)?.visibility === 'public'), 'Imported declarations must belong to the resolved target.');
217
+ check(relation.evidence?.hash === files.get(relation.fromFileId).hash && relation.evidence.path === files.get(relation.fromFileId).path, 'Dependency evidence disagrees with its source.');
218
+ range(relation.evidence.start, relation.evidence.end, files.get(relation.fromFileId), 'Dependency');
219
+ }
220
+ check(snapshot.capabilities && typeof snapshot.capabilities === 'object' && Array.isArray(snapshot.diagnostics), 'Missing architecture coverage or diagnostics.');
221
+ for (const name of ['symbols', 'dependencies', 'pages', 'runtimeCalls']) check(['complete', 'partial', 'unsupported', 'unconfigured'].includes(snapshot.capabilities[name]), 'Invalid architecture coverage.');
222
+ if (snapshot.ui) {
223
+ const recomputed = connectArchitectureObservations(snapshot.ui, snapshot.files, snapshot.packages || []);
224
+ check(stableJson(recomputed.connections) === stableJson(snapshot.ui.connections), 'Controller connections disagree with their evidence.');
225
+ }
226
+ return snapshot;
227
+ }
@@ -1,13 +1,16 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, lstat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { extractCodeIndexes } from './code-indexers/ast-grep.js';
5
- import { asDiagnostic } from './errors.js';
5
+ import { inspectNpmPackages, recheckNpmInputs } from './code-indexers/npm-packages.js';
6
+ import { asDiagnostic, GenesisError } from './errors.js';
6
7
  import { buildCityPresentation } from './city-presentation.js';
7
8
  import { cityPathExcluded } from './stack-city-presentation.js';
8
9
  import { gitContext } from './git.js';
9
10
  import { isProjectContentPath } from './paths.js';
10
11
  import { inspectProgram } from './program.js';
12
+ import { inspectSubsystems } from './subsystems.js';
13
+ import { buildArchitectureFacts } from './architecture.js';
11
14
  import { gitVisibleFileStates } from './project-files.js';
12
15
  import { readStack } from './stack.js';
13
16
  import { sha256, stableJson, writeFileAtomic } from './utils.js';
@@ -83,6 +86,7 @@ function normalizedFunction(entry, file, extractor) {
83
86
  qualifiedName,
84
87
  kind: entry.kind,
85
88
  visibility: entry.visibility,
89
+ ...(entry.visibilityScope ? { visibilityScope: entry.visibilityScope } : {}),
86
90
  container: entry.container || null,
87
91
  path: file.path,
88
92
  fileId: fileId(file.path),
@@ -92,6 +96,7 @@ function normalizedFunction(entry, file, extractor) {
92
96
  async: entry.async === true,
93
97
  generator: entry.generator === true,
94
98
  static: entry.static === true,
99
+ ...(entry.start && entry.end ? { start: entry.start, end: entry.end } : {}),
95
100
  role: file.role,
96
101
  language: file.language,
97
102
  extractor,
@@ -101,6 +106,7 @@ function normalizedFunction(entry, file, extractor) {
101
106
  function mergeIndexedFiles(contributions) {
102
107
  const files = new Map();
103
108
  const functions = [];
109
+ const symbols = [];
104
110
  for (const contribution of contributions) {
105
111
  for (const file of contribution.files || []) {
106
112
  const existing = files.get(file.path);
@@ -115,6 +121,7 @@ function mergeIndexedFiles(contributions) {
115
121
  hash: file.hash,
116
122
  extractors: [],
117
123
  functionIds: [],
124
+ dependencies: file.dependencies || [],
118
125
  };
119
126
  if (!record.extractors.includes(contribution.extractor)) record.extractors.push(contribution.extractor);
120
127
  for (const entry of file.functions || []) {
@@ -122,11 +129,13 @@ function mergeIndexedFiles(contributions) {
122
129
  functions.push(normalized);
123
130
  record.functionIds.push(normalized.id);
124
131
  }
132
+ for (const entry of file.symbols || []) symbols.push({ ...entry, id: `symbol:${file.path}:${entry.qualifiedName}:${entry.kind}:${entry.line}:${entry.column}`, fileId: fileId(file.path), path: file.path, language: file.language, extractor: contribution.extractor });
125
133
  files.set(file.path, record);
126
134
  }
127
135
  }
128
136
  return {
129
137
  files: [...files.values()].sort((left, right) => left.path.localeCompare(right.path)),
138
+ symbols,
130
139
  functions: functions.sort((left, right) => (
131
140
  left.path.localeCompare(right.path) || left.line - right.line || left.qualifiedName.localeCompare(right.qualifiedName)
132
141
  )),
@@ -158,6 +167,7 @@ function machineCity({ cityRegions, components, contributions, diagnostics, inde
158
167
  indexers,
159
168
  diagnostics,
160
169
  functions: merged.functions,
170
+ symbols: merged.symbols,
161
171
  districts: structuralDistricts.map((district) => ({
162
172
  ...district,
163
173
  ...presentation.placementForPath(district.path),
@@ -187,6 +197,7 @@ async function programCity(projectRoot) {
187
197
  subsystem: module.subsystem,
188
198
  districtId: module.subsystem === null ? null : subsystemId(module.subsystem),
189
199
  sources: module.sources,
200
+ mainSource: module.mainSource,
190
201
  sourceFileIds: module.sources.map(fileId),
191
202
  }));
192
203
  return {
@@ -219,7 +230,7 @@ async function programCity(projectRoot) {
219
230
 
220
231
  async function writeIfChanged(projectRoot, relativePath, value) {
221
232
  const location = path.join(projectRoot, relativePath);
222
- const source = stableJson(value);
233
+ const source = stableJson(value, { space: 0 });
223
234
  try {
224
235
  if (await readFile(location, 'utf8') === source) return false;
225
236
  } catch (error) {
@@ -246,10 +257,17 @@ export async function buildProjectIndex({
246
257
  queries = [],
247
258
  stackPackages = [],
248
259
  write = true,
260
+ observations = null,
261
+ signal,
249
262
  } = {}) {
263
+ signal?.throwIfAborted();
264
+ const observationReader = typeof observations === 'function' ? observations : null;
265
+ if (observationReader) observations = null;
250
266
  const root = (await gitContext(projectRoot)).repositoryRoot;
251
267
  const stack = await readStack(root, { stackPackages });
252
- const states = await gitVisibleFileStates(root, { includePath: isProjectContentPath });
268
+ const packageRoots = stack.packageDiscovery.discoveries.flatMap(discovery => discovery.manifests.map(manifest => path.posix.join(path.posix.dirname(manifest), discovery.packageRoot)));
269
+ const includePath = filePath => isProjectContentPath(filePath) && !packageRoots.some(directory => filePath === directory || filePath.startsWith(`${directory}/`));
270
+ const states = await gitVisibleFileStates(root, { includePath });
253
271
  const files = [...states]
254
272
  .filter(([filePath, state]) => (
255
273
  state.exists
@@ -260,7 +278,7 @@ export async function buildProjectIndex({
260
278
  ))
261
279
  .map(([filePath, state]) => ({ path: filePath, hash: state.hash, mode: state.mode }));
262
280
  const indexers = [...new Set(stack.components.flatMap((component) => component.indexers || []))].sort();
263
- const { contributions, diagnostics } = await extractCodeIndexes({ indexers, files, projectRoot: root });
281
+ const { contributions, diagnostics } = await extractCodeIndexes({ indexers, files, projectRoot: root, signal });
264
282
  const machine = machineCity({
265
283
  cityRegions: stack.cityRegions,
266
284
  components: stack.components.map(({ id }) => id),
@@ -269,13 +287,55 @@ export async function buildProjectIndex({
269
287
  indexers,
270
288
  });
271
289
  const program = await programCity(root);
290
+ const subsystemMap = await inspectSubsystems({ projectRoot: root });
291
+ const architectureDiagnostics = [];
292
+ for (const entry of subsystemMap.subsystems) {
293
+ try {
294
+ const parts = entry.sourceRoot === '.' ? [] : entry.sourceRoot.split('/');
295
+ for (let index = 0; index <= parts.length; index++) {
296
+ const info = await lstat(path.join(root, ...parts.slice(0, index)));
297
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('Source root must be a real project directory without symbolic links.');
298
+ }
299
+ } catch (error) { architectureDiagnostics.push({ code: 'SUBSYSTEM_SOURCE_UNAVAILABLE', path: entry.sourceRoot, message: error.message }); }
300
+ }
301
+ const packages = []; const packageInputs = [];
302
+ for (const discovery of stack.packageDiscovery.discoveries) {
303
+ signal?.throwIfAborted();
304
+ if (discovery.adapter !== 'npm') { architectureDiagnostics.push({ code: 'PACKAGE_ADAPTER_UNSUPPORTED', path: discovery.manifests[0], message: `Package adapter ${discovery.adapter} has no installed inspector; public boundaries are unavailable.` }); continue; }
305
+ const inspected = await inspectNpmPackages({ projectRoot: root, discovery, files, indexedFiles: contributions.flatMap(contribution => contribution.files), signal });
306
+ packages.push(...inspected.packages); packageInputs.push(...inspected.inputs); architectureDiagnostics.push(...inspected.diagnostics);
307
+ }
308
+ let previous;
309
+ if (!observations && !observationReader) {
310
+ try { previous = JSON.parse(await readFile(path.join(root, MACHINE_CITY_PATH), 'utf8')).architecture; }
311
+ catch (error) { if (!(error instanceof SyntaxError) && !['ENOENT', 'ENOTDIR'].includes(error.code)) throw error; }
312
+ }
313
+ machine.architecture = buildArchitectureFacts({ machine, subsystemMap, program, uiDiscovery: stack.uiDiscovery, packageDiscovery: stack.packageDiscovery, packages, stackHash: stack.identityHash, diagnostics: architectureDiagnostics, observations, previous, inventory: files, cityRegions: stack.cityRegions });
314
+ if (observationReader) {
315
+ signal?.throwIfAborted();
316
+ observations = await observationReader(machine.architecture);
317
+ signal?.throwIfAborted();
318
+ if (!observations) throw new GenesisError('ARCHITECTURE_OBSERVATIONS_INVALID', 'An architecture inspector must return its observation document.');
319
+ machine.architecture = buildArchitectureFacts({ machine, subsystemMap, program, uiDiscovery: stack.uiDiscovery, packageDiscovery: stack.packageDiscovery, packages, stackHash: stack.identityHash, diagnostics: architectureDiagnostics, observations, inventory: files, cityRegions: stack.cityRegions });
320
+ }
321
+ // Do not publish a join from different revisions or destroy the previous
322
+ // valid artifact after an inspector/parser read racing a source change.
323
+ const finalStates = await gitVisibleFileStates(root, { includePath });
324
+ const finalFiles = [...finalStates].filter(([filePath, state]) => state.exists && !state.symlink && !state.special && state.hash && !cityPathExcluded(filePath, stack.cityExclusions)).map(([filePath, state]) => ({ path: filePath, hash: state.hash, mode: state.mode }));
325
+ const finalStack = await readStack(root, { stackPackages });
326
+ const finalMap = await inspectSubsystems({ projectRoot: root }).catch(() => null);
327
+ if (stableJson(finalFiles) !== stableJson(files) || finalStack.identityHash !== stack.identityHash || (finalMap?.identity || null) !== (subsystemMap?.identity || null)) {
328
+ throw new GenesisError('ARCHITECTURE_SOURCE_CHANGED', 'Source or Stack changed during architecture analysis. The previous index is retained; refresh after the changes settle.');
329
+ }
330
+ signal?.throwIfAborted();
331
+ await recheckNpmInputs(root, packageInputs, signal);
272
332
  const changedFiles = [];
273
333
  if (write) {
274
334
  if (await writeIfChanged(root, MACHINE_CITY_PATH, machine)) changedFiles.push(MACHINE_CITY_PATH);
275
335
  if (await writeIfChanged(root, PROGRAM_CITY_PATH, program)) changedFiles.push(PROGRAM_CITY_PATH);
276
336
  }
277
337
  const allMatches = machine.functions.filter((entry) => functionMatches(entry, queries));
278
- const allDiagnostics = [...diagnostics, ...program.diagnostics];
338
+ const allDiagnostics = [...diagnostics, ...program.diagnostics, ...architectureDiagnostics];
279
339
  return {
280
340
  status: allDiagnostics.length > 0 ? 'completed-with-warning' : 'ready',
281
341
  summary: [