@githolon/dsl 0.20.0 → 0.21.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -444,6 +444,11 @@ export function makeEngineReport(
444
444
  payload?: Record<string, unknown>;
445
445
  };
446
446
  priorState?: Record<string, unknown>;
447
+ capturedPorts?: {
448
+ principal?: string;
449
+ principal_verified?: boolean;
450
+ reads?: readonly { query_id?: string; queryId?: string; args?: Record<string, string>; result?: unknown }[];
451
+ };
447
452
  }) => string {
448
453
  return (job) => {
449
454
  const intent = job.intent ?? {};
@@ -543,7 +548,19 @@ export function makeEngineReport(
543
548
  );
544
549
  }
545
550
  const snapshots = (job.priorState ?? {}) as Record<string, Record<string, unknown>>;
546
- const verdict = decl.assert(snapshots);
551
+ // Mirror engine_entry.planWorkspaceInvariantAssert: surface intent + captured author identity +
552
+ // the committed captured reads (#58 — the captured-read invariant gate), normalising the kernel's
553
+ // snake_case `query_id` to the DSL `queryId` so the law body is wire-shape agnostic. A #266 body
554
+ // that ignores ctx is byte-identical.
555
+ const assertIntent = (intent.payload ?? {}) as Record<string, unknown>;
556
+ const principal = (job.capturedPorts?.principal ?? "") as string;
557
+ const principalVerified = job.capturedPorts?.principal_verified === true;
558
+ const reads = (job.capturedPorts?.reads ?? []).map((r) => ({
559
+ queryId: (r.queryId ?? r.query_id ?? "") as string,
560
+ args: (r.args ?? {}) as Record<string, string>,
561
+ result: r.result,
562
+ }));
563
+ const verdict = decl.assert(snapshots, { intent: assertIntent, principal, principalVerified, reads });
547
564
  return JSON.stringify(verdict);
548
565
  }
549
566
 
@@ -606,7 +606,11 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
606
606
  function planWorkspaceInvariantAssert(job: {
607
607
  intent?: { workspaceInvariant?: { id?: string }; payload?: unknown };
608
608
  priorState?: Record<string, unknown>;
609
- capturedPorts?: { principal?: string; principal_verified?: boolean };
609
+ capturedPorts?: {
610
+ principal?: string;
611
+ principal_verified?: boolean;
612
+ reads?: readonly { query_id?: string; queryId?: string; args?: Record<string, string>; result?: unknown }[];
613
+ };
610
614
  }): string {
611
615
  const id = job.intent?.workspaceInvariant?.id;
612
616
  if (typeof id !== "string") {
@@ -623,7 +627,17 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
623
627
  const principal = (job.capturedPorts?.principal ?? "") as string;
624
628
  // VA-core (Phase 3): the kernel-DERIVED author-verified flag — threaded like `principal`.
625
629
  const principalVerified = job.capturedPorts?.principal_verified === true;
626
- return JSON.stringify(decl.assert(snapshots, { intent, principal, principalVerified }));
630
+ // THE CAPTURED READS (#58 — the captured-read invariant gate): surface the committed
631
+ // `captured_ports.reads` the executor ships in the assert dispatch so a count/aggregate predicate
632
+ // (WorkspaceQuota) can judge over the row LIST a captured read returned. The wire field is the kernel's
633
+ // `CapturedRead` (snake_case `query_id`); normalize to the DSL `queryId` so the law body is wire-shape
634
+ // agnostic. Empty/absent ⇒ `[]` (every pre-#58 / #266 body ignores it — additive, byte-identical).
635
+ const reads = (job.capturedPorts?.reads ?? []).map((r) => ({
636
+ queryId: (r.queryId ?? r.query_id ?? "") as string,
637
+ args: (r.args ?? {}) as Record<string, string>,
638
+ result: r.result,
639
+ }));
640
+ return JSON.stringify(decl.assert(snapshots, { intent, principal, principalVerified, reads }));
627
641
  }
628
642
 
629
643
  function planDerive(job: {
@@ -81,10 +81,15 @@ export const seedGenesis = directive("seedGenesis")
81
81
  .payload(
82
82
  z.object({
83
83
  principal: z.string().min(1),
84
- // EMPTY ⇒ a CONTROL-PLANE genesis: records the founding principal (arming the kernel's genesis
85
- // bootstrap clause for the founding admin grant) WITHOUT a self-id, so the workspace stays
86
- // UNWARRANTED (host-stamped authorship, like root) — deploys/governance/child births keep working
87
- // unsigned. NON-EMPTY ⇒ a TENANT (signed-author) self-id (the source binding author signatures cover).
84
+ // `seedGenesis` is GENERIC over the warrant: it records whatever self-id the genesis recipe hands it.
85
+ // EMPTY ⇒ the workspace stays UNWARRANTED (host-stamped authorship) — the LEGACY/compat lane: a
86
+ // pre-VA chain or a recipe that explicitly opted out (`requiresWarrant: false`) replays byte-identical.
87
+ // NON-EMPTY ⇒ WARRANTED: the self-id is the source binding author signatures cover, so the kernel
88
+ // enforces SIGNED control-plane authorship on every lane (author / receive / verify_chain).
89
+ // THE WARRANT-FLIP (THE GOVERNANCE DEFAULT, 2026-06-27): `buildGenesisChain` now seeds a NON-EMPTY
90
+ // self-id (the workspace name) by default for every governance/platform birth — control-plane authority
91
+ // is born warranted unless the recipe explicitly opts out. The choice lives in the recipe builder, not
92
+ // here; this directive just writes the warrant it is given. Era: the regime is per-workspace, at genesis.
88
93
  workspaceId: z.string(),
89
94
  bornAt: z.string().min(1),
90
95
  }),
@@ -69,6 +69,22 @@ export function refAs(name: string, aggregateType: string, id: string): Invarian
69
69
  return { name, aggregate: aggregateType, id };
70
70
  }
71
71
 
72
+ /**
73
+ * ONE captured write-path read the predicate may consume (#58 — the captured-read lane). The SAME
74
+ * `{queryId, args, result}` the directive's plan recorded onto `captured_ports.reads` (via `read(query,
75
+ * args)` + `.reads(theQuery)` on the directive), CAS-re-derived + byte-compared at the pre-apply position
76
+ * on every lane (author / edge admission / verify_chain). So a captured read the assert reads is a
77
+ * COMMITTED, peer-identical fact — the gate's count-style judgement input (e.g. the WorkspaceQuota
78
+ * invariant counts the workspaces a `workspacesByPrincipal` captured read returned). `result` is the
79
+ * canonical row array `[{id, data}]` `derive_captured_read` produces. The executor ships these into the
80
+ * assert dispatch on EVERY lane via the same `captured_ports` it hands the gate (no kernel/adapter change).
81
+ */
82
+ export interface InvariantCapturedRead {
83
+ readonly queryId: string;
84
+ readonly args: Record<string, string>;
85
+ readonly result: unknown;
86
+ }
87
+
72
88
  /** Derive the BOUNDED declared read-set from the intent payload. Pure — no IO, no live read.
73
89
  * `ctx.principal` is the captured, authenticated AUTHOR identity (e.g. `key:<keyHash>` / a verified uid)
74
90
  * — the same value on every replaying peer — so an invariant can gate by WHO authored (law-gated authz). */
@@ -101,6 +117,18 @@ export type WorkspaceInvariantAssert = (
101
117
  principal?: string;
102
118
  /** VA-core (Phase 3): the kernel-DERIVED author-verified flag — see {@link WorkspaceInvariantReads}. */
103
119
  principalVerified?: boolean;
120
+ /**
121
+ * THE CAPTURED READS (#58 — the captured-read invariant gate). The committed `captured_ports.reads`
122
+ * the directive's plan recorded — present iff the directive DECLARES reads (`.reads(theQuery)`) and
123
+ * its plan called `read(query, args)`. Each is CAS-re-derived + byte-compared at the pre-apply
124
+ * position on every lane, so a count/aggregate judgement over them is a function of the COMMITTED,
125
+ * peer-identical pre-apply state — never a forgeable client claim. This is how a count-style gate
126
+ * (e.g. WorkspaceQuota: `liveWorkspaces < allowance`) judges O(#rows) WITHOUT a new kernel primitive:
127
+ * the `workspaceInvariant.reads` ref-set resolves single instances by id (it cannot ENUMERATE), so a
128
+ * cardinality predicate reads the row LIST off a captured read instead. Empty when the directive
129
+ * declares no reads (every pre-#58 / #266 body ignores it — additive, byte-identical).
130
+ */
131
+ reads?: readonly InvariantCapturedRead[];
104
132
  },
105
133
  ) => WorkspaceInvariantVerdict;
106
134
 
@@ -73,7 +73,8 @@ import { Lww } from "../drivers.js";
73
73
  import { t } from "../fields.js";
74
74
  import { set, withMarker } from "../ops.js";
75
75
  import { query } from "../query.js";
76
- import { workspaceInvariant, refAs } from "./workspace_invariant.js";
76
+ import { read } from "../read.js";
77
+ import { workspaceInvariant, refAs, type WorkspaceInvariantVerdict } from "./workspace_invariant.js";
77
78
  import { writeTuple, removeTuple, RelationTuple, relationTupleInstanceId } from "./relations.js";
78
79
  import { relationType, self, user, key } from "../relation_schema.js";
79
80
  import { GENESIS_WARRANT_ID } from "./genesis.js";
@@ -458,13 +459,21 @@ export function buildGenesisChain(p: {
458
459
  frameworkHash?: string;
459
460
  lawHash?: string;
460
461
  /**
461
- * THE WARRANT-FLIP (VA-1, architecture/identity_keys.md). When TRUE — set iff the genesis recipe
462
- * declares an AuthProvider (an IdP vouches for this subtree's principals) — `seedGenesis` records a
463
- * REAL `workspaceId` (the workspace name as the unique self-id), so the chain is born WARRANTED:
464
- * `verify_captured_principal` enforces signed authorship on all lanes (author / receive / verify_chain).
465
- * FALSE (the default, the legacy lane) keeps `workspaceId: ""` so the workspace stays unwarranted
466
- * (host-stamped authorship, like root) — pre-VA / non-AuthProvider births replay byte-identically.
467
- * NO flag-day: the era is per-workspace, decided at genesis by whether the recipe carries an AuthProvider.
462
+ * THE WARRANT-FLIP (VA-1, architecture/identity_keys.md). Controls whether `seedGenesis` records a REAL
463
+ * `workspaceId` (the workspace name as the unique self-id) so the chain is born WARRANTED — meaning
464
+ * `verify_captured_principal` enforces SIGNED control-plane authorship on every lane (author / edge
465
+ * receive / verify_chain). An empty `workspaceId` keeps the workspace UNWARRANTED (host-stamped authorship,
466
+ * the legacy lane), so a control-plane offer (grantAdmin etc.) is taken at its word — the forge that was
467
+ * open on unwarranted root/co2.
468
+ *
469
+ * THE GOVERNANCE DEFAULT (THE WARRANT-FLIP, 2026-06-27): `buildGenesisChain` only ever runs for a
470
+ * PLATFORM/GOVERNANCE birth (every call sits inside the `if (p.foundingAdminHash)` guard — a genesis that
471
+ * carries control-plane authority: grantAdmin/grantCreation). Control-plane authority is EXACTLY what the
472
+ * signed-author regime protects, so a governance birth is born WARRANTED by DEFAULT (`requiresWarrant`
473
+ * undefined/true ⇒ warranted). The ONLY escape is an explicit `requiresWarrant === false`, retained as an
474
+ * era-safe compat hatch (a pre-VA chain re-built byte-identical). NO flag-day: the era is per-workspace,
475
+ * decided at genesis — existing UNWARRANTED chains keep replaying; only newly-born/re-born governance
476
+ * chains arm the signature requirement.
468
477
  */
469
478
  requiresWarrant?: boolean;
470
479
  }): string {
@@ -495,15 +504,106 @@ export function buildGenesisChain(p: {
495
504
  });
496
505
  return JSON.stringify([
497
506
  ...installSteps,
498
- // THE WARRANT-FLIP (VA-1): a REAL self-id iff the recipe declares an AuthProvider (born warranted ⇒
499
- // signed authorship enforced on every lane); "" keeps the legacy host-stamped lane (byte-identical).
500
- { domain: "workspaces", directiveId: "seedGenesis", actor: founder, payload: { principal: founder, workspaceId: p.requiresWarrant ? p.name : "", bornAt: p.bornAt } },
507
+ // THE WARRANT-FLIP (THE GOVERNANCE DEFAULT): a governance birth is born WARRANTED by default — a REAL
508
+ // self-id (the workspace name) so signed control-plane authorship is enforced on every lane. The ONLY
509
+ // way to stay on the legacy host-stamped lane ("" self-id, byte-identical to a pre-VA chain) is an
510
+ // EXPLICIT `requiresWarrant === false` (the era-safe compat hatch). `p.requiresWarrant !== false`
511
+ // ⇒ warranted whenever the flag is undefined OR true.
512
+ { domain: "workspaces", directiveId: "seedGenesis", actor: founder, payload: { principal: founder, workspaceId: p.requiresWarrant !== false ? p.name : "", bornAt: p.bornAt } },
501
513
  { domain: "workspaces", directiveId: "grantAdmin", actor: founder, payload: { keyHash: p.foundingAdminHash, label: `founding admin (born for ${p.principal})`, grantedBy: p.principal, grantedAt: p.requestedAt } },
502
514
  // No `role` — it was a vestigial node-class label (collapsed with `kind`); birthCloud ignores it now.
503
515
  { domain: "workspaces", directiveId: "birthCloud", actor: "", payload: { principal: p.principal, name: p.name, tier: p.tier, bornAt: p.requestedAt, bornUnder } },
504
516
  ]);
505
517
  }
506
518
 
519
+ // ── THE QUOTA-GATE QUERIES (declared HERE — used by the WorkspaceQuota captured-read invariant) ────────
520
+ // The captured-read invariant lane (#58) needs these QueryDecls at the directive's CONSTRUCTION (the
521
+ // `.reads(...)` call below), which runs at module load BEFORE the read-section further down — so the
522
+ // three the WorkspaceQuota gate consults are declared up here (the rest stay in the read section). They
523
+ // are STILL auto-discovered by shape (export name = wire id), so the read manifest/openapi route them
524
+ // exactly as before; only their definition POSITION moved. The quota gate counts a principal's live
525
+ // (non-retired/-reclaimed) CloudWorkspaces against its allowance (a QuotaGrant else the CloudPolicy default).
526
+
527
+ /** One principal's workspaces — the WorkspaceQuota gate counts the live (non-retired/-reclaimed) ones. */
528
+ export const workspacesByPrincipal = query("workspacesByPrincipal")
529
+ .key("principal")
530
+ .returns(CloudWorkspace);
531
+
532
+ /** THE QUOTA LAW singleton (`scope: "cloud"`) — defaults for allowance + byte cap. */
533
+ export const cloudPolicy = query("cloudPolicy").key("scope").returns(CloudPolicy);
534
+
535
+ /** A principal's quota grants — the LATEST one is the allowance in force (overrides the CloudPolicy default). */
536
+ export const quotaByPrincipal = query("quotaByPrincipal")
537
+ .key("principal")
538
+ .returns(QuotaGrant);
539
+
540
+ /** THE DEFAULT WORKSPACE ALLOWANCE — the CLAUDE.md / CloudPolicy figure when no grant + no policy row. */
541
+ export const DEFAULT_MAX_WORKSPACES_PER_PRINCIPAL = 50;
542
+
543
+ /** A captured-read row as `derive_captured_read` shapes it: `{id, data:{field:value}}`. */
544
+ type QuotaRow = { id?: string; data?: Record<string, unknown> };
545
+
546
+ /**
547
+ * CAPTURE the three quota reads on the write path (the plan side of the WorkspaceQuota gate). Called by
548
+ * both `createWorkspace` and `birthChild` so the captured set is IDENTICAL + UNCONDITIONAL (a stable,
549
+ * byte-deterministic capture on every lane). It returns nothing — the values ride `captured_ports.reads`
550
+ * and the verdict is the invariant's ({@link quotaVerdict}); this just commits the premise.
551
+ */
552
+ function countQuotaReads(principal: string): void {
553
+ read(workspacesByPrincipal, { principal });
554
+ read(quotaByPrincipal, { principal });
555
+ read(cloudPolicy, { scope: "cloud" });
556
+ }
557
+
558
+ /**
559
+ * THE WORKSPACE-QUOTA VERDICT — the PURE predicate the {@link workspaceQuota} invariant runs over the
560
+ * COMMITTED captured reads (the same `{queryId,args,result}` the plan recorded, CAS-re-derived on every
561
+ * lane). Counts the principal's LIVE (non-retired/-reclaimed) CloudWorkspaces and compares against its
562
+ * allowance (the latest QuotaGrant.maxWorkspaces if any, else CloudPolicy.maxWorkspacesPerPrincipal, else
563
+ * the {@link DEFAULT_MAX_WORKSPACES_PER_PRINCIPAL} floor). The NEW workspace is NOT yet in the pre-apply
564
+ * state, so `liveCount < allowance` (strict) is the correct test: the principal may hold AT MOST
565
+ * `allowance`, so a fresh birth is admitted iff it already holds fewer. A principal-less call (should be
566
+ * impossible after S3) fails CLOSED. The verdict is a pure function of the captured reads ⇒ identical on
567
+ * author, edge admission, and verify_chain.
568
+ */
569
+ function quotaVerdict(
570
+ reads: readonly { queryId: string; args: Record<string, string>; result: unknown }[],
571
+ principal: string,
572
+ ): WorkspaceInvariantVerdict {
573
+ if (!principal) return { reject: "quota-missing-principal" };
574
+ const rowsOf = (queryId: string): QuotaRow[] => {
575
+ const hit = reads.find((r) => r.queryId === queryId && r.args.principal === principal);
576
+ return Array.isArray(hit?.result) ? (hit!.result as QuotaRow[]) : [];
577
+ };
578
+ // LIVE = not retired, not reclaimed (the WORKSPACE_STATUSES lifecycle). The captured read already
579
+ // excludes archived rows; status is a plain Lww string in `data`.
580
+ const liveCount = rowsOf(workspacesByPrincipal.id).filter((row) => {
581
+ const status = (row.data?.status ?? "") as string;
582
+ return status !== "retired" && status !== "reclaimed";
583
+ }).length;
584
+ // ALLOWANCE: the LATEST QuotaGrant for the principal (max grantedAt) wins; else the CloudPolicy default;
585
+ // else the hard floor. `derive_captured_read` decodes t.int() as a JSON number.
586
+ const grants = rowsOf(quotaByPrincipal.id);
587
+ let allowance: number | undefined;
588
+ let latestGrantedAt = -Infinity;
589
+ for (const g of grants) {
590
+ const at = Number(g.data?.grantedAt ?? -Infinity);
591
+ const max = Number(g.data?.maxWorkspaces ?? NaN);
592
+ if (Number.isFinite(at) && at > latestGrantedAt && Number.isFinite(max)) {
593
+ latestGrantedAt = at;
594
+ allowance = max;
595
+ }
596
+ }
597
+ if (allowance === undefined) {
598
+ const policyHit = reads.find((r) => r.queryId === cloudPolicy.id && r.args.scope === "cloud");
599
+ const policyRows = Array.isArray(policyHit?.result) ? (policyHit!.result as QuotaRow[]) : [];
600
+ const max = Number(policyRows[0]?.data?.maxWorkspacesPerPrincipal ?? NaN);
601
+ if (Number.isFinite(max)) allowance = max;
602
+ }
603
+ if (allowance === undefined) allowance = DEFAULT_MAX_WORKSPACES_PER_PRINCIPAL;
604
+ return liveCount < allowance ? { accept: true } : { reject: "workspace-quota-exceeded" };
605
+ }
606
+
507
607
  /**
508
608
  * createWorkspace — record the lawful REQUEST for a workspace. Nomos MINTS the
509
609
  * `CloudWorkspace` id (the payload carries none); the record is born
@@ -523,17 +623,30 @@ export const createWorkspace = directive("createWorkspace")
523
623
  tier: z.enum(WORKSPACE_TIERS).default("edge"),
524
624
  /** Parent CloudWorkspace aggregate id; `""` (default) = root. */
525
625
  parent: z.string().default(""),
526
- /** The auth-provider principal uid the request claimed; `""` = unclaimed. */
527
- principal: z.string().default(""),
626
+ /** The auth-provider principal uid the request claimed. REQUIRED (S3 — no anonymous nodes):
627
+ * every birth bottoms out in a principal, so the kernel REFUSES an empty-principal birth at
628
+ * the typed payload gate (the host `if(!principal) 401` was the only enforcer; now it is LAW).
629
+ * Era-safe: pre-change chains only ever recorded born-with-principal records (the host gate
630
+ * already blocked anonymous births), and replay resolves each intent under the law that sealed
631
+ * it (per-position era), so old intents never re-validate against this schema. */
632
+ principal: z.string().min(1),
528
633
  requestedBy: z.string().min(1),
529
634
  requestedAt: epochMs,
530
635
  }),
531
636
  )
532
637
  .plan((p) => {
638
+ // THE WORKSPACE-QUOTA CAPTURED READS (#58 / S2): consult the principal's live-workspace count + its
639
+ // allowance on the write path. Each read is CAS-re-derived + byte-compared at the pre-apply position
640
+ // on every lane, so the WorkspaceQuota invariant judges over the COMMITTED, peer-identical state (never
641
+ // a forged client claim). The plan only CAPTURES them (the verdict is the invariant's job); the values
642
+ // ride `captured_ports.reads`. The captured set must be STABLE across author + replay — always read all
643
+ // three (an unconditional capture is byte-deterministic; a data-dependent one would not be).
644
+ countQuotaReads(p.principal);
533
645
  // No genesis ⇒ a plain lineage record (no foundingAdminHash) — never drained; materialized by boot-on-touch.
534
646
  planBirthRecord(p);
535
647
  return [];
536
648
  })
649
+ .reads(workspacesByPrincipal, quotaByPrincipal, cloudPolicy) // arm the captured-read invariant gate (S2)
537
650
  .requires("creator"); // posture-gated creation: only a #creator holder may request a workspace
538
651
 
539
652
  /**
@@ -733,8 +846,13 @@ export const birthChild = directive("birthChild")
733
846
  z
734
847
  .object({
735
848
  name: z.string().min(1),
736
- /** The auth-provider principal uid the request claimed; `""` = unclaimed (a plain leaf may be). */
737
- principal: z.string().default(""),
849
+ /** The auth-provider principal uid the request claimed. REQUIRED (S3 — no anonymous nodes):
850
+ * THE WORLD RESTS ON A BIRTH UID, so a birth with no principal is refused at the typed
851
+ * payload gate (the host `if(!principal) 401` was the only enforcer; now it is LAW, judged
852
+ * on every lane — author / edge admission / verify_chain). Era-safe: pre-change chains only
853
+ * ever recorded born-with-principal records, and replay resolves each intent under the law
854
+ * that sealed it (per-position era), so old intents never re-validate against this schema. */
855
+ principal: z.string().min(1),
738
856
  /** Parent CloudWorkspace aggregate id; `""` (default) = root. */
739
857
  parent: z.string().default(""),
740
858
  /** Hosting tier; defaults to the edge DO (a genesis/platform birth typically passes `heavy`). */
@@ -769,11 +887,18 @@ export const birthChild = directive("birthChild")
769
887
  }),
770
888
  )
771
889
  .plan((p) => {
890
+ // THE WORKSPACE-QUOTA CAPTURED READS (#58 / S2) — same unconditional capture as createWorkspace, so the
891
+ // WorkspaceQuota invariant counts this principal's live workspaces against its allowance on every lane.
892
+ countQuotaReads(p.principal);
772
893
  const rec = planBirthRecord(p, p.foundingAdminHash ? { foundingAdminHash: p.foundingAdminHash, pool: p.pool } : undefined);
773
894
  // LAW AUTHORS THE GENESIS RECIPE (P1): a genesis birth carries its child's [installDomain×2 →]
774
895
  // seedGenesis→grantAdmin→birthCloud chain. (refine guarantees principal + bornAt here.)
775
896
  if (p.foundingAdminHash) {
776
- 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.requiresWarrant ? { requiresWarrant: true } : {}) });
897
+ // THE GOVERNANCE DEFAULT (THE WARRANT-FLIP): a governance birth is warranted unless the request
898
+ // EXPLICITLY opts out (`requiresWarrant === false`). Thread the flag through ONLY when the request set
899
+ // it — undefined falls to buildGenesisChain's warranted default; an explicit `false` rides the compat
900
+ // hatch (an era-safe legacy re-build).
901
+ 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.requiresWarrant !== undefined ? { requiresWarrant: p.requiresWarrant } : {}) });
777
902
  // THE GENESIS RECIPE IS ALSO A LINEAGE/AUDIT FACT on the parent's chain (the parent records what it
778
903
  // ORDERED — law naming law, legitimate). The kernel does NOT read this field to detect the birth.
779
904
  rec.set("genesisChain", genesisChain);
@@ -786,6 +911,7 @@ export const birthChild = directive("birthChild")
786
911
  }
787
912
  return [];
788
913
  })
914
+ .reads(workspacesByPrincipal, quotaByPrincipal, cloudPolicy) // arm the WorkspaceQuota captured-read gate (S2)
789
915
  .births() // BIRTH IS A DECLARED KERNEL PRIMITIVE (G3): emits the `nomosBirthGate` era key; the kernel folds the carried recipe
790
916
  .requires("creator"); // THE ONE birth authority — a #creator may birth a leaf OR a platform (tier collapsed)
791
917
 
@@ -1248,18 +1374,9 @@ export const workspacesByParent = query("workspacesByParent")
1248
1374
  .key("parent")
1249
1375
  .returns(CloudWorkspace);
1250
1376
 
1251
- /** One principal's workspaces — the quota gate counts the non-retired ones. */
1252
- export const workspacesByPrincipal = query("workspacesByPrincipal")
1253
- .key("principal")
1254
- .returns(CloudWorkspace);
1255
-
1256
- /** THE QUOTA LAW singleton (`scope: "cloud"`) — defaults for allowance + byte cap. */
1257
- export const cloudPolicy = query("cloudPolicy").key("scope").returns(CloudPolicy);
1258
-
1259
- /** A principal's quota grants — the LATEST one is the allowance in force. */
1260
- export const quotaByPrincipal = query("quotaByPrincipal")
1261
- .key("principal")
1262
- .returns(QuotaGrant);
1377
+ // workspacesByPrincipal / cloudPolicy / quotaByPrincipal — DECLARED ABOVE (before createWorkspace), so the
1378
+ // WorkspaceQuota captured-read invariant can reference them at directive construction. (Auto-discovered by
1379
+ // shape regardless of position; they route into the read manifest/openapi exactly as the others here do.)
1263
1380
 
1264
1381
  /** "May this principal create platforms?" — invite-only; active grant = yes. */
1265
1382
  export const platformGrantByPrincipal = query("platformGrantByPrincipal")
@@ -1416,3 +1533,45 @@ export const reclaimRequiresRetired = workspaceInvariant("ReclaimRequiresRetired
1416
1533
  const rec = ws as { status?: string } | undefined;
1417
1534
  return rec?.status === "retired" ? { accept: true } : { reject: "reclaim-requires-retired" };
1418
1535
  });
1536
+
1537
+ // ── THE WORKSPACE-QUOTA GATE (S2 — THE QUOTA LAW, ENFORCED) ────────────────────────────────────────────
1538
+ // The CloudPolicy/QuotaGrant law-state used to be enforced by NOBODY — `createWorkspace`/`birthChild` were
1539
+ // gated only by `.requires("creator")`, so a single #creator could mint unbounded workspaces. NOW the
1540
+ // COUNT quota is REAL law at the gate: each birth directive CAPTURES (via the #58 captured-read lane,
1541
+ // `countQuotaReads`) the principal's live-workspace list + its allowance, and THIS invariant counts them
1542
+ // and refuses once the principal already holds its allowance. It rides the #41 captured-read invariant
1543
+ // gate — O(#workspaces-for-the-principal), no new kernel primitive: the standard `workspaceInvariant.reads`
1544
+ // ref-set resolves single instances by id (it CANNOT enumerate), so the cardinality predicate reads the row
1545
+ // LIST off the captured reads in `ctx.reads` instead (the snapshot ref-set is empty — nothing to resolve by
1546
+ // id). The captured reads are CAS-re-derived + byte-compared at the pre-apply position on every lane
1547
+ // (author / edge admission / verify_chain), so the count the gate judges is the COMMITTED, peer-identical
1548
+ // pre-apply count — a forged/stale client claim is a typed read-conflict refusal BEFORE this even runs.
1549
+ //
1550
+ // HONEST SCOPE: this is the COUNT quota only (live workspaces per principal). The BYTE cap
1551
+ // (`workspaceByteCap` / cumulative pushed bytes) is a CUSTODY fact, NOT chain state — it is metered by the
1552
+ // transport rate-limit lane, never by a kernel cap, and this invariant does NOT pretend to enforce it.
1553
+ //
1554
+ // ERA-SAFE: the era is the bundle. Both the captured-read lane (`nomosReadGate`, emitted iff a directive
1555
+ // declares `.reads(...)`) and the invariant (`nomosInvariantGate`, emitted iff the package declares ≥1
1556
+ // invariant — which governance already does) are PER-INSTALL era keys; every chain sealed under pre-S2 law
1557
+ // carries NEITHER for these directives, so it replays BYTE-GREEN (no captured reads to re-derive, no
1558
+ // WorkspaceQuota invariant to fire). The verdict is a pure function of the captured reads ⇒ identical on
1559
+ // every replaying peer.
1560
+
1561
+ /** createWorkspace: the principal must hold FEWER than its allowance of live workspaces (count quota). */
1562
+ export const workspaceQuotaCreate = workspaceInvariant("WorkspaceQuota")
1563
+ .on("createWorkspace")
1564
+ // The snapshot ref-set is EMPTY — the count comes from the captured reads (ctx.reads), which the standard
1565
+ // ref-resolution cannot produce (it resolves single instances by id, never an enumeration).
1566
+ .reads(() => [])
1567
+ .assert((_snapshots, ctx) =>
1568
+ quotaVerdict(ctx?.reads ?? [], String((ctx?.intent ?? {}).principal ?? "")),
1569
+ );
1570
+
1571
+ /** birthChild: the SAME count quota (a leaf or platform birth draws on the principal's allowance). */
1572
+ export const workspaceQuotaBirth = workspaceInvariant("WorkspaceQuotaBirth")
1573
+ .on("birthChild")
1574
+ .reads(() => [])
1575
+ .assert((_snapshots, ctx) =>
1576
+ quotaVerdict(ctx?.reads ?? [], String((ctx?.intent ?? {}).principal ?? "")),
1577
+ );
package/src/index.ts CHANGED
@@ -386,6 +386,7 @@ export {
386
386
  type WorkspaceInvariantReads,
387
387
  type WorkspaceInvariantAssert,
388
388
  type InvariantRef,
389
+ type InvariantCapturedRead,
389
390
  } from "./framework/workspace_invariant.js";
390
391
  // NATIVE ZANZIBAR RELATION SCHEMA (P1) — the userset-rewrite law as pure data.
391
392
  export {