@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.
- package/LICENSE.md +36 -0
- package/compile_package.mjs +50 -0
- package/package.json +59 -0
- package/src/aggregate.ts +167 -0
- package/src/authoring.ts +119 -0
- package/src/build_package.ts +636 -0
- package/src/certified_read.ts +313 -0
- package/src/codegen_dart.ts +2732 -0
- package/src/codegen_dot.ts +466 -0
- package/src/codegen_provider_dart.ts +358 -0
- package/src/codegen_ts.ts +365 -0
- package/src/codegen_usda.ts +388 -0
- package/src/combined.ts +195 -0
- package/src/compile_engine.ts +567 -0
- package/src/compile_package_main.ts +496 -0
- package/src/compose.ts +317 -0
- package/src/count.ts +218 -0
- package/src/ctx.ts +57 -0
- package/src/derived.ts +138 -0
- package/src/directive.ts +306 -0
- package/src/drivers.ts +95 -0
- package/src/emits_guard.ts +123 -0
- package/src/engine_entry.ts +449 -0
- package/src/exists.ts +170 -0
- package/src/extremum.ts +227 -0
- package/src/fields.ts +291 -0
- package/src/framework/bootstrap.ts +22 -0
- package/src/framework/disclosure.ts +108 -0
- package/src/framework/domain_lifecycle.ts +108 -0
- package/src/framework/identity.ts +537 -0
- package/src/framework/impure_capability.ts +643 -0
- package/src/framework/rbac.ts +418 -0
- package/src/framework/repair.ts +150 -0
- package/src/framework/sync_lifecycle.ts +125 -0
- package/src/framework/workspace_invariant.ts +128 -0
- package/src/framework/workspaces.ts +817 -0
- package/src/index.ts +317 -0
- package/src/manifest.ts +947 -0
- package/src/ops.ts +145 -0
- package/src/ordered_read.ts +228 -0
- package/src/predicate.ts +203 -0
- package/src/query/compile.ts +0 -0
- package/src/query/relations.ts +144 -0
- package/src/query.ts +151 -0
- package/src/read.ts +54 -0
- package/src/relation.ts +189 -0
- package/src/report/csv.ts +54 -0
- package/src/report.ts +401 -0
- package/src/spatial.ts +115 -0
- package/src/sum.ts +194 -0
- package/src/usd.ts +563 -0
- package/src/wire.ts +149 -0
- 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
|
+
}
|