@githolon/dsl 0.32.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.
@@ -14,7 +14,111 @@
14
14
  /// All extend [NomosBridgeError] so existing `catch (NomosBridgeError)` handlers still catch them.
15
15
  library;
16
16
 
17
- import 'webview_bridge.dart' show NomosBridgeError;
17
+ import 'webview_bridge.dart' show NomosBridgeError, NomosErrorInfo;
18
+
19
+ /// Mint the MOST SPECIFIC typed error for a structured refusal — the one construction point the bridge
20
+ /// uses everywhere it surfaces an `errorInfo`. A `nomos.custody.*` code becomes a [CustodyError] (typed,
21
+ /// recoverable, carrying the eviction/escalation decision fields); everything else stays the generic
22
+ /// [NomosBridgeError]. Apps `switch` on the type / `code`, never scrape prose.
23
+ NomosBridgeError nomosErrorFromInfo(NomosErrorInfo info) {
24
+ if (info.code.startsWith('nomos.custody.')) return CustodyError.fromInfo(info);
25
+ return NomosBridgeError(info.message, info: info);
26
+ }
27
+
28
+ /// The custody phase a [CustodyError] was raised in.
29
+ enum CustodyPhase { fold, author, fetch, unknown }
30
+
31
+ /// Where the incomplete custody came from — the axis of the recovery decision.
32
+ enum CustodySource { localSnapshot, cloudPack, unknown }
33
+
34
+ /// A TYPED, RECOVERABLE CUSTODY ERROR — local custody (the clone/snapshot this holon folds from) is
35
+ /// missing history the operation needed (`nomos.custody.shallow_insufficient` /
36
+ /// `nomos.custody.missing_object`), and the in-client gap-fill repair could not complete it.
37
+ ///
38
+ /// This replaces pattern-matching `shallow-insufficient` out of a stringly [NomosBridgeError]: the app
39
+ /// switches on [code] and decides from the structured fields whether to EVICT LOCAL CUSTODY
40
+ /// ([recommendedAction] == `quarantineLocalSnapshotAndReclone` — quarantine the snapshot, `inspectSnapshot`
41
+ /// / `recoverFrom`, reconnect fresh) or ESCALATE CLOUD CUSTODY (`refetchFullHistoryAndRetry` — the cloud
42
+ /// must serve the full chain). Trace IDs ride [NomosBridgeError.info.trace] as on every bridge error.
43
+ class CustodyError extends NomosBridgeError {
44
+ /// The stable machine code, e.g. `nomos.custody.shallow_insufficient`.
45
+ final String code;
46
+
47
+ /// The workspace whose custody is incomplete.
48
+ final String? workspace;
49
+
50
+ /// The ref being folded (e.g. `refs/heads/main`).
51
+ final String? ref;
52
+
53
+ /// The ref's head commit at the time of the fault, when resolvable.
54
+ final String? head;
55
+
56
+ /// The missing object id the fold stopped at, when the kernel named one.
57
+ final String? missingObject;
58
+
59
+ /// The operation phase that hit the gap: fold (read), author (offer), fetch (the repair itself).
60
+ final CustodyPhase phase;
61
+
62
+ /// Provenance of the incomplete custody: a restored local snapshot, or a cloud-served pack.
63
+ final CustodySource source;
64
+
65
+ /// True when a known recovery lane exists (it always does for custody gaps — see [recommendedAction]).
66
+ final bool recoverable;
67
+
68
+ /// The named remedy: `quarantineLocalSnapshotAndReclone` | `refetchFullHistoryAndRetry`.
69
+ final String recommendedAction;
70
+
71
+ CustodyError({
72
+ required this.code,
73
+ required String message,
74
+ this.workspace,
75
+ this.ref,
76
+ this.head,
77
+ this.missingObject,
78
+ this.phase = CustodyPhase.unknown,
79
+ this.source = CustodySource.unknown,
80
+ this.recoverable = true,
81
+ this.recommendedAction = 'refetchFullHistoryAndRetry',
82
+ NomosErrorInfo? info,
83
+ }) : super(message, info: info);
84
+
85
+ /// Decode the structured custody payload the client attaches (`nomos.custody.*` attributes).
86
+ factory CustodyError.fromInfo(NomosErrorInfo info) {
87
+ String? attr(String name) {
88
+ final v = info.attributes['nomos.custody.$name'] ?? info.attributes[name];
89
+ return v?.toString();
90
+ }
91
+
92
+ return CustodyError(
93
+ code: info.code,
94
+ message: info.message,
95
+ workspace: info.workspace,
96
+ ref: attr('ref'),
97
+ head: attr('head'),
98
+ missingObject: attr('missing_object') ?? attr('missingObject'),
99
+ phase: CustodyPhase.values.firstWhere(
100
+ (p) => p.name == attr('phase'),
101
+ orElse: () => CustodyPhase.unknown,
102
+ ),
103
+ source: CustodySource.values.firstWhere(
104
+ (s) => s.name == attr('source'),
105
+ orElse: () => CustodySource.unknown,
106
+ ),
107
+ recoverable: attr('recoverable') != 'false',
108
+ recommendedAction: attr('recommended_action') ?? attr('recommendedAction') ?? 'refetchFullHistoryAndRetry',
109
+ info: info,
110
+ );
111
+ }
112
+
113
+ /// True when the remedy is to quarantine the local snapshot and re-clone (evict local custody).
114
+ bool get shouldEvictLocalCustody => recommendedAction == 'quarantineLocalSnapshotAndReclone';
115
+
116
+ @override
117
+ String toString() =>
118
+ 'CustodyError($code${workspace != null ? ', workspace: $workspace' : ''}'
119
+ '${ref != null ? ', ref: $ref' : ''}${missingObject != null ? ', missing: $missingObject' : ''}'
120
+ ', phase: ${phase.name}, source: ${source.name}, recommendedAction: $recommendedAction): $message';
121
+ }
18
122
 
19
123
  /// The auth-provider access token the cloud was given was missing, malformed, expired, or rejected — the
20
124
  /// cloud answered 401 / "invalid token" on a lane that requires a verified principal (every birth does).
@@ -17,6 +17,8 @@ library;
17
17
  import 'dart:async';
18
18
  import 'dart:math';
19
19
 
20
+ import 'errors.dart' show nomosErrorFromInfo;
21
+
20
22
  /// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
21
23
  const int kWebviewBridgeVersion = 1;
22
24
 
@@ -336,7 +338,7 @@ class NomosBridge {
336
338
  final info = NomosErrorInfo.fromBridgeMessage(m,
337
339
  fallbackMessage: 'the watch read failed',
338
340
  fallbackOperation: 'watch');
339
- ctrl.addError(NomosBridgeError(info.message, info: info));
341
+ ctrl.addError(nomosErrorFromInfo(info));
340
342
  } else {
341
343
  ctrl.add((m['rows'] as List?) ?? const []);
342
344
  }
@@ -349,7 +351,7 @@ class NomosBridge {
349
351
  final info = NomosErrorInfo.fromBridgeMessage(m,
350
352
  fallbackMessage: 'the compat watch failed',
351
353
  fallbackOperation: 'watchCompatibility');
352
- cctrl.addError(NomosBridgeError(info.message, info: info));
354
+ cctrl.addError(nomosErrorFromInfo(info));
353
355
  } else {
354
356
  cctrl.add(((m['rows'] as Map?)?.cast<String, Object?>()) ?? const {});
355
357
  }
@@ -410,7 +412,7 @@ class NomosBridge {
410
412
  fallbackOperation: op,
411
413
  fallbackTrace: trace,
412
414
  fallbackMessage: 'the githolon refused the request');
413
- completer.completeError(NomosBridgeError(info.message, info: info));
415
+ completer.completeError(nomosErrorFromInfo(info));
414
416
  }
415
417
  }
416
418
 
@@ -931,7 +933,7 @@ class NomosBridge {
931
933
  code: 'nomos.bridge.disposed',
932
934
  operation: _pendingOps[entry.key],
933
935
  trace: trace);
934
- c.completeError(NomosBridgeError(info.message, info: info));
936
+ c.completeError(nomosErrorFromInfo(info));
935
937
  }
936
938
  }
937
939
  _pending.clear();
@@ -61,6 +61,36 @@ class FakeRunner implements BridgeTransport {
61
61
  'attributes': {'test.case': 'refusal'}
62
62
  }
63
63
  });
64
+ } else if (intentJson.contains('"directiveId":"custodyGap"')) {
65
+ // The client's typed custody error: structured errorInfo, code=nomos.custody.*.
66
+ _incoming.add({
67
+ 'v': kWebviewBridgeVersion,
68
+ 'id': id,
69
+ 'ok': false,
70
+ 'error':
71
+ 'shallow-insufficient: fold refs/heads/main: find commit: NotFound',
72
+ 'errorInfo': {
73
+ 'code': 'nomos.custody.shallow_insufficient',
74
+ 'message':
75
+ 'shallow-insufficient: fold refs/heads/main: find commit: NotFound',
76
+ 'component': 'nomos.custody',
77
+ 'operation': 'offer',
78
+ 'workspace': 'ws-1',
79
+ 'trace': message['trace'],
80
+ 'attributes': {
81
+ 'nomos.custody.ref': 'refs/heads/main',
82
+ 'nomos.custody.head':
83
+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
84
+ 'nomos.custody.missing_object':
85
+ 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
86
+ 'nomos.custody.phase': 'author',
87
+ 'nomos.custody.source': 'localSnapshot',
88
+ 'nomos.custody.recoverable': true,
89
+ 'nomos.custody.recommended_action':
90
+ 'quarantineLocalSnapshotAndReclone',
91
+ }
92
+ }
93
+ });
64
94
  } else {
65
95
  _incoming.add(reply({'head': 'HEAD-offer'}));
66
96
  }
@@ -420,6 +450,41 @@ void main() {
420
450
  expect(e.info.toTelemetryAttributes()['nomos.operation'], 'offer');
421
451
  });
422
452
 
453
+ test(
454
+ 'a custody fault surfaces as the TYPED CustodyError (switch on code, never scrape prose)',
455
+ () async {
456
+ await bridge.ready;
457
+ Object? err;
458
+ try {
459
+ await bridge.offer(
460
+ intentBytes: base64Encode(utf8.encode(jsonEncode({
461
+ 'v': 2,
462
+ 'intent': {
463
+ 'domain': 'x',
464
+ 'directiveId': 'custodyGap',
465
+ 'payload': {},
466
+ 'domainHash': 'h'
467
+ }
468
+ }))));
469
+ } on NomosBridgeError catch (e) {
470
+ err = e; // still catchable as the generic bridge error
471
+ }
472
+ expect(err, isA<CustodyError>());
473
+ final e = err as CustodyError;
474
+ expect(e.code, 'nomos.custody.shallow_insufficient');
475
+ expect(e.workspace, 'ws-1');
476
+ expect(e.ref, 'refs/heads/main');
477
+ expect(e.head, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
478
+ expect(e.missingObject, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');
479
+ expect(e.phase, CustodyPhase.author);
480
+ expect(e.source, CustodySource.localSnapshot);
481
+ expect(e.recoverable, isTrue);
482
+ expect(e.recommendedAction, 'quarantineLocalSnapshotAndReclone');
483
+ expect(e.shouldEvictLocalCustody, isTrue);
484
+ expect(e.info.trace.traceId, isNotEmpty); // trace IDs ride as on every bridge error
485
+ expect(e.message, contains('shallow-insufficient'));
486
+ });
487
+
423
488
  test(
424
489
  'legacy string-only runner errors still get operation and trace metadata',
425
490
  () async {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.32.0",
3
+ "version": "0.34.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",
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
+ }