@githolon/testing 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/LICENSE.md +45 -0
- package/README.md +159 -0
- package/build.mjs +23 -0
- package/index.d.ts +142 -0
- package/package.json +43 -0
- package/src/index.mjs +449 -0
- package/vendor/engine/engine.mjs +1370 -0
- package/vendor/engine/git-fs.mjs +93 -0
- package/vendor/engine/tree.mjs +73 -0
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
// AUTO-SYNCED from cloud/holon-host/src/engine.mjs by testing/build.mjs — DO NOT EDIT HERE.
|
|
2
|
+
// Nomos Cloud — THE ENGINE PLANE (one machinery, two hosts).
|
|
3
|
+
//
|
|
4
|
+
// Everything that touches a resident holon wasm instance + its /work tree lives HERE,
|
|
5
|
+
// extracted from the HolonDO so the 12GiB CONTAINER tier runs the IDENTICAL logic the
|
|
6
|
+
// edge DO runs (the old container worker was a drifting fork — killed). The hosts differ
|
|
7
|
+
// only in what they inject:
|
|
8
|
+
//
|
|
9
|
+
// * the EDGE host (worker.mjs HolonDO): wasm via the wrangler CompiledWasm import,
|
|
10
|
+
// replica from the DO id, ledgers minted from env.ARTIFACTS, runs in workerd;
|
|
11
|
+
// * the HEAVY host (container/holon-worker.mjs): wasm compiled from disk, replica
|
|
12
|
+
// from the container instance base, ledgers ARRIVE on each op (containers have no
|
|
13
|
+
// Workers bindings — the control-plane DO mints tokens and sends them), runs in Node.
|
|
14
|
+
//
|
|
15
|
+
// CONTROL PLANE STAYS IN THE DO: ownership secrets, manifest overlays, the DLQ store,
|
|
16
|
+
// quotas/rates, alarms. The engine RETURNS data (e.g. dead-letter entries with intent
|
|
17
|
+
// bytes) and the host persists. Runtime-neutral: workerd + Node 22 (globalThis.crypto,
|
|
18
|
+
// fetch, TextEncoder are the only ambient dependencies).
|
|
19
|
+
import { WASI, File, Directory, OpenFile, ConsoleStdout, PreopenDirectory } from "@bjorn3/browser_wasi_shim";
|
|
20
|
+
import git from "isomorphic-git";
|
|
21
|
+
import http from "isomorphic-git/http/web";
|
|
22
|
+
import { makeGitFs } from "./git-fs.mjs";
|
|
23
|
+
import { serializeTree } from "./tree.mjs";
|
|
24
|
+
|
|
25
|
+
const enc = new TextEncoder(), dec = new TextDecoder();
|
|
26
|
+
const BRANCH = "main";
|
|
27
|
+
export const repoArgOf = (ws) => `/work/ws/${ws}`;
|
|
28
|
+
export const gitdirOf = (ws) => `/work/ws/${ws}/nomos.git`;
|
|
29
|
+
|
|
30
|
+
const unpack = (p) => { const v = typeof p === "bigint" ? p : BigInt(p); return { ptr: Number(v >> 32n), len: Number(v & 0xffffffffn) }; };
|
|
31
|
+
export async function sha256hex(t) { const b = await crypto.subtle.digest("SHA-256", enc.encode(t)); return [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join(""); }
|
|
32
|
+
function osEntropyBuffer(n) { const bytes = new Uint8Array(n * 8); for (let o = 0; o < bytes.length; o += 65536) crypto.getRandomValues(bytes.subarray(o, Math.min(o + 65536, bytes.length))); const out = new Array(n), T = 2 ** 53; for (let i = 0; i < n; i++) { let z = 0n; for (let b = 7; b >= 0; b--) z = (z << 8n) | BigInt(bytes[i * 8 + b]); out[i] = Number(z >> 11n) / T; } return out; }
|
|
33
|
+
// Emit BigInt values as RAW JSON integers (a 63-bit replica must not round through a JS Number).
|
|
34
|
+
function stringifyBig(o) { return JSON.stringify(o, (_k, v) => (typeof v === "bigint" ? `@@B:${v}@@` : v)).replace(/"@@B:(\d+)@@"/g, "$1"); }
|
|
35
|
+
const log = (evt, fields = {}) => console.log(JSON.stringify({ evt, t: Date.now(), ...fields }));
|
|
36
|
+
|
|
37
|
+
function call(ex, mode, fields, STDERR) {
|
|
38
|
+
const req = enc.encode(JSON.stringify({ mode, ...fields }));
|
|
39
|
+
const reqPtr = ex.git_holon_alloc(req.length);
|
|
40
|
+
try {
|
|
41
|
+
new Uint8Array(ex.memory.buffer, reqPtr, req.length).set(req);
|
|
42
|
+
const { ptr, len } = unpack(ex.git_holon_call(reqPtr, req.length));
|
|
43
|
+
try {
|
|
44
|
+
const e = JSON.parse(dec.decode(new Uint8Array(ex.memory.buffer, ptr, len).slice()));
|
|
45
|
+
if (!e.ok) throw new Error((e.error || "holon error") + (STDERR.length ? ` | ${STDERR.slice(-4).join(" / ")}` : ""));
|
|
46
|
+
return e.result;
|
|
47
|
+
} finally { ex.git_holon_dealloc(ptr, len); }
|
|
48
|
+
} finally { ex.git_holon_dealloc(reqPtr, req.length); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function seedManifests(m, strings) {
|
|
52
|
+
m.set("domain_manifests.json", new File(enc.encode(strings.read)));
|
|
53
|
+
m.set("identity-manifests.json", new File(enc.encode(strings.identity)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const installPayload = (hash, usda, installedBy) => ({ domainHash: hash, packageUsda: usda, installedBy, authorityScope: "workspace/root", dependencies: [], finalizers: [] });
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Boot the resident wasm + /work tree. `wasmModule` is a PRECOMPILED WebAssembly.Module
|
|
60
|
+
* (the host compiles its way); `manifestStrings` = {read, identity} (baked ∪ overlays —
|
|
61
|
+
* the HOST owns overlay storage and recomputes); `replica` = the host's unique 63-bit id.
|
|
62
|
+
*/
|
|
63
|
+
export async function createEngine({ wasmModule, bootstrapPkg, nomosPkg, manifestStrings, replica, installedBy = "nomos-cloud" }) {
|
|
64
|
+
const hashes = { bootstrap: await sha256hex(bootstrapPkg), nomos: await sha256hex(nomosPkg) };
|
|
65
|
+
const root = new Map();
|
|
66
|
+
seedManifests(root, manifestStrings);
|
|
67
|
+
root.set("domain.package.usda", new File(enc.encode(bootstrapPkg)));
|
|
68
|
+
root.set("ws", new Directory(new Map()));
|
|
69
|
+
const preopen = new PreopenDirectory("/work", root);
|
|
70
|
+
const STDERR = [];
|
|
71
|
+
const fds = [new OpenFile(new File([])), ConsoleStdout.lineBuffered(() => {}), ConsoleStdout.lineBuffered((l) => STDERR.push(l)), preopen];
|
|
72
|
+
const wasi = new WASI(["wasm_git_holon", "reactor"], [], fds, { debug: false });
|
|
73
|
+
const inst = await WebAssembly.instantiate(wasmModule, { wasi_snapshot_preview1: wasi.wasiImport });
|
|
74
|
+
const code = wasi.start(inst);
|
|
75
|
+
if (code !== 0) throw new Error(`reactor init exited ${code}; stderr=${STDERR.join("\n")}`);
|
|
76
|
+
for (const n of ["memory", "git_holon_alloc", "git_holon_dealloc", "git_holon_call"]) if (!inst.exports[n]) throw new Error(`missing export ${n}`);
|
|
77
|
+
const eng = {
|
|
78
|
+
ex: inst.exports, preopen, STDERR, seq: 0, replica, hashes,
|
|
79
|
+
bootstrapPkg, nomosPkg, manifestStrings, installedBy,
|
|
80
|
+
fs: makeGitFs(preopen, "/work", { File, Directory }),
|
|
81
|
+
mounted: new Map(), // ws -> { restored, remoteMain, restoreError }
|
|
82
|
+
mainIntents: new Map(), // ws -> { head, ids:Set, oids:Set } (admission idempotence)
|
|
83
|
+
knownDomainsCache: null,
|
|
84
|
+
};
|
|
85
|
+
return eng;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Swap the manifest strings (a deploy staged a new overlay) + restage every mount. */
|
|
89
|
+
export function setManifests(eng, strings) {
|
|
90
|
+
eng.manifestStrings = strings;
|
|
91
|
+
eng.knownDomainsCache = null;
|
|
92
|
+
eng.directiveRoutesCache = null;
|
|
93
|
+
eng.scopedReadsCache = null;
|
|
94
|
+
eng.placementDirectivesCache = null;
|
|
95
|
+
eng.tenantTypesCache = null;
|
|
96
|
+
eng.globalAggregatesCache = null;
|
|
97
|
+
const root = eng.preopen.dir.contents;
|
|
98
|
+
seedManifests(root, strings);
|
|
99
|
+
const wsRoot = root.get("ws");
|
|
100
|
+
for (const ws of eng.mounted.keys()) {
|
|
101
|
+
const wsDir = wsRoot.contents.get(ws);
|
|
102
|
+
if (wsDir) seedManifests(wsDir.contents, strings);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The domains DEPLOYED to this workspace = the effective identity map's keys. */
|
|
107
|
+
export function knownDomains(eng) {
|
|
108
|
+
if (eng.knownDomainsCache) return eng.knownDomainsCache;
|
|
109
|
+
eng.knownDomainsCache = new Set(Object.keys(JSON.parse(eng.manifestStrings.identity)));
|
|
110
|
+
return eng.knownDomainsCache;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── TRANSPARENT SHARDING: the WRONG-HOME GATE (the edge bailiff — sharding slice 2) ──
|
|
114
|
+
//
|
|
115
|
+
// The LAW lives in two places the bailiff only READS: the identity manifests'
|
|
116
|
+
// hash-bearing `routes` table (which payload field carries a directive's home, derived
|
|
117
|
+
// at compile from the plan itself), and the coordinator chain's `NomosShardAssignment`
|
|
118
|
+
// rows (the shard map as law-state). The worker DECIDES NOTHING — exactly the quota-law
|
|
119
|
+
// posture: it compares the intent's home against the map and answers with a TYPED
|
|
120
|
+
// refusal naming the correct workspace. Since slice 3a (#41) the CHAIN GATE ITSELF also
|
|
121
|
+
// enforces wrong-home (the law's derived `nomosWrongHome:<directive>` workspace
|
|
122
|
+
// invariants, judged against the shard's own NomosShardIdentity law-state at author,
|
|
123
|
+
// admit AND verify_chain) — this bailiff is RETAINED as defense-in-depth in front of it.
|
|
124
|
+
|
|
125
|
+
/** directive routes off the effective identity manifests: `${domain} ${directiveId}` → route. */
|
|
126
|
+
export function directiveRoutes(eng) {
|
|
127
|
+
if (eng.directiveRoutesCache) return eng.directiveRoutesCache;
|
|
128
|
+
const routes = new Map();
|
|
129
|
+
let identity = {};
|
|
130
|
+
try { identity = JSON.parse(eng.manifestStrings.identity); } catch {}
|
|
131
|
+
for (const [domain, manifestJson] of Object.entries(identity)) {
|
|
132
|
+
try {
|
|
133
|
+
const m = typeof manifestJson === "string" ? JSON.parse(manifestJson) : manifestJson;
|
|
134
|
+
for (const r of (m && m.routes) || []) routes.set(`${domain} ${r.directive}`, r);
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
eng.directiveRoutesCache = routes;
|
|
138
|
+
return routes;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The 48-bit route tag of a home key (must agree byte-for-byte with the client mint). */
|
|
142
|
+
const routeTagHexOfHomeKey = async (homeKey) => (await sha256hex(`nomos-route:${homeKey}`)).slice(0, 12);
|
|
143
|
+
/** The tag slot of a kernel-minted id (the UUIDv7 leading 12 hex chars). */
|
|
144
|
+
function routeTagHexOfMintedId(id) {
|
|
145
|
+
const i = id.indexOf("_");
|
|
146
|
+
if (i <= 0) return undefined;
|
|
147
|
+
const body = id.slice(i + 1).replace(/-/g, "").toLowerCase();
|
|
148
|
+
return /^[0-9a-f]{32}$/.test(body) ? body.slice(0, 12) : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Judge ONE routed intent against the shard map. `shardRouting` is HOST-INJECTED:
|
|
153
|
+
* `{ selfLabel, coordinatorWs, assignments() }` — `selfLabel` is this workspace's shard
|
|
154
|
+
* label (`s<k>`) or null when it IS the coordinator; `assignments()` resolves the
|
|
155
|
+
* ACTIVE `NomosShardAssignment` rows from the coordinator's law-state. Returns null to
|
|
156
|
+
* pass, or `{ error, wrongHome?, deadLettered? }`:
|
|
157
|
+
* * assigned elsewhere → the TYPED `wrong-home` refusal naming the correct workspace
|
|
158
|
+
* (never dead-lettered: a routed client self-heals it);
|
|
159
|
+
* * no placement at all → a DEAD-LETTERED jam with the placement remedy (legitimate
|
|
160
|
+
* work, never lost — the DLQ discipline).
|
|
161
|
+
*/
|
|
162
|
+
export async function wrongHomeVerdict(shardRouting, route, payload) {
|
|
163
|
+
const value = payload ? payload[route.key] : undefined;
|
|
164
|
+
if (typeof value !== "string" || !value) return null; // shape problems are the law's verdict, not the bailiff's
|
|
165
|
+
const rows = (await shardRouting.assignments()) || [];
|
|
166
|
+
let assignment = null;
|
|
167
|
+
if (route.via === "axis") {
|
|
168
|
+
assignment = rows.find((a) => a.homeKey === value) || null;
|
|
169
|
+
} else {
|
|
170
|
+
const tag = routeTagHexOfMintedId(value);
|
|
171
|
+
if (tag !== undefined) {
|
|
172
|
+
for (const a of rows) {
|
|
173
|
+
if ((await routeTagHexOfHomeKey(a.homeKey)) === tag) { assignment = a; break; }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!assignment) {
|
|
178
|
+
return {
|
|
179
|
+
error: `no shard placement for this write's home ('${route.key}': ${value.slice(0, 80)}) — author the derived placement intent (birth<Type>) on the coordinator '${shardRouting.coordinatorWs}' first, then POST /dead-letters/retry`,
|
|
180
|
+
deadLettered: true,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// §6 — HOME-MOVING (slice 5): a home mid-handoff accepts NO writes anywhere — the
|
|
184
|
+
// typed refusal PARKS the work (dead-lettered on both sides, never lost); a routed
|
|
185
|
+
// client retries it onto the post-split home once the handoff seals.
|
|
186
|
+
const movingTo = typeof assignment.status === "string" && assignment.status.startsWith("moving:")
|
|
187
|
+
? assignment.status.slice("moving:".length)
|
|
188
|
+
: null;
|
|
189
|
+
if (movingTo) {
|
|
190
|
+
return {
|
|
191
|
+
error: `home-moving: home '${value.slice(0, 80)}' is migrating ${assignment.shard} -> ${movingTo} (shard map v${assignment.mapVersion ?? 1}) — parked; a routed client retries automatically after the handoff seals`,
|
|
192
|
+
homeMoving: { homeKey: value, from: assignment.shard, target: movingTo, workspace: `${shardRouting.coordinatorWs}--${movingTo}` },
|
|
193
|
+
deadLettered: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (shardRouting.selfLabel === assignment.shard) return null;
|
|
197
|
+
const correct = `${shardRouting.coordinatorWs}--${assignment.shard}`;
|
|
198
|
+
return {
|
|
199
|
+
error: `wrong-home: this write's home is assigned to '${correct}' (shard map v${assignment.mapVersion ?? 1}) — the client's shard map is stale; a routed client re-routes and retries automatically`,
|
|
200
|
+
wrongHome: { workspace: correct, ...(assignment.mapVersion !== undefined ? { mapVersion: assignment.mapVersion } : {}) },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── §5.2 — THE DELTA LANE (sharding slice 3): the shard-side EMITTER machinery ──
|
|
205
|
+
//
|
|
206
|
+
// After a shard's edge admission folds a batch, the HOST authors one coalesced
|
|
207
|
+
// `nomosPropagateSummary` Order into the COORDINATOR's chain: per touched
|
|
208
|
+
// (scoped read, group) the ABSOLUTE subtotal + the covered range's per-intent
|
|
209
|
+
// event deltas as content-addressed evidence. The coordinator's ONE gate
|
|
210
|
+
// adjudicates it (frontier contiguity + held-vs-claimed recomputation — the law
|
|
211
|
+
// in workspace_sharding.ts); the engine here only DERIVES the evidence, with the
|
|
212
|
+
// shard's own projection as the oracle (maintained tallies read before/after each
|
|
213
|
+
// admitted intent — O(touched), never the world).
|
|
214
|
+
|
|
215
|
+
/** The estate-total bucket separator (must agree with the law's `summaryBucket`). */
|
|
216
|
+
const SUMMARY_SEP = "\u001f";
|
|
217
|
+
const summaryKey = (readId, group) => `${readId}${SUMMARY_SEP}${group}`;
|
|
218
|
+
const splitSummaryKey = (key) => { const i = key.indexOf(SUMMARY_SEP); return [key.slice(0, i), key.slice(i + 1)]; };
|
|
219
|
+
/** The synthetic per-shard row-count read (capacity headroom — the split policy's input). */
|
|
220
|
+
export const NOMOS_SHARD_ROWS_READ = "nomosShardRows";
|
|
221
|
+
// FRAMEWORK aggregates are EXCLUDED from the row-count read (and from deep-verify's
|
|
222
|
+
// recount): identity/receipt rows enter chains through the deploy + receipt legs
|
|
223
|
+
// (never via session admission), so counting them would let the lanes drift. The
|
|
224
|
+
// load metric is TENANT rows — which is what capacity headroom means anyway.
|
|
225
|
+
const FRAMEWORK_AGG_TYPES = new Set([
|
|
226
|
+
"NomosShardAssignment", "NomosShardIdentity", "NomosHomeReceipt", "NomosShardRegistry",
|
|
227
|
+
"NomosShardPolicy", "NomosSummarySubtotal", "NomosSummaryFrontier", "NomosDeepVerify",
|
|
228
|
+
"NomosCheckpointSeal",
|
|
229
|
+
// CONTROL-PLANE law-state (every chain carries them via installDomain) — never tenant data:
|
|
230
|
+
"PolicyBundle", "DomainInstallation",
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
/** TENANT aggregate types = the read manifest's declared field-kind types minus the
|
|
234
|
+
* framework set — the ONE definition all three row-count lanes share (the capture,
|
|
235
|
+
* the suffix re-derivation, deep-verify's recount): control-plane rows (PolicyBundle,
|
|
236
|
+
* DomainInstallation) and framework law-state never skew the capacity metric. */
|
|
237
|
+
function tenantTypes(eng) {
|
|
238
|
+
if (eng.tenantTypesCache) return eng.tenantTypesCache;
|
|
239
|
+
let readManifest = {};
|
|
240
|
+
try { readManifest = JSON.parse(eng.manifestStrings.read); } catch {}
|
|
241
|
+
// §5.4 (slice 4): GLOBAL reference data is replicated custody on every shard —
|
|
242
|
+
// its rows arrive by the reverse lane (replicateIntents — a host leg the §5.2
|
|
243
|
+
// capture never sees), so counting them as tenant load would let the three
|
|
244
|
+
// row-count lanes drift (capture vs suffix vs deep-verify recount). Excluded
|
|
245
|
+
// from the capacity metric on EVERY lane, by the one shared definition here.
|
|
246
|
+
const globals = globalAggregates(eng);
|
|
247
|
+
const out = new Set(Object.keys(readManifest.aggregateFieldKinds || {}).filter((t) => !FRAMEWORK_AGG_TYPES.has(t) && !globals.has(t)));
|
|
248
|
+
eng.tenantTypesCache = out;
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The derived PLACEMENT directives off the effective identity manifests:
|
|
253
|
+
* packed type `site` ⇒ `birthSite` keyed by payload field `siteId` (the law's naming). */
|
|
254
|
+
export function placementDirectives(eng) {
|
|
255
|
+
if (eng.placementDirectivesCache) return eng.placementDirectivesCache;
|
|
256
|
+
const out = new Map(); // directiveId -> { axis, keyField }
|
|
257
|
+
const pascal = (s) => s.replace(/[_\s-]+(\w)/g, (_m, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase());
|
|
258
|
+
let identity = {};
|
|
259
|
+
try { identity = JSON.parse(eng.manifestStrings.identity); } catch {}
|
|
260
|
+
for (const manifestJson of Object.values(identity)) {
|
|
261
|
+
let m;
|
|
262
|
+
try { m = typeof manifestJson === "string" ? JSON.parse(manifestJson) : manifestJson; } catch { continue; }
|
|
263
|
+
for (const t of (m && m.workspaceTypes) || []) {
|
|
264
|
+
if (!t || t.mode !== "packed") continue;
|
|
265
|
+
const p = pascal(String(t.id));
|
|
266
|
+
out.set(`birth${p}`, { axis: t.id, keyField: `${p.length ? p[0].toLowerCase() + p.slice(1) : p}Id` });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
eng.placementDirectivesCache = out;
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The ESTATE-SCOPED maintained reads off the effective identity manifests.
|
|
274
|
+
* SLICE 4: `where` (the canonical predicate, hash-bearing in the manifest) rides
|
|
275
|
+
* along — the LIVE capture's oracle is the projection's maintained tally (already
|
|
276
|
+
* predicate-aware in the kernel); the SUFFIX re-derivation evaluates it host-side
|
|
277
|
+
* ({@link evalCanonicalPred}) so both lanes agree byte-for-byte. */
|
|
278
|
+
export function scopedReads(eng) {
|
|
279
|
+
if (eng.scopedReadsCache) return eng.scopedReadsCache;
|
|
280
|
+
const out = [];
|
|
281
|
+
let identity = {};
|
|
282
|
+
try { identity = JSON.parse(eng.manifestStrings.identity); } catch {}
|
|
283
|
+
for (const manifestJson of Object.values(identity)) {
|
|
284
|
+
let m;
|
|
285
|
+
try { m = typeof manifestJson === "string" ? JSON.parse(manifestJson) : manifestJson; } catch { continue; }
|
|
286
|
+
for (const c of (m && m.counts) || []) {
|
|
287
|
+
if (c && typeof c.scope === "string") out.push({ id: c.id, kind: "count", of: c.of, by: c.by ?? null, ...(c.where ? { where: c.where } : {}) });
|
|
288
|
+
}
|
|
289
|
+
for (const s of (m && m.sums) || []) {
|
|
290
|
+
if (s && typeof s.scope === "string") out.push({ id: s.id, kind: "sum", of: s.of, by: s.by ?? null, field: s.sumField, ...(s.where ? { where: s.where } : {}) });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
eng.scopedReadsCache = out;
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Evaluate a canonical count/sum predicate (`predicate.ts` CanonicalPred — eq/ne
|
|
299
|
+
* clauses + and/or trees) over a host-folded field map. EXACTLY the kernel's
|
|
300
|
+
* declared semantics: values canonicalize to strings (int fields compare their
|
|
301
|
+
* decimal form); `eq` over an ABSENT field is FALSE, `ne` over an absent field is
|
|
302
|
+
* TRUE (unset ≠ any literal — the §7-D symmetry rule). Used ONLY by the suffix
|
|
303
|
+
* re-derivation fold; the live capture's oracle is the maintained tally itself.
|
|
304
|
+
*/
|
|
305
|
+
export function evalCanonicalPred(pred, get) {
|
|
306
|
+
if (!pred || typeof pred !== "object") return true;
|
|
307
|
+
if (pred.kind === "and") return (pred.clauses || []).every((c) => evalCanonicalPred(c, get));
|
|
308
|
+
if (pred.kind === "or") return (pred.clauses || []).some((c) => evalCanonicalPred(c, get));
|
|
309
|
+
const raw = get(pred.field);
|
|
310
|
+
const present = raw !== undefined && raw !== null;
|
|
311
|
+
const canonical = present ? String(raw) : undefined;
|
|
312
|
+
if (pred.kind === "eq") return present && canonical === pred.value;
|
|
313
|
+
if (pred.kind === "ne") return !present || canonical !== pred.value;
|
|
314
|
+
return true; // an unknown clause kind never silently filters — fail-open here is fail-CLOSED at the gate (values mismatch loudly)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* THE GLOBAL (reference-data) AGGREGATES (§5.4, slice 4): the identity manifests'
|
|
319
|
+
* `workspaceTypes[].globals` — aggregates homed on the COORDINATOR and replicated
|
|
320
|
+
* DOWNWARD into every shard chain by re-admission (the reverse delta lane).
|
|
321
|
+
*/
|
|
322
|
+
export function globalAggregates(eng) {
|
|
323
|
+
if (eng.globalAggregatesCache) return eng.globalAggregatesCache;
|
|
324
|
+
const out = new Set();
|
|
325
|
+
let identity = {};
|
|
326
|
+
try { identity = JSON.parse(eng.manifestStrings.identity); } catch {}
|
|
327
|
+
for (const manifestJson of Object.values(identity)) {
|
|
328
|
+
let m;
|
|
329
|
+
try { m = typeof manifestJson === "string" ? JSON.parse(manifestJson) : manifestJson; } catch { continue; }
|
|
330
|
+
for (const t of (m && m.workspaceTypes) || []) {
|
|
331
|
+
for (const g of (t && t.globals) || []) if (typeof g === "string") out.add(g);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
eng.globalAggregatesCache = out;
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Parse an intent blob's sealed events: [{aggregate, ops: Map(field -> {Str|Int})}]. */
|
|
339
|
+
function parseIntentEvents(blob) {
|
|
340
|
+
let doc;
|
|
341
|
+
try { doc = JSON.parse(dec.decode(blob)); } catch { return []; }
|
|
342
|
+
const events = Array.isArray(doc && doc.events) ? doc.events : [];
|
|
343
|
+
return events.map((ev) => {
|
|
344
|
+
const sets = new Map();
|
|
345
|
+
for (const op of ev.ops || []) {
|
|
346
|
+
const v = op && op.op && op.op.Set;
|
|
347
|
+
if (v === undefined) continue;
|
|
348
|
+
if (typeof v.Str === "string") sets.set(op.field, v.Str);
|
|
349
|
+
else if (typeof v.Int === "number") sets.set(op.field, v.Int);
|
|
350
|
+
}
|
|
351
|
+
return { aggregate: String(ev.aggregate || ""), sets };
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const groupOf = (read, data) => {
|
|
356
|
+
if (read.by === null) return "";
|
|
357
|
+
const v = data ? data[read.by] : undefined;
|
|
358
|
+
return v === undefined || v === null ? undefined : String(v);
|
|
359
|
+
};
|
|
360
|
+
const tallyOf = (eng, ws, read, group) =>
|
|
361
|
+
read.kind === "count" ? count(eng, ws, read.id, group).count : sum(eng, ws, read.id, group).sum;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Begin one admitted intent's summary capture: snapshot the touched aggregates +
|
|
365
|
+
* the maintained tallies of every CANDIDATE (read, group) BEFORE the fold. The
|
|
366
|
+
* candidates come from the intent's own sealed ops (new group values ride the
|
|
367
|
+
* Set ops) plus the pre-state rows (old group values) — bounded by the intent.
|
|
368
|
+
*/
|
|
369
|
+
function beginSummaryCapture(eng, ws, ctx, blob) {
|
|
370
|
+
const events = parseIntentEvents(blob);
|
|
371
|
+
if (events.length === 0) return null;
|
|
372
|
+
const pre = new Map(); // aggregateId -> {type, data} | null
|
|
373
|
+
const candidates = new Set(); // summaryKey(readId, group)
|
|
374
|
+
for (const ev of events) {
|
|
375
|
+
if (!pre.has(ev.aggregate)) {
|
|
376
|
+
const rows = qById(eng, ws, ev.aggregate);
|
|
377
|
+
pre.set(ev.aggregate, rows.length ? { type: rows[0].type, data: rows[0].data || {} } : null);
|
|
378
|
+
}
|
|
379
|
+
const before = pre.get(ev.aggregate);
|
|
380
|
+
const evType = ev.sets.get("__type") ?? (before ? before.type : undefined);
|
|
381
|
+
for (const read of ctx.reads) {
|
|
382
|
+
if (read.of !== evType) continue;
|
|
383
|
+
const oldGroup = before ? groupOf(read, before.data) : undefined;
|
|
384
|
+
if (oldGroup !== undefined) candidates.add(summaryKey(read.id, oldGroup));
|
|
385
|
+
if (read.by === null) candidates.add(summaryKey(read.id, ""));
|
|
386
|
+
else if (ev.sets.has(read.by)) candidates.add(summaryKey(read.id, String(ev.sets.get(read.by))));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Tally every candidate BEFORE the fold; record the batch-start value once per group.
|
|
390
|
+
const preTallies = new Map();
|
|
391
|
+
for (const key of candidates) {
|
|
392
|
+
const [readId, group] = splitSummaryKey(key);
|
|
393
|
+
const read = ctx.reads.find((r) => r.id === readId);
|
|
394
|
+
const v = tallyOf(eng, ws, read, group);
|
|
395
|
+
preTallies.set(key, v);
|
|
396
|
+
if (!ctx.prevs.has(key)) ctx.prevs.set(key, { read, group, value: v });
|
|
397
|
+
}
|
|
398
|
+
return { events, pre, candidates, preTallies };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Finish the capture AFTER the fold: re-tally, diff, append the evidence event. */
|
|
402
|
+
function finishSummaryCapture(eng, ws, ctx, cap, oid, intentId) {
|
|
403
|
+
const deltas = [];
|
|
404
|
+
for (const key of cap.candidates) {
|
|
405
|
+
const [readId, group] = splitSummaryKey(key);
|
|
406
|
+
const read = ctx.reads.find((r) => r.id === readId);
|
|
407
|
+
const after = tallyOf(eng, ws, read, group);
|
|
408
|
+
const before = cap.preTallies.get(key) ?? 0;
|
|
409
|
+
if (after !== before) deltas.push({ readId, group, delta: after - before });
|
|
410
|
+
ctx.values.set(key, { read, group, value: after });
|
|
411
|
+
}
|
|
412
|
+
// The synthetic row-count read: existence flips, with the projection as oracle.
|
|
413
|
+
// The ABSOLUTE rides relative to the coordinator-held value (the HOST closes it
|
|
414
|
+
// at emit time — a shard never enumerates its own whole projection per batch).
|
|
415
|
+
let rowsDelta = 0;
|
|
416
|
+
for (const [aggregateId, before] of cap.pre) {
|
|
417
|
+
const rows = qById(eng, ws, aggregateId);
|
|
418
|
+
const type = rows.length ? rows[0].type : before ? before.type : undefined;
|
|
419
|
+
if (type === undefined || !tenantTypes(eng).has(type)) continue; // tenant rows only
|
|
420
|
+
const existsAfter = rows.length > 0;
|
|
421
|
+
if (!before && existsAfter) rowsDelta += 1;
|
|
422
|
+
else if (before && !existsAfter) rowsDelta -= 1;
|
|
423
|
+
}
|
|
424
|
+
if (rowsDelta !== 0) {
|
|
425
|
+
deltas.push({ readId: NOMOS_SHARD_ROWS_READ, group: "", delta: rowsDelta });
|
|
426
|
+
ctx.rowsDelta += rowsDelta;
|
|
427
|
+
}
|
|
428
|
+
if (deltas.length > 0) ctx.events.push({ oid, intentId: intentId || "", deltas });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The coalesced delta of one admission batch (null when nothing summarizable moved).
|
|
433
|
+
* `rowsDelta` rides separately: the HOST closes the `nomosShardRows` absolute against
|
|
434
|
+
* the coordinator-held value at emit time (held + Σ deltas — the gate re-checks both
|
|
435
|
+
* halves). The other subtotals' `prev` is the batch-start maintained tally, which
|
|
436
|
+
* equals the coordinator-held value exactly when the frontier is contiguous — and the
|
|
437
|
+
* gate refuses otherwise (the suffix re-derivation lane is the remedy).
|
|
438
|
+
*/
|
|
439
|
+
function summaryOfCapture(ctx, fromOid, toOid) {
|
|
440
|
+
if (ctx.events.length === 0 || !toOid || fromOid === toOid) return null;
|
|
441
|
+
const subtotals = [];
|
|
442
|
+
for (const [key, cur] of ctx.values) {
|
|
443
|
+
const prev = ctx.prevs.get(key);
|
|
444
|
+
const prevValue = prev ? prev.value : 0;
|
|
445
|
+
if (cur.value === prevValue) continue; // untouched-in-net — carries nothing
|
|
446
|
+
subtotals.push({
|
|
447
|
+
readId: cur.read.id,
|
|
448
|
+
group: cur.group,
|
|
449
|
+
kind: cur.read.kind,
|
|
450
|
+
prev: prevValue,
|
|
451
|
+
value: cur.value,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (subtotals.length === 0 && ctx.rowsDelta === 0) return null;
|
|
455
|
+
return {
|
|
456
|
+
fromOid: fromOid || "",
|
|
457
|
+
toOid,
|
|
458
|
+
subtotals: subtotals.sort((a, b) => (summaryKey(a.readId, a.group) < summaryKey(b.readId, b.group) ? -1 : 1)),
|
|
459
|
+
events: ctx.events,
|
|
460
|
+
rowsDelta: ctx.rowsDelta,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* THE SUFFIX RE-DERIVATION (the frontier-gap self-heal + /summaries/resync): derive
|
|
466
|
+
* the delta payload for (fromOid → main head] from THE CHAIN ITSELF — a host-side
|
|
467
|
+
* pure fold of the scoped reads' group/sum fields over every commit since genesis
|
|
468
|
+
* (Lww in chain order; scoped reads are predicate-free by the compile gate). Used
|
|
469
|
+
* when the coordinator's recorded frontier does not match a live capture (a lost
|
|
470
|
+
* emission, DO eviction, an operator resync) — O(chain), explicit, logged.
|
|
471
|
+
*/
|
|
472
|
+
export async function deriveSummarySuffix(eng, ws, fromOid) {
|
|
473
|
+
const reads = scopedReads(eng);
|
|
474
|
+
const gitdir = gitdirOf(ws);
|
|
475
|
+
const head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
476
|
+
if (!head) return { ok: false, error: "no local main to derive from" };
|
|
477
|
+
const commits = await git.log({ fs: eng.fs, gitdir, ref: BRANCH });
|
|
478
|
+
commits.reverse(); // genesis → head
|
|
479
|
+
const state = new Map(); // aggregateId -> { type, fields: Map }
|
|
480
|
+
const running = new Map(); // summaryKey -> value
|
|
481
|
+
const bump = (key, d) => running.set(key, (running.get(key) ?? 0) + d);
|
|
482
|
+
let prevs = null; // snapshot at fromOid
|
|
483
|
+
let events = [];
|
|
484
|
+
let inSuffix = fromOid === "" || fromOid === undefined;
|
|
485
|
+
if (inSuffix) prevs = new Map();
|
|
486
|
+
let sawFrom = inSuffix;
|
|
487
|
+
for (const c of commits) {
|
|
488
|
+
let blob = null;
|
|
489
|
+
try { blob = (await git.readBlob({ fs: eng.fs, gitdir, oid: c.oid, filepath: "intent.json" })).blob; } catch {}
|
|
490
|
+
const deltas = [];
|
|
491
|
+
let intentId = "";
|
|
492
|
+
if (blob) {
|
|
493
|
+
let doc = null;
|
|
494
|
+
try { doc = JSON.parse(dec.decode(blob)); } catch {}
|
|
495
|
+
// A STRIKE-BEARING chain (a §6 seal retracted moved history): the incremental
|
|
496
|
+
// event fold below cannot express the retraction (only the kernel's
|
|
497
|
+
// strike-correct reprojection can). Answer `strikeBearing` — the caller closes
|
|
498
|
+
// against the coordinator-HELD values with the PROJECTION as the oracle
|
|
499
|
+
// (summaryCloseAgainstHeld), exactly the posture the live capture already uses.
|
|
500
|
+
if (doc && Array.isArray(doc.strikes) && doc.strikes.length > 0) {
|
|
501
|
+
return { ok: true, strikeBearing: true, head };
|
|
502
|
+
}
|
|
503
|
+
intentId = (doc && doc.id) || "";
|
|
504
|
+
for (const ev of parseIntentEvents(blob)) {
|
|
505
|
+
const cur = state.get(ev.aggregate) ?? { type: undefined, fields: new Map(), exists: false };
|
|
506
|
+
const newType = ev.sets.get("__type");
|
|
507
|
+
if (newType !== undefined) cur.type = newType;
|
|
508
|
+
const tallyBefore = new Map();
|
|
509
|
+
for (const read of reads) {
|
|
510
|
+
if (read.of !== cur.type && read.of !== newType) continue;
|
|
511
|
+
tallyBefore.set(read.id, contribution(read, cur));
|
|
512
|
+
}
|
|
513
|
+
const existedBefore = cur.exists;
|
|
514
|
+
for (const [f, v] of ev.sets) cur.fields.set(f, v);
|
|
515
|
+
cur.exists = true;
|
|
516
|
+
state.set(ev.aggregate, cur);
|
|
517
|
+
const effType = cur.type ?? newType;
|
|
518
|
+
if (!existedBefore && effType !== undefined && tenantTypes(eng).has(effType)) {
|
|
519
|
+
bump(summaryKey(NOMOS_SHARD_ROWS_READ, ""), 1);
|
|
520
|
+
deltas.push({ readId: NOMOS_SHARD_ROWS_READ, group: "", delta: 1 });
|
|
521
|
+
}
|
|
522
|
+
for (const read of reads) {
|
|
523
|
+
if (read.of !== cur.type) continue;
|
|
524
|
+
const before = tallyBefore.get(read.id) ?? null;
|
|
525
|
+
const after = contribution(read, cur);
|
|
526
|
+
for (const d of contributionDiff(read, before, after)) {
|
|
527
|
+
bump(summaryKey(d.readId, d.group), d.delta);
|
|
528
|
+
deltas.push(d);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (inSuffix && deltas.length) {
|
|
534
|
+
// Coalesce same-(read,group) deltas within one intent.
|
|
535
|
+
const byKey = new Map();
|
|
536
|
+
for (const d of deltas) byKey.set(summaryKey(d.readId, d.group), (byKey.get(summaryKey(d.readId, d.group)) ?? 0) + d.delta);
|
|
537
|
+
const merged = [...byKey.entries()].filter(([, d]) => d !== 0).map(([k, d]) => {
|
|
538
|
+
const [readId, group] = splitSummaryKey(k);
|
|
539
|
+
return { readId, group, delta: d };
|
|
540
|
+
});
|
|
541
|
+
if (merged.length) events.push({ oid: c.oid, intentId, deltas: merged });
|
|
542
|
+
}
|
|
543
|
+
if (!inSuffix && c.oid === fromOid) { inSuffix = true; sawFrom = true; prevs = new Map(running); }
|
|
544
|
+
}
|
|
545
|
+
if (!sawFrom) return { ok: false, error: `fromOid ${String(fromOid).slice(0, 12)} is not on this shard's main — cannot derive the suffix` };
|
|
546
|
+
const subtotals = [];
|
|
547
|
+
const keys = new Set([...running.keys(), ...prevs.keys()]);
|
|
548
|
+
for (const key of keys) {
|
|
549
|
+
const value = running.get(key) ?? 0;
|
|
550
|
+
const prev = prevs.get(key) ?? 0;
|
|
551
|
+
if (value === prev) continue;
|
|
552
|
+
const [readId, group] = splitSummaryKey(key);
|
|
553
|
+
const read = reads.find((r) => r.id === readId);
|
|
554
|
+
subtotals.push({ readId, group, kind: readId === NOMOS_SHARD_ROWS_READ ? "rows" : read ? read.kind : "count", prev, value });
|
|
555
|
+
}
|
|
556
|
+
if (subtotals.length === 0) return { ok: true, upToDate: true, head };
|
|
557
|
+
return {
|
|
558
|
+
ok: true,
|
|
559
|
+
head,
|
|
560
|
+
payload: {
|
|
561
|
+
fromOid: fromOid || "",
|
|
562
|
+
toOid: head,
|
|
563
|
+
subtotals: subtotals.sort((a, b) => (summaryKey(a.readId, a.group) < summaryKey(b.readId, b.group) ? -1 : 1)),
|
|
564
|
+
events,
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** One read's contribution shape over a host-folded aggregate (suffix derivation only).
|
|
570
|
+
* SLICE 4: predicate-aware — a `where`-bearing scoped read contributes only while the
|
|
571
|
+
* folded fields satisfy the canonical predicate (same semantics as the kernel's
|
|
572
|
+
* maintained tally, so the suffix lane and the live capture agree byte-for-byte). */
|
|
573
|
+
function contribution(read, cur) {
|
|
574
|
+
if (!cur || !cur.exists || cur.type !== read.of) return null;
|
|
575
|
+
if (read.where && !evalCanonicalPred(read.where, (f) => cur.fields.get(f))) return null;
|
|
576
|
+
const group = read.by === null ? "" : cur.fields.has(read.by) ? String(cur.fields.get(read.by)) : undefined;
|
|
577
|
+
if (group === undefined) return null;
|
|
578
|
+
if (read.kind === "count") return { group, value: 1 };
|
|
579
|
+
const raw = cur.fields.get(read.field);
|
|
580
|
+
return { group, value: typeof raw === "number" ? raw : 0 };
|
|
581
|
+
}
|
|
582
|
+
function contributionDiff(read, before, after) {
|
|
583
|
+
const out = [];
|
|
584
|
+
if (before && after && before.group === after.group) {
|
|
585
|
+
const d = after.value - before.value;
|
|
586
|
+
if (d !== 0) out.push({ readId: read.id, group: after.group, delta: d });
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
if (before) out.push({ readId: read.id, group: before.group, delta: -before.value });
|
|
590
|
+
if (after) out.push({ readId: read.id, group: after.group, delta: after.value });
|
|
591
|
+
return out.filter((d) => d.delta !== 0);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ── §6 — THE SHARD LIFECYCLE (slice 5): migration = re-admission, never trust ──
|
|
595
|
+
//
|
|
596
|
+
// The host legs of a split: the TARGET pulls the moving homes' intents off the
|
|
597
|
+
// SOURCE chain and re-admits EACH through its own gate (`apply_intent` — the same
|
|
598
|
+
// primitive the upload birth and edge admission run: plan re-run, byte-compared,
|
|
599
|
+
// invariants judged); the SOURCE then seals the move with ONE law intent that
|
|
600
|
+
// STRIKES the migrated history out of its fold (append-then-strike — nothing is
|
|
601
|
+
// erased; the kernel's strike-correct reprojection sheds the rows so tallies,
|
|
602
|
+
// deep-verify recount and the capacity load all stay honest) and tombstones the
|
|
603
|
+
// receipts as redirects. Subtotals close/open through the SAME §5.2 summary gate.
|
|
604
|
+
|
|
605
|
+
/** The total projected TENANT row count — the explicit O(rows) audit walk
|
|
606
|
+
* (deep-verify's recount and the strike-bearing summary close share it). */
|
|
607
|
+
export function countTenantRows(eng, ws) {
|
|
608
|
+
let total = 0;
|
|
609
|
+
for (const type of tenantTypes(eng)) {
|
|
610
|
+
total += query(eng, ws, "aggregatesByType", JSON.stringify({ __type: type })).length;
|
|
611
|
+
}
|
|
612
|
+
return total;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Walk a mounted workspace's main (genesis → head) and collect every NON-STRUCK
|
|
617
|
+
* intent homed on one of `homeKeys` — judged by the SAME hash-bearing `routes`
|
|
618
|
+
* table the wrong-home gate reads (`via:"axis"`: the payload field carries the
|
|
619
|
+
* home key; `via:"id"`: the minted id's route tag). Framework intents (receipts,
|
|
620
|
+
* identity, seals, installs) carry no route and never match.
|
|
621
|
+
*/
|
|
622
|
+
export async function collectHomedIntents(eng, ws, homeKeys) {
|
|
623
|
+
const routes = directiveRoutes(eng);
|
|
624
|
+
const tagOf = new Map(); // route tag hex -> homeKey
|
|
625
|
+
for (const hk of homeKeys) tagOf.set(await routeTagHexOfHomeKey(hk), hk);
|
|
626
|
+
const keySet = new Set(homeKeys);
|
|
627
|
+
const gitdir = gitdirOf(ws);
|
|
628
|
+
const commits = await git.log({ fs: eng.fs, gitdir, ref: BRANCH });
|
|
629
|
+
commits.reverse(); // genesis → head
|
|
630
|
+
const docs = [];
|
|
631
|
+
const strikeParity = new Map(); // intentId -> strike count
|
|
632
|
+
for (const c of commits) {
|
|
633
|
+
let doc = null;
|
|
634
|
+
try {
|
|
635
|
+
const { blob } = await git.readBlob({ fs: eng.fs, gitdir, oid: c.oid, filepath: "intent.json" });
|
|
636
|
+
doc = JSON.parse(dec.decode(blob));
|
|
637
|
+
docs.push({ oid: c.oid, blob, doc });
|
|
638
|
+
} catch { continue; }
|
|
639
|
+
for (const target of (doc && Array.isArray(doc.strikes) ? doc.strikes : [])) {
|
|
640
|
+
strikeParity.set(target, (strikeParity.get(target) ?? 0) + 1);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const out = [];
|
|
644
|
+
for (const { oid, blob, doc } of docs) {
|
|
645
|
+
const id = doc && typeof doc.id === "string" ? doc.id : null;
|
|
646
|
+
if (!id || (strikeParity.get(id) ?? 0) % 2 === 1) continue; // struck out of the fold
|
|
647
|
+
const dom = doc.payload && typeof doc.payload.domain === "string" ? doc.payload.domain : null;
|
|
648
|
+
const dirId = doc.payload && typeof doc.payload.directiveId === "string" ? doc.payload.directiveId : null;
|
|
649
|
+
const route = dom && dirId ? routes.get(`${dom} ${dirId}`) : null;
|
|
650
|
+
if (!route) continue;
|
|
651
|
+
const value = doc.payload.payload ? doc.payload.payload[route.key] : undefined;
|
|
652
|
+
if (typeof value !== "string" || !value) continue;
|
|
653
|
+
let homeKey = null;
|
|
654
|
+
if (route.via === "axis") homeKey = keySet.has(value) ? value : null;
|
|
655
|
+
else {
|
|
656
|
+
const tag = routeTagHexOfMintedId(value);
|
|
657
|
+
homeKey = tag !== undefined ? tagOf.get(tag) ?? null : null;
|
|
658
|
+
}
|
|
659
|
+
if (homeKey !== null) out.push({ oid, id, homeKey, blob });
|
|
660
|
+
}
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* MIGRATE the moving homes' intents from `sourceWs`'s chain into THIS workspace
|
|
666
|
+
* (`ws`, the target) — re-admission through the target's OWN gate, oldest first,
|
|
667
|
+
* idempotent by intent id (already-sealed intents are skipped, never double-folded).
|
|
668
|
+
* A LAW refusal jams the move VISIBLY (`refused` returned; the seal never runs over
|
|
669
|
+
* an incomplete migration — fail-closed). Both workspaces must be mounted.
|
|
670
|
+
*/
|
|
671
|
+
export async function migrateHomes(eng, ws, ledger, sourceWs, homeKeys, { onlyIds = null } = {}) {
|
|
672
|
+
const wanted = onlyIds ? new Set(onlyIds) : null;
|
|
673
|
+
const homed = await collectHomedIntents(eng, sourceWs, homeKeys);
|
|
674
|
+
const main = await mainIntentIds(eng, ws);
|
|
675
|
+
const migrated = [], skipped = [], refused = [];
|
|
676
|
+
for (const e of homed) {
|
|
677
|
+
if (wanted && !wanted.has(e.id)) continue;
|
|
678
|
+
if (main.ids.has(e.id)) { skipped.push(e.id); continue; }
|
|
679
|
+
const res = applyIntentBytes(eng, ws, e.blob);
|
|
680
|
+
if (res.ok) {
|
|
681
|
+
main.ids.add(e.id); main.oids.add(res.head); main.head = res.head;
|
|
682
|
+
migrated.push(e.id);
|
|
683
|
+
} else {
|
|
684
|
+
refused.push({ intentId: e.id, oid: e.oid.slice(0, 10), error: String(res.error || "").slice(0, 300) });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (migrated.length) await commitAndPush(eng, ws, ledger);
|
|
688
|
+
log("migrate-homes", { ws, from: sourceWs, migrated: migrated.length, skipped: skipped.length, refused: refused.length });
|
|
689
|
+
return { migrated, skipped, refused };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* SEAL the move on the SOURCE shard's own chain: verify the migration's
|
|
694
|
+
* COMPLETENESS against this chain itself (every live homed intent must be in
|
|
695
|
+
* `migratedIds` — anything missing is returned and the seal DOES NOT run), then
|
|
696
|
+
* author `nomosSealHome` (chunked: receipts tombstoned, migrated intents struck)
|
|
697
|
+
* through the one gate and push durably.
|
|
698
|
+
*/
|
|
699
|
+
export async function sealHomes(eng, ws, ledger, { domain, lawHash, homeKeys, target, migratedIds, sealedAt }) {
|
|
700
|
+
const have = new Set(migratedIds);
|
|
701
|
+
const homed = await collectHomedIntents(eng, ws, homeKeys);
|
|
702
|
+
const missing = homed.filter((e) => !have.has(e.id)).map((e) => e.id);
|
|
703
|
+
if (missing.length) return { ok: false, missing };
|
|
704
|
+
const ids = homed.map((e) => e.id);
|
|
705
|
+
const CHUNK = 1024;
|
|
706
|
+
let head = null, struck = 0;
|
|
707
|
+
for (let i = 0; i < Math.max(1, Math.ceil(ids.length / CHUNK)); i++) {
|
|
708
|
+
const intentIds = ids.slice(i * CHUNK, (i + 1) * CHUNK);
|
|
709
|
+
const res = author(eng, ws, domain, "nomosSealHome", { homeKeys: [...homeKeys], target, intentIds, sealedAt }, lawHash, { rngDraws: 8 });
|
|
710
|
+
if (res && res.ok === false) return { ok: false, error: String(res.error || "the law refused the seal").slice(0, 400) };
|
|
711
|
+
head = res.head;
|
|
712
|
+
struck += intentIds.length;
|
|
713
|
+
}
|
|
714
|
+
await commitAndPush(eng, ws, ledger);
|
|
715
|
+
log("seal-homes", { ws, target, homes: homeKeys.length, struck, head });
|
|
716
|
+
return { ok: true, head, struck };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* CLOSE a shard's subtotals against the coordinator-HELD values with THIS shard's
|
|
721
|
+
* projection as the oracle — the strike-bearing summary lane (a §6 seal just
|
|
722
|
+
* re-shaped the fold; the kernel's strike-correct reprojection is the only honest
|
|
723
|
+
* recomputation). `heldRows` = the coordinator's committed `NomosSummarySubtotal`
|
|
724
|
+
* rows for this shard (+ its recorded frontier). Returns the `nomosPropagateSummary`
|
|
725
|
+
* body — prev = held, value = the live maintained tally, evidence = one event at the
|
|
726
|
+
* chain head carrying the diffs (deep-verify recomputes from the same oracle).
|
|
727
|
+
*/
|
|
728
|
+
export async function summaryCloseAgainstHeld(eng, ws, heldRows, heldFrontier) {
|
|
729
|
+
const head = await git.resolveRef({ fs: eng.fs, gitdir: gitdirOf(ws), ref: BRANCH }).catch(() => null);
|
|
730
|
+
if (!head) return { ok: false, error: "no local main to close against" };
|
|
731
|
+
if (head === heldFrontier) return { ok: true, upToDate: true, head };
|
|
732
|
+
const subtotals = [];
|
|
733
|
+
const deltas = [];
|
|
734
|
+
for (const row of heldRows) {
|
|
735
|
+
const d = row.data || row;
|
|
736
|
+
if (typeof d.readId !== "string") continue;
|
|
737
|
+
const group = typeof d.group === "string" ? d.group : "";
|
|
738
|
+
const held = typeof d.value === "number" ? d.value : 0;
|
|
739
|
+
let now;
|
|
740
|
+
if (d.readId === NOMOS_SHARD_ROWS_READ) now = countTenantRows(eng, ws);
|
|
741
|
+
else if (d.kind === "sum") now = sum(eng, ws, d.readId, group).sum;
|
|
742
|
+
else now = count(eng, ws, d.readId, group).count;
|
|
743
|
+
if (now === held) continue;
|
|
744
|
+
subtotals.push({ readId: d.readId, group, kind: d.kind === "sum" || d.kind === "rows" ? d.kind : "count", prev: held, value: now });
|
|
745
|
+
deltas.push({ readId: d.readId, group, delta: now - held });
|
|
746
|
+
}
|
|
747
|
+
// Head moved but no tally did (e.g. a seal over an empty home): the frontier must
|
|
748
|
+
// still advance or the watermark jams forever — the caller amends it with the
|
|
749
|
+
// declared `nomosAmendSummaryFrontier` primitive (no claims, nothing to recompute).
|
|
750
|
+
if (subtotals.length === 0) return { ok: true, frontierOnly: true, head };
|
|
751
|
+
return {
|
|
752
|
+
ok: true,
|
|
753
|
+
head,
|
|
754
|
+
payload: {
|
|
755
|
+
fromOid: heldFrontier || "",
|
|
756
|
+
toOid: head,
|
|
757
|
+
subtotals: subtotals.sort((a, b) => (summaryKey(a.readId, a.group) < summaryKey(b.readId, b.group) ? -1 : 1)),
|
|
758
|
+
events: [{ oid: head, intentId: "", deltas }],
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ── §5.4 — THE REVERSE LANE (slice 4): global reference data, coordinator → shards ──
|
|
764
|
+
//
|
|
765
|
+
// Aggregates marked `.global(...)` home on the COORDINATOR (the homing walk pins
|
|
766
|
+
// them); their committed intents REPLICATE DOWNWARD into every shard chain by
|
|
767
|
+
// RE-ADMISSION of the SAME sealed bytes (`apply_intent` — plan re-run under the
|
|
768
|
+
// shard's identical law, byte-compared; provenance preserved: author = original
|
|
769
|
+
// author, committer = the shard's kernel). Idempotent by intent id; a shard born
|
|
770
|
+
// later catches up via {@link collectGlobalIntents} at the auto-birth leg.
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Walk a mounted workspace's main (genesis → head) and collect every NON-STRUCK
|
|
774
|
+
* intent whose sealed events touch a GLOBAL aggregate type — the catch-up set a
|
|
775
|
+
* fresh shard replays at birth, and the audit surface for the reverse lane.
|
|
776
|
+
*/
|
|
777
|
+
export async function collectGlobalIntents(eng, ws) {
|
|
778
|
+
const globals = globalAggregates(eng);
|
|
779
|
+
if (globals.size === 0) return [];
|
|
780
|
+
const gitdir = gitdirOf(ws);
|
|
781
|
+
const commits = await git.log({ fs: eng.fs, gitdir, ref: BRANCH });
|
|
782
|
+
commits.reverse(); // genesis → head
|
|
783
|
+
const typeOf = new Map(); // aggregateId -> __type (set once at creation — types never change)
|
|
784
|
+
const strikeParity = new Map();
|
|
785
|
+
const docs = [];
|
|
786
|
+
for (const c of commits) {
|
|
787
|
+
let blob = null, doc = null;
|
|
788
|
+
try {
|
|
789
|
+
blob = (await git.readBlob({ fs: eng.fs, gitdir, oid: c.oid, filepath: "intent.json" })).blob;
|
|
790
|
+
doc = JSON.parse(dec.decode(blob));
|
|
791
|
+
} catch { continue; }
|
|
792
|
+
docs.push({ oid: c.oid, blob, doc });
|
|
793
|
+
for (const target of (doc && Array.isArray(doc.strikes) ? doc.strikes : [])) {
|
|
794
|
+
strikeParity.set(target, (strikeParity.get(target) ?? 0) + 1);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const out = [];
|
|
798
|
+
for (const { oid, blob, doc } of docs) {
|
|
799
|
+
const id = doc && typeof doc.id === "string" ? doc.id : null;
|
|
800
|
+
let touches = false;
|
|
801
|
+
for (const ev of parseIntentEvents(blob)) {
|
|
802
|
+
const newType = ev.sets.get("__type");
|
|
803
|
+
if (newType !== undefined && !typeOf.has(ev.aggregate)) typeOf.set(ev.aggregate, newType);
|
|
804
|
+
const t = newType ?? typeOf.get(ev.aggregate);
|
|
805
|
+
if (t !== undefined && globals.has(t)) touches = true;
|
|
806
|
+
}
|
|
807
|
+
if (!touches || !id || (strikeParity.get(id) ?? 0) % 2 === 1) continue;
|
|
808
|
+
out.push({ oid, id, blob });
|
|
809
|
+
}
|
|
810
|
+
return out;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Re-admit carried GLOBAL intents on THIS workspace's chain (a shard, the reverse
|
|
815
|
+
* lane's receiving leg) — oldest first, idempotent by intent id, durably pushed.
|
|
816
|
+
* A law refusal jams VISIBLY (`refused` returned) — the shard's gate is never
|
|
817
|
+
* bypassed; the coordinator's law ≡ the shard's law by construction, so a refusal
|
|
818
|
+
* here means the laws diverged (redeploy the law, then re-offer).
|
|
819
|
+
*/
|
|
820
|
+
export async function replicateIntents(eng, ws, ledger, intents, { push = true } = {}) {
|
|
821
|
+
const main = await mainIntentIds(eng, ws);
|
|
822
|
+
const replicated = [], skipped = [], refused = [];
|
|
823
|
+
for (const e of intents) {
|
|
824
|
+
if (e.id && main.ids.has(e.id)) { skipped.push(e.id); continue; }
|
|
825
|
+
const res = applyIntentBytes(eng, ws, e.blob);
|
|
826
|
+
if (res.ok) {
|
|
827
|
+
if (e.id) main.ids.add(e.id);
|
|
828
|
+
main.oids.add(res.head); main.head = res.head;
|
|
829
|
+
replicated.push(e.id || res.head);
|
|
830
|
+
} else {
|
|
831
|
+
refused.push({ intentId: e.id || "", error: String(res.error || "").slice(0, 300) });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// push=false is the OFFLINE proof lane only (bench harness — no ledger remote);
|
|
835
|
+
// every host leg keeps durability-before-ack.
|
|
836
|
+
if (replicated.length && push) await commitAndPush(eng, ws, ledger);
|
|
837
|
+
log("replicate-globals", { ws, replicated: replicated.length, skipped: skipped.length, refused: refused.length });
|
|
838
|
+
return { replicated, skipped, refused };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* §5.5 (slice 4) — the CONTENT HASH of the coordinator's committed subtotal state:
|
|
843
|
+
* sha256 over the canonical (row-id-sorted) projection of every
|
|
844
|
+
* `NomosSummarySubtotal` row. The ONE definition every checkpoint party shares —
|
|
845
|
+
* the sealing host claims it, an auditor (deep-verify anchoring, the e2e, any
|
|
846
|
+
* peer that mounts the chain) recomputes it from law-state and byte-compares.
|
|
847
|
+
*/
|
|
848
|
+
export async function checkpointStateHash(eng, ws) {
|
|
849
|
+
const rows = query(eng, ws, "aggregatesByType", JSON.stringify({ __type: "NomosSummarySubtotal" }));
|
|
850
|
+
const canon = rows
|
|
851
|
+
.map((r) => {
|
|
852
|
+
const d = r.data || {};
|
|
853
|
+
return { id: r.id, readId: d.readId, group: d.group, shard: d.shard, kind: d.kind, value: d.value, frontier: d.frontier };
|
|
854
|
+
})
|
|
855
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
856
|
+
return sha256hex(JSON.stringify(canon));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Author one intent on a workspace's chain through ITS OWN gate and push durably —
|
|
861
|
+
* the generic Order leg (`nomosPropagateSummary` into the coordinator, the
|
|
862
|
+
* assignment receipt into a shard, the registry row at auto-birth). A LAW refusal
|
|
863
|
+
* (the gate's typed verdict — frontier-gap, wrong-home, …) returns `{ok:false,
|
|
864
|
+
* error}`; the caller decides the remedy (re-derive, log, jam visibly).
|
|
865
|
+
*/
|
|
866
|
+
export async function authorOn(eng, ws, ledger, domain, directiveId, payload, lawHash, opts = {}) {
|
|
867
|
+
await mountWorkspace(eng, ws, ledger);
|
|
868
|
+
let res;
|
|
869
|
+
try {
|
|
870
|
+
res = author(eng, ws, domain, directiveId, payload, lawHash, opts);
|
|
871
|
+
} catch (e) {
|
|
872
|
+
return { ok: false, error: String((e && e.message) || e).slice(0, 500) };
|
|
873
|
+
}
|
|
874
|
+
if (res && res.ok === false) return { ok: false, error: String(res.error || "the law refused this intent").slice(0, 500) };
|
|
875
|
+
await commitAndPush(eng, ws, ledger);
|
|
876
|
+
return { ok: true, head: res.head };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* THE DEEP-VERIFY OP (§5.5 — replay-on-suspicion, made operational): replay-verify
|
|
881
|
+
* the SHARD's whole chain from genesis (the same wasm `verify_chain` the upload
|
|
882
|
+
* birth runs), then recompute every coordinator-held subtotal for that shard from
|
|
883
|
+
* the shard's OWN maintained tallies and byte-compare. `coordinatorRows` is the
|
|
884
|
+
* coordinator's law-state (`NomosSummarySubtotal` rows for this shard) — the HOST
|
|
885
|
+
* resolves and passes them (the engine has no cross-workspace reach). Auditors pay
|
|
886
|
+
* for verification; ordinary reads stay O(1).
|
|
887
|
+
*/
|
|
888
|
+
export async function deepVerifyShard(eng, shardWs, shardLedger, coordinatorRows) {
|
|
889
|
+
const replay = await verifyChain(eng, shardWs, shardLedger);
|
|
890
|
+
if (!replay.valid) {
|
|
891
|
+
return { verdict: `invalid:${replay.check || "replay"}`, replay, checks: [] };
|
|
892
|
+
}
|
|
893
|
+
await mountWorkspace(eng, shardWs, shardLedger);
|
|
894
|
+
const checks = [];
|
|
895
|
+
let allMatch = true;
|
|
896
|
+
for (const row of coordinatorRows) {
|
|
897
|
+
const d = row.data || row;
|
|
898
|
+
if (typeof d.readId !== "string") continue;
|
|
899
|
+
let recomputed;
|
|
900
|
+
if (d.readId === NOMOS_SHARD_ROWS_READ) {
|
|
901
|
+
// Total projected rows: enumerate the manifest's aggregate types (an explicit
|
|
902
|
+
// O(rows) audit walk — never an implicit read cost).
|
|
903
|
+
recomputed = countTenantRows(eng, shardWs);
|
|
904
|
+
} else if (d.kind === "sum") {
|
|
905
|
+
recomputed = sum(eng, shardWs, d.readId, d.group ?? "").sum;
|
|
906
|
+
} else {
|
|
907
|
+
recomputed = count(eng, shardWs, d.readId, d.group ?? "").count;
|
|
908
|
+
}
|
|
909
|
+
const match = recomputed === d.value;
|
|
910
|
+
if (!match) allMatch = false;
|
|
911
|
+
checks.push({ readId: d.readId, group: d.group ?? "", coordinator: d.value, shard: recomputed, match });
|
|
912
|
+
}
|
|
913
|
+
const head = await git.resolveRef({ fs: eng.fs, gitdir: gitdirOf(shardWs), ref: BRANCH }).catch(() => null);
|
|
914
|
+
return {
|
|
915
|
+
verdict: allMatch ? "verified" : "mismatch",
|
|
916
|
+
head,
|
|
917
|
+
intents: replay.intents,
|
|
918
|
+
plansRerun: replay.plansRerun,
|
|
919
|
+
checks,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Mount a workspace, restoring its repo from the Artifacts ledger ({remote, headers}). */
|
|
924
|
+
export async function mountWorkspace(eng, ws, ledger) {
|
|
925
|
+
if (eng.mounted.has(ws)) return eng.mounted.get(ws);
|
|
926
|
+
const wsContents = new Map();
|
|
927
|
+
seedManifests(wsContents, eng.manifestStrings);
|
|
928
|
+
eng.preopen.dir.contents.get("ws").contents.set(ws, new Directory(wsContents));
|
|
929
|
+
const gitdir = gitdirOf(ws);
|
|
930
|
+
let restored = false, remoteMain = null, restoreError = null;
|
|
931
|
+
try {
|
|
932
|
+
const info = await git.getRemoteInfo({ http, url: ledger.remote, headers: ledger.headers });
|
|
933
|
+
remoteMain = (info.refs && info.refs.heads && info.refs.heads.main) || null;
|
|
934
|
+
if (remoteMain) {
|
|
935
|
+
await git.init({ fs: eng.fs, gitdir, bare: true, defaultBranch: "main" });
|
|
936
|
+
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
937
|
+
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: "main", singleBranch: true, headers: ledger.headers });
|
|
938
|
+
await git.writeRef({ fs: eng.fs, gitdir, ref: "refs/heads/main", value: remoteMain, force: true });
|
|
939
|
+
await git.writeRef({ fs: eng.fs, gitdir, ref: "HEAD", value: "ref: refs/heads/main", force: true, symbolic: true });
|
|
940
|
+
restored = true;
|
|
941
|
+
}
|
|
942
|
+
} catch (e) { restoreError = String((e && e.stack) || e).split("\n").slice(0, 5).join(" | "); }
|
|
943
|
+
const m = { restored, remoteMain, restoreError };
|
|
944
|
+
eng.mounted.set(ws, m);
|
|
945
|
+
return m;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Drop a workspace's in-memory state so the next access re-restores from the ledger. */
|
|
949
|
+
export function unmount(eng, ws) {
|
|
950
|
+
eng.mounted.delete(ws);
|
|
951
|
+
eng.mainIntents.delete(ws);
|
|
952
|
+
const wsRoot = eng.preopen.dir.contents.get("ws");
|
|
953
|
+
if (wsRoot) wsRoot.contents.delete(ws);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function writeWork(eng, name, bytes) { eng.preopen.dir.contents.set(name, new File(bytes)); }
|
|
957
|
+
|
|
958
|
+
export function author(eng, ws, domain, directiveId, payload, controllerHash, opts = {}) {
|
|
959
|
+
const seq = eng.seq++;
|
|
960
|
+
// rngDraws: the captured-entropy budget this intent's plan may consume (each minted id
|
|
961
|
+
// costs 19 draws — see NOMOS_API_SHIM). 64 is the per-write default (3 mints of slack);
|
|
962
|
+
// a BATCH-creating directive needs 19×creates. COMMIT-ONLY-CONSUMED: only the consumed
|
|
963
|
+
// prefix is sealed onto the intent, so a generous buffer is request weight, not ledger weight.
|
|
964
|
+
const envelope = { payload: { domain, directiveId, payload }, captured_ports: { clock: { physical: Date.now(), logical: 0, replica: eng.replica }, rng: osEntropyBuffer(opts.rngDraws ?? 64) }, policy_version: 1, policy_domain: "Nomos", policy_gas: 0, policy_memory: 0 };
|
|
965
|
+
writeWork(eng, `payload-${seq}.json`, enc.encode(JSON.stringify(payload)));
|
|
966
|
+
writeWork(eng, `envelope-${seq}.json`, enc.encode(stringifyBig(envelope)));
|
|
967
|
+
const genesis = !controllerHash;
|
|
968
|
+
// THE DEFERRED-PROJECTION ACK (#47, sharding slice 7): the read-projection
|
|
969
|
+
// catch-up (incl. the derive/combine materialize) moves OFF the author ack path
|
|
970
|
+
// by default — every read op self-heals to head, and the post-ack warm lane
|
|
971
|
+
// (`warmEngine`, which every host already calls) flushes it eagerly. Pass
|
|
972
|
+
// `opts.deferProjection: false` to keep the pre-#47 synchronous refresh.
|
|
973
|
+
const defer = opts.deferProjection !== false;
|
|
974
|
+
const res = JSON.parse(call(eng.ex, "author", { repoArg: repoArgOf(ws), workspace: ws, domain, directiveId, payloadFile: `/work/payload-${seq}.json`, envelopeFile: `/work/envelope-${seq}.json`, seq, actor: "", domainFile: genesis ? "/work/domain.package.usda" : "", domainHash: genesis ? "" : controllerHash, branch: BRANCH, ...(defer ? { deferProjection: true } : {}) }, eng.STDERR));
|
|
975
|
+
if (defer && res.ok) (eng.pendingProjection ??= new Set()).add(ws);
|
|
976
|
+
return res;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** Catch one workspace's read projection up to head NOW (the deferred-ack flush). */
|
|
980
|
+
export function refreshProjection(eng, ws) {
|
|
981
|
+
return JSON.parse(call(eng.ex, "refresh_projection", { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH }, eng.STDERR));
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** Flush every deferred projection refresh (rides the post-ack warm lane). */
|
|
985
|
+
export function flushDeferredProjections(eng) {
|
|
986
|
+
for (const ws of eng.pendingProjection ?? []) {
|
|
987
|
+
try { refreshProjection(eng, ws); } catch {}
|
|
988
|
+
}
|
|
989
|
+
if (eng.pendingProjection) eng.pendingProjection.clear();
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// DURABILITY-BEFORE-ACK: only acked once the write LANDED in the ledger; a failed push
|
|
993
|
+
// unmounts (drops local state) so the next access re-restores from custody.
|
|
994
|
+
export async function commitAndPush(eng, ws, ledger) {
|
|
995
|
+
try {
|
|
996
|
+
await git.push({ fs: eng.fs, http, gitdir: gitdirOf(ws), url: ledger.remote, ref: "main", remoteRef: "main", force: false, headers: ledger.headers });
|
|
997
|
+
} catch (e) {
|
|
998
|
+
unmount(eng, ws);
|
|
999
|
+
throw new Error(`ledger push failed — write NOT durable, rolled back to ledger: ${e}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export const qById = (eng, ws, id) => JSON.parse(call(eng.ex, "query_by_id", { repoArg: repoArgOf(ws), workspace: ws, aggregateId: id, branch: BRANCH }, eng.STDERR));
|
|
1004
|
+
export const query = (eng, ws, queryId, paramsJson) => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryId, paramsJson, branch: BRANCH }, eng.STDERR));
|
|
1005
|
+
export const count = (eng, ws, countId, groupKey) => JSON.parse(call(eng.ex, "count", { repoArg: repoArgOf(ws), workspace: ws, countId, groupKey, branch: BRANCH }, eng.STDERR));
|
|
1006
|
+
export const sum = (eng, ws, sumId, groupKey) => JSON.parse(call(eng.ex, "sum", { repoArg: repoArgOf(ws), workspace: ws, sumId, groupKey, branch: BRANCH }, eng.STDERR));
|
|
1007
|
+
/** The maintained O(1) min/max read (#47 — the slice-4 extremum hand-back made an RPC).
|
|
1008
|
+
* `kind` = "min"|"max"; an absent group answers `value: null` (never a fabricated 0). */
|
|
1009
|
+
export const extremum = (eng, ws, extremumId, kind, groupKey) => JSON.parse(call(eng.ex, "extremum", { repoArg: repoArgOf(ws), workspace: ws, extremumId, kind, groupKey: groupKey ?? "", branch: BRANCH }, eng.STDERR));
|
|
1010
|
+
export const nomosActive = (eng, ws) => qById(eng, ws, `domain-installation:${eng.hashes.nomos}`).length > 0;
|
|
1011
|
+
|
|
1012
|
+
/** Re-admit ONE carried intent (bytes) on local main — the DLQ-retry primitive. */
|
|
1013
|
+
export function applyIntentBytes(eng, ws, bytes) {
|
|
1014
|
+
const name = `intent-in-${eng.seq++}.json`;
|
|
1015
|
+
writeWork(eng, name, bytes);
|
|
1016
|
+
return JSON.parse(call(eng.ex, "apply_intent", { repoArg: repoArgOf(ws), workspace: ws, intentFile: `/work/${name}`, branch: BRANCH }, eng.STDERR));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Main's recent intent ids + oids, cached by head (admission idempotence + walk boundary).
|
|
1020
|
+
async function mainIntentIds(eng, ws) {
|
|
1021
|
+
const gitdir = gitdirOf(ws);
|
|
1022
|
+
const head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
1023
|
+
if (!head) return { head: null, ids: new Set(), oids: new Set() };
|
|
1024
|
+
const cached = eng.mainIntents.get(ws);
|
|
1025
|
+
if (cached && cached.head === head) return cached;
|
|
1026
|
+
const ids = new Set(), oids = new Set();
|
|
1027
|
+
for (const c of await git.log({ fs: eng.fs, gitdir, ref: BRANCH, depth: 500 })) {
|
|
1028
|
+
oids.add(c.oid);
|
|
1029
|
+
try {
|
|
1030
|
+
const { blob } = await git.readBlob({ fs: eng.fs, gitdir, oid: c.oid, filepath: "intent.json" });
|
|
1031
|
+
const id = JSON.parse(dec.decode(blob)).id;
|
|
1032
|
+
if (typeof id === "string" && id) ids.add(id);
|
|
1033
|
+
} catch {}
|
|
1034
|
+
}
|
|
1035
|
+
const m = { head, ids, oids };
|
|
1036
|
+
eng.mainIntents.set(ws, m);
|
|
1037
|
+
return m;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* EDGE ADMISSION (the one machinery): judge every session/<cid> ref. Structural lane
|
|
1042
|
+
* refusals (control-plane intents, undeployed domains) and domain rejections route per
|
|
1043
|
+
* Jack's law: legitimate work DEAD-LETTERS (returned to the host to persist — the
|
|
1044
|
+
* engine has no durable store), obvious attacks drop. Admitted intents merge to main,
|
|
1045
|
+
* durably push, the branch is consumed.
|
|
1046
|
+
*/
|
|
1047
|
+
export async function admitAll(eng, ws, ledger, { refuseDomains = [], shardRouting = null } = {}) {
|
|
1048
|
+
const refuse = new Set(refuseDomains);
|
|
1049
|
+
const routes = shardRouting ? directiveRoutes(eng) : null;
|
|
1050
|
+
const gitdir = gitdirOf(ws);
|
|
1051
|
+
const info = await git.getRemoteInfo({ http, url: ledger.remote, headers: ledger.headers });
|
|
1052
|
+
const sessions = (info.refs && info.refs.heads && info.refs.heads.session) || {};
|
|
1053
|
+
const results = [], deadLetters = [], placements = [], splits = [], globalsOut = [];
|
|
1054
|
+
// §5.4 (slice 4): on the COORDINATOR of a taxonomy that declares `.global(...)`
|
|
1055
|
+
// reference data, every admitted intent touching a global aggregate is surfaced
|
|
1056
|
+
// to the HOST, which replicates the SAME sealed bytes into each shard chain.
|
|
1057
|
+
const globalsDeclared = shardRouting && !shardRouting.selfLabel ? globalAggregates(eng) : null;
|
|
1058
|
+
// ── §5.2 SUMMARY CAPTURE (slice 3): on a SHARD (selfLabel) whose law declares
|
|
1059
|
+
// estate-scoped reads, derive each admitted intent's per-(read, group) deltas with
|
|
1060
|
+
// the shard's own projection as the oracle (tallies sandwiched around the fold —
|
|
1061
|
+
// O(touched)); the HOST authors the coalesced `nomosPropagateSummary` Order into
|
|
1062
|
+
// the coordinator afterwards. The coordinator's gate re-verifies everything.
|
|
1063
|
+
const summaryCtx = shardRouting && shardRouting.selfLabel && scopedReads(eng).length > 0
|
|
1064
|
+
? { reads: scopedReads(eng), prevs: new Map(), values: new Map(), events: [], rowsDelta: 0 }
|
|
1065
|
+
: null;
|
|
1066
|
+
const headBefore = summaryCtx ? await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null) : null;
|
|
1067
|
+
for (const [cid, head] of Object.entries(sessions)) {
|
|
1068
|
+
const ref = `refs/heads/session/${cid}`;
|
|
1069
|
+
try {
|
|
1070
|
+
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1071
|
+
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref, singleBranch: true, headers: ledger.headers });
|
|
1072
|
+
const main = await mainIntentIds(eng, ws);
|
|
1073
|
+
const chain = [];
|
|
1074
|
+
for (const c of await git.log({ fs: eng.fs, gitdir, ref: head, depth: 100 })) {
|
|
1075
|
+
if (main.oids.has(c.oid)) break;
|
|
1076
|
+
chain.push(c.oid);
|
|
1077
|
+
}
|
|
1078
|
+
chain.reverse(); // oldest → newest
|
|
1079
|
+
const admitted = [], rejected = [], skipped = [];
|
|
1080
|
+
for (const oid of chain) {
|
|
1081
|
+
try {
|
|
1082
|
+
const { blob } = await git.readBlob({ fs: eng.fs, gitdir, oid, filepath: "intent.json" });
|
|
1083
|
+
let dom = null, intentId = null, dirId = null;
|
|
1084
|
+
try { const doc = JSON.parse(dec.decode(blob)); intentId = typeof doc.id === "string" ? doc.id : null; dom = doc && doc.payload && typeof doc.payload.domain === "string" ? doc.payload.domain : null; dirId = doc && doc.payload && typeof doc.payload.directiveId === "string" ? doc.payload.directiveId : null; } catch {}
|
|
1085
|
+
// WRITE-AUTHORITY: law (control-plane) intents never ride the session lane —
|
|
1086
|
+
// nor do HOST-FENCED domains (e.g. `workspaces` on root: governance directives
|
|
1087
|
+
// enter ONLY through the admin-gated lane, never open session pushes).
|
|
1088
|
+
if (!dom || dom === "nomos" || dom === "bootstrap" || refuse.has(dom)) {
|
|
1089
|
+
rejected.push({ oid: oid.slice(0, 10), error: `session lane refuses ${dom ? `'${dom}'` : "non-authored"} intents — law deploys go through POST /v1/workspaces/${ws}/domains with the workspace secret` });
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
// LAW-FENCE part 2: the domain must be DEPLOYED to this workspace. An
|
|
1093
|
+
// undeployed domain's write is the canonical LEGITIMATE JAM — dead-letter.
|
|
1094
|
+
if (!knownDomains(eng).has(dom)) {
|
|
1095
|
+
const error = `domain '${dom}' is not deployed to this workspace — deploy its law (POST /v1/workspaces/${ws}/domains with the workspace secret), then POST /v1/workspaces/${ws}/dead-letters/retry`;
|
|
1096
|
+
rejected.push({ oid: oid.slice(0, 10), error, deadLettered: true });
|
|
1097
|
+
deadLetters.push({ intentId, oid: oid.slice(0, 10), session: cid, error, blob, domain: dom, directiveId: dirId });
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
// THE WRONG-HOME GATE (sharding slice 2): a routed directive's home must be
|
|
1101
|
+
// assigned to THIS workspace by the coordinator's law-state shard map.
|
|
1102
|
+
// Misrouted → the typed `wrong-home` refusal naming the correct workspace
|
|
1103
|
+
// (self-healing, never queued); unplaced → a dead-lettered placement jam.
|
|
1104
|
+
if (routes && dom && dirId) {
|
|
1105
|
+
// THE SEAL PEN IS THE BAILIFF'S (slice 5): `nomosSealHome` (strikes the
|
|
1106
|
+
// migrated history out of a source's fold) and `nomosSealHandoff` (flips
|
|
1107
|
+
// the map) are HOST-orchestrated legs of the §6 move — a session-lane
|
|
1108
|
+
// offer of either is an attack on the move lane, refused and DROPPED.
|
|
1109
|
+
if (dirId === "nomosSealHome" || dirId === "nomosSealHandoff") {
|
|
1110
|
+
rejected.push({ oid: oid.slice(0, 10), error: `session lane refuses '${dirId}' — the §6 handoff seals are authored by the host after migration verifies (begin a move with nomosSplitShard, or POST /v1/workspaces/<coordinator>/split)` });
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
const route = routes.get(`${dom} ${dirId}`);
|
|
1114
|
+
if (route) {
|
|
1115
|
+
let payloadObj = null;
|
|
1116
|
+
try { payloadObj = JSON.parse(dec.decode(blob)).payload?.payload ?? null; } catch {}
|
|
1117
|
+
const verdict = await wrongHomeVerdict(shardRouting, route, payloadObj);
|
|
1118
|
+
if (verdict) {
|
|
1119
|
+
rejected.push({ oid: oid.slice(0, 10), error: verdict.error, ...(verdict.wrongHome ? { wrongHome: verdict.wrongHome } : {}), ...(verdict.homeMoving ? { homeMoving: verdict.homeMoving } : {}), ...(verdict.deadLettered ? { deadLettered: true } : {}) });
|
|
1120
|
+
if (verdict.deadLettered) deadLetters.push({ intentId, oid: oid.slice(0, 10), session: cid, error: verdict.error, blob, domain: dom, directiveId: dirId });
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// THE RECEIPT LEG'S BAILIFF (slice 3 — §3): a session-lane assignment
|
|
1125
|
+
// receipt is VERIFIED against the coordinator's shard map before it can
|
|
1126
|
+
// fold (the client authors it optimistically; the map is the law).
|
|
1127
|
+
if (dirId === "nomosReceiveAssignment" && shardRouting) {
|
|
1128
|
+
let p = null;
|
|
1129
|
+
try { p = JSON.parse(dec.decode(blob)).payload?.payload ?? null; } catch {}
|
|
1130
|
+
const homeKey = p && typeof p.homeKey === "string" ? p.homeKey : null;
|
|
1131
|
+
const rows = homeKey ? (await shardRouting.assignments()) || [] : [];
|
|
1132
|
+
const a = homeKey ? rows.find((x) => x.homeKey === homeKey) : null;
|
|
1133
|
+
if (!a) {
|
|
1134
|
+
const error = `assignment receipt for home '${String(homeKey).slice(0, 80)}' has NO placement on the coordinator '${shardRouting.coordinatorWs}' — author the derived placement intent (birth<Type>) first, then POST /dead-letters/retry`;
|
|
1135
|
+
rejected.push({ oid: oid.slice(0, 10), error, deadLettered: true });
|
|
1136
|
+
deadLetters.push({ intentId, oid: oid.slice(0, 10), session: cid, error, blob, domain: dom, directiveId: dirId });
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (a.shard !== shardRouting.selfLabel || (p && p.shard !== shardRouting.selfLabel)) {
|
|
1140
|
+
const correct = `${shardRouting.coordinatorWs}--${a.shard}`;
|
|
1141
|
+
rejected.push({
|
|
1142
|
+
oid: oid.slice(0, 10),
|
|
1143
|
+
error: `wrong-home: the coordinator's shard map assigns home '${homeKey.slice(0, 80)}' to '${correct}' (not this workspace) — a routed client re-routes and retries automatically`,
|
|
1144
|
+
wrongHome: { workspace: correct, ...(a.mapVersion !== undefined ? { mapVersion: a.mapVersion } : {}) },
|
|
1145
|
+
});
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
// IDEMPOTENCE: an intent already sealed on main is acknowledged, not re-folded.
|
|
1151
|
+
if (intentId && main.ids.has(intentId)) {
|
|
1152
|
+
skipped.push({ oid: oid.slice(0, 10), intent: intentId.slice(0, 12), alreadyOnMain: true });
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const cap = summaryCtx ? beginSummaryCapture(eng, ws, summaryCtx, blob) : null;
|
|
1156
|
+
const res = applyIntentBytes(eng, ws, blob);
|
|
1157
|
+
if (res.ok) {
|
|
1158
|
+
main.ids.add(intentId); main.oids.add(res.head); main.head = res.head;
|
|
1159
|
+
admitted.push({ oid: oid.slice(0, 10), head: res.head });
|
|
1160
|
+
if (cap) finishSummaryCapture(eng, ws, summaryCtx, cap, res.head, intentId);
|
|
1161
|
+
// AN ADMITTED PLACEMENT (birth<Type> on the coordinator): surfaced to the
|
|
1162
|
+
// HOST so it can author the assignment RECEIPT into the shard's own chain
|
|
1163
|
+
// (sharding §3 — the receipt leg).
|
|
1164
|
+
if (shardRouting && dirId) {
|
|
1165
|
+
const placement = placementDirectives(eng).get(dirId);
|
|
1166
|
+
if (placement) {
|
|
1167
|
+
let p = null;
|
|
1168
|
+
try { p = JSON.parse(dec.decode(blob)).payload?.payload ?? null; } catch {}
|
|
1169
|
+
const homeKey = p ? p[placement.keyField] : undefined;
|
|
1170
|
+
if (typeof homeKey === "string" && typeof p?.shard === "string") {
|
|
1171
|
+
placements.push({ directive: dirId, homeKey, shard: p.shard, mapVersion: 1 });
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
// AN ADMITTED SPLIT (nomosSplitShard on the coordinator, slice 5):
|
|
1175
|
+
// surfaced to the HOST so it can run the §6 migration legs (re-admit
|
|
1176
|
+
// on the target, seal the source, close/open the subtotals, flip the
|
|
1177
|
+
// map) — a session-authored split COMPLETES like a host-authored one.
|
|
1178
|
+
if (dirId === "nomosSplitShard" && !shardRouting.selfLabel) {
|
|
1179
|
+
let p = null;
|
|
1180
|
+
try { p = JSON.parse(dec.decode(blob)).payload?.payload ?? null; } catch {}
|
|
1181
|
+
if (p && typeof p.fromShard === "string" && typeof p.toShard === "string" && Array.isArray(p.homeKeys)) {
|
|
1182
|
+
splits.push({ fromShard: p.fromShard, toShard: p.toShard, homeKeys: p.homeKeys.filter((k) => typeof k === "string") });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
// AN ADMITTED GLOBAL WRITE (§5.4, slice 4 — coordinator only): the sealed
|
|
1187
|
+
// bytes are surfaced to the HOST, which re-admits them into every shard
|
|
1188
|
+
// chain (the reverse lane — replicated reference data). O(events) per
|
|
1189
|
+
// admitted intent, and only when the law declares `.global(...)`.
|
|
1190
|
+
if (globalsDeclared && globalsDeclared.size > 0) {
|
|
1191
|
+
let touchesGlobal = false;
|
|
1192
|
+
for (const ev of parseIntentEvents(blob)) {
|
|
1193
|
+
const rows = qById(eng, ws, ev.aggregate);
|
|
1194
|
+
const t = ev.sets.get("__type") ?? (rows.length ? rows[0].type : undefined);
|
|
1195
|
+
if (t !== undefined && globalsDeclared.has(t)) { touchesGlobal = true; break; }
|
|
1196
|
+
}
|
|
1197
|
+
if (touchesGlobal) globalsOut.push({ intentId: intentId || "", oid: oid.slice(0, 10), blob });
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
// THE CHAIN GATE'S OWN WRONG-HOME VERDICT (#41): the law's homing invariant
|
|
1201
|
+
// refuses with `wrong-home:<label>` — surface it as the SAME typed refusal
|
|
1202
|
+
// the edge bailiff answers (self-healing, never queued). The bailiff above
|
|
1203
|
+
// remains defense-in-depth; this arm is the gate catching what it can't.
|
|
1204
|
+
const wh = String(res.error || "").match(/wrong-home:(s\d+)/);
|
|
1205
|
+
if (wh) {
|
|
1206
|
+
const coordinator = (shardRouting && shardRouting.coordinatorWs) || (ws.match(/^(.*)--s\d+$/) || [])[1] || ws;
|
|
1207
|
+
rejected.push({
|
|
1208
|
+
oid: oid.slice(0, 10),
|
|
1209
|
+
error: `wrong-home: this write's home is assigned to '${coordinator}--${wh[1]}' — the chain gate's homing invariant refused it; a routed client re-routes and retries automatically`,
|
|
1210
|
+
wrongHome: { workspace: `${coordinator}--${wh[1]}` },
|
|
1211
|
+
});
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
const error = `${String(res.error || "").slice(0, 300)} — dead-lettered; fix the law (or the payload), then POST /v1/workspaces/${ws}/dead-letters/retry`;
|
|
1215
|
+
rejected.push({ oid: oid.slice(0, 10), error, deadLettered: true });
|
|
1216
|
+
deadLetters.push({ intentId, oid: oid.slice(0, 10), session: cid, error, blob, domain: dom, directiveId: dirId });
|
|
1217
|
+
}
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
rejected.push({ oid: oid.slice(0, 10), error: String(e).slice(0, 200) });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (admitted.length) await commitAndPush(eng, ws, ledger);
|
|
1223
|
+
try { await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: ref, delete: true, headers: ledger.headers }); } catch {}
|
|
1224
|
+
results.push({ session: cid, admitted, rejected, ...(skipped.length ? { skipped } : {}) });
|
|
1225
|
+
} catch (e) {
|
|
1226
|
+
results.push({ session: cid, error: String(e).slice(0, 200) });
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
const newMain = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
1230
|
+
log("admit", { ws, sessions: results.length, main: newMain });
|
|
1231
|
+
// The coalesced §5.2 delta of this batch (null when no scoped read moved): the HOST
|
|
1232
|
+
// closes it against the coordinator's recorded frontier and authors the Order.
|
|
1233
|
+
const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore, newMain) : null;
|
|
1234
|
+
return {
|
|
1235
|
+
sessions: results, main: newMain, deadLetters,
|
|
1236
|
+
...(summary ? { summary } : {}),
|
|
1237
|
+
...(placements.length ? { placements } : {}),
|
|
1238
|
+
...(splits.length ? { splits } : {}),
|
|
1239
|
+
...(globalsOut.length ? { globals: globalsOut } : {}),
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* VERIFY AN UPLOADED LEDGER — "the githolon executes and thus validates it". Fresh-mounts
|
|
1245
|
+
* the workspace from custody (never a cached mount — the bytes under judgment are the
|
|
1246
|
+
* ledger's, not memory's) and asks the wasm to replay-verify the WHOLE main chain from
|
|
1247
|
+
* genesis (contiguity → per-intent re-admission with the engine re-run; byte-preserving,
|
|
1248
|
+
* read-only — see wasm_git_holon.rs `verify_chain`). Returns the wasm's verdict object:
|
|
1249
|
+
* `{valid:true, head, intents, plansRerun, controllerHash, installedDomains}` or
|
|
1250
|
+
* `{valid:false, check, error, atIndex?, atCommit?}`. The HOST decides what a refusal
|
|
1251
|
+
* does (archive + refuse to serve); this op never mutates the ledger.
|
|
1252
|
+
*/
|
|
1253
|
+
export async function verifyChain(eng, ws, ledger) {
|
|
1254
|
+
unmount(eng, ws);
|
|
1255
|
+
const m = await mountWorkspace(eng, ws, ledger);
|
|
1256
|
+
if (!m.restored) {
|
|
1257
|
+
return { valid: false, check: "mount", error: m.restoreError || "no refs/heads/main on the ledger — nothing to verify (push refs/heads/main to deliver a chain first)" };
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
return JSON.parse(call(eng.ex, "verify_chain", { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH }, eng.STDERR));
|
|
1261
|
+
} finally {
|
|
1262
|
+
// The verified (or refused) chain must never linger as a servable mount: serving
|
|
1263
|
+
// re-restores from custody AFTER the host blesses the verdict.
|
|
1264
|
+
unmount(eng, ws);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Verify the CURRENTLY MOUNTED in-memory chain — no ledger remount. The
|
|
1270
|
+
* LOCAL-FIRST assert: a holon minted offline (`githolon ledger init`) proves its
|
|
1271
|
+
* own validity with the SAME wasm gate the cloud runs before it is ever
|
|
1272
|
+
* pushed. (The cloud lane keeps verifyChain's remount hygiene — it must verify
|
|
1273
|
+
* the LEDGER's bytes, never a hot mount.)
|
|
1274
|
+
*/
|
|
1275
|
+
export function verifyChainLocal(eng, ws) {
|
|
1276
|
+
return JSON.parse(call(eng.ex, "verify_chain", { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH }, eng.STDERR));
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* ARCHIVE A REFUSED UPLOAD: move custody's `refs/heads/main` to
|
|
1281
|
+
* `refs/heads/refused/<label>` (retirement is a move, never an erasure — the refused
|
|
1282
|
+
* bytes stay inspectable; inside refs/heads/ because that is the namespace custody
|
|
1283
|
+
* accepts writes/deletes on) and delete `main` so the workspace reads as unborn again.
|
|
1284
|
+
* Mounts fresh; leaves no mount.
|
|
1285
|
+
*/
|
|
1286
|
+
export async function archiveRefusedMain(eng, ws, ledger, label) {
|
|
1287
|
+
unmount(eng, ws);
|
|
1288
|
+
const m = await mountWorkspace(eng, ws, ledger);
|
|
1289
|
+
if (!m.restored) { unmount(eng, ws); return { archived: false, error: "no main to archive" }; }
|
|
1290
|
+
const gitdir = gitdirOf(ws);
|
|
1291
|
+
try {
|
|
1292
|
+
await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/refused/${label}`, force: true, headers: ledger.headers });
|
|
1293
|
+
await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/${BRANCH}`, delete: true, headers: ledger.headers });
|
|
1294
|
+
return { archived: true, ref: `refs/heads/refused/${label}` };
|
|
1295
|
+
} finally {
|
|
1296
|
+
unmount(eng, ws);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/** Genesis boot: bootstrap → the nomos controller; idempotent when already Active. */
|
|
1301
|
+
export async function boot(eng, ws, ledger) {
|
|
1302
|
+
const m = await mountWorkspace(eng, ws, ledger);
|
|
1303
|
+
if (nomosActive(eng, ws)) {
|
|
1304
|
+
return { status: "already-initialized", controllerHash: eng.hashes.nomos, rows: qById(eng, ws, `domain-installation:${eng.hashes.nomos}`), mount: m };
|
|
1305
|
+
}
|
|
1306
|
+
const result = author(eng, ws, "bootstrap", "installDomain", installPayload(eng.hashes.nomos, eng.nomosPkg, eng.installedBy), "");
|
|
1307
|
+
await commitAndPush(eng, ws, ledger);
|
|
1308
|
+
return { status: "created", head: result.head, controllerHash: eng.hashes.nomos, rows: qById(eng, ws, `domain-installation:${eng.hashes.nomos}`), mount: m };
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/** Install a tenant package under the nomos controller (validation is the HOST's job). */
|
|
1312
|
+
export async function deploy(eng, ws, ledger, usda, domainHash) {
|
|
1313
|
+
const result = author(eng, ws, "nomos", "installDomain", installPayload(domainHash, usda, eng.installedBy), eng.hashes.nomos);
|
|
1314
|
+
// THE SHARD-IDENTITY ENSURE (#41 — slice 3a): a taxonomy-bearing law carries the
|
|
1315
|
+
// homing invariant, which judges against the workspace's OWN declared identity
|
|
1316
|
+
// (`NomosShardIdentity`, law-state in the shard's own chain). The worker is the
|
|
1317
|
+
// bailiff: it records the CUSTODY fact (this workspace's name ⇒ its shard label)
|
|
1318
|
+
// through the chain's own gate right after the law lands — attributed, replayable,
|
|
1319
|
+
// idempotent (an Ensure; re-deploys re-assert the same state). Coordinator ⇒ label ""
|
|
1320
|
+
// (no homing claim — the invariant holds vacuously there).
|
|
1321
|
+
try {
|
|
1322
|
+
eng.directiveRoutesCache = null; // the just-staged overlay may add routes
|
|
1323
|
+
const routed = new Set([...directiveRoutes(eng).keys()].map((k) => k.split(" ")[0]));
|
|
1324
|
+
if (routed.size) {
|
|
1325
|
+
const m = ws.match(/^(.*)--(s\d+)$/);
|
|
1326
|
+
const label = m ? m[2] : "";
|
|
1327
|
+
const coordinator = m ? m[1] : ws;
|
|
1328
|
+
const existing = qById(eng, ws, "nomos-shard-identity:self");
|
|
1329
|
+
const current = existing.length ? existing[0].data : null;
|
|
1330
|
+
if (!current || current.label !== label || current.coordinator !== coordinator) {
|
|
1331
|
+
for (const dom of routed) {
|
|
1332
|
+
try {
|
|
1333
|
+
const declared = author(eng, ws, dom, "nomosDeclareShardIdentity", { label, coordinator, declaredAt: new Date().toISOString() }, domainHash);
|
|
1334
|
+
if (declared && declared.ok !== false) break; // one declaration IS the workspace's identity
|
|
1335
|
+
} catch {} // a routed domain owned by an earlier law: its own deploy declared it
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
} catch {}
|
|
1340
|
+
await commitAndPush(eng, ws, ledger);
|
|
1341
|
+
return { head: result.head, installation: qById(eng, ws, `domain-installation:${domainHash}`) };
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
export function snapshot(eng, ws) {
|
|
1345
|
+
const wsDir = eng.preopen.dir.contents.get("ws").contents.get(ws);
|
|
1346
|
+
return serializeTree(wsDir);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
export function stats(eng) {
|
|
1350
|
+
return {
|
|
1351
|
+
mounted: eng.mounted.size,
|
|
1352
|
+
wasmLinearMemMB: +(eng.ex.memory.buffer.byteLength / 1048576).toFixed(1),
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** Per-branch projection observables (cursor, projectCalls, sqliteBytes — see #37). */
|
|
1357
|
+
export const projectionStats = (eng) => JSON.parse(call(eng.ex, "projection_stats", {}, eng.STDERR));
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* THE POST-ACK WARM LANE (#34): replenish the wasm engine's pristine-sandbox pool
|
|
1361
|
+
* for every law dispatched since the last call. Call AFTER the response is acked —
|
|
1362
|
+
* DO: `ctx.waitUntil(Promise.resolve().then(() => warmEngine(eng)))`; container:
|
|
1363
|
+
* `setImmediate(() => warmEngine(eng))` — so the NEXT write finds its sandbox
|
|
1364
|
+
* already booted (take-use-discard; a sandbox never executes two plans). Cheap
|
|
1365
|
+
* no-op when the pool is already full or nothing was dispatched.
|
|
1366
|
+
*/
|
|
1367
|
+
export function warmEngine(eng) {
|
|
1368
|
+
flushDeferredProjections(eng); // the deferred-ack projection flush (#47) rides the same lane
|
|
1369
|
+
return JSON.parse(call(eng.ex, "engine_warm", {}, eng.STDERR));
|
|
1370
|
+
}
|