@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/ops.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
* Op helpers — the declarative ops a directive's `plan` returns. They map 1:1 to
|
|
10
|
+
* the kernel's `Op` (Set / AddToSet / SetEntry).
|
|
11
|
+
*
|
|
12
|
+
* Type-safety (the whole point):
|
|
13
|
+
* - the field name is checked via `keyof` against the aggregate's fields;
|
|
14
|
+
* - the value is type-checked against THAT field's declared type.
|
|
15
|
+
* So `set(thing, 'pos', 10)` compiles, but `set(thing, 'poss', …)` and
|
|
16
|
+
* `set(thing, 'pos', 'hi')` are compile errors.
|
|
17
|
+
*/
|
|
18
|
+
import type { AggregateHandle, BoundAggregate } from "./aggregate.js";
|
|
19
|
+
import type { Field, FieldValue } from "./fields.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A FIELD op, retaining the source aggregate id + field for deterministic wire encoding.
|
|
23
|
+
*
|
|
24
|
+
* `aggregateId` is the address the op targets: for an unbound handle it is the
|
|
25
|
+
* aggregate TYPE (legacy one-per-workspace shape); for a bound ref
|
|
26
|
+
* (`instance(h, id)`) it is the concrete INSTANCE id, and `aggregateType` carries
|
|
27
|
+
* the TYPE so the directive runtime can stamp the `__type` provenance `view()` reads (#105).
|
|
28
|
+
*/
|
|
29
|
+
export interface FieldOp {
|
|
30
|
+
readonly aggregateId: string;
|
|
31
|
+
/** The aggregate TYPE, present only when the op targets a bound instance ref. */
|
|
32
|
+
readonly aggregateType?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional explicit referential marker for this op's aggregate bucket. Normal
|
|
35
|
+
* fanout defaults siblings to `Mutate`; framework lifecycle directives can mark
|
|
36
|
+
* a sibling bucket as `ensures`/`creates` without hand-authoring wire events.
|
|
37
|
+
*/
|
|
38
|
+
readonly marker?: PlannedEventMarker;
|
|
39
|
+
readonly field: string;
|
|
40
|
+
readonly kind: "set" | "addToSet" | "setEntry";
|
|
41
|
+
readonly value?: unknown;
|
|
42
|
+
readonly items?: readonly string[];
|
|
43
|
+
readonly entryKey?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type PlannedEventMarker = "creates" | "mutates" | "ensures" | "archives";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A STRIKE op — retract (logically remove) the intent `target`. The kernel folds a
|
|
50
|
+
* strike by PARITY (`struck_ids`): an odd number of active strikes drops the target;
|
|
51
|
+
* striking the strike (even) re-applies it. So `strike` is its own undo/redo — toggle.
|
|
52
|
+
*
|
|
53
|
+
* It is a first-class, MAXIMALLY COMPOSABLE op: a directive's `plan` returns it in the
|
|
54
|
+
* SAME `PlannedOp[]` as field ops, so one plan can `strike(X)` AND author replacement
|
|
55
|
+
* ops in a single atomic intent (that is exactly `replace`). `executeDirectiveToIntent`
|
|
56
|
+
* routes strikes onto `WireIntent.strikes` (the kernel's strikeout channel) and field ops onto
|
|
57
|
+
* `events`. The strike flows through the engine plan output (`{events, strikes}`) so it
|
|
58
|
+
* is VERIFIED at the gate — never a forgeable, out-of-band retraction (determinism law).
|
|
59
|
+
*
|
|
60
|
+
* The unlock for the repair domain (#260): `revert`, `replace`, and `drain` all compose
|
|
61
|
+
* from `strike` + ordinary event ops — no per-hatch machinery.
|
|
62
|
+
*/
|
|
63
|
+
export interface StrikeOp {
|
|
64
|
+
readonly kind: "strike";
|
|
65
|
+
/** The intent id to retract (toggle). */
|
|
66
|
+
readonly target: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A planned op a directive's `plan` returns — a field write or a strike. */
|
|
70
|
+
export type PlannedOp = FieldOp | StrikeOp;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Strike (retract / toggle-off) the intent `target`. Composable in any `plan`
|
|
74
|
+
* alongside `set`/`addToSet`/`setEntry`. See {@link StrikeOp}.
|
|
75
|
+
*/
|
|
76
|
+
export function strike(target: string): StrikeOp {
|
|
77
|
+
return { kind: "strike", target };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** An op target: either a bare TYPE handle (unbound) or an instance-bound ref. */
|
|
81
|
+
type Target<Id extends string = string, F extends Record<string, Field> = Record<string, Field>> =
|
|
82
|
+
| AggregateHandle<Id, F>
|
|
83
|
+
| BoundAggregate<Id, F>;
|
|
84
|
+
|
|
85
|
+
type Fields<A> = A extends Target<string, infer F> ? F : never;
|
|
86
|
+
|
|
87
|
+
/** Resolve a target's `{aggregateId, aggregateType}` — bound → instance id + type. */
|
|
88
|
+
function addressOf(agg: Target): { aggregateId: string; aggregateType?: string } {
|
|
89
|
+
if ((agg as BoundAggregate).__isBoundAggregate === true) {
|
|
90
|
+
const b = agg as BoundAggregate;
|
|
91
|
+
return { aggregateId: b.id, aggregateType: b.type };
|
|
92
|
+
}
|
|
93
|
+
return { aggregateId: (agg as AggregateHandle).id };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Set a scalar field. Field name + value type are both checked. */
|
|
97
|
+
export function set<
|
|
98
|
+
A extends Target,
|
|
99
|
+
K extends keyof Fields<A> & string,
|
|
100
|
+
>(agg: A, field: K, value: FieldValue<Fields<A>[K]>): FieldOp {
|
|
101
|
+
return { ...addressOf(agg), field, kind: "set", value };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Return the same field op with an explicit per-aggregate event marker. */
|
|
105
|
+
export function withMarker(op: FieldOp, marker: PlannedEventMarker): FieldOp {
|
|
106
|
+
return { ...op, marker };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// NOTE (id-mint redesign): the `setReserved` / `ReservedField` mechanism is GONE. Its
|
|
110
|
+
// only member was `__mintSeed` — the per-mint nonce a minted-create recorded so the OLD
|
|
111
|
+
// gate could re-derive the FNV id. The kernel now mints a `"{TypeTag}_{UUIDv7}"` from a
|
|
112
|
+
// HOST-INJECTED rng and the gate VERIFIES the id's shape + type-tag prefix (no re-derive),
|
|
113
|
+
// so there is NO seed to record — a `.creates` plan stamps NOTHING extra. The auto-stamped
|
|
114
|
+
// `__type`/`__id` provenance (the directive runtime adds those) is the only reserved write, and it
|
|
115
|
+
// is the runtime's job, not a plan's. With no reserved field left, this escape hatch is
|
|
116
|
+
// removed (a domain plan only ever calls the schema-checked `set`/`addToSet`/`setEntry`).
|
|
117
|
+
|
|
118
|
+
/** Add items to a set field. The field's element type must be string[]. */
|
|
119
|
+
export function addToSet<
|
|
120
|
+
A extends Target,
|
|
121
|
+
K extends {
|
|
122
|
+
[P in keyof Fields<A>]: FieldValue<Fields<A>[P]> extends string[] ? P : never;
|
|
123
|
+
}[keyof Fields<A>] &
|
|
124
|
+
string,
|
|
125
|
+
>(agg: A, field: K, items: readonly string[]): FieldOp {
|
|
126
|
+
return { ...addressOf(agg), field, kind: "addToSet", items };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Set one entry of a map field. The map's value type is checked. */
|
|
130
|
+
export function setEntry<
|
|
131
|
+
A extends Target,
|
|
132
|
+
K extends {
|
|
133
|
+
[P in keyof Fields<A>]: FieldValue<Fields<A>[P]> extends Record<string, unknown> ? P : never;
|
|
134
|
+
}[keyof Fields<A>] &
|
|
135
|
+
string,
|
|
136
|
+
>(
|
|
137
|
+
agg: A,
|
|
138
|
+
field: K,
|
|
139
|
+
key: string,
|
|
140
|
+
value: FieldValue<Fields<A>[K]> extends Record<string, infer V> ? V : never,
|
|
141
|
+
): FieldOp {
|
|
142
|
+
return { ...addressOf(agg), field, kind: "setEntry", entryKey: key, value };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type { Field };
|
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
* `first(id)` / `take(id, n)` builders — ORDER-SENSITIVE ROW reads.
|
|
10
|
+
*
|
|
11
|
+
* These are deterministic ONLY under a declared total order. The type-level guardrail
|
|
12
|
+
* (spec §2.3): a `first` or `take` is UN-CONSTRUCTIBLE without `.orderBy(key)` — the
|
|
13
|
+
* `*NeedsOrder` intermediate types expose ONLY `.where(...)` and `.orderBy(key)` as their
|
|
14
|
+
* finishing method. A `first("x").of(Listing)` without `.orderBy(...)` is NOT assignable
|
|
15
|
+
* to `DomainModule.firsts` → COMPILE ERROR.
|
|
16
|
+
*
|
|
17
|
+
* This is the OPPOSITE type-state to count/sum/min/max/exists:
|
|
18
|
+
* * count/sum/min/max/exists assert the ABSENCE of `.orderBy` (they are order-INDEPENDENT
|
|
19
|
+
* reductions; adding `.orderBy` would be loosening — LAW 3).
|
|
20
|
+
* * first/take assert the PRESENCE of `.orderBy` (they are order-SENSITIVE; the finished
|
|
21
|
+
* form is UN-CONSTRUCTIBLE without it — the positive guardrail from spec §2.3).
|
|
22
|
+
*
|
|
23
|
+
* KEY type constraint: `.orderBy(key)` is keyof-checked against `PredScalarKeys<F>` —
|
|
24
|
+
* the same scalar kinds (string|enum|ref|int) that produce an EAV index needle. Sets,
|
|
25
|
+
* maps, json, and evidence fields are rejected because an order over a collection is
|
|
26
|
+
* UNDEFINED and therefore non-deterministic (LAW 1).
|
|
27
|
+
*
|
|
28
|
+
* TOTAL ORDER: the lowered SQL always appends `, aggregate_id` as a MANDATORY second
|
|
29
|
+
* ORDER-BY term (spec §3c). A non-unique `<key>` without the tiebreak would make LIMIT
|
|
30
|
+
* row-order-dependent = the death bug. The tiebreak is invisible to the accessor (the
|
|
31
|
+
* dev declared the sort key; the engine adds the tiebreak automatically). The `order_key`
|
|
32
|
+
* field is NON-OPTIONAL in the IR (can never be absent — the no-fallback discipline,
|
|
33
|
+
* `manifest.rs:389-397`).
|
|
34
|
+
*
|
|
35
|
+
* NO ORDER-INDEPENDENT SURFACE: first/take expose NO `.where`-free form without
|
|
36
|
+
* `.orderBy`, and no `.by(...)` group-by (row reads are not partitioned — they return
|
|
37
|
+
* the top-n entities globally, possibly filtered by predicate). This is coherent: a
|
|
38
|
+
* "top-n per group" is a different primitive (window function) and out of Slice 2a scope.
|
|
39
|
+
*/
|
|
40
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
41
|
+
import type { Field } from "./fields.js";
|
|
42
|
+
import { type Predicate, type CanonicalPred, predBuilder, canonicalizePred, type PredScalarKeys } from "./predicate.js";
|
|
43
|
+
|
|
44
|
+
// ─── IR ───────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A FINISHED ordered-read declaration (the read-engine's input shape). The MANDATORY
|
|
48
|
+
* `orderKey` (non-optional at both TS and manifest/Rust levels) is the load-bearing
|
|
49
|
+
* total-order guarantee. `limit` distinguishes `first` (implicit 1) from `take(n)`.
|
|
50
|
+
*
|
|
51
|
+
* ADVERSARIAL CONTRACT: `orderKey` MUST be declared and MUST be a scalar field of the
|
|
52
|
+
* `of`-aggregate. The manifest-load Rust validation (`manifest.rs`) rejects any
|
|
53
|
+
* `OrderedReadSpec` whose `order_key` is absent or not a scalar field of `of` — LOUD
|
|
54
|
+
* reject, no fallback (the `manifest.rs:389-397` discipline).
|
|
55
|
+
*/
|
|
56
|
+
export interface OrderedReadDecl {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
/** The aggregate TYPE id the read returns, e.g. `ListingAggregate`. */
|
|
59
|
+
readonly of: string;
|
|
60
|
+
/**
|
|
61
|
+
* The REQUIRED total-order key (a declared scalar field of `of`). Non-optional: the
|
|
62
|
+
* order-sensitive read is UN-CONSTRUCTIBLE without it (type-level guardrail). The
|
|
63
|
+
* lowered SQL appends `, aggregate_id` as a mandatory tiebreak (spec §3c).
|
|
64
|
+
*/
|
|
65
|
+
readonly orderKey: string;
|
|
66
|
+
/** Whether to sort DESCENDING. Default `false` (ascending). */
|
|
67
|
+
readonly orderDesc: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The optional predicate: ONLY aggregates satisfying this predicate are candidates.
|
|
70
|
+
* ABSENT ⇒ every aggregate of the `of`-type is in the candidate set.
|
|
71
|
+
*/
|
|
72
|
+
readonly where?: CanonicalPred;
|
|
73
|
+
/**
|
|
74
|
+
* `1` for `first(id)`, `n` for `take(id, n)`. Both are strictly `LIMIT n` applied
|
|
75
|
+
* AFTER the full `ORDER BY <key>, aggregate_id` (spec §3 line 49).
|
|
76
|
+
*/
|
|
77
|
+
readonly limit: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── `first` type-state chain ────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Intermediate state for `first`: only `.where(...)` and `.orderBy(key)` are available.
|
|
84
|
+
* `.orderBy(key)` is the ONLY finishing method → `FirstDecl`. Without it, the builder
|
|
85
|
+
* is NOT assignable to `DomainModule.firsts` (`FirstDecl[]`). This is the positive
|
|
86
|
+
* guardrail (spec §2.3): the finished form is UN-CONSTRUCTIBLE without a declared order.
|
|
87
|
+
*/
|
|
88
|
+
export interface FirstNeedsOrder<F extends Record<string, Field> = Record<string, Field>> {
|
|
89
|
+
readonly id: string;
|
|
90
|
+
readonly of: string;
|
|
91
|
+
/** Attach a PREDICATE: only aggregates satisfying it are considered. Optional. */
|
|
92
|
+
where(fn: (p: ReturnType<typeof predBuilder<F>>) => Predicate<F>): FirstNeedsOrder<F>;
|
|
93
|
+
/**
|
|
94
|
+
* Declare the TOTAL ORDER key — the ONLY finishing method. `key` is keyof-checked
|
|
95
|
+
* against the aggregate's scalar fields (`PredScalarKeys<F>`). Returns a finished
|
|
96
|
+
* `OrderedReadDecl`. Optional `.desc()` reversal is controlled by the `desc` parameter
|
|
97
|
+
* (default `false` → ascending).
|
|
98
|
+
*/
|
|
99
|
+
orderBy(key: PredScalarKeys<F>, desc?: boolean): OrderedReadDecl;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The INITIAL, un-typed `first` — its ONLY method is `.of(...)`. A `first(id)` without
|
|
104
|
+
* `.of(...)` cannot be used as a declaration.
|
|
105
|
+
*/
|
|
106
|
+
export interface TypelessFirst {
|
|
107
|
+
readonly id: string;
|
|
108
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): FirstNeedsOrder<F>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── `take` type-state chain ─────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Intermediate state for `take(n)`: only `.where(...)` and `.orderBy(key)` are available.
|
|
115
|
+
* Identical to `FirstNeedsOrder` but carries `limit > 1`. The ONLY finishing method is
|
|
116
|
+
* `.orderBy(key)` → `OrderedReadDecl` with `limit = n`.
|
|
117
|
+
*/
|
|
118
|
+
export interface TakeNeedsOrder<F extends Record<string, Field> = Record<string, Field>> {
|
|
119
|
+
readonly id: string;
|
|
120
|
+
readonly of: string;
|
|
121
|
+
readonly limit: number;
|
|
122
|
+
/** Attach a PREDICATE: only aggregates satisfying it are candidates. Optional. */
|
|
123
|
+
where(fn: (p: ReturnType<typeof predBuilder<F>>) => Predicate<F>): TakeNeedsOrder<F>;
|
|
124
|
+
/**
|
|
125
|
+
* Declare the TOTAL ORDER key — the ONLY finishing method. Mirrors `FirstNeedsOrder`.
|
|
126
|
+
*/
|
|
127
|
+
orderBy(key: PredScalarKeys<F>, desc?: boolean): OrderedReadDecl;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The INITIAL, un-typed `take` — its ONLY method is `.of(...)`. A `take(id, n)` without
|
|
132
|
+
* `.of(...)` cannot be used as a declaration.
|
|
133
|
+
*/
|
|
134
|
+
export interface TypelessTake {
|
|
135
|
+
readonly id: string;
|
|
136
|
+
readonly limit: number;
|
|
137
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): TakeNeedsOrder<F>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Internal factories ───────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function makeFirstNeedsOrder<F extends Record<string, Field>>(
|
|
143
|
+
id: string,
|
|
144
|
+
ofType: string,
|
|
145
|
+
where: CanonicalPred | undefined,
|
|
146
|
+
): FirstNeedsOrder<F> {
|
|
147
|
+
return {
|
|
148
|
+
id,
|
|
149
|
+
of: ofType,
|
|
150
|
+
where(fn: (p: ReturnType<typeof predBuilder<F>>) => Predicate<F>): FirstNeedsOrder<F> {
|
|
151
|
+
const pred = fn(predBuilder<F>());
|
|
152
|
+
const canonical = canonicalizePred(pred as Predicate<Record<string, Field>>);
|
|
153
|
+
return makeFirstNeedsOrder<F>(id, ofType, canonical);
|
|
154
|
+
},
|
|
155
|
+
orderBy(key: PredScalarKeys<F>, desc = false): OrderedReadDecl {
|
|
156
|
+
return {
|
|
157
|
+
id,
|
|
158
|
+
of: ofType,
|
|
159
|
+
orderKey: key as string,
|
|
160
|
+
orderDesc: desc,
|
|
161
|
+
...(where !== undefined ? { where } : {}),
|
|
162
|
+
limit: 1,
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function makeTakeNeedsOrder<F extends Record<string, Field>>(
|
|
169
|
+
id: string,
|
|
170
|
+
ofType: string,
|
|
171
|
+
limit: number,
|
|
172
|
+
where: CanonicalPred | undefined,
|
|
173
|
+
): TakeNeedsOrder<F> {
|
|
174
|
+
return {
|
|
175
|
+
id,
|
|
176
|
+
of: ofType,
|
|
177
|
+
limit,
|
|
178
|
+
where(fn: (p: ReturnType<typeof predBuilder<F>>) => Predicate<F>): TakeNeedsOrder<F> {
|
|
179
|
+
const pred = fn(predBuilder<F>());
|
|
180
|
+
const canonical = canonicalizePred(pred as Predicate<Record<string, Field>>);
|
|
181
|
+
return makeTakeNeedsOrder<F>(id, ofType, limit, canonical);
|
|
182
|
+
},
|
|
183
|
+
orderBy(key: PredScalarKeys<F>, desc = false): OrderedReadDecl {
|
|
184
|
+
return {
|
|
185
|
+
id,
|
|
186
|
+
of: ofType,
|
|
187
|
+
orderKey: key as string,
|
|
188
|
+
orderDesc: desc,
|
|
189
|
+
...(where !== undefined ? { where } : {}),
|
|
190
|
+
limit,
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── Public entry points ──────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Begin a `first` declaration. Returns a `TypelessFirst`: until `.of(aggregate)` and
|
|
200
|
+
* `.orderBy(key)` are called (in order), no finished declaration exists. `first(id)` →
|
|
201
|
+
* `.of(T)` → `.orderBy(key)` is the ONLY path to a usable `OrderedReadDecl` with limit=1.
|
|
202
|
+
*/
|
|
203
|
+
export function first(id: string): TypelessFirst {
|
|
204
|
+
return {
|
|
205
|
+
id,
|
|
206
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): FirstNeedsOrder<F> {
|
|
207
|
+
return makeFirstNeedsOrder<F>(id, aggregate.id, undefined);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Begin a `take(n)` declaration. Returns a `TypelessTake`: until `.of(aggregate)` and
|
|
214
|
+
* `.orderBy(key)` are called, no finished declaration exists. `take(id, n)` → `.of(T)` →
|
|
215
|
+
* `.orderBy(key)` is the ONLY path to a usable `OrderedReadDecl` with limit=n.
|
|
216
|
+
*
|
|
217
|
+
* `n` MUST be ≥ 1. `n = 1` is equivalent to `first` but a distinct declaration id.
|
|
218
|
+
*/
|
|
219
|
+
export function take(id: string, n: number): TypelessTake {
|
|
220
|
+
if (n < 1) throw new Error(`take(${id}, ${n}): n must be ≥ 1`);
|
|
221
|
+
return {
|
|
222
|
+
id,
|
|
223
|
+
limit: n,
|
|
224
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): TakeNeedsOrder<F> {
|
|
225
|
+
return makeTakeNeedsOrder<F>(id, aggregate.id, n, undefined);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
package/src/predicate.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
* Predicate algebra for maintained counts and sums (Slice 1).
|
|
10
|
+
*
|
|
11
|
+
* GRAMMAR (Slice 1 — equality + AND/OR; no join, no order-sensitive op):
|
|
12
|
+
*
|
|
13
|
+
* pred := clause | and(pred, …) | or(pred, …)
|
|
14
|
+
* clause := field<K>.eq(v) | field<K>.ne(v)
|
|
15
|
+
* field := a declared scalar field of the count/sum's `of`-aggregate
|
|
16
|
+
* (string | enum | ref | int — the EAV-index-probeable kinds)
|
|
17
|
+
* value := a literal matching the field's declared type
|
|
18
|
+
*
|
|
19
|
+
* The predicate is a SMALL ALGEBRAIC VALUE — structurally non-SQL, non-code.
|
|
20
|
+
* A dev CANNOT type a non-deterministic or injection-bearing predicate (they
|
|
21
|
+
* cannot write a `WHERE` clause; they compose typed `.field(K).eq(v)` clauses).
|
|
22
|
+
* This satisfies:
|
|
23
|
+
* LAW 1 — determinism by construction (no raw SQL; the predicate evaluates
|
|
24
|
+
* over the HLC-sorted fold, never over a live query).
|
|
25
|
+
* LAW 2 — no raw SQL in a maintained projection: the predicate folds into
|
|
26
|
+
* the Rust diff-maintenance step, NOT into SQL text.
|
|
27
|
+
*
|
|
28
|
+
* NOTE: `field` is restricted to the count/sum's OWN aggregate's scalar fields.
|
|
29
|
+
* A joined-edge predicate (referencing a related aggregate) is explicitly OUT OF
|
|
30
|
+
* SLICE 1 — that requires cross-aggregate invalidation (not built here).
|
|
31
|
+
* The grammar §1.1 + the `ScalarKeys<F>` constraint enforce this statically: a
|
|
32
|
+
* joined-edge field is not in `F`, so `field("joinedEdgeKey")` is a compile error.
|
|
33
|
+
*
|
|
34
|
+
* ORDER-SENSITIVE GUARDRAIL: count/sum expose NO `.first`/`.take`/`.orderBy`.
|
|
35
|
+
* Any future order-sensitive primitive MUST require `.orderBy` to construct
|
|
36
|
+
* (the positive guardrail from spec §2.3). The ABSENCE of those methods here is
|
|
37
|
+
* the precondition assertion: a red test the moment Slice 2 lands `first/take`
|
|
38
|
+
* without the guard. Do NOT add a dead `.orderBy` to count/sum — that is
|
|
39
|
+
* loosening (LAW 3); assert the absence, preserve it.
|
|
40
|
+
*
|
|
41
|
+
* VALUE CANONICALISATION: all predicate `value`s are stored as `string` in the
|
|
42
|
+
* IR (following the `EqFilter.value:string` precedent in `query/compile.ts:56`).
|
|
43
|
+
* For an `int` field the string stores the decimal representation; the Rust
|
|
44
|
+
* evaluator parses it to `Value::Int(n)` for comparison. For enum/string/ref
|
|
45
|
+
* fields it is compared against `Value::Str(s)` DIRECTLY (not the serde-tagged
|
|
46
|
+
* `{"Str":s}` form — that JSON-wrap is a SQL-needle artifact for the report
|
|
47
|
+
* compiler; in-Rust fold maintenance compares `Value`s directly).
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import type { Field, FieldValue } from "./fields.js";
|
|
51
|
+
|
|
52
|
+
// ─── scalar-field key extraction (mirrors report.ts ScalarKeys) ───────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The subset of an aggregate's field-map keys whose field kind is scalar
|
|
56
|
+
* (string | enum | ref | int) — the only kinds that produce an EAV needle the
|
|
57
|
+
* maintained-count predicate can probe. set/map/json/evidence are NOT included:
|
|
58
|
+
* a predicate over a collection field is undefined/non-deterministic.
|
|
59
|
+
*
|
|
60
|
+
* `int` IS included (unlike the report.ts `ScalarKeys` which limits to
|
|
61
|
+
* string|enum|ref|json): a count/sum predicate may filter on an integer field
|
|
62
|
+
* (e.g. `priority eq 1`). The in-Rust evaluator compares `Value::Int(n)` when
|
|
63
|
+
* the field is int-kind.
|
|
64
|
+
*/
|
|
65
|
+
export type PredScalarKeys<F extends Record<string, Field>> = {
|
|
66
|
+
[K in keyof F]: F[K] extends Field<unknown, "string" | "enum" | "ref" | "int"> ? K : never;
|
|
67
|
+
}[keyof F] &
|
|
68
|
+
string;
|
|
69
|
+
|
|
70
|
+
// ─── predicate IR (the wire shape lowered into the manifest) ──────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A single equality or inequality clause.
|
|
74
|
+
*
|
|
75
|
+
* `value` is ALWAYS a `string` in the IR (canonical):
|
|
76
|
+
* - string/enum/ref fields → compared against `Value::Str(value)` in Rust.
|
|
77
|
+
* - int fields → the Rust evaluator parses `value` to `i64` and
|
|
78
|
+
* compares against `Value::Int(n)`. Parse failures
|
|
79
|
+
* are a manifest-validation error, not a runtime one.
|
|
80
|
+
*
|
|
81
|
+
* ABSENT-FIELD SEMANTICS (determinism requirement — see §7-D of the contract):
|
|
82
|
+
* `eq` over an absent field evaluates to FALSE (unset ≠ any literal).
|
|
83
|
+
* `ne` over an absent field evaluates to TRUE (unset ≠ any literal).
|
|
84
|
+
* This must be identical on both the OLD and NEW sides of the incremental diff
|
|
85
|
+
* (the symmetry requirement — lib.rs §3.1).
|
|
86
|
+
*/
|
|
87
|
+
export type PredClause<F extends Record<string, Field> = Record<string, Field>> =
|
|
88
|
+
| { readonly kind: "eq"; readonly field: PredScalarKeys<F> & string; readonly value: string }
|
|
89
|
+
| { readonly kind: "ne"; readonly field: PredScalarKeys<F> & string; readonly value: string };
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A predicate tree: a single clause, or a boolean combination of sub-predicates.
|
|
93
|
+
*
|
|
94
|
+
* Slice 1 supports `eq`/`ne` + AND/OR of clause-trees. NO nesting beyond
|
|
95
|
+
* AND/OR-of-clauses is required for the `publishedCount` proof
|
|
96
|
+
* (`status eq "approved"` is a single clause). The tree is modelled to avoid a
|
|
97
|
+
* forced 2nd pass when OR of clauses is needed.
|
|
98
|
+
*
|
|
99
|
+
* CONTRACT: AND/OR are evaluated over the count/sum's OWN aggregate's folded
|
|
100
|
+
* state ONLY — no cross-aggregate join. The `F` type parameter enforces this:
|
|
101
|
+
* every `field` key is `keyof F` for the `of`-aggregate's field map.
|
|
102
|
+
*/
|
|
103
|
+
export type Predicate<F extends Record<string, Field> = Record<string, Field>> =
|
|
104
|
+
| PredClause<F>
|
|
105
|
+
| { readonly kind: "and"; readonly clauses: readonly Predicate<F>[] }
|
|
106
|
+
| { readonly kind: "or"; readonly clauses: readonly Predicate<F>[] };
|
|
107
|
+
|
|
108
|
+
// ─── builder surface (handed to the .where(p => …) lambda) ───────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The typed predicate builder handed to `.where(p => …)`. `F` is the field map
|
|
112
|
+
* of the count/sum's `of`-aggregate, so:
|
|
113
|
+
* - `p.field("unknownField")` is a COMPILE error (not in `keyof F`).
|
|
114
|
+
* - `p.field("status").eq("unknown")` is a COMPILE error when `status` is
|
|
115
|
+
* `t.enum(["draft","submitted","approved","archived"])` and `"unknown"` is
|
|
116
|
+
* not a member.
|
|
117
|
+
* - `p.field("status").eq(42)` is a COMPILE error (int ≠ enum value type).
|
|
118
|
+
*
|
|
119
|
+
* This is the typed analogue of `report.ts:268-282` `WhereBuilder<F>`, but
|
|
120
|
+
* produces a MAINTAINED-COUNT `Predicate<F>` instead of the entity-graph `Filter`
|
|
121
|
+
* (which lowers to sealed SQL — a different path, not reused here).
|
|
122
|
+
*/
|
|
123
|
+
export interface PredBuilder<F extends Record<string, Field>> {
|
|
124
|
+
/** Pick a scalar field of the `of`-aggregate by its exact (keyof-checked) name. */
|
|
125
|
+
field<K extends PredScalarKeys<F>>(
|
|
126
|
+
key: K,
|
|
127
|
+
): {
|
|
128
|
+
/** Equality: count/include only when this field's value equals [v]. */
|
|
129
|
+
eq(v: FieldValue<F[K]>): PredClause<F>;
|
|
130
|
+
/** Inequality: count/include only when this field's value differs from [v]. */
|
|
131
|
+
ne(v: FieldValue<F[K]>): PredClause<F>;
|
|
132
|
+
};
|
|
133
|
+
/** Logical AND of two or more sub-predicates. */
|
|
134
|
+
and(...cs: Predicate<F>[]): Predicate<F>;
|
|
135
|
+
/** Logical OR of two or more sub-predicates. */
|
|
136
|
+
or(...cs: Predicate<F>[]): Predicate<F>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Construct a `PredBuilder` for the aggregate whose field map is `F`. */
|
|
140
|
+
export function predBuilder<F extends Record<string, Field>>(): PredBuilder<F> {
|
|
141
|
+
return {
|
|
142
|
+
field<K extends PredScalarKeys<F>>(key: K) {
|
|
143
|
+
return {
|
|
144
|
+
eq: (v: FieldValue<F[K]>): PredClause<F> => ({
|
|
145
|
+
kind: "eq",
|
|
146
|
+
field: key,
|
|
147
|
+
// Canonical: store as string (int fields: decimal repr; enum/string/ref: as-is).
|
|
148
|
+
value: String(v),
|
|
149
|
+
}),
|
|
150
|
+
ne: (v: FieldValue<F[K]>): PredClause<F> => ({
|
|
151
|
+
kind: "ne",
|
|
152
|
+
field: key,
|
|
153
|
+
value: String(v),
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
and(...cs: Predicate<F>[]): Predicate<F> {
|
|
158
|
+
return { kind: "and", clauses: cs };
|
|
159
|
+
},
|
|
160
|
+
or(...cs: Predicate<F>[]): Predicate<F> {
|
|
161
|
+
return { kind: "or", clauses: cs };
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── canonical predicate (manifest-stable serialisation) ─────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The manifest-stable (canonical) predicate shape: clauses sorted by
|
|
170
|
+
* `(field, kind, value)`, AND/OR connectives preserved. A byte-identical
|
|
171
|
+
* predicate produces a byte-identical canonical form — the hash is stable.
|
|
172
|
+
*
|
|
173
|
+
* OMIT-WHEN-ABSENT: a count/sum with NO predicate omits `where` entirely from
|
|
174
|
+
* the canonical manifest (same discipline as the `by?` field in `CanonicalCount`).
|
|
175
|
+
* Only a COUNT/SUM WITH a predicate carries a `where` key; predicate-free counts
|
|
176
|
+
* are byte-identical to the form before `where` existed (golden hashes stable).
|
|
177
|
+
*/
|
|
178
|
+
export type CanonicalPred =
|
|
179
|
+
| { readonly kind: "eq"; readonly field: string; readonly value: string }
|
|
180
|
+
| { readonly kind: "ne"; readonly field: string; readonly value: string }
|
|
181
|
+
| { readonly kind: "and"; readonly clauses: readonly CanonicalPred[] }
|
|
182
|
+
| { readonly kind: "or"; readonly clauses: readonly CanonicalPred[] };
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Canonicalize a `Predicate` into a `CanonicalPred`. Clauses within AND/OR are
|
|
186
|
+
* sorted deterministically by their string serialisation so the canonical form
|
|
187
|
+
* is insertion-order–independent (identical predicate logic → identical manifest
|
|
188
|
+
* fragment → identical hash, regardless of the order clauses were composed).
|
|
189
|
+
*/
|
|
190
|
+
export function canonicalizePred(pred: Predicate<Record<string, Field>>): CanonicalPred {
|
|
191
|
+
if (pred.kind === "eq" || pred.kind === "ne") {
|
|
192
|
+
return { kind: pred.kind, field: pred.field, value: pred.value };
|
|
193
|
+
}
|
|
194
|
+
// AND / OR: recurse + sort children by their JSON serialisation.
|
|
195
|
+
const clauses = [...pred.clauses]
|
|
196
|
+
.map(canonicalizePred)
|
|
197
|
+
.sort((a, b) => {
|
|
198
|
+
const sa = JSON.stringify(a);
|
|
199
|
+
const sb = JSON.stringify(b);
|
|
200
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
201
|
+
});
|
|
202
|
+
return { kind: pred.kind, clauses };
|
|
203
|
+
}
|
|
Binary file
|