@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,449 @@
|
|
|
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 ENGINE ENTRY — `registerEngine(config)` (#M4, the one-command compiler).
|
|
10
|
+
*
|
|
11
|
+
* Every tenant engine bundle used to HAND-COPY the same ~500 lines of machinery
|
|
12
|
+
* (`emit_engine.ts` / `emit_nomos_engine.ts`): duck-typed module scans, the
|
|
13
|
+
* `(domain, directiveId)` registry, the derived/combined/invariant registries, the
|
|
14
|
+
* five `planReport` dispatch branches, `portsFromHost`, and the `globalThis.plan`
|
|
15
|
+
* assignments. Worse, `emit_engine.ts` kept a SECOND hand-maintained module list
|
|
16
|
+
* inside its agg-invariant registry — a live drift trap (it had already drifted:
|
|
17
|
+
* the list omitted `co2_repairs`).
|
|
18
|
+
*
|
|
19
|
+
* This module IS that machinery, hoisted into `@githolon/dsl` once. A generated entry
|
|
20
|
+
* shrinks to imports + one call:
|
|
21
|
+
*
|
|
22
|
+
* import * as guestbook from "./domains/guestbook.js";
|
|
23
|
+
* import { registerEngine } from "@githolon/dsl/engine-entry";
|
|
24
|
+
* registerEngine({ domains: { guestbook: [guestbook] } });
|
|
25
|
+
*
|
|
26
|
+
* EVERY registry — directives, deriveds, combineds, relation invariants, aggregate
|
|
27
|
+
* invariants — derives from the SAME `domains` map, so the drift trap is structurally
|
|
28
|
+
* gone. A domain key maps to an ORDERED module list, spread-merged later-wins (the
|
|
29
|
+
* `identity = {...identityCore, ...co2_identity}` pattern), so a tenant can compose
|
|
30
|
+
* framework + tenant modules under one dispatch key.
|
|
31
|
+
*
|
|
32
|
+
* ENGINE-BUNDLE-SAFE: this file (and everything it reaches) imports NO node builtin —
|
|
33
|
+
* it must bundle under esbuild `--platform=neutral` into the sealed QuickJS lump.
|
|
34
|
+
* The contracts mirrored here (wire shapes, vacuous-holds, strikes-conditional
|
|
35
|
+
* return) are documented at the original sites; see `emit_engine.ts` in the co2
|
|
36
|
+
* tenant package for the long-form rationale of each branch.
|
|
37
|
+
*/
|
|
38
|
+
import { executeDirectiveToIntent } from "./wire_encode.js";
|
|
39
|
+
import type { Directive } from "./directive.js";
|
|
40
|
+
import type { AggregateHandle, AggregateInvariantFn, AggregateInvariantVerdict } from "./aggregate.js";
|
|
41
|
+
import type { Ports } from "./ctx.js";
|
|
42
|
+
import type { WireEvent, WireHlc } from "./wire.js";
|
|
43
|
+
import type { DerivedDecl } from "./derived.js";
|
|
44
|
+
import type { CombinedDecl } from "./combined.js";
|
|
45
|
+
import type { InvariantBody, InvariantEvidence, InvariantVerdict } from "./relation.js";
|
|
46
|
+
import type { QueryRow } from "./report.js";
|
|
47
|
+
|
|
48
|
+
/** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
|
|
49
|
+
export type DomainModuleExports = Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
/** A report: declarative query + render; the host feeds rows, the engine renders. */
|
|
52
|
+
export interface EngineReport {
|
|
53
|
+
render(rows: QueryRow[]): string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The one declarative input: dispatch key → ORDERED module list (later wins). */
|
|
57
|
+
export interface EngineEntryConfig {
|
|
58
|
+
/**
|
|
59
|
+
* Domain dispatch key → the modules composing it, spread-merged IN ORDER
|
|
60
|
+
* (later overrides earlier on a name collision — the `identity` union pattern).
|
|
61
|
+
*/
|
|
62
|
+
readonly domains: Record<string, readonly DomainModuleExports[]>;
|
|
63
|
+
/** Optional report registry: `reportId` → a factory `(actor) => Report`. */
|
|
64
|
+
readonly reports?: Record<string, (actor: string) => EngineReport>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface RegistryEntry {
|
|
68
|
+
directive: Directive<unknown>;
|
|
69
|
+
agg: AggregateHandle;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Collect every exported `AggregateHandle` from a domain module, by wire id. */
|
|
73
|
+
function aggregatesOf(mod: DomainModuleExports): Map<string, AggregateHandle> {
|
|
74
|
+
const out = new Map<string, AggregateHandle>();
|
|
75
|
+
for (const v of Object.values(mod)) {
|
|
76
|
+
if (
|
|
77
|
+
v &&
|
|
78
|
+
typeof v === "object" &&
|
|
79
|
+
(v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true
|
|
80
|
+
) {
|
|
81
|
+
const h = v as AggregateHandle;
|
|
82
|
+
out.set(h.id, h);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Collect every exported `Directive` from a domain module, by directive id. */
|
|
89
|
+
function directivesOf(mod: DomainModuleExports): Map<string, Directive<unknown>> {
|
|
90
|
+
const out = new Map<string, Directive<unknown>>();
|
|
91
|
+
for (const v of Object.values(mod)) {
|
|
92
|
+
if (
|
|
93
|
+
v &&
|
|
94
|
+
typeof v === "object" &&
|
|
95
|
+
typeof (v as { id?: unknown }).id === "string" &&
|
|
96
|
+
typeof (v as { plan?: unknown }).plan === "function" &&
|
|
97
|
+
typeof (v as { aggregateId?: unknown }).aggregateId === "string"
|
|
98
|
+
) {
|
|
99
|
+
const d = v as Directive<unknown>;
|
|
100
|
+
out.set(d.id, d);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Collect every exported `DerivedDecl` (disambiguated from combineds by the JOIN axis). */
|
|
107
|
+
function derivedsOf(mod: DomainModuleExports): DerivedDecl[] {
|
|
108
|
+
const out: DerivedDecl[] = [];
|
|
109
|
+
for (const v of Object.values(mod)) {
|
|
110
|
+
if (
|
|
111
|
+
v &&
|
|
112
|
+
typeof v === "object" &&
|
|
113
|
+
typeof (v as { id?: unknown }).id === "string" &&
|
|
114
|
+
typeof (v as { of?: unknown }).of === "string" &&
|
|
115
|
+
typeof (v as { fn?: unknown }).fn === "function" &&
|
|
116
|
+
typeof (v as { refField?: unknown }).refField !== "string" &&
|
|
117
|
+
typeof (v as { reads?: unknown }).reads !== "string"
|
|
118
|
+
) {
|
|
119
|
+
out.push(v as DerivedDecl);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Collect every exported `CombinedDecl` (the `refField`+`reads` JOIN axis marks it). */
|
|
126
|
+
function combinedsOf(mod: DomainModuleExports): CombinedDecl[] {
|
|
127
|
+
const out: CombinedDecl[] = [];
|
|
128
|
+
for (const v of Object.values(mod)) {
|
|
129
|
+
if (
|
|
130
|
+
v &&
|
|
131
|
+
typeof v === "object" &&
|
|
132
|
+
typeof (v as { id?: unknown }).id === "string" &&
|
|
133
|
+
typeof (v as { of?: unknown }).of === "string" &&
|
|
134
|
+
typeof (v as { refField?: unknown }).refField === "string" &&
|
|
135
|
+
typeof (v as { reads?: unknown }).reads === "string" &&
|
|
136
|
+
typeof (v as { fn?: unknown }).fn === "function"
|
|
137
|
+
) {
|
|
138
|
+
out.push(v as CombinedDecl);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Spread-merge a domain key's ORDERED module list (later wins on collision). */
|
|
145
|
+
function mergeModules(mods: readonly DomainModuleExports[]): DomainModuleExports {
|
|
146
|
+
let merged: DomainModuleExports = {};
|
|
147
|
+
for (const m of mods) merged = { ...merged, ...m };
|
|
148
|
+
return merged;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build a DSL `ctx` (Ports) from the host-injected `__ports` scalars. `clock()`
|
|
153
|
+
* synthesises a `WireHlc` from the scalar (the engine leg DISCARDS the produced
|
|
154
|
+
* intent's HLC — the committed HLC/id is host-injected in Rust); `id()` derives a
|
|
155
|
+
* deterministic per-dispatch value; `rng()` replays the captured draw.
|
|
156
|
+
*/
|
|
157
|
+
function portsFromHost(): Ports {
|
|
158
|
+
const hostPorts = (globalThis as { __ports?: { clock(): number; rng(): number } }).__ports;
|
|
159
|
+
const clockScalar = hostPorts ? hostPorts.clock() : 0;
|
|
160
|
+
const rngValue = hostPorts ? hostPorts.rng() : 0;
|
|
161
|
+
let idCounter = 0;
|
|
162
|
+
return {
|
|
163
|
+
clock(): WireHlc {
|
|
164
|
+
return { physical: clockScalar, logical: 0, replica: 0 };
|
|
165
|
+
},
|
|
166
|
+
id(): string {
|
|
167
|
+
idCounter += 1;
|
|
168
|
+
return `eng-${clockScalar}-${idCounter}`;
|
|
169
|
+
},
|
|
170
|
+
rng(): number {
|
|
171
|
+
return rngValue;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The compiled engine surface `registerEngine` returns (and assigns onto `globalThis`). */
|
|
177
|
+
export interface RegisteredEngine {
|
|
178
|
+
plan(job: unknown): WireEvent[] | { events: WireEvent[]; strikes: string[] };
|
|
179
|
+
planReport(job: unknown): string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build ALL registries from the one `domains` map, wire the five dispatch paths, and
|
|
184
|
+
* assign `globalThis.plan` + `globalThis.planReport` (the lump is eval'd as a classic
|
|
185
|
+
* script and `globalThis` is FROZEN before dispatch — registration must happen at
|
|
186
|
+
* top-level eval, which calling this at module top level does).
|
|
187
|
+
*/
|
|
188
|
+
export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
189
|
+
const mergedByDomain = new Map<string, DomainModuleExports>();
|
|
190
|
+
for (const [domainName, mods] of Object.entries(config.domains)) {
|
|
191
|
+
mergedByDomain.set(domainName, mergeModules(mods));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── (domain, directiveId) → {directive, agg} ──────────────────────────────────
|
|
195
|
+
const REGISTRY = new Map<string, RegistryEntry>();
|
|
196
|
+
for (const [domainName, mod] of mergedByDomain) {
|
|
197
|
+
const aggs = aggregatesOf(mod);
|
|
198
|
+
const dirs = directivesOf(mod);
|
|
199
|
+
for (const [dirId, directive] of dirs) {
|
|
200
|
+
const agg = aggs.get(directive.aggregateId);
|
|
201
|
+
if (agg === undefined) {
|
|
202
|
+
// A directive targeting an aggregate not exported by its module is an
|
|
203
|
+
// authoring bug; surface it lazily (only if that directive is invoked).
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
REGISTRY.set(`${domainName}\u0000${dirId}`, { directive, agg });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── aggregate type → derived / combined decls (fn bodies ship HERE, never the ledger) ──
|
|
211
|
+
const DERIVED_REGISTRY = new Map<string, DerivedDecl[]>();
|
|
212
|
+
const COMBINED_REGISTRY = new Map<string, CombinedDecl[]>();
|
|
213
|
+
for (const mod of mergedByDomain.values()) {
|
|
214
|
+
for (const d of derivedsOf(mod)) {
|
|
215
|
+
const list = DERIVED_REGISTRY.get(d.of) ?? [];
|
|
216
|
+
list.push(d);
|
|
217
|
+
DERIVED_REGISTRY.set(d.of, list);
|
|
218
|
+
}
|
|
219
|
+
for (const c of combinedsOf(mod)) {
|
|
220
|
+
const list = COMBINED_REGISTRY.get(c.of) ?? [];
|
|
221
|
+
list.push(c);
|
|
222
|
+
COMBINED_REGISTRY.set(c.of, list);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── relation id → cross-workspace invariant body (off the directives' declaredRelations) ──
|
|
227
|
+
const INVARIANT_REGISTRY = new Map<string, InvariantBody>();
|
|
228
|
+
for (const { directive } of REGISTRY.values()) {
|
|
229
|
+
const relations = (directive as { declaredRelations?: unknown }).declaredRelations;
|
|
230
|
+
if (!Array.isArray(relations)) continue;
|
|
231
|
+
for (const rel of relations) {
|
|
232
|
+
const r = rel as { id?: unknown; hasInvariant?: unknown; invariant?: unknown };
|
|
233
|
+
if (typeof r.id !== "string" || r.hasInvariant !== true) continue;
|
|
234
|
+
if (typeof r.invariant !== "function") {
|
|
235
|
+
// A relation declaring `hasInvariant` MUST ship an executable body — fail-closed.
|
|
236
|
+
throw new Error(
|
|
237
|
+
`engine bundle: relation "${r.id}" declares hasInvariant but ships no executable ` +
|
|
238
|
+
`invariant body — the gate would have nothing to evaluate (cross_workspace.md §2.1).`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
INVARIANT_REGISTRY.set(r.id, r.invariant as InvariantBody);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── aggregate type → aggregate-invariant body — from the SAME map (no second list) ──
|
|
246
|
+
const AGG_INVARIANT_REGISTRY = new Map<string, AggregateInvariantFn>();
|
|
247
|
+
for (const mod of mergedByDomain.values()) {
|
|
248
|
+
for (const v of Object.values(mod)) {
|
|
249
|
+
if (
|
|
250
|
+
v &&
|
|
251
|
+
typeof v === "object" &&
|
|
252
|
+
(v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true &&
|
|
253
|
+
(v as { hasInvariant?: boolean }).hasInvariant === true
|
|
254
|
+
) {
|
|
255
|
+
const h = v as AggregateHandle & { invariant?: AggregateInvariantFn };
|
|
256
|
+
if (typeof h.invariant !== "function") {
|
|
257
|
+
// A handle declaring `hasInvariant` MUST ship an executable body — fail-closed.
|
|
258
|
+
throw new Error(
|
|
259
|
+
`engine bundle: aggregate "${h.id}" declares hasInvariant but ships no executable ` +
|
|
260
|
+
`invariant body — the gate would have nothing to evaluate (#250).`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
AGG_INVARIANT_REGISTRY.set(h.id, h.invariant);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const REPORTS: Record<string, (actor: string) => EngineReport> = config.reports ?? {};
|
|
269
|
+
|
|
270
|
+
// ── the five planReport dispatch branches (wire shapes are the host oracle's contract) ──
|
|
271
|
+
|
|
272
|
+
function planInvariant(job: {
|
|
273
|
+
intent?: { invariant?: { relation?: string }; evidence?: InvariantEvidence };
|
|
274
|
+
}): string {
|
|
275
|
+
const relationId = job.intent?.invariant?.relation;
|
|
276
|
+
if (typeof relationId !== "string") {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`engine invariant: job.intent.invariant must carry {relation}; got ${JSON.stringify(job.intent)}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const body = INVARIANT_REGISTRY.get(relationId);
|
|
282
|
+
if (body === undefined) {
|
|
283
|
+
throw new Error(`engine invariant: no invariant registered for relation "${relationId}"`);
|
|
284
|
+
}
|
|
285
|
+
const evidence = job.intent?.evidence;
|
|
286
|
+
if (evidence === undefined || typeof evidence !== "object") {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`engine invariant: job.intent must carry {evidence}; got ${JSON.stringify(job.intent)}`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const verdict: InvariantVerdict = body(evidence);
|
|
292
|
+
return JSON.stringify(verdict);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function planAggInvariant(job: {
|
|
296
|
+
intent?: { aggregateInvariant?: { of?: string } };
|
|
297
|
+
priorState?: Record<string, unknown>;
|
|
298
|
+
}): string {
|
|
299
|
+
// WIRE SHAPE (the host oracle's contract, EXACT): `{ aggregateInvariant: { of: "<type>" } }`.
|
|
300
|
+
const aggregateType = job.intent?.aggregateInvariant?.of;
|
|
301
|
+
if (typeof aggregateType !== "string") {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`engine aggregateInvariant: job.intent.aggregateInvariant must carry {of}; got ${JSON.stringify(job.intent)}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const body = AGG_INVARIANT_REGISTRY.get(aggregateType);
|
|
307
|
+
// VACUOUS HOLDS: a type with NO declared invariant trivially holds — `{accept:true}`,
|
|
308
|
+
// NOT a throw (the oracle fails CLOSED on a throw, wrongly rejecting every create).
|
|
309
|
+
if (body === undefined) {
|
|
310
|
+
return JSON.stringify({ accept: true } satisfies AggregateInvariantVerdict);
|
|
311
|
+
}
|
|
312
|
+
const snapshot = (job.priorState ?? {}) as Record<string, unknown>;
|
|
313
|
+
const verdict: AggregateInvariantVerdict = body(snapshot);
|
|
314
|
+
return JSON.stringify(verdict);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function planDerive(job: {
|
|
318
|
+
intent?: { derive?: { of?: string } };
|
|
319
|
+
priorState?: Record<string, unknown>;
|
|
320
|
+
}): string {
|
|
321
|
+
const ofType = job.intent?.derive?.of;
|
|
322
|
+
if (typeof ofType !== "string") {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`engine derive: job.intent.derive must carry {of}; got ${JSON.stringify(job.intent)}`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const fields = DERIVED_REGISTRY.get(ofType) ?? [];
|
|
328
|
+
const prior = (job.priorState ?? {}) as Record<string, unknown>;
|
|
329
|
+
const out: Record<string, unknown> = {};
|
|
330
|
+
for (const d of fields) {
|
|
331
|
+
const value = d.fn(prior);
|
|
332
|
+
out[d.id] = value === undefined ? null : value;
|
|
333
|
+
}
|
|
334
|
+
return JSON.stringify(out);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function planCombine(job: {
|
|
338
|
+
intent?: { combine?: { of?: string } };
|
|
339
|
+
priorState?: Record<string, unknown>;
|
|
340
|
+
queryRows?: Record<string, Record<string, unknown> | null> | (Record<string, unknown> | null)[];
|
|
341
|
+
}): string {
|
|
342
|
+
const ofType = job.intent?.combine?.of;
|
|
343
|
+
if (typeof ofType !== "string") {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`engine combine: job.intent.combine must carry {of}; got ${JSON.stringify(job.intent)}`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const fields = COMBINED_REGISTRY.get(ofType) ?? [];
|
|
349
|
+
const owner = (job.priorState ?? {}) as Record<string, unknown>;
|
|
350
|
+
// Preferred: related rows keyed by combined field id (iteration-order independent);
|
|
351
|
+
// the array path remains for older callers (declaration order).
|
|
352
|
+
const relatedById =
|
|
353
|
+
job.queryRows !== undefined && !Array.isArray(job.queryRows) ? job.queryRows : undefined;
|
|
354
|
+
const relatedArray = Array.isArray(job.queryRows) ? job.queryRows : [];
|
|
355
|
+
const out: Record<string, unknown> = {};
|
|
356
|
+
for (let i = 0; i < fields.length; i += 1) {
|
|
357
|
+
const c = fields[i]!;
|
|
358
|
+
const row = relatedById !== undefined ? relatedById[c.id] : relatedArray[i];
|
|
359
|
+
const value = c.fn(owner, row == null ? undefined : (row as Record<string, unknown>));
|
|
360
|
+
out[c.id] = value === undefined ? null : value;
|
|
361
|
+
}
|
|
362
|
+
return JSON.stringify(out);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function planReport(job: {
|
|
366
|
+
intent?: {
|
|
367
|
+
report?: string;
|
|
368
|
+
actor?: string;
|
|
369
|
+
derive?: { of?: string };
|
|
370
|
+
combine?: { of?: string };
|
|
371
|
+
invariant?: { relation?: string };
|
|
372
|
+
aggregateInvariant?: { of?: string };
|
|
373
|
+
evidence?: InvariantEvidence;
|
|
374
|
+
};
|
|
375
|
+
queryRows?: unknown[];
|
|
376
|
+
priorState?: Record<string, unknown>;
|
|
377
|
+
}): string {
|
|
378
|
+
// Branch order mirrors emit_engine.ts: aggregateInvariant → invariant → derive →
|
|
379
|
+
// combine → report (the gate's most recent dispatch key wins over older fan-outs).
|
|
380
|
+
if (job.intent?.aggregateInvariant !== undefined) {
|
|
381
|
+
return planAggInvariant(
|
|
382
|
+
job as { intent?: { aggregateInvariant?: { of?: string } }; priorState?: Record<string, unknown> },
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (job.intent?.invariant !== undefined) {
|
|
386
|
+
return planInvariant(
|
|
387
|
+
job as { intent?: { invariant?: { relation?: string }; evidence?: InvariantEvidence } },
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if (job.intent?.derive !== undefined) {
|
|
391
|
+
return planDerive(
|
|
392
|
+
job as { intent?: { derive?: { of?: string } }; priorState?: Record<string, unknown> },
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (job.intent?.combine !== undefined) {
|
|
396
|
+
return planCombine(
|
|
397
|
+
job as unknown as {
|
|
398
|
+
intent?: { combine?: { of?: string } };
|
|
399
|
+
priorState?: Record<string, unknown>;
|
|
400
|
+
queryRows?:
|
|
401
|
+
| Record<string, Record<string, unknown> | null>
|
|
402
|
+
| (Record<string, unknown> | null)[];
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const reportId = job.intent?.report;
|
|
407
|
+
const actor = job.intent?.actor;
|
|
408
|
+
if (typeof reportId !== "string" || typeof actor !== "string") {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`engine report: job.intent must carry {report, actor}; got ${JSON.stringify(job.intent)}`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const factory = REPORTS[reportId];
|
|
414
|
+
if (factory === undefined) {
|
|
415
|
+
throw new Error(`engine report: no report registered for "${reportId}"`);
|
|
416
|
+
}
|
|
417
|
+
const rows = (Array.isArray(job.queryRows) ? job.queryRows : []) as QueryRow[];
|
|
418
|
+
return factory(actor).render(rows);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function plan(job: {
|
|
422
|
+
intent?: { domain?: string; directiveId?: string; payload?: unknown };
|
|
423
|
+
}): WireEvent[] | { events: WireEvent[]; strikes: string[] } {
|
|
424
|
+
const intent = job.intent ?? {};
|
|
425
|
+
const domain = intent.domain;
|
|
426
|
+
const directiveId = intent.directiveId;
|
|
427
|
+
if (typeof domain !== "string" || typeof directiveId !== "string") {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`engine plan: job.intent must carry {domain, directiveId}; got ${JSON.stringify(intent)}`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const entry = REGISTRY.get(`${domain}\u0000${directiveId}`);
|
|
433
|
+
if (entry === undefined) {
|
|
434
|
+
throw new Error(`engine plan: no directive registered for (${domain}, ${directiveId})`);
|
|
435
|
+
}
|
|
436
|
+
const ctx = portsFromHost();
|
|
437
|
+
const wire = executeDirectiveToIntent(entry.directive, entry.agg, intent.payload as never, ctx);
|
|
438
|
+
// Emit {events, strikes} ONLY when a strike is present, else the bare event array —
|
|
439
|
+
// so every strikes-free directive canonicalizes byte-identically to the pre-strikeout contract.
|
|
440
|
+
return wire.strikes.length ? { events: wire.events, strikes: wire.strikes } : wire.events;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// The engine evals the bundle as a classic script and FREEZES globalThis before
|
|
444
|
+
// dispatch — assign both entries NOW, at top-level eval.
|
|
445
|
+
(globalThis as { plan?: unknown }).plan = plan;
|
|
446
|
+
(globalThis as { planReport?: unknown }).planReport = planReport;
|
|
447
|
+
|
|
448
|
+
return { plan, planReport } as RegisteredEngine;
|
|
449
|
+
}
|
package/src/exists.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
* `exists(id)` builder — a BOOLEAN PREDICATED MEMBERSHIP test over the `counts` table.
|
|
10
|
+
*
|
|
11
|
+
* `exists` is NOT a new maintained table — it is a TYPED READ over the SLICE-1 `counts`
|
|
12
|
+
* table (spec §3 line 47; §6 line 71). An `exists` declaration IS a count declaration
|
|
13
|
+
* in the IR: it serializes into the `counts` array in the manifest, and the read engine
|
|
14
|
+
* maintains it in the `counts` table as an ordinary count. The ONLY difference is in the
|
|
15
|
+
* GENERATED ACCESSOR: it returns `bool` = `count(id, gk) > 0`, not `int`.
|
|
16
|
+
*
|
|
17
|
+
* ZERO new Rust spec type, ZERO new table, ZERO new maintenance. This is the STRONGEST
|
|
18
|
+
* REUSE possible: `exists` cannot diverge from `count` (LAW 4 — one engine; LAW 3 —
|
|
19
|
+
* harden, not loosen: strengthens the existing count, adds nothing parallel).
|
|
20
|
+
*
|
|
21
|
+
* CORRECTNESS: `exists` resolved over `counts` (not `sums`) is correct precisely because
|
|
22
|
+
* `counts` increments membership ±1 and PRUNES ONLY AT n=0 (`lib.rs:1034`, `1141`) —
|
|
23
|
+
* `count > 0` is an exact "at least one member" test. `sums` cannot back `exists` (a
|
|
24
|
+
* genuine zero-sum group is indistinguishable from absence — `manifest.rs:274-276`).
|
|
25
|
+
*
|
|
26
|
+
* ORDER-SENSITIVE GUARDRAIL: exists exposes NO `.first`/`.take`/`.orderBy`. Membership
|
|
27
|
+
* is order-INDEPENDENT (whether any member matches is a set property). Assert the
|
|
28
|
+
* absence; do NOT add dead methods (LAW 3).
|
|
29
|
+
*
|
|
30
|
+
* EMPTY-GROUP SEMANTICS: `exists` returns `false` when the group is absent from the
|
|
31
|
+
* `counts` table (sparse pruning: count=0 rows are deleted, `lib.rs:1141`), and `true`
|
|
32
|
+
* when count ≥ 1. There is NO ambiguity with a zero-sum group (this uses `counts`, not
|
|
33
|
+
* `sums`). This is the CORRECT, HARD contract (LAW 3).
|
|
34
|
+
*/
|
|
35
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
36
|
+
import type { Field } from "./fields.js";
|
|
37
|
+
import { finishCount, type CountDecl, type AnyCount } from "./count.js";
|
|
38
|
+
import { type Predicate, type CanonicalPred, predBuilder, canonicalizePred } from "./predicate.js";
|
|
39
|
+
|
|
40
|
+
// ─── DSL shape ───────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* An `ExistsDecl` is a `CountDecl` carrying a marker that tells the codegen to emit a
|
|
44
|
+
* `bool` accessor (via `count > 0`) rather than an `int` accessor. The Rust read engine
|
|
45
|
+
* sees it as an ordinary `CountDecl` — the marker lives ONLY in the DSL/codegen layer.
|
|
46
|
+
*/
|
|
47
|
+
export interface ExistsDecl extends CountDecl {
|
|
48
|
+
/** Marker: tells the codegen to emit a `bool` accessor, NOT an `int` one. */
|
|
49
|
+
readonly _existsMarker: true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The `ExistsBuilder` builder: carries `id` + `of` (grand-total usable) plus `.where(...)`
|
|
54
|
+
* and `.by(...)` methods. Mirrors `Count<F>` but the finished form is an `ExistsDecl`
|
|
55
|
+
* (which is a `CountDecl`). F is the `of`-aggregate's field map.
|
|
56
|
+
*/
|
|
57
|
+
export interface ExistsBuilder<F extends Record<string, Field> = Record<string, Field>> {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly of: string;
|
|
60
|
+
/** Attach a PREDICATE: only aggregates satisfying it count toward existence. */
|
|
61
|
+
where(fn: (p: ReturnType<typeof predBuilder<F>>) => Predicate<F>): ExistsBuilder<F>;
|
|
62
|
+
/** Partition by a GROUP-BY field: exists per distinct group-key value. */
|
|
63
|
+
by(field: string): ExistsDecl;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The INITIAL, un-typed exists — its ONLY method is `.of(...)`. A `exists(id)` without
|
|
68
|
+
* `.of(...)` cannot be used as a declaration: the aggregate type is not optional. Same
|
|
69
|
+
* un-constructible-without-of pattern as `count(id)` (`count.ts:104-120`).
|
|
70
|
+
*/
|
|
71
|
+
export interface TypelessExists {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): ExistsBuilder<F>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Either form a domain may declare in `DomainModule.exists_`. A bare `ExistsBuilder`
|
|
78
|
+
* (grand-total) or a grouped `ExistsDecl`. Both satisfy `AnyExists`.
|
|
79
|
+
*/
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
export type AnyExists = ExistsDecl | ExistsBuilder<any>;
|
|
82
|
+
|
|
83
|
+
/** Narrow: an `ExistsBuilder` exposes a `by` METHOD; an `ExistsDecl` does not. */
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
function isExistsBuilder(e: AnyExists): e is ExistsBuilder<any> {
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
return typeof (e as ExistsBuilder<any>).by === "function";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Normalize an `AnyExists` to a finished `ExistsDecl`. The grand-total builder
|
|
92
|
+
* becomes `{id, of, _existsMarker: true}` (no `by`); a grouped `.by(field)` result is
|
|
93
|
+
* already an `ExistsDecl` and passes through. Transfers the `_where` canonical predicate
|
|
94
|
+
* when present. The returned `ExistsDecl` is ALSO a `CountDecl` and is appended into
|
|
95
|
+
* the `counts` array in the manifest — the Rust engine sees no difference.
|
|
96
|
+
*/
|
|
97
|
+
export function finishExists(e: AnyExists): ExistsDecl {
|
|
98
|
+
if (isExistsBuilder(e)) {
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
const b = e as any;
|
|
101
|
+
const w: CanonicalPred | undefined = b._where;
|
|
102
|
+
return {
|
|
103
|
+
id: b.id,
|
|
104
|
+
of: b.of,
|
|
105
|
+
...(w !== undefined ? { where: w } : {}),
|
|
106
|
+
_existsMarker: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return e;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Normalize an `AnyExists` to a `CountDecl` for manifest serialization. The `_existsMarker`
|
|
114
|
+
* is EXPLICITLY STRIPPED — the Rust read engine sees an ordinary count. Use `finishExists`
|
|
115
|
+
* for codegen (where the marker drives the bool vs int accessor shape); use this for
|
|
116
|
+
* manifest emit. The stripping is a destructuring reconstruction, NOT a cast, to ensure
|
|
117
|
+
* no extra keys bleed into the IR.
|
|
118
|
+
*/
|
|
119
|
+
export function existsAsCount(e: AnyExists): CountDecl {
|
|
120
|
+
const decl = finishExists(e);
|
|
121
|
+
// Explicitly reconstruct as a clean CountDecl (no _existsMarker, no extra keys).
|
|
122
|
+
return {
|
|
123
|
+
id: decl.id,
|
|
124
|
+
of: decl.of,
|
|
125
|
+
...(decl.where !== undefined ? { where: decl.where } : {}),
|
|
126
|
+
...(decl.by !== undefined ? { by: decl.by } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Internal factory so `.where(...)` can return a new ExistsBuilder without duplicating impl. */
|
|
131
|
+
function makeExistsBuilder<F extends Record<string, Field>>(
|
|
132
|
+
id: string,
|
|
133
|
+
ofType: string,
|
|
134
|
+
where: CanonicalPred | undefined,
|
|
135
|
+
): ExistsBuilder<F> {
|
|
136
|
+
const b = {
|
|
137
|
+
id,
|
|
138
|
+
of: ofType,
|
|
139
|
+
...(where !== undefined ? { _where: where } : {}),
|
|
140
|
+
where(fn: (p: ReturnType<typeof predBuilder<F>>) => Predicate<F>): ExistsBuilder<F> {
|
|
141
|
+
const pred = fn(predBuilder<F>());
|
|
142
|
+
const canonical = canonicalizePred(pred as Predicate<Record<string, Field>>);
|
|
143
|
+
return makeExistsBuilder<F>(id, ofType, canonical);
|
|
144
|
+
},
|
|
145
|
+
by(field: string): ExistsDecl {
|
|
146
|
+
return {
|
|
147
|
+
id,
|
|
148
|
+
of: ofType,
|
|
149
|
+
...(where !== undefined ? { where } : {}),
|
|
150
|
+
by: field,
|
|
151
|
+
_existsMarker: true,
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
} as unknown as ExistsBuilder<F>;
|
|
155
|
+
return b;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Begin an exists declaration. `id` is the exists' canonical name (e.g.
|
|
160
|
+
* `"hasPublished"`). Returns a `TypelessExists`: until `.of(aggregate)` is called, the
|
|
161
|
+
* aggregate type is unknown and no usable declaration exists.
|
|
162
|
+
*/
|
|
163
|
+
export function exists(id: string): TypelessExists {
|
|
164
|
+
return {
|
|
165
|
+
id,
|
|
166
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): ExistsBuilder<F> {
|
|
167
|
+
return makeExistsBuilder<F>(id, aggregate.id, undefined);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|