@githolon/dsl 0.3.0 → 0.5.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 (34) hide show
  1. package/package.json +2 -1
  2. package/src/build_package.ts +4 -0
  3. package/src/capability_exports.ts +55 -0
  4. package/src/codegen_dart.ts +9 -0
  5. package/src/codegen_proof.ts +72 -11
  6. package/src/compile_package_main.ts +262 -17
  7. package/src/directive.ts +35 -10
  8. package/src/engine_entry.ts +5 -1
  9. package/src/framework/capability.ts +215 -0
  10. package/src/framework/impure_capability.ts +25 -3
  11. package/src/framework/workspaces.ts +129 -0
  12. package/src/index.ts +9 -0
  13. package/src/manifest.ts +114 -2
  14. package/src/read.ts +29 -0
  15. package/src/stable_ids.ts +226 -0
  16. package/src/stable_ids_types.ts +40 -0
  17. package/src/usd.ts +54 -0
  18. package/src/usd_layers.ts +65 -1
  19. package/src/wire_encode.ts +15 -0
  20. package/src/workspace_sharding.ts +123 -18
  21. package/src/workspace_type.ts +7 -5
  22. package/dart/.dart_tool/package_config.json +0 -328
  23. package/dart/.dart_tool/package_graph.json +0 -485
  24. package/dart/.dart_tool/pub/bin/test/test.dart-3.11.5.snapshot +0 -0
  25. package/dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjU= +0 -0
  26. package/dart/.dart_tool/version +0 -1
  27. package/dart/build/native_assets/macos/native_assets.json +0 -1
  28. package/dart/build/test_cache/build/89a6598c8854ed031dfc25d83c80860e.cache.dill.track.dill +0 -0
  29. package/dart/build/unit_test_assets/AssetManifest.bin +0 -0
  30. package/dart/build/unit_test_assets/FontManifest.json +0 -1
  31. package/dart/build/unit_test_assets/NOTICES.Z +0 -0
  32. package/dart/build/unit_test_assets/NativeAssetsManifest.json +0 -1
  33. package/dart/build/unit_test_assets/shaders/ink_sparkle.frag +0 -0
  34. package/dart/build/unit_test_assets/shaders/stretch_effect.frag +0 -0
@@ -0,0 +1,215 @@
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/capability.ts — `capability()`: THE MARKETPLACE DECLARATION
10
+ * (`architecture/capability_marketplace.md` §4, ruling M2 — the framework-hidden
11
+ * lane). ONE call declares a typed Order→Receipt capability interface; the dev
12
+ * sees a typed order call and a typed task row, and the framework supplies
13
+ * everything else through the SHIPPED `impureCapability()` quartet — so the gate
14
+ * law (`check_order_receipt`), the kernel recognizer, codegen, and the stable-id
15
+ * lockfile all see a perfectly ordinary impure capability. Hidden means *not
16
+ * hand-rolled*, never *not auditable*: the Order and Receipt facts ride the
17
+ * ledger verbatim.
18
+ *
19
+ * ```ts
20
+ * export const llm = capability("llm.complete", {
21
+ * request: { prompt: t.string(), model: t.string(), maxTokens: t.int() },
22
+ * receipt: { tokensIn: t.int(), tokensOut: t.int() },
23
+ * });
24
+ * // derived, deterministically from the name:
25
+ * // aggregate LlmCompleteTask order orderLlmComplete
26
+ * // complete completeLlmComplete fail/block/deadLetter likewise
27
+ * // capability "llm.complete" (the CapabilityBinding the executor must hold)
28
+ * ```
29
+ *
30
+ * THE RECEIPT-FIELD MECHANISM (stated honestly — the sheet's §4 comment): the
31
+ * complete Receipt's payload is the framework-FIXED `{requestId, resultRef,
32
+ * completedAt}`; typed receipt VALUES ride INSIDE `resultRef` as a JSON object,
33
+ * and the generated `completeResult` ops decode them into the declared typed
34
+ * fields (per-field zod-validated IN the plan — a malformed executor result is a
35
+ * typed plan-halt refusal, never a silent partial fold). Receipt fields are
36
+ * auto-`.optional()` on the aggregate (absent while `requested` — the
37
+ * read-decode discipline). Large bodies belong in content-addressed
38
+ * `resultRef`/`resultEvidence` refs, not inline values.
39
+ *
40
+ * Naming: the capability name uses the shipped binding grammar
41
+ * (`^[a-z0-9][a-z0-9._-]{0,63}$` — dots legal; marketplace interfaces are
42
+ * dot-namespaced by convention) and is carried as the quartet's `capability`
43
+ * declaration — the `CapabilityBinding` the fulfilling executor must hold
44
+ * (`architecture/capability_credentials.md`).
45
+ */
46
+ import { z } from "zod";
47
+ import { instance } from "../aggregate.js";
48
+ import type { Field } from "../fields.js";
49
+ import { set, type PlannedOp } from "../ops.js";
50
+ import { query, type QueryDecl } from "../query.js";
51
+ import {
52
+ IMPURE_CAPABILITY_ENVELOPE,
53
+ impureCapability,
54
+ impureCapabilityAggregate,
55
+ impureCapabilityInstanceId,
56
+ type ImpureCapability,
57
+ } from "./impure_capability.js";
58
+
59
+ /** The shipped CapabilityBinding name grammar (worker + governance law agree). */
60
+ export const CAPABILITY_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
61
+
62
+ /** "llm.complete" → "LlmComplete" — the deterministic id stem. */
63
+ export function capabilityPascal(name: string): string {
64
+ return name
65
+ .split(/[._-]+/)
66
+ .filter(Boolean)
67
+ .map((part) => part[0]!.toUpperCase() + part.slice(1))
68
+ .join("");
69
+ }
70
+
71
+ /** The zod RAW SHAPE derived from request fields (each Field carries its own zod). */
72
+ type RequestShape<Req extends Record<string, Field>> = { [K in keyof Req]: Req[K]["zod"] };
73
+
74
+ /**
75
+ * What `capability()` returns: the full shipped quartet (so every existing
76
+ * consumer — codegen, lowering, the reactor helpers — works unchanged) plus the
77
+ * marketplace surface: the capability NAME and the request/receipt zod schemas
78
+ * (the executor side validates provider results against `receiptSchema` BEFORE
79
+ * building `resultRef`, so a refusal happens host-side first, gate-side always).
80
+ */
81
+ export interface CapabilityDecl<
82
+ Req extends Record<string, Field>,
83
+ Rec extends Record<string, Field>,
84
+ > extends ImpureCapability<string, RequestShape<Req>, Req & Rec> {
85
+ /** The capability name — the `CapabilityBinding` slot the executor must hold. */
86
+ readonly name: string;
87
+ /** The request fields' zod object (what `order<Pascal>`'s params validate as). */
88
+ readonly requestSchema: z.ZodObject<RequestShape<Req>>;
89
+ /**
90
+ * THE EXECUTOR'S SCAN LANE — a declared, indexed query over the task
91
+ * aggregate keyed on `status` (`<camelPascal>TasksByStatus`). The executor
92
+ * polls `{status: "requested"}` through the ordinary RPC query lane — no
93
+ * special scan op, no wasm arm: the projection index IS the work queue.
94
+ */
95
+ readonly tasks: QueryDecl;
96
+ /** The receipt fields' zod object — `{}` when the capability declares none. */
97
+ readonly receiptSchema: z.ZodObject<{ [K in keyof Rec]: Rec[K]["zod"] }>;
98
+ /** Build the `resultRef` JSON for a complete Receipt from typed receipt values. */
99
+ readonly encodeResult: (values: { [K in keyof Rec]: z.infer<Rec[K]["zod"]> }) => string;
100
+ }
101
+
102
+ /**
103
+ * Declare a typed marketplace capability. Sugar over {@link impureCapability} —
104
+ * nothing new on the wire; the one piece of REAL work it adds is the
105
+ * resultRef-decoding `completeResult` (the receipt-field mechanism above).
106
+ *
107
+ * Refusals are COMPILE-TIME loud (thrown at module load, so `nomos-compile`
108
+ * names them): a malformed name, a request/receipt field shadowing the reserved
109
+ * envelope, or a field declared on both sides.
110
+ */
111
+ export function capability<
112
+ const Name extends string,
113
+ Req extends Record<string, Field>,
114
+ Rec extends Record<string, Field> = Record<never, Field>,
115
+ >(name: Name, spec: { request: Req; receipt?: Rec }): CapabilityDecl<Req, Rec> {
116
+ if (!CAPABILITY_NAME_RE.test(name)) {
117
+ throw new Error(
118
+ `capability('${name}'): the name must match ${CAPABILITY_NAME_RE} (lowercase, dot-namespaced — e.g. 'llm.complete'); it doubles as the CapabilityBinding slot the executor must hold`,
119
+ );
120
+ }
121
+ const reserved = new Set(Object.keys(IMPURE_CAPABILITY_ENVELOPE));
122
+ const receipt = (spec.receipt ?? {}) as Rec;
123
+ for (const [side, fields] of [
124
+ ["request", spec.request],
125
+ ["receipt", receipt],
126
+ ] as const) {
127
+ for (const k of Object.keys(fields)) {
128
+ if (reserved.has(k)) {
129
+ throw new Error(
130
+ `capability('${name}'): ${side} field '${k}' shadows the reserved Order→Receipt envelope — rename it (the envelope is framework law: ${[...reserved].join(", ")})`,
131
+ );
132
+ }
133
+ }
134
+ }
135
+ for (const k of Object.keys(receipt)) {
136
+ if (k in spec.request) {
137
+ throw new Error(
138
+ `capability('${name}'): '${k}' is declared on BOTH request and receipt — a field has one writer (the order writes requests, the complete Receipt writes receipts); split or rename it`,
139
+ );
140
+ }
141
+ }
142
+
143
+ const pascal = capabilityPascal(name);
144
+ const aggregateId = `${pascal}Task`;
145
+
146
+ // Receipt fields fold only when the complete Receipt lands — auto-`.optional()`
147
+ // so a freshly-ordered task decodes on the generated read type (the same
148
+ // read-decode discipline the envelope's own optionals follow).
149
+ const receiptFields = Object.fromEntries(
150
+ Object.entries(receipt).map(([k, f]) => [k, f.optional()]),
151
+ ) as Rec;
152
+ const params = { ...spec.request, ...receiptFields } as Req & Rec;
153
+ const taskAggregate = impureCapabilityAggregate(aggregateId, params);
154
+
155
+ const requestShape = Object.fromEntries(
156
+ Object.entries(spec.request).map(([k, f]) => [k, f.zod]),
157
+ ) as RequestShape<Req>;
158
+ const receiptShape = Object.fromEntries(
159
+ Object.entries(receipt).map(([k, f]) => [k, f.zod]),
160
+ ) as { [K in keyof Rec]: Rec[K]["zod"] };
161
+ const receiptEntries = Object.entries(receipt) as [string, Field][];
162
+
163
+ const quartet = impureCapability({
164
+ aggregateId,
165
+ orderDirectiveId: `order${pascal}`,
166
+ completeDirectiveId: `complete${pascal}`,
167
+ failDirectiveId: `fail${pascal}`,
168
+ blockDirectiveId: `block${pascal}`,
169
+ deadLetterDirectiveId: `deadLetter${pascal}`,
170
+ params,
171
+ paramsSchema: requestShape,
172
+ // The loop is dynamic (declared field names), so the per-field `set()` typing
173
+ // is asserted — the PUBLIC payload types stay fully inferred from the zod shape.
174
+ writeParams: (taskId, p) =>
175
+ Object.keys(spec.request).map((k) =>
176
+ set(instance(taskAggregate, taskId), k as never, (p as Record<string, unknown>)[k] as never),
177
+ ),
178
+ // The receipt-field mechanism: typed values ride INSIDE resultRef as JSON;
179
+ // decode + per-field zod-validate IN the plan (pure function of payload —
180
+ // deterministic; a malformed executor result is a typed plan-halt refusal).
181
+ // Absent keys are skipped (partial results are legal — read-decode
182
+ // discipline), present keys must validate.
183
+ ...(receiptEntries.length > 0
184
+ ? {
185
+ completeResult: (taskId: string, resultRef: string): PlannedOp[] => {
186
+ const decoded = JSON.parse(resultRef) as Record<string, unknown>;
187
+ const ops: PlannedOp[] = [];
188
+ for (const [k, f] of receiptEntries) {
189
+ if (decoded[k] === undefined) continue;
190
+ ops.push(set(instance(taskAggregate, taskId), k as never, f.zod.parse(decoded[k]) as never));
191
+ }
192
+ return ops;
193
+ },
194
+ }
195
+ : {}),
196
+ capability: name,
197
+ });
198
+
199
+ const camel = pascal[0]!.toLowerCase() + pascal.slice(1);
200
+ const tasks = query(`${camel}TasksByStatus`).key("status").returns(taskAggregate);
201
+
202
+ return {
203
+ ...quartet,
204
+ aggregate: taskAggregate,
205
+ instanceId: impureCapabilityInstanceId,
206
+ name,
207
+ requestSchema: z.object(requestShape),
208
+ receiptSchema: z.object(receiptShape),
209
+ encodeResult: (values) => JSON.stringify(z.object(receiptShape).parse(values)),
210
+ tasks,
211
+ // The one-export expansion tag (capability_exports.ts): every exports walk
212
+ // discovers this bundle's aggregate + quartet + scan query without re-exports.
213
+ __isCapabilityDecl: true,
214
+ } as CapabilityDecl<Req, Rec>;
215
+ }
@@ -136,9 +136,10 @@ export const IMPURE_CAPABILITY_ENVELOPE = {
136
136
  // GENERIC, written by every impureCapability Receipt (complete OR fail), so the read
137
137
  // projection answers "is Order O closed, by which Receipt, with what result" in O(1)
138
138
  // off the `(closesOrder, order_id)` index — never a ledger scan. Absent while
139
- // `requested` (a fresh order has no closing receipt). Wire-identical to the Rust
140
- // reactor's `closesOrder` op (`git-holon::reactor::receipt_intent`,
141
- // `kernel::manifest_view::IMPURE_CAPABILITY_RECEIPT_FIELDS`).
139
+ // `requested` (a fresh order has no closing receipt). The kernel supplies its
140
+ // framework-fixed Lww driver (`kernel::manifest_view::IMPURE_CAPABILITY_RECEIPT_FIELDS`)
141
+ // and the gate enforces the lifecycle around it (`nomos_executor::check_order_receipt`
142
+ // admit step 5.5: OrphanReceipt / ReceiptOnTerminalOrder, terminal-once as law).
142
143
  closesOrder: t.string().merge(Lww).optional(),
143
144
  } as const;
144
145
 
@@ -348,6 +349,18 @@ export interface ImpureCapability<
348
349
  readonly block: RequirableDirective<z.infer<typeof blockSchema>>;
349
350
  readonly deadLetter: RequirableDirective<z.infer<typeof deadLetterSchema>>;
350
351
  readonly instanceId: (requestId: string) => string;
352
+ /**
353
+ * The CAPABILITY CREDENTIAL this task's provider must hold to fulfil Orders
354
+ * (`architecture/capability_credentials.md`): the law name of a
355
+ * `CapabilityBinding` — e.g. `"openai"`, `"cf-analytics"` — bound per
356
+ * workspace via the bind lane (`githolon capability bind <ws> <name>`). The
357
+ * provider HOST resolves `(workspace, capability)` → the credential VALUE
358
+ * from its own store, fail-closed against the law fact (active, unexpired);
359
+ * the value never rides an intent and never appears here. DECLARATION
360
+ * METADATA only — absent (the default) changes nothing on the wire, so
361
+ * credential-free domains stay byte-identical.
362
+ */
363
+ readonly capability?: string;
351
364
  }
352
365
 
353
366
  /**
@@ -429,6 +442,12 @@ export function impureCapability<
429
442
  * the Rust reactor's `result_fields` (`HandlerSuccess::with_result_field`).
430
443
  */
431
444
  completeResult?: (taskInstanceId: string, resultRef: string) => PlannedOp[];
445
+ /**
446
+ * OPTIONAL: the capability-credential name the provider host must hold a
447
+ * law-state `CapabilityBinding` for (see {@link ImpureCapability.capability}).
448
+ * Omit-when-absent — declaring nothing changes nothing on the wire.
449
+ */
450
+ capability?: string;
432
451
  }): ImpureCapability<Id, ParamsShape, F> {
433
452
  const taskAggregate = impureCapabilityAggregate(spec.aggregateId, spec.params);
434
453
 
@@ -489,6 +508,9 @@ export function impureCapability<
489
508
  block,
490
509
  deadLetter,
491
510
  instanceId: impureCapabilityInstanceId,
511
+ // Omit-when-absent (hash discipline): a capability-free declaration carries
512
+ // NO key at all, so its bundled source — and the package hash — is unchanged.
513
+ ...(spec.capability !== undefined ? { capability: spec.capability } : {}),
492
514
  };
493
515
  }
494
516
 
@@ -1,3 +1,7 @@
1
+ // governance-law revision: capability bindings 2026-06-12 (facts about authority on the
2
+ // ledger, authority itself host-side — architecture/capability_credentials.md). Carries
3
+ // the founding-key-rotation re-entry forward (genesis re-seeds idempotently by hash).
4
+ export const GOVERNANCE_REVISION = "2026-06-12-capability-bindings"; // hash-bearing: re-enters root genesis idempotently
1
5
  // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
6
  // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
7
  // wasm32-wasip1 artifact {kernel · projection · embedded
@@ -203,6 +207,15 @@ export const CloudPolicy = aggregate("CloudPolicy", {
203
207
  maxWorkspacesPerPrincipal: t.int().merge(Lww),
204
208
  /** Cumulative pushed-bytes cap per workspace ledger (bytes). */
205
209
  workspaceByteCap: t.int().merge(Lww),
210
+ /**
211
+ * The workspace-CREATION posture dial (cloud_sovereignty.md S6) —
212
+ * `"open"` (anyone creates), `"invite"` (an active CreationGrant key
213
+ * required), or `"closed"` (admins only). OPTIONAL: absent keeps the
214
+ * implicit posture the cloud has always had (zero active CreationGrants ⇒
215
+ * open, any ⇒ invite-governed) — pre-dial chains and policies behave
216
+ * exactly as before.
217
+ */
218
+ creationPosture: t.string().merge(Lww).optional(),
206
219
  /** Who last set the policy (attribution). */
207
220
  setBy: t.string().merge(Lww),
208
221
  /** Epoch-ms the policy was last set (caller-stamped). */
@@ -245,6 +258,47 @@ export const PlatformGrant = aggregate("PlatformGrant", {
245
258
  status: t.enum(GRANT_STATUSES).merge(Lww),
246
259
  });
247
260
 
261
+ /**
262
+ * CapabilityBinding — A FACT ABOUT AUTHORITY, never authority itself (the
263
+ * doctrine: `architecture/capability_credentials.md`). A workspace's impure
264
+ * capabilities (analytics tokens, provider API keys, upload credentials) need
265
+ * credentials — and credentials are constitutionally incompatible with a
266
+ * replicated chain (every peer would hold them forever; git never forgets;
267
+ * revocation cannot recall bytes). So the LEDGER records only the binding fact:
268
+ * WHICH workspace bound WHICH capability under WHICH credential digest
269
+ * (`keyHash` — the AdminGrant discipline, again), with what scope/expiry, by
270
+ * whom, at which `epoch`. The credential VALUE lives at exactly ONE
271
+ * credential-holding host (the workspace DO's storage, keyed
272
+ * `(workspace, capability)`), is never passed host-to-host, and is unusable
273
+ * the moment the law flips this record — the executing host checks the law
274
+ * fail-closed before every use.
275
+ *
276
+ * EPOCHS ARE PROVENANCE: a rotation revokes the active record and creates the
277
+ * next one (`epoch + 1`, fresh `keyHash`) — so every Receipt fact a provider
278
+ * stamps can name the binding epoch that produced it, and "which credential
279
+ * produced which facts" is answerable forever from law-state alone.
280
+ */
281
+ export const CapabilityBinding = aggregate("CapabilityBinding", {
282
+ /** The bound workspace's CloudWorkspace AGGREGATE id (root-custody lineage). */
283
+ workspaceId: t.string().merge(Lww),
284
+ /** The capability's law name (e.g. "cf-analytics", "openai", "gcs-upload"). */
285
+ capability: t.string().merge(Lww),
286
+ /** sha256 hex of the credential VALUE — the value itself NEVER lands here. */
287
+ keyHash: t.string().merge(Lww),
288
+ /** Monotone per (workspace, capability): 1, 2, … — rotation provenance. */
289
+ epoch: t.int().merge(Lww),
290
+ /** OPTIONAL narrowing label ("read-only", an account tag, …); absent = whole capability. */
291
+ scope: t.string().merge(Lww).optional(),
292
+ /** OPTIONAL epoch-ms expiry; absent = until revoked. Expired = fail-closed at the host. */
293
+ expiresAt: t.int().merge(Lww).optional(),
294
+ /** Who bound it (attribution, on the ledger forever). */
295
+ boundBy: t.string().merge(Lww),
296
+ /** Epoch-ms the binding was authored (caller-stamped). */
297
+ boundAt: t.int().merge(Lww),
298
+ /** `active` | `revoked` — the law's kill switch for the credential's USE. */
299
+ status: t.enum(GRANT_STATUSES).merge(Lww),
300
+ });
301
+
248
302
  // --- workspace directives ---------------------------------------------------------
249
303
 
250
304
  /**
@@ -607,6 +661,66 @@ export const releaseHostname = directive("releaseHostname")
607
661
  return [set(c, "status", "released" as const)];
608
662
  });
609
663
 
664
+ /**
665
+ * bindCapability — record the FACT that a workspace holds a capability
666
+ * credential (worker-authored after the value is stored host-side; the value
667
+ * itself takes the other road — DO storage — and NEVER this one). The payload's
668
+ * only credential-shaped field is `keyHash`, regex-pinned to sha256 hex: a raw
669
+ * secret value structurally cannot enter this directive. `scope`/`expiresAt`
670
+ * are omit-when-absent — a scope-free, expiry-free binding writes neither field.
671
+ */
672
+ export const bindCapability = directive("bindCapability")
673
+ .creates(CapabilityBinding)
674
+ .payload(
675
+ z.object({
676
+ workspaceId: z.string().min(1),
677
+ /** Lowercase capability name — a law identifier, not free text. */
678
+ capability: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/, "expected a lowercase capability name ([a-z0-9._-], max 64)"),
679
+ keyHash: sha256Hex,
680
+ epoch: z.number().int().positive(),
681
+ scope: z.string().min(1).optional(),
682
+ expiresAt: epochMs.optional(),
683
+ boundBy: z.string().min(1),
684
+ boundAt: epochMs,
685
+ }),
686
+ )
687
+ .plan((p) => {
688
+ const b = create(CapabilityBinding)
689
+ .set("workspaceId", p.workspaceId)
690
+ .set("capability", p.capability)
691
+ .set("keyHash", p.keyHash)
692
+ .set("epoch", p.epoch)
693
+ .set("boundBy", p.boundBy)
694
+ .set("boundAt", p.boundAt)
695
+ .set("status", "active" as const);
696
+ // Omit-when-absent: optional facts fold only when the payload carries them.
697
+ if (p.scope !== undefined) b.set("scope", p.scope);
698
+ if (p.expiresAt !== undefined) b.set("expiresAt", p.expiresAt);
699
+ return [];
700
+ });
701
+
702
+ /**
703
+ * revokeCapability — flip a CapabilityBinding to `revoked`. The credential
704
+ * stops being USABLE at fold time (the executing host checks this record,
705
+ * fail-closed, before resolving the value); the record (who bound it, when,
706
+ * who killed it — the revoker rides the intent) stays on the ledger — audit is
707
+ * structural. Rotation = revoke + bind at `epoch + 1`; unbind = revoke + the
708
+ * host deletes the stored value.
709
+ */
710
+ export const revokeCapability = directive("revokeCapability")
711
+ .mutates(CapabilityBinding)
712
+ .payload(
713
+ z.object({
714
+ bindingId: z.string().min(1),
715
+ revokedBy: z.string().min(1),
716
+ revokedAt: epochMs,
717
+ }),
718
+ )
719
+ .plan((p) => {
720
+ const b = instance(CapabilityBinding, p.bindingId);
721
+ return [set(b, "status", "revoked" as const)];
722
+ });
723
+
610
724
  /**
611
725
  * setCloudPolicy — set (or amend) THE QUOTA LAW. An Ensure on the law-state
612
726
  * singleton: the first author births it, later authors amend it in place —
@@ -618,6 +732,10 @@ export const setCloudPolicy = directive("setCloudPolicy")
618
732
  z.object({
619
733
  maxWorkspacesPerPrincipal: z.number().int().positive(),
620
734
  workspaceByteCap: z.number().int().positive(),
735
+ // The creation-posture dial (cloud_sovereignty.md S6). Optional:
736
+ // omitted leaves the field untouched (a partial amend never silently
737
+ // resets the door).
738
+ creationPosture: z.enum(["open", "invite", "closed"]).optional(),
621
739
  setBy: z.string().min(1),
622
740
  setAt: epochMs,
623
741
  }),
@@ -628,6 +746,7 @@ export const setCloudPolicy = directive("setCloudPolicy")
628
746
  withMarker(set(pol, "scope", "cloud"), "ensures"),
629
747
  set(pol, "maxWorkspacesPerPrincipal", p.maxWorkspacesPerPrincipal),
630
748
  set(pol, "workspaceByteCap", p.workspaceByteCap),
749
+ ...(p.creationPosture !== undefined ? [set(pol, "creationPosture", p.creationPosture)] : []),
631
750
  set(pol, "setBy", p.setBy),
632
751
  set(pol, "setAt", p.setAt),
633
752
  ];
@@ -815,3 +934,13 @@ export const hostnameClaimByHostname = query("hostnameClaimByHostname")
815
934
  export const hostnameClaimsByWorkspace = query("hostnameClaimsByWorkspace")
816
935
  .key("workspaceId")
817
936
  .returns(HostnameClaim);
937
+
938
+ /** A workspace's capability bindings — the executor's fail-closed law probe AND the audit list. */
939
+ export const capabilityBindingsByWorkspace = query("capabilityBindingsByWorkspace")
940
+ .key("workspaceId")
941
+ .returns(CapabilityBinding);
942
+
943
+ /** Provenance: which binding (workspace, capability, epoch) a credential digest belongs to. */
944
+ export const capabilityBindingByKeyHash = query("capabilityBindingByKeyHash")
945
+ .key("keyHash")
946
+ .returns(CapabilityBinding);
package/src/index.ts CHANGED
@@ -66,6 +66,9 @@ export { create, existing, type AggregateRef } from "./authoring.js";
66
66
  // THE CAPTURED READ — a plan reads O(1) DSL queries via `read(query, args)`; the result is committed as
67
67
  // the intent's read footprint (deterministic replay + stale-premise detection). The dev calls typed DSL
68
68
  // queries; the library routes them here. The host capability is `nomos.read` (twin of `nomos.rng`).
69
+ // NOT YET DEPLOYABLE (#57): the sealed wasm engine does not provide `nomos.read` yet, so
70
+ // `nomos-compile` REFUSES law that references read() (a named compile error, never a runtime halt).
71
+ // The captured-reads wave (task #58) wires the capability in and lifts the refusal.
69
72
  export { read } from "./read.js";
70
73
  export {
71
74
  query,
@@ -216,6 +219,12 @@ export {
216
219
  type TaskOutcome,
217
220
  type TaskReceipt,
218
221
  } from "./framework/impure_capability.js";
222
+ export {
223
+ capability,
224
+ capabilityPascal,
225
+ CAPABILITY_NAME_RE,
226
+ type CapabilityDecl,
227
+ } from "./framework/capability.js";
219
228
  export {
220
229
  DOMAIN_INSTALLATION_PHASES,
221
230
  PolicyBundle,
package/src/manifest.ts CHANGED
@@ -48,6 +48,14 @@ import {
48
48
  canonicalTaxonomyFragment,
49
49
  type CanonicalDirectiveRoute,
50
50
  } from "./workspace_routing.js";
51
+ import {
52
+ mintAggregateSid,
53
+ mintDomainSid,
54
+ mintFieldSid,
55
+ type StableAggregateId,
56
+ type StableFieldId,
57
+ type StableIds,
58
+ } from "./stable_ids.js";
51
59
 
52
60
  /** One captured `[fieldName, zodKind]` payload field, with optionality. */
53
61
  export interface CanonicalPayloadField {
@@ -126,6 +134,15 @@ export interface CanonicalDirective {
126
134
  * before this key existed (pinned golden hashes stay UNCHANGED).
127
135
  */
128
136
  readonly certifiedReads?: CanonicalCertifiedRead[];
137
+ /**
138
+ * The directive's DECLARED CAPTURED-READ queries (#58 — the captured-read lane):
139
+ * the O(1) DSL queries its `plan` may `read()` on the write path, each carried as
140
+ * `{queryId, key, returns}` (the read RECIPE is the contract — a different key or
141
+ * returns-type is a different read, so it moves the hash). SORTED by queryId.
142
+ * OMITTED ENTIRELY when the directive declares none — a captured-read-free
143
+ * directive is byte-identical in the manifest to before this key existed.
144
+ */
145
+ readonly capturedReads?: CanonicalCapturedRead[];
129
146
  /**
130
147
  * The directive's DECLARED required HOST CAPABILITY ports (`TARGET_deps.dot` cluster_ports;
131
148
  * the HOST/PORT axis, invariant 3): the capability ports the host must PROVIDE for the
@@ -178,6 +195,15 @@ export interface CanonicalRelationEvidence {
178
195
  readonly kind: string;
179
196
  }
180
197
 
198
+ /** One DECLARED captured-read query (#58) — the write-path `read()` recipe in the
199
+ * manifest: the query id, its index key (DECLARED order — index column order), and the
200
+ * aggregate TYPE it returns. */
201
+ export interface CanonicalCapturedRead {
202
+ readonly queryId: string;
203
+ readonly key: string[];
204
+ readonly returns: string;
205
+ }
206
+
181
207
  /** One captured DECLARED CertifiedRead — the profiled write-path read identity in the manifest.
182
208
  * `queryId` is the gate's required-read key; `sql`/`multiRow`/`uniqueTieBreakers` are the recipe
183
209
  * + shape the admission re-run + the profile pin (a recipe change moves the domain hash). */
@@ -334,6 +360,12 @@ export interface CanonicalExtremum {
334
360
  readonly where?: CanonicalPred;
335
361
  /** The group-by field. OMITTED for a grand-total extremum. */
336
362
  readonly by?: string;
363
+ /** THE ESTATE SCOPE (sharding §5, slice 8) — see {@link CanonicalCount.scope}:
364
+ * per-shard extremums ride the §5.2 delta lane as gate-adjudicated coordinator
365
+ * subtotals (extremize-on-monotone; retractions re-derive). OMITTED for
366
+ * home-scope extremums, so an unscoped declaration is byte-identical to before
367
+ * this key existed (golden hashes UNCHANGED). */
368
+ readonly scope?: string;
337
369
  }
338
370
 
339
371
  /**
@@ -372,6 +404,18 @@ export interface CanonicalManifest {
372
404
  readonly domain: string;
373
405
  readonly aggregates: CanonicalAggregate[];
374
406
  readonly directives: CanonicalDirective[];
407
+ /**
408
+ * STABLE IDENTIFIERS (#58 — the foundation: names are LABELS, identity is MINTED by
409
+ * the compiler at first appearance, never dev-assigned). Carries `{stableId, name}`
410
+ * for the domain, every aggregate, and every field — keyed by CURRENT name, each
411
+ * entry holding its `sid` plus the label lineage `was` (prior names, oldest first;
412
+ * compiler-derived from continuity, never dev-written). ALWAYS EMITTED by the post-
413
+ * #58 compiler — this key MOVES every enforcement-era identity hash (named and
414
+ * expected; inert-era artifacts already built stay byte-identical untouched). The
415
+ * EVOLVE GATE diffs old vs new law BY SID; the projection folds via the sid lineage
416
+ * so a renamed entity's old-era data coheres under the new label.
417
+ */
418
+ readonly stableIds?: StableIds;
375
419
  /**
376
420
  * The domain's WORKSPACE INVARIANTS (#266), SORTED by id, PRESENCE ONLY (id + `on`).
377
421
  * OMITTED ENTIRELY when the domain declares none — so an invariant-free domain is
@@ -630,6 +674,23 @@ function canonicalEmits(
630
674
  * NAME (the `reads:{}` key) is incidental to identity (it is `decide`'s ergonomics), so it is
631
675
  * NOT captured — only the `query_id` + recipe.
632
676
  */
677
+ /**
678
+ * Canonicalize a directive's DECLARED captured-read queries (#58) into a manifest
679
+ * fragment. OMIT-WHEN-EMPTY: returns `{}` (no `capturedReads` key) when the directive
680
+ * declares none — the omission keeps read-free hashes unchanged. When non-empty,
681
+ * returns a `capturedReads` array SORTED by queryId, each carrying the read recipe
682
+ * `{queryId, key, returns}` (key in DECLARED order — index column order).
683
+ */
684
+ function canonicalCapturedReads(
685
+ declared: QueryDecl[],
686
+ ): { capturedReads?: CanonicalCapturedRead[] } {
687
+ if (declared.length === 0) return {};
688
+ const capturedReads: CanonicalCapturedRead[] = declared
689
+ .map((q) => ({ queryId: q.id, key: [...q.key], returns: q.returns }))
690
+ .sort((a, b) => (a.queryId < b.queryId ? -1 : a.queryId > b.queryId ? 1 : 0));
691
+ return { capturedReads };
692
+ }
693
+
633
694
  function canonicalCertifiedReads(
634
695
  declared: Record<string, CertifiedReadDecl>,
635
696
  ): { certifiedReads?: CanonicalCertifiedRead[] } {
@@ -841,14 +902,17 @@ function canonicalExtrema(
841
902
  ): { mins?: CanonicalExtremum[] } | { maxes?: CanonicalExtremum[] } {
842
903
  if (declared === undefined || declared.length === 0) return {};
843
904
  const items: CanonicalExtremum[] = [...declared]
844
- .map(finishExtremum)
845
- .map((e) => ({
905
+ .map((raw) => ({ finished: finishExtremum(raw), scope: (raw as { scope?: unknown }).scope }))
906
+ .map(({ finished: e, scope }) => ({
846
907
  id: e.id,
847
908
  kind: e.kind,
848
909
  of: e.of,
849
910
  valueField: e.valueField,
850
911
  ...(e.where !== undefined ? { where: e.where } : {}),
851
912
  ...(e.by !== undefined ? { by: e.by } : {}),
913
+ // OMIT-WHEN-ABSENT (the canonicalSums discipline): a home-scope extremum is
914
+ // byte-identical to before the scope key existed — hash-stable by default.
915
+ ...(typeof scope === "string" ? { scope } : {}),
852
916
  }))
853
917
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
854
918
  return { [keyName]: items };
@@ -879,6 +943,50 @@ function canonicalOrderedReads(
879
943
  return { orderedReads: items };
880
944
  }
881
945
 
946
+ /**
947
+ * The domain's STABLE-ID surface (#58): continuity-carried when the compiler attached
948
+ * `mod.stableIdContinuity` (renames resolved, sids + lineage carried), deterministic
949
+ * first-appearance minting otherwise — so the manifest stays a pure function of the
950
+ * module (+ its attached continuity). Any aggregate/field the continuity does not
951
+ * cover (a genuinely new appearance) mints fresh.
952
+ */
953
+ function stableIdsForModule(
954
+ mod: DomainModule,
955
+ aggregates: CanonicalAggregate[],
956
+ ): StableIds {
957
+ const continuity = mod.stableIdContinuity;
958
+ const domainSid = continuity?.domain ?? mintDomainSid(mod.name);
959
+ // The CURRENT field KINDS off the live handles (the retype arm's identity input —
960
+ // a `t.string()` → `t.int()` retype moves `kind` while the driver stays Lww).
961
+ const handlesById = new Map(mod.aggregates.map((a) => [a.id, a]));
962
+ const out: Record<string, StableAggregateId> = {};
963
+ for (const agg of aggregates) {
964
+ const prior = continuity?.aggregates[agg.id];
965
+ const aggSid = prior?.sid ?? mintAggregateSid(domainSid, agg.id);
966
+ const handleFields = (handlesById.get(agg.id)?.fields ?? {}) as Record<
967
+ string,
968
+ { kind?: string } | undefined
969
+ >;
970
+ const fields: Record<string, StableFieldId> = {};
971
+ for (const fieldName of Object.keys(agg.schema).sort()) {
972
+ const pf = prior?.fields[fieldName];
973
+ const kind = handleFields[fieldName]?.kind;
974
+ fields[fieldName] = {
975
+ sid: pf?.sid ?? mintFieldSid(aggSid, fieldName),
976
+ ...(pf?.was !== undefined && pf.was.length > 0 ? { was: pf.was } : {}),
977
+ // The CURRENT kind, always from the live module (continuity never supplies it).
978
+ ...(kind !== undefined ? { kind } : {}),
979
+ };
980
+ }
981
+ out[agg.id] = {
982
+ sid: aggSid,
983
+ ...(prior?.was !== undefined && prior.was.length > 0 ? { was: prior.was } : {}),
984
+ fields,
985
+ };
986
+ }
987
+ return { v: 1, domain: domainSid, aggregates: out };
988
+ }
989
+
882
990
  /**
883
991
  * Build the canonical semantic manifest for a domain module — purely from the
884
992
  * in-memory DSL objects. No file reads, no bundle, no esbuild: the manifest is a
@@ -910,6 +1018,7 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
910
1018
  // Omit-when-empty: a directive declaring NO boundary contributes neither key,
911
1019
  // so its canonical manifest is byte-identical to before the boundary existed.
912
1020
  ...canonicalReads(d.declaredReads),
1021
+ ...canonicalCapturedReads(d.declaredQueryReads ?? []),
913
1022
  ...canonicalEmits(d.declaredEmits),
914
1023
  ...canonicalCertifiedReads(d.declaredCertifiedReads),
915
1024
  ...canonicalRelations(d.declaredRelations),
@@ -930,6 +1039,9 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
930
1039
  domain: mod.name,
931
1040
  aggregates,
932
1041
  directives,
1042
+ // STABLE IDS (#58): always emitted by this compiler — the one deliberate
1043
+ // hash-mover (enforcement-era identity hashes move; named + expected).
1044
+ stableIds: stableIdsForModule(mod, aggregates),
933
1045
  ...canonicalWorkspaceInvariants(mod.workspaceInvariants),
934
1046
  ...canonicalQueries(mod.queries),
935
1047
  ...canonicalCounts(allCounts.length > 0 ? allCounts : undefined),