@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/derived.ts ADDED
@@ -0,0 +1,138 @@
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
+ * `derived(id)` builder — an ENGINE-PROJECTED, PURE read field (read-engine: derived
10
+ * read fields).
11
+ *
12
+ * READ-CLOSURE, the projection half. A {@link query} declares a NAMED, INDEXED set read;
13
+ * a {@link count} declares a NAMED, MAINTAINED tally; a `derived` declares a NAMED, PURE
14
+ * FUNCTION of ONE aggregate's folded fields (e.g. `SupportSession.isTerminal =
15
+ * state ∈ {terminal states}`). The kernel ledger stays PURE user-intents — a derived
16
+ * value is NEVER stamped into an op/event; it is computed BY THE ENGINE during the
17
+ * projection projection and stored ONLY in the read model, so on a re-fold it is always
18
+ * re-derivable.
19
+ *
20
+ * It mirrors {@link count} at every turn: additive, omit-when-empty, identity-bearing in
21
+ * the CONTRACT sense (the field's NAME/type are part of the read schema), and TYPED —
22
+ * `.of(...)` takes an aggregate HANDLE (never a string id), so a typo'd aggregate type is
23
+ * a COMPILE error, the same convention as `query`'s `.returns`/`count`'s `.of`. The fn
24
+ * BODY (`.as(...)`) is executable behaviour — it ships in the engine bundle and is NOT
25
+ * hashed into the domain identity (the same rule directives' `.plan` bodies follow).
26
+ *
27
+ * The TYPE-STATE: `derived(id)` yields a {@link TypelessDerived} whose ONLY method is
28
+ * `.of(...)`; the {@link DerivedOf} builder (carrying `.as(...)`) is produced solely by
29
+ * `.of(...)`; the finished {@link DerivedDecl} only by `.as(...)`. So a derived field
30
+ * with no `of`-type, or no fn body, cannot be CONSTRUCTED — "every derived field names
31
+ * the aggregate it derives from AND carries a pure fn" is a type-level property, before
32
+ * any runtime check.
33
+ */
34
+ import type { z } from "zod";
35
+ import type { AggregateHandle } from "./aggregate.js";
36
+
37
+ /**
38
+ * The pure derive function: maps ONE aggregate's folded fields (a plain JSON object — the
39
+ * projection's projected `data` for that aggregate) to the derived value. MUST be pure
40
+ * (no ports, no I/O): it runs in the sealed engine over the host-fed `priorState` and its
41
+ * result is stored verbatim into the read model. The value is any JSON-serialisable scalar
42
+ * / object the read schema can decode (the smallest-first target is a `boolean`).
43
+ */
44
+ export type DeriveFn = (aggregate: Record<string, unknown>) => unknown;
45
+
46
+ /**
47
+ * A FINISHED derived-field declaration (the read engine's input shape, mirroring {@link
48
+ * CountDecl}): an id (the stored projection field name), the aggregate TYPE it derives
49
+ * from (`of`), and the pure `fn` body.
50
+ */
51
+ export interface DerivedDecl {
52
+ /** The derived field's NAME — the key the read model stores it under. */
53
+ readonly id: string;
54
+ /** The aggregate TYPE id the field is derived from, e.g. `SupportSessionAggregate`. */
55
+ readonly of: string;
56
+ /** The JSON value schema the engine-projected field returns. */
57
+ readonly returns: z.ZodTypeAny;
58
+ /** The pure fn computing the value from the aggregate's folded fields. */
59
+ readonly fn: DeriveFn;
60
+ }
61
+
62
+ /**
63
+ * The {@link DerivedOf} BUILDER: it has named its `of`-type, so `.as(...)` (which fixes
64
+ * the pure fn and yields the finished {@link DerivedDecl}) is available. This is the ONLY
65
+ * shape carrying `.as` — see {@link TypelessDerived}.
66
+ */
67
+ export interface DerivedOf {
68
+ readonly id: string;
69
+ /** The aggregate TYPE id the field derives from. */
70
+ readonly of: string;
71
+ /**
72
+ * Declare the projected value schema. A derived field without a return schema is not a
73
+ * read contract, because the read model/Dart surface would have to guess.
74
+ */
75
+ returns(schema: z.ZodTypeAny): DerivedReturns;
76
+ }
77
+
78
+ /**
79
+ * The {@link DerivedReturns} BUILDER: it has named its `of`-type AND value schema, so
80
+ * `.as(...)` can fix the executable body and yield the finished declaration.
81
+ */
82
+ export interface DerivedReturns {
83
+ readonly id: string;
84
+ /** The aggregate TYPE id the field derives from. */
85
+ readonly of: string;
86
+ /** The JSON value schema the engine-projected field returns. */
87
+ readonly returns: z.ZodTypeAny;
88
+ /**
89
+ * Fix the PURE derive fn, producing the finished declaration. The fn receives the
90
+ * aggregate's folded fields and returns the derived value.
91
+ */
92
+ as(fn: DeriveFn): DerivedDecl;
93
+ }
94
+
95
+ /**
96
+ * The INITIAL, un-typed derived field — its ONLY method is `.of(...)`. It deliberately
97
+ * has NO `.as` and is not a {@link DerivedDecl}, so `derived("d").as(...)` (skipping the
98
+ * `of`-type) is a COMPILE error and a type-less derived field cannot be constructed. THIS
99
+ * is the type-level "every derived field names the aggregate it derives from" property.
100
+ */
101
+ export interface TypelessDerived {
102
+ readonly id: string;
103
+ /**
104
+ * Declare the aggregate TYPE this field is derived from. Takes a typed HANDLE (never
105
+ * the string id) — a typo'd handle is a compile error, the same convention as
106
+ * `count`'s `.of(...)`. Returns the {@link DerivedOf} builder (the only shape exposing
107
+ * `.as`).
108
+ */
109
+ of(aggregate: AggregateHandle): DerivedOf;
110
+ }
111
+
112
+ /**
113
+ * Begin a derived-field declaration. Returns a {@link TypelessDerived}: until `.of(...)`
114
+ * is called, neither `.as` nor a usable declaration exists — the derived-from type is NOT
115
+ * optional, it is a prerequisite for the field to take any further shape.
116
+ */
117
+ export function derived<const Id extends string>(id: Id): TypelessDerived {
118
+ return {
119
+ id,
120
+ of(aggregate: AggregateHandle): DerivedOf {
121
+ const ofType = aggregate.id;
122
+ return {
123
+ id,
124
+ of: ofType,
125
+ returns(schema: z.ZodTypeAny): DerivedReturns {
126
+ return {
127
+ id,
128
+ of: ofType,
129
+ returns: schema,
130
+ as(fn: DeriveFn): DerivedDecl {
131
+ return { id, of: ofType, returns: schema, fn };
132
+ },
133
+ };
134
+ },
135
+ };
136
+ },
137
+ };
138
+ }
@@ -0,0 +1,306 @@
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
+ * `directive(id)` builder. A directive declares:
10
+ * - a referential marker against an aggregate handle (.creates/.mutates/.ensures/.archives),
11
+ * - a payload schema (Zod — the single source of truth + inference),
12
+ * - a `plan((payload, ctx) => ops)` that returns declarative ops.
13
+ *
14
+ * The marker takes a typed HANDLE, never the string id — a typo'd handle is a
15
+ * compile error. The four markers are the referential relationships from
16
+ * contract §1 (creates/mutates/ensures/archives).
17
+ */
18
+ import type { z } from "zod";
19
+ import type { AggregateHandle } from "./aggregate.js";
20
+ import type { PlannedOp } from "./ops.js";
21
+ import type { Ports } from "./ctx.js";
22
+ import type { CertifiedReadDecl } from "./certified_read.js";
23
+ import type { RelationDecl } from "./relation.js";
24
+
25
+ export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
26
+
27
+ /**
28
+ * A pure, replay-stable derivation of the authz SCOPE an authored intent targets,
29
+ * from its payload — e.g. `(p) => ["site", p.siteId]`. It MUST be a pure function
30
+ * of the payload (no clock, no IO): the executor re-derives the same scope on
31
+ * replay, and `admit`'s authz gate folds the permission projection strictly-before
32
+ * the intent's HLC against exactly this scope.
33
+ */
34
+ export type ScopeFrom<P> = (payload: P) => string[];
35
+
36
+ export interface Directive<P = unknown> {
37
+ readonly id: string;
38
+ readonly marker: ReferentialMarker;
39
+ readonly aggregateId: string;
40
+ readonly payloadSchema: z.ZodType<P>;
41
+ readonly plan: (payload: P, ctx: Ports) => PlannedOp[];
42
+ /**
43
+ * Human-readable description of what this directive does — the SINGLE source of
44
+ * the hover-doc text codegen emits onto each generated payload class + ctor (so
45
+ * the frontend dev sees the intent's meaning at the call site). `undefined` ⇒ the
46
+ * directive declares no prose and codegen synthesises a sensible default from the
47
+ * directive id. Purely additive (a directive that never calls `.doc()` is
48
+ * byte-identical in the canonical manifest to before `description` existed).
49
+ */
50
+ readonly description?: string;
51
+ /**
52
+ * The capability an actor must hold to author this directive (the DECLARATIVE
53
+ * authz pre-condition). `undefined` ⇒ the directive declares no requirement and
54
+ * the gate's authz slot stays inert for it. When set, the kernel threads this
55
+ * into `Authored.required_caps` so `admit`'s authz step flips to `Enforce` once a
56
+ * `RoleCatalogue` is supplied. Defaults to the directive `id` (a directive's name
57
+ * IS its capability — the same convention the `RoleCatalogue` keys caps by).
58
+ */
59
+ readonly requiresCapability?: string;
60
+ /**
61
+ * Pure payload→scope derivation feeding `admit`'s `target` (see {@link ScopeFrom}).
62
+ * `undefined` ⇒ root scope. Authz checks the actor holds `requiresCapability` at
63
+ * a scope COVERING this target.
64
+ */
65
+ readonly scopeFrom?: ScopeFrom<P>;
66
+ /**
67
+ * The directive's DECLARED read boundary: the ref types its `plan` may READ (the
68
+ * "IR for law boundaries" decision — the boundary is part of the domain identity).
69
+ * Deduped + SORTED. Defaults to `[]` (declares no reads). The gate-side enforcement
70
+ * (read ⊆ declared) is a SEPARATE later step; here it only DECLARES.
71
+ */
72
+ readonly declaredReads: string[];
73
+ /**
74
+ * The directive's DECLARED emit boundary: the event types its `plan` may EMIT,
75
+ * each with an optional `max` bound. Part of the domain identity. Defaults to `{}`
76
+ * (declares no emits). Gate-side enforcement (emitted ⊆ declared) is a later step.
77
+ */
78
+ readonly declaredEmits: Record<string, { max?: number }>;
79
+ /**
80
+ * The directive's DECLARED CertifiedReads (survival Constraint 5; `certified_read.md`
81
+ * §"Tenant authoring"): the profiled, named write-path reads its `decide()` consults — the
82
+ * `reads:{ name: q.someProfiledRead(args) }` shape, keyed by the LOCAL binding name. Each
83
+ * value is a profile-conforming {@link CertifiedReadDecl} (compiled through SQL-profile-v0 at
84
+ * `certifiedRead(...).sql(...)`, so an under-specified read is refused at COMPILE). The
85
+ * declared `query_id`s LAND IN THE CERTIFIED MANIFEST (`manifest.ts`), so the ONE write-path
86
+ * gate (`executor::admit` step 5.6) DERIVES the expected declared-read set and REFUSES any
87
+ * admit where a declared read produced no witness (the vacuous-pass close). Defaults to `{}`
88
+ * (declares no write-path reads) — OMITTED ENTIRELY from the manifest when empty, so a
89
+ * read-free directive's canonical manifest is byte-identical to before this existed.
90
+ */
91
+ readonly declaredCertifiedReads: Record<string, CertifiedReadDecl>;
92
+ /**
93
+ * The directive's DECLARED cross-workspace relations (`cross_workspace.md` §2): the relations
94
+ * a cross-workspace PR proposing THIS directive is adjudicated against. The TARGET domain
95
+ * declares what it WILL ACCEPT — the source/target endpoints, the bounded evidence read it
96
+ * discloses, and that an invariant guards it. Each lands in the manifest IR keyed by its
97
+ * `proposes` directive, which the gate (`executor::admit` step 5.5) resolves to verify the
98
+ * carried evidence + evaluate the invariant against the target's OWN law + state. Defaults to
99
+ * `[]` (declares no relation) — OMITTED ENTIRELY from the manifest when empty, so a
100
+ * relation-free directive's canonical manifest is byte-identical to before this existed.
101
+ */
102
+ readonly declaredRelations: RelationDecl[];
103
+ /**
104
+ * The directive's DECLARED required HOST CAPABILITIES — the capability PORTS the HOST must
105
+ * PROVIDE for this directive to run (`TARGET_deps.dot` cluster_ports / `TARGET_flow.dot`
106
+ * cluster_ports, invariant 3). This is the HOST/PORT axis — "can this host PHYSICALLY do
107
+ * this?" — checked ONCE at policy LOAD against what the loading host ADVERTISES, FAIL-CLOSED
108
+ * (`required ⊆ provided`). It is DISTINCT from the AUTHZ axis {@link requiresCapability}
109
+ * ("may this ACTOR?", checked per-actor at admit): the two never conflate — a directive may
110
+ * require a host port AND an actor capability, and they are checked at different doors against
111
+ * different facts. The port id space is an OPEN SET (the framework fixes no list, e.g.
112
+ * `engine-verdict` / `blob-store` / `transcription`). Deduped + SORTED (the SET is the
113
+ * contract; order is incidental). Defaults to `[]` (declares no host requirement) — OMITTED
114
+ * ENTIRELY from the manifest when empty, so a host-capability-free directive's canonical
115
+ * manifest is byte-identical to before this existed (the pinned #136 hashes stay UNCHANGED).
116
+ */
117
+ readonly declaredHostCapabilities: string[];
118
+ }
119
+
120
+ /**
121
+ * The finished directive plus the chainable `.requires(...)` declaration. Domain
122
+ * devs DECLARE their authz pre-condition here rather than re-implementing it inside
123
+ * each `plan` — the kernel resolves it WITHIN the one shared `admit` gate.
124
+ */
125
+ export interface RequirableDirective<P> extends Directive<P> {
126
+ /**
127
+ * Declare the authz pre-condition: the `capability` the actor must hold (defaults
128
+ * to the directive id) at the scope `scopeFrom(payload)` derives (defaults to root).
129
+ * Returns a new `Directive` carrying `requiresCapability` + `scopeFrom`.
130
+ */
131
+ requires(capability?: string, scopeFrom?: ScopeFrom<P>): RequirableDirective<P>;
132
+ /**
133
+ * Declare the directive's READ boundary: the ref types its `plan` may read. Callable
134
+ * MULTIPLE times to accumulate; results are deduped + sorted on the directive's
135
+ * `declaredReads`. Purely additive — a directive that never calls this declares no
136
+ * reads (`[]`) and is byte-identical in the canonical manifest to before this existed.
137
+ * Returns a NEW requirable directive (non-destructive).
138
+ */
139
+ reads(...refTypes: string[]): RequirableDirective<P>;
140
+ /**
141
+ * Declare ONE entry of the directive's EMIT boundary: an `eventType` its `plan` may
142
+ * emit, with an optional `max` count bound. Callable MULTIPLE times to accumulate
143
+ * distinct event types on `declaredEmits` (a repeated event type's later opts win).
144
+ * Purely additive — a directive that never calls this declares no emits (`{}`).
145
+ * Returns a NEW requirable directive (non-destructive).
146
+ */
147
+ emits(eventType: string, opts?: { max?: number }): RequirableDirective<P>;
148
+ /**
149
+ * Declare the directive's human-readable DESCRIPTION (the hover-doc text codegen
150
+ * emits onto the generated payload class). Purely additive + non-destructive —
151
+ * returns a NEW requirable directive carrying `description`. A directive that never
152
+ * calls this declares no prose (codegen falls back to a default derived from the id).
153
+ */
154
+ doc(description: string): RequirableDirective<P>;
155
+ /**
156
+ * Declare the directive's CertifiedReads (survival Constraint 5; `certified_read.md`
157
+ * §"Tenant authoring") — the profiled, named write-path reads its `decide()` consults, the
158
+ * `reads:{ name: q.someProfiledRead(args) }` shape. Each value is a {@link CertifiedReadDecl}
159
+ * (built via `certifiedRead("id").sql("…", {multiRow,uniqueTieBreakers})`, which refuses an
160
+ * under-specified read at COMPILE through SQL-profile-v0). The declared `query_id`s LAND IN
161
+ * THE CERTIFIED MANIFEST, so the ONE write-path gate DERIVES the required-read set and refuses
162
+ * any admit where a declared read produced no witness (the vacuous-pass close). Callable
163
+ * multiple times to accumulate (a repeated local name's later decl wins). Purely additive +
164
+ * non-destructive — a directive that never calls this declares no write-path reads (`{}`) and
165
+ * is byte-identical in the manifest to before this existed. Returns a NEW requirable directive.
166
+ */
167
+ readsCertified(reads: Record<string, CertifiedReadDecl>): RequirableDirective<P>;
168
+ /**
169
+ * Declare a cross-workspace relation (`cross_workspace.md` §2) this directive (the TARGET of a
170
+ * cross-workspace PR's `proposes`) is adjudicated against — built via
171
+ * `relation("Id").endpoints(src,tgt).proposes(directive, evidenceRead)`. The TARGET domain
172
+ * declares what it WILL ACCEPT; the gate resolves it from the certified manifest and verifies
173
+ * the carried evidence + evaluates the invariant against the target's OWN law + state
174
+ * (sovereignty, XWI4). Callable multiple times to accumulate. Purely additive + non-destructive
175
+ * — a directive that never calls this declares no relation (`[]`) and is byte-identical in the
176
+ * manifest to before this existed. Returns a NEW requirable directive.
177
+ */
178
+ declaresRelation(rel: RelationDecl): RequirableDirective<P>;
179
+ /**
180
+ * Declare ONE or more required HOST CAPABILITY ports (the HOST/PORT axis, invariant 3) — the
181
+ * capability PORTS the loading host must PROVIDE for this directive to run, checked at policy
182
+ * LOAD (`required ⊆ provided`, fail-closed), DISTINCT from the AUTHZ `.requires(...)` axis
183
+ * ("may this actor?"). The port id space is an OPEN SET (e.g. `"engine-verdict"`). Callable
184
+ * MULTIPLE times to accumulate; results are deduped + sorted on `declaredHostCapabilities`.
185
+ * Purely additive + non-destructive — a directive that never calls this declares no host
186
+ * requirement (`[]`) and is byte-identical in the canonical manifest to before this existed.
187
+ * Returns a NEW requirable directive.
188
+ */
189
+ requiresHostCapability(...portIds: string[]): RequirableDirective<P>;
190
+ }
191
+
192
+ /** Stage 1: id + a referential marker (which fixes the target aggregate). */
193
+ class DirectiveMarkerStep<Id extends string> {
194
+ constructor(private readonly id: Id) {}
195
+ creates(agg: AggregateHandle): DirectivePayloadStep<Id> {
196
+ return new DirectivePayloadStep(this.id, "creates", agg.id);
197
+ }
198
+ mutates(agg: AggregateHandle): DirectivePayloadStep<Id> {
199
+ return new DirectivePayloadStep(this.id, "mutates", agg.id);
200
+ }
201
+ ensures(agg: AggregateHandle): DirectivePayloadStep<Id> {
202
+ return new DirectivePayloadStep(this.id, "ensures", agg.id);
203
+ }
204
+ archives(agg: AggregateHandle): DirectivePayloadStep<Id> {
205
+ return new DirectivePayloadStep(this.id, "archives", agg.id);
206
+ }
207
+ }
208
+
209
+ /** Stage 2: attach a Zod payload schema (drives `plan`'s payload type). */
210
+ class DirectivePayloadStep<Id extends string> {
211
+ constructor(
212
+ private readonly id: Id,
213
+ private readonly marker: ReferentialMarker,
214
+ private readonly aggregateId: string,
215
+ ) {}
216
+ payload<S extends z.ZodTypeAny>(schema: S): DirectivePlanStep<Id, z.infer<S>> {
217
+ return new DirectivePlanStep(
218
+ this.id,
219
+ this.marker,
220
+ this.aggregateId,
221
+ schema as z.ZodType<z.infer<S>>,
222
+ );
223
+ }
224
+ }
225
+
226
+ /** Stage 3: define `plan`, producing the finished Directive. */
227
+ class DirectivePlanStep<Id extends string, P> {
228
+ constructor(
229
+ private readonly id: Id,
230
+ private readonly marker: ReferentialMarker,
231
+ private readonly aggregateId: string,
232
+ private readonly payloadSchema: z.ZodType<P>,
233
+ ) {}
234
+ plan(fn: (payload: P, ctx: Ports) => PlannedOp[]): RequirableDirective<P> {
235
+ return makeRequirable({
236
+ id: this.id,
237
+ marker: this.marker,
238
+ aggregateId: this.aggregateId,
239
+ payloadSchema: this.payloadSchema,
240
+ plan: fn,
241
+ declaredReads: [],
242
+ declaredEmits: {},
243
+ declaredCertifiedReads: {},
244
+ declaredRelations: [],
245
+ declaredHostCapabilities: [],
246
+ });
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Wrap a `Directive` with the chainable `.requires(...)` declaration. `.requires`
252
+ * is non-destructive: it returns a NEW requirable directive carrying the resolved
253
+ * `requiresCapability` (defaulting to the directive id) + `scopeFrom`, so the
254
+ * declaration site reads `directive(id)…plan(fn).requires("cap", p => […])`.
255
+ */
256
+ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
257
+ return {
258
+ ...base,
259
+ requires(capability?: string, scopeFrom?: ScopeFrom<P>): RequirableDirective<P> {
260
+ return makeRequirable({
261
+ ...base,
262
+ requiresCapability: capability ?? base.id,
263
+ ...(scopeFrom !== undefined ? { scopeFrom } : {}),
264
+ });
265
+ },
266
+ reads(...refTypes: string[]): RequirableDirective<P> {
267
+ // Accumulate onto any prior reads, dedup, sort — order is incidental to the
268
+ // declared boundary; only the SET of read ref types is the contract.
269
+ const merged = [...new Set([...base.declaredReads, ...refTypes])].sort();
270
+ return makeRequirable({ ...base, declaredReads: merged });
271
+ },
272
+ emits(eventType: string, opts?: { max?: number }): RequirableDirective<P> {
273
+ // Accumulate one event type onto any prior emits; a `max` bound is recorded only
274
+ // when supplied (so `{}` vs `{max}` is a real contract difference).
275
+ const merged: Record<string, { max?: number }> = { ...base.declaredEmits };
276
+ merged[eventType] = opts?.max !== undefined ? { max: opts.max } : {};
277
+ return makeRequirable({ ...base, declaredEmits: merged });
278
+ },
279
+ doc(description: string): RequirableDirective<P> {
280
+ return makeRequirable({ ...base, description });
281
+ },
282
+ readsCertified(reads: Record<string, CertifiedReadDecl>): RequirableDirective<P> {
283
+ // Accumulate onto any prior declared CertifiedReads (a repeated local name's later decl
284
+ // wins) — additive, non-destructive. The local binding NAME (the key) is what `decide`
285
+ // reads via `reads.<name>`; the decl's `queryId` is the manifest-landed identity.
286
+ const merged: Record<string, CertifiedReadDecl> = { ...base.declaredCertifiedReads, ...reads };
287
+ return makeRequirable({ ...base, declaredCertifiedReads: merged });
288
+ },
289
+ declaresRelation(rel: RelationDecl): RequirableDirective<P> {
290
+ // Accumulate onto any prior relations — additive, non-destructive. A relation lands in
291
+ // the manifest keyed by its `proposes` directive (cross_workspace.md §3.2.1).
292
+ return makeRequirable({ ...base, declaredRelations: [...base.declaredRelations, rel] });
293
+ },
294
+ requiresHostCapability(...portIds: string[]): RequirableDirective<P> {
295
+ // Accumulate onto any prior host-capability ports, dedup, sort — order is incidental to
296
+ // the HOST/PORT contract; only the SET of required port ids matters. DISTINCT symbol from
297
+ // the AUTHZ `requiresCapability` (the two axes never conflate).
298
+ const merged = [...new Set([...base.declaredHostCapabilities, ...portIds])].sort();
299
+ return makeRequirable({ ...base, declaredHostCapabilities: merged });
300
+ },
301
+ };
302
+ }
303
+
304
+ export function directive<const Id extends string>(id: Id): DirectiveMarkerStep<Id> {
305
+ return new DirectiveMarkerStep(id);
306
+ }
package/src/drivers.ts ADDED
@@ -0,0 +1,95 @@
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
+ * Merge-driver palette. A domain dev tags each field with a Driver; the kernel
10
+ * owns the actual merge algebra. EVERY driver in this palette encodes to a kernel
11
+ * merge arm today — `RemoveWins`/`Counter`/`LastPosition` each have a real
12
+ * `merge_field` arm in `nomos2/kernel/src/lib.rs` (2P-set union, PN-counter
13
+ * per-replica MAX, LWW-by-HLC position), so they encode like the rest. The
14
+ * PALETTE_ONLY mechanism + throw remain for any FUTURE driver added to this
15
+ * palette ahead of its kernel merge arm — never silently emit an unsupported wire driver.
16
+ */
17
+ import type { WireDriver } from "./wire.js";
18
+
19
+ /**
20
+ * Drivers the kernel implements today — each has a real `merge_field` arm in
21
+ * `nomos2/kernel/src/lib.rs`. `MapOf` is the composition primitive; the unit
22
+ * drivers encode to the kernel's serde unit-variant strings.
23
+ */
24
+ export type KernelDriverKind =
25
+ | "Lww"
26
+ | "AddWins"
27
+ | "MapOf"
28
+ | "Conflict"
29
+ | "RemoveWins"
30
+ | "Counter"
31
+ | "LastPosition";
32
+ /**
33
+ * Drivers in the design palette the kernel does not yet merge. Currently EMPTY —
34
+ * every palette driver encodes. Kept as a (never-)union so a future palette
35
+ * addition that outruns its kernel merge arm has a typed home + the throw below.
36
+ */
37
+ export type PaletteOnlyKind = never;
38
+
39
+ export type DriverKind = KernelDriverKind | PaletteOnlyKind;
40
+
41
+ export interface Driver {
42
+ readonly kind: DriverKind;
43
+ /** Only present for MapOf. */
44
+ readonly inner?: Driver;
45
+ }
46
+
47
+ export const Lww: Driver = { kind: "Lww" };
48
+ export const AddWins: Driver = { kind: "AddWins" };
49
+ export const Conflict: Driver = { kind: "Conflict" };
50
+ export const MapOf = (inner: Driver): Driver => ({ kind: "MapOf", inner });
51
+
52
+ // Structural CRDT drivers — each encodes to its kernel `merge_field` arm
53
+ // (`kernel/src/lib.rs`): LastPosition = LWW-by-HLC over a position value,
54
+ // RemoveWins = 2P-set union(adds)−union(removes), Counter = PN-counter
55
+ // per-replica MAX. Authorable against the full vocabulary AND encodable.
56
+ export const LastPosition: Driver = { kind: "LastPosition" };
57
+ export const RemoveWins: Driver = { kind: "RemoveWins" };
58
+ export const Counter: Driver = { kind: "Counter" };
59
+
60
+ // Drivers in the palette the kernel does NOT yet merge — currently NONE. A future
61
+ // palette driver added ahead of its `merge_field` arm goes here; `encodeDriver`
62
+ // throws for it rather than emit a wire driver the kernel can't merge.
63
+ const PALETTE_ONLY: ReadonlySet<DriverKind> = new Set<DriverKind>([]);
64
+
65
+ /** Encode one Driver to its kernel wire shape, or throw if not yet in the kernel. */
66
+ export function encodeDriver(d: Driver): WireDriver {
67
+ switch (d.kind) {
68
+ case "Lww":
69
+ return "Lww";
70
+ case "AddWins":
71
+ return "AddWins";
72
+ case "Conflict":
73
+ return "Conflict";
74
+ // ── structural CRDT drivers (kernel `merge_field` arms) ──
75
+ case "RemoveWins":
76
+ return "RemoveWins";
77
+ case "Counter":
78
+ return "Counter";
79
+ case "LastPosition":
80
+ return "LastPosition";
81
+ case "MapOf":
82
+ if (!d.inner) throw new Error("MapOf driver is missing its inner driver");
83
+ return { MapOf: encodeDriver(d.inner) };
84
+ default:
85
+ if (PALETTE_ONLY.has(d.kind)) {
86
+ throw new Error(
87
+ `Driver '${d.kind}' is in the Nomos palette but not yet in the kernel — ` +
88
+ `cannot encode. Implement its merge_field arm in nomos2/kernel first, ` +
89
+ `or pick one of: Lww, AddWins, MapOf, Conflict, RemoveWins, Counter, ` +
90
+ `LastPosition.`,
91
+ );
92
+ }
93
+ throw new Error(`Unknown driver kind '${(d as Driver).kind}'`);
94
+ }
95
+ }
@@ -0,0 +1,123 @@
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 PURE runtime enforcement primitive for the declared EMIT boundary (#138).
10
+ *
11
+ * `directive.ts` lets a directive DECLARE its emit boundary — `emits(eventType,
12
+ * {max?})` — recording it on `declaredEmits` and (via `manifest.ts` / `usd.ts`)
13
+ * folding it into the domain identity. That declaration is, today, DECORATIVE: the
14
+ * comment in `directive.ts` says "Gate-side enforcement (emitted ⊆ declared) is a
15
+ * later step." This module supplies the missing enforcement PRIMITIVE — the pure
16
+ * function the gate / `executeDirectiveToIntent` will call once the (gated) flip is enabled.
17
+ *
18
+ * It is PURE: no clock, no IO, no global state. Given a directive's declared emit
19
+ * set and the event types its `plan` actually emitted, it either returns (the
20
+ * emission is within the boundary) or throws a typed {@link EmitBoundaryError}. It
21
+ * does NOT invent an event registry, does NOT touch the wire/kernel shapes, and is
22
+ * NOT wired into any live dispatch path here — wiring it in is the later, Jack-gated
23
+ * step (it re-bakes the pinned `r1_nomos.wasm` or plumbs the declared set through the
24
+ * engine host; see the increment's report).
25
+ *
26
+ * The boundary has two clauses, BOTH fail-closed:
27
+ * - UNDECLARED: an emitted event type NOT present in `declared` is refused — the
28
+ * plan emitted something its contract never promised (emitted ⊆ declared).
29
+ * - OVER-MAX: an emitted type present in `declared` with a `max` bound, emitted MORE
30
+ * than `max` times, is refused. A declared type with NO `max` is unbounded in count
31
+ * (it is still bounded in KIND by the undeclared clause).
32
+ */
33
+
34
+ /** One declared emit clause: an optional `max` count bound (absent ⇒ unbounded count). */
35
+ export type DeclaredEmit = { max?: number };
36
+
37
+ /** A directive's declared emit boundary: event type → its clause. */
38
+ export type DeclaredEmits = Record<string, DeclaredEmit>;
39
+
40
+ /** Which boundary clause an emission violated. */
41
+ export type EmitBoundaryRule =
42
+ /** An emitted event type is NOT in the declared set (emitted ⊄ declared). */
43
+ | "undeclared"
44
+ /** A declared type's emitted count exceeded its declared `max`. */
45
+ | "over-max";
46
+
47
+ /** A typed, fail-closed emit-boundary violation. Pure — carries only the offending
48
+ * event type + (for `over-max`) the counts, never IO state. */
49
+ export class EmitBoundaryError extends Error {
50
+ readonly rule: EmitBoundaryRule;
51
+ /** The offending event type. */
52
+ readonly eventType: string;
53
+ /** The number of times `eventType` was emitted. */
54
+ readonly emitted: number;
55
+ /** The declared `max` for `eventType` — only meaningful for `over-max`. */
56
+ readonly max?: number;
57
+ constructor(rule: EmitBoundaryRule, eventType: string, emitted: number, max?: number) {
58
+ super(
59
+ rule === "undeclared"
60
+ ? `emit boundary: event type "${eventType}" was emitted but is not declared`
61
+ : `emit boundary: event type "${eventType}" emitted ${emitted} times exceeds ` +
62
+ `declared max ${max}`,
63
+ );
64
+ this.name = "EmitBoundaryError";
65
+ this.rule = rule;
66
+ this.eventType = eventType;
67
+ this.emitted = emitted;
68
+ if (max !== undefined) this.max = max;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Assert an emission is WITHIN a directive's declared emit boundary (the primitive
74
+ * the gate / `executeDirectiveToIntent` will call once the flip is live). PURE: no IO, no clock,
75
+ * no global state — a function of `(declared, emitted)` alone.
76
+ *
77
+ * - Throws `EmitBoundaryError("undeclared", t)` for the FIRST emitted type `t` not
78
+ * present in `declared` (emitted ⊆ declared).
79
+ * - Throws `EmitBoundaryError("over-max", t, count, max)` for the FIRST declared type
80
+ * `t` whose emitted COUNT exceeds its declared `max`. A clause with no `max` is
81
+ * count-unbounded.
82
+ * - Returns (void) when every emitted type is declared and within its `max`. Empty
83
+ * `emitted` is always within any `declared` (including empty).
84
+ *
85
+ * The undeclared check runs FIRST (in `emitted` order): an undeclared type is the
86
+ * stronger violation (the plan emitted a KIND it never promised), so it is reported
87
+ * before any over-count of a declared type.
88
+ */
89
+ export function assertEmitsWithinDeclared(
90
+ declared: DeclaredEmits,
91
+ emitted: string[],
92
+ ): void {
93
+ // Count each emitted type once, in first-seen order, so reporting is deterministic.
94
+ const counts = new Map<string, number>();
95
+ const order: string[] = [];
96
+ for (const eventType of emitted) {
97
+ const prior = counts.get(eventType);
98
+ if (prior === undefined) {
99
+ counts.set(eventType, 1);
100
+ order.push(eventType);
101
+ } else {
102
+ counts.set(eventType, prior + 1);
103
+ }
104
+ }
105
+
106
+ // Clause 1 (stronger): every emitted KIND must be declared.
107
+ for (const eventType of order) {
108
+ if (!Object.prototype.hasOwnProperty.call(declared, eventType)) {
109
+ throw new EmitBoundaryError("undeclared", eventType, counts.get(eventType)!);
110
+ }
111
+ }
112
+
113
+ // Clause 2: a declared type with a `max` must not be emitted more than `max` times.
114
+ for (const eventType of order) {
115
+ const max = declared[eventType]!.max;
116
+ if (max !== undefined) {
117
+ const count = counts.get(eventType)!;
118
+ if (count > max) {
119
+ throw new EmitBoundaryError("over-max", eventType, count, max);
120
+ }
121
+ }
122
+ }
123
+ }