genesis-compiler 1.7.8 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,10 +49,16 @@ genesis/program/notes/list-notes.md
49
49
  genesis/program/notes/create-note.md
50
50
  ```
51
51
 
52
- The directory is a conceptual subsystem. Each file describes one public
52
+ The directory organizes explanations; `genesis/subsystems.md` assigns each
53
+ module to one directory-bound subsystem. Each file describes one public
53
54
  operation provided by that subsystem: an exported method, API action, route,
54
55
  command, UI operation, or equivalent entry point. Each module cites one or more
55
- real files under `## Sources`. There is no 1:1 source mapping: one operation may
56
+ real files under `## Sources`. Every module requires exactly one
57
+ `- Main: \`path/to/source\`` bullet naming its implementation's reading anchor.
58
+ The main source belongs to the module's owning subsystem; other source bullets
59
+ may cite shared libraries. It need not be an executable entry point. Existing
60
+ modules need an explicitly chosen anchor before their Program can validate;
61
+ indexing never guesses from source-list order. There is no 1:1 source mapping: one operation may
56
62
  span several files, helper files may be omitted, and one source may support
57
63
  multiple operations.
58
64
 
@@ -78,7 +84,7 @@ catalog as separate packages:
78
84
  npm install --global genesis-compiler genesis-stack
79
85
  ```
80
86
 
81
- `genesis-compiler` has no dependency on `genesis-stack` or on any framework.
87
+ `genesis-compiler` has no runtime dependency on `genesis-stack` or on any framework.
82
88
  The catalog is static Markdown and contains the complete curated set of
83
89
  language, database, UI, and framework pieces. A host may supply another catalog
84
90
  through the same contract.
@@ -880,13 +886,23 @@ result. A newer project is never downgraded. Every project-scoped CLI
880
886
  invocation warns when the recorded format does not match the running CLI, and
881
887
  ordinary project operations stop until the migration is resolved.
882
888
 
889
+ ## Development
890
+
891
+ Run `npm ci` in this source checkout, then `npm test`. The test command runs the
892
+ original test files with file concurrency fixed at one. The local first-party
893
+ catalogue is a development-only dependency so catalogue and starter tests work
894
+ after a clean install; it is not required by the published compiler runtime.
895
+
883
896
  ## Public API
884
897
 
885
898
  ```js
886
899
  import {
900
+ ARCHITECTURE_OBSERVATIONS_SCHEMA,
901
+ SOURCE_EXPLANATION_PROMPT_VERSION,
887
902
  adoptProject,
888
903
  addStack,
889
904
  check,
905
+ connectArchitectureObservations,
890
906
  generatePrompt,
891
907
  getContext,
892
908
  indexCodebase,
@@ -904,11 +920,16 @@ import {
904
920
  verifyParsers,
905
921
  parserEnvironment,
906
922
  migrate,
923
+ mergeArchitectureObservations,
907
924
  projectSessionContext,
908
925
  projectTurnContext,
909
926
  setCollaboration,
910
927
  setEngineeringProfile,
928
+ sourceExplanationGuidance,
929
+ sourceExplanationReviewGuidance,
911
930
  syncSkills,
931
+ validateArchitectureFacts,
932
+ validateArchitectureObservations,
912
933
  verify,
913
934
  withTrustedGitRepository,
914
935
  } from 'genesis-compiler';
@@ -942,6 +963,24 @@ text.
942
963
  the functions already declared there, selected Stack guidance, available Agent
943
964
  Skills, and verification commands. `indexCodebase()` regenerates or returns the
944
965
  detailed Machine City and simple Program City projections.
966
+ Its `observations` option accepts either a validated observation document or an
967
+ async callback receiving the fresh architecture facts. A host can run its finite
968
+ inspector inside that callback, avoiding a second source extraction. Genesis
969
+ joins the result and rechecks source, Stack, subsystem and package inputs before
970
+ writing compact derived JSON; failures and cancellation retain the prior index.
971
+ `ARCHITECTURE_OBSERVATIONS_SCHEMA` identifies the neutral inspector document.
972
+ `validateArchitectureObservations()` checks its evidence against supplied files
973
+ and packages; `connectArchitectureObservations()` joins exact owners and
974
+ controller operations. `mergeArchitectureObservations()` combines distinct
975
+ inspectors while rejecting conflicting facts or package snapshots.
976
+ `validateArchitectureFacts()` validates the combined architecture snapshot.
977
+ These operations neither run a framework inspector nor claim complete runtime
978
+ behavior.
979
+ `sourceExplanationGuidance()` and `sourceExplanationReviewGuidance()` supply
980
+ read-only informational interpretation and review prompts. The consumer owns
981
+ bounded source selection, model execution and cache freshness, including
982
+ `SOURCE_EXPLANATION_PROMPT_VERSION`. These explanations do not become Program
983
+ contracts or coding-agent instructions.
945
984
  `inspectEnvironment()` returns normalized Stack resource declarations,
946
985
  public non-secret defaults, value-free missing-input diagnostics, and dotenv
947
986
  projection paths without returning any supplied environment value.
@@ -1008,6 +1047,9 @@ remain evidence; the map does not define application behavior or database DDL.
1008
1047
 
1009
1048
  Owns reusable form authoring, rendering, and answer validation.
1010
1049
 
1050
+ ### Source
1051
+ - Directory `src/forms`
1052
+
1011
1053
  ### Program
1012
1054
  - `genesis/program/forms/render.md`
1013
1055
 
@@ -1019,12 +1061,17 @@ Owns reusable form authoring, rendering, and answer validation.
1019
1061
  ```
1020
1062
 
1021
1063
  Each subsystem has a unique lowercase hyphenated id, title, responsibility
1022
- prose, and exactly the three shown sections. Each Program module is assigned
1064
+ prose, and Source, Program, Data owned and Data used sections. Source declares
1065
+ exactly one canonical project-relative Directory (`.` is allowed); roots are
1066
+ unique. Nested owners take the files beneath their more specific root. Each Program module is assigned
1023
1067
  exactly once, by its complete project-relative path. Each table has at most one
1024
1068
  owner. Data used names a table owned by another declared subsystem. Empty
1025
1069
  sections explicitly say `- Nothing.`; an empty initialized map contains
1026
1070
  `# Subsystems` followed by `- Nothing.`. Data-only and code-only subsystems are
1027
- valid. No subsystem hierarchy is inferred from paths.
1071
+ valid. The ready map is the canonical owner list for authored and indexed views. Stack
1072
+ directory suggestions never split it. Every indexed source needs an owner.
1073
+ Program Sources may include other subsystems’ libraries; that does not change
1074
+ the operation’s unique declared owner.
1028
1075
 
1029
1076
  Table references contain three distinct values: declared Stack resource **id**,
1030
1077
  schema, and table. `default` selects that resource's default schema; other schema
@@ -1045,7 +1092,7 @@ changes. Opening context stays concise; detailed associations load on demand.
1045
1092
  Adoption, Program refresh, work prompts, session guidance and installed workflow
1046
1093
  skills maintain the map alongside Program in the same turn. Review checks it
1047
1094
  read-only; Blueprint-only work leaves it alone. Ordinary private helper edits
1048
- need no map change. Existing projects do not need a bulk update: missing or empty maps preserve
1049
- Program access and instruct the agent to author the map from source before its
1050
- next implementation task. Until then, operations have no declared subsystem;
1051
- there is no folder-based compatibility reader or automatic ownership inference. Initialization creates an empty map without replacing one.
1095
+ need no map change. Existing maps must declare their source roots. Program modules require a ready
1096
+ map and one owner even when the old map was missing or empty. If the map is missing, empty or invalid, Program inspection reports the
1097
+ required authoring work instead of assigning operations from folder names.
1098
+ Initialization creates an empty map without replacing one.
@@ -5,8 +5,10 @@ Genesis reports only facts it can establish mechanically:
5
5
  1. Blueprint, Engineering approach, Stack, and Program files satisfy their
6
6
  small structural formats, and the selected engineering profile exists in
7
7
  the installed versioned catalog.
8
- 2. Program operations live in conceptual subsystem directories and cite real
9
- source files. Optional helper maps are not mechanically certified.
8
+ 2. Each Program operation has one declared directory-bound subsystem owner and
9
+ cites real source files. It names exactly one main source in that subsystem;
10
+ supporting citations may cross subsystem boundaries. Optional helper maps
11
+ and the quality of the chosen reading anchor are not mechanically certified.
10
12
  3. Stack-declared environment inputs are present and non-placeholder. This does
11
13
  not prove that an external service is reachable; a verification command must
12
14
  establish that when it matters.
package/docs/templates.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Project opening and templates
2
2
 
3
+ The official JSKIT choices are **Basic app**, **Database app**, and **Accounts
4
+ app**. Basic keeps the existing `jskit/public` ID and `public` branch. Database
5
+ uses `jskit/database` and the `database` branch, adding MySQL without login.
6
+ Accounts keeps `jskit/accounts` and the `accounts` branch. These are complete
7
+ starter projects with ready tests; importing source still runs no setup commands.
8
+
3
9
  `genesis init` installs the portable Genesis documents, workflow skills, and
4
10
  agent integration in an ordinary Git repository. It preserves existing source.
5
11
  Initialization is a decision to use Genesis; it is not evidence that an existing
@@ -30,6 +36,11 @@ custom agent tool counts as existing content. Ignored files also count, except
30
36
  installed `node_modules` dependencies. Source recognition does not depend on a
31
37
  language detector or an AI model.
32
38
 
39
+ OpenCode's ignored plugin-install metadata also leaves a blank project eligible:
40
+ its exact generated `.opencode/.gitignore`, plugin-only package manifest and
41
+ matching npm lockfile. These files are read, not deleted or repaired. Tracked or
42
+ customized manifests and other OpenCode files still count as existing content.
43
+
33
44
  Opening a session does not run verification, rebuild the source index, install
34
45
  dependencies, prepare a database, or start the application. The inspection is
35
46
  repeated as a small read when opening/resuming a session and when a host refreshes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.7.8",
3
+ "version": "1.8.0",
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": {
@@ -47,14 +47,16 @@
47
47
  "docs/parsers.md"
48
48
  ],
49
49
  "scripts": {
50
- "test": "node --test test/*.test.js"
50
+ "test": "node --test --test-concurrency=1 test/*.test.js"
51
51
  },
52
52
  "dependencies": {
53
53
  "@ast-grep/napi": "^0.45.1",
54
54
  "mdast-util-from-markdown": "^2.0.3",
55
+ "minimatch": "10.2.6",
55
56
  "yaml": "^2.9.0"
56
57
  },
57
58
  "devDependencies": {
58
- "@ast-grep/lang-python": "0.0.6"
59
+ "@ast-grep/lang-python": "0.0.6",
60
+ "genesis-stack": "file:packages/genesis-stack"
59
61
  }
60
62
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.4.0",
3
+ "version": "1.8.0",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
@@ -15,6 +15,11 @@ result. Identify anything not inspected or genuinely uncertain.
15
15
  Edit only `genesis/blueprint.md`, `genesis/subsystems.md`, and explanatory Markdown below
16
16
  `genesis/program/`. Summarize every explanatory file created, updated, or
17
17
  removed.
18
+ Write useful subsystem responsibility explanations covering purpose, scope,
19
+ important parts and collaboration. Preserve substantive existing prose rather
20
+ than replacing it with short visual captions. Reconcile changed directory owners,
21
+ Program membership and required main-source anchors together; validate the map
22
+ and Program after regeneration. Consumers reuse this canonical list.
18
23
 
19
24
  Maintain `genesis/subsystems.md` in the same implementation turn when a change
20
25
  adds, removes, or changes a subsystem responsibility, Program membership, or
@@ -9,7 +9,15 @@ Delete stale or duplicate modules and do not edit code, tests, configuration,
9
9
  Blueprint, Stack, or `.genesis/`.
10
10
 
11
11
  When finished, summarize Program files created, updated, or removed and report
12
- genuine ambiguity.
12
+ genuine ambiguity. Every module must designate exactly one main source with
13
+ `- Main: \`path/to/source\`` in Sources. Inspect its implementation and choose a
14
+ reading anchor in its owning subsystem; do not guess from source-list order.
15
+ Treat each subsystem responsibility as the full useful explanation, not a
16
+ one-line map caption. Preserve substantive descriptions. If boundaries change,
17
+ reconcile the prose, Program assignments and required main anchors with the new
18
+ directory owners, including nested roots. Consumers read the current canonical
19
+ map; there is no separate viewer-owned list to maintain. Validate both subsystem
20
+ declarations and Program membership before reporting completion.
13
21
 
14
22
  Maintain `genesis/subsystems.md` in the same implementation turn when a change
15
23
  adds, removes, or changes a subsystem responsibility, Program membership, or
@@ -43,13 +43,22 @@ One short explanation of why the boundary exists.
43
43
 
44
44
  ## Sources
45
45
 
46
- - `exact/authored/source/path`
46
+ - Main: `exact/authored/main/source/path`
47
+ - `exact/authored/supporting/source/path`
47
48
 
48
49
  ## Public contract
49
50
 
50
51
  Meaningful inputs, outputs, effects, failures, and guarantees.
51
52
  ```
52
53
 
54
+ Every module requires exactly one `- Main:` source bullet. Choose the main
55
+ source explicitly as the place to start reading the implementation; it need not
56
+ be an executable entry point. It must belong to the module's owning subsystem,
57
+ respecting more specific nested directory owners. Other Sources may cite shared
58
+ libraries in other subsystems. Never infer the main file from source-list order,
59
+ imports or file naming. When reconciling older modules, inspect the source and
60
+ mark the anchor explicitly; indexing does not assign one for you.
61
+
53
62
  An optional `## Implementation map` may name only private helpers or seams that
54
63
  materially help a future agent change, trace, or debug the operation. It is
55
64
  informational, not a public guarantee. Tests are evidence and never
@@ -82,6 +91,9 @@ Use this exact version-zero grammar:
82
91
 
83
92
  Owns reusable form rendering, validation, and submission capture.
84
93
 
94
+ ### Source
95
+ - Directory `packages/forms`
96
+
85
97
  ### Program
86
98
  - `genesis/program/forms/render.md`
87
99
 
@@ -93,10 +105,35 @@ Owns reusable form rendering, validation, and submission capture.
93
105
  ```
94
106
 
95
107
  Use one stable lowercase hyphenated id and a human title per subsystem. Each
96
- entry needs responsibility prose and exactly the three sections shown. Empty
108
+ entry needs responsibility prose and the Program, Data owned and Data used sections.
109
+ Responsibility prose is the subsystem's full explanation, not a map caption.
110
+ Explain its purpose, scope, important parts and collaboration with other owners.
111
+ Preserve substantive existing descriptions; do not replace them with one-line
112
+ labels to suit a visualisation. A consumer can shorten the displayed preview.
113
+ Simple support directories may need only a short explanation.
114
+ The required Source section contains exactly one `- Directory` entry with a backticked path: a
115
+ canonical project-relative directory, with `.` allowed for the project root.
116
+ Every subsystem has one directory root; never declare dispersed or multiple
117
+ roots. Stack parent-directory rules discover default subsystem roots but are not
118
+ exhaustive. An agent may recognise and declare another root elsewhere from
119
+ actual responsibility and source evidence. The authored declaration persists
120
+ through ordinary indexing. The authored map is the canonical owner list once ready; Stack roots are adoption
121
+ hints and never create additional owners or split an authored boundary. Cover every
122
+ indexed source, using a project-root owner for remaining support files if needed. UI components and Engine code may
123
+ share one root; UI projection does not change ownership. Empty
97
124
  sections say `- Nothing.`; a new empty map contains only `# Subsystems` and
98
- `- Nothing.`. Every Program module belongs to exactly one subsystem. No folder
99
- inference, globs, duplicated operations, or duplicated table owners.
125
+ `- Nothing.`. Every Program module belongs to exactly one subsystem. Program directory names do not assign ownership. Sources may cite another
126
+ subsystem’s libraries without giving the operation or implementation two owners.
127
+ Do not use globs, duplicate operations or duplicate table owners.
128
+
129
+ When regenerating or deliberately splitting the map, reconcile the explanations
130
+ with the new directory owners. Transfer the applicable responsibility prose and
131
+ explicit Program assignments together, preserving stable ids where the boundary
132
+ is unchanged. Recheck every Program's required main source against its current
133
+ owner, including nested owners; library references retain their own ownership.
134
+ Use the current declarations rather than an earlier consumer's hardcoded names,
135
+ directory layout or cached list. Inspect subsystems and Program and resolve
136
+ missing/duplicate membership and invalid main anchors before claiming completion.
100
137
 
101
138
  A table identity is three separate exact backticked values: the declared Stack
102
139
  resource id, schema, and table. `default` means the resource's default schema;
@@ -164,8 +164,8 @@ an unrun check passed or that passing checks prove the whole product.
164
164
  Maintain `genesis/subsystems.md` in the same implementation turn when a change
165
165
  adds, removes, or changes a subsystem responsibility, Program membership, or
166
166
  data ownership/use. Read the map before changing related code. Reuse existing
167
- subsystems unless a distinct responsibility is evidenced. Declare each Program
168
- module exactly once; folders do not determine membership. Keep table references
167
+ subsystems unless a distinct responsibility is evidenced. Require one Source Directory per subsystem and declare each Program module
168
+ exactly once; Program folder names do not determine membership. Keep table references
169
169
  grounded in schema/migrations; Genesis does not inspect databases. Private
170
170
  helper changes need no map edit when these associations remain unchanged.
171
171
 
@@ -0,0 +1,167 @@
1
+ import path from 'node:path';
2
+ import { GenesisError } from './errors.js';
3
+ import { sha256, stableJson } from './utils.js';
4
+
5
+ export const ARCHITECTURE_OBSERVATIONS_SCHEMA = 'genesis.architecture-observations.v1';
6
+ const collections = ['pages', 'registrations', 'components', 'componentUses', 'entryOperations', 'operationUses'];
7
+ const optionalCollections = ['imports'];
8
+ const states = ['complete', 'partial', 'unsupported', 'unconfigured'];
9
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
10
+ const text = value => typeof value === 'string' && value.length > 0 && !/[\0\r\n]/u.test(value);
11
+ const canonical = value => text(value) && value !== '.' && !/[\\]/u.test(value) && !path.posix.isAbsolute(value) && path.posix.normalize(value) === value && !value.split('/').includes('..');
12
+ const point = value => object(value) && Number.isSafeInteger(value.line) && value.line > 0 && Number.isSafeInteger(value.column) && value.column > 0;
13
+
14
+ function requireFact(condition, message) {
15
+ if (!condition) throw new GenesisError('ARCHITECTURE_OBSERVATIONS_INVALID', message);
16
+ }
17
+
18
+ // A finite, framework-independent JSON contract. Inspectors collect evidence;
19
+ // this owner validates identities and joins without executing application code.
20
+ export function validateArchitectureObservations(document, { files = [], packages = [] } = {}) {
21
+ requireFact(object(document) && document.schema === ARCHITECTURE_OBSERVATIONS_SCHEMA, `Expected ${ARCHITECTURE_OBSERVATIONS_SCHEMA}.`);
22
+ requireFact(text(document.adapter), 'An adapter identity is required.');
23
+ requireFact(Array.isArray(document.inputs), 'inputs must be an array.');
24
+ requireFact(object(document.capabilities), 'capabilities must be an object.');
25
+ 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.');
27
+ requireFact(Array.isArray(document.diagnostics) && document.diagnostics.every(entry => object(entry) && text(entry.code) && text(entry.message)), 'diagnostics must contain code and message.');
28
+ const filesByPath = new Map(files.map(file => [file.path, file]));
29
+ const inputs = new Map();
30
+ for (const input of document.inputs) {
31
+ requireFact(object(input) && canonical(input.path) && text(input.hash), 'Every input needs a canonical project path and content hash.');
32
+ requireFact(!inputs.has(input.path), `Duplicate input ${input.path}.`);
33
+ const file = filesByPath.get(input.path);
34
+ requireFact(file && file.hash === input.hash, `Input ${input.path} does not match the indexed source. Refresh against the same snapshot.`);
35
+ inputs.set(input.path, input);
36
+ }
37
+ 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.`);
40
+ for (const record of document[name] || []) {
41
+ requireFact(object(record) && text(record.id) && !ids.has(record.id), `Invalid or duplicate ${name} identity.`);
42
+ requireFact(['resolved', 'unresolved', 'ambiguous'].includes(record.resolution), `${record.id} needs an explicit resolution.`);
43
+ requireFact(Array.isArray(record.evidence) && record.evidence.length > 0, `${record.id} needs source evidence.`);
44
+ for (const evidence of record.evidence) {
45
+ requireFact(object(evidence) && canonical(evidence.path) && inputs.get(evidence.path)?.hash === evidence.hash, `${record.id} evidence must reference a declared input hash.`);
46
+ requireFact(point(evidence.start) && point(evidence.end) && (evidence.end.line > evidence.start.line || (evidence.end.line === evidence.start.line && evidence.end.column > evidence.start.column)), `${record.id} has an impossible source range.`);
47
+ const lines = filesByPath.get(evidence.path)?.lines;
48
+ requireFact(!Number.isInteger(lines) || (evidence.start.line <= lines && (evidence.end.line <= lines || (evidence.end.line === lines + 1 && evidence.end.column === 1))), `${record.id} range exceeds its source.`);
49
+ }
50
+ ids.set(record.id, { name, record });
51
+ }
52
+ }
53
+ function reference(value, names, label) {
54
+ requireFact(ids.has(value) && names.includes(ids.get(value).name), `${label} refers to an unknown definition.`);
55
+ }
56
+ function source(value, label) { requireFact(inputs.has(value), `${label} must identify a declared input file.`); }
57
+ for (const use of document.imports || []) {
58
+ source(use.fromFile, use.id);
59
+ requireFact(text(use.specifier) && Array.isArray(use.names) && use.names.every(text) && typeof use.typeOnly === 'boolean', `${use.id} requires imported names, a specifier and type-only classification.`);
60
+ requireFact(use.evidence.some(evidence => evidence.path === use.fromFile), `${use.id} needs evidence in its importing file.`);
61
+ if (use.resolution === 'resolved') source(use.targetFile, use.id);
62
+ }
63
+ function operation(key, label) {
64
+ requireFact(object(key) && text(key.protocol) && text(key.scope) && text(key.operation), `${label} requires protocol, scope and operation identity.`);
65
+ requireFact(Object.values(key).every(text), `${label} fields must be non-empty strings.`);
66
+ if (key.protocol === 'http') requireFact(text(key.method) && key.method === key.method.toUpperCase(), `${label} requires an uppercase HTTP method.`);
67
+ }
68
+ for (const page of document.pages) {
69
+ requireFact(text(page.title) && text(page.surface) && text(page.kind), `${page.id} requires title, surface and page/screen/scene kind.`);
70
+ source(page.file, page.id);
71
+ if (page.parentId != null) reference(page.parentId, ['pages'], page.id);
72
+ }
73
+ const packagesById = new Map(packages.map(entry => [entry.id, entry]));
74
+ let packageIdentity;
75
+ for (const component of document.components) {
76
+ requireFact(text(component.title), `${component.id} requires a title.`);
77
+ if (component.origin?.kind === 'package') {
78
+ requireFact(component.file == null && packagesById.has(component.origin.packageId) && text(component.origin.importPath) && text(component.origin.symbol), `${component.id} requires a known package boundary and public import identity.`);
79
+ packageIdentity ??= sha256(stableJson(packages));
80
+ requireFact(document.packageIdentity === packageIdentity, 'Package-backed UI observations must match the inspected package snapshot.');
81
+ } else source(component.file, component.id);
82
+ }
83
+ for (const registration of document.registrations) {
84
+ reference(registration.pageId, ['pages'], registration.id);
85
+ requireFact(text(registration.surface) && text(registration.identity), `${registration.id} requires scoped registration identity.`);
86
+ }
87
+ for (const use of document.componentUses) {
88
+ reference(use.parentId, ['pages', 'components', 'componentUses'], use.id);
89
+ if (use.resolution === 'resolved') reference(use.componentId, ['components'], use.id);
90
+ }
91
+ for (const entry of document.entryOperations) {
92
+ operation(entry.key, entry.id);
93
+ if (entry.resolution === 'resolved') source(entry.controllerFile, entry.id);
94
+ if (entry.controllerSymbol != null) requireFact(text(entry.controllerSymbol), `${entry.id} has an invalid symbol.`);
95
+ }
96
+ for (const use of document.operationUses) {
97
+ reference(use.parentId, ['pages', 'components', 'componentUses'], use.id);
98
+ if (use.resolution === 'resolved') operation(use.key, use.id);
99
+ }
100
+ // Parent graphs must be finite. Shared definitions are not duplicate owners.
101
+ for (const name of ['pages', 'componentUses']) for (const record of document[name]) {
102
+ const seen = new Set([record.id]);
103
+ let parent = ids.get(record.parentId)?.record;
104
+ while (parent) {
105
+ requireFact(!seen.has(parent.id), `Containment cycle at ${record.id}.`);
106
+ seen.add(parent.id); parent = ids.get(parent.parentId)?.record;
107
+ }
108
+ }
109
+ return document;
110
+ }
111
+
112
+ export function connectArchitectureObservations(document, files, packages = []) {
113
+ validateArchitectureObservations(document, { files, packages });
114
+ const byPath = new Map(files.map(file => [file.path, file]));
115
+ const entries = new Map();
116
+ for (const entry of document.entryOperations.filter(record => record.resolution === 'resolved')) {
117
+ const key = stableJson(entry.key);
118
+ if (!entries.has(key)) entries.set(key, []);
119
+ entries.get(key).push(entry);
120
+ }
121
+ const connections = document.operationUses.map(use => {
122
+ const matches = use.resolution === 'resolved' ? entries.get(stableJson(use.key)) || [] : [];
123
+ 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 || [])] };
125
+ });
126
+ return {
127
+ ...document, connections,
128
+ components: document.components.map(component => ({ ...component, fileId: byPath.get(component.file)?.id || null, subsystemId: component.origin?.kind === 'package' ? packages.find(entry => entry.id === component.origin.packageId).subsystemId : byPath.get(component.file).subsystemId })),
129
+ pages: document.pages.map(page => ({ ...page, fileId: byPath.get(page.file).id, subsystemId: byPath.get(page.file).subsystemId })),
130
+ };
131
+ }
132
+
133
+ export function mergeArchitectureObservations(documents, { files = [], packages = [] } = {}) {
134
+ requireFact(Array.isArray(documents) && documents.length > 0, 'At least one observation document is required.');
135
+ const merged = { schema: ARCHITECTURE_OBSERVATIONS_SCHEMA, adapter: '', inputs: [], capabilities: {}, diagnostics: [], ...Object.fromEntries([...collections, ...optionalCollections].map(name => [name, []])) };
136
+ const adapters = new Set();
137
+ for (const document of documents) {
138
+ requireFact(document?.schema === ARCHITECTURE_OBSERVATIONS_SCHEMA && text(document.adapter) && !adapters.has(document.adapter), 'Every inspector needs a unique adapter identity and the observation schema.');
139
+ adapters.add(document.adapter);
140
+ if (document.packageIdentity != null) {
141
+ requireFact(!merged.packageIdentity || merged.packageIdentity === document.packageIdentity, 'Inspectors disagree about the package snapshot.');
142
+ merged.packageIdentity = document.packageIdentity;
143
+ }
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.`);
146
+ const byId = new Map(merged[name].map(record => [name === 'inputs' ? record.path : record.id, record]));
147
+ for (const record of document[name] || []) {
148
+ requireFact(object(record), `Invalid ${name} record.`);
149
+ const id = name === 'inputs' ? record.path : record.id;
150
+ const previous = byId.get(id);
151
+ requireFact(!previous || stableJson(previous) === stableJson(record), `Conflicting ${name} identity ${id}.`);
152
+ if (!previous) { merged[name].push(record); byId.set(id, record); }
153
+ }
154
+ }
155
+ requireFact(Array.isArray(document.diagnostics), 'diagnostics must be an array.');
156
+ merged.diagnostics.push(...document.diagnostics);
157
+ }
158
+ merged.adapter = [...adapters].sort().join('+');
159
+ for (const capability of ['pages', 'components', 'entryOperations', 'operationUses']) {
160
+ const values = documents.map(document => document.capabilities?.[capability]);
161
+ requireFact(values.every(value => states.includes(value)), `Invalid ${capability} coverage.`);
162
+ merged.capabilities[capability] = new Set(values).size === 1 ? values[0] : 'partial';
163
+ }
164
+ const importCoverage = documents.map(document => document.capabilities?.imports || 'unconfigured');
165
+ merged.capabilities.imports = new Set(importCoverage).size === 1 ? importCoverage[0] : 'partial';
166
+ return validateArchitectureObservations(merged, { files, packages });
167
+ }