@githolon/dsl 0.103.1 → 0.103.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -158,7 +158,7 @@ export const workspaceSelfWriteOnce = workspaceInvariant("WorkspaceSelfWriteOnce
158
158
  // pre-apply folded relation tuples — refusing any author who does not HOLD the relation. The schema +
159
159
  // the `.requires` together arm the gate (the `nomosRelations` + `nomosRelationGate` era keys); the
160
160
  // static governance roles use the native relation gate. Creation posture is deliberately dynamic law:
161
- // open/unset admits any proven author, invite checks `creator`, and closed checks `admin`.
161
+ // open admits any proven author, invite checks `creator`, closed checks `admin`, and unset fails closed.
162
162
  //
163
163
  // THE CONTROL-PLANE CARVE-OUT — LEAF-ONLY (`allow: [user(), key()]`, never a userset/wildcard). A
164
164
  // `workspace:self#admin@<group>#member` or `@<type>:*` would let a group/wildcard escalate to admin
@@ -371,8 +371,8 @@ export const CloudPolicy = aggregate("CloudPolicy", {
371
371
  /**
372
372
  * The workspace-CREATION posture dial (cloud_sovereignty.md S6) —
373
373
  * `"open"` (anyone creates), `"invite"` (an active CreationGrant key
374
- * required), or `"closed"` (admins only). OPTIONAL: absent keeps the
375
- * implicit open posture. Invite and closed are explicit law facts; merely
374
+ * required), or `"closed"` (admins only). OPTIONAL in the storage schema for
375
+ * historical replay, but absence fails closed for new births. Merely
376
376
  * granting somebody authority never changes the door for everybody else.
377
377
  */
378
378
  creationPosture: t.string().merge(Lww).optional(),
@@ -529,13 +529,9 @@ export function buildGenesisChain(p: {
529
529
  * from there. Absent ⇒ the recipe resolves an already-installed law by shape (pre-G3 / test lanes). */
530
530
  frameworkHash?: string;
531
531
  lawHash?: string;
532
- /**
533
- * METERED BIRTH (slice A3 — the descendant budget). When present, the child's genesis includes a
534
- * `seedBirthPolicy` step so it is BORN BOUNDED (its own default birth allowance per subject).
535
- * OPT-IN: thread it only when the `lawHash` the order names carries `seedBirthPolicy` (a governance
536
- * package compiled under the metered-birth DSL) — see the poolSteps note below.
537
- */
538
- descendantPool?: number;
532
+ /** METERED BIRTH (slice A3 — the descendant budget). Required: a governance child may not be
533
+ * born without explicit BirthPolicy state. Zero is a legitimate declared "cannot birth" policy. */
534
+ descendantPool: number;
539
535
  /** LINEAGE CONTINUITY (rebirth genesis only): the superseded incarnation's genesis oid. Threads onto the
540
536
  * child's birthCloud record (`rebornFrom`) + the bornUnder commitment — the reborn holon testifies to its
541
537
  * ancestry while `parent` stays the SUPERSEDED record's parent (a rebirth may never reparent). */
@@ -566,26 +562,23 @@ export function buildGenesisChain(p: {
566
562
  parentHead: "",
567
563
  ...(p.rebornFrom ? { rebornFrom: p.rebornFrom } : {}),
568
564
  });
569
- // METERED BIRTH (slice A3 — THE POOL CHAINS TO ROOT): when the order carries a `descendantPool`, the
570
- // child is BORN BOUNDED — a `seedBirthPolicy` genesis step writes the child's own BirthPolicy singleton
571
- // (its default birth allowance) BEFORE the warrant lands, exactly like every other genesis seed (the
572
- // still-warrant-free prefix runs unsigned). Root bounds platforms (this pool), platforms bound homes,
573
- // homes bound projects — the tree meters by induction, with zero cross-workspace reads on any gate.
574
- // OPT-IN (deliberate): the step folds through the CHILD'S OWN gate under the law hash the order names,
575
- // so it may only be threaded when that law carries `seedBirthPolicy` (a law compiled under this DSL) —
576
- // an unconditional default would refuse the genesis of every birth pinning an OLDER law hash. V1
577
- // BOUNDARY: each node enforces its own count-quota; cross-level pool CONSERVATION arithmetic is v2.
578
- const poolSteps =
579
- p.descendantPool !== undefined
580
- ? [
581
- {
582
- domain: "workspaces",
583
- directiveId: "seedBirthPolicy",
584
- actor: founder,
585
- payload: { maxBirthsPerPrincipal: p.descendantPool, setBy: p.principal, setAt: p.bornAt },
586
- },
587
- ]
588
- : [];
565
+ if (!Number.isInteger(p.descendantPool) || p.descendantPool < 0) {
566
+ throw new Error(
567
+ "buildGenesisChain requires an explicit non-negative descendantPool; " +
568
+ "Nomos has no compiled birth-policy fallback",
569
+ );
570
+ }
571
+ // METERED BIRTH (slice A3 — THE POOL CHAINS TO ROOT): every governance child is BORN BOUNDED.
572
+ // Historical chains keep replaying under their historical law; this constructor authors only the
573
+ // current law, whose seedBirthPolicy directive is compiler-carried. Missing policy is invalid genesis.
574
+ const poolSteps = [
575
+ {
576
+ domain: "workspaces",
577
+ directiveId: "seedBirthPolicy",
578
+ actor: founder,
579
+ payload: { maxBirthsPerPrincipal: p.descendantPool, setBy: p.principal, setAt: p.bornAt },
580
+ },
581
+ ];
589
582
  return JSON.stringify([
590
583
  ...installSteps,
591
584
  ...poolSteps,
@@ -634,9 +627,6 @@ export const quotaByPrincipal = query("quotaByPrincipal")
634
627
  .key("principal")
635
628
  .returns(QuotaGrant);
636
629
 
637
- /** THE DEFAULT WORKSPACE ALLOWANCE — the CLAUDE.md / CloudPolicy figure when no grant + no policy row. */
638
- export const DEFAULT_MAX_WORKSPACES_PER_PRINCIPAL = 50;
639
-
640
630
  /** A captured-read row as `derive_captured_read` shapes it: `{id, data:{field:value}}`. */
641
631
  type QuotaRow = { id?: string; data?: Record<string, unknown> };
642
632
 
@@ -656,8 +646,8 @@ function countQuotaReads(principal: string): void {
656
646
  * THE WORKSPACE-QUOTA VERDICT — the PURE predicate the {@link workspaceQuota} invariant runs over the
657
647
  * COMMITTED captured reads (the same `{queryId,args,result}` the plan recorded, CAS-re-derived on every
658
648
  * lane). Counts the principal's LIVE (non-retired/-reclaimed) CloudWorkspaces and compares against its
659
- * allowance (the latest QuotaGrant.maxWorkspaces if any, else CloudPolicy.maxWorkspacesPerPrincipal, else
660
- * the {@link DEFAULT_MAX_WORKSPACES_PER_PRINCIPAL} floor). The NEW workspace is NOT yet in the pre-apply
649
+ * allowance (the latest QuotaGrant.maxWorkspaces if any, else CloudPolicy.maxWorkspacesPerPrincipal).
650
+ * Missing policy is a typed refusal: quota is law, never a compiled allowance. The NEW workspace is NOT yet in the pre-apply
661
651
  * state, so `liveCount < allowance` (strict) is the correct test: the principal may hold AT MOST
662
652
  * `allowance`, so a fresh birth is admitted iff it already holds fewer. A principal-less call (should be
663
653
  * impossible after S3) fails CLOSED. The verdict is a pure function of the captured reads ⇒ identical on
@@ -678,8 +668,8 @@ function quotaVerdict(
678
668
  const status = (row.data?.status ?? "") as string;
679
669
  return status !== "retired" && status !== "reclaimed";
680
670
  }).length;
681
- // ALLOWANCE: the LATEST QuotaGrant for the principal (max grantedAt) wins; else the CloudPolicy default;
682
- // else the hard floor. `derive_captured_read` decodes t.int() as a JSON number.
671
+ // ALLOWANCE: the LATEST QuotaGrant for the principal (max grantedAt) wins; else the explicit
672
+ // CloudPolicy default. `derive_captured_read` decodes t.int() as a JSON number.
683
673
  const grants = rowsOf(quotaByPrincipal.id);
684
674
  let allowance: number | undefined;
685
675
  let latestGrantedAt = -Infinity;
@@ -697,7 +687,7 @@ function quotaVerdict(
697
687
  const max = Number(policyRows[0]?.data?.maxWorkspacesPerPrincipal ?? NaN);
698
688
  if (Number.isFinite(max)) allowance = max;
699
689
  }
700
- if (allowance === undefined) allowance = DEFAULT_MAX_WORKSPACES_PER_PRINCIPAL;
690
+ if (allowance === undefined) return { reject: "workspace-quota-policy-not-configured" };
701
691
  return liveCount < allowance ? { accept: true } : { reject: "workspace-quota-exceeded" };
702
692
  }
703
693
 
@@ -1213,7 +1203,7 @@ export const rebirthConsentByWorkspace = query("rebirthConsentByWorkspace")
1213
1203
  // ─────────────────────────── birthChild — THE ONE BIRTH DIRECTIVE (the `kind`-collapse) ───────────────
1214
1204
  // "There are only workspaces, all equal until a domain is installed." birthChild is the SINGLE directive
1215
1205
  // that births any child — a plain leaf OR a platform; the difference is the GENESIS DATA the payload carries,
1216
- // never a node-class. The same creation posture governs leaf and platform births: open/unset admits any
1206
+ // never a node-class. The same creation posture governs leaf and platform births: open admits any
1217
1207
  // proven author, invite requires `#creator`, and closed requires `#admin`. The prior `#platformCreator`
1218
1208
  // second tier is retired.
1219
1209
  // • genesis ABSENT → a plain lineage record (no foundingAdminHash); boot-on-touch, never drained.
@@ -1245,8 +1235,9 @@ export const birthChild = directive("birthChild")
1245
1235
  /** GENESIS (optional): present ⇒ this child is born running the governance law (a platform). The
1246
1236
  * sha256 of the founding-admin key the requester presented — the provider grants admin to the hash. */
1247
1237
  foundingAdminHash: sha256Hex.optional(),
1248
- /** PLATFORM POOL: how many live children the born platform may parent. Only meaningful with genesis. */
1249
- pool: z.number().int().positive().default(50),
1238
+ /** PLATFORM POOL: explicit law-state for how many live children the born platform may parent.
1239
+ * Required with genesis. There is no framework default. Zero deliberately disables births. */
1240
+ pool: z.number().int().nonnegative().optional(),
1250
1241
  /** GENESIS birth time as an ISO string (caller-stamped — the timestamp doctrine). Arms the warrant
1251
1242
  * in the law-authored genesis chain; required for (and only used by) a genesis birth. */
1252
1243
  bornAt: z.string().optional(),
@@ -1264,11 +1255,6 @@ export const birthChild = directive("birthChild")
1264
1255
  * LAST into the child's genesis. REQUIRED for a genesis birth to complete — a birth without a
1265
1256
  * valid cert is refused atomically (no unwarranted platform lane survives era 0). */
1266
1257
  platformCert: z.string().optional(),
1267
- /** METERED BIRTH (slice A3): the child's own descendant budget — when present, the genesis
1268
- * recipe seeds the child's BirthPolicy (`seedBirthPolicy`) so it is BORN BOUNDED. OPT-IN:
1269
- * only pass it when the `lawHash` the order names carries `seedBirthPolicy` (a governance
1270
- * package compiled under the metered-birth DSL). Only meaningful with a genesis. */
1271
- descendantPool: z.number().int().positive().optional(),
1272
1258
  /** GOVERNANCE POSTURE (governance_postures.md) — the born child's declared posture; default
1273
1259
  * `managed`. Mirrored onto its lineage record so root's bailiff can consult it on the hot path
1274
1260
  * (the tenant's own {@link GovernancePosture} singleton is authoritative). Additive/era-safe. */
@@ -1276,22 +1262,22 @@ export const birthChild = directive("birthChild")
1276
1262
  })
1277
1263
  // A genesis birth (a platform) must carry a principal AND a bornAt — it owns a subtree, a lineage
1278
1264
  // anchor, and a law-authored genesis chain whose warrant is stamped at bornAt.
1279
- .refine((d) => !d.foundingAdminHash || (d.principal.length > 0 && !!d.bornAt), {
1280
- message: "a genesis birth (foundingAdminHash present) requires a non-empty principal and bornAt (ISO)",
1281
- path: ["principal"],
1265
+ .refine((d) => !d.foundingAdminHash || (d.principal.length > 0 && !!d.bornAt && d.pool !== undefined), {
1266
+ message: "a genesis birth (foundingAdminHash present) requires a non-empty principal, bornAt (ISO), and explicit pool",
1267
+ path: ["pool"],
1282
1268
  }),
1283
1269
  )
1284
1270
  .plan((p) => {
1285
1271
  // THE WORKSPACE-QUOTA CAPTURED READS (#58 / S2) — same unconditional capture as createWorkspace, so the
1286
1272
  // WorkspaceQuota invariant counts this principal's live workspaces against its allowance on every lane.
1287
1273
  countQuotaReads(p.principal);
1288
- const rec = planBirthRecord(p, p.foundingAdminHash ? { foundingAdminHash: p.foundingAdminHash, pool: p.pool } : undefined);
1274
+ const rec = planBirthRecord(p, p.foundingAdminHash ? { foundingAdminHash: p.foundingAdminHash, pool: p.pool as number } : undefined);
1289
1275
  // LAW AUTHORS THE GENESIS RECIPE (P1): a genesis birth carries its child's [installDomain×2 →]
1290
1276
  // seedGenesis→grantAdmin→birthCloud chain. (refine guarantees principal + bornAt here.)
1291
1277
  if (p.foundingAdminHash) {
1292
1278
  // ERA 0 — every governance/platform birth is WARRANTED (no compat hatch to thread; buildGenesisChain
1293
1279
  // always records the real self-id).
1294
- const genesisChain = buildGenesisChain({ name: p.name, principal: p.principal, parent: p.parent, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt, ...(p.frameworkHash ? { frameworkHash: p.frameworkHash } : {}), ...(p.lawHash ? { lawHash: p.lawHash } : {}), ...(p.descendantPool !== undefined ? { descendantPool: p.descendantPool } : {}) });
1280
+ const genesisChain = buildGenesisChain({ name: p.name, principal: p.principal, parent: p.parent, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt, descendantPool: p.pool as number, ...(p.frameworkHash ? { frameworkHash: p.frameworkHash } : {}), ...(p.lawHash ? { lawHash: p.lawHash } : {}) });
1295
1281
  // THE GENESIS RECIPE IS ALSO A LINEAGE/AUDIT FACT on the parent's chain (the parent records what it
1296
1282
  // ORDERED — law naming law, legitimate). The kernel does NOT read this field to detect the birth.
1297
1283
  rec.set("genesisChain", genesisChain);
@@ -1642,7 +1628,7 @@ export const revokeAdmin = directive("revokeAdmin")
1642
1628
  */
1643
1629
  // grantCreation — creation authority IS the self-relative `#creator` tuple the CreationGate reads.
1644
1630
  // Writes `workspace:self#creator@key:<keyHash>` (no CreationGrant aggregate). Under posture invite/closed
1645
- // the gate admits only holders of an active #creator subject; under open/unset it admits anyone.
1631
+ // the gate admits only holders of an active #creator subject; under explicit open it admits anyone.
1646
1632
  // #creator is ONE relation over ONE principal graph; a subject is `key:<hash>` (proof by holding a secret —
1647
1633
  // a Bearer) OR `user:<uid>` (proof of identity — an auth-provider token). The relation allows BOTH
1648
1634
  // (allow:[user(),key()]); these are not two authorities, just two ways a principal authenticates. So
@@ -1755,7 +1741,7 @@ export { RelationTuple };
1755
1741
  // Static governance requirements use the full-native relation gate: a directive carrying
1756
1742
  // `.requires("<relation>")` is checked by the ONE wasm gate over PRE-APPLY tuples on every door (author /
1757
1743
  // edge admission / verify_chain replay). Workspace creation is the intentional exception because it is a
1758
- // three-posture policy, not one static role: the invariant below admits a proven author under open/unset,
1744
+ // three-posture policy, not one static role: the invariant below admits a proven author under explicit open,
1759
1745
  // checks `#creator` under invite, and checks `#admin` under closed. The captured author principal is pinned
1760
1746
  // to the sealed Attribution.actor on every peer — so "who may grant or create" is LAW, never a host pre-check. The
1761
1747
  // pre-apply roster means `grantAdmin(@key:self)` cannot self-grant (the not-yet-written subject is absent
@@ -1858,7 +1844,7 @@ export const reclaimRequiresRetired = workspaceInvariant("ReclaimRequiresRetired
1858
1844
 
1859
1845
  // ── THE WORKSPACE-QUOTA GATE (S2 — THE QUOTA LAW, ENFORCED) ────────────────────────────────────────────
1860
1846
  // The native `.requires("creator")` replacement accidentally collapsed a THREE-POSTURE law into one
1861
- // permanently invite-only door. Creation is not a static role check: open/unset admits any PROVEN author;
1847
+ // permanently invite-only door. Creation is not a static role check: open admits any PROVEN author;
1862
1848
  // invite admits #creator; closed admits #admin. Keep that policy in an invariant over the committed prefix,
1863
1849
  // while the native relation evaluator remains the one meaning of "holds #creator/#admin" everywhere else.
1864
1850
  //
@@ -1894,8 +1880,9 @@ function creationPostureVerdict(
1894
1880
  if (actor === "" || claimed === "" || requestedBy === "" || claimed !== proven || requestedBy !== proven) {
1895
1881
  return { reject: "creation.identity-mismatch" };
1896
1882
  }
1897
- const posture = String((snapshots.policy as { creationPosture?: string } | undefined)?.creationPosture ?? "open");
1898
- if (posture === "open" || posture === "") return { accept: true };
1883
+ const posture = String((snapshots.policy as { creationPosture?: string } | undefined)?.creationPosture ?? "");
1884
+ if (posture === "") return { reject: "creation.policy-not-configured" };
1885
+ if (posture === "open") return { accept: true };
1899
1886
  if (posture === "invite") {
1900
1887
  return activeRelationSubject(snapshots.creators, actor)
1901
1888
  ? { accept: true }
@@ -1915,7 +1902,7 @@ const creationPostureReads = readSet(
1915
1902
  readRef("admins", RelationTuple).at(ADMIN_TUPLE_ID),
1916
1903
  );
1917
1904
 
1918
- /** Compatibility create: open/unset ⇒ proven author; invite ⇒ creator; closed ⇒ admin. */
1905
+ /** Compatibility create: open ⇒ proven author; invite ⇒ creator; closed ⇒ admin; unset ⇒ refuse. */
1919
1906
  export const creationPostureCreate = workspaceInvariant("CreationPostureCreate")
1920
1907
  .on("createWorkspace")
1921
1908
  .reads(creationPostureReads)
package/src/index.ts CHANGED
@@ -16,6 +16,13 @@ export {
16
16
  type NomosPackageDefinition,
17
17
  type NomosPackageDomainConfig,
18
18
  } from "./package_config.js";
19
+ export {
20
+ applicationHomeGenesis,
21
+ offerAtHomeGenesis,
22
+ type ApplicationHomeGenesis,
23
+ type ApplicationHomeGenesisFacts,
24
+ type ApplicationHomeGenesisStep,
25
+ } from "./application_home_genesis.js";
19
26
  export type { RuntimePackageDependency } from "./package_dependencies.js";
20
27
  export {
21
28
  aggregate,
@@ -538,7 +545,6 @@ export {
538
545
  childLifecyclesByStatus,
539
546
  birthPolicy,
540
547
  birthGrantByPrincipal,
541
- DEFAULT_MAX_BIRTHS_PER_PRINCIPAL,
542
548
  birthQuotaVerdict,
543
549
  birthQuotaInvariantId,
544
550
  setBirthPolicy,
package/src/law_graph.ts CHANGED
@@ -545,6 +545,13 @@ export interface LawIntentNode {
545
545
  readonly births: boolean;
546
546
  readonly birthRecord?: LawRef<"aggregate">;
547
547
  readonly birthPrefix?: string;
548
+ /** The law-declared genesis verb that establishes this workspace's birth policy. This is a
549
+ * relationship to executable law, never a compiler/runtime allowance value: the kernel uses it
550
+ * only to require an explicit matching step in child genesis before custody can be created. */
551
+ readonly birthPolicySeed?: LawRef<"intent">;
552
+ /** Additional law verbs which this birth requires the child genesis to offer. Cross-domain
553
+ * targets remain typed closure references and are resolved to USDA paths during composition. */
554
+ readonly birthGenesisRequires: readonly LawTargetRef<"intent">[];
548
555
  readonly attestation?: {
549
556
  readonly ownerObject: string;
550
557
  readonly ownerField: string;
@@ -1362,6 +1369,22 @@ export function discoverLawGraph(
1362
1369
  birthRecord: ref(aggregateByLabel.get("NomosBirthRecord")!.node.entity),
1363
1370
  birthPrefix: "nomos-birth-record:",
1364
1371
  }),
1372
+ ...(intent.birthsChild !== true || intentEntities.get("seedBirthPolicy") === undefined ? {} : {
1373
+ birthPolicySeed: ref(intentEntities.get("seedBirthPolicy")!),
1374
+ }),
1375
+ birthGenesisRequires: (intent.birthGenesisRequirements ?? []).map((requirement) => {
1376
+ const local = requirement.domain === module.name
1377
+ ? intentEntities.get(requirement.directiveId)
1378
+ : undefined;
1379
+ return local === undefined
1380
+ ? {
1381
+ kind: "dependency-export" as const,
1382
+ entityKind: "intent" as const,
1383
+ label: requirement.directiveId,
1384
+ domain: requirement.domain,
1385
+ }
1386
+ : ref(local);
1387
+ }),
1365
1388
  ...(intent.attestationAuthorizedGate === undefined ? {} : {
1366
1389
  attestation: {
1367
1390
  ownerObject: intent.attestationAuthorizedGate.ownerObject,
@@ -971,6 +971,16 @@ function emitIntent(
971
971
  `${pad}custom uniform string nomos:birthPrefix = ${quote(intent.birthPrefix ?? "")}`,
972
972
  );
973
973
  }
974
+ if (intent.birthPolicySeed !== undefined) {
975
+ body.push(
976
+ `${pad}custom rel nomos:birthPolicySeed = <${requiredPath(intent.birthPolicySeed.id, paths)}>`,
977
+ );
978
+ }
979
+ if (intent.birthGenesisRequires.length > 0) {
980
+ body.push(
981
+ `${pad}custom rel nomos:birthGenesisRequires = ${targets(intent.birthGenesisRequires, paths, resolveDependency)}`,
982
+ );
983
+ }
974
984
  if (intent.attestation !== undefined) {
975
985
  body.push(
976
986
  `${pad}custom uniform string nomos:attestationOwnerObject = ${quote(intent.attestation.ownerObject)}`,
@@ -1756,7 +1766,7 @@ function emitContainer<T extends { readonly entity: { readonly id: string } }>(
1756
1766
  }
1757
1767
 
1758
1768
  function targets(
1759
- refs: readonly LawTargetRef<"aggregate" | "contract" | "relation" | "policy" | "read">[],
1769
+ refs: readonly LawTargetRef<"aggregate" | "contract" | "relation" | "policy" | "read" | "intent">[],
1760
1770
  paths: ReadonlyMap<string, string>,
1761
1771
  resolveDependency: ResolveDependency,
1762
1772
  ): string {
@@ -4,6 +4,10 @@ import {
4
4
  normalizeRuntimePackageDependencies,
5
5
  type RuntimePackageDependency,
6
6
  } from "./package_dependencies.js";
7
+ import {
8
+ isApplicationHomeGenesis,
9
+ type ApplicationHomeGenesis,
10
+ } from "./application_home_genesis.js";
7
11
 
8
12
  /** The source modules which compose one domain exported by a Nomos package. */
9
13
  export interface NomosPackageDomainConfig {
@@ -21,6 +25,14 @@ export interface NomosPackageDomainConfig {
21
25
  export interface NomosPackageConfig {
22
26
  readonly name: string;
23
27
  readonly domains: readonly NomosPackageDomainConfig[];
28
+ /** Application-root policy compiled into the private receiving-group law.
29
+ * Required for a published application: Nomos never invents a child-workspace allowance. */
30
+ readonly application?: {
31
+ readonly maxChildWorkspacesPerUser: number;
32
+ /** Tenant law which must hold from the application's first usable home state. Every entry is
33
+ * an ordinary offer executed through the child's gate; omission declares no tenant ceremony. */
34
+ readonly homeGenesis?: ApplicationHomeGenesis;
35
+ };
24
36
  /** Separately installed law required at runtime; USDA composition arcs do not belong here. */
25
37
  readonly dependencies?: readonly RuntimePackageDependency[];
26
38
  }
@@ -95,6 +107,24 @@ export function validateNomosPackageConfig(value: unknown): {
95
107
  if (config.dependencies !== undefined && !Array.isArray(config.dependencies)) {
96
108
  throw new Error("package dependencies must be an array");
97
109
  }
110
+ if (config.application !== undefined) {
111
+ const max = config.application.maxChildWorkspacesPerUser;
112
+ if (!Number.isInteger(max) || max < 0) {
113
+ throw new Error(
114
+ "application.maxChildWorkspacesPerUser must be a non-negative integer; " +
115
+ "Nomos has no compiled birth-policy fallback",
116
+ );
117
+ }
118
+ if (
119
+ config.application.homeGenesis !== undefined &&
120
+ !isApplicationHomeGenesis(config.application.homeGenesis)
121
+ ) {
122
+ throw new Error(
123
+ "application.homeGenesis must be declared with applicationHomeGenesis(...); " +
124
+ "Nomos will not reinterpret an untyped genesis recipe",
125
+ );
126
+ }
127
+ }
98
128
  const runtimeDependencies = normalizeRuntimePackageDependencies(
99
129
  config.name,
100
130
  config.dependencies ?? [],