@warlock.js/ai-panoptic 4.8.2 → 4.9.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 (151) hide show
  1. package/cjs/index.cjs +31 -31
  2. package/cjs/index.cjs.map +1 -1
  3. package/esm/collector/collector.d.mts +1 -1
  4. package/esm/collector/collector.d.mts.map +1 -1
  5. package/esm/collector/collector.mjs +1 -1
  6. package/esm/collector/collector.mjs.map +1 -1
  7. package/esm/collector/content-capture.type.d.mts +1 -1
  8. package/esm/collector/content-capture.type.d.mts.map +1 -1
  9. package/esm/collector/extract-span-attributes.d.mts +1 -1
  10. package/esm/collector/extract-span-attributes.d.mts.map +1 -1
  11. package/esm/collector/extract-span-attributes.mjs +1 -1
  12. package/esm/collector/extract-span-attributes.mjs.map +1 -1
  13. package/esm/collector/normalize-error.d.mts +1 -1
  14. package/esm/collector/normalize-error.d.mts.map +1 -1
  15. package/esm/collector/normalize-error.mjs +1 -1
  16. package/esm/collector/normalize-error.mjs.map +1 -1
  17. package/esm/collector/report-to-span.d.mts +1 -1
  18. package/esm/collector/report-to-span.d.mts.map +1 -1
  19. package/esm/collector/report-to-span.mjs +1 -1
  20. package/esm/collector/report-to-span.mjs.map +1 -1
  21. package/esm/collector/report-to-trace.d.mts +1 -1
  22. package/esm/collector/report-to-trace.d.mts.map +1 -1
  23. package/esm/collector/report-to-trace.mjs +1 -1
  24. package/esm/collector/report-to-trace.mjs.map +1 -1
  25. package/esm/config/apply-panoptic-config.d.mts +1 -1
  26. package/esm/config/apply-panoptic-config.d.mts.map +1 -1
  27. package/esm/config/apply-panoptic-config.mjs +1 -1
  28. package/esm/config/apply-panoptic-config.mjs.map +1 -1
  29. package/esm/config/panoptic-config.type.d.mts +1 -1
  30. package/esm/config/panoptic-config.type.d.mts.map +1 -1
  31. package/esm/contracts/collector.contract.d.mts +1 -1
  32. package/esm/contracts/collector.contract.d.mts.map +1 -1
  33. package/esm/contracts/exporter.contract.d.mts +1 -1
  34. package/esm/contracts/exporter.contract.d.mts.map +1 -1
  35. package/esm/contracts/trace.type.d.mts +1 -1
  36. package/esm/contracts/trace.type.d.mts.map +1 -1
  37. package/esm/dashboard/dashboard.d.mts +1 -1
  38. package/esm/dashboard/dashboard.d.mts.map +1 -1
  39. package/esm/dashboard/dashboard.mjs +1 -1
  40. package/esm/dashboard/dashboard.mjs.map +1 -1
  41. package/esm/dashboard/dashboard.type.d.mts +1 -1
  42. package/esm/dashboard/dashboard.type.d.mts.map +1 -1
  43. package/esm/dashboard/parse-query.mjs +1 -1
  44. package/esm/dashboard/parse-query.mjs.map +1 -1
  45. package/esm/dashboard/serve.mjs +1 -1
  46. package/esm/dashboard/serve.mjs.map +1 -1
  47. package/esm/dashboard/trace-filter.d.mts +1 -1
  48. package/esm/dashboard/trace-filter.d.mts.map +1 -1
  49. package/esm/dashboard/trace-filter.mjs +1 -1
  50. package/esm/dashboard/trace-filter.mjs.map +1 -1
  51. package/esm/dashboard/ui.html.mjs +1 -1
  52. package/esm/dashboard/ui.html.mjs.map +1 -1
  53. package/esm/dashboard/warlock-logo.mjs +1 -1
  54. package/esm/dashboard/warlock-logo.mjs.map +1 -1
  55. package/esm/evaluate/evaluate-system-prompt.d.mts +1 -1
  56. package/esm/evaluate/evaluate-system-prompt.d.mts.map +1 -1
  57. package/esm/evaluate/evaluate-system-prompt.mjs +1 -1
  58. package/esm/evaluate/evaluate-system-prompt.mjs.map +1 -1
  59. package/esm/evaluate/evaluate.type.d.mts +1 -1
  60. package/esm/evaluate/evaluate.type.d.mts.map +1 -1
  61. package/esm/evaluate/extract-last-system-prompt.d.mts +1 -1
  62. package/esm/evaluate/extract-last-system-prompt.d.mts.map +1 -1
  63. package/esm/evaluate/extract-last-system-prompt.mjs +1 -1
  64. package/esm/evaluate/extract-last-system-prompt.mjs.map +1 -1
  65. package/esm/evaluate/find-span-by-id.d.mts +1 -1
  66. package/esm/evaluate/find-span-by-id.d.mts.map +1 -1
  67. package/esm/evaluate/find-span-by-id.mjs +1 -1
  68. package/esm/evaluate/find-span-by-id.mjs.map +1 -1
  69. package/esm/exporters/console/console-exporter.d.mts +1 -1
  70. package/esm/exporters/console/console-exporter.d.mts.map +1 -1
  71. package/esm/exporters/console/console-exporter.mjs +1 -1
  72. package/esm/exporters/console/console-exporter.mjs.map +1 -1
  73. package/esm/exporters/console/console-exporter.type.d.mts +1 -1
  74. package/esm/exporters/console/console-exporter.type.d.mts.map +1 -1
  75. package/esm/exporters/console/format-span-io.d.mts +1 -1
  76. package/esm/exporters/console/format-span-io.d.mts.map +1 -1
  77. package/esm/exporters/console/format-span-io.mjs +1 -1
  78. package/esm/exporters/console/format-span-io.mjs.map +1 -1
  79. package/esm/exporters/console/format-span-line.d.mts +1 -1
  80. package/esm/exporters/console/format-span-line.d.mts.map +1 -1
  81. package/esm/exporters/console/format-span-line.mjs +1 -1
  82. package/esm/exporters/console/format-span-line.mjs.map +1 -1
  83. package/esm/exporters/file/file-exporter.d.mts +1 -1
  84. package/esm/exporters/file/file-exporter.d.mts.map +1 -1
  85. package/esm/exporters/file/file-exporter.mjs +1 -1
  86. package/esm/exporters/file/file-exporter.mjs.map +1 -1
  87. package/esm/exporters/file/file-exporter.type.d.mts +1 -1
  88. package/esm/exporters/file/file-exporter.type.d.mts.map +1 -1
  89. package/esm/exporters/langfuse/langfuse-exporter.d.mts +1 -1
  90. package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -1
  91. package/esm/exporters/langfuse/langfuse-exporter.mjs +1 -1
  92. package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
  93. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +1 -1
  94. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -1
  95. package/esm/exporters/otel/otel-api.shim.type.d.mts +1 -1
  96. package/esm/exporters/otel/otel-api.shim.type.d.mts.map +1 -1
  97. package/esm/exporters/otel/otel-exporter.d.mts +1 -1
  98. package/esm/exporters/otel/otel-exporter.d.mts.map +1 -1
  99. package/esm/exporters/otel/otel-exporter.mjs +1 -1
  100. package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
  101. package/esm/exporters/otel/otel-exporter.type.d.mts +1 -1
  102. package/esm/exporters/otel/otel-exporter.type.d.mts.map +1 -1
  103. package/esm/exporters/utils/gen-ai-attributes.d.mts +1 -1
  104. package/esm/exporters/utils/gen-ai-attributes.d.mts.map +1 -1
  105. package/esm/exporters/utils/gen-ai-attributes.mjs +1 -1
  106. package/esm/exporters/utils/gen-ai-attributes.mjs.map +1 -1
  107. package/esm/exporters/utils/total-cost.d.mts +1 -1
  108. package/esm/exporters/utils/total-cost.d.mts.map +1 -1
  109. package/esm/exporters/utils/total-cost.mjs +1 -1
  110. package/esm/exporters/utils/total-cost.mjs.map +1 -1
  111. package/esm/exporters/utils/walk-spans.d.mts +1 -1
  112. package/esm/exporters/utils/walk-spans.d.mts.map +1 -1
  113. package/esm/exporters/utils/walk-spans.mjs +1 -1
  114. package/esm/exporters/utils/walk-spans.mjs.map +1 -1
  115. package/esm/panoptic/panoptic-middleware.d.mts +1 -1
  116. package/esm/panoptic/panoptic-middleware.d.mts.map +1 -1
  117. package/esm/panoptic/panoptic-middleware.mjs +1 -1
  118. package/esm/panoptic/panoptic-middleware.mjs.map +1 -1
  119. package/esm/panoptic/panoptic-target.type.d.mts +1 -1
  120. package/esm/panoptic/panoptic-target.type.d.mts.map +1 -1
  121. package/esm/panoptic/panoptic.d.mts +1 -1
  122. package/esm/panoptic/panoptic.d.mts.map +1 -1
  123. package/esm/panoptic/panoptic.mjs +1 -1
  124. package/esm/panoptic/panoptic.mjs.map +1 -1
  125. package/esm/panoptic/panoptic.type.d.mts +1 -1
  126. package/esm/panoptic/panoptic.type.d.mts.map +1 -1
  127. package/esm/register.mjs +1 -1
  128. package/esm/register.mjs.map +1 -1
  129. package/esm/store/cache-trace-store.d.mts +1 -1
  130. package/esm/store/cache-trace-store.d.mts.map +1 -1
  131. package/esm/store/cache-trace-store.mjs +1 -1
  132. package/esm/store/cache-trace-store.mjs.map +1 -1
  133. package/esm/store/in-memory-trace-store.d.mts +1 -1
  134. package/esm/store/in-memory-trace-store.d.mts.map +1 -1
  135. package/esm/store/in-memory-trace-store.mjs +1 -1
  136. package/esm/store/in-memory-trace-store.mjs.map +1 -1
  137. package/esm/store/match-trace.d.mts +1 -1
  138. package/esm/store/match-trace.d.mts.map +1 -1
  139. package/esm/store/match-trace.mjs +1 -1
  140. package/esm/store/match-trace.mjs.map +1 -1
  141. package/esm/store/sum-usage.d.mts +1 -1
  142. package/esm/store/sum-usage.d.mts.map +1 -1
  143. package/esm/store/sum-usage.mjs +1 -1
  144. package/esm/store/sum-usage.mjs.map +1 -1
  145. package/esm/store/trace-aggregate.type.d.mts +1 -1
  146. package/esm/store/trace-aggregate.type.d.mts.map +1 -1
  147. package/esm/store/trace-query.type.d.mts +1 -1
  148. package/esm/store/trace-query.type.d.mts.map +1 -1
  149. package/esm/store/trace-store.contract.d.mts +1 -1
  150. package/esm/store/trace-store.contract.d.mts.map +1 -1
  151. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"serve.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/serve.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { EvaluateConfig } from \"../evaluate/evaluate.type\";\nimport { evaluateSystemPrompt } from \"../evaluate/evaluate-system-prompt\";\nimport { extractLastSystemPrompt } from \"../evaluate/extract-last-system-prompt\";\nimport { findSpanById } from \"../evaluate/find-span-by-id\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport { parseQuery } from \"./parse-query\";\nimport { dashboardHtml } from \"./ui.html\";\n\n/** Fully-resolved routing config the request handler closes over. */\nexport type ServeConfig = {\n /** Normalized mount path, always ending in `/` (e.g. `\"/\"`). */\n basePath: string;\n /** Header title baked into the served page. */\n title: string;\n /** Bearer token required on every request when set (S4). */\n authToken?: string;\n /** `Host` header allowlist — defends against DNS-rebinding (S4). */\n allowedHosts: string[];\n /**\n * Enables the one write route: `POST\n * {basePath}api/traces/:traceId/spans/:spanId/evaluate`. Absent ⇒ the\n * route 405s (POST isn't accepted by any route) and the served page never\n * renders the Evaluate button.\n */\n evaluate?: EvaluateConfig;\n};\n\n/** Request body accepted by the evaluate route — everything optional. */\ntype EvaluateRequestBody = {\n /** Per-run instructions override; falls back to `config.evaluate.instructions`. */\n instructions?: string;\n};\n\n/** Hard cap on the evaluate route's request body — well beyond a real instructions string. */\nconst MAX_EVALUATE_BODY_BYTES = 64 * 1024;\n\n/**\n * Security response headers added to every dashboard response (S4):\n * block MIME-sniffing, framing, referrer leakage, and lock the page's\n * content sources down to itself (the UI is fully self-contained — no CDN).\n */\nconst SECURITY_HEADERS: Record<string, string> = {\n \"x-content-type-options\": \"nosniff\",\n \"x-frame-options\": \"DENY\",\n \"referrer-policy\": \"no-referrer\",\n \"content-security-policy\":\n \"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'\",\n};\n\n/** Extract the host (no port) from a `Host` header value. */\nfunction hostHeaderName(hostHeader: string | undefined): string | undefined {\n if (!hostHeader) return undefined;\n // IPv6 literal `[::1]:4319` → `[::1]`; otherwise strip `:port`.\n if (hostHeader.startsWith(\"[\")) {\n return hostHeader.slice(0, hostHeader.indexOf(\"]\") + 1);\n }\n const colon = hostHeader.indexOf(\":\");\n return colon === -1 ? hostHeader : hostHeader.slice(0, colon);\n}\n\n/** Constant-ish bearer-token check from `Authorization` header or `?token=`. */\nfunction isAuthorized(req: IncomingMessage, url: URL, token: string): boolean {\n const header = req.headers.authorization;\n if (header && header === `Bearer ${token}`) return true;\n return url.searchParams.get(\"token\") === token;\n}\n\n/**\n * Build the `node:http` request handler for the dashboard over a given\n * trace store. Kept separate from the server lifecycle so it can be unit\n * tested by feeding it a fake `req`/`res` without binding a port.\n *\n * Routes (all under `config.basePath`):\n *\n * - `GET api/traces` → `store.query(parseQuery(searchParams))`\n * - `GET api/traces/:id` → `store.get(id)` or `404`\n * - `GET api/aggregate` → `store.aggregate(parseQuery(searchParams))`\n * - `GET {basePath}` → the self-contained HTML page\n * - `POST api/traces/:traceId/spans/:spanId/evaluate` → ONLY when\n * `config.evaluate` is set; grades the span's last captured system\n * prompt and returns a {@link EvaluateVerdict}. The dashboard's one\n * write route — absent config, POST 405s like it would against any\n * other route (the path is never even pattern-matched).\n *\n * Anything else → `404`. A method the matched route doesn't accept → `405`.\n * Host allowlist + bearer-token auth (S4) are checked for EVERY request,\n * regardless of method, before any routing. The store shapes\n * ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so GET responses\n * are a plain `JSON.stringify` with no serializer.\n *\n * @example\n * const handler = createRequestHandler(store, { basePath: \"/\", title: \"Panoptic\" });\n * http.createServer(handler).listen(4319, \"127.0.0.1\");\n */\nexport function createRequestHandler(\n store: TraceStoreContract,\n config: ServeConfig,\n): (req: IncomingMessage, res: ServerResponse) => void {\n const base = config.basePath;\n const apiPrefix = `${base}api`;\n\n return function handle(req: IncomingMessage, res: ServerResponse): void {\n // `req.url` is path + query only; a dummy origin lets URL parse it.\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n const pathname = url.pathname;\n\n // Host-header allowlist — blocks DNS-rebinding attacks that point a\n // hostile domain at this loopback port (S4). Checked for every method.\n const host = hostHeaderName(req.headers.host);\n if (!host || !config.allowedHosts.includes(host.toLowerCase())) {\n sendJson(res, 403, { error: \"host_not_allowed\" });\n\n return;\n }\n\n // Bearer-token auth when configured (always required off loopback, S4).\n if (config.authToken && !isAuthorized(req, url, config.authToken)) {\n sendJson(res, 401, { error: \"unauthorized\" });\n\n return;\n }\n\n // The one write route — opt-in via `config.evaluate`, so it must be\n // checked before the blanket GET-only gate below.\n if (req.method === \"POST\" && config.evaluate) {\n const match = matchEvaluatePath(pathname, apiPrefix);\n\n if (match) {\n void handleEvaluate(store, config.evaluate, match.traceId, match.spanId, req, res);\n\n return;\n }\n }\n\n if (req.method !== \"GET\") {\n sendJson(res, 405, { error: \"method_not_allowed\" });\n\n return;\n }\n\n if (pathname === `${apiPrefix}/traces`) {\n sendJson(res, 200, store.query(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname.startsWith(`${apiPrefix}/traces/`)) {\n const traceId = decodeURIComponent(pathname.slice(`${apiPrefix}/traces/`.length));\n const trace = traceId.length > 0 ? store.get(traceId) : undefined;\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n sendJson(res, 200, trace);\n\n return;\n }\n\n if (pathname === `${apiPrefix}/aggregate`) {\n sendJson(res, 200, store.aggregate(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname === base || pathname === base.replace(/\\/$/, \"\")) {\n res.writeHead(200, {\n \"content-type\": \"text/html; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(dashboardHtml(base, config.title, Boolean(config.evaluate), config.evaluate?.instructions ?? \"\"));\n\n return;\n }\n\n sendJson(res, 404, { error: \"not_found\" });\n };\n}\n\n/** Write a JSON response with the given status code and security headers. */\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n \"content-type\": \"application/json; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(JSON.stringify(body));\n}\n\n/**\n * Match `{apiPrefix}/traces/:traceId/spans/:spanId/evaluate` — the one\n * write route. Returns `undefined` for anything else, including the plain\n * `{apiPrefix}/traces/:id` read route (no `/spans/.../evaluate` suffix),\n * so the two never collide.\n */\nfunction matchEvaluatePath(\n pathname: string,\n apiPrefix: string,\n): { traceId: string; spanId: string } | undefined {\n const prefix = `${apiPrefix}/traces/`;\n\n if (!pathname.startsWith(prefix)) {\n return undefined;\n }\n\n const match = /^([^/]+)\\/spans\\/([^/]+)\\/evaluate$/.exec(pathname.slice(prefix.length));\n\n if (!match) {\n return undefined;\n }\n\n return {\n traceId: decodeURIComponent(match[1]),\n spanId: decodeURIComponent(match[2]),\n };\n}\n\n/**\n * Collect and JSON-parse a request body, capped at\n * {@link MAX_EVALUATE_BODY_BYTES} so an oversized body can't hold the\n * connection open indefinitely. An empty body resolves to `undefined` —\n * the evaluate route treats that as \"no per-run override\".\n */\nfunction readJsonBody<T>(req: IncomingMessage): Promise<T | undefined> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n\n if (size > MAX_EVALUATE_BODY_BYTES) {\n req.destroy();\n reject(new Error(\"payload_too_large\"));\n\n return;\n }\n\n chunks.push(chunk);\n });\n\n req.on(\"end\", () => {\n if (chunks.length === 0) {\n resolve(undefined);\n\n return;\n }\n\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString(\"utf-8\")) as T);\n } catch {\n reject(new Error(\"invalid_json\"));\n }\n });\n\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Handle `POST {apiPrefix}/traces/:traceId/spans/:spanId/evaluate`. Looks\n * up the trace + span, extracts its last captured system prompt, and\n * grades it via {@link evaluateSystemPrompt}. `judgePromptBody` itself\n * never throws (a broken judge degrades to an issues-only outcome) — the\n * try/catch here only guards `config.evaluate.model` resolution, the one\n * step that CAN throw (e.g. a factory constructing an SDK client).\n */\nasync function handleEvaluate(\n store: TraceStoreContract,\n evaluate: EvaluateConfig,\n traceId: string,\n spanId: string,\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const trace = store.get(traceId);\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n const span = findSpanById(trace.root, spanId);\n\n if (span === undefined) {\n sendJson(res, 404, { error: \"span_not_found\", spanId });\n\n return;\n }\n\n const systemPrompt = extractLastSystemPrompt(span);\n\n if (systemPrompt === undefined) {\n sendJson(res, 422, { error: \"no_system_prompt\" });\n\n return;\n }\n\n let body: EvaluateRequestBody | undefined;\n\n try {\n body = await readJsonBody<EvaluateRequestBody>(req);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"invalid_json\";\n sendJson(res, message === \"payload_too_large\" ? 413 : 400, { error: message });\n\n return;\n }\n\n try {\n const verdict = await evaluateSystemPrompt(systemPrompt, evaluate, body?.instructions);\n sendJson(res, 200, verdict);\n } catch (error) {\n sendJson(res, 502, {\n error: \"evaluate_failed\",\n message: error instanceof Error ? error.message : String(error),\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"serve.mjs","names":[],"sources":["../../../../../../../ai-panoptic/src/dashboard/serve.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { EvaluateConfig } from \"../evaluate/evaluate.type\";\nimport { evaluateSystemPrompt } from \"../evaluate/evaluate-system-prompt\";\nimport { extractLastSystemPrompt } from \"../evaluate/extract-last-system-prompt\";\nimport { findSpanById } from \"../evaluate/find-span-by-id\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport { parseQuery } from \"./parse-query\";\nimport { dashboardHtml } from \"./ui.html\";\n\n/** Fully-resolved routing config the request handler closes over. */\nexport type ServeConfig = {\n /** Normalized mount path, always ending in `/` (e.g. `\"/\"`). */\n basePath: string;\n /** Header title baked into the served page. */\n title: string;\n /** Bearer token required on every request when set (S4). */\n authToken?: string;\n /** `Host` header allowlist — defends against DNS-rebinding (S4). */\n allowedHosts: string[];\n /**\n * Enables the one write route: `POST\n * {basePath}api/traces/:traceId/spans/:spanId/evaluate`. Absent ⇒ the\n * route 405s (POST isn't accepted by any route) and the served page never\n * renders the Evaluate button.\n */\n evaluate?: EvaluateConfig;\n};\n\n/** Request body accepted by the evaluate route — everything optional. */\ntype EvaluateRequestBody = {\n /** Per-run instructions override; falls back to `config.evaluate.instructions`. */\n instructions?: string;\n};\n\n/** Hard cap on the evaluate route's request body — well beyond a real instructions string. */\nconst MAX_EVALUATE_BODY_BYTES = 64 * 1024;\n\n/**\n * Security response headers added to every dashboard response (S4):\n * block MIME-sniffing, framing, referrer leakage, and lock the page's\n * content sources down to itself (the UI is fully self-contained — no CDN).\n */\nconst SECURITY_HEADERS: Record<string, string> = {\n \"x-content-type-options\": \"nosniff\",\n \"x-frame-options\": \"DENY\",\n \"referrer-policy\": \"no-referrer\",\n \"content-security-policy\":\n \"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'\",\n};\n\n/** Extract the host (no port) from a `Host` header value. */\nfunction hostHeaderName(hostHeader: string | undefined): string | undefined {\n if (!hostHeader) return undefined;\n // IPv6 literal `[::1]:4319` → `[::1]`; otherwise strip `:port`.\n if (hostHeader.startsWith(\"[\")) {\n return hostHeader.slice(0, hostHeader.indexOf(\"]\") + 1);\n }\n const colon = hostHeader.indexOf(\":\");\n return colon === -1 ? hostHeader : hostHeader.slice(0, colon);\n}\n\n/** Constant-ish bearer-token check from `Authorization` header or `?token=`. */\nfunction isAuthorized(req: IncomingMessage, url: URL, token: string): boolean {\n const header = req.headers.authorization;\n if (header && header === `Bearer ${token}`) return true;\n return url.searchParams.get(\"token\") === token;\n}\n\n/**\n * Build the `node:http` request handler for the dashboard over a given\n * trace store. Kept separate from the server lifecycle so it can be unit\n * tested by feeding it a fake `req`/`res` without binding a port.\n *\n * Routes (all under `config.basePath`):\n *\n * - `GET api/traces` → `store.query(parseQuery(searchParams))`\n * - `GET api/traces/:id` → `store.get(id)` or `404`\n * - `GET api/aggregate` → `store.aggregate(parseQuery(searchParams))`\n * - `GET {basePath}` → the self-contained HTML page\n * - `POST api/traces/:traceId/spans/:spanId/evaluate` → ONLY when\n * `config.evaluate` is set; grades the span's last captured system\n * prompt and returns a {@link EvaluateVerdict}. The dashboard's one\n * write route — absent config, POST 405s like it would against any\n * other route (the path is never even pattern-matched).\n *\n * Anything else → `404`. A method the matched route doesn't accept → `405`.\n * Host allowlist + bearer-token auth (S4) are checked for EVERY request,\n * regardless of method, before any routing. The store shapes\n * ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so GET responses\n * are a plain `JSON.stringify` with no serializer.\n *\n * @example\n * const handler = createRequestHandler(store, { basePath: \"/\", title: \"Panoptic\" });\n * http.createServer(handler).listen(4319, \"127.0.0.1\");\n */\nexport function createRequestHandler(\n store: TraceStoreContract,\n config: ServeConfig,\n): (req: IncomingMessage, res: ServerResponse) => void {\n const base = config.basePath;\n const apiPrefix = `${base}api`;\n\n return function handle(req: IncomingMessage, res: ServerResponse): void {\n // `req.url` is path + query only; a dummy origin lets URL parse it.\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n const pathname = url.pathname;\n\n // Host-header allowlist — blocks DNS-rebinding attacks that point a\n // hostile domain at this loopback port (S4). Checked for every method.\n const host = hostHeaderName(req.headers.host);\n if (!host || !config.allowedHosts.includes(host.toLowerCase())) {\n sendJson(res, 403, { error: \"host_not_allowed\" });\n\n return;\n }\n\n // Bearer-token auth when configured (always required off loopback, S4).\n if (config.authToken && !isAuthorized(req, url, config.authToken)) {\n sendJson(res, 401, { error: \"unauthorized\" });\n\n return;\n }\n\n // The one write route — opt-in via `config.evaluate`, so it must be\n // checked before the blanket GET-only gate below.\n if (req.method === \"POST\" && config.evaluate) {\n const match = matchEvaluatePath(pathname, apiPrefix);\n\n if (match) {\n void handleEvaluate(store, config.evaluate, match.traceId, match.spanId, req, res);\n\n return;\n }\n }\n\n if (req.method !== \"GET\") {\n sendJson(res, 405, { error: \"method_not_allowed\" });\n\n return;\n }\n\n if (pathname === `${apiPrefix}/traces`) {\n sendJson(res, 200, store.query(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname.startsWith(`${apiPrefix}/traces/`)) {\n const traceId = decodeURIComponent(pathname.slice(`${apiPrefix}/traces/`.length));\n const trace = traceId.length > 0 ? store.get(traceId) : undefined;\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n sendJson(res, 200, trace);\n\n return;\n }\n\n if (pathname === `${apiPrefix}/aggregate`) {\n sendJson(res, 200, store.aggregate(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname === base || pathname === base.replace(/\\/$/, \"\")) {\n res.writeHead(200, {\n \"content-type\": \"text/html; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(dashboardHtml(base, config.title, Boolean(config.evaluate), config.evaluate?.instructions ?? \"\"));\n\n return;\n }\n\n sendJson(res, 404, { error: \"not_found\" });\n };\n}\n\n/** Write a JSON response with the given status code and security headers. */\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n \"content-type\": \"application/json; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(JSON.stringify(body));\n}\n\n/**\n * Match `{apiPrefix}/traces/:traceId/spans/:spanId/evaluate` — the one\n * write route. Returns `undefined` for anything else, including the plain\n * `{apiPrefix}/traces/:id` read route (no `/spans/.../evaluate` suffix),\n * so the two never collide.\n */\nfunction matchEvaluatePath(\n pathname: string,\n apiPrefix: string,\n): { traceId: string; spanId: string } | undefined {\n const prefix = `${apiPrefix}/traces/`;\n\n if (!pathname.startsWith(prefix)) {\n return undefined;\n }\n\n const match = /^([^/]+)\\/spans\\/([^/]+)\\/evaluate$/.exec(pathname.slice(prefix.length));\n\n if (!match) {\n return undefined;\n }\n\n return {\n traceId: decodeURIComponent(match[1]),\n spanId: decodeURIComponent(match[2]),\n };\n}\n\n/**\n * Collect and JSON-parse a request body, capped at\n * {@link MAX_EVALUATE_BODY_BYTES} so an oversized body can't hold the\n * connection open indefinitely. An empty body resolves to `undefined` —\n * the evaluate route treats that as \"no per-run override\".\n */\nfunction readJsonBody<T>(req: IncomingMessage): Promise<T | undefined> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n\n if (size > MAX_EVALUATE_BODY_BYTES) {\n req.destroy();\n reject(new Error(\"payload_too_large\"));\n\n return;\n }\n\n chunks.push(chunk);\n });\n\n req.on(\"end\", () => {\n if (chunks.length === 0) {\n resolve(undefined);\n\n return;\n }\n\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString(\"utf-8\")) as T);\n } catch {\n reject(new Error(\"invalid_json\"));\n }\n });\n\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Handle `POST {apiPrefix}/traces/:traceId/spans/:spanId/evaluate`. Looks\n * up the trace + span, extracts its last captured system prompt, and\n * grades it via {@link evaluateSystemPrompt}. `judgePromptBody` itself\n * never throws (a broken judge degrades to an issues-only outcome) — the\n * try/catch here only guards `config.evaluate.model` resolution, the one\n * step that CAN throw (e.g. a factory constructing an SDK client).\n */\nasync function handleEvaluate(\n store: TraceStoreContract,\n evaluate: EvaluateConfig,\n traceId: string,\n spanId: string,\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const trace = store.get(traceId);\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n const span = findSpanById(trace.root, spanId);\n\n if (span === undefined) {\n sendJson(res, 404, { error: \"span_not_found\", spanId });\n\n return;\n }\n\n const systemPrompt = extractLastSystemPrompt(span);\n\n if (systemPrompt === undefined) {\n sendJson(res, 422, { error: \"no_system_prompt\" });\n\n return;\n }\n\n let body: EvaluateRequestBody | undefined;\n\n try {\n body = await readJsonBody<EvaluateRequestBody>(req);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"invalid_json\";\n sendJson(res, message === \"payload_too_large\" ? 413 : 400, { error: message });\n\n return;\n }\n\n try {\n const verdict = await evaluateSystemPrompt(systemPrompt, evaluate, body?.instructions);\n sendJson(res, 200, verdict);\n } catch (error) {\n sendJson(res, 502, {\n error: \"evaluate_failed\",\n message: error instanceof Error ? error.message : String(error),\n });\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { Trace, TraceSpan } from "../contracts/trace.type.mjs";
2
2
  import { ReportStatus, ReportType } from "@warlock.js/ai";
3
3
 
4
- //#region ../@warlock.js/ai-panoptic/src/dashboard/trace-filter.d.ts
4
+ //#region ../ai-panoptic/src/dashboard/trace-filter.d.ts
5
5
  /**
6
6
  * Client-side trace-list filter criteria. The dashboard polls the full
7
7
  * trace list every 2s and narrows it in the browser with these — search
@@ -1 +1 @@
1
- {"version":3,"file":"trace-filter.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/trace-filter.ts"],"mappings":";;;;;;AAcA;;;;;;;;;KAAY,WAAA;EA6BV;;;AAKU;AAyBZ;EArDE,IAAA;;;AAqDyC;AAwB3C;EAxEE,QAAA,GAAW,YAAA;;;;;EAKX,KAAA,GAAQ,UAAU;EAmEwB;;AAAmB;AA2C/D;;EAxGE,SAAA;EAwGmC;;;;;;EAjGnC,SAAA;EAiGoD;;;;EA5FpD,UAAA;AAAA;;;;AAqGyB;AAG3B;;;;;;;iBA/EgB,cAAA,CAAe,KAAY,EAAL,KAAK;AAmF5B;AAQf;;;;AARe,iBA3DC,aAAA,CAAc,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,WAAW;;iBA2C/C,YAAA,CAAa,MAAA,EAAQ,KAAA,IAAS,MAAA,EAAQ,WAAA,GAAc,KAAA;;AAwBP;AAyB7D;;;cAxCa,cAAA;AAwCa;AAAA,KArCd,YAAA;EAwCW,yEAtCrB,SAAA,UA0Ca;EAxCb,MAAA,EAAQ,KAAK;AAAA;;;AAwCA;AAcf;;iBA9CgB,cAAA,CAAe,MAAA,EAAQ,KAAA,KAAU,YAAY;;;;;;cAyBhD,aAAA;AA8Cb;AAAA,KA3CY,WAAA;8EAEV,SAAA,UAyCsB;EAvCtB,MAAA,EAAQ,KAAK;AAAA;;;;;;;AA8CA;AAef;;;;iBA/CgB,aAAA,CAAc,MAAA,EAAQ,KAAA,KAAU,WAAW;;;;AA+CJ;AAqBvD;cA3Ca,WAAA;;KAGD,SAAA;EA0CV,8FAxCA,IAAA,UA4CA;EA1CA,MAAA,EAAQ,KAAK;AAAA;;;;;AAoDT;AAON;;;;AAAsD;AAiBtD;;iBA7DgB,WAAA,CAAY,MAAA,EAAQ,KAAA,KAAU,SAAS;;KAqB3C,QAAA;EAoDI,uDAlDd,IAAA;EAEA,KAAA,UAgDsC;EA9CtC,MAAA,UA8CgD;EA5ChD,QAAA,UA4CwD;EA1CxD,GAAA,UAiHwB;EA/GxB,GAAA,UA+G+B;EA7G/B,MAAA,UAiIc;EA/Hd,IAAA;AAAA;;AA+H6D;AAc/D;;iBAtIgB,UAAA,CAAW,MAAA,YAAkB,CAAS;;AAsIX;;;;iBArH3B,SAAA,CAAU,KAAY,EAAL,KAAK;;;;;;;;iBAYtB,eAAA,CAAgB,MAAA,EAAQ,KAAA,KAAU,QAAQ;;;;;;;iBAuE1C,UAAA,CAAW,IAAe,EAAT,SAAS;;;;;;;iBAoB1B,aAAA,CAAc,QAAA,UAAkB,OAAe;;;;;iBAc/C,WAAA,CAAY,IAAe,EAAT,SAAS"}
1
+ {"version":3,"file":"trace-filter.d.mts","names":[],"sources":["../../../../../../../ai-panoptic/src/dashboard/trace-filter.ts"],"mappings":";;;;;;AAcA;;;;;;;;;KAAY,WAAA;EA6BV;;;AAKU;AAyBZ;EArDE,IAAA;;;AAqDyC;AAwB3C;EAxEE,QAAA,GAAW,YAAA;;;;;EAKX,KAAA,GAAQ,UAAU;EAmEwB;;AAAmB;AA2C/D;;EAxGE,SAAA;EAwGmC;;;;;;EAjGnC,SAAA;EAiGoD;;;;EA5FpD,UAAA;AAAA;;;;AAqGyB;AAG3B;;;;;;;iBA/EgB,cAAA,CAAe,KAAY,EAAL,KAAK;AAmF5B;AAQf;;;;AARe,iBA3DC,aAAA,CAAc,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,WAAW;;iBA2C/C,YAAA,CAAa,MAAA,EAAQ,KAAA,IAAS,MAAA,EAAQ,WAAA,GAAc,KAAA;;AAwBP;AAyB7D;;;cAxCa,cAAA;AAwCa;AAAA,KArCd,YAAA;EAwCW,yEAtCrB,SAAA,UA0Ca;EAxCb,MAAA,EAAQ,KAAK;AAAA;;;AAwCA;AAcf;;iBA9CgB,cAAA,CAAe,MAAA,EAAQ,KAAA,KAAU,YAAY;;;;;;cAyBhD,aAAA;AA8Cb;AAAA,KA3CY,WAAA;8EAEV,SAAA,UAyCsB;EAvCtB,MAAA,EAAQ,KAAK;AAAA;;;;;;;AA8CA;AAef;;;;iBA/CgB,aAAA,CAAc,MAAA,EAAQ,KAAA,KAAU,WAAW;;;;AA+CJ;AAqBvD;cA3Ca,WAAA;;KAGD,SAAA;EA0CV,8FAxCA,IAAA,UA4CA;EA1CA,MAAA,EAAQ,KAAK;AAAA;;;;;AAoDT;AAON;;;;AAAsD;AAiBtD;;iBA7DgB,WAAA,CAAY,MAAA,EAAQ,KAAA,KAAU,SAAS;;KAqB3C,QAAA;EAoDI,uDAlDd,IAAA;EAEA,KAAA,UAgDsC;EA9CtC,MAAA,UA8CgD;EA5ChD,QAAA,UA4CwD;EA1CxD,GAAA,UAiHwB;EA/GxB,GAAA,UA+G+B;EA7G/B,MAAA,UAiIc;EA/Hd,IAAA;AAAA;;AA+H6D;AAc/D;;iBAtIgB,UAAA,CAAW,MAAA,YAAkB,CAAS;;AAsIX;;;;iBArH3B,SAAA,CAAU,KAAY,EAAL,KAAK;;;;;;;;iBAYtB,eAAA,CAAgB,MAAA,EAAQ,KAAA,KAAU,QAAQ;;;;;;;iBAuE1C,UAAA,CAAW,IAAe,EAAT,SAAS;;;;;;;iBAoB1B,aAAA,CAAc,QAAA,UAAkB,OAAe;;;;;iBAc/C,WAAA,CAAY,IAAe,EAAT,SAAS"}
@@ -1,4 +1,4 @@
1
- //#region ../@warlock.js/ai-panoptic/src/dashboard/trace-filter.ts
1
+ //#region ../ai-panoptic/src/dashboard/trace-filter.ts
2
2
  /** Statuses the errors-only toggle keeps. */
3
3
  const ERROR_STATUSES = ["failed", "cancelled"];
4
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"trace-filter.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/trace-filter.ts"],"sourcesContent":["import type { ReportStatus, ReportType } from \"@warlock.js/ai\";\nimport type { Trace, TraceSpan } from \"../contracts/trace.type\";\n\n/**\n * Client-side trace-list filter criteria. The dashboard polls the full\n * trace list every 2s and narrows it in the browser with these — search\n * box, filter chips, and the errors-only header toggle all feed one\n * {@link TraceFilter}. Kept as a pure, framework-free module so the exact\n * matching rules are unit-tested here and the inlined dashboard JS in\n * {@link import(\"./ui.html\").dashboardHtml} mirrors them 1:1.\n *\n * Every field is optional — an absent field is \"don't care\". An empty\n * filter therefore matches every trace.\n */\nexport type TraceFilter = {\n /**\n * Free-text needle matched case-insensitively against a trace's root\n * span name and session id. Whitespace-trimmed; an empty/blank string\n * is treated as absent.\n */\n text?: string;\n /**\n * Restrict to these terminal statuses (root span status). Empty array\n * is treated as \"don't care\" (matches every status).\n */\n statuses?: ReportStatus[];\n /**\n * Restrict to these executable types (root span type). Empty array is\n * treated as \"don't care\".\n */\n types?: ReportType[];\n /**\n * Exact session-id chip. When set, only traces whose `sessionId`\n * equals this value match. Distinct from {@link TraceFilter.text},\n * which is a fuzzy substring across name + session.\n */\n sessionId?: string;\n /**\n * Exact prompt chip — a `name@version` key (see {@link tracePromptKey}).\n * When set, only traces whose root span resolved that exact named prompt\n * version match. Mirrors {@link TraceFilter.sessionId} but over the\n * prompt-version-linkage attributes the collector stamps on agent spans.\n */\n promptKey?: string;\n /**\n * Errors-only header toggle. When `true`, only `failed` / `cancelled`\n * traces match — independent of (and intersected with) `statuses`.\n */\n errorsOnly?: boolean;\n};\n\n/** Statuses the errors-only toggle keeps. */\nconst ERROR_STATUSES: readonly ReportStatus[] = [\"failed\", \"cancelled\"] as const;\n\n/**\n * Span-attribute keys the collector stamps with prompt-version linkage when\n * an agent ran against a *named* `ai.prompts` builder. Read here (not\n * imported) so this pure module stays decoupled from the collector.\n */\nconst PROMPT_NAME_ATTR = \"agent.promptName\";\nconst PROMPT_VERSION_ATTR = \"agent.promptVersion\";\n\n/**\n * The `name@version` prompt key a trace falls under, read from its root\n * span's prompt-version-linkage attributes (`agent.promptName` /\n * `agent.promptVersion`). Returns `undefined` when the run carried no named\n * prompt (raw-string / anonymous / no prompt), so callers can bucket those\n * under a synthetic \"no prompt\" key or skip them.\n *\n * The version is included so two runs of the same prompt name at different\n * versions are distinct keys — that is the whole point of the linkage: to\n * group / filter by the *exact* prompt revision that produced a run.\n */\nexport function tracePromptKey(trace: Trace): string | undefined {\n const attributes = trace.root.attributes;\n\n if (attributes === undefined) {\n return undefined;\n }\n\n const name = attributes[PROMPT_NAME_ATTR];\n\n if (typeof name !== \"string\" || name.length === 0) {\n return undefined;\n }\n\n const version = attributes[PROMPT_VERSION_ATTR];\n const versionLabel = typeof version === \"string\" && version.length > 0 ? version : \"1\";\n\n return `${name}@${versionLabel}`;\n}\n\n/**\n * Does a single trace pass the given filter? Pure predicate — all\n * conditions are ANDed; an unset field never excludes. Used by\n * {@link filterTraces} and mirrored by the dashboard's inlined JS.\n */\nexport function matchesFilter(trace: Trace, filter: TraceFilter): boolean {\n const root = trace.root;\n\n if (filter.errorsOnly === true && !ERROR_STATUSES.includes(root.status)) {\n return false;\n }\n\n if (filter.statuses !== undefined && filter.statuses.length > 0) {\n if (!filter.statuses.includes(root.status)) {\n return false;\n }\n }\n\n if (filter.types !== undefined && filter.types.length > 0) {\n if (!filter.types.includes(root.type)) {\n return false;\n }\n }\n\n if (filter.sessionId !== undefined && filter.sessionId.length > 0) {\n if (trace.sessionId !== filter.sessionId) {\n return false;\n }\n }\n\n if (filter.promptKey !== undefined && filter.promptKey.length > 0) {\n if (tracePromptKey(trace) !== filter.promptKey) {\n return false;\n }\n }\n\n const text = filter.text?.trim().toLowerCase();\n if (text !== undefined && text.length > 0) {\n const haystack = `${root.name} ${trace.sessionId ?? \"\"}`.toLowerCase();\n if (!haystack.includes(text)) {\n return false;\n }\n }\n\n return true;\n}\n\n/** Narrow a trace list to those matching the filter, order preserved. */\nexport function filterTraces(traces: Trace[], filter: TraceFilter): Trace[] {\n return traces.filter((trace) => matchesFilter(trace, filter));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by\n * session. Traces without a `sessionId` collapse into one synthetic\n * \"(no session)\" bucket so they remain visible.\n */\nexport const NO_SESSION_KEY = \"(no session)\";\n\n/** One session bucket — its key plus the traces under it, order preserved. */\nexport type SessionGroup = {\n /** The `sessionId`, or {@link NO_SESSION_KEY} for sessionless traces. */\n sessionId: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by session id,\n * preserving first-seen order of both the groups and the traces within\n * each. Sessionless traces bucket under {@link NO_SESSION_KEY}.\n */\nexport function groupBySession(traces: Trace[]): SessionGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.sessionId ?? NO_SESSION_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((sessionId) => ({ sessionId, traces: byKey.get(sessionId) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by prompt\n * version. Runs that resolved no named prompt collapse into one synthetic\n * \"(no prompt)\" bucket so they remain visible.\n */\nexport const NO_PROMPT_KEY = \"(no prompt)\";\n\n/** One prompt-version bucket — its `name@version` key plus its traces. */\nexport type PromptGroup = {\n /** The `name@version` key, or {@link NO_PROMPT_KEY} for unlinked traces. */\n promptKey: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by prompt version —\n * the `name@version` key {@link tracePromptKey} derives from each run's\n * prompt-version-linkage attributes. Preserves first-seen order of both the\n * groups and the traces within each. Runs with no named prompt bucket under\n * {@link NO_PROMPT_KEY}.\n *\n * The dashboard offers this as a second group-by dimension beside session,\n * so a reviewer can see every run of \"support@2\" together and compare its\n * cost / failure rate against \"support@3\".\n */\nexport function groupByPrompt(traces: Trace[]): PromptGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = tracePromptKey(trace) ?? NO_PROMPT_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((promptKey) => ({ promptKey, traces: byKey.get(promptKey) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by root\n * type. `root.type` is always present, so this is only a defensive\n * fallback for a malformed trace with no typed root.\n */\nexport const NO_TYPE_KEY = \"(no type)\";\n\n/** One root-type bucket — its type discriminator plus its traces. */\nexport type TypeGroup = {\n /** The root span's `type` (e.g. `agent` / `workflow` / `planner`), or {@link NO_TYPE_KEY}. */\n type: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by root type —\n * the `agent` / `workflow` / `supervisor` / `orchestrator` / `planner` /\n * `tool` / `batch` / `callback` discriminator on each trace's root span.\n * Preserves first-seen order of both the groups and the traces within each.\n *\n * The dashboard offers this as the coarsest group-by dimension beside\n * session and prompt: when a benchmark floods the flat list with hundreds\n * of one primitive (e.g. standalone `agent` runs), this collapses them\n * behind a per-type header so the structural primitives (the lone planner,\n * the team, the supervisors) are one click away instead of a scroll past.\n */\nexport function groupByType(traces: Trace[]): TypeGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => ({ type, traces: byKey.get(type) ?? [] }));\n}\n\n/** One row of the per-type aggregate-stats panel. */\nexport type TypeStat = {\n /** Root type discriminator, or {@link NO_TYPE_KEY}. */\n type: string;\n /** Number of traces of this type. */\n count: number;\n /** Traces whose status is `failed` / `cancelled`. */\n failed: number;\n /** `failed / count` in `[0, 1]`. */\n failRate: number;\n /** Median (p50) root duration in ms. */\n p50: number;\n /** p95 root duration in ms. */\n p95: number;\n /** Summed `usage.total` tokens across the bucket. */\n tokens: number;\n /** Summed per-trace USD cost across the bucket. */\n cost: number;\n};\n\n/**\n * Nearest-rank percentile of `values` at `p` (0–100). Returns 0 for an\n * empty input and sorts a copy so the caller's array is left untouched.\n */\nexport function percentile(values: number[], p: number): number {\n if (values.length === 0) {\n return 0;\n }\n\n const sorted = values.slice().sort((a, b) => a - b);\n const rank = Math.ceil((p / 100) * sorted.length) - 1;\n const index = Math.min(Math.max(rank, 0), sorted.length - 1);\n\n return sorted[index];\n}\n\n/**\n * One trace's total USD cost: its explicit root-usage cost when priced,\n * else the rollup of its subtree. Mirrors the dashboard's `traceCost` so the\n * stats panel and the trace rows agree to the cent.\n */\nexport function traceCost(trace: Trace): number {\n const own = costSumObj(trace.usage.cost);\n return own > 0 ? own : rollupCost(trace.root);\n}\n\n/**\n * Aggregate an (already filtered) trace list into one {@link TypeStat} row\n * per root type — count, failure rate, p50/p95 latency, total tokens, total\n * cost — preserving first-seen type order. Powers the dashboard's per-type\n * stats panel; computed over the same filtered list the trace view shows, so\n * the panel always reflects the active filters.\n */\nexport function aggregateByType(traces: Trace[]): TypeStat[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => {\n const bucket = byKey.get(type) ?? [];\n const durations = bucket.map((trace) => trace.duration);\n let failed = 0;\n let tokens = 0;\n let cost = 0;\n\n for (const trace of bucket) {\n if (ERROR_STATUSES.includes(trace.root.status)) {\n failed += 1;\n }\n\n tokens += trace.usage.total ?? 0;\n cost += traceCost(trace);\n }\n\n return {\n type,\n count: bucket.length,\n failed,\n failRate: bucket.length > 0 ? failed / bucket.length : 0,\n p50: percentile(durations, 50),\n p95: percentile(durations, 95),\n tokens,\n cost,\n };\n });\n}\n\n/**\n * Sum every priced lane of a usage cost object into one USD number.\n * Mirrors the dashboard's `costSumObj` so heatmap intensities computed\n * here match what the page renders.\n */\nfunction costSumObj(cost: TraceSpan[\"usage\"][\"cost\"]): number {\n if (cost === undefined) {\n return 0;\n }\n\n return (\n (cost.input ?? 0) +\n (cost.output ?? 0) +\n (cost.cachedInput ?? 0) +\n (cost.cachedOutput ?? 0) +\n (cost.reasoning ?? 0)\n );\n}\n\n/**\n * Rollup-aware subtree cost: a node's own cost when it carries one\n * (it already rolls up its trips), otherwise the sum of its children.\n * Avoids double-counting wrapper nodes (workflow/supervisor roots carry\n * tokens but no cost). Mirrors the dashboard's `rollupCost`.\n */\nexport function rollupCost(span: TraceSpan): number {\n const own = costSumObj(span.usage.cost);\n if (own > 0) {\n return own;\n }\n\n let sum = 0;\n for (const child of span.children) {\n sum += rollupCost(child);\n }\n\n return sum;\n}\n\n/**\n * Heatmap intensity in `[0, 1]` for a node's `rollupCost` relative to the\n * trace's most-expensive node. The dashboard tints each tree node's left\n * accent by this. `max <= 0` (a free trace) yields `0` for every node so\n * the heatmap simply stays cold rather than dividing by zero.\n */\nexport function heatIntensity(nodeCost: number, maxCost: number): number {\n if (maxCost <= 0 || nodeCost <= 0) {\n return 0;\n }\n\n const ratio = nodeCost / maxCost;\n\n return ratio > 1 ? 1 : ratio;\n}\n\n/**\n * The largest single-node {@link rollupCost} anywhere in a span tree —\n * the denominator for {@link heatIntensity}. Walks the whole subtree.\n */\nexport function maxNodeCost(root: TraceSpan): number {\n let max = rollupCost(root);\n\n for (const child of root.children) {\n const childMax = maxNodeCost(child);\n if (childMax > max) {\n max = childMax;\n }\n }\n\n return max;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"trace-filter.mjs","names":[],"sources":["../../../../../../../ai-panoptic/src/dashboard/trace-filter.ts"],"sourcesContent":["import type { ReportStatus, ReportType } from \"@warlock.js/ai\";\nimport type { Trace, TraceSpan } from \"../contracts/trace.type\";\n\n/**\n * Client-side trace-list filter criteria. The dashboard polls the full\n * trace list every 2s and narrows it in the browser with these — search\n * box, filter chips, and the errors-only header toggle all feed one\n * {@link TraceFilter}. Kept as a pure, framework-free module so the exact\n * matching rules are unit-tested here and the inlined dashboard JS in\n * {@link import(\"./ui.html\").dashboardHtml} mirrors them 1:1.\n *\n * Every field is optional — an absent field is \"don't care\". An empty\n * filter therefore matches every trace.\n */\nexport type TraceFilter = {\n /**\n * Free-text needle matched case-insensitively against a trace's root\n * span name and session id. Whitespace-trimmed; an empty/blank string\n * is treated as absent.\n */\n text?: string;\n /**\n * Restrict to these terminal statuses (root span status). Empty array\n * is treated as \"don't care\" (matches every status).\n */\n statuses?: ReportStatus[];\n /**\n * Restrict to these executable types (root span type). Empty array is\n * treated as \"don't care\".\n */\n types?: ReportType[];\n /**\n * Exact session-id chip. When set, only traces whose `sessionId`\n * equals this value match. Distinct from {@link TraceFilter.text},\n * which is a fuzzy substring across name + session.\n */\n sessionId?: string;\n /**\n * Exact prompt chip — a `name@version` key (see {@link tracePromptKey}).\n * When set, only traces whose root span resolved that exact named prompt\n * version match. Mirrors {@link TraceFilter.sessionId} but over the\n * prompt-version-linkage attributes the collector stamps on agent spans.\n */\n promptKey?: string;\n /**\n * Errors-only header toggle. When `true`, only `failed` / `cancelled`\n * traces match — independent of (and intersected with) `statuses`.\n */\n errorsOnly?: boolean;\n};\n\n/** Statuses the errors-only toggle keeps. */\nconst ERROR_STATUSES: readonly ReportStatus[] = [\"failed\", \"cancelled\"] as const;\n\n/**\n * Span-attribute keys the collector stamps with prompt-version linkage when\n * an agent ran against a *named* `ai.prompts` builder. Read here (not\n * imported) so this pure module stays decoupled from the collector.\n */\nconst PROMPT_NAME_ATTR = \"agent.promptName\";\nconst PROMPT_VERSION_ATTR = \"agent.promptVersion\";\n\n/**\n * The `name@version` prompt key a trace falls under, read from its root\n * span's prompt-version-linkage attributes (`agent.promptName` /\n * `agent.promptVersion`). Returns `undefined` when the run carried no named\n * prompt (raw-string / anonymous / no prompt), so callers can bucket those\n * under a synthetic \"no prompt\" key or skip them.\n *\n * The version is included so two runs of the same prompt name at different\n * versions are distinct keys — that is the whole point of the linkage: to\n * group / filter by the *exact* prompt revision that produced a run.\n */\nexport function tracePromptKey(trace: Trace): string | undefined {\n const attributes = trace.root.attributes;\n\n if (attributes === undefined) {\n return undefined;\n }\n\n const name = attributes[PROMPT_NAME_ATTR];\n\n if (typeof name !== \"string\" || name.length === 0) {\n return undefined;\n }\n\n const version = attributes[PROMPT_VERSION_ATTR];\n const versionLabel = typeof version === \"string\" && version.length > 0 ? version : \"1\";\n\n return `${name}@${versionLabel}`;\n}\n\n/**\n * Does a single trace pass the given filter? Pure predicate — all\n * conditions are ANDed; an unset field never excludes. Used by\n * {@link filterTraces} and mirrored by the dashboard's inlined JS.\n */\nexport function matchesFilter(trace: Trace, filter: TraceFilter): boolean {\n const root = trace.root;\n\n if (filter.errorsOnly === true && !ERROR_STATUSES.includes(root.status)) {\n return false;\n }\n\n if (filter.statuses !== undefined && filter.statuses.length > 0) {\n if (!filter.statuses.includes(root.status)) {\n return false;\n }\n }\n\n if (filter.types !== undefined && filter.types.length > 0) {\n if (!filter.types.includes(root.type)) {\n return false;\n }\n }\n\n if (filter.sessionId !== undefined && filter.sessionId.length > 0) {\n if (trace.sessionId !== filter.sessionId) {\n return false;\n }\n }\n\n if (filter.promptKey !== undefined && filter.promptKey.length > 0) {\n if (tracePromptKey(trace) !== filter.promptKey) {\n return false;\n }\n }\n\n const text = filter.text?.trim().toLowerCase();\n if (text !== undefined && text.length > 0) {\n const haystack = `${root.name} ${trace.sessionId ?? \"\"}`.toLowerCase();\n if (!haystack.includes(text)) {\n return false;\n }\n }\n\n return true;\n}\n\n/** Narrow a trace list to those matching the filter, order preserved. */\nexport function filterTraces(traces: Trace[], filter: TraceFilter): Trace[] {\n return traces.filter((trace) => matchesFilter(trace, filter));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by\n * session. Traces without a `sessionId` collapse into one synthetic\n * \"(no session)\" bucket so they remain visible.\n */\nexport const NO_SESSION_KEY = \"(no session)\";\n\n/** One session bucket — its key plus the traces under it, order preserved. */\nexport type SessionGroup = {\n /** The `sessionId`, or {@link NO_SESSION_KEY} for sessionless traces. */\n sessionId: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by session id,\n * preserving first-seen order of both the groups and the traces within\n * each. Sessionless traces bucket under {@link NO_SESSION_KEY}.\n */\nexport function groupBySession(traces: Trace[]): SessionGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.sessionId ?? NO_SESSION_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((sessionId) => ({ sessionId, traces: byKey.get(sessionId) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by prompt\n * version. Runs that resolved no named prompt collapse into one synthetic\n * \"(no prompt)\" bucket so they remain visible.\n */\nexport const NO_PROMPT_KEY = \"(no prompt)\";\n\n/** One prompt-version bucket — its `name@version` key plus its traces. */\nexport type PromptGroup = {\n /** The `name@version` key, or {@link NO_PROMPT_KEY} for unlinked traces. */\n promptKey: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by prompt version —\n * the `name@version` key {@link tracePromptKey} derives from each run's\n * prompt-version-linkage attributes. Preserves first-seen order of both the\n * groups and the traces within each. Runs with no named prompt bucket under\n * {@link NO_PROMPT_KEY}.\n *\n * The dashboard offers this as a second group-by dimension beside session,\n * so a reviewer can see every run of \"support@2\" together and compare its\n * cost / failure rate against \"support@3\".\n */\nexport function groupByPrompt(traces: Trace[]): PromptGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = tracePromptKey(trace) ?? NO_PROMPT_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((promptKey) => ({ promptKey, traces: byKey.get(promptKey) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by root\n * type. `root.type` is always present, so this is only a defensive\n * fallback for a malformed trace with no typed root.\n */\nexport const NO_TYPE_KEY = \"(no type)\";\n\n/** One root-type bucket — its type discriminator plus its traces. */\nexport type TypeGroup = {\n /** The root span's `type` (e.g. `agent` / `workflow` / `planner`), or {@link NO_TYPE_KEY}. */\n type: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by root type —\n * the `agent` / `workflow` / `supervisor` / `orchestrator` / `planner` /\n * `tool` / `batch` / `callback` discriminator on each trace's root span.\n * Preserves first-seen order of both the groups and the traces within each.\n *\n * The dashboard offers this as the coarsest group-by dimension beside\n * session and prompt: when a benchmark floods the flat list with hundreds\n * of one primitive (e.g. standalone `agent` runs), this collapses them\n * behind a per-type header so the structural primitives (the lone planner,\n * the team, the supervisors) are one click away instead of a scroll past.\n */\nexport function groupByType(traces: Trace[]): TypeGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => ({ type, traces: byKey.get(type) ?? [] }));\n}\n\n/** One row of the per-type aggregate-stats panel. */\nexport type TypeStat = {\n /** Root type discriminator, or {@link NO_TYPE_KEY}. */\n type: string;\n /** Number of traces of this type. */\n count: number;\n /** Traces whose status is `failed` / `cancelled`. */\n failed: number;\n /** `failed / count` in `[0, 1]`. */\n failRate: number;\n /** Median (p50) root duration in ms. */\n p50: number;\n /** p95 root duration in ms. */\n p95: number;\n /** Summed `usage.total` tokens across the bucket. */\n tokens: number;\n /** Summed per-trace USD cost across the bucket. */\n cost: number;\n};\n\n/**\n * Nearest-rank percentile of `values` at `p` (0–100). Returns 0 for an\n * empty input and sorts a copy so the caller's array is left untouched.\n */\nexport function percentile(values: number[], p: number): number {\n if (values.length === 0) {\n return 0;\n }\n\n const sorted = values.slice().sort((a, b) => a - b);\n const rank = Math.ceil((p / 100) * sorted.length) - 1;\n const index = Math.min(Math.max(rank, 0), sorted.length - 1);\n\n return sorted[index];\n}\n\n/**\n * One trace's total USD cost: its explicit root-usage cost when priced,\n * else the rollup of its subtree. Mirrors the dashboard's `traceCost` so the\n * stats panel and the trace rows agree to the cent.\n */\nexport function traceCost(trace: Trace): number {\n const own = costSumObj(trace.usage.cost);\n return own > 0 ? own : rollupCost(trace.root);\n}\n\n/**\n * Aggregate an (already filtered) trace list into one {@link TypeStat} row\n * per root type — count, failure rate, p50/p95 latency, total tokens, total\n * cost — preserving first-seen type order. Powers the dashboard's per-type\n * stats panel; computed over the same filtered list the trace view shows, so\n * the panel always reflects the active filters.\n */\nexport function aggregateByType(traces: Trace[]): TypeStat[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => {\n const bucket = byKey.get(type) ?? [];\n const durations = bucket.map((trace) => trace.duration);\n let failed = 0;\n let tokens = 0;\n let cost = 0;\n\n for (const trace of bucket) {\n if (ERROR_STATUSES.includes(trace.root.status)) {\n failed += 1;\n }\n\n tokens += trace.usage.total ?? 0;\n cost += traceCost(trace);\n }\n\n return {\n type,\n count: bucket.length,\n failed,\n failRate: bucket.length > 0 ? failed / bucket.length : 0,\n p50: percentile(durations, 50),\n p95: percentile(durations, 95),\n tokens,\n cost,\n };\n });\n}\n\n/**\n * Sum every priced lane of a usage cost object into one USD number.\n * Mirrors the dashboard's `costSumObj` so heatmap intensities computed\n * here match what the page renders.\n */\nfunction costSumObj(cost: TraceSpan[\"usage\"][\"cost\"]): number {\n if (cost === undefined) {\n return 0;\n }\n\n return (\n (cost.input ?? 0) +\n (cost.output ?? 0) +\n (cost.cachedInput ?? 0) +\n (cost.cachedOutput ?? 0) +\n (cost.reasoning ?? 0)\n );\n}\n\n/**\n * Rollup-aware subtree cost: a node's own cost when it carries one\n * (it already rolls up its trips), otherwise the sum of its children.\n * Avoids double-counting wrapper nodes (workflow/supervisor roots carry\n * tokens but no cost). Mirrors the dashboard's `rollupCost`.\n */\nexport function rollupCost(span: TraceSpan): number {\n const own = costSumObj(span.usage.cost);\n if (own > 0) {\n return own;\n }\n\n let sum = 0;\n for (const child of span.children) {\n sum += rollupCost(child);\n }\n\n return sum;\n}\n\n/**\n * Heatmap intensity in `[0, 1]` for a node's `rollupCost` relative to the\n * trace's most-expensive node. The dashboard tints each tree node's left\n * accent by this. `max <= 0` (a free trace) yields `0` for every node so\n * the heatmap simply stays cold rather than dividing by zero.\n */\nexport function heatIntensity(nodeCost: number, maxCost: number): number {\n if (maxCost <= 0 || nodeCost <= 0) {\n return 0;\n }\n\n const ratio = nodeCost / maxCost;\n\n return ratio > 1 ? 1 : ratio;\n}\n\n/**\n * The largest single-node {@link rollupCost} anywhere in a span tree —\n * the denominator for {@link heatIntensity}. Walks the whole subtree.\n */\nexport function maxNodeCost(root: TraceSpan): number {\n let max = rollupCost(root);\n\n for (const child of root.children) {\n const childMax = maxNodeCost(child);\n if (childMax > max) {\n max = childMax;\n }\n }\n\n return max;\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { WARLOCK_LOGO_DATA_URI } from "./warlock-logo.mjs";
2
2
 
3
- //#region ../@warlock.js/ai-panoptic/src/dashboard/ui.html.ts
3
+ //#region ../ai-panoptic/src/dashboard/ui.html.ts
4
4
  /**
5
5
  * Build the single self-contained dashboard HTML page. No external
6
6
  * assets, no bundler, no framework — one inlined string that polls the