@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
@@ -0,0 +1,507 @@
1
+ /** Shared query-to-evidence trace lifecycle used by every application surface. */
2
+
3
+ import type { RetrievalTraceConfig } from "../config/retrieval-traces";
4
+ import type { SearchResult, SearchResults } from "../pipeline/types";
5
+ import type {
6
+ RetrievalTraceEventKind,
7
+ RetrievalTraceFingerprints,
8
+ RetrievalTraceTerminalStatus,
9
+ StorePort,
10
+ StoreResult,
11
+ } from "../store/types";
12
+ import type { ContextCapsuleV1 } from "./context-capsule";
13
+
14
+ import {
15
+ SEARCH_RESULT_PLANNER_METADATA,
16
+ SEARCH_RESULTS_TRACE_METADATA,
17
+ } from "../pipeline/types";
18
+ import { err, ok } from "../store/types";
19
+ import {
20
+ MIN_RETRIEVAL_TRACE_RECORDS,
21
+ RetrievalTraceRecorder,
22
+ type RetrievalTraceWriteResult,
23
+ } from "./retrieval-trace";
24
+ import {
25
+ type RetrievalTraceEvidence,
26
+ RetrievalTraceEvidenceOrigins,
27
+ } from "./retrieval-trace-evidence-origin";
28
+
29
+ type EvidenceEventKind = Extract<
30
+ RetrievalTraceEventKind,
31
+ "get" | "open" | "cite" | "pin"
32
+ >;
33
+
34
+ export interface StartRetrievalTraceSessionInput {
35
+ store: StorePort;
36
+ config: RetrievalTraceConfig | undefined;
37
+ query: string;
38
+ goal?: string;
39
+ filters?: Record<string, unknown>;
40
+ /**
41
+ * Lazy by contract: disabled tracing must not perform fingerprint or index
42
+ * work merely to discover that no receipt will be written.
43
+ */
44
+ fingerprints: () =>
45
+ | RetrievalTraceFingerprints
46
+ | Promise<RetrievalTraceFingerprints>;
47
+ clock?: () => number;
48
+ idFactory?: () => string;
49
+ }
50
+
51
+ export interface RetrievalTraceSurfaceMetadata {
52
+ traceId: string;
53
+ }
54
+
55
+ export const RETRIEVAL_TRACE_METADATA = Symbol("gno.retrievalTraceMetadata");
56
+
57
+ export const attachRetrievalTraceMetadata = <T extends object>(
58
+ value: T,
59
+ session: RetrievalTraceSession | undefined
60
+ ): T => {
61
+ const metadata = session?.metadata();
62
+ if (!metadata) return value;
63
+ Object.defineProperty(value, RETRIEVAL_TRACE_METADATA, {
64
+ configurable: false,
65
+ enumerable: false,
66
+ value: metadata,
67
+ writable: false,
68
+ });
69
+ return value;
70
+ };
71
+
72
+ export const getRetrievalTraceMetadata = (
73
+ value: object
74
+ ): RetrievalTraceSurfaceMetadata | undefined =>
75
+ (value as { [RETRIEVAL_TRACE_METADATA]?: RetrievalTraceSurfaceMetadata })[
76
+ RETRIEVAL_TRACE_METADATA
77
+ ];
78
+
79
+ const evidenceFromResult = (
80
+ result: SearchResult,
81
+ fallbackRank: number
82
+ ): RetrievalTraceEvidence | null => {
83
+ const metadata = result[SEARCH_RESULT_PLANNER_METADATA];
84
+ const sourceHash = result.source.sourceHash;
85
+ const mirrorHash = metadata?.mirrorHash ?? result.conversion?.mirrorHash;
86
+ const startLine = metadata?.startLine ?? result.snippetRange?.startLine;
87
+ const endLine = metadata?.endLine ?? result.snippetRange?.endLine;
88
+ const passageHash = metadata?.passageHash;
89
+ if (
90
+ !sourceHash ||
91
+ !mirrorHash ||
92
+ !result.uri.startsWith("gno://") ||
93
+ startLine === undefined ||
94
+ endLine === undefined ||
95
+ !passageHash
96
+ ) {
97
+ return null;
98
+ }
99
+ return {
100
+ docid: result.docid,
101
+ sourceHash,
102
+ mirrorHash,
103
+ uri: result.uri,
104
+ startLine,
105
+ endLine,
106
+ passageHash,
107
+ rank: fallbackRank,
108
+ score: result.score,
109
+ ...(metadata?.seq === undefined ? {} : { seq: metadata.seq }),
110
+ ...(metadata?.retrievalRank === undefined
111
+ ? {}
112
+ : { plannerRank: metadata.retrievalRank }),
113
+ ...(metadata?.sources === undefined ? {} : { sources: metadata.sources }),
114
+ ...(metadata?.graphExpanded === undefined
115
+ ? {}
116
+ : { graphExpanded: metadata.graphExpanded }),
117
+ };
118
+ };
119
+
120
+ const evidenceFromCapsule = (
121
+ capsule: ContextCapsuleV1
122
+ ): RetrievalTraceEvidence[] =>
123
+ capsule.evidence.map((item) => ({
124
+ docid: item.docid,
125
+ sourceHash: item.sourceHash,
126
+ mirrorHash: item.mirrorHash,
127
+ uri: item.uri,
128
+ startLine: item.startLine,
129
+ endLine: item.endLine,
130
+ passageHash: item.passageHash,
131
+ ...(item.selectionRank === undefined ? {} : { rank: item.selectionRank }),
132
+ ...(item.retrievalRank === undefined
133
+ ? {}
134
+ : { plannerRank: item.retrievalRank }),
135
+ ...(item.retrievalSources === undefined
136
+ ? {}
137
+ : { sources: item.retrievalSources }),
138
+ ...(item.graphExpanded === undefined
139
+ ? {}
140
+ : { graphExpanded: item.graphExpanded }),
141
+ }));
142
+
143
+ export class RetrievalTraceSession {
144
+ readonly traceId: string;
145
+
146
+ private counter = 0;
147
+ private available = true;
148
+ private persistedRecords = 0;
149
+ private terminal = false;
150
+ private retrievalRunId: string | null = null;
151
+ private readonly evidenceOrigins = new RetrievalTraceEvidenceOrigins();
152
+
153
+ private constructor(
154
+ private readonly recorder: RetrievalTraceRecorder,
155
+ traceId: string,
156
+ private readonly clock: () => number,
157
+ private readonly operationId = traceId,
158
+ private readonly maxRecords = 100_000
159
+ ) {
160
+ this.traceId = traceId;
161
+ }
162
+
163
+ static async start(
164
+ input: StartRetrievalTraceSessionInput
165
+ ): Promise<StoreResult<RetrievalTraceSession | null>> {
166
+ if (input.config?.enabled !== true) return ok(null);
167
+ if (
168
+ input.config.retention.maxRecordsPerTrace < MIN_RETRIEVAL_TRACE_RECORDS
169
+ ) {
170
+ return ok(null);
171
+ }
172
+ const rawClock = input.clock ?? Date.now;
173
+ let lastTimestamp = -1;
174
+ const clock = (): number => {
175
+ const current = rawClock();
176
+ lastTimestamp = Math.max(current, lastTimestamp + 1);
177
+ return lastTimestamp;
178
+ };
179
+ const recorder = new RetrievalTraceRecorder(input.store, input.config, {
180
+ clock,
181
+ idFactory: input.idFactory,
182
+ });
183
+ const started = await recorder.start({
184
+ query: input.query,
185
+ goal: input.goal,
186
+ filters: input.filters,
187
+ fingerprints: await input.fingerprints(),
188
+ });
189
+ if (!started.ok) return started;
190
+ if (!started.value.recorded) return ok(null);
191
+ const session = new RetrievalTraceSession(
192
+ recorder,
193
+ started.value.traceId,
194
+ clock,
195
+ started.value.traceId,
196
+ input.config.retention.maxRecordsPerTrace
197
+ );
198
+ const queryEvent = await session.appendEvent("query", {});
199
+ if (queryEvent.ok) return ok(session);
200
+ await recorder.finalize(started.value.traceId, "failed");
201
+ return ok(null);
202
+ }
203
+
204
+ static async resume(input: {
205
+ store: StorePort;
206
+ config: RetrievalTraceConfig | undefined;
207
+ traceId: string;
208
+ clock?: () => number;
209
+ }): Promise<StoreResult<RetrievalTraceSession | null>> {
210
+ if (input.config?.enabled !== true) return ok(null);
211
+ const stored = await input.store.getRetrievalTrace(input.traceId);
212
+ if (!stored.ok) return stored;
213
+ if (!stored.value) {
214
+ return err("NOT_FOUND", `Retrieval trace ${input.traceId} was not found`);
215
+ }
216
+ if (stored.value.trace.status !== "open") {
217
+ return err(
218
+ "CONSTRAINT_VIOLATION",
219
+ `Retrieval trace ${input.traceId} is already ${stored.value.trace.status}`
220
+ );
221
+ }
222
+ const rawClock = input.clock ?? Date.now;
223
+ let lastTimestamp = Math.max(
224
+ stored.value.trace.updatedAtMs,
225
+ ...stored.value.runs.map((run) => run.createdAtMs),
226
+ ...stored.value.events.map((event) => event.createdAtMs)
227
+ );
228
+ const clock = (): number => {
229
+ lastTimestamp = Math.max(rawClock(), lastTimestamp + 1);
230
+ return lastTimestamp;
231
+ };
232
+ const session = new RetrievalTraceSession(
233
+ new RetrievalTraceRecorder(input.store, input.config, { clock }),
234
+ input.traceId,
235
+ clock,
236
+ crypto.randomUUID(),
237
+ input.config.retention.maxRecordsPerTrace
238
+ );
239
+ session.persistedRecords =
240
+ stored.value.runs.length +
241
+ stored.value.events.length +
242
+ stored.value.judgments.length +
243
+ stored.value.exports.length;
244
+ session.retrievalRunId =
245
+ stored.value.runs.filter((run) => run.kind === "retrieval").at(-1)
246
+ ?.runId ?? null;
247
+ session.evidenceOrigins.addStoredRuns(stored.value.runs);
248
+ return ok(session);
249
+ }
250
+
251
+ metadata(): RetrievalTraceSurfaceMetadata | undefined {
252
+ return this.available ? { traceId: this.traceId } : undefined;
253
+ }
254
+
255
+ async recordRetrieval(
256
+ result: SearchResults,
257
+ latencyMs?: number
258
+ ): Promise<StoreResult<"inserted" | "duplicate" | "disabled">> {
259
+ if (!this.hasCapacity(2)) return ok("disabled");
260
+ const ranked = result.results.flatMap((item, index) => {
261
+ const evidence = evidenceFromResult(item, index + 1);
262
+ return evidence ? [evidence] : [];
263
+ });
264
+ const traceMetadata = result[SEARCH_RESULTS_TRACE_METADATA];
265
+ const capabilityOutcomes =
266
+ traceMetadata?.capabilityOutcomes ??
267
+ (result.meta.mode === "vector"
268
+ ? [{ capability: "semantic_search", status: "used" as const }]
269
+ : [
270
+ { capability: "lexical_search", status: "used" as const },
271
+ ...(result.meta.vectorsUsed
272
+ ? [{ capability: "semantic_search", status: "used" as const }]
273
+ : []),
274
+ ]);
275
+ const capabilities = capabilityOutcomes
276
+ .filter((outcome) => outcome.status === "used")
277
+ .map((outcome) => outcome.capability);
278
+ const fallbackCodes =
279
+ traceMetadata?.fallbackCodes ??
280
+ result.meta.graphExpansion?.fallbackReasons ??
281
+ [];
282
+ const payload = {
283
+ ranked,
284
+ capabilities,
285
+ fallbackCodes,
286
+ ...(latencyMs === undefined ? {} : { latencyMs }),
287
+ };
288
+ const runId = this.nextId("retrieval-run");
289
+ const run = await this.recorder.appendRun({
290
+ runId,
291
+ traceId: this.traceId,
292
+ idempotencyKey: runId,
293
+ kind: "retrieval",
294
+ payload,
295
+ createdAtMs: this.clock(),
296
+ });
297
+ if (!run.ok) return this.softenWriteFailure();
298
+ this.persistedRecords += 1;
299
+ this.retrievalRunId = runId;
300
+ const retrievalEvent = await this.appendEvent("retrieval", payload, runId);
301
+ if (!retrievalEvent.ok) return retrievalEvent;
302
+ this.evidenceOrigins.add(runId, ranked);
303
+ for (const outcome of capabilityOutcomes) {
304
+ const recorded = await this.recordCapability(
305
+ outcome.capability,
306
+ outcome.status,
307
+ outcome.reasonCode,
308
+ runId
309
+ );
310
+ if (!recorded.ok) return recorded;
311
+ }
312
+ return retrievalEvent;
313
+ }
314
+
315
+ async recordContext(
316
+ capsule: ContextCapsuleV1,
317
+ latencyMs?: number
318
+ ): Promise<StoreResult<"inserted" | "duplicate" | "disabled">> {
319
+ if (!this.hasCapacity(2)) return ok("disabled");
320
+ const payload = {
321
+ evidence: evidenceFromCapsule(capsule),
322
+ capsuleId: capsule.capsuleId,
323
+ ...(latencyMs === undefined ? {} : { latencyMs }),
324
+ };
325
+ const runId = this.nextId("context-run");
326
+ const run = await this.recorder.appendRun({
327
+ runId,
328
+ traceId: this.traceId,
329
+ idempotencyKey: runId,
330
+ kind: "context",
331
+ payload,
332
+ createdAtMs: this.clock(),
333
+ });
334
+ if (!run.ok) return this.softenWriteFailure();
335
+ this.persistedRecords += 1;
336
+ const appended = await this.appendEvent("context", payload, runId);
337
+ if (appended.ok && appended.value !== "disabled") {
338
+ this.evidenceOrigins.addFallback(runId, payload.evidence);
339
+ }
340
+ return appended;
341
+ }
342
+
343
+ async recordEvidence(
344
+ kind: EvidenceEventKind,
345
+ evidence: RetrievalTraceEvidence[],
346
+ latencyMs?: number
347
+ ): Promise<StoreResult<"inserted" | "duplicate" | "disabled">> {
348
+ const payload = {
349
+ evidence,
350
+ ...(latencyMs === undefined ? {} : { latencyMs }),
351
+ };
352
+ if (kind === "get") {
353
+ if (!this.hasCapacity(2)) return ok("disabled");
354
+ const runId = this.nextId("get-run");
355
+ const run = await this.recorder.appendRun({
356
+ runId,
357
+ traceId: this.traceId,
358
+ idempotencyKey: runId,
359
+ kind,
360
+ payload,
361
+ createdAtMs: this.clock(),
362
+ });
363
+ if (!run.ok) return this.softenWriteFailure();
364
+ this.persistedRecords += 1;
365
+ const appended = await this.appendEvent(kind, payload, runId);
366
+ if (appended.ok && appended.value !== "disabled") {
367
+ this.evidenceOrigins.addFallback(runId, evidence);
368
+ }
369
+ return appended;
370
+ }
371
+ if (!this.hasCapacity(1)) return ok("disabled");
372
+ const origins = this.evidenceOrigins.group(evidence);
373
+ if (!origins.ok) return origins;
374
+ if (!this.hasCapacity(origins.value.size)) return ok("disabled");
375
+ let result: "inserted" | "duplicate" | "disabled" = "inserted";
376
+ for (const [runId, runEvidence] of origins.value) {
377
+ const appended = await this.appendEvent(
378
+ kind,
379
+ {
380
+ evidence: runEvidence,
381
+ ...(latencyMs === undefined ? {} : { latencyMs }),
382
+ },
383
+ runId
384
+ );
385
+ if (!appended.ok) return appended;
386
+ result = appended.value;
387
+ }
388
+ return ok(result);
389
+ }
390
+
391
+ async recordCapability(
392
+ capability: string,
393
+ status: "attempted" | "used" | "unavailable" | "failed",
394
+ reasonCode?: string,
395
+ runId = this.retrievalRunId
396
+ ): Promise<StoreResult<"inserted" | "duplicate" | "disabled">> {
397
+ if (!this.hasCapacity(1)) return ok("disabled");
398
+ return this.appendEvent(
399
+ "capability",
400
+ {
401
+ capability,
402
+ status,
403
+ ...(reasonCode === undefined ? {} : { reasonCode }),
404
+ },
405
+ runId
406
+ );
407
+ }
408
+
409
+ async finish(
410
+ status: RetrievalTraceTerminalStatus,
411
+ latencyMs?: number
412
+ ): Promise<StoreResult<"inserted" | "duplicate" | "disabled">> {
413
+ if (this.terminal) return ok("duplicate");
414
+ const completed = await this.appendEvent("complete", {
415
+ outcome: status,
416
+ ...(latencyMs === undefined ? {} : { latencyMs }),
417
+ });
418
+ if (!completed.ok) {
419
+ const finalizedAfterEventFailure = await this.recorder.finalize(
420
+ this.traceId,
421
+ status
422
+ );
423
+ this.terminal = finalizedAfterEventFailure.ok;
424
+ return ok("disabled");
425
+ }
426
+ if (completed.value === "disabled") return completed;
427
+ const finalized = await this.recorder.finalize(this.traceId, status);
428
+ if (finalized.ok) this.terminal = true;
429
+ return finalized;
430
+ }
431
+
432
+ private async appendEvent(
433
+ kind: RetrievalTraceEventKind,
434
+ payload: Record<string, unknown>,
435
+ runId: string | null = null
436
+ ): Promise<StoreResult<"inserted" | "duplicate" | "disabled">> {
437
+ const eventId = this.nextId(kind);
438
+ const appended = await this.recorder.appendEvent({
439
+ eventId,
440
+ traceId: this.traceId,
441
+ runId,
442
+ idempotencyKey: eventId,
443
+ kind,
444
+ payload,
445
+ createdAtMs: this.clock(),
446
+ });
447
+ if (!appended.ok) return this.softenWriteFailure();
448
+ this.persistedRecords += 1;
449
+ return appended;
450
+ }
451
+
452
+ private hasCapacity(records: number): boolean {
453
+ return this.persistedRecords + records + 1 <= this.maxRecords;
454
+ }
455
+
456
+ private async softenWriteFailure(): Promise<
457
+ StoreResult<"inserted" | "duplicate" | "disabled">
458
+ > {
459
+ await this.recorder.finalize(this.traceId, "failed");
460
+ this.available = false;
461
+ this.terminal = true;
462
+ return ok("disabled");
463
+ }
464
+
465
+ private nextId(kind: string): string {
466
+ this.counter += 1;
467
+ return `${this.operationId}:${kind}:${this.counter}`;
468
+ }
469
+ }
470
+
471
+ export type { RetrievalTraceWriteResult };
472
+ export type { RetrievalTraceEvidence };
473
+
474
+ export const evidenceFromExactDocument = (input: {
475
+ docid: string;
476
+ uri: string;
477
+ sourceHash?: string;
478
+ mirrorHash?: string;
479
+ content: string;
480
+ startLine: number;
481
+ endLine: number;
482
+ seq?: number;
483
+ }): RetrievalTraceEvidence | null => {
484
+ if (
485
+ !input.sourceHash ||
486
+ !input.mirrorHash ||
487
+ !input.uri.startsWith("gno://") ||
488
+ !input.content ||
489
+ input.startLine < 1 ||
490
+ input.endLine < input.startLine ||
491
+ input.content.split("\n").length !== input.endLine - input.startLine + 1
492
+ ) {
493
+ return null;
494
+ }
495
+ return {
496
+ docid: input.docid,
497
+ uri: input.uri,
498
+ sourceHash: input.sourceHash,
499
+ mirrorHash: input.mirrorHash,
500
+ startLine: input.startLine,
501
+ endLine: input.endLine,
502
+ passageHash: new Bun.CryptoHasher("sha256")
503
+ .update(input.content)
504
+ .digest("hex"),
505
+ ...(input.seq === undefined ? {} : { seq: input.seq }),
506
+ };
507
+ };