@obsunified/collector 2.2.3 → 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 (32) hide show
  1. package/dist/auth.d.ts +1 -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 +2 -2
  5. package/dist/index.js +2562 -2311
  6. package/dist/index.js.map +1 -1
  7. package/dist/{ingest-auth-C5-8Mly5.d.ts → ingest-auth-BmP5-f96.d.ts} +32 -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 +105 -0
  14. package/src/index.ts +13 -4
  15. package/src/lib/ai-store/evaluations.test.ts +3 -3
  16. package/src/lib/analyses-store.test.ts +6 -6
  17. package/src/lib/db-parity.ts +883 -0
  18. package/src/lib/eval-cases-store.test.ts +1 -1
  19. package/src/lib/eval-cases-store.ts +16 -4
  20. package/src/lib/evidence-references.ts +30 -22
  21. package/src/lib/stage6.test.ts +1 -1
  22. package/src/migrations/parity.test.ts +105 -3
  23. package/src/plugins/ai-receiver.ts +6 -1
  24. package/src/plugins/autosre-routes.ts +251 -140
  25. package/src/plugins/connected-routes/manifest.test.ts +114 -32
  26. package/src/plugins/connected-routes/manifest.ts +59 -76
  27. package/src/plugins/connected-routes.test.ts +10 -8
  28. package/src/plugins/connected-routes.ts +42 -12
  29. package/src/plugins/eval-cases-routes.ts +33 -10
  30. package/src/plugins/evidence-retrieval-routes.ts +60 -25
  31. package/dist/chunk-ZSF5XP3B.js.map +0 -1
  32. /package/dist/{openai-GSJZGFGL.js.map → openai-VDKVVZAV.js.map} +0 -0
@@ -1,40 +1,25 @@
1
+ import { KNOWN_TABS, parseDashboardRouteHash } from "@obsunified/types";
1
2
  import { describe, expect, it } from "vitest";
2
3
  import {
3
4
  hrefForEntity,
5
+ linksFromActions,
6
+ linksFromAi,
4
7
  linksFromArtifacts,
5
8
  linksFromEvalResults,
9
+ linksFromLogs,
10
+ linksFromMetricExemplars,
6
11
  linksFromRetrievalEvents,
12
+ linksFromSpans,
7
13
  linksFromToolCalls,
14
+ linksFromUsage,
15
+ linkToAction,
16
+ linkToAgentRun,
17
+ linkToSession,
18
+ linkToTrace,
8
19
  MAX_LINKS_INLINE,
9
20
  resolveFaultLocusTarget,
10
21
  } from "./manifest";
11
22
 
12
- // Tabs served by apps/web/src/app/router.ts (KNOWN_TABS). Generated hrefs
13
- // must start with one of these or the app falls back to Health.
14
- const ROUTER_TABS = new Set([
15
- "health",
16
- "investigate",
17
- "traces",
18
- "service-map",
19
- "issues",
20
- "logs",
21
- "ai",
22
- "evidence",
23
- "usage",
24
- "replay",
25
- "alerts",
26
- "users",
27
- "agent-runs",
28
- "actions",
29
- "tool-calls",
30
- "profiles",
31
- "tool-reliability",
32
- "evaluations",
33
- "changes",
34
- "findings",
35
- "incidents",
36
- "remediations",
37
- ]);
38
23
  const tabOf = (href: string) =>
39
24
  href.replace(/^#\//, "").split(/[/?#]/)[0] ?? "";
40
25
 
@@ -80,8 +65,105 @@ describe("connected manifest hrefs target real router tabs", () => {
80
65
  "#/evaluations/e1",
81
66
  "#/actions/a1",
82
67
  ]);
83
- for (const href of hrefs)
84
- expect(ROUTER_TABS.has(tabOf(href ?? ""))).toBe(true);
68
+ for (const href of hrefs) {
69
+ expect(KNOWN_TABS.has(tabOf(href ?? ""))).toBe(true);
70
+ expect(parseDashboardRouteHash(href ?? "").ok).toBe(true);
71
+ }
72
+ });
73
+
74
+ it("emits canonical routes from all link builders and round-trips through parseDashboardRouteHash", () => {
75
+ expect(linkToTrace("trace-1").href).toBe("#/traces?trace=trace-1");
76
+ expect(linkToSession("sess/1").href).toBe("#/replay?session=sess%2F1");
77
+ expect(linkToAction("act/1").href).toBe("#/actions/act%2F1");
78
+ expect(linkToAgentRun("run/1").href).toBe("#/agent-runs/run%2F1");
79
+
80
+ const inlineSpans = linksFromSpans(
81
+ [
82
+ {
83
+ traceId: "tr-1",
84
+ spanId: "sp-1",
85
+ serviceName: "api",
86
+ spanName: "GET /",
87
+ } as never,
88
+ ],
89
+ "Spans",
90
+ );
91
+ expect(inlineSpans.links[0]?.href).toBe("#/traces?trace=tr-1&span=sp-1");
92
+ expect(parseDashboardRouteHash(inlineSpans.links[0]?.href ?? "")).toEqual({
93
+ ok: true,
94
+ route: { tab: "traces", traceId: "tr-1", spanId: "sp-1" },
95
+ });
96
+
97
+ const manySpans = linksFromSpans(
98
+ Array.from({ length: MAX_LINKS_INLINE + 1 }, (_, i) => ({
99
+ traceId: "tr-many",
100
+ spanId: `sp-${i}`,
101
+ serviceName: "api",
102
+ spanName: "op",
103
+ })) as never,
104
+ "Spans",
105
+ );
106
+ expect(manySpans.links[0]?.href).toBe("#/traces?q=tr-many");
107
+
108
+ const inlineLogs = linksFromLogs(
109
+ [
110
+ {
111
+ logId: "log-1",
112
+ traceId: "tr-1",
113
+ severity: "ERROR",
114
+ message: "boom",
115
+ } as never,
116
+ ],
117
+ "Logs",
118
+ );
119
+ expect(inlineLogs.links[0]?.href).toBe("#/logs?id=log-1");
120
+
121
+ const inlineUsage = linksFromUsage(
122
+ [
123
+ {
124
+ eventId: "ev-1",
125
+ eventType: "click",
126
+ eventName: "buy",
127
+ } as never,
128
+ ],
129
+ "Usage",
130
+ );
131
+ expect(inlineUsage.links[0]?.href).toBe("#/usage?id=ev-1");
132
+
133
+ const inlineExemplars = linksFromMetricExemplars(
134
+ [
135
+ {
136
+ traceId: "tr-1",
137
+ spanId: "sp-2",
138
+ serviceName: "api",
139
+ metricName: "latency",
140
+ value: 42,
141
+ tsNs: "100",
142
+ } as never,
143
+ ],
144
+ "Exemplars",
145
+ );
146
+ expect(inlineExemplars.links[0]?.href).toBe(
147
+ "#/traces?trace=tr-1&span=sp-2",
148
+ );
149
+
150
+ const inlineAi = linksFromAi(
151
+ [
152
+ {
153
+ callId: "call/1",
154
+ provider: "openai",
155
+ modelName: "gpt-4o",
156
+ } as never,
157
+ ],
158
+ "AI",
159
+ );
160
+ expect(inlineAi.links[0]?.href).toBe("#/ai/calls/call%2F1");
161
+
162
+ const inlineActions = linksFromActions(
163
+ [{ id: "act-1", actionKind: "step", name: "plan" } as never],
164
+ "Actions",
165
+ );
166
+ expect(inlineActions.links[0]?.href).toBe("#/actions/act-1");
85
167
  });
86
168
 
87
169
  it("hrefForEntity only emits router tabs for every evidence kind", () => {
@@ -104,12 +186,12 @@ describe("connected manifest hrefs target real router tabs", () => {
104
186
  "replay",
105
187
  "docs",
106
188
  ]) {
107
- expect(ROUTER_TABS.has(tabOf(hrefForEntity(kind, "id-1"))), kind).toBe(
108
- true,
109
- );
189
+ const href = hrefForEntity(kind, "id-1");
190
+ expect(KNOWN_TABS.has(tabOf(href)), kind).toBe(true);
191
+ expect(parseDashboardRouteHash(href).ok, kind).toBe(true);
110
192
  }
111
193
  expect(hrefForEntity("trace", "abc")).toBe("#/traces?trace=abc");
112
- expect(hrefForEntity("alert", "al 1")).toBe("#/alerts?id=al%201");
194
+ expect(hrefForEntity("alert", "al 1")).toBe("#/alerts?id=al+1");
113
195
  expect(hrefForEntity("ai_call", "c1")).toBe("#/ai/calls/c1");
114
196
  });
115
197
  });
@@ -1,7 +1,10 @@
1
- import type {
2
- CausalProofTier,
3
- EvidenceEntityKind,
4
- RelationshipCertainty,
1
+ import {
2
+ type CausalProofTier,
3
+ dashboardRouteToHref,
4
+ type EvidenceEntityKind,
5
+ type RelationshipCertainty,
6
+ routeForEntity,
7
+ hrefForEntity as sharedHrefForEntity,
5
8
  } from "@obsunified/types";
6
9
  import type {
7
10
  ActionRef,
@@ -19,6 +22,8 @@ import type {
19
22
  import type { CausalConfidence } from "../../lib/identity-index/types";
20
23
  import type { SqlDb } from "../../lib/sql-db";
21
24
 
25
+ export { routeForEntity };
26
+
22
27
  export type ConnectedEntityKind =
23
28
  | "span"
24
29
  | "profile"
@@ -94,7 +99,7 @@ export const linkToTrace = (
94
99
  label?: string,
95
100
  ): ConnectedLink => ({
96
101
  label: label ?? `trace ${traceId.slice(0, 12)}`,
97
- href: `#/traces/${traceId}`,
102
+ href: dashboardRouteToHref({ tab: "traces", traceId }),
98
103
  entityKind: "trace",
99
104
  entityId: traceId,
100
105
  });
@@ -108,7 +113,7 @@ export const linkToSession = (sessionId: string): ConnectedLink => ({
108
113
  sessionId.length > 12
109
114
  ? `session …${sessionId.slice(-12)}`
110
115
  : `session ${sessionId}`,
111
- href: `#/replay?session=${encodeURIComponent(sessionId)}`,
116
+ href: dashboardRouteToHref({ tab: "replay", sessionId }),
112
117
  sample: sessionId,
113
118
  entityKind: "replay",
114
119
  entityId: sessionId,
@@ -131,7 +136,10 @@ export const linksFromSpans = (
131
136
  links: [
132
137
  {
133
138
  label: `${spans.length} spans`,
134
- href: `#/traces?q=${encodeURIComponent(spans[0].traceId)}`,
139
+ href: dashboardRouteToHref({
140
+ tab: "traces",
141
+ query: spans[0].traceId,
142
+ }),
135
143
  count: spans.length,
136
144
  sample: truncate(
137
145
  `${spans[0].serviceName ?? "?"} · ${spans[0].spanName}`,
@@ -144,7 +152,11 @@ export const linksFromSpans = (
144
152
  label: prefix,
145
153
  links: spans.map((s) => ({
146
154
  label: `${s.serviceName ?? "?"} · ${truncate(s.spanName, 40)}`,
147
- href: `#/traces/${s.traceId}#span=${s.spanId}`,
155
+ href: dashboardRouteToHref({
156
+ tab: "traces",
157
+ traceId: s.traceId,
158
+ spanId: s.spanId,
159
+ }),
148
160
  })),
149
161
  };
150
162
  };
@@ -167,7 +179,10 @@ export const linksFromLogs = (
167
179
  links: [
168
180
  {
169
181
  label: `${logs.length} logs`,
170
- href: `#/logs?q=${encodeURIComponent(sample.traceId ?? "")}`,
182
+ href: dashboardRouteToHref({
183
+ tab: "logs",
184
+ ...(sample.traceId ? { query: sample.traceId } : {}),
185
+ }),
171
186
  count: logs.length,
172
187
  sample: truncate(sample.message),
173
188
  },
@@ -178,7 +193,7 @@ export const linksFromLogs = (
178
193
  label: prefix,
179
194
  links: logs.map((l) => ({
180
195
  label: `[${l.severity}] ${truncate(l.message, 60)}`,
181
- href: `#/logs?id=${l.logId}`,
196
+ href: dashboardRouteToHref({ tab: "logs", logId: l.logId }),
182
197
  })),
183
198
  };
184
199
  };
@@ -200,7 +215,7 @@ export const linksFromUsage = (
200
215
  links: [
201
216
  {
202
217
  label: `${events.length} usage events`,
203
- href: `#/usage`,
218
+ href: dashboardRouteToHref({ tab: "usage" }),
204
219
  count: events.length,
205
220
  sample: truncate(`${events[0].eventType} · ${events[0].eventName}`),
206
221
  },
@@ -211,7 +226,7 @@ export const linksFromUsage = (
211
226
  label: prefix,
212
227
  links: events.map((e) => ({
213
228
  label: `${e.eventType} · ${truncate(e.eventName, 50)}`,
214
- href: `#/usage?id=${e.eventId}`,
229
+ href: dashboardRouteToHref({ tab: "usage", usageId: e.eventId }),
215
230
  })),
216
231
  };
217
232
  };
@@ -235,8 +250,8 @@ export const linksFromMetricExemplars = (
235
250
  {
236
251
  label: `${exemplars.length} metric exemplars`,
237
252
  href: sample.traceId
238
- ? `#/traces/${sample.traceId}`
239
- : `#/traces?q=${encodeURIComponent(sample.metricName)}`,
253
+ ? dashboardRouteToHref({ tab: "traces", traceId: sample.traceId })
254
+ : dashboardRouteToHref({ tab: "traces", query: sample.metricName }),
240
255
  count: exemplars.length,
241
256
  sample: truncate(
242
257
  `${sample.serviceName ?? "?"} · ${sample.metricName}=${sample.value}`,
@@ -250,10 +265,12 @@ export const linksFromMetricExemplars = (
250
265
  links: exemplars.map((e) => ({
251
266
  label: `${e.serviceName ?? "?"} · ${truncate(e.metricName, 40)}=${e.value}`,
252
267
  href: e.traceId
253
- ? e.spanId
254
- ? `#/traces/${e.traceId}#span=${e.spanId}`
255
- : `#/traces/${e.traceId}`
256
- : `#/traces?q=${encodeURIComponent(e.metricName)}`,
268
+ ? dashboardRouteToHref({
269
+ tab: "traces",
270
+ traceId: e.traceId,
271
+ ...(e.spanId ? { spanId: e.spanId } : {}),
272
+ })
273
+ : dashboardRouteToHref({ tab: "traces", query: e.metricName }),
257
274
  sample: e.tsNs,
258
275
  })),
259
276
  };
@@ -313,7 +330,11 @@ export const profileLinksForTrace = async (
313
330
  label: `${icon} ${type === "offcpu" ? "Off-CPU profiles" : `${type[0].toUpperCase()}${type.slice(1)} profiles`}`,
314
331
  links: list.map((r) => ({
315
332
  label: `${r.service_name ?? "?"} · ${r.duration_ms}ms`,
316
- href: `#/profiles/${r.id}?trace_id=${encodeURIComponent(traceId)}`,
333
+ href: dashboardRouteToHref({
334
+ tab: "profiles",
335
+ profileId: r.id,
336
+ traceId,
337
+ }),
317
338
  })),
318
339
  });
319
340
  }
@@ -337,7 +358,7 @@ export const linksFromAi = (
337
358
  links: [
338
359
  {
339
360
  label: `${calls.length} AI calls`,
340
- href: `#/ai`,
361
+ href: dashboardRouteToHref({ tab: "ai" }),
341
362
  count: calls.length,
342
363
  sample: truncate(`${calls[0].provider} · ${calls[0].modelName}`),
343
364
  },
@@ -348,7 +369,7 @@ export const linksFromAi = (
348
369
  label: prefix,
349
370
  links: calls.map((c) => ({
350
371
  label: `${c.provider} · ${c.modelName}`,
351
- href: `#/ai/calls/${encodeURIComponent(c.callId)}`,
372
+ href: dashboardRouteToHref({ tab: "ai", aiCallId: c.callId }),
352
373
  entityKind: "ai_call",
353
374
  entityId: c.callId,
354
375
  })),
@@ -360,7 +381,7 @@ export const linkToAction = (
360
381
  label?: string,
361
382
  ): ConnectedLink => ({
362
383
  label: label ?? `action ${actionId.slice(0, 12)}`,
363
- href: `#/actions/${actionId}`,
384
+ href: dashboardRouteToHref({ tab: "actions", actionId }),
364
385
  entityKind: "action",
365
386
  entityId: actionId,
366
387
  });
@@ -370,7 +391,7 @@ export const linkToAgentRun = (
370
391
  label?: string,
371
392
  ): ConnectedLink => ({
372
393
  label: label ?? `agent run ${runId.slice(0, 12)}`,
373
- href: `#/agent-runs/${runId}`,
394
+ href: dashboardRouteToHref({ tab: "agent-runs", agentRunId: runId }),
374
395
  entityKind: "agent_run",
375
396
  entityId: runId,
376
397
  });
@@ -413,7 +434,7 @@ export const linksFromActions = (
413
434
  links: [
414
435
  {
415
436
  label: `${actions.length} actions`,
416
- href: `#/actions`,
437
+ href: dashboardRouteToHref({ tab: "actions" }),
417
438
  count: actions.length,
418
439
  sample: truncate(
419
440
  `${actions[0].actionKind} · ${actions[0].name ?? "unnamed"}`,
@@ -429,7 +450,7 @@ export const linksFromActions = (
429
450
  label: prefix,
430
451
  links: actions.map((a) => ({
431
452
  label: `[${a.actionKind}] ${truncate(a.name ?? "unnamed", 40)}`,
432
- href: `#/actions/${a.id}`,
453
+ href: dashboardRouteToHref({ tab: "actions", actionId: a.id }),
433
454
  entityKind: "action",
434
455
  entityId: a.id,
435
456
  ...actionContextMetadata(a, metadata),
@@ -457,7 +478,7 @@ export const linksFromToolCalls = (
457
478
  links: [
458
479
  {
459
480
  label: `${calls.length} tool calls`,
460
- href: `#/tool-calls`,
481
+ href: dashboardRouteToHref({ tab: "tool-calls" }),
461
482
  count: calls.length,
462
483
  sample: truncate(calls[0].toolName),
463
484
  entityKind: "tool_call",
@@ -471,7 +492,7 @@ export const linksFromToolCalls = (
471
492
  label: prefix,
472
493
  links: calls.map((t) => ({
473
494
  label: `tool: ${t.toolName}`,
474
- href: `#/tool-calls/${t.id}`,
495
+ href: dashboardRouteToHref({ tab: "tool-calls", toolCallId: t.id }),
475
496
  entityKind: "tool_call",
476
497
  entityId: t.id,
477
498
  ...metadataForAction?.(t.actionId),
@@ -495,7 +516,7 @@ export const linksFromRetrievalEvents = (
495
516
  links: events.map((r) => ({
496
517
  label: `retrieve: ${r.retrieverName} (${r.totalResults} docs)`,
497
518
  // No dedicated retrieval tab: the owning action's detail page lists it.
498
- href: `#/actions/${encodeURIComponent(r.actionId)}`,
519
+ href: dashboardRouteToHref({ tab: "actions", actionId: r.actionId }),
499
520
  })),
500
521
  };
501
522
  };
@@ -520,7 +541,7 @@ export const linksFromEvalResults = (
520
541
  label: `eval: ${e.evaluatorName} (${e.passed ? "passed" : "failed"})`,
521
542
  // The Evaluations tab resolves this id to an eval case, falling back
522
543
  // to the case saved from the same action.
523
- href: `#/evaluations/${encodeURIComponent(e.id)}`,
544
+ href: dashboardRouteToHref({ tab: "evaluations", evaluationId: e.id }),
524
545
  entityKind: "eval",
525
546
  entityId: e.id,
526
547
  ...metadataForAction?.(e.actionId),
@@ -544,59 +565,21 @@ export const linksFromArtifacts = (
544
565
  links: arts.map((a) => ({
545
566
  label: `artifact: ${a.artifactName} (${a.artifactType})`,
546
567
  // No dedicated artifact tab: the owning action's detail page lists it.
547
- href: `#/actions/${encodeURIComponent(a.actionId)}`,
568
+ href: dashboardRouteToHref({ tab: "actions", actionId: a.actionId }),
548
569
  })),
549
570
  };
550
571
  };
551
572
 
552
573
  /**
553
- * Dashboard destination for an evidence entity, using only routes the web
554
- * router (`apps/web/src/app/router.ts`) actually serves. Unknown kinds fall
555
- * back to the evidence explorer so a link never lands on the Health tab.
574
+ * Dashboard destination for an evidence entity, delegating to the shared
575
+ * `@obsunified/types` route contract. Unknown kinds fall back to the
576
+ * evidence explorer so a link never lands on the Health tab.
556
577
  */
557
- export const hrefForEntity = (kind: string, id: string): string => {
558
- const enc = encodeURIComponent;
559
- switch (kind) {
560
- case "trace":
561
- return `#/traces?trace=${enc(id)}`;
562
- case "span": {
563
- const [traceId, spanId] = id.split(":");
564
- return spanId && traceId
565
- ? `#/traces?trace=${enc(traceId)}&span=${enc(spanId)}`
566
- : `#/evidence?kind=span&id=${enc(id)}`;
567
- }
568
- case "alert":
569
- return `#/alerts?id=${enc(id)}`;
570
- case "analysis":
571
- return "#/health";
572
- case "service":
573
- return `#/service-map?service=${enc(id)}`;
574
- case "agent_run":
575
- return `#/agent-runs/${enc(id)}`;
576
- case "action":
577
- return `#/actions/${enc(id)}`;
578
- case "tool_call":
579
- return `#/tool-calls/${enc(id)}`;
580
- case "ai_call":
581
- return `#/ai/calls/${enc(id)}`;
582
- case "profile":
583
- return `#/profiles/${enc(id)}`;
584
- case "change":
585
- return `#/changes/${enc(id)}`;
586
- case "finding":
587
- return `#/findings/${enc(id)}`;
588
- case "incident":
589
- return `#/incidents/${enc(id)}`;
590
- case "eval":
591
- return `#/evaluations/${enc(id)}`;
592
- case "log":
593
- return `#/logs?id=${enc(id)}`;
594
- case "replay":
595
- return `#/replay?session=${enc(id)}`;
596
- default:
597
- return `#/evidence?kind=${enc(kind)}&id=${enc(id)}`;
598
- }
599
- };
578
+ export const hrefForEntity = (
579
+ kind: string,
580
+ id: string,
581
+ options?: { traceId?: string; spanId?: string },
582
+ ): string => sharedHrefForEntity(kind, id, options);
600
583
 
601
584
  export interface FaultLocusTarget {
602
585
  entityKind: "service" | "span";
@@ -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
  });
@@ -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,
@@ -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",
@@ -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,6 +37,7 @@ 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,
@@ -456,7 +457,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
456
457
  ? [
457
458
  {
458
459
  label: `${traceIds.length} sampled traces`,
459
- href: `#/traces?trace=${encodeURIComponent(traceIds[0])}`,
460
+ href: dashboardRouteToHref({
461
+ tab: "traces",
462
+ traceId: traceIds[0],
463
+ }),
460
464
  count: traceIds.length,
461
465
  sample: traceIds[0],
462
466
  },
@@ -556,7 +560,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
556
560
  .slice(0, MAX_LINKS_INLINE)
557
561
  .map((toolCall) => ({
558
562
  label: `tool: ${toolCall.tool_name}`,
559
- href: `#/tool-calls/${toolCall.id}`,
563
+ href: dashboardRouteToHref({
564
+ tab: "tool-calls",
565
+ toolCallId: toolCall.id,
566
+ }),
560
567
  entityKind: "tool_call",
561
568
  entityId: toolCall.id,
562
569
  ...traceLevelMetadata("profile tool call"),
@@ -986,7 +993,7 @@ export const connectedRoutesPlugin: CollectorPlugin = {
986
993
  links: [
987
994
  {
988
995
  label: `${run.agentName} (v${run.agentVersion})`,
989
- href: `#/agents/${run.agentId}`,
996
+ href: hrefForEntity("agent", run.agentId),
990
997
  },
991
998
  ],
992
999
  },
@@ -1070,7 +1077,7 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1070
1077
  links: [
1071
1078
  {
1072
1079
  label: `${changeRow.targetNodeId} (${changeRow.environment})`,
1073
- href: `#/health?node=${encodeURIComponent(changeRow.targetNodeId)}`,
1080
+ ...resolveFaultLocusTarget(changeRow.targetNodeId),
1074
1081
  relationshipCertainty: "exact",
1075
1082
  reason: "Target node mutated by this change event.",
1076
1083
  },
@@ -1099,7 +1106,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1099
1106
  label: "Attributed Findings",
1100
1107
  links: findings.results.map((f) => ({
1101
1108
  label: f.title,
1102
- href: `#/findings/${encodeURIComponent(f.id)}`,
1109
+ href: dashboardRouteToHref({
1110
+ tab: "findings",
1111
+ findingId: f.id,
1112
+ }),
1103
1113
  entityKind: "finding",
1104
1114
  entityId: f.id,
1105
1115
  relationshipCertainty: "exact",
@@ -1137,7 +1147,11 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1137
1147
  label: "Failing Spans on Cohort Version",
1138
1148
  links: spans.results.map((s) => ({
1139
1149
  label: `${s.service_name} · ${s.span_name}`,
1140
- 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
+ }),
1141
1155
  entityKind: "span",
1142
1156
  entityId: `${s.trace_id}:${s.span_id}`,
1143
1157
  relationshipCertainty: "exact",
@@ -1167,7 +1181,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1167
1181
  links: [
1168
1182
  {
1169
1183
  label: `incident ${findingRow.incidentId}`,
1170
- href: `#/incidents/${encodeURIComponent(findingRow.incidentId)}`,
1184
+ href: dashboardRouteToHref({
1185
+ tab: "incidents",
1186
+ incidentId: findingRow.incidentId,
1187
+ }),
1171
1188
  entityKind: "incident",
1172
1189
  entityId: findingRow.incidentId,
1173
1190
  relationshipCertainty: "exact",
@@ -1182,7 +1199,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1182
1199
  links: [
1183
1200
  {
1184
1201
  label: `change ${findingRow.contributingChangeId}`,
1185
- href: `#/changes/${encodeURIComponent(findingRow.contributingChangeId)}`,
1202
+ href: dashboardRouteToHref({
1203
+ tab: "changes",
1204
+ changeId: findingRow.contributingChangeId,
1205
+ }),
1186
1206
  entityKind: "change",
1187
1207
  entityId: findingRow.contributingChangeId,
1188
1208
  relationshipCertainty: "exact",
@@ -1206,7 +1226,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1206
1226
  ? [
1207
1227
  {
1208
1228
  label: `contending trace ${findingRow.contendingTraceId}`,
1209
- href: `#/traces/${encodeURIComponent(findingRow.contendingTraceId)}`,
1229
+ href: dashboardRouteToHref({
1230
+ tab: "traces",
1231
+ traceId: findingRow.contendingTraceId,
1232
+ }),
1210
1233
  entityKind: "trace" as const,
1211
1234
  entityId: findingRow.contendingTraceId,
1212
1235
  relationshipCertainty: "inferred" as const,
@@ -1280,7 +1303,10 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1280
1303
  label: "Findings",
1281
1304
  links: incFindings.results.map((f) => ({
1282
1305
  label: f.title,
1283
- href: `#/findings/${encodeURIComponent(f.id)}`,
1306
+ href: dashboardRouteToHref({
1307
+ tab: "findings",
1308
+ findingId: f.id,
1309
+ }),
1284
1310
  entityKind: "finding" as const,
1285
1311
  entityId: f.id,
1286
1312
  relationshipCertainty: "exact" as const,
@@ -1350,7 +1376,11 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1350
1376
  ];
1351
1377
  }
1352
1378
 
1353
- return c.json(manifest);
1379
+ return respondWithContract(
1380
+ c,
1381
+ "GET /internal/connected/:kind/:id",
1382
+ manifest,
1383
+ );
1354
1384
  });
1355
1385
  },
1356
1386
  };