@githolon/dsl 0.102.1 → 0.102.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.2",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -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) =>
@@ -590,28 +590,28 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
590
590
  const person = sharingOrderValue(policy, policy.person, "wire");
591
591
  const role = sharingOrderValue(policy, policy.role, "wire");
592
592
  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
- ]);
593
+ const receiptLines = (receipt: WorkspaceSharingReceipt, refused: boolean): string[] => {
594
+ const condition = sharingReceiptCondition(policy, receipt);
595
+ const body = [
596
+ ` await _client._offer(`,
597
+ ` ${JSON.stringify(receipt.records.id)},`,
598
+ ` <String, Object?>{`,
599
+ ...sharingReceiptPayload(policy, receipt, refused),
600
+ ` },`,
601
+ ` );`,
602
+ ` continue;`,
603
+ ];
604
+ // A receipt with no `when` is the policy's exhaustive fallback. Emitting `if (true)` makes both
605
+ // the following fallback error and any later receipt dead Dart code, turning generated code into an
606
+ // analyzer warning. Keep the same semantics but emit the exhaustive body directly.
607
+ return condition === "true"
608
+ ? body
609
+ : [` if (${condition}) {`, ...body, ` }`];
610
+ };
611
+ const delivered = policy.delivered.flatMap((receipt) => receiptLines(receipt, false));
612
+ const refused = policy.refused.flatMap((receipt) => receiptLines(receipt, true));
613
+ const deliveredExhaustive = policy.delivered.some((receipt) => sharingReceiptCondition(policy, receipt) === "true");
614
+ const refusedExhaustive = policy.refused.some((receipt) => sharingReceiptCondition(policy, receipt) === "true");
615
615
  appLines.push(
616
616
  ` Future<void>? _nmsWorkspaceSharingDrain${index};`,
617
617
  ` StreamSubscription<Object?>? _nmsWorkspaceSharingSync${index};`,
@@ -659,10 +659,14 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
659
659
  ` if (share.awaitingRecipient) continue;`,
660
660
  ` if (share.delivered) {`,
661
661
  ...delivered,
662
- ` throw StateError('No delivered receipt matches ${policy.orders.id} \${NmsInternalNomosRefs.id(order.ref)}');`,
662
+ ...(deliveredExhaustive
663
+ ? []
664
+ : [` throw StateError('No delivered receipt matches ${policy.orders.id} \${NmsInternalNomosRefs.id(order.ref)}');`]),
663
665
  ` }`,
664
666
  ...refused,
665
- ` throw StateError('No refused receipt matches ${policy.orders.id} \${NmsInternalNomosRefs.id(order.ref)}');`,
667
+ ...(refusedExhaustive
668
+ ? []
669
+ : [` throw StateError('No refused receipt matches ${policy.orders.id} \${NmsInternalNomosRefs.id(order.ref)}');`]),
666
670
  ` }`,
667
671
  ` }`,
668
672
  ``,
@@ -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
  ` }`,