@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.
- package/compile_package.mjs +43 -2
- package/compile_proof.d.mts +47 -0
- package/compile_proof.mjs +343 -0
- package/dart/CHANGELOG.md +7 -0
- package/dart/lib/src/dev_cloud.dart +8 -8
- package/dart/lib/src/nms_generated_runtime.dart +2 -0
- package/dart/lib/src/runner/node-entry.mjs +5504 -1236
- package/dart/lib/src/runner/offline-install.mjs +4578 -528
- package/dart/lib/src/test_harness.dart +206 -152
- package/dart/lib/src/webview_bridge.dart +196 -89
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +4 -3
- package/dart/test/adv_realm_interference_test.dart +7 -6
- package/dart/test/cloud_bound_topology_test.dart +3 -3
- package/dart/test/cluster_births_test.dart +4 -3
- package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
- package/dart/test/cluster_local_autosync_test.dart +3 -2
- package/dart/test/cluster_scale_test.dart +3 -2
- package/dart/test/dlq_session_surface_test.dart +7 -6
- package/dart/test/first_app_gauntlet_test.dart +14 -21
- package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
- package/dart/test/four_layer_nested_topology_test.dart +4 -4
- package/dart/test/harness_phase_timeout_test.dart +2 -2
- package/dart/test/local_sync_inert_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +2 -2
- package/dart/test/sealing_client_test.dart +4 -3
- package/dart/test/session_bound_test.dart +8 -7
- package/dart/test/test_harness_test.dart +13 -12
- package/dart/test/vo_read_typing_test.dart +4 -3
- package/dart/test/watch_after_offer_stall_test.dart +4 -3
- package/dart/test/webview_bridge_test.dart +102 -5
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/nomos_types.dart +9 -0
- package/dart_types/lib/src/nomos_ref.dart +20 -0
- package/dart_types/lib/src/optional_ref.dart +85 -0
- package/dart_types/lib/src/sha256_digest.dart +41 -0
- package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/optional_ref_test.dart +44 -0
- package/dart_types/test/sha256_digest_test.dart +18 -0
- package/package.json +9 -5
- package/runtime/generated-application-host.js +5365 -1105
- package/src/aggregate.ts +18 -0
- package/src/application_composition.ts +6 -32
- package/src/attested_core.ts +6 -3
- package/src/birth_meter.ts +1 -0
- package/src/birth_meter_ensure.ts +28 -2
- package/src/births.ts +1 -1
- package/src/build_package.ts +162 -538
- package/src/business_birth.ts +19 -3
- package/src/canonical_json.ts +21 -0
- package/src/check_package_main.ts +76 -35
- package/src/clear_directive.ts +19 -8
- package/src/codegen_dart.ts +305 -22
- package/src/codegen_dart_application.ts +844 -292
- package/src/codegen_dart_client.ts +1084 -407
- package/src/codegen_dart_scenario.ts +12 -12
- package/src/codegen_proof.ts +169 -39
- package/src/codegen_provider_dart.ts +17 -1
- package/src/codegen_ts.ts +298 -106
- package/src/compat_manifest.ts +2 -2
- package/src/compat_package_main.ts +8 -12
- package/src/compile_package_main.ts +1866 -597
- package/src/compose.ts +1 -1
- package/src/directive.ts +208 -15
- package/src/engine_entry.ts +165 -111
- package/src/evolve_preview.ts +43 -81
- package/src/extremum.ts +1 -1
- package/src/fields.ts +183 -4
- package/src/flow_gate.ts +75 -2
- package/src/framework/application_home.ts +2 -0
- package/src/framework/auth_provider_law.ts +2 -2
- package/src/framework/birth_meter.ts +7 -19
- package/src/framework/bootstrap.ts +0 -2
- package/src/framework/business_corrections.ts +9 -5
- package/src/framework/domain_lifecycle.ts +15 -29
- package/src/framework/encryption_scope.ts +2 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +16 -14
- package/src/framework/home.ts +20 -14
- package/src/framework/owned_custody.ts +9 -3
- package/src/framework/platform_admin.ts +3 -3
- package/src/framework/relations.ts +21 -13
- package/src/framework/signing_identity.ts +45 -40
- package/src/framework/workspace_invariant.ts +245 -0
- package/src/framework/workspaces.ts +23 -29
- package/src/index.ts +36 -16
- package/src/intent.ts +178 -6
- package/src/law_check.ts +32 -11
- package/src/law_graph.ts +2902 -0
- package/src/law_graph_usda.ts +1799 -0
- package/src/law_identity.ts +435 -0
- package/src/law_value_type.ts +230 -0
- package/src/lsp_server.ts +61 -31
- package/src/ops.ts +7 -0
- package/src/package_config.ts +141 -0
- package/src/package_dependencies.ts +108 -0
- package/src/package_dependency_usage.ts +150 -0
- package/src/package_source_imports.ts +71 -0
- package/src/plan_source_slice.ts +335 -0
- package/src/point_guard.ts +192 -0
- package/src/precondition.ts +44 -9
- package/src/read_projection_namespace.ts +1 -1
- package/src/runtime_zod.ts +44 -0
- package/src/secondary_ensure.ts +67 -14
- package/src/shape_compat.ts +1 -1
- package/src/stable_id_identity_bridge.ts +149 -0
- package/src/stable_id_lockfile.ts +278 -0
- package/src/stable_ids.ts +7 -2
- package/src/stable_ids_for_module.ts +348 -0
- package/src/stable_ids_types.ts +1 -1
- package/src/sum.ts +1 -1
- package/src/usd_state.ts +1 -1
- package/src/usda_relationship_integrity.ts +66 -0
- package/src/usda_text.ts +18 -0
- package/src/usdz.ts +78 -0
- package/src/value_object.ts +95 -0
- package/src/wire.ts +19 -0
- package/src/wire_encode.ts +22 -0
- package/src/workspace_sharding.ts +52 -36
- package/src/workspace_sharing.ts +28 -4
- package/src/zod_introspection.ts +1 -1
- package/dart/test/cluster_attested_test.dart +0 -201
- package/src/compile_engine.ts +0 -595
- package/src/manifest.ts +0 -1476
- package/src/read_projection_composition.ts +0 -117
- package/src/read_projection_manifest.ts +0 -115
- package/src/relation_composition.ts +0 -286
- package/src/usd.ts +0 -1967
- 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
|
-
}
|