@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.
- package/README.md +14 -7
- package/assets/skill/SKILL.md +54 -12
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +2 -1
- package/spec/AGENTS.md +83 -0
- package/spec/CLAUDE.md +83 -0
- package/spec/bench-fixture.schema.json +137 -0
- package/spec/cli.md +2919 -0
- package/spec/db/schema.sql +442 -0
- package/spec/evals-agentic.md +592 -0
- package/spec/evals.md +1106 -0
- package/spec/mcp.md +2279 -0
- package/spec/output-schemas/activation-verification.schema.json +515 -0
- package/spec/output-schemas/ask.schema.json +564 -0
- package/spec/output-schemas/backlinks.schema.json +131 -0
- package/spec/output-schemas/bench-result.schema.json +120 -0
- package/spec/output-schemas/capture-receipt.schema.json +143 -0
- package/spec/output-schemas/claim-verification.schema.json +291 -0
- package/spec/output-schemas/collection-list.schema.json +45 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +726 -0
- package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
- package/spec/output-schemas/context-list.schema.json +21 -0
- package/spec/output-schemas/doctor.schema.json +313 -0
- package/spec/output-schemas/error.schema.json +30 -0
- package/spec/output-schemas/expansion.schema.json +37 -0
- package/spec/output-schemas/get.schema.json +140 -0
- package/spec/output-schemas/graph-query.schema.json +99 -0
- package/spec/output-schemas/graph.schema.json +371 -0
- package/spec/output-schemas/links-list.schema.json +186 -0
- package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
- package/spec/output-schemas/mcp-http-error.schema.json +30 -0
- package/spec/output-schemas/mcp-job-list.schema.json +58 -0
- package/spec/output-schemas/mcp-job-status.schema.json +224 -0
- package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
- package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
- package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
- package/spec/output-schemas/models-list.schema.json +93 -0
- package/spec/output-schemas/multi-get.schema.json +103 -0
- package/spec/output-schemas/process-status.schema.json +119 -0
- package/spec/output-schemas/query-diagnose.schema.json +123 -0
- package/spec/output-schemas/resident-status.schema.json +154 -0
- package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
- package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
- package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
- package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
- package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
- package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
- package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
- package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
- package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
- package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
- package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
- package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
- package/spec/output-schemas/search-result.schema.json +154 -0
- package/spec/output-schemas/search-results.schema.json +338 -0
- package/spec/output-schemas/similar.schema.json +84 -0
- package/spec/output-schemas/status.schema.json +676 -0
- package/spec/output-schemas/tags-list.schema.json +48 -0
- package/src/app/context-runtime-contract.ts +10 -5
- package/src/app/context-runtime-input.ts +29 -1
- package/src/app/context-runtime-types.ts +7 -0
- package/src/app/context-runtime.ts +20 -2
- package/src/app/context-surface.ts +4 -0
- package/src/app/verified-ask.ts +291 -0
- package/src/cli/commands/ask-format.ts +255 -0
- package/src/cli/commands/ask.ts +144 -183
- package/src/cli/commands/context-build.ts +56 -9
- package/src/cli/commands/get.ts +64 -3
- package/src/cli/commands/query.ts +62 -23
- package/src/cli/commands/replay.ts +140 -0
- package/src/cli/commands/search.ts +48 -3
- package/src/cli/commands/shared.ts +3 -1
- package/src/cli/commands/trace.ts +200 -0
- package/src/cli/commands/vsearch.ts +75 -53
- package/src/cli/program.ts +287 -1
- package/src/config/index.ts +9 -0
- package/src/config/retrieval-traces.ts +56 -0
- package/src/config/types.ts +4 -0
- package/src/core/context-budget.ts +6 -0
- package/src/core/context-capsule-retrieval-schema.ts +4 -0
- package/src/core/context-capsule-schema.ts +17 -0
- package/src/core/context-capsule-validation.ts +3 -2
- package/src/core/context-capsule.ts +18 -0
- package/src/core/context-compiler.ts +44 -25
- package/src/core/context-evidence.ts +6 -0
- package/src/core/retrieval-qrels.ts +405 -0
- package/src/core/retrieval-replay-candidate.ts +368 -0
- package/src/core/retrieval-replay-types.ts +109 -0
- package/src/core/retrieval-replay-validation.ts +89 -0
- package/src/core/retrieval-replay.ts +441 -0
- package/src/core/retrieval-trace-evidence-origin.ts +178 -0
- package/src/core/retrieval-trace-export.ts +113 -0
- package/src/core/retrieval-trace-filter-normalization.ts +27 -0
- package/src/core/retrieval-trace-filters.ts +19 -0
- package/src/core/retrieval-trace-management-helpers.ts +247 -0
- package/src/core/retrieval-trace-management-types.ts +132 -0
- package/src/core/retrieval-trace-management.ts +422 -0
- package/src/core/retrieval-trace-request.ts +141 -0
- package/src/core/retrieval-trace-session.ts +507 -0
- package/src/core/retrieval-trace.ts +472 -0
- package/src/llm/errors.ts +10 -1
- package/src/llm/httpGeneration.ts +11 -1
- package/src/llm/nodeLlamaCpp/generation.ts +54 -10
- package/src/llm/types.ts +6 -0
- package/src/mcp/tools/ask.ts +228 -0
- package/src/mcp/tools/context.ts +87 -15
- package/src/mcp/tools/get.ts +35 -1
- package/src/mcp/tools/index.ts +83 -0
- package/src/mcp/tools/query.ts +95 -64
- package/src/mcp/tools/search.ts +36 -13
- package/src/mcp/tools/trace.ts +143 -0
- package/src/mcp/tools/vsearch.ts +71 -38
- package/src/pipeline/answer.ts +167 -26
- package/src/pipeline/claim-verification-schema.ts +235 -0
- package/src/pipeline/claim-verification.ts +487 -0
- package/src/pipeline/claim-verifier.ts +474 -0
- package/src/pipeline/graph-retrieval.ts +15 -1
- package/src/pipeline/hybrid.ts +151 -43
- package/src/pipeline/search.ts +36 -3
- package/src/pipeline/trace-metadata.ts +47 -0
- package/src/pipeline/types.ts +68 -0
- package/src/pipeline/vsearch.ts +101 -38
- package/src/sdk/client.ts +415 -73
- package/src/sdk/documents.ts +48 -1
- package/src/sdk/index.ts +17 -0
- package/src/sdk/types.ts +28 -0
- package/src/serve/context-capsule.ts +67 -8
- package/src/serve/public/app.tsx +12 -1
- package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/public/pages/Dashboard.tsx +10 -0
- package/src/serve/public/pages/TraceHistory.tsx +478 -0
- package/src/serve/public/pages/trace-history-detail.tsx +224 -0
- package/src/serve/retrieval-trace.ts +28 -0
- package/src/serve/routes/api.ts +508 -68
- package/src/serve/routes/traces.ts +156 -0
- package/src/serve/server.ts +87 -2
- package/src/store/index.ts +31 -0
- package/src/store/migrations/014-retrieval-traces.ts +303 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/retrieval-trace-codec.ts +384 -0
- package/src/store/sqlite/adapter.ts +153 -1
- package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
- package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
- package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
- package/src/store/sqlite/retrieval-trace-store.ts +515 -0
- package/src/store/types.ts +297 -0
- package/src/store/vector/sqlite-vec.ts +76 -1
- package/src/store/vector/types.ts +1 -1
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/** Candidate execution with exact persisted replay scopes. */
|
|
2
|
+
|
|
3
|
+
import type { HybridSearchDeps } from "../pipeline/hybrid";
|
|
4
|
+
import type {
|
|
5
|
+
HybridSearchOptions,
|
|
6
|
+
SearchResult,
|
|
7
|
+
SearchResults,
|
|
8
|
+
SearchResultsTraceMetadata,
|
|
9
|
+
} from "../pipeline/types";
|
|
10
|
+
import type { StoreResult } from "../store/types";
|
|
11
|
+
import type { RetrievalQrelsCase } from "./retrieval-qrels";
|
|
12
|
+
import type { RetrievalReplayCandidate } from "./retrieval-replay-types";
|
|
13
|
+
|
|
14
|
+
import { parseUri } from "../app/constants";
|
|
15
|
+
import { searchHybrid } from "../pipeline/hybrid";
|
|
16
|
+
import { searchBm25 } from "../pipeline/search";
|
|
17
|
+
import { SEARCH_RESULTS_TRACE_METADATA } from "../pipeline/types";
|
|
18
|
+
import { searchVector } from "../pipeline/vsearch";
|
|
19
|
+
import { err, ok } from "../store/types";
|
|
20
|
+
|
|
21
|
+
export interface RetrievalReplayDeps extends HybridSearchDeps {
|
|
22
|
+
indexName?: string;
|
|
23
|
+
modelUris?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const retrievalReplayLimit = (
|
|
27
|
+
source: RetrievalQrelsCase,
|
|
28
|
+
candidate: RetrievalReplayCandidate
|
|
29
|
+
): number => {
|
|
30
|
+
const persistedLimit =
|
|
31
|
+
typeof source.query.filters.limit === "number"
|
|
32
|
+
? source.query.filters.limit
|
|
33
|
+
: undefined;
|
|
34
|
+
return (
|
|
35
|
+
candidate.limit ??
|
|
36
|
+
persistedLimit ??
|
|
37
|
+
(source.baseline.ranked.length > 0 ? source.baseline.ranked.length : 20)
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const retrievalReplayTraceMetadata = (
|
|
42
|
+
candidate: RetrievalReplayCandidate,
|
|
43
|
+
result: SearchResults
|
|
44
|
+
): SearchResultsTraceMetadata => {
|
|
45
|
+
const persisted = result[SEARCH_RESULTS_TRACE_METADATA];
|
|
46
|
+
if (persisted) return persisted;
|
|
47
|
+
if (candidate.type === "bm25") {
|
|
48
|
+
return {
|
|
49
|
+
capabilityOutcomes: [{ capability: "lexical_search", status: "used" }],
|
|
50
|
+
fallbackCodes: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (candidate.type === "vector") {
|
|
54
|
+
return {
|
|
55
|
+
capabilityOutcomes: [{ capability: "semantic_search", status: "used" }],
|
|
56
|
+
fallbackCodes: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
capabilityOutcomes: [
|
|
61
|
+
{ capability: "lexical_search", status: "used" },
|
|
62
|
+
result.meta.vectorsUsed
|
|
63
|
+
? { capability: "semantic_search", status: "used" }
|
|
64
|
+
: {
|
|
65
|
+
capability: "semantic_search",
|
|
66
|
+
status: "unavailable",
|
|
67
|
+
reasonCode: "vector_unavailable",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
fallbackCodes: result.meta.vectorsUsed ? [] : ["vector_unavailable"],
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const buildRetrievalReplaySearchOptions = (
|
|
75
|
+
source: RetrievalQrelsCase,
|
|
76
|
+
candidate: RetrievalReplayCandidate,
|
|
77
|
+
collectionOverride?: string,
|
|
78
|
+
retrievalScope?: HybridSearchOptions["retrievalScope"]
|
|
79
|
+
): HybridSearchOptions => {
|
|
80
|
+
const filters = source.query.filters;
|
|
81
|
+
return {
|
|
82
|
+
limit:
|
|
83
|
+
candidate.limit ??
|
|
84
|
+
(typeof filters.limit === "number"
|
|
85
|
+
? filters.limit
|
|
86
|
+
: source.baseline.ranked.length || undefined),
|
|
87
|
+
minScore:
|
|
88
|
+
typeof filters.minScore === "number" ? filters.minScore : undefined,
|
|
89
|
+
collection:
|
|
90
|
+
collectionOverride ??
|
|
91
|
+
(typeof filters.collection === "string" ? filters.collection : undefined),
|
|
92
|
+
retrievalScope,
|
|
93
|
+
lang: typeof filters.lang === "string" ? filters.lang : undefined,
|
|
94
|
+
full: typeof filters.full === "boolean" ? filters.full : undefined,
|
|
95
|
+
lineNumbers:
|
|
96
|
+
typeof filters.lineNumbers === "boolean"
|
|
97
|
+
? filters.lineNumbers
|
|
98
|
+
: undefined,
|
|
99
|
+
tagsAll: Array.isArray(filters.tagsAll)
|
|
100
|
+
? (filters.tagsAll as string[])
|
|
101
|
+
: undefined,
|
|
102
|
+
tagsAny: Array.isArray(filters.tagsAny)
|
|
103
|
+
? (filters.tagsAny as string[])
|
|
104
|
+
: undefined,
|
|
105
|
+
since: typeof filters.since === "string" ? filters.since : undefined,
|
|
106
|
+
until: typeof filters.until === "string" ? filters.until : undefined,
|
|
107
|
+
categories: Array.isArray(filters.categories)
|
|
108
|
+
? (filters.categories as string[])
|
|
109
|
+
: undefined,
|
|
110
|
+
author: typeof filters.author === "string" ? filters.author : undefined,
|
|
111
|
+
intent: typeof filters.intent === "string" ? filters.intent : undefined,
|
|
112
|
+
exclude: Array.isArray(filters.exclude)
|
|
113
|
+
? (filters.exclude as string[])
|
|
114
|
+
: undefined,
|
|
115
|
+
graph: typeof filters.graph === "boolean" ? filters.graph : undefined,
|
|
116
|
+
noGraph: typeof filters.noGraph === "boolean" ? filters.noGraph : undefined,
|
|
117
|
+
explain: typeof filters.explain === "boolean" ? filters.explain : undefined,
|
|
118
|
+
queryLanguageHint:
|
|
119
|
+
typeof filters.queryLanguageHint === "string"
|
|
120
|
+
? filters.queryLanguageHint
|
|
121
|
+
: undefined,
|
|
122
|
+
candidateLimit:
|
|
123
|
+
candidate.candidateLimit ??
|
|
124
|
+
(typeof filters.candidateLimit === "number"
|
|
125
|
+
? filters.candidateLimit
|
|
126
|
+
: undefined),
|
|
127
|
+
noExpand:
|
|
128
|
+
candidate.noExpand ??
|
|
129
|
+
(typeof filters.noExpand === "boolean" ? filters.noExpand : undefined),
|
|
130
|
+
noRerank:
|
|
131
|
+
candidate.noRerank ??
|
|
132
|
+
(typeof filters.noRerank === "boolean" ? filters.noRerank : undefined),
|
|
133
|
+
queryModes:
|
|
134
|
+
candidate.queryModes ??
|
|
135
|
+
(Array.isArray(filters.queryModes)
|
|
136
|
+
? (filters.queryModes as RetrievalReplayCandidate["queryModes"])
|
|
137
|
+
: undefined),
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const runCandidateOnce = async (
|
|
142
|
+
deps: RetrievalReplayDeps,
|
|
143
|
+
source: RetrievalQrelsCase,
|
|
144
|
+
candidate: RetrievalReplayCandidate,
|
|
145
|
+
options: HybridSearchOptions
|
|
146
|
+
): Promise<StoreResult<SearchResults>> => {
|
|
147
|
+
if (candidate.type === "bm25") {
|
|
148
|
+
return searchBm25(deps.store, source.query.text, options);
|
|
149
|
+
}
|
|
150
|
+
if (candidate.type === "vector") {
|
|
151
|
+
if (!(deps.vectorIndex && deps.embedPort)) {
|
|
152
|
+
return err("QUERY_FAILED", "Candidate vector pipeline is unavailable");
|
|
153
|
+
}
|
|
154
|
+
return searchVector(
|
|
155
|
+
{
|
|
156
|
+
store: deps.store,
|
|
157
|
+
config: deps.config,
|
|
158
|
+
vectorIndex: deps.vectorIndex,
|
|
159
|
+
embedPort: deps.embedPort,
|
|
160
|
+
},
|
|
161
|
+
source.query.text,
|
|
162
|
+
options
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return searchHybrid(deps, source.query.text, options);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
interface ReplayScope {
|
|
169
|
+
collections: Array<string | undefined>;
|
|
170
|
+
uriPrefix: ReturnType<typeof parseUri>;
|
|
171
|
+
finalLimit: number;
|
|
172
|
+
fetchLimit: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const resolveReplayScope = (
|
|
176
|
+
source: RetrievalQrelsCase,
|
|
177
|
+
candidate: RetrievalReplayCandidate
|
|
178
|
+
): StoreResult<ReplayScope> => {
|
|
179
|
+
const filters = source.query.filters;
|
|
180
|
+
const singular =
|
|
181
|
+
typeof filters.collection === "string" ? filters.collection : undefined;
|
|
182
|
+
const plural = Array.isArray(filters.collections)
|
|
183
|
+
? filters.collections.filter(
|
|
184
|
+
(value): value is string => typeof value === "string"
|
|
185
|
+
)
|
|
186
|
+
: [];
|
|
187
|
+
const prefixValue =
|
|
188
|
+
typeof filters.uriPrefix === "string" ? filters.uriPrefix : undefined;
|
|
189
|
+
const uriPrefix = prefixValue ? parseUri(prefixValue) : null;
|
|
190
|
+
if (prefixValue && !uriPrefix) {
|
|
191
|
+
return err("INVALID_INPUT", "Persisted replay URI prefix is invalid");
|
|
192
|
+
}
|
|
193
|
+
if (singular && plural.length > 0 && !plural.includes(singular)) {
|
|
194
|
+
return err(
|
|
195
|
+
"INVALID_INPUT",
|
|
196
|
+
"Persisted singular and plural collection filters conflict"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
let collections: Array<string | undefined> =
|
|
200
|
+
plural.length > 0
|
|
201
|
+
? [...new Set(plural)].sort()
|
|
202
|
+
: singular
|
|
203
|
+
? [singular]
|
|
204
|
+
: [undefined];
|
|
205
|
+
if (singular) collections = [singular];
|
|
206
|
+
if (uriPrefix) {
|
|
207
|
+
if (
|
|
208
|
+
collections[0] !== undefined &&
|
|
209
|
+
!collections.includes(uriPrefix.collection)
|
|
210
|
+
) {
|
|
211
|
+
return err(
|
|
212
|
+
"INVALID_INPUT",
|
|
213
|
+
"Persisted URI prefix falls outside the collection scope"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
collections = [uriPrefix.collection];
|
|
217
|
+
}
|
|
218
|
+
const finalLimit = retrievalReplayLimit(source, candidate);
|
|
219
|
+
const persistedCandidateLimit =
|
|
220
|
+
typeof filters.candidateLimit === "number"
|
|
221
|
+
? filters.candidateLimit
|
|
222
|
+
: undefined;
|
|
223
|
+
const fetchLimit = uriPrefix
|
|
224
|
+
? Math.max(
|
|
225
|
+
finalLimit,
|
|
226
|
+
candidate.candidateLimit ?? persistedCandidateLimit ?? 100
|
|
227
|
+
)
|
|
228
|
+
: finalLimit;
|
|
229
|
+
return ok({ collections, uriPrefix, finalLimit, fetchLimit });
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const resultInPrefix = (
|
|
233
|
+
result: SearchResult,
|
|
234
|
+
prefix: NonNullable<ReplayScope["uriPrefix"]>
|
|
235
|
+
): boolean => {
|
|
236
|
+
const parsed = parseUri(result.uri);
|
|
237
|
+
return Boolean(
|
|
238
|
+
parsed &&
|
|
239
|
+
parsed.collection === prefix.collection &&
|
|
240
|
+
(prefix.path === "" ||
|
|
241
|
+
parsed.path === prefix.path ||
|
|
242
|
+
parsed.path.startsWith(`${prefix.path}/`))
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const compareReplayResults = (
|
|
247
|
+
left: SearchResult,
|
|
248
|
+
right: SearchResult
|
|
249
|
+
): number =>
|
|
250
|
+
right.score - left.score ||
|
|
251
|
+
left.uri.localeCompare(right.uri) ||
|
|
252
|
+
(left.line ?? 0) - (right.line ?? 0) ||
|
|
253
|
+
left.docid.localeCompare(right.docid);
|
|
254
|
+
|
|
255
|
+
export const runRetrievalReplayCandidate = async (
|
|
256
|
+
deps: RetrievalReplayDeps,
|
|
257
|
+
source: RetrievalQrelsCase,
|
|
258
|
+
candidate: RetrievalReplayCandidate
|
|
259
|
+
): Promise<StoreResult<SearchResults>> => {
|
|
260
|
+
const scope = resolveReplayScope(source, candidate);
|
|
261
|
+
if (!scope.ok) return scope;
|
|
262
|
+
let retrievalScope: HybridSearchOptions["retrievalScope"];
|
|
263
|
+
if (scope.value.uriPrefix) {
|
|
264
|
+
const documents = await deps.store.listDocuments(
|
|
265
|
+
scope.value.uriPrefix.collection
|
|
266
|
+
);
|
|
267
|
+
if (!documents.ok) return documents;
|
|
268
|
+
const prefixPath = scope.value.uriPrefix.path;
|
|
269
|
+
retrievalScope = {
|
|
270
|
+
...(prefixPath === "" ? {} : { relPathPrefix: prefixPath }),
|
|
271
|
+
allowedMirrorHashes: [
|
|
272
|
+
...new Set(
|
|
273
|
+
documents.value
|
|
274
|
+
.filter(
|
|
275
|
+
(document) =>
|
|
276
|
+
document.active &&
|
|
277
|
+
document.mirrorHash &&
|
|
278
|
+
(prefixPath === "" ||
|
|
279
|
+
document.relPath === prefixPath ||
|
|
280
|
+
document.relPath.startsWith(`${prefixPath}/`))
|
|
281
|
+
)
|
|
282
|
+
.map((document) => document.mirrorHash!)
|
|
283
|
+
),
|
|
284
|
+
].sort(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const outputs: SearchResults[] = [];
|
|
288
|
+
for (const collection of scope.value.collections) {
|
|
289
|
+
const options = buildRetrievalReplaySearchOptions(
|
|
290
|
+
source,
|
|
291
|
+
candidate,
|
|
292
|
+
collection,
|
|
293
|
+
retrievalScope
|
|
294
|
+
);
|
|
295
|
+
options.limit = scope.value.fetchLimit;
|
|
296
|
+
const result = await runCandidateOnce(deps, source, candidate, options);
|
|
297
|
+
if (!result.ok) return result;
|
|
298
|
+
outputs.push(result.value);
|
|
299
|
+
}
|
|
300
|
+
const unique = new Map<string, SearchResult>();
|
|
301
|
+
for (const output of outputs) {
|
|
302
|
+
for (const result of output.results) {
|
|
303
|
+
if (
|
|
304
|
+
scope.value.uriPrefix &&
|
|
305
|
+
!resultInPrefix(result, scope.value.uriPrefix)
|
|
306
|
+
) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const key = `${result.docid}\0${result.conversion?.mirrorHash ?? ""}\0${
|
|
310
|
+
result.snippetRange?.startLine ?? result.line ?? 0
|
|
311
|
+
}\0${result.snippetRange?.endLine ?? result.line ?? 0}`;
|
|
312
|
+
const previous = unique.get(key);
|
|
313
|
+
if (!previous || compareReplayResults(result, previous) < 0) {
|
|
314
|
+
unique.set(key, result);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const results = [...unique.values()]
|
|
319
|
+
.sort(compareReplayResults)
|
|
320
|
+
.slice(0, scope.value.finalLimit);
|
|
321
|
+
const first = outputs[0];
|
|
322
|
+
if (!first) {
|
|
323
|
+
return err("QUERY_FAILED", "Candidate replay produced no search response");
|
|
324
|
+
}
|
|
325
|
+
const output: SearchResults = {
|
|
326
|
+
results,
|
|
327
|
+
meta: {
|
|
328
|
+
...first.meta,
|
|
329
|
+
totalResults: results.length,
|
|
330
|
+
collection:
|
|
331
|
+
scope.value.collections.length === 1
|
|
332
|
+
? scope.value.collections[0]
|
|
333
|
+
: undefined,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
const capabilityOutcomes = outputs
|
|
337
|
+
.flatMap(
|
|
338
|
+
(item) => item[SEARCH_RESULTS_TRACE_METADATA]?.capabilityOutcomes ?? []
|
|
339
|
+
)
|
|
340
|
+
.filter(
|
|
341
|
+
(item, index, all) =>
|
|
342
|
+
all.findIndex(
|
|
343
|
+
(candidateItem) =>
|
|
344
|
+
candidateItem.capability === item.capability &&
|
|
345
|
+
candidateItem.status === item.status &&
|
|
346
|
+
candidateItem.reasonCode === item.reasonCode
|
|
347
|
+
) === index
|
|
348
|
+
)
|
|
349
|
+
.sort((left, right) =>
|
|
350
|
+
`${left.capability}\0${left.status}\0${left.reasonCode ?? ""}`.localeCompare(
|
|
351
|
+
`${right.capability}\0${right.status}\0${right.reasonCode ?? ""}`
|
|
352
|
+
)
|
|
353
|
+
);
|
|
354
|
+
const fallbackCodes = [
|
|
355
|
+
...new Set(
|
|
356
|
+
outputs.flatMap(
|
|
357
|
+
(item) => item[SEARCH_RESULTS_TRACE_METADATA]?.fallbackCodes ?? []
|
|
358
|
+
)
|
|
359
|
+
),
|
|
360
|
+
].sort();
|
|
361
|
+
if (capabilityOutcomes.length > 0 || fallbackCodes.length > 0) {
|
|
362
|
+
Object.defineProperty(output, SEARCH_RESULTS_TRACE_METADATA, {
|
|
363
|
+
enumerable: false,
|
|
364
|
+
value: { capabilityOutcomes, fallbackCodes },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return ok(output);
|
|
368
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Public contracts for immutable retrieval replay reports. */
|
|
2
|
+
|
|
3
|
+
import type { RetrievalMetrics } from "../bench/metrics";
|
|
4
|
+
import type { RetrievalTraceFingerprints } from "../store/types";
|
|
5
|
+
import type { RetrievalQrelsCase, RetrievalQrel } from "./retrieval-qrels";
|
|
6
|
+
|
|
7
|
+
export type RetrievalReplayCandidateType = "bm25" | "vector" | "hybrid";
|
|
8
|
+
|
|
9
|
+
export interface RetrievalReplayCandidate {
|
|
10
|
+
id: string;
|
|
11
|
+
type: RetrievalReplayCandidateType;
|
|
12
|
+
limit?: number;
|
|
13
|
+
candidateLimit?: number;
|
|
14
|
+
noExpand?: boolean;
|
|
15
|
+
noRerank?: boolean;
|
|
16
|
+
queryModes?: Array<{
|
|
17
|
+
mode: "term" | "intent" | "hyde";
|
|
18
|
+
text: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ReplayRetrievalTraceInput {
|
|
23
|
+
exportId: string;
|
|
24
|
+
candidate: RetrievalReplayCandidate;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type RetrievalReplaySourceState =
|
|
28
|
+
| "unchanged"
|
|
29
|
+
| "stale"
|
|
30
|
+
| "missing"
|
|
31
|
+
| "inactive"
|
|
32
|
+
| "no_indexed_content";
|
|
33
|
+
|
|
34
|
+
export type RetrievalReplayVerdict =
|
|
35
|
+
| "improved"
|
|
36
|
+
| "unchanged"
|
|
37
|
+
| "regressed"
|
|
38
|
+
| "unreplayable";
|
|
39
|
+
|
|
40
|
+
export type RetrievalReplayReason =
|
|
41
|
+
| "manifest_missing"
|
|
42
|
+
| "manifest_hash_mismatch"
|
|
43
|
+
| "trace_missing"
|
|
44
|
+
| "redaction_incompatible"
|
|
45
|
+
| "query_missing"
|
|
46
|
+
| "filters_incomplete"
|
|
47
|
+
| "no_retrieval_run"
|
|
48
|
+
| "ambiguous_missing_expected_run"
|
|
49
|
+
| "source_stale"
|
|
50
|
+
| "source_missing"
|
|
51
|
+
| "candidate_failed";
|
|
52
|
+
|
|
53
|
+
export interface RetrievalReplayQrelResult {
|
|
54
|
+
qrelId: string;
|
|
55
|
+
label: RetrievalQrel["label"];
|
|
56
|
+
relevance: 0 | 1;
|
|
57
|
+
baselineRank: number | null;
|
|
58
|
+
plannerRank: number | null;
|
|
59
|
+
candidateRank: number | null;
|
|
60
|
+
rankDelta: number | null;
|
|
61
|
+
opened: boolean;
|
|
62
|
+
cited: boolean;
|
|
63
|
+
pinned: boolean;
|
|
64
|
+
sourceState: RetrievalReplaySourceState;
|
|
65
|
+
diagnostic: {
|
|
66
|
+
status: string;
|
|
67
|
+
filterReasons: string[];
|
|
68
|
+
stageDropReasons: string[];
|
|
69
|
+
} | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface RetrievalReplayCaseResult {
|
|
73
|
+
caseId: string;
|
|
74
|
+
traceId: string;
|
|
75
|
+
terminalStatus: RetrievalQrelsCase["terminalStatus"];
|
|
76
|
+
verdict: RetrievalReplayVerdict;
|
|
77
|
+
reason: RetrievalReplayReason | null;
|
|
78
|
+
metrics: {
|
|
79
|
+
baseline: RetrievalMetrics;
|
|
80
|
+
candidate: RetrievalMetrics;
|
|
81
|
+
baselineCoverage: number;
|
|
82
|
+
candidateCoverage: number;
|
|
83
|
+
};
|
|
84
|
+
fingerprints: {
|
|
85
|
+
original: RetrievalTraceFingerprints;
|
|
86
|
+
current: RetrievalTraceFingerprints;
|
|
87
|
+
candidate: RetrievalTraceFingerprints;
|
|
88
|
+
};
|
|
89
|
+
capabilityOutcomes: {
|
|
90
|
+
baseline: RetrievalQrelsCase["baseline"]["capabilityOutcomes"];
|
|
91
|
+
candidate: RetrievalQrelsCase["baseline"]["capabilityOutcomes"];
|
|
92
|
+
};
|
|
93
|
+
fallbackCodes: {
|
|
94
|
+
baseline: string[];
|
|
95
|
+
candidate: string[];
|
|
96
|
+
};
|
|
97
|
+
qrels: RetrievalReplayQrelResult[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ReplayRetrievalTraceResult {
|
|
101
|
+
schemaVersion: "1.0";
|
|
102
|
+
exportId: string;
|
|
103
|
+
candidate: RetrievalReplayCandidate;
|
|
104
|
+
verdict: RetrievalReplayVerdict;
|
|
105
|
+
reason: RetrievalReplayReason | null;
|
|
106
|
+
recommendation: "promote" | "keep_baseline" | "manual_review";
|
|
107
|
+
applied: false;
|
|
108
|
+
cases: RetrievalReplayCaseResult[];
|
|
109
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Closed runtime validation for the shared retrieval replay boundary. */
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { StoreResult } from "../store/types";
|
|
6
|
+
import type { ReplayRetrievalTraceInput } from "./retrieval-replay-types";
|
|
7
|
+
|
|
8
|
+
import { err, ok } from "../store/types";
|
|
9
|
+
|
|
10
|
+
const MAX_REPLAY_RESULTS = 10_000;
|
|
11
|
+
const queryModeTextSchema = z
|
|
12
|
+
.string()
|
|
13
|
+
.trim()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(8192)
|
|
16
|
+
.refine((value) => !value.includes("\r"), {
|
|
17
|
+
message: "Replay query mode text must not contain carriage returns",
|
|
18
|
+
})
|
|
19
|
+
.transform((value) => value.normalize("NFC"))
|
|
20
|
+
.pipe(z.string().min(1).max(8192));
|
|
21
|
+
const queryModeSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
mode: z.enum(["term", "intent", "hyde"]),
|
|
24
|
+
text: queryModeTextSchema,
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
const candidateSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
id: z.string().trim().min(1).max(128),
|
|
30
|
+
type: z.enum(["bm25", "vector", "hybrid"]),
|
|
31
|
+
limit: z
|
|
32
|
+
.number()
|
|
33
|
+
.finite()
|
|
34
|
+
.int()
|
|
35
|
+
.positive()
|
|
36
|
+
.max(MAX_REPLAY_RESULTS)
|
|
37
|
+
.optional(),
|
|
38
|
+
candidateLimit: z
|
|
39
|
+
.number()
|
|
40
|
+
.finite()
|
|
41
|
+
.int()
|
|
42
|
+
.positive()
|
|
43
|
+
.max(MAX_REPLAY_RESULTS)
|
|
44
|
+
.optional(),
|
|
45
|
+
noExpand: z.boolean().optional(),
|
|
46
|
+
noRerank: z.boolean().optional(),
|
|
47
|
+
queryModes: z
|
|
48
|
+
.array(queryModeSchema)
|
|
49
|
+
.max(100)
|
|
50
|
+
.superRefine((modes, context) => {
|
|
51
|
+
const seen = new Set<string>();
|
|
52
|
+
let hydeCount = 0;
|
|
53
|
+
for (const [index, mode] of modes.entries()) {
|
|
54
|
+
const key = `${mode.mode}\0${mode.text}`;
|
|
55
|
+
if (seen.has(key)) {
|
|
56
|
+
context.addIssue({
|
|
57
|
+
code: "custom",
|
|
58
|
+
path: [index],
|
|
59
|
+
message: "Replay queryModes must be unique",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
seen.add(key);
|
|
63
|
+
if (mode.mode === "hyde") hydeCount += 1;
|
|
64
|
+
}
|
|
65
|
+
if (hydeCount > 1) {
|
|
66
|
+
context.addIssue({
|
|
67
|
+
code: "custom",
|
|
68
|
+
message: "Replay queryModes allow at most one hyde mode",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.optional(),
|
|
73
|
+
})
|
|
74
|
+
.strict();
|
|
75
|
+
const replayInputSchema = z
|
|
76
|
+
.object({
|
|
77
|
+
exportId: z.string().trim().min(1).max(128),
|
|
78
|
+
candidate: candidateSchema,
|
|
79
|
+
})
|
|
80
|
+
.strict();
|
|
81
|
+
|
|
82
|
+
export const parseReplayRetrievalTraceInput = (
|
|
83
|
+
input: unknown
|
|
84
|
+
): StoreResult<ReplayRetrievalTraceInput> => {
|
|
85
|
+
const parsed = replayInputSchema.safeParse(input);
|
|
86
|
+
return parsed.success
|
|
87
|
+
? ok(parsed.data)
|
|
88
|
+
: err("INVALID_INPUT", "Invalid retrieval replay input", parsed.error);
|
|
89
|
+
};
|