@obsunified/collector 2.2.1 → 2.3.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 (53) hide show
  1. package/dist/auth.d.ts +1 -1
  2. package/dist/auth.js +1 -1
  3. package/dist/{chunk-KMEZKGSO.js → chunk-2FALTDCE.js} +12 -4
  4. package/dist/chunk-2FALTDCE.js.map +1 -0
  5. package/dist/{chunk-ZSF5XP3B.js → chunk-RASSTGNB.js} +30 -22
  6. package/dist/chunk-RASSTGNB.js.map +1 -0
  7. package/dist/index.d.ts +10 -2
  8. package/dist/index.js +2892 -2240
  9. package/dist/index.js.map +1 -1
  10. package/dist/{ingest-auth-C5-8Mly5.d.ts → ingest-auth-BmP5-f96.d.ts} +32 -1
  11. package/dist/{openai-GSJZGFGL.js → openai-VDKVVZAV.js} +2 -2
  12. package/package.json +4 -3
  13. package/src/__tests__/autosre-closed-loop.test.ts +700 -0
  14. package/src/__tests__/db-parity.integration.test.ts +961 -0
  15. package/src/__tests__/internal-contracts-routes.test.ts +451 -0
  16. package/src/auth/dashboard-auth.test.ts +61 -0
  17. package/src/auth/dashboard-auth.ts +17 -2
  18. package/src/framework/env.ts +105 -0
  19. package/src/index.ts +13 -4
  20. package/src/lib/ai-store/calls.ts +43 -23
  21. package/src/lib/ai-store/evaluations.test.ts +3 -3
  22. package/src/lib/ai-store.ts +12 -1
  23. package/src/lib/analyses-store.test.ts +6 -6
  24. package/src/lib/autosre-findings.ts +169 -14
  25. package/src/lib/autosre-remediation-gate.ts +156 -26
  26. package/src/lib/db-parity.ts +883 -0
  27. package/src/lib/eval-cases-store.test.ts +1 -1
  28. package/src/lib/eval-cases-store.ts +16 -4
  29. package/src/lib/evidence-references.ts +30 -22
  30. package/src/lib/projects-store.test.ts +38 -1
  31. package/src/lib/projects-store.ts +23 -4
  32. package/src/lib/sql-db-postgres.integration.test.ts +75 -2
  33. package/src/lib/stage6.test.ts +1 -1
  34. package/src/lib/usage-store.ts +9 -3
  35. package/src/migrations/052_remediation_callback.sql +10 -0
  36. package/src/migrations/053_remediation_callback_delivery.sql +7 -0
  37. package/src/migrations/parity.test.ts +105 -3
  38. package/src/migrations-postgres/052_remediation_callback.sql +10 -0
  39. package/src/migrations-postgres/053_remediation_callback_delivery.sql +7 -0
  40. package/src/plugins/ai-call-by-id.test.ts +73 -0
  41. package/src/plugins/ai-receiver.ts +15 -0
  42. package/src/plugins/autosre-routes.ts +409 -147
  43. package/src/plugins/connected-routes/deep-link-shapes.test.ts +44 -0
  44. package/src/plugins/connected-routes/manifest.test.ts +224 -0
  45. package/src/plugins/connected-routes/manifest.ts +131 -28
  46. package/src/plugins/connected-routes-links.test.ts +96 -0
  47. package/src/plugins/connected-routes.test.ts +13 -11
  48. package/src/plugins/connected-routes.ts +53 -20
  49. package/src/plugins/eval-cases-routes.ts +33 -10
  50. package/src/plugins/evidence-retrieval-routes.ts +89 -24
  51. package/dist/chunk-KMEZKGSO.js.map +0 -1
  52. package/dist/chunk-ZSF5XP3B.js.map +0 -1
  53. /package/dist/{openai-GSJZGFGL.js.map → openai-VDKVVZAV.js.map} +0 -0
@@ -335,7 +335,7 @@ describe("ConnectedRail manifest — span profile section (RFC 0009 #5)", () =>
335
335
  expect(exemplarSection).toBeDefined();
336
336
  expect(exemplarSection?.links[0].label).toContain("http.server.duration");
337
337
  expect(exemplarSection?.links[0].href).toBe(
338
- "#/traces/trace-metric#span=span-hot",
338
+ "#/traces?trace=trace-metric&span=span-hot",
339
339
  );
340
340
  });
341
341
  });
@@ -470,7 +470,7 @@ describe("ConnectedRail manifest — raw signal action back-links", () => {
470
470
  m.down.find((s) => s.label === "Evaluations for this span")?.links[0],
471
471
  ).toMatchObject({
472
472
  label: "eval: groundedness (passed)",
473
- href: "#/evals/eval-explicit",
473
+ href: "#/evaluations/eval-explicit",
474
474
  });
475
475
  expect(
476
476
  m.related.find((s) => s.label === "Agent runs for this span")?.links[0],
@@ -673,7 +673,7 @@ describe("ConnectedRail manifest — raw signal action back-links", () => {
673
673
  ?.links[0],
674
674
  ).toMatchObject({
675
675
  label: "[agent.step] Fallback normalized action",
676
- href: "#/actions/fallback:trace-fallback:agent.step:1",
676
+ href: "#/actions/fallback%3Atrace-fallback%3Aagent.step%3A1",
677
677
  causalConfidence: ActionConfidence.Fallback,
678
678
  source: "trace_id",
679
679
  confidence: 0.55,
@@ -749,7 +749,7 @@ describe("ConnectedRail manifest — raw signal action back-links", () => {
749
749
  ).toMatchObject({ href: "#/tool-calls/tool-explicit" });
750
750
  expect(
751
751
  m.down.find((s) => s.label === "Evaluations for this AI call")?.links[0],
752
- ).toMatchObject({ href: "#/evals/eval-explicit" });
752
+ ).toMatchObject({ href: "#/evaluations/eval-explicit" });
753
753
  expect(
754
754
  m.related.find((s) => s.label === "Agent runs for this AI call")
755
755
  ?.links[0],
@@ -831,12 +831,12 @@ describe("ConnectedRail manifest — profile source entity", () => {
831
831
  const traceSection = m.across.find((s) => s.label === "Sampled traces");
832
832
  expect(traceSection?.links[0]).toMatchObject({
833
833
  label: "trace trace-prof",
834
- href: "#/traces/trace-prof",
834
+ href: "#/traces?trace=trace-prof",
835
835
  });
836
836
 
837
837
  const spanSection = m.across.find((s) => s.label === "Sampled spans");
838
838
  expect(spanSection?.links[0].href).toBe(
839
- "#/traces/trace-prof#span=span-hot",
839
+ "#/traces?trace=trace-prof&span=span-hot",
840
840
  );
841
841
 
842
842
  const actionSection = m.down.find((s) =>
@@ -1108,7 +1108,9 @@ describe("ConnectedRail scenario acceptance contracts", () => {
1108
1108
  const sessionSpans = sessionManifest.across.find((s) =>
1109
1109
  s.label.toLowerCase().includes("span"),
1110
1110
  );
1111
- expect(sessionSpans?.links[0].href).toBe("#/traces/trace-a#span=span-hot");
1111
+ expect(sessionSpans?.links[0].href).toBe(
1112
+ "#/traces?trace=trace-a&span=span-hot",
1113
+ );
1112
1114
 
1113
1115
  const spanManifest = await fetch(
1114
1116
  "/internal/connected/span/trace-a:span-hot",
@@ -1280,7 +1282,7 @@ describe("ConnectedRail scenario acceptance contracts", () => {
1280
1282
  const aiSection = userManifest.across.find((s) =>
1281
1283
  s.label.toLowerCase().includes("ai call"),
1282
1284
  );
1283
- expect(aiSection?.links[0].href).toBe("#/ai?id=ai-heavy-1");
1285
+ expect(aiSection?.links[0].href).toBe("#/ai/calls/ai-heavy-1");
1284
1286
 
1285
1287
  const sessionManifest = await fetch(
1286
1288
  "/internal/connected/usage/sess-heavy?session_id=sess-heavy",
@@ -1289,7 +1291,7 @@ describe("ConnectedRail scenario acceptance contracts", () => {
1289
1291
  s.label.toLowerCase().includes("span"),
1290
1292
  );
1291
1293
  expect(sessionSpans?.links[0].href).toBe(
1292
- "#/traces/trace-ai-heavy#span=span-llm",
1294
+ "#/traces?trace=trace-ai-heavy&span=span-llm",
1293
1295
  );
1294
1296
 
1295
1297
  const spanManifest = await fetch(
@@ -1464,7 +1466,7 @@ describe("ConnectedRail manifest — agent action graph entities (RFC 0010)", ()
1464
1466
  // OTel Trace in related
1465
1467
  const relatedTrace = m.related.find((s) => s.label === "OTel Trace");
1466
1468
  expect(relatedTrace).toBeDefined();
1467
- expect(relatedTrace?.links[0].href).toBe("#/traces/tx-789");
1469
+ expect(relatedTrace?.links[0].href).toBe("#/traces?trace=tx-789");
1468
1470
  });
1469
1471
 
1470
1472
  it("action detail raw manifest exposes causal confidence on action refs", async () => {
@@ -1650,7 +1652,7 @@ describe("ConnectedRail manifest — agent action graph entities (RFC 0010)", ()
1650
1652
  // Up has Agent
1651
1653
  const agentSec = m.up.find((s) => s.label === "Agent");
1652
1654
  expect(agentSec).toBeDefined();
1653
- expect(agentSec?.links[0].href).toBe("#/agents/my-agent");
1655
+ expect(agentSec?.links[0].href).toBe("#/evidence?kind=agent&id=my-agent");
1654
1656
 
1655
1657
  // Down has Actions (Decision Spine)
1656
1658
  const decisionSpine = m.down.find(
@@ -15,7 +15,7 @@
15
15
  * RFC 0006 § Connected rail / Empty-state.
16
16
  */
17
17
 
18
- import type { CausalProofTier } from "@obsunified/types";
18
+ import { type CausalProofTier, dashboardRouteToHref } from "@obsunified/types";
19
19
  import { ActionConfidence } from "@obsunified/types/constants";
20
20
  import type { CollectorPlugin } from "../framework/collector";
21
21
  import { getChangeEvent } from "../lib/autosre-change-correlation";
@@ -37,11 +37,13 @@ import type {
37
37
  import type { SqlDb } from "../lib/sql-db";
38
38
  import { sqlDbFor } from "../lib/sql-db";
39
39
  import { getProjectId } from "./_context";
40
+ import { respondWithContract } from "./autosre-routes";
40
41
 
41
42
  import {
42
43
  type ActionContextLinkMetadata,
43
44
  type ConnectedEntityKind,
44
45
  type ConnectedManifest,
46
+ hrefForEntity,
45
47
  KNOWN_KINDS,
46
48
  linksFromActions,
47
49
  linksFromAi,
@@ -59,6 +61,7 @@ import {
59
61
  linkToTrace,
60
62
  MAX_LINKS_INLINE,
61
63
  profileLinksForTrace,
64
+ resolveFaultLocusTarget,
62
65
  } from "./connected-routes/manifest";
63
66
 
64
67
  export type {
@@ -454,7 +457,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
454
457
  ? [
455
458
  {
456
459
  label: `${traceIds.length} sampled traces`,
457
- href: `#/traces?trace=${encodeURIComponent(traceIds[0])}`,
460
+ href: dashboardRouteToHref({
461
+ tab: "traces",
462
+ traceId: traceIds[0],
463
+ }),
458
464
  count: traceIds.length,
459
465
  sample: traceIds[0],
460
466
  },
@@ -554,7 +560,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
554
560
  .slice(0, MAX_LINKS_INLINE)
555
561
  .map((toolCall) => ({
556
562
  label: `tool: ${toolCall.tool_name}`,
557
- href: `#/tool-calls/${toolCall.id}`,
563
+ href: dashboardRouteToHref({
564
+ tab: "tool-calls",
565
+ toolCallId: toolCall.id,
566
+ }),
558
567
  entityKind: "tool_call",
559
568
  entityId: toolCall.id,
560
569
  ...traceLevelMetadata("profile tool call"),
@@ -984,7 +993,7 @@ export const connectedRoutesPlugin: CollectorPlugin = {
984
993
  links: [
985
994
  {
986
995
  label: `${run.agentName} (v${run.agentVersion})`,
987
- href: `#/agents/${run.agentId}`,
996
+ href: hrefForEntity("agent", run.agentId),
988
997
  },
989
998
  ],
990
999
  },
@@ -1068,7 +1077,7 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1068
1077
  links: [
1069
1078
  {
1070
1079
  label: `${changeRow.targetNodeId} (${changeRow.environment})`,
1071
- href: `#/health?node=${encodeURIComponent(changeRow.targetNodeId)}`,
1080
+ ...resolveFaultLocusTarget(changeRow.targetNodeId),
1072
1081
  relationshipCertainty: "exact",
1073
1082
  reason: "Target node mutated by this change event.",
1074
1083
  },
@@ -1097,7 +1106,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1097
1106
  label: "Attributed Findings",
1098
1107
  links: findings.results.map((f) => ({
1099
1108
  label: f.title,
1100
- href: `#/findings/${encodeURIComponent(f.id)}`,
1109
+ href: dashboardRouteToHref({
1110
+ tab: "findings",
1111
+ findingId: f.id,
1112
+ }),
1101
1113
  entityKind: "finding",
1102
1114
  entityId: f.id,
1103
1115
  relationshipCertainty: "exact",
@@ -1115,10 +1127,11 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1115
1127
  const spans = changeRow.version
1116
1128
  ? await db
1117
1129
  .prepare(
1118
- `SELECT DISTINCT trace_id, span_id, service_name, span_name
1130
+ `SELECT trace_id, span_id, service_name, span_name
1119
1131
  FROM telemetry_spans
1120
1132
  WHERE project_id = ? AND environment = ? AND service_version = ? AND status_code = 2
1121
- ORDER BY start_time DESC LIMIT 5`,
1133
+ GROUP BY trace_id, span_id, service_name, span_name
1134
+ ORDER BY MAX(start_time) DESC LIMIT 5`,
1122
1135
  )
1123
1136
  .bind(projectId, changeRow.environment, changeRow.version)
1124
1137
  .all<{
@@ -1134,7 +1147,11 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1134
1147
  label: "Failing Spans on Cohort Version",
1135
1148
  links: spans.results.map((s) => ({
1136
1149
  label: `${s.service_name} · ${s.span_name}`,
1137
- href: `#/traces/${s.trace_id}#span=${s.span_id}`,
1150
+ href: dashboardRouteToHref({
1151
+ tab: "traces",
1152
+ traceId: s.trace_id,
1153
+ spanId: s.span_id,
1154
+ }),
1138
1155
  entityKind: "span",
1139
1156
  entityId: `${s.trace_id}:${s.span_id}`,
1140
1157
  relationshipCertainty: "exact",
@@ -1164,7 +1181,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1164
1181
  links: [
1165
1182
  {
1166
1183
  label: `incident ${findingRow.incidentId}`,
1167
- href: `#/incidents/${encodeURIComponent(findingRow.incidentId)}`,
1184
+ href: dashboardRouteToHref({
1185
+ tab: "incidents",
1186
+ incidentId: findingRow.incidentId,
1187
+ }),
1168
1188
  entityKind: "incident",
1169
1189
  entityId: findingRow.incidentId,
1170
1190
  relationshipCertainty: "exact",
@@ -1179,7 +1199,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1179
1199
  links: [
1180
1200
  {
1181
1201
  label: `change ${findingRow.contributingChangeId}`,
1182
- href: `#/changes/${encodeURIComponent(findingRow.contributingChangeId)}`,
1202
+ href: dashboardRouteToHref({
1203
+ tab: "changes",
1204
+ changeId: findingRow.contributingChangeId,
1205
+ }),
1183
1206
  entityKind: "change",
1184
1207
  entityId: findingRow.contributingChangeId,
1185
1208
  relationshipCertainty: "exact",
@@ -1195,7 +1218,7 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1195
1218
  links: [
1196
1219
  {
1197
1220
  label: `fault locus: ${findingRow.faultLocusEntityId}`,
1198
- href: `#/traces?q=${encodeURIComponent(findingRow.faultLocusEntityId)}`,
1221
+ ...resolveFaultLocusTarget(findingRow.faultLocusEntityId),
1199
1222
  relationshipCertainty: "exact",
1200
1223
  causalProofTier: proofTier,
1201
1224
  },
@@ -1203,7 +1226,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1203
1226
  ? [
1204
1227
  {
1205
1228
  label: `contending trace ${findingRow.contendingTraceId}`,
1206
- href: `#/traces/${encodeURIComponent(findingRow.contendingTraceId)}`,
1229
+ href: dashboardRouteToHref({
1230
+ tab: "traces",
1231
+ traceId: findingRow.contendingTraceId,
1232
+ }),
1207
1233
  entityKind: "trace" as const,
1208
1234
  entityId: findingRow.contendingTraceId,
1209
1235
  relationshipCertainty: "inferred" as const,
@@ -1249,11 +1275,11 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1249
1275
  links: [
1250
1276
  {
1251
1277
  label: `${incRow.symptomAnchorKind}: ${incRow.symptomAnchorId}`,
1252
- href: `#/alerts?id=${encodeURIComponent(incRow.symptomAnchorId)}`,
1253
- entityKind:
1254
- incRow.symptomAnchorKind === "analysis"
1255
- ? "analysis"
1256
- : "alert",
1278
+ href: hrefForEntity(
1279
+ incRow.symptomAnchorKind,
1280
+ incRow.symptomAnchorId,
1281
+ ),
1282
+ entityKind: incRow.symptomAnchorKind,
1257
1283
  entityId: incRow.symptomAnchorId,
1258
1284
  relationshipCertainty: "exact",
1259
1285
  },
@@ -1277,7 +1303,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1277
1303
  label: "Findings",
1278
1304
  links: incFindings.results.map((f) => ({
1279
1305
  label: f.title,
1280
- href: `#/findings/${encodeURIComponent(f.id)}`,
1306
+ href: dashboardRouteToHref({
1307
+ tab: "findings",
1308
+ findingId: f.id,
1309
+ }),
1281
1310
  entityKind: "finding" as const,
1282
1311
  entityId: f.id,
1283
1312
  relationshipCertainty: "exact" as const,
@@ -1347,7 +1376,11 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1347
1376
  ];
1348
1377
  }
1349
1378
 
1350
- return c.json(manifest);
1379
+ return respondWithContract(
1380
+ c,
1381
+ "GET /internal/connected/:kind/:id",
1382
+ manifest,
1383
+ );
1351
1384
  });
1352
1385
  },
1353
1386
  };
@@ -3,7 +3,10 @@ import type {
3
3
  EvidenceReferenceContract as EvidenceReferenceContractType,
4
4
  JsonValue,
5
5
  } from "@obsunified/types";
6
- import { EVIDENCE_REFERENCE_CONTRACT } from "@obsunified/types";
6
+ import {
7
+ dashboardRouteToHref,
8
+ EVIDENCE_REFERENCE_CONTRACT,
9
+ } from "@obsunified/types";
7
10
  import type { CollectorPlugin } from "../framework/collector";
8
11
  import {
9
12
  type EvalCase,
@@ -20,6 +23,7 @@ import { randomHex } from "../lib/hash";
20
23
  import { sqlDbFor } from "../lib/sql-db";
21
24
  import { getProjectId } from "./_context";
22
25
  import { readJsonObject } from "./_json";
26
+ import { respondWithContract } from "./autosre-routes";
23
27
 
24
28
  interface EvalCaseRequestBody {
25
29
  sourceEntityType?: unknown;
@@ -122,7 +126,10 @@ const evalCaseResultEvidenceReferences = (
122
126
  sourceLabel: `Eval case result "${resultId}"`,
123
127
  sourceId: resultId,
124
128
  sourceKind: "eval_case_result",
125
- sourceRoute: `#/evaluations?case=${encodeURIComponent(evalCase.id)}&result=${encodeURIComponent(resultId)}`,
129
+ sourceRoute: dashboardRouteToHref({
130
+ tab: "evaluations",
131
+ evaluationId: evalCase.id,
132
+ }),
126
133
  sourceName: evalCase.name,
127
134
  },
128
135
  evalCase,
@@ -189,7 +196,12 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
189
196
  const store = new EvalCasesStore(sqlDbFor(c.env));
190
197
  try {
191
198
  const evalCase = await store.createCase(getProjectId(c), input);
192
- return c.json({ evalCase }, 201);
199
+ return respondWithContract(
200
+ c,
201
+ "POST /internal/eval-cases",
202
+ { evalCase },
203
+ 201,
204
+ );
193
205
  } catch (err) {
194
206
  if (err instanceof EvalCaseSourceNotFoundError) {
195
207
  return c.json(
@@ -223,7 +235,7 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
223
235
  sourceEntityId: c.req.query("sourceEntityId"),
224
236
  limit: parseLimit(c.req.query("limit")),
225
237
  });
226
- return c.json({ evalCases });
238
+ return respondWithContract(c, "GET /internal/eval-cases", { evalCases });
227
239
  });
228
240
 
229
241
  app.get("/internal/eval-cases/:id", async (c) => {
@@ -237,7 +249,9 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
237
249
  404,
238
250
  );
239
251
  }
240
- return c.json({ evalCase });
252
+ return respondWithContract(c, "GET /internal/eval-cases/:id", {
253
+ evalCase,
254
+ });
241
255
  });
242
256
 
243
257
  app.post("/internal/eval-runs", async (c) => {
@@ -349,7 +363,12 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
349
363
  const store = new EvalCasesStore(sqlDbFor(c.env));
350
364
  try {
351
365
  const evalRun = await store.createRun(getProjectId(c), input);
352
- return c.json({ evalRun }, 201);
366
+ return respondWithContract(
367
+ c,
368
+ "POST /internal/eval-runs",
369
+ { evalRun },
370
+ 201,
371
+ );
353
372
  } catch (err) {
354
373
  const message = err instanceof Error ? err.message : String(err);
355
374
  if (message.includes("does not reference a case")) {
@@ -377,7 +396,7 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
377
396
  status,
378
397
  limit: parseLimit(c.req.query("limit")),
379
398
  });
380
- return c.json({ evalRuns });
399
+ return respondWithContract(c, "GET /internal/eval-runs", { evalRuns });
381
400
  });
382
401
 
383
402
  app.get("/internal/eval-runs/:id", async (c) => {
@@ -391,7 +410,7 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
391
410
  404,
392
411
  );
393
412
  }
394
- return c.json({ evalRun });
413
+ return respondWithContract(c, "GET /internal/eval-runs/:id", { evalRun });
395
414
  });
396
415
 
397
416
  app.post("/internal/eval-cases/:id/results", async (c) => {
@@ -503,7 +522,9 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
503
522
  evidenceContract: EVIDENCE_REFERENCE_CONTRACT,
504
523
  };
505
524
 
506
- return c.json(
525
+ return respondWithContract(
526
+ c,
527
+ "POST /internal/eval-cases/:id/results",
507
528
  {
508
529
  evalCaseResult,
509
530
  evalRun: refreshedRun,
@@ -569,7 +590,9 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
569
590
  }),
570
591
  );
571
592
 
572
- return c.json({ evalCaseResults });
593
+ return respondWithContract(c, "GET /internal/eval-cases/:id/results", {
594
+ evalCaseResults,
595
+ });
573
596
  });
574
597
  },
575
598
  };
@@ -11,6 +11,10 @@ import type {
11
11
  TelemetrySpanDetail,
12
12
  TelemetryTraceDetailResponse,
13
13
  } from "@obsunified/types";
14
+ import {
15
+ buildFindingFingerprint,
16
+ dashboardRouteToHref,
17
+ } from "@obsunified/types";
14
18
  import type { CollectorPlugin } from "../framework/collector";
15
19
  import {
16
20
  computeChangeBlastRadius,
@@ -18,6 +22,7 @@ import {
18
22
  listChangeEvents,
19
23
  } from "../lib/autosre-change-correlation";
20
24
  import {
25
+ fingerprintInputFromSpan,
21
26
  getInvestigationFinding,
22
27
  searchInvestigationFindings,
23
28
  } from "../lib/autosre-findings";
@@ -39,6 +44,7 @@ import { LogsStore } from "../lib/logs-store";
39
44
  import { decodePprofBlob, summarizeProfileFrames } from "../lib/parse-pprof";
40
45
  import type { SqlDb } from "../lib/sql-db";
41
46
  import { getProjectId } from "./_context";
47
+ import { respondWithContract } from "./autosre-routes";
42
48
  import { fetchReplayChunks } from "./replay-query-routes";
43
49
 
44
50
  interface EvidenceRefPayload {
@@ -156,24 +162,35 @@ const estimateTokens = (value: unknown): number =>
156
162
  Math.ceil(JSON.stringify(value).length / 4);
157
163
 
158
164
  const traceRoute = (traceId: string) =>
159
- `#/traces/${encodeURIComponent(traceId)}`;
165
+ dashboardRouteToHref({ tab: "traces", traceId });
160
166
  const spanRoute = (traceId: string, spanId: string) =>
161
- `${traceRoute(traceId)}#span=${encodeURIComponent(spanId)}`;
162
- const logRoute = (logId: string) => `#/logs?id=${encodeURIComponent(logId)}`;
167
+ dashboardRouteToHref({ tab: "traces", traceId, spanId });
168
+ const logRoute = (logId: string) =>
169
+ dashboardRouteToHref({ tab: "logs", logId });
163
170
  const actionRoute = (actionId: string) =>
164
- `#/actions/${encodeURIComponent(actionId)}`;
171
+ dashboardRouteToHref({ tab: "actions", actionId });
165
172
  const agentRunRoute = (agentRunId: string) =>
166
- `#/agent-runs/${encodeURIComponent(agentRunId)}`;
173
+ dashboardRouteToHref({ tab: "agent-runs", agentRunId });
167
174
  const toolCallRoute = (toolCallId: string) =>
168
- `#/tool-calls/${encodeURIComponent(toolCallId)}`;
175
+ dashboardRouteToHref({ tab: "tool-calls", toolCallId });
169
176
  const replayRoute = (sessionId: string) =>
170
- `#/replay?session=${encodeURIComponent(sessionId)}`;
177
+ dashboardRouteToHref({ tab: "replay", sessionId });
171
178
  const profileRoute = (profileId: string, traceId?: string) =>
172
- `#/profiles/${encodeURIComponent(profileId)}${
173
- traceId ? `?trace_id=${encodeURIComponent(traceId)}` : ""
174
- }`;
179
+ dashboardRouteToHref({
180
+ tab: "profiles",
181
+ profileId,
182
+ ...(traceId ? { traceId } : {}),
183
+ });
175
184
  const aiCallRoute = (callId: string) =>
176
- `#/ai/calls/${encodeURIComponent(callId)}`;
185
+ dashboardRouteToHref({ tab: "ai", aiCallId: callId });
186
+ const evalRoute = (evaluationId: string) =>
187
+ dashboardRouteToHref({ tab: "evaluations", evaluationId });
188
+ const changeEntityRoute = (changeId: string) =>
189
+ dashboardRouteToHref({ tab: "changes", changeId });
190
+ const findingEntityRoute = (findingId: string) =>
191
+ dashboardRouteToHref({ tab: "findings", findingId });
192
+ const incidentEntityRoute = (incidentId: string) =>
193
+ dashboardRouteToHref({ tab: "incidents", incidentId });
177
194
 
178
195
  const evidenceRef = (
179
196
  evidenceId: string,
@@ -1067,7 +1084,7 @@ const actionEvidenceReferences = (
1067
1084
  `eval:${evalResult.id}`,
1068
1085
  "eval",
1069
1086
  evalResult.id,
1070
- `#/evals/${encodeURIComponent(evalResult.id)}`,
1087
+ evalRoute(evalResult.id),
1071
1088
  "evidence.bundle.eval_results",
1072
1089
  evalResult.passed ? 0.75 : 0.9,
1073
1090
  `Eval ${evalResult.evaluatorName} ${evalResult.passed ? "passed" : "failed"}${evalResult.score === null ? "" : ` with score ${evalResult.score}`}.`,
@@ -1607,6 +1624,34 @@ const attachAutoSreTriageContext = async (
1607
1624
  limit: 5,
1608
1625
  });
1609
1626
  }
1627
+ // Secondary match: the same recurrence fingerprint (service + route +
1628
+ // error signature) in this project, so a failure that recurs on a
1629
+ // different span id still surfaces the findings and fixes recorded
1630
+ // before. Entity-id matches stay first.
1631
+ if (faultLocus?.faultLocusSpan) {
1632
+ const spanFp = fingerprintInputFromSpan(
1633
+ faultLocus.faultLocusSpan,
1634
+ environment,
1635
+ );
1636
+ const fingerprint = buildFindingFingerprint({
1637
+ ...spanFp,
1638
+ normalizedOperation: spanFp.normalizedOperation ?? "",
1639
+ errorSignature: spanFp.errorSignature ?? "",
1640
+ }).fingerprint;
1641
+ const byFingerprint = await searchInvestigationFindings(db, {
1642
+ projectId,
1643
+ environment,
1644
+ fingerprint,
1645
+ limit: 5,
1646
+ });
1647
+ const seen = new Set(priorFindingsList.map((f) => f.id));
1648
+ for (const f of byFingerprint) {
1649
+ if (!seen.has(f.id) && priorFindingsList.length < 5) {
1650
+ priorFindingsList.push(f);
1651
+ seen.add(f.id);
1652
+ }
1653
+ }
1654
+ }
1610
1655
  // No unfiltered fallback: unrelated findings would read as relevant
1611
1656
  // prior art. An empty list is the honest answer.
1612
1657
  const priorFindings = priorFindingsList.map((f) => ({
@@ -1621,7 +1666,7 @@ const attachAutoSreTriageContext = async (
1621
1666
 
1622
1667
  for (const br of blastRadii) {
1623
1668
  const changeId = br.change.id;
1624
- const changeRoute = `#/changes/${encodeURIComponent(changeId)}`;
1669
+ const changeRoute = changeEntityRoute(changeId);
1625
1670
  bundle.evidenceReferences.push({
1626
1671
  evidenceId: `change:${changeId}`,
1627
1672
  entityKind: "change",
@@ -1655,7 +1700,7 @@ const attachAutoSreTriageContext = async (
1655
1700
  }
1656
1701
 
1657
1702
  for (const finding of priorFindingsList) {
1658
- const findingRoute = `#/findings/${encodeURIComponent(finding.id)}`;
1703
+ const findingRoute = findingEntityRoute(finding.id);
1659
1704
  bundle.evidenceReferences.push({
1660
1705
  evidenceId: `finding:${finding.id}`,
1661
1706
  entityKind: "finding",
@@ -1867,7 +1912,11 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
1867
1912
  runtime.logger,
1868
1913
  );
1869
1914
  }
1870
- return c.json(await materializeBundle(db, projectId, bundle));
1915
+ return respondWithContract(
1916
+ c,
1917
+ "POST /internal/evidence/bundle",
1918
+ await materializeBundle(db, projectId, bundle),
1919
+ );
1871
1920
  }
1872
1921
 
1873
1922
  if (anchor.entityKind === "incident") {
@@ -1889,7 +1938,7 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
1889
1938
  { kind: incident.symptomAnchorKind, id: incident.symptomAnchorId },
1890
1939
  );
1891
1940
 
1892
- const incRoute = `#/incidents/${encodeURIComponent(incident.id)}`;
1941
+ const incRoute = incidentEntityRoute(incident.id);
1893
1942
  const baseBundle: EvidenceBundle = {
1894
1943
  schemaVersion: BUNDLE_SCHEMA_VERSION,
1895
1944
  intent,
@@ -1958,7 +2007,11 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
1958
2007
  baseBundle,
1959
2008
  runtime.logger,
1960
2009
  );
1961
- return c.json(await materializeBundle(db, projectId, enriched));
2010
+ return respondWithContract(
2011
+ c,
2012
+ "POST /internal/evidence/bundle",
2013
+ await materializeBundle(db, projectId, enriched),
2014
+ );
1962
2015
  }
1963
2016
 
1964
2017
  if (anchor.entityKind === "change") {
@@ -1982,7 +2035,7 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
1982
2035
  null,
1983
2036
  );
1984
2037
 
1985
- const changeRoute = `#/changes/${encodeURIComponent(change.id)}`;
2038
+ const changeRoute = changeEntityRoute(change.id);
1986
2039
  const summary =
1987
2040
  blastRadius?.summary ??
1988
2041
  `Change ${change.id} (${change.kind} ${change.version ?? ""}) in ${change.environment}: ${change.summary}`;
@@ -2071,7 +2124,11 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
2071
2124
  baseBundle,
2072
2125
  runtime.logger,
2073
2126
  );
2074
- return c.json(await materializeBundle(db, projectId, enriched));
2127
+ return respondWithContract(
2128
+ c,
2129
+ "POST /internal/evidence/bundle",
2130
+ await materializeBundle(db, projectId, enriched),
2131
+ );
2075
2132
  }
2076
2133
 
2077
2134
  if (anchor.entityKind === "finding") {
@@ -2099,7 +2156,7 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
2099
2156
  { kind: finding.symptomEntityKind, id: finding.symptomEntityId },
2100
2157
  );
2101
2158
 
2102
- const findingRoute = `#/findings/${encodeURIComponent(finding.id)}`;
2159
+ const findingRoute = findingEntityRoute(finding.id);
2103
2160
  const summary = `${finding.title} (${finding.causalProofTier}, verification=${finding.causalVerificationStatus}, recovery=${finding.recoveryStatus}): ${finding.summary}`;
2104
2161
  const baseBundle: EvidenceBundle = {
2105
2162
  schemaVersion: BUNDLE_SCHEMA_VERSION,
@@ -2189,7 +2246,11 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
2189
2246
  baseBundle,
2190
2247
  runtime.logger,
2191
2248
  );
2192
- return c.json(await materializeBundle(db, projectId, enriched));
2249
+ return respondWithContract(
2250
+ c,
2251
+ "POST /internal/evidence/bundle",
2252
+ await materializeBundle(db, projectId, enriched),
2253
+ );
2193
2254
  }
2194
2255
 
2195
2256
  if (anchor.entityKind !== "trace") {
@@ -2236,7 +2297,11 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
2236
2297
  runtime.logger,
2237
2298
  );
2238
2299
  }
2239
- return c.json(await materializeBundle(db, projectId, bundle));
2300
+ return respondWithContract(
2301
+ c,
2302
+ "POST /internal/evidence/bundle",
2303
+ await materializeBundle(db, projectId, bundle),
2304
+ );
2240
2305
  });
2241
2306
 
2242
2307
  app.get("/internal/evidence/stats", async (c) => {
@@ -2328,7 +2393,7 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
2328
2393
  expanded_at: string;
2329
2394
  }>(),
2330
2395
  ]);
2331
- return c.json({
2396
+ return respondWithContract(c, "GET /internal/evidence/stats", {
2332
2397
  projectId,
2333
2398
  generatedAt: new Date().toISOString(),
2334
2399
  byKind: byKind.results.map((row) => ({
@@ -2366,7 +2431,7 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
2366
2431
  })),
2367
2432
  });
2368
2433
  } catch {
2369
- return c.json({
2434
+ return respondWithContract(c, "GET /internal/evidence/stats", {
2370
2435
  projectId,
2371
2436
  generatedAt: new Date().toISOString(),
2372
2437
  byKind: [],