@obsunified/collector 2.2.3 → 2.4.3

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 (58) hide show
  1. package/dist/auth.d.ts +2 -1
  2. package/dist/{chunk-ZSF5XP3B.js → chunk-RASSTGNB.js} +30 -22
  3. package/dist/chunk-RASSTGNB.js.map +1 -0
  4. package/dist/index.d.ts +264 -62
  5. package/dist/index.js +3390 -2350
  6. package/dist/index.js.map +1 -1
  7. package/dist/{ingest-auth-C5-8Mly5.d.ts → ingest-auth-BTowieER.d.ts} +96 -1
  8. package/dist/{openai-GSJZGFGL.js → openai-VDKVVZAV.js} +2 -2
  9. package/package.json +4 -2
  10. package/src/__tests__/autosre-closed-loop.test.ts +110 -0
  11. package/src/__tests__/db-parity.integration.test.ts +961 -0
  12. package/src/__tests__/internal-contracts-routes.test.ts +451 -0
  13. package/src/framework/env.ts +113 -0
  14. package/src/index.ts +27 -4
  15. package/src/lib/ai-store/evaluations.test.ts +3 -3
  16. package/src/lib/alert-email.ts +104 -0
  17. package/src/lib/alerts-store.test.ts +116 -2
  18. package/src/lib/alerts-store.ts +421 -57
  19. package/src/lib/analyses-store.test.ts +6 -6
  20. package/src/lib/db-parity.ts +883 -0
  21. package/src/lib/eval-cases-store.test.ts +1 -1
  22. package/src/lib/eval-cases-store.ts +16 -4
  23. package/src/lib/evidence-references.ts +30 -22
  24. package/src/lib/metric-alerts.test.ts +769 -0
  25. package/src/lib/metric-alerts.ts +640 -0
  26. package/src/lib/metrics-store.ts +9 -2
  27. package/src/lib/sql-db-postgres.integration.test.ts +247 -0
  28. package/src/lib/stage6.test.ts +1 -1
  29. package/src/lib/store/trace-queries.ts +1 -1
  30. package/src/migrations/054_alert_rules_pending_secs.sql +11 -0
  31. package/src/migrations/055_alert_state_pending_since.sql +12 -0
  32. package/src/migrations/056_metric_point_series_received.sql +9 -0
  33. package/src/migrations/057_alert_evaluations_outcome.sql +11 -0
  34. package/src/migrations/058_alert_state_pending_seen_at.sql +12 -0
  35. package/src/migrations/059_alert_evaluations_error.sql +11 -0
  36. package/src/migrations/parity.test.ts +105 -3
  37. package/src/migrations-postgres/054_alert_rules_pending_secs.sql +7 -0
  38. package/src/migrations-postgres/055_alert_state_pending_since.sql +6 -0
  39. package/src/migrations-postgres/056_metric_point_series_received.sql +7 -0
  40. package/src/migrations-postgres/057_alert_evaluations_outcome.sql +6 -0
  41. package/src/migrations-postgres/058_alert_state_pending_seen_at.sql +6 -0
  42. package/src/migrations-postgres/059_alert_evaluations_error.sql +6 -0
  43. package/src/migrations-postgres/README.md +13 -0
  44. package/src/plugins/ai-receiver.ts +6 -1
  45. package/src/plugins/alerts-evaluator-pending.test.ts +322 -0
  46. package/src/plugins/alerts-evaluator-retention.test.ts +2 -0
  47. package/src/plugins/alerts-evaluator.test.ts +10 -2
  48. package/src/plugins/alerts-evaluator.ts +239 -40
  49. package/src/plugins/alerts-routes.ts +17 -2
  50. package/src/plugins/autosre-routes.ts +251 -140
  51. package/src/plugins/connected-routes/manifest.test.ts +114 -32
  52. package/src/plugins/connected-routes/manifest.ts +59 -76
  53. package/src/plugins/connected-routes.test.ts +10 -8
  54. package/src/plugins/connected-routes.ts +42 -12
  55. package/src/plugins/eval-cases-routes.ts +33 -10
  56. package/src/plugins/evidence-retrieval-routes.ts +60 -25
  57. package/dist/chunk-ZSF5XP3B.js.map +0 -1
  58. /package/dist/{openai-GSJZGFGL.js.map → openai-VDKVVZAV.js.map} +0 -0
package/dist/auth.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import 'hono';
2
- export { e as createIngestAuth, r as resetIngestAuthCache } from './ingest-auth-C5-8Mly5.js';
2
+ export { p as createIngestAuth, s as resetIngestAuthCache } from './ingest-auth-BTowieER.js';
3
+ import '@obsunified/types';
@@ -1,5 +1,6 @@
1
1
  // src/lib/evidence-references.ts
2
- var analysisRoute = (analysisId) => `#/investigate/${encodeURIComponent(analysisId)}`;
2
+ import { dashboardRouteToHref, hrefForEntity } from "@obsunified/types";
3
+ var analysisRoute = (analysisId) => dashboardRouteToHref({ tab: "investigate", investigationId: analysisId });
3
4
  var analysisEvidenceReference = (evidence) => {
4
5
  const route = analysisRoute(evidence.analysisId);
5
6
  const status = evidence.result?.status ?? "unknown";
@@ -48,46 +49,53 @@ var valueFor = (row, headerIndex, names) => {
48
49
  }
49
50
  return null;
50
51
  };
51
- var traceRoute = (traceId) => `#/traces/${encodeURIComponent(traceId)}`;
52
- var spanRoute = (traceId, spanId) => `${traceRoute(traceId)}#span=${encodeURIComponent(spanId)}`;
52
+ var traceRoute = (traceId) => dashboardRouteToHref({ tab: "traces", traceId });
53
+ var spanRoute = (traceId, spanId) => dashboardRouteToHref({ tab: "traces", traceId, spanId });
53
54
  var evidenceRouteFor = (entityKind, entityId, traceId) => {
54
55
  switch (entityKind) {
55
56
  case "analysis":
56
57
  return analysisRoute(entityId);
57
58
  case "action":
58
- return `#/actions/${encodeURIComponent(entityId)}`;
59
+ return dashboardRouteToHref({ tab: "actions", actionId: entityId });
59
60
  case "alert":
60
- return `#/alerts?alert=${encodeURIComponent(entityId)}`;
61
+ return dashboardRouteToHref({ tab: "alerts", alertId: entityId });
61
62
  case "agent_run":
62
- return `#/agent-runs/${encodeURIComponent(entityId)}`;
63
+ return dashboardRouteToHref({ tab: "agent-runs", agentRunId: entityId });
63
64
  case "ai_call":
64
- return `#/ai/calls/${encodeURIComponent(entityId)}`;
65
+ return dashboardRouteToHref({ tab: "ai", aiCallId: entityId });
65
66
  case "eval":
66
- return `#/evals/${encodeURIComponent(entityId)}`;
67
+ return dashboardRouteToHref({
68
+ tab: "evaluations",
69
+ evaluationId: entityId
70
+ });
67
71
  case "log":
68
- return `#/logs?id=${encodeURIComponent(entityId)}`;
72
+ return dashboardRouteToHref({ tab: "logs", logId: entityId });
69
73
  case "profile":
70
- return traceId ? `#/profiles/${encodeURIComponent(entityId)}?trace_id=${encodeURIComponent(traceId)}` : `#/profiles/${encodeURIComponent(entityId)}`;
74
+ return dashboardRouteToHref({
75
+ tab: "profiles",
76
+ profileId: entityId,
77
+ ...traceId ? { traceId } : {}
78
+ });
71
79
  case "replay":
72
- return `#/replay?session=${encodeURIComponent(entityId)}`;
80
+ return dashboardRouteToHref({ tab: "replay", sessionId: entityId });
73
81
  case "service":
74
- return `#/traces?service=${encodeURIComponent(entityId)}`;
82
+ return dashboardRouteToHref({ tab: "service-map", service: entityId });
75
83
  case "span": {
76
84
  const [tracePart, spanPart] = entityId.split(":");
77
- return spanPart ? spanRoute(tracePart, spanPart) : `#/spans/${entityId}`;
85
+ return spanPart ? spanRoute(tracePart, spanPart) : hrefForEntity("span", entityId, traceId ? { traceId } : void 0);
78
86
  }
79
87
  case "tool_call":
80
- return `#/tool-calls/${encodeURIComponent(entityId)}`;
88
+ return dashboardRouteToHref({ tab: "tool-calls", toolCallId: entityId });
81
89
  case "trace":
82
90
  return traceRoute(entityId);
83
91
  case "docs":
84
- return entityId.startsWith("#/") ? entityId : `#/docs/${entityId}`;
92
+ return hrefForEntity("docs", entityId);
85
93
  case "change":
86
- return `#/changes/${encodeURIComponent(entityId)}`;
94
+ return dashboardRouteToHref({ tab: "changes", changeId: entityId });
87
95
  case "finding":
88
- return `#/findings/${encodeURIComponent(entityId)}`;
96
+ return dashboardRouteToHref({ tab: "findings", findingId: entityId });
89
97
  case "incident":
90
- return `#/incidents/${encodeURIComponent(entityId)}`;
98
+ return dashboardRouteToHref({ tab: "incidents", incidentId: entityId });
91
99
  }
92
100
  const exhaustive = entityKind;
93
101
  return exhaustive;
@@ -140,7 +148,7 @@ var instrumentationGapEvidenceReferences = (result) => {
140
148
  const durationText = durationMs === null ? "unknown duration" : `${durationMs}ms`;
141
149
  const ratioText = ratio === null ? "" : `, ${(ratio * 100).toFixed(1)}% of parent`;
142
150
  const thresholdText = thresholdVersion ? ` Thresholds: ${thresholdVersion}.` : "";
143
- const docsRoute = "#/docs/howto/ebpf";
151
+ const docsRoute = hrefForEntity("docs", "howto/ebpf");
144
152
  return {
145
153
  evidenceId: `analysis:${result.analysisId}:instrumentation-gap:${index}:span:${entityId}`,
146
154
  entityKind: "span",
@@ -169,7 +177,7 @@ var instrumentationGapEvidenceReferences = (result) => {
169
177
  label: "Open trace gap data",
170
178
  entityKind: "trace",
171
179
  entityId: traceId,
172
- route: `${traceRoute(traceId)}/gaps`,
180
+ route: traceRoute(traceId),
173
181
  reason: "Review all structured instrumentation blindspots for this trace."
174
182
  },
175
183
  {
@@ -604,7 +612,7 @@ var LlmCallError = class extends Error {
604
612
  };
605
613
  async function generateNarrative(req, config) {
606
614
  if (config.provider === "openai") {
607
- const { generateNarrativeOpenAI } = await import("./openai-GSJZGFGL.js");
615
+ const { generateNarrativeOpenAI } = await import("./openai-VDKVVZAV.js");
608
616
  return generateNarrativeOpenAI(req, config);
609
617
  }
610
618
  return generateNarrativeAnthropic(req, config);
@@ -674,4 +682,4 @@ export {
674
682
  LlmCallError,
675
683
  generateNarrative
676
684
  };
677
- //# sourceMappingURL=chunk-ZSF5XP3B.js.map
685
+ //# sourceMappingURL=chunk-RASSTGNB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/evidence-references.ts","../src/lib/llm.ts"],"sourcesContent":["import type {\n\tAIEvaluationRecord,\n\tAlertEvaluation,\n\tAlertRule,\n\tAnalysisResult,\n\tAskEvidence,\n\tEvidenceCitation,\n\tEvidenceEntityKind,\n\tEvidenceNextPivot,\n\tEvidenceReference,\n\tJsonValue,\n} from \"@obsunified/types\";\nimport { dashboardRouteToHref, hrefForEntity } from \"@obsunified/types\";\n\nconst analysisRoute = (analysisId: string) =>\n\tdashboardRouteToHref({ tab: \"investigate\", investigationId: analysisId });\n\nexport const analysisEvidenceReference = (\n\tevidence: AskEvidence,\n): EvidenceReference => {\n\tconst route = analysisRoute(evidence.analysisId);\n\tconst status = evidence.result?.status ?? \"unknown\";\n\treturn {\n\t\tevidenceId: `ask:analysis:${evidence.analysisId}`,\n\t\tentityKind: \"analysis\",\n\t\tentityId: evidence.analysisId,\n\t\troute,\n\t\tsource: \"ask.run_analysis\",\n\t\tconfidence: evidence.result ? 1 : 0.55,\n\t\treason: evidence.result\n\t\t\t? `Ask consulted analysis \"${evidence.definition.title}\" with latest status ${status}.`\n\t\t\t: `Ask consulted analysis \"${evidence.definition.title}\", but no latest result was available.`,\n\t\tcitations: [\n\t\t\t{\n\t\t\t\tlabel: evidence.definition.title,\n\t\t\t\tentityKind: \"analysis\",\n\t\t\t\tentityId: evidence.analysisId,\n\t\t\t\troute,\n\t\t\t},\n\t\t],\n\t\tsuggestedNextPivots: [\n\t\t\t{\n\t\t\t\tlabel: \"Open investigation\",\n\t\t\t\tentityKind: \"analysis\",\n\t\t\t\tentityId: evidence.analysisId,\n\t\t\t\troute,\n\t\t\t\treason:\n\t\t\t\t\t\"Review the analysis payload, narrative, and connected telemetry rail.\",\n\t\t\t},\n\t\t],\n\t};\n};\n\nexport const askEvidenceReferences = (\n\tevidence: Iterable<AskEvidence>,\n): EvidenceReference[] =>\n\t[...evidence].map((item) => analysisEvidenceReference(item));\n\nconst MAX_ANALYSIS_EVIDENCE_REFERENCES = 50;\n\ninterface EvidenceTable {\n\ttitle?: string | null;\n\theaders: string[];\n\trows: JsonValue[][];\n}\n\ninterface RowEntity {\n\tentityKind: EvidenceEntityKind;\n\tentityId: string;\n\troute: string;\n\tlabel: string;\n\tconfidence: number;\n\tcitations: EvidenceCitation[];\n\tsuggestedNextPivots: EvidenceNextPivot[];\n}\n\nconst isRecord = (value: unknown): value is Record<string, JsonValue> =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value);\n\nconst asString = (value: JsonValue | undefined): string | null => {\n\tif (typeof value === \"string\" && value.trim()) return value.trim();\n\tif (typeof value === \"number\" && Number.isFinite(value)) return String(value);\n\treturn null;\n};\n\nconst normalizeHeader = (header: string) =>\n\theader\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(/[\\s-]+/g, \"_\");\n\nconst valueFor = (\n\trow: JsonValue[],\n\theaderIndex: Map<string, number>,\n\tnames: string[],\n): string | null => {\n\tfor (const name of names) {\n\t\tconst index = headerIndex.get(name);\n\t\tif (index === undefined) continue;\n\t\tconst value = asString(row[index]);\n\t\tif (value) return value;\n\t}\n\treturn null;\n};\n\nconst traceRoute = (traceId: string) =>\n\tdashboardRouteToHref({ tab: \"traces\", traceId });\n\nconst spanRoute = (traceId: string, spanId: string) =>\n\tdashboardRouteToHref({ tab: \"traces\", traceId, spanId });\n\nexport const evidenceRouteFor = (\n\tentityKind: EvidenceEntityKind,\n\tentityId: string,\n\ttraceId?: string | null,\n) => {\n\tswitch (entityKind) {\n\t\tcase \"analysis\":\n\t\t\treturn analysisRoute(entityId);\n\t\tcase \"action\":\n\t\t\treturn dashboardRouteToHref({ tab: \"actions\", actionId: entityId });\n\t\tcase \"alert\":\n\t\t\treturn dashboardRouteToHref({ tab: \"alerts\", alertId: entityId });\n\t\tcase \"agent_run\":\n\t\t\treturn dashboardRouteToHref({ tab: \"agent-runs\", agentRunId: entityId });\n\t\tcase \"ai_call\":\n\t\t\treturn dashboardRouteToHref({ tab: \"ai\", aiCallId: entityId });\n\t\tcase \"eval\":\n\t\t\treturn dashboardRouteToHref({\n\t\t\t\ttab: \"evaluations\",\n\t\t\t\tevaluationId: entityId,\n\t\t\t});\n\t\tcase \"log\":\n\t\t\treturn dashboardRouteToHref({ tab: \"logs\", logId: entityId });\n\t\tcase \"profile\":\n\t\t\treturn dashboardRouteToHref({\n\t\t\t\ttab: \"profiles\",\n\t\t\t\tprofileId: entityId,\n\t\t\t\t...(traceId ? { traceId } : {}),\n\t\t\t});\n\t\tcase \"replay\":\n\t\t\treturn dashboardRouteToHref({ tab: \"replay\", sessionId: entityId });\n\t\tcase \"service\":\n\t\t\treturn dashboardRouteToHref({ tab: \"service-map\", service: entityId });\n\t\tcase \"span\": {\n\t\t\tconst [tracePart, spanPart] = entityId.split(\":\");\n\t\t\treturn spanPart\n\t\t\t\t? spanRoute(tracePart, spanPart)\n\t\t\t\t: hrefForEntity(\"span\", entityId, traceId ? { traceId } : undefined);\n\t\t}\n\t\tcase \"tool_call\":\n\t\t\treturn dashboardRouteToHref({ tab: \"tool-calls\", toolCallId: entityId });\n\t\tcase \"trace\":\n\t\t\treturn traceRoute(entityId);\n\t\tcase \"docs\":\n\t\t\treturn hrefForEntity(\"docs\", entityId);\n\t\tcase \"change\":\n\t\t\treturn dashboardRouteToHref({ tab: \"changes\", changeId: entityId });\n\t\tcase \"finding\":\n\t\t\treturn dashboardRouteToHref({ tab: \"findings\", findingId: entityId });\n\t\tcase \"incident\":\n\t\t\treturn dashboardRouteToHref({ tab: \"incidents\", incidentId: entityId });\n\t}\n\tconst exhaustive: never = entityKind;\n\treturn exhaustive;\n};\n\nconst parseEvidenceTables = (\n\tpayload: Record<string, unknown>,\n): Array<{ sectionName: string; table: EvidenceTable }> => {\n\tconst evidence = payload.evidence;\n\tif (!isRecord(evidence)) return [];\n\n\tconst tables: Array<{ sectionName: string; table: EvidenceTable }> = [];\n\tfor (const [sectionName, section] of Object.entries(evidence)) {\n\t\tif (!isRecord(section)) continue;\n\t\tconst headers = section.headers;\n\t\tconst rows = section.rows;\n\t\tif (\n\t\t\t!Array.isArray(headers) ||\n\t\t\t!headers.every(\n\t\t\t\t(header): header is string => typeof header === \"string\",\n\t\t\t) ||\n\t\t\t!Array.isArray(rows)\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst normalizedRows = rows.filter((row): row is JsonValue[] =>\n\t\t\tArray.isArray(row),\n\t\t);\n\t\ttables.push({\n\t\t\tsectionName,\n\t\t\ttable: {\n\t\t\t\ttitle: asString(section.title),\n\t\t\t\theaders,\n\t\t\t\trows: normalizedRows,\n\t\t\t},\n\t\t});\n\t}\n\treturn tables;\n};\n\nconst instrumentationGapEvidenceReferences = (\n\tresult: Pick<AnalysisResult, \"analysisId\" | \"payload\">,\n): EvidenceReference[] => {\n\tconst gaps = result.payload.instrumentationGaps;\n\tif (!isRecord(gaps)) return [];\n\tconst traceId = asString(gaps.traceId);\n\tconst blindspots = gaps.blindspots;\n\tif (!traceId || !Array.isArray(blindspots)) return [];\n\n\treturn blindspots\n\t\t.filter(isRecord)\n\t\t.map((gap, index): EvidenceReference | null => {\n\t\t\tconst spanId = asString(gap.parentSpanId);\n\t\t\tif (!spanId) return null;\n\t\t\tconst serviceName = asString(gap.parentServiceName);\n\t\t\tconst spanName = asString(gap.parentSpanName);\n\t\t\tconst durationMs =\n\t\t\t\ttypeof gap.durationMs === \"number\" && Number.isFinite(gap.durationMs)\n\t\t\t\t\t? gap.durationMs\n\t\t\t\t\t: null;\n\t\t\tconst ratio =\n\t\t\t\ttypeof gap.ratioOfParent === \"number\" &&\n\t\t\t\tNumber.isFinite(gap.ratioOfParent)\n\t\t\t\t\t? gap.ratioOfParent\n\t\t\t\t\t: null;\n\t\t\tconst recommendation = asString(gap.recommendation);\n\t\t\tconst thresholdVersion =\n\t\t\t\tasString(gap.thresholdVersion) ??\n\t\t\t\t(isRecord(gaps.thresholds) ? asString(gaps.thresholds.version) : null);\n\t\t\tconst entityId = `${traceId}:${spanId}`;\n\t\t\tconst route = spanRoute(traceId, spanId);\n\t\t\tconst label = spanName ? `${spanName} (${spanId})` : `span ${spanId}`;\n\t\t\tconst durationText =\n\t\t\t\tdurationMs === null ? \"unknown duration\" : `${durationMs}ms`;\n\t\t\tconst ratioText =\n\t\t\t\tratio === null ? \"\" : `, ${(ratio * 100).toFixed(1)}% of parent`;\n\t\t\tconst thresholdText = thresholdVersion\n\t\t\t\t? ` Thresholds: ${thresholdVersion}.`\n\t\t\t\t: \"\";\n\t\t\tconst docsRoute = hrefForEntity(\"docs\", \"howto/ebpf\");\n\t\t\treturn {\n\t\t\t\tevidenceId: `analysis:${result.analysisId}:instrumentation-gap:${index}:span:${entityId}`,\n\t\t\t\tentityKind: \"span\",\n\t\t\t\tentityId,\n\t\t\t\troute,\n\t\t\t\tsource: \"analysis.payload.instrumentationGaps\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treason: `Analysis \"${result.analysisId}\" found uninstrumented self-time on ${label}: ${durationText}${ratioText}.${thresholdText}${recommendation ? ` ${recommendation}` : \"\"}`,\n\t\t\t\tcitations: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: `trace ${traceId}`,\n\t\t\t\t\t\tentityKind: \"trace\",\n\t\t\t\t\t\tentityId: traceId,\n\t\t\t\t\t\troute: traceRoute(traceId),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tsuggestedNextPivots: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: \"Open uninstrumented span\",\n\t\t\t\t\t\tentityKind: \"span\",\n\t\t\t\t\t\tentityId,\n\t\t\t\t\t\troute,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t\"Inspect the parent span whose self-time suggests missing child instrumentation.\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: \"Open trace gap data\",\n\t\t\t\t\t\tentityKind: \"trace\",\n\t\t\t\t\t\tentityId: traceId,\n\t\t\t\t\t\troute: traceRoute(traceId),\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t\"Review all structured instrumentation blindspots for this trace.\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: \"Open profiler setup docs\",\n\t\t\t\t\t\tentityKind: \"docs\",\n\t\t\t\t\t\tentityId: \"howto/ebpf\",\n\t\t\t\t\t\troute: docsRoute,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\tserviceName != null\n\t\t\t\t\t\t\t\t? `Use profiler or eBPF setup guidance to cover missing spans in ${serviceName}.`\n\t\t\t\t\t\t\t\t: \"Use profiler or eBPF setup guidance to cover missing spans.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t})\n\t\t.filter((ref): ref is EvidenceReference => ref !== null)\n\t\t.slice(0, MAX_ANALYSIS_EVIDENCE_REFERENCES);\n};\n\nconst entityFromRow = (\n\trow: JsonValue[],\n\theaders: string[],\n): RowEntity | null => {\n\tconst headerIndex = new Map(headers.map((header, index) => [header, index]));\n\tconst traceId = valueFor(row, headerIndex, [\"trace_id\", \"traceid\", \"trace\"]);\n\tconst spanId = valueFor(row, headerIndex, [\"span_id\", \"spanid\"]);\n\tconst logId = valueFor(row, headerIndex, [\"log_id\", \"logid\"]);\n\tconst profileId = valueFor(row, headerIndex, [\"profile_id\", \"profileid\"]);\n\tconst actionId = valueFor(row, headerIndex, [\"action_id\", \"actionid\"]);\n\tconst agentRunId = valueFor(row, headerIndex, [\n\t\t\"agent_run_id\",\n\t\t\"agentrunid\",\n\t\t\"agent_run\",\n\t]);\n\tconst toolCallId = valueFor(row, headerIndex, [\n\t\t\"tool_call_id\",\n\t\t\"toolcallid\",\n\t\t\"tool_call\",\n\t]);\n\tconst evalId = valueFor(row, headerIndex, [\"eval_id\", \"evalid\"]);\n\tconst service = valueFor(row, headerIndex, [\"service\", \"service_name\"]);\n\n\tconst traceCitation = traceId\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tlabel: `trace ${traceId}`,\n\t\t\t\t\tentityKind: \"trace\" as const,\n\t\t\t\t\tentityId: traceId,\n\t\t\t\t\troute: traceRoute(traceId),\n\t\t\t\t},\n\t\t\t]\n\t\t: [];\n\n\tif (traceId && spanId) {\n\t\tconst entityId = `${traceId}:${spanId}`;\n\t\treturn {\n\t\t\tentityKind: \"span\",\n\t\t\tentityId,\n\t\t\troute: spanRoute(traceId, spanId),\n\t\t\tlabel: `span ${spanId}`,\n\t\t\tconfidence: 0.95,\n\t\t\tcitations: traceCitation,\n\t\t\tsuggestedNextPivots: [\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open span\",\n\t\t\t\t\tentityKind: \"span\",\n\t\t\t\t\tentityId,\n\t\t\t\t\troute: spanRoute(traceId, spanId),\n\t\t\t\t\treason: \"Inspect the cited span in its trace context.\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open trace\",\n\t\t\t\t\tentityKind: \"trace\",\n\t\t\t\t\tentityId: traceId,\n\t\t\t\t\troute: traceRoute(traceId),\n\t\t\t\t\treason: \"Review the surrounding trace context.\",\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\tconst candidates: Array<{\n\t\tentityKind: EvidenceEntityKind;\n\t\tentityId: string | null;\n\t\tlabelPrefix: string;\n\t\tconfidence: number;\n\t}> = [\n\t\t{\n\t\t\tentityKind: \"trace\",\n\t\t\tentityId: traceId,\n\t\t\tlabelPrefix: \"trace\",\n\t\t\tconfidence: 0.95,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"action\",\n\t\t\tentityId: actionId,\n\t\t\tlabelPrefix: \"action\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"tool_call\",\n\t\t\tentityId: toolCallId,\n\t\t\tlabelPrefix: \"tool call\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"agent_run\",\n\t\t\tentityId: agentRunId,\n\t\t\tlabelPrefix: \"agent run\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"profile\",\n\t\t\tentityId: profileId,\n\t\t\tlabelPrefix: \"profile\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"log\",\n\t\t\tentityId: logId,\n\t\t\tlabelPrefix: \"log\",\n\t\t\tconfidence: 0.85,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"eval\",\n\t\t\tentityId: evalId,\n\t\t\tlabelPrefix: \"eval\",\n\t\t\tconfidence: 0.85,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"service\",\n\t\t\tentityId: service,\n\t\t\tlabelPrefix: \"service\",\n\t\t\tconfidence: 0.7,\n\t\t},\n\t];\n\n\tconst candidate = candidates.find((item) => item.entityId);\n\tif (!candidate?.entityId) return null;\n\n\tconst route = evidenceRouteFor(\n\t\tcandidate.entityKind,\n\t\tcandidate.entityId,\n\t\ttraceId,\n\t);\n\treturn {\n\t\tentityKind: candidate.entityKind,\n\t\tentityId: candidate.entityId,\n\t\troute,\n\t\tlabel: `${candidate.labelPrefix} ${candidate.entityId}`,\n\t\tconfidence: candidate.confidence,\n\t\tcitations: traceCitation.filter(\n\t\t\t(citation) =>\n\t\t\t\tcitation.entityKind !== candidate.entityKind ||\n\t\t\t\tcitation.entityId !== candidate.entityId,\n\t\t),\n\t\tsuggestedNextPivots: [\n\t\t\t{\n\t\t\t\tlabel: `Open ${candidate.labelPrefix}`,\n\t\t\t\tentityKind: candidate.entityKind,\n\t\t\t\tentityId: candidate.entityId,\n\t\t\t\troute,\n\t\t\t\treason: \"Inspect the cited evidence row.\",\n\t\t\t},\n\t\t],\n\t};\n};\n\nexport const alertEvidenceReferences = (\n\trule: AlertRule,\n\tevaluation?: AlertEvaluation | null,\n): EvidenceReference[] => {\n\tconst route = evidenceRouteFor(\"alert\", rule.id);\n\tconst references: EvidenceReference[] = [\n\t\t{\n\t\t\tevidenceId: evaluation\n\t\t\t\t? `alert:${rule.id}:evaluation:${evaluation.id}`\n\t\t\t\t: `alert:${rule.id}:rule`,\n\t\t\tentityKind: \"alert\",\n\t\t\tentityId: rule.id,\n\t\t\troute,\n\t\t\tsource: evaluation\n\t\t\t\t? \"alert_evaluations\"\n\t\t\t\t: \"alerts.rule_evaluation_preview\",\n\t\t\tconfidence: evaluation ? 1 : 0.8,\n\t\t\treason: evaluation\n\t\t\t\t? `Alert \"${rule.name}\" evaluated to ${evaluation.value} and state ${evaluation.state}.`\n\t\t\t\t: `Alert \"${rule.name}\" was evaluated against its configured threshold.`,\n\t\t\tcitations: [\n\t\t\t\t{\n\t\t\t\t\tlabel: rule.name,\n\t\t\t\t\tentityKind: \"alert\",\n\t\t\t\t\tentityId: rule.id,\n\t\t\t\t\troute,\n\t\t\t\t},\n\t\t\t],\n\t\t\tsuggestedNextPivots: [\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open alert\",\n\t\t\t\t\tentityKind: \"alert\",\n\t\t\t\t\tentityId: rule.id,\n\t\t\t\t\troute,\n\t\t\t\t\treason: \"Inspect the alert rule, threshold, and recent evaluations.\",\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t];\n\n\tif (rule.analysisId) {\n\t\tconst analysisRouteValue = analysisRoute(rule.analysisId);\n\t\treferences.push({\n\t\t\tevidenceId: evaluation\n\t\t\t\t? `alert:${rule.id}:evaluation:${evaluation.id}:analysis:${rule.analysisId}`\n\t\t\t\t: `alert:${rule.id}:analysis:${rule.analysisId}`,\n\t\t\tentityKind: \"analysis\",\n\t\t\tentityId: rule.analysisId,\n\t\t\troute: analysisRouteValue,\n\t\t\tsource: \"alert_rules.analysis_id\",\n\t\t\tconfidence: 0.9,\n\t\t\treason: `Alert \"${rule.name}\" is bound to analysis \"${rule.analysisId}\".`,\n\t\t\tcitations: [\n\t\t\t\t{\n\t\t\t\t\tlabel: rule.name,\n\t\t\t\t\tentityKind: \"alert\",\n\t\t\t\t\tentityId: rule.id,\n\t\t\t\t\troute,\n\t\t\t\t},\n\t\t\t],\n\t\t\tsuggestedNextPivots: [\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open investigation\",\n\t\t\t\t\tentityKind: \"analysis\",\n\t\t\t\t\tentityId: rule.analysisId,\n\t\t\t\t\troute: analysisRouteValue,\n\t\t\t\t\treason: \"Review the analysis result that drives this alert.\",\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t}\n\n\treturn references;\n};\n\nexport const aiEvaluationEvidenceReferences = (\n\tevaluation: Pick<\n\t\tAIEvaluationRecord,\n\t\t\"evaluationId\" | \"traceId\" | \"spanId\" | \"name\" | \"source\"\n\t>,\n): EvidenceReference[] => {\n\tconst spanEntityId = `${evaluation.traceId}:${evaluation.spanId}`;\n\tconst spanRouteValue = evidenceRouteFor(\n\t\t\"span\",\n\t\tspanEntityId,\n\t\tevaluation.traceId,\n\t);\n\treturn [\n\t\t{\n\t\t\tevidenceId: `ai-evaluation:${evaluation.evaluationId}`,\n\t\t\tentityKind: \"eval\",\n\t\t\tentityId: evaluation.evaluationId,\n\t\t\troute: evidenceRouteFor(\"eval\", evaluation.evaluationId),\n\t\t\tsource: `ai_span_evaluations.${evaluation.source}`,\n\t\t\tconfidence: 0.95,\n\t\t\treason: `AI evaluation \"${evaluation.name}\" graded span \"${evaluation.spanId}\".`,\n\t\t\tcitations: [\n\t\t\t\t{\n\t\t\t\t\tlabel: `span ${evaluation.spanId}`,\n\t\t\t\t\tentityKind: \"span\",\n\t\t\t\t\tentityId: spanEntityId,\n\t\t\t\t\troute: spanRouteValue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: `trace ${evaluation.traceId}`,\n\t\t\t\t\tentityKind: \"trace\",\n\t\t\t\t\tentityId: evaluation.traceId,\n\t\t\t\t\troute: evidenceRouteFor(\"trace\", evaluation.traceId),\n\t\t\t\t},\n\t\t\t],\n\t\t\tsuggestedNextPivots: [\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open span\",\n\t\t\t\t\tentityKind: \"span\",\n\t\t\t\t\tentityId: spanEntityId,\n\t\t\t\t\troute: spanRouteValue,\n\t\t\t\t\treason: \"Inspect the evaluated AI span and its surrounding trace.\",\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t];\n};\n\nexport interface SourceEvidenceLinks {\n\tsourceAgentRunId?: string | null;\n\tsourceActionId?: string | null;\n\tsourceToolCallId?: string | null;\n\tsourceTraceId?: string | null;\n\tsourceSpanId?: string | null;\n}\n\nexport const sourceLinkEvidenceReferences = (\n\tsource: {\n\t\tsourceLabel: string;\n\t\tsourceId: string;\n\t\tsourceKind: \"eval_case\" | \"eval_case_result\" | \"eval_run\";\n\t\tsourceRoute?: string | null;\n\t\tsourceName?: string | null;\n\t},\n\tlinks: SourceEvidenceLinks,\n): EvidenceReference[] => {\n\tconst citations: EvidenceCitation[] = [];\n\tif (source.sourceRoute) {\n\t\tcitations.push({\n\t\t\tlabel: source.sourceName ?? source.sourceId,\n\t\t\tentityKind: \"eval\",\n\t\t\tentityId: source.sourceId,\n\t\t\troute: source.sourceRoute,\n\t\t});\n\t}\n\n\tconst candidates: Array<{\n\t\tkind: EvidenceEntityKind;\n\t\tid: string | null | undefined;\n\t\tlabel: string;\n\t\tconfidence: number;\n\t}> = [\n\t\t{\n\t\t\tkind: \"action\",\n\t\t\tid: links.sourceActionId,\n\t\t\tlabel: \"source action\",\n\t\t\tconfidence: 0.92,\n\t\t},\n\t\t{\n\t\t\tkind: \"tool_call\",\n\t\t\tid: links.sourceToolCallId,\n\t\t\tlabel: \"source tool call\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tkind: \"agent_run\",\n\t\t\tid: links.sourceAgentRunId,\n\t\t\tlabel: \"source agent run\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tkind: \"span\",\n\t\t\tid:\n\t\t\t\tlinks.sourceTraceId && links.sourceSpanId\n\t\t\t\t\t? `${links.sourceTraceId}:${links.sourceSpanId}`\n\t\t\t\t\t: null,\n\t\t\tlabel: \"source span\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tkind: \"trace\",\n\t\t\tid: links.sourceTraceId,\n\t\t\tlabel: \"source trace\",\n\t\t\tconfidence: 0.88,\n\t\t},\n\t];\n\n\treturn candidates\n\t\t.filter((candidate): candidate is typeof candidate & { id: string } =>\n\t\t\tBoolean(candidate.id),\n\t\t)\n\t\t.map((candidate) => {\n\t\t\tconst route = evidenceRouteFor(\n\t\t\t\tcandidate.kind,\n\t\t\t\tcandidate.id,\n\t\t\t\tlinks.sourceTraceId,\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tevidenceId: `${source.sourceKind}:${source.sourceId}:${candidate.kind}:${candidate.id}`,\n\t\t\t\tentityKind: candidate.kind,\n\t\t\t\tentityId: candidate.id,\n\t\t\t\troute,\n\t\t\t\tsource: `${source.sourceKind}.source_links`,\n\t\t\t\tconfidence: candidate.confidence,\n\t\t\t\treason: `${source.sourceLabel} links to ${candidate.label} \"${candidate.id}\".`,\n\t\t\t\tcitations,\n\t\t\t\tsuggestedNextPivots: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: `Open ${candidate.label}`,\n\t\t\t\t\t\tentityKind: candidate.kind,\n\t\t\t\t\t\tentityId: candidate.id,\n\t\t\t\t\t\troute,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t\"Inspect the production evidence behind this evaluation item.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t} satisfies EvidenceReference;\n\t\t});\n};\n\nexport const analysisResultEvidenceReferences = (\n\tresult: Pick<AnalysisResult, \"analysisId\" | \"payload\">,\n): EvidenceReference[] => {\n\tconst references: EvidenceReference[] = [\n\t\t...instrumentationGapEvidenceReferences(result),\n\t];\n\tfor (const { sectionName, table } of parseEvidenceTables(result.payload)) {\n\t\tconst headers = table.headers.map(normalizeHeader);\n\t\tfor (const [rowIndex, row] of table.rows.entries()) {\n\t\t\tif (references.length >= MAX_ANALYSIS_EVIDENCE_REFERENCES) {\n\t\t\t\treturn references;\n\t\t\t}\n\t\t\tconst entity = entityFromRow(row, headers);\n\t\t\tif (!entity) continue;\n\n\t\t\tconst title = table.title ?? sectionName;\n\t\t\treferences.push({\n\t\t\t\tevidenceId: `analysis:${result.analysisId}:${sectionName}:${rowIndex}:${entity.entityKind}:${entity.entityId}`,\n\t\t\t\tentityKind: entity.entityKind,\n\t\t\t\tentityId: entity.entityId,\n\t\t\t\troute: entity.route,\n\t\t\t\tsource: `analysis.payload.evidence.${sectionName}`,\n\t\t\t\tconfidence: entity.confidence,\n\t\t\t\treason: `Analysis \"${result.analysisId}\" cited ${entity.label} in \"${title}\" row ${rowIndex + 1}.`,\n\t\t\t\tcitations: entity.citations,\n\t\t\t\tsuggestedNextPivots: entity.suggestedNextPivots,\n\t\t\t});\n\t\t}\n\t}\n\treturn references;\n};\n","/**\n * RFC 0002 Stage 3 — narrative LLM adapter.\n *\n * Single-provider for v1 (Anthropic). The shape is \"stuff in a system\n * prompt that bakes the rendering rules, ask for one declarative\n * sentence, return text\" — a different provider plugs in here without\n * changing callers.\n *\n * Rendering rules from the RFC's UX section, enforced at the system\n * prompt layer so the model can't accidentally drift:\n *\n * 1. Never start with \"I\" or \"Here's\" — declarative, not chatbot.\n * 2. Always cite — if the prompt mentions \"the trace\", a trace_id\n * must follow inline.\n * 3. Include a time anchor (\"starting 8 min ago\", \"since 14:32\").\n * 4. ≤2 sentences. We're labelling tiles, not writing essays.\n */\n\nimport type { AnalysisDefinition, AnalysisResult } from \"@obsunified/types\";\nimport type { ChildSpanRunner } from \"../framework/logger\";\n\nexport interface NarrativeRequest {\n\tdefinition: AnalysisDefinition;\n\tcurrent: AnalysisResult;\n\tprevious: AnalysisResult | null;\n}\n\n/**\n * Provider discriminator. The collector picks one based on which API key\n * env var is set (OpenAI takes precedence when both are set, since users\n * who configure both presumably did so deliberately for OpenAI).\n *\n * anthropic → POST https://api.anthropic.com/v1/messages\n * openai → POST https://api.openai.com/v1/chat/completions\n * (apiUrl override lets you point at openrouter, vLLM,\n * azure-openai-compat, ollama, etc.)\n */\nexport type LlmProvider = \"anthropic\" | \"openai\";\n\nexport interface LlmConfig {\n\tprovider: LlmProvider;\n\tapiKey: string;\n\t/** Model id. Provider-specific. e.g. \"claude-haiku-4-5\" / \"gpt-4o-mini\". */\n\tmodel: string;\n\t/** Override base URL — Anthropic-compatible or OpenAI-compatible. */\n\tapiUrl?: string;\n\t/** Optional abort signal used to cap provider calls from schedulers. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Optional child-span runner. When set, every LLM API call (and tool\n\t * dispatch in the agent loop) is wrapped in an OpenInference-shaped span\n\t * with `gen_ai.system`, `gen_ai.request.model`, and post-call usage\n\t * attributes. Wired by the worker entrypoint via `runtime.withChildSpan`.\n\t */\n\ttracer?: ChildSpanRunner;\n}\n\nconst DEFAULT_API_URL = \"https://api.anthropic.com/v1/messages\";\n\nconst SYSTEM_PROMPT = `You write one-sentence operational narratives for a self-hosted observability dashboard.\n\nHard rules — violations are unusable:\n1. Never start with \"I\", \"Here's\", \"We\", \"It looks\", or any first-person/chatbot framing. Write declarative statements about the system.\n2. If you reference a trace, span, log, or service, include the supplied identifier inline (e.g. \"trace abc12345\").\n3. Include a time anchor: \"starting Nm ago\", \"for the last Nm\", \"since HH:MM\" — pull from the inputs.\n4. ≤ 2 sentences total, ≤ 220 characters. Numbers belong in the sentence, not in a separate label.\n5. If the inputs are insufficient to write a useful sentence, output exactly the string NO_NARRATIVE (no quotes).\n\nTone: like a senior on-call writing a Slack message. Specific, blunt, no hedging, no advice (\"you should...\"), no questions.`;\n\nconst renderTemplate = (\n\ttemplate: string,\n\tdef: AnalysisDefinition,\n\tr: AnalysisResult,\n): string => {\n\tconst scope = (def.scope ?? {}) as Record<string, unknown>;\n\tconst traceIds = Array.isArray(r.payload?.trace_ids)\n\t\t? (r.payload.trace_ids as unknown[])\n\t\t\t\t.filter((x): x is string => typeof x === \"string\")\n\t\t\t\t.slice(0, 5)\n\t\t\t\t.join(\", \")\n\t\t: \"\";\n\tconst subs: Record<string, string> = {\n\t\ttitle: def.title,\n\t\tstatus: r.status,\n\t\tprimary: r.primaryValue === null ? \"n/a\" : String(r.primaryValue),\n\t\tbaseline: r.baselineValue === null ? \"n/a\" : String(r.baselineValue),\n\t\tdelta_pct: r.deltaPct === null ? \"n/a\" : `${r.deltaPct.toFixed(1)}%`,\n\t\tservice: typeof scope.service === \"string\" ? scope.service : \"\",\n\t\ttrace_ids: traceIds,\n\t};\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, k) => subs[k] ?? \"\");\n};\n\nconst buildUserPrompt = ({\n\tdefinition,\n\tcurrent,\n\tprevious,\n}: NarrativeRequest): string => {\n\tconst lines: string[] = [];\n\tconst tmplRendered = renderTemplate(\n\t\tdefinition.narrate?.prompt ?? \"\",\n\t\tdefinition,\n\t\tcurrent,\n\t).trim();\n\tif (tmplRendered) lines.push(tmplRendered);\n\n\tlines.push(\"\");\n\tlines.push(\"Current state:\");\n\tlines.push(`- panel: ${definition.title}`);\n\tlines.push(`- status: ${current.status}`);\n\tif (current.primaryValue !== null)\n\t\tlines.push(`- primary: ${current.primaryValue}`);\n\tif (current.baselineValue !== null)\n\t\tlines.push(`- baseline (1h): ${current.baselineValue}`);\n\tif (current.deltaPct !== null)\n\t\tlines.push(`- delta vs baseline: ${current.deltaPct.toFixed(1)}%`);\n\n\tconst since = previous\n\t\t? Math.max(\n\t\t\t\t1,\n\t\t\t\tMath.round(\n\t\t\t\t\t(new Date(current.generatedAt).getTime() -\n\t\t\t\t\t\tnew Date(previous.generatedAt).getTime()) /\n\t\t\t\t\t\t60000,\n\t\t\t\t),\n\t\t\t)\n\t\t: null;\n\tif (since !== null) {\n\t\tlines.push(\n\t\t\t`- previous run was ${since} minute${since === 1 ? \"\" : \"s\"} ago, status=${previous?.status ?? \"n/a\"}`,\n\t\t);\n\t}\n\n\tconst payloadKeys = Object.keys(current.payload ?? {}).filter(\n\t\t(k) => k !== \"sparkline\",\n\t);\n\tif (payloadKeys.length > 0) {\n\t\tlines.push(\"\");\n\t\tlines.push(\"Payload details:\");\n\t\tfor (const k of payloadKeys) {\n\t\t\tconst v = current.payload[k];\n\t\t\tconst rendered =\n\t\t\t\ttypeof v === \"string\" || typeof v === \"number\" || typeof v === \"boolean\"\n\t\t\t\t\t? String(v)\n\t\t\t\t\t: JSON.stringify(v);\n\t\t\tif (rendered.length <= 200) lines.push(`- ${k}: ${rendered}`);\n\t\t}\n\t}\n\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Output: a single sentence (max two), no preamble, no quotes, no Markdown.\",\n\t);\n\treturn lines.join(\"\\n\");\n};\n\nexport class LlmCallError extends Error {\n\treadonly status: number | null;\n\tconstructor(message: string, status: number | null = null) {\n\t\tsuper(message);\n\t\tthis.name = \"LlmCallError\";\n\t\tthis.status = status;\n\t}\n}\n\n/**\n * Provider dispatcher for narrative generation.\n *\n * Returns `null` when:\n * - the model returns the sentinel NO_NARRATIVE\n * - the response is empty\n * Throws `LlmCallError` on non-2xx HTTP responses so the runner can\n * count the failure against the budget without writing a bogus narrative.\n */\nexport async function generateNarrative(\n\treq: NarrativeRequest,\n\tconfig: LlmConfig,\n): Promise<string | null> {\n\tif (config.provider === \"openai\") {\n\t\t// Lazy import to keep the Anthropic-only path zero-cost on the worker\n\t\t// boot and avoid a circular import between llm.ts and openai.ts.\n\t\tconst { generateNarrativeOpenAI } = await import(\"./openai\");\n\t\treturn generateNarrativeOpenAI(req, config);\n\t}\n\treturn generateNarrativeAnthropic(req, config);\n}\n\nasync function generateNarrativeAnthropic(\n\treq: NarrativeRequest,\n\tconfig: LlmConfig,\n): Promise<string | null> {\n\tconst url = config.apiUrl ?? DEFAULT_API_URL;\n\tconst userPrompt = buildUserPrompt(req);\n\n\tconst tracer = config.tracer;\n\tconst callLLM = async (\n\t\tspan: { setAttribute(k: string, v: unknown): void } | null,\n\t): Promise<string | null> => {\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"x-api-key\": config.apiKey,\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tmodel: config.model,\n\t\t\t\tmax_tokens: 200,\n\t\t\t\tsystem: SYSTEM_PROMPT,\n\t\t\t\tmessages: [{ role: \"user\", content: userPrompt }],\n\t\t\t}),\n\t\t\tsignal: config.signal,\n\t\t});\n\t\tif (span) {\n\t\t\tspan.setAttribute(\"http.response.status_code\", response.status);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tthrow new LlmCallError(\n\t\t\t\t`anthropic ${response.status}: ${text.slice(0, 200)}`,\n\t\t\t\tresponse.status,\n\t\t\t);\n\t\t}\n\n\t\tconst body = (await response.json()) as {\n\t\t\tcontent?: Array<{ type?: string; text?: string }>;\n\t\t\tusage?: { input_tokens?: number; output_tokens?: number };\n\t\t\tstop_reason?: string;\n\t\t};\n\t\tif (body.stop_reason === \"max_tokens\") {\n\t\t\tthrow new LlmCallError(\"anthropic response truncated by max_tokens\");\n\t\t}\n\t\tif (span && body.usage) {\n\t\t\tif (typeof body.usage.input_tokens === \"number\")\n\t\t\t\tspan.setAttribute(\"gen_ai.usage.input_tokens\", body.usage.input_tokens);\n\t\t\tif (typeof body.usage.output_tokens === \"number\")\n\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\"gen_ai.usage.output_tokens\",\n\t\t\t\t\tbody.usage.output_tokens,\n\t\t\t\t);\n\t\t}\n\t\tconst text = (body.content ?? [])\n\t\t\t.filter((block) => block?.type === \"text\")\n\t\t\t.map((block) => block?.text ?? \"\")\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\tif (!text || text === \"NO_NARRATIVE\") return null;\n\t\t// Strip any accidental wrapping quotes the model might add despite the\n\t\t// system prompt.\n\t\treturn text.replace(/^[\"']|[\"']$/g, \"\").trim();\n\t};\n\n\tif (!tracer) return callLLM(null);\n\treturn tracer(\"llm.anthropic.messages\", async (span) => callLLM(span), {\n\t\t\"openinference.span.kind\": \"LLM\",\n\t\t\"gen_ai.system\": \"anthropic\",\n\t\t\"gen_ai.request.model\": config.model,\n\t\t\"gen_ai.request.max_tokens\": 200,\n\t});\n}\n"],"mappings":"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","names":["text"]}
package/dist/index.d.ts CHANGED
@@ -2,9 +2,9 @@ import * as hono from 'hono';
2
2
  import { MiddlewareHandler, Hono, Context } from 'hono';
3
3
  import * as hono_types from 'hono/types';
4
4
  import * as _obsunified_types from '@obsunified/types';
5
- import { StoredSpan, TelemetryOverviewOptions, TelemetryOverviewResponse, TelemetryTraceDetailResponse, TelemetryInstrumentationGapsResponse, TelemetryIssueOptions, TelemetryIssueOverviewResponse, TelemetryIssueDetailResponse, UsageEventRecord, UsageOverviewOptions, UsageOverviewResponse, UsageSessionDetailResponse, UsageSessionSummary, AlertRuleInput, AlertRule, AlertStateRow, AlertState, AlertEvaluationsListResponse, AlertComparison, ChangeBlastRadiusResult, ContentionCandidate, ChangeEvent, ChangeEventInput, InvestigationFinding, InvestigationFindingInput, OntologyNode, OntologyEdge, CanonicalNodeIdentity, OntologyGraphResponse, RelationshipCertainty, CausalTriple, IncidentInvestigation, RemediationProposal, EvidenceEntityKind, RemediationProposalInput, JsonValue, EvidenceReference, EvidenceReferenceContract, Project, IngestKey, IngestKeyWithPlaintext } from '@obsunified/types';
6
- import { b as CollectorEnv, L as Logger, C as ChildSpanRunner, c as CollectorRouteContext } from './ingest-auth-C5-8Mly5.js';
7
- export { a as CollectorApp, d as consoleLogger, e as createIngestAuth } from './ingest-auth-C5-8Mly5.js';
5
+ import { StoredSpan, TelemetryOverviewOptions, TelemetryOverviewResponse, TelemetryTraceDetailResponse, TelemetryInstrumentationGapsResponse, TelemetryIssueOptions, TelemetryIssueOverviewResponse, TelemetryIssueDetailResponse, UsageEventRecord, UsageOverviewOptions, UsageOverviewResponse, UsageSessionDetailResponse, UsageSessionSummary, AlertRuleInput, AlertRule, AlertStateRow, AlertState, AlertEvaluationOutcome, AlertEvaluationsListResponse, AlertComparison, ChangeBlastRadiusResult, ContentionCandidate, ChangeEvent, ChangeEventInput, InvestigationFinding, InvestigationFindingInput, OntologyNode, OntologyEdge, CanonicalNodeIdentity, OntologyGraphResponse, RelationshipCertainty, CausalTriple, IncidentInvestigation, RemediationProposal, EvidenceEntityKind, RemediationProposalInput, JsonValue, EvidenceReference, EvidenceReferenceContract, AlertQueryMetrics, AlertMetricAggregation, AlertMetricField, Project, IngestKey, IngestKeyWithPlaintext } from '@obsunified/types';
6
+ import { g as CollectorEnv, L as Logger, e as ChildSpanRunner, l as CollectorRouteContext, a as AlertEmailTransport } from './ingest-auth-BTowieER.js';
7
+ export { A as AlertEmailMessage, b as AlertNotificationPayload, C as COLLECTOR_HOST_MANAGED_ENV_KEYS, c as COLLECTOR_OPTIONAL_STRING_ENV_KEYS, d as COLLECTOR_STRING_ENV_MANIFEST, f as CollectorApp, h as CollectorHostManagedEnvKey, i as CollectorManifestStringEnvKey, j as CollectorObjectBindingKey, k as CollectorOptionalStringEnvKey, m as CollectorStringEnvKey, n as assertCollectorEnvManifestCompleteness, o as consoleLogger, p as createIngestAuth, q as formatAlertEmail, r as isValidEmailAddress } from './ingest-auth-BTowieER.js';
8
8
  import { ActionConfidence } from '@obsunified/types/constants';
9
9
  import { Pool } from 'pg';
10
10
 
@@ -488,15 +488,178 @@ declare const aiReceiverPlugin: CollectorPlugin;
488
488
 
489
489
  declare const aiSpanPayloadsProcessorPlugin: CollectorPlugin;
490
490
 
491
+ /** A rule's value, and whether a metrics rule found no fresh data. */
492
+ interface AlertRuleEvaluation {
493
+ value: number;
494
+ noData: boolean;
495
+ }
496
+
497
+ interface AlertsStoreOptions {
498
+ /** Permit webhook targets on loopback / private / link-local hosts. */
499
+ allowPrivateWebhookTargets?: boolean;
500
+ /**
501
+ * Whether this collector can send email (a transport is bound as
502
+ * `env.ALERT_EMAIL`). Rules with an email channel are refused otherwise.
503
+ */
504
+ emailDeliveryAvailable?: boolean;
505
+ }
506
+ declare function compareValue(value: number, threshold: number, comparison: AlertComparison): boolean;
507
+ declare class AlertsStore {
508
+ private readonly db;
509
+ private readonly options;
510
+ constructor(db: SqlDb, options?: AlertsStoreOptions);
511
+ validateInput(input: AlertRuleInput): void;
512
+ listRules(projectId: string): Promise<AlertRule[]>;
513
+ /**
514
+ * The newest evaluation of every rule in the project, one query. Falls
515
+ * back to the pre-057/059 column set on older databases.
516
+ */
517
+ private lastEvaluations;
518
+ getRule(id: string, projectId: string): Promise<AlertRule | null>;
519
+ createRule(projectId: string, input: AlertRuleInput): Promise<AlertRule>;
520
+ updateRule(id: string, projectId: string, patch: Partial<AlertRuleInput>): Promise<AlertRule | null>;
521
+ /**
522
+ * Clears a rule's pending period. Tolerates databases that have not
523
+ * applied 055/058 (there is nothing to clear on them).
524
+ */
525
+ resetPending(ruleId: string, projectId: string): Promise<void>;
526
+ /**
527
+ * Runs `current`; on a database that has not applied migration 054 (no
528
+ * `alert_rules.pending_secs`) a rule without a pending duration is
529
+ * written with the legacy statement so rolling deploys keep working. A
530
+ * rule that needs the column is refused with a clear message instead of
531
+ * silently losing its pending duration.
532
+ */
533
+ private withPendingColumnFallback;
534
+ deleteRule(id: string, projectId: string): Promise<boolean>;
535
+ listEnabledRules(): Promise<AlertRule[]>;
536
+ getState(ruleId: string, projectId: string): Promise<AlertStateRow | null>;
537
+ /**
538
+ * Record a state transition with its notification still outstanding
539
+ * (`notified = 0`); `markNotified` flips it once a channel accepts the
540
+ * alert. On a database that has not applied migration 049 the column is
541
+ * missing: the transition is still written (legacy statement) so alerting
542
+ * keeps working through a rolling deploy, and the evaluator treats a row
543
+ * without the column as delivered.
544
+ */
545
+ transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
546
+ /**
547
+ * Mark the transition `(state, lastStateChange)` as delivered. Scoped to
548
+ * the exact transition the caller notified for: if the rule changed state
549
+ * again while the webhook was in flight, the newer transition keeps its
550
+ * own outstanding flag. Returns false when nothing matched (stale or
551
+ * already marked). On a pre-049 schema there is nothing to mark.
552
+ */
553
+ markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
554
+ /**
555
+ * Record (`{ since, seenAt }`) or clear (`null`) the pending period of a
556
+ * rule that has a pending duration (migrations 055, 058): when the
557
+ * condition started holding, and the last evaluation that saw it hold.
558
+ * A rule without a state row gets one in state `ok` since its creation,
559
+ * already delivered (nothing is owed for a rule that never changed
560
+ * state).
561
+ */
562
+ setPending(ruleId: string, projectId: string, pending: {
563
+ since: string;
564
+ seenAt: string;
565
+ } | null, ruleCreatedAt: string): Promise<void>;
566
+ recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean, outcome?: AlertEvaluationOutcome, errorMessage?: string): Promise<void>;
567
+ listEvaluations(params: {
568
+ ruleId: string;
569
+ projectId?: string;
570
+ hours: number;
571
+ limit?: number;
572
+ }): Promise<AlertEvaluationsListResponse>;
573
+ evaluateRule(rule: AlertRule, now?: Date): Promise<number>;
574
+ /** Like `evaluateRule`, plus whether a metrics rule found no fresh data. */
575
+ evaluateRuleWithStatus(rule: AlertRule, now?: Date): Promise<AlertRuleEvaluation>;
576
+ private evaluateCountRule;
577
+ /**
578
+ * Stage 6 — fetch the latest narrative associated with this rule's
579
+ * bound analysis. Used by the evaluator when firing an analysis-bound
580
+ * alert so the webhook payload includes the human-readable
581
+ * narrative, not just a threshold crossing.
582
+ */
583
+ getAnalysisNarrative(projectId: string, analysisId: string): Promise<{
584
+ narrative: string | null;
585
+ status: string | null;
586
+ } | null>;
587
+ private evaluateSpanRule;
588
+ private evaluateLogRule;
589
+ private evaluateUsageRule;
590
+ private evaluateAIRule;
591
+ }
592
+
491
593
  /** Resolve a hostname to its addresses (e.g. `dns.lookup(host, { all: true })`). */
492
594
  type HostLookup = (hostname: string) => Promise<string[]>;
493
595
 
596
+ interface AlertEvaluatorStore {
597
+ listEnabledRules(): Promise<AlertRule[]>;
598
+ evaluateRule(rule: AlertRule): Promise<number>;
599
+ /** Preferred when present: also says whether a metrics rule had no data. */
600
+ evaluateRuleWithStatus?(rule: AlertRule): Promise<AlertRuleEvaluation>;
601
+ getState(ruleId: string, projectId: string): Promise<{
602
+ current_state: AlertState;
603
+ last_state_change: string;
604
+ /** 0 while the transition's notification is owed; absent pre-049 (= delivered). */
605
+ notified?: number;
606
+ /** Start of the current pending period (migration 055); null/absent when not pending. */
607
+ pending_since?: string | null;
608
+ /** Last evaluation that saw the pending condition hold (migration 058). */
609
+ pending_seen_at?: string | null;
610
+ } | null>;
611
+ /** Record (`{ since, seenAt }`) or clear (null) the rule's pending period. */
612
+ setPending(ruleId: string, projectId: string, pending: {
613
+ since: string;
614
+ seenAt: string;
615
+ } | null, ruleCreatedAt: string): Promise<void>;
616
+ /** Flip the exact transition `(state, lastStateChange)` to delivered. */
617
+ markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
618
+ getAnalysisNarrative(projectId: string, analysisId: string): Promise<{
619
+ narrative: string | null;
620
+ status: string | null;
621
+ } | null>;
622
+ transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
623
+ recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean, outcome?: AlertEvaluationOutcome,
624
+ /** With outcome `error`: why (the store sanitizes it). */
625
+ errorMessage?: string): Promise<void>;
626
+ }
627
+ declare function evaluateRuleBatch(store: AlertEvaluatorStore, rules: AlertRule[], options?: {
628
+ logger?: Logger;
629
+ tracer?: ChildSpanRunner;
630
+ concurrency?: number;
631
+ ruleTimeoutMs?: number;
632
+ allowPrivateWebhookTargets?: boolean;
633
+ hostLookup?: HostLookup;
634
+ email?: AlertEmailTransport;
635
+ onAlertFiring?: (rule: AlertRule) => Promise<void>;
636
+ /**
637
+ * Longest gap between two evaluations that still continues a pending
638
+ * hold. Default `DEFAULT_MAX_EVALUATION_GAP_MS` (two 5-minute ticks);
639
+ * pass twice the host's evaluation interval.
640
+ */
641
+ maxEvaluationGapMs?: number;
642
+ /**
643
+ * ISO start of this collector process. A hold last seen before it
644
+ * starts over, as Prometheus resets `for` on restart. Unset on hosts
645
+ * without a long-lived process (Workers), where the gap rule applies.
646
+ */
647
+ holdStartedAt?: string;
648
+ }): Promise<{
649
+ evaluated: number;
650
+ fired: number;
651
+ resolved: number;
652
+ /** Notifications for an earlier transition that were delivered on this tick. */
653
+ redelivered: number;
654
+ }>;
494
655
  declare function evaluateAllRules(env: CollectorEnv, options?: {
495
656
  logger?: Logger;
496
657
  tracer?: ChildSpanRunner;
497
658
  concurrency?: number;
498
659
  ruleTimeoutMs?: number;
499
660
  hostLookup?: HostLookup;
661
+ maxEvaluationGapMs?: number;
662
+ holdStartedAt?: string;
500
663
  }): Promise<{
501
664
  evaluated: number;
502
665
  fired: number;
@@ -1024,64 +1187,6 @@ declare class ActionStore {
1024
1187
  listArtifactsByAction(projectId: string, actionId: string, limit?: number): Promise<ArtifactRef[]>;
1025
1188
  }
1026
1189
 
1027
- interface AlertsStoreOptions {
1028
- /** Permit webhook targets on loopback / private / link-local hosts. */
1029
- allowPrivateWebhookTargets?: boolean;
1030
- }
1031
- declare function compareValue(value: number, threshold: number, comparison: AlertComparison): boolean;
1032
- declare class AlertsStore {
1033
- private readonly db;
1034
- private readonly options;
1035
- constructor(db: SqlDb, options?: AlertsStoreOptions);
1036
- validateInput(input: AlertRuleInput): void;
1037
- listRules(projectId: string): Promise<AlertRule[]>;
1038
- getRule(id: string, projectId: string): Promise<AlertRule | null>;
1039
- createRule(projectId: string, input: AlertRuleInput): Promise<AlertRule>;
1040
- updateRule(id: string, projectId: string, patch: Partial<AlertRuleInput>): Promise<AlertRule | null>;
1041
- deleteRule(id: string, projectId: string): Promise<boolean>;
1042
- listEnabledRules(): Promise<AlertRule[]>;
1043
- getState(ruleId: string, projectId: string): Promise<AlertStateRow | null>;
1044
- /**
1045
- * Record a state transition with its notification still outstanding
1046
- * (`notified = 0`); `markNotified` flips it once a channel accepts the
1047
- * alert. On a database that has not applied migration 049 the column is
1048
- * missing: the transition is still written (legacy statement) so alerting
1049
- * keeps working through a rolling deploy, and the evaluator treats a row
1050
- * without the column as delivered.
1051
- */
1052
- transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
1053
- /**
1054
- * Mark the transition `(state, lastStateChange)` as delivered. Scoped to
1055
- * the exact transition the caller notified for: if the rule changed state
1056
- * again while the webhook was in flight, the newer transition keeps its
1057
- * own outstanding flag. Returns false when nothing matched (stale or
1058
- * already marked). On a pre-049 schema there is nothing to mark.
1059
- */
1060
- markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
1061
- recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean): Promise<void>;
1062
- listEvaluations(params: {
1063
- ruleId: string;
1064
- projectId?: string;
1065
- hours: number;
1066
- limit?: number;
1067
- }): Promise<AlertEvaluationsListResponse>;
1068
- evaluateRule(rule: AlertRule): Promise<number>;
1069
- /**
1070
- * Stage 6 — fetch the latest narrative associated with this rule's
1071
- * bound analysis. Used by the evaluator when firing an analysis-bound
1072
- * alert so the webhook payload includes the human-readable
1073
- * narrative, not just a threshold crossing.
1074
- */
1075
- getAnalysisNarrative(projectId: string, analysisId: string): Promise<{
1076
- narrative: string | null;
1077
- status: string | null;
1078
- } | null>;
1079
- private evaluateSpanRule;
1080
- private evaluateLogRule;
1081
- private evaluateUsageRule;
1082
- private evaluateAIRule;
1083
- }
1084
-
1085
1190
  declare function recordChangeEvent(db: SqlDb, projectId: string, input: ChangeEventInput): Promise<ChangeEvent>;
1086
1191
  declare function listChangeEvents(db: SqlDb, options: {
1087
1192
  projectId: string;
@@ -1698,6 +1803,103 @@ declare class EvalCasesStore {
1698
1803
  private buildMetadata;
1699
1804
  }
1700
1805
 
1806
+ /**
1807
+ * `metrics` alert signal: validation and evaluation of an
1808
+ * `AlertQueryMetrics` against `metric_series` / `metric_point`.
1809
+ *
1810
+ * Evaluation, per selector (numerator, then the optional denominator):
1811
+ *
1812
+ * 1. Series: `metric_series` rows of the project whose name is one of the
1813
+ * selector's names (and whose `service_name` matches, when given), kept
1814
+ * when every listed data-point / resource attribute equals the rule's
1815
+ * value. Attribute values are compared as strings, so `200` matches a
1816
+ * stored `"200"` and vice versa.
1817
+ * 2. Points: the series' points whose `received_at` (the collector's clock,
1818
+ * as for every other alert signal) is inside the rule window, ordered by
1819
+ * producer time (`ts_ns`). The number read is `field` (`value`, or a
1820
+ * histogram/summary `count` / `sum`); points without it are skipped.
1821
+ * 3. Staleness: for `last`, `min`, `max`, `avg` and `sum` a series takes
1822
+ * part only when its newest point is at most `staleSecs` (default 300)
1823
+ * old, so a series that stopped reporting (a renamed host, a replaced
1824
+ * pod) no longer adds its last value. `increase` / `rate` keep every
1825
+ * series with points in the window: growth that happened is real even
1826
+ * if the series stopped afterwards.
1827
+ * 4. Each series is reduced on its own (`reduceSeries`), and the per-series
1828
+ * results are summed. A series with no usable point adds nothing.
1829
+ *
1830
+ * When no series takes part (in the numerator or the denominator) the
1831
+ * result is "no data": the value is 0 and `noData` is true. The rule's
1832
+ * `noData` policy decides what that means (`zero`: evaluate 0 as usual;
1833
+ * `keep`: leave the alert state as it is).
1834
+ *
1835
+ * `age_seconds` ignores the window: it is the seconds since the newest
1836
+ * `received_at` of any matching series. When no matching series has any
1837
+ * stored point (never reported, or everything aged out of retention) it is
1838
+ * the seconds since the rule was created, a finite number that grows until
1839
+ * the threshold is crossed, so "never reported" fires after the same grace
1840
+ * period as "stopped reporting".
1841
+ *
1842
+ * `age_seconds` is never "no data".
1843
+ *
1844
+ * Ratio: with a `denominator`, value = numerator / denominator when the
1845
+ * denominator is > 0 and >= minDenominator (default 0), else 0.
1846
+ *
1847
+ * Cost bounds: at most `MAX_MATCHED_SERIES` series and `MAX_LOADED_POINTS`
1848
+ * points per selector; past either the evaluation fails with an error (the
1849
+ * evaluator records it as an `error` evaluation) rather than returning a
1850
+ * value computed from part of the data. Metric rule windows are at most
1851
+ * `MAX_METRIC_WINDOW_MINS`.
1852
+ */
1853
+
1854
+ /** Throws an Error with a user-facing message when `query` is malformed. */
1855
+ declare function validateMetricQuery(query: unknown): void;
1856
+ interface MetricPointSample {
1857
+ tsNs: string;
1858
+ startTsNs: string | null;
1859
+ value: number | null;
1860
+ count: number | null;
1861
+ sum: number | null;
1862
+ receivedAt: string;
1863
+ }
1864
+ interface SeriesReduceOptions {
1865
+ /**
1866
+ * The series has no history before the window (its `metric_series` row
1867
+ * was created inside it): a cumulative counter's first point then counts
1868
+ * as growth from 0 instead of being the baseline.
1869
+ */
1870
+ bornInWindow?: boolean;
1871
+ aggregation: Exclude<AlertMetricAggregation, "age_seconds">;
1872
+ field: AlertMetricField;
1873
+ /** `metric_series.temporality`: 1 delta, 2 cumulative, null for gauges. */
1874
+ temporality: number | null;
1875
+ /** Start of the window in Unix nanoseconds. */
1876
+ windowStartNs: bigint;
1877
+ windowSecs: number;
1878
+ }
1879
+ /**
1880
+ * Reduces one series' window points to a number, or null when the series
1881
+ * has no point carrying `field` in the window.
1882
+ */
1883
+ declare function reduceSeries(points: MetricPointSample[], opts: SeriesReduceOptions): number | null;
1884
+ interface MetricEvalContext {
1885
+ projectId: string;
1886
+ windowMins: number;
1887
+ /** The rule's `createdAt`; the reference for "never reported". */
1888
+ ruleCreatedAt: string;
1889
+ now: Date;
1890
+ /** Staleness bound for gauge-style aggregations; default 300. */
1891
+ staleSecs?: number;
1892
+ /** Point cap per selector; default MAX_LOADED_POINTS (tests lower it). */
1893
+ maxLoadedPoints?: number;
1894
+ }
1895
+ interface MetricEvaluation {
1896
+ value: number;
1897
+ /** No series took part; see the module comment. Never true for age_seconds. */
1898
+ noData: boolean;
1899
+ }
1900
+ /** Evaluates a validated metrics query to a finite number. */
1901
+ declare function evaluateMetricQuery(db: SqlDb, query: AlertQueryMetrics, ctx: MetricEvalContext): Promise<MetricEvaluation>;
1902
+
1701
1903
  type MetricType = "gauge" | "sum" | "histogram" | "exp_histogram" | "summary";
1702
1904
  /**
1703
1905
  * A metric data point normalized for storage. Series-identifying fields
@@ -1855,4 +2057,4 @@ declare const _default: hono.Hono<{
1855
2057
  Bindings: CollectorEnv;
1856
2058
  }, hono_types.BlankSchema, "/">;
1857
2059
 
1858
- export { type ActionGraphPurgeSummary, type ActionGraphRetentionOptions, type ActionRecord, ActionStore, type AgentRunRecord, AlertsStore, type AnalysesRunSummary, type ArtifactRecord, type BlobListOptions, type BlobListResult, type BlobObject, type BlobPutOptions, type BlobStore, BlobStoreToR2Adapter, type CollectorAuthConfig, type CollectorConfig, CollectorEnv, type CollectorPlugin, CollectorRouteContext, CollectorRuntime, CounterfactualVerificationError, D1Adapter, type DashboardAuthConfig, type EvalCase, type EvalCaseInput, type EvalCaseListOptions, type EvalCaseSourceLinks, EvalCaseSourceNotFoundError, type EvalCaseSourceType, EvalCasesStore, type EvalResultRecord, type EvalRun, type EvalRunCandidate, type EvalRunInput, type EvalRunSourceCase, type EvalRunStatus, type EvalRunsListOptions, type HostLookup, Logger, MetricsStore, PostgresAdapter, type PostgresAdapterOptions, type PostgresTypeParserRegistry, ProjectsStore, R2BlobStore, RemediationGateError, type RetentionCleanupSummary, type RetrievalEventRecord, S3BlobStore, type S3BlobStoreOptions, type ScheduledTaskHandler, type SpanProcessorPlugin, type SqlDb, type SqlDbFactory, type SqlStatement, type TailEvent, TailHub, type TailKind, type ToolCallRecord, type UsageEventProcessorPlugin, type WatchdogEvaluationSummary, acquireIncidentLease, actionRetentionCutoff, actionRoutesPlugin, aiReceiverPlugin, aiSpanPayloadsProcessorPlugin, alertsRoutesPlugin, allPlugins, analysesRoutesPlugin, approveRemediationProposal, askRoutesPlugin, autosreRoutesPlugin, botFilterPlugin, claimRemediationGrant, compareValue, completeRemediationProposal, computeChangeBlastRadius, computeTarjanScc, configurePostgresTypeParsers, connectedRoutesPlugin, createAlertEvaluatorHandler, createAnalysesRunHandler, createDashboardAuth, createDefaultCollectorApp, createRetentionCleanupHandler, createTelemetryCollectorApp, dashboardRoutesPlugin, _default as default, defaultSpanEnrichmentPlugin, dispatchIncidentWebhook, evalCasesRoutesPlugin, evaluateAllRules, evaluateRemediationWatchdogs, evidenceRetrievalRoutesPlugin, extractSpanLineageAndTopology, failRemediationProposal, findCrossTraceContention, genAiNormalizerPlugin, getChangeEvent, getIncidentInvestigation, getInvestigationFinding, getOntologyGraph, getRemediationProposal, identityReceiverPlugin, isEvalCaseSourceType, isEvalRunStatus, issueEnrichmentPlugin, issueInsightsPlugin, listChangeEvents, listIncidentInvestigations, listRemediationProposals, loadTraceFaultLocusFromDb, localizeTraceFaultLocus, logsReceiverPlugin, metricsReceiverPlugin, onboardingRoutesPlugin, openOrDeduplicateIncident, otlpReceiverPlugin, platformRoutesPlugin, profileRoutesPlugin, projectsRoutesPlugin, proposeRemediationAction, purgeExpiredActionGraph, queryRoutesPlugin, recordChangeEvent, recordInvestigationFinding, redactionProcessorPlugin, rejectRemediationProposal, replayQueryRoutesPlugin, replayReceiverPlugin, searchInvestigationFindings, tailRoutesPlugin, timelineRoutesPlugin, uaEnrichmentPlugin, upsertOntologyFromSpans, usageNormalizationPlugin, usagePrivacyPlugin, usageQueryRoutesPlugin, usageReceiverPlugin, usersQueryRoutesPlugin, utmEnrichmentPlugin, verifyPairedCounterfactualProof };
2060
+ export { type ActionGraphPurgeSummary, type ActionGraphRetentionOptions, type ActionRecord, ActionStore, type AgentRunRecord, AlertEmailTransport, AlertsStore, type AnalysesRunSummary, type ArtifactRecord, type BlobListOptions, type BlobListResult, type BlobObject, type BlobPutOptions, type BlobStore, BlobStoreToR2Adapter, type CollectorAuthConfig, type CollectorConfig, CollectorEnv, type CollectorPlugin, CollectorRouteContext, CollectorRuntime, CounterfactualVerificationError, D1Adapter, type DashboardAuthConfig, type EvalCase, type EvalCaseInput, type EvalCaseListOptions, type EvalCaseSourceLinks, EvalCaseSourceNotFoundError, type EvalCaseSourceType, EvalCasesStore, type EvalResultRecord, type EvalRun, type EvalRunCandidate, type EvalRunInput, type EvalRunSourceCase, type EvalRunStatus, type EvalRunsListOptions, type HostLookup, Logger, MetricsStore, PostgresAdapter, type PostgresAdapterOptions, type PostgresTypeParserRegistry, ProjectsStore, R2BlobStore, RemediationGateError, type RetentionCleanupSummary, type RetrievalEventRecord, S3BlobStore, type S3BlobStoreOptions, type ScheduledTaskHandler, type SpanProcessorPlugin, type SqlDb, type SqlDbFactory, type SqlStatement, type TailEvent, TailHub, type TailKind, type ToolCallRecord, type UsageEventProcessorPlugin, type WatchdogEvaluationSummary, acquireIncidentLease, actionRetentionCutoff, actionRoutesPlugin, aiReceiverPlugin, aiSpanPayloadsProcessorPlugin, alertsRoutesPlugin, allPlugins, analysesRoutesPlugin, approveRemediationProposal, askRoutesPlugin, autosreRoutesPlugin, botFilterPlugin, claimRemediationGrant, compareValue, completeRemediationProposal, computeChangeBlastRadius, computeTarjanScc, configurePostgresTypeParsers, connectedRoutesPlugin, createAlertEvaluatorHandler, createAnalysesRunHandler, createDashboardAuth, createDefaultCollectorApp, createRetentionCleanupHandler, createTelemetryCollectorApp, dashboardRoutesPlugin, _default as default, defaultSpanEnrichmentPlugin, dispatchIncidentWebhook, evalCasesRoutesPlugin, evaluateAllRules, evaluateMetricQuery, evaluateRemediationWatchdogs, evaluateRuleBatch, evidenceRetrievalRoutesPlugin, extractSpanLineageAndTopology, failRemediationProposal, findCrossTraceContention, genAiNormalizerPlugin, getChangeEvent, getIncidentInvestigation, getInvestigationFinding, getOntologyGraph, getRemediationProposal, identityReceiverPlugin, isEvalCaseSourceType, isEvalRunStatus, issueEnrichmentPlugin, issueInsightsPlugin, listChangeEvents, listIncidentInvestigations, listRemediationProposals, loadTraceFaultLocusFromDb, localizeTraceFaultLocus, logsReceiverPlugin, metricsReceiverPlugin, onboardingRoutesPlugin, openOrDeduplicateIncident, otlpReceiverPlugin, platformRoutesPlugin, profileRoutesPlugin, projectsRoutesPlugin, proposeRemediationAction, purgeExpiredActionGraph, queryRoutesPlugin, recordChangeEvent, recordInvestigationFinding, redactionProcessorPlugin, reduceSeries, rejectRemediationProposal, replayQueryRoutesPlugin, replayReceiverPlugin, searchInvestigationFindings, tailRoutesPlugin, timelineRoutesPlugin, uaEnrichmentPlugin, upsertOntologyFromSpans, usageNormalizationPlugin, usagePrivacyPlugin, usageQueryRoutesPlugin, usageReceiverPlugin, usersQueryRoutesPlugin, utmEnrichmentPlugin, validateMetricQuery, verifyPairedCounterfactualProof };