@githolon/dsl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE.md +36 -0
  2. package/compile_package.mjs +50 -0
  3. package/package.json +59 -0
  4. package/src/aggregate.ts +167 -0
  5. package/src/authoring.ts +119 -0
  6. package/src/build_package.ts +636 -0
  7. package/src/certified_read.ts +313 -0
  8. package/src/codegen_dart.ts +2732 -0
  9. package/src/codegen_dot.ts +466 -0
  10. package/src/codegen_provider_dart.ts +358 -0
  11. package/src/codegen_ts.ts +365 -0
  12. package/src/codegen_usda.ts +388 -0
  13. package/src/combined.ts +195 -0
  14. package/src/compile_engine.ts +567 -0
  15. package/src/compile_package_main.ts +496 -0
  16. package/src/compose.ts +317 -0
  17. package/src/count.ts +218 -0
  18. package/src/ctx.ts +57 -0
  19. package/src/derived.ts +138 -0
  20. package/src/directive.ts +306 -0
  21. package/src/drivers.ts +95 -0
  22. package/src/emits_guard.ts +123 -0
  23. package/src/engine_entry.ts +449 -0
  24. package/src/exists.ts +170 -0
  25. package/src/extremum.ts +227 -0
  26. package/src/fields.ts +291 -0
  27. package/src/framework/bootstrap.ts +22 -0
  28. package/src/framework/disclosure.ts +108 -0
  29. package/src/framework/domain_lifecycle.ts +108 -0
  30. package/src/framework/identity.ts +537 -0
  31. package/src/framework/impure_capability.ts +643 -0
  32. package/src/framework/rbac.ts +418 -0
  33. package/src/framework/repair.ts +150 -0
  34. package/src/framework/sync_lifecycle.ts +125 -0
  35. package/src/framework/workspace_invariant.ts +128 -0
  36. package/src/framework/workspaces.ts +817 -0
  37. package/src/index.ts +317 -0
  38. package/src/manifest.ts +947 -0
  39. package/src/ops.ts +145 -0
  40. package/src/ordered_read.ts +228 -0
  41. package/src/predicate.ts +203 -0
  42. package/src/query/compile.ts +0 -0
  43. package/src/query/relations.ts +144 -0
  44. package/src/query.ts +151 -0
  45. package/src/read.ts +54 -0
  46. package/src/relation.ts +189 -0
  47. package/src/report/csv.ts +54 -0
  48. package/src/report.ts +401 -0
  49. package/src/spatial.ts +115 -0
  50. package/src/sum.ts +194 -0
  51. package/src/usd.ts +563 -0
  52. package/src/wire.ts +149 -0
  53. package/src/wire_encode.ts +250 -0
@@ -0,0 +1,388 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded
4
+ // QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /**
7
+ * TS → `.usda` — render a COMPILED DOMAIN as a real OpenUSD stage (Jack 2026-06-08).
8
+ *
9
+ * This is the ABSTRACT REPRESENTATION of the domain's LAW — for studying the holon as a maths object,
10
+ * NOT the org-data digital twin. The structure IS the scene. The sibling `emitDomainDot` renders the
11
+ * same structure as a 2D Graphviz graph; this renders it as a navigable 3D USD stage.
12
+ *
13
+ * It lays out the FULL NOMOS FLOW as a vertical stack, so the application reads top-to-bottom:
14
+ *
15
+ * INTENTS (Cone, top) — the dispatch boundary: the ONLY thing a client sends in.
16
+ * │ dispatches
17
+ * DIRECTIVES (Cube, upper) — the sealed plan that computes events from intent payload + ports.
18
+ * │ creates / mutates
19
+ * AGGREGATES (Sphere, middle) — the consistency boundaries / identity units the events fold
20
+ * │ reads into; reference edges between them = the implicit *Id graph.
21
+ * READ PROJECTIONS (Capsule, bottom) — the named, maintained reads back OUT: query/count/sum/
22
+ * derived/combined.
23
+ *
24
+ * Aggregates sit on a CIRCLE in the ground plane; intents/directives float above their target, read
25
+ * projections hang below. Edges are thin `Cylinder` tubes (AR Quick Look renders meshes, not curves),
26
+ * coloured by kind via bound `UsdPreviewSurface` materials. The whole thing is re-centred so the
27
+ * CENTROID sits at the origin (the orbit pivot on spacebar), with an explicit `Camera` aimed there.
28
+ *
29
+ * PURE `DomainModule → string`, deterministic (sorted iteration; build-time trig only — no clock/rng),
30
+ * no file IO. Like `emitDomainDot` it is a PRESENTATION projection — NOT part of the domain identity.
31
+ */
32
+ import type { Field } from "./fields.js";
33
+ import type { DomainModule } from "./codegen_dart.js";
34
+
35
+ export interface UsdaOptions {
36
+ /** Tube radius for the flow edges (stage units). Default 0.3. */
37
+ readonly edgeRadius?: number;
38
+ }
39
+
40
+ type Vec3 = readonly [number, number, number];
41
+ const add = (a: Vec3, b: Vec3): Vec3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
42
+ const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
43
+ const scale = (a: Vec3, s: number): Vec3 => [a[0] * s, a[1] * s, a[2] * s];
44
+ const len = (a: Vec3): number => Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
45
+ /** round to 4dp, drop −0 — deterministic, stable text. */
46
+ const f = (n: number): string => `${Number(n.toFixed(4)) + 0}`;
47
+ const v3 = (a: Vec3): string => `(${f(a[0])}, ${f(a[1])}, ${f(a[2])})`;
48
+
49
+ const COLOR = {
50
+ aggregate: [0.2, 0.45, 0.85] as Vec3, // blue — Sphere
51
+ directive: [0.95, 0.6, 0.15] as Vec3, // orange — Cube
52
+ intent: [0.6, 0.3, 0.8] as Vec3, // purple — Cone (dispatch boundary)
53
+ projection: [0.25, 0.7, 0.55] as Vec3, // teal-green — Capsule (read-out)
54
+ creates: [0.25, 0.75, 0.35] as Vec3, // green — directive→aggregate (create)
55
+ mutates: [0.9, 0.45, 0.2] as Vec3, // orange-red — directive→aggregate (mutate)
56
+ ref: [0.4, 0.75, 0.8] as Vec3, // teal — aggregate→aggregate reference
57
+ intentEdge: [0.72, 0.55, 0.92] as Vec3, // light purple — intent→directive
58
+ readEdge: [0.85, 0.78, 0.35] as Vec3, // yellow — aggregate→projection
59
+ };
60
+ type ColorKey = keyof typeof COLOR;
61
+
62
+ /** USD triple-quoted string literal (carries newlines + quotes faithfully for a `plan` body). */
63
+ function usdString3(s: string): string {
64
+ return `"""${s.replace(/"""/g, '\\"\\"\\"')}"""`;
65
+ }
66
+
67
+ /** A `UsdPreviewSurface` material so the colour survives in AR Quick Look (RealityKit ignores displayColor). */
68
+ function material(matPath: string, color: Vec3): string {
69
+ return [
70
+ ` def Material "${matPath.split("/").pop()}"`,
71
+ ` {`,
72
+ ` token outputs:surface.connect = <${matPath}/surface.outputs:surface>`,
73
+ ` def Shader "surface"`,
74
+ ` {`,
75
+ ` uniform token info:id = "UsdPreviewSurface"`,
76
+ ` color3f inputs:diffuseColor = ${v3(color)}`,
77
+ ` float inputs:metallic = 0`,
78
+ ` float inputs:roughness = 0.55`,
79
+ ` token outputs:surface`,
80
+ ` }`,
81
+ ` }`,
82
+ ].join("\n");
83
+ }
84
+
85
+ /** Infer the aggregate a `*Id`/`*Ids` field points at: strip the suffix, match an aggregate id (ci). */
86
+ function inferRefTarget(fieldName: string, aggIdByLower: Map<string, string>): string | undefined {
87
+ const base = fieldName.replace(/Ids?$/, "");
88
+ if (base === fieldName) return undefined; // no Id/Ids suffix → not a reference field
89
+ return aggIdByLower.get(base.toLowerCase());
90
+ }
91
+
92
+ /** A read projection normalized for rendering: which aggregate it reads, its kind, optional 2nd read. */
93
+ interface Proj {
94
+ readonly id: string;
95
+ readonly kind: string;
96
+ readonly of: string;
97
+ readonly reads?: string;
98
+ }
99
+
100
+ /** Collect the domain's declared read projections (queries/counts/sums/deriveds/combineds). */
101
+ function projectionsOf(mod: DomainModule): Proj[] {
102
+ const m = mod as unknown as Record<string, unknown[]>;
103
+ const out: Proj[] = [];
104
+ const id = (p: unknown) => String((p as { id: string }).id);
105
+ for (const q of (m.queries ?? []) as { id: string; returns: string }[]) out.push({ id: id(q), kind: "query", of: q.returns });
106
+ for (const c of (m.counts ?? []) as { of: string }[]) out.push({ id: id(c), kind: "count", of: c.of });
107
+ for (const s of (m.sums ?? []) as { of: string }[]) out.push({ id: id(s), kind: "sum", of: s.of });
108
+ for (const d of (m.deriveds ?? []) as { of: string }[]) out.push({ id: id(d), kind: "derived", of: d.of });
109
+ for (const cb of (m.combineds ?? []) as { of: string; reads?: string }[])
110
+ out.push({ id: id(cb), kind: "combined", of: cb.of, ...(cb.reads ? { reads: cb.reads } : {}) });
111
+ return out.sort((x, y) => x.id.localeCompare(y.id));
112
+ }
113
+
114
+ /** Render a compiled domain as a real `.usda` stage (the full nomos flow, laid out for legibility). */
115
+ export function emitDomainUsda(mod: DomainModule, opts: UsdaOptions = {}): string {
116
+ const edgeRadius = opts.edgeRadius ?? 0.3;
117
+ const aggregates = [...mod.aggregates].sort((x, y) => x.id.localeCompare(y.id));
118
+ const directives = [...mod.directives].sort((x, y) => x.id.localeCompare(y.id));
119
+ const projections = projectionsOf(mod);
120
+ const safe = (s: string) => s.replace(/[^A-Za-z0-9_]/g, "_");
121
+ const domainPath = `/Domain_${safe(mod.name)}`;
122
+ const matPath = (key: ColorKey) => `${domainPath}/Materials/Mat_${key}`;
123
+
124
+ const N = Math.max(aggregates.length, 1);
125
+ const R = Math.max(7, N * 1.7); // ring radius — grows with the aggregate count
126
+
127
+ // ── aggregate positions: a circle in the ground (XZ) plane ──
128
+ const aggPos = new Map<string, Vec3>();
129
+ const aggAngle = new Map<string, number>();
130
+ aggregates.forEach((a, i) => {
131
+ const θ = (2 * Math.PI * i) / N;
132
+ aggPos.set(a.id, [R * Math.cos(θ), 0, R * Math.sin(θ)]);
133
+ aggAngle.set(a.id, θ);
134
+ });
135
+ const dirOf = (target: string): { outward: Vec3; tangent: Vec3; base: Vec3 } | undefined => {
136
+ const base = aggPos.get(target);
137
+ const θ = aggAngle.get(target);
138
+ if (!base || θ === undefined) return undefined;
139
+ return { base, outward: [Math.cos(θ), 0, Math.sin(θ)], tangent: [-Math.sin(θ), 0, Math.cos(θ)] };
140
+ };
141
+
142
+ // group directives + projections by the aggregate they touch (for tangential fanning)
143
+ const groupBy = <T>(items: T[], key: (t: T) => string): Map<string, T[]> => {
144
+ const g = new Map<string, T[]>();
145
+ for (const it of items) (g.get(key(it)) ?? g.set(key(it), []).get(key(it))!).push(it);
146
+ return g;
147
+ };
148
+
149
+ // ── directive positions: above the target aggregate (y=+7), fanned tangentially ──
150
+ const dirPos = new Map<string, Vec3>();
151
+ for (const [target, ids] of groupBy(directives, (d) => d.aggregateId)) {
152
+ const g = dirOf(target);
153
+ ids.forEach((d, j) => {
154
+ const spread = (j - (ids.length - 1) / 2) * 3;
155
+ dirPos.set(
156
+ d.id,
157
+ g ? add(add(add(g.base, scale(g.outward, 3)), [0, 7, 0]), scale(g.tangent, spread)) : [0, 7, 0],
158
+ );
159
+ });
160
+ }
161
+ // ── intent positions: one per directive (the dispatch front-door), 4.5 above its directive ──
162
+ const intentPos = new Map<string, Vec3>();
163
+ for (const d of directives) intentPos.set(d.id, add(dirPos.get(d.id)!, [0, 4.5, 0]));
164
+
165
+ // ── projection positions: below the target aggregate (y=−7), fanned tangentially + outward ──
166
+ const projPos = new Map<string, Vec3>();
167
+ for (const [target, projs] of groupBy(projections, (p) => p.of)) {
168
+ const g = dirOf(target);
169
+ projs.forEach((p, j) => {
170
+ const spread = (j - (projs.length - 1) / 2) * 3;
171
+ projPos.set(
172
+ p.id,
173
+ g ? add(add(add(g.base, scale(g.outward, 4)), [0, -7, 0]), scale(g.tangent, spread)) : [0, -7, 0],
174
+ );
175
+ });
176
+ }
177
+
178
+ // ── centre the whole thing so the CENTROID is at the origin (the orbit pivot) ──
179
+ const all: Vec3[] = [...aggPos.values(), ...dirPos.values(), ...intentPos.values(), ...projPos.values()];
180
+ const centroid: Vec3 = all.length
181
+ ? scale(all.reduce<Vec3>((acc, p) => add(acc, p), [0, 0, 0]), 1 / all.length)
182
+ : [0, 0, 0];
183
+ const off = (p: Vec3): Vec3 => sub(p, centroid);
184
+
185
+ const aggIdByLower = new Map<string, string>();
186
+ for (const a of aggregates) aggIdByLower.set(a.id.toLowerCase(), a.id);
187
+
188
+ // ── a thin Cylinder mesh spanning p0→p1 (an edge), oriented + coloured + material-bound ──
189
+ const edgeTube = (name: string, p0: Vec3, p1: Vec3, radius: number, key: ColorKey): string => {
190
+ const d = sub(p1, p0);
191
+ const L = len(d);
192
+ const mid = scale(add(p0, p1), 0.5);
193
+ let q: readonly [number, number, number, number] = [1, 0, 0, 0]; // (w, x, y, z) rotate +Z → edge dir
194
+ if (L >= 1e-9) {
195
+ const u: Vec3 = [d[0] / L, d[1] / L, d[2] / L];
196
+ const dot = u[2];
197
+ if (dot < -0.999999) q = [0, 1, 0, 0];
198
+ else if (dot <= 0.999999) {
199
+ const ax = -u[1];
200
+ const ay = u[0];
201
+ const an = Math.sqrt(ax * ax + ay * ay);
202
+ const angle = Math.acos(dot);
203
+ const s = Math.sin(angle / 2);
204
+ q = [Math.cos(angle / 2), (ax / an) * s, (ay / an) * s, 0];
205
+ }
206
+ }
207
+ return [
208
+ ` def Cylinder "${name}" (`,
209
+ ` prepend apiSchemas = ["MaterialBindingAPI"]`,
210
+ ` )`,
211
+ ` {`,
212
+ ` uniform token axis = "Z"`,
213
+ ` double radius = ${radius}`,
214
+ ` double height = ${f(L)}`,
215
+ ` double3 xformOp:translate = ${v3(mid)}`,
216
+ ` quatd xformOp:orient = (${f(q[0])}, ${f(q[1])}, ${f(q[2])}, ${f(q[3])})`,
217
+ ` uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient"]`,
218
+ ` color3f[] primvars:displayColor = [${v3(COLOR[key])}]`,
219
+ ` rel material:binding = <${matPath(key)}>`,
220
+ ` }`,
221
+ ].join("\n");
222
+ };
223
+
224
+ // ── a Gprim node (Sphere/Cube/Cone/Capsule) at a position, coloured + material-bound + metadata ──
225
+ const node = (kind: string, name: string, sizeAttrs: string[], pos: Vec3, key: ColorKey, meta: string[]): string =>
226
+ [
227
+ ` def ${kind} "${safe(name)}" (`,
228
+ ` prepend apiSchemas = ["MaterialBindingAPI"]`,
229
+ ` )`,
230
+ ` {`,
231
+ ...sizeAttrs.map((s) => ` ${s}`),
232
+ ` double3 xformOp:translate = ${v3(pos)}`,
233
+ ` uniform token[] xformOpOrder = ["xformOp:translate"]`,
234
+ ` color3f[] primvars:displayColor = [${v3(COLOR[key])}]`,
235
+ ` rel material:binding = <${matPath(key)}>`,
236
+ ...meta.map((s) => ` ${s}`),
237
+ ` }`,
238
+ ].join("\n");
239
+
240
+ const lines: string[] = [];
241
+ lines.push(`#usda 1.0`);
242
+ lines.push(`(`);
243
+ lines.push(` doc = "Nomos compiled domain '${mod.name}' — abstract law structure (holon-maths projection)"`);
244
+ lines.push(` defaultPrim = "Domain_${safe(mod.name)}"`);
245
+ lines.push(` metersPerUnit = 1`);
246
+ lines.push(` upAxis = "Y"`);
247
+ lines.push(`)`);
248
+ lines.push(``);
249
+ lines.push(`def Xform "Domain_${safe(mod.name)}"`);
250
+ lines.push(`{`);
251
+
252
+ // ── MATERIALS — one UsdPreviewSurface per colour so AR Quick Look shows them ──
253
+ lines.push(` def Scope "Materials"`);
254
+ lines.push(` {`);
255
+ for (const key of Object.keys(COLOR) as ColorKey[]) lines.push(material(matPath(key), COLOR[key]));
256
+ lines.push(` }`);
257
+
258
+ // ── INTENTS — Cone prims (the dispatch boundary); one per directive ──
259
+ lines.push(` def Scope "Intents"`);
260
+ lines.push(` {`);
261
+ for (const d of directives) {
262
+ lines.push(
263
+ node(
264
+ "Cone",
265
+ `${d.id}Intent`,
266
+ [`uniform token axis = "Y"`, `double height = 2`, `double radius = 0.9`],
267
+ off(intentPos.get(d.id)!),
268
+ "intent",
269
+ [
270
+ `custom string nomos:kind = "intent"`,
271
+ `rel nomos:dispatches = <${domainPath}/Directives/${safe(d.id)}>`,
272
+ ],
273
+ ),
274
+ );
275
+ }
276
+ lines.push(` }`);
277
+
278
+ // ── AGGREGATE TYPES — Sphere prims (the consistency boundaries / identity units) ──
279
+ lines.push(` def Scope "Aggregates"`);
280
+ lines.push(` {`);
281
+ for (const a of aggregates) {
282
+ const fields = Object.keys(a.fields as Record<string, Field>).sort();
283
+ const hasInv = Boolean((a as unknown as { invariant?: unknown }).invariant);
284
+ lines.push(
285
+ node("Sphere", a.id, [`double radius = 1.2`], off(aggPos.get(a.id)!), "aggregate", [
286
+ `custom string nomos:kind = "aggregate"`,
287
+ `custom string[] nomos:fields = [${fields.map((x) => `"${x}"`).join(", ")}]`,
288
+ `custom bool nomos:hasAggregateInvariant = ${hasInv ? "true" : "false"}`,
289
+ ]),
290
+ );
291
+ }
292
+ lines.push(` }`);
293
+
294
+ // ── DIRECTIVES — Cube prims (the write transitions), each carrying its `plan` lump ──
295
+ lines.push(` def Scope "Directives"`);
296
+ lines.push(` {`);
297
+ for (const d of directives) {
298
+ const planSrc = String((d as unknown as { plan: unknown }).plan ?? "");
299
+ lines.push(
300
+ node("Cube", d.id, [`double size = 1.6`], off(dirPos.get(d.id)!), "directive", [
301
+ `custom string nomos:kind = "directive"`,
302
+ `custom string nomos:marker = "${d.marker}"`,
303
+ `rel nomos:target = <${domainPath}/Aggregates/${safe(d.aggregateId)}>`,
304
+ `custom string nomos:plan = ${usdString3(planSrc)}`,
305
+ ]),
306
+ );
307
+ }
308
+ lines.push(` }`);
309
+
310
+ // ── READ PROJECTIONS — Capsule prims (the maintained reads back out) ──
311
+ lines.push(` def Scope "Projections"`);
312
+ lines.push(` {`);
313
+ for (const p of projections) {
314
+ const meta = [
315
+ `custom string nomos:kind = "projection"`,
316
+ `custom string nomos:projectionKind = "${p.kind}"`,
317
+ `rel nomos:reads = <${domainPath}/Aggregates/${safe(p.of)}>`,
318
+ ];
319
+ if (p.reads) meta.push(`rel nomos:reads2 = <${domainPath}/Aggregates/${safe(p.reads)}>`);
320
+ lines.push(
321
+ node(
322
+ "Capsule",
323
+ `${p.id}__${p.kind}`,
324
+ [`uniform token axis = "Z"`, `double height = 1.4`, `double radius = 0.7`],
325
+ off(projPos.get(p.id)!),
326
+ "projection",
327
+ meta,
328
+ ),
329
+ );
330
+ }
331
+ lines.push(` }`);
332
+
333
+ // ── THE FLOW — Cylinder tubes wiring the four layers together ──
334
+ lines.push(` def Scope "Flow"`);
335
+ lines.push(` {`);
336
+ // intent → directive
337
+ for (const d of directives)
338
+ lines.push(edgeTube(`${safe(d.id)}Intent__dispatches__${safe(d.id)}`, off(intentPos.get(d.id)!), off(dirPos.get(d.id)!), edgeRadius * 0.7, "intentEdge"));
339
+ // directive →(marker)→ aggregate
340
+ for (const d of directives) {
341
+ const to = aggPos.get(d.aggregateId);
342
+ if (!to) continue;
343
+ const key: ColorKey = d.marker === "mutates" ? "mutates" : "creates";
344
+ lines.push(edgeTube(`${safe(d.id)}__${d.marker}__${safe(d.aggregateId)}`, off(dirPos.get(d.id)!), off(to), edgeRadius, key));
345
+ }
346
+ // aggregate →ref→ aggregate (declared refAggregateId OR inferred *Id/*Ids)
347
+ const drawnRef = new Set<string>();
348
+ for (const a of aggregates) {
349
+ const from = aggPos.get(a.id)!;
350
+ for (const [fname, fld] of Object.entries(a.fields as Record<string, Field>)) {
351
+ const declared = (fld as unknown as { refAggregateId?: string }).refAggregateId;
352
+ const target = declared ?? inferRefTarget(fname, aggIdByLower);
353
+ if (!target || target === a.id) continue;
354
+ const to = aggPos.get(target);
355
+ const k = `${a.id}->${target}`;
356
+ if (!to || drawnRef.has(k)) continue;
357
+ drawnRef.add(k);
358
+ lines.push(edgeTube(`${safe(a.id)}__ref__${safe(target)}`, off(from), off(to), edgeRadius * 0.7, "ref"));
359
+ }
360
+ }
361
+ // aggregate →read→ projection
362
+ for (const p of projections) {
363
+ const from = aggPos.get(p.of);
364
+ if (from) lines.push(edgeTube(`${safe(p.of)}__read__${safe(p.id)}_${p.kind}`, off(from), off(projPos.get(p.id)!), edgeRadius * 0.7, "readEdge"));
365
+ const from2 = p.reads ? aggPos.get(p.reads) : undefined;
366
+ if (from2) lines.push(edgeTube(`${safe(p.reads!)}__read__${safe(p.id)}_${p.kind}`, off(from2), off(projPos.get(p.id)!), edgeRadius * 0.7, "readEdge"));
367
+ }
368
+ lines.push(` }`);
369
+
370
+ lines.push(`}`);
371
+
372
+ // ── an explicit camera aimed at the centroid (origin), so non-Quick-Look viewers frame it too ──
373
+ const h = R * 0.7;
374
+ const dist = R * 2.6;
375
+ const pitch = -(Math.atan2(h, dist) * 180) / Math.PI;
376
+ lines.push(``);
377
+ lines.push(`def Camera "centroidCamera"`);
378
+ lines.push(`{`);
379
+ lines.push(` float focalLength = 35`);
380
+ lines.push(` float horizontalAperture = 36`);
381
+ lines.push(` float2 clippingRange = (0.1, ${f(dist * 3)})`);
382
+ lines.push(` double3 xformOp:translate = (0, ${f(h)}, ${f(dist)})`);
383
+ lines.push(` double xformOp:rotateX = ${f(pitch)}`);
384
+ lines.push(` uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateX"]`);
385
+ lines.push(`}`);
386
+
387
+ return lines.join("\n") + "\n";
388
+ }
@@ -0,0 +1,195 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded
4
+ // QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
5
+ // wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
6
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
+
8
+ /**
9
+ * `combined(id)` builder — an ENGINE-PROJECTED, CROSS-AGGREGATE read field
10
+ * (read-engine: combined read fields). The "combine data" capability: a read that
11
+ * JOINS one aggregate to a RELATED one and materialises the composed value on the
12
+ * owner's row.
13
+ *
14
+ * READ-CLOSURE, the JOIN half. A {@link query} declares a NAMED, INDEXED set read; a
15
+ * {@link count} a NAMED, MAINTAINED tally; a `derived` a NAMED, PURE FUNCTION of ONE
16
+ * aggregate's folded fields; a `combined` declares a NAMED field computed from the
17
+ * owner aggregate AND a RELATED one reached by a typed REF edge (e.g. `Building.siteName
18
+ * = the parent Site's name`). Like `derived`, the value is NEVER stamped into a kernel
19
+ * op/event — it is computed BY THE ENGINE during the projection projection and stored
20
+ * ONLY in the read model, so on a re-fold it is always re-derivable. UNLIKE `derived`,
21
+ * the engine eval is fed the related aggregate's folded `data` (resolved by the owner's
22
+ * ref id) via the SAME query-port path a report's `.render` consumes — so the compose fn
23
+ * is `(owner, related) => value`.
24
+ *
25
+ * ── The CROSS-AGGREGATE INVALIDATION axis (the hard part this primitive declares) ──
26
+ * A combined field on aggregate A that reads aggregate B must be RECOMPUTED when B
27
+ * changes — not only when A changes. The declaration therefore names the QUERIED type
28
+ * (`reads`): the read engine recomputes every owner-A's combined field whenever a
29
+ * projection delta touches an aggregate of that queried type. The compose fn body
30
+ * (`.as(...)`) is executable behaviour — it ships in the engine bundle and is NOT hashed
31
+ * into the domain identity (the same rule `derived`'s `.as` / directives' `.plan` follow).
32
+ *
33
+ * The TYPE-STATE mirrors {@link derived}: `combined(id)` yields a {@link TypelessCombined}
34
+ * whose ONLY method is `.of(...)`; `.of(...)` yields a {@link CombinedOf} whose ONLY
35
+ * method is `.joins(...)`; `.joins(...)` yields a {@link CombinedJoins} carrying `.as`;
36
+ * the finished {@link CombinedDecl} is produced solely by `.as(...)`. So a combined field
37
+ * with no owner-type, no join edge, or no compose fn cannot be CONSTRUCTED — "every
38
+ * combined field names the owner it lives on, the ref edge it joins through, the type it
39
+ * queries, AND a compose fn" is a type-level property, before any runtime check.
40
+ */
41
+ import type { z } from "zod";
42
+ import type { AggregateHandle } from "./aggregate.js";
43
+
44
+ /**
45
+ * The compose function: maps the OWNER aggregate's folded fields (`owner`, a plain JSON
46
+ * object — the projection's projected `data` for it) and the RELATED aggregate's folded
47
+ * fields (`related`, the queried aggregate's `data`, or `undefined` when the ref is unset
48
+ * / the related aggregate is absent or archived) to the combined value. MUST be pure (no
49
+ * ports, no I/O): the engine runs it over the host-fed `owner` (`priorState`) + `related`
50
+ * (a query-port row) and stores the result verbatim into the read model. The value is any
51
+ * JSON-serialisable scalar / object the read schema can decode (the smallest-first target
52
+ * is a parent-name `string`).
53
+ */
54
+ export type CombineFn = (
55
+ owner: Record<string, unknown>,
56
+ related: Record<string, unknown> | undefined,
57
+ ) => unknown;
58
+
59
+ /**
60
+ * A FINISHED combined-field declaration (the read engine's input shape, mirroring {@link
61
+ * DerivedDecl} but with the JOIN axis): an id (the stored projection field name), the
62
+ * OWNER aggregate TYPE (`of`), the REF FIELD on the owner that points at the related
63
+ * aggregate (`refField`), the RELATED aggregate TYPE the read QUERIES (`reads` — the
64
+ * invalidation trigger), and the pure compose `fn`.
65
+ */
66
+ export interface CombinedDecl {
67
+ /** The combined field's NAME — the key the read model stores it under. */
68
+ readonly id: string;
69
+ /** The OWNER aggregate TYPE id the field lives on, e.g. `BuildingAggregate`. */
70
+ readonly of: string;
71
+ /** The owner's REF FIELD whose value is the related aggregate's id, e.g. `siteId`. */
72
+ readonly refField: string;
73
+ /** The RELATED aggregate TYPE the field reads (the cross-aggregate invalidation key),
74
+ * e.g. `SiteRootAggregate`. A delta touching this type recomputes every owner's field. */
75
+ readonly reads: string;
76
+ /** The JSON value schema the engine-projected field returns. */
77
+ readonly returns: z.ZodTypeAny;
78
+ /** The pure compose fn `(owner, related) => value`. */
79
+ readonly fn: CombineFn;
80
+ }
81
+
82
+ /**
83
+ * The {@link CombinedJoins} BUILDER: it has named its owner-type AND its join edge, so
84
+ * `.as(...)` (which fixes the compose fn and yields the finished {@link CombinedDecl}) is
85
+ * available. This is the ONLY shape carrying `.as` — see {@link TypelessCombined}.
86
+ */
87
+ export interface CombinedJoins {
88
+ readonly id: string;
89
+ readonly of: string;
90
+ readonly refField: string;
91
+ readonly reads: string;
92
+ /**
93
+ * Declare the projected value schema. A combined field without a return schema is not a
94
+ * read contract, because the read model/Dart surface would have to guess.
95
+ */
96
+ returns(schema: z.ZodTypeAny): CombinedReturns;
97
+ }
98
+
99
+ /**
100
+ * The {@link CombinedReturns} BUILDER: it has named owner, join edge, related type, AND
101
+ * value schema, so `.as(...)` can fix the executable body and yield the finished decl.
102
+ */
103
+ export interface CombinedReturns {
104
+ readonly id: string;
105
+ readonly of: string;
106
+ readonly refField: string;
107
+ readonly reads: string;
108
+ /** The JSON value schema the engine-projected field returns. */
109
+ readonly returns: z.ZodTypeAny;
110
+ /**
111
+ * Fix the PURE compose fn, producing the finished declaration. The fn receives the
112
+ * owner's folded fields AND the related aggregate's folded fields and returns the value.
113
+ */
114
+ as(fn: CombineFn): CombinedDecl;
115
+ }
116
+
117
+ /**
118
+ * The {@link CombinedOf} BUILDER: it has named its owner-type, so `.joins(...)` (which
119
+ * names the REF FIELD + the related aggregate TYPE) is available. It deliberately has NO
120
+ * `.as` — the join edge is a prerequisite, not optional.
121
+ */
122
+ export interface CombinedOf {
123
+ readonly id: string;
124
+ readonly of: string;
125
+ /**
126
+ * Declare the JOIN: the owner's REF FIELD (`refField`, whose value is the related
127
+ * aggregate's id) AND the RELATED aggregate TYPE this field reads (a typed HANDLE —
128
+ * never a string id, so a typo'd related type is a compile error). The related type is
129
+ * carried as the CROSS-AGGREGATE INVALIDATION key. Returns the {@link CombinedJoins}
130
+ * builder (the only shape exposing `.as`).
131
+ */
132
+ joins(refField: string, related: AggregateHandle): CombinedJoins;
133
+ }
134
+
135
+ /**
136
+ * The INITIAL, un-typed combined field — its ONLY method is `.of(...)`. It deliberately
137
+ * has NO `.joins`/`.as` and is not a {@link CombinedDecl}, so skipping the owner-type is a
138
+ * COMPILE error and a type-less combined field cannot be constructed. THIS is the
139
+ * type-level "every combined field names the owner it lives on" property.
140
+ */
141
+ export interface TypelessCombined {
142
+ readonly id: string;
143
+ /**
144
+ * Declare the OWNER aggregate TYPE this field lives on. Takes a typed HANDLE (never the
145
+ * string id) — a typo'd handle is a compile error, the same convention as `derived`'s
146
+ * `.of(...)`. Returns the {@link CombinedOf} builder.
147
+ */
148
+ of(aggregate: AggregateHandle): CombinedOf;
149
+ }
150
+
151
+ /**
152
+ * Begin a combined-field declaration. Returns a {@link TypelessCombined}: until `.of(...)`
153
+ * then `.joins(...)` are called, neither `.as` nor a usable declaration exists — the
154
+ * owner-type AND the join edge are prerequisites, not optional.
155
+ */
156
+ export function combined<const Id extends string>(id: Id): TypelessCombined {
157
+ return {
158
+ id,
159
+ of(aggregate: AggregateHandle): CombinedOf {
160
+ const ofType = aggregate.id;
161
+ return {
162
+ id,
163
+ of: ofType,
164
+ joins(refField: string, related: AggregateHandle): CombinedJoins {
165
+ const readsType = related.id;
166
+ return {
167
+ id,
168
+ of: ofType,
169
+ refField,
170
+ reads: readsType,
171
+ returns(schema: z.ZodTypeAny): CombinedReturns {
172
+ return {
173
+ id,
174
+ of: ofType,
175
+ refField,
176
+ reads: readsType,
177
+ returns: schema,
178
+ as(fn: CombineFn): CombinedDecl {
179
+ return {
180
+ id,
181
+ of: ofType,
182
+ refField,
183
+ reads: readsType,
184
+ returns: schema,
185
+ fn,
186
+ };
187
+ },
188
+ };
189
+ },
190
+ };
191
+ },
192
+ };
193
+ },
194
+ };
195
+ }