@githolon/dsl 0.103.0 → 0.103.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/births.ts CHANGED
@@ -84,6 +84,11 @@ export interface BusinessBirthDecl<
84
84
  readonly founderRoles: readonly string[];
85
85
  /** The business noun those founder roles govern. */
86
86
  readonly founderScope: string;
87
+ /** Explicit policy inherited by the BORN workspace. Zero declares a leaf workspace. */
88
+ readonly maxChildWorkspacesPerUser: number;
89
+ /** @internal Compiler-derived namespaces installed into the born workspace. The declaration owns
90
+ * the value; this list only repeats that value wherever the composed law requires its own state. */
91
+ __nomosBirthPolicyDomains?: readonly string[];
87
92
  /** Optional business noun used only to make the durable address readable. Defaults to the aggregate id. */
88
93
  readonly namespace?: string;
89
94
  /** Owning business domain, needed only when another domain composes this birth into a wider intent. */
@@ -114,6 +119,11 @@ export interface BusinessBirthSpec<F extends Record<string, Field>> {
114
119
  /** Business noun governed by those roles. Defaults to the born aggregate noun. */
115
120
  readonly scope?: string;
116
121
  };
122
+ /** Law for the workspace being born. This is mandatory because silence must not become a
123
+ * compiler-chosen quota; zero explicitly declares that the born workspace is a leaf. */
124
+ readonly children: {
125
+ readonly maxWorkspacesPerUser: number;
126
+ };
117
127
  /** Additional atomic business initialization which belongs to the same birth. */
118
128
  readonly initialise?: (
119
129
  root: AggregateRef<F>,
@@ -174,6 +184,13 @@ export function birth<F extends Record<string, Field>>(
174
184
  `birth(${aggregate.id}): founder scope '${founderScope}' is not a business noun`,
175
185
  );
176
186
  }
187
+ const maxChildWorkspacesPerUser = spec.children?.maxWorkspacesPerUser;
188
+ if (!Number.isInteger(maxChildWorkspacesPerUser) || maxChildWorkspacesPerUser < 0) {
189
+ throw new Error(
190
+ `birth(${aggregate.id}): children.maxWorkspacesPerUser must be an explicit non-negative integer; ` +
191
+ "zero declares a leaf workspace and Nomos has no compiled fallback",
192
+ );
193
+ }
177
194
  type Payload = BusinessBirthPayload<F>;
178
195
  const declaration = {
179
196
  __isBusinessBirth: true,
@@ -188,6 +205,7 @@ export function birth<F extends Record<string, Field>>(
188
205
  },
189
206
  founderRoles,
190
207
  founderScope,
208
+ maxChildWorkspacesPerUser,
191
209
  ...(spec.namespace !== undefined ? { namespace: spec.namespace } : {}),
192
210
  };
193
211
  const inDomain = (domain: string): BusinessBirthDecl<Payload, F> => {
@@ -51,6 +51,8 @@ import {
51
51
 
52
52
  /** Private recipe selectors. The kernel resolves these against the parent's current installed law. */
53
53
  export const CURRENT_LAW_PREFIX = "nomos:current-law:";
54
+ /** @internal Compiler-only domain enumeration. It carries no allowance or authority. */
55
+ export const BUSINESS_BIRTH_POLICY_DOMAINS_EXPORT = "__nomos_birth_policy_domains_c9f14a";
54
56
 
55
57
  export interface BusinessBirthMetadata {
56
58
  readonly aggregateId: string;
@@ -124,6 +126,7 @@ const applicationIntentOnlyBirths = new WeakSet<object>();
124
126
  function lowerOne<P extends Record<string, unknown>, F extends Record<string, Field>>(
125
127
  decl: BusinessBirthDecl<P, F>,
126
128
  domain: string,
129
+ initialBirthPolicyDomains: readonly string[] = [domain],
127
130
  ): Record<string, unknown> {
128
131
  const fields = (decl.payloadSchema as z.ZodObject<z.ZodRawShape>).shape;
129
132
  for (const field of INTERNAL_FIELDS) {
@@ -245,6 +248,16 @@ function lowerOne<P extends Record<string, unknown>, F extends Record<string, Fi
245
248
  payload: { domainHash: `${CURRENT_LAW_PREFIX}${domain}` },
246
249
  domainHash: `${CURRENT_LAW_PREFIX}${domain}`,
247
250
  },
251
+ ...(decl.__nomosBirthPolicyDomains ?? initialBirthPolicyDomains).map((policyDomain) => ({
252
+ domain: policyDomain,
253
+ directiveId: "seedBirthPolicy",
254
+ actor: founder,
255
+ payload: {
256
+ maxBirthsPerPrincipal: decl.maxChildWorkspacesPerUser,
257
+ setBy: founder,
258
+ setAt: p.bornAt,
259
+ },
260
+ })),
248
261
  {
249
262
  domain,
250
263
  directiveId: seedId,
@@ -396,10 +409,29 @@ export function businessBirthConsequence<
396
409
  return lowered;
397
410
  }
398
411
 
412
+ /** @internal Bind the compiler-derived composed-law namespaces to declarations already captured by
413
+ * wider business intents. This chooses no policy value; it only repeats the tenant-declared value
414
+ * across the namespaces whose USDA says they can birth. */
415
+ export function bindBusinessBirthPolicyDomains(
416
+ mod: Record<string, unknown>,
417
+ domains: readonly string[],
418
+ ): void {
419
+ const declarations = businessBirthsOf(mod);
420
+ if (declarations.length === 0) return;
421
+ const unique = [...new Set(domains)];
422
+ if (unique.length === 0 || unique.some((value) => value.trim().length === 0)) {
423
+ throw new Error("birth policy namespaces must be a non-empty list of domain names");
424
+ }
425
+ for (const declaration of declarations) {
426
+ declaration.__nomosBirthPolicyDomains = unique;
427
+ }
428
+ }
429
+
399
430
  /** Lower the one tenant-facing birth declaration into ordinary, gate-visible framework directives. */
400
431
  export function expandBusinessBirthExports<M extends Record<string, unknown>>(
401
432
  mod: M,
402
433
  domain: string,
434
+ birthPolicyDomains?: readonly string[],
403
435
  ): Record<string, unknown> {
404
436
  const declarations = businessBirthsOf(mod);
405
437
  if (declarations.length === 0) return mod;
@@ -409,7 +441,18 @@ export function expandBusinessBirthExports<M extends Record<string, unknown>>(
409
441
  "ambiguous — model one born aggregate root per domain",
410
442
  );
411
443
  }
412
- const lowered = lowerOne(declarations[0]!, domain);
444
+ const compiledDomains = mod[BUSINESS_BIRTH_POLICY_DOMAINS_EXPORT];
445
+ const selectedDomains = birthPolicyDomains ?? (
446
+ Array.isArray(compiledDomains) && compiledDomains.every((value) => typeof value === "string")
447
+ ? compiledDomains as string[]
448
+ : [domain]
449
+ );
450
+ const policyDomains = [...new Set(selectedDomains)];
451
+ if (policyDomains.length === 0 || policyDomains.some((value) => value.trim().length === 0)) {
452
+ throw new Error(`birth(${declarations[0]!.aggregate.id}): compiled law declares no policy namespaces`);
453
+ }
454
+ bindBusinessBirthPolicyDomains(mod, policyDomains);
455
+ const lowered = lowerOne(declarations[0]!, domain, policyDomains);
413
456
  for (const [key, generated] of Object.entries(lowered)) {
414
457
  if (!Object.prototype.hasOwnProperty.call(mod, key)) continue;
415
458
  const existing = mod[key];
@@ -405,6 +405,7 @@ export function generateDartApplication(
405
405
  applicationMembers.set(name, owner);
406
406
  };
407
407
  claimApplicationMember("activity", "the generated business history");
408
+ claimApplicationMember("model", "the generated model lifecycle");
408
409
  const collectionLines = new Map<string, string[]>();
409
410
  const collectionMembers = new Map<string, Set<string>>();
410
411
  const siblingImports = new Map<string, Set<string>>();
@@ -1168,6 +1169,18 @@ export function generateDartApplication(
1168
1169
  ` for (final record in records) {`,
1169
1170
  ` seen.add(NmsInternalRecordOrigins.require(record).workspace);`,
1170
1171
  ` }`,
1172
+ ` for (final record in await shared()) {`,
1173
+ ` final workspace = NmsInternalRecordOrigins.require(record).workspace;`,
1174
+ ` if (seen.add(workspace)) records.add(record);`,
1175
+ ` }`,
1176
+ ` return records;`,
1177
+ ` }`,
1178
+ ``,
1179
+ ` /// The ${birth.aggregateId} lives shared with the signed-in person.`,
1180
+ ` /// Unlike [available], this does not open lives founded by that person.`,
1181
+ ` Future<List<${rootModel}>> shared() async {`,
1182
+ ` final records = <${rootModel}>[];`,
1183
+ ` final seen = <String>{};`,
1171
1184
  ` final shares = await _client.bridge.query('sharedByObject', {'object': 'workspace:self'}, null, _client.actor);`,
1172
1185
  ` for (final row in shares) {`,
1173
1186
  ` final raw = row['data'];`,
@@ -1191,6 +1204,12 @@ export function generateDartApplication(
1191
1204
  ` return records;`,
1192
1205
  ` }`,
1193
1206
  ``,
1207
+ ` /// React when this person's shared ${birth.aggregateId} lives change.`,
1208
+ ` Stream<List<${rootModel}>> watchShared() =>`,
1209
+ ` _client.bridge`,
1210
+ ` .watch('sharedByObject', {'object': 'workspace:self'}, null, _client.actor, 60000)`,
1211
+ ` .asyncMap((_) => shared());`,
1212
+ ``,
1194
1213
  ` /// React when this person's available ${birth.aggregateId} lives change.`,
1195
1214
  ` Stream<List<${rootModel}>> watchAvailable() =>`,
1196
1215
  ` Stream<List<${rootModel}>>.multi((events) {`,
@@ -1904,7 +1923,9 @@ export function generateDartApplication(
1904
1923
  `import 'package:nomos_flutter/src/application_status.dart' show ApplicationSignIn, ApplicationStatus;`,
1905
1924
  ]
1906
1925
  : []),
1907
- `import 'package:nomos_flutter/src/generated_application_host.dart' show NmsInternalApplicationUserBinding${
1926
+ `import 'package:nomos_flutter/src/generated_application_host.dart' show ${
1927
+ tenantRoot ? "NomosApplicationStartupState, " : ""
1928
+ }NmsInternalApplicationUserBinding${
1908
1929
  tenantRoot || sharingPolicies.length > 0
1909
1930
  ? ", NmsInternalApplicationHost"
1910
1931
  : ""
@@ -1954,6 +1975,9 @@ export function generateDartApplication(
1954
1975
  ` ${application}._(${client} client${
1955
1976
  tenantRoot ? ", User currentUser" : ""
1956
1977
  }) : ${applicationInitializers.join(", ")};`,
1978
+ ``,
1979
+ ` /// Prove, adopt or restore the law this application was generated against.`,
1980
+ ` NomosModel get model => _client.model;`,
1957
1981
  ...(opts.flutterHost === true && tenantRoot
1958
1982
  ? [
1959
1983
  ``,
@@ -1977,10 +2001,12 @@ export function generateDartApplication(
1977
2001
  ` required Widget Function(BuildContext context, ${application} application) builder,`,
1978
2002
  ` Widget Function(BuildContext context)? loadingBuilder,`,
1979
2003
  ` Widget Function(BuildContext context, Object error, VoidCallback retry)? errorBuilder,`,
2004
+ ` ValueChanged<NomosApplicationStartupState>? onStartupStateChanged,`,
1980
2005
  ` }) => NmsInternalApplicationHost.home<${application}>(`,
1981
2006
  ` key: key, cloud: cloud, application: application, signIn: signIn,`,
1982
2007
  ` bind: nomosInternalBind${application}, builder: builder,`,
1983
2008
  ` loadingBuilder: loadingBuilder, errorBuilder: errorBuilder,`,
2009
+ ` onStartupStateChanged: onStartupStateChanged,`,
1984
2010
  ` );`,
1985
2011
  ]
1986
2012
  : []),
@@ -90,6 +90,7 @@ export function generateDartScenario(
90
90
  readonly surfaceName?: string;
91
91
  readonly applicationFile?: string;
92
92
  readonly applicationModules?: readonly DomainModule[];
93
+ readonly applicationMaxChildWorkspacesPerUser?: number;
93
94
  } = {},
94
95
  ): string {
95
96
  const domain = options.surfaceName ?? mod.domain ?? mod.name;
@@ -195,6 +196,9 @@ export function generateDartScenario(
195
196
  ` final packageFiles = await _applicationPackages();`,
196
197
  ` final environment = await NomosTestHarness.ephemeral(`,
197
198
  ` lawUsdzPaths: packageFiles.map((file) => file.path).toList(growable: false),`,
199
+ ...(options.applicationMaxChildWorkspacesPerUser === undefined
200
+ ? []
201
+ : [` applicationMaxChildWorkspacesPerUser: ${options.applicationMaxChildWorkspacesPerUser},`]),
198
202
  ` ownerSubject: person._principal,`,
199
203
  ` workspace: nmsInternalReceivingGroupNameOf(person._principal),`,
200
204
  ` nodeEntry: Platform.environment['NOMOS_NODE_ENTRY'],`,
@@ -224,6 +228,9 @@ export function generateDartScenario(
224
228
  ` final cloud = await NomosTestCloud.start(`,
225
229
  ` serverEntry: serverEntry, platform: applicationName,`,
226
230
  ` lawUsdzPaths: packageFiles.map((file) => file.path).toList(growable: false),`,
231
+ ...(options.applicationMaxChildWorkspacesPerUser === undefined
232
+ ? []
233
+ : [` applicationMaxChildWorkspacesPerUser: ${options.applicationMaxChildWorkspacesPerUser},`]),
227
234
  ` onDiagnostic: Platform.environment['NOMOS_TEST_DIAGNOSTICS'] == '1' ? stderr.writeln : null,`,
228
235
  ` );`,
229
236
  ` late final NomosTestHarness environment;`,
@@ -2441,6 +2441,9 @@ export interface TsProofOptions {
2441
2441
  readonly domainHash: string;
2442
2442
  /** Stable-id structural interface used to teach the offline harness the hidden owned-birth seal slots. */
2443
2443
  readonly interface?: Readonly<Record<string, DomainInterface>>;
2444
+ /** Compiled application-home child allowance. The local proof cloud seeds this exact law fact before
2445
+ * warranting its throwaway platform; absent means this is a domain package, not an application root. */
2446
+ readonly applicationMaxChildWorkspacesPerUser?: number;
2444
2447
  }
2445
2448
 
2446
2449
  /** THE PROOF TAXONOMY (co2 feedback): which legs a domain carries.
@@ -2455,6 +2458,8 @@ export type ProofKind = "compile-valid" | "standalone-provable" | "child-birth-p
2455
2458
  * marker shape; the `proof-legs.json` index is a map of these by domain key). */
2456
2459
  export interface ProofLegs {
2457
2460
  readonly domain: string;
2461
+ /** Application policy carried to the local proof ceremony; never a runner default. */
2462
+ readonly applicationMaxChildWorkspacesPerUser?: number;
2458
2463
  /** THE TAXONOMY (co2 feedback) — the legs this domain has, in proof order. EVERY domain has at least
2459
2464
  * `compile-valid`. OPTIONAL + ADDITIVE: an older-compiler legs file without it is treated as
2460
2465
  * `["standalone-provable"]` (it only emitted standalone-provable domains). */
@@ -3223,7 +3228,12 @@ function generateSealedApplicationProof(
3223
3228
  const domain = mod.domain ?? mod.name;
3224
3229
  const applicationName = tsApplicationName(opts.packageName);
3225
3230
  const opener = `open${applicationName}`;
3226
- const marker = JSON.stringify(primaryLegs);
3231
+ const marker = JSON.stringify({
3232
+ ...primaryLegs,
3233
+ ...(opts.applicationMaxChildWorkspacesPerUser === undefined
3234
+ ? {}
3235
+ : { applicationMaxChildWorkspacesPerUser: opts.applicationMaxChildWorkspacesPerUser }),
3236
+ });
3227
3237
  const standalone = orderedStandaloneCreates(mod)[0];
3228
3238
  const businessBirth = primaryLegs.childBirth?.businessAggregateId !== undefined
3229
3239
  ? primaryLegs.childBirth
@@ -103,7 +103,11 @@ import {
103
103
  import { synthesizePreconditionInvariants } from "./precondition.js";
104
104
  import { BIRTH_FRAMEWORK_QUERIES, synthesizeBirthQuotaInvariants } from "./birth_meter.js";
105
105
  import { withBirthMeterExports } from "./birth_meter_ensure.js";
106
- import { expandBusinessBirthExports } from "./business_birth.js";
106
+ import {
107
+ BUSINESS_BIRTH_POLICY_DOMAINS_EXPORT,
108
+ bindBusinessBirthPolicyDomains,
109
+ expandBusinessBirthExports,
110
+ } from "./business_birth.js";
107
111
  import { withOwnedCustodyExports } from "./framework/owned_custody.js";
108
112
  import { expandRoleExports, rolesOf } from "./role.js";
109
113
  import { collectEngineRouting } from "./engine_entry.js";
@@ -158,6 +162,8 @@ import { checkDomainModules, formatFindings, hasRefusal } from "./law_check.js";
158
162
  import type { NomosCompat } from "./compat_manifest.js";
159
163
  import { requiredKernelFeatures, type FeatureProbeModule } from "./kernel_capabilities.js";
160
164
  import { APPLICATION_HOME_DOMAIN } from "./framework/application_home_domain.js";
165
+ import { applicationHomePolicyLaw } from "./framework/application_home.js";
166
+ import { isApplicationHomeGenesis } from "./application_home_genesis.js";
161
167
  import { resolveApplicationTopology } from "./application_topology.js";
162
168
  import { isBusinessIntent } from "./intent.js";
163
169
  import {
@@ -1603,8 +1609,8 @@ const NOMOS_TYPES_DEP = "^0.8.0";
1603
1609
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1604
1610
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1605
1611
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1606
- const NOMOS_CLIENT_DEP = "^0.64.1";
1607
- const NOMOS_FLUTTER_DEP = "^0.63.1";
1612
+ const NOMOS_CLIENT_DEP = "^0.64.3";
1613
+ const NOMOS_FLUTTER_DEP = "^0.63.4";
1608
1614
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1609
1615
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1610
1616
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -1805,6 +1811,10 @@ function generatedPackageApplication(
1805
1811
  const invitationPolicies = members.flatMap(({ domain, invitationPolicies }) =>
1806
1812
  invitationPolicies.map((policy) => ({ domain, ...policy })));
1807
1813
  const hasInvitations = invitationPolicies.length > 0;
1814
+ const rootMember = members.find(({ domain }) => domain === homeDomain);
1815
+ if (rootMember === undefined) {
1816
+ throw new Error(`generated Dart package application cannot expose the model for home domain '${homeDomain}'`);
1817
+ }
1808
1818
  const duplicate = members.find((candidate, index) =>
1809
1819
  members.findIndex((other) => other.member === candidate.member) !== index);
1810
1820
  if (duplicate !== undefined) {
@@ -1817,6 +1827,7 @@ function generatedPackageApplication(
1817
1827
  ["currentUser", "the generated signed-in application participant"],
1818
1828
  ["currentStatus", "the generated application status"],
1819
1829
  ["watchStatus", "the generated application status watch"],
1830
+ ["model", "the generated model lifecycle"],
1820
1831
  ["recipient", "the generated typed recipient boundary"],
1821
1832
  ["createInvitation", "the generated invitation capability"],
1822
1833
  ["redeemInvitation", "the generated invitation redemption"],
@@ -1836,7 +1847,7 @@ function generatedPackageApplication(
1836
1847
  `import 'package:flutter/widgets.dart';`,
1837
1848
  `import 'package:nomos_flutter/src/application_status.dart' show ApplicationSignIn, ApplicationStatus;`,
1838
1849
  `import 'package:nomos_client/src/nms_generated_runtime.dart' show NmsInternalUsers, NomosSession, User${hasInvitations ? ", Invitation, Recipient" : ""};`,
1839
- `import 'package:nomos_flutter/src/generated_application_host.dart' show NmsInternalApplicationHost, NmsInternalApplicationUserBinding${hasInvitations ? ", NmsInternalInvitationPolicy" : ""};`,
1850
+ `import 'package:nomos_flutter/src/generated_application_host.dart' show NomosApplicationStartupState, NmsInternalApplicationHost, NmsInternalApplicationUserBinding${hasInvitations ? ", NmsInternalInvitationPolicy" : ""};`,
1840
1851
  ...members.map(({ file, prefix }) =>
1841
1852
  `import '${file}.application.dart' as ${prefix};`),
1842
1853
  ``,
@@ -1866,6 +1877,9 @@ function generatedPackageApplication(
1866
1877
  ]),
1867
1878
  ` const ${application}._({required NomosSession nmsSession, ${hasInvitations ? "required NmsInternalApplicationUserBinding nmsUser, " : ""}required this.currentUser, ${members.map(({ member }) => `required this.${member}`).join(", ")}}) : _nmsSession = nmsSession${hasInvitations ? ", _nmsUser = nmsUser" : ""};`,
1868
1879
  ``,
1880
+ ` /// Prove, adopt or restore the package law against this workspace's exact history.`,
1881
+ ` ${rootMember.prefix}.NomosModel get model => ${rootMember.member}.model;`,
1882
+ ``,
1869
1883
  ` /// Current durability and convergence across this business application.`,
1870
1884
  ` ApplicationStatus get currentStatus => NmsInternalApplicationHost.currentStatus(_nmsSession);`,
1871
1885
  ``,
@@ -1916,10 +1930,12 @@ function generatedPackageApplication(
1916
1930
  ` required Widget Function(BuildContext context, ${application} application) builder,`,
1917
1931
  ` Widget Function(BuildContext context)? loadingBuilder,`,
1918
1932
  ` Widget Function(BuildContext context, Object error, VoidCallback retry)? errorBuilder,`,
1933
+ ` ValueChanged<NomosApplicationStartupState>? onStartupStateChanged,`,
1919
1934
  ` }) => NmsInternalApplicationHost.home<${application}>(`,
1920
1935
  ` key: key, cloud: cloud, application: application, signIn: signIn,`,
1921
1936
  ` bind: nomosInternalBind${application}, builder: builder,`,
1922
1937
  ` loadingBuilder: loadingBuilder, errorBuilder: errorBuilder,`,
1938
+ ` onStartupStateChanged: onStartupStateChanged,`,
1923
1939
  ` );`,
1924
1940
  `}`,
1925
1941
  ``,
@@ -2085,6 +2101,7 @@ function emitDartPackages(
2085
2101
  compat?: NomosCompat,
2086
2102
  describePackageName = packages.client,
2087
2103
  lawUsdzPath?: string,
2104
+ applicationMaxChildWorkspacesPerUser?: number,
2088
2105
  ): { written: string[]; cursed: string[] } {
2089
2106
  for (const [role, name] of Object.entries(packages)) {
2090
2107
  if (typeof name !== "string" || !isPubPackageName(name)) {
@@ -2547,6 +2564,9 @@ function emitDartPackages(
2547
2564
  surfaceName,
2548
2565
  applicationFile,
2549
2566
  applicationModules: perDomain.map(({ dd: candidate }) => candidate.modForDart),
2567
+ ...(applicationMaxChildWorkspacesPerUser === undefined
2568
+ ? {}
2569
+ : { applicationMaxChildWorkspacesPerUser }),
2550
2570
  },
2551
2571
  ),
2552
2572
  "utf8",
@@ -2765,6 +2785,52 @@ async function main(): Promise<void> {
2765
2785
  `rename the tenant domain (Nomos will not silently overwrite or reroute it)`,
2766
2786
  );
2767
2787
  }
2788
+ if (cfg.rootPart !== undefined && cfg.application === undefined) {
2789
+ fail(
2790
+ "published application packages must declare application: { maxChildWorkspacesPerUser }; " +
2791
+ "Nomos will not compile a birth-capable home with an implicit quota",
2792
+ );
2793
+ }
2794
+ // A born application home may carry several co-resident law namespaces. BirthPolicy is namespaced
2795
+ // law-state, so genesis must seed every domain that can itself birth; seeding only the private home
2796
+ // domain would leave (for example) `home/birthEstate` correctly refusing its first offer. Discover the
2797
+ // set from the authored modules and bake it into the private genesis plan. The host never supplies it.
2798
+ const applicationBirthPolicyDomains: string[] = [];
2799
+ let applicationHomeGenesisSource:
2800
+ | { readonly modulePath: string; readonly exportName: string }
2801
+ | undefined;
2802
+ if (cfg.rootPart !== undefined) {
2803
+ applicationBirthPolicyDomains.push(APPLICATION_HOME_DOMAIN);
2804
+ }
2805
+ for (const domain of cfg.domains) {
2806
+ let authored: Mod = {};
2807
+ for (const rel of domain.modules) {
2808
+ const abs = path.resolve(cfgDir, rel);
2809
+ if (!existsSync(abs)) fail(`domain '${domain.key}': module not found: ${abs}`);
2810
+ const imported = await import(pathToFileURL(abs).href) as Mod;
2811
+ authored = { ...authored, ...imported };
2812
+ for (const [exportName, value] of Object.entries(imported)) {
2813
+ if (value !== cfg.application?.homeGenesis || !isApplicationHomeGenesis(value)) continue;
2814
+ if (applicationHomeGenesisSource !== undefined) {
2815
+ fail(
2816
+ "application.homeGenesis is exported from more than one configured law module; " +
2817
+ "keep one canonical declaration",
2818
+ );
2819
+ }
2820
+ applicationHomeGenesisSource = { modulePath: abs, exportName };
2821
+ }
2822
+ }
2823
+ const expanded = expandBusinessBirthExports(authored, domain.key);
2824
+ if (directivesOf(expanded).some((dir) => dir.birthsChild === true)) {
2825
+ applicationBirthPolicyDomains.push(domain.key);
2826
+ }
2827
+ }
2828
+ if (cfg.application?.homeGenesis !== undefined && applicationHomeGenesisSource === undefined) {
2829
+ fail(
2830
+ "application.homeGenesis must be exported by one configured law module; " +
2831
+ "genesis is custody law, not an inline compiler-only callback",
2832
+ );
2833
+ }
2768
2834
  const domainsToCompile: DomainConfig[] = [
2769
2835
  ...cfg.domains,
2770
2836
  ...(cfg.rootPart === undefined
@@ -2944,9 +3010,39 @@ async function main(): Promise<void> {
2944
3010
  }
2945
3011
  }
2946
3012
  }
3013
+ if (mods.some((mod) => Object.prototype.hasOwnProperty.call(mod, BUSINESS_BIRTH_POLICY_DOMAINS_EXPORT))) {
3014
+ fail(
3015
+ `domain '${d.key}' exports reserved compiler binding '${BUSINESS_BIRTH_POLICY_DOMAINS_EXPORT}'; ` +
3016
+ "rename the tenant export",
3017
+ );
3018
+ }
3019
+ if (d.key === APPLICATION_HOME_DOMAIN) {
3020
+ const max = cfg.application?.maxChildWorkspacesPerUser;
3021
+ if (max === undefined) {
3022
+ fail("the private application home has no declared maxChildWorkspacesPerUser policy");
3023
+ }
3024
+ const homeGenesis = cfg.application?.homeGenesis;
3025
+ const policyMod = applicationHomePolicyLaw(
3026
+ max,
3027
+ applicationBirthPolicyDomains,
3028
+ homeGenesis,
3029
+ ) as Mod;
3030
+ mods.push(policyMod);
3031
+ sourceExprs.push(
3032
+ `() => require(${JSON.stringify(path.join(DSL_DIR, "src", "framework", "application_home.ts"))})` +
3033
+ `.applicationHomePolicyLaw(` +
3034
+ `${JSON.stringify(max)}, ${JSON.stringify(applicationBirthPolicyDomains)}, ` +
3035
+ (applicationHomeGenesisSource === undefined
3036
+ ? `undefined)`
3037
+ : `require(${JSON.stringify(applicationHomeGenesisSource.modulePath)})` +
3038
+ `[${JSON.stringify(applicationHomeGenesisSource.exportName)}])`),
3039
+ );
3040
+ }
2947
3041
  let merged: Mod = {};
2948
3042
  for (const m of mods) merged = { ...merged, ...m };
2949
- merged = expandBusinessBirthExports(merged, d.key);
3043
+ merged[BUSINESS_BIRTH_POLICY_DOMAINS_EXPORT] = applicationBirthPolicyDomains;
3044
+ bindBusinessBirthPolicyDomains(merged, applicationBirthPolicyDomains);
3045
+ merged = expandBusinessBirthExports(merged, d.key, applicationBirthPolicyDomains);
2950
3046
  merged = withOwnedCustodyExports(merged);
2951
3047
  // `capability()` bundles expand into discoverable entries (the CLI plane's
2952
3048
  // merge point) — queries/sums/sid-minting/manifests all see the pieces.
@@ -3055,6 +3151,10 @@ async function main(): Promise<void> {
3055
3151
  }
3056
3152
  }
3057
3153
 
3154
+ sourceExprs.push(
3155
+ `() => ({ ${JSON.stringify(BUSINESS_BIRTH_POLICY_DOMAINS_EXPORT)}: ` +
3156
+ `${JSON.stringify(applicationBirthPolicyDomains)} })`,
3157
+ );
3058
3158
  const domainSource = ` ${JSON.stringify(d.key)}: [${sourceExprs.join(", ")}],`;
3059
3159
  entryDomainSourceByKey.set(d.key, domainSource);
3060
3160
  entryGeneratedSourceExprsByKey.set(d.key, sourceExprs.slice(d.modules.length));
@@ -3562,6 +3662,15 @@ async function main(): Promise<void> {
3562
3662
  // clients/interfaces; using the pre-closure executable module here published a USDA-only `birth` route.
3563
3663
  const graphModule = closedDomainModulesByName.get(mod.name) ?? mod;
3564
3664
  const discoveredGraph = discoverLawGraph(graphModule, continuity, nextIdentity);
3665
+ for (const node of discoveredGraph.graph.nodes) {
3666
+ if (node.kind === "intent" && node.births && node.birthPolicySeed === undefined) {
3667
+ fail(
3668
+ `domain '${identityName}' birth intent '${node.entity.label}' has no declared birth-policy ` +
3669
+ "genesis verb; Nomos will not compile a birth-capable law whose children can enter " +
3670
+ "custody without explicit policy",
3671
+ );
3672
+ }
3673
+ }
3565
3674
  nextIdentity = discoveredGraph.registry;
3566
3675
  nextLock.identity = nextIdentity;
3567
3676
  typedLawGraphs.set(identityName, discoveredGraph.graph);
@@ -4864,7 +4973,14 @@ async function main(): Promise<void> {
4864
4973
  let birthWarnings: string[] = [];
4865
4974
  let proofNotes: string[] = [];
4866
4975
  try {
4867
- const proofSrc = generateTsProof(presentationDomainModules, { packageName: cfg.name, domainHash, interface: packageInterface });
4976
+ const proofSrc = generateTsProof(presentationDomainModules, {
4977
+ packageName: cfg.name,
4978
+ domainHash,
4979
+ interface: packageInterface,
4980
+ ...(cfg.application?.maxChildWorkspacesPerUser === undefined
4981
+ ? {}
4982
+ : { applicationMaxChildWorkspacesPerUser: cfg.application.maxChildWorkspacesPerUser }),
4983
+ });
4868
4984
  proofPath = path.join(outDir, `${cfg.name}.proof.mts`);
4869
4985
  writeFileSync(proofPath, proofSrc, "utf8");
4870
4986
  // THE PER-DOMAIN LEGS INDEX: every domain with a creating write, so
@@ -5269,7 +5385,7 @@ async function main(): Promise<void> {
5269
5385
  if (dartPackages !== undefined) {
5270
5386
  const r = emitDartPackages(
5271
5387
  presentationDomainModules, dartOut, domainHash, packageInterface, dartPackages,
5272
- dartNames, nomosCompat, cfg.name, lawUsdzPath,
5388
+ dartNames, nomosCompat, cfg.name, lawUsdzPath, cfg.application?.maxChildWorkspacesPerUser,
5273
5389
  );
5274
5390
  dartFiles = r.written;
5275
5391
  dartCursedHints = r.cursed;
package/src/directive.ts CHANGED
@@ -447,6 +447,14 @@ export interface Directive<P = unknown> {
447
447
  * `attestationAuthorizedGate`/`.attestationAuthorized()` naming convention; the declaration is the noun.)
448
448
  */
449
449
  readonly birthsChild?: boolean;
450
+ /** Additional ordinary offers which the born workspace's typed law requires in genesis.
451
+ * This is compiler/runtime metadata attached only to a birthing directive. It contains no
452
+ * payload or fallback value: the kernel checks that the re-derived recipe invokes each named
453
+ * law verb exactly once after installation. */
454
+ readonly birthGenesisRequirements?: readonly {
455
+ readonly domain: string;
456
+ readonly directiveId: string;
457
+ }[];
450
458
  /**
451
459
  * PROOF-TOOLING PRODUCER MARKER (relation-seed leg). Declares that this directive ESTABLISHES the named
452
460
  * relation(s) — i.e. its `plan` folds a `writeTuple` that grants the relation. Purely a static hint for
@@ -9,6 +9,7 @@
9
9
  // routable through the runtime OpenAPI, while TS/Dart/OpenAPI business surfaces omit it.
10
10
 
11
11
  import type { AggregateHandle } from "../aggregate.js";
12
+ import type { ApplicationHomeGenesis } from "../application_home_genesis.js";
12
13
  import type { Directive } from "../directive.js";
13
14
  import type { QueryDecl } from "../query.js";
14
15
  import { APPLICATION_HOME_DOMAIN } from "./application_home_domain.js";
@@ -70,8 +71,24 @@ const aggregate = (value: AggregateHandle): Internal<AggregateHandle> => interna
70
71
  const directive = <P>(value: Directive<P>): Internal<Directive<P>> => internal(value);
71
72
  const query = (value: QueryDecl): Internal<QueryDecl> => internal(value);
72
73
 
73
- // Parent primitive + the law its born home uses.
74
- export const __nomos_birthReceivingGroup = directive(birthReceivingGroupForDomain(APPLICATION_HOME_DOMAIN));
74
+ // Parent primitive + the law its born home uses. The compiler supplies the application's explicit
75
+ // allowance through this factory; there is deliberately no static/default route to discover.
76
+ export function applicationHomePolicyLaw(
77
+ maxChildWorkspacesPerUser: number,
78
+ birthPolicyDomains: readonly string[],
79
+ homeGenesis?: ApplicationHomeGenesis,
80
+ ) {
81
+ return {
82
+ __nomos_birthReceivingGroup: directive(
83
+ birthReceivingGroupForDomain(
84
+ APPLICATION_HOME_DOMAIN,
85
+ maxChildWorkspacesPerUser,
86
+ birthPolicyDomains,
87
+ homeGenesis,
88
+ ),
89
+ ),
90
+ };
91
+ }
75
92
  export const __nomos_ReceivingGroup = aggregate(ReceivingGroup);
76
93
  export const __nomos_seedReceivingGroup = directive(seedReceivingGroup);
77
94
  export const __nomos_ReceivingGroupPolicy = aggregate(ReceivingGroupPolicy);
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * The meter's STRUCTURE (record + queries + quota invariant + auto-capture) is auto-ensured on every
10
10
  * `.births()` law — see `../birth_meter.ts`. THIS module is the part a tenant COMPOSES IN when they
11
- * want to GOVERN the allowance instead of living on the conservative default:
11
+ * uses to govern its required, explicitly seeded allowance:
12
12
  *
13
13
  * - {@link setBirthPolicy} — set/amend the workspace's default allowance (admin-gated).
14
14
  * - {@link grantBirthAllowance} — a per-subject override (admin-gated; latest grant wins).
@@ -56,7 +56,6 @@ export {
56
56
  childLifecyclesByStatus,
57
57
  birthPolicy,
58
58
  birthGrantByPrincipal,
59
- DEFAULT_MAX_BIRTHS_PER_PRINCIPAL,
60
59
  birthQuotaVerdict,
61
60
  birthQuotaInvariantId,
62
61
  } from "../birth_meter.js";
@@ -238,7 +237,7 @@ export const setBirthPolicy = directive("setBirthPolicy")
238
237
  .ensures(BirthPolicy)
239
238
  .payload(
240
239
  z.object({
241
- maxBirthsPerPrincipal: z.number().int().positive(),
240
+ maxBirthsPerPrincipal: z.number().int().nonnegative(),
242
241
  setBy: z.string().min(1),
243
242
  /** Caller-stamped (the timestamp doctrine). */
244
243
  setAt: z.string().min(1),
@@ -257,7 +256,7 @@ export const seedBirthPolicy = directive("seedBirthPolicy")
257
256
  .ensures(BirthPolicy)
258
257
  .payload(
259
258
  z.object({
260
- maxBirthsPerPrincipal: z.number().int().positive(),
259
+ maxBirthsPerPrincipal: z.number().int().nonnegative(),
261
260
  setBy: z.string().min(1),
262
261
  setAt: z.string().min(1),
263
262
  }),
@@ -274,7 +273,7 @@ export const grantBirthAllowance = directive("grantBirthAllowance")
274
273
  .payload(
275
274
  z.object({
276
275
  principal: z.string().min(1),
277
- maxBirths: z.number().int().positive(),
276
+ maxBirths: z.number().int().nonnegative(),
278
277
  grantedBy: z.string().min(1),
279
278
  /** epoch-ms — the recency tiebreak (latest grant wins), caller-stamped. */
280
279
  grantedAt: z.number().int().nonnegative(),