@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,496 @@
|
|
|
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
|
+
* `nomos-compile` — ONE COMMAND: TS DSL domains → a deployable `.package.usda` (#M4).
|
|
10
|
+
*
|
|
11
|
+
* Run from a package that depends on `@githolon/dsl` (esbuild + tsx ride along as its
|
|
12
|
+
* dependencies — no devDependency setup), next to a `nomos.package.mjs` config:
|
|
13
|
+
*
|
|
14
|
+
* export default {
|
|
15
|
+
* name: "guestbook", // → build/guestbook.package.usda
|
|
16
|
+
* domains: [
|
|
17
|
+
* { key: "guestbook", modules: ["./domains/guestbook.ts"] },
|
|
18
|
+
* ],
|
|
19
|
+
* };
|
|
20
|
+
*
|
|
21
|
+
* Per domain: `key` is the engine dispatch key AND the identity-manifest name
|
|
22
|
+
* (`name` overrides the latter); `modules` is the ORDERED module list (later wins on
|
|
23
|
+
* a name collision — the framework `identity` union pattern). Read-side declarations
|
|
24
|
+
* (queries / counts / spatials / deriveds / combineds) are AUTO-DISCOVERED from the
|
|
25
|
+
* merged module exports by SHAPE (both individual decls and exported arrays of them,
|
|
26
|
+
* deduped); anything undiscoverable can be passed explicitly as EXPORT NAMES, e.g.
|
|
27
|
+
* `{ sums: ["GUESTBOOK_SUMS"], extraAggregates: ["SomeFrameworkHandle"] }`.
|
|
28
|
+
* Reports: `{ reports: { my_report_v1: "./report/my_report.ts#myReportV1" } }`.
|
|
29
|
+
*
|
|
30
|
+
* It emits into `outDir` (default `<config dir>/build`):
|
|
31
|
+
* * `<name>.package.usda` — the deployable package (engine bundle + USD IR);
|
|
32
|
+
* its sha256 IS the deploy `domainHash` (`policy:{hash}`).
|
|
33
|
+
* * `domain_manifests.json` — the READ manifest (field kinds + query routes).
|
|
34
|
+
* * `domain_identity_manifests.json` + `domain_identity_hashes.json` (+ excluded
|
|
35
|
+
* sidecar) — the per-domain IDENTITY artifacts (admission gates).
|
|
36
|
+
* * `<name>.deploy.json` — `{packageUsda, readManifest, identityManifests,
|
|
37
|
+
* identityHashes}`: the EXACT body `POST /v1/workspaces/:ws/domains` accepts as
|
|
38
|
+
* `application/json` on Nomos Cloud (package + per-workspace manifest overlay in
|
|
39
|
+
* one deploy).
|
|
40
|
+
*
|
|
41
|
+
* The engine entry is GENERATED (imports + one `registerEngine` call — the machinery
|
|
42
|
+
* lives in `@githolon/dsl/engine-entry`), bundled with the SAME deterministic esbuild
|
|
43
|
+
* settings as every existing package (`--bundle --format=iife --target=es2020
|
|
44
|
+
* --platform=neutral --charset=utf8`, no minify), and refused if it does not assign
|
|
45
|
+
* `globalThis.plan`.
|
|
46
|
+
*/
|
|
47
|
+
import { execFileSync } from "node:child_process";
|
|
48
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
49
|
+
import { createRequire } from "node:module";
|
|
50
|
+
import path from "node:path";
|
|
51
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
52
|
+
|
|
53
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
54
|
+
import { generateDartDomain, type DomainModule } from "./codegen_dart.js";
|
|
55
|
+
import type { AnyCount } from "./count.js";
|
|
56
|
+
import type { QueryDecl } from "./query.js";
|
|
57
|
+
import type { SpatialDecl } from "./spatial.js";
|
|
58
|
+
import type { AnySum } from "./sum.js";
|
|
59
|
+
import type { DerivedDecl } from "./derived.js";
|
|
60
|
+
import type { CombinedDecl } from "./combined.js";
|
|
61
|
+
import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
|
|
62
|
+
import {
|
|
63
|
+
buildIdentity,
|
|
64
|
+
buildReadManifest,
|
|
65
|
+
composeDomainModule,
|
|
66
|
+
emitUsdJsonForModules,
|
|
67
|
+
packageUsda,
|
|
68
|
+
sha256HexUtf8,
|
|
69
|
+
writeIdentity,
|
|
70
|
+
type Mod,
|
|
71
|
+
} from "./build_package.js";
|
|
72
|
+
import { generateTsClient } from "./codegen_ts.js";
|
|
73
|
+
|
|
74
|
+
const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
75
|
+
|
|
76
|
+
interface DomainConfig {
|
|
77
|
+
readonly key: string;
|
|
78
|
+
readonly name?: string;
|
|
79
|
+
readonly modules: readonly string[];
|
|
80
|
+
// Explicit EXPORT-NAME escape hatches (resolved on the merged module exports):
|
|
81
|
+
readonly queries?: readonly string[];
|
|
82
|
+
readonly counts?: readonly string[];
|
|
83
|
+
readonly sums?: readonly string[];
|
|
84
|
+
readonly spatials?: readonly string[];
|
|
85
|
+
readonly deriveds?: readonly string[];
|
|
86
|
+
readonly combineds?: readonly string[];
|
|
87
|
+
readonly impureCapabilities?: readonly string[];
|
|
88
|
+
readonly extraAggregates?: readonly string[];
|
|
89
|
+
readonly readModelAggregates?: readonly string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface PackageConfig {
|
|
93
|
+
readonly name: string;
|
|
94
|
+
readonly domains: readonly DomainConfig[];
|
|
95
|
+
/** reportId → "modulePath#exportName" (the export is `(actor) => Report`). */
|
|
96
|
+
readonly reports?: Record<string, string>;
|
|
97
|
+
readonly outDir?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Typed DART frontend (the Flutter sibling of the generated TS client; #codegen_dart).
|
|
100
|
+
* `false`/absent = skip (default). `true` = emit into `<outDir>/dart/`;
|
|
101
|
+
* `{ out: "relative/dir" }` = emit there (resolved against the config dir).
|
|
102
|
+
* Per composed domain one `<name>.dart` (payload classes + read models + typed read
|
|
103
|
+
* accessors via the SAME `generateDartDomain` the co2 tenant emit uses), PLUS the
|
|
104
|
+
* framework `nomos_dsl` Dart support files vendored alongside (the generated files'
|
|
105
|
+
* `package:nomos_dsl/...` import is rewritten to a relative one) — so a Flutter app
|
|
106
|
+
* can vendor the directory as-is, no pub deps.
|
|
107
|
+
*/
|
|
108
|
+
readonly dart?: boolean | { readonly out?: string };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function fail(msg: string): never {
|
|
112
|
+
console.error(`nomos-compile: ${msg}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── decl auto-discovery (by SHAPE, mirroring the engine entry's duck-types) ───────
|
|
117
|
+
|
|
118
|
+
function isQueryDecl(v: unknown): v is QueryDecl {
|
|
119
|
+
return (
|
|
120
|
+
!!v &&
|
|
121
|
+
typeof v === "object" &&
|
|
122
|
+
typeof (v as { id?: unknown }).id === "string" &&
|
|
123
|
+
Array.isArray((v as { key?: unknown }).key) &&
|
|
124
|
+
((v as { key: unknown[] }).key as unknown[]).every((k) => typeof k === "string") &&
|
|
125
|
+
typeof (v as { returns?: unknown }).returns === "string" &&
|
|
126
|
+
typeof (v as { fn?: unknown }).fn !== "function"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isSpatialDecl(v: unknown): v is SpatialDecl {
|
|
131
|
+
return (
|
|
132
|
+
!!v &&
|
|
133
|
+
typeof v === "object" &&
|
|
134
|
+
typeof (v as { id?: unknown }).id === "string" &&
|
|
135
|
+
typeof (v as { of?: unknown }).of === "string" &&
|
|
136
|
+
typeof (v as { on?: unknown }).on === "string" &&
|
|
137
|
+
typeof (v as { fn?: unknown }).fn !== "function"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isDerivedDecl(v: unknown): v is DerivedDecl {
|
|
142
|
+
return (
|
|
143
|
+
!!v &&
|
|
144
|
+
typeof v === "object" &&
|
|
145
|
+
typeof (v as { id?: unknown }).id === "string" &&
|
|
146
|
+
typeof (v as { of?: unknown }).of === "string" &&
|
|
147
|
+
typeof (v as { fn?: unknown }).fn === "function" &&
|
|
148
|
+
(v as { returns?: unknown }).returns !== undefined &&
|
|
149
|
+
typeof (v as { refField?: unknown }).refField !== "string" &&
|
|
150
|
+
typeof (v as { reads?: unknown }).reads !== "string"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isCombinedDecl(v: unknown): v is CombinedDecl {
|
|
155
|
+
return (
|
|
156
|
+
!!v &&
|
|
157
|
+
typeof v === "object" &&
|
|
158
|
+
typeof (v as { id?: unknown }).id === "string" &&
|
|
159
|
+
typeof (v as { of?: unknown }).of === "string" &&
|
|
160
|
+
typeof (v as { refField?: unknown }).refField === "string" &&
|
|
161
|
+
typeof (v as { reads?: unknown }).reads === "string" &&
|
|
162
|
+
typeof (v as { fn?: unknown }).fn === "function" &&
|
|
163
|
+
(v as { returns?: unknown }).returns !== undefined
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A count decl/builder — `{id, of}` with NONE of the other decl families' marks. */
|
|
168
|
+
function isCountDecl(v: unknown): v is AnyCount {
|
|
169
|
+
if (!v || typeof v !== "object") return false;
|
|
170
|
+
const o = v as Record<string, unknown>;
|
|
171
|
+
return (
|
|
172
|
+
typeof o.id === "string" &&
|
|
173
|
+
typeof o.of === "string" &&
|
|
174
|
+
typeof o.on !== "string" && // spatial
|
|
175
|
+
typeof o.fn !== "function" && // derived/combined
|
|
176
|
+
!Array.isArray(o.key) && // query
|
|
177
|
+
!("field" in o) && // sum / extremum
|
|
178
|
+
!("orderBy" in o) && // ordered read
|
|
179
|
+
!("_existsMarker" in o) && // exists
|
|
180
|
+
typeof o.refField !== "string" && // combined
|
|
181
|
+
o.__isAggregateHandle !== true && // aggregate
|
|
182
|
+
typeof o.aggregateId !== "string" && // directive
|
|
183
|
+
typeof o.plan !== "function"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Scan merged exports for decls matching `match`: individual exports AND exported arrays. */
|
|
188
|
+
function discover<T>(merged: Mod, match: (v: unknown) => v is T): T[] {
|
|
189
|
+
const seen = new Set<string>();
|
|
190
|
+
const out: T[] = [];
|
|
191
|
+
const push = (v: T) => {
|
|
192
|
+
const key = JSON.stringify(v, (_k, val) => (typeof val === "function" ? "<fn>" : val));
|
|
193
|
+
if (!seen.has(key)) {
|
|
194
|
+
seen.add(key);
|
|
195
|
+
out.push(v);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
for (const v of Object.values(merged)) {
|
|
199
|
+
if (match(v)) push(v);
|
|
200
|
+
else if (Array.isArray(v)) for (const el of v) if (match(el)) push(el);
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Resolve explicit EXPORT NAMES (the config escape hatch) on the merged exports. */
|
|
206
|
+
function resolveNamed<T>(merged: Mod, names: readonly string[] | undefined, what: string): T[] {
|
|
207
|
+
const out: T[] = [];
|
|
208
|
+
for (const n of names ?? []) {
|
|
209
|
+
const v = merged[n];
|
|
210
|
+
if (v === undefined) fail(`config names ${what} export '${n}', which the modules do not export`);
|
|
211
|
+
if (Array.isArray(v)) out.push(...(v as T[]));
|
|
212
|
+
else out.push(v as T);
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Dart emission (the Flutter sibling of the TS client; reuses generateDartDomain) ──
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The generated files' support import, exactly as `generateDartDomain` emits it.
|
|
221
|
+
* Vendored output rewrites it to the relative barrel so the directory is
|
|
222
|
+
* self-contained (no pub path-dependency on the framework package).
|
|
223
|
+
*/
|
|
224
|
+
const DART_PACKAGE_IMPORT = `import 'package:nomos_dsl/nomos_dsl.dart';`;
|
|
225
|
+
const DART_RELATIVE_IMPORT = `import 'nomos_dsl.dart';`;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The framework `nomos_dsl` Dart support files the generated code resolves through
|
|
229
|
+
* the barrel (`dsl/dart/lib/`). `schema_validation.dart` is deliberately NOT
|
|
230
|
+
* vendored: nothing the codegen emits references it, and it would drag the external
|
|
231
|
+
* `json_schema` pub dependency into every vendoring app.
|
|
232
|
+
*/
|
|
233
|
+
const DART_SUPPORT_SRC = [
|
|
234
|
+
"wire.dart",
|
|
235
|
+
"driver.dart",
|
|
236
|
+
"dispatch.dart",
|
|
237
|
+
"subscriptions.dart",
|
|
238
|
+
"provider.dart",
|
|
239
|
+
] as const;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Emit the typed Dart for the composed domain modules into `dartOut`: one
|
|
243
|
+
* `<name>.dart` per domain (the SAME `generateDartDomain` path the co2 tenant's
|
|
244
|
+
* `emit_dart.ts` drives) + the vendored `nomos_dsl.dart` barrel and its `src/`
|
|
245
|
+
* support files, with the generated files' `package:` import rewritten relative.
|
|
246
|
+
*/
|
|
247
|
+
function emitDart(modules: readonly DomainModule[], dartOut: string): string[] {
|
|
248
|
+
mkdirSync(path.join(dartOut, "src"), { recursive: true });
|
|
249
|
+
|
|
250
|
+
// 1. the vendored framework support (barrel + src/), copied byte-for-byte except
|
|
251
|
+
// the barrel's schema_validation export (see DART_SUPPORT_SRC).
|
|
252
|
+
const libDir = path.join(DSL_DIR, "dart", "lib");
|
|
253
|
+
const barrel = readFileSync(path.join(libDir, "nomos_dsl.dart"), "utf8")
|
|
254
|
+
.split("\n")
|
|
255
|
+
.filter((line) => !line.includes("src/schema_validation.dart"))
|
|
256
|
+
.join("\n");
|
|
257
|
+
writeFileSync(path.join(dartOut, "nomos_dsl.dart"), barrel, "utf8");
|
|
258
|
+
for (const f of DART_SUPPORT_SRC) {
|
|
259
|
+
copyFileSync(path.join(libDir, "src", f), path.join(dartOut, "src", f));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 2. one generated file per composed domain.
|
|
263
|
+
const written: string[] = [];
|
|
264
|
+
for (const mod of modules) {
|
|
265
|
+
const src = generateDartDomain(mod);
|
|
266
|
+
if (!src.includes(DART_PACKAGE_IMPORT)) {
|
|
267
|
+
fail(
|
|
268
|
+
`generated Dart for domain '${mod.name}' does not carry the expected support import (${DART_PACKAGE_IMPORT}) — codegen_dart header drifted; fix the rewrite in compile_package_main.ts`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const p = path.join(dartOut, `${mod.name}.dart`);
|
|
272
|
+
writeFileSync(p, src.replace(DART_PACKAGE_IMPORT, DART_RELATIVE_IMPORT), "utf8");
|
|
273
|
+
written.push(p);
|
|
274
|
+
}
|
|
275
|
+
return written;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── main ──────────────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
async function main(): Promise<void> {
|
|
281
|
+
const configArg = process.argv[2] ?? "nomos.package.mjs";
|
|
282
|
+
const configPath = path.resolve(process.cwd(), configArg);
|
|
283
|
+
if (!existsSync(configPath)) fail(`config not found: ${configPath}`);
|
|
284
|
+
const cfgDir = path.dirname(configPath);
|
|
285
|
+
|
|
286
|
+
const cfgModule = (await import(pathToFileURL(configPath).href)) as { default?: PackageConfig };
|
|
287
|
+
const cfg = cfgModule.default;
|
|
288
|
+
if (!cfg || typeof cfg.name !== "string" || !Array.isArray(cfg.domains) || cfg.domains.length === 0) {
|
|
289
|
+
fail(`config must default-export { name, domains: [{ key, modules }, ...] }`);
|
|
290
|
+
}
|
|
291
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(cfg.name)) fail(`package name '${cfg.name}' is not a safe basename`);
|
|
292
|
+
|
|
293
|
+
const outDir = path.resolve(cfgDir, cfg.outDir ?? "build");
|
|
294
|
+
mkdirSync(outDir, { recursive: true });
|
|
295
|
+
|
|
296
|
+
// ── import + compose each domain ──
|
|
297
|
+
const entryImports: string[] = [];
|
|
298
|
+
const entryDomains: string[] = [];
|
|
299
|
+
const domainModules: DomainModule[] = [];
|
|
300
|
+
let importSeq = 0;
|
|
301
|
+
|
|
302
|
+
for (const d of cfg.domains) {
|
|
303
|
+
if (typeof d.key !== "string" || !Array.isArray(d.modules) || d.modules.length === 0) {
|
|
304
|
+
fail(`each domain needs { key, modules: [path, ...] }; got ${JSON.stringify(d)}`);
|
|
305
|
+
}
|
|
306
|
+
const mods: Mod[] = [];
|
|
307
|
+
const varNames: string[] = [];
|
|
308
|
+
for (const rel of d.modules) {
|
|
309
|
+
const abs = path.resolve(cfgDir, rel);
|
|
310
|
+
if (!existsSync(abs)) fail(`domain '${d.key}': module not found: ${abs}`);
|
|
311
|
+
const m = (await import(pathToFileURL(abs).href)) as Mod;
|
|
312
|
+
mods.push(m);
|
|
313
|
+
const v = `m${importSeq++}`;
|
|
314
|
+
varNames.push(v);
|
|
315
|
+
entryImports.push(`import * as ${v} from ${JSON.stringify(abs)};`);
|
|
316
|
+
}
|
|
317
|
+
entryDomains.push(` ${JSON.stringify(d.key)}: [${varNames.join(", ")}],`);
|
|
318
|
+
|
|
319
|
+
let merged: Mod = {};
|
|
320
|
+
for (const m of mods) merged = { ...merged, ...m };
|
|
321
|
+
|
|
322
|
+
const queries = [
|
|
323
|
+
...discover<QueryDecl>(merged, isQueryDecl),
|
|
324
|
+
...resolveNamed<QueryDecl>(merged, d.queries, "queries"),
|
|
325
|
+
];
|
|
326
|
+
const counts = [
|
|
327
|
+
...discover<AnyCount>(merged, isCountDecl),
|
|
328
|
+
...resolveNamed<AnyCount>(merged, d.counts, "counts"),
|
|
329
|
+
];
|
|
330
|
+
const spatials = [
|
|
331
|
+
...discover<SpatialDecl>(merged, isSpatialDecl),
|
|
332
|
+
...resolveNamed<SpatialDecl>(merged, d.spatials, "spatials"),
|
|
333
|
+
];
|
|
334
|
+
const deriveds = [
|
|
335
|
+
...discover<DerivedDecl>(merged, isDerivedDecl),
|
|
336
|
+
...resolveNamed<DerivedDecl>(merged, d.deriveds, "deriveds"),
|
|
337
|
+
];
|
|
338
|
+
const combineds = [
|
|
339
|
+
...discover<CombinedDecl>(merged, isCombinedDecl),
|
|
340
|
+
...resolveNamed<CombinedDecl>(merged, d.combineds, "combineds"),
|
|
341
|
+
];
|
|
342
|
+
const sums = resolveNamed<AnySum>(merged, d.sums, "sums");
|
|
343
|
+
const impureCapabilities = resolveNamed<ImpureCapabilityDecl>(
|
|
344
|
+
merged,
|
|
345
|
+
d.impureCapabilities,
|
|
346
|
+
"impureCapabilities",
|
|
347
|
+
);
|
|
348
|
+
const extraAggregates = resolveNamed<AggregateHandle>(merged, d.extraAggregates, "extraAggregates");
|
|
349
|
+
|
|
350
|
+
domainModules.push(
|
|
351
|
+
composeDomainModule({
|
|
352
|
+
name: d.name ?? d.key,
|
|
353
|
+
domain: d.key,
|
|
354
|
+
modules: mods,
|
|
355
|
+
...(extraAggregates.length > 0 ? { extraAggregates } : {}),
|
|
356
|
+
...(queries.length > 0 ? { queries } : {}),
|
|
357
|
+
...(counts.length > 0 ? { counts } : {}),
|
|
358
|
+
...(d.readModelAggregates !== undefined
|
|
359
|
+
? { readModelAggregates: d.readModelAggregates }
|
|
360
|
+
: {}),
|
|
361
|
+
...(deriveds.length > 0 ? { deriveds } : {}),
|
|
362
|
+
...(combineds.length > 0 ? { combineds } : {}),
|
|
363
|
+
...(impureCapabilities.length > 0 ? { impureCapabilities } : {}),
|
|
364
|
+
...(sums.length > 0 ? { sums } : {}),
|
|
365
|
+
...(spatials.length > 0 ? { spatials } : {}),
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── reports ──
|
|
371
|
+
const entryReports: string[] = [];
|
|
372
|
+
for (const [reportId, ref] of Object.entries(cfg.reports ?? {})) {
|
|
373
|
+
const [rel, exportName] = ref.split("#");
|
|
374
|
+
if (!rel || !exportName) fail(`report '${reportId}' must be "modulePath#exportName"; got '${ref}'`);
|
|
375
|
+
const abs = path.resolve(cfgDir, rel);
|
|
376
|
+
if (!existsSync(abs)) fail(`report '${reportId}': module not found: ${abs}`);
|
|
377
|
+
const v = `r${importSeq++}`;
|
|
378
|
+
entryImports.push(`import * as ${v} from ${JSON.stringify(abs)};`);
|
|
379
|
+
entryReports.push(` ${JSON.stringify(reportId)}: ${v}.${exportName},`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── 1. generate + bundle the engine entry ──
|
|
383
|
+
const entrySource = [
|
|
384
|
+
`// AUTO-GENERATED by nomos-compile from ${path.basename(configPath)} — do not edit.`,
|
|
385
|
+
...entryImports,
|
|
386
|
+
`import { registerEngine } from "@githolon/dsl/engine-entry";`,
|
|
387
|
+
``,
|
|
388
|
+
`registerEngine({`,
|
|
389
|
+
` domains: {`,
|
|
390
|
+
...entryDomains,
|
|
391
|
+
` },`,
|
|
392
|
+
...(entryReports.length > 0 ? [` reports: {`, ...entryReports, ` },`] : []),
|
|
393
|
+
`});`,
|
|
394
|
+
``,
|
|
395
|
+
].join("\n");
|
|
396
|
+
const entryPath = path.join(outDir, ".nomos_entry.ts");
|
|
397
|
+
writeFileSync(entryPath, entrySource, "utf8");
|
|
398
|
+
|
|
399
|
+
// Resolve esbuild through Node's resolver (caller's copy first, else the DSL's
|
|
400
|
+
// own dependency) — NOT node_modules/.bin paths, which hoisted/workspace
|
|
401
|
+
// installs relocate. The pinned dependency keeps the engine-lump bytes (and so
|
|
402
|
+
// the content-addressed domainHash) reproducible across machines.
|
|
403
|
+
const esbuildPkgJson = [cfgDir, DSL_DIR]
|
|
404
|
+
.map((dir) => {
|
|
405
|
+
try {
|
|
406
|
+
return createRequire(pathToFileURL(path.join(dir, "noop.js"))).resolve("esbuild/package.json");
|
|
407
|
+
} catch {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
.find((p): p is string => p !== undefined);
|
|
412
|
+
if (!esbuildPkgJson) fail(`esbuild not found — reinstall @githolon/dsl (esbuild is one of its dependencies)`);
|
|
413
|
+
const esbuildPkg = JSON.parse(readFileSync(esbuildPkgJson, "utf8")) as { bin?: string | Record<string, string> };
|
|
414
|
+
const esbuildBinRel = typeof esbuildPkg.bin === "string" ? esbuildPkg.bin : esbuildPkg.bin?.["esbuild"];
|
|
415
|
+
// Exec the bin DIRECTLY (not via `node`): esbuild's postinstall replaces the
|
|
416
|
+
// JS shim with the platform-native binary, which only an exec can run.
|
|
417
|
+
const esbuildBin = path.join(path.dirname(esbuildPkgJson), esbuildBinRel ?? "bin/esbuild");
|
|
418
|
+
|
|
419
|
+
const javascript =
|
|
420
|
+
execFileSync(
|
|
421
|
+
esbuildBin,
|
|
422
|
+
[entryPath, "--bundle", "--format=iife", "--target=es2020", "--platform=neutral", "--charset=utf8"],
|
|
423
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], cwd: cfgDir, maxBuffer: 256 * 1024 * 1024 },
|
|
424
|
+
).trimEnd() + "\n";
|
|
425
|
+
if (!/globalThis\.plan\s*=/.test(javascript) && !/globalThis\["plan"\]\s*=/.test(javascript)) {
|
|
426
|
+
fail(`bundled engine entry does not assign globalThis.plan — refusing to package`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── 2. USD IR + the package envelope ──
|
|
430
|
+
const usdJson = emitUsdJsonForModules(domainModules);
|
|
431
|
+
const text = packageUsda(usdJson, javascript);
|
|
432
|
+
const pkgPath = path.join(outDir, `${cfg.name}.package.usda`);
|
|
433
|
+
writeFileSync(pkgPath, text, "utf8");
|
|
434
|
+
const domainHash = sha256HexUtf8(text);
|
|
435
|
+
|
|
436
|
+
// ── 3. the READ manifest + IDENTITY artifacts ──
|
|
437
|
+
const readManifest = buildReadManifest(domainModules);
|
|
438
|
+
const readPath = path.join(outDir, "domain_manifests.json");
|
|
439
|
+
writeFileSync(readPath, JSON.stringify(readManifest, null, 2) + "\n", "utf8");
|
|
440
|
+
|
|
441
|
+
const identity = buildIdentity(domainModules);
|
|
442
|
+
const { manifestsPath } = writeIdentity(identity, outDir);
|
|
443
|
+
|
|
444
|
+
// ── 4. the one-call deploy body (package + per-workspace manifest overlay) ──
|
|
445
|
+
const deployBody = {
|
|
446
|
+
packageUsda: text,
|
|
447
|
+
readManifest,
|
|
448
|
+
identityManifests: identity.manifests,
|
|
449
|
+
identityHashes: identity.hashes,
|
|
450
|
+
};
|
|
451
|
+
const deployPath = path.join(outDir, `${cfg.name}.deploy.json`);
|
|
452
|
+
writeFileSync(deployPath, JSON.stringify(deployBody) + "\n", "utf8");
|
|
453
|
+
|
|
454
|
+
// ── 5. the typed TS client (#10 — the web sibling of codegen_dart), with the
|
|
455
|
+
// deployed law's content hash baked in (emitted AFTER the package is hashed) ──
|
|
456
|
+
const clientPath = path.join(outDir, `${cfg.name}.client.ts`);
|
|
457
|
+
writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash }), "utf8");
|
|
458
|
+
|
|
459
|
+
// ── 6. the typed Dart frontend (opt-in; the Flutter sibling of step 5, emitted
|
|
460
|
+
// after the package hash like the TS client) ──
|
|
461
|
+
const dartCfg = cfg.dart ?? false;
|
|
462
|
+
if (dartCfg !== false && dartCfg !== true && (typeof dartCfg !== "object" || dartCfg === null)) {
|
|
463
|
+
fail(`config key 'dart' must be false, true, or { out: "relative/dir" }; got ${JSON.stringify(dartCfg)}`);
|
|
464
|
+
}
|
|
465
|
+
let dartOut: string | undefined;
|
|
466
|
+
let dartFiles: string[] = [];
|
|
467
|
+
if (dartCfg !== false) {
|
|
468
|
+
dartOut = path.resolve(
|
|
469
|
+
cfgDir,
|
|
470
|
+
typeof dartCfg === "object" && dartCfg.out !== undefined ? dartCfg.out : path.join(outDir, "dart"),
|
|
471
|
+
);
|
|
472
|
+
dartFiles = emitDart(domainModules, dartOut);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── summary ──
|
|
476
|
+
const rel = (p: string) => path.relative(process.cwd(), p);
|
|
477
|
+
console.log(`nomos-compile: ${cfg.name}`);
|
|
478
|
+
console.log(` package ${rel(pkgPath)} (${text.length} bytes)`);
|
|
479
|
+
console.log(` domainHash ${domainHash}`);
|
|
480
|
+
console.log(` read ${rel(readPath)} (${Object.keys(readManifest.aggregateFieldKinds).length} aggregate(s), ${readManifest.queries.length} quer(y/ies), ${readManifest.counts.length} count(s))`);
|
|
481
|
+
console.log(` identity ${rel(manifestsPath)} (${Object.keys(identity.manifests).length} domain(s)${identity.excluded.length ? `, ${identity.excluded.length} EXCLUDED (palette gap)` : ""})`);
|
|
482
|
+
for (const [dom, h] of Object.entries(identity.hashes)) console.log(` ${dom.padEnd(20)} ${h}`);
|
|
483
|
+
for (const ex of identity.excluded) console.log(` EXCLUDED ${ex.domain}: ${ex.reason}`);
|
|
484
|
+
console.log(` client ${rel(clientPath)} (typed TS client — payloads, read models, query accessors)`);
|
|
485
|
+
if (dartOut !== undefined) {
|
|
486
|
+
console.log(` dart ${rel(dartOut)}${path.sep} (${dartFiles.length} domain file(s) + vendored nomos_dsl support — typed Dart for Flutter)`);
|
|
487
|
+
}
|
|
488
|
+
console.log(` deploy ${rel(deployPath)}`);
|
|
489
|
+
console.log(``);
|
|
490
|
+
console.log(`deploy (Nomos Cloud):`);
|
|
491
|
+
console.log(` curl -X POST -H 'content-type: application/json' \\`);
|
|
492
|
+
console.log(` --data-binary @${rel(deployPath)} \\`);
|
|
493
|
+
console.log(` https://nomos.captainapp.co.uk/v1/workspaces/<ws>/domains`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await main();
|