@gmickel/gno 1.17.0 → 1.19.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 (167) hide show
  1. package/README.md +18 -4
  2. package/assets/skill/SKILL.md +44 -1
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +3 -2
  5. package/spec/AGENTS.md +83 -0
  6. package/spec/CLAUDE.md +83 -0
  7. package/spec/bench-fixture.schema.json +137 -0
  8. package/spec/cli.md +2894 -0
  9. package/spec/db/schema.sql +442 -0
  10. package/spec/evals-agentic.md +510 -0
  11. package/spec/evals.md +1106 -0
  12. package/spec/mcp.md +2229 -0
  13. package/spec/output-schemas/activation-verification.schema.json +515 -0
  14. package/spec/output-schemas/ask.schema.json +366 -0
  15. package/spec/output-schemas/backlinks.schema.json +131 -0
  16. package/spec/output-schemas/bench-result.schema.json +120 -0
  17. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  18. package/spec/output-schemas/collection-list.schema.json +45 -0
  19. package/spec/output-schemas/context-capsule-v1.schema.json +691 -0
  20. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  21. package/spec/output-schemas/context-list.schema.json +21 -0
  22. package/spec/output-schemas/doctor.schema.json +313 -0
  23. package/spec/output-schemas/error.schema.json +30 -0
  24. package/spec/output-schemas/expansion.schema.json +37 -0
  25. package/spec/output-schemas/get.schema.json +140 -0
  26. package/spec/output-schemas/graph-query.schema.json +99 -0
  27. package/spec/output-schemas/graph.schema.json +371 -0
  28. package/spec/output-schemas/links-list.schema.json +186 -0
  29. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  30. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  31. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  32. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  33. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  34. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  35. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  36. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  37. package/spec/output-schemas/models-list.schema.json +93 -0
  38. package/spec/output-schemas/multi-get.schema.json +103 -0
  39. package/spec/output-schemas/process-status.schema.json +119 -0
  40. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  41. package/spec/output-schemas/resident-status.schema.json +154 -0
  42. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  43. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  44. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  45. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  46. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  47. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  48. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  49. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  50. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  51. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  52. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  53. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  54. package/spec/output-schemas/search-result.schema.json +154 -0
  55. package/spec/output-schemas/search-results.schema.json +338 -0
  56. package/spec/output-schemas/similar.schema.json +84 -0
  57. package/spec/output-schemas/status.schema.json +676 -0
  58. package/spec/output-schemas/tags-list.schema.json +48 -0
  59. package/src/app/context-runtime-types.ts +3 -0
  60. package/src/app/context-runtime.ts +15 -1
  61. package/src/cli/commands/ask.ts +106 -36
  62. package/src/cli/commands/context-build.ts +56 -9
  63. package/src/cli/commands/daemon.ts +69 -2
  64. package/src/cli/commands/get.ts +64 -3
  65. package/src/cli/commands/models/pull.ts +13 -3
  66. package/src/cli/commands/query.ts +62 -23
  67. package/src/cli/commands/replay.ts +140 -0
  68. package/src/cli/commands/search.ts +48 -3
  69. package/src/cli/commands/shared.ts +3 -1
  70. package/src/cli/commands/status.ts +2 -0
  71. package/src/cli/commands/trace.ts +200 -0
  72. package/src/cli/commands/vsearch.ts +75 -53
  73. package/src/cli/detach.ts +37 -20
  74. package/src/cli/program.ts +329 -27
  75. package/src/config/index.ts +12 -0
  76. package/src/config/retrieval-traces.ts +56 -0
  77. package/src/config/types.ts +41 -0
  78. package/src/core/context-compiler.ts +11 -4
  79. package/src/core/job-manager.ts +19 -0
  80. package/src/core/mutation-generations.ts +33 -0
  81. package/src/core/retrieval-qrels.ts +405 -0
  82. package/src/core/retrieval-replay-candidate.ts +368 -0
  83. package/src/core/retrieval-replay-types.ts +109 -0
  84. package/src/core/retrieval-replay-validation.ts +89 -0
  85. package/src/core/retrieval-replay.ts +441 -0
  86. package/src/core/retrieval-trace-evidence-origin.ts +175 -0
  87. package/src/core/retrieval-trace-export.ts +113 -0
  88. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  89. package/src/core/retrieval-trace-filters.ts +19 -0
  90. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  91. package/src/core/retrieval-trace-management-types.ts +132 -0
  92. package/src/core/retrieval-trace-management.ts +422 -0
  93. package/src/core/retrieval-trace-request.ts +141 -0
  94. package/src/core/retrieval-trace-session.ts +494 -0
  95. package/src/core/retrieval-trace.ts +472 -0
  96. package/src/llm/cache.ts +13 -3
  97. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  98. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  99. package/src/mcp/context.ts +161 -0
  100. package/src/mcp/http-security.ts +477 -0
  101. package/src/mcp/http-session.ts +272 -0
  102. package/src/mcp/http-transport.ts +370 -0
  103. package/src/mcp/resources/index.ts +141 -134
  104. package/src/mcp/server.ts +19 -79
  105. package/src/mcp/tools/add-collection.ts +3 -1
  106. package/src/mcp/tools/capture.ts +3 -0
  107. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  108. package/src/mcp/tools/context.ts +68 -16
  109. package/src/mcp/tools/embed.ts +62 -52
  110. package/src/mcp/tools/get.ts +35 -1
  111. package/src/mcp/tools/index-cmd.ts +88 -74
  112. package/src/mcp/tools/index.ts +96 -2
  113. package/src/mcp/tools/query.ts +95 -64
  114. package/src/mcp/tools/remove-collection.ts +2 -0
  115. package/src/mcp/tools/search.ts +36 -13
  116. package/src/mcp/tools/status.ts +11 -0
  117. package/src/mcp/tools/sync.ts +16 -14
  118. package/src/mcp/tools/trace.ts +143 -0
  119. package/src/mcp/tools/vsearch.ts +71 -38
  120. package/src/mcp/tools/workspace-write.ts +7 -3
  121. package/src/pipeline/answer.ts +167 -26
  122. package/src/pipeline/graph-retrieval.ts +15 -1
  123. package/src/pipeline/hybrid.ts +151 -43
  124. package/src/pipeline/search.ts +36 -3
  125. package/src/pipeline/trace-metadata.ts +47 -0
  126. package/src/pipeline/types.ts +43 -0
  127. package/src/pipeline/vsearch.ts +101 -38
  128. package/src/sdk/client.ts +380 -71
  129. package/src/sdk/documents.ts +48 -1
  130. package/src/sdk/index.ts +17 -0
  131. package/src/sdk/types.ts +28 -0
  132. package/src/serve/background-runtime.ts +12 -212
  133. package/src/serve/context-capsule.ts +67 -8
  134. package/src/serve/embed-scheduler.ts +74 -43
  135. package/src/serve/index.ts +9 -0
  136. package/src/serve/jobs.ts +78 -80
  137. package/src/serve/public/app.tsx +12 -1
  138. package/src/serve/public/components/HealthCenter.tsx +74 -1
  139. package/src/serve/public/globals.built.css +1 -1
  140. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  141. package/src/serve/public/pages/Dashboard.tsx +11 -0
  142. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  143. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  144. package/src/serve/resident-admission.ts +159 -0
  145. package/src/serve/resident-background-work.ts +39 -0
  146. package/src/serve/resident-request.ts +55 -0
  147. package/src/serve/resident-runtime.ts +490 -0
  148. package/src/serve/resident-status.ts +96 -0
  149. package/src/serve/retrieval-trace.ts +28 -0
  150. package/src/serve/routes/api.ts +629 -239
  151. package/src/serve/routes/mcp.ts +69 -0
  152. package/src/serve/routes/traces.ts +156 -0
  153. package/src/serve/server.ts +276 -37
  154. package/src/serve/status-model.ts +51 -0
  155. package/src/serve/status.ts +5 -0
  156. package/src/store/index.ts +31 -0
  157. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  158. package/src/store/migrations/index.ts +2 -0
  159. package/src/store/retrieval-trace-codec.ts +384 -0
  160. package/src/store/sqlite/adapter.ts +179 -10
  161. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  162. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  163. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  164. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  165. package/src/store/types.ts +297 -0
  166. package/src/store/vector/sqlite-vec.ts +76 -1
  167. package/src/store/vector/types.ts +1 -1
@@ -46,6 +46,7 @@ export interface JobRecord {
46
46
  result?: SyncResult;
47
47
  typedResult?: JobResult;
48
48
  error?: string;
49
+ progress?: { current: number; total: number; currentFile?: string };
49
50
  serverInstanceId: string;
50
51
  }
51
52
 
@@ -148,6 +149,24 @@ export class JobManager {
148
149
  return this.#jobs.get(jobId);
149
150
  }
150
151
 
152
+ getActiveJob(): JobRecord | null {
153
+ if (!this.#activeJobId) return null;
154
+ return this.#jobs.get(this.#activeJobId) ?? null;
155
+ }
156
+
157
+ updateJobProgress(
158
+ jobId: string,
159
+ progress: { current: number; total: number; currentFile?: string }
160
+ ): void {
161
+ const job = this.#jobs.get(jobId);
162
+ if (job) job.progress = progress;
163
+ }
164
+
165
+ clear(): void {
166
+ this.#jobs.clear();
167
+ this.#activeJobId = null;
168
+ }
169
+
151
170
  listJobs(limit: number = 10): { active: JobRecord[]; recent: JobRecord[] } {
152
171
  this.#cleanupExpiredJobs();
153
172
 
@@ -0,0 +1,33 @@
1
+ /** Shared mutation detection for resident content and vector generations. */
2
+
3
+ interface SyncMutationCounts {
4
+ filesAdded?: number;
5
+ filesUpdated?: number;
6
+ filesMarkedInactive?: number;
7
+ totalFilesAdded?: number;
8
+ totalFilesUpdated?: number;
9
+ collections?: readonly SyncMutationCounts[];
10
+ }
11
+
12
+ export function hasContentMutation(result: SyncMutationCounts): boolean {
13
+ return (
14
+ (result.filesAdded ?? result.totalFilesAdded ?? 0) > 0 ||
15
+ (result.filesUpdated ?? result.totalFilesUpdated ?? 0) > 0 ||
16
+ (result.filesMarkedInactive ?? 0) > 0 ||
17
+ result.collections?.some(hasContentMutation) === true
18
+ );
19
+ }
20
+
21
+ export function recordContentMutation(
22
+ result: SyncMutationCounts,
23
+ markMutation: (() => void) | undefined
24
+ ): void {
25
+ if (hasContentMutation(result)) markMutation?.();
26
+ }
27
+
28
+ export function recordIndexMutation(
29
+ embedded: number,
30
+ markMutation: (() => void) | undefined
31
+ ): void {
32
+ if (embedded > 0) markMutation?.();
33
+ }
@@ -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
+ };