@githolon/dsl 0.45.0 → 0.47.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.
@@ -108,7 +108,7 @@ import {
108
108
  layeredRootUsda,
109
109
  type LayeredModuleInput,
110
110
  } from "./usd_layers.js";
111
- import { domainManifest } from "./manifest.js";
111
+ import { domainManifest, canonicalJson } from "./manifest.js";
112
112
  import type { WireSchema } from "./wire.js";
113
113
  import {
114
114
  deriveStableIdContinuity,
@@ -121,6 +121,7 @@ import { generateTsClient, tsClientFactoryName } from "./codegen_ts.js";
121
121
  import { generateOpenApiFragment, mergeOpenApiFragments } from "./codegen_openapi.js";
122
122
  import { domainInterface, type DomainInterface } from "./shape_compat.js";
123
123
  import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
124
+ import type { NomosCompat } from "./compat_manifest.js";
124
125
 
125
126
  const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
126
127
 
@@ -187,7 +188,19 @@ interface PackageConfig {
187
188
  * `package:nomos_client/...` import is rewritten to a relative one) — so a Flutter app
188
189
  * can vendor the directory as-is, no pub deps.
189
190
  */
190
- readonly dart?: boolean | { readonly out?: string; readonly support?: DartSupportImport };
191
+ readonly dart?:
192
+ | boolean
193
+ | {
194
+ readonly out?: string;
195
+ readonly support?: DartSupportImport;
196
+ /**
197
+ * Basename (no `.dart`) of the generated PACKAGE BARREL — the stable public facade emitted at
198
+ * `<out>/<barrel>.dart` over the `src/generated/` files. Set it to the pub package name (so
199
+ * `package:co2_nomos_domains/co2_nomos_domains.dart` resolves); defaults to the snake_cased
200
+ * config `name`.
201
+ */
202
+ readonly barrel?: string;
203
+ };
191
204
  /**
192
205
  * OPT-IN layered USD emission (also `--layered` on the CLI). When true, ALSO emit
193
206
  * `build/layers/<domain>--<module>.usda` (+ `.layer.json`) — one REAL USD layer per
@@ -372,9 +385,22 @@ const DART_SUPPORT_SRC = [
372
385
  * (a real `nomos_client` pub dep) keeps the full barrel. */
373
386
  const VENDORED_BARREL_EXCLUDES = ["src/schema_validation.dart", "src/home.dart"] as const;
374
387
 
375
- function removeVendoredDartSupport(dartOut: string): void {
388
+ /** The generated-owned subdirectory, relative to `dartOut` (the pub package's `lib/`). WIPED and
389
+ * rewritten on every compile — nothing hand-written may live in it. The stable facade (the package
390
+ * barrel) stays at `dartOut/<barrel>.dart`, so a prune/regenerate can never orphan app imports. */
391
+ const DART_GENERATED_SUBDIR = path.join("src", "generated");
392
+
393
+ /** Remove the PRE-barrel flat layout's generated files from `dartOut` root (they now live under
394
+ * `src/generated/`): the vendored support set + the per-domain files. Targeted by exact known
395
+ * name — `dartOut` is typically the tenant package's `lib/`, so never sweep directories. */
396
+ function removeLegacyFlatDartLayout(dartOut: string, moduleNames: readonly string[]): void {
376
397
  rmSync(path.join(dartOut, "nomos_client.dart"), { force: true });
377
- rmSync(path.join(dartOut, "src"), { recursive: true, force: true });
398
+ rmSync(path.join(dartOut, "nomos_framework.dart"), { force: true });
399
+ for (const f of DART_SUPPORT_SRC) rmSync(path.join(dartOut, "src", f), { force: true });
400
+ for (const name of moduleNames) {
401
+ rmSync(path.join(dartOut, `${name}.dart`), { force: true });
402
+ rmSync(path.join(dartOut, `${name}.client.dart`), { force: true });
403
+ }
378
404
  }
379
405
 
380
406
  /**
@@ -382,6 +408,10 @@ function removeVendoredDartSupport(dartOut: string): void {
382
408
  * `<name>.dart` per domain (the SAME `generateDartDomain` path the co2 tenant's
383
409
  * `emit_dart.ts` drives) + the vendored `nomos_client.dart` barrel and its `src/`
384
410
  * support files, with the generated files' `package:` import rewritten relative.
411
+ * Everything lands under `dartOut/src/generated/` (generated-owned, wiped per
412
+ * compile) behind ONE generated package barrel `dartOut/<barrelName>.dart` — the
413
+ * analyzer-clean plug-and-play entrypoint (zero `show`/`hide`; framework-shared
414
+ * symbols dedup into `nomos_framework.dart`, so no export is ambiguous).
385
415
  */
386
416
  function emitDart(
387
417
  modules: readonly DomainModule[],
@@ -389,25 +419,29 @@ function emitDart(
389
419
  domainHash: string,
390
420
  packageInterface: Record<string, DomainInterface> = {},
391
421
  supportImport: DartSupportImport = "vendored",
422
+ barrelName = "nomos_domains",
423
+ compat?: NomosCompat,
392
424
  ): string[] {
393
- mkdirSync(dartOut, { recursive: true });
425
+ const genDir = path.join(dartOut, DART_GENERATED_SUBDIR);
426
+ removeLegacyFlatDartLayout(dartOut, modules.map((m) => m.name));
427
+ rmSync(genDir, { recursive: true, force: true });
428
+ mkdirSync(genDir, { recursive: true });
394
429
 
395
430
  // 1. optional vendored framework support (barrel + src/), copied byte-for-byte except
396
431
  // the barrel's schema_validation export (see DART_SUPPORT_SRC).
397
432
  if (supportImport === "vendored") {
398
- mkdirSync(path.join(dartOut, "src"), { recursive: true });
433
+ mkdirSync(path.join(genDir, "src"), { recursive: true });
399
434
  const libDir = path.join(DSL_DIR, "dart", "lib");
400
435
  const barrel = readFileSync(path.join(libDir, "nomos_client.dart"), "utf8")
401
436
  .split("\n")
402
437
  .filter((line) => !VENDORED_BARREL_EXCLUDES.some((x) => line.includes(x)))
403
438
  .join("\n");
404
- writeFileSync(path.join(dartOut, "nomos_client.dart"), barrel, "utf8");
439
+ writeFileSync(path.join(genDir, "nomos_client.dart"), barrel, "utf8");
405
440
  for (const f of DART_SUPPORT_SRC) {
406
- copyFileSync(path.join(libDir, "src", f), path.join(dartOut, "src", f));
441
+ copyFileSync(path.join(libDir, "src", f), path.join(genDir, "src", f));
407
442
  }
408
- } else {
409
- removeVendoredDartSupport(dartOut);
410
443
  }
444
+ // (package-support mode vendors nothing — the genDir wipe above already cleared any prior set)
411
445
 
412
446
  // 2. FRAMEWORK-SHARED symbol dedup (Dart-codegen hygiene). Framework-owned symbols fold into MANY
413
447
  // domains, so their generated Dart would be DEFINED once per domain — a package barrel re-exporting
@@ -468,7 +502,7 @@ function emitDart(
468
502
  `generated Dart for '${fileBase}' does not carry the expected support import (${DART_PACKAGE_IMPORT}) — codegen_dart header drifted; fix the rewrite in compile_package_main.ts`,
469
503
  );
470
504
  }
471
- const p = path.join(dartOut, `${fileBase}.dart`);
505
+ const p = path.join(genDir, `${fileBase}.dart`);
472
506
  writeFileSync(
473
507
  p,
474
508
  supportImport === "vendored" ? src.replace(DART_PACKAGE_IMPORT, DART_RELATIVE_IMPORT) : src,
@@ -533,13 +567,39 @@ function emitDart(
533
567
  const clientSrc = generateDartBridgeClient(modForDart, {
534
568
  domainHash,
535
569
  interface: packageInterface[modForDart.domain ?? modForDart.name],
570
+ ...(compat !== undefined ? { compat } : {}),
536
571
  supportImport,
537
572
  ...(domainFrameworkImport !== undefined ? { frameworkImportUri: domainFrameworkImport } : {}),
538
573
  });
539
- const cp = path.join(dartOut, `${mod.name}.client.dart`);
574
+ const cp = path.join(genDir, `${mod.name}.client.dart`);
540
575
  writeFileSync(cp, clientSrc, "utf8");
541
576
  written.push(cp);
542
577
  }
578
+
579
+ // 3. THE PACKAGE BARREL — the stable public facade (`dartOut/<barrelName>.dart`), generated-owned
580
+ // and overwritten every compile. ONE import gives an app the support runtime + the shared
581
+ // framework surface + every domain's types and client, with ZERO `show`/`hide` (the framework
582
+ // dedup above makes the exports collision-free by construction; re-exporting the same library
583
+ // through two paths — e.g. a client's own `nomos_framework.dart` re-export — is not ambiguous).
584
+ const genRel = DART_GENERATED_SUBDIR.split(path.sep).join("/");
585
+ const barrelLines = [
586
+ "// GENERATED by nomos-compile — the package's public entrypoint. DO NOT EDIT.",
587
+ "// Regenerated on every compile; generated implementation lives under src/generated/.",
588
+ "library;",
589
+ "",
590
+ supportImport === "vendored"
591
+ ? `export '${genRel}/nomos_client.dart';`
592
+ : `export 'package:nomos_client/nomos_client.dart';`,
593
+ ...(hasFrameworkSymbols ? [`export '${genRel}/${FRAMEWORK_FILE}.dart';`] : []),
594
+ ...modules.flatMap((m) => [
595
+ `export '${genRel}/${m.name}.dart';`,
596
+ `export '${genRel}/${m.name}.client.dart';`,
597
+ ]),
598
+ "",
599
+ ];
600
+ const barrelPath = path.join(dartOut, `${barrelName}.dart`);
601
+ writeFileSync(barrelPath, barrelLines.join("\n"), "utf8");
602
+ written.push(barrelPath);
543
603
  return written;
544
604
  }
545
605
 
@@ -548,6 +608,9 @@ function emitDart(
548
608
  async function main(): Promise<void> {
549
609
  const argv = process.argv.slice(2);
550
610
  const layeredFlag = argv.includes("--layered");
611
+ // §"Shipped (1–4)" follow-up: `githolon compile --analyze` (or NOMOS_ANALYZE=1) runs
612
+ // `dart analyze` on the generated Dart package after emission and fails the compile on red.
613
+ const analyzeFlag = argv.includes("--analyze") || process.env.NOMOS_ANALYZE === "1";
551
614
  const configArg = argv.find((a) => !a.startsWith("--")) ?? "nomos.package.mjs";
552
615
  const configPath = path.resolve(process.cwd(), configArg);
553
616
  if (!existsSync(configPath)) fail(`config not found: ${configPath}`);
@@ -1480,6 +1543,34 @@ async function main(): Promise<void> {
1480
1543
  // compute the identity to surface its per-domain hash in the summary below.
1481
1544
  const identity = buildIdentity(domainModules);
1482
1545
 
1546
+ // ── 3a. the CLIENT/LAW COMPATIBILITY MANIFEST (§5, architecture/plug_and_play_dx.md).
1547
+ // A DIAGNOSIS surface, NEVER a gate: the shape-compat arc (#72) established that clients
1548
+ // fit workspaces by STRUCTURAL INTERFACE (stable-id subtyping), not by law-hash pin. This
1549
+ // block is baked into both generated clients (TS + Dart) and mirrored into the deploy JSON
1550
+ // so a connect() can classify silent / shape-compat / incompatible. FRONTEND-ONLY: it is
1551
+ // emitted like the dart codegen hints — it does NOT enter the USD IR / package bytes /
1552
+ // domainHash (the package `text` above is already hashed and does not move).
1553
+ const dslVersion = ((): string => {
1554
+ try {
1555
+ return JSON.parse(readFileSync(path.join(DSL_DIR, "package.json"), "utf8")).version ?? "unknown";
1556
+ } catch {
1557
+ return "unknown";
1558
+ }
1559
+ })();
1560
+ const compatDomains: NomosCompat["domains"] = {};
1561
+ for (const m of domainModules) {
1562
+ const key = m.domain ?? m.name;
1563
+ const identityHash = identity.hashes[key];
1564
+ compatDomains[key] = identityHash === undefined ? {} : { identityHash };
1565
+ }
1566
+ const nomosCompat: NomosCompat = {
1567
+ packageName: cfg.name,
1568
+ lawHash: domainHash,
1569
+ interfaceHash: sha256HexUtf8(canonicalJson(packageInterface)),
1570
+ domains: compatDomains,
1571
+ generator: { dsl: dslVersion, schema: 1 },
1572
+ };
1573
+
1483
1574
  // ── 3b. the OpenAPI spec — THE PACKAGE'S SELF-DESCRIPTION (the host's runtime routing table;
1484
1575
  // architecture/githolon_host_contract.md "The Self-Describing API"). Each domain emits its fragment
1485
1576
  // from its OWN zod (z.toJSONSchema); the package's spec is the law-blind merge. Deploy STAGES it like
@@ -1524,6 +1615,9 @@ async function main(): Promise<void> {
1524
1615
  // THE SELF-DESCRIBING API: this package's OpenAPI spec (law-derived). The worker stages it on the
1525
1616
  // manifest overlay; a read serves the workspace's merged spec; the host builds its router from it.
1526
1617
  openapi,
1618
+ // THE CLIENT/LAW COMPAT MANIFEST (§5): a DIAGNOSIS surface (law hash + interface hash + generator
1619
+ // provenance) the connect-time classifier reads. Frontend-only — the package bytes above ignore it.
1620
+ nomosCompat,
1527
1621
  };
1528
1622
  const deployPath = path.join(outDir, `${cfg.name}.deploy.json`);
1529
1623
  writeFileSync(deployPath, JSON.stringify(deployBody) + "\n", "utf8");
@@ -1535,6 +1629,7 @@ async function main(): Promise<void> {
1535
1629
  let proofPath: string | undefined;
1536
1630
  let proofSkip: string | undefined;
1537
1631
  let proofDomains: string[] = [];
1632
+ let birthWarnings: string[] = [];
1538
1633
  try {
1539
1634
  const proofSrc = generateTsProof(domainModules, { packageName: cfg.name, domainHash });
1540
1635
  proofPath = path.join(outDir, `${cfg.name}.proof.mts`);
@@ -1547,10 +1642,20 @@ async function main(): Promise<void> {
1547
1642
  if (legsIndex !== null) {
1548
1643
  writeFileSync(path.join(outDir, `${cfg.name}.proof-legs.json`), JSON.stringify(legsIndex, null, 2) + "\n", "utf8");
1549
1644
  proofDomains = Object.keys(legsIndex.domains);
1645
+ birthWarnings = Object.values(legsIndex.domains).flatMap((l) => l.birthDiagnostics ?? []);
1550
1646
  }
1551
1647
  } catch (e) {
1552
1648
  proofSkip = (e as Error).message;
1553
1649
  }
1650
+ // BIRTHS DX: a `.births()` directive that can't be (or can only shakily be) proven says WHY here —
1651
+ // never a silent skip, never an opaque runtime `born=[]` first contact. When the .mts generation
1652
+ // itself failed, still derive the per-domain diagnostics (best-effort).
1653
+ if (proofSkip !== undefined && birthWarnings.length === 0) {
1654
+ try {
1655
+ const idx = proofLegsIndex(domainModules);
1656
+ if (idx !== null) birthWarnings = Object.values(idx.domains).flatMap((l) => l.birthDiagnostics ?? []);
1657
+ } catch { /* diagnostics are best-effort */ }
1658
+ }
1554
1659
 
1555
1660
  const summaryTxt = [
1556
1661
  `${cfg.name} — compiled Nomos domain package`,
@@ -1573,7 +1678,7 @@ async function main(): Promise<void> {
1573
1678
  // ── 5. the typed TS client (#10 — the web sibling of codegen_dart), with the
1574
1679
  // deployed law's content hash baked in (emitted AFTER the package is hashed) ──
1575
1680
  const clientPath = path.join(outDir, `${cfg.name}.client.ts`);
1576
- writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash, interface: packageInterface }), "utf8");
1681
+ writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash, interface: packageInterface, compat: nomosCompat }), "utf8");
1577
1682
 
1578
1683
  // ── 6. the typed Dart frontend (opt-in; the Flutter sibling of step 5, emitted
1579
1684
  // after the package hash like the TS client) ──
@@ -1584,10 +1689,11 @@ async function main(): Promise<void> {
1584
1689
  (typeof dartCfg !== "object" ||
1585
1690
  dartCfg === null ||
1586
1691
  (dartCfg.out !== undefined && typeof dartCfg.out !== "string") ||
1587
- (dartCfg.support !== undefined && dartCfg.support !== "vendored" && dartCfg.support !== "package"))
1692
+ (dartCfg.support !== undefined && dartCfg.support !== "vendored" && dartCfg.support !== "package") ||
1693
+ (dartCfg.barrel !== undefined && !/^[a-z][a-z0-9_]*$/.test(String(dartCfg.barrel))))
1588
1694
  ) {
1589
1695
  fail(
1590
- `config key 'dart' must be false, true, or { out?: "relative/dir", support?: "vendored" | "package" }; got ${JSON.stringify(dartCfg)}`,
1696
+ `config key 'dart' must be false, true, or { out?: "relative/dir", support?: "vendored" | "package", barrel?: "snake_case_name" }; got ${JSON.stringify(dartCfg)}`,
1591
1697
  );
1592
1698
  }
1593
1699
  const dartSupportImport: DartSupportImport =
@@ -1599,7 +1705,16 @@ async function main(): Promise<void> {
1599
1705
  cfgDir,
1600
1706
  typeof dartCfg === "object" && dartCfg.out !== undefined ? dartCfg.out : path.join(outDir, "dart"),
1601
1707
  );
1602
- dartFiles = emitDart(domainModules, dartOut, domainHash, packageInterface, dartSupportImport);
1708
+ // The barrel basename: configured, else the snake_cased package name (a valid Dart library
1709
+ // file name — pub packages import `package:<name>/<name>.dart`).
1710
+ const defaultBarrel = cfg.name
1711
+ .toLowerCase()
1712
+ .replace(/[^a-z0-9]+/g, "_")
1713
+ .replace(/^_+|_+$/g, "")
1714
+ .replace(/^(?=[0-9])/, "n");
1715
+ const dartBarrel =
1716
+ typeof dartCfg === "object" && dartCfg.barrel !== undefined ? dartCfg.barrel : defaultBarrel;
1717
+ dartFiles = emitDart(domainModules, dartOut, domainHash, packageInterface, dartSupportImport, dartBarrel, nomosCompat);
1603
1718
  }
1604
1719
 
1605
1720
  // ── summary ──
@@ -1645,6 +1760,9 @@ async function main(): Promise<void> {
1645
1760
  } else {
1646
1761
  console.log(` proof SKIPPED — ${proofSkip}`);
1647
1762
  }
1763
+ for (const w of birthWarnings) {
1764
+ console.log(` ⚠ births ${w}`);
1765
+ }
1648
1766
  if (dartOut !== undefined) {
1649
1767
  const supportText =
1650
1768
  dartSupportImport === "vendored" ? "vendored nomos_client support" : "package nomos_client imports";
@@ -1666,6 +1784,46 @@ async function main(): Promise<void> {
1666
1784
  } else {
1667
1785
  console.log(`prove it: npm run e2e # offline write -> sync -> admission -> cloud query, live`);
1668
1786
  }
1787
+
1788
+ if (analyzeFlag) runDartAnalyze(dartOut);
1789
+ }
1790
+
1791
+ /**
1792
+ * `--analyze` (or NOMOS_ANALYZE=1): run `dart analyze` on the generated Dart package so a red
1793
+ * analyze fails the compile loudly. NEVER changes any emitted bytes — a pure post-emission check.
1794
+ * - No Dart out (dart config off) or no `pubspec.yaml` at the out dir / one level up → warn + skip
1795
+ * (the generated sources need a pub package around them to analyze).
1796
+ * - `dart` missing from PATH → warn + skip.
1797
+ * - Analyzer red → print its output and exit non-zero.
1798
+ */
1799
+ function runDartAnalyze(dartOut: string | undefined): void {
1800
+ if (dartOut === undefined) {
1801
+ console.warn(` ⚠ analyze skipped — no Dart output (set 'dart' in the config to emit + analyze)`);
1802
+ return;
1803
+ }
1804
+ // The pub package root: the out dir, else one level up (a common layout is
1805
+ // `<pkg>/lib/generated` with the pubspec at `<pkg>/`, but out is usually `<pkg>` itself).
1806
+ const pkgRoot = [dartOut, path.dirname(dartOut)].find((d) => existsSync(path.join(d, "pubspec.yaml")));
1807
+ if (pkgRoot === undefined) {
1808
+ console.warn(
1809
+ ` ⚠ analyze skipped — no pubspec.yaml at ${path.relative(process.cwd(), dartOut)} or its parent; ` +
1810
+ `\`dart analyze\` needs a pub package (add a pubspec.yaml around the generated Dart)`,
1811
+ );
1812
+ return;
1813
+ }
1814
+ try {
1815
+ execFileSync("dart", ["analyze", pkgRoot], { stdio: "inherit" });
1816
+ console.log(` analyze dart analyze GREEN (${path.relative(process.cwd(), pkgRoot)})`);
1817
+ } catch (e) {
1818
+ const err = e as NodeJS.ErrnoException;
1819
+ if (err.code === "ENOENT") {
1820
+ console.warn(` ⚠ analyze skipped — \`dart\` is not on PATH (install the Dart SDK to analyze)`);
1821
+ return;
1822
+ }
1823
+ // A non-zero exit is analyzer RED — execFileSync already streamed the output (stdio: inherit).
1824
+ console.error(` ✗ analyze dart analyze FAILED (${path.relative(process.cwd(), pkgRoot)}) — see output above`);
1825
+ process.exit(1);
1826
+ }
1669
1827
  }
1670
1828
 
1671
1829
  await main();
@@ -32,6 +32,9 @@
32
32
  import type { AggregateHandle } from "../aggregate.js";
33
33
  import type { Directive } from "../directive.js";
34
34
  import type { QueryDecl } from "../query.js";
35
+ import { nomosSourceFrontierEnsure } from "../attested.js";
36
+ import * as birthMeter from "../birth_meter.js";
37
+ import * as birthMeterEnsure from "../birth_meter_ensure.js";
35
38
  import * as disclosure from "./disclosure.js";
36
39
  import * as domainLifecycle from "./domain_lifecycle.js";
37
40
  import * as encryptionIdentity from "./encryption_identity.js";
@@ -47,6 +50,12 @@ import * as workspaces from "./workspaces.js";
47
50
  // Every framework module that DECLARES aggregate / directive / query singletons. Namespace imports so
48
51
  // the registry is derived from the real exports (no hand-kept id list to drift).
49
52
  const FRAMEWORK_NAMESPACES: ReadonlyArray<Record<string, unknown>> = [
53
+ // MERGE-SEAM-INJECTED singletons (`composeDomainModule` folds these into every `.births()`
54
+ // domain WITHOUT a tenant re-export — `withBirthMeterExports` + BIRTH_METER_AGGREGATES/QUERIES).
55
+ // Unbranded, a 2-birthing-domain package defined `NomosBirthRecordEnsurePayload` (+ the meter
56
+ // read models/accessors) once PER domain — the exact ambiguous_export co2 hit on its barrel.
57
+ birthMeter,
58
+ birthMeterEnsure,
50
59
  disclosure,
51
60
  domainLifecycle,
52
61
  encryptionIdentity,
@@ -114,6 +123,15 @@ for (const ns of FRAMEWORK_NAMESPACES) {
114
123
  }
115
124
  }
116
125
 
126
+ // THE ATTESTED-READS INJECTED DIRECTIVE — branded INDIVIDUALLY, not via its namespace: its payload
127
+ // class (`NomosSourceFrontierEnsurePayload`, zero-field) is domain-agnostic and must dedup like the
128
+ // birth-meter's, but the `NomosSourceFrontier` AGGREGATE stays UNBRANDED deliberately — it is
129
+ // NAMESPACED per owning domain at law (it is NOT in `GLOBAL_READ_PROJECTION_AGGREGATE_IDS`), so its
130
+ // per-domain Dart read surface is domain-specific and must stay in each domain's file. Branding the
131
+ // whole namespace would have flipped its law-plane namespacing via the drift-guard pin.
132
+ (nomosSourceFrontierEnsure as { framework?: true }).framework = true;
133
+ directiveIds.add(nomosSourceFrontierEnsure.id);
134
+
117
135
  export const FRAMEWORK_AGGREGATE_IDS: ReadonlySet<string> = aggregateIds;
118
136
  export const FRAMEWORK_DIRECTIVE_IDS: ReadonlySet<string> = directiveIds;
119
137
  export const FRAMEWORK_QUERY_IDS: ReadonlySet<string> = queryIds;
@@ -6,20 +6,20 @@
6
6
  // sibling of `relation_composition.ts` (the scalpel).
7
7
  //
8
8
  // THE MODEL. The kernel's installed-read-manifest accumulator
9
- // (`wasm_git_holon.rs::ReadProjectionAccumulator`) UNIONS every active install's
10
- // `nomosReadProjection` fragment BY KEY — `aggregateFieldKinds` by aggregate TYPE,
9
+ // (`wasm_git_holon.rs::ReadProjectionAccumulator`) UNIONS the CURRENT installed
10
+ // `nomosReadProjection` fragments BY KEY — `aggregateFieldKinds` by aggregate TYPE,
11
11
  // `queries`/`counts`/`spatials`/`sums`/`mins`/`maxes`/`orderedReads` by declared `id`,
12
12
  // `deriveds`/`combineds` by `"<of>\0<id>"` — and (correctly, fail-closed) REFUSES a
13
- // key declared twice with DIVERGENT shapes: "divergent read projection declaration
14
- // for key <k>". At RUNTIME that refusal WEDGES the workspace's read plane.
13
+ // CURRENT key declared twice with DIVERGENT shapes: "divergent read projection
14
+ // declaration for key <k>". Superseded eras never feed the current read manifest.
15
15
  //
16
16
  // Read-projection NAMESPACING (`read_projection_namespace.ts`) makes a NON-GLOBAL
17
17
  // aggregate's keys domain-qualified, so tenant domains never share a key. What CAN
18
18
  // still share a key inside one package: GLOBAL framework aggregates declared by ≥2
19
19
  // domains (byte-identical by construction — same framework version — but a version
20
20
  // skew diverges), and same-named declared reads across domains. This guard turns the
21
- // runtime wedge into a precise COMPILE refusal: identical shared declarations PASS
22
- // (the kernel dedupes them), divergent ones REFUSE with the key and each domain's
21
+ // current-runtime conflict into a precise COMPILE refusal: identical shared declarations
22
+ // PASS (the kernel dedupes them), divergent ones REFUSE with the key and each domain's
23
23
  // shape. Validation only — a conflict-free package is returned untouched (era rule).
24
24
 
25
25
  import type { UsdLayer } from "./usd.js";
@@ -38,9 +38,9 @@ interface ReadProjectionConflict {
38
38
  function scalpelMessage(conflicts: readonly ReadProjectionConflict[]): string {
39
39
  const lines: string[] = [
40
40
  `Refusing to compose the package: shared read-projection key(s) carry DIVERGENT declarations ` +
41
- `across domains — the kernel's installed-read-manifest accumulator refuses exactly this at ` +
42
- `SERVE time ("divergent read projection declaration for key …"), wedging the workspace's ` +
43
- `read plane. Fail-closed compile refusal, not a runtime wedge.`,
41
+ `across domains — the current installed-read-manifest accumulator refuses exactly this at ` +
42
+ `SERVE time ("divergent read projection declaration for key …"). Superseded eras are ignored, ` +
43
+ `but genuinely current law must have one coherent read shape. Fail-closed compile refusal.`,
44
44
  ``,
45
45
  ];
46
46
  for (const c of conflicts) {
@@ -6,11 +6,10 @@
6
6
  // relation-type namespacing (`relation_schema.ts::qualifyRelationType`).
7
7
  //
8
8
  // THE DISEASE. The kernel's installed-read-manifest accumulator
9
- // (`wasm_git_holon.rs::insert_read_projection_value`) unions every ACTIVE install's
10
- // `nomosReadProjection` fragment and — correctly, fail-closed — REFUSES when two
11
- // installs declare the SAME key with DIVERGENT shapes ("divergent read projection
12
- // declaration for key AuthProvider"). Two different domain keys that each declare a
13
- // same-named aggregate therefore WEDGE the workspace's read plane at serve time.
9
+ // (`wasm_git_holon.rs::insert_read_projection_value`) merges every current install's
10
+ // `nomosReadProjection` fragment by key. Superseded eras are ignored, so old read
11
+ // declarations cannot wedge a workspace, but two independent current domain keys that
12
+ // each declare a same-named aggregate are still an ambiguous read surface.
14
13
  //
15
14
  // THE CURE (the maintainer's rule: "Namespaces are a good idea. Use them."): a
16
15
  // NON-GLOBAL aggregate's TYPE NAME is QUALIFIED by its owning domain key at compile —
package/src/usd.ts CHANGED
@@ -288,9 +288,9 @@ export interface UsdLayer {
288
288
  * THE READ-PROJECTION NAMESPACE ERA KEY (read-projection namespacing — the read-layer mirror of
289
289
  * `nomosRelationNamespace`). Present IFF this layer qualified ≥1 (non-global) AGGREGATE TYPE by its
290
290
  * owning domain key (`Entry` → `guestbook__Entry`), so two domains / two installs that each declare
291
- * the same bare aggregate name no longer collide at the kernel's installed-read-manifest accumulator
292
- * ("divergent read projection declaration for key …" — the fail-closed union stays the trust
293
- * boundary; a qualified key is simply a NEW name to it). `types` is the SORTED set of qualified type
291
+ * the same bare aggregate name no longer collide at the kernel's installed-read-manifest accumulator.
292
+ * Runtime evolution ignores superseded eras; qualification keeps independent domains distinct.
293
+ * `types` is the SORTED set of qualified type
294
294
  * names this layer produced; `delimiter` is the same ratified colon-free `"__"`. OMITTED for a layer
295
295
  * whose aggregates are all GLOBAL (the explicit framework allowlist / `{global:true}`) — the marker
296
296
  * is a witness; the qualification itself is baked into the layer's keys.
@@ -349,6 +349,7 @@ function sampleOfDescriptorField(
349
349
  f: ValueObjectFieldDescriptor,
350
350
  fieldName: string,
351
351
  byName: Map<string, ValueObjectDescriptor>,
352
+ includeOptional = false,
352
353
  ): unknown {
353
354
  switch (f.type) {
354
355
  case "string": return `sample-${fieldName}`;
@@ -356,17 +357,17 @@ function sampleOfDescriptorField(
356
357
  case "literal": return f.value;
357
358
  case "boolean": return true;
358
359
  case "enum": return f.values[0];
359
- case "array": return [sampleOfDescriptorField(f.items, fieldName, byName)];
360
+ case "array": return [sampleOfDescriptorField(f.items, fieldName, byName, includeOptional)];
360
361
  case "geometry": return GEOMETRY_SAMPLES[f.geometry];
361
362
  case "object": {
362
363
  const dep = byName.get(f.ref);
363
364
  if (dep === undefined) throw new Error(`voSample: nested value object '${f.ref}' has no descriptor`);
364
- return sampleOfDescriptor(dep, byName);
365
+ return sampleOfDescriptor(dep, byName, includeOptional);
365
366
  }
366
367
  }
367
368
  }
368
369
 
369
- function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueObjectDescriptor>): unknown {
370
+ function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueObjectDescriptor>, includeOptional = false): unknown {
370
371
  if (d.geometry !== undefined) return GEOMETRY_SAMPLES[d.geometry];
371
372
  if (d.union !== undefined) {
372
373
  // FIRST declared arm (keys are sorted — deterministic); `example:` overrides upstream.
@@ -378,15 +379,15 @@ function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueO
378
379
  }
379
380
  const out: Record<string, unknown> = { type: key };
380
381
  for (const [k, f] of Object.entries(arm.fields)) {
381
- if (f.optional === true) continue;
382
- out[k] = sampleOfDescriptorField(f, k, byName);
382
+ if (f.optional === true && !includeOptional) continue;
383
+ out[k] = sampleOfDescriptorField(f, k, byName, includeOptional);
383
384
  }
384
385
  return out;
385
386
  }
386
387
  const out: Record<string, unknown> = {};
387
388
  for (const [k, f] of Object.entries(d.fields ?? {})) {
388
- if (f.optional === true) continue; // optional → omitted (mirrors payload synthesis)
389
- out[k] = sampleOfDescriptorField(f, k, byName);
389
+ if (f.optional === true && !includeOptional) continue; // optional → omitted (mirrors payload synthesis)
390
+ out[k] = sampleOfDescriptorField(f, k, byName, includeOptional);
390
391
  }
391
392
  return out;
392
393
  }
@@ -397,12 +398,12 @@ function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueO
397
398
  * VOs). Proof synthesis calls this instead of guessing at the raw zod — which is exactly the
398
399
  * lane that refused its own sample on constrained shapes (geo min-2 coordinates).
399
400
  */
400
- export function voSample(meta: ValueObjectFieldMeta): unknown {
401
+ export function voSample(meta: ValueObjectFieldMeta, opts?: { includeOptional?: boolean }): unknown {
401
402
  if (meta.example !== undefined) return meta.example;
402
403
  const byName = new Map<string, ValueObjectDescriptor>();
403
404
  for (const d of meta.dependencies) byName.set(d.name, d);
404
405
  byName.set(meta.descriptor.name, meta.descriptor);
405
- return sampleOfDescriptor(meta.descriptor, byName);
406
+ return sampleOfDescriptor(meta.descriptor, byName, opts?.includeOptional === true);
406
407
  }
407
408
 
408
409
  function makeValueObject<T>(