genesis-compiler 1.10.6 → 1.10.8
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 +42 -2
- package/docs/prompt-integration.md +2 -2
- package/package.json +1 -1
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/profiles/engineering/baseline.md +1 -1
- package/src/cli.js +15 -6
- 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/collaboration.js +37 -3
- 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/prompt.js +1 -0
- 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. |
|
|
@@ -293,6 +294,10 @@ than in a host-private policy or a repeated user-message prefix:
|
|
|
293
294
|
|
|
294
295
|
- `concise`
|
|
295
296
|
|
|
297
|
+
## Long checks
|
|
298
|
+
|
|
299
|
+
- `conservative`
|
|
300
|
+
|
|
296
301
|
## Project requirements
|
|
297
302
|
|
|
298
303
|
- Nothing.
|
|
@@ -302,7 +307,7 @@ Inspect or change it without parsing Markdown yourself:
|
|
|
302
307
|
|
|
303
308
|
```bash
|
|
304
309
|
genesis collaboration show
|
|
305
|
-
genesis collaboration set direct balanced expert conclusions "Be candid."
|
|
310
|
+
genesis collaboration set direct balanced expert conclusions normal "Be candid."
|
|
306
311
|
```
|
|
307
312
|
|
|
308
313
|
Genesis owns the supported choices and their concise expansions. Authored
|
|
@@ -314,6 +319,26 @@ Updates should describe actual progress, findings, decisions, or blockers,
|
|
|
314
319
|
rather than announce compliance with guidance or reassure the user about
|
|
315
320
|
unrelated behavior staying unchanged.
|
|
316
321
|
|
|
322
|
+
The Long checks choice sets how agents handle test suites, builds and other long
|
|
323
|
+
checks. `conservative` runs each one as a single unattended, time-limited
|
|
324
|
+
command that writes its output to a local file and ends with one completion
|
|
325
|
+
signal; the agent does not watch it while it runs and passes this on to agents
|
|
326
|
+
it delegates checks to. It saves the most tokens, and results arrive only when
|
|
327
|
+
each check ends. `normal` lets the agent follow a check's progress and stop a
|
|
328
|
+
clearly broken run early, reacting sooner for more tokens. Both keep the
|
|
329
|
+
Engineering baseline's time limit, local output files, exit status and concise
|
|
330
|
+
evidence reading.
|
|
331
|
+
|
|
332
|
+
The choice belongs to the person, so Genesis never makes it for them. `genesis
|
|
333
|
+
init`, migration and `setCollaboration()` calls that do not pass `longChecks`
|
|
334
|
+
leave `## Long checks` out. While it is absent, `longChecks` is `null` and the
|
|
335
|
+
session guidance asks the agent to put both options to the person once, early,
|
|
336
|
+
before the first long check, to record the answer with `genesis collaboration
|
|
337
|
+
set` (the guidance spells out the command with the current choices), and to use
|
|
338
|
+
`conservative` until then. Once the section exists, the guidance carries only
|
|
339
|
+
the chosen option. A present section with any other value is rejected with the
|
|
340
|
+
valid choices listed.
|
|
341
|
+
|
|
317
342
|
Persistent hosts should install `projectSessionContext()` once when a provider
|
|
318
343
|
conversation is created or refreshed. They may then generate task prompts with
|
|
319
344
|
`sessionContextInstalled: true`, preventing the stable Collaboration and
|
|
@@ -465,6 +490,18 @@ extractor warning does not stop ordinary work. They are rebuilt from the
|
|
|
465
490
|
working tree, so they are not committed: `genesis init` adds both paths to the
|
|
466
491
|
project's `.gitignore`.
|
|
467
492
|
|
|
493
|
+
Machine City also lists entry points: commands, library exports, Program
|
|
494
|
+
operations, tests and inspector-derived pages, components, routes, actions and
|
|
495
|
+
jobs. A page entry is titled by its first registration, such as the URL path
|
|
496
|
+
`/app/project/:slug`, else by the page's title. `genesis index --call-web`
|
|
497
|
+
additionally writes `.genesis/call-web.json`,
|
|
498
|
+
every function site and call site with its `probable`, `exact` or `unresolved`
|
|
499
|
+
targets. The generic baseline is syntax and import scope only, so it is never
|
|
500
|
+
`exact`; an inspector may supply exact facts for its files. The call web is
|
|
501
|
+
written only when Git ignores its path (`genesis init` adds the line). Per-file
|
|
502
|
+
extraction, including installed packages' public entry files, is cached inside
|
|
503
|
+
the Git directory, so unchanged files are not parsed again.
|
|
504
|
+
|
|
468
505
|
Local package exports whose entry files are excluded from indexing retain their
|
|
469
506
|
declared import paths with unavailable symbol coverage and a diagnostic. They do
|
|
470
507
|
not invalidate the rest of the architecture. After upgrading Genesis, run
|
|
@@ -988,6 +1025,9 @@ async callback receiving the fresh architecture facts. A host can run its finite
|
|
|
988
1025
|
inspector inside that callback, avoiding a second source extraction. Genesis
|
|
989
1026
|
joins the result and rechecks source, Stack, subsystem and package inputs before
|
|
990
1027
|
writing compact derived JSON; failures and cancellation retain the prior index.
|
|
1028
|
+
`indexCodebase({ callWeb: true })` also builds the call web; `readCallWeb()`
|
|
1029
|
+
returns it only while it matches the saved Machine City, `validateCallWeb()`
|
|
1030
|
+
checks a document and `CALL_WEB_SCHEMA` names its schema.
|
|
991
1031
|
`ARCHITECTURE_OBSERVATIONS_SCHEMA` identifies the neutral inspector document.
|
|
992
1032
|
`validateArchitectureObservations()` checks its evidence against supplied files
|
|
993
1033
|
and packages; `connectArchitectureObservations()` joins exact owners and
|
|
@@ -180,8 +180,8 @@ Genesis keeps each instruction at one useful level:
|
|
|
180
180
|
- `genesis/blueprint.md`, `genesis/program/`, and `genesis/stack.md` are the
|
|
181
181
|
project's own intent, explanation, and selected technical composition.
|
|
182
182
|
- `genesis/collaboration.md` selects Genesis-owned tone, response-length,
|
|
183
|
-
assumed-experience,
|
|
184
|
-
project requirements.
|
|
183
|
+
assumed-experience, explanation-style, and long-checks instructions plus
|
|
184
|
+
optional authored project requirements.
|
|
185
185
|
- `profiles/engineering/*.md` owns the installed versioned engineering
|
|
186
186
|
profiles. `genesis/engineering.md` selects one profile and records explicit
|
|
187
187
|
project requirements. A universal complexity gate is composed ahead of the
|
package/package.json
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
- Do not overengineer or overcomplicate the solution. Do not add speculative abstractions, layers, dependencies, infrastructure, compatibility paths, distributed patterns, cryptography, enterprise patterns, or other advanced machinery unless an explicit requirement or a concrete constraint makes it necessary.
|
|
6
6
|
- If good software requires material complexity beyond these rules, stop before writing it. Explain the requirement, why the direct design is insufficient, and the smallest added complexity proposed, then ask the user to approve or clarify the tradeoff.
|
|
7
7
|
- During implementation, run the relevant checks and batch related cases when they share setup. Follow the project's verification policy for broad or final checks; do not repeat them merely because an item finishes or a session resumes. Preserve required coverage and isolation.
|
|
8
|
-
-
|
|
8
|
+
- Bound each long check by a time limit so it ends even when it hangs; write its complete output to a local file and preserve its exit status. Afterwards read concise counts, timings and actionable failure excerpts first, expanding evidence only when needed; never load full passing reports into agent context. Retain startup and cleanup failures; retry only after a change or new evidence justifies it.
|
|
9
9
|
- For browser verification, batch related actions and return only the relevant values or page region. Capture screenshots at meaningful visual checkpoints or failures, not after every action. Preserve required viewport, identity and security coverage; prefer existing automated cases for repeated checks.
|
package/src/cli.js
CHANGED
|
@@ -55,7 +55,7 @@ const USAGE = `Usage:
|
|
|
55
55
|
genesis adopt [product guidance...]
|
|
56
56
|
genesis codex install
|
|
57
57
|
genesis collaboration show
|
|
58
|
-
genesis collaboration set <tone> <response-length> <experience> <explanation-style> [project requirements...]
|
|
58
|
+
genesis collaboration set <tone> <response-length> <experience> <explanation-style> <long-checks> [project requirements...]
|
|
59
59
|
genesis engineering list
|
|
60
60
|
genesis engineering show [profile]
|
|
61
61
|
genesis engineering set <profile>
|
|
@@ -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
|
}
|
|
@@ -172,10 +178,10 @@ function parseCommand(argv) {
|
|
|
172
178
|
if (action === 'show' && operands.length !== 1) {
|
|
173
179
|
fail('CLI_EXTRA_ARGUMENT', 'Command collaboration show accepts no extra arguments.');
|
|
174
180
|
}
|
|
175
|
-
if (action === 'set' && operands.length <
|
|
181
|
+
if (action === 'set' && operands.length < 6) {
|
|
176
182
|
fail(
|
|
177
183
|
'CLI_COLLABORATION_VALUES_REQUIRED',
|
|
178
|
-
'Command collaboration set requires tone, response length, experience, and
|
|
184
|
+
'Command collaboration set requires tone, response length, experience, explanation style, and long checks.',
|
|
179
185
|
);
|
|
180
186
|
}
|
|
181
187
|
} else if (command === 'stack') {
|
|
@@ -273,6 +279,7 @@ function writeCollaboration(result) {
|
|
|
273
279
|
line(process.stdout, `Response length: ${result.responseLength}`);
|
|
274
280
|
line(process.stdout, `Assumed experience: ${result.experience}`);
|
|
275
281
|
line(process.stdout, `Explanation style: ${result.explanationStyle}`);
|
|
282
|
+
line(process.stdout, `Long checks: ${result.longChecks || 'unset (conservative until chosen)'}`);
|
|
276
283
|
if (result.requirements) {
|
|
277
284
|
line(process.stdout, 'Project requirements:');
|
|
278
285
|
line(process.stdout, result.requirements);
|
|
@@ -386,6 +393,7 @@ function writeResult(command, result) {
|
|
|
386
393
|
line(process.stdout, result.summary);
|
|
387
394
|
line(process.stdout, `Machine City: ${result.paths.machine}`);
|
|
388
395
|
line(process.stdout, `Program City: ${result.paths.program}`);
|
|
396
|
+
if (result.paths.callWeb) line(process.stdout, `Call web: ${result.paths.callWeb}`);
|
|
389
397
|
if (result.totalMatches > 0) {
|
|
390
398
|
line(process.stdout, 'Matching functions:');
|
|
391
399
|
for (const entry of result.matches) {
|
|
@@ -509,7 +517,8 @@ async function execute({ command, operands, options }, { signal, hostContext = n
|
|
|
509
517
|
responseLength: operands[2],
|
|
510
518
|
experience: operands[3],
|
|
511
519
|
explanationStyle: operands[4],
|
|
512
|
-
|
|
520
|
+
longChecks: operands[5],
|
|
521
|
+
...(operands.length > 6 ? { requirements: operands.slice(6).join(' ') } : {}),
|
|
513
522
|
}),
|
|
514
523
|
};
|
|
515
524
|
}
|
|
@@ -558,7 +567,7 @@ async function execute({ command, operands, options }, { signal, hostContext = n
|
|
|
558
567
|
});
|
|
559
568
|
}
|
|
560
569
|
if (command === 'context') return getContext({ paths: operands, projectRoot, stackPackages });
|
|
561
|
-
if (command === 'index') return indexCodebase({ projectRoot, queries: operands, stackPackages });
|
|
570
|
+
if (command === 'index') return indexCodebase({ projectRoot, queries: operands, stackPackages, callWeb: options.callWeb });
|
|
562
571
|
if (command === 'inspect') {
|
|
563
572
|
if (operands[0] === 'section') {
|
|
564
573
|
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.');
|