@oxy-hq/sdk 2.6.0 → 2.9.1

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.cjs CHANGED
@@ -1,8 +1,20 @@
1
1
  // @oxy/sdk - TypeScript SDK for Oxy data platform
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
3
- const require_react = require('./react-BAiiXftp.cjs');
3
+ const require_react = require('./react-CDYGAfGA.cjs');
4
+ let react = require("react");
5
+ react = require_react.__toESM(react, 1);
4
6
 
5
7
  //#region src/anomalies.ts
8
+ /** Which buckets a write may touch when the caller didn't say. Live statuses
9
+ * for ack/dismiss; all three for a reopen, which exists to reach dismissed
10
+ * ones. */
11
+ function defaultScope(status) {
12
+ return status === "new" ? [
13
+ "new",
14
+ "acknowledged",
15
+ "dismissed"
16
+ ] : ["new", "acknowledged"];
17
+ }
6
18
  /**
7
19
  * Client for `/semantic/anomalies*`. Construct via `OxyClient.anomalies`
8
20
  * rather than instantiating directly — the getter wires the request helper
@@ -31,18 +43,25 @@ var AnomaliesClient = class {
31
43
  return qs ? `?${qs}` : "";
32
44
  }
33
45
  /**
34
- * List anomalies in the inbox, newest first.
46
+ * List anomalies in the inbox, ranked worst-first by event severity (active
47
+ * events before dismissed). Pass `order: "recent"` for latest-first.
35
48
  *
36
49
  * @example
37
50
  * ```typescript
38
51
  * // Open / unresolved anomalies only
39
52
  * const { anomalies } = await client.anomalies.list({ status: "new" });
53
+ *
54
+ * // Second page of 25 events
55
+ * const page2 = await client.anomalies.list({ limit: 25, offset: 25 });
56
+ * console.log(`${(page2.offset ?? 25) + 1}+ of ${page2.total ?? "?"}`);
40
57
  * ```
41
58
  */
42
59
  async list(options = {}) {
43
60
  const extra = {};
44
61
  if (options.status) extra.status = options.status;
45
- if (options.limit) extra.limit = String(options.limit);
62
+ if (options.limit !== void 0) extra.limit = String(options.limit);
63
+ if (options.offset !== void 0) extra.offset = String(options.offset);
64
+ if (options.order) extra.order = options.order;
46
65
  return this.request(this.path(this.buildQuery(extra)));
47
66
  }
48
67
  /**
@@ -50,11 +69,20 @@ var AnomaliesClient = class {
50
69
  * workspace, runs the detector, and upserts matching rows into the
51
70
  * inbox. Returns counts of scanned / failed / persisted.
52
71
  *
72
+ * Long-running: the server waits up to 55 s, then returns
73
+ * `pending: true` with zeroed counts while the scan finishes in the
74
+ * background. Always check `pending` before treating `0` as "nothing
75
+ * found", and refetch with {@link list} shortly after.
76
+ *
53
77
  * @example
54
78
  * ```typescript
55
79
  * // Scan against a known-good reference date (matches the seed dataset)
56
80
  * const result = await client.anomalies.scan({ as_of: "2025-12-15" });
57
- * console.log(`${result.anomalies_persisted} anomalies detected`);
81
+ * if (result.pending) {
82
+ * console.log("scan still running — refetch shortly");
83
+ * } else {
84
+ * console.log(`${result.anomalies_persisted} anomalies detected`);
85
+ * }
58
86
  * ```
59
87
  */
60
88
  async scan(options = {}) {
@@ -73,15 +101,156 @@ var AnomaliesClient = class {
73
101
  });
74
102
  }
75
103
  /**
104
+ * Update many anomalies in one request — the batch form of
105
+ * {@link updateStatus}. Identifiers outside the workspace are skipped rather
106
+ * than erroring, so `updated` (rows written) can be lower than what you sent.
107
+ * At most 2000 identifiers across both lists.
108
+ *
109
+ * **Prefer `eventIds`.** Inbox actions are per *event*, and a list response
110
+ * caps how many buckets it returns per event — so acking the bucket ids you
111
+ * received can leave the tail of a long chain behind, `new`, under a clean
112
+ * success. Naming the event lets the server write all of it. `ids` is for
113
+ * rows with no `event_id` (detected before events existed), which can only
114
+ * be named individually.
115
+ *
116
+ * `onlyStatuses` says which of an event's buckets may move. An event can span
117
+ * statuses, so an unbounded write resurrects buckets that were dismissed on
118
+ * purpose — which is why omitting it takes a scope rather than no bound at
119
+ * all: the live statuses (`["new", "acknowledged"]`) for an ack or dismiss,
120
+ * and all three for `status: "new"`, since reopening is how a dismissed
121
+ * anomaly comes back. The server applies that same default, so the safe
122
+ * behaviour does not depend on going through this client. Pass `[]` to opt
123
+ * out of the bound entirely.
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * const { anomalies } = await client.anomalies.list({ status: "new", limit: 50, offset: 0 });
128
+ * // Both lists: events by id, and pre-event rows (no `event_id`) by their own.
129
+ * const eventIds = [...new Set(anomalies.flatMap((a) => (a.event_id ? [a.event_id] : [])))];
130
+ * const ids = anomalies.filter((a) => !a.event_id).map((a) => a.id);
131
+ * const { updated } = await client.anomalies.updateStatusBulk(
132
+ * { ids, eventIds, onlyStatuses: ["new", "acknowledged"] },
133
+ * "acknowledged"
134
+ * );
135
+ * ```
136
+ */
137
+ async updateStatusBulk(target, status) {
138
+ return this.request(this.path(`/status${this.buildQuery()}`), {
139
+ method: "POST",
140
+ body: JSON.stringify({
141
+ ids: target.ids ?? [],
142
+ event_ids: target.eventIds ?? [],
143
+ only_statuses: target.onlyStatuses ?? defaultScope(status),
144
+ status
145
+ })
146
+ });
147
+ }
148
+ /**
76
149
  * Run the metric-tree `explain` for an anomaly and cache the result on
77
- * the row. Subsequent calls return the cached `ExplainResult` instantly.
150
+ * the row. Subsequent calls return the cached `ExplainResult` instantly;
151
+ * pass `{ refresh: true }` to bust the cache and recompute.
152
+ *
153
+ * The uncached path runs a 20-30 s recursive driver search — budget for it
154
+ * (or read `explain_cache` off the row from {@link list} when it's already
155
+ * populated).
78
156
  */
79
- async explain(anomalyId) {
80
- const query = this.buildQuery();
81
- return this.request(this.path(`/${encodeURIComponent(anomalyId)}/explain${query}`), { method: "POST" });
157
+ async explain(anomalyId, options = {}) {
158
+ const extra = {};
159
+ if (options.refresh) extra.refresh = "true";
160
+ return this.request(this.path(`/${encodeURIComponent(anomalyId)}/explain${this.buildQuery(extra)}`), { method: "POST" });
82
161
  }
83
162
  };
84
163
 
164
+ //#endregion
165
+ //#region src/custom-app/base64.ts
166
+ const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
167
+ /** Reverse lookup; 255 marks "not a base64 character". */
168
+ const B64R = /* @__PURE__ */ (() => {
169
+ const t = (/* @__PURE__ */ new Uint8Array(256)).fill(255);
170
+ for (let i = 0; i < 64; i++) t[B64.charCodeAt(i)] = i;
171
+ return t;
172
+ })();
173
+ /**
174
+ * Chunk size for building output in segments. Byte-at-a-time `+=` allocates a
175
+ * rope node per byte, and `String.fromCharCode.apply` blows the argument limit
176
+ * on large inputs; 8k avoids both.
177
+ */
178
+ const CHUNK = 8192;
179
+ function asBytes(input) {
180
+ if (input instanceof Uint8Array) return input;
181
+ if (input instanceof ArrayBuffer) return new Uint8Array(input);
182
+ return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
183
+ }
184
+ /**
185
+ * Encode bytes as standard (padded) base64.
186
+ *
187
+ * ```ts
188
+ * const pdf = new Uint8Array(await renderReport());
189
+ * await ctx.email.send({
190
+ * to: ctx.user.email,
191
+ * subject: "Report",
192
+ * text: "attached",
193
+ * attachments: [{ filename: "report.pdf", content: bytesToBase64(pdf) }]
194
+ * });
195
+ * ```
196
+ *
197
+ * For **text** you generated, skip this entirely and pass the string with
198
+ * `encoding: "utf8"` — it needs no encoder and stays byte-exact for non-ASCII.
199
+ */
200
+ function bytesToBase64(input) {
201
+ const bytes = asBytes(input);
202
+ const parts = [];
203
+ let buf = "";
204
+ for (let i = 0; i < bytes.length; i += 3) {
205
+ const b0 = bytes[i];
206
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
207
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
208
+ const n = b0 << 16 | b1 << 8 | b2;
209
+ buf += B64[n >> 18 & 63] + B64[n >> 12 & 63] + (i + 1 < bytes.length ? B64[n >> 6 & 63] : "=") + (i + 2 < bytes.length ? B64[n & 63] : "=");
210
+ if (buf.length >= CHUNK) {
211
+ parts.push(buf);
212
+ buf = "";
213
+ }
214
+ }
215
+ parts.push(buf);
216
+ return parts.join("");
217
+ }
218
+ /**
219
+ * Decode standard base64 to bytes — e.g. the body from
220
+ * `ctx.storage.get(key, { encoding: "base64" })`.
221
+ *
222
+ * Throws on malformed input rather than returning a short buffer: a truncated
223
+ * decode that reports success is a corrupt file nobody notices.
224
+ */
225
+ function base64ToBytes(base64) {
226
+ let s = String(base64).replace(/[ \t\n\f\r]/g, "");
227
+ if (s.length % 4 === 0) {
228
+ let pad = 0;
229
+ while (pad < 2 && s.charCodeAt(s.length - 1) === 61) {
230
+ s = s.slice(0, -1);
231
+ pad++;
232
+ }
233
+ }
234
+ if (s.indexOf("=") >= 0) throw new TypeError("base64ToBytes: '=' may only appear as trailing padding");
235
+ if (s.length % 4 === 1) throw new TypeError("base64ToBytes: invalid base64 length");
236
+ const out = new Uint8Array(s.length * 3 >> 2);
237
+ let o = 0;
238
+ let buf = 0;
239
+ let bits = 0;
240
+ for (let i = 0; i < s.length; i++) {
241
+ const code = s.charCodeAt(i);
242
+ const v = code < 256 ? B64R[code] : 255;
243
+ if (v === 255) throw new TypeError(`base64ToBytes: invalid base64 character '${s[i]}'`);
244
+ buf = buf << 6 | v;
245
+ bits += 6;
246
+ if (bits >= 8) {
247
+ bits -= 8;
248
+ out[o++] = buf >> bits & 255;
249
+ }
250
+ }
251
+ return out.subarray(0, o);
252
+ }
253
+
85
254
  //#endregion
86
255
  //#region src/custom-app/debug.ts
87
256
  /**
@@ -105,6 +274,547 @@ async function getCustomAppDebug(resolved) {
105
274
  return snapshot;
106
275
  }
107
276
 
277
+ //#endregion
278
+ //#region src/custom-app/metric-tree-fetch.ts
279
+ /** Base path for the metric-tree endpoints of `projectId`. */
280
+ function metricTreePath(projectId) {
281
+ return `/api/projects/${projectId}/semantic/metric-tree`;
282
+ }
283
+ /** GET `url`, decoding JSON or throwing a typed {@link OxyApiError}. */
284
+ async function getJson(fetcher, url, signal) {
285
+ const resp = await fetcher(url, {
286
+ method: "GET",
287
+ signal
288
+ });
289
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
290
+ return await resp.json();
291
+ }
292
+ /** POST `body` (tagged `v: 1`) to `url`, decoding JSON or throwing. */
293
+ async function postJson(fetcher, url, body, signal) {
294
+ const resp = await fetcher(url, {
295
+ method: "POST",
296
+ headers: { "content-type": "application/json" },
297
+ body: JSON.stringify({
298
+ v: 1,
299
+ ...body
300
+ }),
301
+ signal
302
+ });
303
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
304
+ return await resp.json();
305
+ }
306
+
307
+ //#endregion
308
+ //#region src/custom-app/metric-tree-hooks.tsx
309
+ /**
310
+ * Internal engine shared by every metric-tree hook. Runs `run(signal)`
311
+ * whenever `key` changes (and on `refetch`), tracks loading/error, and
312
+ * cancels in-flight work on unmount or input change.
313
+ *
314
+ * `key` is the deep-compare fingerprint of the request; a `null` key
315
+ * means "no request yet" and leaves the hook idle without firing.
316
+ */
317
+ function useMetricTreeEndpoint(key, run, enabled) {
318
+ const [data, setData] = react.useState(null);
319
+ const [loading, setLoading] = react.useState(enabled && key !== null);
320
+ const [error, setError] = react.useState(null);
321
+ const [_nonce, setNonce] = react.useState(0);
322
+ const runRef = react.useRef(run);
323
+ runRef.current = run;
324
+ react.useEffect(() => {
325
+ if (!enabled || key === null) {
326
+ setLoading(false);
327
+ return;
328
+ }
329
+ const ctrl = new AbortController();
330
+ let cancelled = false;
331
+ setLoading(true);
332
+ setError(null);
333
+ runRef.current(ctrl.signal).then((result) => {
334
+ if (cancelled) return;
335
+ setData(result);
336
+ setLoading(false);
337
+ }).catch((err) => {
338
+ if (cancelled) return;
339
+ if (err instanceof DOMException && err.name === "AbortError") return;
340
+ setError(err instanceof Error ? err : new Error(String(err)));
341
+ setLoading(false);
342
+ });
343
+ return () => {
344
+ cancelled = true;
345
+ ctrl.abort();
346
+ };
347
+ }, [key, enabled]);
348
+ return {
349
+ data,
350
+ loading,
351
+ error,
352
+ refetch: react.useCallback(() => setNonce((n) => n + 1), [])
353
+ };
354
+ }
355
+ /**
356
+ * The project's metric tree — measures (nodes) and their component /
357
+ * driver relationships (edges) — or the subtree rooted at `opts.root`.
358
+ * The structural backbone every other metric-tree analysis reads against.
359
+ */
360
+ function useMetricTree(opts = {}) {
361
+ const { projectId, fetcher } = require_react.useOxyApp();
362
+ const enabled = opts.enabled !== false;
363
+ const root = opts.root;
364
+ return useMetricTreeEndpoint(projectId ? JSON.stringify({
365
+ projectId,
366
+ root
367
+ }) : null, (signal) => {
368
+ const qs = root ? `?root=${encodeURIComponent(root)}` : "";
369
+ return getJson(fetcher, `${metricTreePath(projectId)}${qs}`, signal);
370
+ }, enabled);
371
+ }
372
+ /**
373
+ * Ranked drivers of `measureId`, by influence — the "what moves this
374
+ * measure" question. Pass `null` to stay idle until a measure is chosen.
375
+ */
376
+ function useSensitivity(measureId, opts = {}) {
377
+ const { projectId, fetcher } = require_react.useOxyApp();
378
+ const enabled = opts.enabled !== false;
379
+ return useMetricTreeEndpoint(projectId && measureId ? JSON.stringify({
380
+ projectId,
381
+ measureId
382
+ }) : null, (signal) => {
383
+ const path = `${metricTreePath(projectId)}/${encodeURIComponent(measureId)}/sensitivity`;
384
+ return getJson(fetcher, path, signal);
385
+ }, enabled);
386
+ }
387
+ /**
388
+ * Propagate hypothetical `(measure, delta)` changes upward through the
389
+ * tree and return the estimated impact on every downstream measure — a
390
+ * pure metric-tree walk, no warehouse query. Pass `null` to stay idle.
391
+ */
392
+ function usePredict(changes, opts = {}) {
393
+ const { projectId, fetcher } = require_react.useOxyApp();
394
+ const enabled = opts.enabled !== false;
395
+ return useMetricTreeEndpoint(projectId && changes ? JSON.stringify({
396
+ projectId,
397
+ changes
398
+ }) : null, (signal) => postJson(fetcher, `${metricTreePath(projectId)}/predict`, { changes }, signal), enabled);
399
+ }
400
+ /**
401
+ * Period-over-period root-cause decomposition: recursively splits the
402
+ * target measure by components and dimensions until the move concentrates.
403
+ * This is the heavy one — it can fire many warehouse queries and the
404
+ * server caps it at 45s. Pass `null` to defer until periods are chosen.
405
+ */
406
+ function useExplain(request, opts = {}) {
407
+ const { projectId, fetcher } = require_react.useOxyApp();
408
+ const enabled = opts.enabled !== false;
409
+ return useMetricTreeEndpoint(projectId && request ? JSON.stringify({
410
+ projectId,
411
+ request
412
+ }) : null, (signal) => postJson(fetcher, `${metricTreePath(projectId)}/explain`, request, signal), enabled);
413
+ }
414
+ /**
415
+ * Single-period distribution of a measure — an {@link ExplainResult}
416
+ * against an auto-derived immediately-prior baseline. Same renderers as
417
+ * `useExplain`; ignore the delta fields for a pure distribution view.
418
+ */
419
+ function useDistribution(request, opts = {}) {
420
+ const { projectId, fetcher } = require_react.useOxyApp();
421
+ const enabled = opts.enabled !== false;
422
+ return useMetricTreeEndpoint(projectId && request ? JSON.stringify({
423
+ projectId,
424
+ request
425
+ }) : null, (signal) => postJson(fetcher, `${metricTreePath(projectId)}/distribution`, request, signal), enabled);
426
+ }
427
+ /**
428
+ * Segment opportunity sizing for a measure over a period: finds
429
+ * underperforming segments and sizes the addressable upside of closing
430
+ * each rate gap against a benchmark peer. Pass `null` to stay idle until
431
+ * a target + period are chosen.
432
+ */
433
+ function useOpportunity(request, opts = {}) {
434
+ const { projectId, fetcher } = require_react.useOxyApp();
435
+ const enabled = opts.enabled !== false;
436
+ return useMetricTreeEndpoint(projectId && request ? JSON.stringify({
437
+ projectId,
438
+ request
439
+ }) : null, (signal) => postJson(fetcher, `${metricTreePath(projectId)}/opportunity`, request, signal), enabled);
440
+ }
441
+ /**
442
+ * The queryable time dimensions per view (`view.dim` ids) — what a
443
+ * bundle offers as the period axis for `explain` / `opportunity` /
444
+ * `distribution` instead of hardcoding a curated map.
445
+ */
446
+ function useTimeDimensions(opts = {}) {
447
+ const { projectId, fetcher } = require_react.useOxyApp();
448
+ const enabled = opts.enabled !== false;
449
+ return useMetricTreeEndpoint(projectId ? JSON.stringify({
450
+ projectId,
451
+ kind: "time-dimensions"
452
+ }) : null, (signal) => getJson(fetcher, `${metricTreePath(projectId)}/time-dimensions`, signal), enabled);
453
+ }
454
+
455
+ //#endregion
456
+ //#region src/custom-app/sse.ts
457
+ /**
458
+ * Read a `text/event-stream` response, invoking `onEvent` with each parsed
459
+ * JSON frame. Frames that fail to parse are skipped (a malformed frame must
460
+ * not tear down the whole stream). Resolves when the body closes.
461
+ */
462
+ async function readJsonSseStream(resp, onEvent) {
463
+ const reader = resp.body?.getReader();
464
+ if (!reader) throw new Error("SSE response has no body stream");
465
+ const decoder = new TextDecoder();
466
+ let buffer = "";
467
+ for (;;) {
468
+ const { done, value } = await reader.read();
469
+ if (done) break;
470
+ buffer += decoder.decode(value, { stream: true });
471
+ let sep;
472
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
473
+ const frame = buffer.slice(0, sep);
474
+ buffer = buffer.slice(sep + 2);
475
+ let data = "";
476
+ for (const line of frame.split("\n")) if (line.startsWith("data:")) data += line.slice(5).trim();
477
+ if (!data) continue;
478
+ let parsed;
479
+ try {
480
+ parsed = JSON.parse(data);
481
+ } catch {
482
+ continue;
483
+ }
484
+ onEvent(parsed);
485
+ }
486
+ }
487
+ }
488
+
489
+ //#endregion
490
+ //#region src/custom-app/world-model-hooks.tsx
491
+ /** Base path for the world-model endpoints of the active project. */
492
+ function worldModelPath(projectId) {
493
+ return `/api/projects/${projectId}/semantic/world-model`;
494
+ }
495
+ /**
496
+ * The world-model graph — entities (nodes), their measures/dimensions, and
497
+ * how measures promote across the entity hierarchy (edges). Applies the
498
+ * project's `.world-model.yml` display config server-side.
499
+ *
500
+ * @remarks
501
+ * This returns the raw semantic-layer entity graph. For the higher-level
502
+ * node-paradigm interface (`world.metric(id)` speaking `expand` / `explain` /
503
+ * `size`), use {@link useWorldModel} from `./world-node` instead.
504
+ */
505
+ function useWorldModelGraph(opts = {}) {
506
+ const { projectId, fetcher } = require_react.useOxyApp();
507
+ const enabled = opts.enabled !== false;
508
+ const [data, setData] = react.useState(null);
509
+ const [loading, setLoading] = react.useState(enabled && !!projectId);
510
+ const [error, setError] = react.useState(null);
511
+ const [_nonce, setNonce] = react.useState(0);
512
+ react.useEffect(() => {
513
+ if (!enabled || !projectId) {
514
+ setLoading(false);
515
+ return;
516
+ }
517
+ const ctrl = new AbortController();
518
+ let cancelled = false;
519
+ setLoading(true);
520
+ setError(null);
521
+ fetcher(worldModelPath(projectId), {
522
+ method: "GET",
523
+ signal: ctrl.signal
524
+ }).then(async (resp) => {
525
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
526
+ return await resp.json();
527
+ }).then((result) => {
528
+ if (cancelled) return;
529
+ setData(result);
530
+ setLoading(false);
531
+ }).catch((err) => {
532
+ if (cancelled) return;
533
+ if (err instanceof DOMException && err.name === "AbortError") return;
534
+ setError(err instanceof Error ? err : new Error(String(err)));
535
+ setLoading(false);
536
+ });
537
+ return () => {
538
+ cancelled = true;
539
+ ctrl.abort();
540
+ };
541
+ }, [
542
+ enabled,
543
+ projectId,
544
+ fetcher
545
+ ]);
546
+ return {
547
+ data,
548
+ loading,
549
+ error,
550
+ refetch: react.useCallback(() => setNonce((n) => n + 1), [])
551
+ };
552
+ }
553
+ /**
554
+ * List the instances (rows) of `entityId` — a bounded, searchable picker
555
+ * over the entity's primary keys + display label. Pass `null` for `entityId`
556
+ * to stay idle until an entity is chosen.
557
+ */
558
+ function useWorldModelInstances(entityId, opts = {}) {
559
+ const { projectId, fetcher } = require_react.useOxyApp();
560
+ const enabled = opts.enabled !== false;
561
+ const { search, limit } = opts;
562
+ const [data, setData] = react.useState(null);
563
+ const [loading, setLoading] = react.useState(enabled && !!projectId && !!entityId);
564
+ const [error, setError] = react.useState(null);
565
+ const [_nonce, setNonce] = react.useState(0);
566
+ react.useEffect(() => {
567
+ if (!enabled || !projectId || !entityId) {
568
+ setLoading(false);
569
+ return;
570
+ }
571
+ const ctrl = new AbortController();
572
+ let cancelled = false;
573
+ setLoading(true);
574
+ setError(null);
575
+ const params = new URLSearchParams({ entity: entityId });
576
+ if (search) params.set("search", search);
577
+ if (limit != null) params.set("limit", String(limit));
578
+ fetcher(`${worldModelPath(projectId)}/instances?${params}`, {
579
+ method: "GET",
580
+ signal: ctrl.signal
581
+ }).then(async (resp) => {
582
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
583
+ return await resp.json();
584
+ }).then((result) => {
585
+ if (cancelled) return;
586
+ setData(result);
587
+ setLoading(false);
588
+ }).catch((err) => {
589
+ if (cancelled) return;
590
+ if (err instanceof DOMException && err.name === "AbortError") return;
591
+ setError(err instanceof Error ? err : new Error(String(err)));
592
+ setLoading(false);
593
+ });
594
+ return () => {
595
+ cancelled = true;
596
+ ctrl.abort();
597
+ };
598
+ }, [
599
+ enabled,
600
+ projectId,
601
+ entityId,
602
+ search,
603
+ limit,
604
+ fetcher
605
+ ]);
606
+ return {
607
+ data,
608
+ loading,
609
+ error,
610
+ refetch: react.useCallback(() => setNonce((n) => n + 1), [])
611
+ };
612
+ }
613
+ /** Fold one measure-breakdown SSE frame into the accumulated graph. */
614
+ function foldBreakdown(prev, ev) {
615
+ switch (ev.kind) {
616
+ case "init": return {
617
+ root: ev.root,
618
+ nodes: ev.nodes.map((n) => ({
619
+ ...n,
620
+ value: null,
621
+ unvalued_reason: null
622
+ })),
623
+ edges: ev.edges
624
+ };
625
+ case "value":
626
+ if (!prev) return prev;
627
+ return {
628
+ ...prev,
629
+ nodes: prev.nodes.map((n) => n.id === ev.node_id ? {
630
+ ...n,
631
+ value: ev.value,
632
+ unvalued_reason: ev.unvalued_reason
633
+ } : n)
634
+ };
635
+ default: return prev;
636
+ }
637
+ }
638
+ /**
639
+ * Stream the driver-tree breakdown of one instance's measure — the metric
640
+ * decomposition (add/sub/mul/div component graph) with each node's value
641
+ * filling in as it resolves. This is the per-instance RCA view. Pass `null`
642
+ * for `measure` to stay idle.
643
+ */
644
+ function useMeasureBreakdown(entityId, keyValue, measure) {
645
+ const { projectId, fetcher } = require_react.useOxyApp();
646
+ const [breakdown, setBreakdown] = react.useState(null);
647
+ const [loading, setLoading] = react.useState(false);
648
+ const [done, setDone] = react.useState(false);
649
+ const [error, setError] = react.useState(null);
650
+ react.useEffect(() => {
651
+ if (!projectId || !entityId || !keyValue || !measure) {
652
+ setLoading(false);
653
+ return;
654
+ }
655
+ const ctrl = new AbortController();
656
+ let cancelled = false;
657
+ setBreakdown(null);
658
+ setLoading(true);
659
+ setDone(false);
660
+ setError(null);
661
+ const params = new URLSearchParams({
662
+ entity: entityId,
663
+ key: keyValue,
664
+ measure
665
+ });
666
+ fetcher(`${worldModelPath(projectId)}/measure-breakdown?${params}`, {
667
+ method: "GET",
668
+ signal: ctrl.signal
669
+ }).then(async (resp) => {
670
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
671
+ await readJsonSseStream(resp, (ev) => {
672
+ if (cancelled) return;
673
+ if (ev.kind === "done") {
674
+ setDone(true);
675
+ return;
676
+ }
677
+ setBreakdown((prev) => foldBreakdown(prev, ev));
678
+ });
679
+ if (!cancelled) setLoading(false);
680
+ }).catch((err) => {
681
+ if (cancelled) return;
682
+ if (err instanceof DOMException && err.name === "AbortError") return;
683
+ setError(err instanceof Error ? err : new Error(String(err)));
684
+ setLoading(false);
685
+ });
686
+ return () => {
687
+ cancelled = true;
688
+ ctrl.abort();
689
+ };
690
+ }, [
691
+ projectId,
692
+ entityId,
693
+ keyValue,
694
+ measure,
695
+ fetcher
696
+ ]);
697
+ return {
698
+ breakdown,
699
+ loading,
700
+ done,
701
+ error
702
+ };
703
+ }
704
+
705
+ //#endregion
706
+ //#region src/custom-app/world-node.tsx
707
+ /**
708
+ * Thrown by the value verbs (`explain` / `size`) when called on a handle that
709
+ * has been `drill`ed. The metric-tree backend cannot yet scope these analyses
710
+ * to a segment, so failing loud beats returning population numbers for a
711
+ * question that asked about one segment.
712
+ */
713
+ var WorldModelScopeUnsupportedError = class extends Error {
714
+ constructor(verb, scope) {
715
+ super(`${verb} on a drilled (scoped) node is not yet supported by the backend (scope: ${JSON.stringify(scope)}). Call ${verb} on the un-drilled node for population-level analysis.`);
716
+ this.code = "world_model_scope_unsupported";
717
+ this.name = "WorldModelScopeUnsupportedError";
718
+ this.scope = scope;
719
+ }
720
+ };
721
+ /**
722
+ * Build a {@link WorldModelApi} over a project id and fetcher. Framework-
723
+ * agnostic — `useWorldModel()` wraps this for React, but it is directly
724
+ * unit-testable with a mock fetcher.
725
+ */
726
+ function createWorldModel(projectId, fetcher) {
727
+ const base = () => {
728
+ if (!projectId) throw new Error("World Model unavailable: no active project (are you inside <OxyAppProvider>?)");
729
+ return metricTreePath(projectId);
730
+ };
731
+ const tree = (root, signal) => {
732
+ const qs = root ? `?root=${encodeURIComponent(root)}` : "";
733
+ return getJson(fetcher, `${base()}${qs}`, signal);
734
+ };
735
+ const makeHandle = (id, scope) => {
736
+ const scoped = Object.keys(scope).length > 0;
737
+ return {
738
+ id,
739
+ scope,
740
+ async node(signal) {
741
+ const found = (await tree(id, signal)).nodes.find((n) => n.id === id);
742
+ if (!found) throw new Error(`measure '${id}' not found in the metric tree`);
743
+ return found;
744
+ },
745
+ async expand(signal) {
746
+ const t = await tree(id, signal);
747
+ const byId = new Map(t.nodes.map((n) => [n.id, n]));
748
+ const children = [];
749
+ for (const edge of t.edges) {
750
+ if (edge.from !== id) continue;
751
+ const childNode = byId.get(edge.to);
752
+ if (!childNode) continue;
753
+ children.push({
754
+ node: childNode,
755
+ edge,
756
+ handle: makeHandle(edge.to, scope)
757
+ });
758
+ }
759
+ return children;
760
+ },
761
+ drivers(signal) {
762
+ return getJson(fetcher, `${base()}/${encodeURIComponent(id)}/sensitivity`, signal);
763
+ },
764
+ explain(opts, signal) {
765
+ if (scoped) throw new WorldModelScopeUnsupportedError("explain", scope);
766
+ return postJson(fetcher, `${base()}/explain`, {
767
+ target: id,
768
+ ...opts
769
+ }, signal);
770
+ },
771
+ size(opts, signal) {
772
+ if (scoped) throw new WorldModelScopeUnsupportedError("size", scope);
773
+ return postJson(fetcher, `${base()}/opportunity`, {
774
+ target: id,
775
+ ...opts
776
+ }, signal);
777
+ },
778
+ drill(next) {
779
+ return makeHandle(id, {
780
+ ...scope,
781
+ ...next
782
+ });
783
+ }
784
+ };
785
+ };
786
+ return {
787
+ projectId,
788
+ tree,
789
+ metric: (id) => makeHandle(id, {})
790
+ };
791
+ }
792
+ /**
793
+ * The World Model node interface, scoped to the active `<OxyAppProvider>`
794
+ * project. Returns a stable {@link WorldModelApi} — grab a node with
795
+ * `world.metric(id)` and let it speak the verbs.
796
+ *
797
+ * @example
798
+ * ```tsx
799
+ * const world = useWorldModel();
800
+ * const revenue = world.metric("orders.net_revenue");
801
+ * const children = await revenue.expand(); // components + drivers
802
+ * const rca = await revenue.explain({
803
+ * time_dimension: "orders.order_date",
804
+ * current_period: ["2026-06-01", "2026-06-30"],
805
+ * previous_period: ["2026-05-01", "2026-05-31"],
806
+ * });
807
+ * ```
808
+ *
809
+ * @remarks
810
+ * This is the node-paradigm hook. For the raw semantic-layer entity/measure
811
+ * graph, use {@link useWorldModelGraph} instead.
812
+ */
813
+ function useWorldModel() {
814
+ const { projectId, fetcher } = require_react.useOxyApp();
815
+ return react.useMemo(() => createWorldModel(projectId ?? null, fetcher), [projectId, fetcher]);
816
+ }
817
+
108
818
  //#endregion
109
819
  //#region src/metricTree.ts
110
820
  /**
@@ -240,20 +950,36 @@ exports.OxyAnswer = require_react.OxyAnswer;
240
950
  exports.OxyApiError = require_react.OxyApiError;
241
951
  exports.OxyAppProvider = require_react.OxyAppProvider;
242
952
  exports.OxyChat = require_react.OxyChat;
953
+ exports.WorldModelScopeUnsupportedError = WorldModelScopeUnsupportedError;
243
954
  exports._resetCustomAppManifestCacheForTest = require_react._resetCustomAppManifestCacheForTest;
244
955
  exports.apiErrorFromResponse = require_react.apiErrorFromResponse;
956
+ exports.base64ToBytes = base64ToBytes;
957
+ exports.bytesToBase64 = bytesToBase64;
958
+ exports.createWorldModel = createWorldModel;
245
959
  exports.getCustomAppDebug = getCustomAppDebug;
246
960
  exports.getOxyAppLogger = require_react.getOxyAppLogger;
247
961
  exports.interpretCustomAppError = require_react.interpretCustomAppError;
248
962
  exports.loadCustomAppManifest = require_react.loadCustomAppManifest;
249
963
  exports.readInjectedAppConfig = require_react.readInjectedAppConfig;
964
+ exports.readJsonSseStream = readJsonSseStream;
250
965
  exports.setOxyAppLogger = require_react.setOxyAppLogger;
251
966
  exports.useAgentRun = require_react.useAgentRun;
967
+ exports.useDistribution = useDistribution;
968
+ exports.useExplain = useExplain;
252
969
  exports.useFunction = require_react.useFunction;
970
+ exports.useMeasureBreakdown = useMeasureBreakdown;
971
+ exports.useMetricTree = useMetricTree;
972
+ exports.useOpportunity = useOpportunity;
253
973
  exports.useOxyApp = require_react.useOxyApp;
974
+ exports.usePredict = usePredict;
254
975
  exports.useProcedureRun = require_react.useProcedureRun;
255
976
  exports.useQuery = require_react.useQuery;
256
977
  exports.useResolvedManifest = require_react.useResolvedManifest;
257
978
  exports.useSemanticQuery = require_react.useSemanticQuery;
979
+ exports.useSensitivity = useSensitivity;
980
+ exports.useTimeDimensions = useTimeDimensions;
258
981
  exports.useTrackEvent = require_react.useTrackEvent;
982
+ exports.useWorldModel = useWorldModel;
983
+ exports.useWorldModelGraph = useWorldModelGraph;
984
+ exports.useWorldModelInstances = useWorldModelInstances;
259
985
  //# sourceMappingURL=index.cjs.map