@githolon/dsl 0.15.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.
- package/dart/CHANGELOG.md +2 -0
- package/dart/FINDINGS.md +2 -2
- package/dart/LICENSE +45 -0
- package/dart/README.md +9 -0
- package/dart/example/nomos_webview_transport.dart +4 -4
- package/dart/lib/src/offer.dart +2 -2
- package/dart/lib/src/subscriptions.dart +1 -1
- package/dart/lib/src/webview_bridge.dart +52 -0
- package/dart/pubspec.yaml +3 -3
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/webview_bridge_test.dart +1 -1
- package/dart/test/wire_test.dart +1 -1
- package/package.json +1 -1
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/codegen_dart.ts +25 -15
- package/src/codegen_dart_client.ts +54 -5
- package/src/codegen_openapi.ts +239 -0
- package/src/codegen_proof.ts +16 -9
- package/src/codegen_ts.ts +83 -22
- package/src/compile_package_main.ts +117 -29
- package/src/ctx.ts +13 -0
- package/src/directive.ts +1 -1
- package/src/engine_entry.ts +22 -5
- package/src/fields.ts +44 -0
- package/src/flow_gate.ts +208 -0
- package/src/framework/capability.ts +19 -1
- package/src/framework/encryption_scope.ts +9 -1
- package/src/framework/genesis.ts +5 -1
- package/src/framework/relations.ts +23 -4
- package/src/framework/repair.ts +2 -2
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +147 -15
- package/src/index.ts +24 -0
- package/src/manifest.ts +12 -18
- package/src/ops.ts +42 -1
- package/src/principal_evidence.ts +112 -0
- package/src/relation_sugar.ts +10 -4
- package/src/shape_compat.ts +208 -0
- package/src/usd.ts +131 -0
- package/src/usd_layers.ts +43 -0
- package/src/wire.ts +8 -0
- package/src/wire_encode.ts +75 -2
- package/src/workspace_sharding.ts +196 -18
- package/src/workspace_type.ts +3 -2
- /package/dart/lib/{nomos_dsl.dart → nomos_client.dart} +0 -0
|
@@ -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
|
+
}
|
package/src/codegen_proof.ts
CHANGED
|
@@ -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}/
|
|
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}/
|
|
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
|
|
467
|
-
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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
|
@@ -64,6 +64,9 @@ export const tsClientFactoryName = (domain: string): string => `${lcFirst(camel(
|
|
|
64
64
|
/** `tool-library` / `tool_library` / `Tool Library` → `TOOL_LIBRARY_DOMAIN_HASH` — the hash const. */
|
|
65
65
|
export const tsHashConstName = (packageName: string): string => `${upperSnake(packageName)}_DOMAIN_HASH`;
|
|
66
66
|
|
|
67
|
+
/** `tool-library` → `TOOL_LIBRARY_INTERFACE` — the baked structural-interface const (#72). */
|
|
68
|
+
export const interfaceConstName = (packageName: string): string => `${upperSnake(packageName)}_INTERFACE`;
|
|
69
|
+
|
|
67
70
|
// ── zod → TS (the same `_def ?? def` convention as codegen_dart's walker) ─────────
|
|
68
71
|
// The walkers are exported: codegen_proof.ts synthesizes SAMPLE payloads from the
|
|
69
72
|
// same zod shapes this file derives TYPES from — one introspection, two emitters.
|
|
@@ -289,6 +292,10 @@ export interface TsClientOptions {
|
|
|
289
292
|
readonly packageName: string;
|
|
290
293
|
/** sha256 of the built `.package.usda` — the law this client offers under. */
|
|
291
294
|
readonly domainHash: string;
|
|
295
|
+
/** The package's STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates
|
|
296
|
+
* (read shapes, sid-keyed), keyed by domain. Baked in so `connect()` can negotiate
|
|
297
|
+
* shape-compatibility against the workspace's installed offer — NOT by hash. */
|
|
298
|
+
readonly interface?: Record<string, unknown>;
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
/** TRUE when any module declares a PACKED workspace taxonomy (the routed-holon shape).
|
|
@@ -322,6 +329,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
322
329
|
`/** sha256 of the deployed \`.package.usda\` — the installed law this client offers under. */`,
|
|
323
330
|
`export const ${hashConst} = ${JSON.stringify(opts.domainHash)};`,
|
|
324
331
|
``,
|
|
332
|
+
...(opts.interface !== undefined
|
|
333
|
+
? [
|
|
334
|
+
`/** This client's STRUCTURAL INTERFACE REQUIREMENT (#72): the directives (payload shapes)`,
|
|
335
|
+
` * and aggregates (read shapes, sid-keyed) it depends on, keyed by domain. \`connect()\``,
|
|
336
|
+
` * negotiates this against the workspace's installed offer by SHAPE (stable-id subtyping),`,
|
|
337
|
+
` * not by law hash — so the domain can evolve without republishing this client. */`,
|
|
338
|
+
`export const ${interfaceConstName(opts.packageName)} = ${JSON.stringify(opts.interface)} as const;`,
|
|
339
|
+
``,
|
|
340
|
+
]
|
|
341
|
+
: []),
|
|
325
342
|
`/** One projection row: the aggregate's wire type, its id, and the folded fields. */`,
|
|
326
343
|
`export interface Row<T> { type: string; id: string; data: T }`,
|
|
327
344
|
``,
|
|
@@ -347,8 +364,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
347
364
|
` discardDeadLetter(id: string): Promise<{ ok: boolean; removed: number }>;`,
|
|
348
365
|
`}`,
|
|
349
366
|
``,
|
|
350
|
-
`const encodeIntentOffer = (domain: string, directiveId: string, payload: unknown, domainHash: string): Uint8Array =>`,
|
|
351
|
-
` new TextEncoder().encode(JSON.stringify({ v: 2, intent: { domain, directiveId, payload, domainHash } }));`,
|
|
367
|
+
`const encodeIntentOffer = (domain: string, directiveId: string, payload: unknown, domainHash: string, actor?: string): Uint8Array =>`,
|
|
368
|
+
` new TextEncoder().encode(JSON.stringify({ v: 2, intent: { domain, directiveId, payload, domainHash }, ...(actor ? { security: { actor } } : {}) }));`,
|
|
352
369
|
``,
|
|
353
370
|
);
|
|
354
371
|
|
|
@@ -402,8 +419,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
402
419
|
|
|
403
420
|
// ── DERIVED BIRTH LANES (sharding §10 RATIFIED, slice 1) ──
|
|
404
421
|
// A `workspaceType(...).hasMany(...)` taxonomy DERIVES typed birth helpers over
|
|
405
|
-
// the EXISTING
|
|
406
|
-
//
|
|
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
|
|
407
425
|
// (parent param named after the parent TYPE); the `p--ws` composition is internal
|
|
408
426
|
// plumbing — the composed workspace name comes back on the result for `connect()`.
|
|
409
427
|
// PACKED child types derive NO holon-birth helper: a packed birth is a shard-map
|
|
@@ -478,16 +496,25 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
478
496
|
` [k: string]: unknown;`,
|
|
479
497
|
`}`,
|
|
480
498
|
``,
|
|
481
|
-
`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> => {`,
|
|
482
500
|
` const headers: Record<string, string> = {};`,
|
|
483
501
|
` if (auth.authToken !== undefined) headers["x-nomos-auth"] = auth.authToken;`,
|
|
484
502
|
` if (auth.principal !== undefined) headers["x-nomos-principal"] = auth.principal;`,
|
|
485
503
|
` if (auth.bearer !== undefined) headers["authorization"] = \`Bearer \${auth.bearer}\`;`,
|
|
486
504
|
` if (auth.platformAdminKey !== undefined) headers["x-nomos-platform-admin-key"] = auth.platformAdminKey;`,
|
|
487
|
-
`
|
|
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
|
+
` });`,
|
|
488
509
|
` return (await res.json()) as NomosBirthResult;`,
|
|
489
510
|
`};`,
|
|
490
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
|
+
``,
|
|
491
518
|
);
|
|
492
519
|
const emittedBirths = new Set<string>();
|
|
493
520
|
for (const mod of birthModules) {
|
|
@@ -501,13 +528,40 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
501
528
|
}
|
|
502
529
|
const childIds = new Set([...types.values()].flatMap((t) => [...t.childIds]));
|
|
503
530
|
const fnName = (typeId: string) => `birth${cap(camel(typeId))}`;
|
|
504
|
-
|
|
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" }) => {
|
|
505
538
|
if (emittedBirths.has(name)) return;
|
|
506
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
|
+
];
|
|
507
559
|
out.push(
|
|
508
560
|
`/** ${doc} */`,
|
|
509
|
-
`export const ${name} = (
|
|
510
|
-
` nomosBirth(opts.cloud, ${
|
|
561
|
+
`export const ${name} = async (${sig}): Promise<NomosBirthResult> =>`,
|
|
562
|
+
` nomosBirth(opts.cloud, \`/v2/workspaces/\${encodeURIComponent(${lane.parentWs})}/${lane.directiveId}\`, opts, {`,
|
|
563
|
+
` ${bodyFields.join(", ")},`,
|
|
564
|
+
` });`,
|
|
511
565
|
``,
|
|
512
566
|
);
|
|
513
567
|
};
|
|
@@ -515,14 +569,15 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
515
569
|
if (t.mode === "packed") continue; // placement, not a holon birth (slice 2)
|
|
516
570
|
const isPlatform = t.childIds.length > 0;
|
|
517
571
|
if (!childIds.has(t.id)) {
|
|
518
|
-
// A TOP-LEVEL type: births
|
|
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.
|
|
519
574
|
emit(
|
|
520
575
|
fnName(t.id),
|
|
521
|
-
`Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree; PRESENT your chosen platformAdminKey)" : "
|
|
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.`,
|
|
522
577
|
isPlatform ? [`name: string`, `platformAdminKey: string`] : [`name: string`],
|
|
523
578
|
isPlatform
|
|
524
|
-
?
|
|
525
|
-
:
|
|
579
|
+
? { kind: "offer", parentWs: `"root"`, directiveId: "birthPlatform" }
|
|
580
|
+
: { kind: "boot" },
|
|
526
581
|
);
|
|
527
582
|
}
|
|
528
583
|
for (const childId of t.childIds) {
|
|
@@ -532,13 +587,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
532
587
|
const childIsPlatform = child.childIds.length > 0;
|
|
533
588
|
emit(
|
|
534
589
|
fnName(child.id),
|
|
535
|
-
`Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge
|
|
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).`,
|
|
536
591
|
childIsPlatform
|
|
537
592
|
? [`${parentParam}: string`, `name: string`, `platformAdminKey: string`]
|
|
538
593
|
: [`${parentParam}: string`, `name: string`],
|
|
539
|
-
childIsPlatform
|
|
540
|
-
? `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/platforms/\${encodeURIComponent(opts.name)}\``
|
|
541
|
-
: `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/workspaces/\${encodeURIComponent(opts.name)}\``,
|
|
594
|
+
{ kind: "offer", parentWs: `opts.${parentParam}`, directiveId: childIsPlatform ? "birthPlatform" : "createWorkspace" },
|
|
542
595
|
);
|
|
543
596
|
}
|
|
544
597
|
}
|
|
@@ -595,7 +648,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
595
648
|
` * — ids stay internal plumbing — and returns it with the new local head. */`,
|
|
596
649
|
` ${d.id}: async (payload: ${payloadName}): Promise<{ head: string; ${mint.field}: string }> => {`,
|
|
597
650
|
` const ${mint.field} = payload.${mint.field} ?? (await (holon as NomosRoutedHolon).mint(${JSON.stringify(mint.mintType)}${mintOpts}));`,
|
|
598
|
-
` const head = await holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, domainHash));`,
|
|
651
|
+
` const head = await holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, domainHash, actor));`,
|
|
599
652
|
` return { head, ${mint.field} };`,
|
|
600
653
|
` },`,
|
|
601
654
|
);
|
|
@@ -604,8 +657,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
604
657
|
methods.push(
|
|
605
658
|
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
|
|
606
659
|
stamps.length
|
|
607
|
-
? ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, stampNow(payload, ${JSON.stringify(stamps)}), domainHash)),`
|
|
608
|
-
: ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, payload, domainHash)),`,
|
|
660
|
+
? ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, stampNow(payload, ${JSON.stringify(stamps)}), domainHash, actor)),`
|
|
661
|
+
: ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, payload, domainHash, actor)),`,
|
|
609
662
|
);
|
|
610
663
|
}
|
|
611
664
|
|
|
@@ -668,11 +721,19 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
668
721
|
}
|
|
669
722
|
|
|
670
723
|
out.push(
|
|
671
|
-
`/** Typed client for the \`${domain}\` domain, bound to a connected holon
|
|
672
|
-
`
|
|
724
|
+
`/** Typed client for the \`${domain}\` domain, bound to a connected holon.`,
|
|
725
|
+
` * Pass \`{ actor }\` (the verified principal uid) to author AS that user — every write seals it as`,
|
|
726
|
+
` * the captured principal, so the ReBAC \`.requires(...)\` gate + the integrity-dual endorse model`,
|
|
727
|
+
` * judge the real author. Omit it for open (un-gated) domains. */`,
|
|
728
|
+
`export function ${tsClientFactoryName(domain)}(holon: NomosHolon, opts: { domainHash?: string; actor?: string } = {}) {`,
|
|
729
|
+
` const domainHash = opts.domainHash ?? ${hashConst};`,
|
|
730
|
+
` const actor = opts.actor;`,
|
|
673
731
|
` return {`,
|
|
674
732
|
` domain: ${JSON.stringify(domain)} as const,`,
|
|
675
733
|
` domainHash,`,
|
|
734
|
+
...(opts.interface !== undefined
|
|
735
|
+
? [` interface: ${interfaceConstName(opts.packageName)}[${JSON.stringify(domain)}],`]
|
|
736
|
+
: []),
|
|
676
737
|
...methods,
|
|
677
738
|
` };`,
|
|
678
739
|
`}`,
|