@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.
- package/LICENSE.md +36 -0
- package/compile_package.mjs +50 -0
- package/package.json +59 -0
- package/src/aggregate.ts +167 -0
- package/src/authoring.ts +119 -0
- package/src/build_package.ts +636 -0
- package/src/certified_read.ts +313 -0
- package/src/codegen_dart.ts +2732 -0
- package/src/codegen_dot.ts +466 -0
- package/src/codegen_provider_dart.ts +358 -0
- package/src/codegen_ts.ts +365 -0
- package/src/codegen_usda.ts +388 -0
- package/src/combined.ts +195 -0
- package/src/compile_engine.ts +567 -0
- package/src/compile_package_main.ts +496 -0
- package/src/compose.ts +317 -0
- package/src/count.ts +218 -0
- package/src/ctx.ts +57 -0
- package/src/derived.ts +138 -0
- package/src/directive.ts +306 -0
- package/src/drivers.ts +95 -0
- package/src/emits_guard.ts +123 -0
- package/src/engine_entry.ts +449 -0
- package/src/exists.ts +170 -0
- package/src/extremum.ts +227 -0
- package/src/fields.ts +291 -0
- package/src/framework/bootstrap.ts +22 -0
- package/src/framework/disclosure.ts +108 -0
- package/src/framework/domain_lifecycle.ts +108 -0
- package/src/framework/identity.ts +537 -0
- package/src/framework/impure_capability.ts +643 -0
- package/src/framework/rbac.ts +418 -0
- package/src/framework/repair.ts +150 -0
- package/src/framework/sync_lifecycle.ts +125 -0
- package/src/framework/workspace_invariant.ts +128 -0
- package/src/framework/workspaces.ts +817 -0
- package/src/index.ts +317 -0
- package/src/manifest.ts +947 -0
- package/src/ops.ts +145 -0
- package/src/ordered_read.ts +228 -0
- package/src/predicate.ts +203 -0
- package/src/query/compile.ts +0 -0
- package/src/query/relations.ts +144 -0
- package/src/query.ts +151 -0
- package/src/read.ts +54 -0
- package/src/relation.ts +189 -0
- package/src/report/csv.ts +54 -0
- package/src/report.ts +401 -0
- package/src/spatial.ts +115 -0
- package/src/sum.ts +194 -0
- package/src/usd.ts +563 -0
- package/src/wire.ts +149 -0
- 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";
|