@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.
Files changed (53) hide show
  1. package/LICENSE.md +36 -0
  2. package/compile_package.mjs +50 -0
  3. package/package.json +59 -0
  4. package/src/aggregate.ts +167 -0
  5. package/src/authoring.ts +119 -0
  6. package/src/build_package.ts +636 -0
  7. package/src/certified_read.ts +313 -0
  8. package/src/codegen_dart.ts +2732 -0
  9. package/src/codegen_dot.ts +466 -0
  10. package/src/codegen_provider_dart.ts +358 -0
  11. package/src/codegen_ts.ts +365 -0
  12. package/src/codegen_usda.ts +388 -0
  13. package/src/combined.ts +195 -0
  14. package/src/compile_engine.ts +567 -0
  15. package/src/compile_package_main.ts +496 -0
  16. package/src/compose.ts +317 -0
  17. package/src/count.ts +218 -0
  18. package/src/ctx.ts +57 -0
  19. package/src/derived.ts +138 -0
  20. package/src/directive.ts +306 -0
  21. package/src/drivers.ts +95 -0
  22. package/src/emits_guard.ts +123 -0
  23. package/src/engine_entry.ts +449 -0
  24. package/src/exists.ts +170 -0
  25. package/src/extremum.ts +227 -0
  26. package/src/fields.ts +291 -0
  27. package/src/framework/bootstrap.ts +22 -0
  28. package/src/framework/disclosure.ts +108 -0
  29. package/src/framework/domain_lifecycle.ts +108 -0
  30. package/src/framework/identity.ts +537 -0
  31. package/src/framework/impure_capability.ts +643 -0
  32. package/src/framework/rbac.ts +418 -0
  33. package/src/framework/repair.ts +150 -0
  34. package/src/framework/sync_lifecycle.ts +125 -0
  35. package/src/framework/workspace_invariant.ts +128 -0
  36. package/src/framework/workspaces.ts +817 -0
  37. package/src/index.ts +317 -0
  38. package/src/manifest.ts +947 -0
  39. package/src/ops.ts +145 -0
  40. package/src/ordered_read.ts +228 -0
  41. package/src/predicate.ts +203 -0
  42. package/src/query/compile.ts +0 -0
  43. package/src/query/relations.ts +144 -0
  44. package/src/query.ts +151 -0
  45. package/src/read.ts +54 -0
  46. package/src/relation.ts +189 -0
  47. package/src/report/csv.ts +54 -0
  48. package/src/report.ts +401 -0
  49. package/src/spatial.ts +115 -0
  50. package/src/sum.ts +194 -0
  51. package/src/usd.ts +563 -0
  52. package/src/wire.ts +149 -0
  53. 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
+ }