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 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, and explanation-style instructions plus optional authored
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.10.6",
3
+ "version": "1.10.8",
4
4
  "type": "module",
5
5
  "description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with project agent guidance.",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.10.6",
3
+ "version": "1.10.8",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
@@ -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
- - Keep complete test output in local artifacts and preserve exit status. Read concise counts, timings and actionable failure excerpts first, expanding evidence only when needed. Do not load full passing reports or repeatedly poll unchanged logs into agent context. Retain startup and cleanup failures; retry only after a change or new evidence justifies it.
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 < 5) {
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 explanation style.',
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
- ...(operands.length > 5 ? { requirements: operands.slice(5).join(' ') } : {}),
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.imports !== undefined) requireFact(states.includes(document.capabilities.imports), 'Invalid import coverage.');
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]) || (optionalCollections.includes(name) && document[name] === undefined), `${name} must be an array.`);
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
- reference(use.parentId, ['pages', 'components', 'componentUses'], use.id);
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
- return { id: `connection:${use.id}`, useId: use.id, parentId: use.parentId, 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 || [])] };
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]) || (optionalCollections.includes(name) && document[name] === undefined), `${name} must be an array.`);
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.');