@githolon/dsl 0.16.0 → 0.16.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.
@@ -0,0 +1,2 @@
1
+ ## 0.16.0
2
+ - First pub.dev release: kernel wire types, `NomosBridge` protocol, typed offer/query/watch surface.
package/dart/FINDINGS.md CHANGED
@@ -16,14 +16,14 @@ design: the schema is the single source of truth → TS (done), proto (later),
16
16
 
17
17
  ## What was built
18
18
 
19
- `nomos2/dsl/dart/` — a Dart package (`nomos_dsl`):
19
+ `nomos2/dsl/dart/` — a Dart package (`nomos_client`):
20
20
 
21
21
  | Path | Role | Hand-written / generated |
22
22
  |---|---|---|
23
23
  | `lib/src/wire.dart` | Fixed kernel wire types: `Hlc`, `Value` (sealed: `ValueStr`/`ValueInt`/`ValueSet`/`ValueMap`), `WireField`, `Op` (sealed: `OpSet`/`OpAddToSet`/`OpSetEntry`), `FieldOp`, `Event` (`{aggregate, ops}`, #56), `Intent` (`{hlc, events}`, #56). | hand-written |
24
24
  | `lib/src/driver.dart` | `WireDriver` (sealed: `DriverLww`/`DriverAddWins`/`DriverConflict`/`DriverMapOf`) + `Schema`. | hand-written |
25
25
  | `lib/src/generated/trackable_asset.dart` | The `trackable_asset` domain: `TrackableAsset` aggregate, `StatusEnum`, and `CreateAssetPayload`/`MoveAssetPayload`/`TagAssetPayload`/`SetCustomFieldPayload`. | **generated** |
26
- | `lib/nomos_dsl.dart` | Public barrel (exports the three above). | hand-written |
26
+ | `lib/nomos_client.dart` | Public barrel (exports the three above). | hand-written |
27
27
  | `test/wire_test.dart` | Round-trip tests for every wire type + Driver/Schema. | — |
28
28
  | `test/trackable_asset_test.dart` | Read-model aggregate deserialization + directive-payload → Event JSON. | — |
29
29
 
package/dart/LICENSE ADDED
@@ -0,0 +1,45 @@
1
+ # Nomos Pre-Release License (v1)
2
+
3
+ Copyright © 2026 Captain App Ltd. All rights reserved.
4
+
5
+ This is a pre-release of Nomos. This license gives you what you need to BUILD
6
+ with it; we keep the rest for now.
7
+
8
+ ## You may
9
+
10
+ - install and use these packages to author Nomos domains, compile them, and
11
+ deploy them to Nomos Cloud or any Nomos instance Captain App operates or
12
+ authorizes;
13
+ - build, run, and ship applications on top of them — including commercial ones;
14
+ - keep everything that's yours: code you write, and everything these tools
15
+ generate FOR you (scaffolds from `create-githolon` / `githolon generate`, generated
16
+ clients, compiled domain packages) carries NO restriction from us — it is
17
+ yours outright.
18
+
19
+ ## You may not
20
+
21
+ - redistribute these packages or their source, in whole or in part, outside
22
+ your team;
23
+ - modify them or build derivative tools, SDKs, or runtimes from their source;
24
+ - offer the Nomos runtime, or anything materially similar, as a hosted service;
25
+ - reverse-engineer the holon wasm runtime.
26
+
27
+ ## Data retention
28
+
29
+ This is a pre-release we are actively evaluating. Workspaces you retire stop
30
+ counting toward your quota but are NOT deleted: **we retain all workspace data
31
+ (ledgers, law, intents) and may examine it to evaluate how the product is
32
+ used.** Don't put anything in a pre-release workspace you wouldn't want the
33
+ builders to read. If you need something truly expunged, ask:
34
+ jack@captainapp.co.uk.
35
+
36
+ ## The rest
37
+
38
+ Provided **as is**, with no warranty of any kind; to the maximum extent
39
+ permitted by law, Captain App Ltd accepts no liability arising from your use.
40
+ This license terminates automatically if you breach it.
41
+
42
+ Want the kernel source, broader rights, or to do something this doesn't cover?
43
+ **Ask: jack@captainapp.co.uk.** The plan is to open Nomos up gradually, with
44
+ the people actually using it — telling us what you're building is how that
45
+ happens faster.
package/dart/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # nomos_client
2
+
3
+ Dart frontend types for [Nomos](https://github.com/Captain-App/nomos2) — the hand-written kernel
4
+ wire types plus the code-generated per-domain aggregate + directive-payload types. (De)serializes
5
+ the exact serde JSON the Rust kernel + projection produce/consume, and defines the `NomosBridge`
6
+ protocol the runtime ([`nomos_flutter`](https://pub.dev/packages/nomos_flutter)) drives.
7
+
8
+ You normally depend on `nomos_flutter` (which re-exports the bridge surface) rather than this package
9
+ directly. Licensed under the Nomos Pre-Release License — see `LICENSE`.
@@ -1,8 +1,8 @@
1
- // COPY THIS INTO YOUR FLUTTER APP. It is an EXAMPLE, not part of the `nomos_dsl` library: it depends on
2
- // `webview_flutter`, which the pure-Dart `nomos_dsl` package deliberately does NOT depend on (so the bridge
1
+ // COPY THIS INTO YOUR FLUTTER APP. It is an EXAMPLE, not part of the `nomos_client` library: it depends on
2
+ // `webview_flutter`, which the pure-Dart `nomos_client` package deliberately does NOT depend on (so the bridge
3
3
  // protocol logic stays Flutter-free and unit-testable). This ~50-line transport is the only app-specific glue.
4
4
  //
5
- // It implements `BridgeTransport` (from `package:nomos_dsl/nomos_dsl.dart`) over a `webview_flutter`
5
+ // It implements `BridgeTransport` (from `package:nomos_client/nomos_client.dart`) over a `webview_flutter`
6
6
  // WebViewController loading the published `nomos-runner` bundle:
7
7
  // • host → runner: `controller.runJavaScript("window.__nomosSend(<json>)")`
8
8
  // • runner → host: a `JavaScriptChannel("NomosHost")` whose onMessage feeds the bridge's message stream.
@@ -24,7 +24,7 @@
24
24
  import 'dart:async';
25
25
  import 'dart:convert';
26
26
 
27
- import 'package:nomos_dsl/nomos_dsl.dart';
27
+ import 'package:nomos_client/nomos_client.dart';
28
28
  import 'package:webview_flutter/webview_flutter.dart';
29
29
 
30
30
  /// A [BridgeTransport] over a `webview_flutter` controller hosting the `nomos-runner` bundle.
@@ -40,7 +40,7 @@ abstract interface class NomosIntent {
40
40
 
41
41
  /// The kernel write seam: the FRB `NomosClient.author` signature, as a function
42
42
  /// type. `offer` takes this (rather than importing the FRB client) so the
43
- /// generated `nomos_dsl` package stays free of a `flutter_rust_bridge` dependency
43
+ /// generated `nomos_client` package stays free of a `flutter_rust_bridge` dependency
44
44
  /// — the app threads `client.author` in. Returns the new HEAD commit oid (hex).
45
45
  typedef Authorer = String Function({
46
46
  required String domain,
@@ -70,7 +70,7 @@ String offer(NomosIntent intent,
70
70
  // this seam to RESERVE a typed, type-tagged `<Type>_<uuidv7>` id, which it ships in the
71
71
  // create payload; the kernel's GATE verifies its shape + type-tag prefix on author. Like
72
72
  // [Authorer], this is a function type the app threads in (the FRB
73
- // `NomosClient.mintIdForWorkspace`), so the generated `nomos_dsl` package stays free of a
73
+ // `NomosClient.mintIdForWorkspace`), so the generated `nomos_client` package stays free of a
74
74
  // `flutter_rust_bridge` dependency.
75
75
  //
76
76
  // WHERE THE RANDOMNESS LIVES (the model — the dev touches NONE of it): the KERNEL mints
@@ -38,7 +38,7 @@ import 'dart:convert';
38
38
  /// data}` rows (O(matches), never a workspace scan — #140 READ-CLOSURE).
39
39
  ///
40
40
  /// `subscriptions.dart` takes this (rather than importing a host client) so the
41
- /// generated `nomos_dsl` package stays host-agnostic. The app threads `client.query`
41
+ /// generated `nomos_client` package stays host-agnostic. The app threads `client.query`
42
42
  /// in. `paramsJson` is a JSON object carrying the
43
43
  /// key field + value, e.g. `{"estateId":"e-1"}`.
44
44
  typedef Queryer = String Function(String queryId, String paramsJson);
package/dart/pubspec.yaml CHANGED
@@ -5,13 +5,13 @@
5
5
  # wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
6
6
  # If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
7
 
8
- name: nomos_dsl
8
+ name: nomos_client
9
+ repository: https://github.com/Captain-App/nomos2
9
10
  description: >-
10
11
  Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
11
12
  code-generated per-domain aggregate + directive-payload types. (De)serializes
12
13
  the EXACT serde JSON the Rust kernel + projection produce/consume.
13
- version: 0.0.0
14
- publish_to: none
14
+ version: 0.16.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
@@ -5,7 +5,7 @@
5
5
  // wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
6
6
  // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
7
 
8
- import 'package:nomos_dsl/nomos_dsl.dart';
8
+ import 'package:nomos_client/nomos_client.dart';
9
9
  import 'package:test/test.dart';
10
10
 
11
11
  void main() {
@@ -7,7 +7,7 @@ import 'dart:async';
7
7
  import 'dart:convert';
8
8
  import 'dart:io';
9
9
 
10
- import 'package:nomos_dsl/nomos_dsl.dart';
10
+ import 'package:nomos_client/nomos_client.dart';
11
11
  import 'package:test/test.dart';
12
12
 
13
13
  /// A fake runner-over-a-transport: records sent requests, replies the way `webview-runner.mjs` does, and can
@@ -7,7 +7,7 @@
7
7
 
8
8
  import 'dart:convert';
9
9
 
10
- import 'package:nomos_dsl/nomos_dsl.dart';
10
+ import 'package:nomos_client/nomos_client.dart';
11
11
  import 'package:test/test.dart';
12
12
 
13
13
  /// Deep round-trip: a JSON string -> Dart fromJson -> toJson -> re-encoded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.16.0",
3
+ "version": "0.16.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",
@@ -2742,11 +2742,11 @@ export function generateDartDomain(mod: DomainModule): string {
2742
2742
  `// ignore_for_file: unused_import`,
2743
2743
  ``,
2744
2744
  // The generated tenant domains live in the `co2_nomos_domains` package and depend
2745
- // on the framework `nomos_dsl` package by its PUBLIC package: URI — NOT a relative
2745
+ // on the framework `nomos_client` package by its PUBLIC package: URI — NOT a relative
2746
2746
  // `../wire.dart` path into the framework (which broke when the generated dir moved
2747
- // out of nomos_dsl into the co-located tenant package). The framework barrel
2747
+ // out of nomos_client into the co-located tenant package). The framework barrel
2748
2748
  // re-exports wire/dispatch/subscriptions/provider, so one import covers them all.
2749
- `import 'package:nomos_dsl/nomos_dsl.dart';`,
2749
+ `import 'package:nomos_client/nomos_client.dart';`,
2750
2750
  ...(mod.dartImports ?? []).map(dartImportLine),
2751
2751
  ``,
2752
2752
  `// ---- enums ----`,
@@ -22,7 +22,7 @@
22
22
  * * typed O(1) count/sum accessors.
23
23
  *
24
24
  * It IMPORTS the sibling `<name>.dart` (for the read-model + payload classes) and either the vendored
25
- * `nomos_dsl.dart` barrel or the package `nomos_dsl` barrel (for `NomosBridge`). NO manifest/hash impact —
25
+ * `nomos_client.dart` barrel or the package `nomos_client` barrel (for `NomosBridge`). NO manifest/hash impact —
26
26
  * pure frontend codegen, exactly like `codegen_dart.ts`. NO FALLBACK: an unsupported shape throws at compile
27
27
  * time.
28
28
  */
@@ -108,8 +108,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
108
108
  const hashConst = dartHashConstName(mod.name);
109
109
  const supportImport =
110
110
  opts.supportImport === "package"
111
- ? "package:nomos_dsl/nomos_dsl.dart"
112
- : "nomos_dsl.dart";
111
+ ? "package:nomos_client/nomos_client.dart"
112
+ : "nomos_client.dart";
113
113
  const aggById = new Map<string, AggregateHandle>();
114
114
  for (const a of mod.aggregates) aggById.set(a.id, a);
115
115
 
@@ -0,0 +1,239 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted remotes are
2
+ // replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon wasm32-wasip1 artifact.
3
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
4
+
5
+ /**
6
+ * codegen_openapi.ts — THE DOMAIN'S SELF-DESCRIPTION (architecture/githolon_host_contract.md,
7
+ * "The Self-Describing API"). Each domain emits an OpenAPI fragment FROM ITS OWN ZOD at compile
8
+ * time — the same schemas the engine validates, lowered via `z.toJSONSchema`. This is the law's
9
+ * own product, never host code: the host INGESTS the workspace's merged spec at runtime and builds
10
+ * its router from it (path → `offer`/`query` rewrite), so no domain or directive name ever lives in
11
+ * host source.
12
+ *
13
+ * Each operation is an OFFER you can make to the workspace (the one effectful move) or a read:
14
+ * - an offerable intent → `POST /{directiveId}` whose requestBody is the intent's payload JSON
15
+ * Schema. On the wire an intent names exactly one directive, so `directiveId` is the offer
16
+ * SELECTOR (it rides in `x-nomos`, not as "the API"); the directive's PLAN — its fan-out into
17
+ * aggregates/events — is private and never appears here;
18
+ * - every READ → a `GET`: declared queries (+ the hasMany inverse indexes) by their key, and the
19
+ * maintained primitives — counts / sums / extremums (min,max) / spatial — by `group`/`bbox`;
20
+ * - each operation carries an `x-nomos` extension — `{ kind: "offer"|"query"|"count"|"sum"|
21
+ * "extremum"|"spatial", domain, … }` — the ONLY thing the host reads to fold a matched request
22
+ * into the opaque intent/query/read envelope.
23
+ *
24
+ * THE WORKSPACE SPEC is the runtime MERGE of its installed domains' fragments (which are active is
25
+ * `currentLaw`). `mergeOpenApiFragments` is that merge: a plain union today (no domain cascades on
26
+ * another's API SURFACE — scoped reads / `.encrypted` / `requires(cap)` change runtime BEHAVIOUR,
27
+ * not operation SHAPE), with two law-blind structural touch-ups — NAMESPACE a cross-domain path
28
+ * collision under `/{domain}/…` so neither domain's operation clobbers the other (operations carry
29
+ * their domain in `operationId`, so two domains' same-named directives ARE distinct ops and collide),
30
+ * and DEDUPE a byte-identical operation (defensive — e.g. a fragment seen twice). The merge reads
31
+ * only structure; if a real cross-domain cascade ever lands, THIS is the seam for it.
32
+ */
33
+ import { z } from "zod";
34
+ import type { DomainModule } from "./codegen_dart.js";
35
+ import type { Directive } from "./directive.js";
36
+ import type { QueryDecl } from "./query.js";
37
+ import { hasManyIndexes } from "./query.js";
38
+ import { finishCount } from "./count.js";
39
+ import { finishSum } from "./sum.js";
40
+ import { finishExtremum } from "./extremum.js";
41
+
42
+ type AnyDirective = Directive<unknown>;
43
+
44
+ const GROUP_PARAM = { name: "group", in: "query", required: false, schema: { type: "string" } } as const;
45
+
46
+ /** Minimal OpenAPI 3.1 document shape we emit (open object — extensions ride as `x-…` keys). */
47
+ export interface OpenApiDoc {
48
+ openapi: "3.1.0";
49
+ info: Record<string, unknown>;
50
+ paths: Record<string, Record<string, unknown>>;
51
+ }
52
+
53
+ export interface OpenApiFragmentOptions {
54
+ /** The content hash of the installed law (the spec's version + ingest cache key). */
55
+ lawHash?: string;
56
+ }
57
+
58
+ /** Lower a directive's payload zod to JSON Schema; a non-representable schema falls back to a
59
+ * permissive object rather than failing the whole compile (the engine still validates the real zod). */
60
+ function payloadJsonSchema(d: AnyDirective): unknown {
61
+ try {
62
+ return z.toJSONSchema((d as { payloadSchema: z.ZodTypeAny }).payloadSchema, { io: "input" });
63
+ } catch {
64
+ return { type: "object", additionalProperties: true };
65
+ }
66
+ }
67
+
68
+ /** The domain key the engine resolves intents against (the `author` registry key). */
69
+ function domainKey(module: DomainModule): string {
70
+ return module.domain ?? module.name;
71
+ }
72
+
73
+ /**
74
+ * generateOpenApiFragment — one domain → its OpenAPI fragment. Pure: same module ⇒ same fragment.
75
+ * Paths are bare (`/{id}`); the workspace gateway mounts the merged spec under `/ws/{holonId}`, and
76
+ * the merge namespaces any cross-domain collision. The offer/query distinction rides on the HTTP
77
+ * method (POST/GET) AND the authoritative `x-nomos.kind` — the host reads the latter, never the name.
78
+ */
79
+ export function generateOpenApiFragment(
80
+ module: DomainModule,
81
+ opts: OpenApiFragmentOptions = {},
82
+ ): OpenApiDoc {
83
+ const domain = domainKey(module);
84
+ const paths: Record<string, Record<string, unknown>> = {};
85
+
86
+ // THE GATE AUDIT INDEX: every offer must show its protection, so enumerating the spec IS the security
87
+ // audit. `requires` = the .requires relation gate; `invariants` = the workspaceInvariants that judge it
88
+ // (e.g. provisionWorkspace's ownerGrantHash==foundingAdminHash). An offer with NEITHER is a VISIBLE
89
+ // red flag (a raw ungated primitive like relateSubject). The spec hides nothing — that's the point.
90
+ const invariantsOf = (directiveId: string) =>
91
+ (module.workspaceInvariants ?? []).filter((i) => i.on === directiveId).map((i) => i.id);
92
+
93
+ // OFFERABLE INTENTS → POST offers. The payload JSON Schema is the intent's own zod, lowered.
94
+ for (const dir of module.directives as AnyDirective[]) {
95
+ const invs = invariantsOf(dir.id);
96
+ const op: Record<string, unknown> = {
97
+ operationId: `${domain}__${dir.id}`,
98
+ tags: [domain],
99
+ summary: `offer: ${dir.id}`,
100
+ description: dir.marker && dir.aggregateId
101
+ ? `Offer the \`${dir.id}\` intent (${dir.marker} ${dir.aggregateId}). Admitted or refused by the law; the plan is private.`
102
+ : `Offer the \`${dir.id}\` intent. Admitted or refused by the law.`,
103
+ "x-nomos": {
104
+ kind: "offer",
105
+ domain,
106
+ directiveId: dir.id,
107
+ ...(dir.marker ? { marker: dir.marker } : {}),
108
+ ...(dir.aggregateId ? { aggregate: dir.aggregateId } : {}),
109
+ ...(dir.requiresRelationGate ? { requires: dir.requiresRelationGate.relation } : {}),
110
+ ...(invs.length ? { invariants: invs } : {}),
111
+ },
112
+ requestBody: {
113
+ required: true,
114
+ content: { "application/json": { schema: payloadJsonSchema(dir) } },
115
+ },
116
+ responses: {
117
+ "200": { description: "admitted — the sealed intent head" },
118
+ "401": { description: "no identity / identity refused" },
119
+ "422": { description: "the law refused the intent (typed refusal)" },
120
+ },
121
+ };
122
+ (paths[`/${dir.id}`] ??= {}).post = op;
123
+ }
124
+
125
+ // QUERIES → GET reads. Declared queries + the implicit hasMany inverse indexes (the relation IS
126
+ // the index) — both are reads the workspace serves; both belong in the spec.
127
+ const queries: QueryDecl[] = [
128
+ ...(module.queries ?? []),
129
+ ...module.aggregates.flatMap((agg) => hasManyIndexes(agg)),
130
+ ];
131
+ for (const q of queries) {
132
+ const op: Record<string, unknown> = {
133
+ operationId: `${domain}__${q.id}`,
134
+ tags: [domain],
135
+ summary: `read ${q.returns} by ${q.key.join(", ") || "—"}`,
136
+ "x-nomos": { kind: "query", domain, queryId: q.id, key: q.key, returns: q.returns },
137
+ parameters: q.key.map((k) => ({ name: k, in: "query", required: true, schema: { type: "string" } })),
138
+ responses: { "200": { description: `rows of ${q.returns}` } },
139
+ };
140
+ (paths[`/${q.id}`] ??= {}).get = op;
141
+ }
142
+
143
+ // THE MAINTAINED READ PRIMITIVES — counts / sums / extremums (min,max) / spatial. Each is an O(1)
144
+ // read the workspace serves; each gets its GET. (Declared queries above are the indexed-row reads;
145
+ // these are the tallies + the geo index. Derived/combined fields are read-MODEL fields, surfaced on
146
+ // the aggregate's query rows — not their own endpoints.)
147
+ const getRead = (id: string, summary: string, xnomos: Record<string, unknown>, params: unknown[]) => {
148
+ (paths[`/${id}`] ??= {}).get = {
149
+ operationId: `${domain}__${id}`,
150
+ tags: [domain],
151
+ summary,
152
+ "x-nomos": { domain, ...xnomos },
153
+ parameters: params,
154
+ responses: { "200": { description: summary } },
155
+ };
156
+ };
157
+ for (const raw of module.counts ?? []) {
158
+ const c = finishCount(raw);
159
+ getRead(c.id, `count: ${c.of}${c.by ? ` by ${c.by}` : ""}`, { kind: "count", countId: c.id, ...(c.by ? { group: c.by } : {}) }, c.by ? [GROUP_PARAM] : []);
160
+ }
161
+ for (const raw of module.sums ?? []) {
162
+ const s = finishSum(raw);
163
+ getRead(s.id, `sum: ${s.sumField} of ${s.of}${s.by ? ` by ${s.by}` : ""}`, { kind: "sum", sumId: s.id, sumField: s.sumField, ...(s.by ? { group: s.by } : {}) }, s.by ? [GROUP_PARAM] : []);
164
+ }
165
+ for (const [key, kind] of [["mins", "min"], ["maxes", "max"]] as const) {
166
+ for (const raw of (module as unknown as Record<string, unknown[]>)[key] ?? []) {
167
+ const e = finishExtremum(raw as never);
168
+ getRead(e.id, `${kind}: ${e.valueField} of ${e.of}${e.by ? ` by ${e.by}` : ""}`, { kind: "extremum", extremumId: e.id, extremumKind: e.kind, ...(e.by ? { group: e.by } : {}) }, e.by ? [GROUP_PARAM] : []);
169
+ }
170
+ }
171
+ for (const sp of module.spatials ?? []) {
172
+ getRead(sp.id, `spatial: ${sp.of} by ${sp.on} bbox`, { kind: "spatial", spatialId: sp.id, on: sp.on }, [
173
+ { name: "bbox", in: "query", required: true, schema: { type: "string" }, description: "minLng,minLat,maxLng,maxLat" },
174
+ ]);
175
+ }
176
+
177
+ return {
178
+ openapi: "3.1.0",
179
+ info: {
180
+ title: `${domain} (nomos domain)`,
181
+ version: opts.lawHash ? opts.lawHash.slice(0, 12) : "0",
182
+ "x-nomos-domain": domain,
183
+ ...(opts.lawHash ? { "x-nomos-law-hash": opts.lawHash } : {}),
184
+ },
185
+ paths,
186
+ };
187
+ }
188
+
189
+ /** Stable JSON of an operation, to detect a genuine duplicate vs a real collision in the merge. */
190
+ function operationKey(op: unknown): string {
191
+ return JSON.stringify(op);
192
+ }
193
+
194
+ /**
195
+ * mergeOpenApiFragments — the WORKSPACE spec = the union of its installed domains' fragments. The
196
+ * runtime step (driven by `currentLaw`), kept here as a pure, law-blind function:
197
+ * - UNION the paths;
198
+ * - DEDUPE an operation declared identically by several domains (framework aggregates) — one entry;
199
+ * - NAMESPACE a genuine collision (same path+method, DIFFERENT operation, different domains) under
200
+ * `/{domain}/{path}` so neither is clobbered.
201
+ * It reads only structure (paths, methods, the `x-nomos.domain` tag) — never a directive's meaning.
202
+ */
203
+ export function mergeOpenApiFragments(
204
+ fragments: readonly OpenApiDoc[],
205
+ opts: { title?: string; lawHash?: string } = {},
206
+ ): OpenApiDoc {
207
+ const paths: Record<string, Record<string, unknown>> = {};
208
+ for (const frag of fragments) {
209
+ for (const [path, methods] of Object.entries(frag.paths)) {
210
+ for (const [method, op] of Object.entries(methods)) {
211
+ const existing = paths[path]?.[method];
212
+ if (existing === undefined) {
213
+ (paths[path] ??= {})[method] = op;
214
+ } else if (operationKey(existing) === operationKey(op)) {
215
+ // identical (a framework operation shared by many domains) — keep the one entry.
216
+ } else {
217
+ // genuine collision: two domains, same path+method, different operations. Namespace BOTH
218
+ // under their domain so neither wins silently.
219
+ const domOf = (o: unknown): string =>
220
+ String((o as { "x-nomos"?: { domain?: string } })?.["x-nomos"]?.domain ?? "domain");
221
+ const bucket = paths[path]!;
222
+ (paths[`/${domOf(existing)}${path}`] ??= {})[method] = existing;
223
+ (paths[`/${domOf(op)}${path}`] ??= {})[method] = op;
224
+ delete bucket[method];
225
+ if (Object.keys(bucket).length === 0) delete paths[path];
226
+ }
227
+ }
228
+ }
229
+ }
230
+ return {
231
+ openapi: "3.1.0",
232
+ info: {
233
+ title: opts.title ?? "nomos workspace",
234
+ version: opts.lawHash ? opts.lawHash.slice(0, 12) : "0",
235
+ ...(opts.lawHash ? { "x-nomos-law-hash": opts.lawHash } : {}),
236
+ },
237
+ paths,
238
+ };
239
+ }
@@ -332,6 +332,12 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
332
332
  : JSON.stringify(baseValue);
333
333
  const queryParamsObj = query ? Object.fromEntries(query.key.map((k) => [k, baseValue[k]])) : undefined;
334
334
  const queryParams = queryParamsObj ? JSON.stringify(queryParamsObj) : "";
335
+ // The gateway routes a declared query by its id: GET /v2/workspaces/<ws>/<queryId>?<key>=<val>
336
+ // (the host reads each declared key from the querystring — never a generic /query?params= lane).
337
+ const queryQs =
338
+ query && queryParamsObj
339
+ ? query.key.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(String(queryParamsObj[k]))}`).join("&")
340
+ : "";
335
341
  const listAccessor = `list${agg.id}s`;
336
342
 
337
343
  // THE MACHINE-READABLE LEGS (parsed by `githolon`'s offline proof runner —
@@ -376,7 +382,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
376
382
  `try {`,
377
383
  ``,
378
384
  `// ${n()}. a throwaway workspace (the ONE-TIME secret comes back on create)`,
379
- `let r = await fetch(\`\${CLOUD}/v1/workspaces/\${WS}\`, { method: "POST", headers: { "x-nomos-principal": "githolon-proof" } });`,
385
+ `let r = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}\`, { method: "POST", headers: { "x-nomos-principal": "githolon-proof" } });`,
380
386
  `let d = await r.json();`,
381
387
  `if (!d.ok) fail(\`create workspace: \${JSON.stringify(d)}\`);`,
382
388
  `SECRET = d.workspaceSecret;`,
@@ -384,7 +390,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
384
390
  `ok(\`workspace \${WS} created (one-time secret held for cleanup — PROOF_KEEP=1 keeps the workspace and prints it)\`);`,
385
391
  ``,
386
392
  `// ${n()}. deploy YOUR compiled law (build/${opts.packageName}.deploy.json) WITH the secret`,
387
- `r = await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/domains\`, { method: "POST", headers: { "content-type": "application/json", authorization: \`Bearer \${SECRET}\` }, body: JSON.stringify(deploy) });`,
393
+ `r = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/domains\`, { method: "POST", headers: { "content-type": "application/json", authorization: \`Bearer \${SECRET}\` }, body: JSON.stringify(deploy) });`,
388
394
  `d = await r.json();`,
389
395
  `if (!d.ok || d.installation?.[0]?.data?.["status.phase"] !== "Active") fail(\`deploy: \${JSON.stringify(d)}\`);`,
390
396
  `if (d.domainHash !== ${hashConst}) fail(\`deploy hash \${d.domainHash} != the generated client's \${${hashConst}}\`);`,
@@ -463,8 +469,9 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
463
469
 
464
470
  if (query) {
465
471
  out.push(
466
- `// ${n()}. the CLOUD answers the SAME declared query (read-manifest overlay, edge projection)`,
467
- `d = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/query?queryId=${query.id}&params=\${encodeURIComponent(${JSON.stringify(queryParams)})}\`)).json();`,
472
+ `// ${n()}. the CLOUD answers the SAME declared query — routed by the workspace's OWN OpenAPI`,
473
+ `// (GET /v2/workspaces/<ws>/${query.id}?${queryQs}); the host learns the route from the law's spec.`,
474
+ `d = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/${query.id}${queryQs ? `?${queryQs}` : ``}\`)).json();`,
468
475
  `if (!(d.ok && d.rows.length === 1 && d.rows[0].id === createdId)) fail(\`cloud declared query: \${JSON.stringify(d)}\`);`,
469
476
  `ok("cloud declared query ${query.id} returns the admitted row — full loop closed");`,
470
477
  ``,
@@ -472,7 +479,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
472
479
  } else {
473
480
  out.push(
474
481
  `// ${n()}. the CLOUD answers the by-id read (edge projection of the admitted write)`,
475
- `d = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/aggregates/\${encodeURIComponent(createdId)}\`)).json();`,
482
+ `d = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/aggregates/\${encodeURIComponent(createdId)}\`)).json();`,
476
483
  `if (!(d.rows?.length === 1 && d.rows[0].id === createdId)) fail(\`cloud by-id read: \${JSON.stringify(d)}\`);`,
477
484
  `ok("cloud read returns the admitted row — full loop closed");`,
478
485
  ``,
@@ -482,7 +489,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
482
489
  if (count) {
483
490
  out.push(
484
491
  `// ${n()}. the EDGE maintains the same count (same wasm, same law)`,
485
- `d = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/counts/${count.id}${count.by != null ? `?group=\${encodeURIComponent(${JSON.stringify(String(baseValue[count.by]))})}` : ``}\`)).json();`,
492
+ `d = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/${count.id}${count.by != null ? `?group=\${encodeURIComponent(${JSON.stringify(String(baseValue[count.by]))})}` : ``}\`)).json();`,
486
493
  `if (!(d.ok && d.count === 1)) fail(\`cloud count ${count.id}: \${JSON.stringify(d)}\`);`,
487
494
  `ok("cloud count ${count.id} = 1 — the O(1) maintained read, at the edge too");`,
488
495
  ``,
@@ -510,7 +517,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
510
517
  `await ${factory}(writerB).${cd.id}(${concPayload("b")});`,
511
518
  `await writerA.sync();`,
512
519
  `await writerB.sync();`,
513
- `d = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/aggregates/\${encodeURIComponent(createdId)}\`)).json();`,
520
+ `d = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/aggregates/\${encodeURIComponent(createdId)}\`)).json();`,
514
521
  `const merged: string[] = d.rows?.[0]?.data?.[${JSON.stringify(setField)}] ?? [];`,
515
522
  `if (!(merged.includes("proof-a") && merged.includes("proof-b"))) fail(\`AddWins union lost an add: \${JSON.stringify(merged)}\`);`,
516
523
  `ok(\`two blind writers' concurrent adds BOTH survive the merge — ${setField} [\${[...merged].sort().join(", ")}]\`);`,
@@ -529,7 +536,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
529
536
  `// ${n()}. CONVERGENCE: the original instance adopts canonical main in place — no reconnect`,
530
537
  `await holon.pull();`,
531
538
  `const head = await holon.head();`,
532
- `const refsAdv = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/git/info/refs?service=git-upload-pack\`)).text();`,
539
+ `const refsAdv = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/git/info/refs?service=git-upload-pack\`)).text();`,
533
540
  `const remoteMain = (refsAdv.match(/([0-9a-f]{40}) refs\\/heads\\/main/) || [])[1];`,
534
541
  `if (head !== remoteMain) fail(\`not converged in place: local \${head?.slice(0, 10)} vs main \${remoteMain?.slice(0, 10)}\`);`,
535
542
  `ok("converged in place — local head == canonical main");`,
@@ -546,7 +553,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
546
553
  ` console.log(\`workspace \${WS} KEPT — secret \${SECRET}\`);`,
547
554
  ` console.log(\` persist it: githolon secret set \${WS} <the secret above> (then: githolon ws retire \${WS} when done)\`);`,
548
555
  ` } else if (SECRET) {`,
549
- ` const rr = await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/retire\`, { method: "POST", headers: { authorization: \`Bearer \${SECRET}\` } }).then((x) => x.json()).catch(() => ({}));`,
556
+ ` const rr = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/retire\`, { method: "POST", headers: { authorization: \`Bearer \${SECRET}\` } }).then((x) => x.json()).catch(() => ({}));`,
550
557
  ` console.log(\` retired \${WS}: \${(rr as { ok?: boolean }).ok === true}\`);`,
551
558
  ` }`,
552
559
  ` process.exit(runError ? 1 : 0);`,
package/src/codegen_ts.ts CHANGED
@@ -419,8 +419,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
419
419
 
420
420
  // ── DERIVED BIRTH LANES (sharding §10 RATIFIED, slice 1) ──
421
421
  // A `workspaceType(...).hasMany(...)` taxonomy DERIVES typed birth helpers over
422
- // the EXISTING platform machinery (`POST /v1/platforms/...` — derived, never
423
- // forked). Fully typesafe per Jack's ruling: the caller passes LOGICAL names
422
+ // the EXISTING birth machinery (the boot lane + the parent's birthPlatform/createWorkspace
423
+ // governance offers, `POST /v2/workspaces/...` — derived, never forked). Fully typesafe
424
+ // per Jack's ruling: the caller passes LOGICAL names
424
425
  // (parent param named after the parent TYPE); the `p--ws` composition is internal
425
426
  // plumbing — the composed workspace name comes back on the result for `connect()`.
426
427
  // PACKED child types derive NO holon-birth helper: a packed birth is a shard-map
@@ -495,16 +496,25 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
495
496
  ` [k: string]: unknown;`,
496
497
  `}`,
497
498
  ``,
498
- `const nomosBirth = async (cloud: string, path: string, auth: NomosBirthAuth): Promise<NomosBirthResult> => {`,
499
+ `const nomosBirth = async (cloud: string, path: string, auth: NomosBirthAuth, body?: unknown): Promise<NomosBirthResult> => {`,
499
500
  ` const headers: Record<string, string> = {};`,
500
501
  ` if (auth.authToken !== undefined) headers["x-nomos-auth"] = auth.authToken;`,
501
502
  ` if (auth.principal !== undefined) headers["x-nomos-principal"] = auth.principal;`,
502
503
  ` if (auth.bearer !== undefined) headers["authorization"] = \`Bearer \${auth.bearer}\`;`,
503
504
  ` if (auth.platformAdminKey !== undefined) headers["x-nomos-platform-admin-key"] = auth.platformAdminKey;`,
504
- ` const res = await fetch(\`\${cloud.replace(/\\/$/, "")}\${path}\`, { method: "POST", headers });`,
505
+ ` if (body !== undefined) headers["content-type"] = "application/json";`,
506
+ ` const res = await fetch(\`\${cloud.replace(/\\/$/, "")}\${path}\`, {`,
507
+ ` method: "POST", headers, ...(body !== undefined ? { body: JSON.stringify(body) } : {}),`,
508
+ ` });`,
505
509
  ` return (await res.json()) as NomosBirthResult;`,
506
510
  `};`,
507
511
  ``,
512
+ `/** sha256-hex of the chosen platform admin key — the on-chain \`foundingAdminHash\` (the host never mints it). */`,
513
+ `const nomosKeyHash = async (key: string): Promise<string> => {`,
514
+ ` const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key));`,
515
+ ` return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");`,
516
+ `};`,
517
+ ``,
508
518
  );
509
519
  const emittedBirths = new Set<string>();
510
520
  for (const mod of birthModules) {
@@ -518,13 +528,40 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
518
528
  }
519
529
  const childIds = new Set([...types.values()].flatMap((t) => [...t.childIds]));
520
530
  const fnName = (typeId: string) => `birth${cap(camel(typeId))}`;
521
- const emit = (name: string, doc: string, params: string[], pathExpr: string) => {
531
+ // THE GATEWAY BIRTH LANES (post-/v2): a top-level WORKSPACE births on the boot lane
532
+ // (POST /v2/workspaces/<name> — returns the one-time secret); every other birth is a
533
+ // GOVERNANCE OFFER routed by the parent's OpenAPI (POST /v2/workspaces/<parentWs>/<directiveId>),
534
+ // the obligation the birth provider materializes. parentWs = "root" for a top-level platform,
535
+ // else the parent holon's own name. timestamps ride IN the payload (the determinism doctrine);
536
+ // a platform's foundingAdminHash = sha256(your chosen platformAdminKey) — the host never mints it.
537
+ const emit = (name: string, doc: string, params: string[], lane: { kind: "boot" } | { kind: "offer"; parentWs: string; directiveId: "createWorkspace" | "birthPlatform" }) => {
522
538
  if (emittedBirths.has(name)) return;
523
539
  emittedBirths.add(name);
540
+ const sig = `opts: NomosBirthAuth & { cloud: string; ${params.join("; ")} }`;
541
+ if (lane.kind === "boot") {
542
+ out.push(
543
+ `/** ${doc} */`,
544
+ `export const ${name} = (${sig}): Promise<NomosBirthResult> =>`,
545
+ ` nomosBirth(opts.cloud, \`/v2/workspaces/\${encodeURIComponent(opts.name)}\`, opts);`,
546
+ ``,
547
+ );
548
+ return;
549
+ }
550
+ const isPlatformOffer = lane.directiveId === "birthPlatform";
551
+ const bodyFields = [
552
+ `name: opts.name`,
553
+ `parent: ${lane.parentWs}`,
554
+ `principal: opts.principal ?? ""`,
555
+ `requestedBy: opts.principal ?? ""`,
556
+ `requestedAt: Date.now()`,
557
+ ...(isPlatformOffer ? [`foundingAdminHash: await nomosKeyHash(opts.platformAdminKey)`] : []),
558
+ ];
524
559
  out.push(
525
560
  `/** ${doc} */`,
526
- `export const ${name} = (opts: NomosBirthAuth & { cloud: string; ${params.join("; ")} }): Promise<NomosBirthResult> =>`,
527
- ` nomosBirth(opts.cloud, ${pathExpr}, opts);`,
561
+ `export const ${name} = async (${sig}): Promise<NomosBirthResult> =>`,
562
+ ` nomosBirth(opts.cloud, \`/v2/workspaces/\${encodeURIComponent(${lane.parentWs})}/${lane.directiveId}\`, opts, {`,
563
+ ` ${bodyFields.join(", ")},`,
564
+ ` });`,
528
565
  ``,
529
566
  );
530
567
  };
@@ -532,14 +569,15 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
532
569
  if (t.mode === "packed") continue; // placement, not a holon birth (slice 2)
533
570
  const isPlatform = t.childIds.length > 0;
534
571
  if (!childIds.has(t.id)) {
535
- // A TOP-LEVEL type: births directly under the cloud.
572
+ // A TOP-LEVEL type: a workspace births on the boot lane; a platform-kind
573
+ // coordinator births via the root holon's birthPlatform offer.
536
574
  emit(
537
575
  fnName(t.id),
538
- `Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree; PRESENT your chosen platformAdminKey)" : ""} — derived from the taxonomy over the existing ${isPlatform ? "platform" : "workspace"} lane. Placement is automatic (measured shape) — no tier to pick.`,
576
+ `Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree; PRESENT your chosen platformAdminKey) via root's birthPlatform offer" : " on the boot lane (the one-time secret comes back)"} — derived from the taxonomy. Placement is automatic (measured shape) — no tier to pick.`,
539
577
  isPlatform ? [`name: string`, `platformAdminKey: string`] : [`name: string`],
540
578
  isPlatform
541
- ? `\`/v1/platforms/\${encodeURIComponent(opts.name)}\``
542
- : `\`/v1/workspaces/\${encodeURIComponent(opts.name)}\``,
579
+ ? { kind: "offer", parentWs: `"root"`, directiveId: "birthPlatform" }
580
+ : { kind: "boot" },
543
581
  );
544
582
  }
545
583
  for (const childId of t.childIds) {
@@ -549,13 +587,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
549
587
  const childIsPlatform = child.childIds.length > 0;
550
588
  emit(
551
589
  fnName(child.id),
552
- `Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge over the existing platform lane (lineage + pool enforced by the parent's law).${childIsPlatform ? " PRESENT your chosen platformAdminKey." : ""} Placement is automatic (measured shape).`,
590
+ `Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge, a governance offer on the parent holon (POST /v2/workspaces/<${parentParam}>/${childIsPlatform ? "birthPlatform" : "createWorkspace"}; lineage + pool enforced by the parent's law).${childIsPlatform ? " PRESENT your chosen platformAdminKey." : ""} Placement is automatic (measured shape).`,
553
591
  childIsPlatform
554
592
  ? [`${parentParam}: string`, `name: string`, `platformAdminKey: string`]
555
593
  : [`${parentParam}: string`, `name: string`],
556
- childIsPlatform
557
- ? `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/platforms/\${encodeURIComponent(opts.name)}\``
558
- : `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/workspaces/\${encodeURIComponent(opts.name)}\``,
594
+ { kind: "offer", parentWs: `opts.${parentParam}`, directiveId: childIsPlatform ? "birthPlatform" : "createWorkspace" },
559
595
  );
560
596
  }
561
597
  }
@@ -31,7 +31,7 @@
31
31
  * * `<name>.package.usda` — the deployable package (engine bundle + USD IR);
32
32
  * its sha256 IS the deploy `domainHash` (`policy:{hash}`).
33
33
  * * `<name>.deploy.json` — `{packageUsda}`: the EXACT body
34
- * `POST /v1/workspaces/:ws/domains` accepts as `application/json` on Nomos Cloud.
34
+ * `POST /v2/workspaces/:ws/domains` accepts as `application/json` on Nomos Cloud.
35
35
  * Admission/projection manifests are embedded in the package USDA and are not host
36
36
  * sidecars.
37
37
  * * `<name>.proof.mts` — the GENERATED PROOF (codegen_proof.ts): a
@@ -103,6 +103,7 @@ import {
103
103
  } from "./stable_ids.js";
104
104
  import type { UsdLayer } from "./usd.js";
105
105
  import { generateTsClient, tsClientFactoryName } from "./codegen_ts.js";
106
+ import { generateOpenApiFragment, mergeOpenApiFragments } from "./codegen_openapi.js";
106
107
  import { domainInterface, type DomainInterface } from "./shape_compat.js";
107
108
  import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
108
109
 
@@ -162,13 +163,13 @@ interface PackageConfig {
162
163
  * Typed DART frontend (the Flutter sibling of the generated TS client; #codegen_dart).
163
164
  * `false`/absent = skip (default). `true` = emit into `<outDir>/dart/`;
164
165
  * `{ out: "relative/dir" }` = emit there (resolved against the config dir).
165
- * `{ support: "package" }` keeps `package:nomos_dsl/nomos_dsl.dart` imports for Flutter apps that
166
- * already depend on `nomos_dsl`/`nomos_flutter`. The default `"vendored"` mode preserves the old
166
+ * `{ support: "package" }` keeps `package:nomos_client/nomos_client.dart` imports for Flutter apps that
167
+ * already depend on `nomos_client`/`nomos_flutter`. The default `"vendored"` mode preserves the old
167
168
  * self-contained directory.
168
169
  * Per composed domain one `<name>.dart` (payload classes + read models + typed read
169
170
  * accessors via the SAME `generateDartDomain` the co2 tenant emit uses), PLUS the
170
- * framework `nomos_dsl` Dart support files vendored alongside (the generated files'
171
- * `package:nomos_dsl/...` import is rewritten to a relative one) — so a Flutter app
171
+ * framework `nomos_client` Dart support files vendored alongside (the generated files'
172
+ * `package:nomos_client/...` import is rewritten to a relative one) — so a Flutter app
172
173
  * can vendor the directory as-is, no pub deps.
173
174
  */
174
175
  readonly dart?: boolean | { readonly out?: string; readonly support?: DartSupportImport };
@@ -397,11 +398,11 @@ function resolveNamed<T>(merged: Mod, names: readonly string[] | undefined, what
397
398
  * Vendored output rewrites it to the relative barrel so the directory is
398
399
  * self-contained (no pub path-dependency on the framework package).
399
400
  */
400
- const DART_PACKAGE_IMPORT = `import 'package:nomos_dsl/nomos_dsl.dart';`;
401
- const DART_RELATIVE_IMPORT = `import 'nomos_dsl.dart';`;
401
+ const DART_PACKAGE_IMPORT = `import 'package:nomos_client/nomos_client.dart';`;
402
+ const DART_RELATIVE_IMPORT = `import 'nomos_client.dart';`;
402
403
 
403
404
  /**
404
- * The framework `nomos_dsl` Dart support files the generated code resolves through
405
+ * The framework `nomos_client` Dart support files the generated code resolves through
405
406
  * the barrel (`dsl/dart/lib/`). `schema_validation.dart` is deliberately NOT
406
407
  * vendored: nothing the codegen emits references it, and it would drag the external
407
408
  * `json_schema` pub dependency into every vendoring app.
@@ -418,14 +419,14 @@ const DART_SUPPORT_SRC = [
418
419
  ] as const;
419
420
 
420
421
  function removeVendoredDartSupport(dartOut: string): void {
421
- rmSync(path.join(dartOut, "nomos_dsl.dart"), { force: true });
422
+ rmSync(path.join(dartOut, "nomos_client.dart"), { force: true });
422
423
  rmSync(path.join(dartOut, "src"), { recursive: true, force: true });
423
424
  }
424
425
 
425
426
  /**
426
427
  * Emit the typed Dart for the composed domain modules into `dartOut`: one
427
428
  * `<name>.dart` per domain (the SAME `generateDartDomain` path the co2 tenant's
428
- * `emit_dart.ts` drives) + the vendored `nomos_dsl.dart` barrel and its `src/`
429
+ * `emit_dart.ts` drives) + the vendored `nomos_client.dart` barrel and its `src/`
429
430
  * support files, with the generated files' `package:` import rewritten relative.
430
431
  */
431
432
  function emitDart(
@@ -442,11 +443,11 @@ function emitDart(
442
443
  if (supportImport === "vendored") {
443
444
  mkdirSync(path.join(dartOut, "src"), { recursive: true });
444
445
  const libDir = path.join(DSL_DIR, "dart", "lib");
445
- const barrel = readFileSync(path.join(libDir, "nomos_dsl.dart"), "utf8")
446
+ const barrel = readFileSync(path.join(libDir, "nomos_client.dart"), "utf8")
446
447
  .split("\n")
447
448
  .filter((line) => !line.includes("src/schema_validation.dart"))
448
449
  .join("\n");
449
- writeFileSync(path.join(dartOut, "nomos_dsl.dart"), barrel, "utf8");
450
+ writeFileSync(path.join(dartOut, "nomos_client.dart"), barrel, "utf8");
450
451
  for (const f of DART_SUPPORT_SRC) {
451
452
  copyFileSync(path.join(libDir, "src", f), path.join(dartOut, "src", f));
452
453
  }
@@ -1330,6 +1331,19 @@ async function main(): Promise<void> {
1330
1331
  const identity = buildIdentity(domainModules);
1331
1332
  const { manifestsPath } = writeIdentity(identity, outDir);
1332
1333
 
1334
+ // ── 3b. the OpenAPI spec — THE PACKAGE'S SELF-DESCRIPTION (the host's runtime routing table;
1335
+ // architecture/githolon_host_contract.md "The Self-Describing API"). Each domain emits its fragment
1336
+ // from its OWN zod (z.toJSONSchema); the package's spec is the law-blind merge. Deploy STAGES it like
1337
+ // the read overlay; the workspace's full spec is the runtime merge of installed packages' specs
1338
+ // (currentLaw); the host ingests it and builds its router from it — no domain/directive name in host
1339
+ // source. Self-describing for the whole web ecosystem (Swagger/codegen/Hono/agent tool-calling).
1340
+ const openapi = mergeOpenApiFragments(
1341
+ domainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash })),
1342
+ { title: `${cfg.name} (nomos package)`, lawHash: domainHash },
1343
+ );
1344
+ const openapiPath = path.join(outDir, `${cfg.name}.openapi.json`);
1345
+ writeFileSync(openapiPath, JSON.stringify(openapi, null, 2) + "\n", "utf8");
1346
+
1333
1347
  // ── 4. the one-call deploy body (package only) ──
1334
1348
  // SELF-DESCRIBING: `summary` states what the (otherwise hex-opaque) package
1335
1349
  // contains — purely derived from the inputs (no timestamps), ignored by the
@@ -1358,6 +1372,9 @@ async function main(): Promise<void> {
1358
1372
  // THE STRUCTURAL OFFER (#72): the per-domain interface, staged on the manifest overlay so
1359
1373
  // `connect()` pulls the workspace's installed shapes and negotiates by structural subtyping.
1360
1374
  interface: packageInterface,
1375
+ // THE SELF-DESCRIBING API: this package's OpenAPI spec (law-derived). The worker stages it on the
1376
+ // manifest overlay; a read serves the workspace's merged spec; the host builds its router from it.
1377
+ openapi,
1361
1378
  };
1362
1379
  const deployPath = path.join(outDir, `${cfg.name}.deploy.json`);
1363
1380
  writeFileSync(deployPath, JSON.stringify(deployBody) + "\n", "utf8");
@@ -1395,7 +1412,7 @@ async function main(): Promise<void> {
1395
1412
  ` directives: ${d.directives.join(", ") || "—"}`,
1396
1413
  ` queries: ${d.queries.join(", ") || "—"} counts: ${d.counts.join(", ") || "—"} sums: ${d.sums.join(", ") || "—"}`,
1397
1414
  ]),
1398
- `deploy: POST ${cfg.name}.deploy.json to /v1/workspaces/<ws>/domains (or: githolon deploy <ws>)`,
1415
+ `deploy: POST ${cfg.name}.deploy.json to /v2/workspaces/<ws>/domains (or: githolon deploy <ws>)`,
1399
1416
  `typed client: ${cfg.name}.client.ts — ${tsClientFactoryName(domainModules[0]?.domain ?? domainModules[0]?.name ?? cfg.name)}(holon), law hash baked in`,
1400
1417
  proofPath !== undefined
1401
1418
  ? `generated proof: ${cfg.name}.proof.mts — run: githolon proof (live; ends ALL GREEN or names the jam)`
@@ -1469,6 +1486,7 @@ async function main(): Promise<void> {
1469
1486
  }
1470
1487
  }
1471
1488
  console.log(` client ${rel(clientPath)} (typed TS client — payloads, read models, query accessors)`);
1489
+ console.log(` openapi ${rel(openapiPath)} (${Object.keys(openapi.paths).length} operation(s) — the law's self-describing API; the host's runtime routing table)`);
1472
1490
  if (proofPath !== undefined) {
1473
1491
  console.log(` proof ${rel(proofPath)} (a runnable e2e GENERATED from your law — run: githolon proof)`);
1474
1492
  } else {
@@ -1476,7 +1494,7 @@ async function main(): Promise<void> {
1476
1494
  }
1477
1495
  if (dartOut !== undefined) {
1478
1496
  const supportText =
1479
- dartSupportImport === "vendored" ? "vendored nomos_dsl support" : "package nomos_dsl imports";
1497
+ dartSupportImport === "vendored" ? "vendored nomos_client support" : "package nomos_client imports";
1480
1498
  console.log(` dart ${rel(dartOut)}${path.sep} (${dartFiles.length} domain file(s) + ${supportText} — typed Dart for Flutter)`);
1481
1499
  }
1482
1500
  console.log(` deploy ${rel(deployPath)}`);
@@ -1484,7 +1502,7 @@ async function main(): Promise<void> {
1484
1502
  console.log(`deploy (Nomos Cloud):`);
1485
1503
  console.log(` githolon ws create <ws> && githolon deploy <ws> # the secret is saved + sent for you`);
1486
1504
  console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'Authorization: Bearer <workspaceSecret>' \\`);
1487
- console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/v1/workspaces/<ws>/domains`);
1505
+ console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/v2/workspaces/<ws>/domains`);
1488
1506
  if (proofPath !== undefined) {
1489
1507
  console.log(`prove it OFFLINE first: npx githolon proof # the generated proof's offline legs on a LOCAL holon (no cloud workspace)`);
1490
1508
  console.log(` inner loop: npx githolon dev # watch -> recompile -> law live locally -> offline proof, on every save`);
package/src/directive.ts CHANGED
@@ -143,7 +143,7 @@ export interface Directive<P = unknown> {
143
143
  readonly declaredRelations: RelationDecl[];
144
144
  /**
145
145
  * The directive's DECLARED required HOST CAPABILITIES — the capability PORTS the HOST must
146
- * PROVIDE for this directive to run (`TARGET_deps.dot` cluster_ports / `TARGET_flow.dot`
146
+ * PROVIDE for this directive to run (`OFFER_KERNEL_ARCHITECTURE.dot` cluster_ports / `OFFER_FLOW.dot`
147
147
  * cluster_ports, invariant 3). This is the HOST/PORT axis — "can this host PHYSICALLY do
148
148
  * this?" — checked ONCE at policy LOAD against what the loading host ADVERTISES, FAIL-CLOSED
149
149
  * (`required ⊆ provided`). It is DISTINCT from the AUTHZ axis {@link requiresCapability}
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * ── Why this module exists ──────────────────────────────────────────────────
12
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
13
+ * (Jack 2026-06-05, REPAIR.dot). The dogfood proof: repair intents go through
14
14
  * THE ONE GATE — no bespoke crate, no AssumeVerified, no 2nd write path. This is Stage
15
15
  * 2 (the `revert` directive); supersede / upcast / drain are later stages.
16
16
  *
@@ -36,7 +36,7 @@
36
36
  *
37
37
  * ── Scope (Stage 2 only — do NOT add supersede / upcast / drain here) ──────
38
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).
39
+ * drain is a re-dispatch. All are later stages (REPAIR.dot build order §2-5).
40
40
  */
41
41
  import { z } from "zod";
42
42
  import { aggregate, instance } from "../aggregate.js";
@@ -67,7 +67,7 @@ const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256
67
67
  * bootstrap chicken-and-egg. The forge surface a bare `relateSubject` would open is closed by
68
68
  * DOMAIN PLACEMENT: this directive is composed into the CONTROL-PLANE governance law (domain
69
69
  * `workspaces`/`nomos`), which the open session lane structurally refuses
70
- * (`cloud/cloudflare-shell/src/engine.mjs` — only the birth door + the secret-gated `/author` + admin
70
+ * (`adapter/engine.mjs` — only the birth door + the secret-gated `/author` + admin
71
71
  * lanes author it), so no tenant can forge a signer for another principal.
72
72
  *
73
73
  * THE ORDER IS THE CONTRACT (verified, zero gate change): `enrollSigner` (E, unsigned/pre-warrant) →
@@ -73,8 +73,10 @@ import { t } from "../fields.js";
73
73
  import { set, withMarker } from "../ops.js";
74
74
  import { query } from "../query.js";
75
75
  import { workspaceInvariant, refAs } from "./workspace_invariant.js";
76
- import { writeTuple, removeTuple, RelationTuple } from "./relations.js";
76
+ import { writeTuple, removeTuple, RelationTuple, relationTupleInstanceId } from "./relations.js";
77
77
  import { relationType, self, user, key } from "../relation_schema.js";
78
+ import { GENESIS_WARRANT_ID } from "./genesis.js";
79
+ import { signerObject, SIGNER_RELATION } from "./signing_identity.js";
78
80
  // THE GENESIS BOOTSTRAP (platform-birth genesis bootstrap, Approach A): governance carries the
79
81
  // warrant-flip primitives so the birth door can lawfully bootstrap a virgin platform's founding admin —
80
82
  // E `enrollSigner` (the founding signer tuple) → W `seedGenesis` (the GenesisWarrant) → A signed
@@ -211,6 +213,10 @@ export const CloudWorkspace = aggregate("CloudWorkspace", {
211
213
  createdAt: t.int().merge(Lww),
212
214
  /** Epoch-ms the worker completed physical provisioning; absent until then. */
213
215
  provisionedAt: t.int().merge(Lww).optional(),
216
+ /** sha256 hex of the founding-admin key for a kind:platform birth — DATA the birth capability provider
217
+ * grants admin to (never the key). The record (status:requested, kind:platform) IS the birth obligation;
218
+ * the provider drains it, materializes, and authors provisionWorkspace (the receipt). Absent for plain ws. */
219
+ foundingAdminHash: t.string().merge(Lww).optional(),
214
220
  /**
215
221
  * Per-workspace cumulative-ledger byte cap OVERRIDE (bytes); absent = the
216
222
  * cloud-wide {@link CloudPolicy} default applies. Raised on the record via
@@ -381,6 +387,8 @@ export const createWorkspace = directive("createWorkspace")
381
387
  .set("ownerGrantHash", "") // no owner credential until provisioned
382
388
  .set("createdBy", p.requestedBy)
383
389
  .set("createdAt", p.requestedAt);
390
+ // A plain workspace record (no kind:platform) — boot materializes it directly; the birth capability
391
+ // provider only drains kind:platform records, so this is a lineage record, not a genesis order.
384
392
  return [];
385
393
  })
386
394
  .requires("creator"); // posture-gated creation: only a #creator holder may request a workspace
@@ -469,7 +477,8 @@ export const rotateWorkspaceOwner = directive("rotateWorkspaceOwner")
469
477
  .plan((p) => {
470
478
  const ws = instance(CloudWorkspace, p.workspaceId);
471
479
  return [set(ws, "ownerGrantHash", p.ownerGrantHash)];
472
- });
480
+ })
481
+ .requires("admin"); // lifecycle authority is law-state, not a host allowlist
473
482
 
474
483
  /**
475
484
  * setWorkspaceTier — move a workspace between `edge` and `heavy` ON THE RECORD;
@@ -489,7 +498,8 @@ export const setWorkspaceTier = directive("setWorkspaceTier")
489
498
  .plan((p) => {
490
499
  const ws = instance(CloudWorkspace, p.workspaceId);
491
500
  return [set(ws, "tier", p.tier)];
492
- });
501
+ })
502
+ .requires("admin");
493
503
 
494
504
  /**
495
505
  * retireWorkspace — the law withdraws the workspace. The worker thereafter
@@ -508,7 +518,8 @@ export const retireWorkspace = directive("retireWorkspace")
508
518
  .plan((p) => {
509
519
  const ws = instance(CloudWorkspace, p.workspaceId);
510
520
  return [set(ws, "status", "retired" as const)];
511
- });
521
+ })
522
+ .requires("admin");
512
523
 
513
524
  /**
514
525
  * reclaimWorkspace — THE LAWFUL CUSTODY DISCARD. Where {@link retireWorkspace}
@@ -538,7 +549,8 @@ export const reclaimWorkspace = directive("reclaimWorkspace")
538
549
  .plan((p) => {
539
550
  const ws = instance(CloudWorkspace, p.workspaceId);
540
551
  return [set(ws, "status", "reclaimed" as const)];
541
- });
552
+ })
553
+ .requires("admin");
542
554
 
543
555
  /**
544
556
  * birthPlatform — A PLATFORM IS BORN: the cloud, one level down. Records the
@@ -559,7 +571,10 @@ export const birthPlatform = directive("birthPlatform")
559
571
  parent: z.string().default(""),
560
572
  /** The pool: how many live children the platform may parent. */
561
573
  pool: z.number().int().positive().default(50),
562
- tier: z.enum(WORKSPACE_TIERS).default("edge"),
574
+ tier: z.enum(WORKSPACE_TIERS).default("heavy"),
575
+ /** sha256 of the founding-admin key the requester PRESENTED (the provider grants admin to the hash;
576
+ * the requester keeps the key — it is the platform's admin credential, used to govern + deploy). */
577
+ foundingAdminHash: sha256Hex,
563
578
  requestedBy: z.string().min(1),
564
579
  requestedAt: epochMs,
565
580
  }),
@@ -574,12 +589,32 @@ export const birthPlatform = directive("birthPlatform")
574
589
  .set("tier", p.tier)
575
590
  .set("status", "requested" as const)
576
591
  .set("ownerGrantHash", "")
592
+ .set("foundingAdminHash", p.foundingAdminHash) // the provider grants admin to this hash at materialize
577
593
  .set("createdBy", p.requestedBy)
578
594
  .set("createdAt", p.requestedAt);
595
+ // THE RECORD IS THE OBLIGATION: a kind:platform CloudWorkspace at status:requested with no ownerGrantHash
596
+ // is an order the birth capability provider drains (materialize + install the parent's law + seed genesis
597
+ // + grant the founding admin + birthCloud), closing it with provisionWorkspace (the receipt). No separate
598
+ // obligation aggregate — the minted CloudWorkspace IS it (sidesteps the id-mint gate; reuses the lifecycle).
579
599
  return [];
580
600
  })
581
601
  .requires("platformCreator"); // invite-only: an active #platformCreator tuple admits the birth
582
602
 
603
+ // ─────────────────────────── BIRTH AS A NOMOS PRIMITIVE ──────────────────────────────────────────────
604
+ // THE RECORD IS THE OBLIGATION (architecture/githolon_host_contract.md "Birth is a capability"). Birth is
605
+ // an IMPURE capability: the parent ORDERS (the law-gated birthPlatform mints a CloudWorkspace at
606
+ // status:requested, kind:platform, carrying foundingAdminHash — gated by .requires("platformCreator")),
607
+ // the BIRTH CAPABILITY PROVIDER fulfils it (materialize: install the PARENT's law + seed genesis + grant
608
+ // the founding admin + birthCloud — the child's OWN kernel admitting each), and authors provisionWorkspace
609
+ // (the receipt) the parent TRUSTS. No separate obligation aggregate — the minted CloudWorkspace IS the
610
+ // order (it sidesteps the id-mint gate and reuses the requested→provisioned lifecycle). The provider knows
611
+ // HOW (the Capability Exception sanctions a declared effect); the law owns WHO/WHAT; the parent trusts the
612
+ // DID-IT-HAPPEN. A child born this way runs THIS SAME law and births its children identically.
613
+
614
+ /** workspacesByStatus — the birth provider drains `requested` CloudWorkspaces (kind:platform, no
615
+ * ownerGrantHash) → materialize → provisionWorkspace. The law's order queue; the host polls, never decides. */
616
+ export const workspacesByStatus = query("workspacesByStatus").key("status").returns(CloudWorkspace);
617
+
583
618
  /** setPlatformPool — the pool edition of a granted quota request. */
584
619
  export const setPlatformPool = directive("setPlatformPool")
585
620
  .mutates(CloudWorkspace)
@@ -736,7 +771,8 @@ export const claimHostname = directive("claimHostname")
736
771
  .set("claimedAt", p.claimedAt)
737
772
  .set("status", "active" as const);
738
773
  return [];
739
- });
774
+ })
775
+ .requires("admin"); // hostname authority is law-state
740
776
 
741
777
  /** releaseHostname — free the name (the record stays — audit, never erasure). */
742
778
  export const releaseHostname = directive("releaseHostname")
@@ -751,7 +787,8 @@ export const releaseHostname = directive("releaseHostname")
751
787
  .plan((p) => {
752
788
  const c = instance(HostnameClaim, p.claimId);
753
789
  return [set(c, "status", "released" as const)];
754
- });
790
+ })
791
+ .requires("admin");
755
792
 
756
793
  /**
757
794
  * bindCapability — record the FACT that a workspace holds a capability
@@ -789,7 +826,8 @@ export const bindCapability = directive("bindCapability")
789
826
  if (p.scope !== undefined) b.set("scope", p.scope);
790
827
  if (p.expiresAt !== undefined) b.set("expiresAt", p.expiresAt);
791
828
  return [];
792
- });
829
+ })
830
+ .requires("admin"); // capability-bind authority is law-state
793
831
 
794
832
  /**
795
833
  * revokeCapability — flip a CapabilityBinding to `revoked`. The credential
@@ -811,7 +849,8 @@ export const revokeCapability = directive("revokeCapability")
811
849
  .plan((p) => {
812
850
  const b = instance(CapabilityBinding, p.bindingId);
813
851
  return [set(b, "status", "revoked" as const)];
814
- });
852
+ })
853
+ .requires("admin");
815
854
 
816
855
  /**
817
856
  * setCloudPolicy — set (or amend) THE QUOTA LAW. An Ensure on the law-state
@@ -1032,11 +1071,20 @@ export const capabilityBindingByKeyHash = query("capabilityBindingByKeyHash")
1032
1071
  // UserPermission): admin=`workspace:self#admin@key:<keyHash>`, creator=`…#creator@key:…`,
1033
1072
  // platformCreator=`workspace:self#platformCreator@user:<principal>`. grantAdmin/grantCreation/
1034
1073
  // grantPlatformCreation write them via `writeTuple`; the bespoke AdminGrant/CreationGrant aggregates +
1035
- // their queries are GONE. Re-export the framework tuple surface so the COMPILED governance package mints
1036
- // RelationTuple's stable id into the lockfile + identity manifest, the engine registers relateSubject/
1037
- // unrelateSubject for dispatch, and the projected relationTuplesByObject read routes. (QuotaGrant stays
1038
- // an aggregate — a numeric allowance, not a membership.)
1039
- export { relateSubject, unrelateSubject, relationTuplesByObject } from "./relations.js";
1074
+ // their queries are GONE. RelationTuple's stable id rides into the lockfile + identity manifest via the
1075
+ // `export { RelationTuple }` below (grantAdmin/grantCreation/… `.ensures(RelationTuple)`); the projected
1076
+ // relationTuplesByObject read routes via its own re-export. (QuotaGrant stays an aggregate — a numeric
1077
+ // allowance, not a membership.)
1078
+ //
1079
+ // THE RAW TUPLE WRITERS ARE NOT GOVERNANCE OFFERS (the security pass). `relateSubject`/`unrelateSubject`
1080
+ // are the framework PRIMITIVE (still the DSL public API for domains that knowingly compose their own
1081
+ // schema); governance never authored through them — every governance tuple write is a GATED wrapper
1082
+ // (grantAdmin/grantCreation/grantPlatformCreation → `writeTuple`, each `.requires("admin")`). Re-exporting
1083
+ // the bare directives only registered an UNGATED `workspace:self#admin@…` writer for dispatch — a
1084
+ // self-grant-admin hole the moment the /v2 gateway routes every offer. Dropping the re-export removes them
1085
+ // from governance's law/dispatch/spec ENTIRELY (not hidden — genuinely not governance's API; the true
1086
+ // generic "you may only grant a relation you hold" rule is a future `relationFrom` kernel arc).
1087
+ export { relationTuplesByObject } from "./relations.js";
1040
1088
  export { RelationTuple };
1041
1089
 
1042
1090
  // ── GOVERNANCE WRITE-AUTHZ IS NATIVE ZANZIBAR — the ONE gate, no bespoke predicates ───────────────────
@@ -1052,3 +1100,80 @@ export { RelationTuple };
1052
1100
  // in the pre-apply state). SELF-RELATIVE: custody guarantees a workspace only folds its OWN tuples, so the
1053
1101
  // SAME law governs root and every born platform. The host's `_adminOk`/`_creationLawOk`/`_platformCheck`
1054
1102
  // pre-checks are GONE too (a pure bailiff) — the native gate refuses a non-holder at admission.
1103
+
1104
+ // ── THE SECURITY PASS: genesis + receipt gating as LAW (nomos-improving-nomos / the spec audit) ────────
1105
+ // The /v2 gateway routes EVERY offer (nothing hidden — Jack's "make the surface attack-proof"). An offer
1106
+ // with neither a `.requires` gate nor an invariant is a RED FLAG: a raw ungated primitive. The genesis
1107
+ // directives (seedGenesis/enrollSigner) + the self-birth (birthCloud) + the birth receipt
1108
+ // (provisionWorkspace) CANNOT use `.requires` — they run pre-admin / pre-warrant or carry no actor — so
1109
+ // the protection that USED to live in the HOST session-lane refusal of control-plane intents moves here,
1110
+ // onto the ledger, as workspaceInvariants: pure predicates over pre-apply state + the payload, fired at the
1111
+ // ONE gate (author / edge admission / verify_chain replay), era-keyed (nomosInvariantGate). No kernel
1112
+ // change, no host gate — exactly the "evolve security THROUGH nomos" the north star demands.
1113
+ //
1114
+ // WRITE-ONCE-BY-VALUE, not strict create-once: an IDENTICAL re-offer (root's by-hash idempotent genesis
1115
+ // re-entry) passes; only a DIFFERENT binding is refused. So idempotence survives while a forged re-seed
1116
+ // (regime-flip / owner-rebind / signer-graft) becomes a typed refusal on every door.
1117
+
1118
+ /** seedGenesis: the founding GenesisWarrant is write-once-by-value. A virgin chain may seed; re-seeding
1119
+ * the SAME (principal, workspaceId) is idempotent; a DIFFERENT binding — the signed-author regime-flip /
1120
+ * owner-rebind the host session-lane used to refuse — is rejected. */
1121
+ export const genesisWarrantWriteOnce = workspaceInvariant("GenesisWarrantWriteOnce")
1122
+ .on("seedGenesis")
1123
+ .reads(() => [refAs("warrant", "GenesisWarrant", "pre:" + GENESIS_WARRANT_ID)])
1124
+ .assert(({ warrant }, ctx) => {
1125
+ const cur = warrant as { principal?: string; workspaceId?: string } | undefined;
1126
+ if (!cur?.principal) return { accept: true }; // virgin — genesis may seed the founding warrant
1127
+ const p = (ctx?.intent ?? {}) as { principal?: string; workspaceId?: string };
1128
+ return cur.principal === p.principal && (cur.workspaceId ?? "") === (p.workspaceId ?? "")
1129
+ ? { accept: true } // idempotent re-seed (the by-hash genesis re-entry)
1130
+ : { reject: "genesis-warrant-already-bound" };
1131
+ });
1132
+
1133
+ /** enrollSigner: the FOUNDING signer is write-once per principal. A virgin `identity:<principal>#signer`
1134
+ * may enroll its founding key; re-offering the SAME key is idempotent; grafting ANY other key (the
1135
+ * author-as-someone-else impersonation the host session-lane used to refuse) is rejected — a later device
1136
+ * needs a self-bound / admin-gated enrolment lane, never this founding one. */
1137
+ export const founderSignerWriteOnce = workspaceInvariant("FounderSignerWriteOnce")
1138
+ .on("enrollSigner")
1139
+ .reads((ctx) => [
1140
+ refAs(
1141
+ "tuple",
1142
+ "RelationTuple",
1143
+ "pre:" + relationTupleInstanceId(signerObject(String((ctx.intent ?? {}).principal ?? "")), SIGNER_RELATION),
1144
+ ),
1145
+ ])
1146
+ .assert(({ tuple }, ctx) => {
1147
+ const subjects = (tuple as { subjects?: Record<string, unknown> })?.subjects ?? {};
1148
+ const want = "key:" + String((ctx?.intent ?? {}).keyHash ?? "");
1149
+ const others = Object.keys(subjects).filter((k) => k !== want);
1150
+ return others.length === 0 ? { accept: true } : { reject: "founding-signer-already-enrolled" };
1151
+ });
1152
+
1153
+ /** birthCloud: a chain is born ONCE. A second birthCloud would mint a duplicate root-ish CloudWorkspace
1154
+ * AND could flip the self-role via its ensure(WorkspaceSelf) — the governance-freeze attack. Refuse once
1155
+ * WorkspaceSelf.role exists. Genesis births on a role-less chain; materialize births each child once. */
1156
+ export const birthCloudOnce = workspaceInvariant("BirthCloudOnce")
1157
+ .on("birthCloud")
1158
+ .reads(() => [refAs("self", "WorkspaceSelf", "pre:" + WORKSPACE_SELF_ID)])
1159
+ .assert(({ self }) =>
1160
+ (self as { role?: string })?.role ? { reject: "cloud-already-born" } : { accept: true },
1161
+ );
1162
+
1163
+ /** provisionWorkspace: the birth RECEIPT may only close a `requested` record (no hijacking a live
1164
+ * workspace's owner credential), and for a kind:platform birth the receipt's ownerGrantHash MUST equal
1165
+ * the pre-committed foundingAdminHash (the provider grants admin to the key the parent ORDERED, never an
1166
+ * arbitrary one). A plain workspace carries no foundingAdminHash, so its fresh-secret mint stays the
1167
+ * trusted impure capability — birth honesty: the law gates what it can prove, trusts the provider for the
1168
+ * rest. (Worker authors ownerGrantHash=foundingAdminHash for platforms, sha256(freshSecret) for plain.) */
1169
+ export const provisionReceiptGuard = workspaceInvariant("ProvisionReceiptGuard")
1170
+ .on("provisionWorkspace")
1171
+ .reads((ctx) => [refAs("ws", "CloudWorkspace", "pre:" + String((ctx.intent ?? {}).workspaceId ?? ""))])
1172
+ .assert(({ ws }, ctx) => {
1173
+ const rec = ws as { status?: string; foundingAdminHash?: string } | undefined;
1174
+ if (!rec || rec.status !== "requested") return { reject: "provision-requires-requested-workspace" };
1175
+ const p = (ctx?.intent ?? {}) as { ownerGrantHash?: string };
1176
+ if (rec.foundingAdminHash && rec.foundingAdminHash !== p.ownerGrantHash)
1177
+ return { reject: "provision-founding-hash-mismatch" };
1178
+ return { accept: true };
1179
+ });
package/src/manifest.ts CHANGED
@@ -157,7 +157,7 @@ export interface CanonicalDirective {
157
157
  */
158
158
  readonly capturedReads?: CanonicalCapturedRead[];
159
159
  /**
160
- * The directive's DECLARED required HOST CAPABILITY ports (`TARGET_deps.dot` cluster_ports;
160
+ * The directive's DECLARED required HOST CAPABILITY ports (`OFFER_KERNEL_ARCHITECTURE.dot` cluster_ports;
161
161
  * the HOST/PORT axis, invariant 3): the capability ports the host must PROVIDE for the
162
162
  * directive to run, checked FAIL-CLOSED (`required ⊆ provided`) at policy LOAD. SORTED (the
163
163
  * SET is the contract; order is incidental). DISTINCT from {@link requiresCapability} (the
@@ -725,7 +725,7 @@ export const nomosRecordDeepVerify = directive("nomosRecordDeepVerify")
725
725
  * SEAL A CHECKPOINT (§5.5, slice 4): bind the held per-shard frontiers + the
726
726
  * subtotal state hash to the next sequence point. Host-authored on the cadence
727
727
  * (the worker counts admitted deltas against the policy dial) or explicitly
728
- * (`POST /v1/workspaces/<coordinator>/checkpoint`) — but gate-verified either
728
+ * (`POST /v2/workspaces/<coordinator>/checkpoint`) — but gate-verified either
729
729
  * way: the `nomosCheckpointGate` invariant pins every claimed frontier to the
730
730
  * HELD `NomosSummaryFrontier` watermark (`pre:` reads) and the sequence to
731
731
  * held-head + 1. A seal can never claim a frontier the delta lane never
@@ -33,8 +33,8 @@
33
33
  * Everything else is DERIVED — Jack's ratified requirement is a FULLY TYPESAFE
34
34
  * surface: a domain dev NEVER sees or manages a compound key. Homing is derived
35
35
  * from the aggregates' existing `t.ref` chains (zero annotation); birth lanes are
36
- * derived from `hasMany` over the EXISTING platform machinery
37
- * (`POST /v1/platforms/...` — derived, never forked); minted-id home keys are
36
+ * derived from `hasMany` over the EXISTING birth machinery
37
+ * (`POST /v2/workspaces/...` birth offers — derived, never forked); minted-id home keys are
38
38
  * internal plumbing (slice 2).
39
39
  *
40
40
  * THE HOMING WALK (this file): `home(aggregate)` = the NEAREST PACKED AXIS its
File without changes