@githolon/dsl 0.103.4 → 0.103.6

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 (40) hide show
  1. package/dart/CHANGELOG.md +8 -0
  2. package/dart/lib/src/runner/node-entry.mjs +333 -117
  3. package/dart/lib/src/runner/offline-install.mjs +173 -67
  4. package/dart/lib/src/session_bound.dart +111 -2
  5. package/dart/lib/src/test_harness.dart +51 -41
  6. package/dart/lib/src/webview_bridge.dart +714 -60
  7. package/dart/pubspec.lock +1 -1
  8. package/dart/pubspec.yaml +2 -2
  9. package/dart/test/acquire_session_test.dart +62 -5
  10. package/dart/test/cloud_bound_topology_test.dart +59 -38
  11. package/dart/test/session_bound_test.dart +25 -10
  12. package/dart/test/webview_bridge_test.dart +326 -7
  13. package/dart_types/CHANGELOG.md +5 -0
  14. package/dart_types/lib/nomos_types.dart +1 -0
  15. package/dart_types/lib/src/git_object_id.dart +37 -0
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/package.json +1 -1
  18. package/runtime/generated-application-host.js +347 -113
  19. package/src/births.ts +16 -1
  20. package/src/business_birth.ts +55 -34
  21. package/src/codegen_dart.ts +68 -12
  22. package/src/codegen_dart_application.ts +114 -19
  23. package/src/codegen_dart_client.ts +149 -6
  24. package/src/codegen_proof.ts +16 -9
  25. package/src/codegen_provider_dart.ts +5 -0
  26. package/src/codegen_ts.ts +115 -19
  27. package/src/compile_package_main.ts +159 -43
  28. package/src/fields.ts +43 -0
  29. package/src/framework/domain_lifecycle.ts +108 -88
  30. package/src/framework/genesis.ts +2 -1
  31. package/src/framework/home.ts +6 -43
  32. package/src/framework/workspaces.ts +42 -25
  33. package/src/idof_lint.ts +65 -51
  34. package/src/index.ts +21 -1
  35. package/src/intent.ts +11 -1
  36. package/src/law_graph.ts +81 -11
  37. package/src/relation_schema.ts +18 -0
  38. package/src/shape_compat.ts +3 -0
  39. package/src/stable_ids_for_module.ts +7 -1
  40. package/src/value_object.ts +40 -0
@@ -798,6 +798,9 @@ const DART_SUPPORT_CORE_SRC = [
798
798
  "driver.dart",
799
799
  "offer.dart",
800
800
  "subscriptions.dart",
801
+ // Nominal custody/content identities used by generated payloads and read models.
802
+ "sha256_digest.dart",
803
+ "git_object_id.dart",
801
804
  // The tri-state slot (NomosSlot<T>) for `nullable`/`clearable` fields (null_semantics.md) — the
802
805
  // barrel re-exports it and generated read-model/payload classes reference it, so it must vendor.
803
806
  "null_slot.dart",
@@ -845,13 +848,14 @@ const DART_SUPPORT_SRC = [...DART_SUPPORT_CORE_SRC, ...DART_SUPPORT_RUNTIME_SRC]
845
848
  /** The local shim replacing `package:nomos_types/nomos_types.dart` inside vendored runtime files. */
846
849
  const VENDORED_TYPES_SHIM = "nomos_types.dart";
847
850
  const VENDORED_META_DEP = "^1.12.0";
851
+ const VENDORED_DART_SDK_FLOOR = "3.3.0";
848
852
 
849
853
  /**
850
854
  * Vendored runtime files carry package:meta's @internal boundary. Keep the
851
855
  * containing Dart package honest by declaring that implementation dependency
852
856
  * instead of stripping the annotation from generated copies.
853
857
  */
854
- function ensureVendoredMetaDependency(dartOut: string): void {
858
+ function ensureVendoredPackageRequirements(dartOut: string): void {
855
859
  let dir = path.resolve(dartOut);
856
860
  let pubspec: string | undefined;
857
861
  for (let hops = 0; hops < 8; hops++) {
@@ -866,13 +870,37 @@ function ensureVendoredMetaDependency(dartOut: string): void {
866
870
  }
867
871
  if (pubspec === undefined) return;
868
872
  const source = readFileSync(pubspec, "utf8");
869
- if (/^ meta:\s*/m.test(source)) return;
870
- if (!/^dependencies:\s*$/m.test(source)) {
873
+ let next = source;
874
+ const sdkLine = /^(\s+sdk:\s*)([^#\r\n]+)(\s*(?:#.*)?)$/m.exec(next);
875
+ if (sdkLine === null) {
876
+ fail("generated Dart semantic types need an environment.sdk constraint in " + pubspec);
877
+ }
878
+ const declared = sdkLine[2]!.trim().replace(/^['"]|['"]$/g, "");
879
+ const firstVersion = /(\d+)\.(\d+)(?:\.\d+)?/.exec(declared);
880
+ if (firstVersion === null) {
881
+ fail(
882
+ `generated Dart semantic types need SDK >=${VENDORED_DART_SDK_FLOOR}; ` +
883
+ `cannot interpret environment.sdk '${declared}' in ${pubspec}`,
884
+ );
885
+ }
886
+ const major = Number(firstVersion[1]);
887
+ const minor = Number(firstVersion[2]);
888
+ if (major < 3 || (major === 3 && minor < 3)) {
889
+ next = next.replace(
890
+ sdkLine[0],
891
+ `${sdkLine[1]}\">=${VENDORED_DART_SDK_FLOOR} <4.0.0\"${sdkLine[3] ?? ""}`,
892
+ );
893
+ }
894
+ if (/^ meta:\s*/m.test(next)) {
895
+ if (next !== source) writeFileSync(pubspec, next, "utf8");
896
+ return;
897
+ }
898
+ if (!/^dependencies:\s*$/m.test(next)) {
871
899
  fail("generated Dart vendored support needs a dependencies: block in " + pubspec);
872
900
  }
873
901
  writeFileSync(
874
902
  pubspec,
875
- source.replace(
903
+ next.replace(
876
904
  /^dependencies:\s*$/m,
877
905
  "dependencies:\n meta: " + VENDORED_META_DEP,
878
906
  ),
@@ -930,7 +958,7 @@ function buildPackageAggregateDomains(
930
958
  }
931
959
 
932
960
  /**
933
- * CIRCULAR-DEPENDENCY GUARD (domain_contracts.md). A `t.foreignRef` targeting a CONCRETE aggregate makes
961
+ * CIRCULAR-DEPENDENCY GUARD (domain_contracts.md). A typed cross-workspace reference targeting a CONCRETE aggregate makes
934
962
  * the referencing domain's generated file import the target domain's file. Two domains that concretely
935
963
  * reference each other (A→B and B→A) form an import cycle — the exact anti-pattern domain contracts exist
936
964
  * to break. Build the concrete domain→domain edge set and FAIL on any 2-cycle, pointing at contracts as the
@@ -943,8 +971,11 @@ function guardNoConcreteImportCycle(
943
971
  const edges = new Map<string, Set<string>>(); // fromDomainFile → { toDomainFile }
944
972
  for (const mod of modules) {
945
973
  for (const agg of mod.aggregates) {
946
- for (const f of Object.values(agg.fields as Record<string, { foreignRef?: { targetAggregateId: string; targetIsContract?: boolean } }>)) {
947
- const fr = f.foreignRef;
974
+ for (const f of Object.values(agg.fields as Record<string, {
975
+ foreignRef?: { targetAggregateId: string; targetIsContract?: boolean };
976
+ nomosRef?: { targetAggregateId: string; targetIsContract?: boolean };
977
+ }>)) {
978
+ const fr = f.foreignRef ?? f.nomosRef;
948
979
  if (fr === undefined || fr.targetIsContract === true) continue; // contract edges route via contracts.dart
949
980
  const to = aggIdToDartFile.get(fr.targetAggregateId);
950
981
  if (to === undefined || to === mod.name) continue;
@@ -957,9 +988,9 @@ function guardNoConcreteImportCycle(
957
988
  if (edges.get(to)?.has(from) && from < to) {
958
989
  fail(
959
990
  `domain contracts: concrete cross-domain import cycle between '${from}' and '${to}' — their ` +
960
- `generated Dart files would import each other (a t.foreignRef in each targets a concrete aggregate ` +
991
+ `generated Dart files would import each other (a typed custody reference in each targets a concrete aggregate ` +
961
992
  `owned by the other). Break it with a record contract: declare recordContract(...) for the shared shape, ` +
962
- `.implements(...) it on the owning aggregate, and point the foreignRef at the CONTRACT (it resolves ` +
993
+ `.implements(...) it on the owning aggregate, and point the reference at the CONTRACT (it resolves ` +
963
994
  `through the shared contracts.dart, keeping the domain graph acyclic).`,
964
995
  );
965
996
  }
@@ -1291,7 +1322,7 @@ function emitDart(
1291
1322
  // 1. optional vendored framework support (barrel + src/), copied byte-for-byte except
1292
1323
  // the barrel's schema_validation export (see DART_SUPPORT_SRC).
1293
1324
  if (supportImport === "vendored") {
1294
- ensureVendoredMetaDependency(dartOut);
1325
+ ensureVendoredPackageRequirements(dartOut);
1295
1326
  mkdirSync(path.join(genDir, "src"), { recursive: true });
1296
1327
  // Core files come from the pure `nomos_types` package, runtime files from `nomos_client`
1297
1328
  // (the layered split). Runtime files import the core via `package:nomos_types/...` — the
@@ -1427,14 +1458,17 @@ function emitDart(
1427
1458
  // framework import. Only pass the import when this domain actually folds a framework symbol.
1428
1459
  const { modForDart, skips, foldsFramework } = domainDedupOpts(mod, shared);
1429
1460
  const domainFrameworkImport = foldsFramework ? frameworkImportUri : undefined;
1430
- // AUTO cross-domain foreignRef imports: for each foreignRef whose target aggregate lives in ANOTHER
1431
- // tenant domain, import that domain's generated file so `ForeignRef<Target>` + `Target.fromJson` resolve.
1461
+ // AUTO cross-domain typed-reference imports: foreignRef and atomic nomosRef targets in ANOTHER tenant
1462
+ // domain need that domain's generated type so their typed Dart locator can compile.
1432
1463
  // A target that is framework-shared resolves via the frameworkImportUri (not in the tenant map), so it is
1433
1464
  // skipped here. Merged with any manual `dartImports`; deduped by uri.
1434
1465
  const foreignRefImportUris = new Set<string>();
1435
- for (const agg of (modForDart.aggregates as { fields: Record<string, { foreignRef?: { targetAggregateId: string; targetIsContract?: boolean } }> }[])) {
1466
+ for (const agg of (modForDart.aggregates as { fields: Record<string, {
1467
+ foreignRef?: { targetAggregateId: string; targetIsContract?: boolean };
1468
+ nomosRef?: { targetAggregateId: string; targetIsContract?: boolean };
1469
+ }> }[])) {
1436
1470
  for (const f of Object.values(agg.fields)) {
1437
- const fr = f.foreignRef;
1471
+ const fr = f.foreignRef ?? f.nomosRef;
1438
1472
  if (fr === undefined) continue;
1439
1473
  // A CONTRACT target resolves through the shared `contracts.dart` (the acyclic edge), NEVER the
1440
1474
  // concrete target domain — so estate never imports catalogue. (The contract id may EQUAL an
@@ -1519,6 +1553,9 @@ function emitDart(
1519
1553
  const sha256DigestExport = [...sourceByFile.values()].some((source) => source.includes("Sha256Digest"))
1520
1554
  ? ", Sha256Digest"
1521
1555
  : "";
1556
+ const gitObjectIdExport = [...sourceByFile.values()].some((source) => source.includes("GitObjectId"))
1557
+ ? ", GitObjectId"
1558
+ : "";
1522
1559
  const barrelLines = [
1523
1560
  "// GENERATED by nomos-compile — the package's public entrypoint. DO NOT EDIT.",
1524
1561
  "// Regenerated on every compile; generated implementation lives under src/generated/.",
@@ -1542,8 +1579,8 @@ function emitDart(
1542
1579
  // The generated clients import their runtime support directly. Keeping it out of the public barrel is
1543
1580
  // the surface firewall: a tenant import receives business types and application APIs, never raw runtime.
1544
1581
  supportImport === "vendored"
1545
- ? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceFreshness, ReferenceCurrent, ReferenceStale, ReferenceFreshnessUnavailable${sha256DigestExport};`
1546
- : `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceFreshness, ReferenceCurrent, ReferenceStale, ReferenceFreshnessUnavailable${sha256DigestExport};`,
1582
+ ? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceFreshness, ReferenceCurrent, ReferenceStale, ReferenceFreshnessUnavailable${sha256DigestExport}${gitObjectIdExport};`
1583
+ : `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceFreshness, ReferenceCurrent, ReferenceStale, ReferenceFreshnessUnavailable${sha256DigestExport}${gitObjectIdExport};`,
1547
1584
  ...(contractsImportUri !== undefined
1548
1585
  ? tenantDartExport(`${genRel}/${CONTRACTS_FILE}.dart`, sourceByFile.get(CONTRACTS_FILE) ?? "")
1549
1586
  : []),
@@ -1605,12 +1642,12 @@ const dartTypesImportFor = (generatedSource: string): string => {
1605
1642
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
1606
1643
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
1607
1644
  // the pendingConflicts read rows.
1608
- const NOMOS_TYPES_DEP = "^0.8.2";
1645
+ const NOMOS_TYPES_DEP = "^0.8.3";
1609
1646
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1610
1647
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1611
1648
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1612
- const NOMOS_CLIENT_DEP = "^0.64.5";
1613
- const NOMOS_FLUTTER_DEP = "^0.63.6";
1649
+ const NOMOS_CLIENT_DEP = "^0.64.6";
1650
+ const NOMOS_FLUTTER_DEP = "^0.63.8";
1614
1651
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1615
1652
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1616
1653
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -1847,7 +1884,7 @@ function generatedPackageApplication(
1847
1884
  `import 'package:flutter/widgets.dart';`,
1848
1885
  `import 'package:nomos_flutter/src/application_status.dart' show ApplicationSignIn, ApplicationStatus;`,
1849
1886
  `import 'package:nomos_client/src/nms_generated_runtime.dart' show NmsInternalUsers, NomosSession, User${hasInvitations ? ", Invitation, Recipient" : ""};`,
1850
- `import 'package:nomos_flutter/src/generated_application_host.dart' show NomosApplicationStartupState, NmsInternalApplicationHost, NmsInternalApplicationUserBinding${hasInvitations ? ", NmsInternalInvitationPolicy" : ""};`,
1887
+ `import 'package:nomos_flutter/src/generated_application_host.dart' show ApplicationModelChange, NomosApplicationStartupState, NmsInternalApplicationHost, NmsInternalApplicationUserBinding${hasInvitations ? ", NmsInternalInvitationPolicy" : ""};`,
1851
1888
  ...members.map(({ file, prefix }) =>
1852
1889
  `import '${file}.application.dart' as ${prefix};`),
1853
1890
  ``,
@@ -1881,10 +1918,14 @@ function generatedPackageApplication(
1881
1918
  ` ${rootMember.prefix}.NomosModel get model => ${rootMember.member}.model;`,
1882
1919
  ``,
1883
1920
  ` /// Current durability and convergence across this business application.`,
1884
- ` ApplicationStatus get currentStatus => NmsInternalApplicationHost.currentStatus(_nmsSession);`,
1921
+ ` ApplicationStatus get currentStatus => NmsInternalApplicationHost.currentApplicationStatus(_nmsSession);`,
1885
1922
  ``,
1886
1923
  ` /// React whenever this application saves, receives work, goes offline or needs attention.`,
1887
- ` Stream<ApplicationStatus> watchStatus() => NmsInternalApplicationHost.watchStatus(_nmsSession);`,
1924
+ ` Stream<ApplicationStatus> watchStatus() => NmsInternalApplicationHost.watchApplicationStatus(_nmsSession);`,
1925
+ ``,
1926
+ ` /// Wait until accepted local work is in shared custody and this application has caught up.`,
1927
+ ` Future<void> whenCurrent({Duration timeout = const Duration(minutes: 2)}) =>`,
1928
+ ` NmsInternalApplicationHost.whenApplicationCurrent(_nmsSession, timeout: timeout);`,
1888
1929
  ...(hasInvitations
1889
1930
  ? [
1890
1931
  ``,
@@ -1930,12 +1971,16 @@ function generatedPackageApplication(
1930
1971
  ` required Widget Function(BuildContext context, ${application} application) builder,`,
1931
1972
  ` Widget Function(BuildContext context)? loadingBuilder,`,
1932
1973
  ` Widget Function(BuildContext context, Object error, VoidCallback retry)? errorBuilder,`,
1974
+ ` ApplicationModelChange? onModelChange,`,
1933
1975
  ` ValueChanged<NomosApplicationStartupState>? onStartupStateChanged,`,
1976
+ ` Duration startupTimeout = const Duration(seconds: 45),`,
1934
1977
  ` }) => NmsInternalApplicationHost.home<${application}>(`,
1935
1978
  ` key: key, cloud: cloud, application: application, signIn: signIn,`,
1936
1979
  ` bind: nomosInternalBind${application}, builder: builder,`,
1937
1980
  ` loadingBuilder: loadingBuilder, errorBuilder: errorBuilder,`,
1981
+ ` onModelChange: onModelChange,`,
1938
1982
  ` onStartupStateChanged: onStartupStateChanged,`,
1983
+ ` startupTimeout: startupTimeout,`,
1939
1984
  ` );`,
1940
1985
  `}`,
1941
1986
  ``,
@@ -2059,22 +2104,34 @@ function reconcileGeneratedLawAsset(pkgDir: string, generatedAsset: string): boo
2059
2104
  const p = path.join(pkgDir, "pubspec.yaml");
2060
2105
  if (!existsSync(p)) return false;
2061
2106
  const source = readFileSync(p, "utf8");
2107
+ const trailingNewline = source.endsWith("\n");
2062
2108
  const lines = source.split(/\r?\n/);
2109
+ if (trailingNewline) lines.pop();
2063
2110
  const compilerAsset = /^\s+-\s+lib\/src\/generated\/[^\s]+\.(?:deploy\.json|law\.usdz)\s*$/;
2064
2111
  const replacement = ` - ${generatedAsset}`;
2065
- let replaced = false;
2066
- const nextLines = lines.filter((line) => {
2067
- if (!compilerAsset.test(line)) return true;
2068
- if (replaced) return false;
2069
- replaced = true;
2070
- return false;
2071
- });
2072
- if (replaced) {
2073
- const assetHeader = nextLines.findIndex((line) => /^\s{2}assets:\s*$/.test(line));
2074
- if (assetHeader < 0) return false;
2112
+ const nextLines = lines.filter((line) => !compilerAsset.test(line));
2113
+
2114
+ let flutterStart = nextLines.findIndex((line) => /^flutter:\s*(?:#.*)?$/.test(line));
2115
+ if (flutterStart < 0) {
2116
+ if (nextLines.length > 0 && nextLines.at(-1) !== "") nextLines.push("");
2117
+ flutterStart = nextLines.length;
2118
+ nextLines.push("flutter:");
2119
+ }
2120
+ let flutterEnd = flutterStart + 1;
2121
+ while (flutterEnd < nextLines.length) {
2122
+ const line = nextLines[flutterEnd]!;
2123
+ if (line.trim() !== "" && !/^\s/.test(line) && !/^\s*#/.test(line)) break;
2124
+ flutterEnd += 1;
2125
+ }
2126
+ const assetHeader = nextLines.findIndex(
2127
+ (line, index) => index > flutterStart && index < flutterEnd && /^\s{2}assets:\s*(?:#.*)?$/.test(line),
2128
+ );
2129
+ if (assetHeader < 0) {
2130
+ nextLines.splice(flutterEnd, 0, " assets:", replacement);
2131
+ } else {
2075
2132
  nextLines.splice(assetHeader + 1, 0, replacement);
2076
2133
  }
2077
- const next = nextLines.join("\n");
2134
+ const next = `${nextLines.join("\n")}${trailingNewline ? "\n" : ""}`;
2078
2135
  if (next === source) return false;
2079
2136
  writeFileSync(p, next, "utf8");
2080
2137
  return true;
@@ -2196,9 +2253,12 @@ function emitDartPackages(
2196
2253
  const perDomain = modules.map((mod) => ({ mod, dd: domainDedupOpts(mod, shared) }));
2197
2254
  for (const { mod, dd } of perDomain) {
2198
2255
  const frImports = new Set<string>();
2199
- for (const agg of (dd.modForDart.aggregates as { fields: Record<string, { foreignRef?: { targetAggregateId: string; targetIsContract?: boolean } }> }[])) {
2256
+ for (const agg of (dd.modForDart.aggregates as { fields: Record<string, {
2257
+ foreignRef?: { targetAggregateId: string; targetIsContract?: boolean };
2258
+ nomosRef?: { targetAggregateId: string; targetIsContract?: boolean };
2259
+ }> }[])) {
2200
2260
  for (const f of Object.values(agg.fields)) {
2201
- const fr = f.foreignRef;
2261
+ const fr = f.foreignRef ?? f.nomosRef;
2202
2262
  if (fr === undefined) continue;
2203
2263
  // A contract target resolves via contracts.dart (the acyclic edge), not the concrete domain file.
2204
2264
  if (fr.targetIsContract === true) {
@@ -2290,11 +2350,14 @@ function emitDartPackages(
2290
2350
  const sha256DigestExport = [...typesSources.values()].some((source) => source.includes("Sha256Digest"))
2291
2351
  ? ", Sha256Digest"
2292
2352
  : "";
2353
+ const gitObjectIdExport = [...typesSources.values()].some((source) => source.includes("GitObjectId"))
2354
+ ? ", GitObjectId"
2355
+ : "";
2293
2356
  writeFileSync(
2294
2357
  businessSharedPath,
2295
2358
  [
2296
2359
  "// GENERATED by nomos-compile — shared business values and activity; no runtime machinery.",
2297
- `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceFreshness, ReferenceCurrent, ReferenceStale, ReferenceFreshnessUnavailable${sha256DigestExport}, Identity, User, Recipient, Invitation, ActivityEvent, ActivityRef, ActivitySeverity, SurfacedObservation, SurfacedValue, SurfacedResolved, SurfacedPending, SurfacedEvolutionRequired;`,
2360
+ `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceFreshness, ReferenceCurrent, ReferenceStale, ReferenceFreshnessUnavailable${sha256DigestExport}${gitObjectIdExport}, Identity, User, Recipient, Invitation, ActivityEvent, ActivityRef, ActivitySeverity, SurfacedObservation, SurfacedValue, SurfacedResolved, SurfacedPending, SurfacedEvolutionRequired;`,
2298
2361
  ...(sharedBusinessTypeNames.length === 0
2299
2362
  ? []
2300
2363
  : [`export '${FRAMEWORK_FILE}.dart' show ${sharedBusinessTypeNames.join(", ")};`]),
@@ -2358,6 +2421,17 @@ function emitDartPackages(
2358
2421
 
2359
2422
  // ── the CLIENT package — effectful runtime, depends on types + nomos_client ──
2360
2423
  const clientGen = freshGenDir(packages.client);
2424
+ const embeddedLawName = `${describePackageName}.law.usdz`;
2425
+ const applicationLawPath = lawUsdzPath === undefined
2426
+ ? undefined
2427
+ : path.join(clientGen, embeddedLawName);
2428
+ if (applicationLawPath !== undefined) {
2429
+ copyFileSync(lawUsdzPath!, applicationLawPath);
2430
+ written.push(applicationLawPath);
2431
+ }
2432
+ const applicationLawAssetKey = applicationLawPath === undefined
2433
+ ? undefined
2434
+ : `packages/${packages.client}/lib/${genRel}/${embeddedLawName}`;
2361
2435
  const typesImportUri = `package:${packages.types}/${packages.types}.dart`;
2362
2436
  const implementationTypeImports = explicitBarrel
2363
2437
  ? modules.map((m) => `package:${packages.types}/${genRel}/${m.name}.dart`)
@@ -2371,6 +2445,9 @@ function emitDartPackages(
2371
2445
  for (const { mod, dd } of perDomain) {
2372
2446
  const makeClient = (imports: readonly string[]) => generateDartBridgeClient(dd.modForDart, {
2373
2447
  domainHash,
2448
+ ...(applicationLawAssetKey !== undefined
2449
+ ? { lawAssetKey: applicationLawAssetKey }
2450
+ : {}),
2374
2451
  interface: packageInterface[dd.modForDart.domain ?? dd.modForDart.name],
2375
2452
  ...(compat !== undefined ? { compat } : {}),
2376
2453
  supportImport: "package",
@@ -2467,6 +2544,7 @@ function emitDartPackages(
2467
2544
  "library;",
2468
2545
  "",
2469
2546
  `export 'package:${packages.types}/${genRel}/${businessSharedFile}';`,
2547
+ `export 'package:nomos_flutter/src/generated_application_host.dart' show ApplicationModelDecision, ApplicationModelReview;`,
2470
2548
  ...tenantTypesExportKeys
2471
2549
  .map((file) => {
2472
2550
  const names = tenantDartExportNames(typesSources.get(file) ?? "");
@@ -2514,16 +2592,28 @@ function emitDartPackages(
2514
2592
  ` nomos_flutter: ${NOMOS_FLUTTER_DEP}`,
2515
2593
  ` ${packages.types}:`,
2516
2594
  ` path: ../${packages.types} # co-emitted sibling; replace with a hosted constraint when publishing`,
2595
+ ...(applicationLawPath === undefined
2596
+ ? []
2597
+ : [
2598
+ `flutter:`,
2599
+ ` assets:`,
2600
+ ` - lib/${genRel}/${embeddedLawName}`,
2601
+ ]),
2517
2602
  ``,
2518
2603
  ].join("\n"),
2519
2604
  { nomos_client: NOMOS_CLIENT_DEP, nomos_flutter: NOMOS_FLUTTER_DEP },
2520
2605
  );
2606
+ if (applicationLawPath !== undefined) {
2607
+ reconcileGeneratedLawAsset(
2608
+ pkgDir(packages.client),
2609
+ `lib/${genRel}/${embeddedLawName}`,
2610
+ );
2611
+ }
2521
2612
 
2522
2613
  // ── the TEST package (optional) — generated business scenarios, test-only dependency ──
2523
2614
  if (packages.test !== undefined) {
2524
2615
  const testGen = freshGenDir(packages.test);
2525
2616
  if (lawUsdzPath === undefined) fail("dart.packages.test requires the compiled canonical law archive");
2526
- const embeddedLawName = `${describePackageName}.law.usdz`;
2527
2617
  const embeddedLawPath = path.join(testGen, embeddedLawName);
2528
2618
  copyFileSync(lawUsdzPath, embeddedLawPath);
2529
2619
  written.push(embeddedLawPath);
@@ -2791,6 +2881,12 @@ async function main(): Promise<void> {
2791
2881
  "Nomos will not compile a birth-capable home with an implicit quota",
2792
2882
  );
2793
2883
  }
2884
+ if (cfg.application !== undefined && cfg.rootPart === undefined) {
2885
+ fail(
2886
+ "application policy requires rootPart: { name, kind }; " +
2887
+ "Nomos will not silently ignore private application-home law on a loose law fragment",
2888
+ );
2889
+ }
2794
2890
  // A born application home may carry several co-resident law namespaces. BirthPolicy is namespaced
2795
2891
  // law-state, so genesis must seed every domain that can itself birth; seeding only the private home
2796
2892
  // domain would leave (for example) `home/birthEstate` correctly refusing its first offer. Discover the
@@ -3653,6 +3749,13 @@ async function main(): Promise<void> {
3653
3749
  nextLock.identity = nextIdentity;
3654
3750
  const continuity = reconciled.continuity;
3655
3751
  const inferred = proposed.inferred;
3752
+ // Resolve the complete CURRENT stable-id surface exactly once. `continuity`
3753
+ // deliberately starts from the prior release, whereas this value also contains
3754
+ // contracts and explicit evolution routes authored by the candidate. Building
3755
+ // USDA from `continuity` made the first compile update only the lockfile and the
3756
+ // second compile finally publish those semantics into custody.
3757
+ mod.stableIdContinuity = continuity;
3758
+ const emittedStableIds = stableIdsForModule(mod);
3656
3759
  // W4 typed compiler graph: reconcile independently-lived intent/read identities,
3657
3760
  // link every current target as a kind-safe ref, and refuse unresolved structure
3658
3761
  // before any USDA string is rendered. Later W4 stages move emitters/codegen onto
@@ -3661,7 +3764,7 @@ async function main(): Promise<void> {
3661
3764
  // independently of domain declaration order. Build custody from that same closed view used by generated
3662
3765
  // clients/interfaces; using the pre-closure executable module here published a USDA-only `birth` route.
3663
3766
  const graphModule = closedDomainModulesByName.get(mod.name) ?? mod;
3664
- const discoveredGraph = discoverLawGraph(graphModule, continuity, nextIdentity);
3767
+ const discoveredGraph = discoverLawGraph(graphModule, emittedStableIds, nextIdentity);
3665
3768
  for (const node of discoveredGraph.graph.nodes) {
3666
3769
  if (node.kind === "intent" && node.births && node.birthPolicySeed === undefined) {
3667
3770
  fail(
@@ -3674,7 +3777,6 @@ async function main(): Promise<void> {
3674
3777
  nextIdentity = discoveredGraph.registry;
3675
3778
  nextLock.identity = nextIdentity;
3676
3779
  typedLawGraphs.set(identityName, discoveredGraph.graph);
3677
- mod.stableIdContinuity = continuity;
3678
3780
  graphModule.stableIdContinuity = continuity;
3679
3781
  if (inferred.length > 0) inferredRenames.push({ domain: identityName, renames: inferred });
3680
3782
  // ── THE fromBirth EVOLVE GUARD (dart_codegen_parity.md): birth-presence may be declared at a
@@ -3709,7 +3811,7 @@ async function main(): Promise<void> {
3709
3811
  // The lockfile records the EMITTED surface (sids + lineage + kinds — the next
3710
3812
  // compile's pair rule reads the kinds), not the bare continuity.
3711
3813
  nextLock.domains[identityName] = {
3712
- stableIds: stableIdsForModule(mod),
3814
+ stableIds: emittedStableIds,
3713
3815
  schemas: Object.fromEntries(currentAggs.map((a) => [a.id, a.schema])),
3714
3816
  };
3715
3817
  }
@@ -5459,6 +5561,13 @@ async function main(): Promise<void> {
5459
5561
  writeFileSync(file, text, "utf8");
5460
5562
  };
5461
5563
  for (const pkg of pkgDirs) {
5564
+ // Layered Dart output is generated-owned, including the compiler's temporary source-dogfood
5565
+ // override. A package may have been generated previously by a source checkout and then rebuilt
5566
+ // by the published compiler. In that transition, doing nothing here leaves the old absolute
5567
+ // checkout paths live and silently defeats the hosted-package release boundary. Clear the
5568
+ // compiler-owned file on every compile, then recreate it only for an actual source checkout.
5569
+ const overrideFile = path.join(pkg, "pubspec_overrides.yaml");
5570
+ rmSync(overrideFile, { force: true });
5462
5571
  if (localDart) {
5463
5572
  const spec = readFileSync(path.join(pkg, "pubspec.yaml"), "utf8");
5464
5573
  const lines = ["dependency_overrides:"];
@@ -5479,7 +5588,7 @@ async function main(): Promise<void> {
5479
5588
  // when its own pubspec does not name nomos_flutter directly.
5480
5589
  if ((usesFlutter || usesClient) && localDart.flutter) lines.push(` nomos_flutter:`, ` path: ${localDart.flutter}`);
5481
5590
  if (lines.length > 1) {
5482
- writeTextIfChanged(path.join(pkg, "pubspec_overrides.yaml"), lines.join("\n") + "\n");
5591
+ writeTextIfChanged(overrideFile, lines.join("\n") + "\n");
5483
5592
  }
5484
5593
  }
5485
5594
  }
@@ -5503,9 +5612,16 @@ async function main(): Promise<void> {
5503
5612
  "pubspec_overrides.yaml",
5504
5613
  path.join(".dart_tool", "package_config.json"),
5505
5614
  ].map((name) => ({ name, file: path.join(pkg, name) }));
5506
- if (files.some(({ file }) => !existsSync(file))) return undefined;
5615
+ // `pubspec_overrides.yaml` is intentionally absent for installed compilers. Its absence is an
5616
+ // input to the proof, not proof that dependency resolution can never be current.
5617
+ if (files.some(({ name, file }) => name !== "pubspec_overrides.yaml" && !existsSync(file))) return undefined;
5507
5618
  const hash = createHash("sha256").update(`${dartResolutionFormat}\0`);
5508
- for (const { name, file } of files) hash.update(name).update("\0").update(readFileSync(file)).update("\0");
5619
+ for (const { name, file } of files) {
5620
+ hash.update(name).update("\0");
5621
+ if (existsSync(file)) hash.update("present\0").update(readFileSync(file));
5622
+ else hash.update("absent");
5623
+ hash.update("\0");
5624
+ }
5509
5625
  return hash.digest("hex");
5510
5626
  };
5511
5627
  const dartPackageResolutionCurrent = (pkg: string): boolean => {
package/src/fields.ts CHANGED
@@ -162,12 +162,21 @@ export interface VersionedNomosRefValue {
162
162
  */
163
163
  export type Sha256Digest = string & { readonly __nomosSha256Digest: unique symbol };
164
164
 
165
+ /** A Git object identity in Nomos' current SHA-1 custody repositories.
166
+ *
167
+ * This is not a content-law digest. Keeping it nominal prevents a commit/tree address from being
168
+ * accepted by a field that promises a 256-bit semantic identity merely because both travel as text.
169
+ */
170
+ export type GitObjectId = string & { readonly __nomosGitObjectId: unique symbol };
171
+
165
172
  /** Codegen marker for the semantic SHA-256 string type. The marker is deliberately
166
173
  * non-enumerable: Zod remains the runtime/admission schema, while emitters recover
167
174
  * the stronger client type without adding a parallel manifest representation. */
168
175
  export const NOMOS_SHA256_DIGEST_BRAND: unique symbol = Symbol.for("nomos.sha256Digest");
176
+ export const NOMOS_GIT_OBJECT_ID_BRAND: unique symbol = Symbol.for("nomos.gitObjectId");
169
177
 
170
178
  const SHA256_DIGEST_RE = /^[0-9a-f]{64}$/;
179
+ const GIT_OBJECT_ID_RE = /^[0-9a-f]{40}$/;
171
180
 
172
181
  /** Declare and validate a lowercase hexadecimal SHA-256 digest payload value. */
173
182
  export function sha256Digest(): z.ZodType<Sha256Digest> {
@@ -176,6 +185,13 @@ export function sha256Digest(): z.ZodType<Sha256Digest> {
176
185
  return schema as unknown as z.ZodType<Sha256Digest>;
177
186
  }
178
187
 
188
+ /** Declare and validate the object identity used by the current gix custody format. */
189
+ export function gitObjectId(): z.ZodType<GitObjectId> {
190
+ const schema = z.string().regex(GIT_OBJECT_ID_RE, "expected a lowercase hexadecimal Git object id");
191
+ Object.defineProperty(schema, NOMOS_GIT_OBJECT_ID_BRAND, { value: true, enumerable: false });
192
+ return schema as unknown as z.ZodType<GitObjectId>;
193
+ }
194
+
179
195
  /** Recover the SHA-256 semantic marker through omission/null wrappers. */
180
196
  export function isSha256DigestSchema(schema: z.ZodTypeAny): boolean {
181
197
  let current: z.ZodTypeAny | undefined = schema;
@@ -199,6 +215,29 @@ export function isSha256DigestSchema(schema: z.ZodTypeAny): boolean {
199
215
  return false;
200
216
  }
201
217
 
218
+ /** Recover the Git object-id semantic marker through omission/null wrappers. */
219
+ export function isGitObjectIdSchema(schema: z.ZodTypeAny): boolean {
220
+ let current: z.ZodTypeAny | undefined = schema;
221
+ for (let hops = 0; current !== undefined && hops < 16; hops++) {
222
+ if ((current as unknown as Record<symbol, unknown>)[NOMOS_GIT_OBJECT_ID_BRAND] === true) {
223
+ return true;
224
+ }
225
+ const raw = current as unknown as {
226
+ _def?: { type?: unknown; innerType?: z.ZodTypeAny };
227
+ def?: { type?: unknown; innerType?: z.ZodTypeAny };
228
+ };
229
+ const def = raw._def ?? raw.def;
230
+ const kind = def?.type;
231
+ const name = (current as unknown as { _def?: { typeName?: string } })._def?.typeName;
232
+ if (kind !== "optional" && kind !== "default" && kind !== "nullable" &&
233
+ name !== "ZodOptional" && name !== "ZodDefault" && name !== "ZodNullable") {
234
+ return false;
235
+ }
236
+ current = def?.innerType;
237
+ }
238
+ return false;
239
+ }
240
+
202
241
  export const nomosRefValueSchema: z.ZodType<NomosRefValue> = z.object({
203
242
  workspace: z.string().min(1),
204
243
  type: z.string().min(1),
@@ -895,6 +934,10 @@ export const t = {
895
934
  sha256Digest(): Field<Sha256Digest, "string"> {
896
935
  return makeField<Sha256Digest, "string">("string", sha256Digest(), FirstWriterWins);
897
936
  },
937
+ /** A gix custody object identity. Generated clients surface [GitObjectId], not String. */
938
+ gitObjectId(): Field<GitObjectId, "string"> {
939
+ return makeField<GitObjectId, "string">("string", gitObjectId(), FirstWriterWins);
940
+ },
898
941
  int(rules: IntRules = {}): Field<number, "int"> {
899
942
  for (const [name, value] of [["minimum", rules.minimum], ["maximum", rules.maximum]] as const) {
900
943
  if (value !== undefined && !Number.isSafeInteger(value)) {