@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
package/src/report.ts
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
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
|
+
* `report(id).version(n).select(...).render(...)` — the DECLARATIVE report DSL.
|
|
10
|
+
*
|
|
11
|
+
* The product principle: a report author writes ONLY a declarative query + a
|
|
12
|
+
* render and stays CARELESS about determinism. There is ZERO determinism code in
|
|
13
|
+
* an authored report:
|
|
14
|
+
*
|
|
15
|
+
* * `.select(a => a.touchedBy(actor, { within: days(30) }))` builds a declarative
|
|
16
|
+
* `QueryAst` (NO scan, NO projection access in the author's code). The HOST
|
|
17
|
+
* resolves it against the provenance index @ a pinned source commit and feeds
|
|
18
|
+
* the answer rows back — the author never touches the index.
|
|
19
|
+
* * `.render(rows => csv(rows))` is a PURE function from the fed rows to bytes.
|
|
20
|
+
* It runs INSIDE the sealed engine over the host-fed `queryRows` (the
|
|
21
|
+
* query-port seal), so its output is deterministic by construction.
|
|
22
|
+
*
|
|
23
|
+
* The author writes no commit-walk, no ORDER BY, no digest, no replay. All of that
|
|
24
|
+
* hard engineering is front-loaded into Nomos (the provenance index + the captured
|
|
25
|
+
* query port + the in-engine render runner).
|
|
26
|
+
*
|
|
27
|
+
* ── Scope (smallest-first proof) ──
|
|
28
|
+
* Only the `touchedBy(actor, within)` predicate is modelled. Generalising adds
|
|
29
|
+
* predicate builders (geo/derived/taxonomy) to `SelectBuilder` — each one more
|
|
30
|
+
* `QueryAst` variant the host resolver answers; the author DSL is unchanged.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** A time window in milliseconds — `days(30)` etc. (author-friendly, no clock). */
|
|
34
|
+
export function days(n: number): number {
|
|
35
|
+
return n * 24 * 60 * 60 * 1000;
|
|
36
|
+
}
|
|
37
|
+
export function hours(n: number): number {
|
|
38
|
+
return n * 60 * 60 * 1000;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
42
|
+
import type { Field, FieldValue } from "./fields.js";
|
|
43
|
+
import {
|
|
44
|
+
buildRelationRegistry,
|
|
45
|
+
certifyTraversal,
|
|
46
|
+
type CertifyContext,
|
|
47
|
+
type RefEdge,
|
|
48
|
+
} from "./query/relations.js";
|
|
49
|
+
import {
|
|
50
|
+
type EntityGraphQuery,
|
|
51
|
+
type Filter,
|
|
52
|
+
type TouchedBy as CompiledTouchedBy,
|
|
53
|
+
} from "./query/compile.js";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The declarative query AST a `.select` produces. Mirrors the Rust
|
|
57
|
+
* `engine_host::query_port::QueryAst` (tagged union). The HOST resolves this; the
|
|
58
|
+
* author only constructs it via the fluent `SelectBuilder`.
|
|
59
|
+
*
|
|
60
|
+
* Two variants now:
|
|
61
|
+
* * `touchedBy` — the original provenance predicate (escape-hatch / disclosure).
|
|
62
|
+
* * `entityGraph` — the PRIMARY path: a typed entity-graph query Nomos compiles
|
|
63
|
+
* into sealed SQL. Carries the certified relation chain + filters + optional
|
|
64
|
+
* touchedBy + agg. Mirrors `engine_host::query_port::QueryAst::EntityGraph`.
|
|
65
|
+
*/
|
|
66
|
+
export type QueryAst =
|
|
67
|
+
| {
|
|
68
|
+
kind: "touchedBy";
|
|
69
|
+
actor: string;
|
|
70
|
+
withinMillis: number;
|
|
71
|
+
}
|
|
72
|
+
| ({
|
|
73
|
+
kind: "entityGraph";
|
|
74
|
+
} & EntityGraphQuery);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The select surface handed to the author's `select` lambda. Each method is a
|
|
78
|
+
* declarative predicate that RETURNS a `QueryAst` — it does NOT execute anything.
|
|
79
|
+
*/
|
|
80
|
+
export interface SelectBuilder {
|
|
81
|
+
/**
|
|
82
|
+
* `a.touchedBy(actor, { within })` — every aggregate `actor` touched within the
|
|
83
|
+
* window ending at the report's source commit. Answered from the provenance
|
|
84
|
+
* index (the commit-walk author signature + intent HLC), NOT an in-VM scan.
|
|
85
|
+
*/
|
|
86
|
+
touchedBy(actor: string, opts: { within: number }): QueryAst;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const SELECT: SelectBuilder = {
|
|
90
|
+
touchedBy(actor, opts) {
|
|
91
|
+
return { kind: "touchedBy", actor, withinMillis: opts.within };
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** A row the host feeds the render — the report-author-visible provenance touch. */
|
|
96
|
+
export interface QueryRow {
|
|
97
|
+
actor: string;
|
|
98
|
+
aggregateId: string;
|
|
99
|
+
physical: number;
|
|
100
|
+
logical: number;
|
|
101
|
+
replica: number;
|
|
102
|
+
commitOid: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A fully-declared report: an id, a version, a query, and a render. */
|
|
106
|
+
export interface Report {
|
|
107
|
+
readonly id: string;
|
|
108
|
+
readonly version: number;
|
|
109
|
+
/** The declarative query AST (resolved by the host, never the VM). */
|
|
110
|
+
readonly query: QueryAst;
|
|
111
|
+
/** The pure render: fed rows → artifact bytes (runs in the sealed engine). */
|
|
112
|
+
render(rows: QueryRow[]): string;
|
|
113
|
+
readonly __isReport: true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Fluent builder: `report(id).version(n).select(...).render(...)`. */
|
|
117
|
+
class ReportBuilder {
|
|
118
|
+
constructor(
|
|
119
|
+
private readonly id: string,
|
|
120
|
+
private readonly _version: number = 1,
|
|
121
|
+
) {}
|
|
122
|
+
|
|
123
|
+
version(n: number): ReportBuilder {
|
|
124
|
+
return new ReportBuilder(this.id, n);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Declare the query. The lambda receives the declarative `SelectBuilder`. */
|
|
128
|
+
select(fn: (a: SelectBuilder) => QueryAst): ReportWithQuery {
|
|
129
|
+
return new ReportWithQuery(this.id, this._version, fn(SELECT));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class ReportWithQuery {
|
|
134
|
+
constructor(
|
|
135
|
+
private readonly id: string,
|
|
136
|
+
private readonly version: number,
|
|
137
|
+
private readonly query: QueryAst,
|
|
138
|
+
) {}
|
|
139
|
+
|
|
140
|
+
/** Declare the render. Returns the finished, immutable `Report`. */
|
|
141
|
+
render(fn: (rows: QueryRow[]) => string): Report {
|
|
142
|
+
return {
|
|
143
|
+
id: this.id,
|
|
144
|
+
version: this.version,
|
|
145
|
+
query: this.query,
|
|
146
|
+
render: fn,
|
|
147
|
+
__isReport: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Declare a report. The id literal IS the wire id (minify-safe string data). */
|
|
153
|
+
export function report(id: string): ReportBuilder {
|
|
154
|
+
return new ReportBuilder(id);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
158
|
+
// The SEALED-SQL report surface — the ESCAPE HATCH (#130).
|
|
159
|
+
//
|
|
160
|
+
// The declarative `report(...).select(...)` paved road above covers
|
|
161
|
+
// existence/traversal/touchedBy/count. A report whose query needs aggregation
|
|
162
|
+
// (`SUM`/`GROUP BY`), per-row arithmetic, or per-group rollups exceeds those
|
|
163
|
+
// primitives — so its author writes RAW SQL instead, run through the SAME sealed
|
|
164
|
+
// connection (`readmodel::SealedRead`: read-only + immutable + the authorizer
|
|
165
|
+
// deny-list + a forced total ORDER BY). The safety is ENVIRONMENTAL, not
|
|
166
|
+
// linguistic: the seal refuses every nondeterministic/mutating op regardless of
|
|
167
|
+
// the SQL text. The author is still CARELESS about determinism — the seal +
|
|
168
|
+
// capture + re-derive are the engine's job; the author writes only SQL + a render.
|
|
169
|
+
//
|
|
170
|
+
// `asOf`: a report that depends on "now" (e.g. straight-line depreciation =
|
|
171
|
+
// `cost * (1 − age/usefulLife)` where `age = asOf − placedInServiceAt`) MUST NOT
|
|
172
|
+
// read the wall clock — the seal denies `datetime('now')` precisely so a report
|
|
173
|
+
// cannot be nondeterministic. Instead the host resolves `asOf` from the PINNED
|
|
174
|
+
// `source_commit` (the ledger tip's HLC physical time) and BAKES it as a SQL
|
|
175
|
+
// literal — exactly how the entity-graph compiler bakes the `touchedBy` window
|
|
176
|
+
// floor (`pv.physical >= {floor}`). The author declares they need `asOf` via a
|
|
177
|
+
// `{asOf}` placeholder in the SQL; the host substitutes the pinned epoch-ms. The
|
|
178
|
+
// report is then a pure function of `(sqlText, source_commit)` → re-derivable.
|
|
179
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/** A row the host feeds a sealed-SQL report's render: column name → cell text.
|
|
182
|
+
* The cells are the canonical text rendering of the sealed result (the same
|
|
183
|
+
* preimage the `result_digest` is taken over), so the render is deterministic. */
|
|
184
|
+
export type SqlRow = Record<string, string>;
|
|
185
|
+
|
|
186
|
+
/** A fully-declared sealed-SQL report: id, version, raw SQL (with an optional
|
|
187
|
+
* `{asOf}` placeholder the host resolves from the source commit), and a render. */
|
|
188
|
+
export interface SqlReport {
|
|
189
|
+
readonly id: string;
|
|
190
|
+
readonly version: number;
|
|
191
|
+
/** The raw SQL. May contain a single `{asOf}` token → the host bakes the pinned
|
|
192
|
+
* epoch-ms (resolved from `source_commit`, never the wall clock). */
|
|
193
|
+
readonly sql: string;
|
|
194
|
+
/** Whether the SQL depends on `{asOf}` (declared, so the host knows to resolve it). */
|
|
195
|
+
readonly needsAsOf: boolean;
|
|
196
|
+
/** The pure render: host-fed sealed rows → artifact bytes (runs in the engine). */
|
|
197
|
+
render(rows: SqlRow[]): string;
|
|
198
|
+
readonly __isSqlReport: true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class SqlReportBuilder {
|
|
202
|
+
constructor(
|
|
203
|
+
private readonly id: string,
|
|
204
|
+
private readonly _version: number = 1,
|
|
205
|
+
) {}
|
|
206
|
+
|
|
207
|
+
version(n: number): SqlReportBuilder {
|
|
208
|
+
return new SqlReportBuilder(this.id, n);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Declare the raw SQL. A `{asOf}` token is resolved host-side from the pinned
|
|
212
|
+
* source commit (deterministic), never the wall clock. */
|
|
213
|
+
sql(text: string): SqlReportWithQuery {
|
|
214
|
+
return new SqlReportWithQuery(this.id, this._version, text);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class SqlReportWithQuery {
|
|
219
|
+
constructor(
|
|
220
|
+
private readonly id: string,
|
|
221
|
+
private readonly version: number,
|
|
222
|
+
private readonly sqlText: string,
|
|
223
|
+
) {}
|
|
224
|
+
|
|
225
|
+
render(fn: (rows: SqlRow[]) => string): SqlReport {
|
|
226
|
+
return {
|
|
227
|
+
id: this.id,
|
|
228
|
+
version: this.version,
|
|
229
|
+
sql: this.sqlText,
|
|
230
|
+
needsAsOf: this.sqlText.includes("{asOf}"),
|
|
231
|
+
render: fn,
|
|
232
|
+
__isSqlReport: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Declare a sealed-SQL (escape-hatch) report. The author writes raw SQL run
|
|
238
|
+
* through the sealed connection; determinism is the engine's job. */
|
|
239
|
+
export function sqlReport(id: string): SqlReportBuilder {
|
|
240
|
+
return new SqlReportBuilder(id);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
244
|
+
// The TYPED entity-graph query surface — the PRIMARY path.
|
|
245
|
+
//
|
|
246
|
+
// from(Child, ALL)
|
|
247
|
+
// .traverse((c) => c.ref("parentId", Parent)) // .ref is ref-kind-ONLY typed
|
|
248
|
+
// .where((p) => p.field("kind").eq("primary")) // field/value keyof-checked
|
|
249
|
+
// .touchedBy(ACTOR, { within: days(30) })
|
|
250
|
+
// .count() | .select()
|
|
251
|
+
//
|
|
252
|
+
// `.ref(K, Target)`: `K` is constrained to the CURRENT aggregate's ref-kind fields
|
|
253
|
+
// only (a non-ref or unknown key is a COMPILE error); `Target` is the target handle
|
|
254
|
+
// so the chain's field typing follows the traversal. `.where(...).field(K)` is
|
|
255
|
+
// `keyof`-checked and `.eq(v)` is value-type-checked (mirrors `ops.ts:53`).
|
|
256
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
/** The keys of a field map whose field is a (same-workspace) ref — the ONLY keys
|
|
259
|
+
* `.ref` accepts. A renamed/retyped/non-ref key is not in this set → compile error. */
|
|
260
|
+
type RefKeys<F extends Record<string, Field>> = {
|
|
261
|
+
[K in keyof F]: F[K] extends Field<unknown, "ref"> ? K : never;
|
|
262
|
+
}[keyof F] &
|
|
263
|
+
string;
|
|
264
|
+
|
|
265
|
+
/** Scalar (string/enum/ref/json) field keys — the only ones `.where(...).eq` filters
|
|
266
|
+
* (the EAV needle is the JSON-wrapped scalar; non-scalar projection is denied). */
|
|
267
|
+
type ScalarKeys<F extends Record<string, Field>> = {
|
|
268
|
+
[K in keyof F]: F[K] extends Field<unknown, "string" | "enum" | "ref" | "json">
|
|
269
|
+
? K
|
|
270
|
+
: never;
|
|
271
|
+
}[keyof F] &
|
|
272
|
+
string;
|
|
273
|
+
|
|
274
|
+
/** The `where` lambda surface: pick a scalar field, then `.eq(value)`. */
|
|
275
|
+
interface WhereBuilder<F extends Record<string, Field>> {
|
|
276
|
+
field<K extends ScalarKeys<F>>(
|
|
277
|
+
key: K,
|
|
278
|
+
): { eq(value: FieldValue<F[K]>): { __field: K; __value: FieldValue<F[K]> } };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function whereBuilder<F extends Record<string, Field>>(): WhereBuilder<F> {
|
|
282
|
+
return {
|
|
283
|
+
field<K extends ScalarKeys<F>>(key: K) {
|
|
284
|
+
return {
|
|
285
|
+
eq: (value: FieldValue<F[K]>) => ({ __field: key, __value: value }),
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The `traverse` lambda surface: `.ref(K, Target)` — ref-kind-only key + target. */
|
|
292
|
+
interface TraverseBuilder<F extends Record<string, Field>> {
|
|
293
|
+
ref<K extends RefKeys<F>, TId extends string, TF extends Record<string, Field>>(
|
|
294
|
+
key: K,
|
|
295
|
+
target: AggregateHandle<TId, TF>,
|
|
296
|
+
): { __field: K; __target: AggregateHandle<TId, TF> };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function traverseBuilder<F extends Record<string, Field>>(): TraverseBuilder<F> {
|
|
300
|
+
return {
|
|
301
|
+
ref(key, target) {
|
|
302
|
+
return { __field: key, __target: target } as any;
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The typed entity-graph builder. `F` is the field map of the CURRENT (head)
|
|
309
|
+
* aggregate in the chain — `.traverse` switches it to the target's field map, so
|
|
310
|
+
* `.where` after a traverse type-checks against the TRAVERSED aggregate.
|
|
311
|
+
*/
|
|
312
|
+
export class EntityGraphBuilder<F extends Record<string, Field>> {
|
|
313
|
+
constructor(
|
|
314
|
+
private readonly ctx: CertifyContext,
|
|
315
|
+
private readonly root: string,
|
|
316
|
+
/** Resolved traversal chain (certified eagerly so a bad hop fails fast). */
|
|
317
|
+
private readonly traversals: readonly RefEdge[],
|
|
318
|
+
private readonly filters: readonly Filter[],
|
|
319
|
+
/** alias index of the current head aggregate (0 = root). */
|
|
320
|
+
private readonly headAlias: number,
|
|
321
|
+
private readonly touched?: CompiledTouchedBy,
|
|
322
|
+
) {}
|
|
323
|
+
|
|
324
|
+
/** Traverse a ref relation to the target aggregate. The head type switches to the
|
|
325
|
+
* target's field map. The hop is CERTIFIED now (cycle/cross-ws/exists/ref guards). */
|
|
326
|
+
traverse<TId extends string, TF extends Record<string, Field>>(
|
|
327
|
+
fn: (b: TraverseBuilder<F>) => { __field: RefKeys<F>; __target: AggregateHandle<TId, TF> },
|
|
328
|
+
): EntityGraphBuilder<TF> {
|
|
329
|
+
const sel = fn(traverseBuilder<F>());
|
|
330
|
+
const hops = [...this.traversals.map((t) => t.field), sel.__field];
|
|
331
|
+
// Re-certify the whole chain from the root (cycle/depth/exists guards live here).
|
|
332
|
+
const certified = certifyTraversal(this.ctx, this.root, hops);
|
|
333
|
+
return new EntityGraphBuilder<TF>(
|
|
334
|
+
this.ctx,
|
|
335
|
+
this.root,
|
|
336
|
+
certified,
|
|
337
|
+
this.filters,
|
|
338
|
+
this.headAlias + 1,
|
|
339
|
+
this.touched,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** A scalar equality filter on the CURRENT head aggregate. */
|
|
344
|
+
where(
|
|
345
|
+
fn: (b: WhereBuilder<F>) => { __field: ScalarKeys<F>; __value: unknown },
|
|
346
|
+
): EntityGraphBuilder<F> {
|
|
347
|
+
const sel = fn(whereBuilder<F>());
|
|
348
|
+
const filter: Filter = {
|
|
349
|
+
kind: "eq",
|
|
350
|
+
aliasIndex: this.headAlias,
|
|
351
|
+
field: sel.__field,
|
|
352
|
+
value: String(sel.__value),
|
|
353
|
+
};
|
|
354
|
+
return new EntityGraphBuilder<F>(
|
|
355
|
+
this.ctx,
|
|
356
|
+
this.root,
|
|
357
|
+
this.traversals,
|
|
358
|
+
[...this.filters, filter],
|
|
359
|
+
this.headAlias,
|
|
360
|
+
this.touched,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** A `touchedBy(actor, within)` provenance constraint on the ROOT identity. */
|
|
365
|
+
touchedBy(actor: string, opts: { within: number }): EntityGraphBuilder<F> {
|
|
366
|
+
return new EntityGraphBuilder<F>(
|
|
367
|
+
this.ctx,
|
|
368
|
+
this.root,
|
|
369
|
+
this.traversals,
|
|
370
|
+
this.filters,
|
|
371
|
+
this.headAlias,
|
|
372
|
+
{ actor, withinMillis: opts.within },
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Finish as a `select`-shaped query AST (root identities). */
|
|
377
|
+
build(): QueryAst & { kind: "entityGraph" } {
|
|
378
|
+
return {
|
|
379
|
+
kind: "entityGraph",
|
|
380
|
+
root: this.root,
|
|
381
|
+
traversals: this.traversals,
|
|
382
|
+
filters: this.filters,
|
|
383
|
+
...(this.touched ? { touchedBy: this.touched } : {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Finish as a COUNT aggregation. */
|
|
388
|
+
count(): QueryAst & { kind: "entityGraph" } {
|
|
389
|
+
return { ...this.build(), agg: { op: "count" } };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Begin a typed entity-graph query rooted at `root`. `handles` is the registered
|
|
394
|
+
* aggregate universe used to derive the relation graph + certify traversals. */
|
|
395
|
+
export function from<Id extends string, F extends Record<string, Field>>(
|
|
396
|
+
root: AggregateHandle<Id, F>,
|
|
397
|
+
handles: readonly AggregateHandle[],
|
|
398
|
+
): EntityGraphBuilder<F> {
|
|
399
|
+
const ctx = buildRelationRegistry(handles);
|
|
400
|
+
return new EntityGraphBuilder<F>(ctx, root.id, [], [], 0, undefined);
|
|
401
|
+
}
|
package/src/spatial.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
* `spatial(id)` builder — the GEOSPATIAL read KIND of Nomos's read-side closure
|
|
10
|
+
* (the keystone of `docs/placement_and_spatial_reads.md`).
|
|
11
|
+
*
|
|
12
|
+
* READ-CLOSURE, the spatial half. A `query` declares a NAMED, INDEXED set read keyed on
|
|
13
|
+
* scalar fields (B-tree); a `count` declares a NAMED, MAINTAINED tally; a `spatial`
|
|
14
|
+
* declares a NAMED, MAINTAINED **bounding-box index** over an aggregate's **GeoJSON
|
|
15
|
+
* geometry field**, backed by SQLite **R\*Tree**. So "every aggregate whose geometry
|
|
16
|
+
* intersects this bbox" is an R\*Tree probe (`WHERE minX<=? AND maxX>=? AND …`), NEVER an
|
|
17
|
+
* in-VM scan that decodes every aggregate's geometry. The representation (GeoJSON) is kept
|
|
18
|
+
* as the index SOURCE — the index is derived from it, deterministic + rebuildable from the
|
|
19
|
+
* fold like every other managed read (the geometry field's bounding box is a pure function
|
|
20
|
+
* of the folded state @ tip).
|
|
21
|
+
*
|
|
22
|
+
* This module adds ONLY the DECLARATION + its TYPE-STATE; the read engine
|
|
23
|
+
* (`nomos_readmodel`) maintains the R\*Tree side table from the declaration shipped in the
|
|
24
|
+
* runtime manifest. It mirrors `query.ts`/`count.ts` at every turn: additive,
|
|
25
|
+
* omit-when-empty, identity-bearing (a spatial a domain declares is carried into the
|
|
26
|
+
* canonical manifest + becomes part of the domain IDENTITY; a domain that declares NONE is
|
|
27
|
+
* byte-identical to before this existed), and TYPED — `.of(...)` takes an aggregate HANDLE
|
|
28
|
+
* (never a string id), so a typo'd aggregate type is a COMPILE error.
|
|
29
|
+
*
|
|
30
|
+
* The TYPE-STATE: `spatial(id)` yields a {@link TypelessSpatial} whose ONLY method is
|
|
31
|
+
* `.of(...)`; the {@link OfSpatial} builder (carrying `.on(...)`) is produced solely by
|
|
32
|
+
* `.of(...)`; and the finished {@link SpatialDecl} is produced solely by `.on(...)`. So a
|
|
33
|
+
* spatial with no `of`-type or no geometry field cannot be CONSTRUCTED — "every spatial
|
|
34
|
+
* index names the type AND the geometry field it indexes" is a type-level property, before
|
|
35
|
+
* any runtime check (the same discipline as `query`'s mandatory `.key(...)`).
|
|
36
|
+
*/
|
|
37
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
38
|
+
import type { Field } from "./fields.js";
|
|
39
|
+
|
|
40
|
+
type StringKeyOf<T> = Extract<keyof T, string>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A FINISHED spatial-index declaration (the read-engine's input shape, mirroring {@link
|
|
44
|
+
* QueryDecl}/{@link CountDecl}): an id, the aggregate TYPE it indexes (`of`), and the name
|
|
45
|
+
* of the aggregate's GeoJSON geometry field (`on`) whose bounding box is indexed.
|
|
46
|
+
*/
|
|
47
|
+
export interface SpatialDecl {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
/** The aggregate TYPE id the spatial index covers, e.g. `TrackableAsset`. */
|
|
50
|
+
readonly of: string;
|
|
51
|
+
/**
|
|
52
|
+
* The aggregate's GeoJSON GEOMETRY field name — the SOURCE of the bounding box the
|
|
53
|
+
* R\*Tree indexes (e.g. `geoPosition`). The engine reads this field per aggregate,
|
|
54
|
+
* computes its `[minLng,minLat,maxLng,maxLat]` bbox, and upserts the R\*Tree row.
|
|
55
|
+
*/
|
|
56
|
+
readonly on: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The {@link OfSpatial} BUILDER: it has named its `of`-type, so `.on(field)` (the geometry-
|
|
61
|
+
* field step) is available. It carries `id` + `of`; `.on(...)` fixes the geometry field and
|
|
62
|
+
* yields the finished {@link SpatialDecl}. The `F` type parameter carries the `of`-
|
|
63
|
+
* aggregate's field map so `.on("geoPosition")` is `keyof`-checked against the aggregate's
|
|
64
|
+
* declared fields — a typo'd geometry field is a COMPILE error.
|
|
65
|
+
*/
|
|
66
|
+
export interface OfSpatial<F extends Record<string, Field> = Record<string, Field>> {
|
|
67
|
+
readonly id: string;
|
|
68
|
+
/** The aggregate TYPE id the spatial index covers. */
|
|
69
|
+
readonly of: string;
|
|
70
|
+
/**
|
|
71
|
+
* Fix the GeoJSON GEOMETRY field this index is built over, producing the finished
|
|
72
|
+
* declaration. `field` is `keyof`-checked against the `of`-aggregate's fields — a
|
|
73
|
+
* typo'd field is a compile error (the same convention as `count`'s `.by(...)` is
|
|
74
|
+
* a field of the type / `query`'s key fields).
|
|
75
|
+
*/
|
|
76
|
+
on(field: StringKeyOf<F>): SpatialDecl;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The INITIAL, un-typed spatial — its ONLY method is `.of(...)`. It deliberately has NO
|
|
81
|
+
* `.on`, and is not an {@link OfSpatial}/{@link SpatialDecl}, so `spatial("s").on(...)`
|
|
82
|
+
* (skipping the `of`-type) is a COMPILE error and a type-less spatial cannot be
|
|
83
|
+
* constructed. THIS is the type-level "every spatial names the type it indexes" property.
|
|
84
|
+
*/
|
|
85
|
+
export interface TypelessSpatial {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
/**
|
|
88
|
+
* Declare the aggregate TYPE this spatial index covers. Takes a typed HANDLE (never the
|
|
89
|
+
* string id) — a typo'd handle is a compile error, the same convention as `query`'s
|
|
90
|
+
* `.returns(...)` / `count`'s `.of(...)`. Returns the {@link OfSpatial} builder (the
|
|
91
|
+
* only shape exposing `.on`).
|
|
92
|
+
*/
|
|
93
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): OfSpatial<F>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Begin a spatial-index declaration. Returns a {@link TypelessSpatial}: until `.of(...)`
|
|
98
|
+
* is called, neither `.on` nor a finished `SpatialDecl` exists — the indexed type is NOT
|
|
99
|
+
* optional, it is a prerequisite; and until `.on(...)` is called the geometry field is a
|
|
100
|
+
* prerequisite too. So an under-specified spatial index is INEXPRESSIBLE at the type level.
|
|
101
|
+
*/
|
|
102
|
+
export function spatial<const Id extends string>(id: Id): TypelessSpatial {
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): OfSpatial<F> {
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
of: aggregate.id,
|
|
109
|
+
on(field: StringKeyOf<F>): SpatialDecl {
|
|
110
|
+
return { id, of: aggregate.id, on: field };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|