@githolon/dsl 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/codegen_proof.ts +430 -0
- package/src/codegen_ts.ts +8 -6
- package/src/compile_package_main.ts +289 -10
- package/src/engine_entry.ts +236 -83
- package/src/usd_layers.ts +722 -0
- package/src/usd_state.ts +505 -0
package/src/engine_entry.ts
CHANGED
|
@@ -29,6 +29,22 @@
|
|
|
29
29
|
* `identity = {...identityCore, ...co2_identity}` pattern), so a tenant can compose
|
|
30
30
|
* framework + tenant modules under one dispatch key.
|
|
31
31
|
*
|
|
32
|
+
* LAZY PER-DOMAIN BOOT (task #34 — hot-path boot): a domain's module may also be a
|
|
33
|
+
* THUNK `() => moduleExports`. `nomos-compile` emits thunks (`() => require("…")`),
|
|
34
|
+
* which esbuild lazy-wraps (`__esm` init runs at the require call, not at bundle
|
|
35
|
+
* top level) — so the lump's TOP-LEVEL boot no longer executes every domain's zod
|
|
36
|
+
* schema graph + directive construction. Each fresh per-plan sandbox forces ONLY
|
|
37
|
+
* the domain(s) the dispatched intent touches: per-plan boot is O(directives of
|
|
38
|
+
* the touched domain), not O(whole law). Dispatches that are keyed by AGGREGATE
|
|
39
|
+
* TYPE or RELATION (derive / combine / aggregateInvariant / invariant) cannot know
|
|
40
|
+
* which domain to force, so a thunked entry REQUIRES the compiler-emitted
|
|
41
|
+
* `routing` table (built by `collectEngineRouting` over the SAME duck-type scans —
|
|
42
|
+
* one machinery, never a parallel list) mapping each type/relation to the domain
|
|
43
|
+
* keys that declare it. Fail-closed: thunks without routing refuse at top level.
|
|
44
|
+
* The EAGER shape (plain module objects, no routing) keeps the exact pre-#34
|
|
45
|
+
* behaviour — old generated entries remain valid inputs, and old compiled lumps in
|
|
46
|
+
* deployed chains re-verify untouched (they carry their own machinery).
|
|
47
|
+
*
|
|
32
48
|
* ENGINE-BUNDLE-SAFE: this file (and everything it reaches) imports NO node builtin —
|
|
33
49
|
* it must bundle under esbuild `--platform=neutral` into the sealed QuickJS lump.
|
|
34
50
|
* The contracts mirrored here (wire shapes, vacuous-holds, strikes-conditional
|
|
@@ -48,20 +64,56 @@ import type { QueryRow } from "./report.js";
|
|
|
48
64
|
/** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
|
|
49
65
|
export type DomainModuleExports = Record<string, unknown>;
|
|
50
66
|
|
|
67
|
+
/**
|
|
68
|
+
* A domain module SOURCE: the exports bag itself (eager — the pre-#34 shape), or a
|
|
69
|
+
* thunk producing it (lazy — forced on first dispatch touch inside the fresh
|
|
70
|
+
* sandbox; `nomos-compile` emits `() => require("…")`, which esbuild defers).
|
|
71
|
+
*/
|
|
72
|
+
export type DomainModuleSource = DomainModuleExports | (() => DomainModuleExports);
|
|
73
|
+
|
|
51
74
|
/** A report: declarative query + render; the host feeds rows, the engine renders. */
|
|
52
75
|
export interface EngineReport {
|
|
53
76
|
render(rows: QueryRow[]): string;
|
|
54
77
|
}
|
|
55
78
|
|
|
79
|
+
/**
|
|
80
|
+
* THE ROUTING TABLE (lazy boot only): aggregate type / relation id → the domain
|
|
81
|
+
* keys (in `domains` declaration order) whose modules declare it. Emitted by the
|
|
82
|
+
* compiler via {@link collectEngineRouting} — the SAME scans that build the live
|
|
83
|
+
* registries, run once at compile over the same merged modules, so the table
|
|
84
|
+
* cannot drift from what a full force would register. Used by the dispatches that
|
|
85
|
+
* are NOT keyed by domain (derive / combine / aggregateInvariant / invariant) to
|
|
86
|
+
* force only the declaring domain(s). A type/relation ABSENT from its map is one
|
|
87
|
+
* NO domain declares — exactly the eager scan-everything outcome.
|
|
88
|
+
*/
|
|
89
|
+
export interface EngineRouting {
|
|
90
|
+
readonly derivedOf?: Record<string, readonly string[]>;
|
|
91
|
+
readonly combinedOf?: Record<string, readonly string[]>;
|
|
92
|
+
readonly aggregateInvariantOf?: Record<string, readonly string[]>;
|
|
93
|
+
readonly relationOf?: Record<string, readonly string[]>;
|
|
94
|
+
}
|
|
95
|
+
|
|
56
96
|
/** The one declarative input: dispatch key → ORDERED module list (later wins). */
|
|
57
97
|
export interface EngineEntryConfig {
|
|
58
98
|
/**
|
|
59
99
|
* Domain dispatch key → the modules composing it, spread-merged IN ORDER
|
|
60
100
|
* (later overrides earlier on a name collision — the `identity` union pattern).
|
|
101
|
+
* Values may be thunks (lazy boot — see the module header).
|
|
61
102
|
*/
|
|
62
|
-
readonly domains: Record<string, readonly
|
|
103
|
+
readonly domains: Record<string, readonly DomainModuleSource[]>;
|
|
63
104
|
/** Optional report registry: `reportId` → a factory `(actor) => Report`. */
|
|
64
105
|
readonly reports?: Record<string, (actor: string) => EngineReport>;
|
|
106
|
+
/** REQUIRED when any domain module is a thunk; ignored otherwise. */
|
|
107
|
+
readonly routing?: EngineRouting;
|
|
108
|
+
/**
|
|
109
|
+
* AMBIENT-SLOT SEEDS (lazy boot): values imported at the entry's TOP LEVEL purely
|
|
110
|
+
* so their modules' init runs PRE-FREEZE (zod v4 writes
|
|
111
|
+
* `globalThis.__zod_global{Config,Registry}` at module init; the frozen sandbox
|
|
112
|
+
* refuses new globals at lazy-init time). The values are never read — passing
|
|
113
|
+
* them as call arguments is what keeps the imports live under a library's
|
|
114
|
+
* `sideEffects: false` (a bare side-effect import would be tree-shaken away).
|
|
115
|
+
*/
|
|
116
|
+
readonly seeds?: readonly unknown[];
|
|
65
117
|
}
|
|
66
118
|
|
|
67
119
|
interface RegistryEntry {
|
|
@@ -148,6 +200,126 @@ function mergeModules(mods: readonly DomainModuleExports[]): DomainModuleExports
|
|
|
148
200
|
return merged;
|
|
149
201
|
}
|
|
150
202
|
|
|
203
|
+
/**
|
|
204
|
+
* ONE DOMAIN's registries — everything its merged module exports register. Built by
|
|
205
|
+
* the ONE `buildDomainSlice` whether the boot is eager (all domains at top level)
|
|
206
|
+
* or lazy (the touched domain inside the dispatch) — factored, never forked.
|
|
207
|
+
*/
|
|
208
|
+
interface DomainSlice {
|
|
209
|
+
/** directiveId → {directive, agg}. */
|
|
210
|
+
registry: Map<string, RegistryEntry>;
|
|
211
|
+
/** aggregate type → its DerivedDecls (module export order). */
|
|
212
|
+
deriveds: Map<string, DerivedDecl[]>;
|
|
213
|
+
/** aggregate type → its CombinedDecls (module export order). */
|
|
214
|
+
combineds: Map<string, CombinedDecl[]>;
|
|
215
|
+
/** relation id → invariant body (off the registered directives' declaredRelations). */
|
|
216
|
+
invariants: Map<string, InvariantBody>;
|
|
217
|
+
/** aggregate type → aggregate-invariant body. */
|
|
218
|
+
aggInvariants: Map<string, AggregateInvariantFn>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Build one domain's slice from its merged module exports (the original scans). */
|
|
222
|
+
function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
|
|
223
|
+
// ── directiveId → {directive, agg} ────────────────────────────────────────────
|
|
224
|
+
const registry = new Map<string, RegistryEntry>();
|
|
225
|
+
const aggs = aggregatesOf(mod);
|
|
226
|
+
const dirs = directivesOf(mod);
|
|
227
|
+
for (const [dirId, directive] of dirs) {
|
|
228
|
+
const agg = aggs.get(directive.aggregateId);
|
|
229
|
+
if (agg === undefined) {
|
|
230
|
+
// A directive targeting an aggregate not exported by its module is an
|
|
231
|
+
// authoring bug; surface it lazily (only if that directive is invoked).
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
registry.set(dirId, { directive, agg });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── aggregate type → derived / combined decls (fn bodies ship HERE, never the ledger) ──
|
|
238
|
+
const deriveds = new Map<string, DerivedDecl[]>();
|
|
239
|
+
const combineds = new Map<string, CombinedDecl[]>();
|
|
240
|
+
for (const d of derivedsOf(mod)) {
|
|
241
|
+
const list = deriveds.get(d.of) ?? [];
|
|
242
|
+
list.push(d);
|
|
243
|
+
deriveds.set(d.of, list);
|
|
244
|
+
}
|
|
245
|
+
for (const c of combinedsOf(mod)) {
|
|
246
|
+
const list = combineds.get(c.of) ?? [];
|
|
247
|
+
list.push(c);
|
|
248
|
+
combineds.set(c.of, list);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── relation id → cross-workspace invariant body (off the directives' declaredRelations) ──
|
|
252
|
+
const invariants = new Map<string, InvariantBody>();
|
|
253
|
+
for (const { directive } of registry.values()) {
|
|
254
|
+
const relations = (directive as { declaredRelations?: unknown }).declaredRelations;
|
|
255
|
+
if (!Array.isArray(relations)) continue;
|
|
256
|
+
for (const rel of relations) {
|
|
257
|
+
const r = rel as { id?: unknown; hasInvariant?: unknown; invariant?: unknown };
|
|
258
|
+
if (typeof r.id !== "string" || r.hasInvariant !== true) continue;
|
|
259
|
+
if (typeof r.invariant !== "function") {
|
|
260
|
+
// A relation declaring `hasInvariant` MUST ship an executable body — fail-closed.
|
|
261
|
+
throw new Error(
|
|
262
|
+
`engine bundle: relation "${r.id}" declares hasInvariant but ships no executable ` +
|
|
263
|
+
`invariant body — the gate would have nothing to evaluate (cross_workspace.md §2.1).`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
invariants.set(r.id, r.invariant as InvariantBody);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── aggregate type → aggregate-invariant body — from the SAME exports (no second list) ──
|
|
271
|
+
const aggInvariants = new Map<string, AggregateInvariantFn>();
|
|
272
|
+
for (const v of Object.values(mod)) {
|
|
273
|
+
if (
|
|
274
|
+
v &&
|
|
275
|
+
typeof v === "object" &&
|
|
276
|
+
(v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true &&
|
|
277
|
+
(v as { hasInvariant?: boolean }).hasInvariant === true
|
|
278
|
+
) {
|
|
279
|
+
const h = v as AggregateHandle & { invariant?: AggregateInvariantFn };
|
|
280
|
+
if (typeof h.invariant !== "function") {
|
|
281
|
+
// A handle declaring `hasInvariant` MUST ship an executable body — fail-closed.
|
|
282
|
+
throw new Error(
|
|
283
|
+
`engine bundle: aggregate "${h.id}" declares hasInvariant but ships no executable ` +
|
|
284
|
+
`invariant body — the gate would have nothing to evaluate (#250).`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
aggInvariants.set(h.id, h.invariant);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { registry, deriveds, combineds, invariants, aggInvariants };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* COMPILE-TIME companion (build lane; also bundle-safe): run the SAME scans the live
|
|
296
|
+
* registries run, over the SAME merged modules, and return the routing table a lazy
|
|
297
|
+
* entry needs. `nomos-compile` calls this with every domain's merged exports and
|
|
298
|
+
* embeds the result as `config.routing` — so the table and the registries can never
|
|
299
|
+
* disagree (one machinery). Throws the same fail-closed errors `buildDomainSlice`
|
|
300
|
+
* throws (a law whose invariant declarations are inconsistent refuses to COMPILE).
|
|
301
|
+
*/
|
|
302
|
+
export function collectEngineRouting(
|
|
303
|
+
domains: Record<string, readonly DomainModuleExports[]>,
|
|
304
|
+
): EngineRouting {
|
|
305
|
+
const derivedOf: Record<string, string[]> = {};
|
|
306
|
+
const combinedOf: Record<string, string[]> = {};
|
|
307
|
+
const aggregateInvariantOf: Record<string, string[]> = {};
|
|
308
|
+
const relationOf: Record<string, string[]> = {};
|
|
309
|
+
const add = (map: Record<string, string[]>, key: string, domainName: string) => {
|
|
310
|
+
const list = map[key] ?? (map[key] = []);
|
|
311
|
+
if (!list.includes(domainName)) list.push(domainName);
|
|
312
|
+
};
|
|
313
|
+
for (const [domainName, mods] of Object.entries(domains)) {
|
|
314
|
+
const slice = buildDomainSlice(mergeModules(mods));
|
|
315
|
+
for (const type of slice.deriveds.keys()) add(derivedOf, type, domainName);
|
|
316
|
+
for (const type of slice.combineds.keys()) add(combinedOf, type, domainName);
|
|
317
|
+
for (const type of slice.aggInvariants.keys()) add(aggregateInvariantOf, type, domainName);
|
|
318
|
+
for (const relationId of slice.invariants.keys()) add(relationOf, relationId, domainName);
|
|
319
|
+
}
|
|
320
|
+
return { derivedOf, combinedOf, aggregateInvariantOf, relationOf };
|
|
321
|
+
}
|
|
322
|
+
|
|
151
323
|
/**
|
|
152
324
|
* Build a DSL `ctx` (Ports) from the host-injected `__ports` scalars. `clock()`
|
|
153
325
|
* synthesises a `WireHlc` from the scalar (the engine leg DISCARDS the produced
|
|
@@ -180,89 +352,51 @@ export interface RegisteredEngine {
|
|
|
180
352
|
}
|
|
181
353
|
|
|
182
354
|
/**
|
|
183
|
-
* Build
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
355
|
+
* Build the registries from the one `domains` map (eagerly for plain module objects,
|
|
356
|
+
* on first dispatch touch for thunks), wire the five dispatch paths, and assign
|
|
357
|
+
* `globalThis.plan` + `globalThis.planReport` (the lump is eval'd as a classic
|
|
358
|
+
* script and `globalThis` is FROZEN before dispatch — the ASSIGNMENT must happen at
|
|
359
|
+
* top-level eval, which calling this at module top level does; the lazy forcing
|
|
360
|
+
* happens inside the dispatch call and writes no globals).
|
|
187
361
|
*/
|
|
188
362
|
export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
363
|
+
const domainNames = Object.keys(config.domains);
|
|
364
|
+
const lazy = Object.values(config.domains).some((mods) =>
|
|
365
|
+
mods.some((m) => typeof m === "function"),
|
|
366
|
+
);
|
|
367
|
+
// FAIL-CLOSED: a thunked (lazy) entry cannot resolve type/relation-keyed
|
|
368
|
+
// dispatches without the compiler-emitted routing table. Refuse at top level —
|
|
369
|
+
// at lump-build/install time, never silently at dispatch.
|
|
370
|
+
if (lazy && config.routing === undefined) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
"engine bundle: lazy domain thunks require the compiler-emitted routing table " +
|
|
373
|
+
"(registerEngine config.routing) — recompile with nomos-compile.",
|
|
374
|
+
);
|
|
192
375
|
}
|
|
376
|
+
const routing: EngineRouting = config.routing ?? {};
|
|
193
377
|
|
|
194
|
-
// ──
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
REGISTRY.set(`${domainName}\u0000${dirId}`, { directive, agg });
|
|
207
|
-
}
|
|
378
|
+
// ── domain key → its slice (forced once per sandbox; eager boot forces ALL now) ──
|
|
379
|
+
const sliceCache = new Map<string, DomainSlice>();
|
|
380
|
+
function sliceOf(domainName: string): DomainSlice | undefined {
|
|
381
|
+
const cached = sliceCache.get(domainName);
|
|
382
|
+
if (cached !== undefined) return cached;
|
|
383
|
+
const sources = config.domains[domainName];
|
|
384
|
+
if (sources === undefined) return undefined;
|
|
385
|
+
const forced = sources.map((m) => (typeof m === "function" ? m() : m));
|
|
386
|
+
const slice = buildDomainSlice(mergeModules(forced));
|
|
387
|
+
sliceCache.set(domainName, slice);
|
|
388
|
+
return slice;
|
|
208
389
|
}
|
|
390
|
+
if (!lazy) for (const domainName of domainNames) sliceOf(domainName);
|
|
209
391
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
for (const c of combinedsOf(mod)) {
|
|
220
|
-
const list = COMBINED_REGISTRY.get(c.of) ?? [];
|
|
221
|
-
list.push(c);
|
|
222
|
-
COMBINED_REGISTRY.set(c.of, list);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ── relation id → cross-workspace invariant body (off the directives' declaredRelations) ──
|
|
227
|
-
const INVARIANT_REGISTRY = new Map<string, InvariantBody>();
|
|
228
|
-
for (const { directive } of REGISTRY.values()) {
|
|
229
|
-
const relations = (directive as { declaredRelations?: unknown }).declaredRelations;
|
|
230
|
-
if (!Array.isArray(relations)) continue;
|
|
231
|
-
for (const rel of relations) {
|
|
232
|
-
const r = rel as { id?: unknown; hasInvariant?: unknown; invariant?: unknown };
|
|
233
|
-
if (typeof r.id !== "string" || r.hasInvariant !== true) continue;
|
|
234
|
-
if (typeof r.invariant !== "function") {
|
|
235
|
-
// A relation declaring `hasInvariant` MUST ship an executable body — fail-closed.
|
|
236
|
-
throw new Error(
|
|
237
|
-
`engine bundle: relation "${r.id}" declares hasInvariant but ships no executable ` +
|
|
238
|
-
`invariant body — the gate would have nothing to evaluate (cross_workspace.md §2.1).`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
INVARIANT_REGISTRY.set(r.id, r.invariant as InvariantBody);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ── aggregate type → aggregate-invariant body — from the SAME map (no second list) ──
|
|
246
|
-
const AGG_INVARIANT_REGISTRY = new Map<string, AggregateInvariantFn>();
|
|
247
|
-
for (const mod of mergedByDomain.values()) {
|
|
248
|
-
for (const v of Object.values(mod)) {
|
|
249
|
-
if (
|
|
250
|
-
v &&
|
|
251
|
-
typeof v === "object" &&
|
|
252
|
-
(v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true &&
|
|
253
|
-
(v as { hasInvariant?: boolean }).hasInvariant === true
|
|
254
|
-
) {
|
|
255
|
-
const h = v as AggregateHandle & { invariant?: AggregateInvariantFn };
|
|
256
|
-
if (typeof h.invariant !== "function") {
|
|
257
|
-
// A handle declaring `hasInvariant` MUST ship an executable body — fail-closed.
|
|
258
|
-
throw new Error(
|
|
259
|
-
`engine bundle: aggregate "${h.id}" declares hasInvariant but ships no executable ` +
|
|
260
|
-
`invariant body — the gate would have nothing to evaluate (#250).`,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
AGG_INVARIANT_REGISTRY.set(h.id, h.invariant);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
392
|
+
/**
|
|
393
|
+
* The domain keys a type/relation-keyed dispatch consults, IN DECLARATION ORDER
|
|
394
|
+
* (the fold order the eager registries had): the routed list when a routing map
|
|
395
|
+
* is present (lazy — force only the declaring domains), else every domain (eager).
|
|
396
|
+
*/
|
|
397
|
+
function domainsFor(map: Record<string, readonly string[]> | undefined, key: string): readonly string[] {
|
|
398
|
+
if (map !== undefined) return map[key] ?? [];
|
|
399
|
+
return domainNames;
|
|
266
400
|
}
|
|
267
401
|
|
|
268
402
|
const REPORTS: Record<string, (actor: string) => EngineReport> = config.reports ?? {};
|
|
@@ -278,7 +412,12 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
278
412
|
`engine invariant: job.intent.invariant must carry {relation}; got ${JSON.stringify(job.intent)}`,
|
|
279
413
|
);
|
|
280
414
|
}
|
|
281
|
-
|
|
415
|
+
// Later domain wins (the Map.set fold order of the one flat registry).
|
|
416
|
+
let body: InvariantBody | undefined;
|
|
417
|
+
for (const domainName of domainsFor(routing.relationOf, relationId)) {
|
|
418
|
+
const found = sliceOf(domainName)?.invariants.get(relationId);
|
|
419
|
+
if (found !== undefined) body = found;
|
|
420
|
+
}
|
|
282
421
|
if (body === undefined) {
|
|
283
422
|
throw new Error(`engine invariant: no invariant registered for relation "${relationId}"`);
|
|
284
423
|
}
|
|
@@ -303,7 +442,12 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
303
442
|
`engine aggregateInvariant: job.intent.aggregateInvariant must carry {of}; got ${JSON.stringify(job.intent)}`,
|
|
304
443
|
);
|
|
305
444
|
}
|
|
306
|
-
|
|
445
|
+
// Later domain wins (the Map.set fold order of the one flat registry).
|
|
446
|
+
let body: AggregateInvariantFn | undefined;
|
|
447
|
+
for (const domainName of domainsFor(routing.aggregateInvariantOf, aggregateType)) {
|
|
448
|
+
const found = sliceOf(domainName)?.aggInvariants.get(aggregateType);
|
|
449
|
+
if (found !== undefined) body = found;
|
|
450
|
+
}
|
|
307
451
|
// VACUOUS HOLDS: a type with NO declared invariant trivially holds — `{accept:true}`,
|
|
308
452
|
// NOT a throw (the oracle fails CLOSED on a throw, wrongly rejecting every create).
|
|
309
453
|
if (body === undefined) {
|
|
@@ -324,7 +468,11 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
324
468
|
`engine derive: job.intent.derive must carry {of}; got ${JSON.stringify(job.intent)}`,
|
|
325
469
|
);
|
|
326
470
|
}
|
|
327
|
-
|
|
471
|
+
// Domain declaration order, then module export order — the eager fold order.
|
|
472
|
+
const fields: DerivedDecl[] = [];
|
|
473
|
+
for (const domainName of domainsFor(routing.derivedOf, ofType)) {
|
|
474
|
+
fields.push(...(sliceOf(domainName)?.deriveds.get(ofType) ?? []));
|
|
475
|
+
}
|
|
328
476
|
const prior = (job.priorState ?? {}) as Record<string, unknown>;
|
|
329
477
|
const out: Record<string, unknown> = {};
|
|
330
478
|
for (const d of fields) {
|
|
@@ -345,7 +493,11 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
345
493
|
`engine combine: job.intent.combine must carry {of}; got ${JSON.stringify(job.intent)}`,
|
|
346
494
|
);
|
|
347
495
|
}
|
|
348
|
-
|
|
496
|
+
// Domain declaration order, then module export order — the eager fold order.
|
|
497
|
+
const fields: CombinedDecl[] = [];
|
|
498
|
+
for (const domainName of domainsFor(routing.combinedOf, ofType)) {
|
|
499
|
+
fields.push(...(sliceOf(domainName)?.combineds.get(ofType) ?? []));
|
|
500
|
+
}
|
|
349
501
|
const owner = (job.priorState ?? {}) as Record<string, unknown>;
|
|
350
502
|
// Preferred: related rows keyed by combined field id (iteration-order independent);
|
|
351
503
|
// the array path remains for older callers (declaration order).
|
|
@@ -429,7 +581,8 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
429
581
|
`engine plan: job.intent must carry {domain, directiveId}; got ${JSON.stringify(intent)}`,
|
|
430
582
|
);
|
|
431
583
|
}
|
|
432
|
-
|
|
584
|
+
// The plan dispatch IS domain-keyed: force only the dispatched domain.
|
|
585
|
+
const entry = sliceOf(domain)?.registry.get(directiveId);
|
|
433
586
|
if (entry === undefined) {
|
|
434
587
|
throw new Error(`engine plan: no directive registered for (${domain}, ${directiveId})`);
|
|
435
588
|
}
|