@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,636 @@
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
+ * THE GENERIC PACKAGE BUILD SURFACE — `@githolon/dsl/build-package` (#M4).
10
+ *
11
+ * BUILD-TIME ONLY (imports `node:crypto` via `./manifest.js`): reached via the
12
+ * `@githolon/dsl/build-package` subpath, NEVER the runtime barrel — the engine bundle
13
+ * must stay node-builtin-free (esbuild `--platform=neutral`).
14
+ *
15
+ * Hoists, generalized over any tenant's `DomainModule[]`, the machinery the co2
16
+ * package (`ts_packages/co2-nomos-domains`) hand-rolled:
17
+ *
18
+ * * `composeDomainModule(spec)` — the NAMED-KEYS successor of `emit_dart.ts`'s
19
+ * 15-positional-parameter `module(...)` helper (dedupe by wire id, the
20
+ * impure-capability directive check, default read-models, and the
21
+ * HASH-LOAD-BEARING omit-when-empty key discipline).
22
+ * * `buildReadManifest(modules)` — the LEVEL-2 read-projection artifact
23
+ * (`domain_manifests.json`: aggregateFieldKinds + query/count/spatial/derived/
24
+ * combined routing descriptors), hoisted from `emit_manifests.ts`. Field KINDS,
25
+ * not drivers — see that file's header for why the split is load-bearing.
26
+ * * `buildIdentity(modules)` / `writeIdentity(emit, outDir)` — the per-domain
27
+ * IDENTITY manifest + admits_domain_hash registry (#193/#136), hoisted from
28
+ * `emit_identity.ts` (palette-gap domains recorded-and-excluded, the
29
+ * cross-language sha anchor asserted per emitted domain).
30
+ * * `packageUsda(usdJson, javascript)` — the `.package.usda` envelope, byte-exact
31
+ * to `build_nomos.mjs` `packageUsda()` == Rust `domain_package_from_js`
32
+ * (deterministic-rquickjs/src/domain_package.rs).
33
+ * * `emitUsdJsonForModules(modules)` — the one-line USD-IR emit every
34
+ * `emit_*_usd_json.ts` wrapped (`emitUsd` over `/Nomos/<domain>` layers).
35
+ */
36
+ import { createHash } from "node:crypto";
37
+ import { mkdirSync, writeFileSync } from "node:fs";
38
+ import { join } from "node:path";
39
+
40
+ import { z } from "zod";
41
+
42
+ import type { AggregateHandle } from "./aggregate.js";
43
+ import type { Directive } from "./directive.js";
44
+ import type { DomainModule, PermissionVocabulary, DartImport } from "./codegen_dart.js";
45
+ import { finishCount, type AnyCount, type CountDecl } from "./count.js";
46
+ import type { QueryDecl } from "./query.js";
47
+ import type { SpatialDecl } from "./spatial.js";
48
+ import type { AnySum } from "./sum.js";
49
+ import type { DerivedDecl } from "./derived.js";
50
+ import type { CombinedDecl } from "./combined.js";
51
+ import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
52
+ import { domainHash, emitManifestBytes } from "./manifest.js";
53
+ import { emitUsd } from "./usd.js";
54
+
55
+ // ─────────────────────────────────────────────────────────────────────────────────
56
+ // composeDomainModule — the named-keys `module(...)`
57
+ // ─────────────────────────────────────────────────────────────────────────────────
58
+
59
+ /** A domain module's exports bag (scanned by SHAPE, same duck-types as the engine entry). */
60
+ export type Mod = Record<string, unknown>;
61
+
62
+ /** Collect every exported `AggregateHandle` (dedup is the composer's job). */
63
+ export function aggregatesOf(mod: Mod): AggregateHandle[] {
64
+ const out: AggregateHandle[] = [];
65
+ for (const v of Object.values(mod)) {
66
+ if (
67
+ v &&
68
+ typeof v === "object" &&
69
+ (v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true
70
+ ) {
71
+ out.push(v as AggregateHandle);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /** Collect every exported `Directive` ({id, plan, aggregateId} duck-type). */
78
+ export function directivesOf(mod: Mod): Directive<any>[] {
79
+ const out: Directive<any>[] = [];
80
+ for (const v of Object.values(mod)) {
81
+ if (
82
+ v &&
83
+ typeof v === "object" &&
84
+ typeof (v as { id?: unknown }).id === "string" &&
85
+ typeof (v as { plan?: unknown }).plan === "function" &&
86
+ typeof (v as { aggregateId?: unknown }).aggregateId === "string"
87
+ ) {
88
+ out.push(v as Directive<any>);
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+
94
+ /** The NAMED-KEYS spec for one engine domain (the serialization of `module(...)`). */
95
+ export interface DomainModuleSpec {
96
+ /** Artifact basename AND (when `domain` is omitted) the engine dispatch key. */
97
+ readonly name: string;
98
+ /** The engine dispatch key, when it differs from `name` (e.g. merged `identity`). */
99
+ readonly domain?: string;
100
+ /** The ORDERED module list composing this domain (later wins on collision). */
101
+ readonly modules: readonly Mod[];
102
+ /** Extra aggregates not exported by the modules (e.g. re-used framework handles). */
103
+ readonly extraAggregates?: readonly AggregateHandle[];
104
+ readonly queries?: readonly QueryDecl[];
105
+ readonly counts?: readonly AnyCount[];
106
+ readonly readModelAggregates?: readonly string[];
107
+ readonly deriveds?: readonly DerivedDecl[];
108
+ readonly combineds?: readonly CombinedDecl[];
109
+ readonly impureCapabilities?: readonly ImpureCapabilityDecl[];
110
+ readonly sums?: readonly AnySum[];
111
+ readonly spatials?: readonly SpatialDecl[];
112
+ readonly dartImports?: readonly DartImport[];
113
+ readonly dartRefImports?: DomainModule["dartRefImports"];
114
+ readonly permissionVocabulary?: PermissionVocabulary;
115
+ }
116
+
117
+ /**
118
+ * Compose one `DomainModule` from a spec: spread-merge the module list (the SAME
119
+ * later-wins merge the engine entry applies, so the manifest lines up 1:1 with what
120
+ * dispatches), dedupe aggregates by wire id, validate impure capabilities, default
121
+ * the read-models, and apply the HASH-LOAD-BEARING omit-when-empty key discipline
122
+ * (an empty optional key must be ABSENT or every domain's canonical manifest hash drifts).
123
+ */
124
+ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
125
+ let merged: Mod = {};
126
+ for (const m of spec.modules) merged = { ...merged, ...m };
127
+
128
+ const byId = new Map<string, AggregateHandle>();
129
+ for (const a of [...aggregatesOf(merged), ...(spec.extraAggregates ?? [])]) byId.set(a.id, a);
130
+ const aggregates = [...byId.values()];
131
+ const directives = directivesOf(merged);
132
+ const directiveIds = new Set(directives.map((d) => d.id));
133
+ for (const task of spec.impureCapabilities ?? []) {
134
+ for (const directive of [task.order, task.complete, task.fail, task.block, task.deadLetter]) {
135
+ if (!directiveIds.has(directive.id)) {
136
+ throw new Error(
137
+ `domain '${spec.domain ?? spec.name}' declares impureCapability '${task.aggregate.id}' ` +
138
+ `but does not export directive '${directive.id}'`,
139
+ );
140
+ }
141
+ }
142
+ }
143
+ const readModels = spec.readModelAggregates ?? aggregates.map((a) => a.id);
144
+
145
+ const queries = [...(spec.queries ?? [])];
146
+ const counts = [...(spec.counts ?? [])];
147
+ const spatials = [...(spec.spatials ?? [])];
148
+ const deriveds = [...(spec.deriveds ?? [])];
149
+ const combineds = [...(spec.combineds ?? [])];
150
+ const impureCapabilities = [...(spec.impureCapabilities ?? [])];
151
+ const sums = [...(spec.sums ?? [])];
152
+ const dartImports = [...(spec.dartImports ?? [])];
153
+
154
+ return {
155
+ name: spec.name,
156
+ domain: spec.domain ?? spec.name,
157
+ aggregates,
158
+ directives,
159
+ ...(queries.length > 0 ? { queries } : {}),
160
+ ...(counts.length > 0 ? { counts } : {}),
161
+ ...(spatials.length > 0 ? { spatials } : {}),
162
+ ...(deriveds.length > 0 ? { deriveds } : {}),
163
+ ...(combineds.length > 0 ? { combineds } : {}),
164
+ ...(readModels.length > 0 ? { readModelAggregates: [...readModels] } : {}),
165
+ ...(impureCapabilities.length > 0 ? { impureCapabilities } : {}),
166
+ ...(sums.length > 0 ? { sums } : {}),
167
+ ...(dartImports.length > 0 ? { dartImports } : {}),
168
+ ...(spec.dartRefImports !== undefined && spec.dartRefImports.length > 0
169
+ ? { dartRefImports: [...spec.dartRefImports] }
170
+ : {}),
171
+ ...(spec.permissionVocabulary !== undefined
172
+ ? { permissionVocabulary: spec.permissionVocabulary }
173
+ : {}),
174
+ };
175
+ }
176
+
177
+ // ─────────────────────────────────────────────────────────────────────────────────
178
+ // buildReadManifest — domain_manifests.json (hoisted from emit_manifests.ts)
179
+ // ─────────────────────────────────────────────────────────────────────────────────
180
+
181
+ /** One field's read-projection descriptor: the leaf kind (+ a map's value kind). */
182
+ export interface FieldKindDescriptor {
183
+ kind: string;
184
+ mapValueKind?: string;
185
+ }
186
+ export type AggregateSchema = Record<string, FieldKindDescriptor>;
187
+ export type AggregateFieldKinds = Record<string, AggregateSchema>;
188
+
189
+ export interface QueryKeyField {
190
+ field: string;
191
+ kind: string;
192
+ }
193
+ export interface QueryDescriptor {
194
+ id: string;
195
+ key: QueryKeyField[];
196
+ returns: string;
197
+ }
198
+ export interface CountDescriptor {
199
+ id: string;
200
+ of: string;
201
+ by: string | null;
202
+ }
203
+ export interface SpatialDescriptor {
204
+ id: string;
205
+ of: string;
206
+ on: string;
207
+ }
208
+ export interface ProjectionReturnDescriptor {
209
+ type: string;
210
+ nullable: boolean;
211
+ jsonSchema: unknown;
212
+ }
213
+ export interface DerivedDescriptor {
214
+ id: string;
215
+ of: string;
216
+ returns: ProjectionReturnDescriptor;
217
+ }
218
+ export interface CombinedDescriptor {
219
+ id: string;
220
+ of: string;
221
+ refField: string;
222
+ reads: string;
223
+ returns: ProjectionReturnDescriptor;
224
+ }
225
+
226
+ /** The combined read-manifest artifact (the EXACT wire shape the Rust read engine parses). */
227
+ export interface ReadManifest {
228
+ aggregateFieldKinds: AggregateFieldKinds;
229
+ queries: QueryDescriptor[];
230
+ counts: CountDescriptor[];
231
+ spatials: SpatialDescriptor[];
232
+ deriveds: DerivedDescriptor[];
233
+ combineds: CombinedDescriptor[];
234
+ }
235
+
236
+ type ZodLike = {
237
+ _def?: { type?: string; innerType?: ZodLike };
238
+ def?: { type?: string; innerType?: ZodLike };
239
+ };
240
+
241
+ function zodDef(zt: ZodLike): NonNullable<ZodLike["_def"]> {
242
+ return zt._def ?? zt.def ?? {};
243
+ }
244
+
245
+ function zodTypeName(zt: ZodLike): string {
246
+ return zodDef(zt).type ?? "unknown";
247
+ }
248
+
249
+ function describeProjectionReturn(schema: z.ZodTypeAny): ProjectionReturnDescriptor {
250
+ let cur = schema as unknown as ZodLike;
251
+ let nullable = false;
252
+ while (
253
+ zodTypeName(cur) === "optional" ||
254
+ zodTypeName(cur) === "default" ||
255
+ zodTypeName(cur) === "nullable"
256
+ ) {
257
+ nullable = true;
258
+ const inner = zodDef(cur).innerType;
259
+ if (inner === undefined) break;
260
+ cur = inner;
261
+ }
262
+ return {
263
+ type: zodTypeName(cur),
264
+ nullable,
265
+ jsonSchema: z.toJSONSchema(schema),
266
+ };
267
+ }
268
+
269
+ /** Capture one aggregate's field KINDS straight off the DSL `Field` objects. */
270
+ function schemaOf(agg: AggregateHandle): AggregateSchema {
271
+ const fields: AggregateSchema = {};
272
+ // STORED fields only — virtual `t.hasMany` inverses are partitioned into `agg.hasMany`.
273
+ for (const [name, field] of Object.entries(agg.fields)) {
274
+ const descriptor: FieldKindDescriptor = { kind: field.kind };
275
+ if (field.mapValueKind !== undefined) descriptor.mapValueKind = field.mapValueKind;
276
+ fields[name] = descriptor;
277
+ }
278
+ return fields;
279
+ }
280
+
281
+ /** Stable JSON for an aggregate schema, to detect a genuine cross-domain divergence. */
282
+ function stable(schema: AggregateSchema): string {
283
+ const sorted: AggregateSchema = {};
284
+ for (const k of Object.keys(schema).sort()) sorted[k] = schema[k]!;
285
+ return JSON.stringify(sorted);
286
+ }
287
+
288
+ /**
289
+ * Resolve the routing KIND of one query KEY FIELD against the returns-type's field
290
+ * schema. NO FALLBACK: an undeclared key field is a declaration bug — THROW.
291
+ */
292
+ function keyFieldKind(
293
+ byType: AggregateFieldKinds,
294
+ queryId: string,
295
+ returns: string,
296
+ keyField: string,
297
+ ): string {
298
+ const schema = byType[returns];
299
+ if (schema === undefined) {
300
+ throw new Error(
301
+ `build-package: query '${queryId}' returns '${returns}', which has no field ` +
302
+ `schema — the returns handle must be an aggregate emitted into the manifest.`,
303
+ );
304
+ }
305
+ // A dotted key indexes a sub-value of a top-level JSON leaf; kind off the leading field.
306
+ const lookup = keyField.includes(".") ? keyField.split(".")[0]! : keyField;
307
+ const descriptor = schema[lookup];
308
+ if (descriptor === undefined) {
309
+ throw new Error(
310
+ `build-package: query '${queryId}' is keyed on '${keyField}' (field '${lookup}'), ` +
311
+ `which '${returns}' does not declare — a query cannot index an undefined field.`,
312
+ );
313
+ }
314
+ return descriptor.kind;
315
+ }
316
+
317
+ function describeQuery(byType: AggregateFieldKinds, q: QueryDecl): QueryDescriptor {
318
+ return {
319
+ id: q.id,
320
+ key: q.key.map((field) => ({
321
+ field,
322
+ kind: keyFieldKind(byType, q.id, q.returns, field),
323
+ })),
324
+ returns: q.returns,
325
+ };
326
+ }
327
+
328
+ function describeCount(byType: AggregateFieldKinds, c: CountDecl): CountDescriptor {
329
+ if (byType[c.of] === undefined) {
330
+ throw new Error(
331
+ `build-package: count '${c.id}' tallies '${c.of}', which has no field schema — ` +
332
+ `the of-type must be an aggregate emitted into the manifest.`,
333
+ );
334
+ }
335
+ return { id: c.id, of: c.of, by: c.by ?? null };
336
+ }
337
+
338
+ function describeSpatial(byType: AggregateFieldKinds, s: SpatialDecl): SpatialDescriptor {
339
+ const schema = byType[s.of];
340
+ if (schema === undefined) {
341
+ throw new Error(
342
+ `build-package: spatial '${s.id}' covers '${s.of}', which has no field schema — ` +
343
+ `the of-type must be an aggregate emitted into the manifest.`,
344
+ );
345
+ }
346
+ if (schema[s.on] === undefined) {
347
+ throw new Error(
348
+ `build-package: spatial '${s.id}' indexes geometry field '${s.on}' on '${s.of}', ` +
349
+ `which '${s.of}' does not declare — the geometry field must exist (no fallback).`,
350
+ );
351
+ }
352
+ return { id: s.id, of: s.of, on: s.on };
353
+ }
354
+
355
+ function describeDerived(byType: AggregateFieldKinds, d: DerivedDecl): DerivedDescriptor {
356
+ if (byType[d.of] === undefined) {
357
+ throw new Error(
358
+ `build-package: derived '${d.id}' derives from '${d.of}', which has no field ` +
359
+ `schema — the of-type must be an aggregate emitted into the manifest.`,
360
+ );
361
+ }
362
+ return { id: d.id, of: d.of, returns: describeProjectionReturn(d.returns) };
363
+ }
364
+
365
+ function describeCombined(byType: AggregateFieldKinds, c: CombinedDecl): CombinedDescriptor {
366
+ if (byType[c.of] === undefined) {
367
+ throw new Error(
368
+ `build-package: combined '${c.id}' lives on '${c.of}', which has no field ` +
369
+ `schema — the of-type must be an aggregate emitted into the manifest.`,
370
+ );
371
+ }
372
+ if (byType[c.reads] === undefined) {
373
+ throw new Error(
374
+ `build-package: combined '${c.id}' reads '${c.reads}', which has no field ` +
375
+ `schema — the queried type must be an aggregate emitted into the manifest.`,
376
+ );
377
+ }
378
+ return {
379
+ id: c.id,
380
+ of: c.of,
381
+ refField: c.refField,
382
+ reads: c.reads,
383
+ returns: describeProjectionReturn(c.returns),
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Build the single combined READ manifest for the modules (`domain_manifests.json`).
389
+ * Hoisted VERBATIM-in-behaviour from `emit_manifests.ts` `buildManifests` — sorted
390
+ * keys/arrays for byte-stability, divergence detection across domains, NO-FALLBACK
391
+ * throws on undeclared of-types (the Rust parse would reject them anyway, loudly,
392
+ * and break ALL reads for the workspace — fail at build instead).
393
+ */
394
+ export function buildReadManifest(modules: readonly DomainModule[]): ReadManifest {
395
+ const byType: AggregateFieldKinds = {};
396
+ for (const mod of modules) {
397
+ for (const agg of mod.aggregates as AggregateHandle[]) {
398
+ const schema = schemaOf(agg);
399
+ const existing = byType[agg.id];
400
+ if (existing !== undefined && stable(existing) !== stable(schema)) {
401
+ throw new Error(
402
+ `build-package: aggregate '${agg.id}' has DIVERGENT field schemas across ` +
403
+ `domains — the read projection cannot have two shapes for one wire type. ` +
404
+ `Reconcile the aggregate definition.`,
405
+ );
406
+ }
407
+ byType[agg.id] = schema;
408
+ }
409
+ }
410
+ const sorted: AggregateFieldKinds = {};
411
+ for (const k of Object.keys(byType).sort()) sorted[k] = byType[k]!;
412
+
413
+ const byId = new Map<string, QueryDescriptor>();
414
+ for (const mod of modules) {
415
+ for (const q of mod.queries ?? []) {
416
+ const descriptor = describeQuery(sorted, q);
417
+ const existing = byId.get(q.id);
418
+ if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
419
+ throw new Error(
420
+ `build-package: query id '${q.id}' is declared with DIVERGENT routing across ` +
421
+ `domains — one id cannot map to two routes. Reconcile the query declaration.`,
422
+ );
423
+ }
424
+ byId.set(q.id, descriptor);
425
+ }
426
+ }
427
+ const queries = [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
428
+
429
+ const countsById = new Map<string, CountDescriptor>();
430
+ for (const mod of modules) {
431
+ for (const raw of mod.counts ?? []) {
432
+ const c = finishCount(raw);
433
+ const descriptor = describeCount(sorted, c);
434
+ const existing = countsById.get(c.id);
435
+ if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
436
+ throw new Error(
437
+ `build-package: count id '${c.id}' is declared with DIVERGENT maintenance across ` +
438
+ `domains — one id cannot map to two counters. Reconcile the count declaration.`,
439
+ );
440
+ }
441
+ countsById.set(c.id, descriptor);
442
+ }
443
+ }
444
+ const counts = [...countsById.values()].sort((a, b) => a.id.localeCompare(b.id));
445
+
446
+ const spatialsById = new Map<string, SpatialDescriptor>();
447
+ for (const mod of modules) {
448
+ for (const s of mod.spatials ?? []) {
449
+ const descriptor = describeSpatial(sorted, s);
450
+ const existing = spatialsById.get(s.id);
451
+ if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
452
+ throw new Error(
453
+ `build-package: spatial id '${s.id}' is declared with DIVERGENT maintenance across ` +
454
+ `domains — one id cannot map to two indexes. Reconcile the spatial declaration.`,
455
+ );
456
+ }
457
+ spatialsById.set(s.id, descriptor);
458
+ }
459
+ }
460
+ const spatials = [...spatialsById.values()].sort((a, b) => a.id.localeCompare(b.id));
461
+
462
+ const derivedsByNamespaceAndId = new Map<string, DerivedDescriptor>();
463
+ for (const mod of modules) {
464
+ for (const d of mod.deriveds ?? []) {
465
+ const descriptor = describeDerived(sorted, d);
466
+ const key = `${descriptor.of}\u0000${descriptor.id}`;
467
+ const existing = derivedsByNamespaceAndId.get(key);
468
+ if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
469
+ throw new Error(
470
+ `build-package: derived field '${descriptor.of}.${descriptor.id}' is declared ` +
471
+ `with DIVERGENT projection across domains. Reconcile the declaration.`,
472
+ );
473
+ }
474
+ derivedsByNamespaceAndId.set(key, descriptor);
475
+ }
476
+ }
477
+ const deriveds = [...derivedsByNamespaceAndId.values()].sort(
478
+ (a, b) => a.of.localeCompare(b.of) || a.id.localeCompare(b.id),
479
+ );
480
+
481
+ const combinedsByNamespaceAndId = new Map<string, CombinedDescriptor>();
482
+ for (const mod of modules) {
483
+ for (const c of mod.combineds ?? []) {
484
+ const descriptor = describeCombined(sorted, c);
485
+ const key = `${descriptor.of}\u0000${descriptor.id}`;
486
+ const existing = combinedsByNamespaceAndId.get(key);
487
+ if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
488
+ throw new Error(
489
+ `build-package: combined field '${descriptor.of}.${descriptor.id}' is declared ` +
490
+ `with DIVERGENT projection across domains. Reconcile the declaration.`,
491
+ );
492
+ }
493
+ combinedsByNamespaceAndId.set(key, descriptor);
494
+ }
495
+ }
496
+ const combineds = [...combinedsByNamespaceAndId.values()].sort(
497
+ (a, b) => a.of.localeCompare(b.of) || a.id.localeCompare(b.id),
498
+ );
499
+
500
+ return { aggregateFieldKinds: sorted, queries, counts, spatials, deriveds, combineds };
501
+ }
502
+
503
+ // ─────────────────────────────────────────────────────────────────────────────────
504
+ // buildIdentity / writeIdentity — the IDENTITY artifacts (hoisted from emit_identity.ts)
505
+ // ─────────────────────────────────────────────────────────────────────────────────
506
+
507
+ /** One domain that could not produce a canonical identity, with the reason recorded. */
508
+ export interface ExcludedDomain {
509
+ readonly domain: string;
510
+ readonly reason: string;
511
+ }
512
+
513
+ /** The result of emitting the identity manifest over a set of domain modules. */
514
+ export interface IdentityEmit {
515
+ /** `{domain: canonicalManifestString}` — the gate's NOMOS_DOMAIN_MANIFEST shape. */
516
+ readonly manifests: Record<string, string>;
517
+ /** `{domain: sha256(canonicalManifest)}` — the certified hash registry. */
518
+ readonly hashes: Record<string, string>;
519
+ /** Domains OMITTED because they have no canonical identity (palette gap), recorded. */
520
+ readonly excluded: ExcludedDomain[];
521
+ }
522
+
523
+ /**
524
+ * Build the identity manifest + hashes. A domain whose canonical manifest THROWS (a
525
+ * palette-only driver the kernel cannot lower) is RECORDED in `excluded` and omitted —
526
+ * never fabricated. The cross-language anchor (`sha256(bytes) === domainHash`) is
527
+ * asserted for every EMITTED domain; an anchor failure is a hard error.
528
+ */
529
+ export function buildIdentity(modules: readonly DomainModule[]): IdentityEmit {
530
+ const manifests: Record<string, string> = {};
531
+ const hashes: Record<string, string> = {};
532
+ const excluded: ExcludedDomain[] = [];
533
+
534
+ for (const mod of [...modules].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
535
+ let bytes: Buffer;
536
+ let hash: string;
537
+ try {
538
+ bytes = emitManifestBytes(mod);
539
+ hash = domainHash(mod);
540
+ } catch (e) {
541
+ // No canonical identity under the current kernel palette — record, do not fabricate.
542
+ excluded.push({ domain: mod.name, reason: (e as Error).message });
543
+ continue;
544
+ }
545
+ const fileHash = createHash("sha256").update(bytes).digest("hex");
546
+ if (fileHash !== hash) {
547
+ throw new Error(
548
+ `build-package: ${mod.name} manifest-byte sha256 ${fileHash} != domainHash ${hash} — ` +
549
+ `the identity anchor is broken (the gate would reject these bytes).`,
550
+ );
551
+ }
552
+ manifests[mod.name] = bytes.toString("utf8");
553
+ hashes[mod.name] = hash;
554
+ }
555
+
556
+ if (Object.keys(manifests).length === 0) {
557
+ throw new Error(
558
+ "build-package: produced NO domain identities — refusing to emit an empty registry.",
559
+ );
560
+ }
561
+ return { manifests, hashes, excluded };
562
+ }
563
+
564
+ /**
565
+ * Serialize the identity emit to its on-disk artifacts. The consumed maps stay
566
+ * STRICTLY `{domain: value}` (the Rust `parse_identity_map` treats EVERY key as a
567
+ * domain and fail-closes the WHOLE map on one bad entry); the exclusion record goes
568
+ * to a SEPARATE sidecar the gate never reads.
569
+ */
570
+ export function writeIdentity(
571
+ emit: IdentityEmit,
572
+ outDir: string,
573
+ ): { manifestsPath: string; hashesPath: string; excludedPath: string } {
574
+ mkdirSync(outDir, { recursive: true });
575
+ const manifestsPath = join(outDir, "domain_identity_manifests.json");
576
+ const hashesPath = join(outDir, "domain_identity_hashes.json");
577
+ const excludedPath = join(outDir, "domain_identity_excluded.json");
578
+ writeFileSync(manifestsPath, JSON.stringify(emit.manifests, null, 2) + "\n", "utf8");
579
+ writeFileSync(hashesPath, JSON.stringify(emit.hashes, null, 2) + "\n", "utf8");
580
+ writeFileSync(excludedPath, JSON.stringify(emit.excluded, null, 2) + "\n", "utf8");
581
+ return { manifestsPath, hashesPath, excludedPath };
582
+ }
583
+
584
+ // ─────────────────────────────────────────────────────────────────────────────────
585
+ // The .package.usda envelope + the USD-IR emit
586
+ // ─────────────────────────────────────────────────────────────────────────────────
587
+
588
+ /** The package format marker the kernel checks (domain_package.rs / build_nomos.mjs). */
589
+ export const NOMOS_DOMAIN_PACKAGE_FORMAT = "nomos.openusd-ir-package.v1";
590
+
591
+ /** Lowercase hex of the UTF-8 bytes (== build_nomos.mjs `hexUtf8` == Rust `hex_encode`). */
592
+ export function hexUtf8(text: string): string {
593
+ return Buffer.from(text, "utf8").toString("hex");
594
+ }
595
+
596
+ /**
597
+ * The `.package.usda` envelope — BYTE-EXACT to `build_nomos.mjs` `packageUsda()` and
598
+ * Rust `domain_package_from_js` (deterministic-rquickjs/src/domain_package.rs:11-33).
599
+ * The kernel's acceptance is substring-based field extraction over this template; the
600
+ * domainHash the deploy names is sha256 over the WHOLE returned string (trailing
601
+ * newline included) exactly as committed.
602
+ */
603
+ export function packageUsda(usdJson: string, javascript: string): string {
604
+ return `#usda 1.0
605
+ (
606
+ customData = {
607
+ string nomos:format = "${NOMOS_DOMAIN_PACKAGE_FORMAT}"
608
+ }
609
+ )
610
+
611
+ def "NomosDomainPackage"
612
+ {
613
+ custom string nomos:usdJsonHex = "${hexUtf8(usdJson)}"
614
+ custom string nomos:executable:language = "javascript"
615
+ custom string nomos:executable:javascriptHex = "${hexUtf8(javascript)}"
616
+ }
617
+ `;
618
+ }
619
+
620
+ /**
621
+ * The USD-IR JSON for a package's modules: `emitUsd` over `/Nomos/<domain>` layers,
622
+ * `JSON.stringify`'d EXACTLY like every `emit_*_usd_json.ts` (single line, insertion
623
+ * order — `emitUsd`'s sorted layers/prims are what make it reproducible; do NOT
624
+ * canonicalize or pretty-print, the package hash rides on these bytes).
625
+ */
626
+ export function emitUsdJsonForModules(modules: readonly DomainModule[]): string {
627
+ const doc = emitUsd(
628
+ modules.map((mod) => ({ path: `/Nomos/${mod.domain ?? mod.name}`, module: mod })),
629
+ );
630
+ return JSON.stringify(doc);
631
+ }
632
+
633
+ /** sha256 hex over a UTF-8 string — the deploy identity (`policy:{hash}`). */
634
+ export function sha256HexUtf8(text: string): string {
635
+ return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
636
+ }