@githolon/dsl 0.102.4 → 0.103.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.
Files changed (131) hide show
  1. package/compile_package.mjs +43 -2
  2. package/compile_proof.d.mts +47 -0
  3. package/compile_proof.mjs +343 -0
  4. package/dart/CHANGELOG.md +7 -0
  5. package/dart/lib/src/dev_cloud.dart +8 -8
  6. package/dart/lib/src/nms_generated_runtime.dart +2 -0
  7. package/dart/lib/src/runner/node-entry.mjs +5504 -1236
  8. package/dart/lib/src/runner/offline-install.mjs +4578 -528
  9. package/dart/lib/src/test_harness.dart +206 -152
  10. package/dart/lib/src/webview_bridge.dart +196 -89
  11. package/dart/pubspec.lock +1 -1
  12. package/dart/pubspec.yaml +2 -2
  13. package/dart/test/acquire_session_credentials_test.dart +4 -3
  14. package/dart/test/adv_realm_interference_test.dart +7 -6
  15. package/dart/test/cloud_bound_topology_test.dart +3 -3
  16. package/dart/test/cluster_births_test.dart +4 -3
  17. package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
  18. package/dart/test/cluster_local_autosync_test.dart +3 -2
  19. package/dart/test/cluster_scale_test.dart +3 -2
  20. package/dart/test/dlq_session_surface_test.dart +7 -6
  21. package/dart/test/first_app_gauntlet_test.dart +14 -21
  22. package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
  23. package/dart/test/four_layer_nested_topology_test.dart +4 -4
  24. package/dart/test/harness_phase_timeout_test.dart +2 -2
  25. package/dart/test/local_sync_inert_test.dart +1 -1
  26. package/dart/test/parent_signer_recovery_test.dart +2 -2
  27. package/dart/test/sealing_client_test.dart +4 -3
  28. package/dart/test/session_bound_test.dart +8 -7
  29. package/dart/test/test_harness_test.dart +13 -12
  30. package/dart/test/vo_read_typing_test.dart +4 -3
  31. package/dart/test/watch_after_offer_stall_test.dart +4 -3
  32. package/dart/test/webview_bridge_test.dart +102 -5
  33. package/dart_types/CHANGELOG.md +5 -0
  34. package/dart_types/lib/nomos_types.dart +9 -0
  35. package/dart_types/lib/src/nomos_ref.dart +20 -0
  36. package/dart_types/lib/src/optional_ref.dart +85 -0
  37. package/dart_types/lib/src/sha256_digest.dart +41 -0
  38. package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
  39. package/dart_types/pubspec.yaml +1 -1
  40. package/dart_types/test/optional_ref_test.dart +44 -0
  41. package/dart_types/test/sha256_digest_test.dart +18 -0
  42. package/package.json +9 -5
  43. package/runtime/generated-application-host.js +5365 -1105
  44. package/src/aggregate.ts +18 -0
  45. package/src/application_composition.ts +6 -32
  46. package/src/attested_core.ts +6 -3
  47. package/src/birth_meter.ts +1 -0
  48. package/src/birth_meter_ensure.ts +28 -2
  49. package/src/births.ts +1 -1
  50. package/src/build_package.ts +162 -538
  51. package/src/business_birth.ts +19 -3
  52. package/src/canonical_json.ts +21 -0
  53. package/src/check_package_main.ts +76 -35
  54. package/src/clear_directive.ts +19 -8
  55. package/src/codegen_dart.ts +305 -22
  56. package/src/codegen_dart_application.ts +844 -292
  57. package/src/codegen_dart_client.ts +1084 -407
  58. package/src/codegen_dart_scenario.ts +12 -12
  59. package/src/codegen_proof.ts +169 -39
  60. package/src/codegen_provider_dart.ts +17 -1
  61. package/src/codegen_ts.ts +298 -106
  62. package/src/compat_manifest.ts +2 -2
  63. package/src/compat_package_main.ts +8 -12
  64. package/src/compile_package_main.ts +1866 -597
  65. package/src/compose.ts +1 -1
  66. package/src/directive.ts +208 -15
  67. package/src/engine_entry.ts +165 -111
  68. package/src/evolve_preview.ts +43 -81
  69. package/src/extremum.ts +1 -1
  70. package/src/fields.ts +183 -4
  71. package/src/flow_gate.ts +75 -2
  72. package/src/framework/application_home.ts +2 -0
  73. package/src/framework/auth_provider_law.ts +2 -2
  74. package/src/framework/birth_meter.ts +7 -19
  75. package/src/framework/bootstrap.ts +0 -2
  76. package/src/framework/business_corrections.ts +9 -5
  77. package/src/framework/domain_lifecycle.ts +15 -29
  78. package/src/framework/encryption_scope.ts +2 -1
  79. package/src/framework/framework_aggregates.ts +2 -0
  80. package/src/framework/genesis.ts +16 -14
  81. package/src/framework/home.ts +20 -14
  82. package/src/framework/owned_custody.ts +9 -3
  83. package/src/framework/platform_admin.ts +3 -3
  84. package/src/framework/relations.ts +21 -13
  85. package/src/framework/signing_identity.ts +45 -40
  86. package/src/framework/workspace_invariant.ts +245 -0
  87. package/src/framework/workspaces.ts +23 -29
  88. package/src/index.ts +36 -16
  89. package/src/intent.ts +178 -6
  90. package/src/law_check.ts +32 -11
  91. package/src/law_graph.ts +2902 -0
  92. package/src/law_graph_usda.ts +1799 -0
  93. package/src/law_identity.ts +435 -0
  94. package/src/law_value_type.ts +230 -0
  95. package/src/lsp_server.ts +61 -31
  96. package/src/ops.ts +7 -0
  97. package/src/package_config.ts +141 -0
  98. package/src/package_dependencies.ts +108 -0
  99. package/src/package_dependency_usage.ts +150 -0
  100. package/src/package_source_imports.ts +71 -0
  101. package/src/plan_source_slice.ts +335 -0
  102. package/src/point_guard.ts +192 -0
  103. package/src/precondition.ts +44 -9
  104. package/src/read_projection_namespace.ts +1 -1
  105. package/src/runtime_zod.ts +44 -0
  106. package/src/secondary_ensure.ts +67 -14
  107. package/src/shape_compat.ts +1 -1
  108. package/src/stable_id_identity_bridge.ts +149 -0
  109. package/src/stable_id_lockfile.ts +278 -0
  110. package/src/stable_ids.ts +7 -2
  111. package/src/stable_ids_for_module.ts +348 -0
  112. package/src/stable_ids_types.ts +1 -1
  113. package/src/sum.ts +1 -1
  114. package/src/usd_state.ts +1 -1
  115. package/src/usda_relationship_integrity.ts +66 -0
  116. package/src/usda_text.ts +18 -0
  117. package/src/usdz.ts +78 -0
  118. package/src/value_object.ts +95 -0
  119. package/src/wire.ts +19 -0
  120. package/src/wire_encode.ts +22 -0
  121. package/src/workspace_sharding.ts +52 -36
  122. package/src/workspace_sharing.ts +28 -4
  123. package/src/zod_introspection.ts +1 -1
  124. package/dart/test/cluster_attested_test.dart +0 -201
  125. package/src/compile_engine.ts +0 -595
  126. package/src/manifest.ts +0 -1476
  127. package/src/read_projection_composition.ts +0 -117
  128. package/src/read_projection_manifest.ts +0 -115
  129. package/src/relation_composition.ts +0 -286
  130. package/src/usd.ts +0 -1967
  131. package/src/usd_layers.ts +0 -1776
package/src/usd.ts DELETED
@@ -1,1967 +0,0 @@
1
- // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
- // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
- // wasm32-wasip1 artifact {kernel · projection · embedded
4
- // QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
5
- // wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
6
- // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
-
8
- /**
9
- * TS → USD — the UNIFIED composed-IR document (stage 1, increment 1).
10
- *
11
- * Two proven-but-isolated pieces become ONE here:
12
- * - `manifest.ts` (#136): encodes a `DomainModule` to a canonical flat manifest —
13
- * aggregates with field→Driver schemas + directive contracts (target / marker /
14
- * requiresCapability / payload-shape), hashed via `canonicalJson` / `domainHash`.
15
- * - `compose.ts` (#137): the typed, MONOTONIC layer composition — `requires`/`scope`
16
- * rules, `covers()`, fail-closed `CompositionViolation`, composed hash.
17
- *
18
- * The bridge is an OpenUSD-shaped IR document: a real (synthetic) `DomainModule`
19
- * encodes into composable typed prims arranged under a layer PATH; multiple layers
20
- * COMPOSE via the very same typed rules (`compose.ts`'s `mergeRequiresScope`, reused
21
- * — never re-implemented); the composed stage FLATTENS and HASHES. This is the
22
- * foundation of the certified-IR document the gate will later seal.
23
- *
24
- * SYNTHETIC framework units only — paths like `/Nomos/Prelude` and `/Sample/...`;
25
- * no tenant/business domain is named or imported. Build-time only (imports
26
- * `node:crypto` transitively via `manifest.js`); reachable via the `@githolon/dsl/usd`
27
- * subpath, NOT the runtime `index.ts` barrel.
28
- */
29
- import { createHash } from "node:crypto";
30
- import {
31
- canonicalJson,
32
- domainManifest,
33
- type CanonicalManifest,
34
- type CanonicalProjectionReturn,
35
- } from "./manifest.js";
36
- import {
37
- OWNED_KEY_RELATION,
38
- OWNED_READ_RELATION,
39
- type AggregateHandle,
40
- } from "./aggregate.js";
41
- import {
42
- mergeRequiresScope,
43
- CompositionViolation,
44
- type ComposeOptions,
45
- } from "./compose.js";
46
- import type { WireSchema, WireRefMode } from "./wire.js";
47
- import type { DomainModule } from "./codegen_dart.js";
48
- import { domainInterface } from "./shape_compat.js";
49
- import { relationType, user, key, anyUser, qualifiedTypeId, qualifyRelationType } from "./relation_schema.js";
50
- import { harvestPrincipalFlows } from "./wire_encode.js";
51
- import { zodKind, zodObjectShape } from "./codegen_ts.js";
52
- import { composeSharedRelationTypes } from "./relation_composition.js";
53
- import { collectValueObjectUsages } from "./value_object.js";
54
- import {
55
- namespacedAggregateIds,
56
- qualifyReadProjectionType,
57
- READ_PROJECTION_DELIMITER,
58
- } from "./read_projection_namespace.js";
59
- import { qualifyManifestAggregateLabels } from "./read_projection_manifest.js";
60
- import { guardSharedReadProjectionKeys } from "./read_projection_composition.js";
61
- import { OWNED_RECOVERY_RELATION } from "./framework/owned_custody.js";
62
-
63
- /** The referential marker of a directive prim (kernel `RefMode` wire string). */
64
- export type RefMode = WireRefMode;
65
-
66
- /**
67
- * One prim in the USD IR — a discriminated union UNIFYING #136's manifest content
68
- * with #137's composable typed-rule fields.
69
- *
70
- * - `aggregate`: an encoded aggregate, carrying its field→`WireDriver` schema (the
71
- * EXACT `manifest.ts` `encodeKernelSchema` output). Aggregates do not compose-merge
72
- * (an aggregate is a single-source schema); they pass through by path.
73
- * - `directive`: a composable law unit. `requires` is the capability set (#137:
74
- * add/narrow, never silently remove); `scope` is the optional composable scope
75
- * path (#137: narrow-not-widen). `artifactRef` is the certified-artifact handle
76
- * — left `undefined` here (stage 2).
77
- */
78
- export type UsdPrim =
79
- | {
80
- readonly kind: "aggregate";
81
- /** e.g. `/Sample/SampleThingAggregate`. */
82
- readonly path: string;
83
- /** The aggregate type id, e.g. `SampleThingAggregate`. */
84
- readonly type: string;
85
- /** The encoded field→Driver schema (reused from `manifest.ts`). */
86
- readonly schema: WireSchema;
87
- }
88
- | {
89
- readonly kind: "directive";
90
- /** e.g. `/Sample/createThing`. */
91
- readonly path: string;
92
- /** The target aggregate type id. */
93
- readonly target: string;
94
- readonly marker: RefMode;
95
- /** Capability ids that MUST hold (a stronger layer may only ADD). */
96
- readonly requires: string[];
97
- /**
98
- * The composable scope path (#137 narrow-not-widen). `undefined` when the DSL
99
- * declares NO static scope today — we do NOT fabricate one.
100
- */
101
- readonly scope?: string;
102
- /**
103
- * The directive's DECLARED read boundary (sorted ref types) — the "IR for law
104
- * boundaries" carried into the USD IR so `usdHash` reflects it. OMITTED
105
- * (`undefined`) when the directive declares no reads, so boundary-free prims are
106
- * byte-identical to before this field existed.
107
- */
108
- readonly reads?: string[];
109
- /**
110
- * The directive's DECLARED emit boundary: event type → `{ max? }`. OMITTED
111
- * (`undefined`) when no emits are declared (omit-when-empty hash invariance).
112
- */
113
- readonly emits?: Record<string, { max?: number }>;
114
- /**
115
- * MULTI-OUTPUT: the ADDITIONAL aggregates this directive declares it writes (beyond `target`/`marker`),
116
- * `{ target, marker }` sorted. OMITTED (`undefined`) for a single-output directive (omit-when-empty
117
- * hash invariance). Descriptive law metadata — docs/proof/client name every output.
118
- */
119
- readonly secondaryOutputs?: readonly { target: string; marker: WireRefMode }[];
120
- /**
121
- * Standard JSON Schema for the directive payload, emitted from TS/Zod into the
122
- * IR so cross-language tooling validates against the same schema source.
123
- */
124
- readonly payloadJsonSchema?: unknown;
125
- /** Certified-artifact payload reference — `undefined` for now (stage 2). */
126
- readonly artifactRef?: string;
127
- };
128
-
129
- /** One captured query declaration on a USD layer (read-side closure). `key` is the
130
- * index column order, PRESERVED (not sorted). Mirrors `manifest.ts`'s `CanonicalQuery`. */
131
- export interface UsdQuery {
132
- readonly id: string;
133
- readonly key: string[];
134
- readonly returns: string;
135
- }
136
-
137
- /** One captured derived read projection on a USD layer. */
138
- export interface UsdDerived {
139
- readonly id: string;
140
- readonly of: string;
141
- readonly returns: CanonicalProjectionReturn;
142
- }
143
-
144
- /** One captured combined read projection on a USD layer. */
145
- export interface UsdCombined {
146
- readonly id: string;
147
- readonly of: string;
148
- readonly refField: string;
149
- readonly reads: string;
150
- readonly returns: CanonicalProjectionReturn;
151
- }
152
-
153
- /** One field's read-projection descriptor: the field kind plus a map's value kind. */
154
- export interface UsdFieldKindDescriptor {
155
- readonly kind: string;
156
- readonly mapValueKind?: string;
157
- }
158
-
159
- export interface UsdReadProjection {
160
- readonly aggregateFieldKinds: Record<string, Record<string, UsdFieldKindDescriptor>>;
161
- readonly queries: { id: string; key: { field: string; kind: string }[]; returns: string }[];
162
- readonly counts: { id: string; of: string; by: string | null; where?: unknown }[];
163
- readonly spatials: { id: string; of: string; on: string }[];
164
- readonly deriveds: { id: string; of: string; returns: CanonicalProjectionReturn }[];
165
- readonly combineds: { id: string; of: string; refField: string; reads: string; returns: CanonicalProjectionReturn }[];
166
- readonly sums?: { id: string; of: string; field: string; by: string | null; where?: unknown }[];
167
- readonly mins?: { id: string; of: string; field: string; by: string | null; where?: unknown }[];
168
- readonly maxes?: { id: string; of: string; field: string; by: string | null; where?: unknown }[];
169
- readonly orderedReads?: {
170
- id: string;
171
- of: string;
172
- orderKey: string;
173
- orderDesc?: true;
174
- where?: unknown;
175
- limit: number;
176
- }[];
177
- }
178
-
179
- /** One USD layer: a stage path + its prims (canonically sorted by prim path). */
180
- export interface UsdLayer {
181
- readonly path: string;
182
- readonly prims: UsdPrim[];
183
- /**
184
- * The canonical admission/merge manifest for this layer, committed inside the package.
185
- * Hosts must not stage or mutate this view; holons derive it from active installed law.
186
- */
187
- readonly nomosIdentityManifest?: CanonicalManifest;
188
- /**
189
- * The projection schema/read-routing fragment for this layer, committed inside the package.
190
- * It carries the projection-only field-kind data that cannot be recovered from merge drivers.
191
- */
192
- readonly nomosReadProjection?: UsdReadProjection;
193
- /**
194
- * The layer's NAMED, INDEXED read declarations (read-side closure step 1), SORTED
195
- * by id. OMITTED (`undefined`) when the module declares no query, so a query-free
196
- * layer is byte-identical to before this field existed (omit-when-empty hash
197
- * invariance). Carried straight from the module's canonical manifest.
198
- */
199
- readonly queries?: UsdQuery[];
200
- /** The layer's engine-projected derived read fields, sorted by id. */
201
- readonly deriveds?: UsdDerived[];
202
- /** The layer's engine-projected combined read fields, sorted by id. */
203
- readonly combineds?: UsdCombined[];
204
- /**
205
- * The REFERENCE composition arc (LIVRPS: References sit just below Local). Paths of
206
- * OTHER layers in the SAME document whose effective prims compose into THIS layer
207
- * UNDER (weaker than) its own local prims — local is the stronger opinion. A
208
- * first-class declared arc recorded in the IR so the composed identity (`usdHash`)
209
- * reflects WHY a prim is present. OMITTED (`undefined`) when a layer references
210
- * nothing, so a reference-free layer is byte-identical to before this field existed
211
- * (omit-when-empty hash invariance — same discipline as `queries`/`reads`/`emits`).
212
- */
213
- readonly references?: string[];
214
- /**
215
- * The VARIANT composition arc (LIVRPS: Variants sit ABOVE References, BELOW Local —
216
- * stronger than referenced prims, weaker than this layer's own local prims). A layer
217
- * may declare named variant SETS; each set is a family of named ALTERNATIVES, each
218
- * alternative ("variant") holding its OWN prims. WHICH variant of each set composes is
219
- * NOT baked into the document — it is supplied as `ComposeOptions.variantSelection`
220
- * (setName → chosen variantName) at flatten time, so the composed identity is a
221
- * function of `(doc, selection)`.
222
- *
223
- * Shape: setName → variantName → that variant's prims. A reserved variant name `"*"`
224
- * within a set declares that set's DEFAULT variant: when the selection names no choice
225
- * for the set, the `"*"` variant's prims compose; with neither a selection nor a `"*"`
226
- * default, the set contributes NOTHING. A selection naming an unknown set or an unknown
227
- * variant fails closed (`variant-unresolved`) — an explicit selection that cannot be
228
- * resolved is never silently a no-op.
229
- *
230
- * OMITTED (`undefined`) when a layer declares no variant set, so a variant-free layer
231
- * is byte-identical to before this field existed (omit-when-empty hash invariance —
232
- * same discipline as `queries`/`references`/`reads`/`emits`).
233
- */
234
- readonly variantSets?: Record<string, Record<string, UsdPrim[]>>;
235
- /**
236
- * THE INVARIANT-GATE DECLARATION (#41 — "the law is the era"). Present IFF the layer's
237
- * module declares ≥1 invariant (aggregate or workspace): the law's EXPLICIT opt-in to
238
- * invariant EXECUTION at the one gate. The wasm gate runs the engine-backed invariant
239
- * oracles for an intent only when the law resolved for that intent carries this key —
240
- * every bundle compiled before #41 (whose declared invariants the gate held as hashed-
241
- * but-inert law) lacks it, so chains sealed under the inert gate replay green forever.
242
- * OMITTED (`undefined`) for invariant-free law — byte-identical hash, same discipline
243
- * as `queries`/`references`. Hand-adding it to a foreign bundle only opts IN to
244
- * stricter checking; removing it makes new law, for that law's own chains (status quo).
245
- */
246
- /**
247
- * THE ATTRIBUTED-READS DECLARATION (architecture/field_provenance.md — "the law is the
248
- * era"). Present IFF the layer declares ≥1 `.attributed()` field: the law's EXPLICIT
249
- * opt-in to per-field author-provenance capture at the one gate/fold. The hash-bearing
250
- * enforcement rides the aggregate schema's `{"Attributed": <inner>}` wire driver (a
251
- * pre-provenance kernel's strict manifest serde REFUSES it — the clean era refusal);
252
- * this key is the SELF-DESCRIBING surface (which aggregates/fields are attributed)
253
- * hosts and tooling read without re-deriving it from drivers. OMITTED for
254
- * unattributed law — byte-identical hash, the `nomosInvariantGate` discipline.
255
- */
256
- readonly nomosAttributedGate?: {
257
- readonly v: 1;
258
- /** aggregate TYPE → its attributed STORED field names, keys + fields sorted. */
259
- readonly aggregates: Record<string, string[]>;
260
- };
261
- readonly nomosInvariantGate?: {
262
- readonly v: 1;
263
- /** Aggregate types whose declared `invariant` bodies the gate executes, sorted. */
264
- readonly aggregates?: string[];
265
- /** Declared workspace invariants `{id, on}` the gate executes, sorted by id. */
266
- readonly workspaceInvariants?: { id: string; on: string }[];
267
- };
268
- /**
269
- * THE NATIVE ZANZIBAR RELATION SCHEMA (P1 — the userset-rewrite law as PURE DATA). Present IFF the
270
- * layer declares ≥1 `relationType`: the law's EXPLICIT opt-in to the recursive Check/Expand gate.
271
- * The Rust `RewriteEngine` READS this AST (per-type relation rewrites + fail-closed type restrictions
272
- * + the schema-declared depth/fanout caps); the JS lump never runs it. OMITTED for relation-free law
273
- * (byte-identical hash — the exact `nomosInvariantGate` era discipline; pre-feature chains replay
274
- * green, the relation gate inert). Keys SORTED for byte-determinism (the layered flatten reproduces
275
- * these bytes; see usd_layers.ts). Relation refs are NAMES in P1; the #58 stable-id swap is the
276
- * localized follow-up.
277
- */
278
- readonly nomosRelations?: {
279
- readonly v: 1;
280
- /** typeName → { relations: {relName → {rewrite, allowed}}, caps? } — keys sorted. */
281
- readonly types: Record<
282
- string,
283
- {
284
- readonly relations: Record<string, { readonly rewrite: unknown; readonly allowed: unknown[] }>;
285
- readonly caps?: { readonly maxDepth: number; readonly maxFanout: number };
286
- }
287
- >;
288
- };
289
- /**
290
- * THE NATIVE ZANZIBAR WRITE-AUTHZ GATE (P4). Present IFF ≥1 directive declares `.requiresRelation`:
291
- * the one gate (`executor::admit`) refuses an author who does not HOLD the relation the directive
292
- * requires (`engine.check` over the folded tuple store). `requires` maps directiveId → {relation,
293
- * object}. OMITTED for relation-gate-free law (the era rule — byte-identical hash). Keys sorted.
294
- */
295
- readonly nomosRelationGate?: {
296
- readonly v: 1;
297
- readonly requires: Record<string, { readonly relation: string; readonly object: string }>;
298
- };
299
- /**
300
- * THE RELATION-TYPE NAMESPACE ERA KEY (relation-namespacing). Present IFF a layer qualified ≥1
301
- * (non-global) `relationType` by its owning domain key (`estate` → `estate__workspace`), so two
302
- * domains in one package that each declare the same bare type no longer collide at the kernel's
303
- * last-wins schema merge. `types` is the SORTED set of qualified ids this layer produced; `delimiter`
304
- * is the ratified colon-free `"__"` (the kernel splits object addresses on the first colon). OMITTED
305
- * for a layer that declares no relation type OR only GLOBAL (kernel-coupled framework) types — those
306
- * emit BYTE-IDENTICALLY to the pre-namespacing compiler (the era rule; the marker is a witness, the
307
- * kernel needs no new arm because the qualification is already baked into the `nomosRelations` ids).
308
- */
309
- readonly nomosRelationNamespace?: {
310
- readonly v: 1;
311
- readonly delimiter: string;
312
- readonly types: readonly string[];
313
- };
314
- /**
315
- * THE READ-PROJECTION NAMESPACE ERA KEY (read-projection namespacing — the read-layer mirror of
316
- * `nomosRelationNamespace`). Present IFF this layer qualified ≥1 (non-global) AGGREGATE TYPE by its
317
- * owning domain key (`Entry` → `guestbook__Entry`), so two domains / two installs that each declare
318
- * the same bare aggregate name no longer collide at the kernel's installed-read-manifest accumulator.
319
- * Runtime evolution ignores superseded eras; qualification keeps independent domains distinct.
320
- * `types` is the SORTED set of qualified type
321
- * names this layer produced; `delimiter` is the same ratified colon-free `"__"`. OMITTED for a layer
322
- * whose aggregates are all GLOBAL (the explicit framework allowlist / `{global:true}`) — the marker
323
- * is a witness; the qualification itself is baked into the layer's keys.
324
- */
325
- readonly nomosReadProjectionNamespace?: {
326
- readonly v: 1;
327
- readonly delimiter: string;
328
- readonly types: readonly string[];
329
- };
330
- /**
331
- * THE CROSS-CUSTODY ATTESTATION GATE (home_holons §3/§5). Present IFF ≥1 directive declares
332
- * `.attestationAuthorized()`: the SORTED set of their ids. A directive named here is authorized by a
333
- * carried §3 signed quote rather than an author block — the one gate verifies the quote (bound to the
334
- * home's warrant owner) + pins the captured principal to the proven attestation key, and the signed-author
335
- * doors exempt it. OMITTED for law with no such directive (the era rule — byte-identical hash).
336
- */
337
- readonly nomosAttestationGate?: {
338
- readonly v: 1;
339
- readonly directives: Record<
340
- string,
341
- {
342
- readonly ownerObject: string;
343
- readonly ownerField: string;
344
- readonly fields: {
345
- readonly asserterWorkspace: string;
346
- readonly object: string;
347
- readonly relation: string;
348
- readonly attestedAt: string;
349
- readonly publicKey: string;
350
- readonly signature: string;
351
- };
352
- }
353
- >;
354
- };
355
- /**
356
- * THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md). Present IFF ≥1 directive declares
357
- * `.delegatedRole()`: directiveId → {relation, subjectField, certField, parentKeyObject, parentKeyField}.
358
- * The one gate folds the delegated tuple ONLY behind a DelegationCert P signed (rooting at K_root, binding
359
- * the same relation+subject, `.delegatedFrom(parent)`). OMITTED for law with no such directive (era rule).
360
- */
361
- readonly nomosDelegationGate?: {
362
- readonly v: 1;
363
- readonly directives: Record<
364
- string,
365
- {
366
- readonly relation: string;
367
- readonly subjectField: string;
368
- readonly certField: string;
369
- readonly parentKeyObject: string;
370
- readonly parentKeyField: string;
371
- }
372
- >;
373
- };
374
- /**
375
- * THE KEYLESS VERIFIED-FIRST-DEVICE GATE (architecture/keyless_verified_onboard.md). Present IFF ≥1
376
- * directive declares `.verifiedFirstDevice()`: a per-directive payload FIELD MAPPING. A principal's FIRST
377
- * device-signer may then bootstrap on a WARRANTED parent from an ON-CHAIN, device-bound IdP attestation:
378
- * the kernel's `nomosVerifiedFirstEnroll` arm reads THIS declaration — the directive id + the payload
379
- * fields the token / seated keyHash / claimed issuer ride in — and re-verifies the carried `idpToken`
380
- * against the workspace's AuthProvider IDENTICALLY on author/receive/verify_chain (self-serve +
381
- * peer-to-peer, no admin/server). The KERNEL hardcodes NEITHER the directive id NOR the field names — it
382
- * reads both here (the same way it reads `nomosAttestationGate`). OMITTED for law with no such directive
383
- * (the era rule — byte-identical hash; the arm inert).
384
- */
385
- readonly nomosVerifiedFirstEnroll?: {
386
- readonly v: 1;
387
- readonly directives: Record<
388
- string,
389
- {
390
- readonly tokenField: string;
391
- readonly keyHashField: string;
392
- readonly verifiedViaField: string;
393
- }
394
- >;
395
- };
396
- /**
397
- * THE LAWFUL-RECOVERY GATE (architecture/lawful_recovery.md). Present IFF a directive declares
398
- * `.certifiesEra()`: names that directive id + the payload field carrying the signed `EraCertification`.
399
- * The kernel's `nomosEraCertGate` arm admits the custodian-authored intent (verified to `K_root` via the
400
- * certification, not the signer roster) + the walk waives the identity attestation for the certified oids.
401
- * OMITTED for law with no certifyEra directive (the era rule — byte-identical hash; the arm inert).
402
- */
403
- readonly nomosEraCertGate?: {
404
- readonly v: 1;
405
- readonly directiveId: string;
406
- readonly certField: string;
407
- };
408
- /**
409
- * THE BIRTH GATE DECLARATION (G3 — birth is a declared kernel primitive, the unix model). Present IFF ≥1
410
- * directive declares `.births()`: the SORTED set of their ids. A directive named here BIRTHS a child
411
- * workspace as the defined CONSEQUENCE of its offer — after the offer admits, the kernel reads THIS
412
- * declaration (the SAME way it reads `nomosReadGate`/`nomosInvariantGate` — never a domain aggregate),
413
- * takes the genesis recipe the intent carried (its `captured_ports.births`), spawns the child's custody,
414
- * and self-validates the child's whole genesis through the child's OWN gate (the `materialize_genesis`
415
- * keystone). The kernel reads NO domain schema; it reads its own manifest declaration + the carried
416
- * recipe. OMITTED for law with no birth directive (the era rule — byte-identical hash; every pre-feature
417
- * chain replays green, the birth effect inert). SORTED keys for byte-determinism.
418
- */
419
- readonly nomosBirthGate?: {
420
- readonly v: 1;
421
- /** The directive ids declared `.births()` — SORTED. */
422
- readonly directives: string[];
423
- };
424
- /**
425
- * THE BIRTH-RECORD GATE (metered birth, slice A1 — `architecture/metered_birth_and_attested_reads.md`).
426
- * Present IFF the layer declares ≥1 `.births()` directive AND was compiled under the metered-birth
427
- * compiler: the law's EXPLICIT declaration that every birth it performs ALSO writes a structural
428
- * `NomosBirthRecord` event (deterministic id `<prefix><child>`) in the SAME intent as the birth
429
- * effect — the `.births()` lane appends it automatically, so the declaration is self-fulfilling for
430
- * honestly-compiled law and the kernel arm only ever bites a forged/hand-built lump. The kernel's
431
- * `verify_births_witness` reads THIS declaration (the SAME way it reads `nomosBirthGate` — the
432
- * bundle declares, the kernel honors; NO kernel version-if) and refuses a new-era birth whose
433
- * re-derived events carry no record (`birth-record-missing`). OMITTED for pre-feature law — the
434
- * era rule: every chain sealed before this key replays byte-identical, births exactly as today.
435
- */
436
- readonly nomosBirthRecordGate?: {
437
- readonly v: 1;
438
- /** The record aggregate TYPE name (the framework's `NomosBirthRecord`). */
439
- readonly aggregate: string;
440
- /** The deterministic instance-id prefix — the record for child `w` lives at `<prefix><w>`. */
441
- readonly prefix: string;
442
- };
443
- /**
444
- * THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain). Present IFF the law declares
445
- * `requireParentAttestedBirth`: a warranted workspace running this law MUST travel with a SIGNED cert
446
- * chain proving its founder was VERIFIED by its parent at birth, rooting at the pinned `K_root`. The
447
- * kernel's verify doors (author / receive / verify_chain) verify the travelling chain OFFLINE — no
448
- * cross-ws read. OMITTED for law that doesn't declare it (the era rule — byte-identical hash; every
449
- * pre-feature + legacy/non-warranted chain replays green, the check inert).
450
- */
451
- readonly nomosParentAttestedBirth?: {
452
- readonly v: 1;
453
- /** The LAW-DECLARED owner + cert locators (the kernel reads them; names neither). The framework `home`
454
- * convention; a pre-declaration `{v:1}` key falls back to the kernel default (the era rule). */
455
- readonly ownerObject: string;
456
- readonly ownerField: string;
457
- readonly certObject: string;
458
- readonly certField: string;
459
- };
460
- /**
461
- * STABLE IDENTIFIERS (#58 — names are labels, identity is minted). The layer's
462
- * domain/aggregate/field sids + label lineages, carried verbatim from the canonical
463
- * manifest's `stableIds`. ALWAYS present on post-#58-compiled law (the one
464
- * deliberate hash-mover); ABSENT on every bundle compiled before — the era key the
465
- * EVOLVE GATE and the projection's sid-lineage fold switch on. A law-upgrade deploy
466
- * whose old AND new packages both carry this key is DIFFED BY SID at the one gate.
467
- */
468
- readonly nomosStableIds?: import("./stable_ids_types.js").StableIds;
469
- /**
470
- * THE STRUCTURAL INTERFACE (#72) — this domain's directives (payload shapes) + aggregates
471
- * (read shapes, sid-keyed). ON-LEDGER (it rides the package, which installs as the
472
- * `policy:{hash}.bundle`), so a client reads the workspace's OFFER straight from the folded
473
- * law — reactively, host-blind (architecture/shape_compatibility.md). A client negotiates
474
- * shape-compatibility against THIS, not the package hash. Pre-#72 bundles omit it; a client
475
- * then reports `offerKnown:false` and never refuses on its absence (era-safe, graceful).
476
- */
477
- readonly nomosInterface?: import("./shape_compat.js").DomainInterface;
478
- /**
479
- * THE CAPTURED-READ GATE DECLARATION (#58). Present IFF ≥1 directive of this layer
480
- * declares a captured-read query (`.reads(someQuery)`): the law's EXPLICIT opt-in
481
- * to `nomos.read` at the one gate — the engine serves declared reads LIVE from the
482
- * pre-apply state at author, replays the captured footprint at verify, and the gate
483
- * RE-DERIVES each captured read at the pre-apply position (typed read-conflict on
484
- * drift — CAS as law). OMITTED for read-free law (byte-identical hash — the exact
485
- * `nomosInvariantGate` era discipline; bundles without the key behave as before).
486
- */
487
- readonly nomosReadGate?: {
488
- readonly v: 1;
489
- /** Every declared read query's recipe, keyed by query id. */
490
- readonly queries: Record<string, { key: string[]; returns: string }>;
491
- /** directive id → the SORTED query ids its plan may read. */
492
- readonly directives: Record<string, string[]>;
493
- };
494
- /**
495
- * THE ATTESTED-READ GATE DECLARATION (slice B — attested cross-workspace reads,
496
- * `architecture/attested_reads_design.md` §3.2). Present IFF ≥1 directive declares a
497
- * foreign read (`.readsFrom(ws, query)` + a module-level `trustSource(ws, …)`): the
498
- * reader's law names, per trusted source, the source's IDENTITY (genesis), the accepted
499
- * attestor keyHashes (K1 pins), and its freshness/dishonor vocabulary; and per directive,
500
- * the `(queryId, from)` edges its plan may consume. The QUERY SPEC is NOT emitted here —
501
- * the query is the SOURCE's law; the reader declares only the trust edge + the id.
502
- * OMITTED for foreign-read-free law (byte-identical hash — the exact `nomosReadGate` era
503
- * discipline). The `roster` mode names the §1.3 layer (`pinned`=K1, `derived`=K2,
504
- * `tracked`=K3) — one era key from stage 1; later stages ADD modes, never re-shape.
505
- */
506
- readonly nomosAttestedReadGate?: {
507
- readonly v: 1;
508
- readonly sources: Record<
509
- string,
510
- {
511
- genesis: string;
512
- keyHashes?: string[];
513
- maxAgeMs?: number;
514
- minSourceEpoch?: "birth" | number;
515
- monotone?: boolean;
516
- roster: "pinned" | "derived" | "tracked";
517
- onDishonor: "refuse" | "record";
518
- }
519
- >;
520
- /**
521
- * DYNAMIC-SOURCE trust POLICIES (dynamic_source_attested_reads.md): the acceptable-source SET is
522
- * law-state — a reader-side allow-list aggregate. A directive edge may route through a policy
523
- * (`{queryId, policy}`) instead of a fixed workspace; the gate resolves the envelope's genesis
524
- * against the folded `allowList` rows (genesis field) → the row's `keysField` accepted keys.
525
- * OMITTED when no policy is declared (byte-identical to a policy-free bundle — the era rule).
526
- */
527
- readonly policies?: Record<
528
- string,
529
- {
530
- allowList: string;
531
- genesisField: string;
532
- keysField: string;
533
- maxAgeMs?: number;
534
- minSourceEpoch?: "birth" | number;
535
- monotone?: boolean;
536
- onDishonor?: "refuse" | "record";
537
- }
538
- >;
539
- readonly directives: Record<
540
- string,
541
- ({ queryId: string; from: string } | { queryId: string; policy: string })[]
542
- >;
543
- };
544
- /**
545
- * THE READ-VISIBILITY GATE DECLARATION (the native-relations read gate).
546
- * Present IFF ≥1 aggregate of this layer is declared PRIVATE (`visibility:
547
- * requires(cap)`): the law's EXPLICIT opt-in to the githolon gating its own
548
- * reads. The one wasm read boundary serves a read touching a private type ONLY to
549
- * a principal that holds the read RELATION `cap` on the workspace-scope object —
550
- * decided by the native `RewriteEngine.check` over this workspace's own folded
551
- * relation tuples (the compiler ALSO synthesizes a `workspace#<cap>` relation into
552
- * `nomosRelations`). OMITTED for all-public law (byte-identical hash — the EXACT
553
- * `nomosReadGate`/`nomosInvariantGate` era discipline; every pre-feature chain
554
- * serves reads exactly as before, and a public type is never gated). Keyed by
555
- * aggregate TYPE name → required capability (= read relation).
556
- */
557
- readonly nomosReadVisibility?: {
558
- readonly v: 1;
559
- /** Aggregate TYPE name → the capability a read touching it requires. SORTED by type name. */
560
- readonly aggregates: Record<string, { requires: string; ownerField?: string }>;
561
- };
562
- /**
563
- * THE ENCRYPTION GATE DECLARATION (e2e_encryption.md slice 2). Present IFF ≥1 field of
564
- * this layer is `.encrypted()`: the law's EXPLICIT opt-in to at-rest field ciphertext.
565
- * The gate enforces the envelope structure + `scope == the aggregate's read cap` +
566
- * `epoch ≤ active` on every encrypted-field write (key-free, every peer); a key-holder
567
- * decrypts at read time. OMITTED for plaintext law (byte-identical hash — the EXACT
568
- * `nomosReadVisibility` era discipline; every pre-encryption chain replays as before).
569
- * `scope` is the aggregate's `requires(cap)`; `delegatable:false` — an encrypted scope
570
- * is NON-DELEGATABLE (the adversarial-review fix: a delegated read cap must NOT mint a
571
- * key), so the key roster is direct grants only. Keyed by aggregate TYPE, SORTED.
572
- */
573
- readonly nomosEncryption?: {
574
- readonly v: 1;
575
- readonly aggregates: Record<string, { scope: string; fields: string[]; delegatable: false; owned?: true; ownerField?: string }>;
576
- };
577
- /**
578
- * THE CAPABILITY DECLARATION IN LAW (Full-A — capability_marketplace.md §4): the impure-
579
- * capability quartets (obligation/complete/fail/block/deadLetter) lowered into the SEALED
580
- * law, so the holon SURFACES them on the `declared_capabilities` read and the host relays a
581
- * deploy-time "declared — bind it or obligations wait" notice without reaching into root
582
- * law-state (boundness stays P6 — it is cross-holon root state). OMITTED for capability-free
583
- * law (byte-identical hash — the era rule); declaring a capability MOVES the domainHash
584
- * (named, expected). SORTED by aggregateId.
585
- */
586
- readonly nomosCapabilities?: {
587
- readonly v: 1;
588
- readonly capabilities: readonly {
589
- readonly capability: string;
590
- readonly aggregateId: string;
591
- readonly obligation: string;
592
- readonly complete: string;
593
- readonly fail: string;
594
- readonly block: string;
595
- readonly deadLetter: string;
596
- readonly tasksQueryId: string | null;
597
- /** The HTTPS endpoint the obligation is offered to (outbox); absent ⇒ no destination. */
598
- readonly endpoint?: string;
599
- }[];
600
- };
601
- /**
602
- * THE FLOW GATE WAIVERS (architecture/security_model.md) — both halves of the audit surface,
603
- * hashed into the law + replay-verified. `declassify` (confidentiality, rule 4): every acknowledged
604
- * secret-correlated refusal. `endorse` (the integrity dual): every harvested PrincipalEvidence flow
605
- * into an authority/standing/attribution sink — discovery IS law, witnessed fail-closed at runtime.
606
- * Each half is OMITTED when empty (the era rule — a flow-free domain is byte-identical).
607
- */
608
- readonly nomosFlowGate?: {
609
- readonly v: 1;
610
- readonly declassify?: readonly { readonly site: string; readonly reason: string }[];
611
- readonly endorse?: readonly {
612
- readonly directiveId: string;
613
- readonly sink: string;
614
- readonly facet: string;
615
- readonly reason: string | null;
616
- }[];
617
- };
618
- /**
619
- * FIRST-CLASS VALUE OBJECTS (`value_object.ts`) — the canonical descriptors of every
620
- * named value object this layer's aggregates carry (deduped, SORTED by name, nested
621
- * VOs included), so tooling/visualizers SEE the structure a VO field stores. PURELY
622
- * DESCRIPTIVE: the kernel reads no gate off it (a VO field folds exactly like a
623
- * `t.jsonObject()` leaf — one wholesale-replace json value). OMITTED for VO-free law
624
- * (byte-identical hash — the era rule); declaring a VO moves the PACKAGE hash only
625
- * (the canonical manifest/domainHash is untouched — the VO is not wire law).
626
- */
627
- /**
628
- * MULTI-WORKSPACE LOCATORS (`t.foreignRef`) — the self-describing cross-workspace edges this domain
629
- * declares: `{aggregate, field, workspaceField, target, targetDomain?}` sorted by (aggregate, field).
630
- * Descriptive law metadata (codegen emits a typed `ForeignRef` locator + resolve/watch; proof knows the
631
- * edge shape without proving the concrete foreign workspace). A LOCATOR, not an atomic FK — law that
632
- * depends on the foreign fact opts into attested evidence separately (`nomosAttestedReadGate`). OMITTED
633
- * when the domain declares no foreignRef (era-safe — hash-identical to before this key existed).
634
- */
635
- readonly nomosForeignRefs?: {
636
- readonly v: 1;
637
- readonly refs: readonly {
638
- readonly aggregate: string;
639
- readonly field: string;
640
- readonly workspaceField: string;
641
- readonly target: string;
642
- readonly targetDomain?: string;
643
- }[];
644
- };
645
- readonly nomosValueObjects?: {
646
- readonly v: 1;
647
- readonly valueObjects: readonly import("./fields.js").ValueObjectDescriptor[];
648
- /**
649
- * THE VO↔FIELD ATTACHMENT (additive, hash-inert): which aggregate FIELD carries
650
- * which VO — `{aggregate, field, vo}` sorted by aggregate then field. Rides THIS
651
- * key (never the aggregate prim's wire schema) so the canonical manifest/domainHash
652
- * of a VO field stays byte-equal to a plain `t.jsonObject()` field. Optional for
653
- * era tolerance: a 0.33.0-compiled package's key parses unchanged without it.
654
- */
655
- readonly usages?: readonly import("./value_object.js").ValueObjectUsage[];
656
- };
657
- /**
658
- * DOMAIN CONTRACTS (`architecture/domain_contracts.md`) — the public DTO shapes the domain declares.
659
- * ADDITIVE + hash-inert (like `nomosValueObjects`): omit-when-empty, so a contract-free domain is
660
- * byte-identical. Each contract carries its public field shapes (name/kind, VO name, ref target) and
661
- * the concrete aggregate ids that `.implements(...)` it. Drives the shared `contracts.dart` + TS interfaces.
662
- */
663
- readonly nomosContracts?: {
664
- readonly v: 1;
665
- readonly contracts: readonly {
666
- readonly id: string;
667
- readonly fields: readonly { readonly name: string; readonly kind: string; readonly vo?: string; readonly refTarget?: string }[];
668
- readonly implementedBy: readonly string[];
669
- }[];
670
- };
671
- }
672
-
673
- /** The `nomosContracts` descriptor type — a domain's public DTO shapes (era-safe, hash-inert). */
674
- export type NomosContracts = NonNullable<UsdLayer["nomosContracts"]>;
675
-
676
- /**
677
- * Build the additive `nomosContracts` descriptor for a module: each declared contract's public field
678
- * shapes + the concrete aggregate ids that `.implements(...)` it. Returns undefined when the module
679
- * declares no contract (omit-when-empty — a contract-free domain is byte-identical). SHARED by the
680
- * whole-doc emit (`usd.ts`) and the layered emit (`usd_layers.ts`) so both produce identical bytes.
681
- */
682
- export function buildNomosContracts(module: {
683
- readonly aggregates: readonly { id: string; implementsContracts?: readonly string[] }[];
684
- readonly contracts?: readonly import("./contract.js").ContractHandle[];
685
- }): NomosContracts | undefined {
686
- const declared = module.contracts;
687
- if (declared === undefined || declared.length === 0) return undefined;
688
- // implementedBy: aggregate ids per contract id (from `.implements(...)`), sorted for determinism.
689
- const implByContract = new Map<string, string[]>();
690
- for (const agg of module.aggregates) {
691
- for (const cid of agg.implementsContracts ?? []) {
692
- const l = implByContract.get(cid) ?? [];
693
- l.push(agg.id);
694
- implByContract.set(cid, l);
695
- }
696
- }
697
- const contracts = [...declared]
698
- .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
699
- .map((c) => ({
700
- id: c.id,
701
- fields: Object.entries(c.fields).map(([name, f]) => {
702
- const field = f as import("./fields.js").Field;
703
- const vo = field.valueObject?.name;
704
- const refTarget = field.foreignRef?.targetAggregateId ?? field.refAggregateId;
705
- return {
706
- name,
707
- kind: field.kind,
708
- ...(vo !== undefined ? { vo } : {}),
709
- ...(refTarget !== undefined ? { refTarget } : {}),
710
- };
711
- }),
712
- implementedBy: (implByContract.get(c.id) ?? []).slice().sort(),
713
- }));
714
- return { v: 1 as const, contracts };
715
- }
716
-
717
- /** The reserved variant name declaring a set's DEFAULT (USD has a default variant). */
718
- const DEFAULT_VARIANT = "*";
719
-
720
- /** The unified USD IR document: ordered (strength) layers. */
721
- export interface UsdDocument {
722
- readonly layers: UsdLayer[];
723
- }
724
-
725
- /** The payload field kinds of a directive — `"string-required"` for a required `z.string()`, else the
726
- * zod kind (`"optional"`, `"number"`, …). Used to validate a `fromPayload` routing field. Empty when
727
- * the payload isn't a plain object (a shape the check can't reason about — the runtime gate arbitrates). */
728
- function payloadFieldKinds(dir: { payloadSchema?: unknown } | undefined): Map<string, string> {
729
- const m = new Map<string, string>();
730
- const schema = dir?.payloadSchema as Parameters<typeof zodKind>[0] | undefined;
731
- if (schema === undefined) return m;
732
- let shape: Record<string, Parameters<typeof zodKind>[0]>;
733
- try {
734
- if (zodKind(schema) !== "object") return m;
735
- shape = zodObjectShape(schema);
736
- } catch {
737
- return m;
738
- }
739
- for (const [name, f] of Object.entries(shape)) {
740
- const k = zodKind(f);
741
- m.set(name, k === "string" ? "string-required" : k);
742
- }
743
- return m;
744
- }
745
-
746
- /** Sort prims by their `path` (canonical structure). */
747
- function sortPrims(prims: UsdPrim[]): UsdPrim[] {
748
- return [...prims].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
749
- }
750
-
751
- /**
752
- * Encode ONE `DomainModule` into the `UsdPrim`s sitting under `layerPath`. Reuses
753
- * `manifest.ts`'s `domainManifest` (and therefore its `encodeKernelSchema` driver encoding)
754
- * — no driver-encoding logic is duplicated here. Aggregate prims carry the exact
755
- * encoded schema; directive prims map `requiresCapability` → `requires: [cap]`, leave
756
- * `scope`/`artifactRef` `undefined` (no static scope or certified artifact today).
757
- */
758
- function encodeModuleToPrims(layerPath: string, manifest: CanonicalManifest): UsdPrim[] {
759
- const prims: UsdPrim[] = [];
760
-
761
- for (const agg of manifest.aggregates) {
762
- prims.push({
763
- kind: "aggregate",
764
- // The IR prim path carries the (possibly dotted-qualified) type verbatim; the `.usda`
765
- // TEXT lane sanitizes the prim NAME (`usd_layers.ts::primName`) and round-trips the raw
766
- // id via `nomos:id`, so real-USD identifier rules are already satisfied downstream.
767
- path: `${layerPath}/${agg.id}`,
768
- type: agg.id,
769
- schema: agg.schema,
770
- });
771
- }
772
-
773
- for (const d of manifest.directives) {
774
- prims.push({
775
- kind: "directive",
776
- path: `${layerPath}/${d.id}`,
777
- target: d.target,
778
- marker: d.marker,
779
- // The legacy RBAC capability set is GONE (full-native teardown) — native write-authz is the
780
- // `nomosRelationGate` era key (a directive's `.requires(relation)`), read by the one gate, not
781
- // this prim field. Kept empty so the #137 layer-composition shape is unchanged.
782
- requires: [],
783
- // Carry the declared law boundary straight from the canonical manifest, which
784
- // already applies omit-when-empty: `reads`/`emits` are present here ONLY when the
785
- // directive declared them, so a boundary-free prim is byte-identical to before.
786
- ...(d.reads !== undefined ? { reads: d.reads } : {}),
787
- ...(d.emits !== undefined ? { emits: d.emits } : {}),
788
- // MULTI-OUTPUT: the additional aggregates this directive declares it writes (self-describing law).
789
- // Omit-when-empty from the manifest, so a single-output prim is byte-identical to before.
790
- ...(d.secondaryOutputs !== undefined ? { secondaryOutputs: d.secondaryOutputs } : {}),
791
- payloadJsonSchema: d.payloadJsonSchema,
792
- // `scope`/`artifactRef` deliberately omitted (undefined): no static scope in
793
- // the DSL today, payload is the stage-2 certified artifact.
794
- });
795
- }
796
-
797
- return sortPrims(prims);
798
- }
799
-
800
- function aggregateFieldKindsForModule(
801
- mod: DomainModule,
802
- qa: (type: string) => string = (n) => n,
803
- ): Record<string, Record<string, UsdFieldKindDescriptor>> {
804
- const out: Record<string, Record<string, UsdFieldKindDescriptor>> = {};
805
- for (const agg of [...(mod.aggregates as AggregateHandle[])].sort((a, b) =>
806
- a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
807
- )) {
808
- const fields: Record<string, UsdFieldKindDescriptor> = {};
809
- for (const name of Object.keys(agg.fields).sort()) {
810
- const field = agg.fields[name]!;
811
- fields[name] = {
812
- kind: field.kind,
813
- ...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
814
- };
815
- }
816
- out[qa(agg.id)] = fields;
817
- }
818
- return out;
819
- }
820
-
821
- function readKeyFieldKind(
822
- byType: Record<string, Record<string, UsdFieldKindDescriptor>>,
823
- queryId: string,
824
- returns: string,
825
- keyField: string,
826
- ): string {
827
- const schema = byType[returns];
828
- if (schema === undefined) {
829
- throw new Error(
830
- `usd: query '${queryId}' returns '${returns}', which has no field schema in nomosReadProjection`,
831
- );
832
- }
833
- const lookup = keyField.includes(".") ? keyField.split(".")[0]! : keyField;
834
- const descriptor = schema[lookup];
835
- if (descriptor === undefined) {
836
- throw new Error(
837
- `usd: query '${queryId}' is keyed on '${keyField}' (field '${lookup}'), which '${returns}' does not declare`,
838
- );
839
- }
840
- return descriptor.kind;
841
- }
842
-
843
- function readProjectionForModule(
844
- mod: DomainModule,
845
- manifest: CanonicalManifest,
846
- qa: (type: string) => string = (n) => n,
847
- ): UsdReadProjection {
848
- const aggregateFieldKinds = aggregateFieldKindsForModule(mod, qa);
849
- return {
850
- aggregateFieldKinds,
851
- queries: (manifest.queries ?? []).map((q) => ({
852
- id: q.id,
853
- key: q.key.map((field) => ({
854
- field,
855
- kind: readKeyFieldKind(aggregateFieldKinds, q.id, q.returns, field),
856
- })),
857
- returns: q.returns,
858
- })),
859
- counts: (manifest.counts ?? []).map((c) => ({
860
- id: c.id,
861
- of: c.of,
862
- by: c.by ?? null,
863
- ...(c.where !== undefined ? { where: c.where } : {}),
864
- })),
865
- spatials: (manifest.spatials ?? []).map((s) => ({ id: s.id, of: s.of, on: s.on })),
866
- deriveds: (manifest.deriveds ?? []).map((d) => ({ id: d.id, of: d.of, returns: d.returns })),
867
- combineds: (manifest.combineds ?? []).map((c) => ({
868
- id: c.id,
869
- of: c.of,
870
- refField: c.refField,
871
- reads: c.reads,
872
- returns: c.returns,
873
- })),
874
- ...((manifest.sums ?? []).length > 0
875
- ? {
876
- sums: manifest.sums!.map((s) => ({
877
- id: s.id,
878
- of: s.of,
879
- field: s.sumField,
880
- by: s.by ?? null,
881
- ...(s.where !== undefined ? { where: s.where } : {}),
882
- })),
883
- }
884
- : {}),
885
- ...((manifest.mins ?? []).length > 0
886
- ? {
887
- mins: manifest.mins!.map((e) => ({
888
- id: e.id,
889
- of: e.of,
890
- field: e.valueField,
891
- by: e.by ?? null,
892
- ...(e.where !== undefined ? { where: e.where } : {}),
893
- })),
894
- }
895
- : {}),
896
- ...((manifest.maxes ?? []).length > 0
897
- ? {
898
- maxes: manifest.maxes!.map((e) => ({
899
- id: e.id,
900
- of: e.of,
901
- field: e.valueField,
902
- by: e.by ?? null,
903
- ...(e.where !== undefined ? { where: e.where } : {}),
904
- })),
905
- }
906
- : {}),
907
- ...((manifest.orderedReads ?? []).length > 0
908
- ? {
909
- orderedReads: manifest.orderedReads!.map((r) => ({
910
- id: r.id,
911
- of: r.of,
912
- orderKey: r.orderKey,
913
- ...(r.orderDesc ? { orderDesc: true as const } : {}),
914
- ...(r.where !== undefined ? { where: r.where } : {}),
915
- limit: r.limit,
916
- })),
917
- }
918
- : {}),
919
- };
920
- }
921
-
922
- /**
923
- * Lower a layer's `relationType` decls into the canonical `nomosRelations` wire object (P1) —
924
- * keys SORTED for byte-determinism, omit-when-empty (the era rule). Multiple decls for one type
925
- * MERGE their relations (a relation-name collision is an author error); a caps conflict on one
926
- * type is an error. Rewrite-child ORDER is authored (semantic); `allowed` is sorted (it is a set).
927
- */
928
- function serializeRelationSchema(
929
- decls: readonly import("./relation_schema.js").RelationTypeDecl[],
930
- domainKey: string,
931
- ): UsdLayer["nomosRelations"] {
932
- if (decls.length === 0) return undefined;
933
- const namespacedTypes = new Set(decls.filter((decl) => decl.global !== true).map((decl) => decl.type));
934
- type AllowedW = { kind: string; type?: string; ns?: string; relation?: string };
935
- const allowedKey = (a: AllowedW): string =>
936
- a.kind === "direct" ? `direct:${a.type}` : a.kind === "wildcard" ? "wildcard" : `userset:${a.type}#${a.relation}`;
937
- const byType = new Map<string, { relations: Map<string, { rewrite: unknown; allowed: unknown[] }>; caps?: { maxDepth: number; maxFanout: number } }>();
938
- for (const d of decls) {
939
- // RELATION-TYPE NAMESPACING: a tenant type keys under `<domainKey>__<type>` so two domains that each
940
- // declare the same bare type never collide at the kernel's last-wins schema merge; a `global` type
941
- // (kernel-coupled framework type) keys BARE. `computed`/`ttu` targets inside a rewrite are
942
- // type-RELATIVE relation names resolved on the (now-qualified) type — so no rewrite rewrite is needed.
943
- const typeId = qualifiedTypeId(domainKey, d);
944
- let entry = byType.get(typeId);
945
- if (entry === undefined) {
946
- entry = { relations: new Map(), ...(d.caps ? { caps: d.caps } : {}) };
947
- byType.set(typeId, entry);
948
- } else if (d.caps !== undefined) {
949
- if (entry.caps !== undefined && (entry.caps.maxDepth !== d.caps.maxDepth || entry.caps.maxFanout !== d.caps.maxFanout)) {
950
- throw new Error(`relationType("${d.type}"): conflicting caps across declarations`);
951
- }
952
- entry.caps = d.caps;
953
- }
954
- for (const [rel, def] of d.relations) {
955
- if (entry.relations.has(rel)) throw new Error(`relationType("${d.type}"): relation "${rel}" declared in two relationType() blocks`);
956
- // An allowed object/userset subject names another relation type. When that target is local to this
957
- // domain it must carry the SAME qualifier as the schema key and objectOf(...) tuple subject; otherwise
958
- // a perfectly typed parent edge is stored under `<domain>__Parent` while the gate only permits bare
959
- // `Parent`, making tuple-to-userset inheritance silently dead. Framework-global user/key types stay bare.
960
- const allowed = [...def.allowed]
961
- .map((subject) => {
962
- if ((subject.kind === "direct" || subject.kind === "userset") && namespacedTypes.has(subject.type)) {
963
- return { ...subject, type: qualifyRelationType(domainKey, subject.type) };
964
- }
965
- return subject;
966
- })
967
- .sort((x, y) => (allowedKey(x as AllowedW) < allowedKey(y as AllowedW) ? -1 : 1));
968
- entry.relations.set(rel, { rewrite: def.rewrite, allowed });
969
- }
970
- }
971
- const types: Record<string, { relations: Record<string, { rewrite: unknown; allowed: unknown[] }>; caps?: { maxDepth: number; maxFanout: number } }> = {};
972
- for (const type of [...byType.keys()].sort()) {
973
- const entry = byType.get(type)!;
974
- const relations: Record<string, { rewrite: unknown; allowed: unknown[] }> = {};
975
- for (const rel of [...entry.relations.keys()].sort()) relations[rel] = entry.relations.get(rel)!;
976
- types[type] = { relations, ...(entry.caps ? { caps: entry.caps } : {}) };
977
- }
978
- return { v: 1 as const, types };
979
- }
980
-
981
- /**
982
- * Synthesize the NATIVE read relations a layer's private aggregates imply. Each distinct
983
- * `requires(cap)` becomes ONE relation `cap` on the workspace-scope object, allowing the leaf
984
- * subject classes a read principal can be (`user:<uid>`, the `key:<hash>` control-plane subject,
985
- * and the `user:*` public wildcard). The read gate's `RewriteEngine.check(workspace:self, cap,
986
- * Subject::parse(principal))` resolves THIS relation against the tuples a `relateSubject` grant
987
- * folds — so a granted reader is admitted and a non-grantee fail-closes (no tuple). Pure-direct
988
- * `this` rewrite (a flat membership relation — the read-cap is just "who was granted it").
989
- */
990
- function synthesizeReadRelationTypes(
991
- manifest: CanonicalManifest,
992
- ): import("./relation_schema.js").RelationTypeDecl[] {
993
- const caps = new Set<string>();
994
- for (const a of manifest.aggregates) {
995
- if (a.requiresRead !== undefined && a.owned === undefined) caps.add(a.requiresRead);
996
- }
997
- // Pack acquisition happens before per-record filtering/decryption. Any law with owned state therefore
998
- // needs the same obscure capability at workspace grain, and the receiving-group genesis always writes
999
- // that seat so a standalone group law remains self-consistent too. This grants ciphertext transport;
1000
- // each owned record still has its separately synthesized aggregate-grain relation below.
1001
- if (
1002
- manifest.aggregates.some((aggregate) => aggregate.owned !== undefined) ||
1003
- manifest.aggregates.some((aggregate) => aggregate.id === "NomosInternalReceivingGroup7c1f5e")
1004
- ) {
1005
- caps.add(OWNED_READ_RELATION);
1006
- }
1007
- const out: import("./relation_schema.js").RelationTypeDecl[] = [];
1008
- // GLOBAL (un-namespaced): the read gate hardcodes `ObjectRef::new("workspace","self")` in Rust
1009
- // (`wasm_git_holon.rs`), so the synthesized read-visibility `workspace` type MUST stay bare — it is
1010
- // NOT namespaced by the owning domain (site-6 exemption; a Rust change would be needed to qualify it).
1011
- const builder = relationType("workspace").asGlobal();
1012
- // Sorted for byte-determinism (the serializer also sorts, but keep the source stable). PURE
1013
- // DIRECT (`rewrite` omitted ⇒ `null`, not `self()`): a flat membership relation — the read-cap is
1014
- // exactly "who was granted it". (`self()`/`Rewrite::This` inside a rewrite expands to an EMPTY
1015
- // leaf in v1, which would zero the encryption fan-out roster — the direct form is what Expand reads.)
1016
- for (const cap of [...caps].sort()) {
1017
- builder.relation(cap, { allow: [user(), key(), anyUser()] });
1018
- }
1019
- if (caps.size > 0) out.push(builder);
1020
- // `.owned()` is record-grained: its read relation lives on the aggregate object itself, so the
1021
- // existing row filter checks `<qualified-type>:<record-id>#<relation>`. The type remains local and is
1022
- // therefore qualified by the same domain key as the aggregate; only direct user principals may sit it.
1023
- for (const a of [...manifest.aggregates].filter((x) => x.owned !== undefined).sort((x, y) => x.id.localeCompare(y.id))) {
1024
- const owned = a.owned!;
1025
- // `manifest` already carries the read-projection-qualified type (`domain.Note`). Keep that exact
1026
- // spelling global here rather than applying the relation namespace (`domain__Note`): the read gate
1027
- // constructs its per-record object from the projected row type, so schema, tuple and row must agree.
1028
- out.push(relationType(a.id).asGlobal().relation(owned.relation, { allow: [user()] }));
1029
- }
1030
- if (manifest.aggregates.some((x) => x.owned !== undefined)) {
1031
- out.push(
1032
- relationType("nomos-owned-principal")
1033
- .asGlobal()
1034
- .relation(OWNED_KEY_RELATION, { allow: [key()] })
1035
- .relation(OWNED_RECOVERY_RELATION, { allow: [user()] }),
1036
- );
1037
- }
1038
- return out;
1039
- }
1040
-
1041
- /**
1042
- * Encode each `{ path, module }` into a `UsdLayer` of prims under that path, returning
1043
- * the canonical `UsdDocument`: layers SORTED by path, prims SORTED by path.
1044
- */
1045
- export function emitUsd(
1046
- layers: { path: string; module: DomainModule }[],
1047
- ): UsdDocument {
1048
- const out: UsdLayer[] = layers
1049
- .map((l) => {
1050
- // Carry the domain's declared queries straight from the canonical manifest,
1051
- // which already applies omit-when-empty: `queries` is present here ONLY when the
1052
- // module declared them, so a query-free layer is byte-identical to before.
1053
- const bareManifest = domainManifest(l.module);
1054
- // READ-PROJECTION NAMESPACING: every NON-GLOBAL aggregate's type name is qualified by the
1055
- // owning domain key — on the prims, the identity manifest, the stable-ids labels (bare name
1056
- // appended to `was` — the rename lineage), the read projection, and every `of`/`returns`
1057
- // reference — so two domains / two installs that each declare the same bare name never share
1058
- // an installed-read-manifest accumulator key. GLOBAL (framework allowlist / `{global:true}`)
1059
- // aggregates stay bare (the kernel addresses them by bare name). The engine stamps the SAME
1060
- // qualified name at plan runtime (`wire_encode.ts` via the dispatch context), so rows and
1061
- // schema keys agree by construction. `module.domain ?? module.name` — the SAME qualifier key
1062
- // relation namespacing uses (=== `intent.domain` at plan runtime).
1063
- const rpDomainKey = l.module.domain ?? l.module.name;
1064
- const namespacedAggs = namespacedAggregateIds(
1065
- l.module.aggregates as unknown as readonly { id: string; global?: boolean }[],
1066
- );
1067
- const qaType = (n: string): string =>
1068
- namespacedAggs.has(n) ? qualifyReadProjectionType(rpDomainKey, n) : n;
1069
- const manifest = qualifyManifestAggregateLabels(bareManifest, rpDomainKey, namespacedAggs);
1070
- // `.owned()` reserves its deliberately obscure relation spelling. If tenant law nevertheless reaches
1071
- // for it, fail here instead of merging, qualifying or shadowing the declaration: a collision is useful
1072
- // evidence that the tenant is modelling the same concept, but silently capturing it would make their
1073
- // code mean something they did not write.
1074
- const ownedRelations = new Set([OWNED_READ_RELATION, OWNED_KEY_RELATION, OWNED_RECOVERY_RELATION]);
1075
- for (const d of l.module.relationTypes ?? []) {
1076
- for (const relation of d.relations.keys()) {
1077
- if (!ownedRelations.has(relation)) continue;
1078
- throw new Error(
1079
- `relationType(${JSON.stringify(d.type)}): relation ${JSON.stringify(relation)} is reserved by ` +
1080
- `Nomos .owned(). Sorry mate, that concept is already taken by the framework; give the ` +
1081
- `business relation a different name.`,
1082
- );
1083
- }
1084
- }
1085
- // THE READ-PROJECTION NAMESPACE ERA KEY: present iff ≥1 aggregate was qualified (the witness
1086
- // marker — mirrors `nomosRelationNamespace`; the qualification itself is baked into the keys).
1087
- const nomosReadProjectionNamespace =
1088
- namespacedAggs.size > 0
1089
- ? { v: 1 as const, delimiter: READ_PROJECTION_DELIMITER, types: [...namespacedAggs].map(qaType).sort() }
1090
- : undefined;
1091
- // THE INVARIANT-GATE DECLARATION (#41): presence-only, derived from the SAME module
1092
- // the lump's executable bodies come from — sorted, omit-when-empty (hash-stable for
1093
- // invariant-free law; see the `UsdLayer.nomosInvariantGate` banner for the era rule).
1094
- const invariantAggregates = l.module.aggregates
1095
- .filter((a) => a.hasInvariant === true)
1096
- .map((a) => qaType(a.id))
1097
- .sort();
1098
- const wsInvariants = [...(l.module.workspaceInvariants ?? [])]
1099
- .map((w) => ({ id: w.id, on: w.on }))
1100
- .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
1101
- // THE READ-VISIBILITY → NATIVE RELATION lowering (the one-model teardown): a private
1102
- // aggregate's `requires(cap)` IS a native read relation on the workspace-scope object. Each
1103
- // distinct read cap synthesizes a `relationType("workspace").relation(cap, { allow:[user, key,
1104
- // anyUser] })` so the ONE evaluator (`RewriteEngine.check`) resolves it at the read gate — the
1105
- // SAME relation a `relateSubject({ object:"workspace:self", relation:cap, ... })` grant folds
1106
- // into. The `nomosReadVisibility` key (type → cap) is RETAINED as the type-to-relation map; the
1107
- // synthesized schema is what makes that cap a checkable relation. SORTED + omit-when-empty.
1108
- const readRelationTypes = synthesizeReadRelationTypes(manifest);
1109
- // THE OWNING DOMAIN KEY — the relation-namespacing qualifier (the SAME key the openapi routes
1110
- // dispatch by, `module.domain ?? module.name`, so `intent.domain` at plan runtime === the key the
1111
- // schema/gate qualified under; the tuple object a token produces then lines up by construction).
1112
- const domainKey = l.module.domain ?? l.module.name;
1113
- // The NON-GLOBAL (namespaced) relation type names this layer declares — the set whose gate objects
1114
- // must be qualified. A `global` type (governance `workspace`, signer `identity`, read-visibility
1115
- // `workspace`) stays bare on BOTH the schema and the gate side, so it is excluded here.
1116
- const namespacedTypes = new Set(
1117
- [...(l.module.relationTypes ?? []), ...readRelationTypes]
1118
- .filter((d) => d.global !== true)
1119
- .map((d) => d.type),
1120
- );
1121
- // THE AMBIGUITY GUARD (relation-namespacing): a bare `writeTuple({object:"workspace:self", relation:R})`
1122
- // is auto-qualified iff `(type,R)` is a LOCAL non-global authored relation, and left BARE iff it is a
1123
- // GLOBAL / read-visibility cap. If ONE `(type,R)` is BOTH — a non-global authored relation AND a
1124
- // synthesized read-visibility cap (always on type `workspace`) — the bare object is genuinely
1125
- // ambiguous. Refuse at compile with a clear message (the maintainer's rule: never silently pick).
1126
- {
1127
- const readCaps = new Set(readRelationTypes.flatMap((rt) => [...rt.relations.keys()]));
1128
- for (const d of l.module.relationTypes ?? []) {
1129
- if (d.global === true || d.type !== "workspace") continue;
1130
- for (const rel of d.relations.keys()) {
1131
- if (readCaps.has(rel)) {
1132
- throw new Error(
1133
- `relation-namespacing conflict in domain "${domainKey}": relation "workspace#${rel}" is ` +
1134
- `declared BOTH as a local authored relationType("workspace") relation AND as a ` +
1135
- `read-visibility cap (a requires("${rel}") aggregate). A bare writeTuple object for ` +
1136
- `"${rel}" would be ambiguous (qualify vs stay bare). Rename one of them.`,
1137
- );
1138
- }
1139
- }
1140
- }
1141
- }
1142
- // THE RELATION-NAMESPACE ERA KEY (`nomosRelationNamespace`): present IFF this layer qualified ≥1
1143
- // (non-global) relation type — the marker that this bundle's relation ids are domain-qualified.
1144
- // Absent when a layer declares no relation type OR only global ones ⇒ byte-identical to the
1145
- // pre-namespacing compiler (the `nomosRelations`/`nomosInvariantGate` era discipline).
1146
- const qualifiedTypeIds = [...namespacedTypes].map((t) => qualifyRelationType(domainKey, t)).sort();
1147
- const nomosRelationNamespace =
1148
- qualifiedTypeIds.length > 0 ? { v: 1 as const, delimiter: "__", types: qualifiedTypeIds } : undefined;
1149
- // Qualify the TYPE half of a gate object by the owning domain — but ONLY when this layer declares
1150
- // that type as a NON-GLOBAL relation type (so the gate object matches its qualified schema id). A
1151
- // global-type object (`workspace:self` for governance/read-visibility) or a type this layer doesn't
1152
- // declare passes through BARE — byte-identical to the pre-namespacing compiler.
1153
- const qualifyGateObject = (obj: string): string => {
1154
- const colon = obj.indexOf(":");
1155
- if (colon < 0) return obj;
1156
- const ty = obj.slice(0, colon);
1157
- return namespacedTypes.has(ty) ? `${qualifyRelationType(domainKey, ty)}${obj.slice(colon)}` : obj;
1158
- };
1159
- // THE RELATION SCHEMA (P1): per-type rewrites lowered to the nomosRelations AST, UNIONED with the
1160
- // synthesized read relations. Keys are QUALIFIED by the owning domain (namespacing) except global
1161
- // types. SORTED keys (byte-determinism); omit-when-empty (era rule). A read-cap name colliding with
1162
- // an authored `workspace` relation is an author error (caught in the merge).
1163
- const nomosRelations = serializeRelationSchema(
1164
- [...(l.module.relationTypes ?? []), ...readRelationTypes],
1165
- domainKey,
1166
- );
1167
- // THE RELATION GATE (P4): directives declaring `.requiresRelation` → directiveId → {relation,
1168
- // object}. The object's TYPE half is qualified to match the (now-namespaced) schema id. SORTED
1169
- // keys; omit-when-empty (era rule — a relation-gate-free layer is byte-identical).
1170
- const relationGateReq: Record<
1171
- string,
1172
- { relation: string; object: string; objectFrom?: { recordType: string; idField: string } }
1173
- > = {};
1174
- for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
1175
- if (d.requiresRelationGate !== undefined) {
1176
- const objectFrom = d.requiresRelationGate.objectFrom;
1177
- relationGateReq[d.id] = {
1178
- relation: d.requiresRelationGate.relation,
1179
- object: qualifyGateObject(d.requiresRelationGate.object),
1180
- // PER-RECORD object (home_holons §6): carried through to the wasm gate; omitted when absent
1181
- // so a self-relative directive's USD is byte-identical (the era rule). The recordType is the
1182
- // object type of the per-record tuple, so it namespaces exactly like the gate object type.
1183
- ...(objectFrom !== undefined
1184
- ? {
1185
- objectFrom: {
1186
- recordType: namespacedTypes.has(objectFrom.recordType)
1187
- ? qualifyRelationType(domainKey, objectFrom.recordType)
1188
- : objectFrom.recordType,
1189
- idField: objectFrom.idField,
1190
- },
1191
- }
1192
- : {}),
1193
- };
1194
- }
1195
- }
1196
- const nomosRelationGate =
1197
- Object.keys(relationGateReq).length > 0 ? { v: 1 as const, requires: relationGateReq } : undefined;
1198
- // THE CROSS-CUSTODY ATTESTATION GATE (home_holons §3/§5): directives declaring `.attestationAuthorized()`
1199
- // → directiveId → its LAW-DECLARED owner-source {ownerObject, ownerField}. SORTED keys (byte-
1200
- // determinism). Omit-when-empty (era rule). The one gate verifies the carried §3 quote (binding its
1201
- // subject to the named owner) + pins the principal to the attestation key — the kernel hardcodes no id.
1202
- const attestationReq: Record<
1203
- string,
1204
- {
1205
- ownerObject: string;
1206
- ownerField: string;
1207
- fields: { asserterWorkspace: string; object: string; relation: string; attestedAt: string; publicKey: string; signature: string };
1208
- }
1209
- > = {};
1210
- for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
1211
- if (d.attestationAuthorizedGate !== undefined) {
1212
- attestationReq[d.id] = {
1213
- ownerObject: d.attestationAuthorizedGate.ownerObject,
1214
- ownerField: d.attestationAuthorizedGate.ownerField,
1215
- fields: { ...d.attestationAuthorizedGate.fields },
1216
- };
1217
- }
1218
- }
1219
- const nomosAttestationGate =
1220
- Object.keys(attestationReq).length > 0 ? { v: 1 as const, directives: attestationReq } : undefined;
1221
- // THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md): directives declaring `.delegatedRole()`
1222
- // → directiveId → its {relation, subjectField, certField, parentKeyObject, parentKeyField}. SORTED keys
1223
- // (byte-determinism). Omit-when-empty (era rule) — a law with no delegation is byte-identical. The one
1224
- // gate verifies the carried DelegationCert to K_root + binds (relation, subject) + `.delegatedFrom(parent)`.
1225
- const delegationReq: Record<
1226
- string,
1227
- { relation: string; subjectField: string; certField: string; parentKeyObject: string; parentKeyField: string }
1228
- > = {};
1229
- for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
1230
- const g = (d as { delegatedRoleGate?: {
1231
- relation: string; subjectField: string; certField: string; parentKeyObject: string; parentKeyField: string;
1232
- } }).delegatedRoleGate;
1233
- if (g !== undefined) {
1234
- delegationReq[d.id] = {
1235
- relation: g.relation,
1236
- subjectField: g.subjectField,
1237
- certField: g.certField,
1238
- parentKeyObject: g.parentKeyObject,
1239
- parentKeyField: g.parentKeyField,
1240
- };
1241
- }
1242
- }
1243
- const nomosDelegationGate =
1244
- Object.keys(delegationReq).length > 0 ? { v: 1 as const, directives: delegationReq } : undefined;
1245
- // THE ATTESTED-READ GATE (slice B): per-directive `(queryId, from)` edges + the module's
1246
- // trustSource declarations. Emitted iff ≥1 directive declares a foreign read (the era
1247
- // rule). A declared `from` with NO matching trustSource is a NAMED COMPILE ERROR — the
1248
- // kernel would fail it closed anyway; refuse loudly at build instead.
1249
- type ForeignEdgeInternal =
1250
- | { queryId: string; from: string }
1251
- | { queryId: string; policy: string; fromPayload: string };
1252
- type ForeignEdgeEmit = { queryId: string; from: string } | { queryId: string; policy: string };
1253
- const foreignDirectives: Record<string, ForeignEdgeInternal[]> = {};
1254
- for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
1255
- const fr = (d as { declaredForeignReads?: ForeignEdgeInternal[] }).declaredForeignReads;
1256
- if (fr !== undefined && fr.length > 0) foreignDirectives[d.id] = fr.map((e) => ({ ...e }));
1257
- }
1258
- let nomosAttestedReadGate: UsdLayer["nomosAttestedReadGate"];
1259
- if (Object.keys(foreignDirectives).length > 0) {
1260
- const trust = (l.module as { trustSources?: readonly import("./attested.js").TrustSourceDecl[] }).trustSources ?? [];
1261
- const trustByWs = new Map(trust.map((tsrc) => [tsrc.workspace, tsrc]));
1262
- const policies = (l.module as { trustPolicies?: readonly import("./attested.js").TrustPolicyDecl[] }).trustPolicies ?? [];
1263
- const policyById = new Map(policies.map((p) => [p.policyId, p]));
1264
- const aggIds = new Set(l.module.aggregates.map((a) => a.id));
1265
- // A policy-referenced allow-list must be a real aggregate of THIS module (the fold happens in
1266
- // the reader's own state) — else the gate could never resolve it (fail-closed at build).
1267
- for (const p of policies) {
1268
- if (!aggIds.has(p.allowListType)) {
1269
- throw new Error(
1270
- `trustPolicy('${p.policyId}'): its allowList aggregate '${p.allowListType}' is not an ` +
1271
- `aggregate of this domain — the acceptable-source set is the reader's OWN folded state, ` +
1272
- `so the allow-list must be declared (and written by a gated enrolment directive) here.`,
1273
- );
1274
- }
1275
- }
1276
- const sourcesRec: NonNullable<UsdLayer["nomosAttestedReadGate"]>["sources"] = {};
1277
- const policiesRec: NonNullable<NonNullable<UsdLayer["nomosAttestedReadGate"]>["policies"]> = {};
1278
- const emitDirectives: Record<string, ForeignEdgeEmit[]> = {};
1279
- for (const [dirId, edges] of Object.entries(foreignDirectives)) {
1280
- const dir = l.module.directives.find((d) => d.id === dirId);
1281
- const payloadShape = payloadFieldKinds(dir);
1282
- const emitted: ForeignEdgeEmit[] = [];
1283
- for (const e of edges) {
1284
- if ("from" in e) {
1285
- const tsrc = trustByWs.get(e.from);
1286
- if (tsrc === undefined) {
1287
- throw new Error(
1288
- `directive '${dirId}' declares .readsFrom('${e.from}', '${e.queryId}') but the ` +
1289
- `module exports no trustSource('${e.from}', …) — the trust edge must be declared ` +
1290
- `in law (attested_reads_design.md §3.1)`,
1291
- );
1292
- }
1293
- if (sourcesRec[e.from] === undefined) {
1294
- sourcesRec[e.from] = {
1295
- genesis: tsrc.genesis,
1296
- ...(tsrc.keyHashes !== undefined && tsrc.keyHashes.length > 0
1297
- ? { keyHashes: [...tsrc.keyHashes].sort() }
1298
- : {}),
1299
- ...(tsrc.maxAgeMs !== undefined ? { maxAgeMs: tsrc.maxAgeMs } : {}),
1300
- ...(tsrc.minSourceEpoch !== undefined ? { minSourceEpoch: tsrc.minSourceEpoch } : {}),
1301
- ...(tsrc.monotone === true ? { monotone: true } : {}),
1302
- roster: tsrc.roster ?? "pinned",
1303
- onDishonor: tsrc.onDishonor ?? "refuse",
1304
- };
1305
- }
1306
- emitted.push({ queryId: e.queryId, from: e.from });
1307
- } else {
1308
- const pol = policyById.get(e.policy);
1309
- if (pol === undefined) {
1310
- throw new Error(
1311
- `directive '${dirId}' declares .readsFrom(fromPayload('${e.fromPayload}'), '${e.queryId}', ` +
1312
- `{ policy: '${e.policy}' }) but the module exports no trustPolicy('${e.policy}', …) — the ` +
1313
- `policy must be declared in law (dynamic_source_attested_reads.md).`,
1314
- );
1315
- }
1316
- // The routing field must be a REQUIRED string in the directive's payload (the workspace
1317
- // name is chosen at runtime; a missing/optional/non-string field could never route).
1318
- const kind = payloadShape.get(e.fromPayload);
1319
- if (kind === undefined) {
1320
- throw new Error(
1321
- `directive '${dirId}': .readsFrom(fromPayload('${e.fromPayload}'), …) names a payload ` +
1322
- `field that is not declared in the directive's payload — add a required z.string() ` +
1323
- `'${e.fromPayload}' to the payload (it carries the routed source workspace name).`,
1324
- );
1325
- }
1326
- if (kind !== "string-required") {
1327
- throw new Error(
1328
- `directive '${dirId}': the fromPayload routing field '${e.fromPayload}' must be a ` +
1329
- `REQUIRED z.string() in the payload (it names the routed source workspace) — got '${kind}'.`,
1330
- );
1331
- }
1332
- if (policiesRec[e.policy] === undefined) {
1333
- policiesRec[e.policy] = {
1334
- // Qualify the allow-list type by the owning domain iff it is namespaced — the
1335
- // gate scans rows by their stored `__type`, which the engine stamps qualified
1336
- // (`qaType`, the SAME read-projection namespacing rows/schema keys use). A bare
1337
- // (unqualified) allowList would match no folded row → every dynamic read refuses.
1338
- allowList: qaType(pol.allowListType),
1339
- genesisField: pol.genesisField,
1340
- keysField: pol.keysField,
1341
- ...(pol.maxAgeMs !== undefined ? { maxAgeMs: pol.maxAgeMs } : {}),
1342
- ...(pol.minSourceEpoch !== undefined ? { minSourceEpoch: pol.minSourceEpoch } : {}),
1343
- ...(pol.monotone === true ? { monotone: true } : {}),
1344
- ...(pol.onDishonor !== undefined ? { onDishonor: pol.onDishonor } : {}),
1345
- };
1346
- }
1347
- emitted.push({ queryId: e.queryId, policy: e.policy });
1348
- }
1349
- }
1350
- emitDirectives[dirId] = emitted;
1351
- }
1352
- // SORTED-KEY records (byte-determinism — matching the layered flatten).
1353
- const sortedSources: typeof sourcesRec = {};
1354
- for (const k of Object.keys(sourcesRec).sort()) sortedSources[k] = sourcesRec[k]!;
1355
- const sortedPolicies: typeof policiesRec = {};
1356
- for (const k of Object.keys(policiesRec).sort()) sortedPolicies[k] = policiesRec[k]!;
1357
- const sortedDirectives: Record<string, ForeignEdgeEmit[]> = {};
1358
- for (const k of Object.keys(emitDirectives).sort()) sortedDirectives[k] = emitDirectives[k]!;
1359
- nomosAttestedReadGate = {
1360
- v: 1 as const,
1361
- sources: sortedSources,
1362
- ...(Object.keys(sortedPolicies).length > 0 ? { policies: sortedPolicies } : {}),
1363
- directives: sortedDirectives,
1364
- };
1365
- }
1366
- // THE BIRTH GATE (G3 — birth is a declared kernel primitive): the SORTED set of directive ids
1367
- // declaring `.births()`. The kernel reads this off the manifest (never a domain aggregate) to know an
1368
- // admitted directive births, then folds the recipe the intent carried. Omit-when-empty (era rule —
1369
- // a birth-free layer is byte-identical apart from the always-on stableIds key).
1370
- const birthDirectives = [...l.module.directives]
1371
- .filter((d) => (d as { birthsChild?: boolean }).birthsChild === true)
1372
- .map((d) => d.id)
1373
- .sort();
1374
- const nomosBirthGate =
1375
- birthDirectives.length > 0 ? { v: 1 as const, directives: birthDirectives } : undefined;
1376
- // THE BIRTH-RECORD GATE (metered birth, slice A1 — the framework's new era key). Present IFF
1377
- // this layer declares ≥1 `.births()` directive AND was compiled under this compiler: the law's
1378
- // EXPLICIT declaration that every birth it performs ALSO writes a structural NomosBirthRecord
1379
- // in the SAME intent (the `.births()` lane appends it automatically). The kernel's
1380
- // `verify_births_witness` reads THIS key (never a version-if): a carried birth whose re-derived
1381
- // events contain no `<prefix><child>` record event is a typed refusal (`birth-record-missing`).
1382
- // Omit-when-empty (the era rule): every pre-feature bundle lacks the key and births as today.
1383
- const nomosBirthRecordGate =
1384
- birthDirectives.length > 0
1385
- ? { v: 1 as const, aggregate: "NomosBirthRecord", prefix: "nomos-birth-record:" }
1386
- : undefined;
1387
- // THE PEER-SOVEREIGN ROOT-OF-TRUST: the home/child law declared it requires a parent-attested-birth
1388
- // cert chain (the `requireParentAttestedBirth` sentinel). Emit the era key with the LAW-DECLARED owner +
1389
- // cert locators (so the kernel reads them, names neither); a bare marker (no locators) falls back to the
1390
- // framework `home` convention. Omit when absent (a law that doesn't require it is byte-identical).
1391
- const pabSrc = (
1392
- l.module as {
1393
- parentAttestedBirth?: boolean | { ownerObject?: string; ownerField?: string; certObject?: string; certField?: string };
1394
- }
1395
- ).parentAttestedBirth;
1396
- const nomosParentAttestedBirth = pabSrc
1397
- ? {
1398
- v: 1 as const,
1399
- ownerObject: (typeof pabSrc === "object" && pabSrc.ownerObject) || "home-owner:self",
1400
- ownerField: (typeof pabSrc === "object" && pabSrc.ownerField) || "principal",
1401
- certObject: (typeof pabSrc === "object" && pabSrc.certObject) || "birth-attestation:self",
1402
- certField: (typeof pabSrc === "object" && pabSrc.certField) || "certChain",
1403
- }
1404
- : undefined;
1405
- // THE KEYLESS VERIFIED-FIRST-DEVICE GATE (keyless_verified_onboard.md): collect each directive declaring
1406
- // `.verifiedFirstDevice()` → its payload FIELD MAPPING (where the token / seated keyHash / claimed issuer
1407
- // ride). The kernel's nomosVerifiedFirstEnroll arm reads the directive id + field names from THIS
1408
- // declaration (hardcoding neither) and bootstraps a first device-signer from the carried device-bound
1409
- // IdP attestation. Omit-when-empty (the era rule — byte-identical hash; the arm inert).
1410
- const firstEnrollReq: Record<
1411
- string,
1412
- { tokenField: string; keyHashField: string; verifiedViaField: string }
1413
- > = {};
1414
- for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
1415
- if (d.firstDeviceEnrollGate !== undefined) {
1416
- firstEnrollReq[d.id] = {
1417
- tokenField: d.firstDeviceEnrollGate.tokenField,
1418
- keyHashField: d.firstDeviceEnrollGate.keyHashField,
1419
- verifiedViaField: d.firstDeviceEnrollGate.verifiedViaField,
1420
- };
1421
- }
1422
- }
1423
- const nomosVerifiedFirstEnroll =
1424
- Object.keys(firstEnrollReq).length > 0 ? { v: 1 as const, directives: firstEnrollReq } : undefined;
1425
- // LAWFUL RECOVERY: the sole `.certifiesEra()` directive (if any) → the `nomosEraCertGate` era key.
1426
- // (One certifyEra verb per controller law; the last declared wins — the framework declares exactly one.)
1427
- let eraCertGate: UsdLayer["nomosEraCertGate"];
1428
- for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
1429
- if (d.certifiesEraGate !== undefined) {
1430
- eraCertGate = { v: 1 as const, directiveId: d.id, certField: d.certifiesEraGate.certField };
1431
- }
1432
- }
1433
- // THE CAPTURED-READ GATE DECLARATION (#58): collected from the canonical
1434
- // manifest's per-directive `capturedReads` (omit-when-empty — read-free law's
1435
- // USD is byte-identical apart from the always-on stableIds key).
1436
- const rgQueries = new Map<string, { key: string[]; returns: string }>();
1437
- const rgDirectives = new Map<string, string[]>();
1438
- for (const d of manifest.directives) {
1439
- if (d.capturedReads === undefined || d.capturedReads.length === 0) continue;
1440
- rgDirectives.set(d.id, d.capturedReads.map((r) => r.queryId));
1441
- for (const r of d.capturedReads) {
1442
- rgQueries.set(r.queryId, { key: [...r.key], returns: r.returns });
1443
- }
1444
- }
1445
- // SORTED-KEY records (byte-determinism — the layered flatten must reproduce
1446
- // these bytes exactly; see usd_layers.ts).
1447
- const readGateQueries: Record<string, { key: string[]; returns: string }> = {};
1448
- for (const k of [...rgQueries.keys()].sort()) readGateQueries[k] = rgQueries.get(k)!;
1449
- const readGateDirectives: Record<string, string[]> = {};
1450
- for (const k of [...rgDirectives.keys()].sort()) readGateDirectives[k] = rgDirectives.get(k)!;
1451
- // THE READ-VISIBILITY GATE (slice 5.1): every aggregate the manifest carries a
1452
- // `requiresRead` for, keyed by TYPE name → required cap. SORTED keys
1453
- // (byte-determinism); omit-when-empty so an all-public layer is byte-identical.
1454
- const visAggregates: Record<string, { requires: string; ownerField?: string }> = {};
1455
- const encAggregates: Record<string, { scope: string; fields: string[]; delegatable: false; owned?: true; ownerField?: string }> = {};
1456
- // ATTRIBUTED READS (field_provenance.md): aggregate TYPE → its attributed field
1457
- // names — the self-describing nomosAttributedGate era key. Omit-when-empty.
1458
- const attrAggregates: Record<string, string[]> = {};
1459
- for (const a of [...manifest.aggregates].sort((x, y) => (x.id < y.id ? -1 : x.id > y.id ? 1 : 0))) {
1460
- if (a.requiresRead !== undefined) {
1461
- visAggregates[a.id] = {
1462
- requires: a.requiresRead,
1463
- ...(a.owned !== undefined ? { ownerField: a.owned.ownerField } : {}),
1464
- };
1465
- }
1466
- if (a.attributedFields !== undefined && a.attributedFields.length > 0) {
1467
- attrAggregates[a.id] = [...a.attributedFields];
1468
- }
1469
- // THE ENCRYPTION GATE (slice 2): an aggregate with encrypted field(s) — its scope
1470
- // is its read cap (aggregate()/the gate guarantee they coincide). delegatable:false.
1471
- if (a.encryptedFields !== undefined && a.encryptedFields.length > 0 && a.requiresRead !== undefined) {
1472
- encAggregates[a.id] = {
1473
- scope: a.requiresRead,
1474
- fields: [...a.encryptedFields],
1475
- delegatable: false,
1476
- ...(a.owned !== undefined ? { owned: true as const, ownerField: a.owned.ownerField } : {}),
1477
- };
1478
- }
1479
- }
1480
- // THE CAPABILITY DECLARATION IN LAW (Full-A — capability_marketplace.md §4): the
1481
- // impure-capability quartets lowered into the SEALED law, so the holon can SURFACE
1482
- // them on a read (`declared_capabilities`) and the host RELAYS a deploy-time notice
1483
- // ("declares 'x' — bind it or obligations wait") WITHOUT reaching into root law-state.
1484
- // Mirrors the deploy-sidecar `capabilityLowerings` extraction; SORTED by aggregate id;
1485
- // omit-when-empty so capability-free law is byte-identical (the era rule). Declaring a
1486
- // capability now MOVES the domainHash (named, expected — like the stable-ids key).
1487
- const nomosCapabilities = [...((l.module as { impureCapabilities?: readonly unknown[] }).impureCapabilities ?? [])]
1488
- .map((c) => {
1489
- const k = c as {
1490
- aggregate: { id: string };
1491
- obligation: { id: string };
1492
- complete: { id: string };
1493
- fail: { id: string };
1494
- block: { id: string };
1495
- deadLetter: { id: string };
1496
- name?: unknown;
1497
- capability?: unknown;
1498
- endpoint?: unknown;
1499
- tasks?: { id?: unknown };
1500
- };
1501
- const cap = typeof k.name === "string" ? k.name : typeof k.capability === "string" ? k.capability : null;
1502
- if (cap === null) return null; // a binding-free quartet declares nothing (omit-when-absent)
1503
- return {
1504
- capability: cap,
1505
- aggregateId: k.aggregate.id,
1506
- obligation: k.obligation.id,
1507
- complete: k.complete.id,
1508
- fail: k.fail.id,
1509
- block: k.block.id,
1510
- deadLetter: k.deadLetter.id,
1511
- tasksQueryId: typeof k.tasks?.id === "string" ? k.tasks.id : null,
1512
- ...(typeof k.endpoint === "string" ? { endpoint: k.endpoint } : {}),
1513
- };
1514
- })
1515
- .filter((c): c is NonNullable<typeof c> => c !== null)
1516
- .sort((a, b) => (a.aggregateId < b.aggregateId ? -1 : a.aggregateId > b.aggregateId ? 1 : 0));
1517
- // THE DECLASSIFY SITES (nomosFlowGate rule 4 — security_model.md): every acknowledged
1518
- // intended secret-correlated refusal, with its human reason. Hashed into the law (it IS
1519
- // a law-declared waiver) + replay-verified — "the set of declassify sites is the entire
1520
- // audit surface". Omit-when-empty: a domain with no declassify is byte-identical.
1521
- const flowDeclassify = [
1522
- ...[...(l.module.workspaceInvariants ?? [])]
1523
- .filter((wi) => typeof (wi as { declassify?: unknown }).declassify === "string")
1524
- .map((wi) => ({ site: `workspaceInvariant:${wi.id}`, reason: (wi as { declassify: string }).declassify })),
1525
- ...[...(l.module.aggregates as { id: string; invariantDeclassify?: unknown }[])]
1526
- .filter((a) => typeof a.invariantDeclassify === "string")
1527
- .map((a) => ({ site: `aggregateInvariant:${a.id}`, reason: a.invariantDeclassify as string })),
1528
- ...[...((l.module as { impureCapabilities?: readonly { name?: string; capability?: string; aggregate: { id: string }; declassify?: unknown }[] }).impureCapabilities ?? [])]
1529
- .filter((c) => typeof c.declassify === "string")
1530
- .map((c) => ({ site: `capability:${c.name ?? c.capability ?? c.aggregate.id}`, reason: c.declassify as string })),
1531
- ].sort((a, b) => (a.site < b.site ? -1 : a.site > b.site ? 1 : 0));
1532
- // THE ENDORSE FLOWS (the integrity dual — security_model.md): discovery IS law. Run each
1533
- // directive's pure plan with a probe principal + payload and harvest every PrincipalEvidence
1534
- // conversion's declared flow {directiveId, sink, facet, reason}. Hashed into nomosFlowGate +
1535
- // witnessed fail-closed at runtime (Slice B). Omit-when-empty (a principal-free domain is
1536
- // byte-identical). SORTED for determinism.
1537
- // FIRST-CLASS VALUE OBJECTS: the module's collected descriptors (composeDomainModule
1538
- // dedupes + sorts them). Omit-when-empty (era rule — VO-free law is byte-identical).
1539
- const moduleValueObjects = (l.module as { valueObjects?: readonly import("./fields.js").ValueObjectDescriptor[] }).valueObjects;
1540
- // THE VO↔FIELD ATTACHMENT: the usage pointers ride the SAME additive key (see
1541
- // the UsdLayer type) — computed from the live Field metas, sorted, omit-when-empty.
1542
- const voUsages = collectValueObjectUsages(
1543
- l.module.aggregates as unknown as readonly { id: string; fields: Record<string, import("./fields.js").Field> }[],
1544
- );
1545
- // MULTI-WORKSPACE LOCATORS: collect every `t.foreignRef` field across the module's aggregates.
1546
- const foreignRefList: { aggregate: string; field: string; workspaceField: string; target: string; targetDomain?: string }[] = [];
1547
- for (const agg of l.module.aggregates) {
1548
- for (const [fname, f] of Object.entries((agg as { fields: Record<string, { foreignRef?: { workspaceField: string; targetAggregateId: string; targetDomain?: string } }> }).fields)) {
1549
- const fr = f.foreignRef;
1550
- if (fr === undefined) continue;
1551
- foreignRefList.push({ aggregate: agg.id, field: fname, workspaceField: fr.workspaceField, target: fr.targetAggregateId, ...(fr.targetDomain !== undefined ? { targetDomain: fr.targetDomain } : {}) });
1552
- }
1553
- }
1554
- foreignRefList.sort((a, b) => (a.aggregate !== b.aggregate ? (a.aggregate < b.aggregate ? -1 : 1) : a.field < b.field ? -1 : a.field > b.field ? 1 : 0));
1555
- const nomosForeignRefs = foreignRefList.length > 0 ? { v: 1 as const, refs: foreignRefList } : undefined;
1556
- const nomosValueObjects =
1557
- moduleValueObjects !== undefined && moduleValueObjects.length > 0
1558
- ? {
1559
- v: 1 as const,
1560
- valueObjects: [...moduleValueObjects],
1561
- ...(voUsages.length > 0 ? { usages: voUsages } : {}),
1562
- }
1563
- : undefined;
1564
- // DOMAIN CONTRACTS: the additive `nomosContracts` descriptor (hash-inert, omit-when-empty).
1565
- const nomosContracts = buildNomosContracts(l.module);
1566
- const flowEndorse = [...l.module.directives]
1567
- .flatMap((d) => harvestPrincipalFlows(d as never, l.module.domain ?? l.module.name).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
1568
- .sort((a, b) =>
1569
- a.directiveId !== b.directiveId ? (a.directiveId < b.directiveId ? -1 : 1)
1570
- : a.sink !== b.sink ? (a.sink < b.sink ? -1 : 1)
1571
- : a.facet < b.facet ? -1 : a.facet > b.facet ? 1 : 0,
1572
- );
1573
- return {
1574
- path: l.path,
1575
- prims: encodeModuleToPrims(l.path, manifest),
1576
- nomosIdentityManifest: manifest,
1577
- nomosReadProjection: readProjectionForModule(l.module, manifest, qaType),
1578
- ...(manifest.queries !== undefined ? { queries: manifest.queries } : {}),
1579
- ...(manifest.deriveds !== undefined ? { deriveds: manifest.deriveds } : {}),
1580
- ...(manifest.combineds !== undefined ? { combineds: manifest.combineds } : {}),
1581
- // STABLE IDS (#58): every post-#58 compile carries them (the era key).
1582
- ...(manifest.stableIds !== undefined ? { nomosStableIds: manifest.stableIds } : {}),
1583
- // THE STRUCTURAL INTERFACE (#72): the on-ledger OFFER — a client negotiates shape-
1584
- // compatibility against this, reactively, straight from the folded `policy:{hash}.bundle`.
1585
- // The client-facing structural offer stays BARE-labelled (labels are cosmetic there;
1586
- // shape-compat negotiates by sid) — pass the pre-namespacing stable-ids.
1587
- nomosInterface: domainInterface(l.module, bareManifest.stableIds),
1588
- ...(Object.keys(readGateDirectives).length > 0
1589
- ? {
1590
- nomosReadGate: {
1591
- v: 1 as const,
1592
- queries: readGateQueries,
1593
- directives: readGateDirectives,
1594
- },
1595
- }
1596
- : {}),
1597
- ...(invariantAggregates.length > 0 || wsInvariants.length > 0
1598
- ? {
1599
- nomosInvariantGate: {
1600
- v: 1 as const,
1601
- ...(invariantAggregates.length > 0 ? { aggregates: invariantAggregates } : {}),
1602
- ...(wsInvariants.length > 0 ? { workspaceInvariants: wsInvariants } : {}),
1603
- },
1604
- }
1605
- : {}),
1606
- ...(nomosRelations !== undefined ? { nomosRelations } : {}),
1607
- ...(nomosRelationNamespace !== undefined ? { nomosRelationNamespace } : {}),
1608
- ...(nomosReadProjectionNamespace !== undefined ? { nomosReadProjectionNamespace } : {}),
1609
- ...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
1610
- ...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
1611
- ...(nomosDelegationGate !== undefined ? { nomosDelegationGate } : {}),
1612
- ...(nomosAttestedReadGate !== undefined ? { nomosAttestedReadGate } : {}),
1613
- ...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
1614
- ...(nomosBirthRecordGate !== undefined ? { nomosBirthRecordGate } : {}),
1615
- ...(nomosParentAttestedBirth !== undefined ? { nomosParentAttestedBirth } : {}),
1616
- ...(nomosVerifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll } : {}),
1617
- ...(eraCertGate !== undefined ? { nomosEraCertGate: eraCertGate } : {}),
1618
- ...(nomosForeignRefs !== undefined ? { nomosForeignRefs } : {}),
1619
- ...(nomosValueObjects !== undefined ? { nomosValueObjects } : {}),
1620
- ...(nomosContracts !== undefined ? { nomosContracts } : {}),
1621
- ...(Object.keys(visAggregates).length > 0
1622
- ? { nomosReadVisibility: { v: 1 as const, aggregates: visAggregates } }
1623
- : {}),
1624
- ...(Object.keys(encAggregates).length > 0
1625
- ? { nomosEncryption: { v: 1 as const, aggregates: encAggregates } }
1626
- : {}),
1627
- ...(Object.keys(attrAggregates).length > 0
1628
- ? { nomosAttributedGate: { v: 1 as const, aggregates: attrAggregates } }
1629
- : {}),
1630
- ...(nomosCapabilities.length > 0
1631
- ? { nomosCapabilities: { v: 1 as const, capabilities: nomosCapabilities } }
1632
- : {}),
1633
- ...(flowDeclassify.length > 0 || flowEndorse.length > 0
1634
- ? {
1635
- nomosFlowGate: {
1636
- v: 1 as const,
1637
- ...(flowDeclassify.length > 0 ? { declassify: flowDeclassify } : {}),
1638
- ...(flowEndorse.length > 0 ? { endorse: flowEndorse } : {}),
1639
- },
1640
- }
1641
- : {}),
1642
- };
1643
- })
1644
- .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
1645
- // COMPILE-TIME COMPOSITION + fail-closed GUARD for SHARED (global) relation types across the
1646
- // package's per-domain layers: UNION every domain's relations on a shared bare-id type (so nothing
1647
- // is silently dropped) and REFUSE on a divergent shared rewrite (the scalpel). Non-shared /
1648
- // auto-qualified types are untouched → a package with no shared global type is byte-identical.
1649
- // THE READ-PROJECTION DIVERGENCE GUARD (the scalpel, extended to the read layer): for keys that
1650
- // remain SHARED across this package's domains (GLOBAL framework aggregates / same-named declared
1651
- // reads), identical declarations dedupe at the kernel accumulator; a DIVERGENT shared key would
1652
- // wedge the workspace at serve time — refuse it AT COMPILE instead, naming the key and each
1653
- // domain's shape. Validation only (never transforms), so a conflict-free package is untouched.
1654
- guardSharedReadProjectionKeys(out);
1655
- return { layers: composeSharedRelationTypes(out) };
1656
- }
1657
-
1658
- /** Two aggregate prims at the same path are identical iff same type + same schema. */
1659
- function aggregateEqual(
1660
- a: Extract<UsdPrim, { kind: "aggregate" }>,
1661
- b: Extract<UsdPrim, { kind: "aggregate" }>,
1662
- ): boolean {
1663
- return a.type === b.type && canonicalJson(a.schema) === canonicalJson(b.schema);
1664
- }
1665
-
1666
- /**
1667
- * Merge a stronger `over` prim onto a `base` prim at the SAME path, per the #137
1668
- * typed rules — reusing `compose.ts`'s `mergeRequiresScope` for directive prims
1669
- * (requires add/narrow-not-remove; scope narrow-not-widen; widen/remove need
1670
- * `authority`). An uncertified artifact would always be refused — but `artifactRef`
1671
- * is unset here (stage 2), so there is nothing to refuse yet.
1672
- *
1673
- * Mixing prim KINDS at one path, or redefining an aggregate's schema across layers,
1674
- * is not a typed monotonic move — it is a structural conflict, surfaced fail-closed.
1675
- */
1676
- function mergeUsdPrim(base: UsdPrim, over: UsdPrim, opts: ComposeOptions): UsdPrim {
1677
- if (base.kind !== over.kind) {
1678
- throw new Error(
1679
- `USD composition conflict at ${over.path}: cannot merge a ${base.kind} prim ` +
1680
- `with a ${over.kind} prim.`,
1681
- );
1682
- }
1683
-
1684
- if (base.kind === "aggregate" && over.kind === "aggregate") {
1685
- // Aggregates are single-source schemas: an identical re-declaration is a no-op
1686
- // pass-through; a divergent one is a structural conflict (not a monotonic move).
1687
- if (!aggregateEqual(base, over)) {
1688
- throw new Error(
1689
- `USD composition conflict at ${over.path}: aggregate schema redefined ` +
1690
- `across layers.`,
1691
- );
1692
- }
1693
- return over;
1694
- }
1695
-
1696
- // Both directive prims — apply the shared typed requires+scope rule core (#137).
1697
- if (base.kind === "directive" && over.kind === "directive") {
1698
- if (base.target !== over.target) {
1699
- throw new Error(
1700
- `USD composition conflict at ${over.path}: directive target changed from ` +
1701
- `"${base.target}" to "${over.target}".`,
1702
- );
1703
- }
1704
- if (base.marker !== over.marker) {
1705
- throw new Error(
1706
- `USD composition conflict at ${over.path}: directive marker changed from ` +
1707
- `"${base.marker}" to "${over.marker}".`,
1708
- );
1709
- }
1710
- if (
1711
- base.payloadJsonSchema !== undefined &&
1712
- over.payloadJsonSchema !== undefined &&
1713
- canonicalJson(base.payloadJsonSchema) !== canonicalJson(over.payloadJsonSchema)
1714
- ) {
1715
- throw new Error(
1716
- `USD composition conflict at ${over.path}: directive payload schema changed.`,
1717
- );
1718
- }
1719
- const merged = mergeRequiresScope(over.path, base, over, opts);
1720
- const out: Extract<UsdPrim, { kind: "directive" }> = {
1721
- kind: "directive",
1722
- path: over.path,
1723
- target: over.target,
1724
- marker: over.marker,
1725
- requires: merged.requires,
1726
- ...(over.payloadJsonSchema !== undefined
1727
- ? { payloadJsonSchema: over.payloadJsonSchema }
1728
- : base.payloadJsonSchema !== undefined
1729
- ? { payloadJsonSchema: base.payloadJsonSchema }
1730
- : {}),
1731
- ...(merged.scope !== undefined ? { scope: merged.scope } : {}),
1732
- // Preserve the declared law boundary across a monotonic merge (omit-when-empty):
1733
- // the stronger layer's declaration wins when present, else the base's carries
1734
- // through; a boundary-free pair stays byte-identical to before this field existed.
1735
- ...(over.reads !== undefined
1736
- ? { reads: over.reads }
1737
- : base.reads !== undefined
1738
- ? { reads: base.reads }
1739
- : {}),
1740
- ...(over.emits !== undefined
1741
- ? { emits: over.emits }
1742
- : base.emits !== undefined
1743
- ? { emits: base.emits }
1744
- : {}),
1745
- // artifactRef stays unset (a later certified-artifact slice).
1746
- };
1747
- return out;
1748
- }
1749
-
1750
- // Unreachable (kinds proven equal above) — exhaustiveness guard.
1751
- throw new Error(`USD composition: unhandled prim kind at ${over.path}.`);
1752
- }
1753
-
1754
- /** Canonicalize a standalone prim — sort a directive's `requires`. */
1755
- function canonicalizeStandalone(prim: UsdPrim): UsdPrim {
1756
- if (prim.kind === "directive") {
1757
- return { ...prim, requires: [...prim.requires].sort() };
1758
- }
1759
- return prim;
1760
- }
1761
-
1762
- /**
1763
- * Fold a stream of prims (already in weak→strong order) into an effective prim map
1764
- * keyed by path: a path's first contributor passes through (canonicalized); each
1765
- * later (stronger) contributor MERGES per the #137 typed rules (fail-closed
1766
- * `CompositionViolation` on any rule it cannot satisfy).
1767
- */
1768
- function foldPrims(
1769
- acc: Map<string, UsdPrim>,
1770
- prims: Iterable<UsdPrim>,
1771
- opts: ComposeOptions,
1772
- ): void {
1773
- for (const prim of prims) {
1774
- const existing = acc.get(prim.path);
1775
- if (existing === undefined) {
1776
- acc.set(prim.path, canonicalizeStandalone(prim));
1777
- } else {
1778
- acc.set(prim.path, mergeUsdPrim(existing, prim, opts));
1779
- }
1780
- }
1781
- }
1782
-
1783
- /**
1784
- * Resolve ONE layer's EFFECTIVE prims (the REFERENCE arc, LIVRPS: References below
1785
- * Local). Each referenced layer's effective prims are composed FIRST (weaker base),
1786
- * in declared `references[]` order, then THIS layer's own LOCAL prims fold OVER them
1787
- * (stronger) via the same typed `mergeUsdPrim`. Resolution is deterministic and
1788
- * DETECTS CYCLES (A→B→A) via the `stack` of layer paths currently being resolved,
1789
- * failing closed with a `CompositionViolation`. `byPath` indexes the document's
1790
- * layers; a reference to an unknown layer path also fails closed.
1791
- *
1792
- * VARIANTS (LIVRPS: Variants sit ABOVE References, BELOW Local) resolve AFTER the
1793
- * reference fold and BEFORE the local fold: the SELECTED variant of each declared set
1794
- * (or its `"*"` default, else nothing) folds OVER the referenced base (variants are
1795
- * stronger than references) and UNDER the local prims (local is the strongest), all
1796
- * through the same monotonic `mergeUsdPrim`. The selection is `opts.variantSelection`,
1797
- * an explicit flatten input — so the composed identity reflects it. A selection naming
1798
- * an unknown set or variant fails closed (`variant-unresolved`).
1799
- */
1800
- function resolveLayerEffective(
1801
- layer: UsdLayer,
1802
- byPath: Map<string, UsdLayer>,
1803
- opts: ComposeOptions,
1804
- stack: string[],
1805
- ): UsdPrim[] {
1806
- if (stack.includes(layer.path)) {
1807
- throw new CompositionViolation(
1808
- "reference-cycle",
1809
- layer.path,
1810
- `reference cycle detected: ${[...stack, layer.path].join(" -> ")}`,
1811
- );
1812
- }
1813
-
1814
- const acc = new Map<string, UsdPrim>();
1815
-
1816
- // References first (weaker), in declared order — each resolved recursively so
1817
- // transitive references compose and cycles are caught at any depth.
1818
- if (layer.references !== undefined) {
1819
- const nextStack = [...stack, layer.path];
1820
- for (const refPath of layer.references) {
1821
- const refLayer = byPath.get(refPath);
1822
- if (refLayer === undefined) {
1823
- throw new CompositionViolation(
1824
- "reference-unresolved",
1825
- layer.path,
1826
- `references unknown layer path "${refPath}"`,
1827
- );
1828
- }
1829
- foldPrims(acc, resolveLayerEffective(refLayer, byPath, opts, nextStack), opts);
1830
- }
1831
- }
1832
-
1833
- // Variants fold OVER references, UNDER local (LIVRPS variant strength). The selected
1834
- // variant of each declared set — or its `"*"` default — contributes its prims; an
1835
- // unselected, default-less set contributes nothing. A selection for an unknown set or
1836
- // an unknown variant fails closed.
1837
- foldPrims(acc, selectedVariantPrims(layer, opts), opts);
1838
-
1839
- // Local prims fold OVER the referenced + variant base (strongest opinion wins).
1840
- foldPrims(acc, layer.prims, opts);
1841
-
1842
- return [...acc.keys()].sort().map((path) => acc.get(path)!);
1843
- }
1844
-
1845
- /**
1846
- * The prims contributed by THIS layer's variant sets under the active selection
1847
- * (`opts.variantSelection`). For each declared set, in canonical (sorted) set-name
1848
- * order: if the selection names a variant, that variant's prims are used (unknown set
1849
- * or unknown variant → `variant-unresolved`, fail-closed); otherwise the set's `"*"`
1850
- * DEFAULT variant is used when declared, else the set contributes nothing. The
1851
- * selection is also validated against the layer's declared sets so a selection naming
1852
- * a set this layer does not declare fails closed too.
1853
- *
1854
- * Returns the contributed prims in canonical order (set-name, then prim-path) so the
1855
- * fold input is deterministic — the fold itself is order-stable for non-overlapping
1856
- * paths, and any overlap within the variant arc is resolved by the same monotonic rule.
1857
- */
1858
- function selectedVariantPrims(layer: UsdLayer, opts: ComposeOptions): UsdPrim[] {
1859
- const sets = layer.variantSets;
1860
- const selection = opts.variantSelection ?? {};
1861
-
1862
- if (sets === undefined) return [];
1863
-
1864
- const out: UsdPrim[] = [];
1865
- for (const setName of Object.keys(sets).sort()) {
1866
- const variants = sets[setName]!;
1867
- const chosen = selection[setName];
1868
- let variantPrims: UsdPrim[] | undefined;
1869
- if (chosen !== undefined) {
1870
- // A selection naming a variant THIS layer's set does not hold fails closed — an
1871
- // explicit choice that cannot bind to a declared alternative is never silently
1872
- // dropped to the default / nothing.
1873
- variantPrims = variants[chosen];
1874
- if (variantPrims === undefined) {
1875
- throw new CompositionViolation(
1876
- "variant-unresolved",
1877
- layer.path,
1878
- `variant set "${setName}" has no variant named "${chosen}"`,
1879
- );
1880
- }
1881
- } else {
1882
- // No explicit choice: the declared default variant, else nothing.
1883
- variantPrims = variants[DEFAULT_VARIANT];
1884
- }
1885
- if (variantPrims !== undefined) {
1886
- for (const prim of sortPrims(variantPrims)) out.push(prim);
1887
- }
1888
- }
1889
- return out;
1890
- }
1891
-
1892
- /**
1893
- * Validate the WHOLE selection against the document: each selected set name MUST be
1894
- * declared by at least ONE layer. A selection naming a set NO layer declares fails
1895
- * closed (`variant-unresolved`) — an explicit choice that can never bind is never a
1896
- * silent no-op. (The per-variant check — an unknown variant WITHIN a declared set —
1897
- * is enforced per-layer in `selectedVariantPrims`, where the set is in scope.) This is
1898
- * document-scoped because the selection is a single global input to the flatten, while
1899
- * a given set may be declared on only some of the document's layers.
1900
- */
1901
- function assertSelectionResolvable(doc: UsdDocument, opts: ComposeOptions): void {
1902
- const selection = opts.variantSelection;
1903
- if (selection === undefined) return;
1904
- const declared = new Set<string>();
1905
- for (const layer of doc.layers) {
1906
- if (layer.variantSets !== undefined) {
1907
- for (const setName of Object.keys(layer.variantSets)) declared.add(setName);
1908
- }
1909
- }
1910
- for (const setName of Object.keys(selection)) {
1911
- if (!declared.has(setName)) {
1912
- throw new CompositionViolation(
1913
- "variant-unresolved",
1914
- setName,
1915
- `selection names variant set "${setName}" not declared by any layer`,
1916
- );
1917
- }
1918
- }
1919
- }
1920
-
1921
- /**
1922
- * Fold the document's layers IN ORDER into the effective flattened prim set.
1923
- *
1924
- * Per layer, the REFERENCE arc resolves FIRST (weakest), then the VARIANT arc (the
1925
- * selected variant of each declared set per `opts.variantSelection`, stronger than
1926
- * references, weaker than local), then this layer's LOCAL prims (strongest) fold OVER
1927
- * both (LIVRPS), producing the layer's effective prim set; resolution is deterministic
1928
- * and fails closed on cycles / unknown reference paths / unresolved variant selections.
1929
- * Then the existing SUBLAYER fold composes those per-layer
1930
- * effective prim sets in `layers[]` (strength) order, unchanged: a prim present in
1931
- * only one layer passes through; a prim a later layer contributes at a path an earlier
1932
- * layer already defined is MERGED per the #137 typed rules (fail-closed
1933
- * `CompositionViolation`). Returns the prim set SORTED by path.
1934
- */
1935
- export function flattenUsd(
1936
- doc: UsdDocument,
1937
- opts: ComposeOptions = {},
1938
- ): UsdPrim[] {
1939
- // Fail closed on a selection naming a set NO layer declares (document-scoped — the
1940
- // selection is a single global input; a set may live on only some layers).
1941
- assertSelectionResolvable(doc, opts);
1942
-
1943
- const byPath = new Map<string, UsdLayer>();
1944
- for (const layer of doc.layers) byPath.set(layer.path, layer);
1945
-
1946
- const acc = new Map<string, UsdPrim>();
1947
-
1948
- for (const layer of doc.layers) {
1949
- const effective = resolveLayerEffective(layer, byPath, opts, []);
1950
- foldPrims(acc, effective, opts);
1951
- }
1952
-
1953
- return [...acc.keys()]
1954
- .sort()
1955
- .map((path) => acc.get(path)!);
1956
- }
1957
-
1958
- /**
1959
- * The composed-IR identity: sha256 hex of `canonicalJson(flattenUsd(doc, opts))`.
1960
- * Extends #136 (the manifest hash) to a flattened MULTI-LAYER stage, reusing the
1961
- * same byte-stable canonicalizer.
1962
- */
1963
- export function usdHash(doc: UsdDocument, opts: ComposeOptions = {}): string {
1964
- return createHash("sha256")
1965
- .update(canonicalJson(flattenUsd(doc, opts)), "utf8")
1966
- .digest("hex");
1967
- }