@githolon/dsl 0.100.5 → 0.101.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dart/CHANGELOG.md +12 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +220 -71
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/acquire_session_credentials_test.dart +1 -1
  8. package/dart/test/adv_realm_interference_test.dart +1 -1
  9. package/dart/test/bundle_version_pairing_test.dart +11 -22
  10. package/dart/test/cluster_attested_test.dart +1 -1
  11. package/dart/test/cluster_births_test.dart +1 -1
  12. package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
  13. package/dart/test/cluster_local_autosync_test.dart +1 -1
  14. package/dart/test/cluster_scale_test.dart +1 -1
  15. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  16. package/dart/test/dlq_session_surface_test.dart +1 -1
  17. package/dart/test/errors_test.dart +63 -12
  18. package/dart/test/first_app_gauntlet_test.dart +132 -87
  19. package/dart/test/local_sync_inert_test.dart +10 -5
  20. package/dart/test/parent_signer_recovery_test.dart +1 -1
  21. package/dart/test/sealing_client_test.dart +1 -1
  22. package/dart/test/session_bound_test.dart +1 -1
  23. package/dart/test/test_harness_test.dart +2 -2
  24. package/dart/test/vo_read_typing_test.dart +1 -1
  25. package/dart/test/watch_after_offer_stall_test.dart +1 -1
  26. package/dart/test/webview_bridge_test.dart +274 -0
  27. package/dart_types/CHANGELOG.md +6 -0
  28. package/dart_types/lib/src/errors.dart +136 -30
  29. package/dart_types/lib/src/surfaced.dart +48 -2
  30. package/dart_types/lib/src/wire.dart +3 -1
  31. package/dart_types/pubspec.yaml +1 -1
  32. package/dart_types/test/surfaced_test.dart +19 -0
  33. package/package.json +1 -1
  34. package/src/activity.ts +43 -4
  35. package/src/aggregate.ts +132 -22
  36. package/src/attested_core.ts +1 -1
  37. package/src/authoring.ts +119 -6
  38. package/src/birth_meter.ts +4 -4
  39. package/src/birth_meter_ensure.ts +165 -29
  40. package/src/births.ts +53 -22
  41. package/src/build_package.ts +96 -20
  42. package/src/business_birth.ts +79 -16
  43. package/src/codegen_dart.ts +267 -37
  44. package/src/codegen_dart_client.ts +930 -87
  45. package/src/codegen_openapi.ts +28 -5
  46. package/src/codegen_proof.ts +107 -11
  47. package/src/codegen_ts.ts +961 -135
  48. package/src/compile_package_main.ts +172 -53
  49. package/src/conflicts.ts +252 -0
  50. package/src/describe_model.ts +448 -0
  51. package/src/directive.ts +5 -38
  52. package/src/drivers.ts +12 -4
  53. package/src/engine_entry.ts +4 -0
  54. package/src/evolve_preview.ts +72 -11
  55. package/src/fields.ts +201 -55
  56. package/src/framework/application_home.ts +103 -0
  57. package/src/framework/application_home_domain.ts +2 -0
  58. package/src/framework/attested_receipts.ts +2 -2
  59. package/src/framework/auth_provider_law.ts +51 -24
  60. package/src/framework/business_corrections.ts +170 -0
  61. package/src/framework/disclosure.ts +1 -1
  62. package/src/framework/domain_lifecycle.ts +250 -16
  63. package/src/framework/encryption_identity.ts +1 -1
  64. package/src/framework/encryption_scope.ts +21 -13
  65. package/src/framework/era_certification.ts +1 -1
  66. package/src/framework/framework_aggregates.ts +2 -0
  67. package/src/framework/genesis.ts +1 -1
  68. package/src/framework/home.ts +22 -9
  69. package/src/framework/identity.ts +5 -5
  70. package/src/framework/impure_capability.ts +2 -2
  71. package/src/framework/owned_custody.ts +561 -0
  72. package/src/framework/platform_admin.ts +1 -1
  73. package/src/framework/relations.ts +9 -1
  74. package/src/framework/repair.ts +1 -1
  75. package/src/framework/signing_identity.ts +2 -2
  76. package/src/framework/source_roster.ts +1 -1
  77. package/src/framework/workspaces.ts +17 -10
  78. package/src/generated_client_names.ts +39 -0
  79. package/src/index.ts +20 -15
  80. package/src/kernel_capabilities.ts +25 -1
  81. package/src/law_check.ts +4 -1
  82. package/src/manifest.ts +356 -63
  83. package/src/read_projection_manifest.ts +8 -0
  84. package/src/read_projection_namespace.ts +4 -0
  85. package/src/relation.ts +1 -1
  86. package/src/shape_compat.ts +61 -5
  87. package/src/stable_ids.ts +38 -6
  88. package/src/stable_ids_types.ts +30 -0
  89. package/src/surfaced_lint.ts +0 -0
  90. package/src/usd.ts +62 -11
  91. package/src/usd_layers.ts +54 -3
  92. package/src/value_object.ts +23 -19
  93. package/src/wire.ts +1 -0
  94. package/src/wire_encode.ts +48 -4
  95. package/src/workspace_routing.ts +57 -0
  96. package/src/workspace_sharding.ts +9 -9
  97. package/src/certified_read.ts +0 -313
@@ -67,12 +67,14 @@ import {
67
67
  } from "./codegen_dart.js";
68
68
  import type { ContractHandle } from "./contract.js";
69
69
  import { generateDartBridgeClient } from "./codegen_dart_client.js";
70
+ import { buildNomosDescribeModel } from "./describe_model.js";
70
71
  import {
71
72
  isFrameworkAggregate,
72
73
  isFrameworkDirective,
73
74
  isFrameworkQuery,
74
75
  } from "./framework/framework_aggregates.js";
75
76
  import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
77
+ import { isNamespacedAggregate } from "./read_projection_namespace.js";
76
78
  import { finishCount, type AnyCount } from "./count.js";
77
79
  import type { QueryDecl } from "./query.js";
78
80
  import type { SpatialDecl } from "./spatial.js";
@@ -94,6 +96,7 @@ import { synthesizePreconditionInvariants } from "./precondition.js";
94
96
  import { BIRTH_FRAMEWORK_QUERIES, synthesizeBirthQuotaInvariants } from "./birth_meter.js";
95
97
  import { withBirthMeterExports } from "./birth_meter_ensure.js";
96
98
  import { expandBusinessBirthExports } from "./business_birth.js";
99
+ import { withOwnedCustodyExports } from "./framework/owned_custody.js";
97
100
  import { expandRoleExports, rolesOf } from "./role.js";
98
101
  import { collectEngineRouting } from "./engine_entry.js";
99
102
  import {
@@ -134,6 +137,7 @@ import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
134
137
  import { checkDomainModules, formatFindings, hasRefusal } from "./law_check.js";
135
138
  import type { NomosCompat } from "./compat_manifest.js";
136
139
  import { requiredKernelFeatures, type FeatureProbeModule } from "./kernel_capabilities.js";
140
+ import { APPLICATION_HOME_DOMAIN } from "./framework/application_home_domain.js";
137
141
 
138
142
  const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
139
143
 
@@ -494,8 +498,8 @@ const FRAMEWORK_FILE = "nomos_framework";
494
498
  /** The shared basename of the domain-contracts DTO file emitted once per package (domain_contracts.md). */
495
499
  const CONTRACTS_FILE = "contracts";
496
500
 
497
- /** The shared basename of the ACTIVITY REGISTRY file emitted once per package (activity_descriptors.md).
498
- * Absent when no directive declares `.activity(...)` — an activity-free package emits no `activity_registry.dart`. */
501
+ /** The shared basename of the ACCOUNTABLE ACTIVITY REGISTRY emitted once per package when it carries at
502
+ * least one public business directive. `.activity(...)` enriches the generated baseline; it does not gate it. */
499
503
  const ACTIVITY_FILE = "activity_registry";
500
504
 
501
505
  /** Collect every domain contract across the package's modules, deduped by id (later-wins). Sorted by id.
@@ -598,10 +602,13 @@ function collectDartSharedSymbols(
598
602
  const frameworkAggById = new Map<string, AggregateHandle>();
599
603
  const frameworkDirById = new Map<string, ModDirective>();
600
604
  const frameworkQueryById = new Map<string, ModQuery>();
605
+ const isInternal = (value: unknown): boolean =>
606
+ !!value && typeof value === "object"
607
+ && (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
601
608
  for (const mod of modules) {
602
- for (const a of mod.aggregates) if (isFrameworkAggregate(a)) frameworkAggById.set(a.id, a);
603
- for (const d of mod.directives) if (isFrameworkDirective(d)) frameworkDirById.set(d.id, d);
604
- for (const q of mod.queries ?? []) if (isFrameworkQuery(q)) frameworkQueryById.set(q.id, q);
609
+ for (const a of mod.aggregates) if (!isInternal(a) && isFrameworkAggregate(a)) frameworkAggById.set(a.id, a);
610
+ for (const d of mod.directives) if (!isInternal(d) && isFrameworkDirective(d)) frameworkDirById.set(d.id, d);
611
+ for (const q of mod.queries ?? []) if (!isInternal(q) && isFrameworkQuery(q)) frameworkQueryById.set(q.id, q);
605
612
  }
606
613
  const frameworkAggs = [...frameworkAggById.values()];
607
614
  const frameworkDirs = [...frameworkDirById.values()];
@@ -780,6 +787,7 @@ function emitDart(
780
787
  barrelName = "nomos_domains",
781
788
  compat?: NomosCompat,
782
789
  names?: Readonly<Record<string, string>>,
790
+ describePackageName = barrelName,
783
791
  ): string[] {
784
792
  const domainSources: string[] = []; // for flat-mode alias validation (dart.names)
785
793
  const sourceByFile = new Map<string, string>(); // fileBase → src, for precise alias-import selection
@@ -886,14 +894,14 @@ function emitDart(
886
894
  aggregateIdClassNames: packageAggregateIdClassNames(modules),
887
895
  }));
888
896
  }
889
- // 2a-ter. the shared ACTIVITY REGISTRY (`activity_registry.dart`) — the pure `renderActivity`, ONCE. Emitted
890
- // iff a directive opts in via `.activity(...)` (activity-free packages stay byte-identical). Pure
891
- // (nomos_types), so the client folds typed activity rows from the intent log with no runtime import.
897
+ // 2a-ter. the shared ACCOUNTABLE ACTIVITY + DESCRIPTION REGISTRY (`activity_registry.dart`) — emitted
898
+ // ONCE for every package. A read-only package has an empty activity registry but still exposes
899
+ // describe(model/current/rules/history/decisions/evidence).
900
+ // Pure (nomos_types), so the client folds typed rows from the intent log with no runtime import.
892
901
  const packageActivity = collectActivityDescriptors(modules);
893
- const activityImportUri = packageActivity.length > 0 ? `${ACTIVITY_FILE}.dart` : undefined;
894
- if (packageActivity.length > 0) {
895
- writeGenerated(ACTIVITY_FILE, generateDartActivityRegistry(packageActivity));
896
- }
902
+ const describeModel = buildNomosDescribeModel(modules, describePackageName);
903
+ const activityImportUri = `${ACTIVITY_FILE}.dart`;
904
+ writeGenerated(ACTIVITY_FILE, generateDartActivityRegistry(packageActivity));
897
905
  // CROSS-DOMAIN `t.foreignRef` (auto import). A foreignRef field typed `ForeignRef<CatalogueListing>` +
898
906
  // `CatalogueListing.fromJson` needs the TARGET aggregate's Dart class — which lives in the target DOMAIN's
899
907
  // generated file. Within ONE package the compiler knows which domain owns each aggregate, so we auto-emit
@@ -969,7 +977,9 @@ function emitDart(
969
977
  ...(compat !== undefined ? { compat } : {}),
970
978
  supportImport,
971
979
  ...(domainFrameworkImport !== undefined ? { frameworkImportUri: domainFrameworkImport } : {}),
972
- ...(packageActivity.length > 0 ? { emitActivity: true, ...(activityImportUri !== undefined ? { activityImportUri } : {}) } : {}),
980
+ emitActivity: true,
981
+ activityImportUri,
982
+ describeModel,
973
983
  packageAggregateDomains,
974
984
  });
975
985
  const cp = path.join(genDir, `${mod.name}.client.dart`);
@@ -1048,11 +1058,11 @@ const DART_TYPES_IMPORT = `import 'package:nomos_types/nomos_types.dart';`;
1048
1058
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
1049
1059
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
1050
1060
  // the pendingConflicts read rows.
1051
- const NOMOS_TYPES_DEP = "^0.6.0";
1061
+ const NOMOS_TYPES_DEP = "^0.7.0";
1052
1062
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1053
1063
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1054
1064
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1055
- const NOMOS_CLIENT_DEP = "^0.62.0";
1065
+ const NOMOS_CLIENT_DEP = "^0.63.0";
1056
1066
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1057
1067
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1058
1068
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -1142,6 +1152,7 @@ function emitDartPackages(
1142
1152
  packages: { readonly types: string; readonly client: string; readonly test?: string; readonly compat?: string; readonly barrelExports?: "all" | "explicit" },
1143
1153
  names: Readonly<Record<string, string>> | undefined,
1144
1154
  compat?: NomosCompat,
1155
+ describePackageName = packages.client,
1145
1156
  ): { written: string[]; cursed: string[] } {
1146
1157
  for (const [role, name] of Object.entries(packages)) {
1147
1158
  if (typeof name !== "string" || !isPubPackageName(name)) {
@@ -1157,6 +1168,7 @@ function emitDartPackages(
1157
1168
  if (new Set(roleNames).size !== roleNames.length) fail(`dart.packages: types/client/test/compat names must be distinct`);
1158
1169
 
1159
1170
  const packageContracts = collectPackageContracts(modules);
1171
+ const describeModel = buildNomosDescribeModel(modules, describePackageName);
1160
1172
  const shared = collectDartSharedSymbols(modules, packageContracts);
1161
1173
  const written: string[] = [];
1162
1174
  const genRel = DART_GENERATED_SUBDIR.split(path.sep).join("/");
@@ -1219,10 +1231,9 @@ function emitDartPackages(
1219
1231
  aggregateIdClassNames: packageAggregateIdClassNames(modules),
1220
1232
  }));
1221
1233
  }
1222
- // ACTIVITY REGISTRY (layered): the pure `renderActivity` in the TYPES package (nomos_types only). The
1223
- // client resolves it through the types-package barrel (which re-exports every types file). Opt-in only.
1234
+ // ACTIVITY + DESCRIPTION REGISTRY (layered): universal, including packages with empty history.
1224
1235
  const packageActivity = collectActivityDescriptors(modules);
1225
- if (packageActivity.length > 0) writeTypes(ACTIVITY_FILE, generateDartActivityRegistry(packageActivity));
1236
+ writeTypes(ACTIVITY_FILE, generateDartActivityRegistry(packageActivity));
1226
1237
  // CROSS-DOMAIN foreignRef (layered): map aggregate id → its domain's type file, so a foreignRef field's
1227
1238
  // target class resolves via a sibling import (the purity gate now permits sibling type files).
1228
1239
  const aggIdToTypeFile = new Map<string, string>();
@@ -1354,7 +1365,8 @@ function emitDartPackages(
1354
1365
  ...(compat !== undefined ? { compat } : {}),
1355
1366
  supportImport: "package",
1356
1367
  typesImportUri,
1357
- ...(packageActivity.length > 0 ? { emitActivity: true } : {}),
1368
+ emitActivity: true,
1369
+ describeModel,
1358
1370
  packageAggregateDomains: buildPackageAggregateDomains(modules),
1359
1371
  // Under 'explicit' the types barrel withholds per-domain classes — import every domain's TYPES
1360
1372
  // file directly so the client's read-model/payload/ref/typed-id references resolve. (The no-dup-
@@ -1549,6 +1561,20 @@ async function main(): Promise<void> {
1549
1561
  fail(`package name '${cfg.name}' is not a safe basename (letters, digits, spaces, '.', '-', '_')`);
1550
1562
  }
1551
1563
 
1564
+ // ONE BUSINESS BIRTH IS A COMPLETE PRODUCT ENTRYPOINT. The compiler privately appends the
1565
+ // application/home law which turns a signed-in identity into its home workspace; that home can then
1566
+ // call the generated business birth. Tenant config never names this domain. The deliberately obscure
1567
+ // key reduces collision likelihood, but collision handling is still fail-closed: never reinterpret a
1568
+ // tenant's same-named concept as framework machinery.
1569
+ if (cfg.domains.some((d) => d.key === APPLICATION_HOME_DOMAIN)) {
1570
+ fail(
1571
+ `domain key '${APPLICATION_HOME_DOMAIN}' is reserved by Nomos for the private application/home law; ` +
1572
+ `rename the tenant domain (Nomos will not silently overwrite or reroute it)`,
1573
+ );
1574
+ }
1575
+ const domainsToCompile: DomainConfig[] = [...cfg.domains];
1576
+ let applicationHomeInjected = false;
1577
+
1552
1578
  const outDir = path.resolve(cfgDir, cfg.outDir ?? "build");
1553
1579
  mkdirSync(outDir, { recursive: true });
1554
1580
 
@@ -1596,7 +1622,7 @@ async function main(): Promise<void> {
1596
1622
  tasksQueryId: string | null;
1597
1623
  }[] = [];
1598
1624
 
1599
- for (const d of cfg.domains) {
1625
+ for (const d of domainsToCompile) {
1600
1626
  if (typeof d.key !== "string" || !Array.isArray(d.modules) || d.modules.length === 0) {
1601
1627
  fail(`each domain needs { key, modules: [path, ...] }; got ${JSON.stringify(d)}`);
1602
1628
  }
@@ -1617,7 +1643,21 @@ async function main(): Promise<void> {
1617
1643
  }
1618
1644
  let merged: Mod = {};
1619
1645
  for (const m of mods) merged = { ...merged, ...m };
1646
+ const declaresBusinessBirth = Object.values(merged).some(
1647
+ (value) =>
1648
+ value !== null &&
1649
+ typeof value === "object" &&
1650
+ (value as { __isBusinessBirth?: unknown }).__isBusinessBirth === true,
1651
+ );
1652
+ if (declaresBusinessBirth && !applicationHomeInjected) {
1653
+ applicationHomeInjected = true;
1654
+ domainsToCompile.push({
1655
+ key: APPLICATION_HOME_DOMAIN,
1656
+ modules: [path.join(DSL_DIR, "src", "framework", "application_home.ts")],
1657
+ });
1658
+ }
1620
1659
  merged = expandBusinessBirthExports(merged, d.key);
1660
+ merged = withOwnedCustodyExports(merged);
1621
1661
  // `capability()` bundles expand into discoverable entries (the CLI plane's
1622
1662
  // merge point) — queries/sums/sid-minting/manifests all see the pieces.
1623
1663
  merged = expandCapabilityExports(merged);
@@ -1753,7 +1793,7 @@ async function main(): Promise<void> {
1753
1793
  // taxonomy-free packages emit NOTHING here (the slice-1 byte-stability law).
1754
1794
  if (shardingModForDomain !== undefined && shardingLazyExpr !== undefined && !eagerLump) {
1755
1795
  const auxKey = `${d.key}/nomos-sharding`;
1756
- if (cfg.domains.some((x) => x.key === auxKey) || routingInput[auxKey] !== undefined) {
1796
+ if (domainsToCompile.some((x) => x.key === auxKey) || routingInput[auxKey] !== undefined) {
1757
1797
  fail(
1758
1798
  `domain key '${auxKey}' is reserved for the derived sharding law's invariant routing — rename the domain`,
1759
1799
  );
@@ -1773,7 +1813,9 @@ async function main(): Promise<void> {
1773
1813
  if (directivesOf(merged).some((dir) => (dir as { birthsChild?: boolean }).birthsChild === true)) {
1774
1814
  const haveQuery = new Set(queries.map((q) => q.id));
1775
1815
  for (const q of BIRTH_FRAMEWORK_QUERIES) {
1776
- if (!haveQuery.has(q.id)) queries.push(q);
1816
+ if (!haveQuery.has(q.id)) {
1817
+ queries.push({ ...q, businessBirthInternal: true } as QueryDecl);
1818
+ }
1777
1819
  }
1778
1820
  }
1779
1821
  // RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX, same idiom as the birth-meter
@@ -1906,7 +1948,7 @@ async function main(): Promise<void> {
1906
1948
  ? { dartImports: d.dartImports }
1907
1949
  : {}),
1908
1950
  ...(d.dartRefImports !== undefined && d.dartRefImports.length > 0
1909
- ? { dartRefImports: d.dartRefImports }
1951
+ ? { dartRefImports: [...d.dartRefImports] }
1910
1952
  : {}),
1911
1953
  ...(permissionVocabulary !== undefined ? { permissionVocabulary } : {}),
1912
1954
  }),
@@ -2037,6 +2079,13 @@ async function main(): Promise<void> {
2037
2079
  }
2038
2080
  }
2039
2081
 
2082
+ // Presentation is a separate concern from executable law. The private application/home domain remains
2083
+ // in `domainModules` for hashing, routing, self-description, replay and static checking, but generated
2084
+ // tenant clients/proofs/interfaces contain business vocabulary only.
2085
+ const presentationDomainModules = domainModules.filter(
2086
+ (mod) => (mod.domain ?? mod.name) !== APPLICATION_HOME_DOMAIN,
2087
+ );
2088
+
2040
2089
  // ── STABLE-ID CONTINUITY (#58 — names are labels, identity is minted) ──
2041
2090
  // Derive each composed domain's stable ids from the PRIOR compile: the committed
2042
2091
  // `nomos.stable-ids.json` lockfile beside the config (the durable source — survives
@@ -2060,12 +2109,23 @@ async function main(): Promise<void> {
2060
2109
  const identityName = mod.name;
2061
2110
  // The CURRENT canonical aggregate schemas (a pre-continuity manifest pass —
2062
2111
  // aggregates/schemas do not depend on continuity).
2063
- let currentAggs: { id: string; schema: WireSchema; kinds?: Record<string, string> }[];
2112
+ let currentAggs: {
2113
+ id: string;
2114
+ schema: WireSchema;
2115
+ kinds?: Record<string, string>;
2116
+ global?: boolean;
2117
+ }[];
2064
2118
  try {
2065
2119
  const preManifest = domainManifest(mod);
2066
2120
  currentAggs = preManifest.aggregates.map((a) => ({
2067
2121
  id: a.id,
2068
2122
  schema: a.schema,
2123
+ // One framework aggregate may be composed into many independently compiled
2124
+ // domain packages. Give that shared row one global stable-id scope so the
2125
+ // installed read manifest sees the same field contract from every package.
2126
+ global: mod.aggregates.some(
2127
+ (candidate) => candidate.id === a.id && !isNamespacedAggregate(candidate),
2128
+ ),
2069
2129
  // The CURRENT field kinds (off the pre-continuity stableIds emission) — the
2070
2130
  // pair rule's kind axis ("rename + add a field" in one release still infers).
2071
2131
  kinds: Object.fromEntries(
@@ -2149,7 +2209,7 @@ async function main(): Promise<void> {
2149
2209
  // `connect()` negotiates compatibility by SHAPE (stable-id subtyping), not by law hash.
2150
2210
  // architecture/shape_compatibility.md. Multiple modules sharing a domain key merge.
2151
2211
  const packageInterface: Record<string, DomainInterface> = {};
2152
- for (const mod of domainModules) {
2212
+ for (const mod of presentationDomainModules) {
2153
2213
  const key = mod.domain ?? mod.name;
2154
2214
  const di = domainInterface(mod, nextLock.domains[mod.name]?.stableIds);
2155
2215
  const prior = packageInterface[key];
@@ -2171,7 +2231,7 @@ async function main(): Promise<void> {
2171
2231
  // supported: each codegen layer mints a named recursive type for it (TS `type <Name>`, Dart
2172
2232
  // recursive class). Surfaced here so the tenant sees which fields became which named types.
2173
2233
  const recursiveNotes: string[] = [];
2174
- for (const mod of domainModules) {
2234
+ for (const mod of presentationDomainModules) {
2175
2235
  for (const d of (mod.directives ?? []) as Array<{ id?: unknown; payloadSchema?: unknown }>) {
2176
2236
  if (!d || typeof d.id !== "string" || !d.payloadSchema || typeof d.payloadSchema !== "object") continue;
2177
2237
  for (const { path, name } of recursiveLazyTypes(d.payloadSchema as Parameters<typeof recursiveLazyTypes>[0], d.id)) {
@@ -2306,6 +2366,24 @@ async function main(): Promise<void> {
2306
2366
  // JS shim with the platform-native binary, which only an exec can run.
2307
2367
  const esbuildBin = path.join(path.dirname(esbuildPkgJson), esbuildBinRel ?? "bin/esbuild");
2308
2368
 
2369
+ const canonicalizeEngineJavascript = (source: string): string => {
2370
+ // esbuild includes module labels derived from the package's install topology in
2371
+ // non-minified bundles. The same law therefore used to hash differently when the
2372
+ // DSL was reached through this monorepo (../../dsl/src/...) and through a packed
2373
+ // dependency (node_modules/@githolon/dsl/src/...). Canonicalise only esbuild's
2374
+ // generated comment/function labels: tenant strings and executable imports are
2375
+ // deliberately outside these narrow replacements.
2376
+ const body = source
2377
+ .replace(/^\s*"use strict";\s*$/gm, "")
2378
+ .replace(/^(\s*\/\/ )(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)/gm, "$1@githolon/dsl/src/")
2379
+ .replace(/"(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)([^"]+)"\(\) \{/g, '"@githolon/dsl/src/$1"() {')
2380
+ .replace(/^(\s*\/\/ )(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)/gm, "$1zod/")
2381
+ .replace(/"(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)([^"]+)"\(\) \{/g, '"zod/$1"() {')
2382
+ .replace(/\n[ \t]*\n/g, "\n")
2383
+ .trimEnd();
2384
+ return `"use strict";\n${body}\n`;
2385
+ };
2386
+
2309
2387
  const compact = compactFlag || cfg.compact === true;
2310
2388
  let javascript: string;
2311
2389
  if (compact) {
@@ -2344,6 +2422,7 @@ async function main(): Promise<void> {
2344
2422
  { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], cwd: cfgDir, maxBuffer: 256 * 1024 * 1024 },
2345
2423
  ).trimEnd() + "\n";
2346
2424
  }
2425
+ javascript = canonicalizeEngineJavascript(javascript);
2347
2426
  if (!/globalThis\.plan\s*=/.test(javascript) && !/globalThis\["plan"\]\s*=/.test(javascript)) {
2348
2427
  fail(`bundled engine entry does not assign globalThis.plan — refusing to package`);
2349
2428
  }
@@ -2437,7 +2516,7 @@ async function main(): Promise<void> {
2437
2516
  "utf8",
2438
2517
  );
2439
2518
  try {
2440
- execFileSync(process.execPath, [probePath, lumpPath, JSON.stringify(cfg.domains.map((d) => d.key))], {
2519
+ execFileSync(process.execPath, [probePath, lumpPath, JSON.stringify(domainsToCompile.map((d) => d.key))], {
2441
2520
  encoding: "utf8",
2442
2521
  stdio: ["ignore", "ignore", "pipe"],
2443
2522
  });
@@ -2550,7 +2629,7 @@ async function main(): Promise<void> {
2550
2629
  }
2551
2630
  })();
2552
2631
  const compatDomains: NomosCompat["domains"] = {};
2553
- for (const m of domainModules) {
2632
+ for (const m of presentationDomainModules) {
2554
2633
  const key = m.domain ?? m.name;
2555
2634
  const identityHash = identity.hashes[key];
2556
2635
  compatDomains[key] = identityHash === undefined ? {} : { identityHash };
@@ -2559,7 +2638,11 @@ async function main(): Promise<void> {
2559
2638
  // era-key features). Baked into the client so connect() can check it against the target
2560
2639
  // kernel's advertised capabilities BEFORE the law folds (kernel_capabilities.ts).
2561
2640
  const requiresKernel = ((): string[] => {
2562
- const probes: FeatureProbeModule[] = domainModules.map((m) => (m.queries ? { queries: m.queries } : {}));
2641
+ const probes: FeatureProbeModule[] = domainModules.map((m) => ({
2642
+ ...(m.queries ? { queries: m.queries } : {}),
2643
+ ...(m.surfacedResolvers ? { surfacedResolvers: m.surfacedResolvers } : {}),
2644
+ ...(m.aggregates ? { aggregates: m.aggregates } : {}),
2645
+ }));
2563
2646
  try {
2564
2647
  const parsed = JSON.parse(usdJson) as { layers?: Record<string, unknown>[] };
2565
2648
  for (const layer of parsed.layers ?? []) probes.push({ layer });
@@ -2583,12 +2666,16 @@ async function main(): Promise<void> {
2583
2666
  // the read overlay; the workspace's full spec is the runtime merge of installed packages' specs
2584
2667
  // (currentLaw); the host ingests it and builds its router from it — no domain/directive name in host
2585
2668
  // source. Self-describing for the whole web ecosystem (Swagger/codegen/Hono/agent tool-calling).
2586
- const openapi = mergeOpenApiFragments(
2669
+ const publicOpenapi = mergeOpenApiFragments(
2587
2670
  domainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash })),
2588
2671
  { title: `${cfg.name} (nomos package)`, lawHash: domainHash },
2589
2672
  );
2673
+ const runtimeOpenapi = mergeOpenApiFragments(
2674
+ domainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash, includeFrameworkRoutes: true })),
2675
+ { title: `${cfg.name} (nomos package)`, lawHash: domainHash },
2676
+ );
2590
2677
  const openapiPath = path.join(outDir, `${cfg.name}.openapi.json`);
2591
- writeFileSync(openapiPath, JSON.stringify(openapi, null, 2) + "\n", "utf8");
2678
+ writeFileSync(openapiPath, JSON.stringify(publicOpenapi, null, 2) + "\n", "utf8");
2592
2679
 
2593
2680
  // ── 4. the one-call deploy body (package only) ──
2594
2681
  // SELF-DESCRIBING: `summary` states what the (otherwise hex-opaque) package
@@ -2598,7 +2685,7 @@ async function main(): Promise<void> {
2598
2685
  const summary = {
2599
2686
  package: cfg.name,
2600
2687
  domainHash,
2601
- domains: domainModules.map((m) => ({
2688
+ domains: presentationDomainModules.map((m) => ({
2602
2689
  domain: m.domain ?? m.name,
2603
2690
  aggregates: m.aggregates.map((a) => a.id),
2604
2691
  directives: m.directives.map((d) => d.id),
@@ -2620,7 +2707,7 @@ async function main(): Promise<void> {
2620
2707
  interface: packageInterface,
2621
2708
  // THE SELF-DESCRIBING API: this package's OpenAPI spec (law-derived). The worker stages it on the
2622
2709
  // manifest overlay; a read serves the workspace's merged spec; the host builds its router from it.
2623
- openapi,
2710
+ openapi: runtimeOpenapi,
2624
2711
  // THE CLIENT/LAW COMPAT MANIFEST (§5): a DIAGNOSIS surface (law hash + interface hash + generator
2625
2712
  // provenance) the connect-time classifier reads. Frontend-only — the package bytes above ignore it.
2626
2713
  nomosCompat,
@@ -2638,14 +2725,14 @@ async function main(): Promise<void> {
2638
2725
  let birthWarnings: string[] = [];
2639
2726
  let proofNotes: string[] = [];
2640
2727
  try {
2641
- const proofSrc = generateTsProof(domainModules, { packageName: cfg.name, domainHash });
2728
+ const proofSrc = generateTsProof(presentationDomainModules, { packageName: cfg.name, domainHash, interface: packageInterface });
2642
2729
  proofPath = path.join(outDir, `${cfg.name}.proof.mts`);
2643
2730
  writeFileSync(proofPath, proofSrc, "utf8");
2644
2731
  // THE PER-DOMAIN LEGS INDEX: every domain with a creating write, so
2645
2732
  // `githolon proof --domain <key>` proves ANY of them offline (the .mts
2646
2733
  // proves the primary one live). A multi-domain package no longer locks the
2647
2734
  // proof to whichever domain happens to be first.
2648
- const legsIndex = proofLegsIndex(domainModules);
2735
+ const legsIndex = proofLegsIndex(presentationDomainModules, packageInterface);
2649
2736
  if (legsIndex !== null) {
2650
2737
  writeFileSync(path.join(outDir, `${cfg.name}.proof-legs.json`), JSON.stringify(legsIndex, null, 2) + "\n", "utf8");
2651
2738
  proofDomains = Object.keys(legsIndex.domains);
@@ -2662,7 +2749,7 @@ async function main(): Promise<void> {
2662
2749
  // itself failed, still derive the per-domain diagnostics (best-effort).
2663
2750
  if (proofSkip !== undefined && birthWarnings.length === 0) {
2664
2751
  try {
2665
- const idx = proofLegsIndex(domainModules);
2752
+ const idx = proofLegsIndex(presentationDomainModules, packageInterface);
2666
2753
  if (idx !== null) {
2667
2754
  birthWarnings = Object.values(idx.domains).flatMap((l) => l.birthDiagnostics ?? []);
2668
2755
  proofNotes = Object.values(idx.domains).flatMap((l) => l.proofDiagnostics ?? []);
@@ -2680,7 +2767,7 @@ async function main(): Promise<void> {
2680
2767
  ` queries: ${d.queries.join(", ") || "—"} counts: ${d.counts.join(", ") || "—"} sums: ${d.sums.join(", ") || "—"}`,
2681
2768
  ]),
2682
2769
  `deploy: POST ${cfg.name}.deploy.json to /v2/workspaces/<ws>/domains (or: githolon deploy <ws>)`,
2683
- `typed client: ${cfg.name}.client.ts — ${tsClientFactoryName(domainModules[0]?.domain ?? domainModules[0]?.name ?? cfg.name)}(holon), law hash baked in`,
2770
+ `typed client: ${cfg.name}.client.ts — ${tsClientFactoryName(presentationDomainModules[0]?.domain ?? presentationDomainModules[0]?.name ?? cfg.name)}(holon), law hash baked in`,
2684
2771
  proofPath !== undefined
2685
2772
  ? `generated proof: ${cfg.name}.proof.mts — run: githolon proof (live; ends ALL GREEN or names the jam)`
2686
2773
  : `generated proof: SKIPPED — ${proofSkip}`,
@@ -2691,7 +2778,7 @@ async function main(): Promise<void> {
2691
2778
  // ── 5. the typed TS client (#10 — the web sibling of codegen_dart), with the
2692
2779
  // deployed law's content hash baked in (emitted AFTER the package is hashed) ──
2693
2780
  const clientPath = path.join(outDir, `${cfg.name}.client.ts`);
2694
- writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash, interface: packageInterface, compat: nomosCompat }), "utf8");
2781
+ writeFileSync(clientPath, generateTsClient(presentationDomainModules, { packageName: cfg.name, domainHash, interface: packageInterface, compat: nomosCompat }), "utf8");
2695
2782
 
2696
2783
  // ── 6. the typed Dart frontend (opt-in; the Flutter sibling of step 5, emitted
2697
2784
  // after the package hash like the TS client) ──
@@ -2737,7 +2824,7 @@ async function main(): Promise<void> {
2737
2824
  // COMPILE ATOMICITY (co2): the Dart emit is transactional (beginAtomicGenDir) — a mid-emit fail()/throw
2738
2825
  // restores the prior good packages instead of leaving them wiped/partial. Finalized at process exit.
2739
2826
  if (dartPackages !== undefined) {
2740
- const r = emitDartPackages(domainModules, dartOut, domainHash, packageInterface, dartPackages, dartNames, nomosCompat);
2827
+ const r = emitDartPackages(presentationDomainModules, dartOut, domainHash, packageInterface, dartPackages, dartNames, nomosCompat, cfg.name);
2741
2828
  dartFiles = r.written;
2742
2829
  dartCursedHints = r.cursed;
2743
2830
  } else {
@@ -2750,7 +2837,7 @@ async function main(): Promise<void> {
2750
2837
  .replace(/^(?=[0-9])/, "n");
2751
2838
  const dartBarrel =
2752
2839
  typeof dartCfg === "object" && dartCfg.barrel !== undefined ? dartCfg.barrel : defaultBarrel;
2753
- dartFiles = emitDart(domainModules, dartOut, domainHash, packageInterface, dartSupportImport, dartBarrel, nomosCompat, dartNames);
2840
+ dartFiles = emitDart(presentationDomainModules, dartOut, domainHash, packageInterface, dartSupportImport, dartBarrel, nomosCompat, dartNames, cfg.name);
2754
2841
  }
2755
2842
  // COMPILE-VALID MUST MEAN COMPILABLE (co2): `githolon compile` reported success while the generated Dart
2756
2843
  // had hard compiler errors (a duplicate id symbol, missing contract-id defs). Run `dart analyze` over the
@@ -2801,16 +2888,29 @@ async function main(): Promise<void> {
2801
2888
  }
2802
2889
  return undefined;
2803
2890
  })();
2804
- let dartMissing = false;
2805
- let unresolved: string | undefined;
2891
+ // Package generation must not depend on which SDKs happen to be installed on the compiler's
2892
+ // machine. Write every source-dogfood override first; the analyzer loop may legitimately stop
2893
+ // at the first missing `dart`, but that must not leave later solve roots pinned to hosted code.
2806
2894
  for (const pkg of pkgDirs) {
2807
2895
  if (localDart) {
2808
2896
  const spec = readFileSync(path.join(pkg, "pubspec.yaml"), "utf8");
2809
2897
  const lines = ["dependency_overrides:"];
2810
- if (/(^|\n)\s+nomos_client\s*:/.test(spec)) lines.push(` nomos_client:`, ` path: ${localDart.client}`);
2811
- if (/(^|\n)\s+nomos_types\s*:/.test(spec)) lines.push(` nomos_types:`, ` path: ${localDart.types}`);
2898
+ const usesClient = /(^|\n)\s+nomos_client\s*:/.test(spec);
2899
+ const usesTypes = /(^|\n)\s+nomos_types\s*:/.test(spec);
2900
+ if (usesClient) lines.push(` nomos_client:`, ` path: ${localDart.client}`);
2901
+ // A generated CLIENT or TEST package may only mention nomos_client directly, but that local
2902
+ // package depends on the simultaneously-developed nomos_types. Pub overrides do not propagate
2903
+ // from dependencies, so override the transitive core at THIS solve root as well. Otherwise a
2904
+ // perfectly ordinary recompile of an older generated package can fail version solving before
2905
+ // analyzer ever sees the code — exactly the package-skew ceremony this local dogfood gate exists
2906
+ // to absorb for tenant developers.
2907
+ if (usesTypes || usesClient) lines.push(` nomos_types:`, ` path: ${localDart.types}`);
2812
2908
  if (lines.length > 1) writeFileSync(path.join(pkg, "pubspec_overrides.yaml"), lines.join("\n") + "\n", "utf8");
2813
2909
  }
2910
+ }
2911
+ let dartMissing = false;
2912
+ let unresolved: string | undefined;
2913
+ for (const pkg of pkgDirs) {
2814
2914
  try {
2815
2915
  execFileSync("dart", ["pub", "get"], { cwd: pkg, stdio: "pipe", encoding: "utf8" });
2816
2916
  } catch (e) {
@@ -2861,20 +2961,23 @@ async function main(): Promise<void> {
2861
2961
  }
2862
2962
  {
2863
2963
  const sum = (pick: (m: (typeof domainModules)[number]) => readonly unknown[] | undefined) =>
2864
- domainModules.reduce((n, m) => n + (pick(m)?.length ?? 0), 0);
2964
+ presentationDomainModules.reduce((n, m) => n + (pick(m)?.length ?? 0), 0);
2865
2965
  console.log(` reads ${sum((m) => m.queries)} quer(y/ies), ${sum((m) => m.counts)} count(s), ${sum((m) => m.sums)} sum(s) — declared (routed via the openapi)`);
2866
2966
  }
2867
2967
  // SECONDARY-CREATE AUTO-DECLARE (secondary_ensure.ts): surface every type the compiler sanctioned
2868
2968
  // from a plan's deterministic secondary `ensure(...)` — the id-mint-gate DX made visible (the author
2869
2969
  // wrote `ensure(B, …)`; the compiler declared `Ensure → B` so the deterministic id admits).
2870
- for (const m of domainModules) {
2970
+ for (const m of presentationDomainModules) {
2871
2971
  for (const ty of m.autoDeclaredEnsures ?? []) {
2872
2972
  console.log(` ensure auto-declared Ensure→${ty} in '${m.domain ?? m.name}' from a plan's secondary ensure(...) (id-mint gate opened)`);
2873
2973
  }
2874
2974
  }
2875
- console.log(` identity ${Object.keys(identity.manifests).length} domain(s) (carried in the package IR + nomos.stable-ids.json; no sidecar files)${identity.excluded.length ? ` — ${identity.excluded.length} EXCLUDED (palette gap)` : ""}`);
2876
- for (const [dom, h] of Object.entries(identity.hashes)) console.log(` ${dom.padEnd(20)} ${h}`);
2877
- for (const ex of identity.excluded) console.log(` EXCLUDED ${ex.domain}: ${ex.reason}`);
2975
+ const presentedIdentityNames = new Set(presentationDomainModules.map((m) => m.domain ?? m.name));
2976
+ const presentedHashes = Object.entries(identity.hashes).filter(([domain]) => presentedIdentityNames.has(domain));
2977
+ const presentedExcluded = identity.excluded.filter((entry) => presentedIdentityNames.has(entry.domain));
2978
+ console.log(` identity ${presentedHashes.length} business domain(s) (private Nomos lifecycle law included but not exposed)${presentedExcluded.length ? ` — ${presentedExcluded.length} EXCLUDED (palette gap)` : ""}`);
2979
+ for (const [dom, h] of presentedHashes) console.log(` ${dom.padEnd(20)} ${h}`);
2980
+ for (const ex of presentedExcluded) console.log(` EXCLUDED ${ex.domain}: ${ex.reason}`);
2878
2981
  // STABLE IDS (#58): the minted/carried identity surface + every INFERRED rename —
2879
2982
  // the silent lane made visible (if an inference is wrong — you meant remove+add —
2880
2983
  // split the edit into two compiles, or answer the evolve gate with a disposition).
@@ -2886,7 +2989,7 @@ async function main(): Promise<void> {
2886
2989
  );
2887
2990
  }
2888
2991
  }
2889
- for (const m of domainModules) {
2992
+ for (const m of presentationDomainModules) {
2890
2993
  if (m.workspaceTypes !== undefined && m.workspaceTypes.length > 0) {
2891
2994
  console.log(
2892
2995
  ` taxonomy ${m.domain ?? m.name}: ${m.workspaceTypes.length} workspace type(s) — homing derived from t.ref chains; birth lanes typed in the client`,
@@ -2895,7 +2998,7 @@ async function main(): Promise<void> {
2895
2998
  }
2896
2999
  console.log(` client ${rel(clientPath)} (typed TS client — payloads, read models, query accessors)`);
2897
3000
  for (const note of recursiveNotes) console.log(` note ${note}`);
2898
- console.log(` openapi ${rel(openapiPath)} (${Object.keys(openapi.paths).length} operation(s) — the law's self-describing API; the host's runtime routing table)`);
3001
+ console.log(` openapi ${rel(openapiPath)} (${Object.keys(publicOpenapi.paths).length} operation(s) — the public business API; private framework routes ride only in the deploy manifest)`);
2899
3002
  if (proofPath !== undefined) {
2900
3003
  console.log(` proof ${rel(proofPath)} (a runnable e2e GENERATED from your law — run: githolon proof)`);
2901
3004
  } else {
@@ -2911,9 +3014,15 @@ async function main(): Promise<void> {
2911
3014
  // gate's refusal vocabulary, unified into ONE report. `githolon check` runs the same pass;
2912
3015
  // compile runs it implicitly and REFUSES on any refuse-level finding (a statically-provable
2913
3016
  // runtime refusal shipped as law). Warns/notes are printed but never block the compile.
2914
- const checkFindings = checkDomainModules(domainModules);
3017
+ const allCheckFindings = checkDomainModules(domainModules);
3018
+ const isFrameworkFinding = (finding: (typeof allCheckFindings)[number]) =>
3019
+ finding.domain === APPLICATION_HOME_DOMAIN || finding.directiveId?.startsWith("nomos_internal_") === true;
3020
+ const checkFindings = allCheckFindings.filter((finding) => !isFrameworkFinding(finding));
3021
+ const privateRefusals = allCheckFindings.filter(
3022
+ (finding) => isFrameworkFinding(finding) && finding.level === "refuse",
3023
+ );
2915
3024
  if (checkFindings.length > 0) {
2916
- console.log(` check ${checkFindings.length} finding(s) (githolon check):`);
3025
+ console.log(` check ${checkFindings.length} business-law finding(s) (githolon check):`);
2917
3026
  for (const line of formatFindings(checkFindings)) {
2918
3027
  const mark = line.includes("REFUSE") ? "✗" : line.includes("WARN") ? "⚠" : "•";
2919
3028
  console.log(` ${mark} ${line}`);
@@ -2925,6 +3034,16 @@ async function main(): Promise<void> {
2925
3034
  process.exitCode = 1;
2926
3035
  }
2927
3036
  }
3037
+ if (privateRefusals.length > 0) {
3038
+ console.error(
3039
+ ` ✗ Nomos lifecycle self-check failed inside this compiler release. Tenant law is not at fault; ` +
3040
+ `upgrade @githolon/dsl or report the compiler version.`,
3041
+ );
3042
+ if (process.env.NOMOS_DEBUG_FRAMEWORK === "1") {
3043
+ for (const line of formatFindings(privateRefusals)) console.error(` ${line}`);
3044
+ }
3045
+ process.exitCode = 1;
3046
+ }
2928
3047
  if (dartOut !== undefined) {
2929
3048
  if (dartPackages !== undefined) {
2930
3049
  const pkgList = [