@githolon/dsl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE.md +36 -0
  2. package/compile_package.mjs +50 -0
  3. package/package.json +59 -0
  4. package/src/aggregate.ts +167 -0
  5. package/src/authoring.ts +119 -0
  6. package/src/build_package.ts +636 -0
  7. package/src/certified_read.ts +313 -0
  8. package/src/codegen_dart.ts +2732 -0
  9. package/src/codegen_dot.ts +466 -0
  10. package/src/codegen_provider_dart.ts +358 -0
  11. package/src/codegen_ts.ts +365 -0
  12. package/src/codegen_usda.ts +388 -0
  13. package/src/combined.ts +195 -0
  14. package/src/compile_engine.ts +567 -0
  15. package/src/compile_package_main.ts +496 -0
  16. package/src/compose.ts +317 -0
  17. package/src/count.ts +218 -0
  18. package/src/ctx.ts +57 -0
  19. package/src/derived.ts +138 -0
  20. package/src/directive.ts +306 -0
  21. package/src/drivers.ts +95 -0
  22. package/src/emits_guard.ts +123 -0
  23. package/src/engine_entry.ts +449 -0
  24. package/src/exists.ts +170 -0
  25. package/src/extremum.ts +227 -0
  26. package/src/fields.ts +291 -0
  27. package/src/framework/bootstrap.ts +22 -0
  28. package/src/framework/disclosure.ts +108 -0
  29. package/src/framework/domain_lifecycle.ts +108 -0
  30. package/src/framework/identity.ts +537 -0
  31. package/src/framework/impure_capability.ts +643 -0
  32. package/src/framework/rbac.ts +418 -0
  33. package/src/framework/repair.ts +150 -0
  34. package/src/framework/sync_lifecycle.ts +125 -0
  35. package/src/framework/workspace_invariant.ts +128 -0
  36. package/src/framework/workspaces.ts +817 -0
  37. package/src/index.ts +317 -0
  38. package/src/manifest.ts +947 -0
  39. package/src/ops.ts +145 -0
  40. package/src/ordered_read.ts +228 -0
  41. package/src/predicate.ts +203 -0
  42. package/src/query/compile.ts +0 -0
  43. package/src/query/relations.ts +144 -0
  44. package/src/query.ts +151 -0
  45. package/src/read.ts +54 -0
  46. package/src/relation.ts +189 -0
  47. package/src/report/csv.ts +54 -0
  48. package/src/report.ts +401 -0
  49. package/src/spatial.ts +115 -0
  50. package/src/sum.ts +194 -0
  51. package/src/usd.ts +563 -0
  52. package/src/wire.ts +149 -0
  53. package/src/wire_encode.ts +250 -0
@@ -0,0 +1,567 @@
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
+ * USD → engine-bundle — the EXPLICIT, SEPARATE generative stage (#138, increment 1).
10
+ *
11
+ * Jack: "I want TS → USD and then as a separate stage USD → wasm."
12
+ *
13
+ * #137 gave us TS → USD: `emitUsd(layers)` encodes `DomainModule`s into the composed
14
+ * OpenUSD-shaped IR document (`UsdDocument`) — the STRUCTURE / LAW / IDENTITY: which
15
+ * directives + aggregates exist, their target / marker / requires / scope / reads /
16
+ * emits contracts, flattened + hashed by `usdHash`. The USD IR carries the LAW, NOT
17
+ * the executable code.
18
+ *
19
+ * This module is the NEXT stage: USD → the engine module the runtime evals. Today
20
+ * `golden/emit_engine_golden.ts` HAND-ASSEMBLES the registry `{ "domain directiveId"
21
+ * -> { directive, agg } }` straight from the imported domain modules and assigns
22
+ * `globalThis.plan` — skipping the USD intermediate entirely. Here the SAME engine
23
+ * module is generated FROM the composed USD doc instead: the USD directive prims drive
24
+ * WHICH directives the module exposes (the law), and the supplied `plans` BIND each to
25
+ * its executable `.plan()`-bearing `Directive` + target `AggregateHandle` (the code).
26
+ *
27
+ * Two load-bearing properties the explicit stage buys:
28
+ * 1. **Identity flows TS → USD → module.** The compiled module's identity is STAMPED
29
+ * `= usdHash(usdDoc, opts)` — NOT an ad-hoc bundle/registry hash. Re-deriving the
30
+ * module from the same `(doc, opts)` re-stamps byte-identically; a different law
31
+ * (different USD) yields a different module identity.
32
+ * 2. **Fail-closed on USD ↔ plan divergence.** A USD directive prim with no matching
33
+ * plan, or a supplied plan with no matching USD directive prim, REFUSES the module
34
+ * (`EngineCompileError`). The explicit stage certifies that the executable bundle
35
+ * and the composed law are in 1:1 agreement — the inconsistency the hand path
36
+ * cannot catch (it has no law artifact to check against).
37
+ *
38
+ * ADDITIVE + build-time only: imports `node:crypto` transitively via `usd.js`, reached
39
+ * via the `@githolon/dsl/compile-engine` subpath, NOT the runtime `index.ts` barrel. It
40
+ * does NOT bake/touch any pinned `*.wasm` — wiring the REAL bundle through this stage +
41
+ * re-baking is a later, gated step. SYNTHETIC framework units only.
42
+ */
43
+ import type { Directive } from "./directive.js";
44
+ import type { AggregateHandle, AggregateInvariantFn } from "./aggregate.js";
45
+ import type { ComposeOptions } from "./compose.js";
46
+ import { flattenUsd, usdHash, type UsdDocument, type UsdPrim } from "./usd.js";
47
+ import type { DeclaredEmits } from "./emits_guard.js";
48
+ import type {
49
+ WorkspaceInvariantDecl,
50
+ WorkspaceInvariantReads,
51
+ WorkspaceInvariantAssert,
52
+ InvariantRef,
53
+ } from "./framework/workspace_invariant.js";
54
+
55
+ /** A directive's executable binding: its `.plan()`-bearing `Directive` + target handle.
56
+ * This is the EXECUTABLE half the USD IR (the law) does not carry — the stage BINDS it
57
+ * to the law. Shape mirrors `emit_engine_golden.ts`'s hand-assembled `RegistryEntry`. */
58
+ export interface PlanBinding {
59
+ /** The `.plan()`-bearing directive (the executable). `unknown` payload — the stage
60
+ * only routes; payload typing is the per-directive call site's concern. */
61
+ readonly directive: Directive<unknown>;
62
+ /** The directive's target `AggregateHandle` (resolves field kinds when planning). */
63
+ readonly agg: AggregateHandle;
64
+ }
65
+
66
+ /** The executable plans the stage binds to the USD law, keyed by directive id. */
67
+ export type PlanMap = ReadonlyMap<string, PlanBinding>;
68
+
69
+ /** One compiled registry entry: the USD law prim + its bound executable. */
70
+ export interface EngineRegistryEntry {
71
+ /** The directive id (the last segment of the USD prim `path`). */
72
+ readonly directiveId: string;
73
+ /** The target aggregate TYPE id (from the USD directive prim — the LAW). */
74
+ readonly target: string;
75
+ /** The bound executable directive + its target handle (from `plans`). */
76
+ readonly binding: PlanBinding;
77
+ }
78
+
79
+ /**
80
+ * The compiled engine module: the registry the runtime dispatches against PLUS the
81
+ * stamped identity. `registry` is keyed by directive id (the same `{ directive, agg }`
82
+ * shape `emit_engine_golden.ts` assembles by hand). `identity` is `usdHash(usdDoc,
83
+ * opts)` — the composed-IR identity flowed straight through, the "USD → wasm as a
84
+ * separate stage" anchor.
85
+ */
86
+ export interface EngineModule {
87
+ /** directiveId → its compiled registry entry (USD law prim ↔ bound plan). */
88
+ readonly registry: ReadonlyMap<string, EngineRegistryEntry>;
89
+ /** The module identity = `usdHash(usdDoc, opts)` (flows TS → USD → module). */
90
+ readonly identity: string;
91
+ /**
92
+ * directiveId → its DECLARED emit boundary (event type → `{ max? }`), carried
93
+ * STRAIGHT from the USD directive prim's `emits` (#137). A directive declaring no
94
+ * emits maps to `{}`. This is the data the runtime / gate reads to enforce
95
+ * `emitted ⊆ declared` at dispatch via {@link assertEmitsWithinDeclared} — the
96
+ * WIRING of that call is the later, gated flip; compiling the data onto the module
97
+ * is the additive step here. The emit clauses are also statically consistency-checked
98
+ * at compile (the `emits-malformed` rule), so what flows here is already well-formed.
99
+ */
100
+ readonly emitsByDirective: ReadonlyMap<string, DeclaredEmits>;
101
+ /**
102
+ * aggregateTypeId → its declared `invariant` body (#250). OMITTED for aggregate types
103
+ * that carry no invariant. The body ships in the engine bundle, NEVER the ledger —
104
+ * presence-only (`hasInvariant: true`) is what the manifest hashes. The map is keyed by
105
+ * the aggregate handle's `.id` (the wire type string, e.g. `"Roster"`).
106
+ */
107
+ readonly aggregateInvariants: ReadonlyMap<string, AggregateInvariantFn>;
108
+ /**
109
+ * The workspace-invariant declarations for this module (#266), carrying both the
110
+ * `reads` and `assert` executable bodies. The gate dispatches:
111
+ * 1. `workspaceInvariantReads` — call the matching `.reads` body to derive the ref-set.
112
+ * 2. `workspaceInvariant` — call the matching `.assert` body over the resolved snapshots.
113
+ * OMITTED (empty map) when the domain declares no workspace invariants.
114
+ */
115
+ readonly workspaceInvariants: ReadonlyMap<string, WorkspaceInvariantDecl>;
116
+ }
117
+
118
+ /** A divergence the explicit USD → module stage refuses (fail-closed). */
119
+ export type EngineCompileRule =
120
+ /** A USD directive prim has no matching plan in `plans` (the law has no executable). */
121
+ | "plan-missing"
122
+ /** A supplied plan has no matching USD directive prim (executable not in the law). */
123
+ | "plan-extra"
124
+ /** A bound plan's directive/handle disagrees with the USD prim's id/target (the
125
+ * executable does not implement the law it is bound to). */
126
+ | "binding-mismatch"
127
+ /** A directive's DECLARED emit set is malformed: a duplicate event type within the
128
+ * one directive, or a non-positive / non-integer `max`. A conservative STATIC check
129
+ * — it does NOT consult a global event registry (the model has none yet); it only
130
+ * rejects a self-inconsistent declared boundary so the data carried onto the module
131
+ * (and later enforced) is well-formed. */
132
+ | "emits-malformed";
133
+
134
+ /** A typed, fail-closed engine-compile failure (USD ↔ plan divergence). */
135
+ export class EngineCompileError extends Error {
136
+ readonly rule: EngineCompileRule;
137
+ readonly directiveId: string;
138
+ readonly detail: string;
139
+ constructor(rule: EngineCompileRule, directiveId: string, detail: string) {
140
+ super(`engine compile ${rule} at "${directiveId}": ${detail}`);
141
+ this.name = "EngineCompileError";
142
+ this.rule = rule;
143
+ this.directiveId = directiveId;
144
+ this.detail = detail;
145
+ }
146
+ }
147
+
148
+ /** The directive id is the LAST `/`-segment of a USD prim path (e.g.
149
+ * `/Sample/createThing` → `createThing`). */
150
+ function directiveIdOf(path: string): string {
151
+ const segments = path.split("/").filter((s) => s.length > 0);
152
+ return segments[segments.length - 1] ?? path;
153
+ }
154
+
155
+ /**
156
+ * Validate a USD directive prim's declared `emits` and return it as a well-formed
157
+ * {@link DeclaredEmits} (omitted ⇒ `{}`). CONSERVATIVE static consistency only — it
158
+ * does NOT cross-check against a global event registry (the model has none yet); it
159
+ * rejects a SELF-inconsistent declared boundary so the data flowing onto the module
160
+ * (and later enforced) is sound. Throws `emits-malformed` for:
161
+ * - a non-integer / non-positive `max` (a `max` of 0 or negative or fractional can
162
+ * never be satisfied or is meaningless as a count bound);
163
+ * - a duplicate event type (two clauses for the same type). The USD prim already
164
+ * carries an OBJECT (whose keys are inherently unique), so this clause guards the
165
+ * invariant explicitly and is sensitive should the carrier ever become entry-based.
166
+ */
167
+ function validateDeclaredEmits(
168
+ directiveId: string,
169
+ emits: Record<string, { max?: number }> | undefined,
170
+ ): DeclaredEmits {
171
+ if (emits === undefined) return {};
172
+ const out: DeclaredEmits = {};
173
+ for (const eventType of Object.keys(emits)) {
174
+ if (Object.prototype.hasOwnProperty.call(out, eventType)) {
175
+ throw new EngineCompileError(
176
+ "emits-malformed",
177
+ directiveId,
178
+ `declared emit event type "${eventType}" appears more than once.`,
179
+ );
180
+ }
181
+ const clause = emits[eventType]!;
182
+ const max = clause.max;
183
+ if (max !== undefined) {
184
+ if (!Number.isInteger(max) || max <= 0) {
185
+ throw new EngineCompileError(
186
+ "emits-malformed",
187
+ directiveId,
188
+ `declared emit "${eventType}" has an invalid max ${max} — a max must be a ` +
189
+ `positive integer.`,
190
+ );
191
+ }
192
+ out[eventType] = { max };
193
+ } else {
194
+ out[eventType] = {};
195
+ }
196
+ }
197
+ return out;
198
+ }
199
+
200
+ /**
201
+ * Compile the engine module FROM the composed USD doc (the law) + the supplied
202
+ * executable `plans` (the code). The SEPARATE USD → engine-bundle stage.
203
+ *
204
+ * 1. `flattenUsd(usdDoc, opts)` → the effective composed prims; the DIRECTIVE prims
205
+ * are the law the engine module must expose.
206
+ * 2. For each directive prim, the directive id is its path's last segment; bind
207
+ * `plans.get(directiveId)`. The prim's `target` (the law) is checked against the
208
+ * bound directive's `aggregateId` + handle id (the executable) — a disagreement is
209
+ * a `binding-mismatch`. Builds the registry entry `{ directiveId, target, binding }`.
210
+ * 3. Stamp `identity = usdHash(usdDoc, opts)` — identity flows TS → USD → module.
211
+ * 4. FAIL CLOSED on divergence: a directive prim with no plan → `plan-missing`; a plan
212
+ * with no directive prim → `plan-extra`. The module is refused unless the USD law
213
+ * and the executable bundle are in exact 1:1 agreement.
214
+ * 5. Collect aggregate invariant bodies from the bound aggregate handles (those with
215
+ * `hasInvariant: true`). Collect workspace invariant declarations from `wsInvariants`
216
+ * if supplied.
217
+ *
218
+ * Behaviour-preserving vs `emit_engine_golden.ts`: for the same domains, the produced
219
+ * registry exposes the same directive-id set, each bound to the same `{ directive, agg }`
220
+ * — so routing the real bundle through this stage later is a no-op on dispatch.
221
+ *
222
+ * @param wsInvariants - the domain's workspace-invariant declarations (carrying the
223
+ * executable `reads`/`assert` bodies that ship in the engine bundle). Pass only the
224
+ * invariants for the SAME domain the USD doc describes; `makeEngineReport` dispatches
225
+ * them by id.
226
+ */
227
+ export function compileEngineModule(
228
+ usdDoc: UsdDocument,
229
+ plans: PlanMap,
230
+ opts: ComposeOptions = {},
231
+ wsInvariants: readonly WorkspaceInvariantDecl[] = [],
232
+ ): EngineModule {
233
+ const prims = flattenUsd(usdDoc, opts);
234
+ const directivePrims = prims.filter(
235
+ (p): p is Extract<UsdPrim, { kind: "directive" }> => p.kind === "directive",
236
+ );
237
+
238
+ const registry = new Map<string, EngineRegistryEntry>();
239
+ const emitsByDirective = new Map<string, DeclaredEmits>();
240
+ const bound = new Set<string>();
241
+
242
+ for (const prim of directivePrims) {
243
+ const directiveId = directiveIdOf(prim.path);
244
+
245
+ // A directive id appearing twice across the flattened law is itself a structural
246
+ // inconsistency the explicit stage refuses (the registry key would collide).
247
+ if (registry.has(directiveId)) {
248
+ throw new EngineCompileError(
249
+ "binding-mismatch",
250
+ directiveId,
251
+ `USD directive id "${directiveId}" appears at more than one prim path ` +
252
+ `(latest "${prim.path}") — the engine registry key would collide.`,
253
+ );
254
+ }
255
+
256
+ const binding = plans.get(directiveId);
257
+ if (binding === undefined) {
258
+ throw new EngineCompileError(
259
+ "plan-missing",
260
+ directiveId,
261
+ `USD directive prim "${prim.path}" has no matching plan — the composed law ` +
262
+ `declares a directive the executable bundle does not implement.`,
263
+ );
264
+ }
265
+
266
+ // The executable must implement the law it is bound to: the directive's own id must
267
+ // match the USD prim's id, and its declared target (aggregateId / handle id) must
268
+ // match the USD prim's `target`. Otherwise the wrong code is wired to the law.
269
+ if (binding.directive.id !== directiveId) {
270
+ throw new EngineCompileError(
271
+ "binding-mismatch",
272
+ directiveId,
273
+ `bound directive id "${binding.directive.id}" does not match USD directive ` +
274
+ `id "${directiveId}".`,
275
+ );
276
+ }
277
+ if (binding.directive.aggregateId !== prim.target) {
278
+ throw new EngineCompileError(
279
+ "binding-mismatch",
280
+ directiveId,
281
+ `bound directive targets aggregate "${binding.directive.aggregateId}" but the ` +
282
+ `USD law's target is "${prim.target}".`,
283
+ );
284
+ }
285
+ if (binding.agg.id !== prim.target) {
286
+ throw new EngineCompileError(
287
+ "binding-mismatch",
288
+ directiveId,
289
+ `bound aggregate handle "${binding.agg.id}" does not match the USD law's ` +
290
+ `target "${prim.target}".`,
291
+ );
292
+ }
293
+
294
+ // Carry the declared emit boundary off the USD prim (the law), validating it is
295
+ // self-consistent (`emits-malformed`) before it flows onto the module. The set the
296
+ // runtime/gate will enforce `emitted ⊆ declared` against at dispatch.
297
+ const declaredEmits = validateDeclaredEmits(directiveId, prim.emits);
298
+
299
+ registry.set(directiveId, { directiveId, target: prim.target, binding });
300
+ emitsByDirective.set(directiveId, declaredEmits);
301
+ bound.add(directiveId);
302
+ }
303
+
304
+ // Fail closed on the OTHER direction: a supplied plan with NO matching USD directive
305
+ // prim — an executable not present in the composed law.
306
+ for (const directiveId of plans.keys()) {
307
+ if (!bound.has(directiveId)) {
308
+ throw new EngineCompileError(
309
+ "plan-extra",
310
+ directiveId,
311
+ `plan "${directiveId}" has no matching USD directive prim — an executable not ` +
312
+ `present in the composed law.`,
313
+ );
314
+ }
315
+ }
316
+
317
+ // Identity FLOWS TS → USD → module: stamp the composed-IR hash, not a bundle hash.
318
+ const identity = usdHash(usdDoc, opts);
319
+
320
+ // Collect aggregate invariant bodies from the bound handles (step 5).
321
+ // Keyed by aggregate type id (handle.id), carrying only handles that declare an invariant.
322
+ const aggregateInvariants = new Map<string, AggregateInvariantFn>();
323
+ for (const entry of registry.values()) {
324
+ const handle = entry.binding.agg;
325
+ if (handle.hasInvariant === true && handle.invariant !== undefined) {
326
+ aggregateInvariants.set(handle.id, handle.invariant);
327
+ }
328
+ }
329
+
330
+ // Collect workspace invariant declarations by id. The executable `reads`/`assert` bodies
331
+ // are carried straight through — they ship in the engine bundle, not the ledger.
332
+ const workspaceInvariants = new Map<string, WorkspaceInvariantDecl>();
333
+ for (const decl of wsInvariants) {
334
+ workspaceInvariants.set(decl.id, decl);
335
+ }
336
+
337
+ return { registry, identity, emitsByDirective, aggregateInvariants, workspaceInvariants };
338
+ }
339
+
340
+ // #250 follow-up: this module emits `globalThis.plan` only. The engine-backed AGGREGATE
341
+ // INVARIANT oracle (nomos_admission_peer::EngineAggregateInvariant) dispatches a
342
+ // { intent: { aggregateInvariant: { of: "<AggType>" } }, priorState: <snapshot> }
343
+ // job via `globalThis.planReport` (Report mode). The compiled emit should add a
344
+ // `planReport` aggregate-invariant case the SAME way `executeDirectiveToIntent` is dispatched here:
345
+ // look up the aggregate handle by `of`, run its declared `.invariant` body
346
+ // (aggregate(..., { invariant }) — see dsl/src/aggregate.ts; the handle carries
347
+ // `.invariant` + `hasInvariant`) over `priorState`, and return the EXACT verdict
348
+ // JSON.stringify({accept:true}) | JSON.stringify({reject:"<code>"}). Until then the
349
+ // Rust path is proven end-to-end by a per-domain shim in
350
+ // admission-peer/tests/aggregate_invariant_e2e.rs.
351
+
352
+ /**
353
+ * The classic-script `globalThis.plan(job)` equivalent, derived FROM a compiled
354
+ * `EngineModule`. Mirrors `emit_engine_golden.ts`'s `plan`: it reads the dispatch
355
+ * fields, looks up the directive in the USD-derived registry, runs the REAL
356
+ * `executeDirectiveToIntent` (validate → plan → group), and returns the kernel `WireEvent[]`. A
357
+ * throw (unknown directive / Zod failure) is the deterministic engine halt the host
358
+ * quarantines.
359
+ *
360
+ * The registry key here is the directive id (the engine module is one domain's
361
+ * composed law). The hand path keys by `${domain} ${directiveId}` because it spans
362
+ * multiple imported domain modules; a composed USD doc is already the unified law, so
363
+ * the directive id is the dispatch key. Provided so the production cutover can route
364
+ * the runtime entry through the compiled module without re-implementing dispatch.
365
+ *
366
+ * Takes the `executeDirectiveToIntent` implementation as a parameter to keep this module free of
367
+ * the runtime `index.ts` barrel (which would drag `node:crypto` resolution concerns);
368
+ * the call site passes `executeDirectiveToIntent` from `@githolon/dsl`.
369
+ */
370
+ export function makeEnginePlan<Wire>(
371
+ module: EngineModule,
372
+ executeDirectiveToIntent: (
373
+ directive: Directive<unknown>,
374
+ agg: AggregateHandle,
375
+ payload: never,
376
+ ctx: never,
377
+ ) => { events: Wire[]; strikes: string[] },
378
+ portsFromHost: () => never,
379
+ ): (
380
+ job: { intent?: { directiveId?: string; payload?: unknown } },
381
+ ) => Wire[] | { events: Wire[]; strikes: string[] } {
382
+ return (job) => {
383
+ const directiveId = job.intent?.directiveId;
384
+ if (typeof directiveId !== "string") {
385
+ throw new Error(
386
+ `engine plan: job.intent must carry {directiveId}; got ${JSON.stringify(job.intent)}`,
387
+ );
388
+ }
389
+ const entry = module.registry.get(directiveId);
390
+ if (entry === undefined) {
391
+ throw new Error(`engine plan: no directive registered for "${directiveId}"`);
392
+ }
393
+ const ctx = portsFromHost();
394
+ const wire = executeDirectiveToIntent(
395
+ entry.binding.directive,
396
+ entry.binding.agg,
397
+ job.intent?.payload as never,
398
+ ctx,
399
+ );
400
+ // ADDITIVE strikeout (mirrors emit_engine_golden.ts): {events, strikes} only when a
401
+ // strike is present, else the bare event array — strikes-free domains are byte-stable.
402
+ return wire.strikes.length ? { events: wire.events, strikes: wire.strikes } : wire.events;
403
+ };
404
+ }
405
+
406
+ /**
407
+ * The `globalThis.planReport(job)` dispatcher derived FROM a compiled `EngineModule`.
408
+ * Mirrors `makeEnginePlan` (the plan dispatcher) but handles the THREE report-mode
409
+ * dispatch cases the Rust host issues via `globalThis.planReport`:
410
+ *
411
+ * 1. **`aggregateInvariant`** — `job.intent.aggregateInvariant.of` names the aggregate type;
412
+ * `job.priorState` is the post-apply snapshot. Runs the handle's declared `.invariant`
413
+ * body. Returns `JSON.stringify({accept:true})` | `JSON.stringify({reject:"<code>"})`.
414
+ * Vacuous-pass: aggregate types with no declared invariant return `{"accept":true}`.
415
+ *
416
+ * 2. **`workspaceInvariantReads`** — `job.intent.workspaceInvariantReads` carries the
417
+ * invariant id; `job.intent.payload` is the authored intent payload. Runs the
418
+ * declaration's `.reads` body to derive the BOUNDED ref-set. Returns
419
+ * `JSON.stringify([{name, aggregate, id}, ...])`.
420
+ *
421
+ * 3. **`workspaceInvariant`** — `job.intent.workspaceInvariant.id` names the invariant;
422
+ * `job.priorState` is the named snapshot map `{ refName → aggregateSnapshot }`.
423
+ * Runs the declaration's `.assert` body. Returns
424
+ * `JSON.stringify({accept:true})` | `JSON.stringify({reject:"<code>"})`.
425
+ *
426
+ * Dispatch job fields match the Rust host's `build_dispatch_job` serde shape EXACTLY:
427
+ * `capturedPorts` (camelCase), `priorState` (camelCase, present for agg + ws assert,
428
+ * absent for ws reads). A throw (unknown case / missing body) is the deterministic
429
+ * engine halt the Rust host quarantines.
430
+ *
431
+ * The returned function is wired as `globalThis.planReport = makeEngineReport(module)`
432
+ * in the compiled bundle — replacing the hand-written stand-in shims the Rust tests
433
+ * currently use (proven end-to-end by `aggregate_invariant_e2e.rs` and
434
+ * `workspace_invariant_gate.rs`).
435
+ */
436
+ export function makeEngineReport(
437
+ module: EngineModule,
438
+ ): (job: {
439
+ intent?: {
440
+ workspaceInvariantList?: unknown;
441
+ aggregateInvariant?: { of?: string };
442
+ workspaceInvariantReads?: unknown;
443
+ workspaceInvariant?: { id?: string };
444
+ payload?: Record<string, unknown>;
445
+ };
446
+ priorState?: Record<string, unknown>;
447
+ }) => string {
448
+ return (job) => {
449
+ const intent = job.intent ?? {};
450
+
451
+ // ── CASE 0: workspaceInvariantList — DISCOVERY (#266 slice 5c) ─────────────
452
+ // The admitting peer asks the CERTIFIED BUNDLE which workspace invariants it declares
453
+ // (so the gate sources the DECLARED set from the law itself — the bundle's hash IS the
454
+ // domain identity — instead of a parallel manifest). Returns `[{id, on}, …]`; the gate
455
+ // filters by `on == directiveId`. A domain that declares none returns `[]`.
456
+ if (intent.workspaceInvariantList !== undefined) {
457
+ const list = [...module.workspaceInvariants.values()].map((d) => ({
458
+ id: d.id,
459
+ on: d.on,
460
+ }));
461
+ return JSON.stringify(list);
462
+ }
463
+
464
+ // ── CASE 1: aggregateInvariant ─────────────────────────────────────────────
465
+ if (intent.aggregateInvariant !== undefined) {
466
+ const aggType = intent.aggregateInvariant.of;
467
+ if (typeof aggType !== "string") {
468
+ throw new Error(
469
+ `planReport aggregateInvariant: job.intent.aggregateInvariant must carry ` +
470
+ `{of: "<AggType>"}; got ${JSON.stringify(intent.aggregateInvariant)}`,
471
+ );
472
+ }
473
+ const invariantFn = module.aggregateInvariants.get(aggType);
474
+ if (invariantFn === undefined) {
475
+ // Vacuous-pass: the aggregate type has no declared invariant.
476
+ return JSON.stringify({ accept: true });
477
+ }
478
+ const snapshot = job.priorState ?? {};
479
+ const verdict = invariantFn(snapshot);
480
+ return JSON.stringify(verdict);
481
+ }
482
+
483
+ // ── CASE 2: workspaceInvariantReads ───────────────────────────────────────
484
+ if (intent.workspaceInvariantReads !== undefined) {
485
+ // The invariant id is carried in the `workspaceInvariantReads` field.
486
+ // The field value is the invariant id (the Rust host serialises it as the id directly
487
+ // or as an object; the stand-in treats it as truthy → locate by the field's presence).
488
+ // Resolve by extracting the id from the field value: the Rust shape passes
489
+ // { workspaceInvariantReads: { id: "<InvId>" }, payload: <authored> }
490
+ // The stand-in in the Rust test does NOT use `workspaceInvariantReads.id` — it fires
491
+ // on ANY truthy `workspaceInvariantReads` and reads the payload. We follow the same
492
+ // contract: locate the invariant by matching the `on` directive against
493
+ // `payload.directiveId`, OR use the id from the field when present.
494
+ const readsField = intent.workspaceInvariantReads;
495
+ let decl: WorkspaceInvariantDecl | undefined;
496
+ if (
497
+ readsField !== null &&
498
+ typeof readsField === "object" &&
499
+ typeof (readsField as { id?: unknown }).id === "string"
500
+ ) {
501
+ decl = module.workspaceInvariants.get((readsField as { id: string }).id);
502
+ }
503
+ if (decl === undefined) {
504
+ // Fallback: match by `on` directive if the id is not present in the field.
505
+ const directiveId =
506
+ intent.payload !== undefined
507
+ ? (intent.payload as { directiveId?: string }).directiveId
508
+ : undefined;
509
+ if (typeof directiveId === "string") {
510
+ for (const d of module.workspaceInvariants.values()) {
511
+ if (d.on === directiveId) {
512
+ decl = d;
513
+ break;
514
+ }
515
+ }
516
+ }
517
+ }
518
+ if (decl === undefined) {
519
+ throw new Error(
520
+ `planReport workspaceInvariantReads: no declared workspace invariant found ` +
521
+ `for reads dispatch; job.intent=${JSON.stringify(intent)}`,
522
+ );
523
+ }
524
+ const payload = intent.payload ?? {};
525
+ const refs: InvariantRef[] = decl.reads({ intent: payload });
526
+ return JSON.stringify(refs);
527
+ }
528
+
529
+ // ── CASE 3: workspaceInvariant (assert) ───────────────────────────────────
530
+ if (intent.workspaceInvariant !== undefined) {
531
+ const invId = intent.workspaceInvariant.id;
532
+ if (typeof invId !== "string") {
533
+ throw new Error(
534
+ `planReport workspaceInvariant: job.intent.workspaceInvariant must carry ` +
535
+ `{id: "<InvId>"}; got ${JSON.stringify(intent.workspaceInvariant)}`,
536
+ );
537
+ }
538
+ const decl = module.workspaceInvariants.get(invId);
539
+ if (decl === undefined) {
540
+ throw new Error(
541
+ `planReport workspaceInvariant: no declared workspace invariant with id ` +
542
+ `"${invId}" — cannot dispatch assert.`,
543
+ );
544
+ }
545
+ const snapshots = (job.priorState ?? {}) as Record<string, Record<string, unknown>>;
546
+ const verdict = decl.assert(snapshots);
547
+ return JSON.stringify(verdict);
548
+ }
549
+
550
+ throw new Error(
551
+ `planReport: unrecognised dispatch — job.intent must carry one of ` +
552
+ `{aggregateInvariant}, {workspaceInvariantReads}, or {workspaceInvariant}; ` +
553
+ `got ${JSON.stringify(intent)}`,
554
+ );
555
+ };
556
+ }
557
+
558
+ // Re-export the PURE emit-boundary primitive so a consumer reaching for the compiled
559
+ // module's `emitsByDirective` can pair it with the enforcement call from one subpath.
560
+ // The wiring (calling this after a plan returns its events) is the later, gated flip.
561
+ export {
562
+ assertEmitsWithinDeclared,
563
+ EmitBoundaryError,
564
+ type DeclaredEmit,
565
+ type DeclaredEmits,
566
+ type EmitBoundaryRule,
567
+ } from "./emits_guard.js";