@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/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
|
+
}
|
package/src/directive.ts
ADDED
|
@@ -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
|
+
}
|