@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
@@ -28,9 +28,6 @@
28
28
  * is carried in the package USD IR + the committed `nomos.stable-ids.json` lockfile; the old
29
29
  * `domain_identity_*.json` sidecars (and `writeIdentity`) are REMOVED. The per-domain hash is surfaced
30
30
  * in the compile summary.
31
- * * `packageUsda(usdJson, javascript)` — the `.package.usda` envelope, byte-exact
32
- * to `build_nomos.mjs` `packageUsda()` == Rust `domain_package_from_js`
33
- * (deterministic-rquickjs/src/domain_package.rs).
34
31
  * * `emitUsdJsonForModules(modules)` — the one-line USD-IR emit every
35
32
  * `emit_*_usd_json.ts` wrapped (`emitUsd` over `/Nomos/<domain>` layers).
36
33
  */
@@ -53,6 +50,12 @@ import type { CombinedDecl } from "./combined.js";
53
50
  import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
54
51
  import type { WorkspaceTypeDecl } from "./workspace_type.js";
55
52
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
53
+ import {
54
+ applicationLanguageOf,
55
+ assertApplicationLanguageCoversAuthoredDirectives,
56
+ executableDirectivesForLanguage,
57
+ remapWorkspaceInvariantsForLanguage,
58
+ } from "./intent.js";
56
59
  import { synthesizePreconditionInvariants } from "./precondition.js";
57
60
  import {
58
61
  BIRTH_METER_AGGREGATES,
@@ -69,7 +72,11 @@ import {
69
72
  type TrustSourceDecl,
70
73
  type TrustPolicyDecl,
71
74
  } from "./attested.js";
72
- import { isRelationTypeDecl, type RelationTypeDecl } from "./relation_schema.js";
75
+ import {
76
+ discoverRelationTypeDeclarations,
77
+ mergeRelationTypeDeclarations,
78
+ type RelationTypeDecl,
79
+ } from "./relation_schema.js";
73
80
  import { domainHash, emitManifestBytes } from "./manifest.js";
74
81
  import { collectValueObjectDescriptors } from "./value_object.js";
75
82
  import { emitUsd } from "./usd.js";
@@ -86,7 +93,10 @@ import {
86
93
  applyConflictPolicyResolvers,
87
94
  conflictPoliciesOf,
88
95
  } from "./conflicts.js";
89
- import { expandBusinessBirthExports } from "./business_birth.js";
96
+ import {
97
+ expandBusinessBirthExports,
98
+ isBusinessBirthDirective,
99
+ } from "./business_birth.js";
90
100
  import { withOwnedCustodyExports } from "./framework/owned_custody.js";
91
101
  import { withBusinessCorrectionExports } from "./framework/business_corrections.js";
92
102
  import {
@@ -120,7 +130,7 @@ export function aggregatesOf(mod: Mod): AggregateHandle[] {
120
130
  return out;
121
131
  }
122
132
 
123
- /** Collect every exported `contract(...)` handle (`__isContract` duck-type). */
133
+ /** Collect every exported `recordContract(...)` handle (`__isContract` duck-type). */
124
134
  export function contractsOf(mod: Mod): import("./contract.js").ContractHandle[] {
125
135
  const out: import("./contract.js").ContractHandle[] = [];
126
136
  for (const v of Object.values(mod)) {
@@ -193,8 +203,16 @@ export interface DomainModuleSpec {
193
203
  export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
194
204
  const domain = spec.domain ?? spec.name;
195
205
  const conflictPolicies = conflictPoliciesOf(spec.modules, domain);
206
+ const applicationLanguage = applicationLanguageOf(spec.modules, domain);
196
207
  let merged: Mod = {};
197
208
  for (const m of spec.modules) merged = { ...merged, ...m };
209
+ if (applicationLanguage !== undefined) {
210
+ assertApplicationLanguageCoversAuthoredDirectives(
211
+ applicationLanguage,
212
+ directivesOf(merged),
213
+ domain,
214
+ );
215
+ }
198
216
  merged = expandBusinessBirthExports(merged, spec.domain ?? spec.name);
199
217
  merged = withOwnedCustodyExports(merged);
200
218
  merged = withBusinessCorrectionExports(merged);
@@ -219,10 +237,28 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
219
237
  // discovered it off the same expansion). Role-free domains are byte-identical (era rule).
220
238
  merged = expandRoleExports(merged);
221
239
  const roles = rolesOf(merged);
240
+ const exportedRelationTypes = discoverRelationTypeDeclarations(merged);
222
241
 
223
242
  const byId = new Map<string, AggregateHandle>();
224
243
  for (const a of [...aggregatesOf(merged), ...(spec.extraAggregates ?? [])]) byId.set(a.id, a);
225
- const directives = applyConflictPolicyResolvers(directivesOf(merged), conflictPolicies);
244
+ for (const policy of applicationLanguage?.workspaceSharing ?? []) {
245
+ byId.set(policy.invitationLaw.claim.id, policy.invitationLaw.claim);
246
+ }
247
+ const discoveredDirectives = directivesOf(merged);
248
+ const directives = applyConflictPolicyResolvers(
249
+ applicationLanguage === undefined
250
+ ? discoveredDirectives
251
+ : executableDirectivesForLanguage(applicationLanguage, discoveredDirectives, domain),
252
+ conflictPolicies,
253
+ );
254
+ // Invitations are ordinary, replayed law hidden behind the generated application. Each sharing
255
+ // policy contributes one possession-proof redemption directive; it is injected after the tenant
256
+ // language has been closed so it cannot become a second public lane.
257
+ for (const policy of applicationLanguage?.workspaceSharing ?? []) {
258
+ if (!directives.some((candidate) => candidate.id === policy.invitationLaw.redeem.id)) {
259
+ directives.push(policy.invitationLaw.redeem);
260
+ }
261
+ }
226
262
  // RELATIONS AUTO-ENSURE + AUTO-COMPOSE (co2 recovery DX): a domain that USES relation law — declares a
227
263
  // relation schema, a `.requires(...)`-gated directive, or relate-family/signer directives ensuring
228
264
  // RelationTuple — needs the framework RelationTuple aggregate IN the schema on TWO counts: (1) the deploy
@@ -264,6 +300,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
264
300
  }
265
301
  const usesRelations =
266
302
  declaresPrivateRead ||
303
+ exportedRelationTypes.length > 0 ||
267
304
  (spec.relationTypes ?? []).length > 0 ||
268
305
  directives.some((d) => (d as { requiresRelationGate?: unknown }).requiresRelationGate !== undefined) ||
269
306
  directives.some((d) => d.aggregateId === RelationTuple.id);
@@ -343,6 +380,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
343
380
  const queryById = new Map<string, QueryDecl>();
344
381
  for (const query of spec.queries ?? []) queryById.set(query.id, query);
345
382
  const queries = [...queryById.values()];
383
+ for (const policy of applicationLanguage?.workspaceSharing ?? []) {
384
+ if (!queries.some((candidate) => candidate.id === policy.invitationLaw.pendingByRecipient.id)) {
385
+ queries.push(policy.invitationLaw.pendingByRecipient);
386
+ }
387
+ }
346
388
  const counts = [...(spec.counts ?? [])];
347
389
  const spatials = [...(spec.spatials ?? [])];
348
390
  const deriveds = [...(spec.deriveds ?? [])];
@@ -368,7 +410,17 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
368
410
  for (const invariant of spec.workspaceInvariants ?? []) {
369
411
  workspaceInvariantById.set(invariant.id, invariant);
370
412
  }
371
- const workspaceInvariants = [...workspaceInvariantById.values()];
413
+ const workspaceInvariants = applicationLanguage === undefined
414
+ ? [...workspaceInvariantById.values()]
415
+ : remapWorkspaceInvariantsForLanguage(
416
+ [...workspaceInvariantById.values()],
417
+ applicationLanguage,
418
+ );
419
+ for (const policy of applicationLanguage?.workspaceSharing ?? []) {
420
+ if (!workspaceInvariants.some((candidate) => candidate.id === policy.invitationLaw.gate.id)) {
421
+ workspaceInvariants.push(policy.invitationLaw.gate);
422
+ }
423
+ }
372
424
  if (declaresBirth) {
373
425
  const haveInvariant = new Set(workspaceInvariants.map((w) => w.id));
374
426
  for (const lifecycle of [
@@ -411,14 +463,10 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
411
463
  queries.push(internalDelegationQuery);
412
464
  }
413
465
  }
414
- const relationTypeByKey = new Map<string, RelationTypeDecl>();
415
- for (const value of Object.values(merged)) {
416
- if (isRelationTypeDecl(value)) relationTypeByKey.set(`${value.global === true ? "global" : "local"}:${value.type}`, value);
417
- }
418
- for (const value of spec.relationTypes ?? []) {
419
- relationTypeByKey.set(`${value.global === true ? "global" : "local"}:${value.type}`, value);
420
- }
421
- const relationTypes = [...relationTypeByKey.values()];
466
+ const relationTypes = mergeRelationTypeDeclarations([
467
+ ...exportedRelationTypes,
468
+ ...(spec.relationTypes ?? []),
469
+ ]);
422
470
  // RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX — same idiom as the meter
423
471
  // queries above): a domain that declares relation law (a relation schema, a `.requires(...)` gated
424
472
  // directive, or relate-family/signer directives ensuring RelationTuple) gets the framework's
@@ -576,12 +624,26 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
576
624
  directives,
577
625
  spec.domain ?? spec.name,
578
626
  );
627
+ const publicApplicationIntents = applicationLanguage === undefined
628
+ ? undefined
629
+ : [
630
+ ...applicationLanguage.intents,
631
+ ...directives.filter((directive) =>
632
+ isBusinessBirthDirective(directive) &&
633
+ !applicationLanguage.intents.some((intent) => intent.id === directive.id)),
634
+ ];
579
635
 
580
636
  return {
581
637
  name: spec.name,
582
638
  domain: spec.domain ?? spec.name,
583
639
  aggregates,
584
640
  directives,
641
+ ...(publicApplicationIntents !== undefined
642
+ ? { applicationIntents: publicApplicationIntents }
643
+ : {}),
644
+ ...(applicationLanguage !== undefined && applicationLanguage.workspaceSharing.length > 0
645
+ ? { applicationWorkspaceSharing: [...applicationLanguage.workspaceSharing] }
646
+ : {}),
585
647
  // Diagnostic metadata for the compile summary — NOT law (not hashed). Omit-when-empty.
586
648
  ...(autoDeclaredEnsures.length > 0 ? { autoDeclaredEnsures } : {}),
587
649
  // ROLE SUGAR codegen metadata — NOT law (not hashed): drives the `canEdit<Agg>`/`canView<Agg>`
@@ -1125,63 +1187,9 @@ export function buildIdentity(modules: readonly DomainModule[]): IdentityEmit {
1125
1187
  // per-domain hash is surfaced in the compile summary.
1126
1188
 
1127
1189
  // ─────────────────────────────────────────────────────────────────────────────────
1128
- // The .package.usda envelope + the USD-IR emit
1190
+ // The canonical USD-IR emit
1129
1191
  // ─────────────────────────────────────────────────────────────────────────────────
1130
1192
 
1131
- /** The package format marker the kernel checks (domain_package.rs / build_nomos.mjs). */
1132
- export const NOMOS_DOMAIN_PACKAGE_FORMAT = "nomos.openusd-ir-package.v1";
1133
-
1134
- /** Lowercase hex of the UTF-8 bytes (== build_nomos.mjs `hexUtf8` == Rust `hex_encode`). */
1135
- export function hexUtf8(text: string): string {
1136
- return Buffer.from(text, "utf8").toString("hex");
1137
- }
1138
-
1139
- /**
1140
- * The `.package.usda` envelope — BYTE-EXACT to `build_nomos.mjs` `packageUsda()` and
1141
- * Rust `domain_package_from_js` (deterministic-rquickjs/src/domain_package.rs:11-33).
1142
- * The kernel's acceptance is substring-based field extraction over this template; the
1143
- * domainHash the deploy names is sha256 over the WHOLE returned string (trailing
1144
- * newline included) exactly as committed.
1145
- */
1146
- export function packageUsda(usdJson: string, javascript: string, opts: { compact?: boolean } = {}): string {
1147
- // THE COMPACT ERA (opt-in, 2026-07-04): base64 blob fields (`*B64`, 1.33× text) instead of hex
1148
- // (2×). Requires every consumer to carry the decode arm (kernel domain_package.rs extract_blob +
1149
- // the JS parsers) — shipped ahead of this emit, so `compact: true` is the tenant's explicit
1150
- // choice once their stack is current. Hex stays the default emit until a future major; hex
1151
- // packages parse FOREVER (the era rule). sha256(the returned string) is the domainHash either
1152
- // way — a compact recompile is ONE named hash move (batched with minify + locale-strip).
1153
- if (opts.compact) {
1154
- return `#usda 1.0
1155
- (
1156
- customData = {
1157
- string nomos:format = "${NOMOS_DOMAIN_PACKAGE_FORMAT}"
1158
- }
1159
- )
1160
-
1161
- def "NomosDomainPackage"
1162
- {
1163
- custom string nomos:usdJsonB64 = "${Buffer.from(usdJson, "utf8").toString("base64")}"
1164
- custom string nomos:executable:language = "javascript"
1165
- custom string nomos:executable:javascriptB64 = "${Buffer.from(javascript, "utf8").toString("base64")}"
1166
- }
1167
- `;
1168
- }
1169
- return `#usda 1.0
1170
- (
1171
- customData = {
1172
- string nomos:format = "${NOMOS_DOMAIN_PACKAGE_FORMAT}"
1173
- }
1174
- )
1175
-
1176
- def "NomosDomainPackage"
1177
- {
1178
- custom string nomos:usdJsonHex = "${hexUtf8(usdJson)}"
1179
- custom string nomos:executable:language = "javascript"
1180
- custom string nomos:executable:javascriptHex = "${hexUtf8(javascript)}"
1181
- }
1182
- `;
1183
- }
1184
-
1185
1193
  /**
1186
1194
  * The USD-IR JSON for a package's modules: `emitUsd` over `/Nomos/<domain>` layers,
1187
1195
  * `JSON.stringify`'d EXACTLY like every `emit_*_usd_json.ts` (single line, insertion
@@ -1,7 +1,7 @@
1
1
  // NOMOS — one business birth at the tenant surface; compute-science mechanics lower here.
2
2
 
3
3
  import { z } from "zod";
4
- import { BUSINESS_BIRTH_SEED_PAYLOAD_FIELD } from "./aggregate.js";
4
+ import { BUSINESS_BIRTH_SEED_PAYLOAD_FIELD, OWNED_READ_RELATION } from "./aggregate.js";
5
5
  import { ensure } from "./authoring.js";
6
6
  import { directive, type Directive } from "./directive.js";
7
7
  import {
@@ -12,6 +12,7 @@ import {
12
12
  type ParentAttestedBirthMarker,
13
13
  } from "./births.js";
14
14
  import { readFrameworkDeclared } from "./read.js";
15
+ import type { Field } from "./fields.js";
15
16
  import {
16
17
  AuthProvider,
17
18
  authProvider,
@@ -38,7 +39,7 @@ import {
38
39
  birthAttestation,
39
40
  seedBirthAttestation,
40
41
  } from "./framework/home.js";
41
- import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
42
+ import { RelationTuple, relationTuplesByObject, writeTuple } from "./framework/relations.js";
42
43
  import {
43
44
  SignerDelegation,
44
45
  enrollDelegation,
@@ -57,6 +58,9 @@ export interface BusinessBirthMetadata {
57
58
  readonly rootId: string;
58
59
  readonly namespace: string;
59
60
  readonly keyField: string;
61
+ /** Private proof/lowering policy; omitted from every generated tenant payload. */
62
+ readonly founderRoles: readonly string[];
63
+ readonly founderScope: string;
60
64
  }
61
65
 
62
66
  export interface BusinessBirthDirective extends Directive<Record<string, unknown>> {
@@ -104,7 +108,16 @@ function businessBirthsOf(mod: Record<string, unknown>): BusinessBirthDecl[] {
104
108
  );
105
109
  }
106
110
 
107
- function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unknown> {
111
+ // A declared birth remains reusable as framework law, but once a wider business
112
+ // intent composes it there must not also be a generated direct-birth lane. That
113
+ // second lane could skip the intent's parent-workspace consequences and let two
114
+ // apparently valid tenant APIs create observably different worlds.
115
+ const applicationIntentOnlyBirths = new WeakSet<object>();
116
+
117
+ function lowerOne<P extends Record<string, unknown>, F extends Record<string, Field>>(
118
+ decl: BusinessBirthDecl<P, F>,
119
+ domain: string,
120
+ ): Record<string, unknown> {
108
121
  const fields = (decl.payloadSchema as z.ZodObject<z.ZodRawShape>).shape;
109
122
  for (const field of INTERNAL_FIELDS) {
110
123
  if (Object.prototype.hasOwnProperty.call(fields, field)) {
@@ -116,8 +129,11 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
116
129
  }
117
130
 
118
131
  const stem = decl.aggregate.id.replace(/[^A-Za-z0-9]/g, "");
119
- const namespace = (decl.namespace ?? decl.aggregate.id).replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
120
- const rootId = `${namespace}:self`;
132
+ const aggregateNoun = decl.aggregate.id.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
133
+ const namespace = (decl.namespace ?? aggregateNoun).replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
134
+ // Record identity follows the aggregate noun; workspace addressing follows the optional business
135
+ // namespace. Changing a URL/name must never silently change which aggregate row is the root.
136
+ const rootId = `${aggregateNoun}:self`;
121
137
  const seedId = `nomosSeed${stem}`;
122
138
  const birthId = `nomosBirth${stem}`;
123
139
  const publicSchema = decl.payloadSchema as z.ZodObject<z.ZodRawShape>;
@@ -135,9 +151,42 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
135
151
  const seedBase = directive(seedId)
136
152
  .ensures(decl.aggregate)
137
153
  .payload(seedSchema)
138
- .plan((p) => {
139
- decl.plan(ensure(decl.aggregate, rootId), p);
140
- return [];
154
+ .plan((p, ctx) => {
155
+ const businessOps = decl.plan(ensure(decl.aggregate, rootId), p as P, ctx);
156
+ const founder = p.principal as string;
157
+ const grantedAt = p.bornAt as string;
158
+ return [
159
+ ...(Array.isArray(businessOps) ? businessOps : []),
160
+ // Every born workspace is operable from intent zero. This is framework administration, not a
161
+ // tenant role, and therefore never appears in the generated application surface.
162
+ ...writeTuple({
163
+ object: "workspace:self",
164
+ relation: "admin",
165
+ subject: founder,
166
+ grantedBy: founder,
167
+ grantedAt,
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
+ : []),
178
+ // Business ownership remains explicit policy. The tenant names roles; Nomos owns subjects,
179
+ // provenance, tuple mechanics and the atomic genesis ordering underneath them.
180
+ ...decl.founderRoles.flatMap((relation) =>
181
+ writeTuple({
182
+ object: `${decl.founderScope}:self`,
183
+ relation,
184
+ subject: founder,
185
+ grantedBy: founder,
186
+ grantedAt,
187
+ }),
188
+ ),
189
+ ];
141
190
  });
142
191
  const seed = {
143
192
  ...seedBase,
@@ -149,7 +198,7 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
149
198
  .payload(parentSchema)
150
199
  .plan((p) => {
151
200
  const founder = p.principal as string;
152
- const child = workspaceNameForKey(namespace, String(decl.key(p)));
201
+ const child = workspaceNameForKey(namespace, String(decl.key(p as P)));
153
202
  const childPayload = { ...(p as Record<string, unknown>) };
154
203
  if (encryptedBirth) {
155
204
  const rawSeed = childPayload[BUSINESS_BIRTH_SEED_PAYLOAD_FIELD];
@@ -251,8 +300,14 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
251
300
  rootId,
252
301
  namespace,
253
302
  keyField: decl.keyField,
303
+ founderRoles: decl.founderRoles,
304
+ founderScope: decl.founderScope,
254
305
  };
255
- const parent = { ...birthBase, businessBirth: metadata } as BusinessBirthDirective;
306
+ const parent = {
307
+ ...birthBase,
308
+ businessBirth: metadata,
309
+ ...(applicationIntentOnlyBirths.has(decl) ? { businessBirthInternal: true } : {}),
310
+ } as BusinessBirthDirective;
256
311
  return {
257
312
  [`__nomos_${birthId}`]: parent,
258
313
  [`__nomos_${seedId}`]: seed,
@@ -295,6 +350,34 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
295
350
  };
296
351
  }
297
352
 
353
+ /**
354
+ * Lower a declared business birth for use as one consequence of a wider business intent.
355
+ * The child recipe remains private; the outer intent carries the birth gate and can add
356
+ * same-workspace business consequences atomically.
357
+ */
358
+ export function businessBirthConsequence<
359
+ P extends Record<string, unknown>,
360
+ F extends Record<string, Field>,
361
+ >(
362
+ decl: BusinessBirthDecl<P, F>,
363
+ ): BusinessBirthDirective {
364
+ if (decl.domain === undefined) {
365
+ throw new Error(
366
+ `birth(${decl.aggregate.id}) is composed by another intent, so name its owning domain once with ` +
367
+ `.inDomain("...")`,
368
+ );
369
+ }
370
+ applicationIntentOnlyBirths.add(decl);
371
+ const lowered = Object.values(lowerOne(decl, decl.domain)).find(
372
+ (value): value is BusinessBirthDirective =>
373
+ !!value && typeof value === "object" && (value as { businessBirth?: unknown }).businessBirth !== undefined,
374
+ );
375
+ if (lowered === undefined) {
376
+ throw new Error(`birth(${decl.aggregate.id}): Nomos could not lower the declared business birth`);
377
+ }
378
+ return lowered;
379
+ }
380
+
298
381
  /** Lower the one tenant-facing birth declaration into ordinary, gate-visible framework directives. */
299
382
  export function expandBusinessBirthExports<M extends Record<string, unknown>>(
300
383
  mod: M,
@@ -29,6 +29,7 @@ import { directive, type Directive, type RequirableDirective } from "./directive
29
29
  import { instance, type AggregateHandle } from "./aggregate.js";
30
30
  import { clear } from "./ops.js";
31
31
  import type { Field } from "./fields.js";
32
+ import { idOf } from "./value_object.js";
32
33
 
33
34
  /** Capitalize the first character (the clear-directive id joins `clear` + AggId + Field). */
34
35
  function cap(s: string): string {
@@ -51,9 +52,10 @@ export function clearDirectiveId(aggregateId: string, field: string): string {
51
52
  * `clearRequires`.
52
53
  */
53
54
  export function makeClearDirective(agg: AggregateHandle, field: string, requires?: string): Directive<unknown> {
55
+ const payloadId = idOf(agg);
54
56
  const built = directive(clearDirectiveId(agg.id, field))
55
57
  .mutates(agg)
56
- .payload(z.object({ id: z.string().min(1), clearedAt: z.string() }))
58
+ .payload(z.object({ id: payloadId, clearedAt: z.string() }))
57
59
  .plan((p: { id: string; clearedAt: string }) => [clear(instance(agg, p.id), field as never)])
58
60
  .doc(
59
61
  `Explicitly clears the '${field}' field of a ${agg.id} (writes an explicit null — the declared clearable lane).`,