@githolon/dsl 0.2.3 → 0.3.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/package.json +4 -1
- package/src/build_package.ts +124 -3
- package/src/codegen_dart.ts +15 -0
- package/src/codegen_ts.ts +235 -7
- package/src/compile_package_main.ts +246 -5
- package/src/engine_entry.ts +124 -4
- package/src/framework/workspace_invariant.ts +7 -0
- package/src/index.ts +6 -0
- package/src/manifest.ts +56 -7
- package/src/usd.ts +37 -0
- package/src/workspace_routing.ts +585 -0
- package/src/workspace_sharding.ts +1179 -0
- package/src/workspace_type.ts +609 -0
|
@@ -0,0 +1,1179 @@
|
|
|
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
|
+
* THE DERIVED SHARDING LAW (sharding §3/§5/§6 — slice 2 placement, slice 3a homing
|
|
10
|
+
* invariants, slice 3 delta lane + least-loaded placement).
|
|
11
|
+
*
|
|
12
|
+
* `nomos-compile` APPENDS this module to a TAXONOMY-BEARING domain's module list —
|
|
13
|
+
* both in the canonical manifests (the placement law is hash-bearing law) and in the
|
|
14
|
+
* generated engine entry (the plans ship in the lump). A taxonomy-FREE package never
|
|
15
|
+
* imports it (subpath `@githolon/dsl/workspace-sharding`, never the runtime barrel —
|
|
16
|
+
* the slice-1 hash-stability law: its bytes must not enter other tenants' lumps).
|
|
17
|
+
*
|
|
18
|
+
* What it derives, per PACKED axis (`EstateWs.hasMany(SiteHome)` with
|
|
19
|
+
* `SiteHome.packed()`):
|
|
20
|
+
*
|
|
21
|
+
* * `NomosShardAssignment` — THE SHARD MAP AS LAW on the coordinator. SLICE 3:
|
|
22
|
+
* one DETERMINISTIC row per home (`nomos-shard-assignment:<homeKey>`, an Ensure)
|
|
23
|
+
* — placement uniqueness BY CONSTRUCTION, idempotent re-offer by Lww. The shard
|
|
24
|
+
* is picked LEAST-LOADED by the dispatching client/worker over committed
|
|
25
|
+
* subtotal rows (law-state load, §3) and carried IN the payload; the
|
|
26
|
+
* `nomosPlacementUnique:*` invariant refuses a CONFLICTING re-offer typed
|
|
27
|
+
* (`placement-exists:<heldShard>`) — same home, same shard, forever, until the
|
|
28
|
+
* slice-5 move lane flips it through `splitShard`/`sealHandoff`.
|
|
29
|
+
* * `NomosHomeReceipt` — §3's "its assignment" fact IN THE SHARD'S OWN CHAIN (the
|
|
30
|
+
* assignment RECEIPT leg): `nomosReceiveAssignment` ensures one receipt row per
|
|
31
|
+
* home the shard owns, judged by `nomosReceiptHome:*` against the shard's own
|
|
32
|
+
* declared identity. The homing invariant (`nomosWrongHome:<directive>`) now
|
|
33
|
+
* judges routed writes against THESE receipts — shard-local law-state, no static
|
|
34
|
+
* placement formula (the slice-2 fnv pick is retired with least-loaded placement).
|
|
35
|
+
* * THE §5.2 DELTA LANE — `nomosPropagateSummary`: shard admission's coalesced
|
|
36
|
+
* Order carrying per-(read, group) ABSOLUTE subtotals + the covered range's
|
|
37
|
+
* event deltas as content-addressed evidence. The coordinator's ONE gate
|
|
38
|
+
* adjudicates it (ruling 4 — full gate recomputation day one):
|
|
39
|
+
* 1. the PLAN recomputes every claimed absolute from the claimed previous +
|
|
40
|
+
* the carried event deltas and REFUSES on mismatch (pure arithmetic over
|
|
41
|
+
* the payload — replays byte-identically on every peer);
|
|
42
|
+
* 2. the `nomosSummaryGate:*` workspace invariant verifies the CLAIMS against
|
|
43
|
+
* HELD law-state via `pre:` snapshots (the committed pre-apply reads the
|
|
44
|
+
* executor resolves for `pre:`-prefixed refs): `fromOid` must equal the
|
|
45
|
+
* recorded per-shard frontier (typed `frontier-gap:<expected>`), and every
|
|
46
|
+
* claimed `prev` must equal the held subtotal value (typed
|
|
47
|
+
* `summary-mismatch:…`). Held + carried-events ⇒ claimed, byte-compared.
|
|
48
|
+
* 3. the FOLD is plain Lww over `NomosSummarySubtotal` rows — idempotent,
|
|
49
|
+
* replay-safe, kernel-lowered today. The estate total is an ORDINARY
|
|
50
|
+
* maintained sum over subtotal rows (`nomosEstateSummary` by `bucket`) —
|
|
51
|
+
* O(1) by #31. Subtotal rows ARE checkpoints (ruling 1): each carries the
|
|
52
|
+
* shard frontier it is true at; deep-verify (replay-on-suspicion) mounts
|
|
53
|
+
* the shard chain, re-verifies from genesis, recounts, byte-compares.
|
|
54
|
+
* * `NomosShardRegistry` + `NomosShardPolicy` — the OPEN-SHARD set and the split
|
|
55
|
+
* policy as law-state: the worker auto-births `s(k+1)` through the platform lane
|
|
56
|
+
* when EVERY open shard's row count crosses the split threshold (ruling 5 — 75%
|
|
57
|
+
* of the CAPACITY.md wall by default, law-amendable via `nomosSetShardPolicy`).
|
|
58
|
+
* * ROUTE-TAGGED IN-PLAN MINTS — {@link installRouteTaggedMint}: the generated
|
|
59
|
+
* entry of a taxonomy-bearing package wraps the sealed sandbox's
|
|
60
|
+
* `globalThis.nomos.mint` (at lump top-level eval, pre-freeze) so `create(Agg)`
|
|
61
|
+
* mints of PACKED-HOMED aggregates fold the dispatched intent's home ROUTE TAG
|
|
62
|
+
* (48 bits of sha256, §4) into the UUIDv7 timestamp slot — same draw budget
|
|
63
|
+
* (19 captured rng draws), no clock read, deterministic, byte-identical on
|
|
64
|
+
* replay. No wasm change: the tag rides the slot the id-mint law itself demotes
|
|
65
|
+
* to metadata.
|
|
66
|
+
*
|
|
67
|
+
* ENGINE-BUNDLE-SAFE: no node builtin anywhere in this module's import graph.
|
|
68
|
+
*/
|
|
69
|
+
import { z } from "zod";
|
|
70
|
+
|
|
71
|
+
import { aggregate, instance } from "./aggregate.js";
|
|
72
|
+
import { t } from "./fields.js";
|
|
73
|
+
import { Lww } from "./drivers.js";
|
|
74
|
+
import { directive } from "./directive.js";
|
|
75
|
+
import { set, strike, withMarker, type PlannedOp } from "./ops.js";
|
|
76
|
+
import { query } from "./query.js";
|
|
77
|
+
import { sum } from "./sum.js";
|
|
78
|
+
import { refAs, workspaceInvariant } from "./framework/workspace_invariant.js";
|
|
79
|
+
|
|
80
|
+
/** One packed axis the factory derives a placement lane for. */
|
|
81
|
+
export interface ShardingAxisSpec {
|
|
82
|
+
/** The packed workspace-type id (e.g. `site`). */
|
|
83
|
+
readonly axisType: string;
|
|
84
|
+
/** The axis ROOT aggregate's wire id (e.g. `Site`). */
|
|
85
|
+
readonly axisRoot: string;
|
|
86
|
+
/**
|
|
87
|
+
* The INITIAL open-shard count (the parent type's `.pool(n)`): shards `s0…s{n-1}`
|
|
88
|
+
* are open at genesis. SLICE 3: the set GROWS — the worker auto-births `s(k+1)`
|
|
89
|
+
* (recorded as a `NomosShardRegistry` row through the coordinator's own gate)
|
|
90
|
+
* when every open shard crosses the split threshold.
|
|
91
|
+
*/
|
|
92
|
+
readonly shardCount: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One derived directive route the homing invariant judges (`via:"axis"` only — a
|
|
96
|
+
* `via:"id"` misroute targets an aggregate that does not exist on the wrong shard,
|
|
97
|
+
* which the referential gate already refuses; see §3 of the sharding design). */
|
|
98
|
+
export interface ShardingRouteSpec {
|
|
99
|
+
/** The routed directive id. */
|
|
100
|
+
readonly directive: string;
|
|
101
|
+
/** The PACKED workspace-type id the write homes on (matches an axis' `axisType`). */
|
|
102
|
+
readonly home: string;
|
|
103
|
+
/** The top-level payload field carrying the home key. */
|
|
104
|
+
readonly key: string;
|
|
105
|
+
/** How the key rides: `"axis"` = the axis-root id itself; `"id"` = a homed minted id. */
|
|
106
|
+
readonly via: "axis" | "id";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ShardingLawSpec {
|
|
110
|
+
readonly axes: readonly ShardingAxisSpec[];
|
|
111
|
+
/**
|
|
112
|
+
* THE DERIVED ROUTES (slice 2's canonical-manifest `routes`, fed back into the law):
|
|
113
|
+
* per routed directive, the homing invariant `nomosWrongHome:<directive>` is derived
|
|
114
|
+
* so THE CHAIN GATE ITSELF refuses a wrong-home write (#41 — slice 3a; the edge
|
|
115
|
+
* bailiff stays as defense-in-depth). OMITTED routes ⇒ no invariants (slice-2 shape).
|
|
116
|
+
*/
|
|
117
|
+
readonly routes?: readonly ShardingRouteSpec[];
|
|
118
|
+
/**
|
|
119
|
+
* THE PACKED HOMING TABLE (slice 3 — the in-plan mint tagger's input): aggregate
|
|
120
|
+
* wire id → the packed axis type it homes on. Only PACKED-homed aggregates appear.
|
|
121
|
+
* OMITTED ⇒ in-plan mints stay plain (the slice-2 shape).
|
|
122
|
+
*/
|
|
123
|
+
readonly homes?: Readonly<Record<string, string>>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── well-known law-state row ids (deterministic — Ensure singletons / keyed rows) ──
|
|
127
|
+
|
|
128
|
+
/** The well-known Ensure id the homing invariant resolves the identity under. */
|
|
129
|
+
export const NOMOS_SHARD_IDENTITY_ID = "nomos-shard-identity:self";
|
|
130
|
+
/** The shard-policy singleton's row id (coordinator law-state). */
|
|
131
|
+
export const NOMOS_SHARD_POLICY_ID = "nomos-shard-policy:self";
|
|
132
|
+
/**
|
|
133
|
+
* Deterministic-row-id escape: a framework row id must never PARSE as a
|
|
134
|
+
* kernel-minted id (`<tag>_<uuid>` — the FIRST "_" splits, and a minted home key
|
|
135
|
+
* embedded verbatim would make the row id minted-shaped and refuse at the id-mint
|
|
136
|
+
* gate). Embedded key material therefore escapes "_" %-style — injective, pure,
|
|
137
|
+
* sandbox-safe ("%"→"%25" first, then "_"→"%5F").
|
|
138
|
+
*/
|
|
139
|
+
export const escapeRowKey = (s: string): string => s.replace(/%/g, "%25").replace(/_/g, "%5F").replace(/\|/g, "%7C");
|
|
140
|
+
/** The shard map row for a home key (coordinator law-state; uniqueness by construction). */
|
|
141
|
+
export const shardAssignmentRowId = (homeKey: string): string => `nomos-shard-assignment:${escapeRowKey(homeKey)}`;
|
|
142
|
+
/** The shard's own receipt row for a home it owns (shard law-state). */
|
|
143
|
+
export const homeReceiptRowId = (homeKey: string): string => `nomos-home-receipt:${escapeRowKey(homeKey)}`;
|
|
144
|
+
/** The open-shard registry row for a label (coordinator law-state). */
|
|
145
|
+
export const shardRegistryRowId = (label: string): string => `nomos-shard-registry:${label}`;
|
|
146
|
+
/** The per-shard delta-lane frontier watermark (coordinator law-state). */
|
|
147
|
+
export const summaryFrontierRowId = (shard: string): string => `nomos-summary-frontier:${shard}`;
|
|
148
|
+
/** One per-(shard, read, group) committed subtotal row (coordinator law-state). */
|
|
149
|
+
export const summarySubtotalRowId = (shard: string, readId: string, group: string): string =>
|
|
150
|
+
`nomos-subtotal:${shard}|${escapeRowKey(readId)}|${escapeRowKey(group)}`;
|
|
151
|
+
/** The recorded deep-verify verdict row for a shard (coordinator law-state). */
|
|
152
|
+
export const deepVerifyRowId = (shard: string): string => `nomos-deep-verify:${shard}`;
|
|
153
|
+
/** One sealed checkpoint row (§5.5, slice 4 — coordinator law-state, by seal sequence). */
|
|
154
|
+
export const checkpointSealRowId = (seq: number | string): string => `nomos-checkpoint-seal:${seq}`;
|
|
155
|
+
/** The LATEST seal (the head pointer — same aggregate, well-known id; O(1) to find). */
|
|
156
|
+
export const NOMOS_CHECKPOINT_HEAD_ID = "nomos-checkpoint-seal:head";
|
|
157
|
+
|
|
158
|
+
/** The estate-total bucket of a scoped read's group (`nomosEstateSummary` group key).
|
|
159
|
+
* `|` — printable (the kernel's canonical event decode refuses raw control bytes);
|
|
160
|
+
* unambiguous because a SCOPED read's id may not contain `|` (compile-refused);
|
|
161
|
+
* group values are free text (the bucket is only ever CONSTRUCTED, never parsed). */
|
|
162
|
+
export const SUMMARY_BUCKET_SEP = "|";
|
|
163
|
+
export const summaryBucket = (readId: string, group: string): string =>
|
|
164
|
+
`${readId}${SUMMARY_BUCKET_SEP}${group}`;
|
|
165
|
+
|
|
166
|
+
/** The synthetic per-shard row-count read the load policy consumes (capacity headroom). */
|
|
167
|
+
export const NOMOS_SHARD_ROWS_READ = "nomosShardRows";
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* THE MOVE STATUS (slice 5 — §6's `moving(target)`, encoded IN the status string so
|
|
171
|
+
* the assignment row's schema never moves): an assignment mid-handoff reads
|
|
172
|
+
* `moving:<targetLabel>`; `sealHandoff` flips it back to `active` with
|
|
173
|
+
* `shard = target` and the map version bumped. One row per home, always.
|
|
174
|
+
*/
|
|
175
|
+
export const MOVING_STATUS_PREFIX = "moving:";
|
|
176
|
+
export const movingStatus = (target: string): string => `${MOVING_STATUS_PREFIX}${target}`;
|
|
177
|
+
/** The move target of a `moving:<label>` status, or undefined for any other status. */
|
|
178
|
+
export const movingTargetOf = (status: unknown): string | undefined =>
|
|
179
|
+
typeof status === "string" && status.startsWith(MOVING_STATUS_PREFIX)
|
|
180
|
+
? status.slice(MOVING_STATUS_PREFIX.length)
|
|
181
|
+
: undefined;
|
|
182
|
+
|
|
183
|
+
/** The CAPACITY.md wall + ruling-5 default threshold (rows; law-amendable via policy). */
|
|
184
|
+
export const DEFAULT_WALL_ROWS = 581000;
|
|
185
|
+
export const DEFAULT_SPLIT_THRESHOLD_ROWS = 435750; // 75% of the wall — ruling 5
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* THE SHARD MAP ROW (coordinator law-state). One aggregate type across all axes —
|
|
189
|
+
* `homeType` carries the axis. `shard` is the LABEL (`s0`…): the addressable shard
|
|
190
|
+
* workspace is `<coordinator>--<label>` (custody naming, composed by the hosts —
|
|
191
|
+
* ids embed the HOME KEY, never the shard ordinal, §4). SLICE 3: the row id is
|
|
192
|
+
* DETERMINISTIC (`nomos-shard-assignment:<homeKey>`) — one home, one row, forever.
|
|
193
|
+
*/
|
|
194
|
+
export const NomosShardAssignment = aggregate("NomosShardAssignment", {
|
|
195
|
+
homeType: t.string().merge(Lww),
|
|
196
|
+
homeKey: t.string().merge(Lww),
|
|
197
|
+
shard: t.string().merge(Lww),
|
|
198
|
+
mapVersion: t.int().merge(Lww),
|
|
199
|
+
status: t.string().merge(Lww), // active | moving | moved (slice 5 flips these)
|
|
200
|
+
placedAt: t.string().merge(Lww),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* THE SHARD'S OWN IDENTITY — law-state in the shard's OWN chain (§3's "its assignment"
|
|
205
|
+
* fact, the slice-3a slice of it): which shard label this workspace IS. An Ensure
|
|
206
|
+
* singleton (well-known id `nomos-shard-identity:self`): the worker (the bailiff)
|
|
207
|
+
* authors it through the shard's own gate right after the law deploys — a custody fact
|
|
208
|
+
* (the workspace's name) recorded AS LAW, every change attributed + replayable. The
|
|
209
|
+
* homing invariant reads it; a workspace that never declares one (the coordinator, a
|
|
210
|
+
* direct unsharded holon, every pre-#41 chain) is judged vacuously (the edge bailiff
|
|
211
|
+
* remains the outer guard there).
|
|
212
|
+
*/
|
|
213
|
+
export const NomosShardIdentity = aggregate("NomosShardIdentity", {
|
|
214
|
+
scope: t.string().merge(Lww), // always "self" — the singleton key
|
|
215
|
+
label: t.string().merge(Lww), // "s0"…, or "" = the coordinator
|
|
216
|
+
coordinator: t.string().merge(Lww), // the coordinator workspace's name
|
|
217
|
+
declaredAt: t.string().merge(Lww),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* THE ASSIGNMENT RECEIPT (slice 3 — §3's receipt leg): the shard's own chain records
|
|
222
|
+
* WHICH HOMES IT OWNS, one Ensure row per home key. Authored by the coordinator's
|
|
223
|
+
* worker after a placement admits, AND optimistically by the routed client before its
|
|
224
|
+
* first write to a fresh placement (idempotent — same row, same values; the edge
|
|
225
|
+
* bailiff verifies every session-lane receipt against the coordinator's shard map
|
|
226
|
+
* before it merges). The homing invariant judges routed writes against these rows.
|
|
227
|
+
*/
|
|
228
|
+
export const NomosHomeReceipt = aggregate("NomosHomeReceipt", {
|
|
229
|
+
homeKey: t.string().merge(Lww),
|
|
230
|
+
shard: t.string().merge(Lww),
|
|
231
|
+
mapVersion: t.int().merge(Lww),
|
|
232
|
+
status: t.string().merge(Lww), // active | moved (slice 5 — the tombstone IS a redirect: `shard` then names the target)
|
|
233
|
+
receivedAt: t.string().merge(Lww),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
/** The OPEN-SHARD registry row (coordinator law-state; `s0…s{pool-1}` are implicit). */
|
|
237
|
+
export const NomosShardRegistry = aggregate("NomosShardRegistry", {
|
|
238
|
+
label: t.string().merge(Lww),
|
|
239
|
+
status: t.string().merge(Lww), // open | retired (slice 5)
|
|
240
|
+
openedAt: t.string().merge(Lww),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* THE SPLIT POLICY singleton (coordinator law-state — ruling 5: 75% ships, the dial
|
|
245
|
+
* is law-amendable). `wallRows` is the capacitymeter-informed per-shard wall
|
|
246
|
+
* (CAPACITY.md: ~581k under the co2 shape); `splitThresholdRows` is the auto-birth
|
|
247
|
+
* trigger (defaults to 75% of the wall when amended without one).
|
|
248
|
+
*/
|
|
249
|
+
export const NomosShardPolicy = aggregate("NomosShardPolicy", {
|
|
250
|
+
scope: t.string().merge(Lww), // always "self"
|
|
251
|
+
wallRows: t.int().merge(Lww),
|
|
252
|
+
splitThresholdRows: t.int().merge(Lww),
|
|
253
|
+
// §5.5 CHECKPOINT CADENCE (slice 4, ruling 1): seal after every N admitted deltas.
|
|
254
|
+
// Absent ⇒ the host default (64); 0 ⇒ cadence off (explicit seals only).
|
|
255
|
+
checkpointEveryDeltas: t.int().merge(Lww),
|
|
256
|
+
setAt: t.string().merge(Lww),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* THE CHECKPOINT SEAL (§5.5, slice 4 — ruling 1 made periodic): a committed
|
|
261
|
+
* `{shard → frontier}` snapshot + the content hash of the coordinator's subtotal
|
|
262
|
+
* state at the seal. Subtotal rows ARE the checkpoints (each already carries its
|
|
263
|
+
* shard frontier + the gate verdict that sealed it); the seal binds a SET of them
|
|
264
|
+
* to one sequence point, which makes the delta suffix BEHIND the sealed frontiers
|
|
265
|
+
* ARCHIVAL: replay from genesis still works (nothing is erased), but an auditor
|
|
266
|
+
* may anchor at the latest seal and deep-verify forward from it. `frontiers` is
|
|
267
|
+
* canonical JSON (`[{shard, frontier}]`, shard-sorted — the plan re-sorts, so the
|
|
268
|
+
* folded bytes never depend on payload order); `stateHash` is the host's
|
|
269
|
+
* content-addressed claim over the shard-sorted subtotal rows at the seal —
|
|
270
|
+
* deep-verify recomputes it from law-state and byte-compares (anchoring).
|
|
271
|
+
*/
|
|
272
|
+
export const NomosCheckpointSeal = aggregate("NomosCheckpointSeal", {
|
|
273
|
+
seq: t.int().merge(Lww),
|
|
274
|
+
frontiers: t.string().merge(Lww), // canonical JSON: [{shard, frontier}] sorted by shard
|
|
275
|
+
stateHash: t.string().merge(Lww), // sha256 over the coordinator's subtotal state at the seal
|
|
276
|
+
sealedAt: t.string().merge(Lww),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/** The canonical (shard-sorted) frontier-set bytes a seal folds — ONE shape on every peer. */
|
|
280
|
+
export const canonicalSealFrontiers = (
|
|
281
|
+
frontiers: readonly { shard: string; frontier: string }[],
|
|
282
|
+
): string =>
|
|
283
|
+
JSON.stringify(
|
|
284
|
+
[...frontiers]
|
|
285
|
+
.map((f) => ({ shard: f.shard, frontier: f.frontier }))
|
|
286
|
+
.sort((a, b) => (a.shard < b.shard ? -1 : a.shard > b.shard ? 1 : 0)),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* ONE COMMITTED SUBTOTAL — §5.1's `SummarySubtotal`, the checkpoint row (ruling 1):
|
|
291
|
+
* the ABSOLUTE per-(shard, read, group) value, true at the shard commit `frontier`,
|
|
292
|
+
* folded as plain Lww after the gate recomputed it from carried evidence. `bucket`
|
|
293
|
+
* (= `readId group`) is the maintained estate-total's group key; `kind` records
|
|
294
|
+
* what the value tallies (`count` | `sum` | `rows` — the load policy reads `rows`).
|
|
295
|
+
*/
|
|
296
|
+
export const NomosSummarySubtotal = aggregate("NomosSummarySubtotal", {
|
|
297
|
+
readId: t.string().merge(Lww),
|
|
298
|
+
group: t.string().merge(Lww),
|
|
299
|
+
shard: t.string().merge(Lww),
|
|
300
|
+
kind: t.string().merge(Lww),
|
|
301
|
+
bucket: t.string().merge(Lww),
|
|
302
|
+
value: t.int().merge(Lww),
|
|
303
|
+
frontier: t.string().merge(Lww),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/** THE PER-SHARD DELTA FRONTIER watermark (§5.2 step 1 — contiguity law-state). */
|
|
307
|
+
export const NomosSummaryFrontier = aggregate("NomosSummaryFrontier", {
|
|
308
|
+
shard: t.string().merge(Lww),
|
|
309
|
+
frontier: t.string().merge(Lww), // the shard main oid the subtotals are true at
|
|
310
|
+
readManifestHash: t.string().merge(Lww),
|
|
311
|
+
updatedAt: t.string().merge(Lww),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
/** THE RECORDED DEEP-VERIFY VERDICT (§5.5 — replay-on-suspicion, made operational). */
|
|
315
|
+
export const NomosDeepVerify = aggregate("NomosDeepVerify", {
|
|
316
|
+
shard: t.string().merge(Lww),
|
|
317
|
+
frontier: t.string().merge(Lww), // the coordinator-held frontier at check time
|
|
318
|
+
head: t.string().merge(Lww), // the shard main head the replay verified
|
|
319
|
+
verdict: t.string().merge(Lww), // verified | mismatch:<…> | invalid:<…>
|
|
320
|
+
detail: t.string().merge(Lww), // JSON: the per-(read,group) byte-compare table
|
|
321
|
+
checkedAt: t.string().merge(Lww),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
export const nomosDeclareShardIdentity = directive("nomosDeclareShardIdentity")
|
|
325
|
+
.ensures(NomosShardIdentity)
|
|
326
|
+
.payload(
|
|
327
|
+
z.object({
|
|
328
|
+
label: z.string(), // "" = the coordinator
|
|
329
|
+
coordinator: z.string().min(1),
|
|
330
|
+
declaredAt: z.string(),
|
|
331
|
+
}),
|
|
332
|
+
)
|
|
333
|
+
.plan((p) => {
|
|
334
|
+
const me = instance(NomosShardIdentity, NOMOS_SHARD_IDENTITY_ID);
|
|
335
|
+
return [
|
|
336
|
+
withMarker(set(me, "scope", "self"), "ensures"),
|
|
337
|
+
set(me, "label", p.label),
|
|
338
|
+
set(me, "coordinator", p.coordinator),
|
|
339
|
+
set(me, "declaredAt", p.declaredAt),
|
|
340
|
+
];
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
/** The receipt leg: record (idempotently) that THIS chain owns `homeKey`. */
|
|
344
|
+
export const nomosReceiveAssignment = directive("nomosReceiveAssignment")
|
|
345
|
+
.ensures(NomosHomeReceipt)
|
|
346
|
+
.payload(
|
|
347
|
+
z.object({
|
|
348
|
+
homeKey: z.string().min(1),
|
|
349
|
+
shard: z.string().regex(/^s\d+$/),
|
|
350
|
+
mapVersion: z.number().int().min(1),
|
|
351
|
+
receivedAt: z.string(),
|
|
352
|
+
}),
|
|
353
|
+
)
|
|
354
|
+
.plan((p) => {
|
|
355
|
+
const row = instance(NomosHomeReceipt, homeReceiptRowId(p.homeKey));
|
|
356
|
+
return [
|
|
357
|
+
withMarker(set(row, "homeKey", p.homeKey), "ensures"),
|
|
358
|
+
set(row, "shard", p.shard),
|
|
359
|
+
set(row, "mapVersion", p.mapVersion),
|
|
360
|
+
set(row, "status", "active"),
|
|
361
|
+
set(row, "receivedAt", p.receivedAt),
|
|
362
|
+
];
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ── §6 — THE SHARD LIFECYCLE (slice 5: splits / migration / sealHandoff) ─────────────
|
|
366
|
+
//
|
|
367
|
+
// The move is THREE law intents, every leg through a gate, custody never truth:
|
|
368
|
+
// 1. `nomosSplitShard` (coordinator) flips the chosen homes' SAME deterministic
|
|
369
|
+
// assignment rows `active → moving:<target>` — from that commit on, the edge
|
|
370
|
+
// bailiff parks writes to those homes with the typed `home-moving` refusal.
|
|
371
|
+
// 2. MIGRATION = RE-ADMISSION (the host leg, no directive): the target shard pulls
|
|
372
|
+
// the moving homes' intents from the source chain and re-admits EACH through its
|
|
373
|
+
// OWN gate (`apply_intent` — plan re-run, byte-compared, invariants judged;
|
|
374
|
+
// author preserved, committer = the target's kernel). Then `nomosSealHome`
|
|
375
|
+
// (source) STRIKES the migrated intents out of the source's fold (append-then-
|
|
376
|
+
// strike — history is never erased; the projection sheds the rows, so the
|
|
377
|
+
// source's tallies, deep-verify recount and capacity load all stay honest) and
|
|
378
|
+
// tombstones the home's receipt: `status = moved`, `shard = <target>` — the
|
|
379
|
+
// tombstone IS the redirect, and the homing invariant's existing
|
|
380
|
+
// `wrong-home:<receiptShard>` verdict now NAMES the new home for free.
|
|
381
|
+
// 3. `sealHandoff`'s subtotal close/open is one `nomosPropagateSummary` PAIR
|
|
382
|
+
// through the SAME summary gate (the close offers the source's post-strike
|
|
383
|
+
// retally against its held frontier; the open re-derives the target's suffix) —
|
|
384
|
+
// then `nomosSealHandoff` (coordinator) flips the rows `moving → active` with
|
|
385
|
+
// `shard = target` and the map version BUMPED (wrong-home refusals carry it;
|
|
386
|
+
// clients refresh and retry — §4's self-healing lane).
|
|
387
|
+
|
|
388
|
+
/** Begin a move: flip each home's assignment to `moving:<toShard>` (gate-verified). */
|
|
389
|
+
export const nomosSplitShard = directive("nomosSplitShard")
|
|
390
|
+
.mutates(NomosShardAssignment)
|
|
391
|
+
.payload(
|
|
392
|
+
z.object({
|
|
393
|
+
fromShard: z.string().regex(/^s\d+$/),
|
|
394
|
+
toShard: z.string().regex(/^s\d+$/),
|
|
395
|
+
homeKeys: z.array(z.string().min(1)).min(1).max(64),
|
|
396
|
+
startedAt: z.string(),
|
|
397
|
+
}),
|
|
398
|
+
)
|
|
399
|
+
.plan((p) => {
|
|
400
|
+
if (p.fromShard === p.toShard) {
|
|
401
|
+
throw new Error(`split-degenerate: fromShard and toShard are both '${p.toShard}' — a move needs two shards`);
|
|
402
|
+
}
|
|
403
|
+
if (new Set(p.homeKeys).size !== p.homeKeys.length) {
|
|
404
|
+
throw new Error("split-duplicate: a home key appears twice in one split");
|
|
405
|
+
}
|
|
406
|
+
return p.homeKeys.flatMap((homeKey) => {
|
|
407
|
+
const row = instance(NomosShardAssignment, shardAssignmentRowId(homeKey));
|
|
408
|
+
return [set(row, "status", movingStatus(p.toShard))];
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Seal a completed handoff: flip each home's assignment `moving:<toShard>` →
|
|
414
|
+
* `active` on the TARGET shard, with the map version bumped (gate-verified:
|
|
415
|
+
* the held row must be mid-move to exactly this target, and the bump must be
|
|
416
|
+
* exactly held+1 — a stale or replayed seal refuses typed).
|
|
417
|
+
*/
|
|
418
|
+
export const nomosSealHandoff = directive("nomosSealHandoff")
|
|
419
|
+
.mutates(NomosShardAssignment)
|
|
420
|
+
.payload(
|
|
421
|
+
z.object({
|
|
422
|
+
fromShard: z.string().regex(/^s\d+$/),
|
|
423
|
+
toShard: z.string().regex(/^s\d+$/),
|
|
424
|
+
homeKeys: z.array(z.string().min(1)).min(1).max(64),
|
|
425
|
+
mapVersion: z.number().int().min(2),
|
|
426
|
+
sealedAt: z.string(),
|
|
427
|
+
}),
|
|
428
|
+
)
|
|
429
|
+
.plan((p) =>
|
|
430
|
+
p.homeKeys.flatMap((homeKey) => {
|
|
431
|
+
const row = instance(NomosShardAssignment, shardAssignmentRowId(homeKey));
|
|
432
|
+
return [
|
|
433
|
+
set(row, "shard", p.toShard),
|
|
434
|
+
set(row, "status", "active"),
|
|
435
|
+
set(row, "mapVersion", p.mapVersion),
|
|
436
|
+
];
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* THE SOURCE'S TERMINAL SEAL (authored on the SOURCE shard's own chain by the host
|
|
442
|
+
* after migration completes — the edge refuses it on the session lane: the seal pen
|
|
443
|
+
* is the bailiff's): STRIKE every migrated intent out of the fold (append-then-strike
|
|
444
|
+
* — the kernel's strike-correct reprojection sheds the moved rows, so tallies,
|
|
445
|
+
* deep-verify and the capacity load stay honest) and tombstone each home's receipt
|
|
446
|
+
* (`status=moved`, `shard=<target>` — the redirect the homing invariant serves).
|
|
447
|
+
*/
|
|
448
|
+
export const nomosSealHome = directive("nomosSealHome")
|
|
449
|
+
.ensures(NomosHomeReceipt)
|
|
450
|
+
.payload(
|
|
451
|
+
z.object({
|
|
452
|
+
homeKeys: z.array(z.string().min(1)).min(1).max(64),
|
|
453
|
+
target: z.string().regex(/^s\d+$/),
|
|
454
|
+
intentIds: z.array(z.string().min(1)).max(2048),
|
|
455
|
+
sealedAt: z.string(),
|
|
456
|
+
}),
|
|
457
|
+
)
|
|
458
|
+
.plan((p) => {
|
|
459
|
+
const ops: PlannedOp[] = [];
|
|
460
|
+
for (const homeKey of p.homeKeys) {
|
|
461
|
+
const row = instance(NomosHomeReceipt, homeReceiptRowId(homeKey));
|
|
462
|
+
ops.push(
|
|
463
|
+
withMarker(set(row, "homeKey", homeKey), "ensures"),
|
|
464
|
+
set(row, "shard", p.target),
|
|
465
|
+
set(row, "status", "moved"),
|
|
466
|
+
set(row, "receivedAt", p.sealedAt),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
for (const intentId of p.intentIds) ops.push(strike(intentId));
|
|
470
|
+
return ops;
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
/** Open shard `s<k>` (worker-authored when the auto-birth policy trips — ruling 5). */
|
|
474
|
+
export const nomosOpenShard = directive("nomosOpenShard")
|
|
475
|
+
.ensures(NomosShardRegistry)
|
|
476
|
+
.payload(z.object({ label: z.string().regex(/^s\d+$/), openedAt: z.string() }))
|
|
477
|
+
.plan((p) => {
|
|
478
|
+
const row = instance(NomosShardRegistry, shardRegistryRowId(p.label));
|
|
479
|
+
return [
|
|
480
|
+
withMarker(set(row, "label", p.label), "ensures"),
|
|
481
|
+
set(row, "status", "open"),
|
|
482
|
+
set(row, "openedAt", p.openedAt),
|
|
483
|
+
];
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
/** Amend the split policy (the ruling-5 dial; omitted threshold derives 75% of the wall). */
|
|
487
|
+
export const nomosSetShardPolicy = directive("nomosSetShardPolicy")
|
|
488
|
+
.ensures(NomosShardPolicy)
|
|
489
|
+
.payload(
|
|
490
|
+
z.object({
|
|
491
|
+
wallRows: z.number().int().min(1),
|
|
492
|
+
splitThresholdRows: z.number().int().min(1).optional(),
|
|
493
|
+
checkpointEveryDeltas: z.number().int().min(0).optional(), // 0 = cadence off
|
|
494
|
+
setAt: z.string(),
|
|
495
|
+
}),
|
|
496
|
+
)
|
|
497
|
+
.plan((p) => {
|
|
498
|
+
const row = instance(NomosShardPolicy, NOMOS_SHARD_POLICY_ID);
|
|
499
|
+
const threshold = p.splitThresholdRows ?? Math.max(1, Math.floor((p.wallRows * 3) / 4));
|
|
500
|
+
return [
|
|
501
|
+
withMarker(set(row, "scope", "self"), "ensures"),
|
|
502
|
+
set(row, "wallRows", p.wallRows),
|
|
503
|
+
set(row, "splitThresholdRows", threshold),
|
|
504
|
+
...(p.checkpointEveryDeltas !== undefined ? [set(row, "checkpointEveryDeltas", p.checkpointEveryDeltas)] : []),
|
|
505
|
+
set(row, "setAt", p.setAt),
|
|
506
|
+
];
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Amend a shard's delta frontier DIRECTLY (an Ensure on the watermark row). Two jobs:
|
|
511
|
+
* it DECLARES `NomosSummaryFrontier` as an ensures target (the id-mint gate admits an
|
|
512
|
+
* Ensure-introduced aggregate only for declared targets — `nomosPropagateSummary`'s
|
|
513
|
+
* plan writes this row but targets the subtotal type), and it is the slice-5
|
|
514
|
+
* `sealHandoff` primitive (closing/opening frontiers across a move). Day to day the
|
|
515
|
+
* frontier only ever moves THROUGH `nomosPropagateSummary`'s gate.
|
|
516
|
+
*/
|
|
517
|
+
export const nomosAmendSummaryFrontier = directive("nomosAmendSummaryFrontier")
|
|
518
|
+
.ensures(NomosSummaryFrontier)
|
|
519
|
+
.payload(
|
|
520
|
+
z.object({
|
|
521
|
+
shard: z.string().regex(/^s\d+$/),
|
|
522
|
+
frontier: z.string(),
|
|
523
|
+
readManifestHash: z.string(),
|
|
524
|
+
updatedAt: z.string(),
|
|
525
|
+
}),
|
|
526
|
+
)
|
|
527
|
+
.plan((p) => {
|
|
528
|
+
const wm = instance(NomosSummaryFrontier, summaryFrontierRowId(p.shard));
|
|
529
|
+
return [
|
|
530
|
+
withMarker(set(wm, "shard", p.shard), "ensures"),
|
|
531
|
+
set(wm, "frontier", p.frontier),
|
|
532
|
+
set(wm, "readManifestHash", p.readManifestHash),
|
|
533
|
+
set(wm, "updatedAt", p.updatedAt),
|
|
534
|
+
];
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
/** Record a deep-verify verdict (the §5.5 replay-on-suspicion op's committed outcome). */
|
|
538
|
+
export const nomosRecordDeepVerify = directive("nomosRecordDeepVerify")
|
|
539
|
+
.ensures(NomosDeepVerify)
|
|
540
|
+
.payload(
|
|
541
|
+
z.object({
|
|
542
|
+
shard: z.string().regex(/^s\d+$/),
|
|
543
|
+
frontier: z.string(),
|
|
544
|
+
head: z.string(),
|
|
545
|
+
verdict: z.string().min(1),
|
|
546
|
+
detail: z.string(),
|
|
547
|
+
checkedAt: z.string(),
|
|
548
|
+
}),
|
|
549
|
+
)
|
|
550
|
+
.plan((p) => {
|
|
551
|
+
const row = instance(NomosDeepVerify, deepVerifyRowId(p.shard));
|
|
552
|
+
return [
|
|
553
|
+
withMarker(set(row, "shard", p.shard), "ensures"),
|
|
554
|
+
set(row, "frontier", p.frontier),
|
|
555
|
+
set(row, "head", p.head),
|
|
556
|
+
set(row, "verdict", p.verdict),
|
|
557
|
+
set(row, "detail", p.detail),
|
|
558
|
+
set(row, "checkedAt", p.checkedAt),
|
|
559
|
+
];
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* SEAL A CHECKPOINT (§5.5, slice 4): bind the held per-shard frontiers + the
|
|
564
|
+
* subtotal state hash to the next sequence point. Host-authored on the cadence
|
|
565
|
+
* (the worker counts admitted deltas against the policy dial) or explicitly
|
|
566
|
+
* (`POST /v1/workspaces/<coordinator>/checkpoint`) — but gate-verified either
|
|
567
|
+
* way: the `nomosCheckpointGate` invariant pins every claimed frontier to the
|
|
568
|
+
* HELD `NomosSummaryFrontier` watermark (`pre:` reads) and the sequence to
|
|
569
|
+
* held-head + 1. A seal can never claim a frontier the delta lane never
|
|
570
|
+
* recorded, and never replay/skip a sequence point.
|
|
571
|
+
*/
|
|
572
|
+
export const nomosCheckpointSeal = directive("nomosCheckpointSeal")
|
|
573
|
+
.ensures(NomosCheckpointSeal)
|
|
574
|
+
.payload(
|
|
575
|
+
z.object({
|
|
576
|
+
seq: z.number().int().min(1),
|
|
577
|
+
frontiers: z
|
|
578
|
+
.array(z.object({ shard: z.string().regex(/^s\d+$/), frontier: z.string().min(1) }))
|
|
579
|
+
.min(1)
|
|
580
|
+
.max(4096),
|
|
581
|
+
stateHash: z.string().min(1),
|
|
582
|
+
sealedAt: z.string(),
|
|
583
|
+
}),
|
|
584
|
+
)
|
|
585
|
+
.plan((p) => {
|
|
586
|
+
const seen = new Set<string>();
|
|
587
|
+
for (const f of p.frontiers) {
|
|
588
|
+
if (seen.has(f.shard)) throw new Error(`seal-duplicate: shard '${f.shard}' appears twice in one seal`);
|
|
589
|
+
seen.add(f.shard);
|
|
590
|
+
}
|
|
591
|
+
const canonical = canonicalSealFrontiers(p.frontiers);
|
|
592
|
+
const ops: PlannedOp[] = [];
|
|
593
|
+
// The numbered seal row AND the head pointer — same aggregate, two deterministic ids.
|
|
594
|
+
for (const id of [checkpointSealRowId(p.seq), NOMOS_CHECKPOINT_HEAD_ID]) {
|
|
595
|
+
const row = instance(NomosCheckpointSeal, id);
|
|
596
|
+
ops.push(
|
|
597
|
+
withMarker(set(row, "seq", p.seq), "ensures"),
|
|
598
|
+
set(row, "frontiers", canonical),
|
|
599
|
+
set(row, "stateHash", p.stateHash),
|
|
600
|
+
set(row, "sealedAt", p.sealedAt),
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
return ops;
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// ── §5.2 — THE DELTA (the one Order the lane rides) ──────────────────────────────────
|
|
607
|
+
|
|
608
|
+
const summarySubtotalSchema = z.object({
|
|
609
|
+
readId: z.string().min(1),
|
|
610
|
+
group: z.string(), // "" = the grand-total group
|
|
611
|
+
kind: z.enum(["count", "sum", "rows"]),
|
|
612
|
+
prev: z.number().int(), // the CLAIMED previous absolute (gate-verified against held)
|
|
613
|
+
value: z.number().int(), // the CLAIMED new absolute (plan-verified against prev + Σ deltas)
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const summaryEventSchema = z.object({
|
|
617
|
+
oid: z.string().min(1), // the shard commit (content address — deep-verify resolves it)
|
|
618
|
+
intentId: z.string(),
|
|
619
|
+
deltas: z
|
|
620
|
+
.array(z.object({ readId: z.string().min(1), group: z.string(), delta: z.number().int() }))
|
|
621
|
+
.max(64),
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
export const nomosPropagateSummary = directive("nomosPropagateSummary")
|
|
625
|
+
.ensures(NomosSummarySubtotal)
|
|
626
|
+
.payload(
|
|
627
|
+
z.object({
|
|
628
|
+
shard: z.string().regex(/^s\d+$/),
|
|
629
|
+
fromOid: z.string(), // "" = the shard's first delta (a virgin coordinator record)
|
|
630
|
+
toOid: z.string().min(1),
|
|
631
|
+
readManifestHash: z.string(),
|
|
632
|
+
propagatedAt: z.string(),
|
|
633
|
+
subtotals: z.array(summarySubtotalSchema).min(1).max(1024),
|
|
634
|
+
events: z.array(summaryEventSchema).max(4096),
|
|
635
|
+
}),
|
|
636
|
+
)
|
|
637
|
+
.plan((p) => {
|
|
638
|
+
// ── THE GATE RECOMPUTATION, arithmetic half (ruling 4 — day one, in the law) ──
|
|
639
|
+
// Pure over the payload: every claimed absolute must equal the claimed previous
|
|
640
|
+
// plus the carried events' deltas, and every carried delta must land in a claimed
|
|
641
|
+
// subtotal (no hidden effects). The held-state half — fromOid == the recorded
|
|
642
|
+
// frontier, claimed prev == the held subtotal — is the `nomosSummaryGate`
|
|
643
|
+
// invariant's `pre:` reads. Together: held + carried evidence ⇒ claimed, or refuse.
|
|
644
|
+
if (p.fromOid === p.toOid) {
|
|
645
|
+
throw new Error(`summary-empty: fromOid and toOid are both '${p.toOid}' — an empty range propagates nothing`);
|
|
646
|
+
}
|
|
647
|
+
const claimed = new Map<string, { prev: number; value: number; sum: number }>();
|
|
648
|
+
for (const s of p.subtotals) {
|
|
649
|
+
const key = summaryBucket(s.readId, s.group);
|
|
650
|
+
if (claimed.has(key)) throw new Error(`summary-duplicate: subtotal (${s.readId}, '${s.group}') claimed twice`);
|
|
651
|
+
claimed.set(key, { prev: s.prev, value: s.value, sum: 0 });
|
|
652
|
+
}
|
|
653
|
+
for (const ev of p.events) {
|
|
654
|
+
for (const d of ev.deltas) {
|
|
655
|
+
const c = claimed.get(summaryBucket(d.readId, d.group));
|
|
656
|
+
if (c === undefined) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
`summary-incomplete: event ${ev.oid.slice(0, 10)} carries a delta for (${d.readId}, '${d.group}') ` +
|
|
659
|
+
`but no subtotal claims that group — every touched group must carry its absolute`,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
c.sum += d.delta;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
for (const s of p.subtotals) {
|
|
666
|
+
const c = claimed.get(summaryBucket(s.readId, s.group))!;
|
|
667
|
+
if (s.prev + c.sum !== s.value) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`summary-mismatch: (${s.readId}, '${s.group}') claims ${s.value} but prev ${s.prev} + ` +
|
|
670
|
+
`carried deltas ${c.sum} = ${s.prev + c.sum} — the evidence does not reproduce the claim`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const ops = [];
|
|
675
|
+
for (const s of p.subtotals) {
|
|
676
|
+
const row = instance(NomosSummarySubtotal, summarySubtotalRowId(p.shard, s.readId, s.group));
|
|
677
|
+
ops.push(
|
|
678
|
+
withMarker(set(row, "readId", s.readId), "ensures"),
|
|
679
|
+
set(row, "group", s.group),
|
|
680
|
+
set(row, "shard", p.shard),
|
|
681
|
+
set(row, "kind", s.kind),
|
|
682
|
+
set(row, "bucket", summaryBucket(s.readId, s.group)),
|
|
683
|
+
set(row, "value", s.value),
|
|
684
|
+
set(row, "frontier", p.toOid),
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
const wm = instance(NomosSummaryFrontier, summaryFrontierRowId(p.shard));
|
|
688
|
+
ops.push(
|
|
689
|
+
withMarker(set(wm, "shard", p.shard), "ensures"),
|
|
690
|
+
set(wm, "frontier", p.toOid),
|
|
691
|
+
set(wm, "readManifestHash", p.readManifestHash),
|
|
692
|
+
set(wm, "updatedAt", p.propagatedAt),
|
|
693
|
+
);
|
|
694
|
+
return ops;
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// ── helpers ───────────────────────────────────────────────────────────────────────────
|
|
698
|
+
|
|
699
|
+
const pascal = (s: string) =>
|
|
700
|
+
s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase());
|
|
701
|
+
const camel = (s: string) => {
|
|
702
|
+
const p = pascal(s);
|
|
703
|
+
return p.length ? p[0]!.toLowerCase() + p.slice(1) : p;
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
/** The derived placement directive's id for a packed axis type (`site` → `birthSite`). */
|
|
707
|
+
export function placementDirectiveId(axisType: string): string {
|
|
708
|
+
return `birth${pascal(axisType)}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** The derived placement payload's home-key field for a packed axis (`site` → `siteId`). */
|
|
712
|
+
export function placementHomeKeyField(axisType: string): string {
|
|
713
|
+
return `${camel(axisType)}Id`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ── THE ROUTE TAG, engine-side (the §4 self-routing mint — pure JS, no wasm change) ──
|
|
717
|
+
//
|
|
718
|
+
// sha256 over UTF-8, byte-for-byte the client/edge tag (`workspace_routing.ts`
|
|
719
|
+
// `routeTagHexOfHomeKey`): the first 48 bits of sha256("nomos-route:" + homeKey) as
|
|
720
|
+
// 12 lowercase hex chars. Compact, dependency-free, deterministic — it must run
|
|
721
|
+
// inside the sealed QuickJS sandbox (no crypto, no node builtins).
|
|
722
|
+
|
|
723
|
+
const SHA256_K = [
|
|
724
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
725
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
726
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
727
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
728
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
729
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
730
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
731
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
732
|
+
];
|
|
733
|
+
|
|
734
|
+
function utf8Bytes(s: string): number[] {
|
|
735
|
+
const out: number[] = [];
|
|
736
|
+
for (let i = 0; i < s.length; i++) {
|
|
737
|
+
let c = s.charCodeAt(i);
|
|
738
|
+
if (c < 0x80) out.push(c);
|
|
739
|
+
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
|
740
|
+
else if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) {
|
|
741
|
+
const lo = s.charCodeAt(i + 1);
|
|
742
|
+
if (lo >= 0xdc00 && lo <= 0xdfff) {
|
|
743
|
+
c = 0x10000 + ((c - 0xd800) << 10) + (lo - 0xdc00);
|
|
744
|
+
out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
745
|
+
i++;
|
|
746
|
+
} else out.push(0xef, 0xbf, 0xbd);
|
|
747
|
+
} else out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
748
|
+
}
|
|
749
|
+
return out;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** sha256 hex of a string's UTF-8 bytes — pure, sandbox-safe, deterministic. */
|
|
753
|
+
export function sha256HexSync(text: string): string {
|
|
754
|
+
const msg = utf8Bytes(text);
|
|
755
|
+
const bitLen = msg.length * 8;
|
|
756
|
+
msg.push(0x80);
|
|
757
|
+
while (msg.length % 64 !== 56) msg.push(0);
|
|
758
|
+
// 64-bit big-endian length (length < 2^53 — exact in float)
|
|
759
|
+
const hi = Math.floor(bitLen / 0x100000000);
|
|
760
|
+
msg.push((hi >>> 24) & 0xff, (hi >>> 16) & 0xff, (hi >>> 8) & 0xff, hi & 0xff);
|
|
761
|
+
msg.push((bitLen >>> 24) & 0xff, (bitLen >>> 16) & 0xff, (bitLen >>> 8) & 0xff, bitLen & 0xff);
|
|
762
|
+
const h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
763
|
+
const w = new Array<number>(64);
|
|
764
|
+
const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n));
|
|
765
|
+
for (let off = 0; off < msg.length; off += 64) {
|
|
766
|
+
for (let i = 0; i < 16; i++) {
|
|
767
|
+
w[i] = ((msg[off + 4 * i]! << 24) | (msg[off + 4 * i + 1]! << 16) | (msg[off + 4 * i + 2]! << 8) | msg[off + 4 * i + 3]!) >>> 0;
|
|
768
|
+
}
|
|
769
|
+
for (let i = 16; i < 64; i++) {
|
|
770
|
+
const s0 = rotr(w[i - 15]!, 7) ^ rotr(w[i - 15]!, 18) ^ (w[i - 15]! >>> 3);
|
|
771
|
+
const s1 = rotr(w[i - 2]!, 17) ^ rotr(w[i - 2]!, 19) ^ (w[i - 2]! >>> 10);
|
|
772
|
+
w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0;
|
|
773
|
+
}
|
|
774
|
+
let [a, b, c, d, e, f, g, hh] = h as [number, number, number, number, number, number, number, number];
|
|
775
|
+
for (let i = 0; i < 64; i++) {
|
|
776
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
777
|
+
const ch = (e & f) ^ (~e & g);
|
|
778
|
+
const t1 = (hh + S1 + ch + SHA256_K[i]! + w[i]!) >>> 0;
|
|
779
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
780
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
781
|
+
const t2 = (S0 + maj) >>> 0;
|
|
782
|
+
hh = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
|
|
783
|
+
}
|
|
784
|
+
h[0] = (h[0]! + a) >>> 0; h[1] = (h[1]! + b) >>> 0; h[2] = (h[2]! + c) >>> 0; h[3] = (h[3]! + d) >>> 0;
|
|
785
|
+
h[4] = (h[4]! + e) >>> 0; h[5] = (h[5]! + f) >>> 0; h[6] = (h[6]! + g) >>> 0; h[7] = (h[7]! + hh) >>> 0;
|
|
786
|
+
}
|
|
787
|
+
return h.map((x) => x.toString(16).padStart(8, "0")).join("");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** The 48-bit route tag of a home key — agrees byte-for-byte with the client/edge tag. */
|
|
791
|
+
export function routeTagHexSync(homeKey: string): string {
|
|
792
|
+
return sha256HexSync(`nomos-route:${homeKey}`).slice(0, 12);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** The 48-bit tag slot of a minted id (the UUIDv7 leading 12 hex), or undefined. */
|
|
796
|
+
function tagSlotOfMintedId(id: string): string | undefined {
|
|
797
|
+
const i = id.indexOf("_");
|
|
798
|
+
if (i <= 0) return undefined;
|
|
799
|
+
const body = id.slice(i + 1).replace(/-/g, "").toLowerCase();
|
|
800
|
+
return /^[0-9a-f]{32}$/.test(body) ? body.slice(0, 12) : undefined;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* ROUTE-TAGGED IN-PLAN MINTS (§4, the slice-2 boundary closed): wrap the sealed
|
|
805
|
+
* sandbox's `globalThis.plan` and `globalThis.nomos.mint` so that, WHILE a routed
|
|
806
|
+
* directive's plan runs, an in-plan `create(Agg)` mint of a PACKED-HOMED aggregate
|
|
807
|
+
* folds the intent's home ROUTE TAG into the UUIDv7 timestamp slot. Called by the
|
|
808
|
+
* GENERATED ENTRY of a taxonomy-bearing package, at lump top-level eval — BEFORE the
|
|
809
|
+
* engine freezes `globalThis` (the same window `registerEngine` uses for its own
|
|
810
|
+
* assignments). Determinism: the tagged mint consumes the SAME 19 captured rng draws
|
|
811
|
+
* a plain mint consumes and reads no clock — a replayed plan re-mints byte-identical
|
|
812
|
+
* ids. Outside the sealed sandbox (compile-lane imports, tests) this is a no-op.
|
|
813
|
+
*/
|
|
814
|
+
export function installRouteTaggedMint(spec: ShardingLawSpec): void {
|
|
815
|
+
const routes = new Map((spec.routes ?? []).map((r) => [r.directive, r]));
|
|
816
|
+
const homes = spec.homes ?? {};
|
|
817
|
+
if (routes.size === 0 || Object.keys(homes).length === 0) return;
|
|
818
|
+
const g = globalThis as {
|
|
819
|
+
plan?: (job: unknown) => unknown;
|
|
820
|
+
nomos?: { mint(typeTag: string): string; [k: string]: unknown };
|
|
821
|
+
__ports?: { rng(): number };
|
|
822
|
+
};
|
|
823
|
+
const prevPlan = g.plan;
|
|
824
|
+
const baseNomos = g.nomos;
|
|
825
|
+
const ports = g.__ports;
|
|
826
|
+
if (typeof prevPlan !== "function" || !baseNomos || typeof baseNomos.mint !== "function" || !ports) {
|
|
827
|
+
return; // not the sealed sandbox — nothing to install
|
|
828
|
+
}
|
|
829
|
+
let currentTag: string | null = null;
|
|
830
|
+
const nib = () => Math.floor(ports.rng() * 16).toString(16);
|
|
831
|
+
const nibs = (n: number) => { let s = ""; for (let i = 0; i < n; i++) s += nib(); return s; };
|
|
832
|
+
const taggedMint = (typeTag: string): string => {
|
|
833
|
+
if (currentTag === null || homes[String(typeTag)] === undefined) return baseNomos.mint(typeTag);
|
|
834
|
+
const tg = currentTag;
|
|
835
|
+
const variant = "89ab".charAt(Math.floor(ports.rng() * 4));
|
|
836
|
+
return (
|
|
837
|
+
String(typeTag) + "_" + tg.slice(0, 8) + "-" + tg.slice(8, 12) + "-7" + nibs(3) +
|
|
838
|
+
"-" + variant + nibs(3) + "-" + nibs(12)
|
|
839
|
+
);
|
|
840
|
+
};
|
|
841
|
+
g.nomos = Object.freeze({ ...baseNomos, mint: taggedMint });
|
|
842
|
+
g.plan = (job: unknown) => {
|
|
843
|
+
const intent = (job as { intent?: { directiveId?: string; payload?: Record<string, unknown> } } | null)?.intent ?? {};
|
|
844
|
+
const route = typeof intent.directiveId === "string" ? routes.get(intent.directiveId) : undefined;
|
|
845
|
+
currentTag = null;
|
|
846
|
+
if (route !== undefined) {
|
|
847
|
+
const v = intent.payload?.[route.key];
|
|
848
|
+
if (typeof v === "string" && v.length > 0) {
|
|
849
|
+
currentTag = route.via === "axis" ? routeTagHexSync(v) : (tagSlotOfMintedId(v) ?? null);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
return prevPlan(job);
|
|
854
|
+
} finally {
|
|
855
|
+
currentTag = null;
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Build the derived sharding-law module for a taxonomy. Deterministic in `spec`
|
|
862
|
+
* (the generated entry calls it with compile-derived literals, so the lump bytes —
|
|
863
|
+
* and the domain hash — move iff the taxonomy moves).
|
|
864
|
+
*/
|
|
865
|
+
export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown> {
|
|
866
|
+
const mod: Record<string, unknown> = {
|
|
867
|
+
NomosShardAssignment,
|
|
868
|
+
NomosShardIdentity,
|
|
869
|
+
NomosHomeReceipt,
|
|
870
|
+
NomosShardRegistry,
|
|
871
|
+
NomosShardPolicy,
|
|
872
|
+
NomosSummarySubtotal,
|
|
873
|
+
NomosSummaryFrontier,
|
|
874
|
+
NomosDeepVerify,
|
|
875
|
+
NomosCheckpointSeal,
|
|
876
|
+
nomosDeclareShardIdentity,
|
|
877
|
+
nomosReceiveAssignment,
|
|
878
|
+
nomosOpenShard,
|
|
879
|
+
nomosSetShardPolicy,
|
|
880
|
+
nomosPropagateSummary,
|
|
881
|
+
nomosAmendSummaryFrontier,
|
|
882
|
+
nomosRecordDeepVerify,
|
|
883
|
+
nomosCheckpointSeal,
|
|
884
|
+
nomosSplitShard,
|
|
885
|
+
nomosSealHandoff,
|
|
886
|
+
nomosSealHome,
|
|
887
|
+
nomosShardAssignmentByHomeKey: query("nomosShardAssignmentByHomeKey")
|
|
888
|
+
.key("homeKey")
|
|
889
|
+
.returns(NomosShardAssignment),
|
|
890
|
+
nomosShardIdentityByScope: query("nomosShardIdentityByScope")
|
|
891
|
+
.key("scope")
|
|
892
|
+
.returns(NomosShardIdentity),
|
|
893
|
+
nomosSummarySubtotalByShard: query("nomosSummarySubtotalByShard")
|
|
894
|
+
.key("shard")
|
|
895
|
+
.returns(NomosSummarySubtotal),
|
|
896
|
+
nomosShardRegistryByStatus: query("nomosShardRegistryByStatus")
|
|
897
|
+
.key("status")
|
|
898
|
+
.returns(NomosShardRegistry),
|
|
899
|
+
// THE ESTATE TOTAL (§5.1): an ORDINARY maintained sum over committed subtotal
|
|
900
|
+
// rows, grouped by `bucket` = readId group — O(1) by #31. A scoped read's
|
|
901
|
+
// estate value = the coordinator's local tally + this sum at its bucket.
|
|
902
|
+
nomosEstateSummary: sum("nomosEstateSummary", "value").of(NomosSummarySubtotal).by("bucket"),
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// ── §5.2 step 1+2 — THE SUMMARY GATE (held state versus the claims, via `pre:`) ──
|
|
906
|
+
// The plan above already proved claimed = claimedPrev + Σ carried deltas (pure
|
|
907
|
+
// arithmetic). This invariant pins the claims to HELD law-state, read PRE-APPLY
|
|
908
|
+
// (the executor's `pre:` namespace — the plan overwrites these very rows):
|
|
909
|
+
// * contiguity — fromOid must equal the recorded per-shard frontier (a gap,
|
|
910
|
+
// reorder, or fork refuses typed `frontier-gap:<expected>`; the shard re-emits
|
|
911
|
+
// from there — the suffix re-derivation lane);
|
|
912
|
+
// * recomputation — every claimed `prev` must equal the held subtotal value
|
|
913
|
+
// (`summary-mismatch:…`): held + carried events ⇒ claimed, byte-compared.
|
|
914
|
+
mod["nomosSummaryGate_nomosPropagateSummary"] = workspaceInvariant("nomosSummaryGate:nomosPropagateSummary")
|
|
915
|
+
.on("nomosPropagateSummary")
|
|
916
|
+
.reads(({ intent }) => {
|
|
917
|
+
const shard = String(intent["shard"] ?? "");
|
|
918
|
+
const subtotals = Array.isArray(intent["subtotals"]) ? (intent["subtotals"] as { readId?: unknown; group?: unknown }[]) : [];
|
|
919
|
+
return [
|
|
920
|
+
refAs("frontier", "NomosSummaryFrontier", `pre:${summaryFrontierRowId(shard)}`),
|
|
921
|
+
...subtotals.map((s, i) =>
|
|
922
|
+
refAs(`prev${i}`, "NomosSummarySubtotal", `pre:${summarySubtotalRowId(shard, String(s.readId ?? ""), String(s.group ?? ""))}`),
|
|
923
|
+
),
|
|
924
|
+
];
|
|
925
|
+
})
|
|
926
|
+
.assert((snapshots, ctx) => {
|
|
927
|
+
const intent = ctx?.intent ?? {};
|
|
928
|
+
const held = snapshots["frontier"]?.["frontier"];
|
|
929
|
+
const heldFrontier = typeof held === "string" ? held : "";
|
|
930
|
+
const fromOid = typeof intent["fromOid"] === "string" ? (intent["fromOid"] as string) : "";
|
|
931
|
+
if (fromOid !== heldFrontier) {
|
|
932
|
+
return { reject: `frontier-gap:${heldFrontier === "" ? "genesis" : heldFrontier}` };
|
|
933
|
+
}
|
|
934
|
+
const subtotals = Array.isArray(intent["subtotals"]) ? (intent["subtotals"] as { readId?: unknown; group?: unknown; prev?: unknown }[]) : [];
|
|
935
|
+
for (let i = 0; i < subtotals.length; i++) {
|
|
936
|
+
const s = subtotals[i]!;
|
|
937
|
+
const heldRaw = snapshots[`prev${i}`]?.["value"];
|
|
938
|
+
const heldValue = typeof heldRaw === "number" ? heldRaw : 0;
|
|
939
|
+
const claimedPrev = typeof s.prev === "number" ? s.prev : NaN;
|
|
940
|
+
if (claimedPrev !== heldValue) {
|
|
941
|
+
return {
|
|
942
|
+
reject: `summary-mismatch:${String(s.readId)}:held:${heldValue}:claimedPrev:${String(s.prev)}`,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return { accept: true };
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// ── §5.5 — THE CHECKPOINT GATE (slice 4, ruling 1): a seal's claims are judged
|
|
950
|
+
// against HELD law-state, pre-apply (the plan overwrites the head pointer):
|
|
951
|
+
// * IDEMPOTENT RE-OFFER — a seal row already held with the SAME canonical
|
|
952
|
+
// frontiers + state hash re-folds byte-identically (accept), even after
|
|
953
|
+
// the lane moved on; DIFFERENT content under a held sequence refuses
|
|
954
|
+
// typed `seal-exists:<seq>`;
|
|
955
|
+
// * FRONTIER PINNING — every claimed frontier must equal the recorded
|
|
956
|
+
// per-shard watermark (`seal-frontier-mismatch:<shard>:<held>`): a seal
|
|
957
|
+
// can never bind a frontier the delta lane never committed;
|
|
958
|
+
// * SEQUENCING — seq must be the held head + 1 (`seal-replay:<expected>`):
|
|
959
|
+
// no skipped or replayed sequence points. ──
|
|
960
|
+
mod["nomosCheckpointGate_nomosCheckpointSeal"] = workspaceInvariant("nomosCheckpointGate:nomosCheckpointSeal")
|
|
961
|
+
.on("nomosCheckpointSeal")
|
|
962
|
+
.reads(({ intent }) => {
|
|
963
|
+
const frontiers = Array.isArray(intent["frontiers"]) ? (intent["frontiers"] as { shard?: unknown }[]) : [];
|
|
964
|
+
const seq = typeof intent["seq"] === "number" ? (intent["seq"] as number) : "?";
|
|
965
|
+
return [
|
|
966
|
+
refAs("head", "NomosCheckpointSeal", `pre:${NOMOS_CHECKPOINT_HEAD_ID}`),
|
|
967
|
+
refAs("own", "NomosCheckpointSeal", `pre:${checkpointSealRowId(seq)}`),
|
|
968
|
+
...frontiers.map((f, i) =>
|
|
969
|
+
refAs(`front${i}`, "NomosSummaryFrontier", `pre:${summaryFrontierRowId(String(f?.shard ?? ""))}`),
|
|
970
|
+
),
|
|
971
|
+
];
|
|
972
|
+
})
|
|
973
|
+
.assert((snapshots, ctx) => {
|
|
974
|
+
const intent = ctx?.intent ?? {};
|
|
975
|
+
const claimedSeq = typeof intent["seq"] === "number" ? (intent["seq"] as number) : NaN;
|
|
976
|
+
const frontiers = Array.isArray(intent["frontiers"])
|
|
977
|
+
? (intent["frontiers"] as { shard?: unknown; frontier?: unknown }[])
|
|
978
|
+
: [];
|
|
979
|
+
const canonical = canonicalSealFrontiers(
|
|
980
|
+
frontiers.map((f) => ({ shard: String(f?.shard ?? ""), frontier: String(f?.frontier ?? "") })),
|
|
981
|
+
);
|
|
982
|
+
// 1. the idempotent re-offer: the SAME seal re-folds; different content refuses.
|
|
983
|
+
const own = snapshots["own"] ?? {};
|
|
984
|
+
if (typeof own["seq"] === "number") {
|
|
985
|
+
return own["stateHash"] === intent["stateHash"] && own["frontiers"] === canonical
|
|
986
|
+
? { accept: true }
|
|
987
|
+
: { reject: `seal-exists:${String(claimedSeq)}` };
|
|
988
|
+
}
|
|
989
|
+
// 2. every claimed frontier must be the HELD watermark.
|
|
990
|
+
for (let i = 0; i < frontiers.length; i++) {
|
|
991
|
+
const f = frontiers[i] ?? {};
|
|
992
|
+
const heldRaw = snapshots[`front${i}`]?.["frontier"];
|
|
993
|
+
const held = typeof heldRaw === "string" ? heldRaw : "";
|
|
994
|
+
if (held === "" || held !== f.frontier) {
|
|
995
|
+
return { reject: `seal-frontier-mismatch:${String(f.shard)}:${held === "" ? "genesis" : held}` };
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// 3. the next sequence point, exactly.
|
|
999
|
+
const headSeq = typeof (snapshots["head"] ?? {})["seq"] === "number" ? ((snapshots["head"] ?? {})["seq"] as number) : 0;
|
|
1000
|
+
if (claimedSeq !== headSeq + 1) return { reject: `seal-replay:${headSeq + 1}` };
|
|
1001
|
+
return { accept: true };
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
// ── §6 — THE MOVE GATE (slice 5): the lifecycle flips are judged against the HELD
|
|
1005
|
+
// rows (`pre:` reads — the plans overwrite them). A split may only move ACTIVE
|
|
1006
|
+
// homes it actually holds on the named source; a handoff seal may only land on
|
|
1007
|
+
// rows mid-move to exactly its target, with the map version bumped exactly once.
|
|
1008
|
+
// Every refusal is typed with the held state — the named remedy. ──
|
|
1009
|
+
const moveReads = ({ intent }: { intent: Record<string, unknown> }) => {
|
|
1010
|
+
const homeKeys = Array.isArray(intent["homeKeys"]) ? (intent["homeKeys"] as unknown[]) : [];
|
|
1011
|
+
return homeKeys.map((k, i) =>
|
|
1012
|
+
refAs(`held${i}`, "NomosShardAssignment", `pre:${shardAssignmentRowId(String(k ?? ""))}`),
|
|
1013
|
+
);
|
|
1014
|
+
};
|
|
1015
|
+
mod["nomosMoveGate_nomosSplitShard"] = workspaceInvariant("nomosMoveGate:nomosSplitShard")
|
|
1016
|
+
.on("nomosSplitShard")
|
|
1017
|
+
.reads(moveReads)
|
|
1018
|
+
.assert((snapshots, ctx) => {
|
|
1019
|
+
const intent = ctx?.intent ?? {};
|
|
1020
|
+
const fromShard = String(intent["fromShard"] ?? "");
|
|
1021
|
+
const homeKeys = Array.isArray(intent["homeKeys"]) ? (intent["homeKeys"] as unknown[]) : [];
|
|
1022
|
+
for (let i = 0; i < homeKeys.length; i++) {
|
|
1023
|
+
const held = snapshots[`held${i}`] ?? {};
|
|
1024
|
+
const heldShard = held["shard"];
|
|
1025
|
+
if (typeof heldShard !== "string" || heldShard.length === 0) {
|
|
1026
|
+
return { reject: `split-unplaced:${String(homeKeys[i])}` };
|
|
1027
|
+
}
|
|
1028
|
+
if (heldShard !== fromShard) return { reject: `wrong-source:${heldShard}` };
|
|
1029
|
+
const status = held["status"];
|
|
1030
|
+
if (typeof status === "string" && status !== "" && status !== "active") {
|
|
1031
|
+
return { reject: `not-active:${status}` };
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return { accept: true };
|
|
1035
|
+
});
|
|
1036
|
+
mod["nomosMoveGate_nomosSealHandoff"] = workspaceInvariant("nomosMoveGate:nomosSealHandoff")
|
|
1037
|
+
.on("nomosSealHandoff")
|
|
1038
|
+
.reads(moveReads)
|
|
1039
|
+
.assert((snapshots, ctx) => {
|
|
1040
|
+
const intent = ctx?.intent ?? {};
|
|
1041
|
+
const toShard = String(intent["toShard"] ?? "");
|
|
1042
|
+
const homeKeys = Array.isArray(intent["homeKeys"]) ? (intent["homeKeys"] as unknown[]) : [];
|
|
1043
|
+
const claimedVersion = typeof intent["mapVersion"] === "number" ? (intent["mapVersion"] as number) : NaN;
|
|
1044
|
+
for (let i = 0; i < homeKeys.length; i++) {
|
|
1045
|
+
const held = snapshots[`held${i}`] ?? {};
|
|
1046
|
+
const target = movingTargetOf(held["status"]);
|
|
1047
|
+
if (target === undefined) return { reject: `not-moving:${String(held["status"] ?? "unplaced")}` };
|
|
1048
|
+
if (target !== toShard) return { reject: `wrong-target:${target}` };
|
|
1049
|
+
const heldVersion = typeof held["mapVersion"] === "number" ? (held["mapVersion"] as number) : 1;
|
|
1050
|
+
if (claimedVersion !== heldVersion + 1) return { reject: `bad-map-version:${heldVersion + 1}` };
|
|
1051
|
+
}
|
|
1052
|
+
return { accept: true };
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
// ── THE HOMING INVARIANT, per routed `via:"axis"` directive (#41 — slice 3a;
|
|
1056
|
+
// slice 3: judged against the shard's OWN ASSIGNMENT RECEIPTS, not a static
|
|
1057
|
+
// formula — least-loaded placement is law-state, never a pure function) ──
|
|
1058
|
+
// No identity declared (label absent/"") ⇒ vacuously holds — the coordinator, a
|
|
1059
|
+
// direct unsharded workspace, and every pre-#41 chain judge exactly as before (the
|
|
1060
|
+
// edge bailiff stays the outer guard there). A `via:"id"` misroute needs no invariant:
|
|
1061
|
+
// its target does not exist on the wrong shard, which the referential gate refuses.
|
|
1062
|
+
const axisByType = new Map(spec.axes.map((a) => [a.axisType, a]));
|
|
1063
|
+
for (const route of spec.routes ?? []) {
|
|
1064
|
+
if (route.via !== "axis") continue;
|
|
1065
|
+
const axis = axisByType.get(route.home);
|
|
1066
|
+
if (axis === undefined) {
|
|
1067
|
+
throw new Error(
|
|
1068
|
+
`workspace-sharding: route for directive '${route.directive}' homes on '${route.home}' ` +
|
|
1069
|
+
`but no packed axis of that type exists — the taxonomy and the routes disagree. ` +
|
|
1070
|
+
`Recompile from one source (nomos-compile derives both).`,
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
const keyField = route.key;
|
|
1074
|
+
mod[`nomosWrongHome_${route.directive}`] = workspaceInvariant(`nomosWrongHome:${route.directive}`)
|
|
1075
|
+
.on(route.directive)
|
|
1076
|
+
.reads(({ intent }) => [
|
|
1077
|
+
refAs("shardIdentity", "NomosShardIdentity", NOMOS_SHARD_IDENTITY_ID),
|
|
1078
|
+
refAs("receipt", "NomosHomeReceipt", homeReceiptRowId(String(intent[keyField] ?? ""))),
|
|
1079
|
+
])
|
|
1080
|
+
.assert((snapshots, ctx) => {
|
|
1081
|
+
const identity = snapshots["shardIdentity"] ?? {};
|
|
1082
|
+
const label = identity["label"];
|
|
1083
|
+
// No declared identity (or the coordinator's "") ⇒ this workspace makes no
|
|
1084
|
+
// homing claim ⇒ the invariant holds vacuously.
|
|
1085
|
+
if (typeof label !== "string" || label.length === 0) return { accept: true };
|
|
1086
|
+
const homeKey = ctx?.intent?.[keyField];
|
|
1087
|
+
if (typeof homeKey !== "string" || homeKey.length === 0) {
|
|
1088
|
+
// A routed directive whose home field is absent is an unroutable write —
|
|
1089
|
+
// refuse with the named remedy (the field is required by the route law).
|
|
1090
|
+
return { reject: `wrong-home:unroutable:${keyField}` };
|
|
1091
|
+
}
|
|
1092
|
+
const receipt = snapshots["receipt"] ?? {};
|
|
1093
|
+
const receiptShard = receipt["shard"];
|
|
1094
|
+
if (typeof receiptShard !== "string" || receiptShard.length === 0) {
|
|
1095
|
+
// No receipt: this chain has never been assigned the home. The typed remedy
|
|
1096
|
+
// points at the receipt leg; the edge bailiff (coordinator-sighted) NAMES
|
|
1097
|
+
// the correct workspace on the same refusal lane.
|
|
1098
|
+
return { reject: `wrong-home:unassigned:${homeKey}` };
|
|
1099
|
+
}
|
|
1100
|
+
return receiptShard === label
|
|
1101
|
+
? { accept: true }
|
|
1102
|
+
: { reject: `wrong-home:${receiptShard}` };
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// ── THE RECEIPT'S OWN HOMING (a receipt claiming a foreign shard never folds) ──
|
|
1107
|
+
mod["nomosReceiptHome_nomosReceiveAssignment"] = workspaceInvariant("nomosReceiptHome:nomosReceiveAssignment")
|
|
1108
|
+
.on("nomosReceiveAssignment")
|
|
1109
|
+
.reads(() => [refAs("shardIdentity", "NomosShardIdentity", NOMOS_SHARD_IDENTITY_ID)])
|
|
1110
|
+
.assert((snapshots, ctx) => {
|
|
1111
|
+
const label = snapshots["shardIdentity"]?.["label"];
|
|
1112
|
+
if (typeof label !== "string" || label.length === 0) return { accept: true };
|
|
1113
|
+
const claimed = ctx?.intent?.["shard"];
|
|
1114
|
+
return claimed === label ? { accept: true } : { reject: `wrong-home:receipt:${String(claimed)}` };
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
for (const axis of spec.axes) {
|
|
1118
|
+
if (!Number.isInteger(axis.shardCount) || axis.shardCount < 1) {
|
|
1119
|
+
throw new Error(
|
|
1120
|
+
`workspace-sharding: axis '${axis.axisType}' has shardCount ${axis.shardCount} — ` +
|
|
1121
|
+
`declare the initial open-shard count as .pool(n) on the parent workspaceType.`,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
const keyField = placementHomeKeyField(axis.axisType);
|
|
1125
|
+
const axisType = axis.axisType;
|
|
1126
|
+
const dirId = placementDirectiveId(axis.axisType);
|
|
1127
|
+
// THE PLACEMENT (slice 3 — least-loaded): the shard rides IN the payload (picked
|
|
1128
|
+
// by the dispatching client/worker over committed load subtotals — law-state,
|
|
1129
|
+
// never a live read a plan takes); the row id is the home key (ONE active
|
|
1130
|
+
// assignment per home, by construction); a conflicting re-offer refuses typed.
|
|
1131
|
+
mod[dirId] = directive(dirId)
|
|
1132
|
+
.ensures(NomosShardAssignment)
|
|
1133
|
+
.payload(
|
|
1134
|
+
z.object({
|
|
1135
|
+
[keyField]: z.string().min(1),
|
|
1136
|
+
shard: z.string().regex(/^s\d+$/),
|
|
1137
|
+
placedAt: z.string(),
|
|
1138
|
+
}),
|
|
1139
|
+
)
|
|
1140
|
+
.plan((p: Record<string, string>) => {
|
|
1141
|
+
const homeKey = p[keyField]!;
|
|
1142
|
+
const row = instance(NomosShardAssignment, shardAssignmentRowId(homeKey));
|
|
1143
|
+
return [
|
|
1144
|
+
withMarker(set(row, "homeType", axisType), "ensures"),
|
|
1145
|
+
set(row, "homeKey", homeKey),
|
|
1146
|
+
set(row, "shard", p.shard!),
|
|
1147
|
+
set(row, "mapVersion", 1),
|
|
1148
|
+
set(row, "status", "active"),
|
|
1149
|
+
set(row, "placedAt", p.placedAt!),
|
|
1150
|
+
];
|
|
1151
|
+
});
|
|
1152
|
+
// PLACEMENT UNIQUENESS (task #42 item 5): an idempotent re-offer (same shard)
|
|
1153
|
+
// re-folds the same row; a CONFLICTING one refuses typed with the held shard.
|
|
1154
|
+
// THE MOVE-LANE GUARD (slice 5): a re-offer may only reproduce a FRESH placement
|
|
1155
|
+
// byte-for-byte — a home mid-move refuses `home-moving:<target>` (park, retry),
|
|
1156
|
+
// and a home the §6 lane has ever moved (mapVersion > 1) refuses
|
|
1157
|
+
// `placement-exists:<heldShard>` (the plan would regress status/mapVersion to
|
|
1158
|
+
// their birth values; the routed client answers such re-offers from the held
|
|
1159
|
+
// row WITHOUT authoring).
|
|
1160
|
+
mod[`nomosPlacementUnique_${dirId}`] = workspaceInvariant(`nomosPlacementUnique:${dirId}`)
|
|
1161
|
+
.on(dirId)
|
|
1162
|
+
.reads(({ intent }) => [
|
|
1163
|
+
refAs("held", "NomosShardAssignment", `pre:${shardAssignmentRowId(String(intent[keyField] ?? ""))}`),
|
|
1164
|
+
])
|
|
1165
|
+
.assert((snapshots, ctx) => {
|
|
1166
|
+
const held = snapshots["held"] ?? {};
|
|
1167
|
+
const heldShard = held["shard"];
|
|
1168
|
+
if (typeof heldShard !== "string" || heldShard.length === 0) return { accept: true };
|
|
1169
|
+
const target = movingTargetOf(held["status"]);
|
|
1170
|
+
if (target !== undefined) return { reject: `home-moving:${target}` };
|
|
1171
|
+
const heldVersion = typeof held["mapVersion"] === "number" ? (held["mapVersion"] as number) : 1;
|
|
1172
|
+
const offered = ctx?.intent?.["shard"];
|
|
1173
|
+
return offered === heldShard && heldVersion === 1
|
|
1174
|
+
? { accept: true } // the idempotent re-offer — same home, same shard, fresh era
|
|
1175
|
+
: { reject: `placement-exists:${heldShard}` };
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
return mod;
|
|
1179
|
+
}
|