@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/compose.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
* #137 — the OpenUSD-shaped COMPOSED IR (the "stage").
|
|
10
|
+
*
|
|
11
|
+
* A Nomos domain is a STAGE composed from ordered LAYERS
|
|
12
|
+
* (prelude → tenant → customer → jurisdiction → policy). Unlike USD's
|
|
13
|
+
* "strongest opinion wins", composition here is TYPED and MONOTONIC: a stronger
|
|
14
|
+
* (later) layer may only TIGHTEN the law — narrow a scope, add a required
|
|
15
|
+
* capability — unless an explicit `authority` widens it. The flattened stage's
|
|
16
|
+
* canonical hash is the domain identity (extends #136: the canonical-manifest
|
|
17
|
+
* hash; this module reuses `canonicalJson` for byte-stable hashing).
|
|
18
|
+
*
|
|
19
|
+
* This is the MODEL of USD composition (typed monotonic merge), NOT the USD
|
|
20
|
+
* library. Layers/prims here are SYNTHETIC framework units — no tenant/business
|
|
21
|
+
* domain is named or imported.
|
|
22
|
+
*
|
|
23
|
+
* Fail-closed: any rule a merge cannot satisfy is surfaced as a typed
|
|
24
|
+
* `CompositionViolation`, never silently coerced.
|
|
25
|
+
*/
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
import { canonicalJson } from "./manifest.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A composable law unit (modelled on a directive/role). The typed-rule fields:
|
|
31
|
+
* - `requires`: capability ids that MUST hold; a stronger layer may only ADD.
|
|
32
|
+
* - `scope`: a `/`-segmented path (e.g. `site/s1`); a stronger layer may only
|
|
33
|
+
* NARROW (equal or more specific) without authority.
|
|
34
|
+
* - `payload`: an optional artifact reference that MUST be `certified`.
|
|
35
|
+
*/
|
|
36
|
+
export interface PrimDecl {
|
|
37
|
+
readonly requires: string[];
|
|
38
|
+
/** A `/`-segmented scope path; broader = shorter ANCESTOR prefix. */
|
|
39
|
+
readonly scope: string;
|
|
40
|
+
readonly payload?: { artifactHash: string; certified: boolean };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A composition layer: a stage path + its prims keyed by prim path. */
|
|
44
|
+
export interface Layer {
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly prims: Record<string, PrimDecl>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Options for composition — `authority` permits explicit widening / removal. */
|
|
50
|
+
export interface ComposeOptions {
|
|
51
|
+
/** When true, scope-WIDEN and requires-REMOVE are explicitly permitted. */
|
|
52
|
+
readonly authority?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The USD VARIANT selection (LIVRPS — Variants sit above References, below Local).
|
|
55
|
+
* Maps a layer's variant SET name → the chosen variant NAME within it. An explicit
|
|
56
|
+
* input to `flattenUsd`/`usdHash` (NOT baked into the document): the composed law is
|
|
57
|
+
* a function of `(doc, selection)`, so a different selection yields a different
|
|
58
|
+
* commit-pinned composed identity. A set with NO entry here contributes its declared
|
|
59
|
+
* DEFAULT variant if one exists (the `*` key, see `usd.ts`), else NOTHING. A selection
|
|
60
|
+
* naming an unknown set or variant fails closed (`variant-unresolved`).
|
|
61
|
+
*/
|
|
62
|
+
readonly variantSelection?: Record<string, string>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The typed composition rules that can be violated. */
|
|
66
|
+
export type ViolationRule =
|
|
67
|
+
| "requires-remove"
|
|
68
|
+
| "scope-widen"
|
|
69
|
+
| "uncertified-payload"
|
|
70
|
+
// The USD REFERENCE arc (composed in `usd.ts`): a reference cycle, or a
|
|
71
|
+
// reference to a layer path not present in the document, both fail closed.
|
|
72
|
+
| "reference-cycle"
|
|
73
|
+
| "reference-unresolved"
|
|
74
|
+
// The USD VARIANT arc (composed in `usd.ts`): a selection naming a variant set
|
|
75
|
+
// not declared on the layer, or a variant not present within a declared set,
|
|
76
|
+
// both fail closed (an explicit selection that cannot be resolved is never a no-op).
|
|
77
|
+
| "variant-unresolved";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A typed, descriptive composition failure. Thrown (fail-closed) when a merge
|
|
81
|
+
* cannot satisfy a typed rule. Never produced by silent coercion.
|
|
82
|
+
*/
|
|
83
|
+
export class CompositionViolation extends Error {
|
|
84
|
+
readonly rule: ViolationRule;
|
|
85
|
+
readonly primPath: string;
|
|
86
|
+
readonly detail: string;
|
|
87
|
+
constructor(rule: ViolationRule, primPath: string, detail: string) {
|
|
88
|
+
super(`composition ${rule} at ${primPath}: ${detail}`);
|
|
89
|
+
this.name = "CompositionViolation";
|
|
90
|
+
this.rule = rule;
|
|
91
|
+
this.primPath = primPath;
|
|
92
|
+
this.detail = detail;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One flattened prim in the effective stage (canonical: requires SORTED). */
|
|
97
|
+
export interface FlatPrim {
|
|
98
|
+
readonly requires: string[];
|
|
99
|
+
readonly scope: string;
|
|
100
|
+
readonly payload?: { artifactHash: string; certified: boolean };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The effective composed manifest: prims SORTED by path. */
|
|
104
|
+
export interface Composed {
|
|
105
|
+
readonly prims: Record<string, FlatPrim>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Split a `/`-segmented path into non-empty segments. */
|
|
109
|
+
function segments(path: string): string[] {
|
|
110
|
+
return path.split("/").filter((s) => s.length > 0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* `covers(a, b)`: `a` covers `b` iff `a`'s segments are a PREFIX of `b`'s.
|
|
115
|
+
* So `site` covers `site/s1`; equal paths cover each other (a path covers itself).
|
|
116
|
+
*/
|
|
117
|
+
export function covers(a: string, b: string): boolean {
|
|
118
|
+
const sa = segments(a);
|
|
119
|
+
const sb = segments(b);
|
|
120
|
+
if (sa.length > sb.length) return false;
|
|
121
|
+
for (let i = 0; i < sa.length; i++) {
|
|
122
|
+
if (sa[i] !== sb[i]) return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** WIDEN(base, over): `over.scope` covers `base.scope` AND they differ. */
|
|
128
|
+
function isWiden(base: string, over: string): boolean {
|
|
129
|
+
return base !== over && covers(over, base);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** NARROW(base, over): `base.scope` covers `over.scope` (equal counts as allowed). */
|
|
133
|
+
function isNarrowOrEqual(base: string, over: string): boolean {
|
|
134
|
+
return covers(base, over);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The TYPED requires+scope monotonic-merge rule — the single, reusable rule core
|
|
139
|
+
* (#137) shared by `PrimDecl` composition AND the unified USD directive-prim merge
|
|
140
|
+
* (`usd.ts`). It operates on the rule-bearing fields ONLY (capability set + optional
|
|
141
|
+
* scope path), so it can fold a directive prim that has NO static scope (the DSL
|
|
142
|
+
* today emits none): a `scope` of `undefined` on BOTH sides simply skips the scope
|
|
143
|
+
* rule; a present-vs-absent scope is treated as introducing a scope (a NARROW from
|
|
144
|
+
* "root", always allowed) or as REMOVING one (a WIDEN, authority-gated). Returns the
|
|
145
|
+
* merged `{ requires (sorted union), scope? }`, or throws a typed `CompositionViolation`.
|
|
146
|
+
*
|
|
147
|
+
* This is a STRENGTHENING, not a loosening: the existing `string`-scope rules are
|
|
148
|
+
* preserved byte-for-byte (compose.test's 7 still pass); the only additions are the
|
|
149
|
+
* previously-unreachable optional-scope cases, each held to the SAME monotonic law.
|
|
150
|
+
*/
|
|
151
|
+
export function mergeRequiresScope(
|
|
152
|
+
primPath: string,
|
|
153
|
+
base: { requires: readonly string[]; scope?: string },
|
|
154
|
+
over: { requires: readonly string[]; scope?: string },
|
|
155
|
+
opts: ComposeOptions,
|
|
156
|
+
): { requires: string[]; scope?: string } {
|
|
157
|
+
const authority = opts.authority === true;
|
|
158
|
+
|
|
159
|
+
// requires: every base capability MUST remain (⊆ override). The override may
|
|
160
|
+
// ADD caps. A silent removal is a VIOLATION unless authority.
|
|
161
|
+
const overSet = new Set(over.requires);
|
|
162
|
+
if (!authority) {
|
|
163
|
+
for (const cap of base.requires) {
|
|
164
|
+
if (!overSet.has(cap)) {
|
|
165
|
+
throw new CompositionViolation(
|
|
166
|
+
"requires-remove",
|
|
167
|
+
primPath,
|
|
168
|
+
`override drops base capability "${cap}"`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// scope: monotonic NARROW-or-equal wins; WIDEN / REMOVE / disjoint are
|
|
175
|
+
// authority-gated VIOLATIONS. Absent scope = "root" (the broadest), so:
|
|
176
|
+
// - both absent → no scope rule, stays absent.
|
|
177
|
+
// - base absent, over set → over INTRODUCES a scope = NARROW from root → ok.
|
|
178
|
+
// - base set, over absent → over REMOVES the scope = WIDEN to root → authority.
|
|
179
|
+
// - both set → the existing string-path NARROW/WIDEN/disjoint rules.
|
|
180
|
+
let effectiveScope: string | undefined;
|
|
181
|
+
if (base.scope === undefined && over.scope === undefined) {
|
|
182
|
+
effectiveScope = undefined;
|
|
183
|
+
} else if (base.scope === undefined) {
|
|
184
|
+
// Introducing a scope tightens the (root) law — always allowed.
|
|
185
|
+
effectiveScope = over.scope;
|
|
186
|
+
} else if (over.scope === undefined) {
|
|
187
|
+
// Removing the base scope WIDENS to root — authority required.
|
|
188
|
+
if (!authority) {
|
|
189
|
+
throw new CompositionViolation(
|
|
190
|
+
"scope-widen",
|
|
191
|
+
primPath,
|
|
192
|
+
`override removes base scope "${base.scope}" (widens to root)`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
effectiveScope = undefined;
|
|
196
|
+
} else {
|
|
197
|
+
if (!isNarrowOrEqual(base.scope, over.scope)) {
|
|
198
|
+
if (isWiden(base.scope, over.scope)) {
|
|
199
|
+
if (!authority) {
|
|
200
|
+
throw new CompositionViolation(
|
|
201
|
+
"scope-widen",
|
|
202
|
+
primPath,
|
|
203
|
+
`override widens scope from "${base.scope}" to "${over.scope}"`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// Disjoint scopes (neither covers the other) cannot tighten the law.
|
|
208
|
+
throw new CompositionViolation(
|
|
209
|
+
"scope-widen",
|
|
210
|
+
primPath,
|
|
211
|
+
`override scope "${over.scope}" is disjoint from base "${base.scope}"`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
effectiveScope = over.scope;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Flattened requires = union of base + override (sorted).
|
|
219
|
+
const merged = new Set<string>(base.requires);
|
|
220
|
+
for (const cap of over.requires) merged.add(cap);
|
|
221
|
+
const requires = [...merged].sort();
|
|
222
|
+
|
|
223
|
+
return effectiveScope !== undefined
|
|
224
|
+
? { requires, scope: effectiveScope }
|
|
225
|
+
: { requires };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validate a prim's payload rule in isolation: a present payload MUST be
|
|
230
|
+
* `certified: true`. This is NEVER gated by authority — an uncertified artifact
|
|
231
|
+
* is always refused.
|
|
232
|
+
*/
|
|
233
|
+
function assertPayloadCertified(primPath: string, prim: PrimDecl): void {
|
|
234
|
+
if (prim.payload !== undefined && prim.payload.certified !== true) {
|
|
235
|
+
throw new CompositionViolation(
|
|
236
|
+
"uncertified-payload",
|
|
237
|
+
primPath,
|
|
238
|
+
`payload artifact ${prim.payload.artifactHash} is not certified`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Merge a stronger `over` prim onto a `base` prim per the TYPED rules.
|
|
245
|
+
* Returns the flattened effective prim, or throws a `CompositionViolation`.
|
|
246
|
+
*/
|
|
247
|
+
function mergePrim(
|
|
248
|
+
primPath: string,
|
|
249
|
+
base: PrimDecl,
|
|
250
|
+
over: PrimDecl,
|
|
251
|
+
opts: ComposeOptions,
|
|
252
|
+
): FlatPrim {
|
|
253
|
+
// Apply the shared typed requires+scope rule core. A `PrimDecl` always carries a
|
|
254
|
+
// (required, string) scope, so the merged result always carries a string scope.
|
|
255
|
+
const { requires, scope } = mergeRequiresScope(primPath, base, over, opts);
|
|
256
|
+
const effectiveScope = scope ?? over.scope;
|
|
257
|
+
|
|
258
|
+
const flat: FlatPrim = over.payload !== undefined
|
|
259
|
+
? { requires, scope: effectiveScope, payload: { ...over.payload } }
|
|
260
|
+
: { requires, scope: effectiveScope };
|
|
261
|
+
return flat;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Canonicalize a standalone prim into a flattened prim (requires SORTED). */
|
|
265
|
+
function flatFromPrim(prim: PrimDecl): FlatPrim {
|
|
266
|
+
const requires = [...prim.requires].sort();
|
|
267
|
+
return prim.payload !== undefined
|
|
268
|
+
? { requires, scope: prim.scope, payload: { ...prim.payload } }
|
|
269
|
+
: { requires, scope: prim.scope };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Fold the layers in given (strength) order into the effective composed stage.
|
|
274
|
+
* For each prim path present in a later layer that also exists earlier, MERGE
|
|
275
|
+
* per the typed rules. Payload certification is enforced for EVERY contributing
|
|
276
|
+
* prim. Throws `CompositionViolation` (fail-closed) on any rule it cannot satisfy.
|
|
277
|
+
*/
|
|
278
|
+
export function compose(layers: Layer[], opts: ComposeOptions = {}): Composed {
|
|
279
|
+
const acc = new Map<string, FlatPrim>();
|
|
280
|
+
|
|
281
|
+
for (const layer of layers) {
|
|
282
|
+
for (const primPath of Object.keys(layer.prims)) {
|
|
283
|
+
const prim = layer.prims[primPath]!;
|
|
284
|
+
// Payload certification is always enforced, base or override.
|
|
285
|
+
assertPayloadCertified(primPath, prim);
|
|
286
|
+
|
|
287
|
+
const existing = acc.get(primPath);
|
|
288
|
+
if (existing === undefined) {
|
|
289
|
+
acc.set(primPath, flatFromPrim(prim));
|
|
290
|
+
} else {
|
|
291
|
+
const base: PrimDecl = existing.payload !== undefined
|
|
292
|
+
? { requires: existing.requires, scope: existing.scope, payload: existing.payload }
|
|
293
|
+
: { requires: existing.requires, scope: existing.scope };
|
|
294
|
+
acc.set(primPath, mergePrim(primPath, base, prim, opts));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Emit prims SORTED by path for a canonical structure.
|
|
300
|
+
const prims: Record<string, FlatPrim> = {};
|
|
301
|
+
for (const primPath of [...acc.keys()].sort()) {
|
|
302
|
+
prims[primPath] = acc.get(primPath)!;
|
|
303
|
+
}
|
|
304
|
+
return { prims };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Returns the effective composed manifest (canonical structure). */
|
|
308
|
+
export function flatten(layers: Layer[], opts: ComposeOptions = {}): Composed {
|
|
309
|
+
return compose(layers, opts);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** The composed stage's content-hash identity: sha256 hex of `canonicalJson(flatten(...))`. */
|
|
313
|
+
export function composedHash(layers: Layer[], opts: ComposeOptions = {}): string {
|
|
314
|
+
return createHash("sha256")
|
|
315
|
+
.update(canonicalJson(flatten(layers, opts)), "utf8")
|
|
316
|
+
.digest("hex");
|
|
317
|
+
}
|
package/src/count.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
* `count(id)` builder — the AGGREGATION analogue of the {@link query} primitive
|
|
10
|
+
* (read-engine step 3).
|
|
11
|
+
*
|
|
12
|
+
* READ-CLOSURE, the aggregation half. A `query` declares a NAMED, INDEXED set read
|
|
13
|
+
* ("every X where field = Y"); a `count` declares a NAMED, MAINTAINED tally ("how many
|
|
14
|
+
* X, grouped by a key field"). The result of a count is ONE number, so by the perf
|
|
15
|
+
* invariant it MUST be O(1): a counter the read engine MAINTAINS incrementally as the
|
|
16
|
+
* workspace folds, NEVER a `COUNT(*)` scan over the counted set. This module adds ONLY
|
|
17
|
+
* the DECLARATION + its TYPE-STATE; the read engine (`nomos_readmodel`) maintains the
|
|
18
|
+
* counter table from the declaration shipped in the runtime manifest.
|
|
19
|
+
*
|
|
20
|
+
* It mirrors `query.ts` at every turn: additive, omit-when-empty, identity-bearing
|
|
21
|
+
* (a count a domain declares is carried into the canonical manifest + becomes part of
|
|
22
|
+
* the domain IDENTITY; a domain that declares NO count is byte-identical to before this
|
|
23
|
+
* existed), and TYPED — `.of(...)` takes an aggregate HANDLE (never a string id), so a
|
|
24
|
+
* typo'd aggregate type is a COMPILE error, the same convention as `query`'s `.returns`.
|
|
25
|
+
*
|
|
26
|
+
* The TYPE-STATE: `count(id)` yields a {@link TypelessCount} whose ONLY method is
|
|
27
|
+
* `.of(...)`; the {@link Count} builder (carrying `.where(...)` and `.by(...)`) is
|
|
28
|
+
* produced solely by `.of(...)`. So a count with no `of`-type cannot be CONSTRUCTED —
|
|
29
|
+
* "every count names the type it tallies" is a type-level property, before any runtime
|
|
30
|
+
* check.
|
|
31
|
+
*
|
|
32
|
+
* `.where(...)` is OPTIONAL: a predicate filters WHICH aggregates are counted. When
|
|
33
|
+
* absent, every aggregate of the `of`-type is counted (same as before this existed —
|
|
34
|
+
* a predicate-free count is byte-identical in the manifest to the legacy form).
|
|
35
|
+
*
|
|
36
|
+
* `.by(...)` is OPTIONAL: a {@link Count} (a bare `.of(...)`) is ALREADY a usable
|
|
37
|
+
* GRAND-TOTAL declaration (one synthetic group); calling `.by(field)` partitions it.
|
|
38
|
+
* Both a {@link Count} and a grouped {@link CountDecl} satisfy {@link AnyCount}, which
|
|
39
|
+
* is what `DomainModule.counts` accepts — see {@link finishCount} for the normalization.
|
|
40
|
+
*
|
|
41
|
+
* ORDER-SENSITIVE GUARDRAIL: count exposes NO `.first`/`.take`/`.orderBy`. Those
|
|
42
|
+
* operations are order-sensitive and require a declared `.orderBy` to be constructible
|
|
43
|
+
* (spec §2.3). The ABSENCE of those methods here is the precondition assertion for
|
|
44
|
+
* Slice 1 — a red compilecheck the moment Slice 2 lands `first/take` without the guard.
|
|
45
|
+
* Do NOT add a dead `.orderBy` here — that is loosening (LAW 3); assert the absence.
|
|
46
|
+
*/
|
|
47
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
48
|
+
import type { Field } from "./fields.js";
|
|
49
|
+
import { type Predicate, type CanonicalPred, predBuilder, canonicalizePred } from "./predicate.js";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A FINISHED count declaration (the read-engine's input shape, mirroring {@link
|
|
53
|
+
* QueryDecl}): an id, the aggregate TYPE it tallies (`of`), the OPTIONAL predicate
|
|
54
|
+
* (`where`) that filters which aggregates are counted, and the OPTIONAL group-by
|
|
55
|
+
* field (`by`).
|
|
56
|
+
* * `where` PRESENT → only aggregates matching the predicate are counted;
|
|
57
|
+
* * `where` ABSENT → every aggregate of the `of`-type is counted.
|
|
58
|
+
* * `by` PRESENT → maintain one counter per distinct value of that field;
|
|
59
|
+
* * `by` ABSENT → maintain ONE grand-total counter (a single synthetic group).
|
|
60
|
+
*/
|
|
61
|
+
export interface CountDecl {
|
|
62
|
+
readonly id: string;
|
|
63
|
+
/** The aggregate TYPE id the count tallies, e.g. `SiteRootAggregate`. */
|
|
64
|
+
readonly of: string;
|
|
65
|
+
/**
|
|
66
|
+
* The optional predicate: ONLY aggregates satisfying this predicate are counted.
|
|
67
|
+
* ABSENT ⇒ every aggregate of the `of`-type (unfiltered, today's behaviour).
|
|
68
|
+
* Stored as a {@link CanonicalPred} so the manifest fragment is deterministic.
|
|
69
|
+
*/
|
|
70
|
+
readonly where?: CanonicalPred;
|
|
71
|
+
/**
|
|
72
|
+
* The group-by field name (the count is partitioned by this field's value). ABSENT
|
|
73
|
+
* ⇒ a grand total over every (matching) aggregate of the `of`-type (one synthetic group).
|
|
74
|
+
*/
|
|
75
|
+
readonly by?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The {@link Count} BUILDER: it has named its `of`-type, so `.where(...)` (the
|
|
80
|
+
* predicate step) and `.by(...)` (the grouping step) are available. It carries `id`
|
|
81
|
+
* + `of` (so a bare `.of(...)` is ALREADY a usable grand-total unfiltered declaration)
|
|
82
|
+
* and the `.where(...)` and `.by(...)` methods. It is a SIBLING of {@link CountDecl}
|
|
83
|
+
* (not a subtype) — the `by` method and `CountDecl`'s `by` field share a name but
|
|
84
|
+
* never coexist on one object; {@link finishCount} normalizes either to a
|
|
85
|
+
* {@link CountDecl}.
|
|
86
|
+
*
|
|
87
|
+
* The `F` type parameter carries the `of`-aggregate's field map so `.where(p =>
|
|
88
|
+
* p.field("status").eq("approved"))` is `keyof`-checked against the aggregate's
|
|
89
|
+
* fields and value-type-checked against the field's declared type.
|
|
90
|
+
*/
|
|
91
|
+
export interface Count<F extends Record<string, Field> = Record<string, Field>> {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
/** The aggregate TYPE id the count tallies. */
|
|
94
|
+
readonly of: string;
|
|
95
|
+
/**
|
|
96
|
+
* Attach an optional PREDICATE: only aggregates satisfying the predicate are
|
|
97
|
+
* counted. The lambda receives a {@link PredBuilder} whose `.field(K)` is
|
|
98
|
+
* `keyof`-checked against the `of`-aggregate's scalar fields and `.eq(v)` /
|
|
99
|
+
* `.ne(v)` is value-type-checked. Returns a new {@link Count} with the predicate
|
|
100
|
+
* baked in; further `.by(...)` is still available.
|
|
101
|
+
*/
|
|
102
|
+
where(fn: (p: ReturnType<typeof predBuilder<F>>) => Predicate<F>): Count<F>;
|
|
103
|
+
/**
|
|
104
|
+
* Partition the count by a GROUP-BY field — every distinct value of `field` gets its
|
|
105
|
+
* own maintained counter. Returns a finished {@link CountDecl}. Omitting `.by` leaves
|
|
106
|
+
* the count a GRAND TOTAL (this builder already carries `id`/`of`).
|
|
107
|
+
*/
|
|
108
|
+
by(field: string): CountDecl;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The INITIAL, un-typed count — its ONLY method is `.of(...)`. It deliberately has NO
|
|
113
|
+
* `.by`, NO `.where`, and is not a {@link Count}/{@link CountDecl}, so
|
|
114
|
+
* `count("c").by(...)` (skipping the `of`-type) is a COMPILE error and a type-less
|
|
115
|
+
* count cannot be constructed. THIS is the type-level "every count names the type it
|
|
116
|
+
* tallies" property.
|
|
117
|
+
*/
|
|
118
|
+
export interface TypelessCount {
|
|
119
|
+
readonly id: string;
|
|
120
|
+
/**
|
|
121
|
+
* Declare the aggregate TYPE this count tallies. Takes a typed HANDLE (never the
|
|
122
|
+
* string id) — a typo'd handle is a compile error, the same convention as `query`'s
|
|
123
|
+
* `.returns(...)`. Returns the {@link Count} builder (the only shape exposing `.by`
|
|
124
|
+
* and `.where`), which is already a usable grand-total unfiltered count.
|
|
125
|
+
*/
|
|
126
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): Count<F>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Either form a domain may declare in `DomainModule.counts`: a grouped {@link CountDecl}
|
|
131
|
+
* (the result of `.by(field)`) or a bare {@link Count} grand-total builder (the result
|
|
132
|
+
* of `.of(...)`). {@link finishCount} normalizes both to a {@link CountDecl}.
|
|
133
|
+
*
|
|
134
|
+
* `Count<any>` — same rationale as `AnyDirective = Directive<any>` in `codegen_dart.ts`:
|
|
135
|
+
* `Count<F>` is invariant in `F` (the `where` method is both a producer and consumer of
|
|
136
|
+
* `PredBuilder<F>`), so `Count<SpecificFields>` is NOT assignable to
|
|
137
|
+
* `Count<Record<string,Field>>`. The consumer side (manifest + codegen) accesses only
|
|
138
|
+
* the `id`/`of`/`by` string fields and the `CanonicalPred`-typed `_where` slot —
|
|
139
|
+
* it never calls `.where(fn)` — so `any` is safe here.
|
|
140
|
+
*/
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
export type AnyCount = CountDecl | Count<any>;
|
|
143
|
+
|
|
144
|
+
/** Narrow: a {@link Count} builder exposes a `by` METHOD; a {@link CountDecl} does not. */
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
|
+
function isCountBuilder(c: AnyCount): c is Count<any> {
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
return typeof (c as Count<any>).by === "function";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Normalize an {@link AnyCount} to a finished {@link CountDecl}. A grand-total builder
|
|
153
|
+
* (bare `.of(...)`) becomes `{id, of}` (no `by`); a grouped `.by(field)` result is
|
|
154
|
+
* already a `CountDecl` and passes through. A `.where(pred)` result (without `.by`)
|
|
155
|
+
* carries `where` on the builder object — finishCount transfers it. Lets
|
|
156
|
+
* `DomainModule.counts` accept either the builder or the finished decl uniformly
|
|
157
|
+
* (mirrors how a query is always a finished `QueryDecl` because `.key(...)` is
|
|
158
|
+
* mandatory; for counts `.by(...)` is optional, so this normalizer absorbs the
|
|
159
|
+
* grand-total builder).
|
|
160
|
+
*/
|
|
161
|
+
export function finishCount(c: AnyCount): CountDecl {
|
|
162
|
+
if (isCountBuilder(c)) {
|
|
163
|
+
// The builder carries `id`, `of`, and a `_where` private slot set by `.where()`.
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
const b = c as any;
|
|
166
|
+
const w: CanonicalPred | undefined = b._where;
|
|
167
|
+
return {
|
|
168
|
+
id: c.id,
|
|
169
|
+
of: c.of,
|
|
170
|
+
...(w !== undefined ? { where: w } : {}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return c;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Begin a count declaration. Returns a {@link TypelessCount}: until `.of(...)` is
|
|
178
|
+
* called, neither `.by`, `.where` nor a usable count exists — the tallied type is NOT
|
|
179
|
+
* optional, it is a prerequisite for the count to take any further shape.
|
|
180
|
+
*/
|
|
181
|
+
export function count<const Id extends string>(id: Id): TypelessCount {
|
|
182
|
+
return {
|
|
183
|
+
id,
|
|
184
|
+
of<F extends Record<string, Field>>(aggregate: AggregateHandle<string, F>): Count<F> {
|
|
185
|
+
return makeCount<F>(id, aggregate.id, undefined);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Internal factory so `.where(...)` can return a new Count without duplicating impl. */
|
|
191
|
+
function makeCount<F extends Record<string, Field>>(
|
|
192
|
+
id: string,
|
|
193
|
+
ofType: string,
|
|
194
|
+
where: CanonicalPred | undefined,
|
|
195
|
+
): Count<F> {
|
|
196
|
+
// Attach the canonical predicate as a named slot so finishCount can transfer it.
|
|
197
|
+
// Use a spread to satisfy `exactOptionalPropertyTypes`: when `where` is undefined
|
|
198
|
+
// we omit the `_where` key entirely rather than writing `_where: undefined`.
|
|
199
|
+
const c = {
|
|
200
|
+
id,
|
|
201
|
+
of: ofType,
|
|
202
|
+
...(where !== undefined ? { _where: where } : {}),
|
|
203
|
+
where(fn: (p: ReturnType<typeof predBuilder<F>>) => Predicate<F>): Count<F> {
|
|
204
|
+
const pred = fn(predBuilder<F>());
|
|
205
|
+
const canonical = canonicalizePred(pred as Predicate<Record<string, Field>>);
|
|
206
|
+
return makeCount<F>(id, ofType, canonical);
|
|
207
|
+
},
|
|
208
|
+
by(field: string): CountDecl {
|
|
209
|
+
return {
|
|
210
|
+
id,
|
|
211
|
+
of: ofType,
|
|
212
|
+
...(where !== undefined ? { where } : {}),
|
|
213
|
+
by: field,
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
} as unknown as Count<F>;
|
|
217
|
+
return c;
|
|
218
|
+
}
|
package/src/ctx.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
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 directive `ctx` exposes only kernel-owned ports (contract §0 law 3:
|
|
10
|
+
* impurity only through injected ports). Stubbed deterministically for now.
|
|
11
|
+
*
|
|
12
|
+
* - ctx.clock -> HLC components (physical, logical, replica)
|
|
13
|
+
* - ctx.id -> IdGenerator (monotonic allocator port)
|
|
14
|
+
* - ctx.rng -> Random port
|
|
15
|
+
*/
|
|
16
|
+
import type { WireHlc } from "./wire.js";
|
|
17
|
+
|
|
18
|
+
export interface Ports {
|
|
19
|
+
clock(): WireHlc;
|
|
20
|
+
id(): string;
|
|
21
|
+
rng(): number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A deterministic stub set of ports — seeded, replayable. */
|
|
25
|
+
export function deterministicPorts(seed: {
|
|
26
|
+
physical?: number;
|
|
27
|
+
logical?: number;
|
|
28
|
+
replica?: number;
|
|
29
|
+
idPrefix?: string;
|
|
30
|
+
} = {}): Ports {
|
|
31
|
+
let physical = seed.physical ?? 1;
|
|
32
|
+
let logical = seed.logical ?? 0;
|
|
33
|
+
const replica = seed.replica ?? 0;
|
|
34
|
+
const idPrefix = seed.idPrefix ?? "id";
|
|
35
|
+
let idCounter = 0;
|
|
36
|
+
let rngState = (seed.physical ?? 1) >>> 0 || 1;
|
|
37
|
+
return {
|
|
38
|
+
clock(): WireHlc {
|
|
39
|
+
const h: WireHlc = { physical, logical, replica };
|
|
40
|
+
// Advance: same physical -> bump logical (mirrors merge.md local-event rule).
|
|
41
|
+
logical += 1;
|
|
42
|
+
return h;
|
|
43
|
+
},
|
|
44
|
+
id(): string {
|
|
45
|
+
idCounter += 1;
|
|
46
|
+
return `${idPrefix}-${idCounter}`;
|
|
47
|
+
},
|
|
48
|
+
rng(): number {
|
|
49
|
+
// xorshift32 — deterministic.
|
|
50
|
+
rngState ^= rngState << 13;
|
|
51
|
+
rngState ^= rngState >>> 17;
|
|
52
|
+
rngState ^= rngState << 5;
|
|
53
|
+
rngState >>>= 0;
|
|
54
|
+
return rngState / 0xffffffff;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|