@oxy-hq/sdk 2.5.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -5
- package/dist/index.cjs +677 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +708 -16
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +708 -16
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +657 -10
- package/dist/index.mjs.map +1 -1
- package/dist/{react-BeweGSYF.cjs → react-BFFCK4VM.cjs} +34 -29
- package/dist/react-BFFCK4VM.cjs.map +1 -0
- package/dist/{react-BnpR8VRJ.d.mts → react-kHG5gkd-.d.cts} +22 -17
- package/dist/react-kHG5gkd-.d.cts.map +1 -0
- package/dist/{react-BnpR8VRJ.d.cts → react-kHG5gkd-.d.mts} +22 -17
- package/dist/react-kHG5gkd-.d.mts.map +1 -0
- package/dist/{react-CVdWXcu8.mjs → react-sT7E4CbA.mjs} +29 -24
- package/dist/react-sT7E4CbA.mjs.map +1 -0
- package/dist/shell.cjs +6 -5
- package/dist/shell.cjs.map +1 -1
- package/dist/shell.css +28 -6
- package/dist/shell.d.cts +3 -3
- package/dist/shell.d.cts.map +1 -1
- package/dist/shell.d.mts +3 -3
- package/dist/shell.d.mts.map +1 -1
- package/dist/shell.mjs +6 -5
- package/dist/shell.mjs.map +1 -1
- package/package.json +10 -10
- package/dist/react-BeweGSYF.cjs.map +0 -1
- package/dist/react-BnpR8VRJ.d.cts.map +0 -1
- package/dist/react-BnpR8VRJ.d.mts.map +0 -1
- package/dist/react-CVdWXcu8.mjs.map +0 -1
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/anomalies.ts","../src/customer-app/debug.ts","../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/**\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 /** 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 /** Max rows (server caps at 500, defaults to 100). */\n limit?: number;\n}\n\nexport interface ListAnomaliesResponse {\n anomalies: Anomaly[];\n}\n\nexport interface ScanOptions {\n /** Override the reference \"now\" date (YYYY-MM-DD) — useful for demos. */\n as_of?: string;\n}\n\nexport interface ScanResponse {\n monitors_scanned: number;\n monitors_failed: number;\n anomalies_persisted: number;\n}\n\n// ── Client ───────────────────────────────────────────────────────────────────\n\nexport type RequestFn = <T>(endpoint: string, options?: RequestInit) => Promise<T>;\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, newest first.\n *\n * @example\n * ```typescript\n * // Open / unresolved anomalies only\n * const { anomalies } = await client.anomalies.list({ status: \"new\" });\n * ```\n */\n async list(options: ListAnomaliesOptions = {}): Promise<ListAnomaliesResponse> {\n const extra: Record<string, string> = {};\n if (options.status) extra.status = options.status;\n if (options.limit) extra.limit = String(options.limit);\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 * @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 * console.log(`${result.anomalies_persisted} anomalies detected`);\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 * Run the metric-tree `explain` for an anomaly and cache the result on\n * the row. Subsequent calls return the cached `ExplainResult` instantly.\n */\n async explain(anomalyId: string): Promise<ExplainResult> {\n const query = this.buildQuery();\n return this.request<ExplainResult>(\n this.path(`/${encodeURIComponent(anomalyId)}/explain${query}`),\n { method: \"POST\" }\n );\n }\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 { ResolvedCustomerAppManifest } 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 CustomerAppDebugSnapshot {\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 * `loadCustomerAppManifest()` — 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 getCustomerAppDebug(\n resolved: ResolvedCustomerAppManifest\n): Promise<CustomerAppDebugSnapshot> {\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 CustomerAppDebugSnapshot;\n log.log(\"info\", \"debug snapshot\", snapshot as unknown as Record<string, unknown>);\n return snapshot;\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\";\nexport type DriverForm = \"linear\" | \"log-log\" | \"log-linear\" | \"linear-log\";\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 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 direction: DriverDirection;\n strength: DriverStrength;\n confidence: DriverConfidence;\n coefficient?: number | null;\n form: DriverForm;\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}\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// ── 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\nexport interface DriverAttribution {\n driver_measure: string;\n driver_previous: number;\n driver_current: number;\n driver_delta: number;\n coefficient?: number;\n form: DriverForm;\n estimated_target_impact?: number;\n description?: string;\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 /** \"value_share\" (additive) or \"equal\" (ratios). */\n weight_basis: string;\n dimensions: DimensionOpportunity[];\n skipped_dimensions: SkippedDimension[];\n downstream: PredictImpact[];\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 four\n * airlayer metric-tree analyses (tree introspection, sensitivity, predict,\n * explain, opportunity) 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 * Propagate hypothetical `(measure, delta)` changes upward through the\n * tree. Returns the estimated impact on every downstream measure.\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[]): Promise<PredictResult> {\n const query = this.buildQuery();\n return this.request<PredictResult>(this.path(`/predict${query}`), {\n method: \"POST\",\n body: JSON.stringify({ changes })\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"],"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 /** 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 /** Max rows (server caps at 500, defaults to 100). */\n limit?: number;\n}\n\nexport interface ListAnomaliesResponse {\n anomalies: Anomaly[];\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/**\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, newest first.\n *\n * @example\n * ```typescript\n * // Open / unresolved anomalies only\n * const { anomalies } = await client.anomalies.list({ status: \"new\" });\n * ```\n */\n async list(options: ListAnomaliesOptions = {}): Promise<ListAnomaliesResponse> {\n const extra: Record<string, string> = {};\n if (options.status) extra.status = options.status;\n if (options.limit) extra.limit = String(options.limit);\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 * 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 layer 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 layer.\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 DistributionRequest,\n ExplainRequest,\n ExplainResult,\n MetricTree,\n OpportunityRequest,\n OpportunityResult,\n PredictChange,\n PredictResult,\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\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 */\nexport function usePredict(\n changes: PredictChange[] | null,\n opts: EndpointOpts = {}\n): MetricTreeHookResult<PredictResult> {\n const { projectId, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const key = projectId && changes ? JSON.stringify({ projectId, changes }) : null;\n\n return useMetricTreeEndpoint<PredictResult>(\n key,\n (signal) =>\n postJson<PredictResult>(\n fetcher,\n `${metricTreePath(projectId as string)}/predict`,\n { changes },\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-layer 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-layer 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\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, fetcher } = useOxyApp();\n const enabled = opts.enabled !== false;\n const { search, limit } = 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 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]);\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-layer 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\";\nexport type DriverForm = \"linear\" | \"log-log\" | \"log-linear\" | \"linear-log\";\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 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 direction: DriverDirection;\n strength: DriverStrength;\n confidence: DriverConfidence;\n coefficient?: number | null;\n form: DriverForm;\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}\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// ── 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 four\n * airlayer metric-tree analyses (tree introspection, sensitivity, predict,\n * explain, opportunity) 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 * Propagate hypothetical `(measure, delta)` changes upward through the\n * tree. Returns the estimated impact on every downstream measure.\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[]): Promise<PredictResult> {\n const query = this.buildQuery();\n return this.request<PredictResult>(this.path(`/predict${query}`), {\n method: \"POST\",\n body: JSON.stringify({ changes })\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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"}
|
|
@@ -30,7 +30,7 @@ let react = require("react");
|
|
|
30
30
|
react = __toESM(react, 1);
|
|
31
31
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
32
32
|
|
|
33
|
-
//#region src/
|
|
33
|
+
//#region src/custom-app/logger.ts
|
|
34
34
|
let activeLogger = createConsoleLogger();
|
|
35
35
|
/** Replace the global logger. Pass `null` to silence everything. */
|
|
36
36
|
function setOxyAppLogger(logger) {
|
|
@@ -70,9 +70,9 @@ function silentLogger() {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
//#endregion
|
|
73
|
-
//#region src/
|
|
73
|
+
//#region src/custom-app/errors.ts
|
|
74
74
|
/**
|
|
75
|
-
* Error thrown by all
|
|
75
|
+
* Error thrown by all custom-app hooks when an API call returns a
|
|
76
76
|
* non-2xx response. Carries the structured `code` + `hint` the server
|
|
77
77
|
* emits so bundle UIs can render an actionable message instead of
|
|
78
78
|
* "404: { ...json... }".
|
|
@@ -119,12 +119,12 @@ async function apiErrorFromResponse(resp) {
|
|
|
119
119
|
}
|
|
120
120
|
const ARCH_DOC = "internal-docs/customer-apps.md";
|
|
121
121
|
/** Interpret a thrown error as a structured report for UI display. */
|
|
122
|
-
function
|
|
122
|
+
function interpretCustomAppError(err) {
|
|
123
123
|
const message = err instanceof Error ? err.message : String(err);
|
|
124
124
|
if (/Failed to load oxy-app\.json.*HTTP 404/.test(message)) return {
|
|
125
125
|
title: "Manifest not found",
|
|
126
126
|
message,
|
|
127
|
-
hint: "The bundle is being served, but oxy-app.json was not. Check that public/oxy-app.json is committed in the
|
|
127
|
+
hint: "The bundle is being served, but oxy-app.json was not. Check that public/oxy-app.json is committed in the custom-app repo and that the build copied it into the static output. If you're using Next.js, anything under public/ is auto-copied to out/.",
|
|
128
128
|
docs: ARCH_DOC
|
|
129
129
|
};
|
|
130
130
|
if (/Failed to load oxy-app\.json/.test(message)) return {
|
|
@@ -160,7 +160,7 @@ function interpretCustomerAppError(err) {
|
|
|
160
160
|
if (/^403:.*SELECT.*WITH/im.test(message)) return {
|
|
161
161
|
title: "Query rejected — read-only endpoint",
|
|
162
162
|
message,
|
|
163
|
-
hint: "This proxy only runs SELECT or WITH queries. Mutations (INSERT/UPDATE/DELETE/DROP) are not allowed from
|
|
163
|
+
hint: "This proxy only runs SELECT or WITH queries. Mutations (INSERT/UPDATE/DELETE/DROP) are not allowed from custom-app bundles.",
|
|
164
164
|
docs: ARCH_DOC
|
|
165
165
|
};
|
|
166
166
|
if (/^403:/m.test(message)) return {
|
|
@@ -196,7 +196,7 @@ function interpretCustomerAppError(err) {
|
|
|
196
196
|
if (/Unexpected token '<'|<!doctype/i.test(message)) return {
|
|
197
197
|
title: "Fetched HTML where JSON was expected",
|
|
198
198
|
message,
|
|
199
|
-
hint: "Most likely the built bundle is stale — built against an old SDK whose endpoints no longer exist on the server. Rebuild the bundle (vite build) with @oxy-hq/sdk@^2.0.0 and reload. If the bundle is current, check that OXY_APP_BASE_PATH matches the path the
|
|
199
|
+
hint: "Most likely the built bundle is stale — built against an old SDK whose endpoints no longer exist on the server. Rebuild the bundle (vite build) with @oxy-hq/sdk@^2.0.0 and reload. If the bundle is current, check that OXY_APP_BASE_PATH matches the path the custom-app row is served at.",
|
|
200
200
|
docs: ARCH_DOC
|
|
201
201
|
};
|
|
202
202
|
return {
|
|
@@ -208,7 +208,7 @@ function interpretCustomerAppError(err) {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
//#endregion
|
|
211
|
-
//#region src/
|
|
211
|
+
//#region src/custom-app/inject.ts
|
|
212
212
|
/**
|
|
213
213
|
* Read the runtime app-config oxy injected at serve time. Returns
|
|
214
214
|
* `undefined` outside the browser or when the global isn't set
|
|
@@ -221,18 +221,18 @@ function readInjectedAppConfig() {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
//#endregion
|
|
224
|
-
//#region src/
|
|
224
|
+
//#region src/custom-app/manifest.ts
|
|
225
225
|
let cached = null;
|
|
226
226
|
/**
|
|
227
227
|
* Load + validate the manifest. Cached after the first call so callers
|
|
228
228
|
* can invoke this from every component without coordinating.
|
|
229
229
|
*/
|
|
230
|
-
function
|
|
230
|
+
function loadCustomAppManifest(options = {}) {
|
|
231
231
|
if (!cached) cached = fetchAndValidate(options);
|
|
232
232
|
return cached;
|
|
233
233
|
}
|
|
234
234
|
/** For tests: reset the cache between runs. */
|
|
235
|
-
function
|
|
235
|
+
function _resetCustomAppManifestCacheForTest() {
|
|
236
236
|
cached = null;
|
|
237
237
|
}
|
|
238
238
|
async function fetchAndValidate(options) {
|
|
@@ -254,7 +254,7 @@ async function fetchAndValidate(options) {
|
|
|
254
254
|
status: res.status,
|
|
255
255
|
statusText: res.statusText
|
|
256
256
|
});
|
|
257
|
-
throw new Error(`Failed to load oxy-app.json from ${manifestUrl} (HTTP ${res.status}). The
|
|
257
|
+
throw new Error(`Failed to load oxy-app.json from ${manifestUrl} (HTTP ${res.status}). The custom-app repo must commit this file alongside the bundle.`);
|
|
258
258
|
}
|
|
259
259
|
const manifest = validateManifest(await res.json(), manifestUrl);
|
|
260
260
|
const resolved = {
|
|
@@ -319,7 +319,7 @@ const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
|
319
319
|
/**
|
|
320
320
|
* Validate the optional `functions` map. Each key is a function name;
|
|
321
321
|
* each value declares how the function is invoked. Mirrors the
|
|
322
|
-
* server-side validation in `
|
|
322
|
+
* server-side validation in `custom_apps_publish.rs` so a bad
|
|
323
323
|
* manifest fails at build, not at publish.
|
|
324
324
|
*/
|
|
325
325
|
function validateFunctions(raw) {
|
|
@@ -397,7 +397,7 @@ function isRecord(v) {
|
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
//#endregion
|
|
400
|
-
//#region src/
|
|
400
|
+
//#region src/custom-app/function-invoke.ts
|
|
401
401
|
const inflight$1 = /* @__PURE__ */ new Map();
|
|
402
402
|
/**
|
|
403
403
|
* Dedup key for an invocation: function name + its (stable-serialized) body,
|
|
@@ -423,7 +423,7 @@ function sharedFunctionInvoke(key, run) {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
//#endregion
|
|
426
|
-
//#region src/
|
|
426
|
+
//#region src/custom-app/function-sse.ts
|
|
427
427
|
/**
|
|
428
428
|
* Read a `text/event-stream` function response to completion. Resolves with the
|
|
429
429
|
* decoded result + captured logs, or rejects (with `.logs` attached) on an
|
|
@@ -480,7 +480,7 @@ async function readFunctionSseStream(resp) {
|
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
//#endregion
|
|
483
|
-
//#region src/
|
|
483
|
+
//#region src/custom-app/interpolate.ts
|
|
484
484
|
/**
|
|
485
485
|
* Interpolate `{{ params.X }}` and `{{ params.X | sqlquote }}` placeholders
|
|
486
486
|
* in a SQL template.
|
|
@@ -509,7 +509,7 @@ function interpolateSqlParams(sql, params) {
|
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
//#endregion
|
|
512
|
-
//#region src/
|
|
512
|
+
//#region src/custom-app/markdown.ts
|
|
513
513
|
/**
|
|
514
514
|
* Allowlist for `[text](url)` href values in agent-emitted markdown.
|
|
515
515
|
* Markdown comes from an LLM, which sits across an external trust
|
|
@@ -585,7 +585,7 @@ function isTableStart(lines, idx) {
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
//#endregion
|
|
588
|
-
//#region src/
|
|
588
|
+
//#region src/custom-app/query-cache.ts
|
|
589
589
|
const SWR_TTL_MS = 3e4;
|
|
590
590
|
const inflight = /* @__PURE__ */ new Map();
|
|
591
591
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -629,7 +629,7 @@ async function sharedQuery(fetcher, projectId, sql, db, opts = {}) {
|
|
|
629
629
|
}
|
|
630
630
|
|
|
631
631
|
//#endregion
|
|
632
|
-
//#region src/
|
|
632
|
+
//#region src/custom-app/react.tsx
|
|
633
633
|
function defaultFetcher(input, init) {
|
|
634
634
|
return fetch(input, {
|
|
635
635
|
credentials: "include",
|
|
@@ -670,7 +670,7 @@ function OxyAppProvider(props) {
|
|
|
670
670
|
});
|
|
671
671
|
react.useEffect(() => {
|
|
672
672
|
let cancelled = false;
|
|
673
|
-
|
|
673
|
+
loadCustomAppManifest(manifestOptions).then((resolved) => {
|
|
674
674
|
if (!cancelled) setState({
|
|
675
675
|
status: "ready",
|
|
676
676
|
resolved,
|
|
@@ -679,7 +679,7 @@ function OxyAppProvider(props) {
|
|
|
679
679
|
}).catch((e) => {
|
|
680
680
|
if (!cancelled) setState({
|
|
681
681
|
status: "error",
|
|
682
|
-
error:
|
|
682
|
+
error: interpretCustomAppError(e),
|
|
683
683
|
fetcher
|
|
684
684
|
});
|
|
685
685
|
});
|
|
@@ -768,6 +768,11 @@ function useResolvedManifest() {
|
|
|
768
768
|
* this only when you need the fetcher or identity without requiring
|
|
769
769
|
* the manifest to be ready (e.g. inside `useQuery`, or the shell
|
|
770
770
|
* chrome, which must never block the app on the manifest load).
|
|
771
|
+
*
|
|
772
|
+
* Exported for sibling hook modules (`metric-tree-hooks`,
|
|
773
|
+
* `world-model-hooks`) that need the same credentialed fetcher +
|
|
774
|
+
* project scope without re-deriving the context wiring. Not part of
|
|
775
|
+
* the public bundle API — bundle authors use the concrete hooks.
|
|
771
776
|
*/
|
|
772
777
|
function useOxyApp() {
|
|
773
778
|
const ctx = react.useContext(OxyAppContext);
|
|
@@ -781,7 +786,7 @@ function useOxyApp() {
|
|
|
781
786
|
}
|
|
782
787
|
/**
|
|
783
788
|
* Execute an ad-hoc SQL query against the project linked to this
|
|
784
|
-
*
|
|
789
|
+
* custom app. The query is specified inline by the caller; no
|
|
785
790
|
* manifest declaration is involved.
|
|
786
791
|
*
|
|
787
792
|
* Re-runs whenever `input` or enabled `params` change. Use the
|
|
@@ -2312,10 +2317,10 @@ Object.defineProperty(exports, '__toESM', {
|
|
|
2312
2317
|
return __toESM;
|
|
2313
2318
|
}
|
|
2314
2319
|
});
|
|
2315
|
-
Object.defineProperty(exports, '
|
|
2320
|
+
Object.defineProperty(exports, '_resetCustomAppManifestCacheForTest', {
|
|
2316
2321
|
enumerable: true,
|
|
2317
2322
|
get: function () {
|
|
2318
|
-
return
|
|
2323
|
+
return _resetCustomAppManifestCacheForTest;
|
|
2319
2324
|
}
|
|
2320
2325
|
});
|
|
2321
2326
|
Object.defineProperty(exports, 'apiErrorFromResponse', {
|
|
@@ -2330,16 +2335,16 @@ Object.defineProperty(exports, 'getOxyAppLogger', {
|
|
|
2330
2335
|
return getOxyAppLogger;
|
|
2331
2336
|
}
|
|
2332
2337
|
});
|
|
2333
|
-
Object.defineProperty(exports, '
|
|
2338
|
+
Object.defineProperty(exports, 'interpretCustomAppError', {
|
|
2334
2339
|
enumerable: true,
|
|
2335
2340
|
get: function () {
|
|
2336
|
-
return
|
|
2341
|
+
return interpretCustomAppError;
|
|
2337
2342
|
}
|
|
2338
2343
|
});
|
|
2339
|
-
Object.defineProperty(exports, '
|
|
2344
|
+
Object.defineProperty(exports, 'loadCustomAppManifest', {
|
|
2340
2345
|
enumerable: true,
|
|
2341
2346
|
get: function () {
|
|
2342
|
-
return
|
|
2347
|
+
return loadCustomAppManifest;
|
|
2343
2348
|
}
|
|
2344
2349
|
});
|
|
2345
2350
|
Object.defineProperty(exports, 'readInjectedAppConfig', {
|
|
@@ -2402,4 +2407,4 @@ Object.defineProperty(exports, 'useTrackEvent', {
|
|
|
2402
2407
|
return useTrackEvent;
|
|
2403
2408
|
}
|
|
2404
2409
|
});
|
|
2405
|
-
//# sourceMappingURL=react-
|
|
2410
|
+
//# sourceMappingURL=react-BFFCK4VM.cjs.map
|