@githolon/dsl 0.33.0 → 0.34.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.
@@ -54,8 +54,11 @@
54
54
  import { executeDirectiveToIntent } from "./wire_encode.js";
55
55
  import { expandCapabilityExports } from "./capability_exports.js";
56
56
  import { RelationTuple, __setDispatchContext } from "./framework/relations.js";
57
+ import { namespacedAggregateIds, qualifyReadProjectionType, READ_PROJECTION_DELIMITER } from "./read_projection_namespace.js";
57
58
  import { isRelationTypeDecl } from "./relation_schema.js";
58
59
  import { synthesizePreconditionInvariants } from "./precondition.js";
60
+ import { synthesizeBirthQuotaInvariants } from "./birth_meter.js";
61
+ import { withBirthMeterExports } from "./birth_meter_ensure.js";
59
62
  import { generateOpenApiFragment, mergeOpenApiFragments, type OpenApiDoc } from "./codegen_openapi.js";
60
63
  import type { QueryDecl } from "./query.js";
61
64
  import type { AnyCount } from "./count.js";
@@ -243,7 +246,10 @@ function mergeModules(mods: readonly DomainModuleExports[]): DomainModuleExports
243
246
  // `capability()` bundles expand into discoverable entries here — the engine
244
247
  // plane's ONE merge point, so the registry sees the task aggregate + quartet
245
248
  // without per-piece re-exports (capability_exports.ts).
246
- return expandCapabilityExports(merged);
249
+ // METERED BIRTH: a `.births()`-declaring domain gains the birth-meter structural exports
250
+ // (the sanctioning `nomosBirthRecordEnsure` + the meter aggregates) at this same seam, so
251
+ // the registry + `describe()` see the exact surface the composed manifest carries.
252
+ return withBirthMeterExports(expandCapabilityExports(merged));
247
253
  }
248
254
 
249
255
  /**
@@ -270,6 +276,13 @@ interface DomainSlice {
270
276
  * type's relations (governance/signer/read-visibility) are EXCLUDED so their bare objects stay bare.
271
277
  */
272
278
  qualifiableRelations: Set<string>;
279
+ /**
280
+ * READ-PROJECTION NAMESPACING classification: the NON-GLOBAL aggregate ids this domain declares —
281
+ * the set whose runtime `__type` stamp qualifies to `<domain>__<type>` (matching the qualified
282
+ * keys the compiler wrote into the read projection / prims / stable-ids). GLOBAL aggregates (the
283
+ * explicit framework allowlist / `{global:true}`) are excluded so their bare identity survives.
284
+ */
285
+ qualifiableAggregates: Set<string>;
273
286
  }
274
287
 
275
288
  /** Build one domain's slice from its merged module exports (the original scans). */
@@ -376,6 +389,13 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
376
389
  for (const directive of dirs.values()) {
377
390
  for (const w of synthesizePreconditionInvariants(directive)) wsInvariants.set(w.id, w);
378
391
  }
392
+ // METERED BIRTH (slice A2 — mirror of composeDomainModule): synthesize THE BIRTH-QUOTA invariant's
393
+ // executable body for every `.births()` directive, so the SEALED ENGINE registers the same
394
+ // `reads`/`assert` the manifest's `nomosInvariantGate` entry names. Byte-identical for a domain
395
+ // with no birth directive (synthesizes none — the era rule).
396
+ for (const directive of dirs.values()) {
397
+ for (const w of synthesizeBirthQuotaInvariants(directive)) wsInvariants.set(w.id, w);
398
+ }
379
399
 
380
400
  // ── RELATION-NAMESPACING classification — the LOCAL non-global `(type,relation)` set (by shape) ──
381
401
  // A bare `writeTuple({object:"<type>:<id>", relation})` auto-qualifies iff `<type>#<relation>` is here.
@@ -388,7 +408,11 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
388
408
  for (const rel of v.relations.keys()) qualifiableRelations.add(`${v.type}#${rel}`);
389
409
  }
390
410
 
391
- return { registry, deriveds, combineds, invariants, aggInvariants, wsInvariants, qualifiableRelations };
411
+ // ── READ-PROJECTION NAMESPACING classification — the LOCAL non-global aggregate ids (one shared
412
+ // classification with the compiler: `read_projection_namespace.ts`). ──
413
+ const qualifiableAggregates = namespacedAggregateIds([...aggs.values()]);
414
+
415
+ return { registry, deriveds, combineds, invariants, aggInvariants, wsInvariants, qualifiableRelations, qualifiableAggregates };
392
416
  }
393
417
 
394
418
  /**
@@ -413,9 +437,14 @@ export function collectEngineRouting(
413
437
  };
414
438
  for (const [domainName, mods] of Object.entries(domains)) {
415
439
  const slice = buildDomainSlice(mergeModules(mods));
416
- for (const type of slice.deriveds.keys()) add(derivedOf, type, domainName);
417
- for (const type of slice.combineds.keys()) add(combinedOf, type, domainName);
418
- for (const type of slice.aggInvariants.keys()) add(aggregateInvariantOf, type, domainName);
440
+ // Type-keyed dispatches arrive with the ROW's type — QUALIFIED for a namespaced aggregate (the
441
+ // runtime `__type` stamp), bare for a global one / a pre-namespacing row. Route BOTH keys so
442
+ // either era resolves to the declaring domain (the slice lookup strips the qualifier back off).
443
+ const routeKeys = (type: string): string[] =>
444
+ slice.qualifiableAggregates.has(type) ? [type, qualifyReadProjectionType(domainName, type)] : [type];
445
+ for (const type of slice.deriveds.keys()) for (const k of routeKeys(type)) add(derivedOf, k, domainName);
446
+ for (const type of slice.combineds.keys()) for (const k of routeKeys(type)) add(combinedOf, k, domainName);
447
+ for (const type of slice.aggInvariants.keys()) for (const k of routeKeys(type)) add(aggregateInvariantOf, k, domainName);
419
448
  for (const relationId of slice.invariants.keys()) add(relationOf, relationId, domainName);
420
449
  for (const wsInvariantId of slice.wsInvariants.keys()) add(workspaceInvariantOf, wsInvariantId, domainName);
421
450
  }
@@ -509,6 +538,24 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
509
538
  return domainNames;
510
539
  }
511
540
 
541
+ /**
542
+ * Resolve a TYPE-keyed registry entry for a dispatch that may carry a QUALIFIED type name
543
+ * (read-projection namespacing — the runtime `__type` stamp is `<domain>__<type>`): try the
544
+ * presented key, then the bare remainder after this domain's own qualifier. Registries stay
545
+ * keyed by the module's bare handle ids (the source of truth); this is the one un-qualify seam.
546
+ */
547
+ function typeKeyed<T>(map: Map<string, T> | undefined, domainName: string, type: string): T | undefined {
548
+ if (map === undefined) return undefined;
549
+ const direct = map.get(type);
550
+ if (direct !== undefined) return direct;
551
+ // Strip the domain qualifier at the LAST delimiter — bare type names never contain `.`,
552
+ // and the qualifier's domain half is mint-tag-safe-ENCODED (`_`→`-`), so a raw-domainName
553
+ // prefix compare would miss (`co2_platform` qualifies as `co2-platform.`).
554
+ void domainName;
555
+ const i = type.lastIndexOf(READ_PROJECTION_DELIMITER);
556
+ return i > 0 ? map.get(type.slice(i + READ_PROJECTION_DELIMITER.length)) : undefined;
557
+ }
558
+
512
559
  const REPORTS: Record<string, (actor: string) => EngineReport> = config.reports ?? {};
513
560
 
514
561
  // ── the five planReport dispatch branches (wire shapes are the host oracle's contract) ──
@@ -555,7 +602,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
555
602
  // Later domain wins (the Map.set fold order of the one flat registry).
556
603
  let body: AggregateInvariantFn | undefined;
557
604
  for (const domainName of domainsFor(routing.aggregateInvariantOf, aggregateType)) {
558
- const found = sliceOf(domainName)?.aggInvariants.get(aggregateType);
605
+ const found = typeKeyed(sliceOf(domainName)?.aggInvariants, domainName, aggregateType);
559
606
  if (found !== undefined) body = found;
560
607
  }
561
608
  // VACUOUS HOLDS: a type with NO declared invariant trivially holds — `{accept:true}`,
@@ -680,7 +727,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
680
727
  // Domain declaration order, then module export order — the eager fold order.
681
728
  const fields: DerivedDecl[] = [];
682
729
  for (const domainName of domainsFor(routing.derivedOf, ofType)) {
683
- fields.push(...(sliceOf(domainName)?.deriveds.get(ofType) ?? []));
730
+ fields.push(...(typeKeyed(sliceOf(domainName)?.deriveds, domainName, ofType) ?? []));
684
731
  }
685
732
  const prior = (job.priorState ?? {}) as Record<string, unknown>;
686
733
  const out: Record<string, unknown> = {};
@@ -705,7 +752,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
705
752
  // Domain declaration order, then module export order — the eager fold order.
706
753
  const fields: CombinedDecl[] = [];
707
754
  for (const domainName of domainsFor(routing.combinedOf, ofType)) {
708
- fields.push(...(sliceOf(domainName)?.combineds.get(ofType) ?? []));
755
+ fields.push(...(typeKeyed(sliceOf(domainName)?.combineds, domainName, ofType) ?? []));
709
756
  }
710
757
  const owner = (job.priorState ?? {}) as Record<string, unknown>;
711
758
  // Preferred: related rows keyed by combined field id (iteration-order independent);
@@ -822,7 +869,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
822
869
  // token qualifies explicitly. (`module.domain` === `intent.domain`.) Synchronous plan execution ⇒
823
870
  // cleared in the `finally`.
824
871
  const slice = sliceOf(domain)!;
825
- __setDispatchContext({ domain, qualifiable: slice.qualifiableRelations });
872
+ __setDispatchContext({ domain, qualifiable: slice.qualifiableRelations, qualifiableAggregates: slice.qualifiableAggregates });
826
873
  let wire: ReturnType<typeof executeDirectiveToIntent>;
827
874
  try {
828
875
  wire = executeDirectiveToIntent(entry.directive, entry.agg, intent.payload as never, ctx);
@@ -0,0 +1,125 @@
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
+ * framework/birth_meter — THE BIRTH-ALLOWANCE GOVERNANCE SURFACE (metered birth, slice A2/A3).
8
+ *
9
+ * The meter's STRUCTURE (record + queries + quota invariant + auto-capture) is auto-ensured on every
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:
12
+ *
13
+ * - {@link setBirthPolicy} — set/amend the workspace's default allowance (admin-gated).
14
+ * - {@link grantBirthAllowance} — a per-subject override (admin-gated; latest grant wins).
15
+ * - {@link seedBirthPolicy} — THE GENESIS SEED (A3, ungated like `seedGenesis`): the step a
16
+ * parent's birth recipe threads (`buildGenesisChain`'s `descendantPool`) so the child is BORN
17
+ * BOUNDED — the pool chains to root by induction (root bounds platforms, platforms bound
18
+ * homes, homes bound estates). Control-plane protected exactly like `seedGenesis`: the open
19
+ * session lane structurally refuses control-plane intents, and post-genesis writes on a
20
+ * warranted workspace require signed authorship.
21
+ *
22
+ * V1 BOUNDARY (deliberate, stated in `architecture/metered_birth_and_attested_reads.md`): each node
23
+ * enforces ITS OWN count-quota; cross-level pool CONSERVATION arithmetic (a node may not grant its
24
+ * children more than it holds) is v2.
25
+ */
26
+ import { z } from "zod";
27
+ import { instance } from "../aggregate.js";
28
+ import { directive } from "../directive.js";
29
+ import { set, type PlannedOp } from "../ops.js";
30
+ import {
31
+ BirthPolicy,
32
+ BirthGrant,
33
+ BIRTH_POLICY_ID,
34
+ birthGrantId,
35
+ } from "../birth_meter.js";
36
+
37
+ // Re-export the whole meter core so `@githolon/dsl` framework consumers get one import site.
38
+ export {
39
+ NomosBirthRecord,
40
+ BirthPolicy,
41
+ BirthGrant,
42
+ BIRTH_POLICY_ID,
43
+ NOMOS_BIRTH_RECORD_PREFIX,
44
+ birthRecordId,
45
+ birthGrantId,
46
+ birthRecordsByPrincipal,
47
+ birthPolicy,
48
+ birthGrantByPrincipal,
49
+ DEFAULT_MAX_BIRTHS_PER_PRINCIPAL,
50
+ birthQuotaVerdict,
51
+ birthQuotaInvariantId,
52
+ } from "../birth_meter.js";
53
+
54
+ /** The BirthPolicy singleton upsert ops (shared by the governed set and the genesis seed). */
55
+ function birthPolicyOps(p: { maxBirthsPerPrincipal: number; setBy: string; setAt: string }): PlannedOp[] {
56
+ const pol = instance(BirthPolicy, BIRTH_POLICY_ID);
57
+ return [
58
+ set(pol, "scope", "workspace"),
59
+ set(pol, "maxBirthsPerPrincipal", p.maxBirthsPerPrincipal),
60
+ set(pol, "setBy", p.setBy),
61
+ set(pol, "setAt", p.setAt),
62
+ ];
63
+ }
64
+
65
+ /**
66
+ * setBirthPolicy — set (or amend) THE WORKSPACE'S BIRTH ALLOWANCE DEFAULT. An Ensure on the
67
+ * law-state singleton (deterministic id — the `setCloudPolicy` shape), gated on the workspace's
68
+ * admin relation (the existing relation-gate idiom).
69
+ */
70
+ export const setBirthPolicy = directive("setBirthPolicy")
71
+ .ensures(BirthPolicy)
72
+ .payload(
73
+ z.object({
74
+ maxBirthsPerPrincipal: z.number().int().positive(),
75
+ setBy: z.string().min(1),
76
+ /** Caller-stamped (the timestamp doctrine). */
77
+ setAt: z.string().min(1),
78
+ }),
79
+ )
80
+ .plan((p) => birthPolicyOps(p))
81
+ .requires("admin");
82
+
83
+ /**
84
+ * seedBirthPolicy — THE GENESIS SEED (A3). Identical ops to {@link setBirthPolicy} but UNGATED,
85
+ * because it runs INSIDE a child's genesis recipe (before any admin relation exists) — exactly the
86
+ * `seedGenesis` idiom: a birth-door control-plane write the open session lane structurally refuses,
87
+ * and which post-genesis signed-authorship protects on every warranted workspace.
88
+ */
89
+ export const seedBirthPolicy = directive("seedBirthPolicy")
90
+ .ensures(BirthPolicy)
91
+ .payload(
92
+ z.object({
93
+ maxBirthsPerPrincipal: z.number().int().positive(),
94
+ setBy: z.string().min(1),
95
+ setAt: z.string().min(1),
96
+ }),
97
+ )
98
+ .plan((p) => birthPolicyOps(p));
99
+
100
+ /**
101
+ * grantBirthAllowance — a PER-SUBJECT allowance override (the `grantQuota` shape): the LATEST grant
102
+ * (max `grantedAt`) for a subject is the allowance in force, overriding the BirthPolicy default.
103
+ * Admin-gated via the existing relation-gate idiom.
104
+ */
105
+ export const grantBirthAllowance = directive("grantBirthAllowance")
106
+ .ensures(BirthGrant)
107
+ .payload(
108
+ z.object({
109
+ principal: z.string().min(1),
110
+ maxBirths: z.number().int().positive(),
111
+ grantedBy: z.string().min(1),
112
+ /** epoch-ms — the recency tiebreak (latest grant wins), caller-stamped. */
113
+ grantedAt: z.number().int().nonnegative(),
114
+ }),
115
+ )
116
+ .plan((p) => {
117
+ const g = instance(BirthGrant, birthGrantId(p.principal));
118
+ return [
119
+ set(g, "principal", p.principal),
120
+ set(g, "maxBirths", p.maxBirths),
121
+ set(g, "grantedBy", p.grantedBy),
122
+ set(g, "grantedAt", p.grantedAt),
123
+ ];
124
+ })
125
+ .requires("admin");
@@ -165,7 +165,10 @@ export function impureCapabilityAggregate<
165
165
  const Id extends string,
166
166
  P extends Record<string, Field>,
167
167
  >(id: Id, params: P): AggregateHandle<Id, typeof IMPURE_CAPABILITY_ENVELOPE & P> {
168
- return aggregate(id, { ...IMPURE_CAPABILITY_ENVELOPE, ...params });
168
+ // GLOBAL (read-projection namespacing opt-out): the capability lane addresses the envelope
169
+ // by its BARE declared id (`nomosCapabilities.aggregateId` → the obligation rows the holon
170
+ // surfaces), so the envelope's type name must not be domain-qualified.
171
+ return aggregate(id, { ...IMPURE_CAPABILITY_ENVELOPE, ...params }, { global: true });
169
172
  }
170
173
 
171
174
  /**
@@ -235,7 +238,7 @@ const deadLetterSchema = z.object({
235
238
  * handle; the envelope handle just types the envelope subset for `set()`).
236
239
  */
237
240
  function envelopeInstance(aggregateId: string, requestId: string) {
238
- const envelopeHandle = aggregate(aggregateId, IMPURE_CAPABILITY_ENVELOPE);
241
+ const envelopeHandle = aggregate(aggregateId, IMPURE_CAPABILITY_ENVELOPE, { global: true });
239
242
  return instance(envelopeHandle, impureCapabilityInstanceId(requestId));
240
243
  }
241
244
 
@@ -188,7 +188,10 @@ export function seedPlatformAdmin(opts: SeedPlatformAdminOptions): PlatformAdmin
188
188
  relation: t.string().merge(Lww),
189
189
  /** ISO-8601, caller-stamped (determinism — no clock in the plan). */
190
190
  seededAt: t.string().merge(Lww),
191
- });
191
+ // GLOBAL (read-projection namespacing opt-out): a dynamically-named framework aggregate —
192
+ // platform governance law deploys under differing domain keys (root vs each platform), and the
193
+ // founder singleton's bare identity must stay stable across them.
194
+ }, { global: true });
192
195
 
193
196
  /**
194
197
  * GENESIS SEED 1 — stamp the founder singleton at the fixed id. Authored by the platform's birth/genesis
@@ -48,6 +48,7 @@ import { set, setEntry, withMarker, type PlannedOp } from "../ops.js";
48
48
  import { query } from "../query.js";
49
49
  import { endorsementOf, type AttributionPrincipal, type EndorsedAuthority } from "../principal_evidence.js";
50
50
  import { isRelationObjectToken, qualifyRelationType, type RelationObjectToken } from "../relation_schema.js";
51
+ import { qualifyReadProjectionType } from "../read_projection_namespace.js";
51
52
 
52
53
  // ─────────────────────────────── the dispatch context (relation-namespacing) ───────────────────────────────
53
54
  //
@@ -71,6 +72,12 @@ interface DispatchContext {
71
72
  readonly domain: string;
72
73
  /** `<type>#<relation>` for every LOCAL non-global authored relation of this domain — the qualify set. */
73
74
  readonly qualifiable: ReadonlySet<string>;
75
+ /**
76
+ * READ-PROJECTION NAMESPACING: the NON-GLOBAL aggregate ids of the dispatching domain — the set
77
+ * whose event `__type` stamp qualifies to `<domain>__<type>` (matching the compiler's qualified
78
+ * read-projection / prim / stable-id keys). Absent (probe lanes) ⇒ no aggregate qualification.
79
+ */
80
+ readonly qualifiableAggregates?: ReadonlySet<string>;
74
81
  }
75
82
 
76
83
  const EMPTY_QUALIFIABLE: ReadonlySet<string> = new Set<string>();
@@ -90,6 +97,18 @@ export function __setDispatchDomain(domain: string | undefined): void {
90
97
  _ctx = domain === undefined ? undefined : { domain, qualifiable: EMPTY_QUALIFIABLE };
91
98
  }
92
99
 
100
+ /**
101
+ * READ-PROJECTION NAMESPACING (the aggregate-type mirror of `resolveTupleObject`): qualify an event's
102
+ * aggregate TYPE by the dispatching domain IFF the domain declares it as a NON-GLOBAL aggregate — so the
103
+ * `__type` a plan's ops stamp equals the qualified schema key the compiler wrote (`usd.ts`). A global /
104
+ * foreign / unknown type, or a dispatch outside a plan (codegen probes), passes through BARE. Called by
105
+ * `wire_encode.ts::executeDirectiveToIntent` at the one event-emission point.
106
+ */
107
+ export function __qualifyAggregateType(type: string): string {
108
+ if (_ctx === undefined || _ctx.qualifiableAggregates === undefined) return type;
109
+ return _ctx.qualifiableAggregates.has(type) ? qualifyReadProjectionType(_ctx.domain, type) : type;
110
+ }
111
+
93
112
  /** Resolve a `writeTuple`/`removeTuple` object for a given relation (token → qualify; string → classify). */
94
113
  function resolveTupleObject(object: string | RelationObjectToken, relation: string): string {
95
114
  // A TOKEN is explicit sugar — always qualify its type by the dispatching domain.
@@ -482,6 +482,13 @@ export function buildGenesisChain(p: {
482
482
  * chains arm the signature requirement.
483
483
  */
484
484
  requiresWarrant?: boolean;
485
+ /**
486
+ * METERED BIRTH (slice A3 — the descendant budget). When present, the child's genesis includes a
487
+ * `seedBirthPolicy` step so it is BORN BOUNDED (its own default birth allowance per subject).
488
+ * OPT-IN: thread it only when the `lawHash` the order names carries `seedBirthPolicy` (a governance
489
+ * package compiled under the metered-birth DSL) — see the poolSteps note below.
490
+ */
491
+ descendantPool?: number;
485
492
  }): string {
486
493
  const founder = "user:" + p.principal;
487
494
  // THE ORDER-DECLARED LAW INSTALL (G3): the child installs the EXACT law the order names — bytes are NOT in
@@ -508,8 +515,29 @@ export function buildGenesisChain(p: {
508
515
  authorizationHash: "",
509
516
  parentHead: "",
510
517
  });
518
+ // METERED BIRTH (slice A3 — THE POOL CHAINS TO ROOT): when the order carries a `descendantPool`, the
519
+ // child is BORN BOUNDED — a `seedBirthPolicy` genesis step writes the child's own BirthPolicy singleton
520
+ // (its default birth allowance) BEFORE the warrant lands, exactly like every other genesis seed (the
521
+ // still-warrant-free prefix runs unsigned). Root bounds platforms (this pool), platforms bound homes,
522
+ // homes bound estates — the tree meters by induction, with zero cross-workspace reads on any gate.
523
+ // OPT-IN (deliberate): the step folds through the CHILD'S OWN gate under the law hash the order names,
524
+ // so it may only be threaded when that law carries `seedBirthPolicy` (a law compiled under this DSL) —
525
+ // an unconditional default would refuse the genesis of every birth pinning an OLDER law hash. V1
526
+ // BOUNDARY: each node enforces its own count-quota; cross-level pool CONSERVATION arithmetic is v2.
527
+ const poolSteps =
528
+ p.descendantPool !== undefined
529
+ ? [
530
+ {
531
+ domain: "workspaces",
532
+ directiveId: "seedBirthPolicy",
533
+ actor: founder,
534
+ payload: { maxBirthsPerPrincipal: p.descendantPool, setBy: p.principal, setAt: p.bornAt },
535
+ },
536
+ ]
537
+ : [];
511
538
  return JSON.stringify([
512
539
  ...installSteps,
540
+ ...poolSteps,
513
541
  // THE WARRANT-FLIP (THE GOVERNANCE DEFAULT): a governance birth is born WARRANTED by default — a REAL
514
542
  // self-id (the workspace name) so signed control-plane authorship is enforced on every lane. The ONLY
515
543
  // way to stay on the legacy host-stamped lane ("" self-id, byte-identical to a pre-VA chain) is an
@@ -926,6 +954,11 @@ export const birthChild = directive("birthChild")
926
954
  * is born WARRANTED (signed authorship enforced on every lane). Absent/false ⇒ the legacy
927
955
  * host-stamped lane (byte-identical to a pre-VA birth). Only meaningful with a genesis. */
928
956
  requiresWarrant: z.boolean().optional(),
957
+ /** METERED BIRTH (slice A3): the child's own descendant budget — when present, the genesis
958
+ * recipe seeds the child's BirthPolicy (`seedBirthPolicy`) so it is BORN BOUNDED. OPT-IN:
959
+ * only pass it when the `lawHash` the order names carries `seedBirthPolicy` (a governance
960
+ * package compiled under the metered-birth DSL). Only meaningful with a genesis. */
961
+ descendantPool: z.number().int().positive().optional(),
929
962
  })
930
963
  // A genesis birth (a platform) must carry a principal AND a bornAt — it owns a subtree, a lineage
931
964
  // anchor, and a law-authored genesis chain whose warrant is stamped at bornAt.
@@ -946,7 +979,7 @@ export const birthChild = directive("birthChild")
946
979
  // EXPLICITLY opts out (`requiresWarrant === false`). Thread the flag through ONLY when the request set
947
980
  // it — undefined falls to buildGenesisChain's warranted default; an explicit `false` rides the compat
948
981
  // hatch (an era-safe legacy re-build).
949
- 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 } : {}) });
982
+ 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 } : {}), ...(p.descendantPool !== undefined ? { descendantPool: p.descendantPool } : {}) });
950
983
  // THE GENESIS RECIPE IS ALSO A LINEAGE/AUDIT FACT on the parent's chain (the parent records what it
951
984
  // ORDERED — law naming law, legitimate). The kernel does NOT read this field to detect the birth.
952
985
  rec.set("genesisChain", genesisChain);
@@ -1645,3 +1678,11 @@ export const workspaceQuotaBirth = workspaceInvariant("WorkspaceQuotaBirth")
1645
1678
  .assert((_snapshots, ctx) =>
1646
1679
  quotaVerdict(ctx?.reads ?? [], String((ctx?.intent ?? {}).principal ?? "")),
1647
1680
  );
1681
+
1682
+ // ── METERED BIRTH (slice A — the allowance governance in the workspaces domain) ─────────────────────
1683
+ // Re-exported here so any governance package composed from THIS module carries the birth-allowance
1684
+ // surface: `setBirthPolicy`/`grantBirthAllowance` (admin-gated) and `seedBirthPolicy` — the A3 genesis
1685
+ // seed `buildGenesisChain`'s `descendantPool` step dispatches as `workspaces/seedBirthPolicy` (it must
1686
+ // live in the SAME domain key the genesis recipe names). The meter's structural half (record + quota)
1687
+ // is auto-ensured by `.births()` — see `../birth_meter.ts`.
1688
+ export { setBirthPolicy, seedBirthPolicy, grantBirthAllowance, BirthPolicy, BirthGrant, NomosBirthRecord, birthRecordsByPrincipal, birthPolicy, birthGrantByPrincipal } from "./birth_meter.js";
package/src/index.ts CHANGED
@@ -58,6 +58,7 @@ export {
58
58
  type GeoPointValue,
59
59
  type GeoLineStringValue,
60
60
  type GeoPolygonValue,
61
+ type ValueObjectUsage,
61
62
  } from "./value_object.js";
62
63
  export {
63
64
  type PrincipalEvidence,
@@ -331,6 +332,28 @@ export {
331
332
  birthAttestation,
332
333
  parentAttestedBirth,
333
334
  } from "./framework/home.js";
335
+ // METERED BIRTH (slice A — architecture/metered_birth_and_attested_reads.md): the structural birth
336
+ // record + the parent-local birth quota + the allowance governance surface. The record/quota STRUCTURE
337
+ // is auto-ensured on every `.births()` law (zero tenant code); these exports are the GOVERNANCE half a
338
+ // tenant composes in to set/override allowances, plus the constants/predicates tests and tools read.
339
+ export {
340
+ NomosBirthRecord,
341
+ BirthPolicy,
342
+ BirthGrant,
343
+ BIRTH_POLICY_ID,
344
+ NOMOS_BIRTH_RECORD_PREFIX,
345
+ birthRecordId,
346
+ birthGrantId,
347
+ birthRecordsByPrincipal,
348
+ birthPolicy,
349
+ birthGrantByPrincipal,
350
+ DEFAULT_MAX_BIRTHS_PER_PRINCIPAL,
351
+ birthQuotaVerdict,
352
+ birthQuotaInvariantId,
353
+ setBirthPolicy,
354
+ seedBirthPolicy,
355
+ grantBirthAllowance,
356
+ } from "./framework/birth_meter.js";
334
357
  // THE PLATFORM FOUNDING-ADMIN BOOTSTRAP (the platform analog of seedHomeCreator) — a tenant-importable
335
358
  // factory that seats a tenant-named governance relation for a write-once, author-bound founder at genesis.
336
359
  export {
@@ -457,6 +480,17 @@ export {
457
480
  type AllowedSubject,
458
481
  type RelationCaps,
459
482
  } from "./relation_schema.js";
483
+
484
+ // READ-PROJECTION (aggregate-type) NAMESPACING — the read-layer mirror of the relation namespacing:
485
+ // the qualifier, the deliberate framework GLOBAL allowlist, and the classification the compiler +
486
+ // sealed engine share (`aggregate(id, fields, { global: true })` is the dynamic opt-out).
487
+ export {
488
+ READ_PROJECTION_DELIMITER,
489
+ GLOBAL_READ_PROJECTION_AGGREGATE_IDS,
490
+ qualifyReadProjectionType,
491
+ isNamespacedAggregate,
492
+ namespacedAggregateIds,
493
+ } from "./read_projection_namespace.js";
460
494
  // SECURE-BY-DEFAULT SUGAR over the relation schema — plain-English combinators, audiences, presets.
461
495
  // Pure sugar: every export here BUILDS the existing Rewrite/RelationTypeDecl AST (no new wire shape).
462
496
  export {
package/src/read.ts CHANGED
@@ -74,6 +74,28 @@ export function read<T = unknown>(query: QueryDecl, args: Record<string, string>
74
74
  return result;
75
75
  }
76
76
 
77
+ /**
78
+ * THE BIRTH-METER CAPTURE (metered birth, slice A2 — library-internal). Identical semantics to
79
+ * {@link read} — serve the declared query live off `nomos.read` + record `{queryId, args, result}`
80
+ * into the SAME per-dispatch footprint — but with a DISTINCT refusal string, deliberately NOT the
81
+ * {@link CAPTURED_READ_LUMP_MARKER}: the marker greps for TENANT-authored `read()` calls that lack a
82
+ * declaration, whereas this door is only ever invoked by the `.births()` lane (which auto-DECLARES
83
+ * its three queries on the directive), so a lump that merely bundles it must not trip the read-free
84
+ * compile gate. A `.births()` dispatch on a host with no `nomos.read` still halts NAMED (fail-closed).
85
+ */
86
+ export function __captureBirthMeterRead(query: QueryDecl, args: Record<string, string>): void {
87
+ const h = host() as { read?: unknown } | undefined;
88
+ if (typeof h?.read !== "function") {
89
+ throw new Error(
90
+ `birth-meter read('${query.id}'): this host provides no nomos.read port — a .births() ` +
91
+ `dispatch captures its quota reads through the engine/client runtime (tests: stub ` +
92
+ `globalThis.nomos.read).`,
93
+ );
94
+ }
95
+ const result = host().read(query.id, args);
96
+ footprint.push({ queryId: query.id, args, result });
97
+ }
98
+
77
99
  /**
78
100
  * The compile gate's lump sentinel — the EXACT substring of the refusal `read()` throws above.
79
101
  * It exists in a bundled engine lump iff `read` itself survived tree-shaking, i.e. iff some
@@ -0,0 +1,117 @@
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 · SQLite projection · embedded QuickJS engine}.
4
+ //
5
+ // COMPILE-TIME DIVERGENCE GUARD for SHARED read-projection keys — the read-layer
6
+ // sibling of `relation_composition.ts` (the scalpel).
7
+ //
8
+ // THE MODEL. The kernel's installed-read-manifest accumulator
9
+ // (`wasm_git_holon.rs::ReadProjectionAccumulator`) UNIONS every active install's
10
+ // `nomosReadProjection` fragment BY KEY — `aggregateFieldKinds` by aggregate TYPE,
11
+ // `queries`/`counts`/`spatials`/`sums`/`mins`/`maxes`/`orderedReads` by declared `id`,
12
+ // `deriveds`/`combineds` by `"<of>\0<id>"` — and (correctly, fail-closed) REFUSES a
13
+ // key declared twice with DIVERGENT shapes: "divergent read projection declaration
14
+ // for key <k>". At RUNTIME that refusal WEDGES the workspace's read plane.
15
+ //
16
+ // Read-projection NAMESPACING (`read_projection_namespace.ts`) makes a NON-GLOBAL
17
+ // aggregate's keys domain-qualified, so tenant domains never share a key. What CAN
18
+ // still share a key inside one package: GLOBAL framework aggregates declared by ≥2
19
+ // domains (byte-identical by construction — same framework version — but a version
20
+ // skew diverges), and same-named declared reads across domains. This guard turns the
21
+ // runtime wedge into a precise COMPILE refusal: identical shared declarations PASS
22
+ // (the kernel dedupes them), divergent ones REFUSE with the key and each domain's
23
+ // shape. Validation only — a conflict-free package is returned untouched (era rule).
24
+
25
+ import type { UsdLayer } from "./usd.js";
26
+ import { domainOfLayerPath } from "./relation_composition.js";
27
+
28
+ /** One shared-key divergence, gathered for the scalpel message. */
29
+ interface ReadProjectionConflict {
30
+ /** The accumulator section (`aggregateFieldKinds`, `queries`, …). */
31
+ readonly section: string;
32
+ /** The colliding accumulator key (the kernel's exact key). */
33
+ readonly key: string;
34
+ /** Each contributing domain's declaration, in package (weak→strong) order. */
35
+ readonly opinions: { readonly domain: string; readonly shape: string }[];
36
+ }
37
+
38
+ function scalpelMessage(conflicts: readonly ReadProjectionConflict[]): string {
39
+ const lines: string[] = [
40
+ `Refusing to compose the package: shared read-projection key(s) carry DIVERGENT declarations ` +
41
+ `across domains — the kernel's installed-read-manifest accumulator refuses exactly this at ` +
42
+ `SERVE time ("divergent read projection declaration for key …"), wedging the workspace's ` +
43
+ `read plane. Fail-closed compile refusal, not a runtime wedge.`,
44
+ ``,
45
+ ];
46
+ for (const c of conflicts) {
47
+ lines.push(
48
+ ` read projection "${c.section}" key "${c.key.replace("\0", " · ")}" — ${c.opinions.length} divergent declarations:`,
49
+ );
50
+ const w = Math.max(...c.opinions.map((o) => o.domain.length));
51
+ for (const o of c.opinions) lines.push(` · ${o.domain.padEnd(w)} ${o.shape}`);
52
+ }
53
+ lines.push(
54
+ ``,
55
+ `Fix: give the shared key ONE canonical declaration (the domains must agree, byte-for-byte — ` +
56
+ `for a framework aggregate that means the SAME framework version in every domain), or make ` +
57
+ `the declarations DISTINCT (a non-global aggregate auto-qualifies by its owning domain; a ` +
58
+ `declared read can simply take a different id).`,
59
+ );
60
+ return lines.join("\n");
61
+ }
62
+
63
+ type Fragment = NonNullable<UsdLayer["nomosReadProjection"]>;
64
+
65
+ /** The accumulator sections keyed by declared `id` (mirrors `merge_projection_array_by_id`). */
66
+ const BY_ID_SECTIONS = ["queries", "counts", "spatials", "sums", "mins", "maxes", "orderedReads"] as const;
67
+ /** The sections keyed by `"<of>\0<id>"` (mirrors `merge_projection_array_by_type_and_id`). */
68
+ const BY_TYPE_AND_ID_SECTIONS = ["deriveds", "combineds"] as const;
69
+
70
+ /**
71
+ * Guard the package's layers against a shared-read-projection-key divergence — the EXACT key
72
+ * semantics the kernel accumulator merges by. Identical shared declarations pass (dedupe);
73
+ * divergent ones throw the scalpel. Deterministic (sorted keys/sections). Returns the layers
74
+ * UNTOUCHED (validation only).
75
+ *
76
+ * @throws a scalpel `Error` naming the section, key, and each domain's declaration on divergence.
77
+ */
78
+ export function guardSharedReadProjectionKeys(layers: readonly UsdLayer[]): readonly UsdLayer[] {
79
+ // section → key → the contributing {domain, canonical JSON} opinions, in package order.
80
+ const seen = new Map<string, Map<string, { domain: string; shape: string }[]>>();
81
+ const note = (section: string, key: string, domain: string, value: unknown): void => {
82
+ const bySection = seen.get(section) ?? new Map<string, { domain: string; shape: string }[]>();
83
+ seen.set(section, bySection);
84
+ const arr = bySection.get(key) ?? [];
85
+ arr.push({ domain, shape: JSON.stringify(value) });
86
+ bySection.set(key, arr);
87
+ };
88
+
89
+ for (const layer of layers) {
90
+ const frag: Fragment | undefined = layer.nomosReadProjection;
91
+ if (frag === undefined) continue;
92
+ const domain = domainOfLayerPath(layer.path);
93
+ for (const [ty, schema] of Object.entries(frag.aggregateFieldKinds ?? {})) {
94
+ note("aggregateFieldKinds", ty, domain, schema);
95
+ }
96
+ const bag = frag as unknown as Record<string, readonly { id: string; of?: string }[] | undefined>;
97
+ for (const section of BY_ID_SECTIONS) {
98
+ for (const item of bag[section] ?? []) note(section, item.id, domain, item);
99
+ }
100
+ for (const section of BY_TYPE_AND_ID_SECTIONS) {
101
+ for (const item of bag[section] ?? []) note(section, `${item.of}\0${item.id}`, domain, item);
102
+ }
103
+ }
104
+
105
+ const conflicts: ReadProjectionConflict[] = [];
106
+ for (const section of [...seen.keys()].sort()) {
107
+ const bySection = seen.get(section)!;
108
+ for (const key of [...bySection.keys()].sort()) {
109
+ const opinions = bySection.get(key)!;
110
+ if (opinions.length < 2) continue;
111
+ const distinct = new Set(opinions.map((o) => o.shape));
112
+ if (distinct.size > 1) conflicts.push({ section, key, opinions });
113
+ }
114
+ }
115
+ if (conflicts.length > 0) throw new Error(scalpelMessage(conflicts));
116
+ return layers;
117
+ }