@voltro/runtime 0.28.0 → 0.30.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/dist/index.d.ts CHANGED
@@ -56,6 +56,7 @@ import { Row } from '@voltro/database';
56
56
  import { Rpc } from '@effect/rpc';
57
57
  import { RpcGroup } from '@effect/rpc';
58
58
  import { RpcInterceptor } from '@voltro/protocol';
59
+ import { RuleReadStore } from '@voltro/database';
59
60
  import { Sampler } from '@opentelemetry/sdk-trace-base';
60
61
  import { Schedule } from 'effect';
61
62
  import { Schema } from 'effect';
@@ -677,6 +678,8 @@ export declare interface AnalyticsTopEntry {
677
678
 
678
679
  declare type AnyRow = Record<string, unknown>;
679
680
 
681
+ declare type AnyRow_2 = Record<string, unknown>;
682
+
680
683
  export declare const API_KEYS_TABLE = "_voltro_api_keys";
681
684
 
682
685
  /** Admin-gated management routes (issue / list / revoke) for the built-in keys.
@@ -1018,6 +1021,17 @@ export declare const assertCan: (subject: RebacSubject, action: string, resource
1018
1021
  */
1019
1022
  export declare const assertConnectionCipherConfigured: (definitions: ReadonlyArray<ConnectionDefinition>) => void;
1020
1023
 
1024
+ /**
1025
+ * Assign a subject to a variant — STABLE (same subject ⇒ same variant, always)
1026
+ * and BALANCED (proportions converge to the weights over many subjects). The
1027
+ * assignment is salted with the experiment `name`, so one subject buckets
1028
+ * independently across different experiments.
1029
+ *
1030
+ * A `holdout` fraction is carved off the top of the interval FIRST; the
1031
+ * remaining space is split by variant weight. Pure — a client can reproduce it.
1032
+ */
1033
+ export declare const assignVariant: (def: ExperimentDefinition, subject: string) => string;
1034
+
1021
1035
  /**
1022
1036
  * Async cache surface exposed as `ctx.cache` to non-Effect handlers — the
1023
1037
  * facade over the Effect-native `Cache` service from `@voltro/cache`
@@ -1095,6 +1109,82 @@ export declare interface AttachAnalyticsMirrorOptions {
1095
1109
  readonly run?: (effect: Effect.Effect<void, unknown>) => Promise<unknown>;
1096
1110
  }
1097
1111
 
1112
+ /**
1113
+ * Wire every discovered expectation onto the store's CDC channel + a clock tick,
1114
+ * and expose the live signal via a registry. Returns the registry impl + a
1115
+ * detach for graceful shutdown — the SAME shape as the aggregate runner.
1116
+ *
1117
+ * Both boot paths (`voltro dev`, `voltro serve`) must call this and provide the
1118
+ * resulting `ExpectationRegistry` layer, exactly like `AggregateRegistry`.
1119
+ */
1120
+ export declare const attachExpectations: (options: AttachExpectationsOptions) => ExpectationRunnerHandle;
1121
+
1122
+ export declare interface AttachExpectationsOptions {
1123
+ readonly discovered: ReadonlyArray<{
1124
+ readonly definition: ExpectationDefinition;
1125
+ }>;
1126
+ readonly store: DataStore;
1127
+ readonly log: SyncLogger;
1128
+ /** Wall clock. Default `Date.now`. */
1129
+ readonly now?: () => number;
1130
+ /** How often the time-dependent invariants (freshness) are re-checked against
1131
+ * the clock. Default 30s. Ignored when no expectation is time-dependent. */
1132
+ readonly tickIntervalMs?: number;
1133
+ /** Periodic full re-seed interval (drift healing), mirroring the aggregate
1134
+ * runner's reconcile. Default 5m. */
1135
+ readonly reconcileIntervalMs?: number;
1136
+ }
1137
+
1138
+ /**
1139
+ * Wire every discovered experiment onto the store's CDC channel and expose the
1140
+ * live results via a registry. Returns the registry impl + a detach for graceful
1141
+ * shutdown — the SAME shape as `attachExpectations` / `attachFinops`.
1142
+ *
1143
+ * Both boot paths (`voltro dev`, `voltro serve`) must call this and provide the
1144
+ * resulting `ExperimentRegistry` layer, exactly like `AggregateRegistry`
1145
+ * (deferred cli work — see the NOTE at the bottom of this file).
1146
+ */
1147
+ export declare const attachExperiments: (options: AttachExperimentsOptions) => ExperimentRunnerHandle;
1148
+
1149
+ export declare interface AttachExperimentsOptions {
1150
+ readonly discovered: ReadonlyArray<{
1151
+ readonly definition: ExperimentDefinition;
1152
+ }>;
1153
+ readonly store: DataStore;
1154
+ readonly log: SyncLogger;
1155
+ /** Wall clock. Default `Date.now`. */
1156
+ readonly now?: () => number;
1157
+ /** Periodic full re-seed interval (drift healing), mirroring the aggregate
1158
+ * runner's reconcile. Default 5m. */
1159
+ readonly reconcileIntervalMs?: number;
1160
+ }
1161
+
1162
+ /**
1163
+ * Wire every discovered cost budget into a `CostAccountant` + a clock tick, and
1164
+ * expose the live attribution + budget signal via a `CostRegistry`. Returns the
1165
+ * registry impl + the `record` ingestion entry point + a detach — the SAME shape
1166
+ * as `attachExpectations`.
1167
+ *
1168
+ * Both boot paths (`voltro dev`, `voltro serve`) must call this and provide the
1169
+ * resulting `CostRegistry` layer, exactly like `ExpectationRegistry`; the
1170
+ * `record` returned here is the seam the dispatcher's recompute + the query path
1171
+ * call to emit cost events (see the NOTE at the bottom of this file — that call-
1172
+ * site instrumentation + `*.budget.ts` discovery are the deferred cli work).
1173
+ */
1174
+ export declare const attachFinops: (options: AttachFinopsOptions) => FinopsRunnerHandle;
1175
+
1176
+ export declare interface AttachFinopsOptions {
1177
+ readonly discovered: ReadonlyArray<{
1178
+ readonly definition: CostBudgetDefinition;
1179
+ }>;
1180
+ readonly log: SyncLogger;
1181
+ /** Wall clock. Default `Date.now`. */
1182
+ readonly now?: () => number;
1183
+ /** How often windowed budgets are re-checked against the clock for a rollover
1184
+ * recovery. Default 30s. Ignored when no budget is windowed. */
1185
+ readonly tickIntervalMs?: number;
1186
+ }
1187
+
1098
1188
  /** The descriptor shape the audit needs — a `defineQuery` result carries it. */
1099
1189
  export declare interface AuditableQuery {
1100
1190
  readonly name: string;
@@ -1184,7 +1274,30 @@ export declare interface BindEventInput {
1184
1274
  readonly guards?: Guards<unknown> | undefined;
1185
1275
  }
1186
1276
 
1187
- export declare const bindMutation: <Input, Output, E = never>(execute: (input: Input, context: RuntimeContext) => Output | Promise<Output> | Effect.Effect<Output, E, never>, input: Input, spanName?: string, codec?: MutationOutputCodec<unknown>, idempotency?: IdempotencyBinding) => Effect.Effect<Output, E, SubjectService | ConnectionInfo>;
1277
+ export declare const bindMutation: <Input, Output, E = never>(execute: (input: Input, context: RuntimeContext) => Output | Promise<Output> | Effect.Effect<Output, E, never>, input: Input, spanName?: string, codec?: MutationOutputCodec<unknown>, idempotency?: IdempotencyBinding,
1278
+ /**
1279
+ * This procedure's DECLARED error union (`descriptor.error`).
1280
+ *
1281
+ * The third category of failure, and the one two guards below could not see.
1282
+ * A value that is TAGGED but not representable by this union is undeclared by
1283
+ * construction: the untagged catch skips it (it has a `_tag`), the infra list
1284
+ * skips it (it is not on a curated list), and the rpc encoder then cannot
1285
+ * match it and ships the whole `ExitEncoded<…>` decode tree to the browser —
1286
+ * ~2 KB for a one-line cause, with the message at the END so every tool that
1287
+ * truncates shows the useless half.
1288
+ *
1289
+ * A consumer met it with `TenantScopeViolation`. Adding that tag to
1290
+ * `INFRA_ERROR_TAGS` would have been wrong: `effectStore.ts` documents
1291
+ * `error: Schema.Union(TenantScopeViolation, StoreOperationFailed, …)` as a
1292
+ * supported declaration, so an app that DECLARES it must still receive it
1293
+ * typed. The question is therefore not "is this tag infra" but "can THIS
1294
+ * descriptor represent it" — which only the descriptor can answer.
1295
+ *
1296
+ * Omitted ⇒ the check is skipped entirely. A call site that cannot supply a
1297
+ * schema keeps exactly the old behaviour rather than collapsing errors it
1298
+ * cannot classify.
1299
+ */
1300
+ declaredError?: Schema.Schema.All) => Effect.Effect<Output, E, SubjectService | ConnectionInfo>;
1188
1301
 
1189
1302
  /**
1190
1303
  * Bind a non-reactive server→client stream (A3). The executor builds a
@@ -1770,6 +1883,206 @@ export declare interface Coordinator {
1770
1883
  tryClaim(scheduleName: string, scheduledAt: Date): Promise<boolean>;
1771
1884
  }
1772
1885
 
1886
+ /**
1887
+ * The standing compute-cost accountant. Fold `CostEvent`s in with `record`;
1888
+ * read attribution + budget state through the getters; roll windows with `tick`.
1889
+ * Every method is synchronous and allocation-light — it is on the reactive
1890
+ * recompute hot path.
1891
+ */
1892
+ export declare class CostAccountant {
1893
+ private readonly now;
1894
+ private readonly attribution;
1895
+ private readonly counters;
1896
+ private readonly budgetsByName;
1897
+ private readonly totalBudgets;
1898
+ private readonly unitBudgets;
1899
+ constructor(definitions: ReadonlyArray<CostBudgetDefinition>, deps?: CostAccountantDeps);
1900
+ private counterFor;
1901
+ /** Attribute one cost event and evaluate every budget that watches its unit.
1902
+ * Returns the threshold-crossing signals produced (empty in the common,
1903
+ * no-crossing case). */
1904
+ record(event: CostEvent): ReadonlyArray<CostBudgetSignal>;
1905
+ /** Roll every windowed budget whose window has elapsed, with no event —
1906
+ * a budget recovers when its window passes even if the tenant went quiet.
1907
+ * Returns the `recovered` signals produced. */
1908
+ tick(): ReadonlyArray<CostBudgetSignal>;
1909
+ /** Reset attribution + every budget counter for one tenant, or ALL tenants
1910
+ * when `tenantId` is `undefined` (the system tenant is `null`, and IS
1911
+ * targetable). Returns the `recovered` signals for any budget that was
1912
+ * breached. Use for a chargeback-period rollover the windows don't express,
1913
+ * or a test reset. */
1914
+ reset(tenantId?: string | null): ReadonlyArray<CostBudgetSignal>;
1915
+ private allCounterTenants;
1916
+ attributionSnapshot(): ReadonlyArray<TenantCostState>;
1917
+ tenantState(tenantId: string | null): TenantCostState | null;
1918
+ budgetStates(): ReadonlyArray<CostBudgetState>;
1919
+ budgetState(name: string, tenantId: string | null): CostBudgetState | null;
1920
+ breaches(): ReadonlyArray<CostBudgetState>;
1921
+ }
1922
+
1923
+ export declare interface CostAccountantDeps {
1924
+ /** Wall clock. Injectable for deterministic tests. Default `Date.now`. */
1925
+ readonly now?: () => number;
1926
+ readonly log?: SyncLogger;
1927
+ }
1928
+
1929
+ export declare interface CostBudgetDefinition {
1930
+ /** Brand marker — lets the cli discriminate default-exported budget
1931
+ * definitions from arbitrary objects during file discovery. */
1932
+ readonly _voltroCostBudget: true;
1933
+ readonly name: string;
1934
+ readonly limit: number;
1935
+ /** Resolved cost unit, or `null` for a total-across-all-units budget. */
1936
+ readonly unit: string | null;
1937
+ readonly warnAt: number;
1938
+ /** Resolved tumbling-window length in ms, or `null` for a cumulative budget. */
1939
+ readonly windowMs: number | null;
1940
+ readonly severity: CostBudgetSeverity;
1941
+ readonly description?: string;
1942
+ }
1943
+
1944
+ export declare interface CostBudgetDefinitionInput {
1945
+ /** Stable id within the app. Unique across all cost budgets. */
1946
+ readonly name: string;
1947
+ /** The per-tenant ceiling, in the budget's `unit` (or the tenant's grand
1948
+ * total when `unit` is omitted). A tenant crossing this is `exceeded`. */
1949
+ readonly limit: number;
1950
+ /** Which cost unit this budget meters. Omitted ⇒ the tenant's TOTAL across
1951
+ * every unit (the common "total compute per tenant" cap). When set, only
1952
+ * `CostEvent`s of this unit count toward the budget. */
1953
+ readonly unit?: string;
1954
+ /** Fraction of `limit` (0..1) at which the budget goes `warn` — the early
1955
+ * signal before the hard cap. Default `0.8`. Set `1` to disable the warn
1956
+ * band (straight ok→exceeded). */
1957
+ readonly warnAt?: number;
1958
+ /** Tumbling window as an interval string (`'1h'`, `'24h'`). The per-tenant
1959
+ * counter resets at each window boundary (and a breached budget RECOVERS).
1960
+ * Omitted ⇒ a cumulative budget that only resets on an explicit
1961
+ * `reset(tenantId)` — the since-boot chargeback ceiling. */
1962
+ readonly window?: string;
1963
+ /** Alerting priority carried on the breach signal. Default `'warn'`. */
1964
+ readonly severity?: CostBudgetSeverity;
1965
+ /** Human-facing note surfaced in devtools / the breach message. */
1966
+ readonly description?: string;
1967
+ }
1968
+
1969
+ /** How loud a budget breach is. A classification carried on the signal for
1970
+ * alerting priority — the framework never BLOCKS compute on a budget here
1971
+ * (that is a caller's choice, the way `requireAiBudget` fails a call); a cost
1972
+ * budget is an observability-grade signal over work that already happened. */
1973
+ export declare type CostBudgetSeverity = 'info' | 'warn' | 'critical';
1974
+
1975
+ /** Emitted when a budget crosses a threshold for a tenant. `warn` / `exceeded`
1976
+ * are upward crossings (event-driven, with a cause); `recovered` is a window
1977
+ * rollover / reset back to `ok` (no cause). */
1978
+ export declare interface CostBudgetSignal {
1979
+ readonly kind: 'warn' | 'exceeded' | 'recovered';
1980
+ readonly state: CostBudgetState;
1981
+ readonly cause: CostCause | null;
1982
+ }
1983
+
1984
+ /** The live state of one budget for one tenant — the row an inspect endpoint /
1985
+ * dashboard reads. */
1986
+ export declare interface CostBudgetState {
1987
+ readonly budget: string;
1988
+ readonly tenantId: string | null;
1989
+ readonly status: CostBudgetStatus;
1990
+ readonly severity: CostBudgetSeverity;
1991
+ /** The metered cost against this budget in the current window. */
1992
+ readonly spent: number;
1993
+ /** The hard ceiling. */
1994
+ readonly limit: number;
1995
+ /** The absolute warn ceiling (`warnAt * limit`). */
1996
+ readonly warnThreshold: number;
1997
+ /** The unit this budget meters, or `null` for a total-across-units budget. */
1998
+ readonly unit: string | null;
1999
+ /** When the current window opened (windowed budgets), else `null`. */
2000
+ readonly windowStartedAt: number | null;
2001
+ /** When this budget last entered its current `status`. */
2002
+ readonly since: number | null;
2003
+ readonly lastUpdatedAt: number | null;
2004
+ /** The most recent breach's cause, retained across recovery for the audit
2005
+ * trail. `null` if it has never been breached. */
2006
+ readonly lastCause: CostCause | null;
2007
+ readonly description?: string;
2008
+ }
2009
+
2010
+ /** A budget's status for one tenant. `ok` below the warn band, `warn` at/over
2011
+ * `warnAt * limit`, `exceeded` at/over `limit`. Monotonic within a window;
2012
+ * resets to `ok` on window rollover / explicit reset. */
2013
+ export declare type CostBudgetStatus = 'ok' | 'warn' | 'exceeded';
2014
+
2015
+ /** What incurred the cost that tipped a budget across a threshold — resolved
2016
+ * from the `CostEvent`. `null` when the transition was window-driven (a budget
2017
+ * RECOVERING because its window rolled over, with no event). */
2018
+ export declare interface CostCause {
2019
+ readonly unit: string;
2020
+ readonly subscriptionId?: string | null;
2021
+ readonly procedure?: string | null;
2022
+ readonly traceId?: string | null;
2023
+ }
2024
+
2025
+ /**
2026
+ * One unit of attributed compute. A compute site (a reactive recompute in the
2027
+ * dispatcher, a query, a fan-out delivery, or any pluggable cost source) emits
2028
+ * one of these tagged with the tenant/subscription that caused it, and the
2029
+ * accountant folds it into the standing per-tenant total AND every budget that
2030
+ * watches its `unit`.
2031
+ *
2032
+ * `amount` is a NORMALISED cost figure in whatever unit the caller chose — 1 per
2033
+ * recompute, rows scanned, milliseconds elapsed, a priced micro-USD. The
2034
+ * framework does not impose a cost model; `unit` names which one this event is
2035
+ * denominated in, and a budget either targets that named unit or the tenant's
2036
+ * grand total. This is the "pluggable cost unit" seam.
2037
+ */
2038
+ export declare interface CostEvent {
2039
+ /** Active org id the compute is attributed to. `null` = system / untenanted
2040
+ * work (a schedule, a resumed workflow — `SYSTEM_SUBJECT.tenantId`). */
2041
+ readonly tenantId: string | null;
2042
+ /** The live subscription that drove this compute, when the cost came from a
2043
+ * reactive recompute — the per-subscription attribution the plan calls for.
2044
+ * Absent for non-subscription compute (a one-shot query). */
2045
+ readonly subscriptionId?: string | null;
2046
+ /** Which cost unit `amount` is denominated in — `'recompute'`, `'query'`,
2047
+ * `'fanout'`, or any custom unit the caller meters. */
2048
+ readonly unit: string;
2049
+ /** The normalised cost contribution in `unit`. Non-finite / negative amounts
2050
+ * are ignored by the accountant (a cost cannot be negative). */
2051
+ readonly amount: number;
2052
+ /** The rpc tag of the call that incurred the cost, for the "why is my bill
2053
+ * high" breakdown + the breach cause. */
2054
+ readonly procedure?: string | null;
2055
+ /** The trace the offending compute ran under — joins to `voltro logs --trace`. */
2056
+ readonly traceId?: string | null;
2057
+ /** Wall-clock ms the cost was incurred. Default: the accountant's clock. */
2058
+ readonly at?: number;
2059
+ }
2060
+
2061
+ /**
2062
+ * Framework-provided registry of per-tenant compute-cost attribution + every
2063
+ * budget the cli discovered. Handler / inspect code reads the live signal here,
2064
+ * exactly as it reads `ExpectationRegistry`.
2065
+ */
2066
+ export declare class CostRegistry extends CostRegistry_base {
2067
+ }
2068
+
2069
+ declare const CostRegistry_base: Context.TagClass<CostRegistry, "@voltro/CostRegistry", CostRegistryApi>;
2070
+
2071
+ export declare interface CostRegistryApi {
2072
+ /** Per-tenant compute-cost attribution — the chargeback feed. */
2073
+ readonly attribution: () => ReadonlyArray<TenantCostState>;
2074
+ /** One tenant's attribution, or `null` when nothing has been attributed. */
2075
+ readonly tenant: (tenantId: string | null) => TenantCostState | null;
2076
+ /** Every (budget, tenant) state — the full budget snapshot. */
2077
+ readonly budgets: () => ReadonlyArray<CostBudgetState>;
2078
+ /** One budget's state for one tenant, or `null` when unseen. */
2079
+ readonly budget: (name: string, tenantId: string | null) => CostBudgetState | null;
2080
+ /** Only the currently warn/exceeded budget states — the alerting view. */
2081
+ readonly breaches: () => ReadonlyArray<CostBudgetState>;
2082
+ /** Subscribe to budget threshold crossings. Returns an unsubscribe fn. */
2083
+ readonly subscribe: (listener: (s: CostBudgetSignal) => void) => () => void;
2084
+ }
2085
+
1773
2086
  /** Define a cumulative counter. Increment with `incrementMetric` / `Metric.increment`. */
1774
2087
  export declare const counter: (name: string, description?: string) => Metric.Metric.Counter<number>;
1775
2088
 
@@ -2005,6 +2318,32 @@ export declare const DEFAULT_REFRESH_SKEW_MS = 60000;
2005
2318
  */
2006
2319
  export declare const DEFAULT_ROW_FILTER_RETRY: Schedule.Schedule<unknown, unknown>;
2007
2320
 
2321
+ /**
2322
+ * The message of an UNDECLARED throw, bounded, with nothing else attached.
2323
+ *
2324
+ * An executor that throws a plain `TypeError` produces a DEFECT, not a typed
2325
+ * failure — so `catchIf(isInfraError)` above never sees it (that guards the E
2326
+ * channel) and the rpc encoder tried to match it against the descriptor's
2327
+ * `error:` union. It cannot match, by definition: a defect is precisely the
2328
+ * thing that is not in the union. What reached the browser was the entire
2329
+ * decode tree — every union member, the full `ExitEncoded<…>` type, and the
2330
+ * real cause on the last line. ~2 KB of type names, which one consumer's
2331
+ * account page rendered verbatim where a reason belonged, and which every app
2332
+ * otherwise has to condense heuristically to avoid putting a schema on screen.
2333
+ *
2334
+ * `message` ONLY — no stack, no `cause` chain, no own fields. The same
2335
+ * reasoning as `wireErrorFromCause`: a nested object can carry a DSN or a
2336
+ * token. A message is what the server already logged and what a human needs;
2337
+ * the full original stays in the server log via `logHandlerFailure`.
2338
+ *
2339
+ * Note the deliberate asymmetry with `isInfraError`, which collapses to the
2340
+ * generic text instead: a `SqlError`'s message names internal `table.column`
2341
+ * detail, so its text is withheld on purpose. An arbitrary app defect has no
2342
+ * such known shape — withholding it too would leave the app exactly where it
2343
+ * started, with a reason it cannot show.
2344
+ */
2345
+ export declare const defectMessage: (defect: unknown) => string;
2346
+
2008
2347
  /**
2009
2348
  * Construct an aggregate definition. The returned object brands itself
2010
2349
  * so the cli's file-discovery pass picks it up from default exports.
@@ -2043,6 +2382,27 @@ export declare const defineAggregate: <Row>(input: AggregateDefinitionInput<Row>
2043
2382
  */
2044
2383
  export declare const defineConnection: <D extends ConnectionDefinition>(definition: D) => D;
2045
2384
 
2385
+ /**
2386
+ * Declare a per-tenant compute-cost budget. Validates the shape at declaration
2387
+ * time so a malformed budget fails LOUD at boot rather than silently never
2388
+ * firing (the same discipline as `defineExpectation`).
2389
+ *
2390
+ * ```ts
2391
+ * // apps/api/budgets/tenantCompute.budget.ts
2392
+ * import { defineCostBudget } from '@voltro/runtime'
2393
+ *
2394
+ * export default defineCostBudget({
2395
+ * name: 'tenant-recompute-hourly',
2396
+ * unit: 'recompute',
2397
+ * limit: 100_000, // 100k recomputes per tenant per hour
2398
+ * warnAt: 0.8, // warn at 80k
2399
+ * window: '1h',
2400
+ * severity: 'warn',
2401
+ * })
2402
+ * ```
2403
+ */
2404
+ export declare const defineCostBudget: (input: CostBudgetDefinitionInput) => CostBudgetDefinition;
2405
+
2046
2406
  export declare const defineEventTrigger: <EventPayload = unknown, WorkflowPayload = EventPayload>(spec: Omit<WorkflowEventTriggerDefinition<EventPayload, WorkflowPayload>, "_tag"> | (Omit<WorkflowEventTriggerDefinition<EventPayload, WorkflowPayload>, "_tag" | "event"> & {
2047
2407
  /** A declared event. Its `name` becomes the matched event. */
2048
2408
  readonly on: {
@@ -2058,6 +2418,46 @@ export declare const defineEventTrigger: <EventPayload = unknown, WorkflowPayloa
2058
2418
  */
2059
2419
  export declare const defineExecutor: <D extends ExecutorDescriptor, F extends (input: ExecutorInput<D>, ctx: AppContext) => ExecutorReturn<D, unknown, unknown>>(_descriptor: D, fn: F) => ((input: ExecutorInput<D>, ctx: AppContext) => ReturnType<F>);
2060
2420
 
2421
+ /**
2422
+ * Declare a data-quality expectation. Validates the shape at declaration time so
2423
+ * a malformed contract fails LOUD at boot rather than silently never firing.
2424
+ *
2425
+ * ```ts
2426
+ * // apps/api/expectations/orderFreshness.expectation.ts
2427
+ * import { defineExpectation } from '@voltro/runtime'
2428
+ *
2429
+ * export default defineExpectation({
2430
+ * name: 'orders-fresh',
2431
+ * on: { table: 'orders' },
2432
+ * invariant: { kind: 'freshness', column: 'createdAt', maxAgeMs: 10 * 60_000 },
2433
+ * severity: 'critical',
2434
+ * })
2435
+ * ```
2436
+ */
2437
+ export declare const defineExpectation: (input: ExpectationDefinitionInput) => ExpectationDefinition;
2438
+
2439
+ /**
2440
+ * Declare an online experiment. Validates the shape at declaration time so a
2441
+ * malformed experiment fails LOUD at boot rather than silently never producing a
2442
+ * result (the same discipline as `defineExpectation` / `defineCostBudget`).
2443
+ *
2444
+ * ```ts
2445
+ * // apps/api/experiments/checkoutColor.experiment.ts
2446
+ * import { defineExperiment } from '@voltro/runtime'
2447
+ *
2448
+ * export default defineExperiment({
2449
+ * name: 'checkout-button-color',
2450
+ * on: { table: 'orders' },
2451
+ * subject: 'userId', // stable per-user bucketing
2452
+ * variants: [{ name: 'control' }, { name: 'green' }],
2453
+ * holdout: 0.1, // 10% see nothing, for a clean baseline
2454
+ * metric: { kind: 'conversionRate', column: 'completed' },
2455
+ * baseline: 'control',
2456
+ * })
2457
+ * ```
2458
+ */
2459
+ export declare const defineExperiment: (input: ExperimentDefinitionInput) => ExperimentDefinition;
2460
+
2061
2461
  /**
2062
2462
  * Declare who delivers an effect. One per `*.outbox.ts` file.
2063
2463
  *
@@ -2129,6 +2529,21 @@ export { DialectReplicationAdapter }
2129
2529
  * delete → every field `value→undefined`; update → only the changed fields. */
2130
2530
  export declare const diffChange: (c: CdcChange) => RowDiff;
2131
2531
 
2532
+ export declare interface DiscoveredCostBudget {
2533
+ readonly file: string;
2534
+ readonly definition: CostBudgetDefinition;
2535
+ }
2536
+
2537
+ export declare interface DiscoveredExpectation {
2538
+ readonly file: string;
2539
+ readonly definition: ExpectationDefinition;
2540
+ }
2541
+
2542
+ export declare interface DiscoveredExperiment {
2543
+ readonly file: string;
2544
+ readonly definition: ExperimentDefinition;
2545
+ }
2546
+
2132
2547
  export declare interface DiscoveredWorkflowLike<Context = unknown> {
2133
2548
  readonly definition: WorkflowDefinitionLike;
2134
2549
  readonly buildExecute: (ctx: Context) => (payload: never, executionId: string) => Effect.Effect<unknown, unknown, unknown>;
@@ -2266,6 +2681,19 @@ export declare interface DispatcherDependencies {
2266
2681
  * the trace buffer so the dashboards show each data transfer's latency.
2267
2682
  * No-op when absent (tests, embedders that don't trace). */
2268
2683
  readonly recordDelivery?: (delivery: SubscriptionDelivery) => void;
2684
+ /**
2685
+ * Optional reactive-compute cost sink (the finops runner's `record`). Called
2686
+ * once per reactive RECOMPUTE delivery — a change re-ran an affected
2687
+ * subscription and pushed it a delta — with `unit: 'recompute', amount: 1`
2688
+ * attributed to the subscription's tenant. Sibling to `recordDelivery`: same
2689
+ * call sites, a different ledger (chargeback / budget vs latency trace).
2690
+ *
2691
+ * No-op when absent (tests, embedders, apps with no `*.budget.ts`). The hot
2692
+ * path pays nothing in that case — the `CostEvent` is only allocated inside
2693
+ * the `recordCost !== undefined` guard, never eagerly. Wired by both boot
2694
+ * paths ONLY when finops is attached; see `attachFinops`.
2695
+ */
2696
+ readonly recordCost?: (event: CostEvent) => void;
2269
2697
  /** Optional snapshot cache (Layer 3). Absent → queries are never cached;
2270
2698
  * present → subscriptions whose descriptor opted in (a `cacheBinding`
2271
2699
  * is passed to `subscribe`) share + cache their initial snapshot. */
@@ -2402,6 +2830,21 @@ export declare const enterRequestLoader: (loader: DataLoader) => void;
2402
2830
  * Promise-wrapped to satisfy the async contract. */
2403
2831
  export declare const envSecretsBackend: SecretsBackend;
2404
2832
 
2833
+ /**
2834
+ * Evaluate every declared rule of every touched table's rows. Throws the first
2835
+ * error-severity `BusinessRuleViolation` (rolling back the transaction);
2836
+ * warning-severity violations log and continue.
2837
+ *
2838
+ * A predicate that itself throws a `BusinessRuleViolation` (the author built one
2839
+ * directly) is re-thrown as-is; any OTHER thrown value propagates unchanged — a
2840
+ * real failure (a DB error) must not be silently reclassified as a violation.
2841
+ */
2842
+ export declare const evaluateTouchedRules: (params: {
2843
+ readonly touched: ReadonlyArray<TouchedRow>;
2844
+ readonly store: RuleReadStore;
2845
+ readonly subject: unknown;
2846
+ }) => Promise<void>;
2847
+
2405
2848
  /**
2406
2849
  * How many deliveries one client may fall behind before the oldest are dropped.
2407
2850
  *
@@ -2778,6 +3221,451 @@ export declare type ExecutorOutput<D extends ExecutorDescriptor> = Schema.Schema
2778
3221
  */
2779
3222
  export declare type ExecutorReturn<D extends ExecutorDescriptor, E, R> = ExecutorOutput<D> | Promise<ExecutorOutput<D>> | Effect.Effect<ExecutorOutput<D>, E, R> | ReactiveReturn<D>;
2780
3223
 
3224
+ /** The write that tipped an expectation into violation, resolved from the CDC
3225
+ * `ChangeEvent` that caused it — the provenance link the plan calls for. `null`
3226
+ * when the transition was CLOCK-driven (a freshness SLA aging out with no
3227
+ * write) rather than write-driven: the honest answer is "no write caused this;
3228
+ * the ABSENCE of writes did". */
3229
+ export declare interface ExpectationCause {
3230
+ readonly table: string;
3231
+ readonly op: 'insert' | 'update' | 'delete';
3232
+ /** The trace the offending write ran under — joins to `voltro logs --trace`
3233
+ * and the audit sink. */
3234
+ readonly traceId?: string | null;
3235
+ /** The acting identity behind the offending write. */
3236
+ readonly subjectId?: string | null;
3237
+ /** The rpc tag of the call that made the offending write, when carried. */
3238
+ readonly procedure?: string | null;
3239
+ }
3240
+
3241
+ export declare interface ExpectationDefinition {
3242
+ /** Brand marker — lets the cli discriminate default-exported expectation
3243
+ * definitions from arbitrary objects during file discovery. */
3244
+ readonly _voltroExpectation: true;
3245
+ readonly name: string;
3246
+ readonly on: {
3247
+ readonly table: string;
3248
+ readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3249
+ };
3250
+ readonly invariant: ExpectationInvariant;
3251
+ readonly severity: ExpectationSeverity;
3252
+ readonly description?: string;
3253
+ }
3254
+
3255
+ export declare interface ExpectationDefinitionInput {
3256
+ /** Stable id within the app. Unique across all expectations. */
3257
+ readonly name: string;
3258
+ /** The watched table. Its CDC deltas drive re-evaluation. An optional `where`
3259
+ * narrows the population the invariant is asserted over — a pure predicate
3260
+ * evaluated server-side per row (rows failing it are excluded from the
3261
+ * metric). It never leaves the server, so it can be any predicate. */
3262
+ readonly on: {
3263
+ readonly table: string;
3264
+ readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3265
+ };
3266
+ readonly invariant: ExpectationInvariant;
3267
+ /** Alerting priority carried on the violation signal. Default `'warn'`. */
3268
+ readonly severity?: ExpectationSeverity;
3269
+ /** Human-facing note surfaced in devtools / the violation message. */
3270
+ readonly description?: string;
3271
+ }
3272
+
3273
+ /**
3274
+ * Evaluates ONE expectation. Seed once, then feed CDC deltas via `applyChange`
3275
+ * and (for a time-dependent invariant) clock ticks via `tick`. Each call
3276
+ * re-reads the maintained metric, updates the live state, and returns a
3277
+ * transition when the holding/violated boundary is crossed.
3278
+ */
3279
+ export declare class ExpectationEvaluator {
3280
+ private readonly def;
3281
+ private readonly deps;
3282
+ private readonly engine;
3283
+ private readonly now;
3284
+ private seeded;
3285
+ private status;
3286
+ private metric;
3287
+ private threshold;
3288
+ private since;
3289
+ private lastEvaluatedAt;
3290
+ private lastCause;
3291
+ constructor(def: ExpectationDefinition, deps: ExpectationEvaluatorDeps);
3292
+ get timeDependent(): boolean;
3293
+ /** Whether a row is in the asserted population (honours `on.where`). */
3294
+ private inScope;
3295
+ /** Seed the metric from the current base rows and evaluate the initial state.
3296
+ * Never emits a transition (there is no prior state to cross from). */
3297
+ seed(): Promise<void>;
3298
+ /**
3299
+ * Apply one CDC change and re-evaluate. Returns a transition if the change
3300
+ * (or the reshaped population) crossed the boundary. A change to a row that is
3301
+ * out of scope on BOTH images is ignored.
3302
+ */
3303
+ applyChange(event: ChangeEvent): Promise<ExpectationTransition | null>;
3304
+ /** Re-evaluate against the current clock WITHOUT a write — for a
3305
+ * time-dependent invariant (freshness) whose metric ages on its own. Returns
3306
+ * a transition if the clock alone crossed the boundary. */
3307
+ tick(): ExpectationTransition | null;
3308
+ private commitReading;
3309
+ state(): ExpectationState;
3310
+ }
3311
+
3312
+ export declare interface ExpectationEvaluatorDeps {
3313
+ /** Read the full current base rows — used to SEED and to resolve a min/max
3314
+ * rescan (freshness) from the post-commit table. */
3315
+ readonly queryBase: () => Promise<ReadonlyArray<Row_5>>;
3316
+ /** Wall clock. Injectable for deterministic tests. Default `Date.now`. */
3317
+ readonly now?: () => number;
3318
+ readonly log?: SyncLogger;
3319
+ }
3320
+
3321
+ /**
3322
+ * The data-quality invariant an expectation asserts. Each variant is chosen so
3323
+ * it reduces the watched table to a SINGLE incrementally-maintained metric that
3324
+ * rides the IVM engine (or a delta-driven filtered count), then compares that
3325
+ * metric against a threshold:
3326
+ *
3327
+ * - `freshness` — max(column) → `age = now - max`; violated when
3328
+ * `age > maxAgeMs`. Rides the IVM `max` maintainer (incl. its
3329
+ * rescan-on-extreme-delete protocol). TIME-DEPENDENT: it is
3330
+ * re-checked on a clock tick as well as on write, because the
3331
+ * metric ages with the wall clock even when no write arrives —
3332
+ * which is exactly when a freshness SLA matters.
3333
+ * - `nullRate` — fraction of rows whose `column` is null; violated when
3334
+ * `rate > maxRate`. Delta-maintained filtered count.
3335
+ * - `rowCount` — COUNT(*) of the table; violated when out of `[min, max]`.
3336
+ * - `valueBounds` — fraction of rows whose numeric `column` falls outside
3337
+ * `[min, max]` (the distribution / range check). Violated when
3338
+ * that fraction exceeds `maxViolationRate`, or — when no rate
3339
+ * is given — when ANY row is out of bounds.
3340
+ */
3341
+ export declare type ExpectationInvariant = {
3342
+ readonly kind: 'freshness';
3343
+ readonly column: string;
3344
+ readonly maxAgeMs: number;
3345
+ } | {
3346
+ readonly kind: 'nullRate';
3347
+ readonly column: string;
3348
+ readonly maxRate: number;
3349
+ } | {
3350
+ readonly kind: 'rowCount';
3351
+ readonly min?: number;
3352
+ readonly max?: number;
3353
+ } | {
3354
+ readonly kind: 'valueBounds';
3355
+ readonly column: string;
3356
+ readonly min?: number;
3357
+ readonly max?: number;
3358
+ /** When set, the expectation tolerates up to this FRACTION (0..1) of
3359
+ * out-of-bounds rows before firing. Omitted ⇒ a single out-of-bounds row
3360
+ * violates. */
3361
+ readonly maxViolationRate?: number;
3362
+ };
3363
+
3364
+ /**
3365
+ * Framework-provided registry of every expectation the cli discovered + the
3366
+ * evaluator maintains. Handler / inspect code reads the live signal here.
3367
+ */
3368
+ export declare class ExpectationRegistry extends ExpectationRegistry_base {
3369
+ }
3370
+
3371
+ declare const ExpectationRegistry_base: Context.TagClass<ExpectationRegistry, "@voltro/ExpectationRegistry", ExpectationRegistryApi>;
3372
+
3373
+ export declare interface ExpectationRegistryApi {
3374
+ /** Snapshot every registered expectation's current state — the inspect feed. */
3375
+ readonly snapshot: () => ReadonlyArray<ExpectationState>;
3376
+ /** One expectation's state by name, or `null` when unknown. */
3377
+ readonly get: (name: string) => ExpectationState | null;
3378
+ /** Only the currently-violated expectations — the alerting view. */
3379
+ readonly violations: () => ReadonlyArray<ExpectationState>;
3380
+ /** Subscribe to holding/violated transitions. Returns an unsubscribe fn. */
3381
+ readonly subscribe: (listener: (t: ExpectationTransition) => void) => () => void;
3382
+ }
3383
+
3384
+ export declare interface ExpectationRunnerHandle {
3385
+ readonly registry: ExpectationRegistryApi;
3386
+ readonly detach: () => void;
3387
+ readonly registered: ReadonlyArray<string>;
3388
+ /** Test/debug seam: run one clock re-check of the time-dependent
3389
+ * expectations at `nowMs` (or the injected clock) and return the snapshot.
3390
+ * This is what the periodic tick timer calls. */
3391
+ readonly runTick: () => void;
3392
+ }
3393
+
3394
+ /** How loud a violation is. Purely a classification carried on the signal —
3395
+ * the framework never blocks a write on any severity (that is a business
3396
+ * rule's job); severity drives alerting priority downstream. */
3397
+ export declare type ExpectationSeverity = 'info' | 'warn' | 'critical';
3398
+
3399
+ /** The live state of one expectation — the row an inspect endpoint / dashboard
3400
+ * reads. */
3401
+ export declare interface ExpectationState {
3402
+ readonly name: string;
3403
+ readonly table: string;
3404
+ readonly invariant: ExpectationInvariant['kind'];
3405
+ readonly severity: ExpectationSeverity;
3406
+ readonly status: ExpectationStatus;
3407
+ /** The measured metric: freshness → age in ms; nullRate / valueBounds(rate) →
3408
+ * a fraction; rowCount → the count; valueBounds(count) → number of bad rows.
3409
+ * `null` when the metric is undefined over the current data. */
3410
+ readonly metric: number | null;
3411
+ /** The threshold the metric is compared against (for display). */
3412
+ readonly threshold: number | null;
3413
+ /** When the expectation entered its current `status`. */
3414
+ readonly since: Date | null;
3415
+ /** When it was last (re-)evaluated. */
3416
+ readonly lastEvaluatedAt: Date | null;
3417
+ /** The most recent violation's provenance, retained across recovery for the
3418
+ * audit trail. `null` if it has never been violated. */
3419
+ readonly lastCause: ExpectationCause | null;
3420
+ readonly description?: string;
3421
+ }
3422
+
3423
+ /** Whether an expectation currently HOLDS. `unknown` = not yet seeded, or a
3424
+ * metric that is undefined over the current data (e.g. freshness of an empty
3425
+ * table — neither fresh nor stale). Transitions are emitted only into/out of
3426
+ * `violated`. */
3427
+ export declare type ExpectationStatus = 'holding' | 'violated' | 'unknown';
3428
+
3429
+ /** Emitted when an expectation crosses the holding/violated boundary. */
3430
+ export declare interface ExpectationTransition {
3431
+ readonly kind: 'violated' | 'recovered';
3432
+ readonly state: ExpectationState;
3433
+ /** The causing write (present for a write-driven violation; `null` for a
3434
+ * clock-driven one or a recovery). */
3435
+ readonly cause: ExpectationCause | null;
3436
+ }
3437
+
3438
+ /** What caused a live recompute — resolved from the CDC `ChangeEvent`. `null`
3439
+ * for a non-write recompute (a reconcile re-seed). */
3440
+ export declare interface ExperimentCause {
3441
+ readonly table: string;
3442
+ readonly op: 'insert' | 'update' | 'delete';
3443
+ /** The trace the write ran under — joins to `voltro logs --trace`. */
3444
+ readonly traceId?: string | null;
3445
+ /** The acting identity behind the write. */
3446
+ readonly subjectId?: string | null;
3447
+ /** The rpc tag of the call that made the write, when carried. */
3448
+ readonly procedure?: string | null;
3449
+ }
3450
+
3451
+ export declare interface ExperimentDefinition {
3452
+ /** Brand marker — lets the cli discriminate default-exported experiment
3453
+ * definitions from arbitrary objects during file discovery. */
3454
+ readonly _voltroExperiment: true;
3455
+ readonly name: string;
3456
+ readonly on: {
3457
+ readonly table: string;
3458
+ readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3459
+ };
3460
+ /** Resolved to a function: reads the subject and normalises it to a non-empty
3461
+ * string, or `null` when the row carries no subject. */
3462
+ readonly subject: (row: Readonly<Record<string, unknown>>) => string | null;
3463
+ readonly variants: ReadonlyArray<ExperimentVariant>;
3464
+ /** Resolved holdout fraction in [0, 1); `0` when none. */
3465
+ readonly holdout: number;
3466
+ readonly metric: ExperimentMetric;
3467
+ readonly baseline: string;
3468
+ readonly description?: string;
3469
+ }
3470
+
3471
+ export declare interface ExperimentDefinitionInput {
3472
+ /** Stable id within the app. Unique across all experiments. Also the
3473
+ * assignment SALT, so the same subject buckets independently per experiment. */
3474
+ readonly name: string;
3475
+ /** The watched table. Its CDC deltas drive the live recompute. An optional
3476
+ * `where` narrows the population the experiment observes — a pure predicate
3477
+ * evaluated server-side per row (rows failing it are excluded). It never
3478
+ * leaves the server, so it can be any predicate. */
3479
+ readonly on: {
3480
+ readonly table: string;
3481
+ readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3482
+ };
3483
+ /** The stable assignment key extractor. */
3484
+ readonly subject: ExperimentSubject;
3485
+ /** The arms. At least two (a control + one treatment). */
3486
+ readonly variants: ReadonlyArray<ExperimentVariantSpec>;
3487
+ /** Fraction (0..1) of subjects held out ENTIRELY — assigned to the reserved
3488
+ * `holdout` variant and never to a treatment. Omitted ⇒ no holdout. */
3489
+ readonly holdout?: number;
3490
+ /** The success metric maintained per variant. */
3491
+ readonly metric: ExperimentMetric;
3492
+ /** Which variant lift/difference is computed AGAINST. Must name one of
3493
+ * `variants`. Default: the first variant (by convention the control). */
3494
+ readonly baseline?: string;
3495
+ /** Human-facing note surfaced in devtools / the results view. */
3496
+ readonly description?: string;
3497
+ }
3498
+
3499
+ /**
3500
+ * Evaluates ONE experiment. Seed once from the base rows, then feed CDC deltas
3501
+ * via `applyChange`; each call re-maintains the per-variant metric + exposure and
3502
+ * recomputes the live result. Returns an `ExperimentUpdate` for any relevant
3503
+ * write (one whose old OR new image is assigned), else `null`.
3504
+ */
3505
+ export declare class ExperimentEvaluator {
3506
+ private readonly def;
3507
+ private readonly deps;
3508
+ private readonly exposure;
3509
+ private readonly metric;
3510
+ private readonly metricIsCount;
3511
+ private readonly now;
3512
+ private seeded;
3513
+ private lastUpdatedAt;
3514
+ constructor(def: ExperimentDefinition, deps: ExperimentEvaluatorDeps);
3515
+ /** The variant an in-scope row is assigned to, or `null` when the row is out
3516
+ * of the `where` population or carries no subject. */
3517
+ private variantOf;
3518
+ private inScope;
3519
+ /** Seed both maintainers from the current base rows. Never emits (there is no
3520
+ * prior result to compare). */
3521
+ seed(): Promise<void>;
3522
+ /**
3523
+ * Apply one CDC change and recompute. Returns an update when the change was
3524
+ * relevant (assigned on either image), else `null`. A change to a row that is
3525
+ * out of scope on BOTH images is ignored.
3526
+ */
3527
+ applyChange(event: ChangeEvent): ExperimentUpdate | null;
3528
+ /** The live result — per-variant metric + lift/diff vs the baseline. */
3529
+ result(): ExperimentResult;
3530
+ /** Resolve one variant's metric value, filling the "no group yet" gap: `count`
3531
+ * → 0, `sum` → 0 (empty sum), `avg` / `conversionRate` → `null` (undefined
3532
+ * over no samples). */
3533
+ private metricValueFor;
3534
+ get isSeeded(): boolean;
3535
+ }
3536
+
3537
+ export declare interface ExperimentEvaluatorDeps {
3538
+ /** Read the full current base rows — used to SEED and to reconcile. */
3539
+ readonly queryBase: () => Promise<ReadonlyArray<Row_6>>;
3540
+ /** Wall clock. Injectable for deterministic tests. Default `Date.now`. */
3541
+ readonly now?: () => number;
3542
+ readonly log?: SyncLogger;
3543
+ }
3544
+
3545
+ /** The success metric an experiment maintains PER VARIANT. Every kind reduces to
3546
+ * a bounded IVM aggregate grouped by the assigned variant, so it rides the same
3547
+ * incremental engine as `defineAggregate` (no batch re-scan):
3548
+ *
3549
+ * - `count` — rows assigned to the variant. The metric IS the
3550
+ * exposure (sample size).
3551
+ * - `sum` — Σ of a numeric `column` over the variant's rows.
3552
+ * - `avg` — mean of a numeric `column` — the per-observation
3553
+ * average (revenue-per-order, latency, …). Comparable
3554
+ * across variants of unequal size.
3555
+ * - `conversionRate` — the FRACTION of the variant's rows that "converted":
3556
+ * `row[column]` is truthy, or `=== equals` when given.
3557
+ * Modelled as `avg` of a 0/1 projection, so it too is a
3558
+ * live IVM aggregate. The classic A/B success metric.
3559
+ *
3560
+ * `count` / `sum` reflect BOTH exposure and effect, so their cross-variant
3561
+ * comparison only means "more/less total"; `avg` / `conversionRate` are
3562
+ * per-subject rates and are the ones a lift reads honestly. The result always
3563
+ * carries each variant's `exposure`, so an unequal split is visible either way. */
3564
+ export declare type ExperimentMetric = {
3565
+ readonly kind: 'count';
3566
+ } | {
3567
+ readonly kind: 'sum';
3568
+ readonly column: string;
3569
+ } | {
3570
+ readonly kind: 'avg';
3571
+ readonly column: string;
3572
+ } | {
3573
+ readonly kind: 'conversionRate';
3574
+ readonly column: string;
3575
+ /** When set, a row converted iff `row[column] === equals`. Omitted ⇒
3576
+ * converted iff `row[column]` is truthy. */
3577
+ readonly equals?: unknown;
3578
+ };
3579
+
3580
+ export declare type ExperimentMetricKind = ExperimentMetric['kind'];
3581
+
3582
+ /**
3583
+ * Framework-provided registry of every experiment the cli discovered + the
3584
+ * evaluator maintains. Handler / inspect code reads the live results here,
3585
+ * exactly as it reads `ExpectationRegistry` / `CostRegistry`.
3586
+ */
3587
+ export declare class ExperimentRegistry extends ExperimentRegistry_base {
3588
+ }
3589
+
3590
+ declare const ExperimentRegistry_base: Context.TagClass<ExperimentRegistry, "@voltro/ExperimentRegistry", ExperimentRegistryApi>;
3591
+
3592
+ export declare interface ExperimentRegistryApi {
3593
+ /** Snapshot every registered experiment's current result — the inspect feed. */
3594
+ readonly snapshot: () => ReadonlyArray<ExperimentResult>;
3595
+ /** One experiment's result by name, or `null` when unknown. */
3596
+ readonly get: (name: string) => ExperimentResult | null;
3597
+ /** Subscribe to live result recomputes. Returns an unsubscribe fn. */
3598
+ readonly subscribe: (listener: (u: ExperimentUpdate) => void) => () => void;
3599
+ }
3600
+
3601
+ /** The live state of one experiment — the row an inspect endpoint / results
3602
+ * dashboard reads. Recomputed on every relevant write. */
3603
+ export declare interface ExperimentResult {
3604
+ readonly name: string;
3605
+ readonly table: string;
3606
+ readonly metric: ExperimentMetricKind;
3607
+ readonly baseline: string;
3608
+ /** Rows assigned to ANY variant (Σ exposure). */
3609
+ readonly totalExposure: number;
3610
+ readonly variants: ReadonlyArray<ExperimentVariantResult>;
3611
+ /** When the result was last recomputed (a relevant write arrived). */
3612
+ readonly lastUpdatedAt: Date | null;
3613
+ readonly description?: string;
3614
+ }
3615
+
3616
+ export declare interface ExperimentRunnerHandle {
3617
+ readonly registry: ExperimentRegistryApi;
3618
+ readonly detach: () => void;
3619
+ readonly registered: ReadonlyArray<string>;
3620
+ }
3621
+
3622
+ /** How the assignment key is read from a row. A column NAME (its value is the
3623
+ * subject) or a function returning the subject (a composite key, a tenant id,
3624
+ * a hashed cookie). Returning `null` / `undefined` / an empty value ⇒ the row
3625
+ * is UNASSIGNED and excluded from every variant. */
3626
+ export declare type ExperimentSubject = string | ((row: Readonly<Record<string, unknown>>) => string | number | null | undefined);
3627
+
3628
+ /** Emitted whenever an experiment's result recomputes on a relevant write. The
3629
+ * results view subscribes to this to update in real time. */
3630
+ export declare interface ExperimentUpdate {
3631
+ readonly result: ExperimentResult;
3632
+ readonly cause: ExperimentCause | null;
3633
+ }
3634
+
3635
+ /** Resolved variant — weight defaulted. */
3636
+ export declare interface ExperimentVariant {
3637
+ readonly name: string;
3638
+ readonly weight: number;
3639
+ }
3640
+
3641
+ /** One variant's live result within an experiment. */
3642
+ export declare interface ExperimentVariantResult {
3643
+ readonly variant: string;
3644
+ /** True for the variant lift/diff is measured against. */
3645
+ readonly isBaseline: boolean;
3646
+ /** True for the reserved holdout arm. */
3647
+ readonly isHoldout: boolean;
3648
+ /** Sample size — rows assigned to this variant (the exposure). */
3649
+ readonly exposure: number;
3650
+ /** The maintained metric value. `null` when the metric is undefined over the
3651
+ * variant's current data (no valid samples for an avg / rate). */
3652
+ readonly metric: number | null;
3653
+ /** Relative lift vs the baseline: `(metric − baseMetric) / baseMetric`. `null`
3654
+ * when either value is null or the baseline metric is 0 (undefined lift).
3655
+ * Always `null` on the baseline row itself. */
3656
+ readonly lift: number | null;
3657
+ /** Absolute difference vs the baseline: `metric − baseMetric`. `null` when
3658
+ * either value is null. Always `null` on the baseline row. */
3659
+ readonly diff: number | null;
3660
+ }
3661
+
3662
+ /** A named arm of the experiment. `weight` skews the split (default 1 = equal);
3663
+ * a subject is assigned in proportion to its weight over the total. */
3664
+ export declare interface ExperimentVariantSpec {
3665
+ readonly name: string;
3666
+ readonly weight?: number;
3667
+ }
3668
+
2781
3669
  /** Build an Effect `ExternalSpan` parent from a `traceparent` header so
2782
3670
  * inbound HTTP work continues the caller's trace. Returns undefined
2783
3671
  * when the header is missing/invalid (no parent → a fresh root span). */
@@ -2800,6 +3688,20 @@ export declare interface FieldChange {
2800
3688
  */
2801
3689
  export declare const fingerprintFor: (table: string, predicate: Predicate, indexHint?: string) => string;
2802
3690
 
3691
+ export declare interface FinopsRunnerHandle {
3692
+ readonly registry: CostRegistryApi;
3693
+ /** The ingestion entry point compute sites call to attribute one unit of
3694
+ * cost. Emits any budget-crossing signals to the registry's subscribers. */
3695
+ readonly record: (event: CostEvent) => void;
3696
+ /** Reset attribution + budgets for one tenant (or all). */
3697
+ readonly reset: (tenantId?: string | null) => void;
3698
+ readonly detach: () => void;
3699
+ readonly registered: ReadonlyArray<string>;
3700
+ /** Test/debug seam: run one clock re-check of the windowed budgets. This is
3701
+ * what the periodic tick timer calls. */
3702
+ readonly runTick: () => void;
3703
+ }
3704
+
2803
3705
  export declare interface FluentStore extends Omit<MutationStore, 'update' | 'delete' | 'query'> {
2804
3706
  /**
2805
3707
  * Diff-based many-to-many link writer for a junction table. `anchor` names the
@@ -2828,7 +3730,7 @@ export declare interface FluentStore extends Omit<MutationStore, 'update' | 'del
2828
3730
  * SPI every dialect store implements, and it genuinely does return untyped
2829
3731
  * rows off the wire. The type is re-applied here, at the handler boundary.
2830
3732
  */
2831
- query<R = Row_5>(descriptor: QueryDescriptor<R>): Promise<ReadonlyArray<R>>;
3733
+ query<R = Row_7>(descriptor: QueryDescriptor<R>): Promise<ReadonlyArray<R>>;
2832
3734
  /**
2833
3735
  * Terminal: EXACTLY one row, TYPED. Fails with `NoRowFound` on zero matches
2834
3736
  * and equally on two or more.
@@ -2849,18 +3751,18 @@ export declare interface FluentStore extends Omit<MutationStore, 'update' | 'del
2849
3751
  * IS `query()` underneath — the probe just asks for one row more than it
2850
3752
  * needs so "the first of several" cannot masquerade as "the one you meant".
2851
3753
  */
2852
- one<R = Row_5>(query: QueryLike<R>): Promise<R>;
3754
+ one<R = Row_7>(query: QueryLike<R>): Promise<R>;
2853
3755
  /** Terminal: the first matching row or `null`, TYPED. Use when "any match" is
2854
3756
  * genuinely what you mean. */
2855
- first<R = Row_5>(query: QueryLike<R>): Promise<R | null>;
3757
+ first<R = Row_7>(query: QueryLike<R>): Promise<R | null>;
2856
3758
  /** Alias of `first` — the first matching row or `null`, TYPED. */
2857
- maybeOne<R = Row_5>(query: QueryLike<R>): Promise<R | null>;
3759
+ maybeOne<R = Row_7>(query: QueryLike<R>): Promise<R | null>;
2858
3760
  /** Fluent, scope-applying read builder: `select('notes').where(...).all()`. */
2859
3761
  select(table: string): SelectBuilder;
2860
3762
  /** Fluent predicate update: `update('notes').where('id', id).set({...})`. */
2861
3763
  update(table: string): UpdateBuilder;
2862
3764
  /** Keyed update by primary key — returns the post-image. */
2863
- update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row_5 | null>;
3765
+ update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row_7 | null>;
2864
3766
  /** Fluent predicate delete: `delete('notes').where('id', id).soft()`. */
2865
3767
  delete(table: string): DeleteBuilder;
2866
3768
  /** Keyed delete by primary key. */
@@ -2885,6 +3787,31 @@ export declare interface FluentStoreBackend {
2885
3787
  readonly delete: DataStore['delete'];
2886
3788
  }
2887
3789
 
3790
+ /**
3791
+ * The columns a given SUBJECT may not see on the wire for `tableName`: the
3792
+ * union of the table's `.serverOnly()` columns (hidden from EVERYONE, so
3793
+ * subject-independent) and its `.readableBy(...)` columns whose scope set the
3794
+ * subject does NOT intersect (Part B — field-level read permissions).
3795
+ *
3796
+ * A subject sees a `.readableBy(...)` column iff it holds AT LEAST ONE of the
3797
+ * declared scopes, checked against its EFFECTIVE scope set (raw subject scopes ∪
3798
+ * rbac role-derived scopes) via `hasEffectiveScope` — so `admin:full` sees every
3799
+ * such column, exactly as it satisfies every guard.
3800
+ *
3801
+ * SUBJECT-INDEPENDENCE is deliberate and load-bearing: when the table declares
3802
+ * NO `.readableBy(...)` column, this returns exactly `serverOnlyColumnNames` —
3803
+ * the same set for every subject — so the Dispatcher's per-subscriber strip
3804
+ * collapses to Part A's behaviour and its read/diff memo still shares one array
3805
+ * across all subscribers of a change. Only a table that actually carries a
3806
+ * `.readableBy(...)` column pays the per-subject cost (different subjects get
3807
+ * different stripped arrays, the same accepted cost `.serverOnly()` already has
3808
+ * whenever it strips anything).
3809
+ *
3810
+ * An unknown / unregistered table forbids nothing (returns `[]`) rather than
3811
+ * throwing — same posture as `serverOnlyColumnNames`.
3812
+ */
3813
+ export declare const forbiddenColumnsForSubject: (tableName: string, subject: Subject) => ReadonlyArray<string>;
3814
+
2888
3815
  /** Forget the calling subject's credential. Returns whether a row was removed. */
2889
3816
  export declare const forgetCredential: (store: VaultStore, connectionId: string, subjectId: string) => Promise<boolean>;
2890
3817
 
@@ -2988,6 +3915,14 @@ export declare interface GroupState {
2988
3915
  /** What a user handler may return. */
2989
3916
  export declare type HandlerBody = void | Promise<unknown> | Effect.Effect<unknown, unknown, never>;
2990
3917
 
3918
+ /**
3919
+ * Deterministic hash of a string into the unit interval `[0, 1)` — FNV-1a
3920
+ * (32-bit), no `node:crypto`, so it runs identically on server and client. The
3921
+ * distribution is uniform enough for balanced bucketing; it is NOT a
3922
+ * cryptographic hash and is not meant to be one (an assignment is not a secret).
3923
+ */
3924
+ export declare const hashUnitInterval: (s: string) => number;
3925
+
2991
3926
  /** Define a histogram. `boundaries` default to the framework duration buckets. */
2992
3927
  export declare const histogramMetric: (name: string, boundaries?: MetricBoundaries.MetricBoundaries, description?: string) => Metric.Metric.Histogram<number>;
2993
3928
 
@@ -2998,6 +3933,12 @@ export declare const histogramMetric: (name: string, boundaries?: MetricBoundari
2998
3933
  */
2999
3934
  export declare const historyProvenance: (history: ReadonlyArray<RowHistoryEntry>, key: ProvenanceKey) => ProvenanceResult;
3000
3935
 
3936
+ /** The reserved variant name a `holdout` fraction is assigned to. A holdout is
3937
+ * carved off the TOP of the unit interval BEFORE the weighted split, so it is
3938
+ * never influenced by adding/removing a treatment. No user variant may take
3939
+ * this name when `holdout > 0`. */
3940
+ export declare const HOLDOUT_VARIANT = "holdout";
3941
+
3001
3942
  /** A secrets backend over a plain JSON HTTP API — no SDK. Wraps fetch + cache.
3002
3943
  * Vault (`/v1/secret/data/...`), Doppler, and the cloud control-plane all fit. */
3003
3944
  export declare const httpSecretsBackend: (opts: HttpSecretsOptions) => SecretsBackend;
@@ -3283,8 +4224,14 @@ export declare const invertChange: (c: ForwardChange) => InverseOp | null;
3283
4224
 
3284
4225
  export declare const isAggregateDefinition: (value: unknown) => value is AggregateDefinition;
3285
4226
 
4227
+ export declare const isCostBudgetDefinition: (value: unknown) => value is CostBudgetDefinition;
4228
+
3286
4229
  export declare const isEmptyTenantScopedRead: (descriptor: QueryDescriptor, tenantScopedTables: ReadonlySet<string>, tenantId: string | null | undefined) => boolean;
3287
4230
 
4231
+ export declare const isExpectationDefinition: (value: unknown) => value is ExpectationDefinition;
4232
+
4233
+ export declare const isExperimentDefinition: (value: unknown) => value is ExperimentDefinition;
4234
+
3288
4235
  /** Foreign-key-violation SQLSTATE / errno across dialects (pg 23503, mysql/maria
3289
4236
  * 1452, mssql 547, sqlite). Used to turn an opaque tenant-FK SqlError into a
3290
4237
  * typed error. */
@@ -3828,6 +4775,18 @@ export declare const makeQuerySubscriber: <D>(deps: QuerySubscriberDeps<D>) => (
3828
4775
 
3829
4776
  export declare const makeRouterActivity: () => RouterActivity;
3830
4777
 
4778
+ /**
4779
+ * Wrap a transactional store so single-row writes on USER tables are recorded
4780
+ * as their POST-WRITE row. Inserts and updates are captured; a delete removes
4781
+ * the row (there is no post-write state to validate), so — like undo's bulk
4782
+ * boundary — deletes are NOT re-validated in v1. Everything else delegates
4783
+ * untouched. Multiple writes to the same (table, id) keep the latest row.
4784
+ */
4785
+ export declare const makeRuleCapture: (tx: unknown) => {
4786
+ readonly tx: unknown;
4787
+ drain(): ReadonlyArray<TouchedRow>;
4788
+ };
4789
+
3831
4790
  /**
3832
4791
  * Build the registry from a list of table descriptors. Cheap — runs once
3833
4792
  * at boot. The returned object is read-only.
@@ -4093,6 +5052,11 @@ export declare interface MutationLike {
4093
5052
  readonly name: string;
4094
5053
  readonly source?: string | ReadonlyArray<string> | undefined;
4095
5054
  readonly guards?: ReadonlyArray<AnyCheckSpec> | undefined;
5055
+ readonly target?: {
5056
+ readonly table: string;
5057
+ } | ReadonlyArray<{
5058
+ readonly table: string;
5059
+ }> | undefined;
4096
5060
  };
4097
5061
  executor(input: unknown, ctx: unknown): unknown;
4098
5062
  }
@@ -4347,9 +5311,13 @@ export declare interface OneShotQueryRunnerDeps<D> extends QueryProducerDeps<D>
4347
5311
  /**
4348
5312
  * Execute a FINALIZED (tenant-scoped) descriptor → rows. On the WS path the
4349
5313
  * dispatcher owns this step; a ONE-SHOT read (a `publicApi` REST GET, where
4350
- * there is no subscription to drive) needs it inline.
5314
+ * there is no subscription to drive) needs it inline. The resolved request
5315
+ * `context` is handed alongside so the caller can apply the SAME
5316
+ * subject-aware wire-column strip the dispatcher does (`.serverOnly()` ∪ the
5317
+ * `.readableBy(...)` columns this subject lacks) — the parity that keeps the
5318
+ * REST projection from shipping a column the socket path would strip.
4351
5319
  */
4352
- readonly queryRows: (descriptor: D) => Promise<ReadonlyArray<unknown>>;
5320
+ readonly queryRows: (descriptor: D, context: ServeRequestContext) => Promise<ReadonlyArray<unknown>>;
4353
5321
  }
4354
5322
 
4355
5323
  /**
@@ -4606,7 +5574,7 @@ export declare interface PreviewStore {
4606
5574
  /** Project a stored row into the wire shape. Deliberately total over the
4607
5575
  * columns the descriptor declares — and deliberately silent about the token
4608
5576
  * columns, which have no wire representation at all. */
4609
- export declare const projectConnectionState: (definition: ConnectionDefinition, row: AnyRow | undefined) => ConnectionState;
5577
+ export declare const projectConnectionState: (definition: ConnectionDefinition, row: AnyRow_2 | undefined) => ConnectionState;
4610
5578
 
4611
5579
  /** Exact-match property filter. Values are AND'd. */
4612
5580
  export declare type PropertyFilter = Readonly<Record<string, string | number | boolean | null>>;
@@ -4746,7 +5714,7 @@ export declare interface QueryFinalizeOptions {
4746
5714
 
4747
5715
  /** A typed builder or its descriptor — the single-row terminals accept either,
4748
5716
  * so a call site never has to reach for `.descriptor` just to use them. */
4749
- export declare type QueryLike<R = Row_5> = QueryDescriptor<R> | {
5717
+ export declare type QueryLike<R = Row_7> = QueryDescriptor<R> | {
4750
5718
  readonly descriptor: QueryDescriptor<R>;
4751
5719
  };
4752
5720
 
@@ -4778,6 +5746,26 @@ export declare type ReactionAct = {
4778
5746
  } | {
4779
5747
  readonly kind: 'workflow';
4780
5748
  readonly workflow: string;
5749
+ /**
5750
+ * Shape the workflow's payload from the change. Omit and the CHANGED ROW is
5751
+ * the payload, as before.
5752
+ *
5753
+ * Without this the workflow's payload schema is dictated by the watched
5754
+ * TABLE's row shape rather than by what the workflow needs, which drags two
5755
+ * problems into every reaction-started workflow:
5756
+ *
5757
+ * • every column travels, including ones the workflow has no business
5758
+ * seeing — a reaction on a users table hands the whole row to a
5759
+ * narration workflow;
5760
+ * • the row shape is DIALECT-dependent at the edges. A `timestamp()`
5761
+ * column arrives as a `Date` on MariaDB and a number elsewhere, so an
5762
+ * app normalising on both sides of an idempotency key is doing the
5763
+ * framework's job.
5764
+ *
5765
+ * A mapper fixes both at the declaration: the workflow declares the payload
5766
+ * it wants and the reaction adapts.
5767
+ */
5768
+ readonly payload?: (event: ReactionEvent) => unknown;
4781
5769
  };
4782
5770
 
4783
5771
  export declare interface ReactionDefinition {
@@ -4804,10 +5792,35 @@ export declare interface ReactionGuards {
4804
5792
  /** Per-tenant AI spend ceiling (USD). Checked via the injected `checkBudget`
4805
5793
  * (the app wires `requireAiBudget`). Over budget → the reaction refuses. */
4806
5794
  readonly costBudgetUsd?: number;
4807
- /** Per-reaction rate cap — at most `limit` firings per `windowMs`. */
5795
+ /**
5796
+ * Rate cap — at most `limit` firings per `windowMs`, PER KEY.
5797
+ *
5798
+ * ── Two defects this shape replaces, both reported from production ────────
5799
+ *
5800
+ * It read as a per-key cap and was neither. The runner keyed the limiter on
5801
+ * the REACTION NAME, so one cap covered every row and every tenant the
5802
+ * reaction watched: an app with a hundred tenants got a hundredth of the
5803
+ * throughput it declared, and the busiest tenant starved the rest.
5804
+ *
5805
+ * And the limiter was in-memory, per process. With three replicas the
5806
+ * effective cap was 3×, and nothing about the declaration said so — the same
5807
+ * config produced a different limit depending on how many pods happened to be
5808
+ * running.
5809
+ *
5810
+ * `key` is now REQUIRED to get per-entity behaviour, and the limiter is
5811
+ * DURABLE (a claim in the shared store, the same INSERT-wins arbiter the cron
5812
+ * scheduler uses), so the cap is the cap regardless of replica count.
5813
+ *
5814
+ * Omitting `key` keeps the old GLOBAL meaning — which is a legitimate thing to
5815
+ * want (a cap on a scarce downstream), just not what the old field appeared to
5816
+ * offer.
5817
+ */
4808
5818
  readonly rateLimit?: {
4809
5819
  readonly limit: number;
4810
5820
  readonly windowMs: number;
5821
+ /** Partitions the cap. `(event) => event.new.tenantId` for per-tenant,
5822
+ * `(event) => String(event.new.id)` for per-row. */
5823
+ readonly key?: (event: ReactionEvent) => string;
4811
5824
  };
4812
5825
  }
4813
5826
 
@@ -4815,7 +5828,17 @@ export declare type ReactionOp = 'insert' | 'update' | 'delete';
4815
5828
 
4816
5829
  export declare type ReactionOutcome = 'acted' | 'skipped-op' | 'skipped-when' | 'skipped-dedupe' | 'skipped-ratelimit' | 'skipped-budget';
4817
5830
 
4818
- /** A simple per-key sliding-window rate limiter (in-memory, per process). */
5831
+ /**
5832
+ * A per-key sliding-window rate limiter — IN-MEMORY, PER PROCESS.
5833
+ *
5834
+ * Correct for a single-process deployment and wrong for every other one: with N
5835
+ * replicas the effective cap is N×, silently. It stays as the FALLBACK the
5836
+ * runner uses when no durable claimer is wired (dev on the memory store), and
5837
+ * the runner logs once when it falls back, because "my limit is 3× what I
5838
+ * declared" is not something anyone discovers by reading config.
5839
+ *
5840
+ * Prefer {@link ReactionRunDeps.claimRateSlot}.
5841
+ */
4819
5842
  export declare class ReactionRateLimiter {
4820
5843
  private readonly hits;
4821
5844
  allow(key: string, limit: number, windowMs: number, now: number): boolean;
@@ -4836,6 +5859,14 @@ export declare interface ReactionRunDeps {
4836
5859
  /** Per-tenant AI budget check — true = within budget. Injected (wraps
4837
5860
  * requireAiBudget). Only consulted when `guards.costBudgetUsd` is set. */
4838
5861
  readonly checkBudget?: (tenantId: string | null, budgetUsd: number) => boolean | Promise<boolean>;
5862
+ /**
5863
+ * Atomically claim ONE rate slot. `true` = we got it and may fire.
5864
+ *
5865
+ * Injected by the serve layer over the shared store, so the cap holds across
5866
+ * replicas. Absent → the runner falls back to {@link ReactionRateLimiter},
5867
+ * which is per-process and therefore N× on N replicas.
5868
+ */
5869
+ readonly claimRateSlot?: (key: string) => Promise<boolean>;
4839
5870
  readonly rateLimiter?: ReactionRateLimiter;
4840
5871
  readonly now?: () => number;
4841
5872
  }
@@ -5124,6 +6155,8 @@ export declare interface RegistryTableLike {
5124
6155
  readonly expr: string;
5125
6156
  readonly stored: boolean;
5126
6157
  };
6158
+ /** `crdtText()` column — server-merged on write (see crdtColumns above). */
6159
+ readonly crdtManaged?: boolean;
5127
6160
  }>;
5128
6161
  readonly appliedMixins?: ReadonlyArray<{
5129
6162
  readonly id?: string;
@@ -5546,6 +6579,9 @@ export declare interface ResourcePolicy {
5546
6579
  readonly implies?: Readonly<Record<string, ReadonlyArray<string>>>;
5547
6580
  }
5548
6581
 
6582
+ /** The full ordered list of result variants for a definition, holdout last. */
6583
+ export declare const resultVariantNames: (def: ExperimentDefinition) => ReadonlyArray<string>;
6584
+
5549
6585
  export { RetentionSpec }
5550
6586
 
5551
6587
  export { retentionTtlMsFromEnv }
@@ -5605,6 +6641,10 @@ declare type Row_4 = Readonly<Record<string, unknown>>;
5605
6641
 
5606
6642
  declare type Row_5 = Readonly<Record<string, unknown>>;
5607
6643
 
6644
+ declare type Row_6 = Readonly<Record<string, unknown>>;
6645
+
6646
+ declare type Row_7 = Readonly<Record<string, unknown>>;
6647
+
5608
6648
  export declare interface RowDiff {
5609
6649
  readonly table: string;
5610
6650
  readonly op: 'insert' | 'update' | 'delete';
@@ -6273,6 +7313,12 @@ export declare interface SchemaInfo {
6273
7313
  * them and the dialect rejects an explicit value.
6274
7314
  */
6275
7315
  readonly generatedColumns: ReadonlySet<string>;
7316
+ /**
7317
+ * CRDT-managed columns (`crdtText()`). The MutationStore folds an incoming
7318
+ * encoded update into the stored state (`mergeCrdtStates`) before the write,
7319
+ * so concurrent clients converge. Empty set for the common non-CRDT table.
7320
+ */
7321
+ readonly crdtColumns: ReadonlySet<string>;
6276
7322
  /**
6277
7323
  * Columns that a query predicate can hit and benefit from an index:
6278
7324
  * - column-level `.index()`-flagged columns
@@ -6321,6 +7367,10 @@ export declare interface SchemaRegistry {
6321
7367
  * optional half. */
6322
7368
  hasExpires(table: string): boolean;
6323
7369
  hasTenant(table: string): boolean;
7370
+ /** True when the table carries `localFirst()` — it is mirrored to the client,
7371
+ * synced bi-directionally, and its `crdtText()` fields converge via CRDT
7372
+ * merge. The discovery signal for the client-sync-set builder. */
7373
+ hasLocalFirst(table: string): boolean;
6324
7374
  /** Field-existence check — used to skip stamping a column that the
6325
7375
  * table doesn't actually declare (extra defensive). */
6326
7376
  hasField(table: string, field: string): boolean;
@@ -6362,6 +7412,12 @@ export declare interface SchemaRegistry {
6362
7412
  * Postgres reject an explicit value.
6363
7413
  */
6364
7414
  generatedColumns(table: string): ReadonlySet<string>;
7415
+ /**
7416
+ * CRDT-managed columns (`crdtText()`) for a table. Empty set if none.
7417
+ * Read by the MutationStore write path to run the authoritative server-side
7418
+ * CRDT merge (`mergeCrdtStates`) before an INSERT/UPDATE persists.
7419
+ */
7420
+ crdtColumns(table: string): ReadonlySet<string>;
6365
7421
  /**
6366
7422
  * Optional pre-INSERT schema decoder for a table. `undefined` when
6367
7423
  * the table didn't ship `.validate(schema)`. The MutationStore runs
@@ -6468,6 +7524,13 @@ export declare interface ServeRequestContext {
6468
7524
  readonly rowFilter?: RowFilterScope;
6469
7525
  }
6470
7526
 
7527
+ /**
7528
+ * The `.serverOnly()` column names of a table by NAME, or `[]` when the table is
7529
+ * not registered (a computed query, a raw-SQL descriptor, an unknown source) —
7530
+ * an unknown table strips nothing rather than throwing.
7531
+ */
7532
+ export declare const serverOnlyColumnNames: (tableName: string) => ReadonlyArray<string>;
7533
+
6471
7534
  /** One leak: a wire query that declares a serverOnly column in its output. */
6472
7535
  export declare interface ServerOnlyLeak {
6473
7536
  readonly query: string;
@@ -6490,6 +7553,18 @@ export declare type ServeRpcInterceptor = (base: Effect.Effect<unknown, unknown,
6490
7553
  readonly subject: unknown;
6491
7554
  readonly traceId: string;
6492
7555
  readonly spanId?: string;
7556
+ /**
7557
+ * The declared write-target TABLE names of this mutation/action, derived
7558
+ * from the descriptor's `target:` block. Absent when the descriptor
7559
+ * declares no target (and always absent for queries).
7560
+ *
7561
+ * Carried so an interceptor — the audit sink is the motivating consumer —
7562
+ * can see WHICH tables an operation writes without reaching for the
7563
+ * descriptor registry. Purely additive: an interceptor that ignores it is
7564
+ * unaffected. It is the descriptor's DECLARED target, not the rows actually
7565
+ * written; a change-set carry (the rows/ids) is a separate, heavier seam.
7566
+ */
7567
+ readonly target?: ReadonlyArray<string>;
6493
7568
  }) => Effect.Effect<unknown, unknown, never>;
6494
7569
 
6495
7570
  /** Register the process-wide connection resolver (or clear with `undefined`).
@@ -6780,6 +7855,45 @@ declare const StoreOperationFailed_base: Schema.TaggedErrorClass<StoreOperationF
6780
7855
  cause: typeof Schema.String;
6781
7856
  }>;
6782
7857
 
7858
+ /**
7859
+ * Drop `columns` from every row. Preserves array AND per-row object identity when
7860
+ * nothing is actually removed: an empty column set returns the input array
7861
+ * unchanged, and a row that carries none of the columns is returned as-is. That
7862
+ * identity preservation is load-bearing for the dispatcher's diff memo, which
7863
+ * keys on `prev` object identity — a gratuitous clone would defeat it.
7864
+ */
7865
+ export declare const stripColumnsFromRows: <R extends Row>(rows: ReadonlyArray<R>, columns: ReadonlyArray<string>) => ReadonlyArray<R>;
7866
+
7867
+ /**
7868
+ * Strip a source table's columns this SUBJECT may not see — `.serverOnly()` ∪
7869
+ * the `.readableBy(...)` columns the subject lacks a scope for — from a row set
7870
+ * produced for the wire. The subject-aware counterpart of
7871
+ * {@link stripServerOnlyForWire}; the two share the same value-level
7872
+ * {@link stripColumnsFromRows} pass (and its array/row identity preservation),
7873
+ * so a table with no `.serverOnly()` and no `.readableBy(...)` column returns the
7874
+ * input array unchanged for every subject.
7875
+ *
7876
+ * Same ACCEPTED GAP as `stripServerOnlyForWire`: only TOP-LEVEL columns of
7877
+ * `tableName` are matched — an eager-loaded relation or a renamed projection
7878
+ * changes the shape and is not covered here.
7879
+ */
7880
+ export declare const stripForbiddenForWire: <R extends Row>(tableName: string, rows: ReadonlyArray<R>, subject: Subject) => ReadonlyArray<R>;
7881
+
7882
+ /**
7883
+ * Strip a source table's `.serverOnly()` columns from a row set produced for the
7884
+ * wire. The runtime half of the exposure policy: declare `.serverOnly()` ONCE at
7885
+ * the schema and every query/subscription output respects it, regardless of
7886
+ * whether the handler was hand-written or a `crud.*` helper.
7887
+ *
7888
+ * ACCEPTED GAP (documented): only TOP-LEVEL columns of `tableName` are matched.
7889
+ * An eager-loaded relation nests another table's columns under a relation key,
7890
+ * and a computed/renamed projection changes the shape — neither is covered here
7891
+ * (mark the nested column `.serverOnly()` on ITS table and read it through a
7892
+ * `crud.*`/redacted path, or omit it from the output schema so the boot audit
7893
+ * blocks it). The marker + `crud.*` path covers the common flat-row case.
7894
+ */
7895
+ export declare const stripServerOnlyForWire: <R extends Row>(tableName: string, rows: ReadonlyArray<R>) => ReadonlyArray<R>;
7896
+
6783
7897
  export declare interface SubscribeContext {
6784
7898
  /** Logger scoped to the subscriber file (`subscribe:<filename>`). */
6785
7899
  readonly log: SyncLogger;
@@ -7037,6 +8151,22 @@ declare const TableValidationFailed_base: Schema.TaggedErrorClass<TableValidatio
7037
8151
  }>>;
7038
8152
  }>;
7039
8153
 
8154
+ /** The standing compute-cost attribution for one tenant — the chargeback /
8155
+ * showback row + the "why is my bill high" breakdown. Maintained incrementally:
8156
+ * every `CostEvent` folds into `total`, `byUnit`, and `bySubscription` in O(1).
8157
+ * Cumulative since boot (or since the last `reset(tenantId)`). */
8158
+ export declare interface TenantCostState {
8159
+ readonly tenantId: string | null;
8160
+ /** Grand total across every unit. */
8161
+ readonly total: number;
8162
+ /** Per-unit breakdown (`recompute` → n, `query` → n, …). */
8163
+ readonly byUnit: Readonly<Record<string, number>>;
8164
+ /** Per-subscription breakdown — the per-subscription attribution. Only
8165
+ * subscription-tagged events contribute. */
8166
+ readonly bySubscription: Readonly<Record<string, number>>;
8167
+ readonly lastUpdatedAt: number | null;
8168
+ }
8169
+
7040
8170
  /**
7041
8171
  * A write was attempted against a `tenant()`-scoped table, but the
7042
8172
  * authenticated subject's `tenantId` is null — either anonymous, or
@@ -7138,6 +8268,14 @@ export declare interface TopNQuery {
7138
8268
  readonly tenantId?: string | null;
7139
8269
  }
7140
8270
 
8271
+ /** A row a mutation wrote, carried to rule evaluation. `row` is the POST-WRITE
8272
+ * state (insert result / update result). */
8273
+ declare interface TouchedRow {
8274
+ readonly table: string;
8275
+ readonly id: string;
8276
+ readonly row: AnyRow;
8277
+ }
8278
+
7141
8279
  export declare interface TraceContext {
7142
8280
  readonly traceId: string;
7143
8281
  readonly spanId: string;
@@ -7472,6 +8610,18 @@ export declare const useAggregate: <Row>(def: AggregateDefinition<Row>) => Effec
7472
8610
  */
7473
8611
  export declare const useAnalytics: () => Effect.Effect<AnalyticsSinkImpl, never, AnalyticsSink>;
7474
8612
 
8613
+ /** Sugar: read one budget's current state for a tenant from a handler. Returns
8614
+ * `null` when the budget name is not registered (none attached, or a typo). */
8615
+ export declare const useCostBudget: (def: CostBudgetDefinition, tenantId: string | null) => Effect.Effect<CostBudgetState | null, never, CostRegistry>;
8616
+
8617
+ /** Sugar: read one expectation's current state from a handler. Returns `null`
8618
+ * when the name is not registered (no expectations attached, or a typo). */
8619
+ export declare const useExpectation: (def: ExpectationDefinition) => Effect.Effect<ExpectationState | null, never, ExpectationRegistry>;
8620
+
8621
+ /** Sugar: read one experiment's current result from a handler. Returns `null`
8622
+ * when the name is not registered (none attached, or a typo). */
8623
+ export declare const useExperiment: (def: ExperimentDefinition) => Effect.Effect<ExperimentResult | null, never, ExperimentRegistry>;
8624
+
7475
8625
  declare type VaultStore = Pick<DataStore, 'query' | 'insert' | 'update' | 'updateMany' | 'delete' | 'upsert'>;
7476
8626
 
7477
8627
  /** The pure core of live revocation: given a subject + action + a row-set,
@@ -7496,6 +8646,10 @@ export declare const VOLTRO_AUDIT_MIXIN_ID: "voltro/audit";
7496
8646
  * consumer). Kept in step by `expiresMixinId.test.ts`. */
7497
8647
  export declare const VOLTRO_EXPIRES_MIXIN_ID: "voltro/expires";
7498
8648
 
8649
+ /** Re-declared here for the same reason as the ids above (no mixin import).
8650
+ * Kept in step by `localFirstMixinId.test.ts`. */
8651
+ export declare const VOLTRO_LOCAL_FIRST_MIXIN_ID: "voltro/localFirst";
8652
+
7499
8653
  export declare const VOLTRO_SOFT_DELETE_MIXIN_ID: "voltro/softDelete";
7500
8654
 
7501
8655
  export declare const VOLTRO_TENANT_MIXIN_ID: "voltro/tenant";
@@ -7689,6 +8843,44 @@ export declare type WhereOp = '=' | '!=' | '<>' | '>' | '>=' | '<' | '<=' | 'in'
7689
8843
  */
7690
8844
  export declare const withRowFilter: <R extends ServeRequestContext>(request: R) => Promise<R>;
7691
8845
 
8846
+ /**
8847
+ * What the gate decided. `passthrough` is the common case — no controls
8848
+ * declared — and is distinct from `start` so the facade can skip the commit
8849
+ * bookkeeping entirely rather than calling a no-op.
8850
+ */
8851
+ export declare type WorkflowAdmissionOutcome = {
8852
+ readonly kind: 'passthrough';
8853
+ } | {
8854
+ readonly kind: 'start';
8855
+ /** What to start WITH. Differs from the caller's payload only for a batch
8856
+ * that filled up on this arrival. */
8857
+ readonly payload: unknown;
8858
+ /** Called once the engine returns an execution id: links the ledger row,
8859
+ * consumes any batched intents, evicts a singleton incumbent. */
8860
+ readonly commit: (executionId: string) => Promise<void>;
8861
+ } | {
8862
+ readonly kind: 'queued';
8863
+ readonly intentId: string;
8864
+ readonly mode: string;
8865
+ readonly dueAt: number;
8866
+ } | {
8867
+ readonly kind: 'dropped';
8868
+ readonly retryAfterMs: number;
8869
+ } | {
8870
+ readonly kind: 'skipped';
8871
+ readonly executionId: string;
8872
+ };
8873
+
8874
+ export declare interface WorkflowAdmissionRequest {
8875
+ readonly workflowName: string;
8876
+ readonly payload: unknown;
8877
+ readonly callerContext: WorkflowCallerContext | undefined;
8878
+ /** `true` for `run(...)` and `start(..., { wait: true })` — a caller blocking
8879
+ * for the RESULT. A deferring control has no coherent answer for one, so the
8880
+ * gate refuses it by name rather than doing something surprising. */
8881
+ readonly waiting: boolean;
8882
+ }
8883
+
7692
8884
  export declare interface WorkflowCallerContext {
7693
8885
  readonly subject?: unknown;
7694
8886
  readonly traceId?: string | null;
@@ -7703,6 +8895,19 @@ export declare interface WorkflowChildOptions {
7703
8895
  /* Excluded from this release type: callerContext */
7704
8896
  }
7705
8897
 
8898
+ /**
8899
+ * Reject a blocking caller on a workflow whose flow control can DEFER.
8900
+ *
8901
+ * `run(...)` and `start({ wait: true })` block for the run's RESULT, and there
8902
+ * is no result to return for a start that was collapsed into a future run.
8903
+ * Silently starting it anyway would break the declared limit; silently
8904
+ * returning nothing would break the caller's type. So it is an error that names
8905
+ * both halves.
8906
+ */
8907
+ export declare class WorkflowDeferredResultError extends Error {
8908
+ constructor(workflowName: string, method: string, mode: string);
8909
+ }
8910
+
7706
8911
  export declare interface WorkflowDefinitionLike {
7707
8912
  readonly name: string;
7708
8913
  readonly payloadSchema: Schema.Schema.Any;
@@ -7790,6 +8995,17 @@ export declare interface WorkflowFacadeOptions {
7790
8995
  * waiting for its next storage tick. No-op when unset (single replica / no
7791
8996
  * broker → the poll interval covers it). */
7792
8997
  readonly onEnqueue?: () => void;
8998
+ /**
8999
+ * Declarative flow control — the admission boundary every start passes
9000
+ * through when the workflow declares `debounce` / `singleton` / `concurrency`
9001
+ * / `throttle` / `rateLimit` / `batch`, or when an operator paused it.
9002
+ *
9003
+ * Injected, and absent by default, for the same reason `listRuns` is: the
9004
+ * runtime stays storage-agnostic and the CLI owns the tables. Absent ⇒ every
9005
+ * start takes exactly the path it took before flow control existed, which is
9006
+ * what makes this feature free for a workflow that declares none.
9007
+ */
9008
+ readonly admitStart?: (input: WorkflowAdmissionRequest) => Promise<WorkflowAdmissionOutcome>;
7793
9009
  }
7794
9010
 
7795
9011
  export declare interface WorkflowLayerExecutionContext {
@@ -7830,6 +9046,20 @@ export declare class WorkflowPayloadError extends Error {
7830
9046
  missingFields: ReadonlyArray<string>, detail: string);
7831
9047
  }
7832
9048
 
9049
+ /**
9050
+ * A blocking caller hit a `rateLimit` cap.
9051
+ *
9052
+ * `start()` reports a drop through the handle, because a fire-and-forget caller
9053
+ * has somewhere to put it. `run()` has no such place — its return type is the
9054
+ * workflow's success value — so the drop has to be an error, or it would look
9055
+ * like a run that returned `undefined`.
9056
+ */
9057
+ export declare class WorkflowRateLimitedError extends Error {
9058
+ readonly workflowName: string;
9059
+ readonly retryAfterMs: number;
9060
+ constructor(workflowName: string, retryAfterMs: number);
9061
+ }
9062
+
7833
9063
  /** Result of {@link WorkflowsAppContext.redrive} — whether the failed run's
7834
9064
  * durable journal was re-driven, how many failed step attempts were reset so
7835
9065
  * they re-execute, and a `reason` when it declined (no journal / still
@@ -7861,6 +9091,24 @@ export declare interface WorkflowRunListFilter {
7861
9091
  readonly workflowName?: string;
7862
9092
  readonly tag?: string;
7863
9093
  readonly status?: WorkflowRunRecordStatus;
9094
+ /** Several statuses at once (`['failed', 'cancelled']`). Ignored when the
9095
+ * single `status` is also set — one of them has to win, and the singular,
9096
+ * older spelling is the one existing callers already rely on. */
9097
+ readonly statuses?: ReadonlyArray<WorkflowRunRecordStatus>;
9098
+ /** Case-insensitive substring match on the workflow tag — the search-box
9099
+ * semantic, where `tag`/`workflowName` are exact. */
9100
+ readonly tagContains?: string;
9101
+ /** Exact match on the run's recorded `source` (`workflow-rpc`,
9102
+ * `app-context`, `inspect`, `schedule:<name>`, …). */
9103
+ readonly source?: string;
9104
+ /** Prefix match against the run id OR the execution id — what a human
9105
+ * pastes from a log line. Case-sensitive, because ids are. */
9106
+ readonly idPrefix?: string;
9107
+ /** Only runs started at/after this instant. With `startedBefore` this is
9108
+ * the time-range view; each bound works alone too. */
9109
+ readonly startedAfter?: Date;
9110
+ /** Only runs started strictly before this instant. */
9111
+ readonly startedBefore?: Date;
7864
9112
  /** The DEAD-LETTER view: failed runs an operator has NOT yet discarded
7865
9113
  * (`status = 'failed' AND discardedAt IS NULL`). Since the framework applies no
7866
9114
  * retry, a `failed` run is terminal — this is the queue of unhandled failures.
@@ -7967,6 +9215,14 @@ export declare interface WorkflowSignalTarget {
7967
9215
  readonly workflowName?: string;
7968
9216
  }
7969
9217
 
9218
+ /** A blocking caller lost a `singleton: { mode: 'skip' }` race. Carries the
9219
+ * incumbent's execution id, so the caller can wait on THAT run instead. */
9220
+ export declare class WorkflowSingletonHeldError extends Error {
9221
+ readonly workflowName: string;
9222
+ readonly executionId: string;
9223
+ constructor(workflowName: string, executionId: string);
9224
+ }
9225
+
7970
9226
  export declare interface WorkflowStartOptions {
7971
9227
  readonly wait?: boolean;
7972
9228
  /* Excluded from this release type: callerContext */
@@ -7975,8 +9231,14 @@ export declare interface WorkflowStartOptions {
7975
9231
  export declare const workflowToRpc: (workflow: WorkflowDefinitionLike) => Rpc.Rpc<string, Schema.Schema.Any, Schema.Struct<{
7976
9232
  id: typeof Schema.String;
7977
9233
  workflowName: typeof Schema.String;
7978
- executionId: typeof Schema.String;
7979
- status: Schema.Literal<["running"]>;
9234
+ executionId: Schema.NullOr<typeof Schema.String>;
9235
+ status: Schema.Literal<["running", "queued", "dropped", "skipped"]>;
9236
+ deferral: Schema.optional<Schema.Struct<{
9237
+ mode: typeof Schema.String;
9238
+ dueAt: Schema.NullOr<typeof Schema.Number>;
9239
+ retryAfterMs: Schema.NullOr<typeof Schema.Number>;
9240
+ intentId: Schema.NullOr<typeof Schema.String>;
9241
+ }>>;
7980
9242
  }>, Schema.Schema.All, never>;
7981
9243
 
7982
9244
  export declare interface WorkflowUpdateOptions {