@githolon/dsl 0.33.0 → 0.34.1

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.33.0",
3
+ "version": "0.34.1",
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",
package/src/aggregate.ts CHANGED
@@ -166,6 +166,16 @@ export interface AggregateHandle<
166
166
  * `readModelAggregates`). Identified by OBJECT IDENTITY (the branded singleton), so a tenant
167
167
  * aggregate that merely SHARES a name is never misclassified. OMITTED on tenant aggregates. */
168
168
  readonly framework?: true;
169
+ /**
170
+ * READ-PROJECTION NAMESPACING opt-out (`read_projection_namespace.ts`). TRUE ⇒ this
171
+ * aggregate's type name stays GLOBAL (bare, un-namespaced) on every surface — for
172
+ * KERNEL-COUPLED framework aggregates whose bare name/instance-id convention the
173
+ * kernel hardcodes. Statically-named framework aggregates ride the explicit
174
+ * allowlist instead; this flag is the DYNAMICALLY-NAMED factory lane (e.g.
175
+ * `platformAdmin()`'s founder aggregate, impure-capability envelopes). NOT hashed
176
+ * itself — its EFFECT (the bare vs qualified name) is what the law carries.
177
+ */
178
+ readonly global?: boolean;
169
179
  /** Presence-only flag (hashed); OMITTED ENTIRELY when no invariant is declared. */
170
180
  readonly hasInvariant?: true;
171
181
  /** The executable body — ships in the engine bundle, NOT the manifest. */
@@ -230,7 +240,7 @@ export interface AudienceMethods<Id extends string, F extends Record<string, Fie
230
240
  export function aggregate<const Id extends string, F extends Record<string, Field>>(
231
241
  id: Id,
232
242
  fields: F,
233
- opts?: { invariant?: AggregateInvariantFn; invariantDeclassify?: string; cap?: number; visibility?: ReadVisibility },
243
+ opts?: { invariant?: AggregateInvariantFn; invariantDeclassify?: string; cap?: number; visibility?: ReadVisibility; global?: boolean },
234
244
  ): AggregateHandle<Id, F> & AudienceMethods<Id, F> {
235
245
  const { stored, hasMany } = partitionFields(fields);
236
246
  if (opts?.cap !== undefined && (!Number.isInteger(opts.cap) || opts.cap < 1)) {
@@ -279,6 +289,7 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
279
289
  : {}),
280
290
  ...(opts?.invariantDeclassify !== undefined ? { invariantDeclassify: opts.invariantDeclassify } : {}),
281
291
  ...(opts?.cap !== undefined ? { cap: opts.cap } : {}),
292
+ ...(opts?.global === true ? { global: true } : {}),
282
293
  ...(encryptedFields.length > 0 ? { encryptedFields } : {}),
283
294
  ...(metaFields.length > 0 ? { metaFields } : {}),
284
295
  };
package/src/authoring.ts CHANGED
@@ -27,6 +27,7 @@ import type { AggregateHandle } from "./aggregate.js";
27
27
  import type { Field } from "./fields.js";
28
28
  import type { FieldOp, PlannedOp, PlannedEventMarker } from "./ops.js";
29
29
  import { endorsementOf } from "./principal_evidence.js";
30
+ import { __qualifyAggregateType } from "./framework/relations.js";
30
31
 
31
32
  // ── the per-dispatch authoring sink — library state the engine never sees (NOT a side channel out of
32
33
  // the sandbox; it is internal to the directive runtime, drained synchronously by `executeDirectiveToIntent`). ──
@@ -121,7 +122,9 @@ class Ref implements AggregateRef {
121
122
 
122
123
  /** Ask Nomos to CREATE a new aggregate of the handle's type → a fluent reference (id minted, no choice). */
123
124
  export function create(agg: AggregateHandle): AggregateRef {
124
- return new Ref(agg, mint(agg.id), "creates");
125
+ // READ-PROJECTION NAMESPACING: mint under the QUALIFIED type tag — the kernel's id-mint gate
126
+ // verifies a Create's id was minted for EXACTLY the declared (qualified) `__type`.
127
+ return new Ref(agg, mint(__qualifyAggregateType(agg.id)), "creates");
125
128
  }
126
129
 
127
130
  /** A reference to an EXISTING aggregate the dev already holds an id for (e.g. from the payload / a read). */
@@ -0,0 +1,300 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded
4
+ // QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /**
7
+ * THE BIRTH METER (metered birth, slice A — `architecture/metered_birth_and_attested_reads.md`).
8
+ *
9
+ * THE ONE-HOP VOUCHING PRINCIPLE: trust is established BY INDUCTION AT BIRTH and verified LOCALLY —
10
+ * a parent vouches for its child IN ITS OWN CHAIN; nobody walks to root. The parent's own ledger
11
+ * already witnesses every birth it performs (the admitted `.births()` intent is IN the parent's
12
+ * chain), so the count of a parent's children is a PARENT-LOCAL fact and the existing
13
+ * same-workspace #58 captured-read lane covers it. NO cross-workspace reads anywhere in this slice.
14
+ *
15
+ * Three structural pieces, all era-ruled by the bundle (a pre-feature bundle carries none of this
16
+ * and replays byte-identical; a bundle recompiled under this DSL carries all of it by construction):
17
+ *
18
+ * A1 — THE BIRTH RECORD IS STRUCTURAL. Every `.births()` directive's intent ALSO writes a
19
+ * {@link NomosBirthRecord} event (one deterministic-id record per child; a re-birth of the
20
+ * same name UPDATES the record — Lww) in the SAME intent as the birth effect. The library
21
+ * appends it in `executeDirectiveToIntent` (the ONE plan-output assembly BOTH lanes — client
22
+ * authoring and the sealed engine — funnel through), so tenant law gets it by construction,
23
+ * zero tenant code. The kernel arm (`verify_births_witness` + the `nomosBirthRecordGate` era
24
+ * key) REFUSES a birth whose intent carries no record — ONLY for bundles declaring the key.
25
+ *
26
+ * A2 — THE BIRTH QUOTA IS PARENT-LOCAL LAW. The `.births()` lane auto-captures
27
+ * {@link birthRecordsByPrincipal} + the parent's allowance facts (the #58 captured-read lane,
28
+ * CAS-re-derived on every lane), and a synthesized framework `workspaceInvariant` (the exact
29
+ * WorkspaceQuota shape) refuses `birth-quota-exceeded` once the subject already holds its
30
+ * allowance. Allowance resolution: the latest {@link BirthGrant} for the subject if any, else
31
+ * the workspace's {@link BirthPolicy} singleton, else {@link DEFAULT_MAX_BIRTHS_PER_PRINCIPAL}.
32
+ *
33
+ * A3 — THE POOL CHAINS TO ROOT. A parent births a child WITH the child's own descendant budget
34
+ * (`buildGenesisChain`'s `descendantPool` threads a `seedBirthPolicy` genesis step), so every
35
+ * child is BORN BOUNDED: root bounds platforms, platforms bound homes, homes bound estates —
36
+ * the tree meters by induction. V1 BOUNDARY (deliberate): each node enforces ITS OWN
37
+ * count-quota only; cross-level pool CONSERVATION arithmetic (a child may not grant more than
38
+ * it holds) is v2 — stated in the design doc, not silently pretended here.
39
+ *
40
+ * THE METER SUBJECT: the payload-carried birth subject (`principal`, else `owner` — the SAME
41
+ * convention `birthChild`/`birthHome` already carry), a pure function of the committed payload so
42
+ * it is identical on author / edge admission / verify_chain. A `.births()` payload carrying NEITHER
43
+ * fails CLOSED (`birth-quota-missing-subject`) — un-meterable ⇒ un-born, the ratified doctrine.
44
+ */
45
+ import { aggregate, instance, type AggregateHandle } from "./aggregate.js";
46
+ import { t } from "./fields.js";
47
+ import { Lww } from "./drivers.js";
48
+ import { query, type QueryDecl } from "./query.js";
49
+ import { __captureBirthMeterRead } from "./read.js";
50
+ import { set, withMarker, type PlannedOp } from "./ops.js";
51
+ import type { WorkspaceInvariantDecl, WorkspaceInvariantVerdict } from "./framework/workspace_invariant.js";
52
+ import type { BirthRecipe } from "./births.js";
53
+
54
+ // ── A1 — the structural birth record ───────────────────────────────────────────────────────────────
55
+
56
+ /** The deterministic instance-id prefix — `nomos-birth-record:<child>` (one record per child name;
57
+ * a re-birth of the same name updates the SAME record, Lww). The kernel's `nomosBirthRecordGate`
58
+ * era key carries this prefix so the wasm arm reads it from LAW, never hardcodes it. */
59
+ export const NOMOS_BIRTH_RECORD_PREFIX = "nomos-birth-record:";
60
+
61
+ /** The birth record's deterministic aggregate instance id for a child workspace name. */
62
+ export function birthRecordId(child: string): string {
63
+ return NOMOS_BIRTH_RECORD_PREFIX + child;
64
+ }
65
+
66
+ /**
67
+ * NomosBirthRecord — THE STRUCTURAL BIRTH RECORD (A1). One row per child this workspace has
68
+ * birthed, written AUTOMATICALLY in the same intent as the birth effect (the `.births()` lane).
69
+ * The framework twin of `DomainInstallation`: a kernel-recognized, law-folded lifecycle fact.
70
+ */
71
+ export const NomosBirthRecord = aggregate("NomosBirthRecord", {
72
+ /** The child workspace name the birth effect spawned. */
73
+ child: t.string().merge(Lww),
74
+ /** The birth SUBJECT — the payload-carried `principal`/`owner` the quota meters (see banner). */
75
+ principal: t.string().merge(Lww),
76
+ /** sha256 of the child's genesis chain JSON (the recipe the offer carried — content-addressed lineage). */
77
+ childGenesisHash: t.string().merge(Lww),
78
+ /** Caller-stamped birth time from the payload (`bornAt`, else `requestedAt`) — the timestamp doctrine. */
79
+ bornAt: t.string().merge(Lww),
80
+ });
81
+
82
+ /** BirthPolicy — the workspace's birth-allowance singleton (id `birth-policy:workspace`), the
83
+ * `CloudPolicy` shape one level down: the default max live births per subject in THIS workspace. */
84
+ export const BIRTH_POLICY_ID = "birth-policy:workspace";
85
+ export const BirthPolicy = aggregate("BirthPolicy", {
86
+ /** The query key (constant `"workspace"`) — a declared field so `birthPolicy` can index it. */
87
+ scope: t.string().merge(Lww),
88
+ maxBirthsPerPrincipal: t.int().merge(Lww),
89
+ setBy: t.string().merge(Lww),
90
+ /** Caller-stamped (ISO or epoch-ms string) — the timestamp doctrine. */
91
+ setAt: t.string().merge(Lww),
92
+ });
93
+
94
+ /** BirthGrant — a per-subject allowance override (id `birth-grant:<principal>`), the `QuotaGrant`
95
+ * shape: the LATEST grant (max grantedAt) for a subject is the allowance in force. */
96
+ export function birthGrantId(principal: string): string {
97
+ return "birth-grant:" + principal;
98
+ }
99
+ export const BirthGrant = aggregate("BirthGrant", {
100
+ principal: t.string().merge(Lww),
101
+ maxBirths: t.int().merge(Lww),
102
+ grantedBy: t.string().merge(Lww),
103
+ /** epoch-ms — the recency tiebreak (latest grant wins), caller-stamped. */
104
+ grantedAt: t.int().merge(Lww),
105
+ });
106
+
107
+ // ── the meter's declared queries (the #58 captured-read recipes) ────────────────────────────────────
108
+
109
+ /** A subject's birth records — the quota gate counts them (parent-LOCAL; no cross-ws read). */
110
+ export const birthRecordsByPrincipal = query("birthRecordsByPrincipal")
111
+ .key("principal")
112
+ .returns(NomosBirthRecord);
113
+
114
+ /** The workspace's birth-policy singleton (`scope: "workspace"`). */
115
+ export const birthPolicy = query("birthPolicy").key("scope").returns(BirthPolicy);
116
+
117
+ /** A subject's allowance grants — the LATEST one overrides the BirthPolicy default. */
118
+ export const birthGrantByPrincipal = query("birthGrantByPrincipal")
119
+ .key("principal")
120
+ .returns(BirthGrant);
121
+
122
+ /** The three meter reads every `.births()` directive auto-declares (`nomosReadGate`). */
123
+ export const BIRTH_METER_QUERIES: readonly QueryDecl[] = [
124
+ birthRecordsByPrincipal,
125
+ birthPolicy,
126
+ birthGrantByPrincipal,
127
+ ];
128
+
129
+ /**
130
+ * THE DEFAULT BIRTH ALLOWANCE when a workspace carries neither a BirthPolicy singleton nor a grant
131
+ * for the subject: a CONSERVATIVE floor for non-root workspaces (a home births a handful of estate
132
+ * children; a platform that needs more seeds a BirthPolicy at its own genesis — the A3 induction —
133
+ * or grants per-subject allowances). Root/platform governance already carries the 50-workspace
134
+ * `WorkspaceQuota`; THIS default only ever binds law that never set its own policy.
135
+ */
136
+ export const DEFAULT_MAX_BIRTHS_PER_PRINCIPAL = 5;
137
+
138
+ // ── the meter subject + the auto-record/auto-capture halves the `.births()` lane calls ─────────────
139
+
140
+ /** The payload-carried birth SUBJECT the meter counts under: `principal`, else `owner`, else `""`
141
+ * (fail-closed at the invariant). A pure function of the committed payload — identical on every lane. */
142
+ export function birthMeterSubject(payload: Record<string, unknown>): string {
143
+ // The payload-carried subject convention, in priority order: `principal` (birthChild/createWorkspace),
144
+ // `owner` (birthHome), `ownerSubject` (the estate-birth shape). A pure function of the committed
145
+ // payload — identical on every lane; empty ⇒ the quota invariant fails closed.
146
+ for (const key of ["principal", "owner", "ownerSubject"]) {
147
+ const v = payload[key];
148
+ if (typeof v === "string" && v.length > 0) return v;
149
+ }
150
+ return "";
151
+ }
152
+
153
+ /** The record's caller-stamped birth time off the payload (`bornAt`, else `requestedAt`, else `""`). */
154
+ export function birthMeterBornAt(payload: Record<string, unknown>): string {
155
+ const b = payload["bornAt"];
156
+ if (typeof b === "string" && b.length > 0) return b;
157
+ const r = payload["requestedAt"];
158
+ if (typeof r === "number" || (typeof r === "string" && r.length > 0)) return String(r);
159
+ return "";
160
+ }
161
+
162
+ /** sha256 via the ONE deterministic host port (`nomos.sha256` — the same global `homeNameOf` uses,
163
+ * provided by the sealed engine AND the client runtime, so both lanes hash identically). */
164
+ function sha256(text: string): string {
165
+ const host = (globalThis as { nomos?: { sha256?: (s: string) => string } }).nomos;
166
+ if (host === undefined || typeof host.sha256 !== "function") {
167
+ throw new Error(
168
+ "birth-meter: the `nomos.sha256` host port is not available — a `.births()` dispatch needs the " +
169
+ "engine/client runtime (tests: stub `globalThis.nomos.sha256`).",
170
+ );
171
+ }
172
+ return host.sha256(text);
173
+ }
174
+
175
+ /**
176
+ * CAPTURE the three meter reads on the write path (A2 — the plan side of the birth-quota gate; the
177
+ * exact `countQuotaReads` shape). Called by `executeDirectiveToIntent` for every `.births()` dispatch
178
+ * that planned ≥1 birth — the values ride `captured_ports.reads`, CAS-re-derived + byte-compared at
179
+ * the pre-apply position on every lane; the verdict is the synthesized invariant's.
180
+ */
181
+ export function captureBirthMeterReads(subject: string): void {
182
+ __captureBirthMeterRead(birthRecordsByPrincipal, { principal: subject });
183
+ __captureBirthMeterRead(birthGrantByPrincipal, { principal: subject });
184
+ __captureBirthMeterRead(birthPolicy, { scope: "workspace" });
185
+ }
186
+
187
+ /**
188
+ * THE AUTO-RECORD (A1): the `NomosBirthRecord` field ops for ONE planned birth, appended by
189
+ * `executeDirectiveToIntent` to the SAME intent that carries the birth effect. Deterministic-id
190
+ * upsert (Lww — a re-birth of the same child name updates the record), a fan-out sibling event
191
+ * exactly like `installDomain`'s `DomainInstallation` writes.
192
+ */
193
+ export function planBirthRecordOps(
194
+ recipe: BirthRecipe,
195
+ subject: string,
196
+ bornAt: string,
197
+ ): PlannedOp[] {
198
+ const rec = instance(NomosBirthRecord, birthRecordId(recipe.workspace));
199
+ return [
200
+ // ENSURE, not a bare update: the record id is deterministic and the FIRST birth of a child writes a
201
+ // row that does not yet exist — the referential gate refuses a Mutate of a non-existent id, so the
202
+ // event must lower `Ensure` (create-or-update), exactly how `installDomain` lowers its
203
+ // DomainInstallation singleton (`withMarker(..., "ensures")`). One marked op sets the whole event's
204
+ // marker; both lanes share THIS function, so the op shape is byte-identical by construction.
205
+ withMarker(set(rec, "child", recipe.workspace), "ensures"),
206
+ set(rec, "principal", subject),
207
+ set(rec, "childGenesisHash", sha256(JSON.stringify(recipe.genesisChain))),
208
+ set(rec, "bornAt", bornAt),
209
+ ];
210
+ }
211
+
212
+ // ── A2 — the quota verdict + the synthesized per-directive invariant ────────────────────────────────
213
+
214
+ /** A captured-read row as `derive_captured_read` shapes it: `{id, data:{field:value}}`. */
215
+ type MeterRow = { id?: string; data?: Record<string, unknown> };
216
+
217
+ /**
218
+ * THE BIRTH-QUOTA VERDICT — the PURE predicate over the COMMITTED captured reads (the same
219
+ * `{queryId,args,result}` the plan recorded, CAS-re-derived on every lane). Counts the subject's
220
+ * birth records and compares against its allowance (the latest BirthGrant.maxBirths if any, else
221
+ * BirthPolicy.maxBirthsPerPrincipal, else {@link DEFAULT_MAX_BIRTHS_PER_PRINCIPAL}). The NEW birth's
222
+ * record is NOT yet in the pre-apply state, so `count < allowance` (strict) is the correct test. A
223
+ * subject-less birth fails CLOSED. Identical on author, edge admission, and verify_chain.
224
+ */
225
+ export function birthQuotaVerdict(
226
+ reads: readonly { queryId: string; args: Record<string, string>; result: unknown }[],
227
+ subject: string,
228
+ ): WorkspaceInvariantVerdict {
229
+ if (!subject) return { reject: "birth-quota-missing-subject" };
230
+ const rowsOf = (queryId: string, argKey: string, argValue: string): MeterRow[] => {
231
+ const hit = reads.find((r) => r.queryId === queryId && r.args[argKey] === argValue);
232
+ return Array.isArray(hit?.result) ? (hit!.result as MeterRow[]) : [];
233
+ };
234
+ const count = rowsOf(birthRecordsByPrincipal.id, "principal", subject).length;
235
+ // ALLOWANCE: the LATEST BirthGrant (max grantedAt) wins; else the BirthPolicy singleton; else the floor.
236
+ let allowance: number | undefined;
237
+ let latestGrantedAt = -Infinity;
238
+ for (const g of rowsOf(birthGrantByPrincipal.id, "principal", subject)) {
239
+ const at = Number(g.data?.grantedAt ?? -Infinity);
240
+ const max = Number(g.data?.maxBirths ?? NaN);
241
+ if (Number.isFinite(at) && at > latestGrantedAt && Number.isFinite(max)) {
242
+ latestGrantedAt = at;
243
+ allowance = max;
244
+ }
245
+ }
246
+ if (allowance === undefined) {
247
+ const policyRows = rowsOf(birthPolicy.id, "scope", "workspace");
248
+ const max = Number(policyRows[0]?.data?.maxBirthsPerPrincipal ?? NaN);
249
+ if (Number.isFinite(max)) allowance = max;
250
+ }
251
+ if (allowance === undefined) allowance = DEFAULT_MAX_BIRTHS_PER_PRINCIPAL;
252
+ return count < allowance ? { accept: true } : { reject: "birth-quota-exceeded" };
253
+ }
254
+
255
+ /** The synthesized invariant's stable, deterministic id for a `.births()` directive. */
256
+ export function birthQuotaInvariantId(directiveId: string): string {
257
+ return `${directiveId}__birthQuota`;
258
+ }
259
+
260
+ /**
261
+ * Synthesize THE BIRTH-QUOTA workspace invariant for ONE directive (A2). `[]` unless the directive
262
+ * declares `.births()` (the era rule — a birth-free directive synthesizes nothing, byte-identical).
263
+ * Applied at the SAME three sites the precondition desugaring rides (`composeDomainModule`, the
264
+ * engine slice build, the openapi decls) so the manifest's `nomosInvariantGate` entry and the sealed
265
+ * engine's executable body can never disagree. The verdict reads the row LIST off the captured reads
266
+ * (`ctx.reads` — the snapshot ref-set is empty; ref-resolution cannot enumerate), the exact
267
+ * WorkspaceQuota shape.
268
+ */
269
+ export function synthesizeBirthQuotaInvariants(directive: {
270
+ id: string;
271
+ birthsChild?: boolean;
272
+ }): WorkspaceInvariantDecl[] {
273
+ if (directive.birthsChild !== true) return [];
274
+ return [
275
+ {
276
+ id: birthQuotaInvariantId(directive.id),
277
+ on: directive.id,
278
+ reads: () => [],
279
+ assert: (_snapshots, ctx) => {
280
+ // A `.births()` plan that planned NO birth (e.g. birthChild without a genesis) captures no
281
+ // meter reads and consumes no allowance — accept without judging (nothing was born).
282
+ const reads = (ctx?.reads ?? []) as {
283
+ queryId: string;
284
+ args: Record<string, string>;
285
+ result: unknown;
286
+ }[];
287
+ if (!reads.some((r) => r.queryId === birthRecordsByPrincipal.id)) return { accept: true };
288
+ return birthQuotaVerdict(reads, birthMeterSubject(ctx?.intent ?? {}));
289
+ },
290
+ __isWorkspaceInvariant: true as const,
291
+ },
292
+ ];
293
+ }
294
+
295
+ /** The meter aggregates every `.births()`-declaring domain auto-ensures (the RelationTuple courtesy). */
296
+ export const BIRTH_METER_AGGREGATES: readonly AggregateHandle[] = [
297
+ NomosBirthRecord,
298
+ BirthPolicy,
299
+ BirthGrant,
300
+ ] as unknown as readonly AggregateHandle[];
@@ -0,0 +1,67 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded
4
+ // QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /**
7
+ * THE BIRTH-RECORD SANCTIONING DECLARATION (metered birth, slice A1 — the id-mint-gate half).
8
+ *
9
+ * The auto-appended `NomosBirthRecord` event is an ENSURE at a deterministic id
10
+ * (`nomos-birth-record:<child>`). The kernel's id-mint gate admits an Ensure-INTRODUCED typed id
11
+ * ONLY through the sanctioned upsert-by-deterministic-key lane: the CERTIFIED manifest must declare
12
+ * the type as an `ensures` TARGET — i.e. the law must carry a directive whose marker is `Ensure`
13
+ * targeting `NomosBirthRecord` (exactly how `installDomain` sanctions `DomainInstallation`, or the
14
+ * governance `setBirthPolicy` sanctions `BirthPolicy`). A tenant `.births()` law writes the record
15
+ * by construction but authors NO such directive — so this module supplies it, injected at the ONE
16
+ * module-merge seam every plane shares (compose / engine slice / self-describe / compile openapi),
17
+ * keeping the compiled openapi and the lump's `describe()` byte-consistent.
18
+ *
19
+ * {@link nomosBirthRecordEnsure} is DELIBERATELY USELESS TO DISPATCH: its plan writes NOTHING (zero
20
+ * ops — it cannot forge a record), it exists purely so the law's manifest/USD carries the
21
+ * `Ensure → NomosBirthRecord` declaration the id-mint gate reads. Era-ruled like everything else in
22
+ * the meter: only a `.births()`-declaring domain gains it; every other law is byte-identical.
23
+ */
24
+ import { z } from "zod";
25
+ import { directive } from "./directive.js";
26
+ import type { Directive } from "./directive.js";
27
+ import {
28
+ NomosBirthRecord,
29
+ BirthPolicy,
30
+ BirthGrant,
31
+ } from "./birth_meter.js";
32
+
33
+ /**
34
+ * The sanctioning declaration — an `ensures(NomosBirthRecord)` directive whose plan emits ZERO ops.
35
+ * Its only load-bearing property is its manifest/USD presence (marker `Ensure`, target
36
+ * `NomosBirthRecord`), which opens the id-mint gate's declared-ensures lane for the auto-appended
37
+ * birth record. Dispatching it is a harmless no-op (no events, nothing folds).
38
+ */
39
+ export const nomosBirthRecordEnsure = directive("nomosBirthRecordEnsure")
40
+ .ensures(NomosBirthRecord)
41
+ .payload(z.object({}).strict())
42
+ .plan(() => []);
43
+
44
+ /**
45
+ * Inject the birth-meter's structural exports into a merged module-exports bag IFF it declares ≥1
46
+ * `.births()` directive (the RelationTuple "discoverable without re-exports" courtesy, at the one
47
+ * merge seam): the sanctioning {@link nomosBirthRecordEnsure} directive + the meter aggregates (so
48
+ * the engine registry / self-describe see the same surface the composed manifest carries). A
49
+ * birth-free bag is returned UNTOUCHED (the era rule — byte-identical). Existing keys are never
50
+ * overwritten (a law composing the framework governance surface keeps its own bindings).
51
+ */
52
+ export function withBirthMeterExports<M extends Record<string, unknown>>(mod: M): M {
53
+ const declaresBirth = Object.values(mod).some(
54
+ (v) =>
55
+ v !== null &&
56
+ typeof v === "object" &&
57
+ (v as { birthsChild?: boolean }).birthsChild === true &&
58
+ typeof (v as Directive<unknown>).plan === "function",
59
+ );
60
+ if (!declaresBirth) return mod;
61
+ const out: Record<string, unknown> = { ...mod };
62
+ if (!("nomosBirthRecordEnsure" in out)) out["nomosBirthRecordEnsure"] = nomosBirthRecordEnsure;
63
+ if (!("NomosBirthRecord" in out)) out["NomosBirthRecord"] = NomosBirthRecord;
64
+ if (!("BirthPolicy" in out)) out["BirthPolicy"] = BirthPolicy;
65
+ if (!("BirthGrant" in out)) out["BirthGrant"] = BirthGrant;
66
+ return out as M;
67
+ }
@@ -54,6 +54,12 @@ import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
54
54
  import type { WorkspaceTypeDecl } from "./workspace_type.js";
55
55
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
56
56
  import { synthesizePreconditionInvariants } from "./precondition.js";
57
+ import {
58
+ BIRTH_METER_AGGREGATES,
59
+ BIRTH_METER_QUERIES,
60
+ synthesizeBirthQuotaInvariants,
61
+ } from "./birth_meter.js";
62
+ import { withBirthMeterExports } from "./birth_meter_ensure.js";
57
63
  import type { RelationTypeDecl } from "./relation_schema.js";
58
64
  import { domainHash, emitManifestBytes } from "./manifest.js";
59
65
  import { collectValueObjectDescriptors } from "./value_object.js";
@@ -146,6 +152,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
146
152
  // `capability()` bundles expand into discoverable entries (the package plane's
147
153
  // merge point) — the quartet validation below then passes without re-exports.
148
154
  merged = expandCapabilityExports(merged);
155
+ // METERED BIRTH: inject the sanctioning `nomosBirthRecordEnsure` directive + meter aggregates
156
+ // into a `.births()`-declaring domain at the SAME seam the engine merge applies it — the
157
+ // Ensure→NomosBirthRecord manifest declaration is what admits the auto-appended record through
158
+ // the id-mint gate's declared-ensures lane. Birth-free domains are byte-identical (era rule).
159
+ merged = withBirthMeterExports(merged);
149
160
 
150
161
  const byId = new Map<string, AggregateHandle>();
151
162
  for (const a of [...aggregatesOf(merged), ...(spec.extraAggregates ?? [])]) byId.set(a.id, a);
@@ -158,6 +169,18 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
158
169
  if (!byId.has(RelationTuple.id) && directives.some((d) => d.aggregateId === RelationTuple.id)) {
159
170
  byId.set(RelationTuple.id, RelationTuple);
160
171
  }
172
+ // BIRTH-METER AUTO-ENSURE (metered birth, slice A — the same "discoverable without re-exports"
173
+ // courtesy as RelationTuple): a domain declaring ≥1 `.births()` directive gets the meter aggregates
174
+ // (NomosBirthRecord/BirthPolicy/BirthGrant) injected so the auto-record folds + projects, and the
175
+ // meter queries injected so the captured reads route/re-derive. Era-safe: only a birth-declaring
176
+ // domain gains them, so no existing (birth-free) manifest hash drifts — and a birth-declaring domain
177
+ // recompiled under this DSL moves hash ANYWAY (the meter IS its new era).
178
+ const declaresBirth = directives.some((d) => (d as { birthsChild?: boolean }).birthsChild === true);
179
+ if (declaresBirth) {
180
+ for (const a of BIRTH_METER_AGGREGATES) {
181
+ if (!byId.has(a.id)) byId.set(a.id, a);
182
+ }
183
+ }
161
184
  const aggregates = [...byId.values()];
162
185
  const directiveIds = new Set(directives.map((d) => d.id));
163
186
  for (const task of spec.impureCapabilities ?? []) {
@@ -188,6 +211,17 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
188
211
  // hand-authored `workspaceInvariant(...)` occupies — so downstream treats them identically. Era-safe:
189
212
  // a directive with no precondition synthesizes nothing (byte-identical manifest).
190
213
  for (const d of directives) workspaceInvariants.push(...synthesizePreconditionInvariants(d));
214
+ // METERED BIRTH (slice A2): synthesize THE BIRTH-QUOTA invariant for every `.births()` directive —
215
+ // the same desugaring lane as preconditions, so the manifest (`nomosInvariantGate`) and the sealed
216
+ // engine (which synthesizes from the SAME directives in `buildDomainSlice`) can never disagree.
217
+ for (const d of directives) workspaceInvariants.push(...synthesizeBirthQuotaInvariants(d));
218
+ // The meter's declared queries route like any exported query (read manifest + captured-read re-derive).
219
+ if (declaresBirth) {
220
+ const haveQuery = new Set(queries.map((q) => q.id));
221
+ for (const q of BIRTH_METER_QUERIES) {
222
+ if (!haveQuery.has(q.id)) queries.push(q);
223
+ }
224
+ }
191
225
  const relationTypes = [...(spec.relationTypes ?? [])];
192
226
  // FIRST-CLASS VALUE OBJECTS (value_object.ts): the deduped, sorted descriptors of every
193
227
  // named VO the domain's aggregates carry (transitive nested VOs included) — the input to
@@ -1835,6 +1835,13 @@ function emitDirective(
1835
1835
  const ctorParams = specs
1836
1836
  .map((s) => ` ${s.optional ? "" : "required "}this.${s.name},`)
1837
1837
  .join("\n");
1838
+ // Zero-field payload (e.g. a `.births()` sanctioning directive with an empty
1839
+ // object schema): `const X({});` / a dangling blank field block is INVALID Dart —
1840
+ // emit a plain `const X();` and an empty map literal instead.
1841
+ const hasFields = specs.length > 0;
1842
+ const ctorLines = hasFields
1843
+ ? [` const ${className}({`, ctorParams, ` });`]
1844
+ : [` const ${className}();`];
1838
1845
 
1839
1846
  // toPayloadJson: serialise only typed fields to data. No Event, no Hlc; the
1840
1847
  // GitHolon executes the installed domain plan. Optional
@@ -1870,12 +1877,9 @@ function emitDirective(
1870
1877
  ` /// The referential marker (creates/mutates/ensures/archives).`,
1871
1878
  ` static const String marker = '${d.marker}';`,
1872
1879
  ``,
1873
- fieldDecls,
1874
- ``,
1880
+ ...(hasFields ? [fieldDecls, ``] : []),
1875
1881
  ` /// ${docFirst}`,
1876
- ` const ${className}({`,
1877
- ctorParams,
1878
- ` });`,
1882
+ ...ctorLines,
1879
1883
  ``,
1880
1884
  ` @override`,
1881
1885
  ` String get domain => domainName;`,
@@ -1766,9 +1766,10 @@ function v2IdentityAndCleanupPreamble(opts: TsProofOptions, hashConst: string, n
1766
1766
  }
1767
1767
 
1768
1768
  /**
1769
- * THE V2 CATCH/FINALLY (G1, gap 4): retire the throwaway workspace on every exit via the OPEN lane (no
1770
- * Bearer — there is no host secret to present; a retire is a SIGNED governance offer in the real lane, but
1771
- * the open self-retire suffices to clean up a throwaway). PROOF_KEEP=1 keeps it.
1769
+ * THE V2 CATCH/FINALLY (G1, gap 4 · 0.34.1): retirement is a SIGNED governance offer — the open self-retire
1770
+ * lane is DEAD post-warrant-flip, so the proof itself never retires. Under `githolon proof --live`
1771
+ * (NOMOS_PROOF_MANAGED=1) the CLI created the workspace REGISTERED and retires it lawfully after exit;
1772
+ * standalone, the proof warns LOUDLY and names the manual command. PROOF_KEEP=1 keeps it.
1772
1773
  */
1773
1774
  function v2CatchFinally(): string[] {
1774
1775
  return [
@@ -1776,13 +1777,18 @@ function v2CatchFinally(): string[] {
1776
1777
  ` runError = e;`,
1777
1778
  ` console.error("✗ " + ((e as Error)?.message ?? e));`,
1778
1779
  `} finally {`,
1779
- ` // THE CLEANUP CONTRACT: never an orphaned cloud workspace. Retired on every exit path (open lane — no`,
1780
- ` // host secret exists post-G1); PROOF_KEEP=1 keeps it and prints the persist command.`,
1780
+ ` // THE CLEANUP CONTRACT: never a SILENTLY orphaned cloud workspace. Retirement is a SIGNED`,
1781
+ ` // governance offer (retireOwnWorkspace at the parent) — the old open-lane POST /retire is dead`,
1782
+ ` // post-warrant-flip. \`githolon proof --live\` orchestrates it (NOMOS_PROOF_MANAGED=1: it created`,
1783
+ ` // the workspace REGISTERED and retires it after this process exits). Run standalone, this proof`,
1784
+ ` // cannot lawfully retire (no signing key) — so it says so LOUDLY and names the exact command.`,
1781
1785
  ` if (born_ws && process.env.PROOF_KEEP === "1") {`,
1782
1786
  ` console.log(\`workspace \${WS} KEPT — retire it when done: githolon ws retire \${WS}\`);`,
1787
+ ` } else if (born_ws && process.env.NOMOS_PROOF_MANAGED === "1") {`,
1788
+ ` console.log(\` workspace \${WS} — retirement handled by githolon proof --live (signed retireOwnWorkspace)\`);`,
1783
1789
  ` } else if (born_ws) {`,
1784
- ` const rr = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/retire\`, { method: "POST", headers: { "x-nomos-principal": PRINCIPAL } }).then((x) => x.json()).catch(() => ({}));`,
1785
- ` console.log(\` retired \${WS}: \${(rr as { ok?: boolean }).ok === true}\`);`,
1790
+ ` console.error(\`⚠ THROWAWAY WORKSPACE NOT RETIRED: \${WS} (retirement is a signed governance offer this standalone proof cannot author)\`);`,
1791
+ ` console.error(\`⚠ retire it manually: githolon ws retire \${WS} (run the managed lane next time: githolon proof --live)\`);`,
1786
1792
  ` }`,
1787
1793
  ` process.exit(runError ? 1 : 0);`,
1788
1794
  `}`,
@@ -41,6 +41,7 @@
41
41
  * re-baking is a later, gated step. SYNTHETIC framework units only.
42
42
  */
43
43
  import type { Directive } from "./directive.js";
44
+ import { isNamespacedAggregate, READ_PROJECTION_DELIMITER } from "./read_projection_namespace.js";
44
45
  import type { AggregateHandle, AggregateInvariantFn } from "./aggregate.js";
45
46
  import type { ComposeOptions } from "./compose.js";
46
47
  import { flattenUsd, usdHash, type UsdDocument, type UsdPrim } from "./usd.js";
@@ -274,7 +275,17 @@ export function compileEngineModule(
274
275
  `id "${directiveId}".`,
275
276
  );
276
277
  }
277
- if (binding.directive.aggregateId !== prim.target) {
278
+ // READ-PROJECTION NAMESPACING: the USD law's target is the QUALIFIED type name for a
279
+ // non-global aggregate (`<domain>__<handleId>`); the executable binding keeps the bare
280
+ // handle id (the source literal). Accept either the bare id (global / pre-namespacing law)
281
+ // or the domain-qualified form (the prim's own layer domain — the 2nd path segment).
282
+ const aggGlobal = (binding.agg as { global?: boolean }).global;
283
+ const targetMatches = (aggId: string): boolean =>
284
+ prim.target === aggId ||
285
+ (isNamespacedAggregate(aggGlobal === undefined ? { id: aggId } : { id: aggId, global: aggGlobal }) &&
286
+ prim.target.endsWith(`${READ_PROJECTION_DELIMITER}${aggId}`) &&
287
+ prim.target.length > READ_PROJECTION_DELIMITER.length + aggId.length);
288
+ if (!targetMatches(binding.directive.aggregateId)) {
278
289
  throw new EngineCompileError(
279
290
  "binding-mismatch",
280
291
  directiveId,
@@ -282,7 +293,7 @@ export function compileEngineModule(
282
293
  `USD law's target is "${prim.target}".`,
283
294
  );
284
295
  }
285
- if (binding.agg.id !== prim.target) {
296
+ if (!targetMatches(binding.agg.id)) {
286
297
  throw new EngineCompileError(
287
298
  "binding-mismatch",
288
299
  directiveId,