@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/CHANGELOG.md +9 -0
- package/dart/lib/src/dev_cloud.dart +61 -18
- package/dart/lib/src/runner/node-entry.mjs +27 -6
- package/dart/lib/src/runner/offline-install.mjs +80 -18
- package/dart/lib/src/test_harness.dart +874 -554
- package/dart/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +14 -6
- package/src/application_composition.ts +117 -2
- package/src/business_birth.ts +11 -9
- package/src/codegen_dart_application.ts +42 -31
- package/src/codegen_dart_client.ts +9 -5
- package/src/codegen_dart_scenario.ts +68 -9
- package/src/compile_package_main.ts +168 -42
- package/src/framework/home.ts +7 -1
- package/src/law_check.ts +13 -5
- package/src/usd_layers.ts +68 -51
- package/src/workspace_sharing.ts +5 -3
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.
|
|
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.
|
|
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
|
|
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)
|
|
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
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
): {
|
|
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
|
-
|
|
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
|
+
}
|
package/src/business_birth.ts
CHANGED
|
@@ -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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
663
|
+
...(deliveredExhaustive
|
|
664
|
+
? []
|
|
665
|
+
: [` throw StateError('No delivered receipt matches ${policy.orders.id} \${NmsInternalNomosRefs.id(order.ref)}');`]),
|
|
663
666
|
` }`,
|
|
664
667
|
...refused,
|
|
665
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 = ${
|
|
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 = ${
|
|
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 = ${
|
|
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
|
|
195
|
+
` final packageFiles = await _applicationPackages();`,
|
|
196
196
|
` final environment = await NomosTestHarness.ephemeral(`,
|
|
197
|
-
` deployJsonPaths:
|
|
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,
|
|
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
|
|
223
|
+
` final packageFiles = await _applicationPackages();`,
|
|
224
224
|
` final cloud = await NomosTestCloud.start(`,
|
|
225
|
-
` serverEntry: serverEntry, platform: applicationName
|
|
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:
|
|
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,
|
|
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
|
-
`
|
|
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
|
` }`,
|