@oxy-hq/sdk 2.12.0 → 2.16.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 (47) hide show
  1. package/README.md +67 -0
  2. package/dist/{function-context-D8eyZuw_.d.cts → function-context-BNpL5bFb.d.cts} +223 -20
  3. package/dist/function-context-BNpL5bFb.d.cts.map +1 -0
  4. package/dist/{function-context-D8eyZuw_.d.mts → function-context-BNpL5bFb.d.mts} +223 -20
  5. package/dist/function-context-BNpL5bFb.d.mts.map +1 -0
  6. package/dist/index.cjs +80 -16
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +164 -3
  9. package/dist/index.d.cts.map +1 -1
  10. package/dist/index.d.mts +164 -3
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +78 -16
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/ops.d.cts +1 -1
  15. package/dist/ops.d.mts +1 -1
  16. package/dist/{react-DW7Z96sD.d.mts → react-CljeXJuw.d.cts} +142 -8
  17. package/dist/react-CljeXJuw.d.cts.map +1 -0
  18. package/dist/{react-DW7Z96sD.d.cts → react-CljeXJuw.d.mts} +142 -8
  19. package/dist/react-CljeXJuw.d.mts.map +1 -0
  20. package/dist/{react-DcT-mUPj.cjs → react-Dvkv2deI.cjs} +110 -59
  21. package/dist/react-Dvkv2deI.cjs.map +1 -0
  22. package/dist/{react-BXGyzgz0.mjs → react-OW1t_J0M.mjs} +103 -26
  23. package/dist/react-OW1t_J0M.mjs.map +1 -0
  24. package/dist/rolldown-runtime-KC0qvQup.cjs +34 -0
  25. package/dist/shell.cjs +38 -3
  26. package/dist/shell.cjs.map +1 -1
  27. package/dist/shell.d.cts +26 -3
  28. package/dist/shell.d.cts.map +1 -1
  29. package/dist/shell.d.mts +26 -3
  30. package/dist/shell.d.mts.map +1 -1
  31. package/dist/shell.mjs +34 -2
  32. package/dist/shell.mjs.map +1 -1
  33. package/dist/testing.cjs +4431 -0
  34. package/dist/testing.cjs.map +1 -0
  35. package/dist/testing.d.cts +656 -0
  36. package/dist/testing.d.cts.map +1 -0
  37. package/dist/testing.d.mts +656 -0
  38. package/dist/testing.d.mts.map +1 -0
  39. package/dist/testing.mjs +4395 -0
  40. package/dist/testing.mjs.map +1 -0
  41. package/package.json +22 -9
  42. package/dist/function-context-D8eyZuw_.d.cts.map +0 -1
  43. package/dist/function-context-D8eyZuw_.d.mts.map +0 -1
  44. package/dist/react-BXGyzgz0.mjs.map +0 -1
  45. package/dist/react-DW7Z96sD.d.cts.map +0 -1
  46. package/dist/react-DW7Z96sD.d.mts.map +0 -1
  47. package/dist/react-DcT-mUPj.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/anomalies.ts","../src/custom-app/base64.ts","../src/custom-app/debug.ts","../src/custom-app/metric-tree-fetch.ts","../src/custom-app/metric-tree-hooks.tsx","../src/custom-app/sse.ts","../src/custom-app/world-model-hooks.tsx","../src/custom-app/world-node.tsx","../src/metricTree.ts"],"sourcesContent":["// Anomaly inbox types + client. Surfaces the `/semantic/anomalies*`\n// endpoints — list, scan, status, explain — so SDK consumers can render\n// the same inbox the Oxy IDE uses.\n\nimport type { OxyConfig } from \"./config\";\nimport type { ExplainResult } from \"./metricTree\";\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\nexport type AnomalyStatus = \"new\" | \"acknowledged\" | \"dismissed\";\nexport type AnomalySeverity = \"low\" | \"medium\" | \"high\";\n\n/** One filter pinning an anomaly (or a failed monitor) to a segment. */\nexport interface AnomalyFilter {\n /** Fully-qualified dimension id, e.g. `\"sales_daily.restaurant_id\"`. */\n member: string;\n /** Matched values (OR within a filter). */\n values: string[];\n}\n\n/**\n * One row in the anomaly inbox. Detected by `oxy-metric-monitoring` per\n * `.monitor.yml` entry; upserted by repeat scans so unresolved anomalies\n * stay visible without piling up duplicates.\n */\nexport interface Anomaly {\n id: string;\n workspace_id: string;\n measure: string;\n time_dimension: string;\n granularity: string;\n period_start: string;\n period_end: string;\n observed: number;\n expected: number;\n lower_bound: number;\n upper_bound: number;\n z_score: number;\n severity: AnomalySeverity | string;\n status: AnomalyStatus | string;\n label?: string | null;\n /**\n * Stable key derived from the monitor's filters (e.g.\n * `\"sales_daily.restaurant_id=loc-abc\"`). Empty for chain-wide monitors.\n */\n dimension_key: string;\n /**\n * Raw filters identifying the segment; `null` for chain-wide monitors.\n * Always present on the wire (the server serializes it unconditionally),\n * hence required-nullable rather than optional — same shape as\n * {@link ScanFailure.filters}.\n */\n filters: AnomalyFilter[] | null;\n /**\n * Groups consecutive flagged buckets of one segment into a single event, so a\n * surge spanning Mon/Wed/Thu reads as one problem rather than three. `null`\n * for rows detected before events existed. This is what\n * {@link AnomaliesClient.updateStatusBulk} wants as `eventIds` — a status\n * action applies to the whole event.\n */\n event_id?: string | null;\n /** Cached ExplainResult — populated by `POST /anomalies/:id/explain`. */\n explain_cache?: ExplainResult | null;\n explain_cached_at?: string | null;\n detected_at: string;\n updated_at: string;\n}\n\nexport interface ListAnomaliesOptions {\n status?: AnomalyStatus | string;\n /**\n * Max **events** (server caps at 500, defaults to 100). Every bucket of a\n * returned event comes back, so the row count is `limit × buckets-per-event`.\n * With `order: \"recent\"` it is a plain row limit instead.\n */\n limit?: number;\n /**\n * How many **events** to skip (rows, with `order: \"recent\"`) — same unit as\n * `limit`, so page `n` is `offset: (n - 1) * limit`. Defaults to 0.\n *\n * Bounded: past the server's maximum depth the request is refused with a 400\n * rather than served a repeat of the last reachable page, so a runaway\n * `offset += limit` loop ends loudly instead of spinning. Every response\n * echoes that depth as `max_offset`, so a loop can stop before reaching it.\n */\n offset?: number;\n /**\n * `\"recent\"` returns latest-first (`detected_at DESC`). Omit for the default\n * worst-first ranking by event severity (active events before dismissed).\n */\n order?: \"recent\";\n}\n\nexport interface ListAnomaliesResponse {\n anomalies: Anomaly[];\n /**\n * Total matching the filter across every page — **events** under the default\n * ranking, rows under `order: \"recent\"`. Same unit as `limit`/`offset`, so\n * `Math.ceil(total / limit)` is the page count. Note it will not equal\n * `anomalies.length` under the default ranking even on a single page: each\n * event returns all of its buckets.\n *\n * **Absent** in two cases, and a client that pages has to handle both. Send\n * neither `limit` nor `offset` and you have asked for \"the top N\", so there\n * is no total behind the answer — the field is omitted rather than filled\n * with the page's own length. Pass a `limit` (with `offset: 0` for the first\n * page) to get a real total to loop against.\n *\n * It is also dropped when the count query itself fails: the page rows are\n * already in hand, and the server serves them without their denominator\n * rather than failing a request it could answer. So a page you asked for\n * with `limit` can still come back untotalled — page off `anomalies.length`\n * and `max_offset` in that case rather than treating it as zero.\n */\n total?: number;\n /**\n * The page actually served. `limit` is clamped to 1..=500, so it can come\n * back smaller than you asked for and every page number you compute must\n * divide by this rather than by what you sent. `offset` is *not* clamped —\n * too deep a request is refused with a 400 (see `max_offset`), so this echoes\n * the offset you sent whenever there is a response at all.\n *\n * Optional because a replica still running a pre-paging build emits neither,\n * which is a live shape during a rolling deploy. Fall back to what you asked\n * for rather than doing arithmetic on `undefined`.\n */\n limit?: number;\n offset?: number;\n /**\n * The deepest `offset` the server will serve — past it a request is refused\n * with a 400. Read it rather than hardcoding a copy: a paging loop bounded by\n * this stops cleanly instead of ending on an error.\n */\n max_offset?: number;\n /**\n * Event keys whose buckets were trimmed to the server's per-event cap (50) —\n * an `event_id`, or `ungrouped:<row id>` for a row detected before events\n * existed. For those events `anomalies` holds the worst buckets, not all of\n * them, so a status write should name the event through `updateStatusBulk`'s\n * `eventIds` rather than enumerating the buckets you received.\n *\n * Only meaningful under the default ranking, which pages *events* and\n * returns each whole — there, an absence means complete. With\n * `order: \"recent\"` the page is row-limited, so an event can straddle its\n * boundary instead; this list stays empty and every event should be treated\n * as possibly partial.\n */\n truncated_events?: string[];\n}\n\nexport interface BulkUpdateStatusResponse {\n /** Buckets actually written. Lower than what you sent when a row was\n * deleted, moved out of `onlyStatus`, or belongs to another workspace. */\n updated: number;\n /** Distinct anomalies behind those buckets — events, plus standalone\n * pre-event rows. The unit a UI counts in, and one only the server can\n * compute: naming an event never told you how many buckets it held.\n *\n * An anomaly counts as updated once *any* of its buckets is written. Name\n * events through `eventIds` and that is the whole anomaly; name one bucket\n * of a long chain through `ids` and this still reports `1` while the rest\n * keep their old status. `ids` is for pre-event rows, which hold one bucket\n * each — using it for anything else buys a partial write. */\n events_updated: number;\n}\n\nexport interface ScanOptions {\n /** Override the reference \"now\" date (YYYY-MM-DD) — useful for demos. */\n as_of?: string;\n}\n\n/** One `.monitor.yml` entry that errored during a scan. */\nexport interface ScanFailure {\n measure: string;\n time_dimension: string;\n granularity: string;\n label: string | null;\n /** Segment key for a `group_by`/filtered monitor; empty for chain-wide. */\n dimension_key: string;\n /** Raw filters identifying the segment; null for chain-wide monitors. */\n filters: AnomalyFilter[] | null;\n error: string;\n}\n\nexport interface ScanResponse {\n monitors_scanned: number;\n monitors_failed: number;\n anomalies_persisted: number;\n /**\n * True when the scan is still running server-side (it exceeded the 55 s\n * synchronous window, or a scan started within the last 60 s and this call\n * was debounced). The counts are all `0` in that case — they are NOT a\n * \"nothing found\" result. Refetch with `list()` after a short delay.\n */\n pending: boolean;\n /**\n * Per-monitor failures. Empty array (never absent) on a clean scan and on\n * the `pending` path, where failures aren't known yet.\n */\n failures: ScanFailure[];\n}\n\nexport interface ExplainOptions {\n /** Recompute even when the row already has a cached result. */\n refresh?: boolean;\n}\n\n// ── Client ───────────────────────────────────────────────────────────────────\n\nexport type RequestFn = <T>(endpoint: string, options?: RequestInit) => Promise<T>;\n\n/** Which buckets a write may touch when the caller didn't say. Live statuses\n * for ack/dismiss; all three for a reopen, which exists to reach dismissed\n * ones. */\nfunction defaultScope(status: AnomalyStatus): AnomalyStatus[] {\n return status === \"new\" ? [\"new\", \"acknowledged\", \"dismissed\"] : [\"new\", \"acknowledged\"];\n}\n\n/**\n * Client for `/semantic/anomalies*`. Construct via `OxyClient.anomalies`\n * rather than instantiating directly — the getter wires the request helper\n * so auth, timeout, and branch propagation come along for free.\n *\n * @example\n * ```typescript\n * const { anomalies } = await client.anomalies.list({ status: \"new\" });\n * for (const a of anomalies) {\n * console.log(a.label ?? a.measure, a.severity, a.z_score.toFixed(2));\n * }\n * ```\n */\nexport class AnomaliesClient {\n private readonly request: RequestFn;\n private readonly config: OxyConfig;\n\n constructor(config: OxyConfig, request: RequestFn) {\n this.config = config;\n this.request = request;\n }\n\n private path(suffix: string): string {\n return `/${this.config.projectId}/semantic/anomalies${suffix}`;\n }\n\n private buildQuery(extra: Record<string, string> = {}): string {\n const params: Record<string, string> = { ...extra };\n if (this.config.branch) params.branch = this.config.branch;\n const qs = new URLSearchParams(params).toString();\n return qs ? `?${qs}` : \"\";\n }\n\n /**\n * List anomalies in the inbox, ranked worst-first by event severity (active\n * events before dismissed). Pass `order: \"recent\"` for latest-first.\n *\n * @example\n * ```typescript\n * // Open / unresolved anomalies only\n * const { anomalies } = await client.anomalies.list({ status: \"new\" });\n *\n * // Second page of 25 events\n * const page2 = await client.anomalies.list({ limit: 25, offset: 25 });\n * console.log(`${(page2.offset ?? 25) + 1}+ of ${page2.total ?? \"?\"}`);\n * ```\n */\n async list(options: ListAnomaliesOptions = {}): Promise<ListAnomaliesResponse> {\n const extra: Record<string, string> = {};\n if (options.status) extra.status = options.status;\n // Presence, not truthiness. The server reads \"is this caller paging?\" off\n // whether these params were sent at all, so dropping `offset: 0` on a\n // falsy check would make the first iteration of a paging loop a non-paging\n // request — one that reports `total` as just the rows it returned, ending\n // the loop after a single page.\n if (options.limit !== undefined) extra.limit = String(options.limit);\n if (options.offset !== undefined) extra.offset = String(options.offset);\n if (options.order) extra.order = options.order;\n // No trailing slash before the query — axum 307-redirects \"/anomalies/\"\n // to \"/anomalies\", and the redirect fails CORS preflight in browsers.\n return this.request<ListAnomaliesResponse>(this.path(this.buildQuery(extra)));\n }\n\n /**\n * Trigger a full scan. Iterates every `.monitor.yml` entry in the\n * workspace, runs the detector, and upserts matching rows into the\n * inbox. Returns counts of scanned / failed / persisted.\n *\n * Long-running: the server waits up to 55 s, then returns\n * `pending: true` with zeroed counts while the scan finishes in the\n * background. Always check `pending` before treating `0` as \"nothing\n * found\", and refetch with {@link list} shortly after.\n *\n * @example\n * ```typescript\n * // Scan against a known-good reference date (matches the seed dataset)\n * const result = await client.anomalies.scan({ as_of: \"2025-12-15\" });\n * if (result.pending) {\n * console.log(\"scan still running — refetch shortly\");\n * } else {\n * console.log(`${result.anomalies_persisted} anomalies detected`);\n * }\n * ```\n */\n async scan(options: ScanOptions = {}): Promise<ScanResponse> {\n const extra: Record<string, string> = {};\n if (options.as_of) extra.as_of = options.as_of;\n return this.request<ScanResponse>(this.path(`/scan${this.buildQuery(extra)}`), {\n method: \"POST\"\n });\n }\n\n /**\n * Update an anomaly's status (acknowledge / dismiss / re-open).\n */\n async updateStatus(anomalyId: string, status: AnomalyStatus): Promise<Anomaly> {\n const query = this.buildQuery();\n return this.request<Anomaly>(this.path(`/${encodeURIComponent(anomalyId)}/status${query}`), {\n method: \"POST\",\n body: JSON.stringify({ status })\n });\n }\n\n /**\n * Update many anomalies in one request — the batch form of\n * {@link updateStatus}. Identifiers outside the workspace are skipped rather\n * than erroring, so `updated` (rows written) can be lower than what you sent.\n * At most 2000 identifiers across both lists.\n *\n * **Prefer `eventIds`.** Inbox actions are per *event*, and a list response\n * caps how many buckets it returns per event — so acking the bucket ids you\n * received can leave the tail of a long chain behind, `new`, under a clean\n * success. Naming the event lets the server write all of it. `ids` is for\n * rows with no `event_id` (detected before events existed), which can only\n * be named individually.\n *\n * `onlyStatuses` says which of an event's buckets may move. An event can span\n * statuses, so an unbounded write resurrects buckets that were dismissed on\n * purpose — which is why omitting it takes a scope rather than no bound at\n * all: the live statuses (`[\"new\", \"acknowledged\"]`) for an ack or dismiss,\n * and all three for `status: \"new\"`, since reopening is how a dismissed\n * anomaly comes back. The server applies that same default, so the safe\n * behaviour does not depend on going through this client. Pass `[]` to opt\n * out of the bound entirely.\n *\n * @example\n * ```typescript\n * const { anomalies } = await client.anomalies.list({ status: \"new\", limit: 50, offset: 0 });\n * // Both lists: events by id, and pre-event rows (no `event_id`) by their own.\n * const eventIds = [...new Set(anomalies.flatMap((a) => (a.event_id ? [a.event_id] : [])))];\n * const ids = anomalies.filter((a) => !a.event_id).map((a) => a.id);\n * const { updated } = await client.anomalies.updateStatusBulk(\n * { ids, eventIds, onlyStatuses: [\"new\", \"acknowledged\"] },\n * \"acknowledged\"\n * );\n * ```\n */\n async updateStatusBulk(\n target: { ids?: string[]; eventIds?: string[]; onlyStatuses?: AnomalyStatus[] },\n status: AnomalyStatus\n ): Promise<BulkUpdateStatusResponse> {\n return this.request<BulkUpdateStatusResponse>(this.path(`/status${this.buildQuery()}`), {\n method: \"POST\",\n body: JSON.stringify({\n ids: target.ids ?? [],\n event_ids: target.eventIds ?? [],\n // Defaults to a scope, never to \"no bound\": an empty list tells the\n // server to write every bucket of the named events, dismissed ones\n // included, and that is the single state this design says must not be\n // reversed by accident.\n //\n // Reopening is the exception. `status: \"new\"` is how a dismissed\n // anomaly comes back, so excluding `dismissed` there would make the\n // one call that needs it a silent no-op.\n only_statuses: target.onlyStatuses ?? defaultScope(status),\n status\n })\n });\n }\n\n /**\n * Run the metric-tree `explain` for an anomaly and cache the result on\n * the row. Subsequent calls return the cached `ExplainResult` instantly;\n * pass `{ refresh: true }` to bust the cache and recompute.\n *\n * The uncached path runs a 20-30 s recursive driver search — budget for it\n * (or read `explain_cache` off the row from {@link list} when it's already\n * populated).\n */\n async explain(anomalyId: string, options: ExplainOptions = {}): Promise<ExplainResult> {\n const extra: Record<string, string> = {};\n if (options.refresh) extra.refresh = \"true\";\n return this.request<ExplainResult>(\n this.path(`/${encodeURIComponent(anomalyId)}/explain${this.buildQuery(extra)}`),\n { method: \"POST\" }\n );\n }\n}\n","// Base64 for binary that has to cross a boundary as text — an email\n// attachment's `content`, a `ctx.storage.put` body.\n//\n// These are plain functions, deliberately NOT `btoa`/`atob`:\n//\n// - `btoa` takes a Latin1 STRING. Handing it a `Uint8Array` is the classic\n// footgun: the spec stringifies it, so a PDF starting `%PDF` silently\n// encodes the text \"37,80,68,70\" and you ship a corrupt file. A named\n// function that takes bytes cannot be misused that way.\n// - Being ordinary bundled JS, they behave identically in the Oxy Functions\n// isolate, in Node/vitest, and in a browser. Anything reached through a\n// global risks being a different implementation in each.\n\nconst B64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/** Reverse lookup; 255 marks \"not a base64 character\". */\nconst B64R = /* @__PURE__ */ (() => {\n const t = new Uint8Array(256).fill(255);\n for (let i = 0; i < 64; i++) t[B64.charCodeAt(i)] = i;\n return t;\n})();\n\n/**\n * Chunk size for building output in segments. Byte-at-a-time `+=` allocates a\n * rope node per byte, and `String.fromCharCode.apply` blows the argument limit\n * on large inputs; 8k avoids both.\n */\nconst CHUNK = 8192;\n\nfunction asBytes(input: Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array {\n if (input instanceof Uint8Array) return input;\n if (input instanceof ArrayBuffer) return new Uint8Array(input);\n return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);\n}\n\n/**\n * Encode bytes as standard (padded) base64.\n *\n * ```ts\n * const pdf = new Uint8Array(await renderReport());\n * await ctx.email.send({\n * to: ctx.user.email,\n * subject: \"Report\",\n * text: \"attached\",\n * attachments: [{ filename: \"report.pdf\", content: bytesToBase64(pdf) }]\n * });\n * ```\n *\n * For **text** you generated, skip this entirely and pass the string with\n * `encoding: \"utf8\"` — it needs no encoder and stays byte-exact for non-ASCII.\n */\nexport function bytesToBase64(input: Uint8Array | ArrayBuffer | ArrayBufferView): string {\n const bytes = asBytes(input);\n const parts: string[] = [];\n let buf = \"\";\n for (let i = 0; i < bytes.length; i += 3) {\n const b0 = bytes[i];\n const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;\n const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;\n const n = (b0 << 16) | (b1 << 8) | b2;\n buf +=\n B64[(n >> 18) & 63] +\n B64[(n >> 12) & 63] +\n (i + 1 < bytes.length ? B64[(n >> 6) & 63] : \"=\") +\n (i + 2 < bytes.length ? B64[n & 63] : \"=\");\n if (buf.length >= CHUNK) {\n parts.push(buf);\n buf = \"\";\n }\n }\n parts.push(buf);\n return parts.join(\"\");\n}\n\n/**\n * Decode standard base64 to bytes — e.g. the body from\n * `ctx.storage.get(key, { encoding: \"base64\" })`.\n *\n * Throws on malformed input rather than returning a short buffer: a truncated\n * decode that reports success is a corrupt file nobody notices.\n */\nexport function base64ToBytes(base64: string): Uint8Array {\n let s = String(base64).replace(/[ \\t\\n\\f\\r]/g, \"\");\n // Strip padding first, and only at a multiple of 4 — matching WHATWG. A\n // decoder that stopped at the first \"=\" would silently truncate\n // `base64ToBytes(chunkA + chunkB)` when chunkA carries its own padding.\n if (s.length % 4 === 0) {\n let pad = 0;\n while (pad < 2 && s.charCodeAt(s.length - 1) === 61 /* = */) {\n s = s.slice(0, -1);\n pad++;\n }\n }\n if (s.indexOf(\"=\") >= 0) {\n throw new TypeError(\"base64ToBytes: '=' may only appear as trailing padding\");\n }\n if (s.length % 4 === 1) {\n throw new TypeError(\"base64ToBytes: invalid base64 length\");\n }\n const out = new Uint8Array((s.length * 3) >> 2);\n let o = 0;\n let buf = 0;\n let bits = 0;\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n const v = code < 256 ? B64R[code] : 255;\n if (v === 255) {\n throw new TypeError(`base64ToBytes: invalid base64 character '${s[i]}'`);\n }\n buf = (buf << 6) | v;\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n out[o++] = (buf >> bits) & 0xff;\n }\n }\n return out.subarray(0, o);\n}\n","// Bundle-side accessor for the server's diagnostic snapshot.\n//\n// `GET /api/customer-apps/<org>/<app>/debug` returns a structured\n// snapshot of what oxy currently sees about a registered customer\n// app: the app row, bundle dir resolution, and parsed manifest (or\n// parse error). Useful when a bundle isn't loading what you expected\n// and you want to verify what the server actually sees — without\n// needing terminal access.\n//\n// The `products` field on the snapshot is a legacy artifact carried\n// for server-side compatibility; in v2 the bundle owns its queries\n// via `useQuery` and the field is always empty for v2 manifests.\n\nimport { getOxyAppLogger } from \"./logger\";\nimport type { ResolvedCustomAppManifest } from \"./manifest\";\n\n/** Untyped at the boundary — keep it loose so server-side schema\n * additions don't break older bundles. Stable enough for inspection\n * but not a contract clients should depend on field-by-field. */\nexport interface CustomAppDebugSnapshot {\n org_slug: string;\n app_slug: string;\n app: {\n id: string;\n slug: string;\n name: string;\n status: string;\n source_type: string;\n project_id: string;\n branch: string;\n };\n bundle_dir: string | null;\n bundle_dir_exists: boolean;\n /** Raw parsed manifest from the server — kept loose so schema additions don't break older bundles. */\n manifest: Record<string, unknown> | null;\n manifest_error: string | null;\n products: Array<{ name: string; producer: string }>;\n}\n\n/**\n * Fetch the server-side diagnostic snapshot for this bundle. Pair with\n * `loadCustomAppManifest()` — pass its result here. Logs the\n * snapshot through the SDK logger so it appears in the bundle's\n * console at info level.\n */\nexport async function getCustomAppDebug(\n resolved: ResolvedCustomAppManifest\n): Promise<CustomAppDebugSnapshot> {\n const log = getOxyAppLogger();\n const { apiBaseUrl, orgSlug, appSlug } = resolved;\n const url =\n `${apiBaseUrl}/api/customer-apps/` +\n `${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/debug`;\n\n log.log(\"debug\", \"fetching debug snapshot\", { url });\n const res = await fetch(url, { credentials: \"same-origin\" });\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\");\n throw new Error(\n `Failed to fetch debug snapshot (HTTP ${res.status}): ${detail || res.statusText}`\n );\n }\n const snapshot = (await res.json()) as CustomAppDebugSnapshot;\n log.log(\"info\", \"debug snapshot\", snapshot as unknown as Record<string, unknown>);\n return snapshot;\n}\n","// Shared fetch helpers for the `/api/projects/{id}/semantic/metric-tree*`\n// endpoints. Both the metric-tree analysis hooks (`metric-tree-hooks.tsx`)\n// and the higher-level World Model node interface (`world-node.tsx`) enter\n// the semantic model through these, so the request envelope (`{ v: 1, … }`)\n// and error decoding stay identical across the two surfaces.\n\nimport { apiErrorFromResponse } from \"./errors\";\nimport type { AppFetcher } from \"./react\";\n\n/** Base path for the metric-tree endpoints of `projectId`. */\nexport function metricTreePath(projectId: string): string {\n return `/api/projects/${projectId}/semantic/metric-tree`;\n}\n\n/** GET `url`, decoding JSON or throwing a typed {@link OxyApiError}. */\nexport async function getJson<Data>(\n fetcher: AppFetcher,\n url: string,\n signal?: AbortSignal\n): Promise<Data> {\n const resp = await fetcher(url, { method: \"GET\", signal });\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n return (await resp.json()) as Data;\n}\n\n/** POST `body` (tagged `v: 1`) to `url`, decoding JSON or throwing. */\nexport async function postJson<Data>(\n fetcher: AppFetcher,\n url: string,\n body: unknown,\n signal?: AbortSignal\n): Promise<Data> {\n const resp = await fetcher(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: 1, ...(body as object) }),\n signal\n });\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n return (await resp.json()) as Data;\n}\n","// React hooks for the metric-tree analysis ops, exposed to custom-app\n// bundles so an app can run drivers / what-if / RCA / opportunity sizing\n// without hand-rolling fetch against the semantic model.\n//\n// These wrap the `/api/projects/{id}/semantic/metric-tree*` endpoints —\n// the same airlayer analyses the IDE's World Model and Metric Tree\n// surfaces drive — behind the shared `OxyAppProvider` fetcher (session\n// cookie in-workspace, dev-proxy token cross-origin). Response shapes\n// reuse the wire types in `../metricTree`, so a bundle typed against a\n// hook result matches what the server serializes verbatim.\n//\n// Pattern mirrors `useSemanticQuery`: each hook fetches when enabled and\n// its input is present, re-runs on input change (deep-compared via JSON),\n// and exposes `refetch`. Read-only inputs (`null`) keep a hook idle — the\n// natural fit for \"run once the user picks a target measure\".\n\nimport * as React from \"react\";\nimport type {\n BaselineRequest,\n BaselineResponse,\n DistributionRequest,\n ExplainRequest,\n ExplainResult,\n MetricTree,\n OpportunityRequest,\n OpportunityResult,\n PredictChange,\n PredictOptions,\n PredictResult,\n ProjectionRequest,\n ProjectionResponse,\n SensitivityResult,\n TimeDimensionsResponse\n} from \"../metricTree\";\nimport { getJson, metricTreePath, postJson } from \"./metric-tree-fetch\";\nimport { useOxyApp } from \"./react\";\n\n/** Shared result envelope for every metric-tree hook. */\nexport interface MetricTreeHookResult<Data> {\n data: Data | null;\n loading: boolean;\n error: Error | null;\n /** Force a re-run, bypassing nothing — the server honors `?refresh`. */\n refetch: () => void;\n}\n\ninterface EndpointOpts {\n /** Set false to skip the request (e.g. waiting on a user selection). */\n enabled?: boolean;\n}\n\n/**\n * Internal engine shared by every metric-tree hook. Runs `run(signal)`\n * whenever `key` changes (and on `refetch`), tracks loading/error, and\n * cancels in-flight work on unmount or input change.\n *\n * `key` is the deep-compare fingerprint of the request; a `null` key\n * means \"no request yet\" and leaves the hook idle without firing.\n */\nfunction useMetricTreeEndpoint<Data>(\n key: string | null,\n run: (signal: AbortSignal) => Promise<Data>,\n enabled: boolean\n): MetricTreeHookResult<Data> {\n const [data, setData] = React.useState<Data | null>(null);\n const [loading, setLoading] = React.useState<boolean>(enabled && key !== null);\n const [error, setError] = React.useState<Error | null>(null);\n const [_nonce, setNonce] = React.useState(0);\n\n // `run` is re-created each render; pin the latest in a ref so the effect\n // depends only on `key`/`enabled`/`nonce` and doesn't re-fire on every\n // parent render.\n const runRef = React.useRef(run);\n runRef.current = run;\n\n React.useEffect(() => {\n if (!enabled || key === null) {\n setLoading(false);\n return;\n }\n const ctrl = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n\n runRef\n .current(ctrl.signal)\n .then((result) => {\n if (cancelled) return;\n setData(result);\n setLoading(false);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n });\n\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n }, [key, enabled]);\n\n const refetch = React.useCallback(() => setNonce((n) => n + 1), []);\n return { data, loading, error, refetch };\n}\n\n// ── useMetricTree ─────────────────────────────────────────────────────────────\n\nexport interface UseMetricTreeOpts extends EndpointOpts {\n /** Optional measure id to root the returned subtree at. */\n root?: string;\n}\n\n/**\n * The project's metric tree — measures (nodes) and their component /\n * driver relationships (edges) — or the subtree rooted at `opts.root`.\n * The structural backbone every other metric-tree analysis reads against.\n */\nexport function useMetricTree(opts: UseMetricTreeOpts = {}): MetricTreeHookResult<MetricTree> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const root = opts.root;\n const key = projectId ? JSON.stringify({ projectId, root }) : null;\n\n return useMetricTreeEndpoint<MetricTree>(\n key,\n (signal) => {\n const qs = root ? `?root=${encodeURIComponent(root)}` : \"\";\n return getJson<MetricTree>(fetcher, `${metricTreePath(projectId as string)}${qs}`, signal);\n },\n enabled\n );\n}\n\n// ── useSensitivity (drivers) ──────────────────────────────────────────────────\n\n/**\n * Ranked drivers of `measureId`, by influence — the \"what moves this\n * measure\" question. Pass `null` to stay idle until a measure is chosen.\n */\nexport function useSensitivity(\n measureId: string | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<SensitivityResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && measureId ? JSON.stringify({ projectId, measureId }) : null;\n\n return useMetricTreeEndpoint<SensitivityResult>(\n key,\n (signal) => {\n const path = `${metricTreePath(projectId as string)}/${encodeURIComponent(\n measureId as string\n )}/sensitivity`;\n return getJson<SensitivityResult>(fetcher, path, signal);\n },\n enabled\n );\n}\n\n// ── usePredict (what-if) ──────────────────────────────────────────────────────\n\nexport interface UsePredictOpts extends EndpointOpts, PredictOptions {}\n\n/**\n * Propagate hypothetical `(measure, delta)` changes upward through the\n * tree and return the estimated impact on every downstream measure — a\n * pure metric-tree walk, no warehouse query. Pass `null` to stay idle.\n *\n * Because it is database-free it can only use the coefficients it is GIVEN.\n * Without `opts.coefficients` from {@link useBaseline}, every driver edge\n * whose `.view.yml` declares no `coefficient:` contributes nothing and its\n * downstream measures are simply absent from `impacts` — no error, no\n * refusal. Without `opts.values`, multiplicative component edges come back\n * `unquantifiable` rather than sized.\n */\nexport function usePredict(\n changes: PredictChange[] | null,\n opts: UsePredictOpts = {}\n): MetricTreeHookResult<PredictResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const { values, coefficients } = opts;\n const body = {\n changes,\n ...(values ? { values } : {}),\n // Sent verbatim, refusals included — the server ignores entries carrying\n // no coefficient, and filtering them here would just be a second place for\n // the two sides to disagree.\n ...(coefficients?.length ? { coefficients } : {})\n };\n const key = projectId && changes ? JSON.stringify({ projectId, body }) : null;\n\n return useMetricTreeEndpoint<PredictResult>(\n key,\n (signal) =>\n postJson<PredictResult>(\n fetcher,\n `${metricTreePath(projectId as string)}/predict`,\n body,\n signal\n ),\n enabled\n );\n}\n\n// ── useBaseline (scenario levels + fitted coefficients) ───────────────────────\n\n/**\n * Value a scenario's starting point, and measure the coefficients it needs.\n *\n * Two warehouse reads: the current value of every node reachable from\n * `request.roots`, and — for driver edges declaring no `coefficient:` — a fit\n * over the window. Both are expensive, which is why they live here and not in\n * {@link usePredict}: predict is database-free by design so it can re-run per\n * keystroke, and it CANNOT measure a coefficient itself.\n *\n * That is the whole reason to call this. Feed `data.values` and `data.fitted`\n * into `usePredict`; omit them and an undeclared edge propagates nothing.\n * Pass `null` to stay idle until levers and a period are chosen.\n */\nexport function useBaseline(\n request: BaselineRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<BaselineResponse> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<BaselineResponse>(\n key,\n (signal) =>\n postJson<BaselineResponse>(\n fetcher,\n `${metricTreePath(projectId as string)}/baseline`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useProjection (scenario forecasting over time) ────────────────────────────\n\n/**\n * Bucketed history for the levers and everything downstream, plus the forward\n * curve the forecaster expects next — the scenario's time axis.\n *\n * One warehouse query, so it belongs on a window change, not on a lever edit.\n * It returns the BASELINE curve only: the scenario's second curve is\n * arithmetic over this and a `usePredict` result — a proportional shift\n * landing `lag` buckets in — composed client-side precisely so editing a lever\n * costs no query.\n *\n * Treat a series with a `refusal` as a stated absence: it must not render as a\n * flat forward line. Pass `null` to stay idle.\n */\nexport function useProjection(\n request: ProjectionRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<ProjectionResponse> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<ProjectionResponse>(\n key,\n (signal) =>\n postJson<ProjectionResponse>(\n fetcher,\n `${metricTreePath(projectId as string)}/projection`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useExplain (RCA) ──────────────────────────────────────────────────────────\n\n/**\n * Period-over-period root-cause decomposition: recursively splits the\n * target measure by components and dimensions until the move concentrates.\n * This is the heavy one — it can fire many warehouse queries and the\n * server caps it at 45s. Pass `null` to defer until periods are chosen.\n */\nexport function useExplain(\n request: ExplainRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<ExplainResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<ExplainResult>(\n key,\n (signal) =>\n postJson<ExplainResult>(\n fetcher,\n `${metricTreePath(projectId as string)}/explain`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useDistribution ───────────────────────────────────────────────────────────\n\n/**\n * Single-period distribution of a measure — an {@link ExplainResult}\n * against an auto-derived immediately-prior baseline. Same renderers as\n * `useExplain`; ignore the delta fields for a pure distribution view.\n */\nexport function useDistribution(\n request: DistributionRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<ExplainResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<ExplainResult>(\n key,\n (signal) =>\n postJson<ExplainResult>(\n fetcher,\n `${metricTreePath(projectId as string)}/distribution`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useOpportunity (sizing) ───────────────────────────────────────────────────\n\n/**\n * Segment opportunity sizing for a measure over a period: finds\n * underperforming segments and sizes the addressable upside of closing\n * each rate gap against a benchmark peer. Pass `null` to stay idle until\n * a target + period are chosen.\n */\nexport function useOpportunity(\n request: OpportunityRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<OpportunityResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<OpportunityResult>(\n key,\n (signal) =>\n postJson<OpportunityResult>(\n fetcher,\n `${metricTreePath(projectId as string)}/opportunity`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useTimeDimensions ─────────────────────────────────────────────────────────\n\n/**\n * The queryable time dimensions per view (`view.dim` ids) — what a\n * bundle offers as the period axis for `explain` / `opportunity` /\n * `distribution` instead of hardcoding a curated map.\n */\nexport function useTimeDimensions(\n opts: EndpointOpts = {}\n): MetricTreeHookResult<TimeDimensionsResponse> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId ? JSON.stringify({ projectId, kind: \"time-dimensions\" }) : null;\n\n return useMetricTreeEndpoint<TimeDimensionsResponse>(\n key,\n (signal) =>\n getJson<TimeDimensionsResponse>(\n fetcher,\n `${metricTreePath(projectId as string)}/time-dimensions`,\n signal\n ),\n enabled\n );\n}\n","// Minimal `text/event-stream` reader for the world-model streams.\n//\n// Unlike `function-sse.ts` (which reads a single terminal function result),\n// the world-model `instance-detail` / `measure-breakdown` endpoints emit a\n// sequence of `kind`-tagged JSON events on the default (unnamed) SSE event,\n// terminating with a `{ kind: \"done\" }` frame and then closing the stream.\n// This reader parses each `data:` frame's JSON and hands it to `onEvent`; the\n// caller folds the events into accumulated state. It resolves when the stream\n// closes (or the signal aborts) — the hook decides what \"done\" means.\n\n/**\n * Read a `text/event-stream` response, invoking `onEvent` with each parsed\n * JSON frame. Frames that fail to parse are skipped (a malformed frame must\n * not tear down the whole stream). Resolves when the body closes.\n */\nexport async function readJsonSseStream<E>(\n resp: Response,\n onEvent: (event: E) => void\n): Promise<void> {\n const reader = resp.body?.getReader();\n if (!reader) {\n throw new Error(\"SSE response has no body stream\");\n }\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n let sep: number;\n while ((sep = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const frame = buffer.slice(0, sep);\n buffer = buffer.slice(sep + 2);\n let data = \"\";\n for (const line of frame.split(\"\\n\")) {\n // Ignore `event:`/`id:`/`retry:` lines — the world-model streams put\n // everything in `data:` on the default event.\n if (line.startsWith(\"data:\")) data += line.slice(5).trim();\n }\n if (!data) continue;\n let parsed: E;\n try {\n parsed = JSON.parse(data) as E;\n } catch {\n continue;\n }\n onEvent(parsed);\n }\n }\n}\n","// React hooks for the world-model surface, exposed to custom-app bundles\n// so an app can render the semantic-model graph, browse an entity's\n// instances, and drill into an instance's detail / measure driver-tree.\n//\n// Wraps the `/api/projects/{id}/semantic/world-model*` endpoints behind the\n// shared `OxyAppProvider` fetcher. The two drill-down endpoints\n// (`instance-detail`, `measure-breakdown`) stream `kind`-tagged SSE events;\n// their hooks fold the stream into accumulated state so a bundle can render\n// progressively (skeletons fill in as measure values resolve).\n\nimport * as React from \"react\";\nimport type {\n WmInstancesResponse,\n WmMeasureBreakdown,\n WmMeasureBreakdownEvent,\n WorldModel\n} from \"../worldModel\";\nimport { apiErrorFromResponse } from \"./errors\";\nimport { useOxyApp } from \"./react\";\nimport { readJsonSseStream } from \"./sse\";\n\n/** Base path for the world-model endpoints of the active project. */\nfunction worldModelPath(projectId: string): string {\n return `/api/projects/${projectId}/semantic/world-model`;\n}\n\n// ── useWorldModelGraph (graph) ────────────────────────────────────────────────\n\nexport interface UseWorldModelGraphResult {\n data: WorldModel | null;\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\n/**\n * The world-model graph — entities (nodes), their measures/dimensions, and\n * how measures promote across the entity hierarchy (edges). Applies the\n * project's `.world-model.yml` display config server-side.\n *\n * @remarks\n * This returns the raw semantic-model entity graph. For the higher-level\n * node-paradigm interface (`world.metric(id)` speaking `expand` / `explain` /\n * `size`), use {@link useWorldModel} from `./world-node` instead.\n */\nexport function useWorldModelGraph(opts: { enabled?: boolean } = {}): UseWorldModelGraphResult {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const [data, setData] = React.useState<WorldModel | null>(null);\n const [loading, setLoading] = React.useState<boolean>(enabled && !!projectId);\n const [error, setError] = React.useState<Error | null>(null);\n const [_nonce, setNonce] = React.useState(0);\n\n React.useEffect(() => {\n if (!enabled || !projectId) {\n setLoading(false);\n return;\n }\n const ctrl = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n fetcher(worldModelPath(projectId), { method: \"GET\", signal: ctrl.signal })\n .then(async (resp) => {\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n return (await resp.json()) as WorldModel;\n })\n .then((result) => {\n if (cancelled) return;\n setData(result);\n setLoading(false);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n });\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n }, [enabled, projectId, fetcher]);\n\n const refetch = React.useCallback(() => setNonce((n) => n + 1), []);\n return { data, loading, error, refetch };\n}\n\n// ── useWorldModelInstances ────────────────────────────────────────────────────\n\nexport interface UseWorldModelInstancesOpts {\n /** Substring/prefix search over the entity's display field. */\n search?: string;\n /** Max rows to return (default 50 server-side). */\n limit?: number;\n enabled?: boolean;\n /**\n * `\"reach\"` — only the instances whose place the viewer reaches, filtered\n * inside the scan so a page is a page of the right set. Needs the entity\n * bound to the org's locations registry (refused otherwise); an instance\n * whose key is unmapped is not in anyone's reach. The bundle's own app is\n * sent along so app-admin standing counts.\n */\n scope?: \"reach\";\n}\n\nexport interface UseWorldModelInstancesResult {\n data: WmInstancesResponse | null;\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\n/**\n * List the instances (rows) of `entityId` — a bounded, searchable picker\n * over the entity's primary keys + display label. Pass `null` for `entityId`\n * to stay idle until an entity is chosen.\n */\nexport function useWorldModelInstances(\n entityId: string | null,\n opts: UseWorldModelInstancesOpts = {}\n): UseWorldModelInstancesResult {\n const { projectId, appId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const { search, limit, scope } = opts;\n const [data, setData] = React.useState<WmInstancesResponse | null>(null);\n const [loading, setLoading] = React.useState<boolean>(enabled && !!projectId && !!entityId);\n const [error, setError] = React.useState<Error | null>(null);\n const [_nonce, setNonce] = React.useState(0);\n\n React.useEffect(() => {\n if (!enabled || !projectId || !entityId) {\n setLoading(false);\n return;\n }\n const ctrl = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n const params = new URLSearchParams({ entity: entityId });\n if (search) params.set(\"search\", search);\n if (limit != null) params.set(\"limit\", String(limit));\n if (scope) {\n params.set(\"scope\", scope);\n if (appId) params.set(\"app\", appId);\n }\n fetcher(`${worldModelPath(projectId)}/instances?${params}`, {\n method: \"GET\",\n signal: ctrl.signal\n })\n .then(async (resp) => {\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n return (await resp.json()) as WmInstancesResponse;\n })\n .then((result) => {\n if (cancelled) return;\n setData(result);\n setLoading(false);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n });\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n }, [enabled, projectId, entityId, search, limit, fetcher, scope, appId]);\n\n const refetch = React.useCallback(() => setNonce((n) => n + 1), []);\n return { data, loading, error, refetch };\n}\n\n// ── useMeasureBreakdown (driver tree, SSE) ────────────────────────────────────\n\nexport interface UseMeasureBreakdownResult {\n /** Accumulated breakdown graph; null until the `init` frame. */\n breakdown: WmMeasureBreakdown | null;\n loading: boolean;\n done: boolean;\n error: Error | null;\n}\n\n/** Fold one measure-breakdown SSE frame into the accumulated graph. */\nfunction foldBreakdown(\n prev: WmMeasureBreakdown | null,\n ev: WmMeasureBreakdownEvent\n): WmMeasureBreakdown | null {\n switch (ev.kind) {\n case \"init\":\n return {\n root: ev.root,\n nodes: ev.nodes.map((n) => ({ ...n, value: null, unvalued_reason: null })),\n edges: ev.edges\n };\n case \"value\": {\n if (!prev) return prev;\n return {\n ...prev,\n nodes: prev.nodes.map((n) =>\n n.id === ev.node_id ? { ...n, value: ev.value, unvalued_reason: ev.unvalued_reason } : n\n )\n };\n }\n default:\n return prev;\n }\n}\n\n/**\n * Stream the driver-tree breakdown of one instance's measure — the metric\n * decomposition (add/sub/mul/div component graph) with each node's value\n * filling in as it resolves. This is the per-instance RCA view. Pass `null`\n * for `measure` to stay idle.\n */\nexport function useMeasureBreakdown(\n entityId: string | null,\n keyValue: string | null,\n measure: string | null\n): UseMeasureBreakdownResult {\n const { projectId, fetcher } = useOxyApp();\n const [breakdown, setBreakdown] = React.useState<WmMeasureBreakdown | null>(null);\n const [loading, setLoading] = React.useState<boolean>(false);\n const [done, setDone] = React.useState<boolean>(false);\n const [error, setError] = React.useState<Error | null>(null);\n\n React.useEffect(() => {\n if (!projectId || !entityId || !keyValue || !measure) {\n setLoading(false);\n return;\n }\n const ctrl = new AbortController();\n let cancelled = false;\n setBreakdown(null);\n setLoading(true);\n setDone(false);\n setError(null);\n\n const params = new URLSearchParams({ entity: entityId, key: keyValue, measure });\n fetcher(`${worldModelPath(projectId)}/measure-breakdown?${params}`, {\n method: \"GET\",\n signal: ctrl.signal\n })\n .then(async (resp) => {\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n await readJsonSseStream<WmMeasureBreakdownEvent>(resp, (ev) => {\n if (cancelled) return;\n if (ev.kind === \"done\") {\n setDone(true);\n return;\n }\n setBreakdown((prev) => foldBreakdown(prev, ev));\n });\n if (!cancelled) setLoading(false);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n });\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n }, [projectId, entityId, keyValue, measure, fetcher]);\n\n return { breakdown, loading, done, error };\n}\n","// The World Model **node interface** — the higher-level \"node paradigm\" from\n// `docs/build/sdk/world-model.mdx`. Everything in the World Model is a node,\n// and every node speaks the same verbs: `expand` (one hop of relationships),\n// `drill` (narrow to a segment), `explain` (period-over-period root cause),\n// and `size` (peer-gap opportunity). Render a node, let the user pick a verb,\n// get more nodes back, recurse.\n//\n// This is a thin composition layer over the metric-tree analyses that already\n// ship (`metric-tree-hooks.tsx` / the `MetricTreeClient`): the verbs map onto\n// the same `/semantic/metric-tree*` endpoints, so a bundle typed against a\n// handle matches what the server serializes verbatim. It adds no new backend.\n//\n// The logic lives in a framework-agnostic `createWorldModel(projectId,\n// fetcher)` factory; `useWorldModel()` is a thin `useMemo` over it, scoped to\n// the active `<OxyAppProvider>` project.\n//\n// **Alpha.** Per the doc, the node paradigm is a design preview and may\n// change. `drill` is the one verb the backend cannot yet honor — the\n// metric-tree endpoints take no segment/instance filter (the opportunity\n// endpoint explicitly refuses it), and structural verbs are scope-invariant.\n// So `drill` returns a scoped handle for interface fidelity, but the value\n// verbs (`explain`/`size`) on a drilled handle throw\n// {@link WorldModelScopeUnsupportedError} rather than silently returning\n// population numbers for a scoped question.\n\nimport * as React from \"react\";\nimport type {\n ExplainRequest,\n ExplainResult,\n MetricEdge,\n MetricNode,\n MetricTree,\n OpportunityRequest,\n OpportunityResult,\n SensitivityResult\n} from \"../metricTree\";\nimport { getJson, metricTreePath, postJson } from \"./metric-tree-fetch\";\nimport type { AppFetcher } from \"./react\";\nimport { useOxyApp } from \"./react\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\n/** A `dimension → value` scope narrowed onto a node via {@link MetricHandle.drill}. */\nexport type MetricScope = Readonly<Record<string, string>>;\n\n/** Options for {@link MetricHandle.explain} — an {@link ExplainRequest} minus\n * the `target`, which the handle supplies from its own id. */\nexport type ExplainOpts = Omit<ExplainRequest, \"target\">;\n\n/** Options for {@link MetricHandle.size} — an {@link OpportunityRequest} minus\n * the `target`. */\nexport type SizeOpts = Omit<OpportunityRequest, \"target\">;\n\n/** One child revealed by {@link MetricHandle.expand}: the child measure's\n * node, the edge that connects it to the parent, and a handle to recurse. */\nexport interface ExpandedNode {\n /** The child measure (a component or a driver of the parent). */\n node: MetricNode;\n /** The parent → child edge — `kind`, `direction`, `strength`, `form`, … */\n edge: MetricEdge;\n /** A live handle on the child, carrying the parent's scope. */\n handle: MetricHandle;\n}\n\n/**\n * A live handle on one metric node. Carry it around and call a verb; every\n * verb returns either more nodes (`expand`), a scoped handle (`drill`), or an\n * analysis result (`explain` / `size` / `drivers`).\n */\nexport interface MetricHandle {\n /** Fully-qualified measure id (`view.measure`). */\n readonly id: string;\n /** The scope narrowed onto this handle by `drill` (empty for a root handle). */\n readonly scope: MetricScope;\n /** The measure's own tree node (label, expr, is_composite). */\n node(signal?: AbortSignal): Promise<MetricNode>;\n /** One hop of relationships — the metric's components and drivers as child nodes. */\n expand(signal?: AbortSignal): Promise<ExpandedNode[]>;\n /** The declared drivers of this measure, ranked by influence (sensitivity). */\n drivers(signal?: AbortSignal): Promise<SensitivityResult>;\n /** Root-cause a period-over-period move: why it dropped or climbed. */\n explain(opts: ExplainOpts, signal?: AbortSignal): Promise<ExplainResult>;\n /** Compare this node to its peers across each dimension and size the gap. */\n size(opts: SizeOpts, signal?: AbortSignal): Promise<OpportunityResult>;\n /** Narrow into a segment or entity instance — returns a scoped handle. */\n drill(scope: Record<string, string>): MetricHandle;\n}\n\n/**\n * The World Model interface, scoped to one project. The whole surface hangs\n * off this: grab a {@link MetricHandle} with `metric(id)` and the handle\n * speaks the verbs, or pull the whole graph with `tree(root?)`.\n */\nexport interface WorldModelApi {\n /** The active project id, or `null` before `<OxyAppProvider>` resolves one. */\n readonly projectId: string | null;\n /** The metric tree, rooted anywhere you like (default: the whole tree). */\n tree(root?: string, signal?: AbortSignal): Promise<MetricTree>;\n /** A live handle on one measure node. */\n metric(id: string): MetricHandle;\n}\n\n/**\n * Thrown by the value verbs (`explain` / `size`) when called on a handle that\n * has been `drill`ed. The metric-tree backend cannot yet scope these analyses\n * to a segment, so failing loud beats returning population numbers for a\n * question that asked about one segment.\n */\nexport class WorldModelScopeUnsupportedError extends Error {\n readonly code = \"world_model_scope_unsupported\";\n readonly scope: MetricScope;\n constructor(verb: string, scope: MetricScope) {\n super(\n `${verb} on a drilled (scoped) node is not yet supported by the backend ` +\n `(scope: ${JSON.stringify(scope)}). Call ${verb} on the un-drilled node ` +\n `for population-level analysis.`\n );\n this.name = \"WorldModelScopeUnsupportedError\";\n this.scope = scope;\n }\n}\n\n// ── Factory ───────────────────────────────────────────────────────────────────\n\n/**\n * Build a {@link WorldModelApi} over a project id and fetcher. Framework-\n * agnostic — `useWorldModel()` wraps this for React, but it is directly\n * unit-testable with a mock fetcher.\n */\nexport function createWorldModel(projectId: string | null, fetcher: AppFetcher): WorldModelApi {\n const base = (): string => {\n if (!projectId) {\n throw new Error(\n \"World Model unavailable: no active project (are you inside <OxyAppProvider>?)\"\n );\n }\n return metricTreePath(projectId);\n };\n\n const tree = (root?: string, signal?: AbortSignal): Promise<MetricTree> => {\n const qs = root ? `?root=${encodeURIComponent(root)}` : \"\";\n return getJson<MetricTree>(fetcher, `${base()}${qs}`, signal);\n };\n\n const makeHandle = (id: string, scope: MetricScope): MetricHandle => {\n const scoped = Object.keys(scope).length > 0;\n return {\n id,\n scope,\n async node(signal) {\n const t = await tree(id, signal);\n const found = t.nodes.find((n) => n.id === id);\n if (!found) throw new Error(`measure '${id}' not found in the metric tree`);\n return found;\n },\n async expand(signal) {\n const t = await tree(id, signal);\n const byId = new Map(t.nodes.map((n) => [n.id, n] as const));\n // `from` is the parent, `to` the child (component/driver) — the same\n // orientation the IDE metric-tree graph lays out top-down.\n const children: ExpandedNode[] = [];\n for (const edge of t.edges) {\n if (edge.from !== id) continue;\n const childNode = byId.get(edge.to);\n if (!childNode) continue;\n children.push({ node: childNode, edge, handle: makeHandle(edge.to, scope) });\n }\n return children;\n },\n drivers(signal) {\n return getJson<SensitivityResult>(\n fetcher,\n `${base()}/${encodeURIComponent(id)}/sensitivity`,\n signal\n );\n },\n explain(opts, signal) {\n if (scoped) throw new WorldModelScopeUnsupportedError(\"explain\", scope);\n return postJson<ExplainResult>(\n fetcher,\n `${base()}/explain`,\n { target: id, ...opts },\n signal\n );\n },\n size(opts, signal) {\n if (scoped) throw new WorldModelScopeUnsupportedError(\"size\", scope);\n return postJson<OpportunityResult>(\n fetcher,\n `${base()}/opportunity`,\n { target: id, ...opts },\n signal\n );\n },\n drill(next) {\n return makeHandle(id, { ...scope, ...next });\n }\n };\n };\n\n return {\n projectId,\n tree,\n metric: (id: string) => makeHandle(id, {})\n };\n}\n\n// ── Hook ──────────────────────────────────────────────────────────────────────\n\n/**\n * The World Model node interface, scoped to the active `<OxyAppProvider>`\n * project. Returns a stable {@link WorldModelApi} — grab a node with\n * `world.metric(id)` and let it speak the verbs.\n *\n * @example\n * ```tsx\n * const world = useWorldModel();\n * const revenue = world.metric(\"orders.net_revenue\");\n * const children = await revenue.expand(); // components + drivers\n * const rca = await revenue.explain({\n * time_dimension: \"orders.order_date\",\n * current_period: [\"2026-06-01\", \"2026-06-30\"],\n * previous_period: [\"2026-05-01\", \"2026-05-31\"],\n * });\n * ```\n *\n * @remarks\n * This is the node-paradigm hook. For the raw semantic-model entity/measure\n * graph, use {@link useWorldModelGraph} instead.\n */\nexport function useWorldModel(): WorldModelApi {\n const { projectId, fetcher } = useOxyApp();\n return React.useMemo(() => createWorldModel(projectId ?? null, fetcher), [projectId, fetcher]);\n}\n","// Metric-tree types + client. Mirrors `airlayer::engine::metric_tree*`\n// over the `/<project_id>/semantic/metric-tree*` HTTP endpoints. Serde\n// emits snake_case so these field names match the wire format verbatim.\n\nimport type { OxyConfig } from \"./config\";\n\n// ── Tree ──────────────────────────────────────────────────────────────────────\n\nexport type EdgeKind = \"component\" | \"driver\";\nexport type DriverDirection = \"positive\" | \"negative\" | \"unknown\";\nexport type DriverStrength = \"strong\" | \"moderate\" | \"weak\";\nexport type DriverConfidence = \"high\" | \"medium\" | \"low\";\n/** The shape of a driver relationship.\n *\n * The THIRD hand-maintained mirror of this enum (airlayer's is canonical,\n * `web-app/src/types/metricTree.ts` is the second). Nothing enforces that they\n * agree, and the last time one fell behind — `oxy-semantic`, five variants\n * short — a valid `.view.yml` stopped parsing. A type-only union fails more\n * softly: an SDK consumer reading a tree with a quadratic edge just gets a\n * union that cannot hold it. Add new shapes here whenever airlayer grows one. */\nexport type DriverForm =\n | \"linear\"\n | \"log-log\"\n | \"log-linear\"\n | \"linear-log\"\n | \"quadratic\"\n | \"cubic\"\n | \"sqrt\"\n | \"inverse\"\n | \"linear-log-quadratic\";\n\nexport interface MetricNode {\n id: string;\n view: string;\n measure: string;\n label: string;\n description?: string | null;\n measure_type: string;\n is_composite: boolean;\n /** Whether this measure can be drilled into. Serialized rather than\n * re-derived: `measure_type` misses eligible composites, and edge presence\n * over-admits nested / cross-view / multiplicative passthroughs the engine\n * refuses. Non-optional — it is on every metric-tree response. */\n drillable: boolean;\n expr?: string | null;\n}\n\nexport interface MetricEdge {\n from: string;\n to: string;\n kind: EdgeKind;\n /** Sign of a component edge; omitted (defaults to +1) for most edges. */\n sign?: number;\n /** Arithmetic operator joining a component child to its parent. Omitted\n * when it is `add` — airlayer skips the field at its default — so absent\n * MEANS `add`, never \"unknown\". Only `mul` / `div` are multiplicative;\n * `add` / `sub` propagate exactly. */\n operator?: \"add\" | \"sub\" | \"mul\" | \"div\";\n direction: DriverDirection;\n strength: DriverStrength;\n confidence: DriverConfidence;\n coefficient?: number | null;\n form: DriverForm;\n /** Whether `form` was declared in the YAML or inferred by the fit. */\n form_declared?: boolean;\n intercept?: number | null;\n lag?: number | null;\n description?: string | null;\n refs?: string[] | null;\n}\n\nexport interface MetricTree {\n nodes: MetricNode[];\n edges: MetricEdge[];\n root?: string | null;\n /** Refusals raised while building the tree — a driver declaring both\n * `coefficient:` and `coefficients:`, or a wrong-width vector. Absent when\n * empty, so a lever that moves nothing still has a way to say why. */\n warnings?: string[];\n}\n\n// ── Sensitivity ──────────────────────────────────────────────────────────────\n\nexport interface SensitivityDriver {\n measure: string;\n path: string[];\n edge_kind: string;\n effective_coefficient?: number | null;\n form?: DriverForm | null;\n direction: DriverDirection;\n strength: DriverStrength;\n lag?: number | null;\n description?: string | null;\n}\n\nexport interface SensitivityResult {\n target: string;\n drivers: SensitivityDriver[];\n}\n\n// ── Predict ──────────────────────────────────────────────────────────────────\n\nexport interface PredictChange {\n measure: string;\n delta: number;\n}\n\nexport interface PredictImpact {\n measure: string;\n estimated_delta: number;\n confidence: string;\n path: string[];\n form: DriverForm;\n lag?: number | null;\n}\n\nexport interface PredictResult {\n inputs: PredictChange[];\n impacts: PredictImpact[];\n}\n\n/** node_id → the measure's value over the baseline window. */\nexport type MeasureValues = Record<string, number>;\n\n/** A driver edge's coefficient, measured from history by the baseline query.\n *\n * Either `coefficient` is set or `refusal` is — never both, never neither.\n * A refusal is a result: it is why a measure downstream of the change shows\n * no number. Echo the whole array into `predict` verbatim, refusals included;\n * the server ignores entries carrying no coefficient, and filtering them here\n * would be a second place for the two sides to disagree. */\nexport interface FittedDriver {\n from: string;\n to: string;\n lag?: number;\n /** The form the slope was measured in. The same number reads as dollars per\n * dollar under `linear` and as a percent-per-percent elasticity under\n * `log-log`, so a bare figure has no unit. */\n form?: DriverForm;\n /** Paired observations behind the fit.\n *\n * `| null` because this mirrors an `Option<f64>` on a GIT-PINNED struct:\n * `skip_serializing_if` is a serde attribute today, not a guarantee, so a\n * reader must accept both encodings. Compare with `!= null`, never\n * `!== undefined` — and the type has to admit both, or the safe read looks\n * like dead code. Same rule on `t_stat`, `t_stats`, `se_terms` and\n * `coefficient` below. */\n n?: number | null;\n n_panels?: number;\n n_nonpositive?: number;\n /** The FIRST basis term — the whole answer for a single-term form; for a\n * shape that can turn it is only the slope, so read `coefficients`.\n * `| null` because it is an `Option<f64>` on the wire. */\n coefficient?: number | null;\n /** One coefficient per basis term, in basis order. This is what propagation\n * evaluates. */\n coefficients?: number[];\n se?: number;\n /** Elements `| null` for the reason stated on `n`. */\n se_terms?: (number | null)[];\n /** `| null` for the reason stated on `n`. */\n t_stat?: number | null;\n /** `t` per basis term, in basis order — `[1]` is the second basis term, the\n * squared one under every shape that can turn. Elements `| null` for the\n * reason stated on `n`. */\n t_stats?: (number | null)[];\n /** Sufficient statistics of the basis over the rows the fit used. Not\n * diagnostic: the fit is per row and a change is a window aggregate, and a\n * curved response cannot cross that gap without these. Echo verbatim. */\n moments?: { n?: number; s1?: number; s2?: number };\n /** `[min, max]` driver values observed. A change beyond this spread is\n * refused rather than extrapolated. */\n domain?: [number, number];\n /** The response sampled as `[change fraction, delta]`. Read this instead of\n * interpreting the coefficients — peak, break-even and saturation are all\n * properties of these samples, so a reader written against them keeps\n * working when a new shape is added. */\n profile?: [number, number][];\n form_source?: \"declared\" | \"inferred\";\n /** Every shape considered, scored comparably (AIC in y-space, lower better).\n * Empty when the form was declared. `all_terms_significant` false means the\n * candidate was never eligible, however good its score. */\n candidates?: { form: DriverForm; aic: number; all_terms_significant: boolean }[];\n refusal?: string;\n}\n\n/** Why a reachable node has no baseline value. */\nexport interface UnvaluedNode {\n id: string;\n reason?: string | null;\n}\n\n/** Narrow the baseline to one world-model instance. Omit to value the whole\n * population. */\nexport interface BaselineInstance {\n entity: string;\n /** JSON array for a composite key, else a bare scalar. */\n key: string;\n}\n\nexport interface BaselineRequest {\n /** The nodes you intend to change. Values are fetched for these plus\n * everything forward-reachable from them — not the whole tree. */\n roots: string[];\n time_dimension: string;\n /** `[start, end]` inclusive date strings. */\n period: [string, string];\n instance?: BaselineInstance | null;\n}\n\nexport interface BaselineResponse {\n values: MeasureValues;\n unvalued: UnvaluedNode[];\n resolved_period: [string, string];\n /** Why the baseline produced no values, in words worth showing. Absent when\n * measures were valued normally. */\n baseline_note?: string | null;\n /** Coefficients fitted for driver edges that declare none, plus refusals.\n * Absent when every reachable driver edge already declares one. */\n fitted?: FittedDriver[];\n}\n\nexport interface PredictOptions {\n /** Current values for the measures involved. Supplying them lets\n * multiplicative edges be sized instead of returned `unquantifiable`. */\n values?: MeasureValues;\n /** The baseline's `fitted` array, verbatim. */\n coefficients?: FittedDriver[];\n}\n\n// ── Projection (scenario forecasting over time) ──────────────────────────────\n\nexport type ProjectionGranularity = \"day\" | \"week\" | \"month\";\n\n/**\n * The scenario's time axis. `baseline` answers \"what is this measure worth\n * over the window\"; this answers \"what has it been doing, and what does it do\n * next\".\n *\n * The history window is deliberately its own, NOT the baseline's: the\n * forecaster refuses anything under eight seasonal cycles (56 daily buckets,\n * 32 weekly, 24 monthly), and a 30-day scenario baseline reused here would\n * make \"no forecast\" the normal answer.\n */\nexport interface ProjectionRequest {\n /** Lever node ids. Curves are drawn for these plus everything\n * forward-reachable from them — the same set {@link BaselineRequest} values. */\n roots: string[];\n time_dimension: string;\n /** `[start, end]` inclusive date strings for the HISTORY. */\n period: [string, string];\n /** Narrow to one world-model instance. Omit to project the whole\n * population — same picker the baseline uses. */\n instance?: BaselineInstance | null;\n /** Bucket width. Defaults to `day` server-side. */\n granularity?: ProjectionGranularity;\n /** Buckets to project past the last historical one. 1..=365; outside that\n * it is a 400, never a silent clamp — a horizon quietly truncated reads as\n * a forecast that genuinely ends in March. */\n horizon: number;\n /** Seasonal periods, in buckets, applied to every measure in the request.\n *\n * Omitting it is not \"use the default\" — it means *resolve per measure*\n * from whatever `.monitor.yml` already watches that series, which is what\n * keeps this band the band an anomaly had to breach. Send it only to pin a\n * cycle nobody has declared. Each period must be >= 2; `[]` is a 400. */\n seasonality?: number[];\n}\n\nexport interface HistoryPoint {\n /** Bucket start, `YYYY-MM-DD`. */\n date: string;\n value: number;\n}\n\nexport interface ForecastPoint {\n date: string;\n point: number;\n /** The prediction interval. `null` / absent means the model returned no\n * band — unknown spread, NOT a band of zero width. Never collapse these\n * onto `point`: a zero-width band is a claim of certainty nobody made. */\n lower?: number | null;\n upper?: number | null;\n}\n\n/** One measure's baseline curve: what happened, then what comes next.\n *\n * An empty `forecast` carrying a `refusal` is a state, not a gap — most often\n * \"too little history to fit\", or the warehouse refusing this one measure. It\n * must never render as a flat forward line, which is what any code defaulting\n * the missing curve to \"unchanged\" would draw. */\nexport interface MeasureProjection {\n measure: string;\n history: HistoryPoint[];\n forecast: ForecastPoint[];\n refusal?: string | null;\n /** The seasonal periods this curve was decomposed against — resolved per\n * measure, so two series in one response can legitimately differ. */\n seasonality: number[];\n}\n\nexport interface ProjectionResponse {\n granularity: ProjectionGranularity;\n /** Echoed back: the query is expensive and callers cache on it. */\n resolved_period: [string, string];\n horizon: number;\n series: MeasureProjection[];\n /** Why the WHOLE projection is empty, when it is. Absent when at least one\n * measure produced history — a partial failure is each measure's own\n * `refusal`, never a banner over curves that are drawing fine. */\n projection_note?: string | null;\n}\n\n// ── Explain (RCA) ────────────────────────────────────────────────────────────\n\nexport type SplitKind =\n | { type: \"component\"; child_measure: string }\n | { type: \"dimension\"; dimension: string; value: string }\n | { type: \"uniform_degradation\"; dimension: string; num_elements: number }\n | { type: \"cross_cutting\"; dimension: string; value: string; measures: string[] };\n\nexport interface ExplainSibling {\n split: SplitKind;\n measure: string;\n delta: number;\n root_fraction: number;\n}\n\nexport interface ExplainNode {\n split: SplitKind;\n measure: string;\n filters: unknown[];\n delta: number;\n concentration: number;\n root_fraction: number;\n siblings?: ExplainSibling[];\n dimension_count?: number;\n children?: ExplainNode[];\n}\n\n/** Whether a driver's observed move pushes the target the way it actually\n * moved (`contributing`) or against it (`counteracting` — it offset part of\n * the move rather than causing it). `unknown` when no signed claim is\n * available: `direction: unknown` with no coefficient, or a flat\n * driver/target. */\nexport type DriverContribution = \"contributing\" | \"counteracting\" | \"unknown\";\n\n/** A driver's move split into the part its base forced and the part its own\n * ratio contributed. Emitted only when the driver genuinely tracks a sibling\n * rather than moving on its own — presence is the claim.\n * `base_driven_delta + ratio_driven_delta === driver_delta`. */\nexport interface PassthroughSplit {\n base_measure: string;\n ratio_previous: number;\n ratio_current: number;\n base_driven_delta: number;\n ratio_driven_delta: number;\n}\n\nexport interface DriverAttribution {\n driver_measure: string;\n driver_previous: number;\n driver_current: number;\n driver_delta: number;\n /** Both optional: an `explain_cache` row written before these fields shipped\n * is served verbatim, so absent means unclassified — not a default. */\n direction?: DriverDirection;\n contribution?: DriverContribution;\n coefficient?: number;\n form: DriverForm;\n /** Absent for a purely qualitative driver (no coefficient). */\n estimated_target_impact?: number;\n description?: string;\n passthrough?: PassthroughSplit;\n}\n\nexport type ExplainWarning =\n | {\n type: \"simpsons_paradox\";\n dimension: string;\n aggregate_delta: number;\n segment_directions: [string, number][];\n }\n | {\n type: \"opposing_offset\";\n component_a: string;\n component_b: string;\n delta_a: number;\n delta_b: number;\n }\n | {\n type: \"non_additive_dimension_split\";\n measure: string;\n measure_type: string;\n dimension: string;\n };\n\nexport interface ExplainConfigOverride {\n deep?: boolean;\n max_depth?: number;\n coverage_threshold?: number;\n}\n\nexport interface ExplainRequest {\n target: string;\n time_dimension: string;\n current_period: [string, string];\n previous_period: [string, string];\n config?: ExplainConfigOverride;\n}\n\nexport interface ExplainResult {\n target: string;\n target_delta: number;\n target_previous: number;\n target_current: number;\n time_dimension: string;\n current_period: [string, string];\n previous_period: [string, string];\n nodes: ExplainNode[];\n coverage: number;\n driver_attribution?: DriverAttribution[];\n alternatives?: unknown[];\n warnings?: ExplainWarning[];\n}\n\n// ── Opportunity ──────────────────────────────────────────────────────────────\n\nexport interface SegmentOpportunity {\n segment: string;\n current_value: number;\n volume: number;\n benchmark: number;\n gap: number;\n /** Match-the-best upside in measure units. */\n upside: number;\n}\n\nexport interface DimensionOpportunity {\n dimension: string;\n cardinality: number;\n /** \"best_peer\" or \"p75\". */\n benchmark_basis: string;\n total_upside: number;\n segments: SegmentOpportunity[];\n other_segments_skipped: number;\n}\n\nexport interface SkippedDimension {\n dimension: string;\n reason: string;\n}\n\nexport interface OpportunityRequest {\n target: string;\n time_dimension: string;\n period: [string, string];\n}\n\nexport interface OpportunityResult {\n target: string;\n period: [string, string];\n overall_value: number;\n /**\n * \"rows\" (rate-based additive sizing — the only basis that yields a sized\n * upside figure), \"value_share\" (additive) or \"equal\" (ratios).\n */\n weight_basis: string;\n dimensions: DimensionOpportunity[];\n skipped_dimensions: SkippedDimension[];\n downstream: PredictImpact[];\n}\n\n// ── Distribution ─────────────────────────────────────────────────────────────\n\n/**\n * Single-period structural decomposition. The server auto-derives the\n * baseline as the equal-length window immediately before `period`, then\n * returns an {@link ExplainResult}-shaped payload (so the same renderers\n * work). Ignore the delta fields when rendering a pure distribution.\n */\nexport interface DistributionRequest {\n target: string;\n time_dimension: string;\n /** `[start, end]` inclusive date strings. */\n period: [string, string];\n}\n\n// ── Time dimensions ──────────────────────────────────────────────────────────\n\nexport interface TimeDimensionsResponse {\n /** view name → fully-qualified time-dimension ids (`view.dim`). */\n by_view: Record<string, string[]>;\n}\n\n// ── Client ───────────────────────────────────────────────────────────────────\n\n/**\n * Shape of the inner request helper exposed by `OxyClient`. The metric-tree\n * client reuses it to inherit auth headers, timeout, baseUrl, and project\n * scoping rather than reimplementing fetch end-to-end.\n */\nexport type RequestFn = <T>(endpoint: string, options?: RequestInit) => Promise<T>;\n\n/**\n * Client for the `/semantic/metric-tree*` endpoints. Surfaces the airlayer\n * metric-tree analyses — tree introspection, sensitivity, explain, opportunity\n * — plus the three legs of scenario forecasting (`baseline` levels,\n * `predict` propagation, `projection` curves) over typed methods.\n *\n * Construction is internal to {@link OxyClient} — call `client.metricTree`\n * to access an instance rather than building one yourself.\n *\n * @example\n * ```typescript\n * const client = await OxyClient.create({ projectId: \"...\", apiKey: \"...\" });\n * const tree = await client.metricTree.getTree();\n * const drivers = await client.metricTree.getSensitivity(\"orders.net_revenue\");\n * ```\n */\nexport class MetricTreeClient {\n private readonly request: RequestFn;\n private readonly config: OxyConfig;\n\n constructor(config: OxyConfig, request: RequestFn) {\n this.config = config;\n this.request = request;\n }\n\n private path(suffix: string): string {\n return `/${this.config.projectId}/semantic/metric-tree${suffix}`;\n }\n\n private buildQuery(extra: Record<string, string> = {}): string {\n const params: Record<string, string> = { ...extra };\n if (this.config.branch) params.branch = this.config.branch;\n const qs = new URLSearchParams(params).toString();\n return qs ? `?${qs}` : \"\";\n }\n\n /**\n * Fetch the full metric tree, or the subtree rooted at `root`.\n *\n * @param root - Optional fully-qualified measure id to root the tree at.\n * @returns Nodes (measures) and edges (component / driver relationships).\n *\n * @example\n * ```typescript\n * const tree = await client.metricTree.getTree();\n * const subtree = await client.metricTree.getTree(\"orders.net_revenue\");\n * ```\n */\n async getTree(root?: string): Promise<MetricTree> {\n const query = this.buildQuery(root ? { root } : {});\n return this.request<MetricTree>(this.path(query));\n }\n\n /**\n * Rank the declared drivers of a measure by influence.\n *\n * @param measureId - Fully-qualified measure id (`view.measure`).\n *\n * @example\n * ```typescript\n * const sensitivity = await client.metricTree.getSensitivity(\"orders.net_revenue\");\n * for (const driver of sensitivity.drivers) {\n * console.log(driver.measure, driver.direction, driver.strength);\n * }\n * ```\n */\n async getSensitivity(measureId: string): Promise<SensitivityResult> {\n const query = this.buildQuery();\n return this.request<SensitivityResult>(\n this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`)\n );\n }\n\n /**\n * Value a change's starting point, and measure the coefficients it needs.\n *\n * Two warehouse reads: the current value of every node reachable from\n * `roots`, and — for driver edges that declare no `coefficient:` — a fit\n * over the window. Both are expensive, which is why they live here and not\n * in `predict`: `predict` is database-free by design so it can re-run per\n * keystroke, and it CANNOT measure a coefficient itself.\n *\n * That is the whole reason to call this. Pass `fitted` back into `predict`\n * and an undeclared edge propagates; omit it and `predict` has nothing to\n * multiply by, so the impact is simply absent — no error, no refusal, just a\n * downstream measure that never appears.\n *\n * @example\n * ```typescript\n * const baseline = await client.metricTree.getBaseline({\n * roots: [\"marketing_spend.total_spend\"],\n * time_dimension: \"orders.order_date\",\n * period: [\"2025-09-01\", \"2025-09-30\"],\n * });\n * const result = await client.metricTree.predict(\n * [{ measure: \"marketing_spend.total_spend\", delta: 10000 }],\n * { values: baseline.values, coefficients: baseline.fitted }\n * );\n * ```\n */\n async getBaseline(request: BaselineRequest): Promise<BaselineResponse> {\n const query = this.buildQuery();\n return this.request<BaselineResponse>(this.path(`/baseline${query}`), {\n method: \"POST\",\n body: JSON.stringify(request)\n });\n }\n\n /**\n * Propagate hypothetical `(measure, delta)` changes upward through the\n * tree. Returns the estimated impact on every downstream measure.\n *\n * Database-free, so it re-runs cheaply — and so it can only use\n * coefficients it is GIVEN. Without `options.coefficients` from\n * {@link getBaseline}, every edge whose `.view.yml` declares no\n * `coefficient:` contributes nothing and its downstream measures are\n * silently missing from `impacts`. Without `options.values`, multiplicative\n * component edges come back `unquantifiable` rather than sized.\n *\n * @example\n * ```typescript\n * const result = await client.metricTree.predict([\n * { measure: \"marketing_spend.total_spend\", delta: 10000 },\n * ]);\n * ```\n */\n async predict(changes: PredictChange[], options: PredictOptions = {}): Promise<PredictResult> {\n const query = this.buildQuery();\n return this.request<PredictResult>(this.path(`/predict${query}`), {\n method: \"POST\",\n body: JSON.stringify({\n changes,\n ...(options.values ? { values: options.values } : {}),\n // Sent verbatim, refusals included — the server ignores entries\n // carrying no coefficient, and filtering them here would just be a\n // second place for the two sides to disagree.\n ...(options.coefficients?.length ? { coefficients: options.coefficients } : {})\n })\n });\n }\n\n /**\n * Draw the scenario's time axis: bucketed history for the levers and\n * everything downstream, plus the forward curve the detector's own model\n * expects next.\n *\n * The third leg of scenario forecasting. {@link getBaseline} gives levels\n * and coefficients, {@link predict} propagates a change with no database at\n * all, and this gives time — one warehouse query, so treat it like the\n * baseline: fetch on a window change, not on a lever edit.\n *\n * **Returns the BASELINE curve only.** The scenario's second curve is\n * arithmetic over this and a `predict` result — a proportional shift landing\n * `lag` buckets in — and is composed client-side deliberately, so editing a\n * lever costs no query.\n *\n * @example\n * ```typescript\n * const projection = await client.metricTree.getProjection({\n * roots: [\"marketing_spend.total_spend\"],\n * time_dimension: \"orders.order_date\",\n * period: [\"2024-09-01\", \"2025-08-31\"],\n * granularity: \"day\",\n * horizon: 30,\n * });\n * for (const series of projection.series) {\n * if (series.refusal) console.warn(series.measure, series.refusal);\n * }\n * ```\n */\n async getProjection(request: ProjectionRequest): Promise<ProjectionResponse> {\n const query = this.buildQuery();\n return this.request<ProjectionResponse>(this.path(`/projection${query}`), {\n method: \"POST\",\n body: JSON.stringify(request)\n });\n }\n\n /**\n * Period-over-period root-cause decomposition. Recursively splits the\n * target measure by components and dimensions until the move concentrates.\n *\n * @example\n * ```typescript\n * const result = await client.metricTree.explain({\n * target: \"financials.operating_profit\",\n * time_dimension: \"financials.month\",\n * current_period: [\"2025-09-01\", \"2025-09-30\"],\n * previous_period: [\"2025-08-01\", \"2025-08-31\"],\n * });\n * ```\n */\n async explain(request: ExplainRequest): Promise<ExplainResult> {\n const query = this.buildQuery();\n return this.request<ExplainResult>(this.path(`/explain${query}`), {\n method: \"POST\",\n body: JSON.stringify(request)\n });\n }\n\n /**\n * Size the upside opportunity for a measure by finding underperforming\n * segments. Skips high-cardinality dimensions and trims the long tail.\n *\n * @example\n * ```typescript\n * const result = await client.metricTree.findOpportunities({\n * target: \"orders.net_revenue\",\n * time_dimension: \"orders.order_date\",\n * period: [\"2025-09-01\", \"2025-09-30\"],\n * });\n * for (const dim of result.dimensions) {\n * console.log(dim.dimension, \"+\", dim.total_upside);\n * }\n * ```\n */\n async findOpportunities(request: OpportunityRequest): Promise<OpportunityResult> {\n const query = this.buildQuery();\n return this.request<OpportunityResult>(this.path(`/opportunity${query}`), {\n method: \"POST\",\n body: JSON.stringify(request)\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/anomalies.ts","../src/custom-app/base64.ts","../src/custom-app/debug.ts","../src/custom-app/metric-tree-fetch.ts","../src/custom-app/metric-tree-hooks.tsx","../src/custom-app/sse.ts","../src/custom-app/world-model-hooks.tsx","../src/custom-app/world-node.tsx","../src/metricTree.ts","../src/peerCohort.ts"],"sourcesContent":["// Anomaly inbox types + client. Surfaces the `/semantic/anomalies*`\n// endpoints — list, scan, status, explain — so SDK consumers can render\n// the same inbox the Oxy IDE uses.\n\nimport type { OxyConfig } from \"./config\";\nimport type { ExplainResult } from \"./metricTree\";\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\nexport type AnomalyStatus = \"new\" | \"acknowledged\" | \"dismissed\";\nexport type AnomalySeverity = \"low\" | \"medium\" | \"high\";\n\n/** One filter pinning an anomaly (or a failed monitor) to a segment. */\nexport interface AnomalyFilter {\n /** Fully-qualified dimension id, e.g. `\"sales_daily.restaurant_id\"`. */\n member: string;\n /** Matched values (OR within a filter). */\n values: string[];\n}\n\n/**\n * One row in the anomaly inbox. Detected by `oxy-metric-monitoring` per\n * `.monitor.yml` entry; upserted by repeat scans so unresolved anomalies\n * stay visible without piling up duplicates.\n */\nexport interface Anomaly {\n id: string;\n workspace_id: string;\n measure: string;\n time_dimension: string;\n granularity: string;\n period_start: string;\n period_end: string;\n observed: number;\n expected: number;\n lower_bound: number;\n upper_bound: number;\n z_score: number;\n severity: AnomalySeverity | string;\n status: AnomalyStatus | string;\n label?: string | null;\n /**\n * Stable key derived from the monitor's filters (e.g.\n * `\"sales_daily.restaurant_id=loc-abc\"`). Empty for chain-wide monitors.\n */\n dimension_key: string;\n /**\n * Raw filters identifying the segment; `null` for chain-wide monitors.\n * Always present on the wire (the server serializes it unconditionally),\n * hence required-nullable rather than optional — same shape as\n * {@link ScanFailure.filters}.\n */\n filters: AnomalyFilter[] | null;\n /**\n * Groups consecutive flagged buckets of one segment into a single event, so a\n * surge spanning Mon/Wed/Thu reads as one problem rather than three. `null`\n * for rows detected before events existed. This is what\n * {@link AnomaliesClient.updateStatusBulk} wants as `eventIds` — a status\n * action applies to the whole event.\n */\n event_id?: string | null;\n /** Cached ExplainResult — populated by `POST /anomalies/:id/explain`. */\n explain_cache?: ExplainResult | null;\n explain_cached_at?: string | null;\n detected_at: string;\n updated_at: string;\n}\n\nexport interface ListAnomaliesOptions {\n status?: AnomalyStatus | string;\n /**\n * Max **events** (server caps at 500, defaults to 100). Every bucket of a\n * returned event comes back, so the row count is `limit × buckets-per-event`.\n * With `order: \"recent\"` it is a plain row limit instead.\n */\n limit?: number;\n /**\n * How many **events** to skip (rows, with `order: \"recent\"`) — same unit as\n * `limit`, so page `n` is `offset: (n - 1) * limit`. Defaults to 0.\n *\n * Bounded: past the server's maximum depth the request is refused with a 400\n * rather than served a repeat of the last reachable page, so a runaway\n * `offset += limit` loop ends loudly instead of spinning. Every response\n * echoes that depth as `max_offset`, so a loop can stop before reaching it.\n */\n offset?: number;\n /**\n * `\"recent\"` returns latest-first (`detected_at DESC`). Omit for the default\n * worst-first ranking by event severity (active events before dismissed).\n */\n order?: \"recent\";\n}\n\nexport interface ListAnomaliesResponse {\n anomalies: Anomaly[];\n /**\n * Total matching the filter across every page — **events** under the default\n * ranking, rows under `order: \"recent\"`. Same unit as `limit`/`offset`, so\n * `Math.ceil(total / limit)` is the page count. Note it will not equal\n * `anomalies.length` under the default ranking even on a single page: each\n * event returns all of its buckets.\n *\n * **Absent** in two cases, and a client that pages has to handle both. Send\n * neither `limit` nor `offset` and you have asked for \"the top N\", so there\n * is no total behind the answer — the field is omitted rather than filled\n * with the page's own length. Pass a `limit` (with `offset: 0` for the first\n * page) to get a real total to loop against.\n *\n * It is also dropped when the count query itself fails: the page rows are\n * already in hand, and the server serves them without their denominator\n * rather than failing a request it could answer. So a page you asked for\n * with `limit` can still come back untotalled — page off `anomalies.length`\n * and `max_offset` in that case rather than treating it as zero.\n */\n total?: number;\n /**\n * The page actually served. `limit` is clamped to 1..=500, so it can come\n * back smaller than you asked for and every page number you compute must\n * divide by this rather than by what you sent. `offset` is *not* clamped —\n * too deep a request is refused with a 400 (see `max_offset`), so this echoes\n * the offset you sent whenever there is a response at all.\n *\n * Optional because a replica still running a pre-paging build emits neither,\n * which is a live shape during a rolling deploy. Fall back to what you asked\n * for rather than doing arithmetic on `undefined`.\n */\n limit?: number;\n offset?: number;\n /**\n * The deepest `offset` the server will serve — past it a request is refused\n * with a 400. Read it rather than hardcoding a copy: a paging loop bounded by\n * this stops cleanly instead of ending on an error.\n */\n max_offset?: number;\n /**\n * Event keys whose buckets were trimmed to the server's per-event cap (50) —\n * an `event_id`, or `ungrouped:<row id>` for a row detected before events\n * existed. For those events `anomalies` holds the worst buckets, not all of\n * them, so a status write should name the event through `updateStatusBulk`'s\n * `eventIds` rather than enumerating the buckets you received.\n *\n * Only meaningful under the default ranking, which pages *events* and\n * returns each whole — there, an absence means complete. With\n * `order: \"recent\"` the page is row-limited, so an event can straddle its\n * boundary instead; this list stays empty and every event should be treated\n * as possibly partial.\n */\n truncated_events?: string[];\n}\n\nexport interface BulkUpdateStatusResponse {\n /** Buckets actually written. Lower than what you sent when a row was\n * deleted, moved out of `onlyStatus`, or belongs to another workspace. */\n updated: number;\n /** Distinct anomalies behind those buckets — events, plus standalone\n * pre-event rows. The unit a UI counts in, and one only the server can\n * compute: naming an event never told you how many buckets it held.\n *\n * An anomaly counts as updated once *any* of its buckets is written. Name\n * events through `eventIds` and that is the whole anomaly; name one bucket\n * of a long chain through `ids` and this still reports `1` while the rest\n * keep their old status. `ids` is for pre-event rows, which hold one bucket\n * each — using it for anything else buys a partial write. */\n events_updated: number;\n}\n\nexport interface ScanOptions {\n /** Override the reference \"now\" date (YYYY-MM-DD) — useful for demos. */\n as_of?: string;\n}\n\n/** One `.monitor.yml` entry that errored during a scan. */\nexport interface ScanFailure {\n measure: string;\n time_dimension: string;\n granularity: string;\n label: string | null;\n /** Segment key for a `group_by`/filtered monitor; empty for chain-wide. */\n dimension_key: string;\n /** Raw filters identifying the segment; null for chain-wide monitors. */\n filters: AnomalyFilter[] | null;\n error: string;\n}\n\nexport interface ScanResponse {\n monitors_scanned: number;\n monitors_failed: number;\n anomalies_persisted: number;\n /**\n * True when the scan is still running server-side (it exceeded the 55 s\n * synchronous window, or a scan started within the last 60 s and this call\n * was debounced). The counts are all `0` in that case — they are NOT a\n * \"nothing found\" result. Refetch with `list()` after a short delay.\n */\n pending: boolean;\n /**\n * Per-monitor failures. Empty array (never absent) on a clean scan and on\n * the `pending` path, where failures aren't known yet.\n */\n failures: ScanFailure[];\n}\n\nexport interface ExplainOptions {\n /** Recompute even when the row already has a cached result. */\n refresh?: boolean;\n}\n\n// ── Client ───────────────────────────────────────────────────────────────────\n\nexport type RequestFn = <T>(endpoint: string, options?: RequestInit) => Promise<T>;\n\n/** Which buckets a write may touch when the caller didn't say. Live statuses\n * for ack/dismiss; all three for a reopen, which exists to reach dismissed\n * ones. */\nfunction defaultScope(status: AnomalyStatus): AnomalyStatus[] {\n return status === \"new\" ? [\"new\", \"acknowledged\", \"dismissed\"] : [\"new\", \"acknowledged\"];\n}\n\n/**\n * Client for `/semantic/anomalies*`. Construct via `OxyClient.anomalies`\n * rather than instantiating directly — the getter wires the request helper\n * so auth, timeout, and branch propagation come along for free.\n *\n * @example\n * ```typescript\n * const { anomalies } = await client.anomalies.list({ status: \"new\" });\n * for (const a of anomalies) {\n * console.log(a.label ?? a.measure, a.severity, a.z_score.toFixed(2));\n * }\n * ```\n */\nexport class AnomaliesClient {\n private readonly request: RequestFn;\n private readonly config: OxyConfig;\n\n constructor(config: OxyConfig, request: RequestFn) {\n this.config = config;\n this.request = request;\n }\n\n private path(suffix: string): string {\n return `/${this.config.projectId}/semantic/anomalies${suffix}`;\n }\n\n private buildQuery(extra: Record<string, string> = {}): string {\n const params: Record<string, string> = { ...extra };\n if (this.config.branch) params.branch = this.config.branch;\n const qs = new URLSearchParams(params).toString();\n return qs ? `?${qs}` : \"\";\n }\n\n /**\n * List anomalies in the inbox, ranked worst-first by event severity (active\n * events before dismissed). Pass `order: \"recent\"` for latest-first.\n *\n * @example\n * ```typescript\n * // Open / unresolved anomalies only\n * const { anomalies } = await client.anomalies.list({ status: \"new\" });\n *\n * // Second page of 25 events\n * const page2 = await client.anomalies.list({ limit: 25, offset: 25 });\n * console.log(`${(page2.offset ?? 25) + 1}+ of ${page2.total ?? \"?\"}`);\n * ```\n */\n async list(options: ListAnomaliesOptions = {}): Promise<ListAnomaliesResponse> {\n const extra: Record<string, string> = {};\n if (options.status) extra.status = options.status;\n // Presence, not truthiness. The server reads \"is this caller paging?\" off\n // whether these params were sent at all, so dropping `offset: 0` on a\n // falsy check would make the first iteration of a paging loop a non-paging\n // request — one that reports `total` as just the rows it returned, ending\n // the loop after a single page.\n if (options.limit !== undefined) extra.limit = String(options.limit);\n if (options.offset !== undefined) extra.offset = String(options.offset);\n if (options.order) extra.order = options.order;\n // No trailing slash before the query — axum 307-redirects \"/anomalies/\"\n // to \"/anomalies\", and the redirect fails CORS preflight in browsers.\n return this.request<ListAnomaliesResponse>(this.path(this.buildQuery(extra)));\n }\n\n /**\n * Trigger a full scan. Iterates every `.monitor.yml` entry in the\n * workspace, runs the detector, and upserts matching rows into the\n * inbox. Returns counts of scanned / failed / persisted.\n *\n * Long-running: the server waits up to 55 s, then returns\n * `pending: true` with zeroed counts while the scan finishes in the\n * background. Always check `pending` before treating `0` as \"nothing\n * found\", and refetch with {@link list} shortly after.\n *\n * @example\n * ```typescript\n * // Scan against a known-good reference date (matches the seed dataset)\n * const result = await client.anomalies.scan({ as_of: \"2025-12-15\" });\n * if (result.pending) {\n * console.log(\"scan still running — refetch shortly\");\n * } else {\n * console.log(`${result.anomalies_persisted} anomalies detected`);\n * }\n * ```\n */\n async scan(options: ScanOptions = {}): Promise<ScanResponse> {\n const extra: Record<string, string> = {};\n if (options.as_of) extra.as_of = options.as_of;\n return this.request<ScanResponse>(this.path(`/scan${this.buildQuery(extra)}`), {\n method: \"POST\"\n });\n }\n\n /**\n * Update an anomaly's status (acknowledge / dismiss / re-open).\n */\n async updateStatus(anomalyId: string, status: AnomalyStatus): Promise<Anomaly> {\n const query = this.buildQuery();\n return this.request<Anomaly>(this.path(`/${encodeURIComponent(anomalyId)}/status${query}`), {\n method: \"POST\",\n body: JSON.stringify({ status })\n });\n }\n\n /**\n * Update many anomalies in one request — the batch form of\n * {@link updateStatus}. Identifiers outside the workspace are skipped rather\n * than erroring, so `updated` (rows written) can be lower than what you sent.\n * At most 2000 identifiers across both lists.\n *\n * **Prefer `eventIds`.** Inbox actions are per *event*, and a list response\n * caps how many buckets it returns per event — so acking the bucket ids you\n * received can leave the tail of a long chain behind, `new`, under a clean\n * success. Naming the event lets the server write all of it. `ids` is for\n * rows with no `event_id` (detected before events existed), which can only\n * be named individually.\n *\n * `onlyStatuses` says which of an event's buckets may move. An event can span\n * statuses, so an unbounded write resurrects buckets that were dismissed on\n * purpose — which is why omitting it takes a scope rather than no bound at\n * all: the live statuses (`[\"new\", \"acknowledged\"]`) for an ack or dismiss,\n * and all three for `status: \"new\"`, since reopening is how a dismissed\n * anomaly comes back. The server applies that same default, so the safe\n * behaviour does not depend on going through this client. Pass `[]` to opt\n * out of the bound entirely.\n *\n * @example\n * ```typescript\n * const { anomalies } = await client.anomalies.list({ status: \"new\", limit: 50, offset: 0 });\n * // Both lists: events by id, and pre-event rows (no `event_id`) by their own.\n * const eventIds = [...new Set(anomalies.flatMap((a) => (a.event_id ? [a.event_id] : [])))];\n * const ids = anomalies.filter((a) => !a.event_id).map((a) => a.id);\n * const { updated } = await client.anomalies.updateStatusBulk(\n * { ids, eventIds, onlyStatuses: [\"new\", \"acknowledged\"] },\n * \"acknowledged\"\n * );\n * ```\n */\n async updateStatusBulk(\n target: { ids?: string[]; eventIds?: string[]; onlyStatuses?: AnomalyStatus[] },\n status: AnomalyStatus\n ): Promise<BulkUpdateStatusResponse> {\n return this.request<BulkUpdateStatusResponse>(this.path(`/status${this.buildQuery()}`), {\n method: \"POST\",\n body: JSON.stringify({\n ids: target.ids ?? [],\n event_ids: target.eventIds ?? [],\n // Defaults to a scope, never to \"no bound\": an empty list tells the\n // server to write every bucket of the named events, dismissed ones\n // included, and that is the single state this design says must not be\n // reversed by accident.\n //\n // Reopening is the exception. `status: \"new\"` is how a dismissed\n // anomaly comes back, so excluding `dismissed` there would make the\n // one call that needs it a silent no-op.\n only_statuses: target.onlyStatuses ?? defaultScope(status),\n status\n })\n });\n }\n\n /**\n * Run the metric-tree `explain` for an anomaly and cache the result on\n * the row. Subsequent calls return the cached `ExplainResult` instantly;\n * pass `{ refresh: true }` to bust the cache and recompute.\n *\n * The uncached path runs a 20-30 s recursive driver search — budget for it\n * (or read `explain_cache` off the row from {@link list} when it's already\n * populated).\n */\n async explain(anomalyId: string, options: ExplainOptions = {}): Promise<ExplainResult> {\n const extra: Record<string, string> = {};\n if (options.refresh) extra.refresh = \"true\";\n return this.request<ExplainResult>(\n this.path(`/${encodeURIComponent(anomalyId)}/explain${this.buildQuery(extra)}`),\n { method: \"POST\" }\n );\n }\n}\n","// Base64 for binary that has to cross a boundary as text — an email\n// attachment's `content`, a `ctx.storage.put` body.\n//\n// These are plain functions, deliberately NOT `btoa`/`atob`:\n//\n// - `btoa` takes a Latin1 STRING. Handing it a `Uint8Array` is the classic\n// footgun: the spec stringifies it, so a PDF starting `%PDF` silently\n// encodes the text \"37,80,68,70\" and you ship a corrupt file. A named\n// function that takes bytes cannot be misused that way.\n// - Being ordinary bundled JS, they behave identically in the Oxy Functions\n// isolate, in Node/vitest, and in a browser. Anything reached through a\n// global risks being a different implementation in each.\n\nconst B64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/** Reverse lookup; 255 marks \"not a base64 character\". */\nconst B64R = /* @__PURE__ */ (() => {\n const t = new Uint8Array(256).fill(255);\n for (let i = 0; i < 64; i++) t[B64.charCodeAt(i)] = i;\n return t;\n})();\n\n/**\n * Chunk size for building output in segments. Byte-at-a-time `+=` allocates a\n * rope node per byte, and `String.fromCharCode.apply` blows the argument limit\n * on large inputs; 8k avoids both.\n */\nconst CHUNK = 8192;\n\nfunction asBytes(input: Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array {\n if (input instanceof Uint8Array) return input;\n if (input instanceof ArrayBuffer) return new Uint8Array(input);\n return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);\n}\n\n/**\n * Encode bytes as standard (padded) base64.\n *\n * ```ts\n * const pdf = new Uint8Array(await renderReport());\n * await ctx.email.send({\n * to: ctx.user.email,\n * subject: \"Report\",\n * text: \"attached\",\n * attachments: [{ filename: \"report.pdf\", content: bytesToBase64(pdf) }]\n * });\n * ```\n *\n * For **text** you generated, skip this entirely and pass the string with\n * `encoding: \"utf8\"` — it needs no encoder and stays byte-exact for non-ASCII.\n */\nexport function bytesToBase64(input: Uint8Array | ArrayBuffer | ArrayBufferView): string {\n const bytes = asBytes(input);\n const parts: string[] = [];\n let buf = \"\";\n for (let i = 0; i < bytes.length; i += 3) {\n const b0 = bytes[i];\n const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;\n const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;\n const n = (b0 << 16) | (b1 << 8) | b2;\n buf +=\n B64[(n >> 18) & 63] +\n B64[(n >> 12) & 63] +\n (i + 1 < bytes.length ? B64[(n >> 6) & 63] : \"=\") +\n (i + 2 < bytes.length ? B64[n & 63] : \"=\");\n if (buf.length >= CHUNK) {\n parts.push(buf);\n buf = \"\";\n }\n }\n parts.push(buf);\n return parts.join(\"\");\n}\n\n/**\n * Decode standard base64 to bytes — e.g. the body from\n * `ctx.storage.get(key, { encoding: \"base64\" })`.\n *\n * Throws on malformed input rather than returning a short buffer: a truncated\n * decode that reports success is a corrupt file nobody notices.\n */\nexport function base64ToBytes(base64: string): Uint8Array {\n let s = String(base64).replace(/[ \\t\\n\\f\\r]/g, \"\");\n // Strip padding first, and only at a multiple of 4 — matching WHATWG. A\n // decoder that stopped at the first \"=\" would silently truncate\n // `base64ToBytes(chunkA + chunkB)` when chunkA carries its own padding.\n if (s.length % 4 === 0) {\n let pad = 0;\n while (pad < 2 && s.charCodeAt(s.length - 1) === 61 /* = */) {\n s = s.slice(0, -1);\n pad++;\n }\n }\n if (s.indexOf(\"=\") >= 0) {\n throw new TypeError(\"base64ToBytes: '=' may only appear as trailing padding\");\n }\n if (s.length % 4 === 1) {\n throw new TypeError(\"base64ToBytes: invalid base64 length\");\n }\n const out = new Uint8Array((s.length * 3) >> 2);\n let o = 0;\n let buf = 0;\n let bits = 0;\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n const v = code < 256 ? B64R[code] : 255;\n if (v === 255) {\n throw new TypeError(`base64ToBytes: invalid base64 character '${s[i]}'`);\n }\n buf = (buf << 6) | v;\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n out[o++] = (buf >> bits) & 0xff;\n }\n }\n return out.subarray(0, o);\n}\n","// Bundle-side accessor for the server's diagnostic snapshot.\n//\n// `GET /api/customer-apps/<org>/<app>/debug` returns a structured\n// snapshot of what oxy currently sees about a registered customer\n// app: the app row, bundle dir resolution, and parsed manifest (or\n// parse error). Useful when a bundle isn't loading what you expected\n// and you want to verify what the server actually sees — without\n// needing terminal access.\n//\n// The `products` field on the snapshot is a legacy artifact carried\n// for server-side compatibility; in v2 the bundle owns its queries\n// via `useQuery` and the field is always empty for v2 manifests.\n\nimport { getOxyAppLogger } from \"./logger\";\nimport type { ResolvedCustomAppManifest } from \"./manifest\";\n\n/** Untyped at the boundary — keep it loose so server-side schema\n * additions don't break older bundles. Stable enough for inspection\n * but not a contract clients should depend on field-by-field. */\nexport interface CustomAppDebugSnapshot {\n org_slug: string;\n app_slug: string;\n app: {\n id: string;\n slug: string;\n name: string;\n status: string;\n source_type: string;\n project_id: string;\n branch: string;\n };\n bundle_dir: string | null;\n bundle_dir_exists: boolean;\n /** Raw parsed manifest from the server — kept loose so schema additions don't break older bundles. */\n manifest: Record<string, unknown> | null;\n manifest_error: string | null;\n products: Array<{ name: string; producer: string }>;\n}\n\n/**\n * Fetch the server-side diagnostic snapshot for this bundle. Pair with\n * `loadCustomAppManifest()` — pass its result here. Logs the\n * snapshot through the SDK logger so it appears in the bundle's\n * console at info level.\n */\nexport async function getCustomAppDebug(\n resolved: ResolvedCustomAppManifest\n): Promise<CustomAppDebugSnapshot> {\n const log = getOxyAppLogger();\n const { apiBaseUrl, orgSlug, appSlug } = resolved;\n const url =\n `${apiBaseUrl}/api/customer-apps/` +\n `${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/debug`;\n\n log.log(\"debug\", \"fetching debug snapshot\", { url });\n const res = await fetch(url, { credentials: \"same-origin\" });\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\");\n throw new Error(\n `Failed to fetch debug snapshot (HTTP ${res.status}): ${detail || res.statusText}`\n );\n }\n const snapshot = (await res.json()) as CustomAppDebugSnapshot;\n log.log(\"info\", \"debug snapshot\", snapshot as unknown as Record<string, unknown>);\n return snapshot;\n}\n","// Shared fetch helpers for the `/api/projects/{id}/semantic/metric-tree*`\n// endpoints. Both the metric-tree analysis hooks (`metric-tree-hooks.tsx`)\n// and the higher-level World Model node interface (`world-node.tsx`) enter\n// the semantic model through these, so the request envelope (`{ v: 1, … }`)\n// and error decoding stay identical across the two surfaces.\n\nimport { apiErrorFromResponse } from \"./errors\";\nimport type { AppFetcher } from \"./react\";\n\n/** Base path for the metric-tree endpoints of `projectId`. */\nexport function metricTreePath(projectId: string): string {\n return `/api/projects/${projectId}/semantic/metric-tree`;\n}\n\n/** GET `url`, decoding JSON or throwing a typed {@link OxyApiError}. */\nexport async function getJson<Data>(\n fetcher: AppFetcher,\n url: string,\n signal?: AbortSignal\n): Promise<Data> {\n const resp = await fetcher(url, { method: \"GET\", signal });\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n return (await resp.json()) as Data;\n}\n\n/** POST `body` (tagged `v: 1`) to `url`, decoding JSON or throwing. */\nexport async function postJson<Data>(\n fetcher: AppFetcher,\n url: string,\n body: unknown,\n signal?: AbortSignal\n): Promise<Data> {\n const resp = await fetcher(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: 1, ...(body as object) }),\n signal\n });\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n return (await resp.json()) as Data;\n}\n","// React hooks for the metric-tree analysis ops, exposed to custom-app\n// bundles so an app can run drivers / what-if / RCA / opportunity sizing\n// without hand-rolling fetch against the semantic model.\n//\n// These wrap the `/api/projects/{id}/semantic/metric-tree*` endpoints —\n// the same airlayer analyses the IDE's World Model and Metric Tree\n// surfaces drive — behind the shared `OxyAppProvider` fetcher (session\n// cookie in-workspace, dev-proxy token cross-origin). Response shapes\n// reuse the wire types in `../metricTree`, so a bundle typed against a\n// hook result matches what the server serializes verbatim.\n//\n// Pattern mirrors `useSemanticQuery`: each hook fetches when enabled and\n// its input is present, re-runs on input change (deep-compared via JSON),\n// and exposes `refetch`. Read-only inputs (`null`) keep a hook idle — the\n// natural fit for \"run once the user picks a target measure\".\n\nimport * as React from \"react\";\nimport type {\n BaselineRequest,\n BaselineResponse,\n DistributionRequest,\n ExplainRequest,\n ExplainResult,\n MetricTree,\n OpportunityRequest,\n OpportunityResult,\n PredictChange,\n PredictOptions,\n PredictResult,\n ProjectionRequest,\n ProjectionResponse,\n SensitivityResult,\n TimeDimensionsResponse\n} from \"../metricTree\";\nimport { asReportableError } from \"./errors\";\nimport { getJson, metricTreePath, postJson } from \"./metric-tree-fetch\";\nimport { useOxyApp } from \"./react\";\n\n/** Shared result envelope for every metric-tree hook. */\nexport interface MetricTreeHookResult<Data> {\n data: Data | null;\n loading: boolean;\n error: Error | null;\n /** Force a re-run, bypassing nothing — the server honors `?refresh`. */\n refetch: () => void;\n}\n\ninterface EndpointOpts {\n /** Set false to skip the request (e.g. waiting on a user selection). */\n enabled?: boolean;\n}\n\n/**\n * Internal engine shared by every metric-tree hook. Runs `run(signal)`\n * whenever `key` changes (and on `refetch`), tracks loading/error, and\n * cancels in-flight work on unmount or input change.\n *\n * `key` is the deep-compare fingerprint of the request; a `null` key\n * means \"no request yet\" and leaves the hook idle without firing.\n */\nfunction useMetricTreeEndpoint<Data>(\n key: string | null,\n run: (signal: AbortSignal) => Promise<Data>,\n enabled: boolean\n): MetricTreeHookResult<Data> {\n const [data, setData] = React.useState<Data | null>(null);\n const [loading, setLoading] = React.useState<boolean>(enabled && key !== null);\n const [error, setError] = React.useState<Error | null>(null);\n const [nonce, setNonce] = React.useState(0);\n\n // `run` is re-created each render; pin the latest in a ref so the effect\n // depends only on `key`/`enabled`/`nonce` and doesn't re-fire on every\n // parent render.\n const runRef = React.useRef(run);\n runRef.current = run;\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: nonce is refetch's re-run trigger, not a value the effect reads\n React.useEffect(() => {\n if (!enabled || key === null) {\n setLoading(false);\n return;\n }\n const ctrl = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n\n runRef\n .current(ctrl.signal)\n .then((result) => {\n if (cancelled) return;\n setData(result);\n setLoading(false);\n })\n .catch((err: unknown) => {\n // `cancelled` marks the runs WE tore down (it is set next to\n // `ctrl.abort()`); nothing else earns silence. Matching `AbortError`\n // by name also swallowed aborts we did not cause and pinned the hook\n // on `loading: true` with no error.\n if (cancelled) return;\n setError(asReportableError(err));\n setLoading(false);\n });\n\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n // `nonce` is what `refetch()` bumps — without it here the effect never\n // re-ran and a failed analysis could not be retried at all.\n }, [key, enabled, nonce]);\n\n const refetch = React.useCallback(() => setNonce((n) => n + 1), []);\n return { data, loading, error, refetch };\n}\n\n// ── useMetricTree ─────────────────────────────────────────────────────────────\n\nexport interface UseMetricTreeOpts extends EndpointOpts {\n /** Optional measure id to root the returned subtree at. */\n root?: string;\n}\n\n/**\n * The project's metric tree — measures (nodes) and their component /\n * driver relationships (edges) — or the subtree rooted at `opts.root`.\n * The structural backbone every other metric-tree analysis reads against.\n */\nexport function useMetricTree(opts: UseMetricTreeOpts = {}): MetricTreeHookResult<MetricTree> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const root = opts.root;\n const key = projectId ? JSON.stringify({ projectId, root }) : null;\n\n return useMetricTreeEndpoint<MetricTree>(\n key,\n (signal) => {\n const qs = root ? `?root=${encodeURIComponent(root)}` : \"\";\n return getJson<MetricTree>(fetcher, `${metricTreePath(projectId as string)}${qs}`, signal);\n },\n enabled\n );\n}\n\n// ── useSensitivity (drivers) ──────────────────────────────────────────────────\n\n/**\n * Ranked drivers of `measureId`, by influence — the \"what moves this\n * measure\" question. Pass `null` to stay idle until a measure is chosen.\n */\nexport function useSensitivity(\n measureId: string | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<SensitivityResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && measureId ? JSON.stringify({ projectId, measureId }) : null;\n\n return useMetricTreeEndpoint<SensitivityResult>(\n key,\n (signal) => {\n const path = `${metricTreePath(projectId as string)}/${encodeURIComponent(\n measureId as string\n )}/sensitivity`;\n return getJson<SensitivityResult>(fetcher, path, signal);\n },\n enabled\n );\n}\n\n// ── usePredict (what-if) ──────────────────────────────────────────────────────\n\nexport interface UsePredictOpts extends EndpointOpts, PredictOptions {}\n\n/**\n * Propagate hypothetical `(measure, delta)` changes upward through the\n * tree and return the estimated impact on every downstream measure — a\n * pure metric-tree walk, no warehouse query. Pass `null` to stay idle.\n *\n * Because it is database-free it can only use the coefficients it is GIVEN.\n * Without `opts.coefficients` from {@link useBaseline}, every driver edge\n * whose `.view.yml` declares no `coefficient:` contributes nothing and its\n * downstream measures are simply absent from `impacts` — no error, no\n * refusal. Without `opts.values`, multiplicative component edges come back\n * `unquantifiable` rather than sized.\n */\nexport function usePredict(\n changes: PredictChange[] | null,\n opts: UsePredictOpts = {}\n): MetricTreeHookResult<PredictResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const { values, coefficients } = opts;\n const body = {\n changes,\n ...(values ? { values } : {}),\n // Sent verbatim, refusals included — the server ignores entries carrying\n // no coefficient, and filtering them here would just be a second place for\n // the two sides to disagree.\n ...(coefficients?.length ? { coefficients } : {})\n };\n const key = projectId && changes ? JSON.stringify({ projectId, body }) : null;\n\n return useMetricTreeEndpoint<PredictResult>(\n key,\n (signal) =>\n postJson<PredictResult>(\n fetcher,\n `${metricTreePath(projectId as string)}/predict`,\n body,\n signal\n ),\n enabled\n );\n}\n\n// ── useBaseline (scenario levels + fitted coefficients) ───────────────────────\n\n/**\n * Value a scenario's starting point, and measure the coefficients it needs.\n *\n * Two warehouse reads: the current value of every node reachable from\n * `request.roots`, and — for driver edges declaring no `coefficient:` — a fit\n * over the window. Both are expensive, which is why they live here and not in\n * {@link usePredict}: predict is database-free by design so it can re-run per\n * keystroke, and it CANNOT measure a coefficient itself.\n *\n * That is the whole reason to call this. Feed `data.values` and `data.fitted`\n * into `usePredict`; omit them and an undeclared edge propagates nothing.\n * Pass `null` to stay idle until levers and a period are chosen.\n */\nexport function useBaseline(\n request: BaselineRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<BaselineResponse> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<BaselineResponse>(\n key,\n (signal) =>\n postJson<BaselineResponse>(\n fetcher,\n `${metricTreePath(projectId as string)}/baseline`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useProjection (scenario forecasting over time) ────────────────────────────\n\n/**\n * Bucketed history for the levers and everything downstream, plus the forward\n * curve the forecaster expects next — the scenario's time axis.\n *\n * One warehouse query, so it belongs on a window change, not on a lever edit.\n * It returns the BASELINE curve only: the scenario's second curve is\n * arithmetic over this and a `usePredict` result — a proportional shift\n * landing `lag` buckets in — composed client-side precisely so editing a lever\n * costs no query.\n *\n * Treat a series with a `refusal` as a stated absence: it must not render as a\n * flat forward line. Pass `null` to stay idle.\n */\nexport function useProjection(\n request: ProjectionRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<ProjectionResponse> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<ProjectionResponse>(\n key,\n (signal) =>\n postJson<ProjectionResponse>(\n fetcher,\n `${metricTreePath(projectId as string)}/projection`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useExplain (RCA) ──────────────────────────────────────────────────────────\n\n/**\n * Period-over-period root-cause decomposition: recursively splits the\n * target measure by components and dimensions until the move concentrates.\n * This is the heavy one — it can fire many warehouse queries and the\n * server caps it at 45s. Pass `null` to defer until periods are chosen.\n */\nexport function useExplain(\n request: ExplainRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<ExplainResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<ExplainResult>(\n key,\n (signal) =>\n postJson<ExplainResult>(\n fetcher,\n `${metricTreePath(projectId as string)}/explain`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useDistribution ───────────────────────────────────────────────────────────\n\n/**\n * Single-period distribution of a measure — an {@link ExplainResult}\n * against an auto-derived immediately-prior baseline. Same renderers as\n * `useExplain`; ignore the delta fields for a pure distribution view.\n */\nexport function useDistribution(\n request: DistributionRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<ExplainResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<ExplainResult>(\n key,\n (signal) =>\n postJson<ExplainResult>(\n fetcher,\n `${metricTreePath(projectId as string)}/distribution`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useOpportunity (sizing) ───────────────────────────────────────────────────\n\n/**\n * Segment opportunity sizing for a measure over a period: finds\n * underperforming segments and sizes the addressable upside of closing\n * each rate gap against a benchmark peer. Pass `null` to stay idle until\n * a target + period are chosen.\n */\nexport function useOpportunity(\n request: OpportunityRequest | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<OpportunityResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && request ? JSON.stringify({ projectId, request }) : null;\n\n return useMetricTreeEndpoint<OpportunityResult>(\n key,\n (signal) =>\n postJson<OpportunityResult>(\n fetcher,\n `${metricTreePath(projectId as string)}/opportunity`,\n request,\n signal\n ),\n enabled\n );\n}\n\n// ── useTimeDimensions ─────────────────────────────────────────────────────────\n\n/**\n * The queryable time dimensions per view (`view.dim` ids) — what a\n * bundle offers as the period axis for `explain` / `opportunity` /\n * `distribution` instead of hardcoding a curated map.\n */\nexport function useTimeDimensions(\n opts: EndpointOpts = {}\n): MetricTreeHookResult<TimeDimensionsResponse> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId ? JSON.stringify({ projectId, kind: \"time-dimensions\" }) : null;\n\n return useMetricTreeEndpoint<TimeDimensionsResponse>(\n key,\n (signal) =>\n getJson<TimeDimensionsResponse>(\n fetcher,\n `${metricTreePath(projectId as string)}/time-dimensions`,\n signal\n ),\n enabled\n );\n}\n","// Minimal `text/event-stream` reader for the world-model streams.\n//\n// Unlike `function-sse.ts` (which reads a single terminal function result),\n// the world-model `instance-detail` / `measure-breakdown` endpoints emit a\n// sequence of `kind`-tagged JSON events on the default (unnamed) SSE event,\n// terminating with a `{ kind: \"done\" }` frame and then closing the stream.\n// This reader parses each `data:` frame's JSON and hands it to `onEvent`; the\n// caller folds the events into accumulated state. It resolves when the stream\n// closes (or the signal aborts) — the hook decides what \"done\" means.\n\n/**\n * Read a `text/event-stream` response, invoking `onEvent` with each parsed\n * JSON frame. Frames that fail to parse are skipped (a malformed frame must\n * not tear down the whole stream). Resolves when the body closes.\n */\nexport async function readJsonSseStream<E>(\n resp: Response,\n onEvent: (event: E) => void\n): Promise<void> {\n const reader = resp.body?.getReader();\n if (!reader) {\n throw new Error(\"SSE response has no body stream\");\n }\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n let sep: number;\n while ((sep = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const frame = buffer.slice(0, sep);\n buffer = buffer.slice(sep + 2);\n let data = \"\";\n for (const line of frame.split(\"\\n\")) {\n // Ignore `event:`/`id:`/`retry:` lines — the world-model streams put\n // everything in `data:` on the default event.\n if (line.startsWith(\"data:\")) data += line.slice(5).trim();\n }\n if (!data) continue;\n let parsed: E;\n try {\n parsed = JSON.parse(data) as E;\n } catch {\n continue;\n }\n onEvent(parsed);\n }\n }\n}\n","// React hooks for the world-model surface, exposed to custom-app bundles\n// so an app can render the semantic-model graph, browse an entity's\n// instances, and drill into an instance's detail / measure driver-tree.\n//\n// Wraps the `/api/projects/{id}/semantic/world-model*` endpoints behind the\n// shared `OxyAppProvider` fetcher. The two drill-down endpoints\n// (`instance-detail`, `measure-breakdown`) stream `kind`-tagged SSE events;\n// their hooks fold the stream into accumulated state so a bundle can render\n// progressively (skeletons fill in as measure values resolve).\n\nimport * as React from \"react\";\nimport type {\n WmInstancesResponse,\n WmMeasureBreakdown,\n WmMeasureBreakdownEvent,\n WorldModel\n} from \"../worldModel\";\nimport { apiErrorFromResponse, asReportableError } from \"./errors\";\nimport { useOxyApp } from \"./react\";\nimport { readJsonSseStream } from \"./sse\";\n\n/** Base path for the world-model endpoints of the active project. */\nfunction worldModelPath(projectId: string): string {\n return `/api/projects/${projectId}/semantic/world-model`;\n}\n\n// ── useWorldModelGraph (graph) ────────────────────────────────────────────────\n\nexport interface UseWorldModelGraphResult {\n data: WorldModel | null;\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\n/**\n * The world-model graph — entities (nodes), their measures/dimensions, and\n * how measures promote across the entity hierarchy (edges). Applies the\n * project's `.world-model.yml` display config server-side.\n *\n * @remarks\n * This returns the raw semantic-model entity graph. For the higher-level\n * node-paradigm interface (`world.metric(id)` speaking `expand` / `explain` /\n * `size`), use {@link useWorldModel} from `./world-node` instead.\n */\nexport function useWorldModelGraph(opts: { enabled?: boolean } = {}): UseWorldModelGraphResult {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const [data, setData] = React.useState<WorldModel | null>(null);\n const [loading, setLoading] = React.useState<boolean>(enabled && !!projectId);\n const [error, setError] = React.useState<Error | null>(null);\n const [nonce, setNonce] = React.useState(0);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: nonce is refetch's re-run trigger, not a value the effect reads\n React.useEffect(() => {\n if (!enabled || !projectId) {\n setLoading(false);\n return;\n }\n const ctrl = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n fetcher(worldModelPath(projectId), { method: \"GET\", signal: ctrl.signal })\n .then(async (resp) => {\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n return (await resp.json()) as WorldModel;\n })\n .then((result) => {\n if (cancelled) return;\n setData(result);\n setLoading(false);\n })\n .catch((err: unknown) => {\n // Only our own teardown is silent, and `cancelled` — set next to\n // `ctrl.abort()` — marks it. Any other abort is a real failure, and\n // matching it by name left the hook loading forever with no error.\n if (cancelled) return;\n setError(asReportableError(err));\n setLoading(false);\n });\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n // `nonce` is what `refetch()` bumps; leaving it out made refetch a no-op.\n }, [enabled, projectId, fetcher, nonce]);\n\n const refetch = React.useCallback(() => setNonce((n) => n + 1), []);\n return { data, loading, error, refetch };\n}\n\n// ── useWorldModelInstances ────────────────────────────────────────────────────\n\nexport interface UseWorldModelInstancesOpts {\n /** Substring/prefix search over the entity's display field. */\n search?: string;\n /** Max rows to return (default 50 server-side). */\n limit?: number;\n enabled?: boolean;\n /**\n * `\"reach\"` — only the instances whose place the viewer reaches, filtered\n * inside the scan so a page is a page of the right set. Needs the entity\n * bound to the org's locations registry (refused otherwise); an instance\n * whose key is unmapped is not in anyone's reach. The bundle's own app is\n * sent along so app-admin standing counts.\n */\n scope?: \"reach\";\n}\n\nexport interface UseWorldModelInstancesResult {\n data: WmInstancesResponse | null;\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\n/**\n * List the instances (rows) of `entityId` — a bounded, searchable picker\n * over the entity's primary keys + display label. Pass `null` for `entityId`\n * to stay idle until an entity is chosen.\n */\nexport function useWorldModelInstances(\n entityId: string | null,\n opts: UseWorldModelInstancesOpts = {}\n): UseWorldModelInstancesResult {\n const { projectId, appId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const { search, limit, scope } = opts;\n const [data, setData] = React.useState<WmInstancesResponse | null>(null);\n const [loading, setLoading] = React.useState<boolean>(enabled && !!projectId && !!entityId);\n const [error, setError] = React.useState<Error | null>(null);\n const [nonce, setNonce] = React.useState(0);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: nonce is refetch's re-run trigger, not a value the effect reads\n React.useEffect(() => {\n if (!enabled || !projectId || !entityId) {\n setLoading(false);\n return;\n }\n const ctrl = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n const params = new URLSearchParams({ entity: entityId });\n if (search) params.set(\"search\", search);\n if (limit != null) params.set(\"limit\", String(limit));\n if (scope) {\n params.set(\"scope\", scope);\n if (appId) params.set(\"app\", appId);\n }\n fetcher(`${worldModelPath(projectId)}/instances?${params}`, {\n method: \"GET\",\n signal: ctrl.signal\n })\n .then(async (resp) => {\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n return (await resp.json()) as WmInstancesResponse;\n })\n .then((result) => {\n if (cancelled) return;\n setData(result);\n setLoading(false);\n })\n .catch((err: unknown) => {\n // Only our own teardown is silent, and `cancelled` — set next to\n // `ctrl.abort()` — marks it. Any other abort is a real failure, and\n // matching it by name left the hook loading forever with no error.\n if (cancelled) return;\n setError(asReportableError(err));\n setLoading(false);\n });\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n // `nonce` is what `refetch()` bumps; leaving it out made refetch a no-op.\n }, [enabled, projectId, entityId, search, limit, fetcher, scope, appId, nonce]);\n\n const refetch = React.useCallback(() => setNonce((n) => n + 1), []);\n return { data, loading, error, refetch };\n}\n\n// ── useMeasureBreakdown (driver tree, SSE) ────────────────────────────────────\n\nexport interface UseMeasureBreakdownResult {\n /** Accumulated breakdown graph; null until the `init` frame. */\n breakdown: WmMeasureBreakdown | null;\n loading: boolean;\n done: boolean;\n error: Error | null;\n}\n\n/** Fold one measure-breakdown SSE frame into the accumulated graph. */\nfunction foldBreakdown(\n prev: WmMeasureBreakdown | null,\n ev: WmMeasureBreakdownEvent\n): WmMeasureBreakdown | null {\n switch (ev.kind) {\n case \"init\":\n return {\n root: ev.root,\n nodes: ev.nodes.map((n) => ({ ...n, value: null, unvalued_reason: null })),\n edges: ev.edges\n };\n case \"value\": {\n if (!prev) return prev;\n return {\n ...prev,\n nodes: prev.nodes.map((n) =>\n n.id === ev.node_id ? { ...n, value: ev.value, unvalued_reason: ev.unvalued_reason } : n\n )\n };\n }\n default:\n return prev;\n }\n}\n\n/**\n * Stream the driver-tree breakdown of one instance's measure — the metric\n * decomposition (add/sub/mul/div component graph) with each node's value\n * filling in as it resolves. This is the per-instance RCA view. Pass `null`\n * for `measure` to stay idle.\n */\nexport function useMeasureBreakdown(\n entityId: string | null,\n keyValue: string | null,\n measure: string | null\n): UseMeasureBreakdownResult {\n const { projectId, fetcher } = useOxyApp();\n const [breakdown, setBreakdown] = React.useState<WmMeasureBreakdown | null>(null);\n const [loading, setLoading] = React.useState<boolean>(false);\n const [done, setDone] = React.useState<boolean>(false);\n const [error, setError] = React.useState<Error | null>(null);\n\n React.useEffect(() => {\n if (!projectId || !entityId || !keyValue || !measure) {\n setLoading(false);\n return;\n }\n const ctrl = new AbortController();\n let cancelled = false;\n setBreakdown(null);\n setLoading(true);\n setDone(false);\n setError(null);\n\n const params = new URLSearchParams({ entity: entityId, key: keyValue, measure });\n fetcher(`${worldModelPath(projectId)}/measure-breakdown?${params}`, {\n method: \"GET\",\n signal: ctrl.signal\n })\n .then(async (resp) => {\n if (!resp.ok) throw await apiErrorFromResponse(resp);\n await readJsonSseStream<WmMeasureBreakdownEvent>(resp, (ev) => {\n if (cancelled) return;\n if (ev.kind === \"done\") {\n setDone(true);\n return;\n }\n setBreakdown((prev) => foldBreakdown(prev, ev));\n });\n if (!cancelled) setLoading(false);\n })\n .catch((err: unknown) => {\n // Only our own teardown is silent, and `cancelled` — set next to\n // `ctrl.abort()` — marks it. Any other abort is a real failure, and\n // matching it by name left the hook loading forever with no error.\n if (cancelled) return;\n setError(asReportableError(err));\n setLoading(false);\n });\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n }, [projectId, entityId, keyValue, measure, fetcher]);\n\n return { breakdown, loading, done, error };\n}\n","// The World Model **node interface** — the higher-level \"node paradigm\" from\n// `docs/build/sdk/world-model.mdx`. Everything in the World Model is a node,\n// and every node speaks the same verbs: `expand` (one hop of relationships),\n// `drill` (narrow to a segment), `explain` (period-over-period root cause),\n// and `size` (peer-gap opportunity). Render a node, let the user pick a verb,\n// get more nodes back, recurse.\n//\n// This is a thin composition layer over the metric-tree analyses that already\n// ship (`metric-tree-hooks.tsx` / the `MetricTreeClient`): the verbs map onto\n// the same `/semantic/metric-tree*` endpoints, so a bundle typed against a\n// handle matches what the server serializes verbatim. It adds no new backend.\n//\n// The logic lives in a framework-agnostic `createWorldModel(projectId,\n// fetcher)` factory; `useWorldModel()` is a thin `useMemo` over it, scoped to\n// the active `<OxyAppProvider>` project.\n//\n// **Alpha.** Per the doc, the node paradigm is a design preview and may\n// change. `drill` is the one verb the backend cannot yet honor — the\n// metric-tree endpoints take no segment/instance filter (the opportunity\n// endpoint explicitly refuses it), and structural verbs are scope-invariant.\n// So `drill` returns a scoped handle for interface fidelity, but the value\n// verbs (`explain`/`size`) on a drilled handle throw\n// {@link WorldModelScopeUnsupportedError} rather than silently returning\n// population numbers for a scoped question.\n\nimport * as React from \"react\";\nimport type {\n ExplainRequest,\n ExplainResult,\n MetricEdge,\n MetricNode,\n MetricTree,\n OpportunityRequest,\n OpportunityResult,\n SensitivityResult\n} from \"../metricTree\";\nimport { getJson, metricTreePath, postJson } from \"./metric-tree-fetch\";\nimport type { AppFetcher } from \"./react\";\nimport { useOxyApp } from \"./react\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\n/** A `dimension → value` scope narrowed onto a node via {@link MetricHandle.drill}. */\nexport type MetricScope = Readonly<Record<string, string>>;\n\n/** Options for {@link MetricHandle.explain} — an {@link ExplainRequest} minus\n * the `target`, which the handle supplies from its own id. */\nexport type ExplainOpts = Omit<ExplainRequest, \"target\">;\n\n/** Options for {@link MetricHandle.size} — an {@link OpportunityRequest} minus\n * the `target`. */\nexport type SizeOpts = Omit<OpportunityRequest, \"target\">;\n\n/** One child revealed by {@link MetricHandle.expand}: the child measure's\n * node, the edge that connects it to the parent, and a handle to recurse. */\nexport interface ExpandedNode {\n /** The child measure (a component or a driver of the parent). */\n node: MetricNode;\n /** The parent → child edge — `kind`, `direction`, `strength`, `form`, … */\n edge: MetricEdge;\n /** A live handle on the child, carrying the parent's scope. */\n handle: MetricHandle;\n}\n\n/**\n * A live handle on one metric node. Carry it around and call a verb; every\n * verb returns either more nodes (`expand`), a scoped handle (`drill`), or an\n * analysis result (`explain` / `size` / `drivers`).\n */\nexport interface MetricHandle {\n /** Fully-qualified measure id (`view.measure`). */\n readonly id: string;\n /** The scope narrowed onto this handle by `drill` (empty for a root handle). */\n readonly scope: MetricScope;\n /** The measure's own tree node (label, expr, is_composite). */\n node(signal?: AbortSignal): Promise<MetricNode>;\n /** One hop of relationships — the metric's components and drivers as child nodes. */\n expand(signal?: AbortSignal): Promise<ExpandedNode[]>;\n /** The declared drivers of this measure, ranked by influence (sensitivity). */\n drivers(signal?: AbortSignal): Promise<SensitivityResult>;\n /** Root-cause a period-over-period move: why it dropped or climbed. */\n explain(opts: ExplainOpts, signal?: AbortSignal): Promise<ExplainResult>;\n /** Compare this node to its peers across each dimension and size the gap. */\n size(opts: SizeOpts, signal?: AbortSignal): Promise<OpportunityResult>;\n /** Narrow into a segment or entity instance — returns a scoped handle. */\n drill(scope: Record<string, string>): MetricHandle;\n}\n\n/**\n * The World Model interface, scoped to one project. The whole surface hangs\n * off this: grab a {@link MetricHandle} with `metric(id)` and the handle\n * speaks the verbs, or pull the whole graph with `tree(root?)`.\n */\nexport interface WorldModelApi {\n /** The active project id, or `null` before `<OxyAppProvider>` resolves one. */\n readonly projectId: string | null;\n /** The metric tree, rooted anywhere you like (default: the whole tree). */\n tree(root?: string, signal?: AbortSignal): Promise<MetricTree>;\n /** A live handle on one measure node. */\n metric(id: string): MetricHandle;\n}\n\n/**\n * Thrown by the value verbs (`explain` / `size`) when called on a handle that\n * has been `drill`ed. The metric-tree backend cannot yet scope these analyses\n * to a segment, so failing loud beats returning population numbers for a\n * question that asked about one segment.\n */\nexport class WorldModelScopeUnsupportedError extends Error {\n readonly code = \"world_model_scope_unsupported\";\n readonly scope: MetricScope;\n constructor(verb: string, scope: MetricScope) {\n super(\n `${verb} on a drilled (scoped) node is not yet supported by the backend ` +\n `(scope: ${JSON.stringify(scope)}). Call ${verb} on the un-drilled node ` +\n `for population-level analysis.`\n );\n this.name = \"WorldModelScopeUnsupportedError\";\n this.scope = scope;\n }\n}\n\n// ── Factory ───────────────────────────────────────────────────────────────────\n\n/**\n * Build a {@link WorldModelApi} over a project id and fetcher. Framework-\n * agnostic — `useWorldModel()` wraps this for React, but it is directly\n * unit-testable with a mock fetcher.\n */\nexport function createWorldModel(projectId: string | null, fetcher: AppFetcher): WorldModelApi {\n const base = (): string => {\n if (!projectId) {\n throw new Error(\n \"World Model unavailable: no active project (are you inside <OxyAppProvider>?)\"\n );\n }\n return metricTreePath(projectId);\n };\n\n const tree = (root?: string, signal?: AbortSignal): Promise<MetricTree> => {\n const qs = root ? `?root=${encodeURIComponent(root)}` : \"\";\n return getJson<MetricTree>(fetcher, `${base()}${qs}`, signal);\n };\n\n const makeHandle = (id: string, scope: MetricScope): MetricHandle => {\n const scoped = Object.keys(scope).length > 0;\n return {\n id,\n scope,\n async node(signal) {\n const t = await tree(id, signal);\n const found = t.nodes.find((n) => n.id === id);\n if (!found) throw new Error(`measure '${id}' not found in the metric tree`);\n return found;\n },\n async expand(signal) {\n const t = await tree(id, signal);\n const byId = new Map(t.nodes.map((n) => [n.id, n] as const));\n // `from` is the parent, `to` the child (component/driver) — the same\n // orientation the IDE metric-tree graph lays out top-down.\n const children: ExpandedNode[] = [];\n for (const edge of t.edges) {\n if (edge.from !== id) continue;\n const childNode = byId.get(edge.to);\n if (!childNode) continue;\n children.push({ node: childNode, edge, handle: makeHandle(edge.to, scope) });\n }\n return children;\n },\n drivers(signal) {\n return getJson<SensitivityResult>(\n fetcher,\n `${base()}/${encodeURIComponent(id)}/sensitivity`,\n signal\n );\n },\n explain(opts, signal) {\n if (scoped) throw new WorldModelScopeUnsupportedError(\"explain\", scope);\n return postJson<ExplainResult>(\n fetcher,\n `${base()}/explain`,\n { target: id, ...opts },\n signal\n );\n },\n size(opts, signal) {\n if (scoped) throw new WorldModelScopeUnsupportedError(\"size\", scope);\n return postJson<OpportunityResult>(\n fetcher,\n `${base()}/opportunity`,\n { target: id, ...opts },\n signal\n );\n },\n drill(next) {\n return makeHandle(id, { ...scope, ...next });\n }\n };\n };\n\n return {\n projectId,\n tree,\n metric: (id: string) => makeHandle(id, {})\n };\n}\n\n// ── Hook ──────────────────────────────────────────────────────────────────────\n\n/**\n * The World Model node interface, scoped to the active `<OxyAppProvider>`\n * project. Returns a stable {@link WorldModelApi} — grab a node with\n * `world.metric(id)` and let it speak the verbs.\n *\n * @example\n * ```tsx\n * const world = useWorldModel();\n * const revenue = world.metric(\"orders.net_revenue\");\n * const children = await revenue.expand(); // components + drivers\n * const rca = await revenue.explain({\n * time_dimension: \"orders.order_date\",\n * current_period: [\"2026-06-01\", \"2026-06-30\"],\n * previous_period: [\"2026-05-01\", \"2026-05-31\"],\n * });\n * ```\n *\n * @remarks\n * This is the node-paradigm hook. For the raw semantic-model entity/measure\n * graph, use {@link useWorldModelGraph} instead.\n */\nexport function useWorldModel(): WorldModelApi {\n const { projectId, fetcher } = useOxyApp();\n return React.useMemo(() => createWorldModel(projectId ?? null, fetcher), [projectId, fetcher]);\n}\n","// Metric-tree types + client. Mirrors `airlayer::engine::metric_tree*`\n// over the `/<project_id>/semantic/metric-tree*` HTTP endpoints. Serde\n// emits snake_case so these field names match the wire format verbatim.\n\nimport type { OxyConfig } from \"./config\";\nimport type { BenchmarkStatistic } from \"./peerCohort\";\n\n// ── Tree ──────────────────────────────────────────────────────────────────────\n\nexport type EdgeKind = \"component\" | \"driver\";\nexport type DriverDirection = \"positive\" | \"negative\" | \"unknown\";\nexport type DriverStrength = \"strong\" | \"moderate\" | \"weak\";\nexport type DriverConfidence = \"high\" | \"medium\" | \"low\";\n/** The shape of a driver relationship.\n *\n * The THIRD hand-maintained mirror of this enum (airlayer's is canonical,\n * `web-app/src/types/metricTree.ts` is the second). Nothing enforces that they\n * agree, and the last time one fell behind — `oxy-semantic`, five variants\n * short — a valid `.view.yml` stopped parsing. A type-only union fails more\n * softly: an SDK consumer reading a tree with a quadratic edge just gets a\n * union that cannot hold it. Add new shapes here whenever airlayer grows one. */\nexport type DriverForm =\n | \"linear\"\n | \"log-log\"\n | \"log-linear\"\n | \"linear-log\"\n | \"quadratic\"\n | \"cubic\"\n | \"sqrt\"\n | \"inverse\"\n | \"linear-log-quadratic\";\n\nexport interface MetricNode {\n id: string;\n view: string;\n measure: string;\n label: string;\n description?: string | null;\n measure_type: string;\n is_composite: boolean;\n /** Whether this measure can be drilled into. Serialized rather than\n * re-derived: `measure_type` misses eligible composites, and edge presence\n * over-admits nested / cross-view / multiplicative passthroughs the engine\n * refuses. Non-optional — it is on every metric-tree response. */\n drillable: boolean;\n expr?: string | null;\n}\n\nexport interface MetricEdge {\n from: string;\n to: string;\n kind: EdgeKind;\n /** Sign of a component edge; omitted (defaults to +1) for most edges. */\n sign?: number;\n /** Arithmetic operator joining a component child to its parent. Omitted\n * when it is `add` — airlayer skips the field at its default — so absent\n * MEANS `add`, never \"unknown\". Only `mul` / `div` are multiplicative;\n * `add` / `sub` propagate exactly. */\n operator?: \"add\" | \"sub\" | \"mul\" | \"div\";\n direction: DriverDirection;\n strength: DriverStrength;\n confidence: DriverConfidence;\n coefficient?: number | null;\n form: DriverForm;\n /** Whether `form` was declared in the YAML or inferred by the fit. */\n form_declared?: boolean;\n intercept?: number | null;\n lag?: number | null;\n description?: string | null;\n refs?: string[] | null;\n}\n\nexport interface MetricTree {\n nodes: MetricNode[];\n edges: MetricEdge[];\n root?: string | null;\n /** Refusals raised while building the tree — a driver declaring both\n * `coefficient:` and `coefficients:`, or a wrong-width vector. Absent when\n * empty, so a lever that moves nothing still has a way to say why. */\n warnings?: string[];\n}\n\n// ── Sensitivity ──────────────────────────────────────────────────────────────\n\nexport interface SensitivityDriver {\n measure: string;\n path: string[];\n edge_kind: string;\n effective_coefficient?: number | null;\n form?: DriverForm | null;\n direction: DriverDirection;\n strength: DriverStrength;\n lag?: number | null;\n description?: string | null;\n}\n\nexport interface SensitivityResult {\n target: string;\n drivers: SensitivityDriver[];\n}\n\n// ── Predict ──────────────────────────────────────────────────────────────────\n\nexport interface PredictChange {\n measure: string;\n delta: number;\n}\n\nexport interface PredictImpact {\n measure: string;\n estimated_delta: number;\n confidence: string;\n path: string[];\n form: DriverForm;\n lag?: number | null;\n}\n\nexport interface PredictResult {\n inputs: PredictChange[];\n impacts: PredictImpact[];\n}\n\n/** node_id → the measure's value over the baseline window. */\nexport type MeasureValues = Record<string, number>;\n\n/** A driver edge's coefficient, measured from history by the baseline query.\n *\n * Either `coefficient` is set or `refusal` is — never both, never neither.\n * A refusal is a result: it is why a measure downstream of the change shows\n * no number. Echo the whole array into `predict` verbatim, refusals included;\n * the server ignores entries carrying no coefficient, and filtering them here\n * would be a second place for the two sides to disagree. */\nexport interface FittedDriver {\n from: string;\n to: string;\n lag?: number;\n /** The form the slope was measured in. The same number reads as dollars per\n * dollar under `linear` and as a percent-per-percent elasticity under\n * `log-log`, so a bare figure has no unit. */\n form?: DriverForm;\n /** Paired observations behind the fit.\n *\n * `| null` because this mirrors an `Option<f64>` on a GIT-PINNED struct:\n * `skip_serializing_if` is a serde attribute today, not a guarantee, so a\n * reader must accept both encodings. Compare with `!= null`, never\n * `!== undefined` — and the type has to admit both, or the safe read looks\n * like dead code. Same rule on `t_stat`, `t_stats`, `se_terms` and\n * `coefficient` below. */\n n?: number | null;\n n_panels?: number;\n n_nonpositive?: number;\n /** The FIRST basis term — the whole answer for a single-term form; for a\n * shape that can turn it is only the slope, so read `coefficients`.\n * `| null` because it is an `Option<f64>` on the wire. */\n coefficient?: number | null;\n /** One coefficient per basis term, in basis order. This is what propagation\n * evaluates. */\n coefficients?: number[];\n se?: number;\n /** Elements `| null` for the reason stated on `n`. */\n se_terms?: (number | null)[];\n /** `| null` for the reason stated on `n`. */\n t_stat?: number | null;\n /** `t` per basis term, in basis order — `[1]` is the second basis term, the\n * squared one under every shape that can turn. Elements `| null` for the\n * reason stated on `n`. */\n t_stats?: (number | null)[];\n /** Sufficient statistics of the basis over the rows the fit used. Not\n * diagnostic: the fit is per row and a change is a window aggregate, and a\n * curved response cannot cross that gap without these. Echo verbatim. */\n moments?: { n?: number; s1?: number; s2?: number };\n /** `[min, max]` driver values observed. A change beyond this spread is\n * refused rather than extrapolated. */\n domain?: [number, number];\n /** The response sampled as `[change fraction, delta]`. Read this instead of\n * interpreting the coefficients — peak, break-even and saturation are all\n * properties of these samples, so a reader written against them keeps\n * working when a new shape is added. */\n profile?: [number, number][];\n form_source?: \"declared\" | \"inferred\";\n /** Every shape considered, scored comparably (AIC in y-space, lower better).\n * Empty when the form was declared. `all_terms_significant` false means the\n * candidate was never eligible, however good its score. */\n candidates?: { form: DriverForm; aic: number; all_terms_significant: boolean }[];\n refusal?: string;\n}\n\n/** Why a reachable node has no baseline value. */\nexport interface UnvaluedNode {\n id: string;\n reason?: string | null;\n}\n\n/** Narrow the baseline to one world-model instance. Omit to value the whole\n * population. */\nexport interface BaselineInstance {\n entity: string;\n /** JSON array for a composite key, else a bare scalar. */\n key: string;\n}\n\nexport interface BaselineRequest {\n /** The nodes you intend to change. Values are fetched for these plus\n * everything forward-reachable from them — not the whole tree. */\n roots: string[];\n time_dimension: string;\n /** `[start, end]` inclusive date strings. */\n period: [string, string];\n instance?: BaselineInstance | null;\n}\n\nexport interface BaselineResponse {\n values: MeasureValues;\n unvalued: UnvaluedNode[];\n resolved_period: [string, string];\n /** Why the baseline produced no values, in words worth showing. Absent when\n * measures were valued normally. */\n baseline_note?: string | null;\n /** Coefficients fitted for driver edges that declare none, plus refusals.\n * Absent when every reachable driver edge already declares one. */\n fitted?: FittedDriver[];\n}\n\nexport interface PredictOptions {\n /** Current values for the measures involved. Supplying them lets\n * multiplicative edges be sized instead of returned `unquantifiable`. */\n values?: MeasureValues;\n /** The baseline's `fitted` array, verbatim. */\n coefficients?: FittedDriver[];\n}\n\n// ── Projection (scenario forecasting over time) ──────────────────────────────\n\nexport type ProjectionGranularity = \"day\" | \"week\" | \"month\";\n\n/**\n * The scenario's time axis. `baseline` answers \"what is this measure worth\n * over the window\"; this answers \"what has it been doing, and what does it do\n * next\".\n *\n * The history window is deliberately its own, NOT the baseline's: the\n * forecaster refuses anything under eight seasonal cycles (56 daily buckets,\n * 32 weekly, 24 monthly), and a 30-day scenario baseline reused here would\n * make \"no forecast\" the normal answer.\n */\nexport interface ProjectionRequest {\n /** Lever node ids. Curves are drawn for these plus everything\n * forward-reachable from them — the same set {@link BaselineRequest} values. */\n roots: string[];\n time_dimension: string;\n /** `[start, end]` inclusive date strings for the HISTORY. */\n period: [string, string];\n /** Narrow to one world-model instance. Omit to project the whole\n * population — same picker the baseline uses. */\n instance?: BaselineInstance | null;\n /** Bucket width. Defaults to `day` server-side. */\n granularity?: ProjectionGranularity;\n /** Buckets to project past the last historical one. 1..=365; outside that\n * it is a 400, never a silent clamp — a horizon quietly truncated reads as\n * a forecast that genuinely ends in March. */\n horizon: number;\n /** Seasonal periods, in buckets, applied to every measure in the request.\n *\n * Omitting it is not \"use the default\" — it means *resolve per measure*\n * from whatever `.monitor.yml` already watches that series, which is what\n * keeps this band the band an anomaly had to breach. Send it only to pin a\n * cycle nobody has declared. Each period must be >= 2; `[]` is a 400. */\n seasonality?: number[];\n}\n\nexport interface HistoryPoint {\n /** Bucket start, `YYYY-MM-DD`. */\n date: string;\n value: number;\n}\n\nexport interface ForecastPoint {\n date: string;\n point: number;\n /** The prediction interval. `null` / absent means the model returned no\n * band — unknown spread, NOT a band of zero width. Never collapse these\n * onto `point`: a zero-width band is a claim of certainty nobody made. */\n lower?: number | null;\n upper?: number | null;\n}\n\n/** One measure's baseline curve: what happened, then what comes next.\n *\n * An empty `forecast` carrying a `refusal` is a state, not a gap — most often\n * \"too little history to fit\", or the warehouse refusing this one measure. It\n * must never render as a flat forward line, which is what any code defaulting\n * the missing curve to \"unchanged\" would draw. */\nexport interface MeasureProjection {\n measure: string;\n history: HistoryPoint[];\n forecast: ForecastPoint[];\n refusal?: string | null;\n /** The seasonal periods this curve was decomposed against — resolved per\n * measure, so two series in one response can legitimately differ. */\n seasonality: number[];\n}\n\nexport interface ProjectionResponse {\n granularity: ProjectionGranularity;\n /** Echoed back: the query is expensive and callers cache on it. */\n resolved_period: [string, string];\n horizon: number;\n series: MeasureProjection[];\n /** Why the WHOLE projection is empty, when it is. Absent when at least one\n * measure produced history — a partial failure is each measure's own\n * `refusal`, never a banner over curves that are drawing fine. */\n projection_note?: string | null;\n}\n\n// ── Explain (RCA) ────────────────────────────────────────────────────────────\n\nexport type SplitKind =\n | { type: \"component\"; child_measure: string }\n | { type: \"dimension\"; dimension: string; value: string }\n | { type: \"uniform_degradation\"; dimension: string; num_elements: number }\n | { type: \"cross_cutting\"; dimension: string; value: string; measures: string[] };\n\nexport interface ExplainSibling {\n split: SplitKind;\n measure: string;\n delta: number;\n root_fraction: number;\n}\n\nexport interface ExplainNode {\n split: SplitKind;\n measure: string;\n filters: unknown[];\n delta: number;\n concentration: number;\n root_fraction: number;\n siblings?: ExplainSibling[];\n dimension_count?: number;\n children?: ExplainNode[];\n}\n\n/** Whether a driver's observed move pushes the target the way it actually\n * moved (`contributing`) or against it (`counteracting` — it offset part of\n * the move rather than causing it). `unknown` when no signed claim is\n * available: `direction: unknown` with no coefficient, or a flat\n * driver/target. */\nexport type DriverContribution = \"contributing\" | \"counteracting\" | \"unknown\";\n\n/** A driver's move split into the part its base forced and the part its own\n * ratio contributed. Emitted only when the driver genuinely tracks a sibling\n * rather than moving on its own — presence is the claim.\n * `base_driven_delta + ratio_driven_delta === driver_delta`. */\nexport interface PassthroughSplit {\n base_measure: string;\n ratio_previous: number;\n ratio_current: number;\n base_driven_delta: number;\n ratio_driven_delta: number;\n}\n\nexport interface DriverAttribution {\n driver_measure: string;\n driver_previous: number;\n driver_current: number;\n driver_delta: number;\n /** Both optional: an `explain_cache` row written before these fields shipped\n * is served verbatim, so absent means unclassified — not a default. */\n direction?: DriverDirection;\n contribution?: DriverContribution;\n coefficient?: number;\n form: DriverForm;\n /** Absent for a purely qualitative driver (no coefficient). */\n estimated_target_impact?: number;\n description?: string;\n passthrough?: PassthroughSplit;\n}\n\nexport type ExplainWarning =\n | {\n type: \"simpsons_paradox\";\n dimension: string;\n aggregate_delta: number;\n segment_directions: [string, number][];\n }\n | {\n type: \"opposing_offset\";\n component_a: string;\n component_b: string;\n delta_a: number;\n delta_b: number;\n }\n | {\n type: \"non_additive_dimension_split\";\n measure: string;\n measure_type: string;\n dimension: string;\n };\n\nexport interface ExplainConfigOverride {\n deep?: boolean;\n max_depth?: number;\n coverage_threshold?: number;\n}\n\nexport interface ExplainRequest {\n target: string;\n time_dimension: string;\n current_period: [string, string];\n previous_period: [string, string];\n config?: ExplainConfigOverride;\n}\n\nexport interface ExplainResult {\n target: string;\n target_delta: number;\n target_previous: number;\n target_current: number;\n time_dimension: string;\n current_period: [string, string];\n previous_period: [string, string];\n nodes: ExplainNode[];\n coverage: number;\n driver_attribution?: DriverAttribution[];\n alternatives?: unknown[];\n warnings?: ExplainWarning[];\n}\n\n// ── Opportunity ──────────────────────────────────────────────────────────────\n\nexport interface SegmentOpportunity {\n segment: string;\n current_value: number;\n volume: number;\n benchmark: number;\n gap: number;\n /** Match-the-best upside in measure units. */\n upside: number;\n}\n\nexport interface DimensionOpportunity {\n dimension: string;\n cardinality: number;\n /** \"best_peer\" or \"p75\". */\n benchmark_basis: string;\n total_upside: number;\n segments: SegmentOpportunity[];\n other_segments_skipped: number;\n}\n\nexport interface SkippedDimension {\n dimension: string;\n reason: string;\n}\n\nexport interface OpportunityRequest {\n target: string;\n time_dimension: string;\n period: [string, string];\n /**\n * How a segment's benchmark is computed from its peers. Defaults to\n * `\"p75\"` server-side when omitted.\n *\n * An airlayer upgrade deleted the engine's adaptive selection — best-peer\n * for a dimension with too few segments for a percentile to mean anything,\n * p75 once there were enough — in favor of an explicit required argument.\n * No single fixed value reproduced the old behavior, so rather than\n * freezing one server-side, the choice is now the caller's.\n */\n statistic?: BenchmarkStatistic;\n}\n\nexport interface OpportunityResult {\n target: string;\n period: [string, string];\n overall_value: number;\n /**\n * \"rows\" (rate-based additive sizing — the only basis that yields a sized\n * upside figure), \"value_share\" (additive) or \"equal\" (ratios).\n */\n weight_basis: string;\n dimensions: DimensionOpportunity[];\n skipped_dimensions: SkippedDimension[];\n downstream: PredictImpact[];\n}\n\n// ── Distribution ─────────────────────────────────────────────────────────────\n\n/**\n * Single-period structural decomposition. The server auto-derives the\n * baseline as the equal-length window immediately before `period`, then\n * returns an {@link ExplainResult}-shaped payload (so the same renderers\n * work). Ignore the delta fields when rendering a pure distribution.\n */\nexport interface DistributionRequest {\n target: string;\n time_dimension: string;\n /** `[start, end]` inclusive date strings. */\n period: [string, string];\n}\n\n// ── Time dimensions ──────────────────────────────────────────────────────────\n\nexport interface TimeDimensionsResponse {\n /** view name → fully-qualified time-dimension ids (`view.dim`). */\n by_view: Record<string, string[]>;\n}\n\n// ── Client ───────────────────────────────────────────────────────────────────\n\n/**\n * Shape of the inner request helper exposed by `OxyClient`. The metric-tree\n * client reuses it to inherit auth headers, timeout, baseUrl, and project\n * scoping rather than reimplementing fetch end-to-end.\n */\nexport type RequestFn = <T>(endpoint: string, options?: RequestInit) => Promise<T>;\n\n/**\n * Client for the `/semantic/metric-tree*` endpoints. Surfaces the airlayer\n * metric-tree analyses — tree introspection, sensitivity, explain, opportunity\n * — plus the three legs of scenario forecasting (`baseline` levels,\n * `predict` propagation, `projection` curves) over typed methods.\n *\n * Construction is internal to {@link OxyClient} — call `client.metricTree`\n * to access an instance rather than building one yourself.\n *\n * @example\n * ```typescript\n * const client = await OxyClient.create({ projectId: \"...\", apiKey: \"...\" });\n * const tree = await client.metricTree.getTree();\n * const drivers = await client.metricTree.getSensitivity(\"orders.net_revenue\");\n * ```\n */\nexport class MetricTreeClient {\n private readonly request: RequestFn;\n private readonly config: OxyConfig;\n\n constructor(config: OxyConfig, request: RequestFn) {\n this.config = config;\n this.request = request;\n }\n\n private path(suffix: string): string {\n return `/${this.config.projectId}/semantic/metric-tree${suffix}`;\n }\n\n private buildQuery(extra: Record<string, string> = {}): string {\n const params: Record<string, string> = { ...extra };\n if (this.config.branch) params.branch = this.config.branch;\n const qs = new URLSearchParams(params).toString();\n return qs ? `?${qs}` : \"\";\n }\n\n /**\n * Fetch the full metric tree, or the subtree rooted at `root`.\n *\n * @param root - Optional fully-qualified measure id to root the tree at.\n * @returns Nodes (measures) and edges (component / driver relationships).\n *\n * @example\n * ```typescript\n * const tree = await client.metricTree.getTree();\n * const subtree = await client.metricTree.getTree(\"orders.net_revenue\");\n * ```\n */\n async getTree(root?: string): Promise<MetricTree> {\n const query = this.buildQuery(root ? { root } : {});\n return this.request<MetricTree>(this.path(query));\n }\n\n /**\n * Rank the declared drivers of a measure by influence.\n *\n * @param measureId - Fully-qualified measure id (`view.measure`).\n *\n * @example\n * ```typescript\n * const sensitivity = await client.metricTree.getSensitivity(\"orders.net_revenue\");\n * for (const driver of sensitivity.drivers) {\n * console.log(driver.measure, driver.direction, driver.strength);\n * }\n * ```\n */\n async getSensitivity(measureId: string): Promise<SensitivityResult> {\n const query = this.buildQuery();\n return this.request<SensitivityResult>(\n this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`)\n );\n }\n\n /**\n * Value a change's starting point, and measure the coefficients it needs.\n *\n * Two warehouse reads: the current value of every node reachable from\n * `roots`, and — for driver edges that declare no `coefficient:` — a fit\n * over the window. Both are expensive, which is why they live here and not\n * in `predict`: `predict` is database-free by design so it can re-run per\n * keystroke, and it CANNOT measure a coefficient itself.\n *\n * That is the whole reason to call this. Pass `fitted` back into `predict`\n * and an undeclared edge propagates; omit it and `predict` has nothing to\n * multiply by, so the impact is simply absent — no error, no refusal, just a\n * downstream measure that never appears.\n *\n * @example\n * ```typescript\n * const baseline = await client.metricTree.getBaseline({\n * roots: [\"marketing_spend.total_spend\"],\n * time_dimension: \"orders.order_date\",\n * period: [\"2025-09-01\", \"2025-09-30\"],\n * });\n * const result = await client.metricTree.predict(\n * [{ measure: \"marketing_spend.total_spend\", delta: 10000 }],\n * { values: baseline.values, coefficients: baseline.fitted }\n * );\n * ```\n */\n async getBaseline(request: BaselineRequest): Promise<BaselineResponse> {\n const query = this.buildQuery();\n return this.request<BaselineResponse>(this.path(`/baseline${query}`), {\n method: \"POST\",\n body: JSON.stringify(request)\n });\n }\n\n /**\n * Propagate hypothetical `(measure, delta)` changes upward through the\n * tree. Returns the estimated impact on every downstream measure.\n *\n * Database-free, so it re-runs cheaply — and so it can only use\n * coefficients it is GIVEN. Without `options.coefficients` from\n * {@link getBaseline}, every edge whose `.view.yml` declares no\n * `coefficient:` contributes nothing and its downstream measures are\n * silently missing from `impacts`. Without `options.values`, multiplicative\n * component edges come back `unquantifiable` rather than sized.\n *\n * @example\n * ```typescript\n * const result = await client.metricTree.predict([\n * { measure: \"marketing_spend.total_spend\", delta: 10000 },\n * ]);\n * ```\n */\n async predict(changes: PredictChange[], options: PredictOptions = {}): Promise<PredictResult> {\n const query = this.buildQuery();\n return this.request<PredictResult>(this.path(`/predict${query}`), {\n method: \"POST\",\n body: JSON.stringify({\n changes,\n ...(options.values ? { values: options.values } : {}),\n // Sent verbatim, refusals included — the server ignores entries\n // carrying no coefficient, and filtering them here would just be a\n // second place for the two sides to disagree.\n ...(options.coefficients?.length ? { coefficients: options.coefficients } : {})\n })\n });\n }\n\n /**\n * Draw the scenario's time axis: bucketed history for the levers and\n * everything downstream, plus the forward curve the detector's own model\n * expects next.\n *\n * The third leg of scenario forecasting. {@link getBaseline} gives levels\n * and coefficients, {@link predict} propagates a change with no database at\n * all, and this gives time — one warehouse query, so treat it like the\n * baseline: fetch on a window change, not on a lever edit.\n *\n * **Returns the BASELINE curve only.** The scenario's second curve is\n * arithmetic over this and a `predict` result — a proportional shift landing\n * `lag` buckets in — and is composed client-side deliberately, so editing a\n * lever costs no query.\n *\n * @example\n * ```typescript\n * const projection = await client.metricTree.getProjection({\n * roots: [\"marketing_spend.total_spend\"],\n * time_dimension: \"orders.order_date\",\n * period: [\"2024-09-01\", \"2025-08-31\"],\n * granularity: \"day\",\n * horizon: 30,\n * });\n * for (const series of projection.series) {\n * if (series.refusal) console.warn(series.measure, series.refusal);\n * }\n * ```\n */\n async getProjection(request: ProjectionRequest): Promise<ProjectionResponse> {\n const query = this.buildQuery();\n return this.request<ProjectionResponse>(this.path(`/projection${query}`), {\n method: \"POST\",\n body: JSON.stringify(request)\n });\n }\n\n /**\n * Period-over-period root-cause decomposition. Recursively splits the\n * target measure by components and dimensions until the move concentrates.\n *\n * @example\n * ```typescript\n * const result = await client.metricTree.explain({\n * target: \"financials.operating_profit\",\n * time_dimension: \"financials.month\",\n * current_period: [\"2025-09-01\", \"2025-09-30\"],\n * previous_period: [\"2025-08-01\", \"2025-08-31\"],\n * });\n * ```\n */\n async explain(request: ExplainRequest): Promise<ExplainResult> {\n const query = this.buildQuery();\n return this.request<ExplainResult>(this.path(`/explain${query}`), {\n method: \"POST\",\n body: JSON.stringify(request)\n });\n }\n\n /**\n * Size the upside opportunity for a measure by finding underperforming\n * segments. Skips high-cardinality dimensions and trims the long tail.\n *\n * @example\n * ```typescript\n * const result = await client.metricTree.findOpportunities({\n * target: \"orders.net_revenue\",\n * time_dimension: \"orders.order_date\",\n * period: [\"2025-09-01\", \"2025-09-30\"],\n * });\n * for (const dim of result.dimensions) {\n * console.log(dim.dimension, \"+\", dim.total_upside);\n * }\n * ```\n */\n async findOpportunities(request: OpportunityRequest): Promise<OpportunityResult> {\n const query = this.buildQuery();\n return this.request<OpportunityResult>(this.path(`/opportunity${query}`), {\n method: \"POST\",\n body: JSON.stringify(request)\n });\n }\n}\n","// Peer-cohort types + client. Mirrors `airlayer::engine::cohort::PeerCohortResult`\n// over the `/<project_id>/semantic/cohort` HTTP endpoint. Serde emits snake_case\n// so these field names match the wire format verbatim.\n\nimport type { OxyConfig } from \"./config\";\n\n// ── Cohort ───────────────────────────────────────────────────────────────────\n\n/** How a subject's benchmark is computed from its peers. Serde snake_case:\n * `\"median\"` | `\"p75\"` | `\"best_peer\"`. */\nexport type BenchmarkStatistic = \"median\" | \"p75\" | \"best_peer\";\n\n/**\n * Why a subject was left out of the comparison entirely — distinct from\n * `sufficient: false` on a {@link CohortSubject}, which still gets a computed\n * comparison. This is the fix for a reference implementation where an excluded\n * store \"simply did not appear in the list and no screen said why\": every\n * exclusion here carries a reason, and a consumer MUST render these rather\n * than silently drop them.\n */\nexport interface ExcludedSubject {\n /** For a NULL entity key, a synthesized stable id of the form\n * `\"(null) [<require values>]\"` rather than an empty string. */\n key: string;\n /** Human-readable prose explaining the exclusion — NOT a coded enum, so\n * render it verbatim rather than switching on it. */\n reason: string;\n}\n\n/**\n * One subject's comparison against its peer cohort.\n *\n * `sufficient` reflects whether `peer_count` met the cohort's declared\n * `min_peers`, but `min_peers` is a REPORTING predicate, never a filter: a\n * subject below the threshold is still returned with `baseline` and `gap`\n * computed from whatever peers it has. Never filter on `sufficient`\n * client-side — surface it so the caller can decide whether to act on a\n * comparison built from a thin peer set.\n */\nexport interface CohortSubject {\n key: string;\n value: number;\n /** The peer benchmark value. `0.0` when `peer_count === 0` — this is NOT a\n * baseline of zero, there simply is none. Check `peer_count` before\n * treating `baseline` as meaningful. */\n baseline: number;\n /** Oriented so a positive value always means opportunity, regardless of\n * whether the underlying measure is \"bigger is better\" or the reverse.\n * `0.0` when there are no peers to compare against. */\n gap: number;\n /** Non-reciprocal: appearing in another subject's `peers` does not imply\n * this subject lists them back. */\n peers: string[];\n peer_count: number;\n /** `peer_count >= cohort's min_peers`. A reporting flag, not a gate — see\n * the type-level doc above. */\n sufficient: boolean;\n}\n\n/**\n * Result of resolving one subject's peer cohort and every peer's comparison\n * against it.\n */\nexport interface PeerCohortResult {\n entity: string;\n cohort: string;\n measure: string;\n statistic: BenchmarkStatistic;\n /** `[start, end]` inclusive date strings for the measured period. */\n period: [string, string];\n /**\n * The window peers were drawn from, when the cohort declares a peer band.\n * `Some(period)` when the cohort declares a band with no explicit\n * `window:` of its own; absent only when the cohort declares no band at\n * all. Anchored at `period`'s START and extended backward, so it always\n * CONTAINS `period` — never a disjoint comparison window.\n *\n * `| null` because this mirrors an `Option<(String, String)>` on a\n * GIT-PINNED struct: `skip_serializing_if` is a serde attribute today, not\n * a guarantee, so a reader must accept both encodings. Compare with\n * `!= null`, never `!== undefined`.\n */\n band_window?: [string, string] | null;\n subjects: CohortSubject[];\n excluded: ExcludedSubject[];\n}\n\n/**\n * Request to resolve a peer cohort. `cohort` and `band_window` are echoed\n * back on {@link PeerCohortResult} specifically so a UI rendering them cannot\n * drift from the query that produced them.\n */\nexport interface CohortRequest {\n /**\n * The bare name of an `entities:` entry in the semantic model — e.g.\n * `\"restaurant_id\"` — NOT a column name and NOT a qualified `view.entity`\n * (`\"stores.store\"`). The server compares this verbatim against the\n * entity half of a measure's `default_cohort: \"entity.cohort_name\"` and\n * against `Entity::name` when locating the bound view; a qualified name\n * fails both checks and a scoped caller gets `403 cohort_scope_unavailable`.\n */\n entity: string;\n measure: string;\n time_dimension: string;\n /** `[start, end]` inclusive date strings. */\n period: [string, string];\n /** Falls back to the measure's `default_cohort:` when omitted. */\n cohort?: string;\n /** Defaults to `\"median\"` server-side when omitted. */\n statistic?: string;\n}\n\n// ── Client ───────────────────────────────────────────────────────────────────\n\n/**\n * Shape of the inner request helper exposed by `OxyClient`. The peer-cohort\n * client reuses it to inherit auth headers, timeout, baseUrl, and project\n * scoping rather than reimplementing fetch end-to-end.\n */\nexport type RequestFn = <T>(endpoint: string, options?: RequestInit) => Promise<T>;\n\n/**\n * Client for the `/semantic/cohort` endpoint. Surfaces airlayer's peer-cohort\n * benchmarking — comparing an entity's subjects against their declared peers\n * on a measure, with exclusions explained rather than silently dropped.\n *\n * Construction is internal to {@link OxyClient} — call `client.peerCohort`\n * to access an instance rather than building one yourself.\n *\n * @example\n * ```typescript\n * const client = await OxyClient.create({ projectId: \"...\", apiKey: \"...\" });\n * const result = await client.peerCohort.resolve({\n * entity: \"restaurant_id\",\n * measure: \"orders.net_revenue\",\n * time_dimension: \"orders.order_date\",\n * period: [\"2025-09-01\", \"2025-09-30\"],\n * });\n * for (const excluded of result.excluded) {\n * console.warn(excluded.key, excluded.reason);\n * }\n * ```\n */\nexport class PeerCohortClient {\n private readonly request: RequestFn;\n private readonly config: OxyConfig;\n\n constructor(config: OxyConfig, request: RequestFn) {\n this.config = config;\n this.request = request;\n }\n\n private path(suffix: string): string {\n return `/${this.config.projectId}${suffix}`;\n }\n\n private buildQuery(extra: Record<string, string> = {}): string {\n const params: Record<string, string> = { ...extra };\n if (this.config.branch) params.branch = this.config.branch;\n const qs = new URLSearchParams(params).toString();\n return qs ? `?${qs}` : \"\";\n }\n\n /**\n * Resolve a subject's peer cohort and every peer's comparison against it.\n *\n * @example\n * ```typescript\n * const result = await client.peerCohort.resolve({\n * entity: \"restaurant_id\",\n * measure: \"orders.net_revenue\",\n * time_dimension: \"orders.order_date\",\n * period: [\"2025-09-01\", \"2025-09-30\"],\n * });\n * ```\n */\n async resolve(request: CohortRequest): Promise<PeerCohortResult> {\n const query = this.buildQuery();\n return this.request<PeerCohortResult>(this.path(`/semantic/cohort${query}`), {\n method: \"POST\",\n body: JSON.stringify(request)\n });\n }\n}\n"],"mappings":"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"}
package/dist/ops.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { _ as OxyReach } from "./function-context-D8eyZuw_.cjs";
2
+ import { b as OxyReach } from "./function-context-BNpL5bFb.cjs";
3
3
  //#region src/ops/index.d.ts
4
4
  /** What `ctx.user.reach` carries — one shape, declared once on the context type. */
5
5
  type Reach = OxyReach;
package/dist/ops.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { _ as OxyReach } from "./function-context-D8eyZuw_.mjs";
2
+ import { b as OxyReach } from "./function-context-BNpL5bFb.mjs";
3
3
  //#region src/ops/index.d.ts
4
4
  /** What `ctx.user.reach` carries — one shape, declared once on the context type. */
5
5
  type Reach = OxyReach;