@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/manifest.ts
DELETED
|
@@ -1,1476 +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
|
-
* Domain identity = canonical SEMANTIC manifest hash (#136).
|
|
10
|
-
*
|
|
11
|
-
* "The bundle is not the law." A domain's IDENTITY in the certification gate is a
|
|
12
|
-
* content hash. Historically that hash was `sha256(stripPathMarkers(esbuild_bundle))`
|
|
13
|
-
* (see `golden/build_golden.mjs`): it depended on esbuild's output format — version,
|
|
14
|
-
* whitespace, declaration ordering — and on the domain's file LOCATION. That is
|
|
15
|
-
* fragile: a bundler upgrade or a file move changes the identity of unchanged logic.
|
|
16
|
-
*
|
|
17
|
-
* This module derives identity instead from the domain's CANONICAL MANIFEST — built
|
|
18
|
-
* directly from the in-memory DSL objects (`DomainModule`). It captures the domain's
|
|
19
|
-
* CONTRACT: its aggregates' field→driver schemas, and each directive's target /
|
|
20
|
-
* marker / required capability / scope-presence / payload SHAPE. It deliberately does
|
|
21
|
-
* NOT capture executable `plan`/`scopeFrom` BEHAVIOUR — that is a separate
|
|
22
|
-
* `artifact_hash` (out of scope here). The consequence is the desired property:
|
|
23
|
-
* refactoring a directive's `plan` body while keeping its signature identical does
|
|
24
|
-
* NOT change the domain hash. The hash tracks the contract, not the code, and is
|
|
25
|
-
* independent of both bundling AND file location while remaining sensitive to real
|
|
26
|
-
* semantic/contract changes.
|
|
27
|
-
*/
|
|
28
|
-
import { createHash } from "node:crypto";
|
|
29
|
-
import { z } from "zod";
|
|
30
|
-
import { encodeKernelSchema, encodeRefMode } from "./wire_encode.js";
|
|
31
|
-
import { encodeDriver } from "./drivers.js";
|
|
32
|
-
import type { WireSchema, WireRefMode } from "./wire.js";
|
|
33
|
-
import type { ReferentialMarker } from "./directive.js";
|
|
34
|
-
import type { DomainModule } from "./codegen_dart.js";
|
|
35
|
-
import type { Field } from "./fields.js";
|
|
36
|
-
import {
|
|
37
|
-
BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
|
|
38
|
-
OWNED_CUSTODY_PAYLOAD_FIELD,
|
|
39
|
-
OWNED_KEY_PAYLOAD_FIELD,
|
|
40
|
-
OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
|
|
41
|
-
OWNED_READ_RELATION,
|
|
42
|
-
} from "./aggregate.js";
|
|
43
|
-
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
44
|
-
import type { QueryDecl } from "./query.js";
|
|
45
|
-
import { finishCount, type AnyCount } from "./count.js";
|
|
46
|
-
import type { SpatialDecl } from "./spatial.js";
|
|
47
|
-
import { finishSum, type AnySum } from "./sum.js";
|
|
48
|
-
import { finishExtremum, type AnyExtremum } from "./extremum.js";
|
|
49
|
-
import { existsAsCount, type AnyExists } from "./exists.js";
|
|
50
|
-
import type { OrderedReadDecl } from "./ordered_read.js";
|
|
51
|
-
import type { CanonicalPred } from "./predicate.js";
|
|
52
|
-
import { assertFlowGate } from "./flow_gate.js";
|
|
53
|
-
import type { DerivedDecl } from "./derived.js";
|
|
54
|
-
import type { CombinedDecl } from "./combined.js";
|
|
55
|
-
import type { RelationDecl } from "./relation.js";
|
|
56
|
-
import { type CanonicalWorkspaceType } from "./workspace_type.js";
|
|
57
|
-
import {
|
|
58
|
-
canonicalTaxonomyFragment,
|
|
59
|
-
type CanonicalDirectiveRoute,
|
|
60
|
-
} from "./workspace_routing.js";
|
|
61
|
-
import {
|
|
62
|
-
mintAggregateSid,
|
|
63
|
-
mintDomainSid,
|
|
64
|
-
mintFieldSid,
|
|
65
|
-
type StableAggregateId,
|
|
66
|
-
type StableFieldId,
|
|
67
|
-
type StableFieldEvolution,
|
|
68
|
-
type StableIds,
|
|
69
|
-
} from "./stable_ids.js";
|
|
70
|
-
|
|
71
|
-
/** One captured `[fieldName, zodKind]` payload field, with optionality. */
|
|
72
|
-
export interface CanonicalPayloadField {
|
|
73
|
-
/** The payload field name. */
|
|
74
|
-
readonly name: string;
|
|
75
|
-
/** The Zod 4 `def.type` of the field's unwrapped type, e.g. "string". */
|
|
76
|
-
readonly type: string;
|
|
77
|
-
/** True when the field is `optional` / `default` wrapped. */
|
|
78
|
-
readonly optional: boolean;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** One captured projected read-field return schema. */
|
|
82
|
-
export interface CanonicalProjectionReturn {
|
|
83
|
-
/** The Zod 4 `def.type` of the unwrapped return type, e.g. "boolean". */
|
|
84
|
-
readonly type: string;
|
|
85
|
-
/** True when the projected value may be null/absent. */
|
|
86
|
-
readonly nullable: boolean;
|
|
87
|
-
/** The return value as standard JSON Schema, emitted from the same TS/Zod source. */
|
|
88
|
-
readonly jsonSchema: unknown;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** One captured aggregate: its id + its field→driver schema (keys SORTED). */
|
|
92
|
-
export interface CanonicalAggregate {
|
|
93
|
-
readonly id: string;
|
|
94
|
-
readonly schema: WireSchema;
|
|
95
|
-
/**
|
|
96
|
-
* PRESENCE ONLY — the body is executable (like a directive `plan`), NOT hashed.
|
|
97
|
-
* OMITTED ENTIRELY when absent, so a predicate-free aggregate is byte-identical
|
|
98
|
-
* in the manifest to before this field existed (golden hashes UNCHANGED).
|
|
99
|
-
*/
|
|
100
|
-
readonly hasInvariant?: true;
|
|
101
|
-
/** The LAW-DECLARED instance cap; omitted when uncapped (hash-stable). */
|
|
102
|
-
readonly cap?: number;
|
|
103
|
-
/**
|
|
104
|
-
* READ VISIBILITY (slice 5.1) — the capability a read TOUCHING this type requires.
|
|
105
|
-
* PRESENT only when the type is declared `visibility: requires(cap)` (PRIVATE);
|
|
106
|
-
* OMITTED ENTIRELY for the default `public`, so a public-only domain is
|
|
107
|
-
* byte-identical in the manifest to before this key existed (golden hashes
|
|
108
|
-
* UNCHANGED — mirrors `cap`/`hasInvariant`). It IS law (the githolon refuses an
|
|
109
|
-
* unauthorized read on it), so adding/changing it MOVES the hash.
|
|
110
|
-
*/
|
|
111
|
-
readonly requiresRead?: string;
|
|
112
|
-
/**
|
|
113
|
-
* E2E ENCRYPTION (slice 2) — the STORED field names whose values are `.encrypted()`,
|
|
114
|
-
* SORTED. PRESENT only when ≥1 field is encrypted; OMITTED otherwise (a plaintext
|
|
115
|
-
* aggregate is byte-identical). It IS law (the gate enforces the envelope + the scope),
|
|
116
|
-
* so adding/changing it MOVES the hash. The scope is the aggregate's `requiresRead`.
|
|
117
|
-
*/
|
|
118
|
-
readonly encryptedFields?: string[];
|
|
119
|
-
/** Secure-by-default owned lowering. Presence means `requiresRead` is a PER-RECORD relation (not a
|
|
120
|
-
* workspace-wide capability), `ownerField` is framework-stamped, and the encrypted slots use the
|
|
121
|
-
* stateful-client owned-key lane. */
|
|
122
|
-
readonly owned?: { readonly ownerField: string; readonly relation: string };
|
|
123
|
-
/** Stored framework fields intentionally hidden from generated business models. */
|
|
124
|
-
readonly internalFields?: readonly string[];
|
|
125
|
-
/**
|
|
126
|
-
* ATTRIBUTED READS (architecture/field_provenance.md) — the STORED field names whose
|
|
127
|
-
* folded values keep author provenance (`.attributed()`), SORTED. PRESENT only when
|
|
128
|
-
* ≥1 field is attributed; OMITTED otherwise (an unattributed aggregate is
|
|
129
|
-
* byte-identical). The hash-bearing LAW is the wrapped `{"Attributed": <inner>}`
|
|
130
|
-
* driver in `schema`; this key is the self-describing mirror codegen/openapi read.
|
|
131
|
-
*/
|
|
132
|
-
readonly attributedFields?: string[];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** One captured directive contract — NO `plan`/`scopeFrom` behaviour. */
|
|
136
|
-
export interface CanonicalDirective {
|
|
137
|
-
readonly id: string;
|
|
138
|
-
/** The target aggregate id. */
|
|
139
|
-
readonly target: string;
|
|
140
|
-
readonly marker: WireRefMode;
|
|
141
|
-
/**
|
|
142
|
-
* MULTI-OUTPUT DIRECTIVES: additional aggregates this directive declares it writes, beyond the primary
|
|
143
|
-
* `target`/`marker` — `{ target, marker }` pairs SORTED by (target, marker). Descriptive law metadata
|
|
144
|
-
* (docs/proof/client name every output). OMITTED ENTIRELY when the directive declares none, so a
|
|
145
|
-
* single-output directive is byte-identical to before this key existed (pinned golden hashes unchanged).
|
|
146
|
-
*/
|
|
147
|
-
readonly secondaryOutputs?: readonly { target: string; marker: WireRefMode }[];
|
|
148
|
-
/** The payload's top-level object shape as SORTED `[name, type]` pairs. */
|
|
149
|
-
readonly payload: CanonicalPayloadField[];
|
|
150
|
-
/**
|
|
151
|
-
* The directive payload as standard JSON Schema, emitted from the same TS/Zod source
|
|
152
|
-
* that the sealed GitHolon engine validates. This is the cross-language validation
|
|
153
|
-
* artifact: Dart/hosts can validate dynamic maps with a JSON Schema library without
|
|
154
|
-
* re-authoring the schema in Dart.
|
|
155
|
-
*/
|
|
156
|
-
readonly payloadJsonSchema: unknown;
|
|
157
|
-
/**
|
|
158
|
-
* The directive's DECLARED read boundary (sorted ref types). The "IR for law
|
|
159
|
-
* boundaries": the boundary is part of the domain identity. OMITTED ENTIRELY when
|
|
160
|
-
* the directive declares no reads — so a boundary-free directive is byte-identical
|
|
161
|
-
* to before this key existed (pinned golden hashes stay UNCHANGED).
|
|
162
|
-
*/
|
|
163
|
-
readonly reads?: string[];
|
|
164
|
-
/**
|
|
165
|
-
* The directive's DECLARED emit boundary: event type → `{ max? }`. Keys sorted by
|
|
166
|
-
* `canonicalJson`. OMITTED ENTIRELY when the directive declares no emits, for the
|
|
167
|
-
* same omit-when-empty hash-invariance reason as {@link reads}.
|
|
168
|
-
*/
|
|
169
|
-
readonly emits?: Record<string, { max?: number }>;
|
|
170
|
-
/**
|
|
171
|
-
* The directive's DECLARED CAPTURED-READ queries (#58 — the captured-read lane):
|
|
172
|
-
* the O(1) DSL queries its `plan` may `read()` on the write path, each carried as
|
|
173
|
-
* `{queryId, key, returns}` (the read RECIPE is the contract — a different key or
|
|
174
|
-
* returns-type is a different read, so it moves the hash). SORTED by queryId.
|
|
175
|
-
* OMITTED ENTIRELY when the directive declares none — a captured-read-free
|
|
176
|
-
* directive is byte-identical in the manifest to before this key existed.
|
|
177
|
-
*/
|
|
178
|
-
readonly capturedReads?: CanonicalCapturedRead[];
|
|
179
|
-
/**
|
|
180
|
-
* The directive's DECLARED required HOST CAPABILITY ports (`OFFER_KERNEL_ARCHITECTURE.dot` cluster_ports;
|
|
181
|
-
* the HOST/PORT axis, invariant 3): the capability ports the host must PROVIDE for the
|
|
182
|
-
* directive to run, checked FAIL-CLOSED (`required ⊆ provided`) at policy LOAD. SORTED (the
|
|
183
|
-
* SET is the contract; order is incidental). DISTINCT from {@link requiresCapability} (the
|
|
184
|
-
* AUTHZ axis "may this actor?", a single string) — this is the HOST axis "can this host
|
|
185
|
-
* physically?" (a set of open-set port ids). Part of the domain identity: a different required
|
|
186
|
-
* port is a different contract, so adding/changing one MOVES the hash. OMITTED ENTIRELY when
|
|
187
|
-
* the directive declares no host capability — so a host-capability-free directive is
|
|
188
|
-
* byte-identical in the manifest to before this key existed (pinned golden hashes stay
|
|
189
|
-
* UNCHANGED), exactly how {@link reads}/{@link relations} stay hash-neutral.
|
|
190
|
-
*/
|
|
191
|
-
readonly requiresHostCapability?: string[];
|
|
192
|
-
/**
|
|
193
|
-
* A law-declared business conflict resolver. The generated client carries an immutable
|
|
194
|
-
* framework snapshot beside this directive's payload, and every admission/replay door
|
|
195
|
-
* compare-and-sets it against the exact pending observation set. Omitted for ordinary
|
|
196
|
-
* directives, so ordinary business actions carry no conflict vocabulary.
|
|
197
|
-
*/
|
|
198
|
-
readonly conflictResolution?: {
|
|
199
|
-
readonly aggregate: string;
|
|
200
|
-
readonly field: string;
|
|
201
|
-
};
|
|
202
|
-
/**
|
|
203
|
-
* The directive's DECLARED cross-workspace relations (`cross_workspace.md` §2): the relations
|
|
204
|
-
* a cross-workspace PR proposing this directive is adjudicated against. Each carries the
|
|
205
|
-
* source/target endpoints, the bounded evidence read it discloses (the disclosure contract +
|
|
206
|
-
* anti-smuggle key, §2.4), and whether an invariant guards it (PRESENCE only — the predicate
|
|
207
|
-
* body is executable behaviour, NOT hashed, exactly like a directive `plan`). SORTED BY the
|
|
208
|
-
* relation `id`. Part of the domain identity: the gate (`kernel::manifest_view`) resolves the
|
|
209
|
-
* relation by the proposed directive + verifies a carried witness against the declared evidence
|
|
210
|
-
* read; a recipe change (different aggregate/fields/predicate) MOVES the hash. OMITTED ENTIRELY
|
|
211
|
-
* when the directive declares no relation — so a relation-free directive is byte-identical in
|
|
212
|
-
* the manifest to before this key existed (pinned golden hashes stay UNCHANGED).
|
|
213
|
-
*/
|
|
214
|
-
readonly relations?: CanonicalRelation[];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** One captured cross-workspace relation — the SEMANTIC half of X1 in the manifest
|
|
218
|
-
* (`cross_workspace.md` §2.1 / §2.4). `id`/`source`/`target`/`proposes` are the endpoints +
|
|
219
|
-
* the proposed directive; `evidence` is the bounded declared read (disclosure contract +
|
|
220
|
-
* anti-smuggle key); `hasInvariant` is the guard's PRESENCE (the body is executable, NOT
|
|
221
|
-
* hashed). A recipe change moves the domain hash. */
|
|
222
|
-
export interface CanonicalRelation {
|
|
223
|
-
readonly id: string;
|
|
224
|
-
readonly source: string;
|
|
225
|
-
readonly target: string;
|
|
226
|
-
readonly proposes: string;
|
|
227
|
-
readonly evidence: CanonicalRelationEvidence;
|
|
228
|
-
readonly hasInvariant: boolean;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** The bounded evidence read a relation discloses over the SOURCE, in the manifest: the source
|
|
232
|
-
* aggregate, the exact SELECT fields (SORTED — the set is the contract), the WHERE predicate,
|
|
233
|
-
* and the freshness `kind`. This IS the disclosure contract + the anti-smuggle key. */
|
|
234
|
-
export interface CanonicalRelationEvidence {
|
|
235
|
-
readonly aggregate: string;
|
|
236
|
-
readonly select: string[];
|
|
237
|
-
readonly where: string;
|
|
238
|
-
readonly kind: string;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** One DECLARED captured-read query (#58) — the write-path `read()` recipe in the
|
|
242
|
-
* manifest: the query id, its index key (DECLARED order — index column order), and the
|
|
243
|
-
* aggregate TYPE it returns. */
|
|
244
|
-
export interface CanonicalCapturedRead {
|
|
245
|
-
readonly queryId: string;
|
|
246
|
-
readonly key: string[];
|
|
247
|
-
readonly returns: string;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* One captured query declaration — the read-side closure's named, INDEXED read.
|
|
253
|
-
* `key` is the index column order, PRESERVED (NOT sorted): the order of the index
|
|
254
|
-
* key fields is part of the contract, so two queries differing only in key order
|
|
255
|
-
* are distinct identities.
|
|
256
|
-
*/
|
|
257
|
-
export interface CanonicalQuery {
|
|
258
|
-
readonly id: string;
|
|
259
|
-
/** Index key fields, in DECLARED order (index column order — preserved). */
|
|
260
|
-
readonly key: string[];
|
|
261
|
-
/** The aggregate TYPE id the query returns, e.g. `SampleThingAggregate`. */
|
|
262
|
-
readonly returns: string;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* One captured count declaration — the read-side closure's named, MAINTAINED tally.
|
|
267
|
-
* Mirrors {@link CanonicalQuery}: a count's `of`-type + (optional) `where` predicate +
|
|
268
|
-
* (optional) `by` group-by field are part of the domain CONTRACT (changing them changes
|
|
269
|
-
* the maintained counter), so they are part of the identity.
|
|
270
|
-
*
|
|
271
|
-
* OMIT-WHEN-ABSENT:
|
|
272
|
-
* `where` is OMITTED ENTIRELY for a predicate-free count — so an unfiltered count is
|
|
273
|
-
* byte-identical in the manifest to before `where` existed (golden hashes UNCHANGED).
|
|
274
|
-
* `by` is OMITTED ENTIRELY for a grand-total count (same discipline, always was).
|
|
275
|
-
*/
|
|
276
|
-
export interface CanonicalCount {
|
|
277
|
-
readonly id: string;
|
|
278
|
-
/** The aggregate TYPE id the count tallies, e.g. `SiteRootAggregate`. */
|
|
279
|
-
readonly of: string;
|
|
280
|
-
/**
|
|
281
|
-
* The predicate filtering which aggregates are counted. OMITTED when no predicate
|
|
282
|
-
* (an unfiltered count is byte-identical in the manifest to before this existed).
|
|
283
|
-
* The canonical form is insertion-order–independent (clauses sorted deterministically
|
|
284
|
-
* by `canonicalizePred`), so byte-identical predicate logic → byte-identical hash.
|
|
285
|
-
*/
|
|
286
|
-
readonly where?: CanonicalPred;
|
|
287
|
-
/** The group-by field, in DECLARED form. OMITTED for a grand-total count. */
|
|
288
|
-
readonly by?: string;
|
|
289
|
-
/**
|
|
290
|
-
* THE ESTATE SCOPE (sharding §5, slice 3): the workspace-type id this read is
|
|
291
|
-
* lifted to via `scoped(read, Ws)` — its per-shard values ride the §5.2 delta
|
|
292
|
-
* lane as gate-recomputed coordinator subtotals. OMITTED for home-scope reads
|
|
293
|
-
* (every pre-slice-3 count is byte-identical — the `cap(n)` discipline).
|
|
294
|
-
*/
|
|
295
|
-
readonly scope?: string;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* One captured SPATIAL index declaration — the read-side closure's named, MAINTAINED
|
|
300
|
-
* GEOSPATIAL (R*Tree) index. Mirrors {@link CanonicalCount}: a spatial index's `of`-type and
|
|
301
|
-
* the geometry field it indexes (`on`) are part of the domain CONTRACT (changing either
|
|
302
|
-
* changes the maintained bounding-box index), so they are part of the identity. There is no
|
|
303
|
-
* optional/omit-when-absent sub-field — both `id`/`of`/`on` are always present.
|
|
304
|
-
*/
|
|
305
|
-
export interface CanonicalSpatial {
|
|
306
|
-
readonly id: string;
|
|
307
|
-
/** The aggregate TYPE id the spatial index covers, e.g. `TrackableAsset`. */
|
|
308
|
-
readonly of: string;
|
|
309
|
-
/** The GeoJSON geometry field whose bounding box is indexed, e.g. `geoPosition`. */
|
|
310
|
-
readonly on: string;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* One captured sum declaration — the read-side closure's named, MAINTAINED numeric
|
|
315
|
-
* aggregation. Analogous to {@link CanonicalCount}: a sum's `of`-type, `sumField`,
|
|
316
|
-
* optional `where` predicate, and optional `by` group-by field are all part of the
|
|
317
|
-
* domain CONTRACT. Changing any of them changes the meaning of the maintained total.
|
|
318
|
-
*
|
|
319
|
-
* OMIT-WHEN-ABSENT: `where` and `by` are OMITTED ENTIRELY when not declared — so a
|
|
320
|
-
* predicate-free, grand-total sum is byte-identical to the minimal `{id, of, sumField}`
|
|
321
|
-
* form (golden hashes stable). Same discipline as {@link CanonicalCount}.
|
|
322
|
-
*/
|
|
323
|
-
export interface CanonicalSum {
|
|
324
|
-
readonly id: string;
|
|
325
|
-
/** The aggregate TYPE id the sum tallies. */
|
|
326
|
-
readonly of: string;
|
|
327
|
-
/**
|
|
328
|
-
* The `int`-kind field whose values are accumulated, e.g. `"itemValue"`. MUST be
|
|
329
|
-
* an `int`-kind field of the `of`-aggregate (validated at manifest-load in Rust).
|
|
330
|
-
*/
|
|
331
|
-
readonly sumField: string;
|
|
332
|
-
/**
|
|
333
|
-
* The predicate filtering which aggregates contribute. OMITTED when no predicate.
|
|
334
|
-
*/
|
|
335
|
-
readonly where?: CanonicalPred;
|
|
336
|
-
/** The group-by field. OMITTED for a grand-total sum. */
|
|
337
|
-
readonly by?: string;
|
|
338
|
-
/** THE ESTATE SCOPE (sharding §5, slice 3) — see {@link CanonicalCount.scope}. */
|
|
339
|
-
readonly scope?: string;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* One captured DERIVED read field — the read-side closure's engine-projected pure field.
|
|
344
|
-
* Mirrors {@link CanonicalCount}: a derived field's NAME (`id`) + the aggregate it derives
|
|
345
|
-
* from (`of`) are part of the domain CONTRACT (the read schema), so they are part of the
|
|
346
|
-
* identity. The fn BODY is DELIBERATELY NOT captured — it is executable behaviour, exactly
|
|
347
|
-
* like a directive's `plan` (which `domainManifest` also omits), so refactoring the derive
|
|
348
|
-
* fn while keeping its `(id, of)` identical does NOT change the domain hash.
|
|
349
|
-
*/
|
|
350
|
-
export interface CanonicalDerived {
|
|
351
|
-
readonly id: string;
|
|
352
|
-
/** The aggregate TYPE id the field is derived from, e.g. `SupportSessionAggregate`. */
|
|
353
|
-
readonly of: string;
|
|
354
|
-
/** The projected value schema. The fn body is not hashed; the value contract is. */
|
|
355
|
-
readonly returns: CanonicalProjectionReturn;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* One captured COMBINED read field — the read-side closure's engine-projected CROSS-
|
|
360
|
-
* AGGREGATE JOIN field. Mirrors {@link CanonicalDerived} but with the JOIN axis: a
|
|
361
|
-
* combined field's NAME (`id`), the OWNER aggregate (`of`), the REF FIELD it joins
|
|
362
|
-
* through (`refField`), and the RELATED type it queries (`reads`) are all part of the
|
|
363
|
-
* domain CONTRACT (the read schema + the cross-aggregate invalidation edge), so they are
|
|
364
|
-
* part of the identity. The fn BODY is DELIBERATELY NOT captured — executable behaviour,
|
|
365
|
-
* exactly like a directive's `plan` / a derived field's fn, so refactoring the compose fn
|
|
366
|
-
* while keeping its `(id, of, refField, reads)` identical does NOT change the domain hash.
|
|
367
|
-
*/
|
|
368
|
-
export interface CanonicalCombined {
|
|
369
|
-
readonly id: string;
|
|
370
|
-
/** The OWNER aggregate TYPE id the field lives on, e.g. `BuildingAggregate`. */
|
|
371
|
-
readonly of: string;
|
|
372
|
-
/** The owner's REF FIELD whose value is the related aggregate's id, e.g. `siteId`. */
|
|
373
|
-
readonly refField: string;
|
|
374
|
-
/** The RELATED aggregate TYPE the field reads (the invalidation key), e.g. `SiteRootAggregate`. */
|
|
375
|
-
readonly reads: string;
|
|
376
|
-
/** The projected value schema. The fn body is not hashed; the value contract is. */
|
|
377
|
-
readonly returns: CanonicalProjectionReturn;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* One captured MIN or MAX declaration — structurally identical to {@link CanonicalSum}
|
|
382
|
-
* (same `of`/`valueField`/`where?`/`by?` shape) except it carries an explicit `kind`
|
|
383
|
-
* discriminant (`"min"` | `"max"`). OMIT-WHEN-ABSENT: `where` and `by` follow the same
|
|
384
|
-
* omit-when-not-declared discipline as `CanonicalSum`.
|
|
385
|
-
*/
|
|
386
|
-
export interface CanonicalExtremum {
|
|
387
|
-
readonly id: string;
|
|
388
|
-
readonly kind: "min" | "max";
|
|
389
|
-
/** The aggregate TYPE id the extremum tallies. */
|
|
390
|
-
readonly of: string;
|
|
391
|
-
/** The `int`-kind field being extremised. */
|
|
392
|
-
readonly valueField: string;
|
|
393
|
-
/** The predicate filtering which aggregates contribute. OMITTED when no predicate. */
|
|
394
|
-
readonly where?: CanonicalPred;
|
|
395
|
-
/** The group-by field. OMITTED for a grand-total extremum. */
|
|
396
|
-
readonly by?: string;
|
|
397
|
-
/** THE ESTATE SCOPE (sharding §5, slice 8) — see {@link CanonicalCount.scope}:
|
|
398
|
-
* per-shard extremums ride the §5.2 delta lane as gate-adjudicated coordinator
|
|
399
|
-
* subtotals (extremize-on-monotone; retractions re-derive). OMITTED for
|
|
400
|
-
* home-scope extremums, so an unscoped declaration is byte-identical to before
|
|
401
|
-
* this key existed (golden hashes UNCHANGED). */
|
|
402
|
-
readonly scope?: string;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* One captured ORDERED READ declaration — an order-sensitive row read with a MANDATORY
|
|
407
|
-
* declared total order. `orderKey` is NON-OPTIONAL (a missing key is a manifest
|
|
408
|
-
* validation error, rejected LOUDLY at parse with no fallback). `limit` = 1 for `first`,
|
|
409
|
-
* `n` for `take(n)`. OMIT-WHEN-ABSENT: `where` is omitted when no predicate.
|
|
410
|
-
*/
|
|
411
|
-
export interface CanonicalOrderedRead {
|
|
412
|
-
readonly id: string;
|
|
413
|
-
/** The aggregate TYPE id the read returns. */
|
|
414
|
-
readonly of: string;
|
|
415
|
-
/** The REQUIRED total-order key (a declared scalar field of `of`). */
|
|
416
|
-
readonly orderKey: string;
|
|
417
|
-
/** Whether to sort descending. Default absent = ascending. */
|
|
418
|
-
readonly orderDesc?: true;
|
|
419
|
-
/** The optional predicate. OMITTED when no predicate. */
|
|
420
|
-
readonly where?: CanonicalPred;
|
|
421
|
-
/** LIMIT — 1 for `first`, n for `take(n)`. */
|
|
422
|
-
readonly limit: number;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* A workspace invariant's PRESENCE in the hashed identity surface (#266): its id + the
|
|
427
|
-
* directive it fires `on`. PRESENCE ONLY — the executable `reads`/`assert` bodies are NOT
|
|
428
|
-
* hashed (they ship in the engine bundle), exactly like the aggregate `hasInvariant` flag.
|
|
429
|
-
* Changing a body does NOT move the domain hash; adding/removing an invariant DOES (it is law).
|
|
430
|
-
*/
|
|
431
|
-
export interface CanonicalWorkspaceInvariant {
|
|
432
|
-
readonly id: string;
|
|
433
|
-
readonly on: string;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/** A domain's canonical semantic manifest — the hashed identity surface. */
|
|
437
|
-
export interface CanonicalManifest {
|
|
438
|
-
readonly domain: string;
|
|
439
|
-
readonly aggregates: CanonicalAggregate[];
|
|
440
|
-
readonly directives: CanonicalDirective[];
|
|
441
|
-
/**
|
|
442
|
-
* STABLE IDENTIFIERS (#58 — the foundation: names are LABELS, identity is MINTED by
|
|
443
|
-
* the compiler at first appearance, never dev-assigned). Carries `{stableId, name}`
|
|
444
|
-
* for the domain, every aggregate, and every field — keyed by CURRENT name, each
|
|
445
|
-
* entry holding its `sid` plus the label lineage `was` (prior names, oldest first;
|
|
446
|
-
* compiler-derived from continuity, never dev-written). ALWAYS EMITTED by the post-
|
|
447
|
-
* #58 compiler — this key MOVES every enforcement-era identity hash (named and
|
|
448
|
-
* expected; inert-era artifacts already built stay byte-identical untouched). The
|
|
449
|
-
* EVOLVE GATE diffs old vs new law BY SID; the projection folds via the sid lineage
|
|
450
|
-
* so a renamed entity's old-era data coheres under the new label.
|
|
451
|
-
*/
|
|
452
|
-
readonly stableIds?: StableIds;
|
|
453
|
-
/**
|
|
454
|
-
* The domain's WORKSPACE INVARIANTS (#266), SORTED by id, PRESENCE ONLY (id + `on`).
|
|
455
|
-
* OMITTED ENTIRELY when the domain declares none — so an invariant-free domain is
|
|
456
|
-
* byte-identical to before this key existed (pinned golden hashes UNCHANGED).
|
|
457
|
-
*/
|
|
458
|
-
readonly workspaceInvariants?: CanonicalWorkspaceInvariant[];
|
|
459
|
-
/**
|
|
460
|
-
* The domain's NAMED, INDEXED read declarations, SORTED by id. OMITTED ENTIRELY
|
|
461
|
-
* when the domain declares no query — so a query-free domain is byte-identical to
|
|
462
|
-
* before this key existed (the pinned golden hash stays UNCHANGED). Each query's
|
|
463
|
-
* `key` preserves its DECLARED order (index column order), unlike a directive's
|
|
464
|
-
* incidental read SET.
|
|
465
|
-
*/
|
|
466
|
-
readonly queries?: CanonicalQuery[];
|
|
467
|
-
/**
|
|
468
|
-
* The domain's NAMED, MAINTAINED count declarations, SORTED by id. OMITTED ENTIRELY
|
|
469
|
-
* when the domain declares no count — so a count-free domain is byte-identical to
|
|
470
|
-
* before this key existed (the pinned golden hash stays UNCHANGED), exactly like
|
|
471
|
-
* {@link queries}. The aggregation analogue of the read-side closure. Each entry may
|
|
472
|
-
* carry an optional `where` predicate (Slice 1 — omit-when-absent; predicate-free
|
|
473
|
-
* counts are byte-identical to before `where` was introduced).
|
|
474
|
-
*/
|
|
475
|
-
readonly counts?: CanonicalCount[];
|
|
476
|
-
/**
|
|
477
|
-
* The domain's NAMED, MAINTAINED SPATIAL (R*Tree) index declarations, SORTED by id.
|
|
478
|
-
* OMITTED ENTIRELY when the domain declares no spatial index — so a spatial-free domain is
|
|
479
|
-
* byte-identical to before this key existed (the pinned golden hash stays UNCHANGED),
|
|
480
|
-
* exactly like {@link counts}. The geospatial analogue of the read-side closure. Each entry
|
|
481
|
-
* carries `{id, of, on}` — the geometry field whose bounding box the engine maintains.
|
|
482
|
-
*/
|
|
483
|
-
readonly spatials?: CanonicalSpatial[];
|
|
484
|
-
/**
|
|
485
|
-
* The domain's NAMED, MAINTAINED sum declarations, SORTED by id. OMITTED ENTIRELY
|
|
486
|
-
* when the domain declares no sum — so a sum-free domain is byte-identical to before
|
|
487
|
-
* this key existed (the pinned golden hash stays UNCHANGED), exactly like
|
|
488
|
-
* {@link counts}. The numeric-accumulation analogue of count (Slice 1).
|
|
489
|
-
*/
|
|
490
|
-
readonly sums?: CanonicalSum[];
|
|
491
|
-
/**
|
|
492
|
-
* The domain's DERIVED read-field declarations, SORTED by id. OMITTED ENTIRELY when the
|
|
493
|
-
* domain declares no derived field — so a derived-free domain is byte-identical to before
|
|
494
|
-
* this key existed (the pinned golden hash stays UNCHANGED), exactly like {@link counts}.
|
|
495
|
-
* Each carries ONLY `{id, of}` — the fn body is NOT hashed (executable behaviour, like a
|
|
496
|
-
* directive's `plan`).
|
|
497
|
-
*/
|
|
498
|
-
readonly deriveds?: CanonicalDerived[];
|
|
499
|
-
/**
|
|
500
|
-
* The domain's COMBINED read-field declarations, SORTED by id. OMITTED ENTIRELY when the
|
|
501
|
-
* domain declares no combined field — so a combined-free domain is byte-identical to
|
|
502
|
-
* before this key existed (the pinned golden hash stays UNCHANGED), exactly like
|
|
503
|
-
* {@link deriveds}. Each carries `{id, of, refField, reads}` — the fn body is NOT hashed.
|
|
504
|
-
*/
|
|
505
|
-
readonly combineds?: CanonicalCombined[];
|
|
506
|
-
/**
|
|
507
|
-
* The domain's MAINTAINED MIN declarations, SORTED by id. OMITTED ENTIRELY when the
|
|
508
|
-
* domain declares no min — so a min-free domain is byte-identical to before this key
|
|
509
|
-
* existed (golden hashes unchanged). Same omit-when-absent discipline as `sums`.
|
|
510
|
-
*/
|
|
511
|
-
readonly mins?: CanonicalExtremum[];
|
|
512
|
-
/**
|
|
513
|
-
* The domain's MAINTAINED MAX declarations, SORTED by id. OMITTED ENTIRELY when the
|
|
514
|
-
* domain declares no max. Same discipline as `mins`.
|
|
515
|
-
*/
|
|
516
|
-
readonly maxes?: CanonicalExtremum[];
|
|
517
|
-
/**
|
|
518
|
-
* The domain's ORDERED ROW READ declarations (first/take.orderBy), SORTED by id.
|
|
519
|
-
* OMITTED ENTIRELY when the domain declares none. Each carries the REQUIRED `orderKey`
|
|
520
|
-
* (non-optional at both TS and manifest levels), optional predicate, and `limit`.
|
|
521
|
-
*/
|
|
522
|
-
readonly orderedReads?: CanonicalOrderedRead[];
|
|
523
|
-
/**
|
|
524
|
-
* The domain's FIRST-CLASS WORKSPACE TYPES (sharding §10 RATIFIED, slice 1), SORTED
|
|
525
|
-
* by id. OMITTED ENTIRELY when the domain declares none — so a taxonomy-free domain
|
|
526
|
-
* is byte-identical to before this key existed (pinned golden hashes UNCHANGED; the
|
|
527
|
-
* `cap(n)` discipline). The taxonomy IS law: a type's root/mode/children/globals/
|
|
528
|
-
* pool/cap all move the domain hash (per-type optional keys omitted-when-absent).
|
|
529
|
-
*/
|
|
530
|
-
readonly workspaceTypes?: CanonicalWorkspaceType[];
|
|
531
|
-
/**
|
|
532
|
-
* The DERIVED HOMING TABLE (aggregate wire id → workspace-type id) — present iff
|
|
533
|
-
* {@link workspaceTypes} is. Derived (never hand-written) by the fail-closed homing
|
|
534
|
-
* walk over the aggregates' `t.ref` chains, and HASH-BEARING: a homing move is a
|
|
535
|
-
* law change (placement of FACTS is custody, but which chain a fact is BORN into
|
|
536
|
-
* is law — the shard gate's `wrong-home` invariant pins to this table).
|
|
537
|
-
*/
|
|
538
|
-
readonly homes?: Record<string, string>;
|
|
539
|
-
/**
|
|
540
|
-
* THE DERIVED DIRECTIVE ROUTING TABLE (sharding slice 2) — present only when the
|
|
541
|
-
* domain declares a taxonomy with at least one packed-homed directive, SORTED by
|
|
542
|
-
* directive id, HASH-BEARING (the client routes by it; the shard gate's typed
|
|
543
|
-
* `wrong-home` refusal pins to it). OMITTED ENTIRELY otherwise (hash-stable).
|
|
544
|
-
*/
|
|
545
|
-
readonly routes?: CanonicalDirectiveRoute[];
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/** Minimal structural view of Zod 4 schema internals we read deterministically. */
|
|
549
|
-
interface ZodLike {
|
|
550
|
-
_def?: {
|
|
551
|
-
type?: string;
|
|
552
|
-
innerType?: ZodLike;
|
|
553
|
-
shape?: Record<string, ZodLike> | (() => Record<string, ZodLike>);
|
|
554
|
-
};
|
|
555
|
-
def?: {
|
|
556
|
-
type?: string;
|
|
557
|
-
innerType?: ZodLike;
|
|
558
|
-
shape?: Record<string, ZodLike> | (() => Record<string, ZodLike>);
|
|
559
|
-
};
|
|
560
|
-
shape?: Record<string, ZodLike>;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function zodDef(zod: ZodLike): NonNullable<ZodLike["_def"]> {
|
|
564
|
-
return zod._def ?? zod.def ?? {};
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function zodTypeName(zod: ZodLike): string {
|
|
568
|
-
const def = zodDef(zod);
|
|
569
|
-
return def.type ?? "unknown";
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function zodObjectShape(zod: ZodLike): Record<string, ZodLike> | undefined {
|
|
573
|
-
const shape = zodDef(zod).shape;
|
|
574
|
-
if (typeof shape === "function") return shape();
|
|
575
|
-
if (shape !== undefined) return shape;
|
|
576
|
-
return zod.shape;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Sort an object's keys so the schema's field order is irrelevant to identity. The
|
|
581
|
-
* authored field order on an aggregate or payload is incidental; only the SET of
|
|
582
|
-
* field→driver / field→type pairs is the contract.
|
|
583
|
-
*/
|
|
584
|
-
function sortedSchema(schema: WireSchema): WireSchema {
|
|
585
|
-
const out: WireSchema = {};
|
|
586
|
-
for (const key of Object.keys(schema).sort()) out[key] = schema[key]!;
|
|
587
|
-
return out;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Unwrap ZodOptional / ZodDefault to recover `{ optional, type }`. The OPTIONALITY
|
|
592
|
-
* is part of the contract (an optional field is a different shape); the wrapper
|
|
593
|
-
* identity (Optional vs Default) is NOT — both mean "may be absent" to a caller.
|
|
594
|
-
*/
|
|
595
|
-
function unwrapPayloadType(zod: ZodLike): { optional: boolean; type: string } {
|
|
596
|
-
let cur = zod;
|
|
597
|
-
let optional = false;
|
|
598
|
-
// Peel optional/default wrappers (possibly nested) to the inner contract type.
|
|
599
|
-
while (zodTypeName(cur) === "optional" || zodTypeName(cur) === "default") {
|
|
600
|
-
optional = true;
|
|
601
|
-
const inner = zodDef(cur).innerType;
|
|
602
|
-
if (inner === undefined) break;
|
|
603
|
-
cur = inner;
|
|
604
|
-
}
|
|
605
|
-
return { optional, type: zodTypeName(cur) };
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function unwrapProjectionReturn(zod: ZodLike): { nullable: boolean; type: string } {
|
|
609
|
-
let cur = zod;
|
|
610
|
-
let nullable = false;
|
|
611
|
-
while (
|
|
612
|
-
zodTypeName(cur) === "optional" ||
|
|
613
|
-
zodTypeName(cur) === "default" ||
|
|
614
|
-
zodTypeName(cur) === "nullable"
|
|
615
|
-
) {
|
|
616
|
-
nullable = true;
|
|
617
|
-
const inner = zodDef(cur).innerType;
|
|
618
|
-
if (inner === undefined) break;
|
|
619
|
-
cur = inner;
|
|
620
|
-
}
|
|
621
|
-
return { nullable, type: zodTypeName(cur) };
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function canonicalProjectionReturn(schema: unknown): CanonicalProjectionReturn {
|
|
625
|
-
const { nullable, type } = unwrapProjectionReturn(schema as ZodLike);
|
|
626
|
-
return {
|
|
627
|
-
type,
|
|
628
|
-
nullable,
|
|
629
|
-
jsonSchema: z.toJSONSchema(schema as z.ZodTypeAny),
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Canonically capture a directive's payload: the top-level object's fields as
|
|
635
|
-
* SORTED `[name, type]` pairs (with optionality). A non-object top-level schema
|
|
636
|
-
* (rare) captures as a single synthetic field carrying its type name.
|
|
637
|
-
*/
|
|
638
|
-
function canonicalPayload(payloadSchema: unknown): CanonicalPayloadField[] {
|
|
639
|
-
const zod = payloadSchema as ZodLike;
|
|
640
|
-
const shape = zodObjectShape(zod);
|
|
641
|
-
if (zodTypeName(zod) !== "object" || shape === undefined) {
|
|
642
|
-
// Not a top-level object: thing the bare type so a shape change still moves
|
|
643
|
-
// the hash, without inventing field structure.
|
|
644
|
-
return [{ name: "", type: zodTypeName(zod), optional: false }];
|
|
645
|
-
}
|
|
646
|
-
return Object.keys(shape)
|
|
647
|
-
.sort()
|
|
648
|
-
.map((name) => {
|
|
649
|
-
const { optional, type } = unwrapPayloadType(shape[name]!);
|
|
650
|
-
return { name, type, optional };
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function canonicalPayloadJsonSchema(payloadSchema: unknown): unknown {
|
|
655
|
-
return z.toJSONSchema(payloadSchema as z.ZodTypeAny);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Canonicalize a directive's declared READ boundary into a manifest fragment.
|
|
660
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no `reads` key at all) when nothing is declared —
|
|
661
|
-
* the omission, not an empty array, is what keeps boundary-free hashes unchanged.
|
|
662
|
-
* When non-empty, returns a SORTED `reads` array (order is incidental to the boundary).
|
|
663
|
-
*/
|
|
664
|
-
function canonicalReads(declared: string[]): { reads?: string[] } {
|
|
665
|
-
if (declared.length === 0) return {};
|
|
666
|
-
return { reads: [...declared].sort() };
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/** Canonicalize a directive's MULTI-OUTPUT secondary writes → `{ target, marker }` sorted by (target,
|
|
670
|
-
* marker). Omit-when-empty (the era rule — a single-output directive contributes no key). */
|
|
671
|
-
function canonicalSecondaryOutputs(
|
|
672
|
-
declared: readonly { marker: ReferentialMarker; aggregateId: string }[] | undefined,
|
|
673
|
-
): { secondaryOutputs?: { target: string; marker: WireRefMode }[] } {
|
|
674
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
675
|
-
const out = declared
|
|
676
|
-
.map((s) => ({ target: s.aggregateId, marker: encodeRefMode(s.marker) }))
|
|
677
|
-
.sort((a, b) => (a.target !== b.target ? (a.target < b.target ? -1 : 1) : a.marker < b.marker ? -1 : a.marker > b.marker ? 1 : 0));
|
|
678
|
-
return { secondaryOutputs: out };
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Canonicalize a directive's declared required HOST CAPABILITY ports into a manifest fragment
|
|
683
|
-
* (the HOST/PORT axis, invariant 3). OMIT-WHEN-EMPTY: returns `{}` (no `requiresHostCapability`
|
|
684
|
-
* key at all) when the directive declares none — the omission, not an empty array, is what keeps
|
|
685
|
-
* host-capability-free hashes UNCHANGED. When non-empty, returns a SORTED `requiresHostCapability`
|
|
686
|
-
* array (order is incidental to the port SET). DISTINCT from `canonicalReads`/`requiresCapability`
|
|
687
|
-
* (the two axes — HOST vs AUTHZ — never conflate in the manifest).
|
|
688
|
-
*/
|
|
689
|
-
function canonicalHostCapabilities(declared: string[]): { requiresHostCapability?: string[] } {
|
|
690
|
-
if (declared.length === 0) return {};
|
|
691
|
-
return { requiresHostCapability: [...declared].sort() };
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* Canonicalize a directive's declared EMIT boundary into a manifest fragment.
|
|
696
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no `emits` key) when nothing is declared. When
|
|
697
|
-
* non-empty, returns an `emits` object whose keys `canonicalJson` will sort, each
|
|
698
|
-
* value `{ max }` only when a bound is set (so `{}` vs `{max}` differs in identity).
|
|
699
|
-
*/
|
|
700
|
-
function canonicalEmits(
|
|
701
|
-
declared: Record<string, { max?: number }>,
|
|
702
|
-
): { emits?: Record<string, { max?: number }> } {
|
|
703
|
-
const keys = Object.keys(declared);
|
|
704
|
-
if (keys.length === 0) return {};
|
|
705
|
-
const emits: Record<string, { max?: number }> = {};
|
|
706
|
-
for (const key of keys.sort()) {
|
|
707
|
-
const max = declared[key]!.max;
|
|
708
|
-
emits[key] = max !== undefined ? { max } : {};
|
|
709
|
-
}
|
|
710
|
-
return { emits };
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Canonicalize a directive's DECLARED captured-read queries (#58) into a manifest
|
|
715
|
-
* fragment. OMIT-WHEN-EMPTY: returns `{}` (no `capturedReads` key) when the directive
|
|
716
|
-
* declares none — the omission keeps read-free hashes unchanged. When non-empty,
|
|
717
|
-
* returns a `capturedReads` array SORTED by queryId, each carrying the read recipe
|
|
718
|
-
* `{queryId, key, returns}` (key in DECLARED order — index column order).
|
|
719
|
-
*/
|
|
720
|
-
function canonicalCapturedReads(
|
|
721
|
-
declared: QueryDecl[],
|
|
722
|
-
): { capturedReads?: CanonicalCapturedRead[] } {
|
|
723
|
-
if (declared.length === 0) return {};
|
|
724
|
-
const capturedReads: CanonicalCapturedRead[] = declared
|
|
725
|
-
.map((q) => ({ queryId: q.id, key: [...q.key], returns: q.returns }))
|
|
726
|
-
.sort((a, b) => (a.queryId < b.queryId ? -1 : a.queryId > b.queryId ? 1 : 0));
|
|
727
|
-
return { capturedReads };
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Canonicalize a directive's declared cross-workspace relations into a manifest fragment
|
|
732
|
-
* (`cross_workspace.md` §2). OMIT-WHEN-EMPTY: returns `{}` (no `relations` key) when the
|
|
733
|
-
* directive declares none — the omission keeps relation-free hashes unchanged. When non-empty,
|
|
734
|
-
* a `relations` array SORTED BY relation `id`, each carrying the endpoints + the bounded
|
|
735
|
-
* evidence read (SELECT fields SORTED) + `hasInvariant` (presence only). A recipe change
|
|
736
|
-
* (aggregate/fields/predicate) MOVES the domain hash — a different relation is a different
|
|
737
|
-
* contract. The invariant PREDICATE body is NOT captured (executable behaviour, like `plan`).
|
|
738
|
-
*/
|
|
739
|
-
function canonicalRelations(
|
|
740
|
-
declared: RelationDecl[],
|
|
741
|
-
): { relations?: CanonicalRelation[] } {
|
|
742
|
-
if (declared.length === 0) return {};
|
|
743
|
-
const relations: CanonicalRelation[] = declared
|
|
744
|
-
.map((r) => ({
|
|
745
|
-
id: r.id,
|
|
746
|
-
source: r.source,
|
|
747
|
-
target: r.target,
|
|
748
|
-
proposes: r.proposes,
|
|
749
|
-
evidence: {
|
|
750
|
-
aggregate: r.evidence.aggregate,
|
|
751
|
-
select: [...r.evidence.select].sort(),
|
|
752
|
-
where: r.evidence.where,
|
|
753
|
-
kind: r.evidence.kind,
|
|
754
|
-
},
|
|
755
|
-
hasInvariant: r.hasInvariant,
|
|
756
|
-
}))
|
|
757
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
758
|
-
return { relations };
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Canonicalize a domain's declared QUERIES into a manifest fragment.
|
|
763
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no `queries` key at all) when the domain declares
|
|
764
|
-
* none — the omission, not an empty array, is what keeps query-free hashes unchanged.
|
|
765
|
-
* When non-empty, returns a `queries` array SORTED by id (the SET of queries is the
|
|
766
|
-
* contract), each carrying its `key` in DECLARED order (index column order, NOT
|
|
767
|
-
* sorted) and its `returns` aggregate type id.
|
|
768
|
-
*/
|
|
769
|
-
function canonicalQueries(
|
|
770
|
-
declared: QueryDecl[] | undefined,
|
|
771
|
-
): { queries?: CanonicalQuery[] } {
|
|
772
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
773
|
-
const queries: CanonicalQuery[] = [...declared]
|
|
774
|
-
.map((q) => ({ id: q.id, key: [...q.key], returns: q.returns }))
|
|
775
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
776
|
-
return { queries };
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* Canonicalize a domain's WORKSPACE INVARIANTS into a manifest fragment (#266) — the
|
|
781
|
-
* presence-only analogue of {@link canonicalQueries}. OMIT-WHEN-EMPTY: returns `{}` (no key)
|
|
782
|
-
* when none are declared, so an invariant-free domain hashes identically to before. Each entry
|
|
783
|
-
* is PRESENCE ONLY (id + the directive it is `on`), SORTED by id; the executable `reads`/
|
|
784
|
-
* `assert` bodies are NEVER hashed (they ship in the engine bundle). So changing a body leaves
|
|
785
|
-
* the hash unchanged, while adding/removing an invariant moves it (it is law — #131).
|
|
786
|
-
*/
|
|
787
|
-
function canonicalWorkspaceInvariants(
|
|
788
|
-
declared: WorkspaceInvariantDecl[] | undefined,
|
|
789
|
-
): { workspaceInvariants?: CanonicalWorkspaceInvariant[] } {
|
|
790
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
791
|
-
const workspaceInvariants: CanonicalWorkspaceInvariant[] = [...declared]
|
|
792
|
-
.map((wi) => ({ id: wi.id, on: wi.on }))
|
|
793
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
794
|
-
return { workspaceInvariants };
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* Canonicalize a domain's declared COUNTS into a manifest fragment — the aggregation
|
|
799
|
-
* analogue of {@link canonicalQueries}.
|
|
800
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no `counts` key at all) when the domain declares none —
|
|
801
|
-
* the omission, not an empty array, is what keeps count-free hashes unchanged. When
|
|
802
|
-
* non-empty, returns a `counts` array SORTED by id (the SET of counts is the contract),
|
|
803
|
-
* each carrying its `of`-type and — only when declared — its `by` group-by field (a
|
|
804
|
-
* grand-total count omits `by` entirely, so it adds no phantom key to the hash).
|
|
805
|
-
*/
|
|
806
|
-
function canonicalCounts(
|
|
807
|
-
declared: AnyCount[] | undefined,
|
|
808
|
-
): { counts?: CanonicalCount[] } {
|
|
809
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
810
|
-
const counts: CanonicalCount[] = [...declared]
|
|
811
|
-
.map((raw) => ({ finished: finishCount(raw), scope: (raw as { scope?: unknown }).scope }))
|
|
812
|
-
.map(({ finished: c, scope }) => ({
|
|
813
|
-
id: c.id,
|
|
814
|
-
of: c.of,
|
|
815
|
-
// OMIT-WHEN-ABSENT: `where`, `by` and `scope` are omitted when not declared so
|
|
816
|
-
// a predicate-free / grand-total / home-scope count is byte-identical to the
|
|
817
|
-
// legacy form (hash-stable — the same discipline as every other optional key).
|
|
818
|
-
...(c.where !== undefined ? { where: c.where } : {}),
|
|
819
|
-
...(c.by !== undefined ? { by: c.by } : {}),
|
|
820
|
-
...(typeof scope === "string" ? { scope } : {}),
|
|
821
|
-
}))
|
|
822
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
823
|
-
return { counts };
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* Canonicalize a domain's declared SPATIAL indexes into a manifest fragment — the
|
|
828
|
-
* geospatial analogue of {@link canonicalCounts}.
|
|
829
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no `spatials` key at all) when the domain declares none —
|
|
830
|
-
* the omission, not an empty array, is what keeps spatial-free hashes unchanged. When
|
|
831
|
-
* non-empty, returns a `spatials` array SORTED by id (the SET of spatial indexes is the
|
|
832
|
-
* contract), each carrying its `of`-type and the `on` geometry field whose bounding box is
|
|
833
|
-
* indexed.
|
|
834
|
-
*/
|
|
835
|
-
function canonicalSpatials(
|
|
836
|
-
declared: SpatialDecl[] | undefined,
|
|
837
|
-
): { spatials?: CanonicalSpatial[] } {
|
|
838
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
839
|
-
const spatials: CanonicalSpatial[] = [...declared]
|
|
840
|
-
.map((s) => ({ id: s.id, of: s.of, on: s.on }))
|
|
841
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
842
|
-
return { spatials };
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
/**
|
|
846
|
-
* Canonicalize a domain's declared SUMS into a manifest fragment — the numeric-
|
|
847
|
-
* accumulation analogue of {@link canonicalCounts}.
|
|
848
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no `sums` key at all) when the domain declares none.
|
|
849
|
-
* When non-empty, returns a `sums` array SORTED by id. `where` and `by` are omitted
|
|
850
|
-
* when not declared (same omit-when-absent discipline as `CanonicalCount`).
|
|
851
|
-
*/
|
|
852
|
-
function canonicalSums(declared: AnySum[] | undefined): { sums?: CanonicalSum[] } {
|
|
853
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
854
|
-
const sums: CanonicalSum[] = [...declared]
|
|
855
|
-
.map((raw) => ({ finished: finishSum(raw), scope: (raw as { scope?: unknown }).scope }))
|
|
856
|
-
.map(({ finished: s, scope }) => ({
|
|
857
|
-
id: s.id,
|
|
858
|
-
of: s.of,
|
|
859
|
-
sumField: s.sumField,
|
|
860
|
-
...(s.where !== undefined ? { where: s.where } : {}),
|
|
861
|
-
...(s.by !== undefined ? { by: s.by } : {}),
|
|
862
|
-
...(typeof scope === "string" ? { scope } : {}),
|
|
863
|
-
}))
|
|
864
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
865
|
-
return { sums };
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* Canonicalize a domain's declared DERIVED read fields into a manifest fragment — the
|
|
870
|
-
* engine-projected analogue of {@link canonicalCounts}.
|
|
871
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no `deriveds` key at all) when the domain declares none —
|
|
872
|
-
* the omission, not an empty array, is what keeps derived-free hashes unchanged. When
|
|
873
|
-
* non-empty, returns a `deriveds` array SORTED by id (the SET of derived fields is the
|
|
874
|
-
* contract), each carrying ONLY its `{id, of}` — the fn BODY is NOT hashed (executable
|
|
875
|
-
* behaviour, exactly like a directive's `plan` body, which this manifest also omits).
|
|
876
|
-
*/
|
|
877
|
-
function canonicalDeriveds(
|
|
878
|
-
declared: DerivedDecl[] | undefined,
|
|
879
|
-
): { deriveds?: CanonicalDerived[] } {
|
|
880
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
881
|
-
const deriveds: CanonicalDerived[] = [...declared]
|
|
882
|
-
.map((d) => ({ id: d.id, of: d.of, returns: canonicalProjectionReturn(d.returns) }))
|
|
883
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
884
|
-
return { deriveds };
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* Canonicalize a domain's declared COMBINED read fields into a manifest fragment — the
|
|
889
|
-
* cross-aggregate JOIN analogue of {@link canonicalDeriveds}.
|
|
890
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no `combineds` key at all) when the domain declares none —
|
|
891
|
-
* the omission, not an empty array, is what keeps combined-free hashes unchanged. When
|
|
892
|
-
* non-empty, returns a `combineds` array SORTED by id (the SET of combined fields is the
|
|
893
|
-
* contract), each carrying `{id, of, refField, reads}` — the JOIN edge + invalidation key
|
|
894
|
-
* are part of the contract; the fn BODY is NOT hashed (executable behaviour, like a
|
|
895
|
-
* directive's `plan` body, which this manifest also omits).
|
|
896
|
-
*/
|
|
897
|
-
function canonicalCombineds(
|
|
898
|
-
declared: CombinedDecl[] | undefined,
|
|
899
|
-
): { combineds?: CanonicalCombined[] } {
|
|
900
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
901
|
-
const combineds: CanonicalCombined[] = [...declared]
|
|
902
|
-
.map((c) => ({
|
|
903
|
-
id: c.id,
|
|
904
|
-
of: c.of,
|
|
905
|
-
refField: c.refField,
|
|
906
|
-
reads: c.reads,
|
|
907
|
-
returns: canonicalProjectionReturn(c.returns),
|
|
908
|
-
}))
|
|
909
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
910
|
-
return { combineds };
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
/**
|
|
914
|
-
* Canonicalize a domain's declared MINS or MAXES into a manifest fragment.
|
|
915
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no key at all) when the domain declares none.
|
|
916
|
-
* When non-empty, returns the array SORTED by id. `where` and `by` follow the same
|
|
917
|
-
* omit-when-not-declared discipline as {@link canonicalSums}.
|
|
918
|
-
*/
|
|
919
|
-
function canonicalExtrema(
|
|
920
|
-
declared: AnyExtremum[] | undefined,
|
|
921
|
-
keyName: "mins" | "maxes",
|
|
922
|
-
): { mins?: CanonicalExtremum[] } | { maxes?: CanonicalExtremum[] } {
|
|
923
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
924
|
-
const items: CanonicalExtremum[] = [...declared]
|
|
925
|
-
.map((raw) => ({ finished: finishExtremum(raw), scope: (raw as { scope?: unknown }).scope }))
|
|
926
|
-
.map(({ finished: e, scope }) => ({
|
|
927
|
-
id: e.id,
|
|
928
|
-
kind: e.kind,
|
|
929
|
-
of: e.of,
|
|
930
|
-
valueField: e.valueField,
|
|
931
|
-
...(e.where !== undefined ? { where: e.where } : {}),
|
|
932
|
-
...(e.by !== undefined ? { by: e.by } : {}),
|
|
933
|
-
// OMIT-WHEN-ABSENT (the canonicalSums discipline): a home-scope extremum is
|
|
934
|
-
// byte-identical to before the scope key existed — hash-stable by default.
|
|
935
|
-
...(typeof scope === "string" ? { scope } : {}),
|
|
936
|
-
}))
|
|
937
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
938
|
-
return { [keyName]: items };
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
/**
|
|
942
|
-
* Canonicalize a domain's declared ORDERED READS (first/take) into a manifest fragment.
|
|
943
|
-
* OMIT-WHEN-EMPTY: returns `{}` (no key at all) when the domain declares none.
|
|
944
|
-
* When non-empty, returns an `orderedReads` array SORTED by id. `where` is omitted when
|
|
945
|
-
* not declared; `orderDesc` is omitted when false (default = ascending). `orderKey` is
|
|
946
|
-
* ALWAYS present (it is required — never optional).
|
|
947
|
-
*/
|
|
948
|
-
function canonicalOrderedReads(
|
|
949
|
-
declared: OrderedReadDecl[] | undefined,
|
|
950
|
-
): { orderedReads?: CanonicalOrderedRead[] } {
|
|
951
|
-
if (declared === undefined || declared.length === 0) return {};
|
|
952
|
-
const items: CanonicalOrderedRead[] = [...declared]
|
|
953
|
-
.map((d) => ({
|
|
954
|
-
id: d.id,
|
|
955
|
-
of: d.of,
|
|
956
|
-
orderKey: d.orderKey,
|
|
957
|
-
// OMIT-WHEN-DEFAULT: only include `orderDesc` when it is `true` (non-default).
|
|
958
|
-
...(d.orderDesc ? { orderDesc: true as const } : {}),
|
|
959
|
-
...(d.where !== undefined ? { where: d.where } : {}),
|
|
960
|
-
limit: d.limit,
|
|
961
|
-
}))
|
|
962
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
963
|
-
return { orderedReads: items };
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
/**
|
|
967
|
-
* The domain's STABLE-ID surface (#58): continuity-carried when the compiler attached
|
|
968
|
-
* `mod.stableIdContinuity` (renames resolved, sids + lineage carried), deterministic
|
|
969
|
-
* first-appearance minting otherwise — so the manifest stays a pure function of the
|
|
970
|
-
* module (+ its attached continuity). Any aggregate/field the continuity does not
|
|
971
|
-
* cover (a genuinely new appearance) mints fresh.
|
|
972
|
-
*/
|
|
973
|
-
function stableIdsForModule(
|
|
974
|
-
mod: DomainModule,
|
|
975
|
-
aggregates: CanonicalAggregate[],
|
|
976
|
-
): StableIds {
|
|
977
|
-
const continuity = mod.stableIdContinuity;
|
|
978
|
-
const domainSid = continuity?.domain ?? mintDomainSid(mod.name);
|
|
979
|
-
// The CURRENT field KINDS off the live handles (the retype arm's identity input —
|
|
980
|
-
// a `t.string()` → `t.int()` retype moves `kind` while the driver stays Lww).
|
|
981
|
-
const handlesById = new Map(mod.aggregates.map((a) => [a.id, a]));
|
|
982
|
-
const out: Record<string, StableAggregateId> = {};
|
|
983
|
-
for (const agg of aggregates) {
|
|
984
|
-
const prior = continuity?.aggregates[agg.id];
|
|
985
|
-
const aggSid = prior?.sid ?? mintAggregateSid(domainSid, agg.id);
|
|
986
|
-
const handleFields = (handlesById.get(agg.id)?.fields ?? {}) as Record<string, Field | undefined>;
|
|
987
|
-
const fields: Record<string, StableFieldId> = {};
|
|
988
|
-
for (const fieldName of Object.keys(agg.schema).sort()) {
|
|
989
|
-
const pf = prior?.fields[fieldName];
|
|
990
|
-
const kind = handleFields[fieldName]?.kind;
|
|
991
|
-
const handle = handleFields[fieldName];
|
|
992
|
-
const details = handle === undefined ? undefined : fieldValueContractDetails(handle);
|
|
993
|
-
const driver = handle === undefined ? undefined : encodeDriver(handle.driver);
|
|
994
|
-
const schema =
|
|
995
|
-
handle === undefined || kind === undefined || details === undefined || driver === undefined
|
|
996
|
-
? undefined
|
|
997
|
-
: fieldSchemaIdentity(aggSid, pf?.sid ?? mintFieldSid(aggSid, fieldName), kind, details.contract, driver);
|
|
998
|
-
const evolutions =
|
|
999
|
-
handle === undefined || schema === undefined || driver === undefined || details === undefined
|
|
1000
|
-
? []
|
|
1001
|
-
: fieldEvolutionRules(handle, pf, aggSid, pf?.sid ?? mintFieldSid(aggSid, fieldName), details, driver, schema);
|
|
1002
|
-
fields[fieldName] = {
|
|
1003
|
-
sid: pf?.sid ?? mintFieldSid(aggSid, fieldName),
|
|
1004
|
-
...(pf?.was !== undefined && pf.was.length > 0 ? { was: pf.was } : {}),
|
|
1005
|
-
// The CURRENT kind, always from the live module (continuity never supplies it).
|
|
1006
|
-
...(kind !== undefined ? { kind } : {}),
|
|
1007
|
-
...(details !== undefined ? { contract: details.contract, shape: details.shape } : {}),
|
|
1008
|
-
...(schema !== undefined ? { schema } : {}),
|
|
1009
|
-
...(driver !== undefined ? { driver } : {}),
|
|
1010
|
-
...(evolutions.length > 0 ? { evolutions } : {}),
|
|
1011
|
-
// Birth-presence law rides identity: the evolve guard reads it (fromBirth may not be
|
|
1012
|
-
// NEWLY declared on a sid that existed without it).
|
|
1013
|
-
...(handleFields[fieldName]?.isFromBirth === true ? { fromBirth: true as const } : {}),
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
out[agg.id] = {
|
|
1017
|
-
sid: aggSid,
|
|
1018
|
-
...(prior?.was !== undefined && prior.was.length > 0 ? { was: prior.was } : {}),
|
|
1019
|
-
fields,
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
return { v: 1, domain: domainSid, aggregates: out };
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
/**
|
|
1026
|
-
* Identity of the value a generated TS/Dart conflict resolver is allowed to call `T`.
|
|
1027
|
-
* This is deliberately narrower than the domain hash (unrelated law may roll out without
|
|
1028
|
-
* invalidating a conflict) and richer than `Field.kind` (enum arms, value-object members,
|
|
1029
|
-
* nullability, references and encrypted/plain decoding all matter to the public type).
|
|
1030
|
-
*/
|
|
1031
|
-
function fieldValueContractDetails(field: Field): { contract: string; shape: unknown } {
|
|
1032
|
-
const valueObjectContract = (meta: NonNullable<Field["valueObject"]>) => ({
|
|
1033
|
-
descriptor: meta.descriptor,
|
|
1034
|
-
dependencies: meta.dependencies,
|
|
1035
|
-
...(meta.geometry !== undefined ? { geometry: meta.geometry } : {}),
|
|
1036
|
-
});
|
|
1037
|
-
const contractShape = normalizeFieldContract({
|
|
1038
|
-
v: "nomos-field-value-contract-v1",
|
|
1039
|
-
kind: field.kind,
|
|
1040
|
-
jsonSchema: z.toJSONSchema(field.zod as z.ZodTypeAny),
|
|
1041
|
-
...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
|
|
1042
|
-
...(field.jsonShape !== undefined ? { jsonShape: field.jsonShape } : {}),
|
|
1043
|
-
...(field.valueObject !== undefined
|
|
1044
|
-
? { valueObject: valueObjectContract(field.valueObject) }
|
|
1045
|
-
: {}),
|
|
1046
|
-
...(field.mapValueVo !== undefined
|
|
1047
|
-
? { mapValueVo: valueObjectContract(field.mapValueVo) }
|
|
1048
|
-
: {}),
|
|
1049
|
-
...(field.refAggregateId !== undefined ? { refAggregateId: field.refAggregateId } : {}),
|
|
1050
|
-
...(field.foreignRef !== undefined ? { foreignRef: field.foreignRef } : {}),
|
|
1051
|
-
...(field.nullSemantic !== undefined ? { nullSemantic: field.nullSemantic } : {}),
|
|
1052
|
-
...(field.isClearable === true ? { clearable: true } : {}),
|
|
1053
|
-
...(field.isEncrypted === true ? { encrypted: true } : {}),
|
|
1054
|
-
});
|
|
1055
|
-
const contract = createHash("sha256")
|
|
1056
|
-
.update(
|
|
1057
|
-
`nomos-field-value-contract:v1:${canonicalJson(contractShape)}`,
|
|
1058
|
-
"utf8",
|
|
1059
|
-
)
|
|
1060
|
-
.digest("hex");
|
|
1061
|
-
return { contract, shape: contractShape };
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
function fieldSchemaIdentity(
|
|
1065
|
-
aggregateSid: string,
|
|
1066
|
-
fieldSid: string,
|
|
1067
|
-
kind: string,
|
|
1068
|
-
valueContract: string,
|
|
1069
|
-
driver: unknown,
|
|
1070
|
-
): string {
|
|
1071
|
-
return createHash("sha256")
|
|
1072
|
-
.update(
|
|
1073
|
-
canonicalJson({
|
|
1074
|
-
v: "nomos-field-schema-v2",
|
|
1075
|
-
aggregate: aggregateSid,
|
|
1076
|
-
field: fieldSid,
|
|
1077
|
-
kind,
|
|
1078
|
-
valueContract,
|
|
1079
|
-
driver,
|
|
1080
|
-
}),
|
|
1081
|
-
"utf8",
|
|
1082
|
-
)
|
|
1083
|
-
.digest("hex");
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
function fieldEvolutionRules(
|
|
1087
|
-
field: Field,
|
|
1088
|
-
prior: StableFieldId | undefined,
|
|
1089
|
-
aggregateSid: string,
|
|
1090
|
-
fieldSid: string,
|
|
1091
|
-
target: { contract: string; shape: unknown },
|
|
1092
|
-
driver: unknown,
|
|
1093
|
-
targetSchema: string,
|
|
1094
|
-
): StableFieldEvolution[] {
|
|
1095
|
-
const bySource = new Map<string, StableFieldEvolution>();
|
|
1096
|
-
const add = (rule: StableFieldEvolution, origin: string) => {
|
|
1097
|
-
if (rule.from === rule.to) return;
|
|
1098
|
-
const existing = bySource.get(rule.from);
|
|
1099
|
-
if (existing !== undefined && canonicalJson(existing) !== canonicalJson(rule)) {
|
|
1100
|
-
throw new Error(
|
|
1101
|
-
`field evolution collision for source contract ${rule.from}: ${origin} disagrees with an existing route. ` +
|
|
1102
|
-
`One source contract must have exactly one deterministic destination.`,
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
bySource.set(rule.from, rule);
|
|
1106
|
-
};
|
|
1107
|
-
|
|
1108
|
-
for (const rule of prior?.evolutions ?? []) add(rule, "the prior release");
|
|
1109
|
-
|
|
1110
|
-
for (const declaration of field.evolutions ?? []) {
|
|
1111
|
-
const source = fieldValueContractDetails(declaration.source);
|
|
1112
|
-
const sourceSchema = fieldSchemaIdentity(
|
|
1113
|
-
aggregateSid,
|
|
1114
|
-
fieldSid,
|
|
1115
|
-
declaration.source.kind,
|
|
1116
|
-
source.contract,
|
|
1117
|
-
driver,
|
|
1118
|
-
);
|
|
1119
|
-
if (declaration.mode === "identity" && !safeIdentityProjection(source.shape, target.shape)) {
|
|
1120
|
-
throw new Error(
|
|
1121
|
-
`.evolvesFrom(source) is not a safe identity widening: some source values are not valid ` +
|
|
1122
|
-
`under the target field. Supply a finite business transform as the second argument.`,
|
|
1123
|
-
);
|
|
1124
|
-
}
|
|
1125
|
-
add(
|
|
1126
|
-
{
|
|
1127
|
-
from: sourceSchema,
|
|
1128
|
-
to: targetSchema,
|
|
1129
|
-
source: {
|
|
1130
|
-
kind: declaration.source.kind,
|
|
1131
|
-
...(declaration.source.mapValueKind !== undefined
|
|
1132
|
-
? { mapValueKind: declaration.source.mapValueKind }
|
|
1133
|
-
: {}),
|
|
1134
|
-
},
|
|
1135
|
-
mode: declaration.mode,
|
|
1136
|
-
...(declaration.cases !== undefined ? { cases: declaration.cases } : {}),
|
|
1137
|
-
},
|
|
1138
|
-
".evolvesFrom()",
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// The compiler silently owns genuinely compatible widening between consecutive releases.
|
|
1143
|
-
// The prior lock carries the full value descriptor, so this is a proof, not a cast.
|
|
1144
|
-
if (
|
|
1145
|
-
prior?.schema !== undefined &&
|
|
1146
|
-
prior.schema !== targetSchema &&
|
|
1147
|
-
prior.shape !== undefined &&
|
|
1148
|
-
canonicalJson(prior.driver) === canonicalJson(driver) &&
|
|
1149
|
-
safeAutomaticWidening(prior.shape, target.shape) &&
|
|
1150
|
-
!bySource.has(prior.schema)
|
|
1151
|
-
) {
|
|
1152
|
-
add(
|
|
1153
|
-
{
|
|
1154
|
-
from: prior.schema,
|
|
1155
|
-
to: targetSchema,
|
|
1156
|
-
source: {
|
|
1157
|
-
kind: prior.kind ?? field.kind,
|
|
1158
|
-
...((prior.shape as { mapValueKind?: string } | undefined)?.mapValueKind !== undefined
|
|
1159
|
-
? { mapValueKind: (prior.shape as { mapValueKind: string }).mapValueKind }
|
|
1160
|
-
: {}),
|
|
1161
|
-
},
|
|
1162
|
-
mode: "identity",
|
|
1163
|
-
},
|
|
1164
|
-
"the compiler's compatible-widening proof",
|
|
1165
|
-
);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Every retained source route must reach the one current target. This is the compile-time
|
|
1169
|
-
// completeness gate for all still-supported eras, not merely the immediately prior law.
|
|
1170
|
-
for (const source of bySource.keys()) {
|
|
1171
|
-
const seen = new Set<string>();
|
|
1172
|
-
let cursor = source;
|
|
1173
|
-
while (cursor !== targetSchema) {
|
|
1174
|
-
if (seen.has(cursor)) {
|
|
1175
|
-
throw new Error(`field evolution graph cycles at ${cursor}; every source must terminate at ${targetSchema}`);
|
|
1176
|
-
}
|
|
1177
|
-
seen.add(cursor);
|
|
1178
|
-
const next = bySource.get(cursor);
|
|
1179
|
-
if (next === undefined) {
|
|
1180
|
-
throw new Error(
|
|
1181
|
-
`field evolution route from ${source} stops at ${cursor}, not the current contract ${targetSchema}. ` +
|
|
1182
|
-
`Carry the intermediate source with .evolvesFrom(...) before this rollout.`,
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
cursor = next.to;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
return [...bySource.values()].sort((a, b) => (a.from < b.from ? -1 : a.from > b.from ? 1 : 0));
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
/** Explicit identity projection: the tenant declares the business meaning is continuous;
|
|
1193
|
-
* the compiler still proves every old value is accepted and its physical decoding is unchanged. */
|
|
1194
|
-
function safeIdentityProjection(sourceShape: unknown, targetShape: unknown): boolean {
|
|
1195
|
-
if (sourceShape === null || targetShape === null || typeof sourceShape !== "object" || typeof targetShape !== "object") {
|
|
1196
|
-
return canonicalJson(sourceShape) === canonicalJson(targetShape);
|
|
1197
|
-
}
|
|
1198
|
-
const source = sourceShape as Record<string, unknown>;
|
|
1199
|
-
const target = targetShape as Record<string, unknown>;
|
|
1200
|
-
const physical = (shape: Record<string, unknown>) => {
|
|
1201
|
-
const { kind, mapValueKind, jsonShape, encrypted } = shape;
|
|
1202
|
-
return { kind, mapValueKind, jsonShape, encrypted };
|
|
1203
|
-
};
|
|
1204
|
-
return canonicalJson(physical(source)) === canonicalJson(physical(target))
|
|
1205
|
-
&& jsonSchemaSubset(source.jsonSchema, target.jsonSchema);
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
/** Automatic widening is stricter: every non-schema business descriptor must remain
|
|
1209
|
-
* identical. Brands, units, references and null meaning require an explicit declaration. */
|
|
1210
|
-
function safeAutomaticWidening(sourceShape: unknown, targetShape: unknown): boolean {
|
|
1211
|
-
if (sourceShape === null || targetShape === null || typeof sourceShape !== "object" || typeof targetShape !== "object") {
|
|
1212
|
-
return canonicalJson(sourceShape) === canonicalJson(targetShape);
|
|
1213
|
-
}
|
|
1214
|
-
const source = sourceShape as Record<string, unknown>;
|
|
1215
|
-
const target = targetShape as Record<string, unknown>;
|
|
1216
|
-
const metadata = (shape: Record<string, unknown>) => {
|
|
1217
|
-
const { jsonSchema: _jsonSchema, ...rest } = shape;
|
|
1218
|
-
return rest;
|
|
1219
|
-
};
|
|
1220
|
-
return canonicalJson(metadata(source)) === canonicalJson(metadata(target))
|
|
1221
|
-
&& jsonSchemaSubset(source.jsonSchema, target.jsonSchema);
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
function jsonSchemaSubset(source: unknown, target: unknown): boolean {
|
|
1225
|
-
if (canonicalJson(source) === canonicalJson(target)) return true;
|
|
1226
|
-
if (source === null || target === null || typeof source !== "object" || typeof target !== "object") return false;
|
|
1227
|
-
const s = source as Record<string, unknown>;
|
|
1228
|
-
const t = target as Record<string, unknown>;
|
|
1229
|
-
if (Array.isArray(s.enum)) return s.enum.every((value) => jsonSchemaAcceptsLiteral(t, value));
|
|
1230
|
-
if (Object.prototype.hasOwnProperty.call(s, "const")) return jsonSchemaAcceptsLiteral(t, s.const);
|
|
1231
|
-
const sUnion = (Array.isArray(s.anyOf) ? s.anyOf : Array.isArray(s.oneOf) ? s.oneOf : undefined);
|
|
1232
|
-
if (sUnion !== undefined) return sUnion.every((arm) => jsonSchemaSubset(arm, target));
|
|
1233
|
-
const tUnion = (Array.isArray(t.anyOf) ? t.anyOf : Array.isArray(t.oneOf) ? t.oneOf : undefined);
|
|
1234
|
-
if (tUnion !== undefined) return tUnion.some((arm) => jsonSchemaSubset(source, arm));
|
|
1235
|
-
const sourceTypes = new Set(Array.isArray(s.type) ? s.type : s.type === undefined ? [] : [s.type]);
|
|
1236
|
-
const targetTypes = new Set(Array.isArray(t.type) ? t.type : t.type === undefined ? [] : [t.type]);
|
|
1237
|
-
if ([...sourceTypes].some((type) => !targetTypes.has(type))) return false;
|
|
1238
|
-
if (sourceTypes.has("object")) {
|
|
1239
|
-
const sp = (s.properties ?? {}) as Record<string, unknown>;
|
|
1240
|
-
const tp = (t.properties ?? {}) as Record<string, unknown>;
|
|
1241
|
-
const sr = new Set(Array.isArray(s.required) ? s.required as string[] : []);
|
|
1242
|
-
const tr = new Set(Array.isArray(t.required) ? t.required as string[] : []);
|
|
1243
|
-
if ([...tr].some((name) => !sr.has(name))) return false;
|
|
1244
|
-
for (const [name, schema] of Object.entries(sp)) {
|
|
1245
|
-
if (tp[name] === undefined) {
|
|
1246
|
-
if (t.additionalProperties === false) return false;
|
|
1247
|
-
} else if (!jsonSchemaSubset(schema, tp[name])) {
|
|
1248
|
-
return false;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
if (s.additionalProperties !== false && t.additionalProperties === false) return false;
|
|
1252
|
-
}
|
|
1253
|
-
if (sourceTypes.has("array") && s.items !== undefined && t.items !== undefined) {
|
|
1254
|
-
if (!jsonSchemaSubset(s.items, t.items)) return false;
|
|
1255
|
-
}
|
|
1256
|
-
return sourceTypes.size > 0 && [...sourceTypes].every((type) => targetTypes.has(type));
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
function jsonSchemaAcceptsLiteral(schema: Record<string, unknown>, value: unknown): boolean {
|
|
1260
|
-
if (Array.isArray(schema.enum)) return schema.enum.some((candidate) => canonicalJson(candidate) === canonicalJson(value));
|
|
1261
|
-
if (Object.prototype.hasOwnProperty.call(schema, "const")) return canonicalJson(schema.const) === canonicalJson(value);
|
|
1262
|
-
const union = Array.isArray(schema.anyOf) ? schema.anyOf : Array.isArray(schema.oneOf) ? schema.oneOf : undefined;
|
|
1263
|
-
if (union !== undefined) return union.some((arm) => arm !== null && typeof arm === "object" && jsonSchemaAcceptsLiteral(arm as Record<string, unknown>, value));
|
|
1264
|
-
const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value === "number" ? "number" : typeof value;
|
|
1265
|
-
const types = new Set(Array.isArray(schema.type) ? schema.type : schema.type === undefined ? [] : [schema.type]);
|
|
1266
|
-
return types.has(actual) || (actual === "number" && types.has("integer") && Number.isInteger(value));
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
/** JSON Schema contains a handful of set-valued arrays whose authored order is not part
|
|
1270
|
-
* of the accepted value language. Normalize those (and VO enum `values`) so a cosmetic
|
|
1271
|
-
* enum/union reorder does not manufacture an N/N+1 incompatibility. */
|
|
1272
|
-
function normalizeFieldContract(value: unknown, parentKey = ""): unknown {
|
|
1273
|
-
if (Array.isArray(value)) {
|
|
1274
|
-
const items = value.map((item) => normalizeFieldContract(item));
|
|
1275
|
-
if (["enum", "required", "type", "values", "oneOf", "anyOf", "allOf"].includes(parentKey)) {
|
|
1276
|
-
return items.sort((a, b) => {
|
|
1277
|
-
const left = canonicalJson(a);
|
|
1278
|
-
const right = canonicalJson(b);
|
|
1279
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
return items;
|
|
1283
|
-
}
|
|
1284
|
-
if (value !== null && typeof value === "object") {
|
|
1285
|
-
return Object.fromEntries(
|
|
1286
|
-
Object.entries(value as Record<string, unknown>).map(([key, child]) => [
|
|
1287
|
-
key,
|
|
1288
|
-
normalizeFieldContract(child, key),
|
|
1289
|
-
]),
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
return value;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
/**
|
|
1296
|
-
* Build the canonical semantic manifest for a domain module — purely from the
|
|
1297
|
-
* in-memory DSL objects. No file reads, no bundle, no esbuild: the manifest is a
|
|
1298
|
-
* function of the module alone, so it is bundler- AND location-independent.
|
|
1299
|
-
*/
|
|
1300
|
-
export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
1301
|
-
for (const agg of mod.aggregates) {
|
|
1302
|
-
if (agg.audience === undefined) {
|
|
1303
|
-
throw new Error(
|
|
1304
|
-
`aggregate '${agg.id}' has no audience. Nomos will not guess who may read business state: ` +
|
|
1305
|
-
`finish the declaration with .owned(), .readableBy("<business relation>"), or the deliberate ` +
|
|
1306
|
-
`.public() exception.`,
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
for (const directive of mod.directives) {
|
|
1311
|
-
const shape = zodObjectShape(directive.payloadSchema as ZodLike);
|
|
1312
|
-
if (shape === undefined) continue;
|
|
1313
|
-
for (const reserved of [
|
|
1314
|
-
OWNED_KEY_PAYLOAD_FIELD,
|
|
1315
|
-
OWNED_CUSTODY_PAYLOAD_FIELD,
|
|
1316
|
-
OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
|
|
1317
|
-
BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
|
|
1318
|
-
]) {
|
|
1319
|
-
if (!Object.prototype.hasOwnProperty.call(shape, reserved)) continue;
|
|
1320
|
-
const frameworkBirthCarrier = reserved === BUSINESS_BIRTH_SEED_PAYLOAD_FIELD
|
|
1321
|
-
&& (directive as { businessBirth?: unknown }).businessBirth !== undefined;
|
|
1322
|
-
if (frameworkBirthCarrier) continue;
|
|
1323
|
-
throw new Error(
|
|
1324
|
-
`directive '${directive.id}': payload field '${reserved}' is reserved by Nomos ownership. ` +
|
|
1325
|
-
`Sorry mate, that concept is already taken by the framework; give the business payload field ` +
|
|
1326
|
-
`a different name.`,
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
const aggregates: CanonicalAggregate[] = mod.aggregates
|
|
1331
|
-
.map((agg) => {
|
|
1332
|
-
return {
|
|
1333
|
-
id: agg.id,
|
|
1334
|
-
schema: sortedSchema(encodeKernelSchema(agg)),
|
|
1335
|
-
// OMIT-WHEN-ABSENT: only emit `hasInvariant` when the aggregate declares one,
|
|
1336
|
-
// so a predicate-free aggregate is byte-identical in the manifest to before this
|
|
1337
|
-
// key existed (golden hashes UNCHANGED — mirrors directive `plan` omission).
|
|
1338
|
-
...(agg.hasInvariant === true ? { hasInvariant: true as const } : {}),
|
|
1339
|
-
// The LAW-DECLARED instance cap — hash-bearing (it IS law), omit-when-absent.
|
|
1340
|
-
...(agg.cap !== undefined ? { cap: agg.cap } : {}),
|
|
1341
|
-
// READ VISIBILITY (slice 5.1) — the capability a read of this type requires.
|
|
1342
|
-
// Hash-bearing (it IS law); omit-when-public so all-public domains are byte-stable.
|
|
1343
|
-
...(agg.visibility !== undefined ? { requiresRead: agg.visibility.requires } : {}),
|
|
1344
|
-
// E2E ENCRYPTION (slice 2) — the encrypted field names. Hash-bearing law;
|
|
1345
|
-
// omit-when-none so plaintext aggregates are byte-stable.
|
|
1346
|
-
...(agg.encryptedFields !== undefined ? { encryptedFields: [...agg.encryptedFields] } : {}),
|
|
1347
|
-
...(agg.audience?.kind === "owned"
|
|
1348
|
-
? { owned: { ownerField: agg.audience.ownerField, relation: OWNED_READ_RELATION } }
|
|
1349
|
-
: {}),
|
|
1350
|
-
...(agg.internalFields !== undefined ? { internalFields: [...agg.internalFields].sort() } : {}),
|
|
1351
|
-
// ATTRIBUTED READS — the attributed field names (the driver wrap in `schema` is
|
|
1352
|
-
// the hash-bearing law; this mirror is what codegen reads). Omit-when-none.
|
|
1353
|
-
...(agg.attributedFields !== undefined ? { attributedFields: [...agg.attributedFields] } : {}),
|
|
1354
|
-
};
|
|
1355
|
-
})
|
|
1356
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1357
|
-
|
|
1358
|
-
const conflictResolutionByDirective = new Map(
|
|
1359
|
-
(mod.surfacedResolvers ?? []).map((binding) => [binding.directiveId, binding] as const),
|
|
1360
|
-
);
|
|
1361
|
-
const directives: CanonicalDirective[] = mod.directives
|
|
1362
|
-
.map((d) => ({
|
|
1363
|
-
id: d.id,
|
|
1364
|
-
target: d.aggregateId,
|
|
1365
|
-
marker: encodeRefMode(d.marker),
|
|
1366
|
-
...canonicalSecondaryOutputs(d.secondaryOutputs),
|
|
1367
|
-
payload: canonicalPayload(d.payloadSchema),
|
|
1368
|
-
payloadJsonSchema: canonicalPayloadJsonSchema(d.payloadSchema),
|
|
1369
|
-
// Omit-when-empty: a directive declaring NO boundary contributes neither key,
|
|
1370
|
-
// so its canonical manifest is byte-identical to before the boundary existed.
|
|
1371
|
-
...canonicalReads(d.declaredReads),
|
|
1372
|
-
...canonicalCapturedReads(d.declaredQueryReads ?? []),
|
|
1373
|
-
...canonicalEmits(d.declaredEmits),
|
|
1374
|
-
...canonicalRelations(d.declaredRelations),
|
|
1375
|
-
...canonicalHostCapabilities(d.declaredHostCapabilities),
|
|
1376
|
-
...(conflictResolutionByDirective.has(d.id)
|
|
1377
|
-
? {
|
|
1378
|
-
conflictResolution: {
|
|
1379
|
-
aggregate: conflictResolutionByDirective.get(d.id)!.aggregateId,
|
|
1380
|
-
field: conflictResolutionByDirective.get(d.id)!.field,
|
|
1381
|
-
},
|
|
1382
|
-
}
|
|
1383
|
-
: {}),
|
|
1384
|
-
}))
|
|
1385
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1386
|
-
|
|
1387
|
-
// THE INFORMATION-FLOW GATE (architecture/security_model.md): rule 2 (no private
|
|
1388
|
-
// captured read on the chain) + rule 1 (no encrypt-then-index) are enforced as ONE
|
|
1389
|
-
// pass over the assembled canonical manifest, just before return (see assertFlowGate).
|
|
1390
|
-
// E2E ENCRYPTION — critical #5 (e2e_encryption.md §5): the captured-read seal is rule 2.
|
|
1391
|
-
|
|
1392
|
-
// exists_ entries are COUNT declarations in the IR — merge them into `counts` so the
|
|
1393
|
-
// Rust engine sees them as ordinary counts (the marker lives only in the DSL/codegen
|
|
1394
|
-
// layer). Combine mod.counts + normalized exists_ before canonicalizing counts.
|
|
1395
|
-
const existsAsCountDecls: AnyCount[] = (mod.exists_ ?? []).map(existsAsCount);
|
|
1396
|
-
const allCounts: AnyCount[] = [...(mod.counts ?? []), ...existsAsCountDecls];
|
|
1397
|
-
|
|
1398
|
-
// Omit-when-empty: a domain declaring NO query / NO count / NO sum contributes neither
|
|
1399
|
-
// key, so its canonical manifest is byte-identical to before the read-closure layer
|
|
1400
|
-
// existed. `sums` follows the same omit-when-empty discipline as `counts`.
|
|
1401
|
-
const manifest: CanonicalManifest = {
|
|
1402
|
-
domain: mod.name,
|
|
1403
|
-
aggregates,
|
|
1404
|
-
directives,
|
|
1405
|
-
// STABLE IDS (#58): always emitted by this compiler — the one deliberate
|
|
1406
|
-
// hash-mover (enforcement-era identity hashes move; named + expected).
|
|
1407
|
-
stableIds: stableIdsForModule(mod, aggregates),
|
|
1408
|
-
...canonicalWorkspaceInvariants(mod.workspaceInvariants),
|
|
1409
|
-
...canonicalQueries(mod.queries),
|
|
1410
|
-
...canonicalCounts(allCounts.length > 0 ? allCounts : undefined),
|
|
1411
|
-
...canonicalSpatials(mod.spatials),
|
|
1412
|
-
...canonicalSums(mod.sums),
|
|
1413
|
-
...canonicalDeriveds(mod.deriveds),
|
|
1414
|
-
...canonicalCombineds(mod.combineds),
|
|
1415
|
-
...(canonicalExtrema(mod.mins, "mins") as { mins?: CanonicalExtremum[] }),
|
|
1416
|
-
...(canonicalExtrema(mod.maxes, "maxes") as { maxes?: CanonicalExtremum[] }),
|
|
1417
|
-
...canonicalOrderedReads(mod.orderedReads),
|
|
1418
|
-
// Omit-when-empty: a taxonomy-free domain contributes NO taxonomy key (hash-
|
|
1419
|
-
// stable). When declared, this RUNS the fail-closed homing walk AND the slice-2
|
|
1420
|
-
// route derivation — an unhomeable or unroutable taxonomy never produces an
|
|
1421
|
-
// identity (workspace_type.ts + workspace_routing.ts).
|
|
1422
|
-
...canonicalTaxonomyFragment({
|
|
1423
|
-
name: mod.name,
|
|
1424
|
-
aggregates: mod.aggregates,
|
|
1425
|
-
directives: mod.directives,
|
|
1426
|
-
...(mod.workspaceTypes !== undefined ? { workspaceTypes: mod.workspaceTypes } : {}),
|
|
1427
|
-
}),
|
|
1428
|
-
};
|
|
1429
|
-
// THE STATIC INFORMATION-FLOW GATE — leaky law is unrepresentable (a typed compile
|
|
1430
|
-
// error naming the field, the sink, and the rule). Rules 1/2/5 read the manifest IR;
|
|
1431
|
-
// rule 4 also needs the raw module (the executable invariant reads + declassify waivers).
|
|
1432
|
-
assertFlowGate(manifest, mod as never);
|
|
1433
|
-
return manifest;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
/** Recursively sort object keys; arrays are already in sorted/stable order. */
|
|
1437
|
-
function canonicalize(value: unknown): unknown {
|
|
1438
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
1439
|
-
if (value !== null && typeof value === "object") {
|
|
1440
|
-
const obj = value as Record<string, unknown>;
|
|
1441
|
-
const out: Record<string, unknown> = {};
|
|
1442
|
-
for (const key of Object.keys(obj).sort()) out[key] = canonicalize(obj[key]);
|
|
1443
|
-
return out;
|
|
1444
|
-
}
|
|
1445
|
-
return value;
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
/**
|
|
1449
|
-
* Serialize ANY value to JSON with recursively SORTED object keys (byte-stable).
|
|
1450
|
-
* Used for the domain manifest here AND reused by the composed-IR layer (#137) to
|
|
1451
|
-
* hash a flattened stage — the canonicalizer is value-generic, so the input type is
|
|
1452
|
-
* `unknown` rather than narrowed to `CanonicalManifest`.
|
|
1453
|
-
*/
|
|
1454
|
-
export function canonicalJson(manifest: unknown): string {
|
|
1455
|
-
return JSON.stringify(canonicalize(manifest));
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
/** The domain's content-hash identity: sha256 hex of its canonical manifest JSON. */
|
|
1459
|
-
export function domainHash(mod: DomainModule): string {
|
|
1460
|
-
return createHash("sha256").update(canonicalJson(domainManifest(mod)), "utf8").digest("hex");
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
/**
|
|
1464
|
-
* The SHIPPED ARTIFACT bytes for a domain (#136) — the EXACT canonical-manifest JSON
|
|
1465
|
-
* bytes whose sha256 IS the domain's identity. This is the byte string an emit step
|
|
1466
|
-
* writes to `<domain>.manifest.json` and the gate's reproducibility check recomputes
|
|
1467
|
-
* `hash_code` over. The invariant the whole gate rests on:
|
|
1468
|
-
*
|
|
1469
|
-
* sha256(emitManifestBytes(mod)) === domainHash(mod)
|
|
1470
|
-
*
|
|
1471
|
-
* i.e. the file bytes equal the bytes `domainHash` hashes (compact, recursively
|
|
1472
|
-
* sorted keys, UTF-8). "The bundle is not the law" — THIS is.
|
|
1473
|
-
*/
|
|
1474
|
-
export function emitManifestBytes(mod: DomainModule): Buffer {
|
|
1475
|
-
return Buffer.from(canonicalJson(domainManifest(mod)), "utf8");
|
|
1476
|
-
}
|