@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 +1 -1
- package/src/aggregate.ts +12 -1
- package/src/authoring.ts +4 -1
- package/src/birth_meter.ts +300 -0
- package/src/birth_meter_ensure.ts +67 -0
- package/src/build_package.ts +34 -0
- package/src/codegen_dart.ts +9 -5
- package/src/codegen_proof.ts +13 -7
- package/src/compile_engine.ts +13 -2
- package/src/compile_package_main.ts +28 -1
- package/src/directive.ts +10 -1
- package/src/engine_entry.ts +56 -9
- package/src/framework/birth_meter.ts +125 -0
- package/src/framework/impure_capability.ts +5 -2
- package/src/framework/platform_admin.ts +4 -1
- package/src/framework/relations.ts +19 -0
- package/src/framework/workspaces.ts +50 -4
- package/src/index.ts +34 -0
- package/src/read.ts +22 -0
- package/src/read_projection_composition.ts +117 -0
- package/src/read_projection_manifest.ts +94 -0
- package/src/read_projection_namespace.ts +152 -0
- package/src/usd.ts +121 -11
- package/src/usd_layers.ts +71 -0
- package/src/value_object.ts +43 -0
- package/src/wire_encode.ts +34 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "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
|
-
|
|
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
|
+
}
|
package/src/build_package.ts
CHANGED
|
@@ -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
|
package/src/codegen_dart.ts
CHANGED
|
@@ -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
|
-
|
|
1877
|
-
ctorParams,
|
|
1878
|
-
` });`,
|
|
1882
|
+
...ctorLines,
|
|
1879
1883
|
``,
|
|
1880
1884
|
` @override`,
|
|
1881
1885
|
` String get domain => domainName;`,
|
package/src/codegen_proof.ts
CHANGED
|
@@ -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):
|
|
1770
|
-
*
|
|
1771
|
-
* the
|
|
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
|
|
1780
|
-
` //
|
|
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
|
-
`
|
|
1785
|
-
` console.
|
|
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
|
`}`,
|
package/src/compile_engine.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
296
|
+
if (!targetMatches(binding.agg.id)) {
|
|
286
297
|
throw new EngineCompileError(
|
|
287
298
|
"binding-mismatch",
|
|
288
299
|
directiveId,
|