@gmickel/gno 1.18.0 → 1.20.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 (153) hide show
  1. package/README.md +14 -7
  2. package/assets/skill/SKILL.md +54 -12
  3. package/assets/skill/mcp-reference.md +7 -2
  4. package/assets/skill/recipes/citation-and-provenance.md +32 -9
  5. package/package.json +2 -1
  6. package/spec/AGENTS.md +83 -0
  7. package/spec/CLAUDE.md +83 -0
  8. package/spec/bench-fixture.schema.json +137 -0
  9. package/spec/cli.md +2919 -0
  10. package/spec/db/schema.sql +442 -0
  11. package/spec/evals-agentic.md +592 -0
  12. package/spec/evals.md +1106 -0
  13. package/spec/mcp.md +2279 -0
  14. package/spec/output-schemas/activation-verification.schema.json +515 -0
  15. package/spec/output-schemas/ask.schema.json +564 -0
  16. package/spec/output-schemas/backlinks.schema.json +131 -0
  17. package/spec/output-schemas/bench-result.schema.json +120 -0
  18. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  19. package/spec/output-schemas/claim-verification.schema.json +291 -0
  20. package/spec/output-schemas/collection-list.schema.json +45 -0
  21. package/spec/output-schemas/context-capsule-v1.schema.json +726 -0
  22. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  23. package/spec/output-schemas/context-list.schema.json +21 -0
  24. package/spec/output-schemas/doctor.schema.json +313 -0
  25. package/spec/output-schemas/error.schema.json +30 -0
  26. package/spec/output-schemas/expansion.schema.json +37 -0
  27. package/spec/output-schemas/get.schema.json +140 -0
  28. package/spec/output-schemas/graph-query.schema.json +99 -0
  29. package/spec/output-schemas/graph.schema.json +371 -0
  30. package/spec/output-schemas/links-list.schema.json +186 -0
  31. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  32. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  33. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  34. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  35. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  36. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  37. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  38. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  39. package/spec/output-schemas/models-list.schema.json +93 -0
  40. package/spec/output-schemas/multi-get.schema.json +103 -0
  41. package/spec/output-schemas/process-status.schema.json +119 -0
  42. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  43. package/spec/output-schemas/resident-status.schema.json +154 -0
  44. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  45. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  46. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  47. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  48. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  49. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  50. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  51. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  52. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  53. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  54. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  55. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  56. package/spec/output-schemas/search-result.schema.json +154 -0
  57. package/spec/output-schemas/search-results.schema.json +338 -0
  58. package/spec/output-schemas/similar.schema.json +84 -0
  59. package/spec/output-schemas/status.schema.json +676 -0
  60. package/spec/output-schemas/tags-list.schema.json +48 -0
  61. package/src/app/context-runtime-contract.ts +10 -5
  62. package/src/app/context-runtime-input.ts +29 -1
  63. package/src/app/context-runtime-types.ts +7 -0
  64. package/src/app/context-runtime.ts +20 -2
  65. package/src/app/context-surface.ts +4 -0
  66. package/src/app/verified-ask.ts +291 -0
  67. package/src/cli/commands/ask-format.ts +255 -0
  68. package/src/cli/commands/ask.ts +144 -183
  69. package/src/cli/commands/context-build.ts +56 -9
  70. package/src/cli/commands/get.ts +64 -3
  71. package/src/cli/commands/query.ts +62 -23
  72. package/src/cli/commands/replay.ts +140 -0
  73. package/src/cli/commands/search.ts +48 -3
  74. package/src/cli/commands/shared.ts +3 -1
  75. package/src/cli/commands/trace.ts +200 -0
  76. package/src/cli/commands/vsearch.ts +75 -53
  77. package/src/cli/program.ts +287 -1
  78. package/src/config/index.ts +9 -0
  79. package/src/config/retrieval-traces.ts +56 -0
  80. package/src/config/types.ts +4 -0
  81. package/src/core/context-budget.ts +6 -0
  82. package/src/core/context-capsule-retrieval-schema.ts +4 -0
  83. package/src/core/context-capsule-schema.ts +17 -0
  84. package/src/core/context-capsule-validation.ts +3 -2
  85. package/src/core/context-capsule.ts +18 -0
  86. package/src/core/context-compiler.ts +44 -25
  87. package/src/core/context-evidence.ts +6 -0
  88. package/src/core/retrieval-qrels.ts +405 -0
  89. package/src/core/retrieval-replay-candidate.ts +368 -0
  90. package/src/core/retrieval-replay-types.ts +109 -0
  91. package/src/core/retrieval-replay-validation.ts +89 -0
  92. package/src/core/retrieval-replay.ts +441 -0
  93. package/src/core/retrieval-trace-evidence-origin.ts +178 -0
  94. package/src/core/retrieval-trace-export.ts +113 -0
  95. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  96. package/src/core/retrieval-trace-filters.ts +19 -0
  97. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  98. package/src/core/retrieval-trace-management-types.ts +132 -0
  99. package/src/core/retrieval-trace-management.ts +422 -0
  100. package/src/core/retrieval-trace-request.ts +141 -0
  101. package/src/core/retrieval-trace-session.ts +507 -0
  102. package/src/core/retrieval-trace.ts +472 -0
  103. package/src/llm/errors.ts +10 -1
  104. package/src/llm/httpGeneration.ts +11 -1
  105. package/src/llm/nodeLlamaCpp/generation.ts +54 -10
  106. package/src/llm/types.ts +6 -0
  107. package/src/mcp/tools/ask.ts +228 -0
  108. package/src/mcp/tools/context.ts +87 -15
  109. package/src/mcp/tools/get.ts +35 -1
  110. package/src/mcp/tools/index.ts +83 -0
  111. package/src/mcp/tools/query.ts +95 -64
  112. package/src/mcp/tools/search.ts +36 -13
  113. package/src/mcp/tools/trace.ts +143 -0
  114. package/src/mcp/tools/vsearch.ts +71 -38
  115. package/src/pipeline/answer.ts +167 -26
  116. package/src/pipeline/claim-verification-schema.ts +235 -0
  117. package/src/pipeline/claim-verification.ts +487 -0
  118. package/src/pipeline/claim-verifier.ts +474 -0
  119. package/src/pipeline/graph-retrieval.ts +15 -1
  120. package/src/pipeline/hybrid.ts +151 -43
  121. package/src/pipeline/search.ts +36 -3
  122. package/src/pipeline/trace-metadata.ts +47 -0
  123. package/src/pipeline/types.ts +68 -0
  124. package/src/pipeline/vsearch.ts +101 -38
  125. package/src/sdk/client.ts +415 -73
  126. package/src/sdk/documents.ts +48 -1
  127. package/src/sdk/index.ts +17 -0
  128. package/src/sdk/types.ts +28 -0
  129. package/src/serve/context-capsule.ts +67 -8
  130. package/src/serve/public/app.tsx +12 -1
  131. package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
  132. package/src/serve/public/globals.built.css +1 -1
  133. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  134. package/src/serve/public/pages/Ask.tsx +42 -4
  135. package/src/serve/public/pages/Dashboard.tsx +10 -0
  136. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  137. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  138. package/src/serve/retrieval-trace.ts +28 -0
  139. package/src/serve/routes/api.ts +508 -68
  140. package/src/serve/routes/traces.ts +156 -0
  141. package/src/serve/server.ts +87 -2
  142. package/src/store/index.ts +31 -0
  143. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  144. package/src/store/migrations/index.ts +2 -0
  145. package/src/store/retrieval-trace-codec.ts +384 -0
  146. package/src/store/sqlite/adapter.ts +153 -1
  147. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  148. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  149. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  150. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  151. package/src/store/types.ts +297 -0
  152. package/src/store/vector/sqlite-vec.ts +76 -1
  153. package/src/store/vector/types.ts +1 -1
@@ -29,6 +29,14 @@ const positiveIntegerSchema = z.number().int().positive();
29
29
  const nonNegativeIntegerSchema = z.number().int().nonnegative();
30
30
  const COLLECTION_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
31
31
  const collectionSchema = nonEmptyTextSchema.max(64).regex(COLLECTION_PATTERN);
32
+ const retrievalSourceSchema = z.enum([
33
+ "bm25",
34
+ "vector",
35
+ "bm25_variant",
36
+ "vector_variant",
37
+ "hyde",
38
+ "graph",
39
+ ]);
32
40
  const compareCodeUnits = (left: string, right: string): number =>
33
41
  left < right ? -1 : left > right ? 1 : 0;
34
42
 
@@ -244,6 +252,15 @@ export const contextCapsuleEvidenceSchema = z
244
252
  contextIds: z.array(sha256Schema).max(128),
245
253
  retrievalRank: positiveIntegerSchema,
246
254
  selectionRank: positiveIntegerSchema,
255
+ retrievalSources: z
256
+ .array(retrievalSourceSchema)
257
+ .min(1)
258
+ .max(6)
259
+ .refine((sources) => new Set(sources).size === sources.length, {
260
+ message: "retrievalSources must be unique",
261
+ })
262
+ .optional(),
263
+ graphExpanded: z.boolean().optional(),
247
264
  facets: z.array(nonEmptyTextSchema.max(512)).max(128),
248
265
  trust: z.literal("untrusted"),
249
266
  egress: z.enum([
@@ -352,11 +352,12 @@ export const validateContextCapsulePayload = (
352
352
  });
353
353
  }
354
354
  const semanticRequested = value.retrieval.depthPolicy !== "fast";
355
+ const rerankRequested =
356
+ value.retrieval.request.rerankRequested ?? semanticRequested;
355
357
  if (
356
358
  value.retrieval.capabilityStates.semanticSearch.requested !==
357
359
  semanticRequested ||
358
- value.retrieval.capabilityStates.reranking.requested !==
359
- semanticRequested ||
360
+ value.retrieval.capabilityStates.reranking.requested !== rerankRequested ||
360
361
  value.retrieval.capabilityStates.graphExpansion.requested !==
361
362
  value.retrieval.request.graphRequested
362
363
  ) {
@@ -84,6 +84,17 @@ const normalizePayload = (
84
84
  value.retrieval.request.lang === null
85
85
  ? null
86
86
  : normalizeText(value.retrieval.request.lang),
87
+ ...(value.retrieval.request.intent === undefined
88
+ ? {}
89
+ : {
90
+ intent:
91
+ value.retrieval.request.intent === null
92
+ ? null
93
+ : normalizeText(value.retrieval.request.intent),
94
+ }),
95
+ ...(value.retrieval.request.exclude === undefined
96
+ ? {}
97
+ : { exclude: normalizeSet(value.retrieval.request.exclude) }),
87
98
  queryModes: value.retrieval.request.queryModes.map((mode) => ({
88
99
  ...mode,
89
100
  text: normalizeText(mode.text),
@@ -110,6 +121,13 @@ const normalizePayload = (
110
121
  documentDate: normalizeDocumentDate(item.documentDate),
111
122
  observedAt: normalizeDate(item.observedAt),
112
123
  contextIds: normalizeSet(item.contextIds),
124
+ ...(item.retrievalSources === undefined
125
+ ? {}
126
+ : {
127
+ retrievalSources: [...new Set(item.retrievalSources)].sort(
128
+ compareCodeUnits
129
+ ),
130
+ }),
113
131
  facets: normalizeSet(item.facets),
114
132
  })),
115
133
  guidance: {
@@ -29,6 +29,7 @@ import type { ContextConfiguredGuidance } from "./context-guidance";
29
29
  import { decorateUriForIndex } from "../app/constants";
30
30
  import { canonicalizeIndexName } from "../app/index-name";
31
31
  import { resolveTemporalRange } from "../pipeline/temporal";
32
+ import { attachSearchResultPlannerMetadata } from "../pipeline/trace-metadata";
32
33
  import {
33
34
  SEARCH_RESULT_PLANNER_METADATA,
34
35
  type SearchResultPlannerMetadata,
@@ -63,8 +64,8 @@ export interface ContextRetrievalRequest extends HybridSearchOptions {
63
64
  export interface ContextRetrievalCandidate {
64
65
  result: SearchResult;
65
66
  retrievalRank: number;
66
- retrievalSources: FusionSource[];
67
- graphExpanded: boolean;
67
+ retrievalSources?: FusionSource[];
68
+ graphExpanded?: boolean;
68
69
  contextIds: string[];
69
70
  observedAt: string | null;
70
71
  }
@@ -96,9 +97,13 @@ export interface ContextCompilerInput {
96
97
  categories?: string[];
97
98
  author?: string;
98
99
  lang?: string;
100
+ intent?: string;
101
+ exclude?: string[];
102
+ minScore?: number;
99
103
  since?: string;
100
104
  until?: string;
101
105
  graph?: boolean;
106
+ noRerank?: boolean;
102
107
  limit?: number;
103
108
  candidateLimit?: number;
104
109
  /** Frozen once by the caller; never defaulted from wall-clock time. */
@@ -163,16 +168,9 @@ const compareCodeUnits = (left: string, right: string): number => {
163
168
  };
164
169
 
165
170
  const plannerMeta = (
166
- result: SearchResult,
167
- fallbackRank: number
168
- ): SearchResultPlannerMetadata =>
169
- result[SEARCH_RESULT_PLANNER_METADATA] ?? {
170
- retrievalRank: fallbackRank,
171
- mirrorHash: result.conversion?.mirrorHash ?? "",
172
- seq: 0,
173
- sources: [],
174
- graphExpanded: false,
175
- };
171
+ result: SearchResult
172
+ ): SearchResultPlannerMetadata | undefined =>
173
+ result[SEARCH_RESULT_PLANNER_METADATA];
176
174
 
177
175
  const compareSearchResults = (
178
176
  left: SearchResult,
@@ -218,7 +216,9 @@ const referenceFromResult = (
218
216
  const normalizeMaterialized = <T>(
219
217
  draft: ContextMaterializedDraft<T>,
220
218
  facets: string[],
221
- retrievalRank: number
219
+ retrievalRank: number,
220
+ retrievalSources: FusionSource[] | undefined,
221
+ graphExpanded: boolean | undefined
222
222
  ): MaterializedContextCandidate<T> => {
223
223
  const text = draft.text;
224
224
  if (
@@ -249,6 +249,10 @@ const normalizeMaterialized = <T>(
249
249
  text,
250
250
  facets,
251
251
  retrievalRank,
252
+ ...(retrievalSources === undefined
253
+ ? {}
254
+ : { retrievalSources: [...retrievalSources].sort(compareCodeUnits) }),
255
+ ...(graphExpanded === undefined ? {} : { graphExpanded }),
252
256
  value: draft.value,
253
257
  };
254
258
  };
@@ -351,10 +355,13 @@ export const planContextEvidence = async <T, P>(
351
355
  categories: input.categories,
352
356
  author: input.author,
353
357
  lang: input.lang,
358
+ intent: input.intent,
359
+ exclude: input.exclude,
360
+ minScore: input.minScore,
354
361
  since: temporalRange.since,
355
362
  until: temporalRange.until,
356
363
  graph: hasRerankBudget ? input.graph : false,
357
- noRerank: hasRerankBudget ? undefined : true,
364
+ noRerank: input.noRerank || !hasRerankBudget ? true : undefined,
358
365
  limit: resultLimit === undefined ? undefined : Math.max(1, resultLimit),
359
366
  candidateLimit:
360
367
  rerankLimit === undefined ? undefined : Math.max(1, rerankLimit),
@@ -363,14 +370,20 @@ export const planContextEvidence = async <T, P>(
363
370
  }
364
371
  const decoratedResults = responses
365
372
  .flatMap((response) => response.results)
366
- .map((result) => ({
367
- ...result,
368
- uri: decorateUriForIndex(result.uri, indexName),
369
- }));
373
+ .map((result) => {
374
+ const decorated = {
375
+ ...result,
376
+ uri: decorateUriForIndex(result.uri, indexName),
377
+ };
378
+ const metadata = result[SEARCH_RESULT_PLANNER_METADATA];
379
+ return metadata
380
+ ? attachSearchResultPlannerMetadata(decorated, metadata)
381
+ : decorated;
382
+ });
370
383
  const results = decoratedResults
371
384
  .map((result, index) => ({
372
385
  result,
373
- retrievalRank: plannerMeta(result, index + 1).retrievalRank,
386
+ retrievalRank: plannerMeta(result)?.retrievalRank ?? index + 1,
374
387
  }))
375
388
  .sort(
376
389
  (left, right) =>
@@ -400,8 +413,8 @@ export const planContextEvidence = async <T, P>(
400
413
  const referencesByCandidate: ContextCandidateReference[] = [];
401
414
 
402
415
  for (const [index, result] of results.entries()) {
403
- const meta = plannerMeta(result, index + 1);
404
- for (const source of meta.sources) retrievalSources.add(source);
416
+ const meta = plannerMeta(result);
417
+ for (const source of meta?.sources ?? []) retrievalSources.add(source);
405
418
  const retrievalReference = referenceFromResult(result);
406
419
  if (!isContextUriInScope(result.uri, indexName, collections, uriPrefix)) {
407
420
  for (const facet of facetPlan) {
@@ -424,9 +437,13 @@ export const planContextEvidence = async <T, P>(
424
437
  }
425
438
  plannedCandidates.push({
426
439
  result,
427
- retrievalRank: meta.retrievalRank,
428
- retrievalSources: [...meta.sources].sort(compareCodeUnits),
429
- graphExpanded: meta.graphExpanded,
440
+ retrievalRank: meta?.retrievalRank ?? index + 1,
441
+ ...(meta === undefined
442
+ ? {}
443
+ : {
444
+ retrievalSources: [...meta.sources].sort(compareCodeUnits),
445
+ graphExpanded: meta.graphExpanded,
446
+ }),
430
447
  contextIds:
431
448
  guidance.idsByResultIdentity.get(
432
449
  contextGuidanceResultIdentity(result)
@@ -479,7 +496,9 @@ export const planContextEvidence = async <T, P>(
479
496
  normalizeMaterialized(
480
497
  outcome.candidate,
481
498
  matchedFacets,
482
- plannedCandidate.retrievalRank
499
+ plannedCandidate.retrievalRank,
500
+ plannedCandidate.retrievalSources,
501
+ plannedCandidate.graphExpanded
483
502
  )
484
503
  );
485
504
  }
@@ -490,6 +490,12 @@ export const toContextCapsuleEvidence = (
490
490
  text: candidate.text,
491
491
  retrievalRank: candidate.retrievalRank,
492
492
  selectionRank,
493
+ ...(candidate.retrievalSources === undefined
494
+ ? {}
495
+ : { retrievalSources: [...candidate.retrievalSources] }),
496
+ ...(candidate.graphExpanded === undefined
497
+ ? {}
498
+ : { graphExpanded: candidate.graphExpanded }),
493
499
  facets: [...candidate.facets],
494
500
  };
495
501
  };
@@ -0,0 +1,405 @@
1
+ /** Canonical qrels artifacts derived only from persisted retrieval receipts. */
2
+
3
+ import type {
4
+ RetrievalTraceBundle,
5
+ RetrievalTraceFingerprints,
6
+ RetrievalTraceJudgmentLabel,
7
+ RetrievalTraceTerminalStatus,
8
+ StoreResult,
9
+ } from "../store/types";
10
+ import type { EvidenceTarget } from "./retrieval-trace-management-helpers";
11
+
12
+ import { hashTraceCanonical } from "../store/retrieval-trace-codec";
13
+ import { err, ok } from "../store/types";
14
+ import { parseRetrievalTraceFilters } from "./retrieval-trace-filters";
15
+ import { stableTarget, targetKey } from "./retrieval-trace-management-helpers";
16
+
17
+ export interface RetrievalQrelsEvidence {
18
+ docid: string;
19
+ sourceHash: string;
20
+ mirrorHash: string;
21
+ uri: string;
22
+ seq: number | null;
23
+ startLine: number;
24
+ endLine: number;
25
+ passageHash: string;
26
+ rank: number | null;
27
+ plannerRank: number | null;
28
+ score: number | null;
29
+ sources: string[];
30
+ graphExpanded: boolean;
31
+ }
32
+
33
+ export interface RetrievalQrel {
34
+ qrelId: string;
35
+ judgmentId: string;
36
+ label: RetrievalTraceJudgmentLabel;
37
+ relevance: 0 | 1;
38
+ baselineMissing: boolean;
39
+ target: EvidenceTarget;
40
+ evidence: RetrievalQrelsEvidence | null;
41
+ }
42
+
43
+ export interface RetrievalQrelsCase {
44
+ caseId: string;
45
+ traceId: string;
46
+ retrievalRunId: string;
47
+ terminalStatus: RetrievalTraceTerminalStatus;
48
+ query: {
49
+ text: string;
50
+ digest: string;
51
+ goalText: string | null;
52
+ goalDigest: string | null;
53
+ filters: Record<string, unknown>;
54
+ };
55
+ fingerprints: RetrievalTraceFingerprints;
56
+ baseline: {
57
+ ranked: RetrievalQrelsEvidence[];
58
+ capabilities: string[];
59
+ capabilityOutcomes: Array<{
60
+ capability: string;
61
+ status: "attempted" | "used" | "unavailable" | "failed";
62
+ reasonCode: string | null;
63
+ }>;
64
+ fallbackCodes: string[];
65
+ outcomes: {
66
+ opened: RetrievalQrelsEvidence[];
67
+ cited: RetrievalQrelsEvidence[];
68
+ pinned: RetrievalQrelsEvidence[];
69
+ };
70
+ };
71
+ judgments: {
72
+ history: Array<{
73
+ judgmentId: string;
74
+ label: RetrievalTraceJudgmentLabel;
75
+ targetKind: string;
76
+ target: EvidenceTarget;
77
+ createdAtMs: number;
78
+ canonicalDigest: string;
79
+ }>;
80
+ effective: string[];
81
+ };
82
+ qrels: RetrievalQrel[];
83
+ }
84
+
85
+ export interface RetrievalTraceQrelsArtifact {
86
+ schemaVersion: "1.0";
87
+ format: "qrels";
88
+ cases: RetrievalQrelsCase[];
89
+ }
90
+
91
+ const exactEvidence = (value: unknown): RetrievalQrelsEvidence | null => {
92
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
93
+ const item = value as Record<string, unknown>;
94
+ if (
95
+ typeof item.docid !== "string" ||
96
+ typeof item.sourceHash !== "string" ||
97
+ typeof item.mirrorHash !== "string" ||
98
+ typeof item.uri !== "string" ||
99
+ !item.uri.startsWith("gno://") ||
100
+ typeof item.startLine !== "number" ||
101
+ typeof item.endLine !== "number" ||
102
+ typeof item.passageHash !== "string"
103
+ ) {
104
+ return null;
105
+ }
106
+ return {
107
+ docid: item.docid,
108
+ sourceHash: item.sourceHash,
109
+ mirrorHash: item.mirrorHash,
110
+ uri: item.uri,
111
+ seq: typeof item.seq === "number" ? item.seq : null,
112
+ startLine: item.startLine,
113
+ endLine: item.endLine,
114
+ passageHash: item.passageHash,
115
+ rank: typeof item.rank === "number" ? item.rank : null,
116
+ plannerRank: typeof item.plannerRank === "number" ? item.plannerRank : null,
117
+ score: typeof item.score === "number" ? item.score : null,
118
+ sources: Array.isArray(item.sources)
119
+ ? item.sources
120
+ .filter((entry): entry is string => typeof entry === "string")
121
+ .sort()
122
+ : [],
123
+ graphExpanded: item.graphExpanded === true,
124
+ };
125
+ };
126
+
127
+ const evidenceArray = (
128
+ payload: Record<string, unknown>,
129
+ key: "ranked" | "evidence"
130
+ ): RetrievalQrelsEvidence[] =>
131
+ Array.isArray(payload[key])
132
+ ? payload[key].flatMap((value) => {
133
+ const parsed = exactEvidence(value);
134
+ return parsed ? [parsed] : [];
135
+ })
136
+ : [];
137
+
138
+ const sameDocument = (
139
+ target: EvidenceTarget,
140
+ evidence: RetrievalQrelsEvidence
141
+ ): boolean =>
142
+ (target.sourceHash !== undefined &&
143
+ target.sourceHash === evidence.sourceHash) ||
144
+ (target.docid !== undefined && target.docid === evidence.docid) ||
145
+ (target.uri !== undefined && target.uri === evidence.uri);
146
+
147
+ const evidenceMatches = (
148
+ target: EvidenceTarget,
149
+ evidence: RetrievalQrelsEvidence
150
+ ): boolean =>
151
+ sameDocument(target, evidence) &&
152
+ (target.seq === undefined || target.seq === evidence.seq) &&
153
+ (target.startLine === undefined || target.startLine === evidence.startLine) &&
154
+ (target.endLine === undefined || target.endLine === evidence.endLine) &&
155
+ (target.passageHash === undefined ||
156
+ target.passageHash === evidence.passageHash);
157
+
158
+ const effectiveJudgments = (
159
+ judgments: RetrievalTraceBundle["judgments"]
160
+ ): RetrievalTraceBundle["judgments"] => {
161
+ const latest = new Map<string, RetrievalTraceBundle["judgments"][number]>();
162
+ for (const judgment of judgments) {
163
+ const target = stableTarget(judgment.target);
164
+ if (!target) continue;
165
+ if (judgment.targetKind === "query") continue;
166
+ const key = targetKey(judgment.targetKind, target);
167
+ const previous = latest.get(key);
168
+ if (
169
+ !previous ||
170
+ judgment.createdAtMs > previous.createdAtMs ||
171
+ (judgment.createdAtMs === previous.createdAtMs &&
172
+ judgment.judgmentId > previous.judgmentId)
173
+ ) {
174
+ latest.set(key, judgment);
175
+ }
176
+ }
177
+ return [...latest.values()].sort((left, right) =>
178
+ left.judgmentId.localeCompare(right.judgmentId)
179
+ );
180
+ };
181
+
182
+ const capabilityOutcomes = (
183
+ trace: RetrievalTraceBundle,
184
+ runId: string
185
+ ): RetrievalQrelsCase["baseline"]["capabilityOutcomes"] =>
186
+ trace.events
187
+ .filter((event) => event.kind === "capability" && event.runId === runId)
188
+ .flatMap((event) => {
189
+ const { capability, status, reasonCode } = event.payload;
190
+ if (
191
+ typeof capability !== "string" ||
192
+ !["attempted", "used", "unavailable", "failed"].includes(String(status))
193
+ ) {
194
+ return [];
195
+ }
196
+ return [
197
+ {
198
+ capability,
199
+ status: status as "attempted" | "used" | "unavailable" | "failed",
200
+ reasonCode: typeof reasonCode === "string" ? reasonCode : null,
201
+ },
202
+ ];
203
+ })
204
+ .sort((left, right) =>
205
+ `${left.capability}\0${left.status}\0${left.reasonCode ?? ""}`.localeCompare(
206
+ `${right.capability}\0${right.status}\0${right.reasonCode ?? ""}`
207
+ )
208
+ );
209
+
210
+ const outcomeEvidence = (
211
+ trace: RetrievalTraceBundle,
212
+ kind: "open" | "cite" | "pin",
213
+ runId: string
214
+ ): RetrievalQrelsEvidence[] =>
215
+ trace.events
216
+ .filter((event) => event.kind === kind && event.runId === runId)
217
+ .flatMap((event) => evidenceArray(event.payload, "evidence"));
218
+
219
+ const assignedRun = (
220
+ judgment: RetrievalTraceBundle["judgments"][number],
221
+ retrievalRuns: RetrievalTraceBundle["runs"]
222
+ ): StoreResult<string> => {
223
+ if (retrievalRuns.some((run) => run.runId === judgment.runId)) {
224
+ return ok(judgment.runId!);
225
+ }
226
+ if (
227
+ judgment.runId === null &&
228
+ judgment.label === "missing_expected" &&
229
+ retrievalRuns.length === 1
230
+ ) {
231
+ return ok(retrievalRuns[0]!.runId);
232
+ }
233
+ return err(
234
+ "CONSTRAINT_VIOLATION",
235
+ "ambiguous_missing_expected_run: judgment cannot be assigned to one retrieval run"
236
+ );
237
+ };
238
+
239
+ const sortedStringSet = (value: unknown): string[] =>
240
+ Array.isArray(value)
241
+ ? [
242
+ ...new Set(
243
+ value.filter((item): item is string => typeof item === "string")
244
+ ),
245
+ ].sort()
246
+ : [];
247
+
248
+ export const buildRetrievalQrelsArtifact = (
249
+ bundles: RetrievalTraceBundle[]
250
+ ): StoreResult<RetrievalTraceQrelsArtifact> => {
251
+ const cases: RetrievalQrelsCase[] = [];
252
+ for (const trace of [...bundles].sort((a, b) =>
253
+ a.trace.traceId.localeCompare(b.trace.traceId)
254
+ )) {
255
+ const header = trace.trace;
256
+ if (
257
+ header.status === "open" ||
258
+ header.redactionMode !== "replay" ||
259
+ !header.replayCapable
260
+ ) {
261
+ return err(
262
+ "CONSTRAINT_VIOLATION",
263
+ `redaction_incompatible: ${header.traceId} is not a terminal replay receipt`
264
+ );
265
+ }
266
+ if (!header.queryText || !header.queryDigest) {
267
+ return err(
268
+ "CONSTRAINT_VIOLATION",
269
+ `query_missing: ${header.traceId} lacks replay query text or digest`
270
+ );
271
+ }
272
+ const filters = parseRetrievalTraceFilters(header.filters);
273
+ if (!filters.ok) return filters;
274
+ const retrievalRuns = trace.runs.filter((run) => run.kind === "retrieval");
275
+ if (retrievalRuns.length === 0) {
276
+ return err("CONSTRAINT_VIOLATION", `no_retrieval_run: ${header.traceId}`);
277
+ }
278
+ const rankedByRun = new Map(
279
+ retrievalRuns.map((run) => [
280
+ run.runId,
281
+ evidenceArray(run.payload, "ranked"),
282
+ ])
283
+ );
284
+ const historyByRun = new Map<string, RetrievalTraceBundle["judgments"]>();
285
+ for (const judgment of trace.judgments) {
286
+ const assignment = assignedRun(judgment, retrievalRuns);
287
+ if (!assignment.ok) return assignment;
288
+ const current = historyByRun.get(assignment.value) ?? [];
289
+ current.push(judgment);
290
+ historyByRun.set(assignment.value, current);
291
+ }
292
+ const effectiveByRun = new Map<string, RetrievalTraceBundle["judgments"]>();
293
+ for (const run of retrievalRuns) {
294
+ effectiveByRun.set(
295
+ run.runId,
296
+ effectiveJudgments(historyByRun.get(run.runId) ?? [])
297
+ );
298
+ }
299
+ if (
300
+ ![...effectiveByRun.values()]
301
+ .flat()
302
+ .some(
303
+ ({ label }) => label === "relevant" || label === "missing_expected"
304
+ )
305
+ ) {
306
+ return err(
307
+ "CONSTRAINT_VIOLATION",
308
+ `qrels export requires relevant or missing_expected judgments: ${header.traceId}`
309
+ );
310
+ }
311
+ for (const run of retrievalRuns) {
312
+ const runJudgments = effectiveByRun.get(run.runId) ?? [];
313
+ if (runJudgments.length === 0) continue;
314
+ const runOutcomes = {
315
+ opened: outcomeEvidence(trace, "open", run.runId),
316
+ cited: outcomeEvidence(trace, "cite", run.runId),
317
+ pinned: outcomeEvidence(trace, "pin", run.runId),
318
+ };
319
+ const allExact = [
320
+ ...(rankedByRun.get(run.runId) ?? []),
321
+ ...runOutcomes.opened,
322
+ ...runOutcomes.cited,
323
+ ...runOutcomes.pinned,
324
+ ];
325
+ const qrels: RetrievalQrel[] = [];
326
+ for (const judgment of runJudgments) {
327
+ const target = stableTarget(judgment.target);
328
+ if (!target) {
329
+ return err("CONSTRAINT_VIOLATION", "Judgment target is incomplete");
330
+ }
331
+ const evidence =
332
+ judgment.label === "missing_expected"
333
+ ? null
334
+ : (allExact.find((item) => evidenceMatches(target, item)) ?? null);
335
+ if (judgment.label !== "missing_expected" && !evidence) {
336
+ return err(
337
+ "CONSTRAINT_VIOLATION",
338
+ `Judgment ${judgment.judgmentId} lacks exact evidence provenance`
339
+ );
340
+ }
341
+ qrels.push({
342
+ qrelId: `qrel-${hashTraceCanonical({
343
+ judgmentId: judgment.judgmentId,
344
+ label: judgment.label,
345
+ target,
346
+ }).slice(0, 40)}`,
347
+ judgmentId: judgment.judgmentId,
348
+ label: judgment.label,
349
+ relevance: judgment.label === "irrelevant" ? 0 : 1,
350
+ baselineMissing: judgment.label === "missing_expected",
351
+ target,
352
+ evidence,
353
+ });
354
+ }
355
+ cases.push({
356
+ caseId: `trace-case-${hashTraceCanonical({
357
+ traceId: header.traceId,
358
+ runId: run.runId,
359
+ }).slice(0, 40)}`,
360
+ traceId: header.traceId,
361
+ retrievalRunId: run.runId,
362
+ terminalStatus: header.status,
363
+ query: {
364
+ text: header.queryText,
365
+ digest: header.queryDigest,
366
+ goalText: header.goalText,
367
+ goalDigest: header.goalDigest,
368
+ filters: filters.value,
369
+ },
370
+ fingerprints: header.fingerprints,
371
+ baseline: {
372
+ ranked: rankedByRun.get(run.runId) ?? [],
373
+ capabilities: sortedStringSet(run.payload.capabilities),
374
+ capabilityOutcomes: capabilityOutcomes(trace, run.runId),
375
+ fallbackCodes: sortedStringSet(run.payload.fallbackCodes),
376
+ outcomes: runOutcomes,
377
+ },
378
+ judgments: {
379
+ history: (historyByRun.get(run.runId) ?? []).flatMap((judgment) => {
380
+ const target = stableTarget(judgment.target);
381
+ return target
382
+ ? [
383
+ {
384
+ judgmentId: judgment.judgmentId,
385
+ label: judgment.label,
386
+ targetKind: judgment.targetKind,
387
+ target,
388
+ createdAtMs: judgment.createdAtMs,
389
+ canonicalDigest: judgment.canonicalDigest,
390
+ },
391
+ ]
392
+ : [];
393
+ }),
394
+ effective: runJudgments.map((judgment) => judgment.judgmentId),
395
+ },
396
+ qrels: qrels.sort((a, b) => a.qrelId.localeCompare(b.qrelId)),
397
+ });
398
+ }
399
+ }
400
+ return ok({
401
+ schemaVersion: "1.0",
402
+ format: "qrels",
403
+ cases: cases.sort((a, b) => a.caseId.localeCompare(b.caseId)),
404
+ });
405
+ };