@oxy-hq/sdk 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,6 +1,8 @@
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-BFFCK4VM.cjs');
4
+ let react = require("react");
5
+ react = require_react.__toESM(react, 1);
4
6
 
5
7
  //#region src/anomalies.ts
6
8
  /**
@@ -50,11 +52,20 @@ var AnomaliesClient = class {
50
52
  * workspace, runs the detector, and upserts matching rows into the
51
53
  * inbox. Returns counts of scanned / failed / persisted.
52
54
  *
55
+ * Long-running: the server waits up to 55 s, then returns
56
+ * `pending: true` with zeroed counts while the scan finishes in the
57
+ * background. Always check `pending` before treating `0` as "nothing
58
+ * found", and refetch with {@link list} shortly after.
59
+ *
53
60
  * @example
54
61
  * ```typescript
55
62
  * // Scan against a known-good reference date (matches the seed dataset)
56
63
  * const result = await client.anomalies.scan({ as_of: "2025-12-15" });
57
- * console.log(`${result.anomalies_persisted} anomalies detected`);
64
+ * if (result.pending) {
65
+ * console.log("scan still running — refetch shortly");
66
+ * } else {
67
+ * console.log(`${result.anomalies_persisted} anomalies detected`);
68
+ * }
58
69
  * ```
59
70
  */
60
71
  async scan(options = {}) {
@@ -74,14 +85,110 @@ var AnomaliesClient = class {
74
85
  }
75
86
  /**
76
87
  * Run the metric-tree `explain` for an anomaly and cache the result on
77
- * the row. Subsequent calls return the cached `ExplainResult` instantly.
88
+ * the row. Subsequent calls return the cached `ExplainResult` instantly;
89
+ * pass `{ refresh: true }` to bust the cache and recompute.
90
+ *
91
+ * The uncached path runs a 20-30 s recursive driver search — budget for it
92
+ * (or read `explain_cache` off the row from {@link list} when it's already
93
+ * populated).
78
94
  */
79
- async explain(anomalyId) {
80
- const query = this.buildQuery();
81
- return this.request(this.path(`/${encodeURIComponent(anomalyId)}/explain${query}`), { method: "POST" });
95
+ async explain(anomalyId, options = {}) {
96
+ const extra = {};
97
+ if (options.refresh) extra.refresh = "true";
98
+ return this.request(this.path(`/${encodeURIComponent(anomalyId)}/explain${this.buildQuery(extra)}`), { method: "POST" });
82
99
  }
83
100
  };
84
101
 
102
+ //#endregion
103
+ //#region src/custom-app/base64.ts
104
+ const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
105
+ /** Reverse lookup; 255 marks "not a base64 character". */
106
+ const B64R = /* @__PURE__ */ (() => {
107
+ const t = (/* @__PURE__ */ new Uint8Array(256)).fill(255);
108
+ for (let i = 0; i < 64; i++) t[B64.charCodeAt(i)] = i;
109
+ return t;
110
+ })();
111
+ /**
112
+ * Chunk size for building output in segments. Byte-at-a-time `+=` allocates a
113
+ * rope node per byte, and `String.fromCharCode.apply` blows the argument limit
114
+ * on large inputs; 8k avoids both.
115
+ */
116
+ const CHUNK = 8192;
117
+ function asBytes(input) {
118
+ if (input instanceof Uint8Array) return input;
119
+ if (input instanceof ArrayBuffer) return new Uint8Array(input);
120
+ return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
121
+ }
122
+ /**
123
+ * Encode bytes as standard (padded) base64.
124
+ *
125
+ * ```ts
126
+ * const pdf = new Uint8Array(await renderReport());
127
+ * await ctx.email.send({
128
+ * to: ctx.user.email,
129
+ * subject: "Report",
130
+ * text: "attached",
131
+ * attachments: [{ filename: "report.pdf", content: bytesToBase64(pdf) }]
132
+ * });
133
+ * ```
134
+ *
135
+ * For **text** you generated, skip this entirely and pass the string with
136
+ * `encoding: "utf8"` — it needs no encoder and stays byte-exact for non-ASCII.
137
+ */
138
+ function bytesToBase64(input) {
139
+ const bytes = asBytes(input);
140
+ const parts = [];
141
+ let buf = "";
142
+ for (let i = 0; i < bytes.length; i += 3) {
143
+ const b0 = bytes[i];
144
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
145
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
146
+ const n = b0 << 16 | b1 << 8 | b2;
147
+ buf += B64[n >> 18 & 63] + B64[n >> 12 & 63] + (i + 1 < bytes.length ? B64[n >> 6 & 63] : "=") + (i + 2 < bytes.length ? B64[n & 63] : "=");
148
+ if (buf.length >= CHUNK) {
149
+ parts.push(buf);
150
+ buf = "";
151
+ }
152
+ }
153
+ parts.push(buf);
154
+ return parts.join("");
155
+ }
156
+ /**
157
+ * Decode standard base64 to bytes — e.g. the body from
158
+ * `ctx.storage.get(key, { encoding: "base64" })`.
159
+ *
160
+ * Throws on malformed input rather than returning a short buffer: a truncated
161
+ * decode that reports success is a corrupt file nobody notices.
162
+ */
163
+ function base64ToBytes(base64) {
164
+ let s = String(base64).replace(/[ \t\n\f\r]/g, "");
165
+ if (s.length % 4 === 0) {
166
+ let pad = 0;
167
+ while (pad < 2 && s.charCodeAt(s.length - 1) === 61) {
168
+ s = s.slice(0, -1);
169
+ pad++;
170
+ }
171
+ }
172
+ if (s.indexOf("=") >= 0) throw new TypeError("base64ToBytes: '=' may only appear as trailing padding");
173
+ if (s.length % 4 === 1) throw new TypeError("base64ToBytes: invalid base64 length");
174
+ const out = new Uint8Array(s.length * 3 >> 2);
175
+ let o = 0;
176
+ let buf = 0;
177
+ let bits = 0;
178
+ for (let i = 0; i < s.length; i++) {
179
+ const code = s.charCodeAt(i);
180
+ const v = code < 256 ? B64R[code] : 255;
181
+ if (v === 255) throw new TypeError(`base64ToBytes: invalid base64 character '${s[i]}'`);
182
+ buf = buf << 6 | v;
183
+ bits += 6;
184
+ if (bits >= 8) {
185
+ bits -= 8;
186
+ out[o++] = buf >> bits & 255;
187
+ }
188
+ }
189
+ return out.subarray(0, o);
190
+ }
191
+
85
192
  //#endregion
86
193
  //#region src/custom-app/debug.ts
87
194
  /**
@@ -105,6 +212,547 @@ async function getCustomAppDebug(resolved) {
105
212
  return snapshot;
106
213
  }
107
214
 
215
+ //#endregion
216
+ //#region src/custom-app/metric-tree-fetch.ts
217
+ /** Base path for the metric-tree endpoints of `projectId`. */
218
+ function metricTreePath(projectId) {
219
+ return `/api/projects/${projectId}/semantic/metric-tree`;
220
+ }
221
+ /** GET `url`, decoding JSON or throwing a typed {@link OxyApiError}. */
222
+ async function getJson(fetcher, url, signal) {
223
+ const resp = await fetcher(url, {
224
+ method: "GET",
225
+ signal
226
+ });
227
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
228
+ return await resp.json();
229
+ }
230
+ /** POST `body` (tagged `v: 1`) to `url`, decoding JSON or throwing. */
231
+ async function postJson(fetcher, url, body, signal) {
232
+ const resp = await fetcher(url, {
233
+ method: "POST",
234
+ headers: { "content-type": "application/json" },
235
+ body: JSON.stringify({
236
+ v: 1,
237
+ ...body
238
+ }),
239
+ signal
240
+ });
241
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
242
+ return await resp.json();
243
+ }
244
+
245
+ //#endregion
246
+ //#region src/custom-app/metric-tree-hooks.tsx
247
+ /**
248
+ * Internal engine shared by every metric-tree hook. Runs `run(signal)`
249
+ * whenever `key` changes (and on `refetch`), tracks loading/error, and
250
+ * cancels in-flight work on unmount or input change.
251
+ *
252
+ * `key` is the deep-compare fingerprint of the request; a `null` key
253
+ * means "no request yet" and leaves the hook idle without firing.
254
+ */
255
+ function useMetricTreeEndpoint(key, run, enabled) {
256
+ const [data, setData] = react.useState(null);
257
+ const [loading, setLoading] = react.useState(enabled && key !== null);
258
+ const [error, setError] = react.useState(null);
259
+ const [_nonce, setNonce] = react.useState(0);
260
+ const runRef = react.useRef(run);
261
+ runRef.current = run;
262
+ react.useEffect(() => {
263
+ if (!enabled || key === null) {
264
+ setLoading(false);
265
+ return;
266
+ }
267
+ const ctrl = new AbortController();
268
+ let cancelled = false;
269
+ setLoading(true);
270
+ setError(null);
271
+ runRef.current(ctrl.signal).then((result) => {
272
+ if (cancelled) return;
273
+ setData(result);
274
+ setLoading(false);
275
+ }).catch((err) => {
276
+ if (cancelled) return;
277
+ if (err instanceof DOMException && err.name === "AbortError") return;
278
+ setError(err instanceof Error ? err : new Error(String(err)));
279
+ setLoading(false);
280
+ });
281
+ return () => {
282
+ cancelled = true;
283
+ ctrl.abort();
284
+ };
285
+ }, [key, enabled]);
286
+ return {
287
+ data,
288
+ loading,
289
+ error,
290
+ refetch: react.useCallback(() => setNonce((n) => n + 1), [])
291
+ };
292
+ }
293
+ /**
294
+ * The project's metric tree — measures (nodes) and their component /
295
+ * driver relationships (edges) — or the subtree rooted at `opts.root`.
296
+ * The structural backbone every other metric-tree analysis reads against.
297
+ */
298
+ function useMetricTree(opts = {}) {
299
+ const { projectId, fetcher } = require_react.useOxyApp();
300
+ const enabled = opts.enabled !== false;
301
+ const root = opts.root;
302
+ return useMetricTreeEndpoint(projectId ? JSON.stringify({
303
+ projectId,
304
+ root
305
+ }) : null, (signal) => {
306
+ const qs = root ? `?root=${encodeURIComponent(root)}` : "";
307
+ return getJson(fetcher, `${metricTreePath(projectId)}${qs}`, signal);
308
+ }, enabled);
309
+ }
310
+ /**
311
+ * Ranked drivers of `measureId`, by influence — the "what moves this
312
+ * measure" question. Pass `null` to stay idle until a measure is chosen.
313
+ */
314
+ function useSensitivity(measureId, opts = {}) {
315
+ const { projectId, fetcher } = require_react.useOxyApp();
316
+ const enabled = opts.enabled !== false;
317
+ return useMetricTreeEndpoint(projectId && measureId ? JSON.stringify({
318
+ projectId,
319
+ measureId
320
+ }) : null, (signal) => {
321
+ const path = `${metricTreePath(projectId)}/${encodeURIComponent(measureId)}/sensitivity`;
322
+ return getJson(fetcher, path, signal);
323
+ }, enabled);
324
+ }
325
+ /**
326
+ * Propagate hypothetical `(measure, delta)` changes upward through the
327
+ * tree and return the estimated impact on every downstream measure — a
328
+ * pure metric-tree walk, no warehouse query. Pass `null` to stay idle.
329
+ */
330
+ function usePredict(changes, opts = {}) {
331
+ const { projectId, fetcher } = require_react.useOxyApp();
332
+ const enabled = opts.enabled !== false;
333
+ return useMetricTreeEndpoint(projectId && changes ? JSON.stringify({
334
+ projectId,
335
+ changes
336
+ }) : null, (signal) => postJson(fetcher, `${metricTreePath(projectId)}/predict`, { changes }, signal), enabled);
337
+ }
338
+ /**
339
+ * Period-over-period root-cause decomposition: recursively splits the
340
+ * target measure by components and dimensions until the move concentrates.
341
+ * This is the heavy one — it can fire many warehouse queries and the
342
+ * server caps it at 45s. Pass `null` to defer until periods are chosen.
343
+ */
344
+ function useExplain(request, opts = {}) {
345
+ const { projectId, fetcher } = require_react.useOxyApp();
346
+ const enabled = opts.enabled !== false;
347
+ return useMetricTreeEndpoint(projectId && request ? JSON.stringify({
348
+ projectId,
349
+ request
350
+ }) : null, (signal) => postJson(fetcher, `${metricTreePath(projectId)}/explain`, request, signal), enabled);
351
+ }
352
+ /**
353
+ * Single-period distribution of a measure — an {@link ExplainResult}
354
+ * against an auto-derived immediately-prior baseline. Same renderers as
355
+ * `useExplain`; ignore the delta fields for a pure distribution view.
356
+ */
357
+ function useDistribution(request, opts = {}) {
358
+ const { projectId, fetcher } = require_react.useOxyApp();
359
+ const enabled = opts.enabled !== false;
360
+ return useMetricTreeEndpoint(projectId && request ? JSON.stringify({
361
+ projectId,
362
+ request
363
+ }) : null, (signal) => postJson(fetcher, `${metricTreePath(projectId)}/distribution`, request, signal), enabled);
364
+ }
365
+ /**
366
+ * Segment opportunity sizing for a measure over a period: finds
367
+ * underperforming segments and sizes the addressable upside of closing
368
+ * each rate gap against a benchmark peer. Pass `null` to stay idle until
369
+ * a target + period are chosen.
370
+ */
371
+ function useOpportunity(request, opts = {}) {
372
+ const { projectId, fetcher } = require_react.useOxyApp();
373
+ const enabled = opts.enabled !== false;
374
+ return useMetricTreeEndpoint(projectId && request ? JSON.stringify({
375
+ projectId,
376
+ request
377
+ }) : null, (signal) => postJson(fetcher, `${metricTreePath(projectId)}/opportunity`, request, signal), enabled);
378
+ }
379
+ /**
380
+ * The queryable time dimensions per view (`view.dim` ids) — what a
381
+ * bundle offers as the period axis for `explain` / `opportunity` /
382
+ * `distribution` instead of hardcoding a curated map.
383
+ */
384
+ function useTimeDimensions(opts = {}) {
385
+ const { projectId, fetcher } = require_react.useOxyApp();
386
+ const enabled = opts.enabled !== false;
387
+ return useMetricTreeEndpoint(projectId ? JSON.stringify({
388
+ projectId,
389
+ kind: "time-dimensions"
390
+ }) : null, (signal) => getJson(fetcher, `${metricTreePath(projectId)}/time-dimensions`, signal), enabled);
391
+ }
392
+
393
+ //#endregion
394
+ //#region src/custom-app/sse.ts
395
+ /**
396
+ * Read a `text/event-stream` response, invoking `onEvent` with each parsed
397
+ * JSON frame. Frames that fail to parse are skipped (a malformed frame must
398
+ * not tear down the whole stream). Resolves when the body closes.
399
+ */
400
+ async function readJsonSseStream(resp, onEvent) {
401
+ const reader = resp.body?.getReader();
402
+ if (!reader) throw new Error("SSE response has no body stream");
403
+ const decoder = new TextDecoder();
404
+ let buffer = "";
405
+ for (;;) {
406
+ const { done, value } = await reader.read();
407
+ if (done) break;
408
+ buffer += decoder.decode(value, { stream: true });
409
+ let sep;
410
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
411
+ const frame = buffer.slice(0, sep);
412
+ buffer = buffer.slice(sep + 2);
413
+ let data = "";
414
+ for (const line of frame.split("\n")) if (line.startsWith("data:")) data += line.slice(5).trim();
415
+ if (!data) continue;
416
+ let parsed;
417
+ try {
418
+ parsed = JSON.parse(data);
419
+ } catch {
420
+ continue;
421
+ }
422
+ onEvent(parsed);
423
+ }
424
+ }
425
+ }
426
+
427
+ //#endregion
428
+ //#region src/custom-app/world-model-hooks.tsx
429
+ /** Base path for the world-model endpoints of the active project. */
430
+ function worldModelPath(projectId) {
431
+ return `/api/projects/${projectId}/semantic/world-model`;
432
+ }
433
+ /**
434
+ * The world-model graph — entities (nodes), their measures/dimensions, and
435
+ * how measures promote across the entity hierarchy (edges). Applies the
436
+ * project's `.world-model.yml` display config server-side.
437
+ *
438
+ * @remarks
439
+ * This returns the raw semantic-layer entity graph. For the higher-level
440
+ * node-paradigm interface (`world.metric(id)` speaking `expand` / `explain` /
441
+ * `size`), use {@link useWorldModel} from `./world-node` instead.
442
+ */
443
+ function useWorldModelGraph(opts = {}) {
444
+ const { projectId, fetcher } = require_react.useOxyApp();
445
+ const enabled = opts.enabled !== false;
446
+ const [data, setData] = react.useState(null);
447
+ const [loading, setLoading] = react.useState(enabled && !!projectId);
448
+ const [error, setError] = react.useState(null);
449
+ const [_nonce, setNonce] = react.useState(0);
450
+ react.useEffect(() => {
451
+ if (!enabled || !projectId) {
452
+ setLoading(false);
453
+ return;
454
+ }
455
+ const ctrl = new AbortController();
456
+ let cancelled = false;
457
+ setLoading(true);
458
+ setError(null);
459
+ fetcher(worldModelPath(projectId), {
460
+ method: "GET",
461
+ signal: ctrl.signal
462
+ }).then(async (resp) => {
463
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
464
+ return await resp.json();
465
+ }).then((result) => {
466
+ if (cancelled) return;
467
+ setData(result);
468
+ setLoading(false);
469
+ }).catch((err) => {
470
+ if (cancelled) return;
471
+ if (err instanceof DOMException && err.name === "AbortError") return;
472
+ setError(err instanceof Error ? err : new Error(String(err)));
473
+ setLoading(false);
474
+ });
475
+ return () => {
476
+ cancelled = true;
477
+ ctrl.abort();
478
+ };
479
+ }, [
480
+ enabled,
481
+ projectId,
482
+ fetcher
483
+ ]);
484
+ return {
485
+ data,
486
+ loading,
487
+ error,
488
+ refetch: react.useCallback(() => setNonce((n) => n + 1), [])
489
+ };
490
+ }
491
+ /**
492
+ * List the instances (rows) of `entityId` — a bounded, searchable picker
493
+ * over the entity's primary keys + display label. Pass `null` for `entityId`
494
+ * to stay idle until an entity is chosen.
495
+ */
496
+ function useWorldModelInstances(entityId, opts = {}) {
497
+ const { projectId, fetcher } = require_react.useOxyApp();
498
+ const enabled = opts.enabled !== false;
499
+ const { search, limit } = opts;
500
+ const [data, setData] = react.useState(null);
501
+ const [loading, setLoading] = react.useState(enabled && !!projectId && !!entityId);
502
+ const [error, setError] = react.useState(null);
503
+ const [_nonce, setNonce] = react.useState(0);
504
+ react.useEffect(() => {
505
+ if (!enabled || !projectId || !entityId) {
506
+ setLoading(false);
507
+ return;
508
+ }
509
+ const ctrl = new AbortController();
510
+ let cancelled = false;
511
+ setLoading(true);
512
+ setError(null);
513
+ const params = new URLSearchParams({ entity: entityId });
514
+ if (search) params.set("search", search);
515
+ if (limit != null) params.set("limit", String(limit));
516
+ fetcher(`${worldModelPath(projectId)}/instances?${params}`, {
517
+ method: "GET",
518
+ signal: ctrl.signal
519
+ }).then(async (resp) => {
520
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
521
+ return await resp.json();
522
+ }).then((result) => {
523
+ if (cancelled) return;
524
+ setData(result);
525
+ setLoading(false);
526
+ }).catch((err) => {
527
+ if (cancelled) return;
528
+ if (err instanceof DOMException && err.name === "AbortError") return;
529
+ setError(err instanceof Error ? err : new Error(String(err)));
530
+ setLoading(false);
531
+ });
532
+ return () => {
533
+ cancelled = true;
534
+ ctrl.abort();
535
+ };
536
+ }, [
537
+ enabled,
538
+ projectId,
539
+ entityId,
540
+ search,
541
+ limit,
542
+ fetcher
543
+ ]);
544
+ return {
545
+ data,
546
+ loading,
547
+ error,
548
+ refetch: react.useCallback(() => setNonce((n) => n + 1), [])
549
+ };
550
+ }
551
+ /** Fold one measure-breakdown SSE frame into the accumulated graph. */
552
+ function foldBreakdown(prev, ev) {
553
+ switch (ev.kind) {
554
+ case "init": return {
555
+ root: ev.root,
556
+ nodes: ev.nodes.map((n) => ({
557
+ ...n,
558
+ value: null,
559
+ unvalued_reason: null
560
+ })),
561
+ edges: ev.edges
562
+ };
563
+ case "value":
564
+ if (!prev) return prev;
565
+ return {
566
+ ...prev,
567
+ nodes: prev.nodes.map((n) => n.id === ev.node_id ? {
568
+ ...n,
569
+ value: ev.value,
570
+ unvalued_reason: ev.unvalued_reason
571
+ } : n)
572
+ };
573
+ default: return prev;
574
+ }
575
+ }
576
+ /**
577
+ * Stream the driver-tree breakdown of one instance's measure — the metric
578
+ * decomposition (add/sub/mul/div component graph) with each node's value
579
+ * filling in as it resolves. This is the per-instance RCA view. Pass `null`
580
+ * for `measure` to stay idle.
581
+ */
582
+ function useMeasureBreakdown(entityId, keyValue, measure) {
583
+ const { projectId, fetcher } = require_react.useOxyApp();
584
+ const [breakdown, setBreakdown] = react.useState(null);
585
+ const [loading, setLoading] = react.useState(false);
586
+ const [done, setDone] = react.useState(false);
587
+ const [error, setError] = react.useState(null);
588
+ react.useEffect(() => {
589
+ if (!projectId || !entityId || !keyValue || !measure) {
590
+ setLoading(false);
591
+ return;
592
+ }
593
+ const ctrl = new AbortController();
594
+ let cancelled = false;
595
+ setBreakdown(null);
596
+ setLoading(true);
597
+ setDone(false);
598
+ setError(null);
599
+ const params = new URLSearchParams({
600
+ entity: entityId,
601
+ key: keyValue,
602
+ measure
603
+ });
604
+ fetcher(`${worldModelPath(projectId)}/measure-breakdown?${params}`, {
605
+ method: "GET",
606
+ signal: ctrl.signal
607
+ }).then(async (resp) => {
608
+ if (!resp.ok) throw await require_react.apiErrorFromResponse(resp);
609
+ await readJsonSseStream(resp, (ev) => {
610
+ if (cancelled) return;
611
+ if (ev.kind === "done") {
612
+ setDone(true);
613
+ return;
614
+ }
615
+ setBreakdown((prev) => foldBreakdown(prev, ev));
616
+ });
617
+ if (!cancelled) setLoading(false);
618
+ }).catch((err) => {
619
+ if (cancelled) return;
620
+ if (err instanceof DOMException && err.name === "AbortError") return;
621
+ setError(err instanceof Error ? err : new Error(String(err)));
622
+ setLoading(false);
623
+ });
624
+ return () => {
625
+ cancelled = true;
626
+ ctrl.abort();
627
+ };
628
+ }, [
629
+ projectId,
630
+ entityId,
631
+ keyValue,
632
+ measure,
633
+ fetcher
634
+ ]);
635
+ return {
636
+ breakdown,
637
+ loading,
638
+ done,
639
+ error
640
+ };
641
+ }
642
+
643
+ //#endregion
644
+ //#region src/custom-app/world-node.tsx
645
+ /**
646
+ * Thrown by the value verbs (`explain` / `size`) when called on a handle that
647
+ * has been `drill`ed. The metric-tree backend cannot yet scope these analyses
648
+ * to a segment, so failing loud beats returning population numbers for a
649
+ * question that asked about one segment.
650
+ */
651
+ var WorldModelScopeUnsupportedError = class extends Error {
652
+ constructor(verb, scope) {
653
+ 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.`);
654
+ this.code = "world_model_scope_unsupported";
655
+ this.name = "WorldModelScopeUnsupportedError";
656
+ this.scope = scope;
657
+ }
658
+ };
659
+ /**
660
+ * Build a {@link WorldModelApi} over a project id and fetcher. Framework-
661
+ * agnostic — `useWorldModel()` wraps this for React, but it is directly
662
+ * unit-testable with a mock fetcher.
663
+ */
664
+ function createWorldModel(projectId, fetcher) {
665
+ const base = () => {
666
+ if (!projectId) throw new Error("World Model unavailable: no active project (are you inside <OxyAppProvider>?)");
667
+ return metricTreePath(projectId);
668
+ };
669
+ const tree = (root, signal) => {
670
+ const qs = root ? `?root=${encodeURIComponent(root)}` : "";
671
+ return getJson(fetcher, `${base()}${qs}`, signal);
672
+ };
673
+ const makeHandle = (id, scope) => {
674
+ const scoped = Object.keys(scope).length > 0;
675
+ return {
676
+ id,
677
+ scope,
678
+ async node(signal) {
679
+ const found = (await tree(id, signal)).nodes.find((n) => n.id === id);
680
+ if (!found) throw new Error(`measure '${id}' not found in the metric tree`);
681
+ return found;
682
+ },
683
+ async expand(signal) {
684
+ const t = await tree(id, signal);
685
+ const byId = new Map(t.nodes.map((n) => [n.id, n]));
686
+ const children = [];
687
+ for (const edge of t.edges) {
688
+ if (edge.from !== id) continue;
689
+ const childNode = byId.get(edge.to);
690
+ if (!childNode) continue;
691
+ children.push({
692
+ node: childNode,
693
+ edge,
694
+ handle: makeHandle(edge.to, scope)
695
+ });
696
+ }
697
+ return children;
698
+ },
699
+ drivers(signal) {
700
+ return getJson(fetcher, `${base()}/${encodeURIComponent(id)}/sensitivity`, signal);
701
+ },
702
+ explain(opts, signal) {
703
+ if (scoped) throw new WorldModelScopeUnsupportedError("explain", scope);
704
+ return postJson(fetcher, `${base()}/explain`, {
705
+ target: id,
706
+ ...opts
707
+ }, signal);
708
+ },
709
+ size(opts, signal) {
710
+ if (scoped) throw new WorldModelScopeUnsupportedError("size", scope);
711
+ return postJson(fetcher, `${base()}/opportunity`, {
712
+ target: id,
713
+ ...opts
714
+ }, signal);
715
+ },
716
+ drill(next) {
717
+ return makeHandle(id, {
718
+ ...scope,
719
+ ...next
720
+ });
721
+ }
722
+ };
723
+ };
724
+ return {
725
+ projectId,
726
+ tree,
727
+ metric: (id) => makeHandle(id, {})
728
+ };
729
+ }
730
+ /**
731
+ * The World Model node interface, scoped to the active `<OxyAppProvider>`
732
+ * project. Returns a stable {@link WorldModelApi} — grab a node with
733
+ * `world.metric(id)` and let it speak the verbs.
734
+ *
735
+ * @example
736
+ * ```tsx
737
+ * const world = useWorldModel();
738
+ * const revenue = world.metric("orders.net_revenue");
739
+ * const children = await revenue.expand(); // components + drivers
740
+ * const rca = await revenue.explain({
741
+ * time_dimension: "orders.order_date",
742
+ * current_period: ["2026-06-01", "2026-06-30"],
743
+ * previous_period: ["2026-05-01", "2026-05-31"],
744
+ * });
745
+ * ```
746
+ *
747
+ * @remarks
748
+ * This is the node-paradigm hook. For the raw semantic-layer entity/measure
749
+ * graph, use {@link useWorldModelGraph} instead.
750
+ */
751
+ function useWorldModel() {
752
+ const { projectId, fetcher } = require_react.useOxyApp();
753
+ return react.useMemo(() => createWorldModel(projectId ?? null, fetcher), [projectId, fetcher]);
754
+ }
755
+
108
756
  //#endregion
109
757
  //#region src/metricTree.ts
110
758
  /**
@@ -240,20 +888,36 @@ exports.OxyAnswer = require_react.OxyAnswer;
240
888
  exports.OxyApiError = require_react.OxyApiError;
241
889
  exports.OxyAppProvider = require_react.OxyAppProvider;
242
890
  exports.OxyChat = require_react.OxyChat;
891
+ exports.WorldModelScopeUnsupportedError = WorldModelScopeUnsupportedError;
243
892
  exports._resetCustomAppManifestCacheForTest = require_react._resetCustomAppManifestCacheForTest;
244
893
  exports.apiErrorFromResponse = require_react.apiErrorFromResponse;
894
+ exports.base64ToBytes = base64ToBytes;
895
+ exports.bytesToBase64 = bytesToBase64;
896
+ exports.createWorldModel = createWorldModel;
245
897
  exports.getCustomAppDebug = getCustomAppDebug;
246
898
  exports.getOxyAppLogger = require_react.getOxyAppLogger;
247
899
  exports.interpretCustomAppError = require_react.interpretCustomAppError;
248
900
  exports.loadCustomAppManifest = require_react.loadCustomAppManifest;
249
901
  exports.readInjectedAppConfig = require_react.readInjectedAppConfig;
902
+ exports.readJsonSseStream = readJsonSseStream;
250
903
  exports.setOxyAppLogger = require_react.setOxyAppLogger;
251
904
  exports.useAgentRun = require_react.useAgentRun;
905
+ exports.useDistribution = useDistribution;
906
+ exports.useExplain = useExplain;
252
907
  exports.useFunction = require_react.useFunction;
908
+ exports.useMeasureBreakdown = useMeasureBreakdown;
909
+ exports.useMetricTree = useMetricTree;
910
+ exports.useOpportunity = useOpportunity;
253
911
  exports.useOxyApp = require_react.useOxyApp;
912
+ exports.usePredict = usePredict;
254
913
  exports.useProcedureRun = require_react.useProcedureRun;
255
914
  exports.useQuery = require_react.useQuery;
256
915
  exports.useResolvedManifest = require_react.useResolvedManifest;
257
916
  exports.useSemanticQuery = require_react.useSemanticQuery;
917
+ exports.useSensitivity = useSensitivity;
918
+ exports.useTimeDimensions = useTimeDimensions;
258
919
  exports.useTrackEvent = require_react.useTrackEvent;
920
+ exports.useWorldModel = useWorldModel;
921
+ exports.useWorldModelGraph = useWorldModelGraph;
922
+ exports.useWorldModelInstances = useWorldModelInstances;
259
923
  //# sourceMappingURL=index.cjs.map