@githolon/dsl 0.1.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.
Files changed (53) hide show
  1. package/LICENSE.md +36 -0
  2. package/compile_package.mjs +50 -0
  3. package/package.json +59 -0
  4. package/src/aggregate.ts +167 -0
  5. package/src/authoring.ts +119 -0
  6. package/src/build_package.ts +636 -0
  7. package/src/certified_read.ts +313 -0
  8. package/src/codegen_dart.ts +2732 -0
  9. package/src/codegen_dot.ts +466 -0
  10. package/src/codegen_provider_dart.ts +358 -0
  11. package/src/codegen_ts.ts +365 -0
  12. package/src/codegen_usda.ts +388 -0
  13. package/src/combined.ts +195 -0
  14. package/src/compile_engine.ts +567 -0
  15. package/src/compile_package_main.ts +496 -0
  16. package/src/compose.ts +317 -0
  17. package/src/count.ts +218 -0
  18. package/src/ctx.ts +57 -0
  19. package/src/derived.ts +138 -0
  20. package/src/directive.ts +306 -0
  21. package/src/drivers.ts +95 -0
  22. package/src/emits_guard.ts +123 -0
  23. package/src/engine_entry.ts +449 -0
  24. package/src/exists.ts +170 -0
  25. package/src/extremum.ts +227 -0
  26. package/src/fields.ts +291 -0
  27. package/src/framework/bootstrap.ts +22 -0
  28. package/src/framework/disclosure.ts +108 -0
  29. package/src/framework/domain_lifecycle.ts +108 -0
  30. package/src/framework/identity.ts +537 -0
  31. package/src/framework/impure_capability.ts +643 -0
  32. package/src/framework/rbac.ts +418 -0
  33. package/src/framework/repair.ts +150 -0
  34. package/src/framework/sync_lifecycle.ts +125 -0
  35. package/src/framework/workspace_invariant.ts +128 -0
  36. package/src/framework/workspaces.ts +817 -0
  37. package/src/index.ts +317 -0
  38. package/src/manifest.ts +947 -0
  39. package/src/ops.ts +145 -0
  40. package/src/ordered_read.ts +228 -0
  41. package/src/predicate.ts +203 -0
  42. package/src/query/compile.ts +0 -0
  43. package/src/query/relations.ts +144 -0
  44. package/src/query.ts +151 -0
  45. package/src/read.ts +54 -0
  46. package/src/relation.ts +189 -0
  47. package/src/report/csv.ts +54 -0
  48. package/src/report.ts +401 -0
  49. package/src/spatial.ts +115 -0
  50. package/src/sum.ts +194 -0
  51. package/src/usd.ts +563 -0
  52. package/src/wire.ts +149 -0
  53. package/src/wire_encode.ts +250 -0
@@ -0,0 +1,418 @@
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} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
5
+ // wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
6
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
+
8
+ /**
9
+ * framework/rbac.ts — the FRAMEWORK RBAC mechanism (tenant-agnostic).
10
+ *
11
+ * ── Why this module exists (the framework↔tenant line) ───────────────────────
12
+ * Multi-layer RBAC role-bindings are framework MECHANISM, not tenant content. The
13
+ * kernel ships the engine (`identity::authz` — `PermissionState::bindings_for` +
14
+ * `effective_caps_for` + chain delegation + captured-deps) and the one shared
15
+ * `admit` gate resolves the DECLARED `.requires(cap, scopeFrom)` pre-condition
16
+ * against the folded permission projection. A tenant should therefore NOT
17
+ * re-implement grant/revoke/delegate — it should DECLARE its roles→capabilities
18
+ * (a `RoleCatalogue`) and its resource vocabulary, then USE these framework
19
+ * primitives to author the binding-leaf writes.
20
+ *
21
+ * This module is the DSL surface for that mechanism:
22
+ * • `roleBindingSchema(resourceTypes)` — the stored binding-leaf shape (the
23
+ * EXACT JSON the kernel `identity::authz::RoleBinding` parses out of the
24
+ * `UserPermission.permissions` map);
25
+ * • `userPermissionAggregate(resourceTypes)` — the per-user aggregate shape +
26
+ * the `userPermissionsInstanceId`/`permissionId` deterministic id rules
27
+ * (`user-permissions-<userId>`, `perm-<userId>-<resourceType>-<resourceId>`);
28
+ * • `grant` / `revoke` / `delegate` / `revokeDelegation` — the four primitive
29
+ * PLAN BUILDERS. Each takes a `scope = [resourceType, resourceId]` (the
30
+ * framework's universal scope shape) + the actor/role/provenance and returns
31
+ * the `PlannedOp[]` that write the SAME binding leaf the authz engine reads.
32
+ * • `roleCatalogue(roles)` — the declaration helper a tenant uses to declare its
33
+ * roles→capabilities (the `RoleCatalogue` the kernel resolves a `role` string
34
+ * against; opaque to the framework — the framework provides the SHAPE, the
35
+ * tenant the CONTENT).
36
+ *
37
+ * ── CRITICAL — the kernel-read wire shape is FROZEN (`authz.rs` reads it) ──────
38
+ * `identity::authz::PermissionState::bindings_for` reads JSON `RoleBinding` leaves
39
+ * out of `UserPermission.permissions` (a `MapOf`, keyed
40
+ * `user-permissions-<userId>`). The AUTHZ-RELEVANT fields the kernel parses are
41
+ * frozen — `permissionId / userId / resourceType / resourceId / role / status /
42
+ * grantedBy / grantedAt`, the revocation provenance (`revokedBy / revokedAt /
43
+ * revocationReason`), and the OPTIONAL delegation fields (`delegator / attenuation
44
+ * / expires`) which the kernel serde-defaults so a plain grant leaf is unchanged.
45
+ * The OPTIONAL book-keeping (`displayName / lastViewed / resourceCount`) is NOT in
46
+ * the kernel's `RoleBinding` struct — serde drops unknown leaf fields — so it is
47
+ * invisible to authz and free to use framework-generic names (`resourceCount` is
48
+ * the tenant-agnostic book-keeping count; a tenant maps its own domain noun onto
49
+ * it at the directive surface). This is a refactor of WHO authors the leaf, NOT a
50
+ * change to any field the kernel reads.
51
+ */
52
+ import { z } from "zod";
53
+ import { aggregate, instance, type AggregateHandle } from "../aggregate.js";
54
+ import { t } from "../fields.js";
55
+ import { Lww, MapOf } from "../drivers.js";
56
+ import { set, setEntry, type PlannedOp } from "../ops.js";
57
+
58
+ /** Permission lifecycle — the framework binding-status vocabulary. */
59
+ export const PERMISSION_STATUSES = ["active", "revoked", "suspended"] as const;
60
+ export type PermissionStatus = (typeof PERMISSION_STATUSES)[number];
61
+
62
+ /**
63
+ * The framework binding-leaf schema, parameterised over a tenant's resource-type
64
+ * vocabulary. A binding is the `(actor, role, scope)` triple from
65
+ * identity-access §"Multi-layer RBAC", scope = `(resourceType, resourceId)`, plus
66
+ * provenance, optional book-keeping, and the optional delegation fields. This
67
+ * lowers to ONE JSON Str leaf (the kernel stores Str|Int leaves only — gap G2).
68
+ *
69
+ * `resourceTypes` is the tenant's enum; the framework does not fix it. The rest of
70
+ * the shape is framework-fixed because the kernel `authz.rs` `RoleBinding` parses
71
+ * it field-for-field.
72
+ */
73
+ export function roleBindingSchema<const T extends readonly [string, ...string[]]>(
74
+ resourceTypes: T,
75
+ ) {
76
+ return z.object({
77
+ permissionId: z.string(),
78
+ userId: z.string(),
79
+ // scope = (resourceType, resourceId), the org→workspace→resource address.
80
+ resourceType: z.enum(resourceTypes),
81
+ resourceId: z.string(),
82
+ // role within that scope (a role = a capability set; resolved by the kernel
83
+ // authz slice against the tenant `RoleCatalogue`; opaque string here).
84
+ role: z.string(),
85
+ status: z.enum(PERMISSION_STATUSES),
86
+ grantedBy: z.string(),
87
+ grantedAt: z.string(),
88
+ // book-keeping (tenant `updatePermissionMetadata`): all optional.
89
+ displayName: z.string().optional(),
90
+ lastViewed: z.string().optional(),
91
+ resourceCount: z.number().int().optional(),
92
+ // revocation provenance, set when status flips to "revoked".
93
+ revokedBy: z.string().optional(),
94
+ revokedAt: z.string().optional(),
95
+ revocationReason: z.string().optional(),
96
+ // ── DELEGATION (identity-access §"delegated authority") ──
97
+ // `delegator` set ⇒ this binding is a DELEGATION (actor A handed a capability
98
+ // subset down to `userId`). Honoured only if A held those caps at the
99
+ // evaluation point-in-time (kernel `effective_caps_for` recurses to a
100
+ // delegator-less root). All three OPTIONAL so a plain grant leaf is unchanged.
101
+ delegator: z.string().optional(),
102
+ // the capability subset handed down (the attenuation ceiling).
103
+ attenuation: z.array(z.string()).optional(),
104
+ // point-in-time expiry (ISO/HLC string mirror).
105
+ expires: z.string().optional(),
106
+ });
107
+ }
108
+
109
+ /**
110
+ * The generic framework binding-leaf type. A tenant narrows `resourceType` via its
111
+ * own `roleBindingSchema(RESOURCE_TYPES)`; the framework primitives below accept a
112
+ * plain `string` resourceType (the tenant's enum is a subtype) so the mechanism is
113
+ * tenant-agnostic.
114
+ */
115
+ export type FrameworkRoleBinding = {
116
+ permissionId: string;
117
+ userId: string;
118
+ resourceType: string;
119
+ resourceId: string;
120
+ role: string;
121
+ status: PermissionStatus;
122
+ grantedBy: string;
123
+ grantedAt: string;
124
+ displayName?: string;
125
+ lastViewed?: string;
126
+ resourceCount?: number;
127
+ revokedBy?: string;
128
+ revokedAt?: string;
129
+ revocationReason?: string;
130
+ delegator?: string;
131
+ attenuation?: string[];
132
+ expires?: string;
133
+ };
134
+
135
+ /**
136
+ * The per-user permissions aggregate SHAPE — framework mechanism. Wire id
137
+ * `UserPermission` matches the live aggregate type the kernel folds and
138
+ * `bindings_for` reads. Multi-instance, keyed `user-permissions-<userId>`.
139
+ *
140
+ * - userId Lww (identity)
141
+ * - permissions MapOf(Lww) — the map-of-driven role-binding entries; each value
142
+ * is a JSON-encoded binding (a `t.json` Str leaf), merged per-key
143
+ * by Lww (the closest-expressible map driver — gap G1)
144
+ * - lastViewedAt Lww (aggregate-level "last touched")
145
+ */
146
+ export const userPermissionAggregate = aggregate("UserPermission", {
147
+ userId: t.string().merge(Lww),
148
+ permissions: t.map(t.json()).merge(MapOf(Lww)),
149
+ // The grant/ensure primitive folds ONLY userId + a permissions entry (writeBinding),
150
+ // never lastViewedAt — a freshly-ensured per-user record lacks it — OPTIONAL on
151
+ // the read type (read-decode gap).
152
+ lastViewedAt: t.string().merge(Lww).optional(),
153
+ });
154
+ export type UserPermissionAggregateHandle = typeof userPermissionAggregate;
155
+
156
+ /**
157
+ * Deterministic per-user aggregate instance id — `user-permissions-<userId>`.
158
+ * Reproduces the Dart aggregate-id rule so two users' bindings fold into two
159
+ * distinct aggregates (never colliding on the shared type) and replay is stable.
160
+ */
161
+ export function userPermissionsInstanceId(userId: string): string {
162
+ return `user-permissions-${userId}`;
163
+ }
164
+
165
+ /**
166
+ * Deterministic permission-id (map key) — `perm-<userId>-<resourceType>-<resourceId>`.
167
+ * Reproduces the Dart `execute()` rule so the same (user, scope) always maps to the
168
+ * same map key (kills the dup-grant re-dispatch class).
169
+ */
170
+ export function permissionId(input: {
171
+ userId: string;
172
+ resourceType: string;
173
+ resourceId: string;
174
+ }): string {
175
+ return `perm-${input.userId}-${input.resourceType}-${input.resourceId}`;
176
+ }
177
+
178
+ /**
179
+ * A framework scope: `[resourceType, resourceId]`. This is the universal scope the
180
+ * framework `grant`/`revoke`/`delegate` primitives take and the same shape a
181
+ * directive's `.requires(cap, p => [resourceType, resourceId])` derives so the
182
+ * authz gate checks the actor holds the capability at a covering scope.
183
+ */
184
+ export type Scope = readonly [resourceType: string, resourceId: string];
185
+
186
+ /** Bind the per-user aggregate handle to a concrete user instance. */
187
+ function userInstance(userId: string) {
188
+ return instance(userPermissionAggregate, userPermissionsInstanceId(userId));
189
+ }
190
+
191
+ /**
192
+ * Strip `undefined`-valued keys so the JSON leaf is byte-stable (no `"k":null`,
193
+ * no `"k":undefined`-dropped key carrying a slot). Returns a `FrameworkRoleBinding`
194
+ * — the omitted-when-undefined optionals are exactly the schema's `.optional()`s.
195
+ */
196
+ function compact(obj: Record<string, unknown>): FrameworkRoleBinding {
197
+ const out: Record<string, unknown> = {};
198
+ for (const [k, v] of Object.entries(obj)) {
199
+ if (v !== undefined) out[k] = v;
200
+ }
201
+ return out as FrameworkRoleBinding;
202
+ }
203
+
204
+ /** Common fields every grant-family primitive consumes. */
205
+ interface BindingInput {
206
+ userId: string;
207
+ scope: Scope;
208
+ role: string;
209
+ grantedBy: string;
210
+ grantedAt: string;
211
+ displayName?: string | undefined;
212
+ }
213
+
214
+ /**
215
+ * Build a role-binding leaf for one (user, scope) pair. The single place the
216
+ * framework constructs the wire leaf the kernel `authz.rs` reads — every primitive
217
+ * routes through here so the shape can never drift.
218
+ */
219
+ function buildBinding(
220
+ p: BindingInput & {
221
+ status: PermissionStatus;
222
+ revokedBy?: string | undefined;
223
+ revokedAt?: string | undefined;
224
+ revocationReason?: string | undefined;
225
+ delegator?: string | undefined;
226
+ attenuation?: string[] | undefined;
227
+ expires?: string | undefined;
228
+ lastViewed?: string | undefined;
229
+ resourceCount?: number | undefined;
230
+ },
231
+ ): { pid: string; binding: FrameworkRoleBinding } {
232
+ const [resourceType, resourceId] = p.scope;
233
+ const pid = permissionId({ userId: p.userId, resourceType, resourceId });
234
+ const binding = compact({
235
+ permissionId: pid,
236
+ userId: p.userId,
237
+ resourceType,
238
+ resourceId,
239
+ role: p.role,
240
+ status: p.status,
241
+ grantedBy: p.grantedBy,
242
+ grantedAt: p.grantedAt,
243
+ displayName: p.displayName,
244
+ lastViewed: p.lastViewed,
245
+ resourceCount: p.resourceCount,
246
+ revokedBy: p.revokedBy,
247
+ revokedAt: p.revokedAt,
248
+ revocationReason: p.revocationReason,
249
+ delegator: p.delegator,
250
+ attenuation: p.attenuation,
251
+ expires: p.expires,
252
+ });
253
+ return { pid, binding };
254
+ }
255
+
256
+ /**
257
+ * Lower a built binding to the `PlannedOp[]` for a per-user aggregate. `ensure`
258
+ * (the default) also writes the `userId` Set so the first grant materialises the
259
+ * aggregate (matching `EnsuresEntityPayload`); a plain status-flip (revoke) omits
260
+ * it (`ensure = false`) since the aggregate already exists.
261
+ */
262
+ function writeBinding(
263
+ userId: string,
264
+ pid: string,
265
+ binding: FrameworkRoleBinding,
266
+ ensure = true,
267
+ ): PlannedOp[] {
268
+ const upa = userInstance(userId);
269
+ const ops: PlannedOp[] = [];
270
+ if (ensure) ops.push(set(upa, "userId", userId));
271
+ ops.push(setEntry(upa, "permissions", pid, JSON.stringify(binding)));
272
+ return ops;
273
+ }
274
+
275
+ /**
276
+ * grant — the framework PRIMITIVE: write one ACTIVE role-binding for `userId` at
277
+ * `scope`. The collapse target of a tenant grant-family directive (the scope's
278
+ * `resourceType` is fixed per tenant directive). Ensures the per-user aggregate.
279
+ */
280
+ export function grant(p: BindingInput): PlannedOp[] {
281
+ const { pid, binding } = buildBinding({ ...p, status: "active" });
282
+ return writeBinding(p.userId, pid, binding, true);
283
+ }
284
+
285
+ /**
286
+ * revoke — the framework PRIMITIVE: flip the binding at `scope` to `status:revoked`
287
+ * (an Lww write over the SAME map entry — gap G1: ideally remove-wins). Re-authors
288
+ * the full leaf since a JSON leaf is replaced wholesale by Lww.
289
+ */
290
+ export function revoke(
291
+ p: BindingInput & {
292
+ revokedBy: string;
293
+ revokedAt: string;
294
+ reason?: string | undefined;
295
+ },
296
+ ): PlannedOp[] {
297
+ const { pid, binding } = buildBinding({
298
+ ...p,
299
+ status: "revoked",
300
+ revokedBy: p.revokedBy,
301
+ revokedAt: p.revokedAt,
302
+ revocationReason: p.reason,
303
+ });
304
+ return writeBinding(p.userId, pid, binding, false);
305
+ }
306
+
307
+ /**
308
+ * delegate — the framework PRIMITIVE: actor `delegatedBy` (A) hands a capability
309
+ * subset down to `userId` (B) at `scope`, writing a `delegator`-stamped, attenuated
310
+ * ACTIVE binding into B's per-user map. Mirrors `grant`'s write shape but the leaf
311
+ * carries `delegator`/`attenuation`/`expires`. Self-authenticating (A signs the
312
+ * authoring intent); honoured only if A held the caps point-in-time (kernel
313
+ * `effective_caps_for`). Ensures B's aggregate.
314
+ */
315
+ export function delegate(p: {
316
+ userId: string;
317
+ scope: Scope;
318
+ role: string;
319
+ attenuation: string[];
320
+ delegatedBy: string;
321
+ grantedAt: string;
322
+ expires?: string | undefined;
323
+ displayName?: string | undefined;
324
+ }): PlannedOp[] {
325
+ const { pid, binding } = buildBinding({
326
+ userId: p.userId,
327
+ scope: p.scope,
328
+ role: p.role,
329
+ grantedBy: p.delegatedBy,
330
+ grantedAt: p.grantedAt,
331
+ status: "active",
332
+ delegator: p.delegatedBy,
333
+ attenuation: p.attenuation,
334
+ expires: p.expires,
335
+ displayName: p.displayName,
336
+ });
337
+ return writeBinding(p.userId, pid, binding, true);
338
+ }
339
+
340
+ /**
341
+ * revokeDelegation — the framework PRIMITIVE: flip a delegated binding to revoked
342
+ * (an Lww write over the SAME entry, like `revoke`, but carrying the
343
+ * `delegator`/`attenuation` provenance). Revoking the DELEGATOR's ROOT binding (a
344
+ * plain `revoke`) is the load-bearing case — the chained delegate loses its caps
345
+ * point-in-time via the fold with NO propagation; this is the symmetric surface
346
+ * for revoking a delegated leaf directly. Mutates (the aggregate exists).
347
+ */
348
+ export function revokeDelegation(p: {
349
+ userId: string;
350
+ scope: Scope;
351
+ role: string;
352
+ attenuation: string[];
353
+ delegatedBy: string;
354
+ grantedAt: string;
355
+ revokedBy: string;
356
+ revokedAt: string;
357
+ reason?: string | undefined;
358
+ }): PlannedOp[] {
359
+ const { pid, binding } = buildBinding({
360
+ userId: p.userId,
361
+ scope: p.scope,
362
+ role: p.role,
363
+ grantedBy: p.delegatedBy,
364
+ grantedAt: p.grantedAt,
365
+ status: "revoked",
366
+ delegator: p.delegatedBy,
367
+ attenuation: p.attenuation,
368
+ revokedBy: p.revokedBy,
369
+ revokedAt: p.revokedAt,
370
+ revocationReason: p.reason,
371
+ });
372
+ return writeBinding(p.userId, pid, binding, false);
373
+ }
374
+
375
+ /**
376
+ * updateMetadata — the framework PRIMITIVE for the book-keeping write
377
+ * (`lastViewed`/`resourceCount`/`displayName`) on an existing ACTIVE binding, plus the
378
+ * aggregate-level `lastViewedAt`. NOT authz (it never changes role/scope/status) —
379
+ * a tenant wraps it as its own directive (e.g. `updatePermissionMetadata`).
380
+ */
381
+ export function updateMetadata(
382
+ p: BindingInput & {
383
+ lastViewed?: string | undefined;
384
+ resourceCount?: number | undefined;
385
+ },
386
+ ): PlannedOp[] {
387
+ const { pid, binding } = buildBinding({
388
+ ...p,
389
+ status: "active",
390
+ lastViewed: p.lastViewed,
391
+ resourceCount: p.resourceCount,
392
+ });
393
+ const ops = writeBinding(p.userId, pid, binding, false);
394
+ if (p.lastViewed !== undefined) {
395
+ ops.push(set(userInstance(p.userId), "lastViewedAt", p.lastViewed));
396
+ }
397
+ return ops;
398
+ }
399
+
400
+ /**
401
+ * A tenant `RoleCatalogue` declaration: `role → capability set` (the directive
402
+ * names a role grants). This is the SHAPE the kernel `identity::authz::RoleCatalogue`
403
+ * (a `BTreeMap<String, BTreeSet<Capability>>`) resolves a binding's `role` string
404
+ * against. The framework provides the declaration HELPER + type; the tenant
405
+ * supplies the CONTENT (its roles, its capabilities). Capabilities are directive
406
+ * names (a directive's name IS its capability — the `.requires(cap)` convention).
407
+ */
408
+ export type RoleCatalogue = Readonly<Record<string, readonly string[]>>;
409
+
410
+ /**
411
+ * Declare a tenant `RoleCatalogue`. Pure passthrough that brands the shape + freezes
412
+ * it so a tenant declares `roleCatalogue({ siteAdmin: ["grantSiteAccess", …] })`
413
+ * once and the framework/kernel resolve a `role` against it. Tenant-agnostic: the
414
+ * framework fixes the SHAPE, never the roles.
415
+ */
416
+ export function roleCatalogue<const C extends RoleCatalogue>(roles: C): C {
417
+ return Object.freeze({ ...roles }) as C;
418
+ }
@@ -0,0 +1,150 @@
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} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
5
+ // wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
6
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
+
8
+ /**
9
+ * framework/repair.ts — the REPAIR surface as a tenant-style Nomos domain (#260 Stage 2).
10
+ *
11
+ * ── Why this module exists ──────────────────────────────────────────────────
12
+ * "Nomos is powerful enough to define its own repair surface as a Nomos domain"
13
+ * (Jack 2026-06-05, TARGET_repair.dot). The dogfood proof: repair intents go through
14
+ * THE ONE GATE — no bespoke crate, no AssumeVerified, no 2nd write path. This is Stage
15
+ * 2 (the `revert` directive); supersede / upcast / drain are later stages.
16
+ *
17
+ * ── The RepairRecord aggregate ──────────────────────────────────────────────
18
+ * A `revert` is an ACCOUNTABLE act: it leaves an immutable audit record (RepairRecord)
19
+ * in the ledger so every repair is traceable — who struck what, why, and when. This
20
+ * aggregate also satisfies the directive builder's mandatory aggregate-marker
21
+ * requirement (a strike-only directive has no aggregate to `.creates`/`.mutates` unless
22
+ * we supply one — the audit record is the natural candidate).
23
+ *
24
+ * ── The `revert` directive ──────────────────────────────────────────────────
25
+ * `revert` is the KEYSTONE (#260 Stage 2):
26
+ * • `.creates(RepairRecord)` — keyed by a KERNEL-MINTED `recordId`
27
+ * (`RepairRecord_<uuidv7>`). The kernel mints it (the marker-driven front-door),
28
+ * captures it, and commits it; replay reads the captured id (idempotent by
29
+ * capture, never re-minted). There is no peer-supplied id — every peer runs the
30
+ * same kernel and the id-mint gate verifies the minted shape on admit.
31
+ * • `.plan(...)` returns BOTH `strike(p.target)` AND the audit `set(...)` ops
32
+ * in one `PlannedOp[]` — one atomic intent carrying both channels.
33
+ * • The `strike(target)` op flows through the engine plan output (`{events, strikes}`)
34
+ * so it is VERIFIED at the gate — never a forgeable, out-of-band retraction
35
+ * (determinism law, ops.ts: "the strike flows through the verified plan output").
36
+ *
37
+ * ── Scope (Stage 2 only — do NOT add supersede / upcast / drain here) ──────
38
+ * supersede needs policy-control event shapes; upcast is an on-read migration;
39
+ * drain is a re-dispatch. All are later stages (TARGET_repair.dot build order §2-5).
40
+ */
41
+ import { z } from "zod";
42
+ import { aggregate, instance } from "../aggregate.js";
43
+ import { t } from "../fields.js";
44
+ import { Lww } from "../drivers.js";
45
+ import { directive } from "../directive.js";
46
+ import { set, strike } from "../ops.js";
47
+
48
+ // ── The repair-kind vocabulary (Stage 2: "revert" only; expand later) ───────
49
+
50
+ /** The known repair operations. Stage 2 carries only `"revert"`. */
51
+ // `revert` = a pure strikeout (generic, framework-only). `replace` = strikeout PLUS
52
+ // corrected events — only expressible in a COMPOSED domain (e.g. tenant_repairs = repair ⊕ tenant),
53
+ // because the corrected events are in the TENANT's vocabulary. The repair domain owns the
54
+ // kind vocabulary + the audit record; a composed domain authors the replace directive.
55
+ export const REPAIR_KINDS = ["revert", "replace"] as const;
56
+ export type RepairKind = (typeof REPAIR_KINDS)[number];
57
+
58
+ // ── RepairRecord aggregate ──────────────────────────────────────────────────
59
+
60
+ /**
61
+ * The RepairRecord aggregate — the immutable audit log of a repair act.
62
+ *
63
+ * Every repair is a RECORDED, ACCOUNTABLE act: the record carries WHO struck WHAT,
64
+ * WHY, and WHEN — so the ledger history is never silently mutated. This serves two
65
+ * purposes:
66
+ *
67
+ * 1. AUDIT. A struck intent (e.g. a bad move) is retracted from the fold, but the
68
+ * RepairRecord survives as evidence that the retraction happened, authorised by
69
+ * whom, and for what reason. Operators can query "who reverted what".
70
+ *
71
+ * 2. DIRECTIVE MARKER. The DSL directive builder requires every directive to carry
72
+ * a `.creates` or `.mutates` marker (the referential-marker invariant). A
73
+ * pure `strike`-only directive (no field ops) has no natural aggregate to create
74
+ * or mutate. RepairRecord IS that aggregate — a create keyed by a KERNEL-MINTED
75
+ * `recordId` (idempotent on replay by capture, like every create).
76
+ *
77
+ * Fields are all Lww (the "immutable-after-create" gap, same as identity.ts NOTE 1a):
78
+ * a RepairRecord should never be rewritten, but there is no `immutableAfterCreate`
79
+ * driver yet — Lww is the safe fallback (a re-author merely converges, never corrupts).
80
+ */
81
+ export const RepairRecord = aggregate("RepairRecord", {
82
+ /** The aggregate instance id — a KERNEL-MINTED `RepairRecord_<uuidv7>`. */
83
+ recordId: t.string().merge(Lww),
84
+ /** The kind of repair (Stage 2: "revert" only). */
85
+ kind: t.enum(REPAIR_KINDS).merge(Lww),
86
+ /** The id of the struck intent — the target of the retraction. */
87
+ target: t.string().merge(Lww),
88
+ /** Human-readable reason for the repair (audit trail). */
89
+ reason: t.string().merge(Lww),
90
+ /** Who authored the repair (actor id / display name). */
91
+ revertedBy: t.string().merge(Lww),
92
+ /** Epoch-ms the repair was authored. */
93
+ revertedAt: t.int().merge(Lww),
94
+ });
95
+
96
+ // ── `revert` directive ──────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * `revert` — the keystone directive of the repair domain.
100
+ *
101
+ * Lowers to a `WireIntent` carrying BOTH channels in one atomic commit:
102
+ * • `strikes = [p.target]` — the kernel strikeout (folded by parity; parity-even
103
+ * is a redo). Verified at the gate: the engine
104
+ * re-derives the strike from the plan, so it is
105
+ * IMPOSSIBLE to forge.
106
+ * • `events = [RepairRecord]` — the Create of the audit record, keyed by a
107
+ * KERNEL-MINTED `recordId` (`RepairRecord_<uuidv7>`),
108
+ * minted by the marker-driven front-door, captured +
109
+ * committed (idempotent on replay by capture).
110
+ *
111
+ * The `recordId` is KERNEL-MINTED — there is no peer-supplied id. Every peer runs the
112
+ * same deterministic kernel; the kernel mints the id from a host-injected entropy
113
+ * capability whose draw is captured into the ledger, and the id-mint gate verifies the
114
+ * minted shape + type tag on admit (the same path every create takes).
115
+ */
116
+ export const revert = directive("revert")
117
+ .creates(RepairRecord)
118
+ .payload(
119
+ z.object({
120
+ /** The RepairRecord instance id — a KERNEL-MINTED `RepairRecord_<uuidv7>`. */
121
+ recordId: z.string(),
122
+ /** The intent id to retract (strike). */
123
+ target: z.string(),
124
+ /** Human-readable reason (audit). */
125
+ reason: z.string(),
126
+ /** Who is performing the repair. */
127
+ revertedBy: z.string(),
128
+ /** Epoch-ms of authorship (captured deterministically). */
129
+ revertedAt: z.number().int(),
130
+ }),
131
+ )
132
+ .plan((p) => {
133
+ // Key the RepairRecord by its KERNEL-MINTED `recordId` (idempotent on replay by
134
+ // capture). instance(RepairRecord, recordId) → the create's `__id` (the marker-driven
135
+ // front-door mints this id; the gate verifies its shape + type tag on admit).
136
+ const rec = instance(RepairRecord, p.recordId);
137
+ return [
138
+ // CHANNEL 1: the strikeout — retract the target intent.
139
+ // `strike(target)` routes onto WireIntent.strikes (via lower.ts partition);
140
+ // the engine emits the {events, strikes} object so the gate can verify it.
141
+ strike(p.target),
142
+ // CHANNEL 2: the audit record — immutable log of this repair act.
143
+ set(rec, "recordId", p.recordId),
144
+ set(rec, "kind", "revert"),
145
+ set(rec, "target", p.target),
146
+ set(rec, "reason", p.reason),
147
+ set(rec, "revertedBy", p.revertedBy),
148
+ set(rec, "revertedAt", p.revertedAt),
149
+ ];
150
+ });