@oxy-hq/sdk 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +44 -4
  2. package/dist/function-context-D8eyZuw_.d.cts +720 -0
  3. package/dist/function-context-D8eyZuw_.d.cts.map +1 -0
  4. package/dist/function-context-D8eyZuw_.d.mts +720 -0
  5. package/dist/function-context-D8eyZuw_.d.mts.map +1 -0
  6. package/dist/index.cjs +156 -11
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +338 -638
  9. package/dist/index.d.cts.map +1 -1
  10. package/dist/index.d.mts +338 -638
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +155 -12
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/ops.cjs +85 -0
  15. package/dist/ops.cjs.map +1 -0
  16. package/dist/ops.d.cts +61 -0
  17. package/dist/ops.d.cts.map +1 -0
  18. package/dist/ops.d.mts +61 -0
  19. package/dist/ops.d.mts.map +1 -0
  20. package/dist/ops.mjs +79 -0
  21. package/dist/ops.mjs.map +1 -0
  22. package/dist/{react-DqnINwTi.mjs → react-BXGyzgz0.mjs} +8 -3
  23. package/dist/react-BXGyzgz0.mjs.map +1 -0
  24. package/dist/{react-CLONxcnA.d.cts → react-DW7Z96sD.d.cts} +8 -1
  25. package/dist/{react-CLONxcnA.d.cts.map → react-DW7Z96sD.d.cts.map} +1 -1
  26. package/dist/{react-CLONxcnA.d.mts → react-DW7Z96sD.d.mts} +8 -1
  27. package/dist/{react-CLONxcnA.d.mts.map → react-DW7Z96sD.d.mts.map} +1 -1
  28. package/dist/{react-riTxd9ce.cjs → react-DcT-mUPj.cjs} +8 -3
  29. package/dist/react-DcT-mUPj.cjs.map +1 -0
  30. package/dist/shell.cjs +34 -4
  31. package/dist/shell.cjs.map +1 -1
  32. package/dist/shell.d.cts +39 -7
  33. package/dist/shell.d.cts.map +1 -1
  34. package/dist/shell.d.mts +39 -7
  35. package/dist/shell.d.mts.map +1 -1
  36. package/dist/shell.mjs +34 -5
  37. package/dist/shell.mjs.map +1 -1
  38. package/package.json +12 -1
  39. package/dist/react-DqnINwTi.mjs.map +0 -1
  40. package/dist/react-riTxd9ce.cjs.map +0 -1
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
 
2
- import { $ as loadCustomAppManifest, A as UseSemanticQueryOpts, B as FunctionError, C as UseProcedureRunInput, D as UseQueryOpts, E as UseQueryInput, F as useProcedureRun, G as apiErrorFromResponse, H as FunctionResult, I as useQuery, J as OxyAppFunctionManifest, K as interpretCustomAppError, L as useResolvedManifest, M as useAgentRun, N as useFunction, O as UseQueryResult, P as useOxyApp, Q as _resetCustomAppManifestCacheForTest, R as useSemanticQuery, S as UseFunctionResult, T as UseProcedureRunResult, U as CustomAppErrorReport, V as FunctionLog, W as OxyApiError, X as OxyAppPerformanceManifest, Y as OxyAppManifest, Z as ResolvedCustomAppManifest, _ as SemanticFilter, a as AppFetcher, b as UseAgentRunInput, c as OxyAppProvider, d as OxyChatProps, f as ProcedureProgress, g as SemanticDateRangeOp, h as SemanticArrayOp, i as AgentSqlArtifact, j as UseSemanticQueryResult, k as UseSemanticQueryInput, l as OxyAppProviderProps, m as ProcedureRunState, n as AgentRunEvent, o as OxyAnswer, p as ProcedureResult, q as LoadManifestOptions, r as AgentRunState, s as OxyAnswerProps, t as AgentArtifact, u as OxyChat, v as SemanticScalarOp, w as UseProcedureRunOpts, x as UseAgentRunResult, y as SemanticTimeDimension, z as useTrackEvent } from "./react-CLONxcnA.mjs";
2
+ import { $ as loadCustomAppManifest, A as UseSemanticQueryOpts, B as FunctionError, C as UseProcedureRunInput, D as UseQueryOpts, E as UseQueryInput, F as useProcedureRun, G as apiErrorFromResponse, H as FunctionResult, I as useQuery, J as OxyAppFunctionManifest, K as interpretCustomAppError, L as useResolvedManifest, M as useAgentRun, N as useFunction, O as UseQueryResult, P as useOxyApp, Q as _resetCustomAppManifestCacheForTest, R as useSemanticQuery, S as UseFunctionResult, T as UseProcedureRunResult, U as CustomAppErrorReport, V as FunctionLog, W as OxyApiError, X as OxyAppPerformanceManifest, Y as OxyAppManifest, Z as ResolvedCustomAppManifest, _ as SemanticFilter, a as AppFetcher, b as UseAgentRunInput, c as OxyAppProvider, d as OxyChatProps, f as ProcedureProgress, g as SemanticDateRangeOp, h as SemanticArrayOp, i as AgentSqlArtifact, j as UseSemanticQueryResult, k as UseSemanticQueryInput, l as OxyAppProviderProps, m as ProcedureRunState, n as AgentRunEvent, o as OxyAnswer, p as ProcedureResult, q as LoadManifestOptions, r as AgentRunState, s as OxyAnswerProps, t as AgentArtifact, u as OxyChat, v as SemanticScalarOp, w as UseProcedureRunOpts, x as UseAgentRunResult, y as SemanticTimeDimension, z as useTrackEvent } from "./react-DW7Z96sD.mjs";
3
+ import { C as StorageDownloadUrl, D as StoragePutResult, E as StoragePutOptions, O as StorageUploadUrl, S as OxyWarehouseApi, T as StorageObject, _ as OxyReach, a as OxyEmailApi, b as OxyStorageApi, c as OxyFunctionHandler, d as OxyFunctionUser, f as OxyIdentityKind, g as OxyOrgTeam, h as OxyOrgPlace, i as OxyAirwayApi, k as StorageUploadUrlInput, l as OxyFunctionRequest, m as OxyOrgAssignment, n as EmailSendInput, o as OxyFetchResult, p as OxyOltpApi, r as EmailSendResult, s as OxyFunctionContext, t as EmailAttachment, u as OxyFunctionRow, v as OxySecretsApi, w as StorageListPage, x as OxyTransaction, y as OxySemanticApi } from "./function-context-D8eyZuw_.mjs";
3
4
  //#region src/config.d.ts
4
5
  /**
5
6
  * Configuration for the Oxy SDK
@@ -44,7 +45,15 @@ type EdgeKind = "component" | "driver";
44
45
  type DriverDirection = "positive" | "negative" | "unknown";
45
46
  type DriverStrength = "strong" | "moderate" | "weak";
46
47
  type DriverConfidence = "high" | "medium" | "low";
47
- type DriverForm = "linear" | "log-log" | "log-linear" | "linear-log";
48
+ /** The shape of a driver relationship.
49
+ *
50
+ * The THIRD hand-maintained mirror of this enum (airlayer's is canonical,
51
+ * `web-app/src/types/metricTree.ts` is the second). Nothing enforces that they
52
+ * agree, and the last time one fell behind — `oxy-semantic`, five variants
53
+ * short — a valid `.view.yml` stopped parsing. A type-only union fails more
54
+ * softly: an SDK consumer reading a tree with a quadratic edge just gets a
55
+ * union that cannot hold it. Add new shapes here whenever airlayer grows one. */
56
+ type DriverForm = "linear" | "log-log" | "log-linear" | "linear-log" | "quadratic" | "cubic" | "sqrt" | "inverse" | "linear-log-quadratic";
48
57
  interface MetricNode {
49
58
  id: string;
50
59
  view: string;
@@ -53,6 +62,11 @@ interface MetricNode {
53
62
  description?: string | null;
54
63
  measure_type: string;
55
64
  is_composite: boolean;
65
+ /** Whether this measure can be drilled into. Serialized rather than
66
+ * re-derived: `measure_type` misses eligible composites, and edge presence
67
+ * over-admits nested / cross-view / multiplicative passthroughs the engine
68
+ * refuses. Non-optional — it is on every metric-tree response. */
69
+ drillable: boolean;
56
70
  expr?: string | null;
57
71
  }
58
72
  interface MetricEdge {
@@ -61,11 +75,18 @@ interface MetricEdge {
61
75
  kind: EdgeKind;
62
76
  /** Sign of a component edge; omitted (defaults to +1) for most edges. */
63
77
  sign?: number;
78
+ /** Arithmetic operator joining a component child to its parent. Omitted
79
+ * when it is `add` — airlayer skips the field at its default — so absent
80
+ * MEANS `add`, never "unknown". Only `mul` / `div` are multiplicative;
81
+ * `add` / `sub` propagate exactly. */
82
+ operator?: "add" | "sub" | "mul" | "div";
64
83
  direction: DriverDirection;
65
84
  strength: DriverStrength;
66
85
  confidence: DriverConfidence;
67
86
  coefficient?: number | null;
68
87
  form: DriverForm;
88
+ /** Whether `form` was declared in the YAML or inferred by the fit. */
89
+ form_declared?: boolean;
69
90
  intercept?: number | null;
70
91
  lag?: number | null;
71
92
  description?: string | null;
@@ -75,6 +96,10 @@ interface MetricTree {
75
96
  nodes: MetricNode[];
76
97
  edges: MetricEdge[];
77
98
  root?: string | null;
99
+ /** Refusals raised while building the tree — a driver declaring both
100
+ * `coefficient:` and `coefficients:`, or a wrong-width vector. Absent when
101
+ * empty, so a lever that moves nothing still has a way to say why. */
102
+ warnings?: string[];
78
103
  }
79
104
  interface SensitivityDriver {
80
105
  measure: string;
@@ -107,6 +132,191 @@ interface PredictResult {
107
132
  inputs: PredictChange[];
108
133
  impacts: PredictImpact[];
109
134
  }
135
+ /** node_id → the measure's value over the baseline window. */
136
+ type MeasureValues = Record<string, number>;
137
+ /** A driver edge's coefficient, measured from history by the baseline query.
138
+ *
139
+ * Either `coefficient` is set or `refusal` is — never both, never neither.
140
+ * A refusal is a result: it is why a measure downstream of the change shows
141
+ * no number. Echo the whole array into `predict` verbatim, refusals included;
142
+ * the server ignores entries carrying no coefficient, and filtering them here
143
+ * would be a second place for the two sides to disagree. */
144
+ interface FittedDriver {
145
+ from: string;
146
+ to: string;
147
+ lag?: number;
148
+ /** The form the slope was measured in. The same number reads as dollars per
149
+ * dollar under `linear` and as a percent-per-percent elasticity under
150
+ * `log-log`, so a bare figure has no unit. */
151
+ form?: DriverForm;
152
+ /** Paired observations behind the fit.
153
+ *
154
+ * `| null` because this mirrors an `Option<f64>` on a GIT-PINNED struct:
155
+ * `skip_serializing_if` is a serde attribute today, not a guarantee, so a
156
+ * reader must accept both encodings. Compare with `!= null`, never
157
+ * `!== undefined` — and the type has to admit both, or the safe read looks
158
+ * like dead code. Same rule on `t_stat`, `t_stats`, `se_terms` and
159
+ * `coefficient` below. */
160
+ n?: number | null;
161
+ n_panels?: number;
162
+ n_nonpositive?: number;
163
+ /** The FIRST basis term — the whole answer for a single-term form; for a
164
+ * shape that can turn it is only the slope, so read `coefficients`.
165
+ * `| null` because it is an `Option<f64>` on the wire. */
166
+ coefficient?: number | null;
167
+ /** One coefficient per basis term, in basis order. This is what propagation
168
+ * evaluates. */
169
+ coefficients?: number[];
170
+ se?: number;
171
+ /** Elements `| null` for the reason stated on `n`. */
172
+ se_terms?: (number | null)[];
173
+ /** `| null` for the reason stated on `n`. */
174
+ t_stat?: number | null;
175
+ /** `t` per basis term, in basis order — `[1]` is the second basis term, the
176
+ * squared one under every shape that can turn. Elements `| null` for the
177
+ * reason stated on `n`. */
178
+ t_stats?: (number | null)[];
179
+ /** Sufficient statistics of the basis over the rows the fit used. Not
180
+ * diagnostic: the fit is per row and a change is a window aggregate, and a
181
+ * curved response cannot cross that gap without these. Echo verbatim. */
182
+ moments?: {
183
+ n?: number;
184
+ s1?: number;
185
+ s2?: number;
186
+ };
187
+ /** `[min, max]` driver values observed. A change beyond this spread is
188
+ * refused rather than extrapolated. */
189
+ domain?: [number, number];
190
+ /** The response sampled as `[change fraction, delta]`. Read this instead of
191
+ * interpreting the coefficients — peak, break-even and saturation are all
192
+ * properties of these samples, so a reader written against them keeps
193
+ * working when a new shape is added. */
194
+ profile?: [number, number][];
195
+ form_source?: "declared" | "inferred";
196
+ /** Every shape considered, scored comparably (AIC in y-space, lower better).
197
+ * Empty when the form was declared. `all_terms_significant` false means the
198
+ * candidate was never eligible, however good its score. */
199
+ candidates?: {
200
+ form: DriverForm;
201
+ aic: number;
202
+ all_terms_significant: boolean;
203
+ }[];
204
+ refusal?: string;
205
+ }
206
+ /** Why a reachable node has no baseline value. */
207
+ interface UnvaluedNode {
208
+ id: string;
209
+ reason?: string | null;
210
+ }
211
+ /** Narrow the baseline to one world-model instance. Omit to value the whole
212
+ * population. */
213
+ interface BaselineInstance {
214
+ entity: string;
215
+ /** JSON array for a composite key, else a bare scalar. */
216
+ key: string;
217
+ }
218
+ interface BaselineRequest {
219
+ /** The nodes you intend to change. Values are fetched for these plus
220
+ * everything forward-reachable from them — not the whole tree. */
221
+ roots: string[];
222
+ time_dimension: string;
223
+ /** `[start, end]` inclusive date strings. */
224
+ period: [string, string];
225
+ instance?: BaselineInstance | null;
226
+ }
227
+ interface BaselineResponse {
228
+ values: MeasureValues;
229
+ unvalued: UnvaluedNode[];
230
+ resolved_period: [string, string];
231
+ /** Why the baseline produced no values, in words worth showing. Absent when
232
+ * measures were valued normally. */
233
+ baseline_note?: string | null;
234
+ /** Coefficients fitted for driver edges that declare none, plus refusals.
235
+ * Absent when every reachable driver edge already declares one. */
236
+ fitted?: FittedDriver[];
237
+ }
238
+ interface PredictOptions {
239
+ /** Current values for the measures involved. Supplying them lets
240
+ * multiplicative edges be sized instead of returned `unquantifiable`. */
241
+ values?: MeasureValues;
242
+ /** The baseline's `fitted` array, verbatim. */
243
+ coefficients?: FittedDriver[];
244
+ }
245
+ type ProjectionGranularity = "day" | "week" | "month";
246
+ /**
247
+ * The scenario's time axis. `baseline` answers "what is this measure worth
248
+ * over the window"; this answers "what has it been doing, and what does it do
249
+ * next".
250
+ *
251
+ * The history window is deliberately its own, NOT the baseline's: the
252
+ * forecaster refuses anything under eight seasonal cycles (56 daily buckets,
253
+ * 32 weekly, 24 monthly), and a 30-day scenario baseline reused here would
254
+ * make "no forecast" the normal answer.
255
+ */
256
+ interface ProjectionRequest {
257
+ /** Lever node ids. Curves are drawn for these plus everything
258
+ * forward-reachable from them — the same set {@link BaselineRequest} values. */
259
+ roots: string[];
260
+ time_dimension: string;
261
+ /** `[start, end]` inclusive date strings for the HISTORY. */
262
+ period: [string, string];
263
+ /** Narrow to one world-model instance. Omit to project the whole
264
+ * population — same picker the baseline uses. */
265
+ instance?: BaselineInstance | null;
266
+ /** Bucket width. Defaults to `day` server-side. */
267
+ granularity?: ProjectionGranularity;
268
+ /** Buckets to project past the last historical one. 1..=365; outside that
269
+ * it is a 400, never a silent clamp — a horizon quietly truncated reads as
270
+ * a forecast that genuinely ends in March. */
271
+ horizon: number;
272
+ /** Seasonal periods, in buckets, applied to every measure in the request.
273
+ *
274
+ * Omitting it is not "use the default" — it means *resolve per measure*
275
+ * from whatever `.monitor.yml` already watches that series, which is what
276
+ * keeps this band the band an anomaly had to breach. Send it only to pin a
277
+ * cycle nobody has declared. Each period must be >= 2; `[]` is a 400. */
278
+ seasonality?: number[];
279
+ }
280
+ interface HistoryPoint {
281
+ /** Bucket start, `YYYY-MM-DD`. */
282
+ date: string;
283
+ value: number;
284
+ }
285
+ interface ForecastPoint {
286
+ date: string;
287
+ point: number;
288
+ /** The prediction interval. `null` / absent means the model returned no
289
+ * band — unknown spread, NOT a band of zero width. Never collapse these
290
+ * onto `point`: a zero-width band is a claim of certainty nobody made. */
291
+ lower?: number | null;
292
+ upper?: number | null;
293
+ }
294
+ /** One measure's baseline curve: what happened, then what comes next.
295
+ *
296
+ * An empty `forecast` carrying a `refusal` is a state, not a gap — most often
297
+ * "too little history to fit", or the warehouse refusing this one measure. It
298
+ * must never render as a flat forward line, which is what any code defaulting
299
+ * the missing curve to "unchanged" would draw. */
300
+ interface MeasureProjection {
301
+ measure: string;
302
+ history: HistoryPoint[];
303
+ forecast: ForecastPoint[];
304
+ refusal?: string | null;
305
+ /** The seasonal periods this curve was decomposed against — resolved per
306
+ * measure, so two series in one response can legitimately differ. */
307
+ seasonality: number[];
308
+ }
309
+ interface ProjectionResponse {
310
+ granularity: ProjectionGranularity;
311
+ /** Echoed back: the query is expensive and callers cache on it. */
312
+ resolved_period: [string, string];
313
+ horizon: number;
314
+ series: MeasureProjection[];
315
+ /** Why the WHOLE projection is empty, when it is. Absent when at least one
316
+ * measure produced history — a partial failure is each measure's own
317
+ * `refusal`, never a banner over curves that are drawing fine. */
318
+ projection_note?: string | null;
319
+ }
110
320
  type SplitKind = {
111
321
  type: "component";
112
322
  child_measure: string;
@@ -280,9 +490,10 @@ interface TimeDimensionsResponse {
280
490
  */
281
491
  type RequestFn$1 = <T>(endpoint: string, options?: RequestInit) => Promise<T>;
282
492
  /**
283
- * Client for the `/semantic/metric-tree*` endpoints. Surfaces the four
284
- * airlayer metric-tree analyses (tree introspection, sensitivity, predict,
285
- * explain, opportunity) over typed methods.
493
+ * Client for the `/semantic/metric-tree*` endpoints. Surfaces the airlayer
494
+ * metric-tree analyses — tree introspection, sensitivity, explain, opportunity
495
+ * — plus the three legs of scenario forecasting (`baseline` levels,
496
+ * `predict` propagation, `projection` curves) over typed methods.
286
497
  *
287
498
  * Construction is internal to {@link OxyClient} — call `client.metricTree`
288
499
  * to access an instance rather than building one yourself.
@@ -327,10 +538,45 @@ declare class MetricTreeClient {
327
538
  * ```
328
539
  */
329
540
  getSensitivity(measureId: string): Promise<SensitivityResult>;
541
+ /**
542
+ * Value a change's starting point, and measure the coefficients it needs.
543
+ *
544
+ * Two warehouse reads: the current value of every node reachable from
545
+ * `roots`, and — for driver edges that declare no `coefficient:` — a fit
546
+ * over the window. Both are expensive, which is why they live here and not
547
+ * in `predict`: `predict` is database-free by design so it can re-run per
548
+ * keystroke, and it CANNOT measure a coefficient itself.
549
+ *
550
+ * That is the whole reason to call this. Pass `fitted` back into `predict`
551
+ * and an undeclared edge propagates; omit it and `predict` has nothing to
552
+ * multiply by, so the impact is simply absent — no error, no refusal, just a
553
+ * downstream measure that never appears.
554
+ *
555
+ * @example
556
+ * ```typescript
557
+ * const baseline = await client.metricTree.getBaseline({
558
+ * roots: ["marketing_spend.total_spend"],
559
+ * time_dimension: "orders.order_date",
560
+ * period: ["2025-09-01", "2025-09-30"],
561
+ * });
562
+ * const result = await client.metricTree.predict(
563
+ * [{ measure: "marketing_spend.total_spend", delta: 10000 }],
564
+ * { values: baseline.values, coefficients: baseline.fitted }
565
+ * );
566
+ * ```
567
+ */
568
+ getBaseline(request: BaselineRequest): Promise<BaselineResponse>;
330
569
  /**
331
570
  * Propagate hypothetical `(measure, delta)` changes upward through the
332
571
  * tree. Returns the estimated impact on every downstream measure.
333
572
  *
573
+ * Database-free, so it re-runs cheaply — and so it can only use
574
+ * coefficients it is GIVEN. Without `options.coefficients` from
575
+ * {@link getBaseline}, every edge whose `.view.yml` declares no
576
+ * `coefficient:` contributes nothing and its downstream measures are
577
+ * silently missing from `impacts`. Without `options.values`, multiplicative
578
+ * component edges come back `unquantifiable` rather than sized.
579
+ *
334
580
  * @example
335
581
  * ```typescript
336
582
  * const result = await client.metricTree.predict([
@@ -338,7 +584,37 @@ declare class MetricTreeClient {
338
584
  * ]);
339
585
  * ```
340
586
  */
341
- predict(changes: PredictChange[]): Promise<PredictResult>;
587
+ predict(changes: PredictChange[], options?: PredictOptions): Promise<PredictResult>;
588
+ /**
589
+ * Draw the scenario's time axis: bucketed history for the levers and
590
+ * everything downstream, plus the forward curve the detector's own model
591
+ * expects next.
592
+ *
593
+ * The third leg of scenario forecasting. {@link getBaseline} gives levels
594
+ * and coefficients, {@link predict} propagates a change with no database at
595
+ * all, and this gives time — one warehouse query, so treat it like the
596
+ * baseline: fetch on a window change, not on a lever edit.
597
+ *
598
+ * **Returns the BASELINE curve only.** The scenario's second curve is
599
+ * arithmetic over this and a `predict` result — a proportional shift landing
600
+ * `lag` buckets in — and is composed client-side deliberately, so editing a
601
+ * lever costs no query.
602
+ *
603
+ * @example
604
+ * ```typescript
605
+ * const projection = await client.metricTree.getProjection({
606
+ * roots: ["marketing_spend.total_spend"],
607
+ * time_dimension: "orders.order_date",
608
+ * period: ["2024-09-01", "2025-08-31"],
609
+ * granularity: "day",
610
+ * horizon: 30,
611
+ * });
612
+ * for (const series of projection.series) {
613
+ * if (series.refusal) console.warn(series.measure, series.refusal);
614
+ * }
615
+ * ```
616
+ */
617
+ getProjection(request: ProjectionRequest): Promise<ProjectionResponse>;
342
618
  /**
343
619
  * Period-over-period root-cause decomposition. Recursively splits the
344
620
  * target measure by components and dimensions until the move concentrates.
@@ -735,636 +1011,6 @@ interface CustomAppDebugSnapshot {
735
1011
  */
736
1012
  declare function getCustomAppDebug(resolved: ResolvedCustomAppManifest): Promise<CustomAppDebugSnapshot>;
737
1013
  //#endregion
738
- //#region src/custom-app/function-context.d.ts
739
- /**
740
- * The request passed as the first argument to a function's default export.
741
- *
742
- * The host hands the isolate the raw request body as a string (see
743
- * `req_json` in `runtime.rs`); parse it yourself, e.g.
744
- * `JSON.parse(req.body || "{}")`. This is intentionally *not* a full Web
745
- * `Request` — there is no `.json()` / headers object in v1.
746
- */
747
- interface OxyFunctionRequest {
748
- /** Raw request body as received (JSON string for a JSON POST). */
749
- body: string;
750
- }
751
- /** A single row from a `ctx.query` / `ctx.queryStream` result. */
752
- type OxyFunctionRow = Record<string, unknown>;
753
- /** One org team the caller belongs to, as reported by {@link OxyFunctionUser.teams}. */
754
- interface OxyOrgTeam {
755
- id: string;
756
- name: string;
757
- }
758
- /**
759
- * Who — or what — invoked this function.
760
- *
761
- * `"system"` means **no caller to attribute this to** — not necessarily "no
762
- * human caused it". A schedule tick, an Airway transform step, and an operator's
763
- * manual *Run now* all take this path: they run under the org owner's `id` (the
764
- * invocation record needs a real user FK) with every caller field absent. So on
765
- * a manual run a person really did click, and there is still no way to reach
766
- * them; the platform does not carry the triggering operator through the job
767
- * queue.
768
- *
769
- * Any branch that emails "the person who clicked" or renders a personal view
770
- * must check this rather than sniff the synthetic `email` — and must have a
771
- * sensible answer for the case where there is nobody to send to.
772
- */
773
- type OxyIdentityKind = "user" | "system";
774
- /**
775
- * Identity of the invoking user (route) or the system identity (schedule,
776
- * Airway step, or a manual job run).
777
- *
778
- * Assembled server-side on every invocation from the authenticated session —
779
- * **nothing on it is client-supplied**, which is the entire reason to read
780
- * identity here instead of from the request body. See
781
- * `internal-docs/custom-apps-user-identity.md` for the full contract, including
782
- * what the client-side `useShellContext()` can and cannot be trusted for.
783
- */
784
- interface OxyFunctionUser {
785
- /**
786
- * `users.id`. On a `"system"` invocation this is the org owner's id and not a
787
- * caller — check {@link kind} before attributing anything to it.
788
- */
789
- id: string;
790
- /**
791
- * Their email; `schedule+<fn>@system.oxy` when {@link kind} is `"system"`; and
792
- * **`null` for a frontline worker** — a crew member enrolled by PIN on a shared
793
- * device has no mailbox, and the platform stores none rather than inventing
794
- * one. That null is the one field that tells the crew from the office inside
795
- * a function, because a worker can never hold org membership (see
796
- * `orgRole`, which is absent for them too). Treat it as `string | null` in
797
- * app logic; it was typed `string` before the crew existed.
798
- */
799
- email: string | null;
800
- /**
801
- * The org that owns this app — the tenant boundary for anything the function
802
- * reads or writes.
803
- *
804
- * Servers before 2026-08-21 mistakenly sent this as `org_id`, so `orgId` read
805
- * `undefined` there; both keys are populated now. If your function filters SQL
806
- * on it, that is exactly the bug to re-check.
807
- */
808
- orgId: string;
809
- /** Display name. Absent on a `"system"` invocation. User-controlled free text —
810
- * fine for a greeting or an audit row, never a key, and escape it before it
811
- * reaches HTML or SQL. */
812
- name?: string;
813
- /** Avatar URL. Absent when unset or on a `"system"` invocation. */
814
- picture?: string;
815
- /**
816
- * The caller's role **within this app**, derived server-side from app
817
- * membership (with org-owner / Oxy-staff break-glass). Absent when they hold
818
- * no membership.
819
- *
820
- * This is the value to gate a privileged surface on — it cannot be forged by
821
- * the client, unlike a query param or a client-side flag:
822
- *
823
- * ```ts
824
- * if (ctx.user.appRole !== "admin") {
825
- * return Response.json({ error: "forbidden" }, { status: 403 });
826
- * }
827
- * ```
828
- *
829
- * Note it is deliberately NOT the org role: an app admin administers one app
830
- * without holding org-Admin (which also carries billing and member management).
831
- *
832
- * A `"system"` invocation runs under the org owner, so this reads `"admin"`
833
- * there — a schedule carries owner authority by construction. Add a
834
- * {@link kind} check when a surface must be human-only.
835
- */
836
- appRole?: "admin" | "member";
837
- /**
838
- * The caller's role in the owning **org**. Absent when they reach the app
839
- * without an org membership (Oxy staff on break-glass) or on a `"system"`
840
- * invocation.
841
- *
842
- * Informational, not a gate — org standing and app standing are separate
843
- * rings. Use it to explain ("ask your org admin to connect a warehouse"), to
844
- * label, or to route; gate on {@link appRole}.
845
- */
846
- orgRole?: "owner" | "admin" | "member";
847
- /**
848
- * The org teams the caller belongs to, name-sorted, and scoped to this app's
849
- * org — teams they hold in other orgs are never reported. Empty when they
850
- * belong to none.
851
- *
852
- * Optional because a server older than 2026-08-21 does not send it: use
853
- * `ctx.user.teams?.some(...)`, never `ctx.user.teams.some(...)`, or the
854
- * function throws on that server rather than degrading.
855
- *
856
- * Useful for *shaping* a view (default the Finance team to the finance tab).
857
- * Not a permission: a team only grants anything on an app through an app team
858
- * grant, which is already folded into {@link appRole}. Gating on a team name
859
- * invents a permission the platform cannot revoke.
860
- */
861
- teams?: OxyOrgTeam[];
862
- /**
863
- * Whether there is a caller to attribute this invocation to.
864
- *
865
- * On a current server this is exact — `ctx.user.kind === "system"` is the
866
- * check.
867
- *
868
- * Optional for the same reason as {@link teams}: a server older than
869
- * 2026-08-21 does not send it. Note there is no safe *inference* to fall back
870
- * on, in either direction — `=== "system"` reads `false` for a cron tick, and
871
- * `!== "user"` reads `true` for a real person. An older server genuinely
872
- * cannot tell you.
873
- *
874
- * So if you must support one, don't infer: a schedule invokes the function
875
- * with the `input` you configured on it, which is yours to mark.
876
- *
877
- * ```ts
878
- * const body = JSON.parse(req.body || "{}");
879
- * const isSystem = ctx.user.kind ? ctx.user.kind === "system" : body._trigger === "schedule";
880
- * ```
881
- */
882
- kind?: OxyIdentityKind;
883
- }
884
- /** Result of a `ctx.fetch` call. */
885
- interface OxyFetchResult {
886
- status: number;
887
- /** Response body, decoded per the requested {@link OxyFetchInit.encoding}. */
888
- body: string;
889
- /** Echoes how `body` was encoded (`"utf8"` unless base64 was requested). */
890
- encoding?: "utf8" | "base64";
891
- }
892
- /**
893
- * `init` for `ctx.fetch` — the standard `RequestInit` fields the host honours
894
- * (`method`, `headers`, `body`) plus how to decode the response.
895
- */
896
- type OxyFetchInit = RequestInit & {
897
- /**
898
- * How to decode the response body. `"utf8"` (default) is **lossy for
899
- * binary** — every non-UTF-8 byte becomes U+FFFD, so a fetched PDF/PNG comes
900
- * back corrupt. Pass `"base64"` for any binary response, e.g. to hand it
901
- * straight to an email attachment.
902
- */
903
- encoding?: "utf8" | "base64";
904
- };
905
- /**
906
- * `ctx.warehouse.*` — one of the app's configured databases by name.
907
- *
908
- * The writes require the database in the function's `destinations` allowlist;
909
- * `query` does not, because that allowlist is about modifying a project's
910
- * warehouse, and a `postgres_managed` database resolves the read-only analyst
911
- * for every caller regardless.
912
- */
913
- interface OxyWarehouseApi {
914
- /**
915
- * Read from a named database.
916
- *
917
- * `ctx.query` only ever reaches the project's DEFAULT database, so this is
918
- * how an app reads its own per-org OLTP store, which sits beside whatever
919
- * warehouse the project analyses.
920
- */
921
- query(database: string, sql: string): Promise<{
922
- rows: OxyFunctionRow[];
923
- truncated: boolean;
924
- }>;
925
- insert(database: string, table: string, rows: OxyFunctionRow[]): Promise<unknown>;
926
- exec(database: string, sql: string): Promise<unknown>;
927
- upsert(database: string, table: string, rows: OxyFunctionRow[], conflictColumns: string[]): Promise<unknown>;
928
- }
929
- /**
930
- * The handle `ctx.tx` passes to your callback — a pinned connection with an
931
- * open transaction.
932
- *
933
- * Both methods take **bound parameters** (`$1`, `$2`, …). Never build SQL by
934
- * concatenating request data: `ctx.warehouse.exec` takes a bare string, but a
935
- * transaction exists for surfaces that accept end-user input, and placeholders
936
- * are the only thing that makes that safe.
937
- *
938
- * The handle is live only for the duration of the callback. Using it after the
939
- * callback returns throws — it is not a connection you can stash.
940
- */
941
- interface OxyTransaction {
942
- /** Run a row-returning statement (including `INSERT … RETURNING`). */
943
- query(sql: string, params?: unknown[]): Promise<OxyFunctionRow[]>;
944
- /** Run a statement for its effect; resolves to the number of rows affected. */
945
- exec(sql: string, params?: unknown[]): Promise<number>;
946
- }
947
- /**
948
- * `ctx.oltp` — read and WRITE the app's OWN per-org OLTP schema (`app_<writer>`)
949
- * on the managed Postgres tenant, and nothing else.
950
- *
951
- * This is the write half `ctx.warehouse` cannot give an app: for a
952
- * `postgres_managed` database `ctx.warehouse` resolves the read-only analyst
953
- * (org-wide read, the org's `raw_*` extracts included), so a write authenticates
954
- * and then fails `permission denied`. `ctx.oltp` resolves the app's **writer**
955
- * role instead — DML rights scoped to the one `app_<writer>` schema, so it is
956
- * narrower on reads (no `raw_*`) and finally writable.
957
- *
958
- * Gated by the fail-closed `oltp` manifest capability (`"oltp": { "enabled":
959
- * true }`) — a pure gate. The target schema is derived from the app's own slug
960
- * (`oltp-bookings` → `app_oltp_bookings`), never named in the manifest, so a
961
- * manifest cannot point `ctx.oltp` at another app's schema. The store must be
962
- * provisioned first (ask whoever operates the org). No database name is passed —
963
- * the app's own store is implicit.
964
- *
965
- * Both methods take **bound parameters** (`$1`, `$2`, …). Never build SQL by
966
- * concatenating request data — a booking form is exactly the surface that takes
967
- * end-user input, and placeholders are the only thing that makes it safe. Each
968
- * call auto-commits; a failed statement rolls back.
969
- *
970
- * **Cost:** each call opens its own connection to the tenant (a TCP + TLS
971
- * handshake, and a wake-up if the compute was idle) and its own transaction, so
972
- * a per-row loop pays that per row. Prefer one statement over many — a
973
- * multi-row `INSERT`, an `INSERT … SELECT`, or `INSERT … RETURNING` to avoid a
974
- * follow-up read — and reach for `ctx.oltp` a handful of times per request, not
975
- * in a hot loop.
976
- *
977
- * ```ts
978
- * const [row] = await ctx.oltp.query(
979
- * "INSERT INTO bookings (name, party_size) VALUES ($1, $2) RETURNING id",
980
- * [name, partySize],
981
- * );
982
- * ```
983
- */
984
- interface OxyOltpApi {
985
- /** Run a row-returning statement (including `INSERT … RETURNING`). */
986
- query(sql: string, params?: unknown[]): Promise<OxyFunctionRow[]>;
987
- /** Run a statement for its effect; resolves to the number of rows affected. */
988
- exec(sql: string, params?: unknown[]): Promise<number>;
989
- }
990
- /** `ctx.secrets` — write app-scoped secrets (gated by the `secrets.write` capability). */
991
- interface OxySecretsApi {
992
- set(key: string, value: string): Promise<void>;
993
- }
994
- /** `ctx.semantic` — airlayer-compiled semantic queries (inherits the pre-agg fast path). */
995
- interface OxySemanticApi {
996
- query(spec: Record<string, unknown>): Promise<unknown>;
997
- }
998
- /** `ctx.airway` — seed/await an Airway ELT pipeline run. */
999
- interface OxyAirwayApi {
1000
- run(pipelineRef: string, variables?: Record<string, unknown> | null): Promise<{
1001
- runId: string;
1002
- }>;
1003
- }
1004
- /**
1005
- * Input to `ctx.email.send`. Platform-injected: the sender mailbox (`from`) is
1006
- * platform-controlled and **not** an accepted field — passing it is a typed
1007
- * error. Provide `html` and/or `text` as the body (render a template to HTML
1008
- * with `render` from `@oxy-hq/sdk/email`).
1009
- */
1010
- interface EmailSendInput {
1011
- /** Recipient address(es). Required. */
1012
- to: string | string[];
1013
- /** CC address(es). */
1014
- cc?: string | string[];
1015
- /** BCC address(es). */
1016
- bcc?: string | string[];
1017
- /** Reply-To address — the only sender-identity field an author may set. */
1018
- replyTo?: string;
1019
- /** Subject line. Required. */
1020
- subject: string;
1021
- /** HTML body. Provide at least one of `html` / `text`. */
1022
- html?: string;
1023
- /** Plain-text body. Provide at least one of `html` / `text`. */
1024
- text?: string;
1025
- /**
1026
- * Optional idempotency key (≤256 chars). Accepted and validated in v1 but a
1027
- * no-op until the persisted idempotency table lands — adopt it now so
1028
- * background (retried) sends become exactly-once once it does.
1029
- */
1030
- idempotencyKey?: string;
1031
- /**
1032
- * Files to attach. Max 20 per send, and **10 MiB decoded in total** — SES
1033
- * caps a whole message near 40 MB, so for anything larger store the file with
1034
- * {@link OxyStorageApi} and email a presigned link instead of inlining it.
1035
- *
1036
- * `content` is base64 by default; for generated text set
1037
- * `encoding: "utf8"` and attach the string as-is.
1038
- */
1039
- attachments?: EmailAttachment[];
1040
- }
1041
- /** One attachment on {@link EmailSendInput}. */
1042
- interface EmailAttachment {
1043
- /** Filename shown to the recipient. Required; path separators are stripped. */
1044
- filename: string;
1045
- /**
1046
- * File contents, interpreted per {@link EmailAttachment.encoding} — base64 by
1047
- * default, which is the only way binary crosses the isolate boundary.
1048
- */
1049
- content: string;
1050
- /**
1051
- * How `content` is encoded. Defaults to `"base64"`.
1052
- *
1053
- * Use `"utf8"` to attach text the function just generated (CSV, JSON, HTML)
1054
- * — it needs no encoder and is byte-exact for non-ASCII. `btoa` is the wrong
1055
- * tool there: it encodes U+0080..U+00FF as *Latin1*, so accented text comes
1056
- * out as mojibake rather than as an error. For binary, take base64 straight
1057
- * from the source — `ctx.storage.get(key, { encoding: "base64" })` or
1058
- * `ctx.fetch(url, { encoding: "base64" })` — or {@link bytesToBase64} for a
1059
- * `Uint8Array` you built yourself.
1060
- */
1061
- encoding?: "base64" | "utf8";
1062
- /** MIME type; defaults to `application/octet-stream`. */
1063
- contentType?: string;
1064
- /** Render inline (e.g. an image referenced as `cid:<contentId>`) instead of as a download. */
1065
- inline?: boolean;
1066
- /** Content-ID for an inline part, referenced from the HTML body as `cid:<contentId>`. */
1067
- contentId?: string;
1068
- }
1069
- /** Result of a successful `ctx.email.send`. */
1070
- interface EmailSendResult {
1071
- /** Provider (SES) message id of the sent message. */
1072
- messageId: string;
1073
- }
1074
- /** `ctx.email` — send email (gated by the `email.send` capability). */
1075
- interface OxyEmailApi {
1076
- send(input: EmailSendInput): Promise<EmailSendResult>;
1077
- }
1078
- /** Input to `ctx.storage.getUploadUrl`. */
1079
- interface StorageUploadUrlInput {
1080
- /**
1081
- * Destination path inside the app's silo, e.g. `"uploads/q1-report.pdf"`.
1082
- * Segments are sanitized server-side and cannot escape the silo. Omit to use
1083
- * `filename`, which is placed under `uploads/`.
1084
- */
1085
- pathname?: string;
1086
- /** Shorthand for `pathname: "uploads/<filename>"`. */
1087
- filename?: string;
1088
- /** MIME type; bound into the presigned PUT signature. Inferred when omitted. */
1089
- contentType?: string;
1090
- /**
1091
- * Exact byte length of the upload, bound into the signature — S3 rejects a
1092
- * body of any other size. Capped by the server's upload ceiling (100 MiB by
1093
- * default).
1094
- */
1095
- contentLength: number;
1096
- /** Presign lifetime in seconds (default 900; max 604800 — SigV4's own limit). */
1097
- expiresInSeconds?: number;
1098
- }
1099
- /** A minted presigned upload. */
1100
- interface StorageUploadUrl {
1101
- /** Presigned PUT — the browser uploads the file bytes directly to this URL. */
1102
- url: string;
1103
- /**
1104
- * The stored key. Record it (e.g. on a row in your warehouse) — it is how you
1105
- * fetch, list or link to the asset later. A random suffix is added so two
1106
- * people uploading `report.pdf` don't collide.
1107
- */
1108
- key: string;
1109
- /** ISO-8601 expiry of the presigned URL. */
1110
- expiresAt: string;
1111
- /**
1112
- * Retention tag for this key, present only when your app declares a matching
1113
- * `storage.retention` rule in `oxy-app.json` (e.g. `"oxy-ttl=30d"`).
1114
- *
1115
- * **When present, the upload MUST send it as the `x-amz-tagging` header** — it
1116
- * is bound into the signature, so omitting it fails the PUT with a signature
1117
- * mismatch rather than storing an untagged object:
1118
- *
1119
- * ```ts
1120
- * const { url, tagging } = await ctx.storage.getUploadUrl({ ... });
1121
- * await fetch(url, {
1122
- * method: "PUT",
1123
- * body: file,
1124
- * headers: {
1125
- * "Content-Type": file.type,
1126
- * ...(tagging ? { "x-amz-tagging": tagging } : {}),
1127
- * },
1128
- * });
1129
- * ```
1130
- *
1131
- * Signing it is deliberate: a browser that could drop the header could opt any
1132
- * upload out of the app's own retention policy.
1133
- */
1134
- tagging?: string;
1135
- }
1136
- /** A minted presigned download. */
1137
- interface StorageDownloadUrl {
1138
- url: string;
1139
- expiresAt: string;
1140
- }
1141
- /** One asset in the app's silo. */
1142
- interface StorageObject {
1143
- key: string;
1144
- size: number;
1145
- contentType?: string | null;
1146
- /** ISO-8601. */
1147
- lastModified?: string | null;
1148
- }
1149
- /** One page of {@link OxyStorageApi.list}. */
1150
- interface StorageListPage {
1151
- objects: StorageObject[];
1152
- /** Pass back as `cursor` to fetch the next page; `null` when complete. */
1153
- cursor: string | null;
1154
- hasMore: boolean;
1155
- }
1156
- /** Options for {@link OxyStorageApi.put}. */
1157
- interface StoragePutOptions {
1158
- /** MIME type. Inferred from the pathname's extension when omitted. */
1159
- contentType?: string;
1160
- /**
1161
- * How `body` is encoded. `"base64"` is what makes **binary** generated assets
1162
- * (PDF, PNG, Parquet) possible — a UTF-8 string would corrupt them.
1163
- */
1164
- encoding?: "utf8" | "base64";
1165
- /** Append a short random component before the extension to avoid collisions. */
1166
- addRandomSuffix?: boolean;
1167
- /**
1168
- * Replace an existing asset at this path. Defaults to `false` — writing over
1169
- * an asset by accident is worse than an error, so this is opt-in.
1170
- */
1171
- allowOverwrite?: boolean;
1172
- /** `Cache-Control: max-age=<seconds>` stored on the object. */
1173
- cacheControlMaxAge?: number;
1174
- }
1175
- /** Result of a `put` (and of `copy`). */
1176
- interface StoragePutResult {
1177
- key: string;
1178
- size: number;
1179
- contentType: string;
1180
- }
1181
- /**
1182
- * `ctx.storage` — this app's **asset store**, covering both kinds of file an app
1183
- * produces, in one silo (`customer-app-storage/<app_id>/`):
1184
- *
1185
- * - **Uploaded** — a human picks a file; `getUploadUrl` mints a presigned PUT and
1186
- * the browser uploads **straight to S3**, so uploads aren't bounded by the
1187
- * request-body limit and the bytes never pass through your function.
1188
- * - **Generated** — your function produces the file (a rendered PDF, a CSV
1189
- * export, a chart PNG) and writes it with `put`, using
1190
- * `{ encoding: "base64" }` for binary.
1191
- *
1192
- * Gated by the fail-closed `storage.read` / `storage.write` capabilities in
1193
- * `oxy-app.json`. Every asset is private; reads are always presigned and
1194
- * time-boxed. Keys are confined to your app — another app's key is rejected.
1195
- *
1196
- * ```ts
1197
- * // Uploaded: mint a URL, browser PUTs to it, then record `key`.
1198
- * const { url, key } = await ctx.storage.getUploadUrl({
1199
- * filename: "q1-report.pdf", contentType: "application/pdf", contentLength: size,
1200
- * });
1201
- *
1202
- * // Generated: write a CSV your function just built.
1203
- * const { key } = await ctx.storage.put("generated/jan.csv", csv);
1204
- *
1205
- * // Either way: email a link that outlives the request.
1206
- * const { url: link } = await ctx.storage.getDownloadUrl(key, {
1207
- * expiresInSeconds: 604800, download: true,
1208
- * });
1209
- * ```
1210
- */
1211
- interface OxyStorageApi {
1212
- /** Mint a presigned PUT for a browser upload (requires `storage.write`). */
1213
- getUploadUrl(input: StorageUploadUrlInput): Promise<StorageUploadUrl>;
1214
- /**
1215
- * Mint a presigned GET (requires `storage.read`). `download: true` forces a
1216
- * save-as via `Content-Disposition`, which is what an emailed link wants.
1217
- */
1218
- getDownloadUrl(key: string, opts?: {
1219
- expiresInSeconds?: number;
1220
- download?: boolean;
1221
- }): Promise<StorageDownloadUrl>;
1222
- /**
1223
- * Write a generated asset (requires `storage.write`). Capped at 6 MiB — for
1224
- * anything larger, mint a presigned upload URL and stream to it.
1225
- */
1226
- put(pathname: string, body: string, opts?: StoragePutOptions): Promise<StoragePutResult>;
1227
- /** Read an asset back; `null` when absent (requires `storage.read`). */
1228
- get(key: string, opts?: {
1229
- encoding?: "utf8" | "base64";
1230
- }): Promise<{
1231
- body: string;
1232
- contentType: string | null;
1233
- size: number;
1234
- encoding: string;
1235
- } | null>;
1236
- /** Metadata without the body; `null` when absent (requires `storage.read`). */
1237
- head(key: string): Promise<StorageObject | null>;
1238
- /**
1239
- * One page of assets (requires `storage.read`). Paginated deliberately — pass
1240
- * the returned `cursor` back to walk a large silo without loading it all.
1241
- */
1242
- list(opts?: {
1243
- prefix?: string;
1244
- limit?: number;
1245
- cursor?: string;
1246
- }): Promise<StorageListPage>;
1247
- /**
1248
- * Delete one or many assets (requires `storage.write`). Idempotent — deleting
1249
- * an absent key is a no-op success. `deleted` is the number of keys **accepted**
1250
- * for deletion (an absent key counts too), not a count of keys that existed.
1251
- */
1252
- delete(keyOrKeys: string | string[]): Promise<{
1253
- deleted: number;
1254
- }>;
1255
- /** Server-side copy within the app's silo (requires `storage.write`). */
1256
- copy(fromKey: string, toPathname: string, opts?: {
1257
- allowOverwrite?: boolean;
1258
- }): Promise<StoragePutResult>;
1259
- }
1260
- /**
1261
- * The data-plane context passed as the second argument to a function's default
1262
- * export. Mirrors the host-assembled `ctx` (`__buildCtx` in `runtime.rs`);
1263
- * every member is a host-provided async function bridged to a Rust backend.
1264
- */
1265
- interface OxyFunctionContext {
1266
- /** Invoking user (route) or system identity (schedule/airway). */
1267
- user: OxyFunctionUser;
1268
- /**
1269
- * The org's people directory. Requires `"org": { "read": true }` in this
1270
- * function's manifest entry — without it the call is rejected before any
1271
- * query reaches the database.
1272
- *
1273
- * For naming a person: an assignee, a roster entry, who submitted something.
1274
- * Returns a display name and a role, and deliberately **no email, no phone,
1275
- * no location**.
1276
- *
1277
- * Who is in it: **people who can reach this app**. Org members, plus frontline
1278
- * workers holding a grant on this app — `kind` tells them apart, so a caller
1279
- * that must not name a worker can refuse on the field rather than by
1280
- * convention. A worker's `role` is `null`: that vocabulary is org membership's
1281
- * and a worker has none.
1282
- *
1283
- * REQUIRED, like every sibling here — `oltp`, `secrets`, `email`, `storage`,
1284
- * `airway` are all gated and all declared required. The binding is
1285
- * unconditional: `__buildCtx` is a static string that attaches `org` whatever
1286
- * the manifest says, and the refusal lives in the op, not in the binding. An
1287
- * optional member would therefore be a lie in the other direction, and under
1288
- * `strict` it makes `ctx.org.people()` — the spelling in every doc here and
1289
- * the only one the host binds — fail with "possibly undefined".
1290
- */
1291
- org: {
1292
- people(): Promise<{
1293
- people: Array<{
1294
- id: string;
1295
- name: string;
1296
- /** The org role, or `null` for a frontline worker. */
1297
- role: string | null;
1298
- kind: "member" | "frontline";
1299
- }>;
1300
- total: number;
1301
- }>;
1302
- };
1303
- /** Read-only view of the app's configured secrets (project-scoped). */
1304
- env: Record<string, string>;
1305
- /** Structured per-invocation logging (captured + surfaced with the response). */
1306
- log(...args: unknown[]): void;
1307
- /** Read-only SQL (SELECT/WITH only), function-scoped row cap. Resolves to the rows. */
1308
- query(sql: string): Promise<OxyFunctionRow[]>;
1309
- /** Read-only SQL with a higher row cap, yielded to the caller in batches. */
1310
- queryStream(sql: string, opts?: {
1311
- batchSize?: number;
1312
- }): AsyncGenerator<OxyFunctionRow[], void, unknown>;
1313
- /**
1314
- * SSRF-allowlisted outbound HTTP with a response-size cap. Pass
1315
- * `{ encoding: "base64" }` for a binary response — the default UTF-8 decode
1316
- * corrupts it.
1317
- */
1318
- fetch(url: string, init?: OxyFetchInit): Promise<OxyFetchResult>;
1319
- warehouse: OxyWarehouseApi;
1320
- /**
1321
- * Run several statements atomically on one connection: commits when your
1322
- * callback resolves, rolls back when it throws, and rethrows your error
1323
- * either way. Resolves to whatever the callback returns.
1324
- *
1325
- * `database` must be in this function's manifest `destinations` — a
1326
- * transaction is a write, and the same fail-closed allowlist applies. Postgres
1327
- * only; other backends reject `ctx.tx` rather than faking it.
1328
- *
1329
- * **Do not catch a failed statement and return normally.** A statement the
1330
- * server rejects aborts the whole transaction, and `COMMIT` on an aborted
1331
- * transaction does not fail — Postgres applies nothing and reports success —
1332
- * so `ctx.tx` refuses to commit and throws instead, naming the statement that
1333
- * poisoned it. Let the error propagate.
1334
- *
1335
- * ```ts
1336
- * const orderId = await ctx.tx("appdb", async (tx) => {
1337
- * const [{ id }] = await tx.query(
1338
- * "INSERT INTO orders (table_no) VALUES ($1) RETURNING id",
1339
- * [tableNo],
1340
- * );
1341
- * for (const it of items) {
1342
- * await tx.exec(
1343
- * "INSERT INTO order_items (order_id, sku, qty) VALUES ($1, $2, $3)",
1344
- * [id, it.sku, it.qty],
1345
- * );
1346
- * }
1347
- * return id;
1348
- * });
1349
- * ```
1350
- */
1351
- tx<T>(database: string, fn: (tx: OxyTransaction) => Promise<T> | T): Promise<T>;
1352
- /**
1353
- * Read/write the app's OWN per-org OLTP schema (derived from its slug). The
1354
- * write half `ctx.warehouse` cannot give an app on a managed database. Gated
1355
- * by the fail-closed `oltp` manifest capability (`{ enabled: true }`). See
1356
- * {@link OxyOltpApi}.
1357
- */
1358
- oltp: OxyOltpApi;
1359
- secrets: OxySecretsApi;
1360
- semantic: OxySemanticApi;
1361
- airway: OxyAirwayApi;
1362
- email: OxyEmailApi;
1363
- storage: OxyStorageApi;
1364
- }
1365
- /** Signature of a function's default export: `export default async (req, ctx) => Response`. */
1366
- type OxyFunctionHandler = (req: OxyFunctionRequest, ctx: OxyFunctionContext) => Promise<Response> | Response;
1367
- //#endregion
1368
1014
  //#region src/custom-app/inject.d.ts
1369
1015
  /**
1370
1016
  * Shape of `window.__OXY_APP__` written by oxy at serve time.
@@ -1432,12 +1078,48 @@ declare function useMetricTree(opts?: UseMetricTreeOpts): MetricTreeHookResult<M
1432
1078
  * measure" question. Pass `null` to stay idle until a measure is chosen.
1433
1079
  */
1434
1080
  declare function useSensitivity(measureId: string | null, opts?: EndpointOpts): MetricTreeHookResult<SensitivityResult>;
1081
+ interface UsePredictOpts extends EndpointOpts, PredictOptions {}
1435
1082
  /**
1436
1083
  * Propagate hypothetical `(measure, delta)` changes upward through the
1437
1084
  * tree and return the estimated impact on every downstream measure — a
1438
1085
  * pure metric-tree walk, no warehouse query. Pass `null` to stay idle.
1086
+ *
1087
+ * Because it is database-free it can only use the coefficients it is GIVEN.
1088
+ * Without `opts.coefficients` from {@link useBaseline}, every driver edge
1089
+ * whose `.view.yml` declares no `coefficient:` contributes nothing and its
1090
+ * downstream measures are simply absent from `impacts` — no error, no
1091
+ * refusal. Without `opts.values`, multiplicative component edges come back
1092
+ * `unquantifiable` rather than sized.
1439
1093
  */
1440
- declare function usePredict(changes: PredictChange[] | null, opts?: EndpointOpts): MetricTreeHookResult<PredictResult>;
1094
+ declare function usePredict(changes: PredictChange[] | null, opts?: UsePredictOpts): MetricTreeHookResult<PredictResult>;
1095
+ /**
1096
+ * Value a scenario's starting point, and measure the coefficients it needs.
1097
+ *
1098
+ * Two warehouse reads: the current value of every node reachable from
1099
+ * `request.roots`, and — for driver edges declaring no `coefficient:` — a fit
1100
+ * over the window. Both are expensive, which is why they live here and not in
1101
+ * {@link usePredict}: predict is database-free by design so it can re-run per
1102
+ * keystroke, and it CANNOT measure a coefficient itself.
1103
+ *
1104
+ * That is the whole reason to call this. Feed `data.values` and `data.fitted`
1105
+ * into `usePredict`; omit them and an undeclared edge propagates nothing.
1106
+ * Pass `null` to stay idle until levers and a period are chosen.
1107
+ */
1108
+ declare function useBaseline(request: BaselineRequest | null, opts?: EndpointOpts): MetricTreeHookResult<BaselineResponse>;
1109
+ /**
1110
+ * Bucketed history for the levers and everything downstream, plus the forward
1111
+ * curve the forecaster expects next — the scenario's time axis.
1112
+ *
1113
+ * One warehouse query, so it belongs on a window change, not on a lever edit.
1114
+ * It returns the BASELINE curve only: the scenario's second curve is
1115
+ * arithmetic over this and a `usePredict` result — a proportional shift
1116
+ * landing `lag` buckets in — composed client-side precisely so editing a lever
1117
+ * costs no query.
1118
+ *
1119
+ * Treat a series with a `refusal` as a stated absence: it must not render as a
1120
+ * flat forward line. Pass `null` to stay idle.
1121
+ */
1122
+ declare function useProjection(request: ProjectionRequest | null, opts?: EndpointOpts): MetricTreeHookResult<ProjectionResponse>;
1441
1123
  /**
1442
1124
  * Period-over-period root-cause decomposition: recursively splits the
1443
1125
  * target measure by components and dimensions until the move concentrates.
@@ -1523,6 +1205,16 @@ interface WorldModel {
1523
1205
  interface WmInstance {
1524
1206
  key: string;
1525
1207
  display: string;
1208
+ /**
1209
+ * The org location this instance is, when the entity is bound to the
1210
+ * locations registry and the key is mapped; absent otherwise.
1211
+ */
1212
+ location?: {
1213
+ id: string;
1214
+ name: string;
1215
+ kind?: string | null;
1216
+ parent_id?: string | null;
1217
+ };
1526
1218
  }
1527
1219
  interface WmInstancesResponse {
1528
1220
  total: number;
@@ -1609,6 +1301,14 @@ interface UseWorldModelInstancesOpts {
1609
1301
  /** Max rows to return (default 50 server-side). */
1610
1302
  limit?: number;
1611
1303
  enabled?: boolean;
1304
+ /**
1305
+ * `"reach"` — only the instances whose place the viewer reaches, filtered
1306
+ * inside the scan so a page is a page of the right set. Needs the entity
1307
+ * bound to the org's locations registry (refused otherwise); an instance
1308
+ * whose key is unmapped is not in anyone's reach. The bundle's own app is
1309
+ * sent along so app-admin standing counts.
1310
+ */
1311
+ scope?: "reach";
1612
1312
  }
1613
1313
  interface UseWorldModelInstancesResult {
1614
1314
  data: WmInstancesResponse | null;
@@ -1732,5 +1432,5 @@ declare function createWorldModel(projectId: string | null, fetcher: AppFetcher)
1732
1432
  */
1733
1433
  declare function useWorldModel(): WorldModelApi;
1734
1434
  //#endregion
1735
- export { type AdditivityClass, type AgentArtifact, type AgentRunEvent, type AgentRunState, type AgentSqlArtifact, AnomaliesClient, type Anomaly, type AnomalyFilter, type AnomalySeverity, type AnomalyStatus, type AppFetcher, type BulkUpdateStatusResponse, type CustomAppDebugSnapshot, type CustomAppErrorReport, type DimensionOpportunity, type DistributionRequest, type DriverAttribution, type DriverConfidence, type DriverDirection, type DriverForm, type DriverStrength, type EdgeKind, type EmailAttachment, type EmailSendInput, type EmailSendResult, type ExpandedNode, type ExplainConfigOverride, type ExplainNode, type ExplainOptions, type ExplainOpts, type ExplainRequest, type ExplainResult, type ExplainSibling, type ExplainWarning, type FunctionError, type FunctionLog, type FunctionResult, type ListAnomaliesOptions, type ListAnomaliesResponse, type LoadManifestOptions, type MetricEdge, type MetricHandle, type MetricNode, type MetricScope, type MetricTree, MetricTreeClient, type MetricTreeHookResult, type OpportunityRequest, type OpportunityResult, type OxyAirwayApi, OxyAnswer, type OxyAnswerProps, OxyApiError, type OxyAppFunctionManifest, type OxyAppLogLevel, type OxyAppLogger, type OxyAppManifest, type OxyAppPerformanceManifest, OxyAppProvider, type OxyAppProviderProps, OxyChat, type OxyChatProps, type OxyEmailApi, type OxyFetchResult, type OxyFunctionContext, type OxyFunctionHandler, type OxyFunctionRequest, type OxyFunctionRow, type OxyFunctionUser, type OxyIdentityKind, type OxyInjectedAppConfig, type OxyOltpApi, type OxyOrgTeam, type OxySecretsApi, type OxySemanticApi, type OxyStorageApi, type OxyTransaction, type OxyWarehouseApi, type PredictChange, type PredictImpact, type PredictResult, type ProcedureProgress, type ProcedureResult, type ProcedureRunState, type ResolvedCustomAppManifest, type ScanFailure, type ScanOptions, type ScanResponse, type SegmentOpportunity, type SemanticArrayOp, type SemanticDateRangeOp, type SemanticFilter, type SemanticScalarOp, type SemanticTimeDimension, type SensitivityDriver, type SensitivityResult, type SizeOpts, type SkippedDimension, type SplitKind, type StorageDownloadUrl, type StorageListPage, type StorageObject, type StoragePutOptions, type StoragePutResult, type StorageUploadUrl, type StorageUploadUrlInput, type TimeDimensionsResponse, type UseAgentRunInput, type UseAgentRunResult, type UseFunctionResult, type UseMeasureBreakdownResult, type UseMetricTreeOpts, type UseProcedureRunInput, type UseProcedureRunOpts, type UseProcedureRunResult, type UseQueryInput, type UseQueryOpts, type UseQueryResult, type UseSemanticQueryInput, type UseSemanticQueryOpts, type UseSemanticQueryResult, type UseWorldModelGraphResult, type UseWorldModelInstancesOpts, type UseWorldModelInstancesResult, type WmBreakdownEdge, type WmBreakdownNode, type WmEntityCount, type WmFilterCountsResponse, type WmInstance, type WmInstancesResponse, type WmMeasureBreakdown, type WmMeasureBreakdownEvent, type WorldModel, type WorldModelApi, type WorldModelDimension, type WorldModelEdge, type WorldModelEntity, type WorldModelInducedMeasure, type WorldModelMeasure, WorldModelScopeUnsupportedError, _resetCustomAppManifestCacheForTest, apiErrorFromResponse, base64ToBytes, bytesToBase64, createWorldModel, getCustomAppDebug, getOxyAppLogger, interpretCustomAppError, loadCustomAppManifest, readInjectedAppConfig, readJsonSseStream, setOxyAppLogger, useAgentRun, useDistribution, useExplain, useFunction, useMeasureBreakdown, useMetricTree, useOpportunity, useOxyApp, usePredict, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery, useSensitivity, useTimeDimensions, useTrackEvent, useWorldModel, useWorldModelGraph, useWorldModelInstances };
1435
+ export { type AdditivityClass, type AgentArtifact, type AgentRunEvent, type AgentRunState, type AgentSqlArtifact, AnomaliesClient, type Anomaly, type AnomalyFilter, type AnomalySeverity, type AnomalyStatus, type AppFetcher, type BaselineInstance, type BaselineRequest, type BaselineResponse, type BulkUpdateStatusResponse, type CustomAppDebugSnapshot, type CustomAppErrorReport, type DimensionOpportunity, type DistributionRequest, type DriverAttribution, type DriverConfidence, type DriverDirection, type DriverForm, type DriverStrength, type EdgeKind, type EmailAttachment, type EmailSendInput, type EmailSendResult, type ExpandedNode, type ExplainConfigOverride, type ExplainNode, type ExplainOptions, type ExplainOpts, type ExplainRequest, type ExplainResult, type ExplainSibling, type ExplainWarning, type FittedDriver, type ForecastPoint, type FunctionError, type FunctionLog, type FunctionResult, type HistoryPoint, type ListAnomaliesOptions, type ListAnomaliesResponse, type LoadManifestOptions, type MeasureProjection, type MeasureValues, type MetricEdge, type MetricHandle, type MetricNode, type MetricScope, type MetricTree, MetricTreeClient, type MetricTreeHookResult, type OpportunityRequest, type OpportunityResult, type OxyAirwayApi, OxyAnswer, type OxyAnswerProps, OxyApiError, type OxyAppFunctionManifest, type OxyAppLogLevel, type OxyAppLogger, type OxyAppManifest, type OxyAppPerformanceManifest, OxyAppProvider, type OxyAppProviderProps, OxyChat, type OxyChatProps, type OxyEmailApi, type OxyFetchResult, type OxyFunctionContext, type OxyFunctionHandler, type OxyFunctionRequest, type OxyFunctionRow, type OxyFunctionUser, type OxyIdentityKind, type OxyInjectedAppConfig, type OxyOltpApi, type OxyOrgAssignment, type OxyOrgPlace, type OxyOrgTeam, type OxyReach, type OxySecretsApi, type OxySemanticApi, type OxyStorageApi, type OxyTransaction, type OxyWarehouseApi, type PredictChange, type PredictImpact, type PredictOptions, type PredictResult, type ProcedureProgress, type ProcedureResult, type ProcedureRunState, type ProjectionGranularity, type ProjectionRequest, type ProjectionResponse, type ResolvedCustomAppManifest, type ScanFailure, type ScanOptions, type ScanResponse, type SegmentOpportunity, type SemanticArrayOp, type SemanticDateRangeOp, type SemanticFilter, type SemanticScalarOp, type SemanticTimeDimension, type SensitivityDriver, type SensitivityResult, type SizeOpts, type SkippedDimension, type SplitKind, type StorageDownloadUrl, type StorageListPage, type StorageObject, type StoragePutOptions, type StoragePutResult, type StorageUploadUrl, type StorageUploadUrlInput, type TimeDimensionsResponse, type UnvaluedNode, type UseAgentRunInput, type UseAgentRunResult, type UseFunctionResult, type UseMeasureBreakdownResult, type UseMetricTreeOpts, type UsePredictOpts, type UseProcedureRunInput, type UseProcedureRunOpts, type UseProcedureRunResult, type UseQueryInput, type UseQueryOpts, type UseQueryResult, type UseSemanticQueryInput, type UseSemanticQueryOpts, type UseSemanticQueryResult, type UseWorldModelGraphResult, type UseWorldModelInstancesOpts, type UseWorldModelInstancesResult, type WmBreakdownEdge, type WmBreakdownNode, type WmEntityCount, type WmFilterCountsResponse, type WmInstance, type WmInstancesResponse, type WmMeasureBreakdown, type WmMeasureBreakdownEvent, type WorldModel, type WorldModelApi, type WorldModelDimension, type WorldModelEdge, type WorldModelEntity, type WorldModelInducedMeasure, type WorldModelMeasure, WorldModelScopeUnsupportedError, _resetCustomAppManifestCacheForTest, apiErrorFromResponse, base64ToBytes, bytesToBase64, createWorldModel, getCustomAppDebug, getOxyAppLogger, interpretCustomAppError, loadCustomAppManifest, readInjectedAppConfig, readJsonSseStream, setOxyAppLogger, useAgentRun, useBaseline, useDistribution, useExplain, useFunction, useMeasureBreakdown, useMetricTree, useOpportunity, useOxyApp, usePredict, useProcedureRun, useProjection, useQuery, useResolvedManifest, useSemanticQuery, useSensitivity, useTimeDimensions, useTrackEvent, useWorldModel, useWorldModelGraph, useWorldModelInstances };
1736
1436
  //# sourceMappingURL=index.d.mts.map