@githolon/dsl 0.2.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -19,6 +19,8 @@
19
19
  "./manifest": "./src/manifest.ts",
20
20
  "./compose": "./src/compose.ts",
21
21
  "./usd": "./src/usd.ts",
22
+ "./usd-layers": "./src/usd_layers.ts",
23
+ "./usd-state": "./src/usd_state.ts",
22
24
  "./engine-entry": "./src/engine_entry.ts",
23
25
  "./build-package": "./src/build_package.ts",
24
26
  "./compile-engine": "./src/compile_engine.ts",
@@ -64,15 +64,25 @@ import type { DerivedDecl } from "./derived.js";
64
64
  import type { CombinedDecl } from "./combined.js";
65
65
  import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
66
66
  import {
67
+ aggregatesOf,
67
68
  buildIdentity,
68
69
  buildReadManifest,
69
70
  composeDomainModule,
71
+ directivesOf,
70
72
  emitUsdJsonForModules,
71
73
  packageUsda,
72
74
  sha256HexUtf8,
73
75
  writeIdentity,
74
76
  type Mod,
75
77
  } from "./build_package.js";
78
+ import { collectEngineRouting } from "./engine_entry.js";
79
+ import {
80
+ emitLayeredDomain,
81
+ flattenLayeredUsd,
82
+ layeredRootUsda,
83
+ type LayeredModuleInput,
84
+ } from "./usd_layers.js";
85
+ import type { UsdLayer } from "./usd.js";
76
86
  import { generateTsClient, tsClientFactoryName } from "./codegen_ts.js";
77
87
  import { generateTsProof } from "./codegen_proof.js";
78
88
 
@@ -111,6 +121,17 @@ interface PackageConfig {
111
121
  * can vendor the directory as-is, no pub deps.
112
122
  */
113
123
  readonly dart?: boolean | { readonly out?: string };
124
+ /**
125
+ * OPT-IN layered USD emission (also `--layered` on the CLI). When true, ALSO emit
126
+ * `build/layers/<domain>--<module>.usda` (+ `.layer.json`) — one REAL USD layer per
127
+ * authored MODULE — and `build/<name>.layered.usda`, a root whose subLayers stack
128
+ * them (USD strongest-first == Nomos module order reversed). The flattened
129
+ * `.package.usda` emission and the domainHash DO NOT MOVE; the compile asserts
130
+ * fail-closed that the layer stack flattens BYTE-IDENTICALLY to the canonical
131
+ * composed IR (the monoid homomorphism — see `usd_layers.ts` /
132
+ * `docs/usd_layered_composition.md`).
133
+ */
134
+ readonly layered?: boolean;
114
135
  }
115
136
 
116
137
  function fail(msg: string): never {
@@ -283,7 +304,9 @@ function emitDart(modules: readonly DomainModule[], dartOut: string): string[] {
283
304
  // ── main ──────────────────────────────────────────────────────────────────────────
284
305
 
285
306
  async function main(): Promise<void> {
286
- const configArg = process.argv[2] ?? "nomos.package.mjs";
307
+ const argv = process.argv.slice(2);
308
+ const layeredFlag = argv.includes("--layered");
309
+ const configArg = argv.find((a) => !a.startsWith("--")) ?? "nomos.package.mjs";
287
310
  const configPath = path.resolve(process.cwd(), configArg);
288
311
  if (!existsSync(configPath)) fail(`config not found: ${configPath}`);
289
312
  const cfgDir = path.dirname(configPath);
@@ -304,9 +327,21 @@ async function main(): Promise<void> {
304
327
  mkdirSync(outDir, { recursive: true });
305
328
 
306
329
  // ── import + compose each domain ──
330
+ // LAZY PER-DOMAIN BOOT (#34): the generated entry registers each domain's modules
331
+ // as `() => require("…")` THUNKS — esbuild lazy-wraps every required module
332
+ // (`__esm` init at the require call, not at bundle top level), so a fresh
333
+ // per-plan sandbox boots ONLY the domain the dispatched intent touches. The
334
+ // routing table for type/relation-keyed dispatches is computed below by the
335
+ // SAME scans the engine registries run (`collectEngineRouting` — one machinery).
336
+ // `NOMOS_LUMP_BOOT=eager` keeps the pre-#34 whole-law top-level boot (static
337
+ // imports, no routing) — the equivalence baseline the #34 suite compares against.
338
+ const eagerLump = process.env.NOMOS_LUMP_BOOT === "eager";
307
339
  const entryImports: string[] = [];
308
340
  const entryDomains: string[] = [];
341
+ const routingInput: Record<string, Mod[]> = {};
309
342
  const domainModules: DomainModule[] = [];
343
+ const layered = layeredFlag || cfg.layered === true;
344
+ const layeredDomains: { key: string; inputs: LayeredModuleInput[] }[] = [];
310
345
  let importSeq = 0;
311
346
 
312
347
  for (const d of cfg.domains) {
@@ -314,17 +349,22 @@ async function main(): Promise<void> {
314
349
  fail(`each domain needs { key, modules: [path, ...] }; got ${JSON.stringify(d)}`);
315
350
  }
316
351
  const mods: Mod[] = [];
317
- const varNames: string[] = [];
352
+ const sourceExprs: string[] = [];
318
353
  for (const rel of d.modules) {
319
354
  const abs = path.resolve(cfgDir, rel);
320
355
  if (!existsSync(abs)) fail(`domain '${d.key}': module not found: ${abs}`);
321
356
  const m = (await import(pathToFileURL(abs).href)) as Mod;
322
357
  mods.push(m);
323
- const v = `m${importSeq++}`;
324
- varNames.push(v);
325
- entryImports.push(`import * as ${v} from ${JSON.stringify(abs)};`);
358
+ if (eagerLump) {
359
+ const v = `m${importSeq++}`;
360
+ sourceExprs.push(v);
361
+ entryImports.push(`import * as ${v} from ${JSON.stringify(abs)};`);
362
+ } else {
363
+ sourceExprs.push(`() => require(${JSON.stringify(abs)})`);
364
+ }
326
365
  }
327
- entryDomains.push(` ${JSON.stringify(d.key)}: [${varNames.join(", ")}],`);
366
+ entryDomains.push(` ${JSON.stringify(d.key)}: [${sourceExprs.join(", ")}],`);
367
+ routingInput[d.key] = mods;
328
368
 
329
369
  let merged: Mod = {};
330
370
  for (const m of mods) merged = { ...merged, ...m };
@@ -375,6 +415,51 @@ async function main(): Promise<void> {
375
415
  ...(spatials.length > 0 ? { spatials } : {}),
376
416
  }),
377
417
  );
418
+
419
+ // ── opt-in: ONE USD LAYER PER MODULE (Φ applied per module, not once over the
420
+ // composed fold) — the layered emission `usd_layers.ts` proves flattens back
421
+ // to the EXACT canonical IR (asserted below, fail-closed). Per-module decls
422
+ // are discovered the same way as above, on each module's OWN exports; config
423
+ // export-names resolve on whichever module actually exports them; config
424
+ // `extraAggregates` ride the FIRST (weakest) layer, prelude-style. Counts /
425
+ // sums / spatials never enter the USD IR, so they have no layer placement.
426
+ if (layered) {
427
+ const usedStems = new Set<string>();
428
+ const inputs: LayeredModuleInput[] = mods.map((m, i) => {
429
+ let stem = path.basename(d.modules[i]!).replace(/\.[^.]*$/, "");
430
+ for (let n = 2; usedStems.has(stem); n++) stem = `${stem}-${n}`;
431
+ usedStems.add(stem);
432
+
433
+ const presentNames = (names: readonly string[] | undefined) =>
434
+ (names ?? []).filter((n) => (m as Record<string, unknown>)[n] !== undefined);
435
+ const layerQueries = [
436
+ ...discover<QueryDecl>(m, isQueryDecl),
437
+ ...resolveNamed<QueryDecl>(m, presentNames(d.queries), "queries"),
438
+ ];
439
+ const layerDeriveds = [
440
+ ...discover<DerivedDecl>(m, isDerivedDecl),
441
+ ...resolveNamed<DerivedDecl>(m, presentNames(d.deriveds), "deriveds"),
442
+ ];
443
+ const layerCombineds = [
444
+ ...discover<CombinedDecl>(m, isCombinedDecl),
445
+ ...resolveNamed<CombinedDecl>(m, presentNames(d.combineds), "combineds"),
446
+ ];
447
+ const byId = new Map<string, AggregateHandle>();
448
+ for (const a of [...aggregatesOf(m), ...(i === 0 ? extraAggregates : [])]) byId.set(a.id, a);
449
+
450
+ const module: DomainModule = {
451
+ name: d.name ?? d.key,
452
+ domain: d.key,
453
+ aggregates: [...byId.values()],
454
+ directives: directivesOf(m),
455
+ ...(layerQueries.length > 0 ? { queries: layerQueries } : {}),
456
+ ...(layerDeriveds.length > 0 ? { deriveds: layerDeriveds } : {}),
457
+ ...(layerCombineds.length > 0 ? { combineds: layerCombineds } : {}),
458
+ };
459
+ return { name: stem, module };
460
+ });
461
+ layeredDomains.push({ key: d.key, inputs });
462
+ }
378
463
  }
379
464
 
380
465
  // ── reports ──
@@ -384,14 +469,76 @@ async function main(): Promise<void> {
384
469
  if (!rel || !exportName) fail(`report '${reportId}' must be "modulePath#exportName"; got '${ref}'`);
385
470
  const abs = path.resolve(cfgDir, rel);
386
471
  if (!existsSync(abs)) fail(`report '${reportId}': module not found: ${abs}`);
387
- const v = `r${importSeq++}`;
388
- entryImports.push(`import * as ${v} from ${JSON.stringify(abs)};`);
389
- entryReports.push(` ${JSON.stringify(reportId)}: ${v}.${exportName},`);
472
+ if (eagerLump) {
473
+ const v = `r${importSeq++}`;
474
+ entryImports.push(`import * as ${v} from ${JSON.stringify(abs)};`);
475
+ entryReports.push(` ${JSON.stringify(reportId)}: ${v}.${exportName},`);
476
+ } else {
477
+ // A lazy report factory: the module's `__esm` init runs on first render call.
478
+ entryReports.push(
479
+ ` ${JSON.stringify(reportId)}: (actor: string) => (require(${JSON.stringify(abs)}) as any).${exportName}(actor),`,
480
+ );
481
+ }
390
482
  }
391
483
 
392
484
  // ── 1. generate + bundle the engine entry ──
485
+ // The routing table is computed by the SAME duck-type scans the engine registries
486
+ // run, over the SAME imported modules (collectEngineRouting — one machinery, so a
487
+ // law whose invariant declarations are inconsistent refuses to COMPILE, and the
488
+ // table can never disagree with what a full force would register).
489
+ const routing = collectEngineRouting(routingInput);
490
+ // AMBIENT-SLOT SEEDING (lazy boot only): zod v4 writes
491
+ // `globalThis.__zod_global{Config,Registry}` at MODULE INIT. The sealed sandbox
492
+ // freezes globalThis after the lump's top level — an eager lump's zod init ran
493
+ // pre-freeze, but a lazy lump would init zod inside the dispatch, post-freeze,
494
+ // and the frozen global refuses the new slots. So the entry eagerly imports the
495
+ // TWO tiny zod core modules that own those writes (resolved to the same files
496
+ // the domain modules' `zod` import reaches — same esbuild module instances);
497
+ // the heavy classic schema graph stays deferred. The frozen-sandbox boot probe
498
+ // below fail-closes the compile if any OTHER module writes globals at lazy init.
499
+ // Namespace imports + a `seeds` call argument (NOT bare side-effect imports):
500
+ // zod ships `sideEffects: false`, so esbuild would tree-shake a bare import away;
501
+ // passing the namespaces into the registerEngine CALL forces their init at the
502
+ // entry's top level — pre-freeze, exactly where the eager lump ran them.
503
+ const zodSeedImports: string[] = [];
504
+ const zodSeedNames: string[] = [];
505
+ if (!eagerLump) {
506
+ const zodPkgJson = [cfgDir, DSL_DIR]
507
+ .map((dir) => {
508
+ try {
509
+ return createRequire(pathToFileURL(path.join(dir, "noop.js"))).resolve("zod/package.json");
510
+ } catch {
511
+ return undefined;
512
+ }
513
+ })
514
+ .find((p): p is string => p !== undefined);
515
+ const zodCoreFiles = zodPkgJson
516
+ ? ["core.js", "registries.js"].map((f) => path.join(path.dirname(zodPkgJson), "v4", "core", f))
517
+ : [];
518
+ const seedFiles =
519
+ zodCoreFiles.length > 0 && zodCoreFiles.every((f) => existsSync(f))
520
+ ? zodCoreFiles
521
+ : zodPkgJson !== undefined
522
+ ? // Unknown zod layout — seed by initializing the whole library at top
523
+ // level (loses some laziness, never correctness).
524
+ ["zod"]
525
+ : [];
526
+ for (const f of seedFiles) {
527
+ const v = `__seed${zodSeedNames.length}`;
528
+ zodSeedImports.push(`import * as ${v} from ${JSON.stringify(f)};`);
529
+ zodSeedNames.push(v);
530
+ }
531
+ }
393
532
  const entrySource = [
394
533
  `// AUTO-GENERATED by nomos-compile from ${path.basename(configPath)} — do not edit.`,
534
+ ...(eagerLump
535
+ ? []
536
+ : [
537
+ `// LAZY PER-DOMAIN BOOT (#34): each domain registers as require-thunks; esbuild`,
538
+ `// defers every module's init to its first dispatch touch inside the fresh sandbox.`,
539
+ `declare function require(id: string): Record<string, unknown>;`,
540
+ ...zodSeedImports,
541
+ ]),
395
542
  ...entryImports,
396
543
  `import { registerEngine } from "@githolon/dsl/engine-entry";`,
397
544
  ``,
@@ -400,6 +547,8 @@ async function main(): Promise<void> {
400
547
  ...entryDomains,
401
548
  ` },`,
402
549
  ...(entryReports.length > 0 ? [` reports: {`, ...entryReports, ` },`] : []),
550
+ ...(eagerLump ? [] : [` routing: ${JSON.stringify(routing)},`]),
551
+ ...(zodSeedNames.length > 0 ? [` seeds: [${zodSeedNames.join(", ")}],`] : []),
403
552
  `});`,
404
553
  ``,
405
554
  ].join("\n");
@@ -436,6 +585,57 @@ async function main(): Promise<void> {
436
585
  fail(`bundled engine entry does not assign globalThis.plan — refusing to package`);
437
586
  }
438
587
 
588
+ // ── 1b. THE FROZEN-SANDBOX BOOT PROBE (lazy boot only, fail-closed) ──
589
+ // The sealed engine freezes globalThis after the lump's top level; a lazy
590
+ // domain's module init runs LATER, inside the dispatch. Any module that writes
591
+ // an ambient global at init (the zod slots are pre-seeded above; this guards
592
+ // everything else) would deterministically throw at FIRST DISPATCH in
593
+ // production. Reproduce the sandbox order here — eval the real bundle in a
594
+ // fresh frozen context, then force every domain — and refuse to package on any
595
+ // failure that is not the expected unknown-directive refusal.
596
+ if (!eagerLump) {
597
+ // A CHILD process: the probe must freeze a REAL global object (a `vm` context
598
+ // global refuses Object.freeze), and the lump must run STRICT (the engine
599
+ // evals it with the STRICT flag — sloppy mode would silently no-op the very
600
+ // frozen-global writes the probe exists to catch).
601
+ const lumpPath = path.join(outDir, ".nomos_lump.js");
602
+ writeFileSync(lumpPath, javascript, "utf8");
603
+ const probePath = path.join(outDir, ".nomos_boot_probe.cjs");
604
+ writeFileSync(
605
+ probePath,
606
+ [
607
+ `// AUTO-GENERATED by nomos-compile — the frozen-sandbox lazy-boot probe.`,
608
+ `const fs = require("node:fs");`,
609
+ `const lump = fs.readFileSync(process.argv[2], "utf8");`,
610
+ `const domains = JSON.parse(process.argv[3]);`,
611
+ `(0, eval)('"use strict";\\n' + lump); // indirect eval → real global scope, strict like the engine`,
612
+ `Object.freeze(globalThis); // the sealed sandbox freezes before dispatch`,
613
+ `for (const key of domains) {`,
614
+ ` let msg;`,
615
+ ` try { globalThis.plan({ intent: { domain: key, directiveId: "__nomos_boot_probe__" } }); msg = "unexpected-success"; }`,
616
+ ` catch (e) { msg = String((e && e.message) || e); }`,
617
+ ` if (!/no directive registered/.test(msg)) { console.error(key + "\\u0000" + msg); process.exit(3); }`,
618
+ `}`,
619
+ ``,
620
+ ].join("\n"),
621
+ "utf8",
622
+ );
623
+ try {
624
+ execFileSync(process.execPath, [probePath, lumpPath, JSON.stringify(cfg.domains.map((d) => d.key))], {
625
+ encoding: "utf8",
626
+ stdio: ["ignore", "ignore", "pipe"],
627
+ });
628
+ } catch (e) {
629
+ const stderr = String((e as { stderr?: string }).stderr ?? "").trim();
630
+ const [domainKey, msg] = stderr.includes("\u0000") ? stderr.split("\u0000") : ["?", stderr];
631
+ fail(
632
+ `domain '${domainKey}' failed its frozen-sandbox lazy-boot probe — a module ` +
633
+ `initializer touches ambient globals after the freeze (the engine would ` +
634
+ `refuse its first dispatch): ${msg}`,
635
+ );
636
+ }
637
+ }
638
+
439
639
  // ── 2. USD IR + the package envelope ──
440
640
  const usdJson = emitUsdJsonForModules(domainModules);
441
641
  const text = packageUsda(usdJson, javascript);
@@ -443,6 +643,48 @@ async function main(): Promise<void> {
443
643
  writeFileSync(pkgPath, text, "utf8");
444
644
  const domainHash = sha256HexUtf8(text);
445
645
 
646
+ // ── 2b. opt-in LAYERED emission — additive artifacts ONLY (the flattened package
647
+ // above and its domainHash are already written and do not move). The
648
+ // homomorphism is ASSERTED fail-closed on every layered compile: the
649
+ // per-module layer stack must flatten BYTE-IDENTICALLY to the canonical
650
+ // composed IR embedded in the package, or nothing layered is emitted.
651
+ let layeredRootPath: string | undefined;
652
+ let layeredFileCount = 0;
653
+ if (layered) {
654
+ const safe = (s: string) => s.replace(/[^A-Za-z0-9._-]/g, "_");
655
+ const layersDir = path.join(outDir, "layers");
656
+ mkdirSync(layersDir, { recursive: true });
657
+ const allLayers: UsdLayer[] = [];
658
+ const relPaths: string[] = [];
659
+ const pendingWrites: { path: string; text: string }[] = [];
660
+ for (const ld of layeredDomains) {
661
+ for (const emitted of emitLayeredDomain(ld.key, ld.inputs)) {
662
+ const base = `${safe(ld.key)}--${safe(emitted.name)}`;
663
+ pendingWrites.push({ path: path.join(layersDir, `${base}.usda`), text: emitted.usda });
664
+ pendingWrites.push({
665
+ path: path.join(layersDir, `${base}.layer.json`),
666
+ text: JSON.stringify(emitted.layer) + "\n",
667
+ });
668
+ allLayers.push(emitted.layer);
669
+ relPaths.push(`layers/${base}.usda`);
670
+ }
671
+ }
672
+ const flatJson = JSON.stringify(flattenLayeredUsd(allLayers));
673
+ if (flatJson !== usdJson) {
674
+ fail(
675
+ `layered emission does not flatten to the canonical composed IR — the module fold and ` +
676
+ `the layer-stack fold DISAGREE for this package (usually a re-declared directive ` +
677
+ `silently dropping an omit-when-empty reads/emits boundary, or a re-declared ` +
678
+ `aggregate dropping a field: USD shows the weaker opinion through; the module fold ` +
679
+ `does not). Refusing to emit the layered artifact (the flattened package is unaffected).`,
680
+ );
681
+ }
682
+ for (const w of pendingWrites) writeFileSync(w.path, w.text, "utf8");
683
+ layeredRootPath = path.join(outDir, `${cfg.name}.layered.usda`);
684
+ writeFileSync(layeredRootPath, layeredRootUsda(relPaths), "utf8");
685
+ layeredFileCount = relPaths.length;
686
+ }
687
+
446
688
  // ── 3. the READ manifest + IDENTITY artifacts ──
447
689
  const readManifest = buildReadManifest(domainModules);
448
690
  const readPath = path.join(outDir, "domain_manifests.json");
@@ -536,6 +778,11 @@ async function main(): Promise<void> {
536
778
  console.log(`nomos-compile: ${cfg.name}`);
537
779
  console.log(` package ${rel(pkgPath)} (${text.length} bytes)`);
538
780
  console.log(` domainHash ${domainHash}`);
781
+ if (layeredRootPath !== undefined) {
782
+ console.log(
783
+ ` layered ${rel(layeredRootPath)} (${layeredFileCount} module layer(s) in build/layers/ — flatten-verified byte-identical to the canonical IR)`,
784
+ );
785
+ }
539
786
  console.log(` read ${rel(readPath)} (${Object.keys(readManifest.aggregateFieldKinds).length} aggregate(s), ${readManifest.queries.length} quer(y/ies), ${readManifest.counts.length} count(s))`);
540
787
  console.log(` identity ${rel(manifestsPath)} (${Object.keys(identity.manifests).length} domain(s)${identity.excluded.length ? `, ${identity.excluded.length} EXCLUDED (palette gap)` : ""})`);
541
788
  for (const [dom, h] of Object.entries(identity.hashes)) console.log(` ${dom.padEnd(20)} ${h}`);