genesis-compiler 1.10.6 → 1.10.7
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 +17 -1
- package/package.json +1 -1
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/src/cli.js +9 -2
- package/src/index/architecture-observations.js +94 -7
- package/src/index/architecture.js +21 -2
- package/src/index/call-web.js +419 -0
- package/src/index/code-index.js +154 -16
- package/src/index/code-indexers/ast-grep.js +453 -77
- package/src/index/code-indexers/npm-packages.js +4 -3
- package/src/index/code-indexers/worker.js +5 -2
- package/src/index/entry-points.js +269 -0
- package/src/index/extraction-cache.js +81 -0
- package/src/index/project-files.js +7 -3
- package/src/index/project-inspection.js +5 -2
- package/src/index/utils.js +34 -0
- package/src/index.js +3 -1
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ genesis/
|
|
|
39
39
|
.genesis/ local, git-ignored (`genesis init` adds the ignore lines)
|
|
40
40
|
machine-city.json derived detailed source and function map
|
|
41
41
|
program-city.json derived simple subsystem and operation map
|
|
42
|
+
call-web.json derived function call web (`genesis index --call-web`)
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
Program paths describe public operations rather than languages or source
|
|
@@ -253,7 +254,7 @@ files alone does not refresh its existing context.
|
|
|
253
254
|
| `genesis stack add <piece...>` | Resolves component dependencies/conflicts, records the selection, materializes missing project contracts, synchronizes declared skills, and prints the one-time preparation prompt. It does not install application packages or run an agent. |
|
|
254
255
|
| `genesis skills sync` | Refreshes selected, unmodified managed skills while preserving customized and unmanaged copies. |
|
|
255
256
|
| `genesis context <path...>` | Returns path-focused Program explanations, indexed functions, selected technology guidance, exact project contracts, and relevant skills. |
|
|
256
|
-
| `genesis index [function-or-path...]` | Regenerates the derived code indexes and optionally searches the existing function/method inventory. |
|
|
257
|
+
| `genesis index [--call-web] [function-or-path...]` | Regenerates the derived code indexes and optionally searches the existing function/method inventory. `--call-web` also writes the derived function call web. |
|
|
257
258
|
| `genesis inspect environment` | Reports declared resources, public defaults, missing input names, and environment-file paths without returning secret values. |
|
|
258
259
|
| `genesis inspect skills` | Reports selected skill freshness and ownership without changing files. |
|
|
259
260
|
| `genesis inspect section <name>` | Returns one exact consumer-owned Stack section without interpreting or executing it. |
|
|
@@ -465,6 +466,18 @@ extractor warning does not stop ordinary work. They are rebuilt from the
|
|
|
465
466
|
working tree, so they are not committed: `genesis init` adds both paths to the
|
|
466
467
|
project's `.gitignore`.
|
|
467
468
|
|
|
469
|
+
Machine City also lists entry points: commands, library exports, Program
|
|
470
|
+
operations, tests and inspector-derived pages, components, routes, actions and
|
|
471
|
+
jobs. A page entry is titled by its first registration, such as the URL path
|
|
472
|
+
`/app/project/:slug`, else by the page's title. `genesis index --call-web`
|
|
473
|
+
additionally writes `.genesis/call-web.json`,
|
|
474
|
+
every function site and call site with its `probable`, `exact` or `unresolved`
|
|
475
|
+
targets. The generic baseline is syntax and import scope only, so it is never
|
|
476
|
+
`exact`; an inspector may supply exact facts for its files. The call web is
|
|
477
|
+
written only when Git ignores its path (`genesis init` adds the line). Per-file
|
|
478
|
+
extraction, including installed packages' public entry files, is cached inside
|
|
479
|
+
the Git directory, so unchanged files are not parsed again.
|
|
480
|
+
|
|
468
481
|
Local package exports whose entry files are excluded from indexing retain their
|
|
469
482
|
declared import paths with unavailable symbol coverage and a diagnostic. They do
|
|
470
483
|
not invalidate the rest of the architecture. After upgrading Genesis, run
|
|
@@ -988,6 +1001,9 @@ async callback receiving the fresh architecture facts. A host can run its finite
|
|
|
988
1001
|
inspector inside that callback, avoiding a second source extraction. Genesis
|
|
989
1002
|
joins the result and rechecks source, Stack, subsystem and package inputs before
|
|
990
1003
|
writing compact derived JSON; failures and cancellation retain the prior index.
|
|
1004
|
+
`indexCodebase({ callWeb: true })` also builds the call web; `readCallWeb()`
|
|
1005
|
+
returns it only while it matches the saved Machine City, `validateCallWeb()`
|
|
1006
|
+
checks a document and `CALL_WEB_SCHEMA` names its schema.
|
|
991
1007
|
`ARCHITECTURE_OBSERVATIONS_SCHEMA` identifies the neutral inspector document.
|
|
992
1008
|
`validateArchitectureObservations()` checks its evidence against supplied files
|
|
993
1009
|
and packages; `connectArchitectureObservations()` joins exact owners and
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -65,7 +65,7 @@ const USAGE = `Usage:
|
|
|
65
65
|
genesis templates list
|
|
66
66
|
genesis templates apply <catalogue:technology/variant>
|
|
67
67
|
genesis context <path...>
|
|
68
|
-
genesis index [function-or-path...]
|
|
68
|
+
genesis index [--call-web] [function-or-path...]
|
|
69
69
|
genesis parsers list
|
|
70
70
|
genesis parsers install <language...> | --all
|
|
71
71
|
genesis parsers verify <language...> | --all
|
|
@@ -87,6 +87,7 @@ Options:
|
|
|
87
87
|
--task <task> Select the prompt task (default: work)
|
|
88
88
|
--directory <path> Parser cache directory (parsers commands only)
|
|
89
89
|
--all Select every parser (parsers install/verify only)
|
|
90
|
+
--call-web Also build .genesis/call-web.json (index only)
|
|
90
91
|
--json Emit one machine-readable result
|
|
91
92
|
--claude-part <1-4> Emit one bounded Claude session-hook part
|
|
92
93
|
-h, --help Show this help
|
|
@@ -114,6 +115,7 @@ function parseCommand(argv) {
|
|
|
114
115
|
json: { type: 'boolean', default: false },
|
|
115
116
|
'claude-part': { type: 'string' },
|
|
116
117
|
all: { type: 'boolean', default: false },
|
|
118
|
+
'call-web': { type: 'boolean', default: false },
|
|
117
119
|
directory: { type: 'string' },
|
|
118
120
|
'project-root': { type: 'string' },
|
|
119
121
|
'stack-package': { type: 'string', multiple: true, default: [] },
|
|
@@ -128,6 +130,7 @@ function parseCommand(argv) {
|
|
|
128
130
|
json: parsed.values.json,
|
|
129
131
|
claudePart: parsed.values['claude-part'],
|
|
130
132
|
all: parsed.values.all,
|
|
133
|
+
callWeb: parsed.values['call-web'],
|
|
131
134
|
directory: parsed.values.directory,
|
|
132
135
|
projectRoot: parsed.values['project-root'],
|
|
133
136
|
stackPackages: parsed.values['stack-package'],
|
|
@@ -150,6 +153,9 @@ function parseCommand(argv) {
|
|
|
150
153
|
if (options.templateSources.length && command !== 'templates') {
|
|
151
154
|
fail('CLI_OPTION_NOT_APPLICABLE', `Option --template-source is not applicable to ${command}.`);
|
|
152
155
|
}
|
|
156
|
+
if (options.callWeb && command !== 'index') {
|
|
157
|
+
fail('CLI_OPTION_NOT_APPLICABLE', `Option --call-web is not applicable to ${command}.`);
|
|
158
|
+
}
|
|
153
159
|
if (options.task !== undefined && command !== 'prompt') {
|
|
154
160
|
fail('CLI_OPTION_NOT_APPLICABLE', `Option --task is not applicable to ${command}.`);
|
|
155
161
|
}
|
|
@@ -386,6 +392,7 @@ function writeResult(command, result) {
|
|
|
386
392
|
line(process.stdout, result.summary);
|
|
387
393
|
line(process.stdout, `Machine City: ${result.paths.machine}`);
|
|
388
394
|
line(process.stdout, `Program City: ${result.paths.program}`);
|
|
395
|
+
if (result.paths.callWeb) line(process.stdout, `Call web: ${result.paths.callWeb}`);
|
|
389
396
|
if (result.totalMatches > 0) {
|
|
390
397
|
line(process.stdout, 'Matching functions:');
|
|
391
398
|
for (const entry of result.matches) {
|
|
@@ -558,7 +565,7 @@ async function execute({ command, operands, options }, { signal, hostContext = n
|
|
|
558
565
|
});
|
|
559
566
|
}
|
|
560
567
|
if (command === 'context') return getContext({ paths: operands, projectRoot, stackPackages });
|
|
561
|
-
if (command === 'index') return indexCodebase({ projectRoot, queries: operands, stackPackages });
|
|
568
|
+
if (command === 'index') return indexCodebase({ projectRoot, queries: operands, stackPackages, callWeb: options.callWeb });
|
|
562
569
|
if (command === 'inspect') {
|
|
563
570
|
if (operands[0] === 'section') {
|
|
564
571
|
return {
|
|
@@ -5,7 +5,22 @@ import { sha256, stableJson } from './utils.js';
|
|
|
5
5
|
export const ARCHITECTURE_OBSERVATIONS_SCHEMA = 'genesis.architecture-observations.v1';
|
|
6
6
|
const collections = ['pages', 'registrations', 'components', 'componentUses', 'entryOperations', 'operationUses'];
|
|
7
7
|
const optionalCollections = ['imports'];
|
|
8
|
+
// Call-web collections appear only when an inspector declares them, so
|
|
9
|
+
// documents and merges without them keep their exact v1 shape.
|
|
10
|
+
const callCollections = ['functions', 'calls', 'entryPoints'];
|
|
8
11
|
const states = ['complete', 'partial', 'unsupported', 'unconfigured'];
|
|
12
|
+
export const FUNCTION_KINDS = Object.freeze(['function', 'method', 'constructor', 'arrow', 'closure', 'module']);
|
|
13
|
+
export const CALL_CERTAINTIES = Object.freeze(['exact', 'probable', 'unresolved']);
|
|
14
|
+
export const CALL_METHODS = Object.freeze(['checker', 'di-binding', 'route-action', 'argument-flow', 'import-scope', 'same-file-name', 'self', 'receiver-name', 'none']);
|
|
15
|
+
export const EXACT_CALL_METHODS = Object.freeze(['checker', 'di-binding', 'route-action', 'self', 'argument-flow']);
|
|
16
|
+
export const ENTRY_POINT_KINDS = Object.freeze(['page', 'component', 'route', 'action', 'library-export', 'cli-command', 'job', 'program-operation', 'test']);
|
|
17
|
+
export const MAX_CALL_TARGETS = 16;
|
|
18
|
+
export const MAX_CALLS_PER_FILE = 2000;
|
|
19
|
+
export const MAX_CALLEE_LENGTH = 120;
|
|
20
|
+
// A file's top-level code is `fn:<path>#module`; every other function is named by its start.
|
|
21
|
+
export const functionId = (file, start, kind) => kind === 'module' ? `fn:${file}#module` : `fn:${file}#${start.line}:${start.column}`;
|
|
22
|
+
const before = (left, right) => left.line < right.line || (left.line === right.line && left.column < right.column);
|
|
23
|
+
const notAfter = (left, right) => !before(right, left);
|
|
9
24
|
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
25
|
const text = value => typeof value === 'string' && value.length > 0 && !/[\0\r\n]/u.test(value);
|
|
11
26
|
const canonical = value => text(value) && value !== '.' && !/[\\]/u.test(value) && !path.posix.isAbsolute(value) && path.posix.normalize(value) === value && !value.split('/').includes('..');
|
|
@@ -15,6 +30,45 @@ function requireFact(condition, message) {
|
|
|
15
30
|
if (!condition) throw new GenesisError('ARCHITECTURE_OBSERVATIONS_INVALID', message);
|
|
16
31
|
}
|
|
17
32
|
|
|
33
|
+
function validateCallWebFacts(document, ids, inputs, functionReference, operation) {
|
|
34
|
+
requireFact(document.calls === undefined || Array.isArray(document.functions), 'calls require functions.');
|
|
35
|
+
const functions = new Map();
|
|
36
|
+
for (const entry of document.functions || []) {
|
|
37
|
+
requireFact(inputs.has(entry.file), `${entry.id} must identify a declared input file.`);
|
|
38
|
+
requireFact(point(entry.start) && point(entry.end) && before(entry.start, entry.end), `${entry.id} has an impossible source range.`);
|
|
39
|
+
requireFact(entry.id === functionId(entry.file, entry.start, entry.kind), `${entry.id} must be fn:<path>#<start line>:<start column> (fn:<path>#module for module code).`);
|
|
40
|
+
requireFact(FUNCTION_KINDS.includes(entry.kind) && typeof entry.name === 'string' && !/[\0\r\n]/u.test(entry.name), `${entry.id} needs a known kind and a name (empty when anonymous).`);
|
|
41
|
+
requireFact(entry.kind !== 'module' || (entry.start.line === 1 && entry.start.column === 1), `${entry.id} module code starts at 1:1.`);
|
|
42
|
+
requireFact(entry.container == null || text(entry.container), `${entry.id} has an invalid container.`);
|
|
43
|
+
requireFact(entry.exported === undefined || typeof entry.exported === 'boolean', `${entry.id} has an invalid export flag.`);
|
|
44
|
+
requireFact(entry.evidence.every(evidence => evidence.path === entry.file), `${entry.id} evidence must be in its own file.`);
|
|
45
|
+
functions.set(entry.id, entry);
|
|
46
|
+
}
|
|
47
|
+
const perFile = new Map();
|
|
48
|
+
for (const call of document.calls || []) {
|
|
49
|
+
const from = functions.get(call.fromFunctionId);
|
|
50
|
+
requireFact(from && Array.isArray(call.targets) && call.targets.every(target => functions.has(target)) && new Set(call.targets).size === call.targets.length, `${call.id} must reference known caller and target functions.`);
|
|
51
|
+
requireFact(CALL_CERTAINTIES.includes(call.certainty) && CALL_METHODS.includes(call.method), `${call.id} needs a known certainty and method.`);
|
|
52
|
+
requireFact(typeof call.callee === 'string' && call.callee.length <= MAX_CALLEE_LENGTH && !call.callee.includes('\0'), `${call.id} callee text must be at most ${MAX_CALLEE_LENGTH} characters.`);
|
|
53
|
+
if (call.certainty === 'exact') requireFact(call.resolution === 'resolved' && call.targets.length === 1 && EXACT_CALL_METHODS.includes(call.method), `${call.id} exact calls need one resolved target from a resolver.`);
|
|
54
|
+
if (call.certainty === 'probable') requireFact(call.method !== 'none' && ((call.resolution === 'resolved' && call.targets.length === 1) || (call.resolution === 'ambiguous' && call.targets.length >= 2 && call.targets.length <= MAX_CALL_TARGETS)), `${call.id} probable calls need one resolved or 2-${MAX_CALL_TARGETS} ambiguous targets.`);
|
|
55
|
+
if (call.certainty === 'unresolved') requireFact(call.resolution === 'unresolved' && call.targets.length === 0 && call.method === 'none', `${call.id} unresolved calls have no targets and no method.`);
|
|
56
|
+
requireFact(call.evidence.every(evidence => evidence.path === from.file && notAfter(from.start, evidence.start) && notAfter(evidence.end, from.end)), `${call.id} evidence must lie inside its caller.`);
|
|
57
|
+
perFile.set(from.file, (perFile.get(from.file) || 0) + 1);
|
|
58
|
+
requireFact(perFile.get(from.file) <= MAX_CALLS_PER_FILE, `${from.file} has more than ${MAX_CALLS_PER_FILE} call records; truncate with CALLS_TRUNCATED.`);
|
|
59
|
+
}
|
|
60
|
+
for (const entry of document.entryPoints || []) {
|
|
61
|
+
requireFact(ENTRY_POINT_KINDS.includes(entry.kind) && text(entry.title), `${entry.id} needs a known kind and a title.`);
|
|
62
|
+
requireFact(inputs.has(entry.file), `${entry.id} must identify a declared input file.`);
|
|
63
|
+
requireFact(Array.isArray(entry.functionIds), `${entry.id} needs function identities.`);
|
|
64
|
+
for (const value of entry.functionIds) functionReference(value, entry.id);
|
|
65
|
+
if (entry.pageId != null) requireFact(ids.get(entry.pageId)?.name === 'pages', `${entry.id} refers to an unknown page.`);
|
|
66
|
+
if (entry.componentId != null) requireFact(ids.get(entry.componentId)?.name === 'components', `${entry.id} refers to an unknown component.`);
|
|
67
|
+
if (entry.operationKey != null) operation(entry.operationKey, entry.id);
|
|
68
|
+
if (entry.actionId != null) requireFact(text(entry.actionId), `${entry.id} has an invalid action identity.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
18
72
|
// A finite, framework-independent JSON contract. Inspectors collect evidence;
|
|
19
73
|
// this owner validates identities and joins without executing application code.
|
|
20
74
|
export function validateArchitectureObservations(document, { files = [], packages = [] } = {}) {
|
|
@@ -23,7 +77,7 @@ export function validateArchitectureObservations(document, { files = [], package
|
|
|
23
77
|
requireFact(Array.isArray(document.inputs), 'inputs must be an array.');
|
|
24
78
|
requireFact(object(document.capabilities), 'capabilities must be an object.');
|
|
25
79
|
for (const name of ['pages', 'components', 'entryOperations', 'operationUses']) requireFact(states.includes(document.capabilities[name]), `Missing or invalid ${name} capability.`);
|
|
26
|
-
if (document.capabilities
|
|
80
|
+
for (const name of ['imports', ...callCollections]) if (document.capabilities[name] !== undefined) requireFact(states.includes(document.capabilities[name]), `Invalid ${name} coverage.`);
|
|
27
81
|
requireFact(Array.isArray(document.diagnostics) && document.diagnostics.every(entry => object(entry) && text(entry.code) && text(entry.message)), 'diagnostics must contain code and message.');
|
|
28
82
|
const filesByPath = new Map(files.map(file => [file.path, file]));
|
|
29
83
|
const inputs = new Map();
|
|
@@ -35,8 +89,8 @@ export function validateArchitectureObservations(document, { files = [], package
|
|
|
35
89
|
inputs.set(input.path, input);
|
|
36
90
|
}
|
|
37
91
|
const ids = new Map();
|
|
38
|
-
for (const name of [...collections, ...optionalCollections]) {
|
|
39
|
-
requireFact(Array.isArray(document[name]) || (
|
|
92
|
+
for (const name of [...collections, ...optionalCollections, ...callCollections]) {
|
|
93
|
+
requireFact(Array.isArray(document[name]) || (!collections.includes(name) && document[name] === undefined), `${name} must be an array.`);
|
|
40
94
|
for (const record of document[name] || []) {
|
|
41
95
|
requireFact(object(record) && text(record.id) && !ids.has(record.id), `Invalid or duplicate ${name} identity.`);
|
|
42
96
|
requireFact(['resolved', 'unresolved', 'ambiguous'].includes(record.resolution), `${record.id} needs an explicit resolution.`);
|
|
@@ -93,10 +147,24 @@ export function validateArchitectureObservations(document, { files = [], package
|
|
|
93
147
|
if (entry.resolution === 'resolved') source(entry.controllerFile, entry.id);
|
|
94
148
|
if (entry.controllerSymbol != null) requireFact(text(entry.controllerSymbol), `${entry.id} has an invalid symbol.`);
|
|
95
149
|
}
|
|
150
|
+
// Without a function table (a saved compact copy), caller identities stay opaque strings.
|
|
151
|
+
const hasFunctions = Array.isArray(document.functions);
|
|
152
|
+
function functionReference(value, label) {
|
|
153
|
+
if (hasFunctions) reference(value, ['functions'], label);
|
|
154
|
+
else requireFact(text(value), `${label} has an invalid function identity.`);
|
|
155
|
+
}
|
|
156
|
+
for (const entry of document.entryOperations) {
|
|
157
|
+
if (entry.controllerFunctionId !== undefined) functionReference(entry.controllerFunctionId, entry.id);
|
|
158
|
+
if (entry.actionId !== undefined) requireFact(text(entry.actionId), `${entry.id} has an invalid action identity.`);
|
|
159
|
+
}
|
|
96
160
|
for (const use of document.operationUses) {
|
|
97
|
-
|
|
161
|
+
requireFact(use.parentId != null || use.fromFunctionId !== undefined, `${use.id} needs a page/component parent or a caller function.`);
|
|
162
|
+
if (use.parentId != null) reference(use.parentId, ['pages', 'components', 'componentUses'], use.id);
|
|
163
|
+
if (use.fromFunctionId !== undefined) functionReference(use.fromFunctionId, use.id);
|
|
98
164
|
if (use.resolution === 'resolved') operation(use.key, use.id);
|
|
165
|
+
if (use.keyCertainty !== undefined) requireFact(use.resolution === 'resolved' && ['exact', 'probable'].includes(use.keyCertainty), `${use.id} key certainty applies only to a resolved key.`);
|
|
99
166
|
}
|
|
167
|
+
validateCallWebFacts(document, ids, inputs, functionReference, operation);
|
|
100
168
|
// Parent graphs must be finite. Shared definitions are not duplicate owners.
|
|
101
169
|
for (const name of ['pages', 'componentUses']) for (const record of document[name]) {
|
|
102
170
|
const seen = new Set([record.id]);
|
|
@@ -121,7 +189,9 @@ export function connectArchitectureObservations(document, files, packages = [])
|
|
|
121
189
|
const connections = document.operationUses.map(use => {
|
|
122
190
|
const matches = use.resolution === 'resolved' ? entries.get(stableJson(use.key)) || [] : [];
|
|
123
191
|
const entry = matches.length === 1 ? matches[0] : null;
|
|
124
|
-
|
|
192
|
+
// Caller facts appear only for function-level uses; an exact key alone makes an exact bridge.
|
|
193
|
+
const bridge = use.fromFunctionId === undefined ? {} : { fromFunctionId: use.fromFunctionId, controllerFunctionId: entry?.controllerFunctionId ?? null, certainty: entry ? (use.keyCertainty === 'exact' ? 'exact' : 'probable') : null };
|
|
194
|
+
return { id: `connection:${use.id}`, useId: use.id, parentId: use.parentId ?? null, key: use.key || null, resolution: entry ? 'resolved' : matches.length > 1 ? 'ambiguous' : 'unresolved', entryId: entry?.id || null, controllerFileId: entry ? byPath.get(entry.controllerFile).id : null, controllerSymbol: entry?.controllerSymbol || null, evidence: [...use.evidence, ...(entry?.evidence || [])], ...bridge };
|
|
125
195
|
});
|
|
126
196
|
return {
|
|
127
197
|
...document, connections,
|
|
@@ -130,9 +200,14 @@ export function connectArchitectureObservations(document, files, packages = [])
|
|
|
130
200
|
};
|
|
131
201
|
}
|
|
132
202
|
|
|
203
|
+
const sameFunction = (left, right) => ['file', 'name', 'kind'].every(field => left[field] === right[field])
|
|
204
|
+
&& stableJson([left.start, left.end]) === stableJson([right.start, right.end]);
|
|
205
|
+
|
|
133
206
|
export function mergeArchitectureObservations(documents, { files = [], packages = [] } = {}) {
|
|
134
207
|
requireFact(Array.isArray(documents) && documents.length > 0, 'At least one observation document is required.');
|
|
135
208
|
const merged = { schema: ARCHITECTURE_OBSERVATIONS_SCHEMA, adapter: '', inputs: [], capabilities: {}, diagnostics: [], ...Object.fromEntries([...collections, ...optionalCollections].map(name => [name, []])) };
|
|
209
|
+
const declared = callCollections.filter(name => documents.some(document => document?.[name] !== undefined));
|
|
210
|
+
for (const name of declared) merged[name] = [];
|
|
136
211
|
const adapters = new Set();
|
|
137
212
|
for (const document of documents) {
|
|
138
213
|
requireFact(document?.schema === ARCHITECTURE_OBSERVATIONS_SCHEMA && text(document.adapter) && !adapters.has(document.adapter), 'Every inspector needs a unique adapter identity and the observation schema.');
|
|
@@ -141,13 +216,20 @@ export function mergeArchitectureObservations(documents, { files = [], packages
|
|
|
141
216
|
requireFact(!merged.packageIdentity || merged.packageIdentity === document.packageIdentity, 'Inspectors disagree about the package snapshot.');
|
|
142
217
|
merged.packageIdentity = document.packageIdentity;
|
|
143
218
|
}
|
|
144
|
-
for (const name of ['inputs', ...collections, ...optionalCollections]) {
|
|
145
|
-
requireFact(Array.isArray(document[name]) || (
|
|
219
|
+
for (const name of ['inputs', ...collections, ...optionalCollections, ...declared]) {
|
|
220
|
+
requireFact(Array.isArray(document[name]) || (!collections.includes(name) && name !== 'inputs' && document[name] === undefined), `${name} must be an array.`);
|
|
146
221
|
const byId = new Map(merged[name].map(record => [name === 'inputs' ? record.path : record.id, record]));
|
|
147
222
|
for (const record of document[name] || []) {
|
|
148
223
|
requireFact(object(record), `Invalid ${name} record.`);
|
|
149
224
|
const id = name === 'inputs' ? record.path : record.id;
|
|
150
225
|
const previous = byId.get(id);
|
|
226
|
+
if (name === 'functions') {
|
|
227
|
+
// The same function seen by two inspectors is one record; on
|
|
228
|
+
// disagreement the inspector earlier in Stack order wins.
|
|
229
|
+
if (previous && !sameFunction(previous, record)) merged.diagnostics.push({ code: 'FUNCTION_CONFLICT', path: record.file, message: `${document.adapter} and ${previous.adapter} disagree about ${id}; ${previous.adapter} is kept.` });
|
|
230
|
+
if (!previous) { const kept = { ...record, adapter: document.adapter }; merged[name].push(kept); byId.set(id, kept); }
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
151
233
|
requireFact(!previous || stableJson(previous) === stableJson(record), `Conflicting ${name} identity ${id}.`);
|
|
152
234
|
if (!previous) { merged[name].push(record); byId.set(id, record); }
|
|
153
235
|
}
|
|
@@ -163,5 +245,10 @@ export function mergeArchitectureObservations(documents, { files = [], packages
|
|
|
163
245
|
}
|
|
164
246
|
const importCoverage = documents.map(document => document.capabilities?.imports || 'unconfigured');
|
|
165
247
|
merged.capabilities.imports = new Set(importCoverage).size === 1 ? importCoverage[0] : 'partial';
|
|
248
|
+
for (const capability of callCollections) {
|
|
249
|
+
if (!documents.some(document => document.capabilities?.[capability] !== undefined)) continue;
|
|
250
|
+
const values = documents.map(document => document.capabilities?.[capability] || 'unconfigured');
|
|
251
|
+
merged.capabilities[capability] = new Set(values).size === 1 ? values[0] : 'partial';
|
|
252
|
+
}
|
|
166
253
|
return validateArchitectureObservations(merged, { files, packages });
|
|
167
254
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { connectArchitectureObservations } from './architecture-observations.js';
|
|
2
|
+
import { connectArchitectureObservations, ENTRY_POINT_KINDS } from './architecture-observations.js';
|
|
3
3
|
import { buildCityPresentation } from './city-presentation.js';
|
|
4
4
|
import { GenesisError } from './errors.js';
|
|
5
5
|
import { sha256, stableJson } from './utils.js';
|
|
6
6
|
|
|
7
7
|
const owns = (root, file) => root === '.' || file.startsWith(`${root}/`);
|
|
8
|
+
// Function and call tables live in the call web; Machine City keeps the rest.
|
|
9
|
+
const withoutCallTables = ({ functions, calls, ...rest }) => rest;
|
|
8
10
|
|
|
9
11
|
function resolveReference(file, reference, filesByPath, packages) {
|
|
10
12
|
const specifier = reference.specifier;
|
|
@@ -80,7 +82,7 @@ export function buildArchitectureFacts({ machine, subsystemMap, program = null,
|
|
|
80
82
|
})));
|
|
81
83
|
const symbols = [...machine.functions, ...(machine.symbols || [])].map(symbol => ({ ...symbol, start: symbol.start || { line: symbol.line, column: symbol.column }, rangeAvailable: Boolean(symbol.end) }));
|
|
82
84
|
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;
|
|
85
|
+
let ui = observations ? withoutCallTables(connectArchitectureObservations(observations, files, packages)) : null;
|
|
84
86
|
if (!observations && previous?.ui) {
|
|
85
87
|
// Source-only indexing must not erase unchanged consumer observations.
|
|
86
88
|
// A changed snapshot requires its inspector; never carry guesses forward.
|
|
@@ -219,6 +221,23 @@ export function validateArchitectureFacts(snapshot) {
|
|
|
219
221
|
}
|
|
220
222
|
check(snapshot.capabilities && typeof snapshot.capabilities === 'object' && Array.isArray(snapshot.diagnostics), 'Missing architecture coverage or diagnostics.');
|
|
221
223
|
for (const name of ['symbols', 'dependencies', 'pages', 'runtimeCalls']) check(['complete', 'partial', 'unsupported', 'unconfigured'].includes(snapshot.capabilities[name]), 'Invalid architecture coverage.');
|
|
224
|
+
for (const name of ['functions', 'calls', 'entryPoints']) check(snapshot.capabilities[name] === undefined || ['complete', 'partial', 'unsupported', 'unconfigured'].includes(snapshot.capabilities[name]), 'Invalid architecture coverage.');
|
|
225
|
+
const filesByPath = new Map([...files.values()].map(file => [file.path, file]));
|
|
226
|
+
if (snapshot.entryPoints !== undefined) {
|
|
227
|
+
check(Array.isArray(snapshot.entryPoints), 'entryPoints must be an array.');
|
|
228
|
+
const ids = new Set();
|
|
229
|
+
for (const entry of snapshot.entryPoints) {
|
|
230
|
+
check(entry && typeof entry.id === 'string' && entry.id.length > 0 && !ids.has(entry.id) && ENTRY_POINT_KINDS.includes(entry.kind) && typeof entry.title === 'string' && entry.title.length > 0, 'Invalid or duplicate entry point.');
|
|
231
|
+
ids.add(entry.id);
|
|
232
|
+
check(filesByPath.has(entry.file) && Array.isArray(entry.functionIds) && entry.functionIds.every(id => typeof id === 'string' && id.length > 0), `${entry.id} must name a known file and function identities.`);
|
|
233
|
+
check(Array.isArray(entry.evidence) && entry.evidence.length > 0 && entry.evidence.every(evidence => filesByPath.get(evidence?.path)?.hash === evidence.hash), `${entry.id} evidence is stale.`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (snapshot.callWeb != null) {
|
|
237
|
+
const { callWeb } = snapshot;
|
|
238
|
+
check(callWeb.path === '.genesis/call-web.json' && typeof callWeb.identity === 'string' && typeof callWeb.sourceIdentity === 'string' && callWeb.counts && typeof callWeb.counts === 'object' && callWeb.coverage && typeof callWeb.coverage === 'object', 'Invalid call-web summary.');
|
|
239
|
+
for (const name of ['functions', 'calls', 'exact', 'probable', 'unresolved']) check(Number.isSafeInteger(callWeb.counts[name]) && callWeb.counts[name] >= 0, 'Invalid call-web counts.');
|
|
240
|
+
}
|
|
222
241
|
if (snapshot.ui) {
|
|
223
242
|
const recomputed = connectArchitectureObservations(snapshot.ui, snapshot.files, snapshot.packages || []);
|
|
224
243
|
check(stableJson(recomputed.connections) === stableJson(snapshot.ui.connections), 'Controller connections disagree with their evidence.');
|