@githolon/dsl 0.102.1 → 0.102.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dart/pubspec.yaml CHANGED
@@ -8,7 +8,7 @@ description: >-
8
8
  Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
9
9
  code-generated per-domain aggregate + directive-payload types. (De)serializes
10
10
  the EXACT serde JSON the Rust kernel + projection produce/consume.
11
- version: 0.64.0
11
+ version: 0.64.1
12
12
 
13
13
  environment:
14
14
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.102.1",
3
+ "version": "0.102.3",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -1834,12 +1834,20 @@ function parseBulkReadForbidden(status, bodyText) {
1834
1834
  // ../cloud/web-client/src/kernel_provider_wasm.ts
1835
1835
  var enc2 = new TextEncoder();
1836
1836
  var dec2 = new TextDecoder();
1837
- var snapB64 = (bytes) => {
1837
+ var nmsInternalSnapshotToBase64 = (bytes) => {
1838
+ if (typeof Buffer !== "undefined") {
1839
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
1840
+ }
1838
1841
  let s = "";
1839
- for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode.apply(null, bytes.subarray(i, i + 32768));
1842
+ for (let i = 0; i < bytes.length; i += 32768) {
1843
+ s += String.fromCharCode.apply(null, bytes.subarray(i, i + 32768));
1844
+ }
1840
1845
  return btoa(s);
1841
1846
  };
1842
- var snapUnb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
1847
+ var nmsInternalSnapshotFromBase64 = (s) => {
1848
+ if (typeof Buffer !== "undefined") return Buffer.from(s, "base64");
1849
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
1850
+ };
1843
1851
  var unpack = (p) => {
1844
1852
  const v = typeof p === "bigint" ? p : BigInt(p);
1845
1853
  return { ptr: Number(v >> 32n), len: Number(v & 0xffffffffn) };
@@ -1884,7 +1892,7 @@ function wasmModuleFor(base, fetchImpl) {
1884
1892
  function parseSnapshot(bytes) {
1885
1893
  try {
1886
1894
  const o = JSON.parse(dec2.decode(bytes));
1887
- if (o && o.v === 2 && typeof o.tree === "string") return { treeBytes: snapUnb64(o.tree), ckpt: o.ckpt ?? null, frontier: o.frontier ?? null, syncedBase: o.syncedBase ?? null };
1895
+ if (o && o.v === 2 && typeof o.tree === "string") return { treeBytes: nmsInternalSnapshotFromBase64(o.tree), ckpt: o.ckpt ?? null, frontier: o.frontier ?? null, syncedBase: o.syncedBase ?? null };
1888
1896
  } catch {
1889
1897
  }
1890
1898
  return { treeBytes: bytes, ckpt: null, frontier: null, syncedBase: null };
@@ -1893,7 +1901,7 @@ function buildSnapshot(treeBytes, ckpt, syncedBase) {
1893
1901
  if ((!ckpt || !ckpt.blob && !ckpt.frontier) && !syncedBase) return treeBytes;
1894
1902
  return enc2.encode(JSON.stringify({
1895
1903
  v: 2,
1896
- tree: snapB64(treeBytes),
1904
+ tree: nmsInternalSnapshotToBase64(treeBytes),
1897
1905
  ckpt: ckpt && ckpt.blob ? ckpt.blob : null,
1898
1906
  frontier: ckpt ? ckpt.frontier ?? null : null,
1899
1907
  cursor: ckpt ? ckpt.cursor ?? null : null,
@@ -2133,7 +2141,7 @@ function createWasmEnginePlane({ span } = {}) {
2133
2141
  }
2134
2142
  const buf = new Uint8Array(await r.arrayBuffer());
2135
2143
  if (!buf.length) return { applied: false };
2136
- view.applyPack(snapB64(buf));
2144
+ view.applyPack(nmsInternalSnapshotToBase64(buf));
2137
2145
  return { applied: true };
2138
2146
  } finally {
2139
2147
  if (end) end();
@@ -1,6 +1,7 @@
1
1
  // NOMOS — OpenUSD is the application/package IR. JavaScript config is authoring input only.
2
2
 
3
3
  import { execFileSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
4
5
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
6
  import path from "node:path";
6
7
 
@@ -40,6 +41,25 @@ export interface PackageRootPart {
40
41
  readonly kind: Exclude<PartKind, "group">;
41
42
  }
42
43
 
44
+ export type ApplicationPackagePlacement = "reference" | "payload";
45
+
46
+ export interface ApplicationDeploymentPackage {
47
+ /** The authored application part that gives this package its placement semantics. */
48
+ readonly part: string;
49
+ /** Reference packages serve on the application workspace; payload packages are retained for child birth. */
50
+ readonly placement: ApplicationPackagePlacement;
51
+ readonly packageName: string;
52
+ readonly sourceIdentity: string;
53
+ readonly assetPath: string;
54
+ readonly deployPath: string;
55
+ }
56
+
57
+ export interface ApplicationDeploymentPlan {
58
+ readonly application: string;
59
+ readonly stagePath: string;
60
+ readonly packages: readonly ApplicationDeploymentPackage[];
61
+ }
62
+
43
63
  const safeUsdName = (value: string, what: string): void => {
44
64
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
45
65
  throw new Error(`${what} '${value}' is not a USD identifier (letters, digits and '_' only; cannot start with a digit)`);
@@ -203,7 +223,12 @@ export function compileApplicationComposition(
203
223
  config: ApplicationCompositionConfig,
204
224
  configPath: string,
205
225
  options: { readonly requireUsdcat?: boolean } = {},
206
- ): { readonly outputPath: string; readonly usda: string; readonly referencedAssets: readonly string[] } {
226
+ ): {
227
+ readonly outputPath: string;
228
+ readonly usda: string;
229
+ readonly referencedAssets: readonly string[];
230
+ readonly deploymentPackages: readonly ApplicationDeploymentPackage[];
231
+ } {
207
232
  const configDir = path.dirname(configPath);
208
233
  const outDir = path.resolve(configDir, config.outDir ?? "build");
209
234
  mkdirSync(outDir, { recursive: true });
@@ -259,7 +284,8 @@ export function compileApplicationComposition(
259
284
  rmSync(candidatePath, { force: true });
260
285
  throw error;
261
286
  }
262
- return { outputPath, usda, referencedAssets: [...referencedAssets].sort() };
287
+ const deploymentPackages = applicationDeploymentPlan(outputPath).packages;
288
+ return { outputPath, usda, referencedAssets: [...referencedAssets].sort(), deploymentPackages };
263
289
  }
264
290
 
265
291
  export function isApplicationCompositionConfig(value: unknown): value is ApplicationCompositionConfig {
@@ -280,3 +306,92 @@ export function packageRootOfUsda(assetPath: string): { readonly name: string; r
280
306
  if (identifier === undefined) throw new Error(`package asset '${assetPath}' root '${name}' has no content identifier`);
281
307
  return { name, kind, identifier };
282
308
  }
309
+
310
+ /**
311
+ * Resolve the operational package set directly from the compiled application USDA.
312
+ *
313
+ * This is deliberately a projection, not a second configuration format: deployment reads the same
314
+ * reference/payload arcs that OpenUSD composes. Package assets bind those arcs to their compiler-emitted
315
+ * deploy bodies using the package's content identity, so a stale or hand-matched sidecar refuses loudly.
316
+ */
317
+ export function applicationDeploymentPlan(stagePath: string): ApplicationDeploymentPlan {
318
+ if (!existsSync(stagePath)) throw new Error(`application stage not found: ${stagePath}`);
319
+ const stage = readFileSync(stagePath, "utf8");
320
+ const application = /custom\s+string\s+nomos:application\s*=\s*"([^"]+)"/.exec(stage)?.[1];
321
+ if (application === undefined) {
322
+ throw new Error(`'${stagePath}' is not a compiled Nomos application stage (nomos:application is absent)`);
323
+ }
324
+
325
+ type Use = { part: string; placement: ApplicationPackagePlacement; assetPath: string };
326
+ const uses: Use[] = [];
327
+ const scope = /def\s+Scope\s+"([A-Za-z_][A-Za-z0-9_]*)"\s*\(([\s\S]*?)\n\s*\)\s*\{/g;
328
+ for (const match of stage.matchAll(scope)) {
329
+ const part = match[1]!;
330
+ const metadata = match[2]!;
331
+ const reference = /prepend\s+references\s*=\s*@([^@]+)@<([^>]+)>/.exec(metadata);
332
+ if (reference === null) continue;
333
+ const payload = /prepend\s+payload\s*=\s*@([^@]+)@<([^>]+)>/.exec(metadata);
334
+ const asset = reference[1]!;
335
+ if (isResolverAsset(asset)) {
336
+ throw new Error(`application deployment cannot resolve '${asset}' without an installed asset resolver`);
337
+ }
338
+ const assetPath = path.resolve(path.dirname(stagePath), asset);
339
+ uses.push({ part, placement: payload === null ? "reference" : "payload", assetPath });
340
+ }
341
+ if (uses.length === 0) throw new Error(`application '${application}' contains no deployable package references`);
342
+
343
+ const byAsset = new Map<string, Use>();
344
+ for (const use of uses) {
345
+ const prior = byAsset.get(use.assetPath);
346
+ if (prior !== undefined && prior.placement !== use.placement) {
347
+ throw new Error(
348
+ `package asset '${use.assetPath}' is both a serving reference ('${prior.part}') and a deferred payload ` +
349
+ `('${use.part}'); split those responsibilities into separate packages`,
350
+ );
351
+ }
352
+ byAsset.set(use.assetPath, prior ?? use);
353
+ }
354
+
355
+ const packages = [...byAsset.values()].map((use): ApplicationDeploymentPackage => {
356
+ if (!existsSync(use.assetPath)) throw new Error(`application package asset not found: ${use.assetPath}`);
357
+ const exported = packageRootOfUsda(use.assetPath);
358
+ const identity = /^nomos:package:([^:]+):([0-9a-f]{64})$/.exec(exported.identifier);
359
+ if (identity === null) {
360
+ throw new Error(`application asset '${use.assetPath}' has invalid Nomos package identity '${exported.identifier}'`);
361
+ }
362
+ const packageName = identity[1]!;
363
+ const sourceIdentity = identity[2]!;
364
+ const deployPath = path.join(path.dirname(use.assetPath), `${packageName}.deploy.json`);
365
+ if (!existsSync(deployPath)) {
366
+ throw new Error(`package '${packageName}' has no sibling deploy body: ${deployPath} (compile the package first)`);
367
+ }
368
+ let body: {
369
+ summary?: { package?: unknown; sourceIdentity?: unknown; domainHash?: unknown };
370
+ packageUsda?: unknown;
371
+ };
372
+ try {
373
+ body = JSON.parse(readFileSync(deployPath, "utf8")) as typeof body;
374
+ } catch (error) {
375
+ throw new Error(`package '${packageName}' deploy body is not valid JSON: ${(error as Error).message}`);
376
+ }
377
+ if (body.summary?.package !== packageName) {
378
+ throw new Error(`package asset '${use.assetPath}' names '${packageName}', but '${deployPath}' does not`);
379
+ }
380
+ if (body.summary?.sourceIdentity !== sourceIdentity) {
381
+ throw new Error(
382
+ `package '${packageName}' asset/deploy skew: source identity ${sourceIdentity.slice(0, 12)}… does not match ` +
383
+ `${String(body.summary?.sourceIdentity ?? "missing").slice(0, 12)}…; recompile the package and application together`,
384
+ );
385
+ }
386
+ if (typeof body.packageUsda !== "string" || body.packageUsda.length === 0) {
387
+ throw new Error(`package '${packageName}' deploy body has no packageUsda`);
388
+ }
389
+ const lawHash = createHash("sha256").update(Buffer.from(body.packageUsda, "utf8")).digest("hex");
390
+ if (body.summary?.domainHash !== lawHash) {
391
+ throw new Error(`package '${packageName}' deploy body is internally stale (packageUsda hash does not match its summary)`);
392
+ }
393
+ return { ...use, packageName, sourceIdentity, deployPath };
394
+ });
395
+ packages.sort((a, b) => Number(a.placement === "payload") - Number(b.placement === "payload"));
396
+ return { application, stagePath, packages };
397
+ }
@@ -166,15 +166,17 @@ function lowerOne<P extends Record<string, unknown>, F extends Record<string, Fi
166
166
  grantedBy: founder,
167
167
  grantedAt,
168
168
  }),
169
- ...(decl.aggregate.audience?.kind === "owned"
170
- ? writeTuple({
171
- object: "workspace:self",
172
- relation: OWNED_READ_RELATION,
173
- subject: founder,
174
- grantedBy: founder,
175
- grantedAt,
176
- })
177
- : []),
169
+ // The workspace contains framework-private identity/key records even when its root BUSINESS
170
+ // aggregate is public. Bulk custody cannot selectively clone Git history, so every business-born
171
+ // workspace must seat its founder on the framework transport relation from genesis. Per-record
172
+ // visibility remains governed independently; this grant only makes the sovereign replica reopenable.
173
+ ...writeTuple({
174
+ object: "workspace:self",
175
+ relation: OWNED_READ_RELATION,
176
+ subject: founder,
177
+ grantedBy: founder,
178
+ grantedAt,
179
+ }),
178
180
  // Business ownership remains explicit policy. The tenant names roles; Nomos owns subjects,
179
181
  // provenance, tuple mechanics and the atomic genesis ordering underneath them.
180
182
  ...decl.founderRoles.flatMap((relation) =>
@@ -31,6 +31,7 @@ import {
31
31
  } from "./framework/framework_aggregates.js";
32
32
  import { isBusinessIntent } from "./intent.js";
33
33
  import { contractTypeName } from "./contract.js";
34
+ import { admitsNull } from "./null_meta.js";
34
35
  import type {
35
36
  WorkspaceSharingPolicy,
36
37
  WorkspaceSharingReceipt,
@@ -590,28 +591,28 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
590
591
  const person = sharingOrderValue(policy, policy.person, "wire");
591
592
  const role = sharingOrderValue(policy, policy.role, "wire");
592
593
  const scope = sharingOrderValue(policy, policy.scope, "wire");
593
- const delivered = policy.delivered.flatMap((receipt) => [
594
- ` if (${sharingReceiptCondition(policy, receipt)}) {`,
595
- ` await _client._offer(`,
596
- ` ${JSON.stringify(receipt.records.id)},`,
597
- ` <String, Object?>{`,
598
- ...sharingReceiptPayload(policy, receipt, false),
599
- ` },`,
600
- ` );`,
601
- ` continue;`,
602
- ` }`,
603
- ]);
604
- const refused = policy.refused.flatMap((receipt) => [
605
- ` if (${sharingReceiptCondition(policy, receipt)}) {`,
606
- ` await _client._offer(`,
607
- ` ${JSON.stringify(receipt.records.id)},`,
608
- ` <String, Object?>{`,
609
- ...sharingReceiptPayload(policy, receipt, true),
610
- ` },`,
611
- ` );`,
612
- ` continue;`,
613
- ` }`,
614
- ]);
594
+ const receiptLines = (receipt: WorkspaceSharingReceipt, refused: boolean): string[] => {
595
+ const condition = sharingReceiptCondition(policy, receipt);
596
+ const body = [
597
+ ` await _client._offer(`,
598
+ ` ${JSON.stringify(receipt.records.id)},`,
599
+ ` <String, Object?>{`,
600
+ ...sharingReceiptPayload(policy, receipt, refused),
601
+ ` },`,
602
+ ` );`,
603
+ ` continue;`,
604
+ ];
605
+ // A receipt with no `when` is the policy's exhaustive fallback. Emitting `if (true)` makes both
606
+ // the following fallback error and any later receipt dead Dart code, turning generated code into an
607
+ // analyzer warning. Keep the same semantics but emit the exhaustive body directly.
608
+ return condition === "true"
609
+ ? body
610
+ : [` if (${condition}) {`, ...body, ` }`];
611
+ };
612
+ const delivered = policy.delivered.flatMap((receipt) => receiptLines(receipt, false));
613
+ const refused = policy.refused.flatMap((receipt) => receiptLines(receipt, true));
614
+ const deliveredExhaustive = policy.delivered.some((receipt) => sharingReceiptCondition(policy, receipt) === "true");
615
+ const refusedExhaustive = policy.refused.some((receipt) => sharingReceiptCondition(policy, receipt) === "true");
615
616
  appLines.push(
616
617
  ` Future<void>? _nmsWorkspaceSharingDrain${index};`,
617
618
  ` StreamSubscription<Object?>? _nmsWorkspaceSharingSync${index};`,
@@ -659,10 +660,14 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
659
660
  ` if (share.awaitingRecipient) continue;`,
660
661
  ` if (share.delivered) {`,
661
662
  ...delivered,
662
- ` throw StateError('No delivered receipt matches ${policy.orders.id} \${NmsInternalNomosRefs.id(order.ref)}');`,
663
+ ...(deliveredExhaustive
664
+ ? []
665
+ : [` throw StateError('No delivered receipt matches ${policy.orders.id} \${NmsInternalNomosRefs.id(order.ref)}');`]),
663
666
  ` }`,
664
667
  ...refused,
665
- ` throw StateError('No refused receipt matches ${policy.orders.id} \${NmsInternalNomosRefs.id(order.ref)}');`,
668
+ ...(refusedExhaustive
669
+ ? []
670
+ : [` throw StateError('No refused receipt matches ${policy.orders.id} \${NmsInternalNomosRefs.id(order.ref)}');`]),
666
671
  ` }`,
667
672
  ` }`,
668
673
  ``,
@@ -1047,7 +1052,10 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1047
1052
  ? sourceFieldMember.slice(0, -2)
1048
1053
  : `${sourceFieldMember}Ref`;
1049
1054
  const method = member(stem);
1050
- const memberNullable = field.isFromBirth !== true;
1055
+ const memberNullable = admitsNull(field) || field.isFromBirth !== true;
1056
+ const sourceFieldValue = admitsNull(field)
1057
+ ? `record.${sourceFieldMember}.valueOrNull`
1058
+ : `record.${sourceFieldMember}`;
1051
1059
  const isSameDomain = sameDomainRefs.some(([name]) => name === fieldName);
1052
1060
  const isCrossDomain = crossDomainRefs.some(([name]) => name === fieldName);
1053
1061
  if (!isSameDomain && !isCrossDomain) continue;
@@ -1060,7 +1068,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1060
1068
  ` Future<${targetModel}?> ${method}(${sourceModel} record) async {`,
1061
1069
  ...(memberNullable
1062
1070
  ? [
1063
- ` final id = record.${sourceFieldMember};`,
1071
+ ` final id = ${sourceFieldValue};`,
1064
1072
  ` if (id == null) return null;`,
1065
1073
  ` final rows = await _client.${targetById}(NmsInternalNomosRefs.id(id));`,
1066
1074
  ]
@@ -1091,7 +1099,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1091
1099
  ` Future<${targetModel}?> ${method}(${sourceModel} record) async {`,
1092
1100
  ...(memberNullable
1093
1101
  ? [
1094
- ` final id = record.${sourceFieldMember};`,
1102
+ ` final id = ${sourceFieldValue};`,
1095
1103
  ` if (id == null) return null;`,
1096
1104
  ` final application = await ${opener}(`,
1097
1105
  ` _client.bridge,`,
@@ -1103,7 +1111,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1103
1111
  ` );`,
1104
1112
  ]
1105
1113
  : [
1106
- ` final id = record.${sourceFieldMember};`,
1114
+ ` final id = ${sourceFieldValue};`,
1107
1115
  ` final application = await ${opener}(`,
1108
1116
  ` _client.bridge,`,
1109
1117
  ` id,`,
@@ -1122,7 +1130,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1122
1130
  ` Future<${targetApplication}?> ${applicationMethod}(${sourceModel} record) {`,
1123
1131
  ...(memberNullable
1124
1132
  ? [
1125
- ` final id = record.${sourceFieldMember};`,
1133
+ ` final id = ${sourceFieldValue};`,
1126
1134
  ` if (id == null) return Future<${targetApplication}?>.value(null);`,
1127
1135
  ` return ${opener}(`,
1128
1136
  ` _client.bridge,`,
@@ -1147,12 +1155,15 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1147
1155
  ? contractTypeName(foreign.targetAggregateId)
1148
1156
  : concreteClassName(foreign.targetAggregateId);
1149
1157
  const fieldMember = member(fieldName);
1158
+ const fieldValue = admitsNull(field)
1159
+ ? `record.${fieldMember}.valueOrNull`
1160
+ : `record.${fieldMember}`;
1150
1161
  const stem = fieldMember.endsWith("Id") ? fieldMember.slice(0, -2) : fieldMember;
1151
1162
  const method = member(`${stem}Of`);
1152
1163
  addCollectionMember(aggregate.id, method, [
1153
1164
  ` /// Follow [record]'s ${fieldName} to its typed business record.`,
1154
1165
  ` Future<${target}?> ${method}(${sourceModel} record) async {`,
1155
- ` final reference = record.${fieldMember};`,
1166
+ ` final reference = ${fieldValue};`,
1156
1167
  ` if (reference == null) return null;`,
1157
1168
  ` final target = await NmsInternalForeignRefs.resolve(reference, _client.bridge.foreignReader());`,
1158
1169
  ` if (target == null) return null;`,
@@ -1175,7 +1186,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1175
1186
  addCollectionMember(aggregate.id, applicationMethod, [
1176
1187
  ` /// Open the business application containing [record]'s ${fieldName}.`,
1177
1188
  ` Future<${targetApplication}?> ${applicationMethod}(${sourceModel} record) {`,
1178
- ` final reference = record.${fieldMember};`,
1189
+ ` final reference = ${fieldValue};`,
1179
1190
  ` if (reference == null) return Future<${targetApplication}?>.value(null);`,
1180
1191
  ` return ${opener}(`,
1181
1192
  ` _client.bridge,`,
@@ -62,6 +62,7 @@ import { buildNomosDescribeModel, type NomosDescribeModel } from "./describe_mod
62
62
  import { assertModelClientMemberAvailable } from "./generated_client_names.js";
63
63
  import { ownedRecordActionDirectiveId } from "./framework/owned_custody.js";
64
64
  import { contractTypeName } from "./contract.js";
65
+ import { admitsNull } from "./null_meta.js";
65
66
 
66
67
  const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
67
68
  const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
@@ -1268,14 +1269,15 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1268
1269
  // the DSL schema's `.optional()` — a partial fold decodes every scalar `null` when absent
1269
1270
  // (`emitReadModel`'s own doc: "PARTIAL folds"), so the member is nullable UNLESS the field is
1270
1271
  // `.fromBirth()` (birth-presence law — the ONE case the projection types non-null).
1271
- const memberNullable = f.isFromBirth !== true;
1272
+ const memberNullable = admitsNull(f) || f.isFromBirth !== true;
1273
+ const memberValue = admitsNull(f) ? `${member}.valueOrNull` : member;
1272
1274
  lines.push(
1273
1275
  ` /// Resolve \`${fieldName}\` (${agg.id}.${fieldName} -> ${targetType}) by dispatching to`,
1274
1276
  ` /// [${className}.${byIdMethod}] — the existing by-id read lane, never a second one.`,
1275
1277
  ` Future<${targetModel}?> ${method}(${className} client) async {`,
1276
1278
  ...(memberNullable
1277
1279
  ? [
1278
- ` final id = ${member};`,
1280
+ ` final id = ${memberValue};`,
1279
1281
  ` if (id == null) return null;`,
1280
1282
  ` final rows = await client.${byIdMethod}(NmsInternalNomosRefs.id(id));`,
1281
1283
  ]
@@ -1301,7 +1303,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1301
1303
  const member = dartMemberName(fieldName);
1302
1304
  const methodStem = member.length > 2 && member.endsWith("Id") ? member.slice(0, -2) : `${member}Ref`;
1303
1305
  const method = safeMethod(methodStem);
1304
- const memberNullable = f.isFromBirth !== true;
1306
+ const memberNullable = admitsNull(f) || f.isFromBirth !== true;
1307
+ const memberValue = admitsNull(f) ? `${member}.valueOrNull` : member;
1305
1308
  lines.push(
1306
1309
  ` /// Resolve \`${fieldName}\` (${agg.id}.${fieldName} -> ${targetType}, a CROSS-DOMAIN ref) by`,
1307
1310
  ` /// dispatching to [${targetClient}.${byIdMethod}] on the target domain's client — same by-id`,
@@ -1310,7 +1313,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1310
1313
  ` Future<${targetModel}?> ${method}(${targetClient} client) async {`,
1311
1314
  ...(memberNullable
1312
1315
  ? [
1313
- ` final id = ${member};`,
1316
+ ` final id = ${memberValue};`,
1314
1317
  ` if (id == null) return null;`,
1315
1318
  ` final rows = await client.${byIdMethod}(NmsInternalNomosRefs.id(id));`,
1316
1319
  ]
@@ -1341,6 +1344,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1341
1344
  }
1342
1345
  const targetConcrete = concreteClassName(targetType);
1343
1346
  const member = dartMemberName(fieldName);
1347
+ const memberValue = admitsNull(f) ? `${member}.valueOrNull` : member;
1344
1348
  // The stored locator member is a typed `ForeignRef<Target>?` getter, so the resolver takes a
1345
1349
  // `remote…` name to avoid colliding with it (and to read as "the far side of the reference").
1346
1350
  const method = safeMethod(`remote${cap(camel(member))}`);
@@ -1350,7 +1354,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1350
1354
  ` /// implementation opens the foreign workspace on demand; tenant code sees only this business method.`,
1351
1355
  ` /// Null locator (absent / empty id or workspace) ⇒ null.`,
1352
1356
  ` Future<${targetConcrete}?> ${method}(NomosEngine engine, {String? cloud}) {`,
1353
- ` final ref = ${member};`,
1357
+ ` final ref = ${memberValue};`,
1354
1358
  ` if (ref == null) return Future<${targetConcrete}?>.value(null);`,
1355
1359
  ` return NmsInternalForeignRefs.resolve(ref, engine.foreignReader(cloud: cloud));`,
1356
1360
  ` }`,
@@ -192,9 +192,9 @@ export function generateDartScenario(
192
192
  ` void Function(Future<void> Function())? registerTearDown,`,
193
193
  ` }) async {`,
194
194
  ` final person = signedInAs ?? ${testPerson}.named('test');`,
195
- ` final packageFile = await _applicationPackage();`,
195
+ ` final packageFiles = await _applicationPackages();`,
196
196
  ` final environment = await NomosTestHarness.ephemeral(`,
197
- ` deployJsonPaths: [packageFile.path],`,
197
+ ` deployJsonPaths: packageFiles.map((file) => file.path).toList(growable: false),`,
198
198
  ` ownerSubject: person._principal,`,
199
199
  ` workspace: nmsInternalReceivingGroupNameOf(person._principal),`,
200
200
  ` nodeEntry: Platform.environment['NOMOS_NODE_ENTRY'],`,
@@ -204,7 +204,7 @@ export function generateDartScenario(
204
204
  ` final application = await ${bind}(`,
205
205
  ` environment.session(environment.workspace), NmsInternalApplicationUserBinding.fromActor(person._principal),`,
206
206
  ` );`,
207
- ` final scenario = ${scenario}._(environment, person._principal, packageFile, application);`,
207
+ ` final scenario = ${scenario}._(environment, person._principal, packageFiles.last, application);`,
208
208
  ` registerTearDown?.call(scenario.close);`,
209
209
  ` return scenario;`,
210
210
  ` }`,
@@ -220,24 +220,27 @@ export function generateDartScenario(
220
220
  ` throw StateError('Connected scenarios require the Nomos test cloud. Run this test through githolon test.');`,
221
221
  ` }`,
222
222
  ` final applicationName = 'test-application-\${DateTime.now().microsecondsSinceEpoch}';`,
223
- ` final packageFile = await _applicationPackage();`,
223
+ ` final packageFiles = await _applicationPackages();`,
224
224
  ` final cloud = await NomosTestCloud.start(`,
225
- ` serverEntry: serverEntry, platform: applicationName, deployJsonPath: packageFile.path,`,
225
+ ` serverEntry: serverEntry, platform: applicationName,`,
226
+ ` deployJsonPaths: packageFiles.map((file) => file.path).toList(growable: false),`,
227
+ ` onDiagnostic: Platform.environment['NOMOS_TEST_DIAGNOSTICS'] == '1' ? stderr.writeln : null,`,
226
228
  ` );`,
227
229
  ` late final NomosTestHarness environment;`,
228
230
  ` try {`,
229
231
  ` environment = await NomosTestHarness.cloudBound(`,
230
- ` cloud: cloud, deployJsonPaths: [packageFile.path],`,
232
+ ` cloud: cloud, deployJsonPaths: packageFiles.map((file) => file.path).toList(growable: false),`,
231
233
  ` platformWorkspace: applicationName, subject: person._principal,`,
232
234
  ` nodeEntry: Platform.environment['NOMOS_NODE_ENTRY'],`,
233
235
  ` phaseTimeout: const Duration(minutes: 2),`,
236
+ ` onDiagnostic: Platform.environment['NOMOS_TEST_DIAGNOSTICS'] == '1' ? stderr.writeln : null,`,
234
237
  ` );`,
235
238
  ` } catch (_) {`,
236
239
  ` await cloud.stop();`,
237
240
  ` rethrow;`,
238
241
  ` }`,
239
242
  ` final application = await ${bind}(environment.homeSession, NmsInternalApplicationUserBinding.fromActor(person._principal));`,
240
- ` final scenario = ${scenario}._(environment, person._principal, packageFile, application);`,
243
+ ` final scenario = ${scenario}._(environment, person._principal, packageFiles.last, application);`,
241
244
  ` registerTearDown?.call(scenario.close);`,
242
245
  ` return scenario;`,
243
246
  ` }`,
@@ -273,8 +276,64 @@ export function generateDartScenario(
273
276
  ` try { await _packageFile.parent.delete(recursive: true); } catch (_) {}`,
274
277
  ` }`,
275
278
  ``,
276
- ` static Future<File> _applicationPackage() async {`,
279
+ ` /// Resolve the whole application from its compiled OpenUSD stage. Reference-only packages are`,
280
+ ` /// installed on the test platform first; payload packages follow and become the child law. A`,
281
+ ` /// one-package project has no application stage and keeps the package-only shorthand.`,
282
+ ` static Future<List<File>> _applicationPackages() async {`,
277
283
  ` WidgetsFlutterBinding.ensureInitialized();`,
284
+ ` Directory? cursor = Directory.current.absolute;`,
285
+ ` File? applicationStage;`,
286
+ ` for (var depth = 0; depth < 10; depth++) {`,
287
+ ` final build = Directory('\${cursor!.path}\${Platform.pathSeparator}build');`,
288
+ ` if (build.existsSync()) {`,
289
+ ` final stages = build`,
290
+ ` .listSync()`,
291
+ ` .whereType<File>()`,
292
+ ` .where((file) => file.path.endsWith('.application.usda'))`,
293
+ ` .toList(growable: false);`,
294
+ ` if (stages.length > 1) {`,
295
+ ` throw StateError('More than one Nomos application stage is visible in \${build.path}; set one application boundary.');`,
296
+ ` }`,
297
+ ` if (stages.length == 1) { applicationStage = stages.single; break; }`,
298
+ ` }`,
299
+ ` final parent = cursor.parent;`,
300
+ ` if (parent.path == cursor.path) break;`,
301
+ ` cursor = parent;`,
302
+ ` }`,
303
+ ` if (applicationStage != null) {`,
304
+ ` final stage = await applicationStage.readAsString();`,
305
+ ` final placements = <String, bool>{};`,
306
+ ` final scopes = RegExp(r'def\\s+Scope\\s+"[A-Za-z_][A-Za-z0-9_]*"\\s*\\(([\\s\\S]*?)\\n\\s*\\)\\s*\\{').allMatches(stage);`,
307
+ ` for (final scope in scopes) {`,
308
+ ` final metadata = scope.group(1)!;`,
309
+ ` final reference = RegExp(r'prepend\\s+references\\s*=\\s*@([^@]+)@<[^>]+>').firstMatch(metadata);`,
310
+ ` if (reference == null) continue;`,
311
+ ` final payload = RegExp(r'prepend\\s+payload\\s*=\\s*@([^@]+)@<[^>]+>').hasMatch(metadata);`,
312
+ ` final asset = File(Uri.file(applicationStage.path).resolve(reference.group(1)!).toFilePath());`,
313
+ ` if (!asset.existsSync()) throw StateError('Application package asset is missing: \${asset.path}');`,
314
+ ` final packageAsset = await asset.readAsString();`,
315
+ ` final identity = RegExp(r'asset\\s+identifier\\s*=\\s*@nomos:package:([^:]+):[0-9a-f]{64}@').firstMatch(packageAsset);`,
316
+ ` if (identity == null) throw StateError('Application reference is not a compiled Nomos package: \${asset.path}');`,
317
+ ` final deploy = File('\${asset.parent.path}\${Platform.pathSeparator}\${identity.group(1)!}.deploy.json');`,
318
+ ` if (!deploy.existsSync()) throw StateError('Application package has no deploy body: \${deploy.path}');`,
319
+ ` final prior = placements[deploy.path];`,
320
+ ` if (prior != null && prior != payload) {`,
321
+ ` throw StateError('The same package is both a serving reference and deferred payload: \${deploy.path}');`,
322
+ ` }`,
323
+ ` placements[deploy.path] = payload;`,
324
+ ` }`,
325
+ ` if (placements.isEmpty) throw StateError('The application stage contains no deployable Nomos packages.');`,
326
+ ` final ordered = placements.entries.toList()`,
327
+ ` ..sort((a, b) => (a.value ? 1 : 0).compareTo(b.value ? 1 : 0));`,
328
+ ` final dir = await Directory.systemTemp.createTemp('nms-application-packages-');`,
329
+ ` final files = <File>[];`,
330
+ ` for (final entry in ordered) {`,
331
+ ` final source = File(entry.key);`,
332
+ ` final name = source.uri.pathSegments.last;`,
333
+ ` files.add(await source.copy('\${dir.path}\${Platform.pathSeparator}\$name'));`,
334
+ ` }`,
335
+ ` return files;`,
336
+ ` }`,
278
337
  ` final assetKeys = [`,
279
338
  ` 'packages/${testPackage}/lib/src/generated/${deployFile}',`,
280
339
  ` 'lib/src/generated/${deployFile}',`,
@@ -286,7 +345,7 @@ export function generateDartScenario(
286
345
  ` final dir = await Directory.systemTemp.createTemp('nms-application-package-');`,
287
346
  ` final file = File('\${dir.path}\${Platform.pathSeparator}${deployFile}');`,
288
347
  ` await file.writeAsBytes(data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes), flush: true);`,
289
- ` return file;`,
348
+ ` return [file];`,
290
349
  ` } catch (error) {`,
291
350
  ` lastError = error;`,
292
351
  ` }`,