@voyantjs/workflows 0.28.3 → 0.29.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 (59) hide show
  1. package/dist/driver.d.ts +237 -0
  2. package/dist/driver.d.ts.map +1 -0
  3. package/dist/driver.js +53 -0
  4. package/dist/events/compile.d.ts +34 -0
  5. package/dist/events/compile.d.ts.map +1 -0
  6. package/dist/events/compile.js +204 -0
  7. package/dist/events/index.d.ts +8 -0
  8. package/dist/events/index.d.ts.map +1 -0
  9. package/dist/events/index.js +11 -0
  10. package/dist/events/input-mapper.d.ts +24 -0
  11. package/dist/events/input-mapper.d.ts.map +1 -0
  12. package/dist/events/input-mapper.js +169 -0
  13. package/dist/events/manifest-builder.d.ts +32 -0
  14. package/dist/events/manifest-builder.d.ts.map +1 -0
  15. package/dist/events/manifest-builder.js +66 -0
  16. package/dist/events/payload-hash.d.ts +46 -0
  17. package/dist/events/payload-hash.d.ts.map +1 -0
  18. package/dist/events/payload-hash.js +98 -0
  19. package/dist/events/predicate.d.ts +77 -0
  20. package/dist/events/predicate.d.ts.map +1 -0
  21. package/dist/events/predicate.js +347 -0
  22. package/dist/events/registry.d.ts +37 -0
  23. package/dist/events/registry.d.ts.map +1 -0
  24. package/dist/events/registry.js +47 -0
  25. package/dist/handler/index.d.ts +8 -0
  26. package/dist/handler/index.d.ts.map +1 -1
  27. package/dist/handler/index.js +1 -0
  28. package/dist/http-ingest.d.ts +54 -0
  29. package/dist/http-ingest.d.ts.map +1 -0
  30. package/dist/http-ingest.js +214 -0
  31. package/dist/protocol/index.d.ts +17 -2
  32. package/dist/protocol/index.d.ts.map +1 -1
  33. package/dist/runtime/ctx.d.ts +9 -0
  34. package/dist/runtime/ctx.d.ts.map +1 -1
  35. package/dist/runtime/ctx.js +17 -0
  36. package/dist/runtime/executor.d.ts +7 -0
  37. package/dist/runtime/executor.d.ts.map +1 -1
  38. package/dist/runtime/executor.js +1 -0
  39. package/dist/trigger.d.ts +28 -14
  40. package/dist/trigger.d.ts.map +1 -1
  41. package/dist/trigger.js +4 -4
  42. package/dist/workflow.d.ts +10 -0
  43. package/dist/workflow.d.ts.map +1 -1
  44. package/package.json +14 -2
  45. package/src/driver.ts +277 -0
  46. package/src/events/compile.ts +268 -0
  47. package/src/events/index.ts +42 -0
  48. package/src/events/input-mapper.ts +201 -0
  49. package/src/events/manifest-builder.ts +97 -0
  50. package/src/events/payload-hash.ts +110 -0
  51. package/src/events/predicate.ts +390 -0
  52. package/src/events/registry.ts +88 -0
  53. package/src/handler/index.ts +9 -0
  54. package/src/http-ingest.ts +299 -0
  55. package/src/protocol/index.ts +17 -2
  56. package/src/runtime/ctx.ts +29 -0
  57. package/src/runtime/executor.ts +8 -0
  58. package/src/trigger.ts +31 -15
  59. package/src/workflow.ts +11 -0
package/src/driver.ts ADDED
@@ -0,0 +1,277 @@
1
+ // The WorkflowDriver contract.
2
+ //
3
+ // A driver is the runtime-side object that backs `createApp({ workflows })`:
4
+ // it owns manifest registration, run triggering, event ingest, and (optionally)
5
+ // admin reads. Concrete drivers live in downstream packages
6
+ // (`@voyantjs/workflows-orchestrator` for InMemory, `-node` for Mode 2 / Postgres,
7
+ // `-cloudflare` for Mode 1 / DO+KV).
8
+ //
9
+ // Drivers are constructed via *factories* — `DriverFactory` is a function the
10
+ // framework invokes after `createApp()` has assembled its `ModuleContainer`.
11
+ // This lets concrete factories accept their environment-specific options
12
+ // (DO namespaces, DB pool, etc.) at user-call time and receive framework
13
+ // deps (services, logger, …) at boot time, without a setter API.
14
+ //
15
+ // Authoritative architecture: docs/architecture/workflows-runtime-architecture.md §6.
16
+
17
+ import type { WorkflowManifest } from "./protocol/index.js"
18
+ import type { ListRunsOptions, Run, RunDetail, RunSummary, TriggerOptions } from "./trigger.js"
19
+ import type { EnvironmentName, WaitpointKind } from "./types.js"
20
+ import type { WorkflowDefinition } from "./workflow.js"
21
+
22
+ // ---- Structural deps (kept local to avoid an @voyantjs/core dep here) ----
23
+
24
+ /**
25
+ * Read-only view of a service container. Step bodies resolve services via
26
+ * `ctx.services.resolve(...)`. The framework's `ModuleContainer`
27
+ * (in `@voyantjs/core`) satisfies this shape structurally; we don't import
28
+ * it directly to keep `@voyantjs/workflows` a leaf package.
29
+ */
30
+ export interface ServiceResolver {
31
+ resolve<T>(name: string): T
32
+ has(name: string): boolean
33
+ }
34
+
35
+ /**
36
+ * Structural shape of an event envelope as ingested by a driver. Matches
37
+ * `EventEnvelope` from `@voyantjs/core` (`name`, `data`, `metadata?`, `emittedAt`),
38
+ * declared structurally so the SDK package doesn't import core.
39
+ */
40
+ export interface IngestEventEnvelope<TData = unknown> {
41
+ /** Event name in `<resource>.<pastTenseAction>` form. */
42
+ name: string
43
+ /** Business payload. */
44
+ data: TData
45
+ /** Optional metadata. `metadata.eventId` is the canonical idempotency seed
46
+ * (a fresh ULID, stamped by the framework's EventBus forwarder). External
47
+ * callers may supply their own. See architecture doc §15.2. */
48
+ metadata?: Record<string, unknown> & { eventId?: string }
49
+ /** ISO timestamp string. */
50
+ emittedAt: string
51
+ }
52
+
53
+ /**
54
+ * Minimal logger contract drivers can rely on. Matches the framework logger's
55
+ * call signature without taking a hard dependency on it.
56
+ */
57
+ export type DriverLogger = (
58
+ level: "debug" | "info" | "warn" | "error",
59
+ msg: string,
60
+ data?: object,
61
+ ) => void
62
+
63
+ // ---- Driver-factory wiring ----
64
+
65
+ /**
66
+ * Deps the framework injects into a `DriverFactory` at boot. Driver factories
67
+ * close over their environment-specific options (DO bindings, DB pool, etc.)
68
+ * and read framework deps from this argument.
69
+ */
70
+ export interface DriverFactoryDeps {
71
+ /** Read-only view of the framework's `ModuleContainer`. */
72
+ services: ServiceResolver
73
+ /** Framework logger. */
74
+ logger: DriverLogger
75
+ /** Injectable clock — defaults to `() => Date.now()`. */
76
+ now?: () => number
77
+ }
78
+
79
+ /**
80
+ * Concrete driver factories return this. `createApp()` calls it once, after
81
+ * the container is built, to obtain the `WorkflowDriver`.
82
+ */
83
+ export type DriverFactory = (deps: DriverFactoryDeps) => WorkflowDriver
84
+
85
+ // ---- Event ingest types ----
86
+
87
+ /**
88
+ * Argument to `driver.ingestEvent(...)`. The framework's EventBus forwarder
89
+ * builds this from the core `EventEnvelope`; external HTTP callers (the
90
+ * optional ingest adapter, voyant-cloud) build it from a wire payload.
91
+ */
92
+ export interface IngestEventArgs {
93
+ environment: EnvironmentName
94
+ envelope: IngestEventEnvelope
95
+ /** Optional caller-supplied idempotency override. When absent, the driver
96
+ * derives a key per match from `metadata.eventId` (or a content hash
97
+ * fallback). See architecture doc §15.2. */
98
+ idempotencyKey?: string
99
+ }
100
+
101
+ /** Per-filter outcome from a single `ingestEvent` call. */
102
+ export type IngestMatch =
103
+ | {
104
+ filterId: string
105
+ targetWorkflowId: string
106
+ runId: string
107
+ idempotencyKey: string
108
+ status: "queued"
109
+ }
110
+ | {
111
+ filterId: string
112
+ status: "skipped"
113
+ reason: "where_eval_error" | "input_projection_error" | "input_schema_violation"
114
+ details?: string
115
+ }
116
+ | {
117
+ filterId: string
118
+ targetWorkflowId: string
119
+ status: "error"
120
+ reason: string
121
+ }
122
+
123
+ export type IngestEventResponse =
124
+ | {
125
+ ok: true
126
+ eventId: string
127
+ matches: IngestMatch[]
128
+ }
129
+ | {
130
+ ok: false
131
+ reason:
132
+ | "manifest_not_registered"
133
+ | "environment_mismatch"
134
+ | "payload_too_large"
135
+ | "trigger_failed_for_all_matches"
136
+ message?: string
137
+ }
138
+
139
+ // ---- Driver — execution contract ----
140
+
141
+ /**
142
+ * The mandatory driver contract. Every concrete driver — InMemory, Mode 2
143
+ * (Postgres), Mode 1 (CF edge) — implements all five methods. The compliance
144
+ * test suite (`driver-compliance.test.ts`) is the contract.
145
+ */
146
+ export interface WorkflowDriver {
147
+ /**
148
+ * Idempotent. Same manifest body returns the same `versionId` across calls.
149
+ * Failures are NOT swallowed — `createApp()`'s bootstrap surfaces rejections
150
+ * (see architecture doc §21.22).
151
+ */
152
+ registerManifest(args: {
153
+ environment: EnvironmentName
154
+ manifest: WorkflowManifest
155
+ }): Promise<{ versionId: string }>
156
+
157
+ /**
158
+ * Trigger a workflow run by id or definition handle. Honors
159
+ * `opts.idempotencyKey` for dedup (returns the existing run on conflict).
160
+ */
161
+ trigger<TIn, TOut>(
162
+ workflow: WorkflowDefinition<TIn, TOut> | string,
163
+ input: TIn,
164
+ opts?: TriggerOptions,
165
+ ): Promise<Run<TOut>>
166
+
167
+ /**
168
+ * Ingest one event. Synchronously (from the caller's POV) loads the
169
+ * registered manifest, evaluates `where`, projects `input`, and triggers
170
+ * one run per match. See architecture doc §15.
171
+ */
172
+ ingestEvent(args: IngestEventArgs): Promise<IngestEventResponse>
173
+
174
+ /**
175
+ * Read the registered manifest. Used at boot for version-mismatch detection
176
+ * and by the dashboard for filter inspection.
177
+ */
178
+ getManifest(args: { environment: EnvironmentName }): Promise<WorkflowManifest | null>
179
+
180
+ /**
181
+ * Optional. Drains in-flight steps, refuses new triggers, awaits time-wheel
182
+ * exit. Default `gracefulMs` is 30_000.
183
+ */
184
+ shutdown?(opts?: { gracefulMs?: number }): Promise<void>
185
+
186
+ /**
187
+ * Optional read-side surface. Drivers that can support listings, run detail,
188
+ * and journal streams declare this; consumers (the dashboard) duck-type on
189
+ * `driver.admin`. See architecture doc §6.2.
190
+ */
191
+ admin?: WorkflowAdmin | Partial<WorkflowAdmin>
192
+ }
193
+
194
+ // ---- Driver — admin (optional) ----
195
+
196
+ /**
197
+ * Read-side operations. Implemented natively by the Mode 2 driver against
198
+ * Postgres, partially by Mode 1 (single-run reads only — `listRuns` is not
199
+ * implemented in self-host Mode 1; voyant-cloud provides an index layer).
200
+ * See architecture doc §6.2 + §8.3.
201
+ */
202
+ export interface WorkflowAdmin {
203
+ listRuns(opts?: ListRunsOptions): Promise<{ runs: RunSummary[]; nextCursor?: string }>
204
+ getRun(runId: string): Promise<RunDetail | null>
205
+ cancelRun(runId: string, opts?: { reason?: string; compensate?: boolean }): Promise<void>
206
+ /**
207
+ * Subscribe to journal events for a run. The async-iterable shape lets
208
+ * dashboards stream live without polling.
209
+ */
210
+ streamRun(runId: string): AsyncIterable<AdminStreamEvent>
211
+ }
212
+
213
+ /**
214
+ * The admin streaming surface re-uses the protocol's `StreamEvent` for
215
+ * step/waitpoint/log events; this type is its alias to keep the import
216
+ * surface narrow.
217
+ */
218
+ export type AdminStreamEvent =
219
+ | { kind: "run.snapshot"; at: number; status: string; metadata: Record<string, unknown> }
220
+ | { kind: "step.started"; at: number; stepId: string; runtime: "edge" | "node" }
221
+ | { kind: "step.ok"; at: number; stepId: string; durationMs: number }
222
+ | { kind: "step.err"; at: number; stepId: string; error: { code: string; message: string } }
223
+ | { kind: "waitpoint.registered"; at: number; waitpointId: string; waitpointKind: WaitpointKind }
224
+ | { kind: "waitpoint.resolved"; at: number; waitpointId: string }
225
+ | { kind: "log"; at: number; level: "info" | "warn" | "error"; message: string }
226
+ | { kind: "run.finished"; at: number; status: string }
227
+
228
+ // ---- Errors ----
229
+
230
+ /**
231
+ * Base class for typed driver errors. Concrete subclasses below; consumers
232
+ * `instanceof` to handle specific cases.
233
+ */
234
+ export class WorkflowDriverError extends Error {
235
+ readonly code: string
236
+ readonly cause?: unknown
237
+
238
+ constructor(code: string, message: string, opts?: { cause?: unknown }) {
239
+ super(message)
240
+ this.name = "WorkflowDriverError"
241
+ this.code = code
242
+ this.cause = opts?.cause
243
+ }
244
+ }
245
+
246
+ export class ManifestNotRegisteredError extends WorkflowDriverError {
247
+ constructor(environment: EnvironmentName) {
248
+ super(
249
+ "manifest_not_registered",
250
+ `No manifest is registered for environment "${environment}". ` +
251
+ `createApp() must complete its bootstrap before driver.ingestEvent(...) can fire.`,
252
+ )
253
+ this.name = "ManifestNotRegisteredError"
254
+ }
255
+ }
256
+
257
+ export class EventTooLargeError extends WorkflowDriverError {
258
+ readonly bytes: number
259
+ readonly limit: number
260
+
261
+ constructor(bytes: number, limit = 256 * 1024) {
262
+ super("payload_too_large", `Event payload is ${bytes} bytes, exceeding the ${limit}-byte cap.`)
263
+ this.name = "EventTooLargeError"
264
+ this.bytes = bytes
265
+ this.limit = limit
266
+ }
267
+ }
268
+
269
+ export class EnvironmentMismatchError extends WorkflowDriverError {
270
+ constructor(eventEnv: EnvironmentName, manifestEnv: EnvironmentName) {
271
+ super(
272
+ "environment_mismatch",
273
+ `Event environment "${eventEnv}" does not match the registered manifest's environment "${manifestEnv}".`,
274
+ )
275
+ this.name = "EnvironmentMismatchError"
276
+ }
277
+ }
@@ -0,0 +1,268 @@
1
+ // Compile an `EventFilterDeclaration` (authored in user code) into an
2
+ // `EventFilterRuntimeEntry`. Validates the predicate + mapper at
3
+ // registration time so authoring errors fail fast; computes the
4
+ // content-derived `id` / `payloadHash` so the manifest is deterministic.
5
+ //
6
+ // Called from `trigger.on(...)` in `../trigger.js`.
7
+
8
+ import type { EventFilterManifestEntry } from "../protocol/index.js"
9
+ import type { EventFilterDeclaration } from "../trigger.js"
10
+ import { type InputMapper, validateInputMapper } from "./input-mapper.js"
11
+ import { canonicalJson, sha256 } from "./payload-hash.js"
12
+ import { type PredicateExpr, validatePredicate } from "./predicate.js"
13
+ import { type EventFilterRuntimeEntry, getEventFilterRegistry } from "./registry.js"
14
+
15
+ export class EventFilterCompileError extends Error {
16
+ readonly errors: string[]
17
+
18
+ constructor(message: string, errors: string[] = []) {
19
+ super(message)
20
+ this.name = "EventFilterCompileError"
21
+ this.errors = errors
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Compile an authored declaration into a runtime entry. Throws
27
+ * `EventFilterCompileError` on shape errors so authoring problems fail
28
+ * at module-load time.
29
+ */
30
+ export async function compileEventFilter<T>(
31
+ eventType: string,
32
+ declaration: EventFilterDeclaration<T>,
33
+ ): Promise<EventFilterRuntimeEntry> {
34
+ if (typeof eventType !== "string" || eventType.length === 0) {
35
+ throw new EventFilterCompileError(
36
+ `trigger.on(eventType, ...): eventType must be a non-empty string`,
37
+ )
38
+ }
39
+ if (typeof declaration !== "object" || declaration === null) {
40
+ throw new EventFilterCompileError(
41
+ `trigger.on("${eventType}", ...): declaration must be an object`,
42
+ )
43
+ }
44
+ if (!declaration.target || typeof declaration.target !== "object") {
45
+ throw new EventFilterCompileError(
46
+ `trigger.on("${eventType}", ...): "target" must be a workflow definition (got ${typeof declaration.target})`,
47
+ )
48
+ }
49
+ const targetWorkflowId =
50
+ typeof (declaration.target as { id?: unknown }).id === "string"
51
+ ? (declaration.target as { id: string }).id
52
+ : ""
53
+ if (targetWorkflowId.length === 0) {
54
+ throw new EventFilterCompileError(
55
+ `trigger.on("${eventType}", ...): "target.id" must be a non-empty string`,
56
+ )
57
+ }
58
+
59
+ // Validate `where` if supplied.
60
+ const where = (declaration as { where?: PredicateExpr }).where
61
+ if (where !== undefined) {
62
+ const result = validatePredicate(where)
63
+ if (!result.ok) {
64
+ throw new EventFilterCompileError(
65
+ `trigger.on("${eventType}", target=${targetWorkflowId}): invalid where clause`,
66
+ result.errors,
67
+ )
68
+ }
69
+ }
70
+
71
+ // Validate `input` if supplied.
72
+ const input = (declaration as { input?: InputMapper }).input
73
+ if (input !== undefined) {
74
+ const result = validateInputMapper(input)
75
+ if (!result.ok) {
76
+ throw new EventFilterCompileError(
77
+ `trigger.on("${eventType}", target=${targetWorkflowId}): invalid input mapper`,
78
+ result.errors,
79
+ )
80
+ }
81
+ }
82
+
83
+ // Reject the legacy `match` callback explicitly so authoring errors are obvious.
84
+ if (typeof (declaration as { match?: unknown }).match === "function") {
85
+ throw new EventFilterCompileError(
86
+ `trigger.on("${eventType}"): the "match" callback is no longer supported. ` +
87
+ `Use the structured "where" predicate instead. See architecture doc §12.`,
88
+ )
89
+ }
90
+
91
+ // Canonical content-derived id. Stable across re-deploys because the
92
+ // canonicalized JSON of the declaration is identical for byte-equivalent
93
+ // sources.
94
+ const canonicalDeclaration = {
95
+ eventType,
96
+ where: where ?? null,
97
+ input: input ?? null,
98
+ targetWorkflowId,
99
+ }
100
+ const fullHash = await sha256(canonicalDeclaration)
101
+ const payloadHash = fullHash
102
+ // Filter id seeds from the same hash but stays human-friendly in logs.
103
+ const id = `ef_${fullHash.slice(0, 16)}`
104
+
105
+ const manifest: EventFilterManifestEntry = {
106
+ id,
107
+ eventType,
108
+ payloadHash,
109
+ targetWorkflowId,
110
+ ...(where !== undefined ? { where } : {}),
111
+ ...(input !== undefined ? { input } : {}),
112
+ }
113
+
114
+ const entry: EventFilterRuntimeEntry = {
115
+ id,
116
+ eventType,
117
+ manifest,
118
+ declaration: declaration as EventFilterDeclaration<unknown>,
119
+ targetWorkflowId,
120
+ }
121
+ return entry
122
+ }
123
+
124
+ /**
125
+ * Compile + register in one step. The synchronous wrapper for `trigger.on()`
126
+ * that user code calls — validates the declaration, computes the
127
+ * content-derived id, registers the entry, and returns it.
128
+ *
129
+ * Returning the full {@link EventFilterRuntimeEntry} is what makes the
130
+ * authoring shape from the architecture doc work directly — modules can
131
+ * write `eventFilters: [trigger.on(...)]` without a registry round-trip:
132
+ * the entry already structurally satisfies `EventFilterDescriptor`
133
+ * (matching `id` + `eventType` fields) and carries the `manifest` payload
134
+ * `createApp()`'s wireWorkflowRuntime needs to register with the driver.
135
+ */
136
+ export function compileAndRegister<T>(
137
+ eventType: string,
138
+ declaration: EventFilterDeclaration<T>,
139
+ ): EventFilterRuntimeEntry {
140
+ // Run validation + compile synchronously where possible. The async
141
+ // boundary is the SHA-256 digest from Web Crypto; we bridge via a
142
+ // synchronous canonical-JSON hash so trigger.on() can stay sync.
143
+ const entry = compileEventFilterSync(eventType, declaration)
144
+ getEventFilterRegistry().add(entry)
145
+ return entry
146
+ }
147
+
148
+ /**
149
+ * Synchronous compile — uses a deterministic but non-cryptographic hash
150
+ * over the canonical JSON. Suitable for the registry id (we just need
151
+ * stable + collision-resistant-enough across realistic registration
152
+ * counts). The crypto-grade `sha256(...)` is used at *manifest build*
153
+ * time when async is fine.
154
+ */
155
+ export function compileEventFilterSync<T>(
156
+ eventType: string,
157
+ declaration: EventFilterDeclaration<T>,
158
+ ): EventFilterRuntimeEntry {
159
+ if (typeof eventType !== "string" || eventType.length === 0) {
160
+ throw new EventFilterCompileError(
161
+ `trigger.on(eventType, ...): eventType must be a non-empty string`,
162
+ )
163
+ }
164
+ if (typeof declaration !== "object" || declaration === null) {
165
+ throw new EventFilterCompileError(
166
+ `trigger.on("${eventType}", ...): declaration must be an object`,
167
+ )
168
+ }
169
+ if (!declaration.target || typeof declaration.target !== "object") {
170
+ throw new EventFilterCompileError(
171
+ `trigger.on("${eventType}", ...): "target" must be a workflow definition (got ${typeof declaration.target})`,
172
+ )
173
+ }
174
+ const targetWorkflowId =
175
+ typeof (declaration.target as { id?: unknown }).id === "string"
176
+ ? (declaration.target as { id: string }).id
177
+ : ""
178
+ if (targetWorkflowId.length === 0) {
179
+ throw new EventFilterCompileError(
180
+ `trigger.on("${eventType}", ...): "target.id" must be a non-empty string`,
181
+ )
182
+ }
183
+
184
+ const where = (declaration as { where?: PredicateExpr }).where
185
+ if (where !== undefined) {
186
+ const result = validatePredicate(where)
187
+ if (!result.ok) {
188
+ throw new EventFilterCompileError(
189
+ `trigger.on("${eventType}", target=${targetWorkflowId}): invalid where clause`,
190
+ result.errors,
191
+ )
192
+ }
193
+ }
194
+
195
+ const input = (declaration as { input?: InputMapper }).input
196
+ if (input !== undefined) {
197
+ const result = validateInputMapper(input)
198
+ if (!result.ok) {
199
+ throw new EventFilterCompileError(
200
+ `trigger.on("${eventType}", target=${targetWorkflowId}): invalid input mapper`,
201
+ result.errors,
202
+ )
203
+ }
204
+ }
205
+
206
+ if (typeof (declaration as { match?: unknown }).match === "function") {
207
+ throw new EventFilterCompileError(
208
+ `trigger.on("${eventType}"): the "match" callback is no longer supported. ` +
209
+ `Use the structured "where" predicate instead. See architecture doc §12.`,
210
+ )
211
+ }
212
+
213
+ const canonicalDeclaration = {
214
+ eventType,
215
+ where: where ?? null,
216
+ input: input ?? null,
217
+ targetWorkflowId,
218
+ }
219
+ const json = canonicalJson(canonicalDeclaration)
220
+ const shortId = nonCryptoHash16(json)
221
+ const id = `ef_${shortId}`
222
+
223
+ const manifest: EventFilterManifestEntry = {
224
+ id,
225
+ eventType,
226
+ // payloadHash on the manifest is stable (same canonical JSON) but
227
+ // upgraded to the crypto-grade sha256 at manifest-build time. Until
228
+ // then it carries the same short hash so consumers that read it pre-
229
+ // build (e.g. dashboards in dev mode) still see something sensible.
230
+ payloadHash: shortId,
231
+ targetWorkflowId,
232
+ ...(where !== undefined ? { where } : {}),
233
+ ...(input !== undefined ? { input } : {}),
234
+ }
235
+
236
+ return {
237
+ id,
238
+ eventType,
239
+ manifest,
240
+ declaration: declaration as EventFilterDeclaration<unknown>,
241
+ targetWorkflowId,
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Deterministic 16-hex-char hash for synchronous registration. Uses FNV-1a
247
+ * 64-bit folded into hex — collision-resistant enough at the cardinality of
248
+ * "filters per project" (low thousands at most). The cryptographic SHA-256
249
+ * is the source of truth for manifest-level identity; this is just enough
250
+ * to be a stable id for the in-process registry.
251
+ */
252
+ function nonCryptoHash16(text: string): string {
253
+ // FNV-1a 64-bit, computed as two 32-bit halves to avoid bigint overhead
254
+ // in tight loops. Output: 16 hex chars (concatenation of the two halves).
255
+ let h1 = 0xcbf29ce4
256
+ let h2 = 0x84222325
257
+ for (let i = 0; i < text.length; i++) {
258
+ const c = text.charCodeAt(i)
259
+ h1 = (h1 ^ c) >>> 0
260
+ h2 = (h2 ^ c) >>> 0
261
+ // Multiply by 0x100000001b3 split into the two halves.
262
+ const lo = h2 * 0x01b3
263
+ const hi = h1 * 0x01b3 + Math.floor(lo / 0x100000000)
264
+ h2 = (lo & 0xffffffff) >>> 0
265
+ h1 = hi >>> 0
266
+ }
267
+ return h1.toString(16).padStart(8, "0") + h2.toString(16).padStart(8, "0")
268
+ }
@@ -0,0 +1,42 @@
1
+ // `@voyantjs/workflows/events` — the events runtime: structured `where`
2
+ // predicate + `input` mapper, event-filter registry, manifest builder.
3
+ //
4
+ // Authoritative architecture: docs/architecture/workflows-runtime-architecture.md
5
+ // §12 (trigger.on runtime), §13 (DSLs), §14 (manifest lifecycle).
6
+
7
+ export {
8
+ compileAndRegister,
9
+ compileEventFilterSync,
10
+ EventFilterCompileError,
11
+ } from "./compile.js"
12
+ export type { InputMapper } from "./input-mapper.js"
13
+ export {
14
+ type InputMapperValidationResult,
15
+ projectInput,
16
+ validateInputMapper,
17
+ } from "./input-mapper.js"
18
+ export {
19
+ type BuildManifestArgs,
20
+ buildManifest,
21
+ } from "./manifest-builder.js"
22
+ export {
23
+ canonicalize,
24
+ canonicalJson,
25
+ deriveStableEventId,
26
+ sha256,
27
+ shortHash,
28
+ } from "./payload-hash.js"
29
+ export {
30
+ evaluatePredicate,
31
+ type PathOrLit,
32
+ type PredicateEnvelope,
33
+ type PredicateExpr,
34
+ type PredicateValidationResult,
35
+ resolvePath,
36
+ validatePredicate,
37
+ } from "./predicate.js"
38
+ export {
39
+ __resetEventFilterRegistry,
40
+ type EventFilterRuntimeEntry,
41
+ getEventFilterRegistry,
42
+ } from "./registry.js"