@voltro/runtime 0.28.0 → 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.
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;
@@ -1770,6 +1860,206 @@ export declare interface Coordinator {
1770
1860
  tryClaim(scheduleName: string, scheduledAt: Date): Promise<boolean>;
1771
1861
  }
1772
1862
 
1863
+ /**
1864
+ * The standing compute-cost accountant. Fold `CostEvent`s in with `record`;
1865
+ * read attribution + budget state through the getters; roll windows with `tick`.
1866
+ * Every method is synchronous and allocation-light — it is on the reactive
1867
+ * recompute hot path.
1868
+ */
1869
+ export declare class CostAccountant {
1870
+ private readonly now;
1871
+ private readonly attribution;
1872
+ private readonly counters;
1873
+ private readonly budgetsByName;
1874
+ private readonly totalBudgets;
1875
+ private readonly unitBudgets;
1876
+ constructor(definitions: ReadonlyArray<CostBudgetDefinition>, deps?: CostAccountantDeps);
1877
+ private counterFor;
1878
+ /** Attribute one cost event and evaluate every budget that watches its unit.
1879
+ * Returns the threshold-crossing signals produced (empty in the common,
1880
+ * no-crossing case). */
1881
+ record(event: CostEvent): ReadonlyArray<CostBudgetSignal>;
1882
+ /** Roll every windowed budget whose window has elapsed, with no event —
1883
+ * a budget recovers when its window passes even if the tenant went quiet.
1884
+ * Returns the `recovered` signals produced. */
1885
+ tick(): ReadonlyArray<CostBudgetSignal>;
1886
+ /** Reset attribution + every budget counter for one tenant, or ALL tenants
1887
+ * when `tenantId` is `undefined` (the system tenant is `null`, and IS
1888
+ * targetable). Returns the `recovered` signals for any budget that was
1889
+ * breached. Use for a chargeback-period rollover the windows don't express,
1890
+ * or a test reset. */
1891
+ reset(tenantId?: string | null): ReadonlyArray<CostBudgetSignal>;
1892
+ private allCounterTenants;
1893
+ attributionSnapshot(): ReadonlyArray<TenantCostState>;
1894
+ tenantState(tenantId: string | null): TenantCostState | null;
1895
+ budgetStates(): ReadonlyArray<CostBudgetState>;
1896
+ budgetState(name: string, tenantId: string | null): CostBudgetState | null;
1897
+ breaches(): ReadonlyArray<CostBudgetState>;
1898
+ }
1899
+
1900
+ export declare interface CostAccountantDeps {
1901
+ /** Wall clock. Injectable for deterministic tests. Default `Date.now`. */
1902
+ readonly now?: () => number;
1903
+ readonly log?: SyncLogger;
1904
+ }
1905
+
1906
+ export declare interface CostBudgetDefinition {
1907
+ /** Brand marker — lets the cli discriminate default-exported budget
1908
+ * definitions from arbitrary objects during file discovery. */
1909
+ readonly _voltroCostBudget: true;
1910
+ readonly name: string;
1911
+ readonly limit: number;
1912
+ /** Resolved cost unit, or `null` for a total-across-all-units budget. */
1913
+ readonly unit: string | null;
1914
+ readonly warnAt: number;
1915
+ /** Resolved tumbling-window length in ms, or `null` for a cumulative budget. */
1916
+ readonly windowMs: number | null;
1917
+ readonly severity: CostBudgetSeverity;
1918
+ readonly description?: string;
1919
+ }
1920
+
1921
+ export declare interface CostBudgetDefinitionInput {
1922
+ /** Stable id within the app. Unique across all cost budgets. */
1923
+ readonly name: string;
1924
+ /** The per-tenant ceiling, in the budget's `unit` (or the tenant's grand
1925
+ * total when `unit` is omitted). A tenant crossing this is `exceeded`. */
1926
+ readonly limit: number;
1927
+ /** Which cost unit this budget meters. Omitted ⇒ the tenant's TOTAL across
1928
+ * every unit (the common "total compute per tenant" cap). When set, only
1929
+ * `CostEvent`s of this unit count toward the budget. */
1930
+ readonly unit?: string;
1931
+ /** Fraction of `limit` (0..1) at which the budget goes `warn` — the early
1932
+ * signal before the hard cap. Default `0.8`. Set `1` to disable the warn
1933
+ * band (straight ok→exceeded). */
1934
+ readonly warnAt?: number;
1935
+ /** Tumbling window as an interval string (`'1h'`, `'24h'`). The per-tenant
1936
+ * counter resets at each window boundary (and a breached budget RECOVERS).
1937
+ * Omitted ⇒ a cumulative budget that only resets on an explicit
1938
+ * `reset(tenantId)` — the since-boot chargeback ceiling. */
1939
+ readonly window?: string;
1940
+ /** Alerting priority carried on the breach signal. Default `'warn'`. */
1941
+ readonly severity?: CostBudgetSeverity;
1942
+ /** Human-facing note surfaced in devtools / the breach message. */
1943
+ readonly description?: string;
1944
+ }
1945
+
1946
+ /** How loud a budget breach is. A classification carried on the signal for
1947
+ * alerting priority — the framework never BLOCKS compute on a budget here
1948
+ * (that is a caller's choice, the way `requireAiBudget` fails a call); a cost
1949
+ * budget is an observability-grade signal over work that already happened. */
1950
+ export declare type CostBudgetSeverity = 'info' | 'warn' | 'critical';
1951
+
1952
+ /** Emitted when a budget crosses a threshold for a tenant. `warn` / `exceeded`
1953
+ * are upward crossings (event-driven, with a cause); `recovered` is a window
1954
+ * rollover / reset back to `ok` (no cause). */
1955
+ export declare interface CostBudgetSignal {
1956
+ readonly kind: 'warn' | 'exceeded' | 'recovered';
1957
+ readonly state: CostBudgetState;
1958
+ readonly cause: CostCause | null;
1959
+ }
1960
+
1961
+ /** The live state of one budget for one tenant — the row an inspect endpoint /
1962
+ * dashboard reads. */
1963
+ export declare interface CostBudgetState {
1964
+ readonly budget: string;
1965
+ readonly tenantId: string | null;
1966
+ readonly status: CostBudgetStatus;
1967
+ readonly severity: CostBudgetSeverity;
1968
+ /** The metered cost against this budget in the current window. */
1969
+ readonly spent: number;
1970
+ /** The hard ceiling. */
1971
+ readonly limit: number;
1972
+ /** The absolute warn ceiling (`warnAt * limit`). */
1973
+ readonly warnThreshold: number;
1974
+ /** The unit this budget meters, or `null` for a total-across-units budget. */
1975
+ readonly unit: string | null;
1976
+ /** When the current window opened (windowed budgets), else `null`. */
1977
+ readonly windowStartedAt: number | null;
1978
+ /** When this budget last entered its current `status`. */
1979
+ readonly since: number | null;
1980
+ readonly lastUpdatedAt: number | null;
1981
+ /** The most recent breach's cause, retained across recovery for the audit
1982
+ * trail. `null` if it has never been breached. */
1983
+ readonly lastCause: CostCause | null;
1984
+ readonly description?: string;
1985
+ }
1986
+
1987
+ /** A budget's status for one tenant. `ok` below the warn band, `warn` at/over
1988
+ * `warnAt * limit`, `exceeded` at/over `limit`. Monotonic within a window;
1989
+ * resets to `ok` on window rollover / explicit reset. */
1990
+ export declare type CostBudgetStatus = 'ok' | 'warn' | 'exceeded';
1991
+
1992
+ /** What incurred the cost that tipped a budget across a threshold — resolved
1993
+ * from the `CostEvent`. `null` when the transition was window-driven (a budget
1994
+ * RECOVERING because its window rolled over, with no event). */
1995
+ export declare interface CostCause {
1996
+ readonly unit: string;
1997
+ readonly subscriptionId?: string | null;
1998
+ readonly procedure?: string | null;
1999
+ readonly traceId?: string | null;
2000
+ }
2001
+
2002
+ /**
2003
+ * One unit of attributed compute. A compute site (a reactive recompute in the
2004
+ * dispatcher, a query, a fan-out delivery, or any pluggable cost source) emits
2005
+ * one of these tagged with the tenant/subscription that caused it, and the
2006
+ * accountant folds it into the standing per-tenant total AND every budget that
2007
+ * watches its `unit`.
2008
+ *
2009
+ * `amount` is a NORMALISED cost figure in whatever unit the caller chose — 1 per
2010
+ * recompute, rows scanned, milliseconds elapsed, a priced micro-USD. The
2011
+ * framework does not impose a cost model; `unit` names which one this event is
2012
+ * denominated in, and a budget either targets that named unit or the tenant's
2013
+ * grand total. This is the "pluggable cost unit" seam.
2014
+ */
2015
+ export declare interface CostEvent {
2016
+ /** Active org id the compute is attributed to. `null` = system / untenanted
2017
+ * work (a schedule, a resumed workflow — `SYSTEM_SUBJECT.tenantId`). */
2018
+ readonly tenantId: string | null;
2019
+ /** The live subscription that drove this compute, when the cost came from a
2020
+ * reactive recompute — the per-subscription attribution the plan calls for.
2021
+ * Absent for non-subscription compute (a one-shot query). */
2022
+ readonly subscriptionId?: string | null;
2023
+ /** Which cost unit `amount` is denominated in — `'recompute'`, `'query'`,
2024
+ * `'fanout'`, or any custom unit the caller meters. */
2025
+ readonly unit: string;
2026
+ /** The normalised cost contribution in `unit`. Non-finite / negative amounts
2027
+ * are ignored by the accountant (a cost cannot be negative). */
2028
+ readonly amount: number;
2029
+ /** The rpc tag of the call that incurred the cost, for the "why is my bill
2030
+ * high" breakdown + the breach cause. */
2031
+ readonly procedure?: string | null;
2032
+ /** The trace the offending compute ran under — joins to `voltro logs --trace`. */
2033
+ readonly traceId?: string | null;
2034
+ /** Wall-clock ms the cost was incurred. Default: the accountant's clock. */
2035
+ readonly at?: number;
2036
+ }
2037
+
2038
+ /**
2039
+ * Framework-provided registry of per-tenant compute-cost attribution + every
2040
+ * budget the cli discovered. Handler / inspect code reads the live signal here,
2041
+ * exactly as it reads `ExpectationRegistry`.
2042
+ */
2043
+ export declare class CostRegistry extends CostRegistry_base {
2044
+ }
2045
+
2046
+ declare const CostRegistry_base: Context.TagClass<CostRegistry, "@voltro/CostRegistry", CostRegistryApi>;
2047
+
2048
+ export declare interface CostRegistryApi {
2049
+ /** Per-tenant compute-cost attribution — the chargeback feed. */
2050
+ readonly attribution: () => ReadonlyArray<TenantCostState>;
2051
+ /** One tenant's attribution, or `null` when nothing has been attributed. */
2052
+ readonly tenant: (tenantId: string | null) => TenantCostState | null;
2053
+ /** Every (budget, tenant) state — the full budget snapshot. */
2054
+ readonly budgets: () => ReadonlyArray<CostBudgetState>;
2055
+ /** One budget's state for one tenant, or `null` when unseen. */
2056
+ readonly budget: (name: string, tenantId: string | null) => CostBudgetState | null;
2057
+ /** Only the currently warn/exceeded budget states — the alerting view. */
2058
+ readonly breaches: () => ReadonlyArray<CostBudgetState>;
2059
+ /** Subscribe to budget threshold crossings. Returns an unsubscribe fn. */
2060
+ readonly subscribe: (listener: (s: CostBudgetSignal) => void) => () => void;
2061
+ }
2062
+
1773
2063
  /** Define a cumulative counter. Increment with `incrementMetric` / `Metric.increment`. */
1774
2064
  export declare const counter: (name: string, description?: string) => Metric.Metric.Counter<number>;
1775
2065
 
@@ -2005,6 +2295,32 @@ export declare const DEFAULT_REFRESH_SKEW_MS = 60000;
2005
2295
  */
2006
2296
  export declare const DEFAULT_ROW_FILTER_RETRY: Schedule.Schedule<unknown, unknown>;
2007
2297
 
2298
+ /**
2299
+ * The message of an UNDECLARED throw, bounded, with nothing else attached.
2300
+ *
2301
+ * An executor that throws a plain `TypeError` produces a DEFECT, not a typed
2302
+ * failure — so `catchIf(isInfraError)` above never sees it (that guards the E
2303
+ * channel) and the rpc encoder tried to match it against the descriptor's
2304
+ * `error:` union. It cannot match, by definition: a defect is precisely the
2305
+ * thing that is not in the union. What reached the browser was the entire
2306
+ * decode tree — every union member, the full `ExitEncoded<…>` type, and the
2307
+ * real cause on the last line. ~2 KB of type names, which one consumer's
2308
+ * account page rendered verbatim where a reason belonged, and which every app
2309
+ * otherwise has to condense heuristically to avoid putting a schema on screen.
2310
+ *
2311
+ * `message` ONLY — no stack, no `cause` chain, no own fields. The same
2312
+ * reasoning as `wireErrorFromCause`: a nested object can carry a DSN or a
2313
+ * token. A message is what the server already logged and what a human needs;
2314
+ * the full original stays in the server log via `logHandlerFailure`.
2315
+ *
2316
+ * Note the deliberate asymmetry with `isInfraError`, which collapses to the
2317
+ * generic text instead: a `SqlError`'s message names internal `table.column`
2318
+ * detail, so its text is withheld on purpose. An arbitrary app defect has no
2319
+ * such known shape — withholding it too would leave the app exactly where it
2320
+ * started, with a reason it cannot show.
2321
+ */
2322
+ export declare const defectMessage: (defect: unknown) => string;
2323
+
2008
2324
  /**
2009
2325
  * Construct an aggregate definition. The returned object brands itself
2010
2326
  * so the cli's file-discovery pass picks it up from default exports.
@@ -2043,6 +2359,27 @@ export declare const defineAggregate: <Row>(input: AggregateDefinitionInput<Row>
2043
2359
  */
2044
2360
  export declare const defineConnection: <D extends ConnectionDefinition>(definition: D) => D;
2045
2361
 
2362
+ /**
2363
+ * Declare a per-tenant compute-cost budget. Validates the shape at declaration
2364
+ * time so a malformed budget fails LOUD at boot rather than silently never
2365
+ * firing (the same discipline as `defineExpectation`).
2366
+ *
2367
+ * ```ts
2368
+ * // apps/api/budgets/tenantCompute.budget.ts
2369
+ * import { defineCostBudget } from '@voltro/runtime'
2370
+ *
2371
+ * export default defineCostBudget({
2372
+ * name: 'tenant-recompute-hourly',
2373
+ * unit: 'recompute',
2374
+ * limit: 100_000, // 100k recomputes per tenant per hour
2375
+ * warnAt: 0.8, // warn at 80k
2376
+ * window: '1h',
2377
+ * severity: 'warn',
2378
+ * })
2379
+ * ```
2380
+ */
2381
+ export declare const defineCostBudget: (input: CostBudgetDefinitionInput) => CostBudgetDefinition;
2382
+
2046
2383
  export declare const defineEventTrigger: <EventPayload = unknown, WorkflowPayload = EventPayload>(spec: Omit<WorkflowEventTriggerDefinition<EventPayload, WorkflowPayload>, "_tag"> | (Omit<WorkflowEventTriggerDefinition<EventPayload, WorkflowPayload>, "_tag" | "event"> & {
2047
2384
  /** A declared event. Its `name` becomes the matched event. */
2048
2385
  readonly on: {
@@ -2058,6 +2395,46 @@ export declare const defineEventTrigger: <EventPayload = unknown, WorkflowPayloa
2058
2395
  */
2059
2396
  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
2397
 
2398
+ /**
2399
+ * Declare a data-quality expectation. Validates the shape at declaration time so
2400
+ * a malformed contract fails LOUD at boot rather than silently never firing.
2401
+ *
2402
+ * ```ts
2403
+ * // apps/api/expectations/orderFreshness.expectation.ts
2404
+ * import { defineExpectation } from '@voltro/runtime'
2405
+ *
2406
+ * export default defineExpectation({
2407
+ * name: 'orders-fresh',
2408
+ * on: { table: 'orders' },
2409
+ * invariant: { kind: 'freshness', column: 'createdAt', maxAgeMs: 10 * 60_000 },
2410
+ * severity: 'critical',
2411
+ * })
2412
+ * ```
2413
+ */
2414
+ export declare const defineExpectation: (input: ExpectationDefinitionInput) => ExpectationDefinition;
2415
+
2416
+ /**
2417
+ * Declare an online experiment. Validates the shape at declaration time so a
2418
+ * malformed experiment fails LOUD at boot rather than silently never producing a
2419
+ * result (the same discipline as `defineExpectation` / `defineCostBudget`).
2420
+ *
2421
+ * ```ts
2422
+ * // apps/api/experiments/checkoutColor.experiment.ts
2423
+ * import { defineExperiment } from '@voltro/runtime'
2424
+ *
2425
+ * export default defineExperiment({
2426
+ * name: 'checkout-button-color',
2427
+ * on: { table: 'orders' },
2428
+ * subject: 'userId', // stable per-user bucketing
2429
+ * variants: [{ name: 'control' }, { name: 'green' }],
2430
+ * holdout: 0.1, // 10% see nothing, for a clean baseline
2431
+ * metric: { kind: 'conversionRate', column: 'completed' },
2432
+ * baseline: 'control',
2433
+ * })
2434
+ * ```
2435
+ */
2436
+ export declare const defineExperiment: (input: ExperimentDefinitionInput) => ExperimentDefinition;
2437
+
2061
2438
  /**
2062
2439
  * Declare who delivers an effect. One per `*.outbox.ts` file.
2063
2440
  *
@@ -2129,6 +2506,21 @@ export { DialectReplicationAdapter }
2129
2506
  * delete → every field `value→undefined`; update → only the changed fields. */
2130
2507
  export declare const diffChange: (c: CdcChange) => RowDiff;
2131
2508
 
2509
+ export declare interface DiscoveredCostBudget {
2510
+ readonly file: string;
2511
+ readonly definition: CostBudgetDefinition;
2512
+ }
2513
+
2514
+ export declare interface DiscoveredExpectation {
2515
+ readonly file: string;
2516
+ readonly definition: ExpectationDefinition;
2517
+ }
2518
+
2519
+ export declare interface DiscoveredExperiment {
2520
+ readonly file: string;
2521
+ readonly definition: ExperimentDefinition;
2522
+ }
2523
+
2132
2524
  export declare interface DiscoveredWorkflowLike<Context = unknown> {
2133
2525
  readonly definition: WorkflowDefinitionLike;
2134
2526
  readonly buildExecute: (ctx: Context) => (payload: never, executionId: string) => Effect.Effect<unknown, unknown, unknown>;
@@ -2266,6 +2658,19 @@ export declare interface DispatcherDependencies {
2266
2658
  * the trace buffer so the dashboards show each data transfer's latency.
2267
2659
  * No-op when absent (tests, embedders that don't trace). */
2268
2660
  readonly recordDelivery?: (delivery: SubscriptionDelivery) => void;
2661
+ /**
2662
+ * Optional reactive-compute cost sink (the finops runner's `record`). Called
2663
+ * once per reactive RECOMPUTE delivery — a change re-ran an affected
2664
+ * subscription and pushed it a delta — with `unit: 'recompute', amount: 1`
2665
+ * attributed to the subscription's tenant. Sibling to `recordDelivery`: same
2666
+ * call sites, a different ledger (chargeback / budget vs latency trace).
2667
+ *
2668
+ * No-op when absent (tests, embedders, apps with no `*.budget.ts`). The hot
2669
+ * path pays nothing in that case — the `CostEvent` is only allocated inside
2670
+ * the `recordCost !== undefined` guard, never eagerly. Wired by both boot
2671
+ * paths ONLY when finops is attached; see `attachFinops`.
2672
+ */
2673
+ readonly recordCost?: (event: CostEvent) => void;
2269
2674
  /** Optional snapshot cache (Layer 3). Absent → queries are never cached;
2270
2675
  * present → subscriptions whose descriptor opted in (a `cacheBinding`
2271
2676
  * is passed to `subscribe`) share + cache their initial snapshot. */
@@ -2402,6 +2807,21 @@ export declare const enterRequestLoader: (loader: DataLoader) => void;
2402
2807
  * Promise-wrapped to satisfy the async contract. */
2403
2808
  export declare const envSecretsBackend: SecretsBackend;
2404
2809
 
2810
+ /**
2811
+ * Evaluate every declared rule of every touched table's rows. Throws the first
2812
+ * error-severity `BusinessRuleViolation` (rolling back the transaction);
2813
+ * warning-severity violations log and continue.
2814
+ *
2815
+ * A predicate that itself throws a `BusinessRuleViolation` (the author built one
2816
+ * directly) is re-thrown as-is; any OTHER thrown value propagates unchanged — a
2817
+ * real failure (a DB error) must not be silently reclassified as a violation.
2818
+ */
2819
+ export declare const evaluateTouchedRules: (params: {
2820
+ readonly touched: ReadonlyArray<TouchedRow>;
2821
+ readonly store: RuleReadStore;
2822
+ readonly subject: unknown;
2823
+ }) => Promise<void>;
2824
+
2405
2825
  /**
2406
2826
  * How many deliveries one client may fall behind before the oldest are dropped.
2407
2827
  *
@@ -2778,6 +3198,451 @@ export declare type ExecutorOutput<D extends ExecutorDescriptor> = Schema.Schema
2778
3198
  */
2779
3199
  export declare type ExecutorReturn<D extends ExecutorDescriptor, E, R> = ExecutorOutput<D> | Promise<ExecutorOutput<D>> | Effect.Effect<ExecutorOutput<D>, E, R> | ReactiveReturn<D>;
2780
3200
 
3201
+ /** The write that tipped an expectation into violation, resolved from the CDC
3202
+ * `ChangeEvent` that caused it — the provenance link the plan calls for. `null`
3203
+ * when the transition was CLOCK-driven (a freshness SLA aging out with no
3204
+ * write) rather than write-driven: the honest answer is "no write caused this;
3205
+ * the ABSENCE of writes did". */
3206
+ export declare interface ExpectationCause {
3207
+ readonly table: string;
3208
+ readonly op: 'insert' | 'update' | 'delete';
3209
+ /** The trace the offending write ran under — joins to `voltro logs --trace`
3210
+ * and the audit sink. */
3211
+ readonly traceId?: string | null;
3212
+ /** The acting identity behind the offending write. */
3213
+ readonly subjectId?: string | null;
3214
+ /** The rpc tag of the call that made the offending write, when carried. */
3215
+ readonly procedure?: string | null;
3216
+ }
3217
+
3218
+ export declare interface ExpectationDefinition {
3219
+ /** Brand marker — lets the cli discriminate default-exported expectation
3220
+ * definitions from arbitrary objects during file discovery. */
3221
+ readonly _voltroExpectation: true;
3222
+ readonly name: string;
3223
+ readonly on: {
3224
+ readonly table: string;
3225
+ readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3226
+ };
3227
+ readonly invariant: ExpectationInvariant;
3228
+ readonly severity: ExpectationSeverity;
3229
+ readonly description?: string;
3230
+ }
3231
+
3232
+ export declare interface ExpectationDefinitionInput {
3233
+ /** Stable id within the app. Unique across all expectations. */
3234
+ readonly name: string;
3235
+ /** The watched table. Its CDC deltas drive re-evaluation. An optional `where`
3236
+ * narrows the population the invariant is asserted over — a pure predicate
3237
+ * evaluated server-side per row (rows failing it are excluded from the
3238
+ * metric). It never leaves the server, so it can be any predicate. */
3239
+ readonly on: {
3240
+ readonly table: string;
3241
+ readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3242
+ };
3243
+ readonly invariant: ExpectationInvariant;
3244
+ /** Alerting priority carried on the violation signal. Default `'warn'`. */
3245
+ readonly severity?: ExpectationSeverity;
3246
+ /** Human-facing note surfaced in devtools / the violation message. */
3247
+ readonly description?: string;
3248
+ }
3249
+
3250
+ /**
3251
+ * Evaluates ONE expectation. Seed once, then feed CDC deltas via `applyChange`
3252
+ * and (for a time-dependent invariant) clock ticks via `tick`. Each call
3253
+ * re-reads the maintained metric, updates the live state, and returns a
3254
+ * transition when the holding/violated boundary is crossed.
3255
+ */
3256
+ export declare class ExpectationEvaluator {
3257
+ private readonly def;
3258
+ private readonly deps;
3259
+ private readonly engine;
3260
+ private readonly now;
3261
+ private seeded;
3262
+ private status;
3263
+ private metric;
3264
+ private threshold;
3265
+ private since;
3266
+ private lastEvaluatedAt;
3267
+ private lastCause;
3268
+ constructor(def: ExpectationDefinition, deps: ExpectationEvaluatorDeps);
3269
+ get timeDependent(): boolean;
3270
+ /** Whether a row is in the asserted population (honours `on.where`). */
3271
+ private inScope;
3272
+ /** Seed the metric from the current base rows and evaluate the initial state.
3273
+ * Never emits a transition (there is no prior state to cross from). */
3274
+ seed(): Promise<void>;
3275
+ /**
3276
+ * Apply one CDC change and re-evaluate. Returns a transition if the change
3277
+ * (or the reshaped population) crossed the boundary. A change to a row that is
3278
+ * out of scope on BOTH images is ignored.
3279
+ */
3280
+ applyChange(event: ChangeEvent): Promise<ExpectationTransition | null>;
3281
+ /** Re-evaluate against the current clock WITHOUT a write — for a
3282
+ * time-dependent invariant (freshness) whose metric ages on its own. Returns
3283
+ * a transition if the clock alone crossed the boundary. */
3284
+ tick(): ExpectationTransition | null;
3285
+ private commitReading;
3286
+ state(): ExpectationState;
3287
+ }
3288
+
3289
+ export declare interface ExpectationEvaluatorDeps {
3290
+ /** Read the full current base rows — used to SEED and to resolve a min/max
3291
+ * rescan (freshness) from the post-commit table. */
3292
+ readonly queryBase: () => Promise<ReadonlyArray<Row_5>>;
3293
+ /** Wall clock. Injectable for deterministic tests. Default `Date.now`. */
3294
+ readonly now?: () => number;
3295
+ readonly log?: SyncLogger;
3296
+ }
3297
+
3298
+ /**
3299
+ * The data-quality invariant an expectation asserts. Each variant is chosen so
3300
+ * it reduces the watched table to a SINGLE incrementally-maintained metric that
3301
+ * rides the IVM engine (or a delta-driven filtered count), then compares that
3302
+ * metric against a threshold:
3303
+ *
3304
+ * - `freshness` — max(column) → `age = now - max`; violated when
3305
+ * `age > maxAgeMs`. Rides the IVM `max` maintainer (incl. its
3306
+ * rescan-on-extreme-delete protocol). TIME-DEPENDENT: it is
3307
+ * re-checked on a clock tick as well as on write, because the
3308
+ * metric ages with the wall clock even when no write arrives —
3309
+ * which is exactly when a freshness SLA matters.
3310
+ * - `nullRate` — fraction of rows whose `column` is null; violated when
3311
+ * `rate > maxRate`. Delta-maintained filtered count.
3312
+ * - `rowCount` — COUNT(*) of the table; violated when out of `[min, max]`.
3313
+ * - `valueBounds` — fraction of rows whose numeric `column` falls outside
3314
+ * `[min, max]` (the distribution / range check). Violated when
3315
+ * that fraction exceeds `maxViolationRate`, or — when no rate
3316
+ * is given — when ANY row is out of bounds.
3317
+ */
3318
+ export declare type ExpectationInvariant = {
3319
+ readonly kind: 'freshness';
3320
+ readonly column: string;
3321
+ readonly maxAgeMs: number;
3322
+ } | {
3323
+ readonly kind: 'nullRate';
3324
+ readonly column: string;
3325
+ readonly maxRate: number;
3326
+ } | {
3327
+ readonly kind: 'rowCount';
3328
+ readonly min?: number;
3329
+ readonly max?: number;
3330
+ } | {
3331
+ readonly kind: 'valueBounds';
3332
+ readonly column: string;
3333
+ readonly min?: number;
3334
+ readonly max?: number;
3335
+ /** When set, the expectation tolerates up to this FRACTION (0..1) of
3336
+ * out-of-bounds rows before firing. Omitted ⇒ a single out-of-bounds row
3337
+ * violates. */
3338
+ readonly maxViolationRate?: number;
3339
+ };
3340
+
3341
+ /**
3342
+ * Framework-provided registry of every expectation the cli discovered + the
3343
+ * evaluator maintains. Handler / inspect code reads the live signal here.
3344
+ */
3345
+ export declare class ExpectationRegistry extends ExpectationRegistry_base {
3346
+ }
3347
+
3348
+ declare const ExpectationRegistry_base: Context.TagClass<ExpectationRegistry, "@voltro/ExpectationRegistry", ExpectationRegistryApi>;
3349
+
3350
+ export declare interface ExpectationRegistryApi {
3351
+ /** Snapshot every registered expectation's current state — the inspect feed. */
3352
+ readonly snapshot: () => ReadonlyArray<ExpectationState>;
3353
+ /** One expectation's state by name, or `null` when unknown. */
3354
+ readonly get: (name: string) => ExpectationState | null;
3355
+ /** Only the currently-violated expectations — the alerting view. */
3356
+ readonly violations: () => ReadonlyArray<ExpectationState>;
3357
+ /** Subscribe to holding/violated transitions. Returns an unsubscribe fn. */
3358
+ readonly subscribe: (listener: (t: ExpectationTransition) => void) => () => void;
3359
+ }
3360
+
3361
+ export declare interface ExpectationRunnerHandle {
3362
+ readonly registry: ExpectationRegistryApi;
3363
+ readonly detach: () => void;
3364
+ readonly registered: ReadonlyArray<string>;
3365
+ /** Test/debug seam: run one clock re-check of the time-dependent
3366
+ * expectations at `nowMs` (or the injected clock) and return the snapshot.
3367
+ * This is what the periodic tick timer calls. */
3368
+ readonly runTick: () => void;
3369
+ }
3370
+
3371
+ /** How loud a violation is. Purely a classification carried on the signal —
3372
+ * the framework never blocks a write on any severity (that is a business
3373
+ * rule's job); severity drives alerting priority downstream. */
3374
+ export declare type ExpectationSeverity = 'info' | 'warn' | 'critical';
3375
+
3376
+ /** The live state of one expectation — the row an inspect endpoint / dashboard
3377
+ * reads. */
3378
+ export declare interface ExpectationState {
3379
+ readonly name: string;
3380
+ readonly table: string;
3381
+ readonly invariant: ExpectationInvariant['kind'];
3382
+ readonly severity: ExpectationSeverity;
3383
+ readonly status: ExpectationStatus;
3384
+ /** The measured metric: freshness → age in ms; nullRate / valueBounds(rate) →
3385
+ * a fraction; rowCount → the count; valueBounds(count) → number of bad rows.
3386
+ * `null` when the metric is undefined over the current data. */
3387
+ readonly metric: number | null;
3388
+ /** The threshold the metric is compared against (for display). */
3389
+ readonly threshold: number | null;
3390
+ /** When the expectation entered its current `status`. */
3391
+ readonly since: Date | null;
3392
+ /** When it was last (re-)evaluated. */
3393
+ readonly lastEvaluatedAt: Date | null;
3394
+ /** The most recent violation's provenance, retained across recovery for the
3395
+ * audit trail. `null` if it has never been violated. */
3396
+ readonly lastCause: ExpectationCause | null;
3397
+ readonly description?: string;
3398
+ }
3399
+
3400
+ /** Whether an expectation currently HOLDS. `unknown` = not yet seeded, or a
3401
+ * metric that is undefined over the current data (e.g. freshness of an empty
3402
+ * table — neither fresh nor stale). Transitions are emitted only into/out of
3403
+ * `violated`. */
3404
+ export declare type ExpectationStatus = 'holding' | 'violated' | 'unknown';
3405
+
3406
+ /** Emitted when an expectation crosses the holding/violated boundary. */
3407
+ export declare interface ExpectationTransition {
3408
+ readonly kind: 'violated' | 'recovered';
3409
+ readonly state: ExpectationState;
3410
+ /** The causing write (present for a write-driven violation; `null` for a
3411
+ * clock-driven one or a recovery). */
3412
+ readonly cause: ExpectationCause | null;
3413
+ }
3414
+
3415
+ /** What caused a live recompute — resolved from the CDC `ChangeEvent`. `null`
3416
+ * for a non-write recompute (a reconcile re-seed). */
3417
+ export declare interface ExperimentCause {
3418
+ readonly table: string;
3419
+ readonly op: 'insert' | 'update' | 'delete';
3420
+ /** The trace the write ran under — joins to `voltro logs --trace`. */
3421
+ readonly traceId?: string | null;
3422
+ /** The acting identity behind the write. */
3423
+ readonly subjectId?: string | null;
3424
+ /** The rpc tag of the call that made the write, when carried. */
3425
+ readonly procedure?: string | null;
3426
+ }
3427
+
3428
+ export declare interface ExperimentDefinition {
3429
+ /** Brand marker — lets the cli discriminate default-exported experiment
3430
+ * definitions from arbitrary objects during file discovery. */
3431
+ readonly _voltroExperiment: true;
3432
+ readonly name: string;
3433
+ readonly on: {
3434
+ readonly table: string;
3435
+ readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3436
+ };
3437
+ /** Resolved to a function: reads the subject and normalises it to a non-empty
3438
+ * string, or `null` when the row carries no subject. */
3439
+ readonly subject: (row: Readonly<Record<string, unknown>>) => string | null;
3440
+ readonly variants: ReadonlyArray<ExperimentVariant>;
3441
+ /** Resolved holdout fraction in [0, 1); `0` when none. */
3442
+ readonly holdout: number;
3443
+ readonly metric: ExperimentMetric;
3444
+ readonly baseline: string;
3445
+ readonly description?: string;
3446
+ }
3447
+
3448
+ export declare interface ExperimentDefinitionInput {
3449
+ /** Stable id within the app. Unique across all experiments. Also the
3450
+ * assignment SALT, so the same subject buckets independently per experiment. */
3451
+ readonly name: string;
3452
+ /** The watched table. Its CDC deltas drive the live recompute. An optional
3453
+ * `where` narrows the population the experiment observes — a pure predicate
3454
+ * evaluated server-side per row (rows failing it are excluded). It never
3455
+ * leaves the server, so it can be any predicate. */
3456
+ readonly on: {
3457
+ readonly table: string;
3458
+ readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3459
+ };
3460
+ /** The stable assignment key extractor. */
3461
+ readonly subject: ExperimentSubject;
3462
+ /** The arms. At least two (a control + one treatment). */
3463
+ readonly variants: ReadonlyArray<ExperimentVariantSpec>;
3464
+ /** Fraction (0..1) of subjects held out ENTIRELY — assigned to the reserved
3465
+ * `holdout` variant and never to a treatment. Omitted ⇒ no holdout. */
3466
+ readonly holdout?: number;
3467
+ /** The success metric maintained per variant. */
3468
+ readonly metric: ExperimentMetric;
3469
+ /** Which variant lift/difference is computed AGAINST. Must name one of
3470
+ * `variants`. Default: the first variant (by convention the control). */
3471
+ readonly baseline?: string;
3472
+ /** Human-facing note surfaced in devtools / the results view. */
3473
+ readonly description?: string;
3474
+ }
3475
+
3476
+ /**
3477
+ * Evaluates ONE experiment. Seed once from the base rows, then feed CDC deltas
3478
+ * via `applyChange`; each call re-maintains the per-variant metric + exposure and
3479
+ * recomputes the live result. Returns an `ExperimentUpdate` for any relevant
3480
+ * write (one whose old OR new image is assigned), else `null`.
3481
+ */
3482
+ export declare class ExperimentEvaluator {
3483
+ private readonly def;
3484
+ private readonly deps;
3485
+ private readonly exposure;
3486
+ private readonly metric;
3487
+ private readonly metricIsCount;
3488
+ private readonly now;
3489
+ private seeded;
3490
+ private lastUpdatedAt;
3491
+ constructor(def: ExperimentDefinition, deps: ExperimentEvaluatorDeps);
3492
+ /** The variant an in-scope row is assigned to, or `null` when the row is out
3493
+ * of the `where` population or carries no subject. */
3494
+ private variantOf;
3495
+ private inScope;
3496
+ /** Seed both maintainers from the current base rows. Never emits (there is no
3497
+ * prior result to compare). */
3498
+ seed(): Promise<void>;
3499
+ /**
3500
+ * Apply one CDC change and recompute. Returns an update when the change was
3501
+ * relevant (assigned on either image), else `null`. A change to a row that is
3502
+ * out of scope on BOTH images is ignored.
3503
+ */
3504
+ applyChange(event: ChangeEvent): ExperimentUpdate | null;
3505
+ /** The live result — per-variant metric + lift/diff vs the baseline. */
3506
+ result(): ExperimentResult;
3507
+ /** Resolve one variant's metric value, filling the "no group yet" gap: `count`
3508
+ * → 0, `sum` → 0 (empty sum), `avg` / `conversionRate` → `null` (undefined
3509
+ * over no samples). */
3510
+ private metricValueFor;
3511
+ get isSeeded(): boolean;
3512
+ }
3513
+
3514
+ export declare interface ExperimentEvaluatorDeps {
3515
+ /** Read the full current base rows — used to SEED and to reconcile. */
3516
+ readonly queryBase: () => Promise<ReadonlyArray<Row_6>>;
3517
+ /** Wall clock. Injectable for deterministic tests. Default `Date.now`. */
3518
+ readonly now?: () => number;
3519
+ readonly log?: SyncLogger;
3520
+ }
3521
+
3522
+ /** The success metric an experiment maintains PER VARIANT. Every kind reduces to
3523
+ * a bounded IVM aggregate grouped by the assigned variant, so it rides the same
3524
+ * incremental engine as `defineAggregate` (no batch re-scan):
3525
+ *
3526
+ * - `count` — rows assigned to the variant. The metric IS the
3527
+ * exposure (sample size).
3528
+ * - `sum` — Σ of a numeric `column` over the variant's rows.
3529
+ * - `avg` — mean of a numeric `column` — the per-observation
3530
+ * average (revenue-per-order, latency, …). Comparable
3531
+ * across variants of unequal size.
3532
+ * - `conversionRate` — the FRACTION of the variant's rows that "converted":
3533
+ * `row[column]` is truthy, or `=== equals` when given.
3534
+ * Modelled as `avg` of a 0/1 projection, so it too is a
3535
+ * live IVM aggregate. The classic A/B success metric.
3536
+ *
3537
+ * `count` / `sum` reflect BOTH exposure and effect, so their cross-variant
3538
+ * comparison only means "more/less total"; `avg` / `conversionRate` are
3539
+ * per-subject rates and are the ones a lift reads honestly. The result always
3540
+ * carries each variant's `exposure`, so an unequal split is visible either way. */
3541
+ export declare type ExperimentMetric = {
3542
+ readonly kind: 'count';
3543
+ } | {
3544
+ readonly kind: 'sum';
3545
+ readonly column: string;
3546
+ } | {
3547
+ readonly kind: 'avg';
3548
+ readonly column: string;
3549
+ } | {
3550
+ readonly kind: 'conversionRate';
3551
+ readonly column: string;
3552
+ /** When set, a row converted iff `row[column] === equals`. Omitted ⇒
3553
+ * converted iff `row[column]` is truthy. */
3554
+ readonly equals?: unknown;
3555
+ };
3556
+
3557
+ export declare type ExperimentMetricKind = ExperimentMetric['kind'];
3558
+
3559
+ /**
3560
+ * Framework-provided registry of every experiment the cli discovered + the
3561
+ * evaluator maintains. Handler / inspect code reads the live results here,
3562
+ * exactly as it reads `ExpectationRegistry` / `CostRegistry`.
3563
+ */
3564
+ export declare class ExperimentRegistry extends ExperimentRegistry_base {
3565
+ }
3566
+
3567
+ declare const ExperimentRegistry_base: Context.TagClass<ExperimentRegistry, "@voltro/ExperimentRegistry", ExperimentRegistryApi>;
3568
+
3569
+ export declare interface ExperimentRegistryApi {
3570
+ /** Snapshot every registered experiment's current result — the inspect feed. */
3571
+ readonly snapshot: () => ReadonlyArray<ExperimentResult>;
3572
+ /** One experiment's result by name, or `null` when unknown. */
3573
+ readonly get: (name: string) => ExperimentResult | null;
3574
+ /** Subscribe to live result recomputes. Returns an unsubscribe fn. */
3575
+ readonly subscribe: (listener: (u: ExperimentUpdate) => void) => () => void;
3576
+ }
3577
+
3578
+ /** The live state of one experiment — the row an inspect endpoint / results
3579
+ * dashboard reads. Recomputed on every relevant write. */
3580
+ export declare interface ExperimentResult {
3581
+ readonly name: string;
3582
+ readonly table: string;
3583
+ readonly metric: ExperimentMetricKind;
3584
+ readonly baseline: string;
3585
+ /** Rows assigned to ANY variant (Σ exposure). */
3586
+ readonly totalExposure: number;
3587
+ readonly variants: ReadonlyArray<ExperimentVariantResult>;
3588
+ /** When the result was last recomputed (a relevant write arrived). */
3589
+ readonly lastUpdatedAt: Date | null;
3590
+ readonly description?: string;
3591
+ }
3592
+
3593
+ export declare interface ExperimentRunnerHandle {
3594
+ readonly registry: ExperimentRegistryApi;
3595
+ readonly detach: () => void;
3596
+ readonly registered: ReadonlyArray<string>;
3597
+ }
3598
+
3599
+ /** How the assignment key is read from a row. A column NAME (its value is the
3600
+ * subject) or a function returning the subject (a composite key, a tenant id,
3601
+ * a hashed cookie). Returning `null` / `undefined` / an empty value ⇒ the row
3602
+ * is UNASSIGNED and excluded from every variant. */
3603
+ export declare type ExperimentSubject = string | ((row: Readonly<Record<string, unknown>>) => string | number | null | undefined);
3604
+
3605
+ /** Emitted whenever an experiment's result recomputes on a relevant write. The
3606
+ * results view subscribes to this to update in real time. */
3607
+ export declare interface ExperimentUpdate {
3608
+ readonly result: ExperimentResult;
3609
+ readonly cause: ExperimentCause | null;
3610
+ }
3611
+
3612
+ /** Resolved variant — weight defaulted. */
3613
+ export declare interface ExperimentVariant {
3614
+ readonly name: string;
3615
+ readonly weight: number;
3616
+ }
3617
+
3618
+ /** One variant's live result within an experiment. */
3619
+ export declare interface ExperimentVariantResult {
3620
+ readonly variant: string;
3621
+ /** True for the variant lift/diff is measured against. */
3622
+ readonly isBaseline: boolean;
3623
+ /** True for the reserved holdout arm. */
3624
+ readonly isHoldout: boolean;
3625
+ /** Sample size — rows assigned to this variant (the exposure). */
3626
+ readonly exposure: number;
3627
+ /** The maintained metric value. `null` when the metric is undefined over the
3628
+ * variant's current data (no valid samples for an avg / rate). */
3629
+ readonly metric: number | null;
3630
+ /** Relative lift vs the baseline: `(metric − baseMetric) / baseMetric`. `null`
3631
+ * when either value is null or the baseline metric is 0 (undefined lift).
3632
+ * Always `null` on the baseline row itself. */
3633
+ readonly lift: number | null;
3634
+ /** Absolute difference vs the baseline: `metric − baseMetric`. `null` when
3635
+ * either value is null. Always `null` on the baseline row. */
3636
+ readonly diff: number | null;
3637
+ }
3638
+
3639
+ /** A named arm of the experiment. `weight` skews the split (default 1 = equal);
3640
+ * a subject is assigned in proportion to its weight over the total. */
3641
+ export declare interface ExperimentVariantSpec {
3642
+ readonly name: string;
3643
+ readonly weight?: number;
3644
+ }
3645
+
2781
3646
  /** Build an Effect `ExternalSpan` parent from a `traceparent` header so
2782
3647
  * inbound HTTP work continues the caller's trace. Returns undefined
2783
3648
  * when the header is missing/invalid (no parent → a fresh root span). */
@@ -2800,6 +3665,20 @@ export declare interface FieldChange {
2800
3665
  */
2801
3666
  export declare const fingerprintFor: (table: string, predicate: Predicate, indexHint?: string) => string;
2802
3667
 
3668
+ export declare interface FinopsRunnerHandle {
3669
+ readonly registry: CostRegistryApi;
3670
+ /** The ingestion entry point compute sites call to attribute one unit of
3671
+ * cost. Emits any budget-crossing signals to the registry's subscribers. */
3672
+ readonly record: (event: CostEvent) => void;
3673
+ /** Reset attribution + budgets for one tenant (or all). */
3674
+ readonly reset: (tenantId?: string | null) => void;
3675
+ readonly detach: () => void;
3676
+ readonly registered: ReadonlyArray<string>;
3677
+ /** Test/debug seam: run one clock re-check of the windowed budgets. This is
3678
+ * what the periodic tick timer calls. */
3679
+ readonly runTick: () => void;
3680
+ }
3681
+
2803
3682
  export declare interface FluentStore extends Omit<MutationStore, 'update' | 'delete' | 'query'> {
2804
3683
  /**
2805
3684
  * Diff-based many-to-many link writer for a junction table. `anchor` names the
@@ -2828,7 +3707,7 @@ export declare interface FluentStore extends Omit<MutationStore, 'update' | 'del
2828
3707
  * SPI every dialect store implements, and it genuinely does return untyped
2829
3708
  * rows off the wire. The type is re-applied here, at the handler boundary.
2830
3709
  */
2831
- query<R = Row_5>(descriptor: QueryDescriptor<R>): Promise<ReadonlyArray<R>>;
3710
+ query<R = Row_7>(descriptor: QueryDescriptor<R>): Promise<ReadonlyArray<R>>;
2832
3711
  /**
2833
3712
  * Terminal: EXACTLY one row, TYPED. Fails with `NoRowFound` on zero matches
2834
3713
  * and equally on two or more.
@@ -2849,18 +3728,18 @@ export declare interface FluentStore extends Omit<MutationStore, 'update' | 'del
2849
3728
  * IS `query()` underneath — the probe just asks for one row more than it
2850
3729
  * needs so "the first of several" cannot masquerade as "the one you meant".
2851
3730
  */
2852
- one<R = Row_5>(query: QueryLike<R>): Promise<R>;
3731
+ one<R = Row_7>(query: QueryLike<R>): Promise<R>;
2853
3732
  /** Terminal: the first matching row or `null`, TYPED. Use when "any match" is
2854
3733
  * genuinely what you mean. */
2855
- first<R = Row_5>(query: QueryLike<R>): Promise<R | null>;
3734
+ first<R = Row_7>(query: QueryLike<R>): Promise<R | null>;
2856
3735
  /** Alias of `first` — the first matching row or `null`, TYPED. */
2857
- maybeOne<R = Row_5>(query: QueryLike<R>): Promise<R | null>;
3736
+ maybeOne<R = Row_7>(query: QueryLike<R>): Promise<R | null>;
2858
3737
  /** Fluent, scope-applying read builder: `select('notes').where(...).all()`. */
2859
3738
  select(table: string): SelectBuilder;
2860
3739
  /** Fluent predicate update: `update('notes').where('id', id).set({...})`. */
2861
3740
  update(table: string): UpdateBuilder;
2862
3741
  /** Keyed update by primary key — returns the post-image. */
2863
- update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row_5 | null>;
3742
+ update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row_7 | null>;
2864
3743
  /** Fluent predicate delete: `delete('notes').where('id', id).soft()`. */
2865
3744
  delete(table: string): DeleteBuilder;
2866
3745
  /** Keyed delete by primary key. */
@@ -2885,6 +3764,31 @@ export declare interface FluentStoreBackend {
2885
3764
  readonly delete: DataStore['delete'];
2886
3765
  }
2887
3766
 
3767
+ /**
3768
+ * The columns a given SUBJECT may not see on the wire for `tableName`: the
3769
+ * union of the table's `.serverOnly()` columns (hidden from EVERYONE, so
3770
+ * subject-independent) and its `.readableBy(...)` columns whose scope set the
3771
+ * subject does NOT intersect (Part B — field-level read permissions).
3772
+ *
3773
+ * A subject sees a `.readableBy(...)` column iff it holds AT LEAST ONE of the
3774
+ * declared scopes, checked against its EFFECTIVE scope set (raw subject scopes ∪
3775
+ * rbac role-derived scopes) via `hasEffectiveScope` — so `admin:full` sees every
3776
+ * such column, exactly as it satisfies every guard.
3777
+ *
3778
+ * SUBJECT-INDEPENDENCE is deliberate and load-bearing: when the table declares
3779
+ * NO `.readableBy(...)` column, this returns exactly `serverOnlyColumnNames` —
3780
+ * the same set for every subject — so the Dispatcher's per-subscriber strip
3781
+ * collapses to Part A's behaviour and its read/diff memo still shares one array
3782
+ * across all subscribers of a change. Only a table that actually carries a
3783
+ * `.readableBy(...)` column pays the per-subject cost (different subjects get
3784
+ * different stripped arrays, the same accepted cost `.serverOnly()` already has
3785
+ * whenever it strips anything).
3786
+ *
3787
+ * An unknown / unregistered table forbids nothing (returns `[]`) rather than
3788
+ * throwing — same posture as `serverOnlyColumnNames`.
3789
+ */
3790
+ export declare const forbiddenColumnsForSubject: (tableName: string, subject: Subject) => ReadonlyArray<string>;
3791
+
2888
3792
  /** Forget the calling subject's credential. Returns whether a row was removed. */
2889
3793
  export declare const forgetCredential: (store: VaultStore, connectionId: string, subjectId: string) => Promise<boolean>;
2890
3794
 
@@ -2988,6 +3892,14 @@ export declare interface GroupState {
2988
3892
  /** What a user handler may return. */
2989
3893
  export declare type HandlerBody = void | Promise<unknown> | Effect.Effect<unknown, unknown, never>;
2990
3894
 
3895
+ /**
3896
+ * Deterministic hash of a string into the unit interval `[0, 1)` — FNV-1a
3897
+ * (32-bit), no `node:crypto`, so it runs identically on server and client. The
3898
+ * distribution is uniform enough for balanced bucketing; it is NOT a
3899
+ * cryptographic hash and is not meant to be one (an assignment is not a secret).
3900
+ */
3901
+ export declare const hashUnitInterval: (s: string) => number;
3902
+
2991
3903
  /** Define a histogram. `boundaries` default to the framework duration buckets. */
2992
3904
  export declare const histogramMetric: (name: string, boundaries?: MetricBoundaries.MetricBoundaries, description?: string) => Metric.Metric.Histogram<number>;
2993
3905
 
@@ -2998,6 +3910,12 @@ export declare const histogramMetric: (name: string, boundaries?: MetricBoundari
2998
3910
  */
2999
3911
  export declare const historyProvenance: (history: ReadonlyArray<RowHistoryEntry>, key: ProvenanceKey) => ProvenanceResult;
3000
3912
 
3913
+ /** The reserved variant name a `holdout` fraction is assigned to. A holdout is
3914
+ * carved off the TOP of the unit interval BEFORE the weighted split, so it is
3915
+ * never influenced by adding/removing a treatment. No user variant may take
3916
+ * this name when `holdout > 0`. */
3917
+ export declare const HOLDOUT_VARIANT = "holdout";
3918
+
3001
3919
  /** A secrets backend over a plain JSON HTTP API — no SDK. Wraps fetch + cache.
3002
3920
  * Vault (`/v1/secret/data/...`), Doppler, and the cloud control-plane all fit. */
3003
3921
  export declare const httpSecretsBackend: (opts: HttpSecretsOptions) => SecretsBackend;
@@ -3283,8 +4201,14 @@ export declare const invertChange: (c: ForwardChange) => InverseOp | null;
3283
4201
 
3284
4202
  export declare const isAggregateDefinition: (value: unknown) => value is AggregateDefinition;
3285
4203
 
4204
+ export declare const isCostBudgetDefinition: (value: unknown) => value is CostBudgetDefinition;
4205
+
3286
4206
  export declare const isEmptyTenantScopedRead: (descriptor: QueryDescriptor, tenantScopedTables: ReadonlySet<string>, tenantId: string | null | undefined) => boolean;
3287
4207
 
4208
+ export declare const isExpectationDefinition: (value: unknown) => value is ExpectationDefinition;
4209
+
4210
+ export declare const isExperimentDefinition: (value: unknown) => value is ExperimentDefinition;
4211
+
3288
4212
  /** Foreign-key-violation SQLSTATE / errno across dialects (pg 23503, mysql/maria
3289
4213
  * 1452, mssql 547, sqlite). Used to turn an opaque tenant-FK SqlError into a
3290
4214
  * typed error. */
@@ -3828,6 +4752,18 @@ export declare const makeQuerySubscriber: <D>(deps: QuerySubscriberDeps<D>) => (
3828
4752
 
3829
4753
  export declare const makeRouterActivity: () => RouterActivity;
3830
4754
 
4755
+ /**
4756
+ * Wrap a transactional store so single-row writes on USER tables are recorded
4757
+ * as their POST-WRITE row. Inserts and updates are captured; a delete removes
4758
+ * the row (there is no post-write state to validate), so — like undo's bulk
4759
+ * boundary — deletes are NOT re-validated in v1. Everything else delegates
4760
+ * untouched. Multiple writes to the same (table, id) keep the latest row.
4761
+ */
4762
+ export declare const makeRuleCapture: (tx: unknown) => {
4763
+ readonly tx: unknown;
4764
+ drain(): ReadonlyArray<TouchedRow>;
4765
+ };
4766
+
3831
4767
  /**
3832
4768
  * Build the registry from a list of table descriptors. Cheap — runs once
3833
4769
  * at boot. The returned object is read-only.
@@ -4093,6 +5029,11 @@ export declare interface MutationLike {
4093
5029
  readonly name: string;
4094
5030
  readonly source?: string | ReadonlyArray<string> | undefined;
4095
5031
  readonly guards?: ReadonlyArray<AnyCheckSpec> | undefined;
5032
+ readonly target?: {
5033
+ readonly table: string;
5034
+ } | ReadonlyArray<{
5035
+ readonly table: string;
5036
+ }> | undefined;
4096
5037
  };
4097
5038
  executor(input: unknown, ctx: unknown): unknown;
4098
5039
  }
@@ -4347,9 +5288,13 @@ export declare interface OneShotQueryRunnerDeps<D> extends QueryProducerDeps<D>
4347
5288
  /**
4348
5289
  * Execute a FINALIZED (tenant-scoped) descriptor → rows. On the WS path the
4349
5290
  * dispatcher owns this step; a ONE-SHOT read (a `publicApi` REST GET, where
4350
- * there is no subscription to drive) needs it inline.
5291
+ * there is no subscription to drive) needs it inline. The resolved request
5292
+ * `context` is handed alongside so the caller can apply the SAME
5293
+ * subject-aware wire-column strip the dispatcher does (`.serverOnly()` ∪ the
5294
+ * `.readableBy(...)` columns this subject lacks) — the parity that keeps the
5295
+ * REST projection from shipping a column the socket path would strip.
4351
5296
  */
4352
- readonly queryRows: (descriptor: D) => Promise<ReadonlyArray<unknown>>;
5297
+ readonly queryRows: (descriptor: D, context: ServeRequestContext) => Promise<ReadonlyArray<unknown>>;
4353
5298
  }
4354
5299
 
4355
5300
  /**
@@ -4606,7 +5551,7 @@ export declare interface PreviewStore {
4606
5551
  /** Project a stored row into the wire shape. Deliberately total over the
4607
5552
  * columns the descriptor declares — and deliberately silent about the token
4608
5553
  * columns, which have no wire representation at all. */
4609
- export declare const projectConnectionState: (definition: ConnectionDefinition, row: AnyRow | undefined) => ConnectionState;
5554
+ export declare const projectConnectionState: (definition: ConnectionDefinition, row: AnyRow_2 | undefined) => ConnectionState;
4610
5555
 
4611
5556
  /** Exact-match property filter. Values are AND'd. */
4612
5557
  export declare type PropertyFilter = Readonly<Record<string, string | number | boolean | null>>;
@@ -4746,7 +5691,7 @@ export declare interface QueryFinalizeOptions {
4746
5691
 
4747
5692
  /** A typed builder or its descriptor — the single-row terminals accept either,
4748
5693
  * so a call site never has to reach for `.descriptor` just to use them. */
4749
- export declare type QueryLike<R = Row_5> = QueryDescriptor<R> | {
5694
+ export declare type QueryLike<R = Row_7> = QueryDescriptor<R> | {
4750
5695
  readonly descriptor: QueryDescriptor<R>;
4751
5696
  };
4752
5697
 
@@ -5124,6 +6069,8 @@ export declare interface RegistryTableLike {
5124
6069
  readonly expr: string;
5125
6070
  readonly stored: boolean;
5126
6071
  };
6072
+ /** `crdtText()` column — server-merged on write (see crdtColumns above). */
6073
+ readonly crdtManaged?: boolean;
5127
6074
  }>;
5128
6075
  readonly appliedMixins?: ReadonlyArray<{
5129
6076
  readonly id?: string;
@@ -5546,6 +6493,9 @@ export declare interface ResourcePolicy {
5546
6493
  readonly implies?: Readonly<Record<string, ReadonlyArray<string>>>;
5547
6494
  }
5548
6495
 
6496
+ /** The full ordered list of result variants for a definition, holdout last. */
6497
+ export declare const resultVariantNames: (def: ExperimentDefinition) => ReadonlyArray<string>;
6498
+
5549
6499
  export { RetentionSpec }
5550
6500
 
5551
6501
  export { retentionTtlMsFromEnv }
@@ -5605,6 +6555,10 @@ declare type Row_4 = Readonly<Record<string, unknown>>;
5605
6555
 
5606
6556
  declare type Row_5 = Readonly<Record<string, unknown>>;
5607
6557
 
6558
+ declare type Row_6 = Readonly<Record<string, unknown>>;
6559
+
6560
+ declare type Row_7 = Readonly<Record<string, unknown>>;
6561
+
5608
6562
  export declare interface RowDiff {
5609
6563
  readonly table: string;
5610
6564
  readonly op: 'insert' | 'update' | 'delete';
@@ -6273,6 +7227,12 @@ export declare interface SchemaInfo {
6273
7227
  * them and the dialect rejects an explicit value.
6274
7228
  */
6275
7229
  readonly generatedColumns: ReadonlySet<string>;
7230
+ /**
7231
+ * CRDT-managed columns (`crdtText()`). The MutationStore folds an incoming
7232
+ * encoded update into the stored state (`mergeCrdtStates`) before the write,
7233
+ * so concurrent clients converge. Empty set for the common non-CRDT table.
7234
+ */
7235
+ readonly crdtColumns: ReadonlySet<string>;
6276
7236
  /**
6277
7237
  * Columns that a query predicate can hit and benefit from an index:
6278
7238
  * - column-level `.index()`-flagged columns
@@ -6321,6 +7281,10 @@ export declare interface SchemaRegistry {
6321
7281
  * optional half. */
6322
7282
  hasExpires(table: string): boolean;
6323
7283
  hasTenant(table: string): boolean;
7284
+ /** True when the table carries `localFirst()` — it is mirrored to the client,
7285
+ * synced bi-directionally, and its `crdtText()` fields converge via CRDT
7286
+ * merge. The discovery signal for the client-sync-set builder. */
7287
+ hasLocalFirst(table: string): boolean;
6324
7288
  /** Field-existence check — used to skip stamping a column that the
6325
7289
  * table doesn't actually declare (extra defensive). */
6326
7290
  hasField(table: string, field: string): boolean;
@@ -6362,6 +7326,12 @@ export declare interface SchemaRegistry {
6362
7326
  * Postgres reject an explicit value.
6363
7327
  */
6364
7328
  generatedColumns(table: string): ReadonlySet<string>;
7329
+ /**
7330
+ * CRDT-managed columns (`crdtText()`) for a table. Empty set if none.
7331
+ * Read by the MutationStore write path to run the authoritative server-side
7332
+ * CRDT merge (`mergeCrdtStates`) before an INSERT/UPDATE persists.
7333
+ */
7334
+ crdtColumns(table: string): ReadonlySet<string>;
6365
7335
  /**
6366
7336
  * Optional pre-INSERT schema decoder for a table. `undefined` when
6367
7337
  * the table didn't ship `.validate(schema)`. The MutationStore runs
@@ -6468,6 +7438,13 @@ export declare interface ServeRequestContext {
6468
7438
  readonly rowFilter?: RowFilterScope;
6469
7439
  }
6470
7440
 
7441
+ /**
7442
+ * The `.serverOnly()` column names of a table by NAME, or `[]` when the table is
7443
+ * not registered (a computed query, a raw-SQL descriptor, an unknown source) —
7444
+ * an unknown table strips nothing rather than throwing.
7445
+ */
7446
+ export declare const serverOnlyColumnNames: (tableName: string) => ReadonlyArray<string>;
7447
+
6471
7448
  /** One leak: a wire query that declares a serverOnly column in its output. */
6472
7449
  export declare interface ServerOnlyLeak {
6473
7450
  readonly query: string;
@@ -6490,6 +7467,18 @@ export declare type ServeRpcInterceptor = (base: Effect.Effect<unknown, unknown,
6490
7467
  readonly subject: unknown;
6491
7468
  readonly traceId: string;
6492
7469
  readonly spanId?: string;
7470
+ /**
7471
+ * The declared write-target TABLE names of this mutation/action, derived
7472
+ * from the descriptor's `target:` block. Absent when the descriptor
7473
+ * declares no target (and always absent for queries).
7474
+ *
7475
+ * Carried so an interceptor — the audit sink is the motivating consumer —
7476
+ * can see WHICH tables an operation writes without reaching for the
7477
+ * descriptor registry. Purely additive: an interceptor that ignores it is
7478
+ * unaffected. It is the descriptor's DECLARED target, not the rows actually
7479
+ * written; a change-set carry (the rows/ids) is a separate, heavier seam.
7480
+ */
7481
+ readonly target?: ReadonlyArray<string>;
6493
7482
  }) => Effect.Effect<unknown, unknown, never>;
6494
7483
 
6495
7484
  /** Register the process-wide connection resolver (or clear with `undefined`).
@@ -6780,6 +7769,45 @@ declare const StoreOperationFailed_base: Schema.TaggedErrorClass<StoreOperationF
6780
7769
  cause: typeof Schema.String;
6781
7770
  }>;
6782
7771
 
7772
+ /**
7773
+ * Drop `columns` from every row. Preserves array AND per-row object identity when
7774
+ * nothing is actually removed: an empty column set returns the input array
7775
+ * unchanged, and a row that carries none of the columns is returned as-is. That
7776
+ * identity preservation is load-bearing for the dispatcher's diff memo, which
7777
+ * keys on `prev` object identity — a gratuitous clone would defeat it.
7778
+ */
7779
+ export declare const stripColumnsFromRows: <R extends Row>(rows: ReadonlyArray<R>, columns: ReadonlyArray<string>) => ReadonlyArray<R>;
7780
+
7781
+ /**
7782
+ * Strip a source table's columns this SUBJECT may not see — `.serverOnly()` ∪
7783
+ * the `.readableBy(...)` columns the subject lacks a scope for — from a row set
7784
+ * produced for the wire. The subject-aware counterpart of
7785
+ * {@link stripServerOnlyForWire}; the two share the same value-level
7786
+ * {@link stripColumnsFromRows} pass (and its array/row identity preservation),
7787
+ * so a table with no `.serverOnly()` and no `.readableBy(...)` column returns the
7788
+ * input array unchanged for every subject.
7789
+ *
7790
+ * Same ACCEPTED GAP as `stripServerOnlyForWire`: only TOP-LEVEL columns of
7791
+ * `tableName` are matched — an eager-loaded relation or a renamed projection
7792
+ * changes the shape and is not covered here.
7793
+ */
7794
+ export declare const stripForbiddenForWire: <R extends Row>(tableName: string, rows: ReadonlyArray<R>, subject: Subject) => ReadonlyArray<R>;
7795
+
7796
+ /**
7797
+ * Strip a source table's `.serverOnly()` columns from a row set produced for the
7798
+ * wire. The runtime half of the exposure policy: declare `.serverOnly()` ONCE at
7799
+ * the schema and every query/subscription output respects it, regardless of
7800
+ * whether the handler was hand-written or a `crud.*` helper.
7801
+ *
7802
+ * ACCEPTED GAP (documented): only TOP-LEVEL columns of `tableName` are matched.
7803
+ * An eager-loaded relation nests another table's columns under a relation key,
7804
+ * and a computed/renamed projection changes the shape — neither is covered here
7805
+ * (mark the nested column `.serverOnly()` on ITS table and read it through a
7806
+ * `crud.*`/redacted path, or omit it from the output schema so the boot audit
7807
+ * blocks it). The marker + `crud.*` path covers the common flat-row case.
7808
+ */
7809
+ export declare const stripServerOnlyForWire: <R extends Row>(tableName: string, rows: ReadonlyArray<R>) => ReadonlyArray<R>;
7810
+
6783
7811
  export declare interface SubscribeContext {
6784
7812
  /** Logger scoped to the subscriber file (`subscribe:<filename>`). */
6785
7813
  readonly log: SyncLogger;
@@ -7037,6 +8065,22 @@ declare const TableValidationFailed_base: Schema.TaggedErrorClass<TableValidatio
7037
8065
  }>>;
7038
8066
  }>;
7039
8067
 
8068
+ /** The standing compute-cost attribution for one tenant — the chargeback /
8069
+ * showback row + the "why is my bill high" breakdown. Maintained incrementally:
8070
+ * every `CostEvent` folds into `total`, `byUnit`, and `bySubscription` in O(1).
8071
+ * Cumulative since boot (or since the last `reset(tenantId)`). */
8072
+ export declare interface TenantCostState {
8073
+ readonly tenantId: string | null;
8074
+ /** Grand total across every unit. */
8075
+ readonly total: number;
8076
+ /** Per-unit breakdown (`recompute` → n, `query` → n, …). */
8077
+ readonly byUnit: Readonly<Record<string, number>>;
8078
+ /** Per-subscription breakdown — the per-subscription attribution. Only
8079
+ * subscription-tagged events contribute. */
8080
+ readonly bySubscription: Readonly<Record<string, number>>;
8081
+ readonly lastUpdatedAt: number | null;
8082
+ }
8083
+
7040
8084
  /**
7041
8085
  * A write was attempted against a `tenant()`-scoped table, but the
7042
8086
  * authenticated subject's `tenantId` is null — either anonymous, or
@@ -7138,6 +8182,14 @@ export declare interface TopNQuery {
7138
8182
  readonly tenantId?: string | null;
7139
8183
  }
7140
8184
 
8185
+ /** A row a mutation wrote, carried to rule evaluation. `row` is the POST-WRITE
8186
+ * state (insert result / update result). */
8187
+ declare interface TouchedRow {
8188
+ readonly table: string;
8189
+ readonly id: string;
8190
+ readonly row: AnyRow;
8191
+ }
8192
+
7141
8193
  export declare interface TraceContext {
7142
8194
  readonly traceId: string;
7143
8195
  readonly spanId: string;
@@ -7472,6 +8524,18 @@ export declare const useAggregate: <Row>(def: AggregateDefinition<Row>) => Effec
7472
8524
  */
7473
8525
  export declare const useAnalytics: () => Effect.Effect<AnalyticsSinkImpl, never, AnalyticsSink>;
7474
8526
 
8527
+ /** Sugar: read one budget's current state for a tenant from a handler. Returns
8528
+ * `null` when the budget name is not registered (none attached, or a typo). */
8529
+ export declare const useCostBudget: (def: CostBudgetDefinition, tenantId: string | null) => Effect.Effect<CostBudgetState | null, never, CostRegistry>;
8530
+
8531
+ /** Sugar: read one expectation's current state from a handler. Returns `null`
8532
+ * when the name is not registered (no expectations attached, or a typo). */
8533
+ export declare const useExpectation: (def: ExpectationDefinition) => Effect.Effect<ExpectationState | null, never, ExpectationRegistry>;
8534
+
8535
+ /** Sugar: read one experiment's current result from a handler. Returns `null`
8536
+ * when the name is not registered (none attached, or a typo). */
8537
+ export declare const useExperiment: (def: ExperimentDefinition) => Effect.Effect<ExperimentResult | null, never, ExperimentRegistry>;
8538
+
7475
8539
  declare type VaultStore = Pick<DataStore, 'query' | 'insert' | 'update' | 'updateMany' | 'delete' | 'upsert'>;
7476
8540
 
7477
8541
  /** The pure core of live revocation: given a subject + action + a row-set,
@@ -7496,6 +8560,10 @@ export declare const VOLTRO_AUDIT_MIXIN_ID: "voltro/audit";
7496
8560
  * consumer). Kept in step by `expiresMixinId.test.ts`. */
7497
8561
  export declare const VOLTRO_EXPIRES_MIXIN_ID: "voltro/expires";
7498
8562
 
8563
+ /** Re-declared here for the same reason as the ids above (no mixin import).
8564
+ * Kept in step by `localFirstMixinId.test.ts`. */
8565
+ export declare const VOLTRO_LOCAL_FIRST_MIXIN_ID: "voltro/localFirst";
8566
+
7499
8567
  export declare const VOLTRO_SOFT_DELETE_MIXIN_ID: "voltro/softDelete";
7500
8568
 
7501
8569
  export declare const VOLTRO_TENANT_MIXIN_ID: "voltro/tenant";