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
|
@@ -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
|
+
}
|
package/src/index/code-index.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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: [
|