@githolon/dsl 0.101.6 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dart/CHANGELOG.md +13 -0
  2. package/dart/lib/nomos_client.dart +3 -52
  3. package/dart/lib/src/dev_cloud.dart +5 -0
  4. package/dart/lib/src/home.dart +10 -15
  5. package/dart/lib/src/nms_conformance_runtime.dart +12 -0
  6. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  7. package/dart/lib/src/nms_test_environment.dart +8 -0
  8. package/dart/lib/src/node_runner_transport.dart +56 -18
  9. package/dart/lib/src/provider.dart +5 -1
  10. package/dart/lib/src/record_origin.dart +55 -0
  11. package/dart/lib/src/runner/node-entry.mjs +9105 -0
  12. package/dart/lib/src/runner/offline-install.mjs +4729 -0
  13. package/dart/lib/src/session_bound.dart +23 -10
  14. package/dart/lib/src/source_trust.dart +51 -0
  15. package/dart/lib/src/test_harness.dart +490 -58
  16. package/dart/lib/src/webview_bridge.dart +167 -58
  17. package/dart/lib/src/workspace_name.dart +7 -1
  18. package/dart/pubspec.lock +3 -3
  19. package/dart/pubspec.yaml +4 -2
  20. package/dart/test/acquire_session_credentials_test.dart +34 -17
  21. package/dart/test/acquire_session_test.dart +109 -24
  22. package/dart/test/adv_realm_interference_test.dart +227 -99
  23. package/dart/test/bundle_version_pairing_test.dart +1 -1
  24. package/dart/test/cloud_bound_topology_test.dart +1 -1
  25. package/dart/test/cluster_attested_test.dart +52 -35
  26. package/dart/test/cluster_births_test.dart +5 -2
  27. package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
  28. package/dart/test/cluster_local_autosync_test.dart +5 -2
  29. package/dart/test/cluster_scale_test.dart +5 -2
  30. package/dart/test/compat_test.dart +1 -1
  31. package/dart/test/dlq_session_surface_test.dart +2 -1
  32. package/dart/test/errors_test.dart +1 -1
  33. package/dart/test/first_app_gauntlet_test.dart +78 -51
  34. package/dart/test/four_layer_nested_topology_test.dart +1 -1
  35. package/dart/test/harness_phase_timeout_test.dart +1 -1
  36. package/dart/test/home_parity_test.dart +9 -5
  37. package/dart/test/local_sync_inert_test.dart +22 -14
  38. package/dart/test/node_runner_transport_test.dart +9 -5
  39. package/dart/test/null_slot_test.dart +1 -1
  40. package/dart/test/parent_signer_recovery_test.dart +5 -2
  41. package/dart/test/schema_validation_test.dart +1 -1
  42. package/dart/test/sealing_client_test.dart +2 -1
  43. package/dart/test/session_bound_test.dart +92 -119
  44. package/dart/test/stale_bundle_test.dart +1 -1
  45. package/dart/test/test_harness_test.dart +77 -22
  46. package/dart/test/vo_read_typing_test.dart +32 -14
  47. package/dart/test/watch_after_offer_stall_test.dart +2 -1
  48. package/dart/test/webview_bridge_test.dart +17 -4
  49. package/dart/test/wire_test.dart +1 -1
  50. package/dart_types/CHANGELOG.md +8 -0
  51. package/dart_types/lib/nomos_types.dart +7 -6
  52. package/dart_types/lib/src/activity.dart +23 -13
  53. package/dart_types/lib/src/foreign_ref.dart +61 -112
  54. package/dart_types/lib/src/invitation.dart +96 -0
  55. package/dart_types/lib/src/nomos_ref.dart +97 -59
  56. package/dart_types/pubspec.yaml +1 -1
  57. package/dart_types/test/surfaced_test.dart +125 -19
  58. package/package.json +7 -3
  59. package/runtime/generated-application-host.js +8851 -0
  60. package/src/activity.ts +2 -2
  61. package/src/aggregate.ts +24 -1
  62. package/src/application_composition.ts +282 -0
  63. package/src/application_topology.ts +151 -0
  64. package/src/attested_core.ts +2 -2
  65. package/src/authoring_context.ts +9 -0
  66. package/src/birth_meter_ensure.ts +1 -1
  67. package/src/births.ts +53 -5
  68. package/src/build_package.ts +79 -71
  69. package/src/business_birth.ts +93 -10
  70. package/src/clear_directive.ts +3 -1
  71. package/src/codegen_dart.ts +369 -420
  72. package/src/codegen_dart_application.ts +1508 -0
  73. package/src/codegen_dart_client.ts +156 -95
  74. package/src/codegen_dart_scenario.ts +299 -0
  75. package/src/codegen_openapi.ts +8 -3
  76. package/src/codegen_proof.ts +1113 -1137
  77. package/src/codegen_provider_dart.ts +12 -1
  78. package/src/codegen_ts.ts +1176 -496
  79. package/src/compat_package_main.ts +22 -10
  80. package/src/compile_package_main.ts +1536 -337
  81. package/src/contract.ts +10 -4
  82. package/src/dart_barrel_guard.ts +5 -1
  83. package/src/describe_model.ts +13 -5
  84. package/src/directive.ts +244 -5
  85. package/src/engine_entry.ts +56 -13
  86. package/src/engine_javascript.ts +31 -0
  87. package/src/evolve_preview.ts +3 -12
  88. package/src/framework/application_home.ts +24 -10
  89. package/src/framework/capability.ts +15 -1
  90. package/src/framework/domain_lifecycle.ts +8 -0
  91. package/src/framework/framework_aggregates.ts +2 -2
  92. package/src/framework/genesis.ts +11 -1
  93. package/src/framework/home.ts +178 -75
  94. package/src/framework/impure_capability.ts +24 -7
  95. package/src/framework/relations.ts +13 -5
  96. package/src/framework/workspaces.ts +13 -12
  97. package/src/generated_client_names.ts +42 -3
  98. package/src/idof_lint.ts +65 -13
  99. package/src/index.ts +46 -12
  100. package/src/intent.ts +587 -0
  101. package/src/law_check.ts +64 -16
  102. package/src/precondition.ts +16 -4
  103. package/src/read_projection_namespace.ts +2 -7
  104. package/src/recipient.ts +33 -0
  105. package/src/relation_schema.ts +93 -0
  106. package/src/secondary_ensure.ts +11 -2
  107. package/src/shape_compat.ts +11 -1
  108. package/src/usd.ts +30 -8
  109. package/src/usd_layers.ts +447 -246
  110. package/src/usd_state.ts +1 -1
  111. package/src/value_object.ts +49 -13
  112. package/src/wire_encode.ts +124 -26
  113. package/src/workspace_sharing.ts +269 -0
  114. package/src/zod_introspection.ts +84 -0
  115. package/dart/example/nomos_secure_key_store.dart +0 -82
  116. package/dart/example/nomos_webview_transport.dart +0 -63
  117. package/dart/lib/nomos_headless.dart +0 -50
  118. package/dart/lib/testing.dart +0 -14
  119. package/dart/test/co2_product_gauntlet_test.dart +0 -244
  120. package/src/framework/identity.ts +0 -537
package/src/activity.ts CHANGED
@@ -25,7 +25,7 @@ export type ActivitySeverity = "info" | "success" | "warning" | "critical";
25
25
  /** A declared navigation target: the referenced aggregate/contract TYPE + the payload field path holding its
26
26
  * id. Replaces the enricher's blind id-probing — the reference is declared, not guessed. */
27
27
  export interface ActivityRefInput {
28
- /** The target type — an aggregate handle or a `contract(...)` (reuse contracts for the nav target). */
28
+ /** The target type — an aggregate handle or a `recordContract(...)` (reuse contracts for the nav target). */
29
29
  readonly type: AggregateHandle | ContractHandle;
30
30
  /** The payload field path holding the target's id (dotted for nested, e.g. `"location.siteId"`), OR the
31
31
  * sentinel `"@created"` — the id the LAW MINTED for this intent's created aggregate of `type` (read from
@@ -187,7 +187,7 @@ export function normalizeActivity(
187
187
  const targetId = (r?.type as { id?: unknown })?.id;
188
188
  const isTarget = typeof targetId === "string" && ((r.type as { __isAggregateHandle?: unknown }).__isAggregateHandle === true || isContractHandle(r.type));
189
189
  if (!isTarget) {
190
- throw new Error(`${where}: ref '${name}'.type must be an aggregate handle or a contract(...) (got ${typeof r?.type}).`);
190
+ throw new Error(`${where}: ref '${name}'.type must be an aggregate handle or a recordContract(...) (got ${typeof r?.type}).`);
191
191
  }
192
192
  if (typeof r.from !== "string" || r.from.length === 0) {
193
193
  throw new Error(`${where}: ref '${name}'.from must be a payload field path string.`);
package/src/aggregate.ts CHANGED
@@ -324,11 +324,27 @@ export interface AudienceMethods<Id extends string, F extends Record<string, Fie
324
324
  /** A legal Dart TYPE identifier for `.dartName()` — PascalCase-ish (leading upper, then word chars). */
325
325
  const DART_TYPE_NAME_RE = /^[A-Z][A-Za-z0-9_]*$/;
326
326
 
327
+ type AggregateOptions = {
328
+ invariant?: AggregateInvariantFn;
329
+ invariantDeclassify?: string;
330
+ cap?: number;
331
+ visibility?: ReadVisibility;
332
+ global?: boolean;
333
+ attributed?: boolean;
334
+ };
335
+
327
336
  export function aggregate<const Id extends string, F extends Record<string, Field>>(
328
337
  id: Id,
329
338
  fields: F,
330
- opts?: { invariant?: AggregateInvariantFn; invariantDeclassify?: string; cap?: number; visibility?: ReadVisibility; global?: boolean; attributed?: boolean },
339
+ opts?: AggregateOptions,
331
340
  ): AggregateHandle<Id, F> & AudienceMethods<Id, F> {
341
+ if (["user", "identity"].includes(id.toLowerCase())) {
342
+ throw new Error(
343
+ `aggregate '${id}': '${id}' is a first-class Nomos authority concept, not application state. Sorry mate, that ` +
344
+ `concept is already taken by the framework; model the business concept as Person, Customer, ` +
345
+ `Employee, Member, or another domain-specific name.`,
346
+ );
347
+ }
332
348
  for (const reserved of [
333
349
  OWNER_FIELD,
334
350
  OWNED_KEY_PAYLOAD_FIELD,
@@ -620,6 +636,13 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
620
636
  `matching ${DART_TYPE_NAME_RE} (PascalCase — a leading uppercase letter, then letters/digits/underscore)`,
621
637
  );
622
638
  }
639
+ if (name === "User" || name === "Identity") {
640
+ throw new Error(
641
+ `aggregate '${id}': .dartName(${JSON.stringify(name)}) cannot claim a first-class Nomos authority type. ` +
642
+ `Sorry mate, that concept is already taken by the framework; use Person, Customer, ` +
643
+ `Employee, Member, or another domain-specific name.`,
644
+ );
645
+ }
623
646
  return build(audience, visibility, contracts, name);
624
647
  },
625
648
  }) as unknown as AggregateHandle<Id, F> & AudienceMethods<Id, F>;
@@ -0,0 +1,282 @@
1
+ // NOMOS — OpenUSD is the application/package IR. JavaScript config is authoring input only.
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+ import path from "node:path";
6
+
7
+ import { usdaString, usdaStringArray } from "./usd_layers.js";
8
+
9
+ export type PartKind = "component" | "assembly" | "group";
10
+
11
+ export interface CompositionArc {
12
+ /** Content-addressed package/part asset, relative to the application stage or a resolver URI. */
13
+ readonly asset: string;
14
+ /** Absolute prim path exported by that asset. */
15
+ readonly prim: `/${string}`;
16
+ }
17
+
18
+ export interface ApplicationPart {
19
+ readonly name: string;
20
+ readonly kind: PartKind;
21
+ /** A package or part composed greedily into the application namespace. */
22
+ readonly reference?: CompositionArc;
23
+ /** The selectively loaded business replica for this known part. */
24
+ readonly payload?: CompositionArc;
25
+ /** Stable application-level address; never a workspace or Holon identifier. */
26
+ readonly address?: string;
27
+ readonly children?: readonly ApplicationPart[];
28
+ }
29
+
30
+ export interface ApplicationCompositionConfig {
31
+ readonly application: string;
32
+ /** Optional concrete instance identity. Omit for a reusable application definition. */
33
+ readonly instance?: string;
34
+ readonly root: ApplicationPart;
35
+ readonly outDir?: string;
36
+ }
37
+
38
+ export interface PackageRootPart {
39
+ readonly name: string;
40
+ readonly kind: Exclude<PartKind, "group">;
41
+ }
42
+
43
+ const safeUsdName = (value: string, what: string): void => {
44
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
45
+ throw new Error(`${what} '${value}' is not a USD identifier (letters, digits and '_' only; cannot start with a digit)`);
46
+ }
47
+ };
48
+
49
+ const validateArc = (arc: CompositionArc, at: string): void => {
50
+ if (arc.asset.length === 0 || /[@\r\n]/.test(arc.asset)) {
51
+ throw new Error(`${at} names an unsafe or empty asset path`);
52
+ }
53
+ if (!/^\/(?:[A-Za-z_][A-Za-z0-9_]*)(?:\/[A-Za-z_][A-Za-z0-9_]*)*$/.test(arc.prim)) {
54
+ throw new Error(`${at} must name an absolute exported prim path`);
55
+ }
56
+ };
57
+
58
+ /** Fail-closed structural validation before a composition is emitted. */
59
+ export function validateApplicationComposition(config: ApplicationCompositionConfig): void {
60
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(config.application)) {
61
+ throw new Error(`application '${config.application}' is not a safe content/artifact name`);
62
+ }
63
+ safeUsdName(config.root.name, "application root");
64
+ if (config.root.kind !== "assembly") {
65
+ throw new Error(`application root '${config.root.name}' must be an assembly, got ${config.root.kind}`);
66
+ }
67
+
68
+ const addresses = new Set<string>();
69
+ const visit = (part: ApplicationPart, at: string): void => {
70
+ safeUsdName(part.name, `part at ${at}`);
71
+ const here = `${at}/${part.name}`;
72
+ const children = part.children ?? [];
73
+ const siblingNames = new Set<string>();
74
+ for (const child of children) {
75
+ if (siblingNames.has(child.name)) throw new Error(`namespace collision at ${here}: child '${child.name}' appears more than once`);
76
+ siblingNames.add(child.name);
77
+ }
78
+ if (part.kind === "component" && children.length > 0) {
79
+ throw new Error(`component '${here}' cannot declare child parts`);
80
+ }
81
+ if (part.kind === "group") {
82
+ if (part.reference !== undefined || part.payload !== undefined || part.address !== undefined) {
83
+ throw new Error(`group '${here}' is namespace glue and cannot carry a reference, payload or part address`);
84
+ }
85
+ }
86
+ if (part.payload !== undefined && part.reference === undefined) {
87
+ throw new Error(`part '${here}' declares a payload without the package/part reference that makes it known`);
88
+ }
89
+ if (part.reference !== undefined) validateArc(part.reference, `reference on '${here}'`);
90
+ if (part.payload !== undefined) validateArc(part.payload, `payload on '${here}'`);
91
+ if (part.address !== undefined) {
92
+ if (part.address.trim().length === 0) throw new Error(`part '${here}' has an empty address`);
93
+ if (addresses.has(part.address)) throw new Error(`part address '${part.address}' is used more than once`);
94
+ addresses.add(part.address);
95
+ }
96
+ for (const child of children) visit(child, here);
97
+ };
98
+ visit(config.root, "");
99
+ }
100
+
101
+ function emitPart(part: ApplicationPart, indent: string, rootMetadata?: readonly string[]): string[] {
102
+ const metadata = [
103
+ `${indent} kind = ${usdaString(part.kind)}`,
104
+ ...(part.reference !== undefined
105
+ ? [`${indent} prepend references = @${part.reference.asset}@<${part.reference.prim}>`]
106
+ : []),
107
+ ...(part.payload !== undefined
108
+ ? [`${indent} prepend payload = @${part.payload.asset}@<${part.payload.prim}>`]
109
+ : []),
110
+ ];
111
+ const body = [
112
+ ...(rootMetadata ?? []),
113
+ ...(part.address !== undefined
114
+ ? [`${indent} custom string nomos:partAddress = ${usdaString(part.address)}`]
115
+ : []),
116
+ ...(part.children ?? []).flatMap((child) => ["", ...emitPart(child, `${indent} `)]),
117
+ ];
118
+ return [
119
+ `${indent}def Scope ${usdaString(part.name)} (`,
120
+ ...metadata,
121
+ `${indent})`,
122
+ `${indent}{`,
123
+ ...body,
124
+ `${indent}}`,
125
+ ];
126
+ }
127
+
128
+ /** Emit the application definition itself. This USDA is the configuration that travels. */
129
+ export function applicationCompositionUsda(config: ApplicationCompositionConfig): string {
130
+ validateApplicationComposition(config);
131
+ return [
132
+ "#usda 1.0",
133
+ "(",
134
+ ` defaultPrim = ${usdaString(config.root.name)}`,
135
+ ")",
136
+ "",
137
+ ...emitPart(config.root, "", [
138
+ ` custom string nomos:application = ${usdaString(config.application)}`,
139
+ ...(config.instance !== undefined
140
+ ? [` custom string nomos:applicationInstance = ${usdaString(config.instance)}`]
141
+ : []),
142
+ ]),
143
+ "",
144
+ ].join("\n");
145
+ }
146
+
147
+ /**
148
+ * Publish one package root as a real OpenUSD asset. The root references the authored layer stack;
149
+ * it does not embed a second JSON law or executable application lump.
150
+ */
151
+ export function packageAssetUsda(input: {
152
+ readonly packageName: string;
153
+ readonly packageIdentity: string;
154
+ readonly domains: readonly string[];
155
+ readonly root: PackageRootPart;
156
+ readonly lawAsset: string;
157
+ }): string {
158
+ safeUsdName(input.root.name, "package root");
159
+ if (input.root.kind !== "component" && input.root.kind !== "assembly") {
160
+ throw new Error(`package root '${input.root.name}' must be a component or assembly`);
161
+ }
162
+ if (input.domains.length === 0) throw new Error(`package '${input.packageName}' exports no domains`);
163
+ if (/[@\r\n:]/.test(input.packageName) || /[@\r\n]/.test(input.packageIdentity)) {
164
+ throw new Error("package name or identity is unsafe for a content-addressed asset identifier");
165
+ }
166
+ if (input.lawAsset.length === 0 || /[@\r\n]/.test(input.lawAsset)) throw new Error("package law asset path is unsafe or empty");
167
+ return [
168
+ "#usda 1.0",
169
+ "(",
170
+ ` defaultPrim = ${usdaString(input.root.name)}`,
171
+ ")",
172
+ "",
173
+ `def Scope ${usdaString(input.root.name)} (`,
174
+ ` kind = ${usdaString(input.root.kind)}`,
175
+ ` prepend references = @${input.lawAsset}@</Nomos>`,
176
+ ` assetInfo = {`,
177
+ ` asset identifier = @nomos:package:${input.packageName}:${input.packageIdentity}@`,
178
+ ` string name = ${usdaString(input.packageName)}`,
179
+ ` }`,
180
+ `)`,
181
+ `{`,
182
+ ` custom string nomos:packageName = ${usdaString(input.packageName)}`,
183
+ ` custom string[] nomos:domains = ${usdaStringArray(input.domains)}`,
184
+ ` custom string nomos:rootPart = ${usdaString(input.root.name)}`,
185
+ `}`,
186
+ "",
187
+ ].join("\n");
188
+ }
189
+
190
+ const isResolverAsset = (asset: string): boolean => /^[A-Za-z][A-Za-z0-9+.-]*:/.test(asset);
191
+
192
+ function mapArcs(part: ApplicationPart, map: (arc: CompositionArc) => CompositionArc): ApplicationPart {
193
+ return {
194
+ ...part,
195
+ ...(part.reference !== undefined ? { reference: map(part.reference) } : {}),
196
+ ...(part.payload !== undefined ? { payload: map(part.payload) } : {}),
197
+ ...(part.children !== undefined ? { children: part.children.map((child) => mapArcs(child, map)) } : {}),
198
+ };
199
+ }
200
+
201
+ /** Compile an authoring config into the sole travelling application configuration: USDA. */
202
+ export function compileApplicationComposition(
203
+ config: ApplicationCompositionConfig,
204
+ configPath: string,
205
+ options: { readonly requireUsdcat?: boolean } = {},
206
+ ): { readonly outputPath: string; readonly usda: string; readonly referencedAssets: readonly string[] } {
207
+ const configDir = path.dirname(configPath);
208
+ const outDir = path.resolve(configDir, config.outDir ?? "build");
209
+ mkdirSync(outDir, { recursive: true });
210
+ const outputPath = path.join(outDir, `${config.application}.application.usda`);
211
+ const referencedAssets = new Set<string>();
212
+ const validateLocalReferences = (part: ApplicationPart, at: string): void => {
213
+ const here = `${at}/${part.name}`;
214
+ if (part.reference !== undefined && !isResolverAsset(part.reference.asset)) {
215
+ const absolute = path.resolve(configDir, part.reference.asset);
216
+ if (!existsSync(absolute)) throw new Error(`application asset not found: ${absolute}`);
217
+ const exported = packageRootOfUsda(absolute);
218
+ if (!exported.identifier.startsWith("nomos:package:")) {
219
+ throw new Error(`reference on '${here}' does not resolve to a published Nomos package asset: ${absolute}`);
220
+ }
221
+ if (part.reference.prim !== `/${exported.name}`) {
222
+ throw new Error(`reference on '${here}' asks for '${part.reference.prim}', but '${absolute}' exports '/${exported.name}'`);
223
+ }
224
+ if (part.kind !== exported.kind) {
225
+ throw new Error(`part '${here}' declares kind '${part.kind}', but referenced package root '/${exported.name}' declares kind '${exported.kind}'`);
226
+ }
227
+ }
228
+ for (const child of part.children ?? []) validateLocalReferences(child, here);
229
+ };
230
+ validateLocalReferences(config.root, "");
231
+ const resolved = mapArcs(config.root, (arc) => {
232
+ if (isResolverAsset(arc.asset)) {
233
+ throw new Error(`asset resolver cannot yet prove '${arc.asset}' locally; refusing an unresolved application composition`);
234
+ }
235
+ const absolute = path.resolve(configDir, arc.asset);
236
+ if (!existsSync(absolute)) throw new Error(`application asset not found: ${absolute}`);
237
+ referencedAssets.add(absolute);
238
+ const relative = path.relative(outDir, absolute).split(path.sep).join("/");
239
+ return { ...arc, asset: relative.startsWith(".") ? relative : `./${relative}` };
240
+ });
241
+ const usda = applicationCompositionUsda({ ...config, root: resolved });
242
+ const candidatePath = path.join(outDir, `.${config.application}.application.candidate.usda`);
243
+ writeFileSync(candidatePath, usda, "utf8");
244
+
245
+ const usdcat = existsSync("/usr/bin/usdcat") ? "/usr/bin/usdcat" : undefined;
246
+ try {
247
+ if (usdcat === undefined && options.requireUsdcat === true) {
248
+ throw new Error("stock OpenUSD `usdcat` is required but was not found at /usr/bin/usdcat");
249
+ }
250
+ if (usdcat !== undefined) {
251
+ for (const asset of [...referencedAssets].sort()) {
252
+ execFileSync(usdcat, ["--loadOnly", asset], { stdio: ["ignore", "ignore", "pipe"] });
253
+ }
254
+ execFileSync(usdcat, ["--loadOnly", candidatePath], { stdio: ["ignore", "ignore", "pipe"] });
255
+ execFileSync(usdcat, ["--flatten", candidatePath], { stdio: ["ignore", "ignore", "pipe"] });
256
+ }
257
+ renameSync(candidatePath, outputPath);
258
+ } catch (error) {
259
+ rmSync(candidatePath, { force: true });
260
+ throw error;
261
+ }
262
+ return { outputPath, usda, referencedAssets: [...referencedAssets].sort() };
263
+ }
264
+
265
+ export function isApplicationCompositionConfig(value: unknown): value is ApplicationCompositionConfig {
266
+ if (value === null || typeof value !== "object") return false;
267
+ const candidate = value as { application?: unknown; root?: unknown };
268
+ return typeof candidate.application === "string" && candidate.root !== null && typeof candidate.root === "object";
269
+ }
270
+
271
+ /** Read-only helper used by diagnostics and future asset resolvers. */
272
+ export function packageRootOfUsda(assetPath: string): { readonly name: string; readonly kind: string; readonly identifier: string } {
273
+ const text = readFileSync(assetPath, "utf8");
274
+ const name = /defaultPrim\s*=\s*"([A-Za-z_][A-Za-z0-9_]*)"/.exec(text)?.[1];
275
+ if (name === undefined) throw new Error(`package asset '${assetPath}' has no defaultPrim`);
276
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
277
+ const kind = new RegExp(`def\\s+Scope\\s+"${escaped}"\\s*\\([\\s\\S]*?kind\\s*=\\s*"([^"]+)"`).exec(text)?.[1];
278
+ if (kind === undefined) throw new Error(`package asset '${assetPath}' root '${name}' has no kind`);
279
+ const identifier = /asset\s+identifier\s*=\s*@([^@]+)@/.exec(text)?.[1];
280
+ if (identifier === undefined) throw new Error(`package asset '${assetPath}' root '${name}' has no content identifier`);
281
+ return { name, kind, identifier };
282
+ }
@@ -0,0 +1,151 @@
1
+ import type { DomainModule } from "./codegen_dart.js";
2
+
3
+ type BusinessBirthMetadata = {
4
+ readonly aggregateId?: unknown;
5
+ };
6
+
7
+ type TopologyDirective = DomainModule["directives"][number] & {
8
+ readonly birthsChild?: unknown;
9
+ readonly businessBirth?: BusinessBirthMetadata;
10
+ readonly businessBirthInternal?: unknown;
11
+ };
12
+
13
+ export interface ApplicationTopology {
14
+ /** Co-resident business domains bound to the application workspace. */
15
+ readonly rootDomains: readonly string[];
16
+ /** Deterministic primary facet for generated narration/scenarios; not a storage-topology choice. */
17
+ readonly homeDomain: string;
18
+ readonly homeModule: DomainModule;
19
+ }
20
+
21
+ const domainOf = (module: DomainModule): string => module.domain ?? module.name;
22
+
23
+ const publicLanguageOf = (module: DomainModule): readonly TopologyDirective[] =>
24
+ (module.applicationIntents ?? module.directives)
25
+ .filter((directive) =>
26
+ (directive as TopologyDirective).businessBirthInternal !== true,
27
+ ) as readonly TopologyDirective[];
28
+
29
+ /**
30
+ * Resolve which business domains run together in the application workspace.
31
+ *
32
+ * This is deliberately inferred from business law rather than configured as a framework topology knob:
33
+ *
34
+ * Domains are vocabulary, not storage partitions. They are co-resident by default. A declared
35
+ * cross-domain business birth is the evidence that its target belongs in a child workspace.
36
+ *
37
+ * The primary domain is only a deterministic generated-surface/narration choice: an explicit receiving-group
38
+ * language wins, then a literal `home`, then declaration order. It does not move the other root domains.
39
+ */
40
+ export function resolveApplicationTopology(
41
+ modules: readonly DomainModule[],
42
+ ): ApplicationTopology {
43
+ if (modules.length === 0) {
44
+ throw new Error("generated application topology has no business domains");
45
+ }
46
+
47
+ const byDomain = new Map<string, DomainModule>();
48
+ for (const module of modules) {
49
+ const domain = domainOf(module);
50
+ if (byDomain.has(domain)) {
51
+ throw new Error(
52
+ `generated application topology contains domain '${domain}' more than once`,
53
+ );
54
+ }
55
+ byDomain.set(domain, module);
56
+ }
57
+
58
+ if (modules.length === 1) {
59
+ const homeModule = modules[0]!;
60
+ const homeDomain = domainOf(homeModule);
61
+ return { rootDomains: [homeDomain], homeDomain, homeModule };
62
+ }
63
+
64
+ const declaredReceivingGroups = modules.filter((module) =>
65
+ publicLanguageOf(module).some((directive) =>
66
+ directive.id === "nomosInternalBirthReceivingGroup7c1f5e" &&
67
+ directive.birthsChild === true &&
68
+ directive.aggregateId === "NomosInternalReceivingGroup7c1f5e",
69
+ ),
70
+ );
71
+ if (declaredReceivingGroups.length > 1) {
72
+ throw new Error(
73
+ `generated application topology has more than one receiving-group domain: ${declaredReceivingGroups
74
+ .map(domainOf)
75
+ .sort()
76
+ .join(", ")}. An accountable participant gets one receiving group; keep one group birth language.`,
77
+ );
78
+ }
79
+
80
+ const aggregateOwners = new Map<string, Set<string>>();
81
+ for (const module of modules) {
82
+ const domain = domainOf(module);
83
+ for (const aggregate of module.aggregates) {
84
+ const owners = aggregateOwners.get(aggregate.id) ?? new Set<string>();
85
+ owners.add(domain);
86
+ aggregateOwners.set(aggregate.id, owners);
87
+ }
88
+ }
89
+
90
+ const targetedByAnotherDomain = new Set<string>();
91
+ const childrenByDomain = new Map<string, Set<string>>();
92
+ const birthSources = new Set<string>();
93
+ for (const module of modules) {
94
+ const source = domainOf(module);
95
+ for (const directive of publicLanguageOf(module)) {
96
+ const aggregateId = directive.businessBirth?.aggregateId;
97
+ if (typeof aggregateId !== "string") continue;
98
+ const owners = aggregateOwners.get(aggregateId);
99
+ if (owners === undefined || owners.size !== 1) {
100
+ throw new Error(
101
+ `generated application topology cannot locate the one domain owning born aggregate ` +
102
+ `'${aggregateId}' from '${source}/${directive.id}'`,
103
+ );
104
+ }
105
+ const target = [...owners][0]!;
106
+ birthSources.add(source);
107
+ if (target !== source) {
108
+ targetedByAnotherDomain.add(target);
109
+ const children = childrenByDomain.get(source) ?? new Set<string>();
110
+ children.add(target);
111
+ childrenByDomain.set(source, children);
112
+ }
113
+ }
114
+ }
115
+
116
+ const visiting = new Set<string>();
117
+ const visited = new Set<string>();
118
+ const walk = (domain: string, path: readonly string[]): void => {
119
+ if (visiting.has(domain)) {
120
+ throw new Error(
121
+ `generated application topology contains a cross-workspace birth cycle: ${[...path, domain].join(" -> ")}`,
122
+ );
123
+ }
124
+ if (visited.has(domain)) return;
125
+ visiting.add(domain);
126
+ for (const child of childrenByDomain.get(domain) ?? []) walk(child, [...path, domain]);
127
+ visiting.delete(domain);
128
+ visited.add(domain);
129
+ };
130
+ for (const domain of byDomain.keys()) walk(domain, []);
131
+
132
+ const rootModules = modules.filter((module) => !targetedByAnotherDomain.has(domainOf(module)));
133
+ const rootDomains = rootModules.map(domainOf);
134
+ if (rootDomains.length === 0) {
135
+ throw new Error(
136
+ "Nomos cannot determine an application workspace because every business domain is born by another domain.",
137
+ );
138
+ }
139
+
140
+ const receivingGroupModule = declaredReceivingGroups[0];
141
+ if (receivingGroupModule !== undefined && !rootModules.includes(receivingGroupModule)) {
142
+ throw new Error(
143
+ `generated application topology places its receiving-group domain '${domainOf(receivingGroupModule)}' inside a child workspace`,
144
+ );
145
+ }
146
+ const homeModule = receivingGroupModule
147
+ ?? rootModules.find((module) => domainOf(module) === "home")
148
+ ?? rootModules.find((module) => birthSources.has(domainOf(module)))
149
+ ?? rootModules[0]!;
150
+ return { rootDomains, homeDomain: domainOf(homeModule), homeModule };
151
+ }
@@ -341,7 +341,7 @@ export function sourceFrontierId(sourceGenesis: string): string {
341
341
  * it); the gate's monotone arm refuses any attestation BEHIND it. Reader-chain state — re-folded
342
342
  * at replay, so the predicate is deterministic forever.
343
343
  */
344
- export const NomosSourceFrontier = aggregate("NomosSourceFrontier", {
344
+ export const NomosSourceFrontier = Object.assign(aggregate("NomosSourceFrontier", {
345
345
  /** The source workspace name the edge was declared under (routing/diagnostics). */
346
346
  source: t.string().merge(Lww),
347
347
  /** The highest source signer-epoch an admitted attestation has shown this reader. */
@@ -350,7 +350,7 @@ export const NomosSourceFrontier = aggregate("NomosSourceFrontier", {
350
350
  maxAttestedAtSeen: t.int().merge(Lww),
351
351
  /** The source head oid at the highest-seen position (diagnostics/audit). */
352
352
  headAtMax: t.string().merge(Lww),
353
- }).public();
353
+ }).public(), { businessBirthInternal: true as const });
354
354
 
355
355
 
356
356
  // ── the ensure() hook — breaks the read.ts → authoring.ts module-init cycle ────────────────────
@@ -0,0 +1,9 @@
1
+ // NOMOS — internal compile boundary. This identifier is replaced with the literal `true` only
2
+ // while building the executable law. Ordinary tenant-domain imports leave it undefined, so every
3
+ // declaration-time validation still runs in Node and fails beside the code that caused it.
4
+ declare const __nmsRuntimeLaw: boolean | undefined;
5
+
6
+ /** True only inside compiler-emitted executable law, never in tenant authoring code. */
7
+ export function isRuntimeLaw(): boolean {
8
+ return typeof __nmsRuntimeLaw !== "undefined" && __nmsRuntimeLaw === true;
9
+ }
@@ -76,7 +76,7 @@ const bornFounderSignerWriteOnce = workspaceInvariant("FounderSignerWriteOnce")
76
76
  refAs(
77
77
  "tuple",
78
78
  "RelationTuple",
79
- "pre:" + relationTupleInstanceId(signerObject(String((ctx.intent ?? {}).principal ?? "")), "signer"),
79
+ "pre:" + relationTupleInstanceId(signerObject(String(ctx.principal ?? "")), "signer"),
80
80
  ),
81
81
  ])
82
82
  .assert(({ tuple }, ctx) => {
package/src/births.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { z } from "zod";
7
7
  import type { AggregateHandle } from "./aggregate.js";
8
8
  import type { AggregateRef } from "./authoring.js";
9
+ import type { Ports } from "./ctx.js";
9
10
  import type { Field, FieldValue } from "./fields.js";
10
11
 
11
12
  /**
@@ -72,15 +73,23 @@ export interface BusinessBirthDecl<
72
73
  > {
73
74
  readonly __isBusinessBirth: true;
74
75
  readonly aggregate: AggregateHandle<string, F>;
75
- readonly payloadSchema: z.ZodObject<z.ZodRawShape>;
76
+ readonly payloadSchema: z.ZodObject<z.ZodRawShape> & z.ZodType<P>;
76
77
  /** A stable business key the caller already knows (an estate reference, account number, etc.). */
77
78
  readonly key: (payload: P) => string;
78
79
  /** The declared business fact used by {@link key}; retained for generated clients to pre-address custody. */
79
80
  readonly keyField: string;
80
81
  /** Framework-derived initial authoring. Tenant code marks aggregate fields `.fromBirth()` instead. */
81
- readonly plan: (root: AggregateRef<F>, payload: P) => unknown;
82
+ readonly plan: (root: AggregateRef<F>, payload: P, ctx: Ports) => unknown;
83
+ /** Business roles the founder holds from the first valid state. Nomos seats them inside genesis. */
84
+ readonly founderRoles: readonly string[];
85
+ /** The business noun those founder roles govern. */
86
+ readonly founderScope: string;
82
87
  /** Optional business noun used only to make the durable address readable. Defaults to the aggregate id. */
83
88
  readonly namespace?: string;
89
+ /** Owning business domain, needed only when another domain composes this birth into a wider intent. */
90
+ readonly domain?: string;
91
+ /** Name the owning business domain once so other intents can compose this birth without a raw recipe. */
92
+ readonly inDomain: (domain: string) => BusinessBirthDecl<P, F>;
84
93
  }
85
94
 
86
95
  export type BirthFactKeys<F extends Record<string, Field>> = {
@@ -99,6 +108,18 @@ export interface BusinessBirthSpec<F extends Record<string, Field>> {
99
108
  /** One declared birth fact that gives this business life its stable address. */
100
109
  readonly key: BirthStringFactKeys<F>;
101
110
  readonly namespace?: string;
111
+ /** Business policy: roles held by the founder from the aggregate's first valid state. */
112
+ readonly founder?: {
113
+ readonly roles: readonly string[];
114
+ /** Business noun governed by those roles. Defaults to the born aggregate noun. */
115
+ readonly scope?: string;
116
+ };
117
+ /** Additional atomic business initialization which belongs to the same birth. */
118
+ readonly initialise?: (
119
+ root: AggregateRef<F>,
120
+ payload: BusinessBirthPayload<F>,
121
+ ctx: Ports,
122
+ ) => unknown;
102
123
  }
103
124
 
104
125
  /** @internal Record an already-lowered child recipe during a framework directive plan. Application law
@@ -137,19 +158,46 @@ export function birth<F extends Record<string, Field>>(
137
158
  );
138
159
  }
139
160
  const payloadSchema = z.object(Object.fromEntries(facts.map(([name, field]) => [name, field.zod])));
161
+ const founderRoles = [...new Set(spec.founder?.roles ?? [])];
162
+ for (const role of founderRoles) {
163
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(role)) {
164
+ throw new Error(
165
+ `birth(${aggregate.id}): founder role '${role}' is not a business role name`,
166
+ );
167
+ }
168
+ }
169
+ const founderScope = (spec.founder?.scope ?? aggregate.id)
170
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
171
+ .toLowerCase();
172
+ if (!/^[a-z][a-z0-9-]*$/.test(founderScope)) {
173
+ throw new Error(
174
+ `birth(${aggregate.id}): founder scope '${founderScope}' is not a business noun`,
175
+ );
176
+ }
140
177
  type Payload = BusinessBirthPayload<F>;
141
- return {
178
+ const declaration = {
142
179
  __isBusinessBirth: true,
143
180
  aggregate,
144
181
  payloadSchema,
145
182
  key: (payload: Payload) => String(payload[spec.key]),
146
183
  keyField: spec.key,
147
- plan: (root: AggregateRef<F>, payload: Payload) => {
184
+ plan: (root: AggregateRef<F>, payload: Payload, ctx: Ports) => {
148
185
  const writable = root as unknown as { set(field: string, value: unknown): unknown };
149
186
  for (const [name] of facts) writable.set(name, payload[name as keyof Payload]);
187
+ return spec.initialise?.(root, payload, ctx);
150
188
  },
189
+ founderRoles,
190
+ founderScope,
151
191
  ...(spec.namespace !== undefined ? { namespace: spec.namespace } : {}),
152
- } as BusinessBirthDecl<Payload, F>;
192
+ };
193
+ const inDomain = (domain: string): BusinessBirthDecl<Payload, F> => {
194
+ const name = domain.trim();
195
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
196
+ throw new Error(`birth(${aggregate.id}).inDomain(...): '${domain}' is not a business domain name`);
197
+ }
198
+ return { ...declaration, domain: name, inDomain } as unknown as BusinessBirthDecl<Payload, F>;
199
+ };
200
+ return { ...declaration, inDomain } as unknown as BusinessBirthDecl<Payload, F>;
153
201
  }
154
202
  footprint.push(recipeOrAggregate as BirthRecipe);
155
203
  }