@gmickel/gno 1.21.0 → 1.23.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 +26 -2
- package/assets/skill/SKILL.md +15 -0
- package/package.json +2 -1
- package/spec/cli.md +80 -20
- package/spec/evals-agentic.md +83 -0
- package/spec/evals.md +6 -0
- package/spec/mcp.md +18 -0
- package/spec/output-schemas/publish-artifact.schema.json +284 -0
- package/spec/output-schemas/query-diagnose-v1.schema.json +123 -0
- package/spec/output-schemas/query-diagnose.schema.json +89 -2
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +1 -0
- package/src/app/context-surface.ts +4 -2
- package/src/cli/commands/ask.ts +31 -20
- package/src/cli/commands/context-build.ts +17 -7
- package/src/cli/commands/query.ts +58 -37
- package/src/cli/commands/search.ts +29 -19
- package/src/cli/commands/vsearch.ts +31 -22
- package/src/cli/options.ts +39 -0
- package/src/cli/program.ts +48 -0
- package/src/config/defaults.ts +10 -1
- package/src/config/types.ts +71 -0
- package/src/core/project-affinity-surface.ts +114 -0
- package/src/core/project-affinity.ts +330 -0
- package/src/core/validation.ts +20 -1
- package/src/mcp/tools/ask.ts +10 -1
- package/src/mcp/tools/context.ts +18 -0
- package/src/mcp/tools/index.ts +13 -2
- package/src/mcp/tools/query.ts +12 -0
- package/src/mcp/tools/search.ts +7 -0
- package/src/mcp/tools/vsearch.ts +7 -0
- package/src/pipeline/diagnose.ts +48 -3
- package/src/pipeline/explain.ts +54 -13
- package/src/pipeline/hybrid.ts +100 -59
- package/src/pipeline/project-affinity.ts +162 -0
- package/src/pipeline/search.ts +76 -10
- package/src/pipeline/types.ts +9 -0
- package/src/pipeline/vsearch.ts +117 -91
- package/src/publish/artifact-validation.ts +259 -0
- package/src/publish/artifact.ts +234 -118
- package/src/publish/export-service.ts +5 -9
- package/src/publish/metadata.ts +195 -0
- package/src/sdk/client.ts +80 -20
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +20 -7
- package/src/serve/context-capsule.ts +18 -1
- package/src/serve/routes/api.ts +69 -0
package/src/mcp/tools/query.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
normalizeContentTypes,
|
|
27
27
|
} from "../../config";
|
|
28
28
|
import { resolveDepthPolicy } from "../../core/depth-policy";
|
|
29
|
+
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
29
30
|
import {
|
|
30
31
|
finishRetrievalTraceAfterError,
|
|
31
32
|
retrievalTraceFilters,
|
|
@@ -49,6 +50,7 @@ import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
|
49
50
|
|
|
50
51
|
interface QueryInput {
|
|
51
52
|
query: string;
|
|
53
|
+
projectHints?: string[];
|
|
52
54
|
collection?: string;
|
|
53
55
|
limit?: number;
|
|
54
56
|
minScore?: number;
|
|
@@ -227,6 +229,10 @@ export function handleQuery(
|
|
|
227
229
|
hasStructuredModes,
|
|
228
230
|
});
|
|
229
231
|
const { noExpand, noRerank } = depthPolicy;
|
|
232
|
+
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
233
|
+
ctx.config,
|
|
234
|
+
args.projectHints
|
|
235
|
+
);
|
|
230
236
|
const expandUri =
|
|
231
237
|
!noExpand && !hasStructuredModes
|
|
232
238
|
? resolveModelUri(ctx.config, "expand", undefined, args.collection)
|
|
@@ -253,6 +259,7 @@ export function handleQuery(
|
|
|
253
259
|
queryModes,
|
|
254
260
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
255
261
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
262
|
+
projectAffinity,
|
|
256
263
|
};
|
|
257
264
|
|
|
258
265
|
try {
|
|
@@ -440,6 +447,10 @@ export function handleQueryDiagnose(
|
|
|
440
447
|
hasStructuredModes,
|
|
441
448
|
});
|
|
442
449
|
const { noExpand, noRerank } = depthPolicy;
|
|
450
|
+
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
451
|
+
ctx.config,
|
|
452
|
+
args.projectHints
|
|
453
|
+
);
|
|
443
454
|
|
|
444
455
|
if (!args.fast) {
|
|
445
456
|
const embedResult = await llm.createEmbeddingPort(embedUri, {
|
|
@@ -524,6 +535,7 @@ export function handleQueryDiagnose(
|
|
|
524
535
|
queryModes,
|
|
525
536
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
526
537
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
538
|
+
projectAffinity,
|
|
527
539
|
contentTypeRules,
|
|
528
540
|
contentTypeRulesFingerprint:
|
|
529
541
|
fingerprintContentTypeRules(contentTypeRules),
|
package/src/mcp/tools/search.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { SearchResult, SearchResults } from "../../pipeline/types";
|
|
|
11
11
|
import type { ToolContext } from "../server";
|
|
12
12
|
|
|
13
13
|
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
14
|
+
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
14
15
|
import {
|
|
15
16
|
finishRetrievalTraceAfterError,
|
|
16
17
|
retrievalTraceFilters,
|
|
@@ -22,6 +23,7 @@ import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
|
22
23
|
|
|
23
24
|
interface SearchInput {
|
|
24
25
|
query: string;
|
|
26
|
+
projectHints?: string[];
|
|
25
27
|
collection?: string;
|
|
26
28
|
limit?: number;
|
|
27
29
|
minScore?: number;
|
|
@@ -113,6 +115,10 @@ export function handleSearch(
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
119
|
+
ctx.config,
|
|
120
|
+
args.projectHints
|
|
121
|
+
);
|
|
116
122
|
const options = {
|
|
117
123
|
limit: args.limit ?? 5,
|
|
118
124
|
minScore: args.minScore,
|
|
@@ -126,6 +132,7 @@ export function handleSearch(
|
|
|
126
132
|
author: args.author,
|
|
127
133
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
128
134
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
135
|
+
projectAffinity,
|
|
129
136
|
};
|
|
130
137
|
let traceSession: RetrievalTraceSession | undefined;
|
|
131
138
|
try {
|
package/src/mcp/tools/vsearch.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ToolContext } from "../server";
|
|
|
12
12
|
|
|
13
13
|
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
14
14
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
15
|
+
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
15
16
|
import {
|
|
16
17
|
finishRetrievalTraceAfterError,
|
|
17
18
|
retrievalTraceFilters,
|
|
@@ -34,6 +35,7 @@ import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
|
34
35
|
|
|
35
36
|
interface VsearchInput {
|
|
36
37
|
query: string;
|
|
38
|
+
projectHints?: string[];
|
|
37
39
|
collection?: string;
|
|
38
40
|
limit?: number;
|
|
39
41
|
minScore?: number;
|
|
@@ -135,6 +137,10 @@ export function handleVsearch(
|
|
|
135
137
|
undefined,
|
|
136
138
|
args.collection
|
|
137
139
|
);
|
|
140
|
+
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
141
|
+
ctx.config,
|
|
142
|
+
args.projectHints
|
|
143
|
+
);
|
|
138
144
|
const options = {
|
|
139
145
|
limit: args.limit ?? 5,
|
|
140
146
|
minScore: args.minScore,
|
|
@@ -147,6 +153,7 @@ export function handleVsearch(
|
|
|
147
153
|
author: args.author,
|
|
148
154
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
149
155
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
156
|
+
projectAffinity,
|
|
150
157
|
};
|
|
151
158
|
let traceSession: RetrievalTraceSession | undefined;
|
|
152
159
|
const traceStart = await startRetrievalTraceRequest({
|
package/src/pipeline/diagnose.ts
CHANGED
|
@@ -19,6 +19,12 @@ import { resolveDocRef } from "../core/ref-parser";
|
|
|
19
19
|
import { err, ok } from "../store/types";
|
|
20
20
|
import { evaluateQueryTargetFilters } from "./filters";
|
|
21
21
|
import { searchHybrid } from "./hybrid";
|
|
22
|
+
import {
|
|
23
|
+
getProjectAffinityMetadata,
|
|
24
|
+
type ProjectAffinityScoringInput,
|
|
25
|
+
type ProjectAffinityScoreMetadata,
|
|
26
|
+
scoreProjectAffinity,
|
|
27
|
+
} from "./project-affinity";
|
|
22
28
|
|
|
23
29
|
export type QueryDiagnoseTargetStatus =
|
|
24
30
|
| "not_found"
|
|
@@ -46,7 +52,7 @@ export interface QueryDiagnoseStage {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export interface QueryDiagnoseResult {
|
|
49
|
-
schemaVersion: "1.0";
|
|
55
|
+
schemaVersion: "1.0" | "1.1";
|
|
50
56
|
query: string;
|
|
51
57
|
target: {
|
|
52
58
|
ref: string;
|
|
@@ -65,6 +71,7 @@ export interface QueryDiagnoseResult {
|
|
|
65
71
|
filterReasons: string[];
|
|
66
72
|
};
|
|
67
73
|
stages: QueryDiagnoseStage[];
|
|
74
|
+
affinity?: ProjectAffinityScoreMetadata;
|
|
68
75
|
chunk: {
|
|
69
76
|
seq: number | null;
|
|
70
77
|
startLine: number | null;
|
|
@@ -155,6 +162,15 @@ function findTargetCandidate(
|
|
|
155
162
|
);
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
const hasTrustedProjectAffinityInput = (
|
|
166
|
+
input: ProjectAffinityScoringInput | undefined
|
|
167
|
+
): boolean =>
|
|
168
|
+
input?.enabled !== false &&
|
|
169
|
+
Boolean(
|
|
170
|
+
input?.resolution.matches.length ||
|
|
171
|
+
input?.resolution.roots.some((root) => root.source !== "remote_hint")
|
|
172
|
+
);
|
|
173
|
+
|
|
158
174
|
export async function diagnoseQueryTarget(
|
|
159
175
|
deps: HybridSearchDeps,
|
|
160
176
|
query: string,
|
|
@@ -276,8 +292,28 @@ export async function diagnoseQueryTarget(
|
|
|
276
292
|
chunks.find((chunk) => chunk.seq === firstMatched?.seq) ??
|
|
277
293
|
chunks[0] ??
|
|
278
294
|
null;
|
|
295
|
+
const targetResult = searchResult.value.results.find(
|
|
296
|
+
(result) => result.uri === doc.uri
|
|
297
|
+
);
|
|
298
|
+
const lastMatched = trace?.stages
|
|
299
|
+
.toReversed()
|
|
300
|
+
.flatMap((stage) => stage.candidates)
|
|
301
|
+
.find(
|
|
302
|
+
(candidate) =>
|
|
303
|
+
candidate.mirrorHash === doc.mirrorHash && targetSeqs.has(candidate.seq)
|
|
304
|
+
);
|
|
305
|
+
const affinity =
|
|
306
|
+
(targetResult ? getProjectAffinityMetadata(targetResult) : undefined) ??
|
|
307
|
+
(lastMatched && options.projectAffinity
|
|
308
|
+
? scoreProjectAffinity(
|
|
309
|
+
lastMatched.score,
|
|
310
|
+
doc.collection,
|
|
311
|
+
options.projectAffinity,
|
|
312
|
+
{ kind: "hybrid_blended", score: lastMatched.score }
|
|
313
|
+
)
|
|
314
|
+
: null);
|
|
279
315
|
|
|
280
|
-
|
|
316
|
+
const baseResult: QueryDiagnoseResult = {
|
|
281
317
|
...buildBaseResult(query, options.target, "diagnosed", doc, {
|
|
282
318
|
graphHints,
|
|
283
319
|
chunkCount: chunks.length,
|
|
@@ -298,5 +334,14 @@ export async function diagnoseQueryTarget(
|
|
|
298
334
|
totalResults: searchResult.value.meta.totalResults,
|
|
299
335
|
queryModes: searchResult.value.meta.queryModes,
|
|
300
336
|
},
|
|
301
|
-
}
|
|
337
|
+
};
|
|
338
|
+
return ok(
|
|
339
|
+
affinity && hasTrustedProjectAffinityInput(options.projectAffinity)
|
|
340
|
+
? {
|
|
341
|
+
...baseResult,
|
|
342
|
+
schemaVersion: "1.1",
|
|
343
|
+
affinity,
|
|
344
|
+
}
|
|
345
|
+
: baseResult
|
|
346
|
+
);
|
|
302
347
|
}
|
package/src/pipeline/explain.ts
CHANGED
|
@@ -11,8 +11,11 @@ import type {
|
|
|
11
11
|
ExplainResult,
|
|
12
12
|
QueryModeSummary,
|
|
13
13
|
RerankedCandidate,
|
|
14
|
+
SearchResult,
|
|
14
15
|
} from "./types";
|
|
15
16
|
|
|
17
|
+
import { SEARCH_RESULT_PLANNER_METADATA } from "./types";
|
|
18
|
+
|
|
16
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
20
|
// Formatter
|
|
18
21
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -35,7 +38,8 @@ export function formatResultExplain(results: ExplainResult[]): string {
|
|
|
35
38
|
r.fusionScore !== undefined ||
|
|
36
39
|
r.bm25Score !== undefined ||
|
|
37
40
|
r.vecScore !== undefined ||
|
|
38
|
-
r.rerankScore !== undefined
|
|
41
|
+
r.rerankScore !== undefined ||
|
|
42
|
+
r.projectAffinity !== undefined
|
|
39
43
|
) {
|
|
40
44
|
msg += " (";
|
|
41
45
|
if (r.fusionScore !== undefined) {
|
|
@@ -59,6 +63,12 @@ export function formatResultExplain(results: ExplainResult[]): string {
|
|
|
59
63
|
}
|
|
60
64
|
msg += `rerank=${r.rerankScore.toFixed(2)}`;
|
|
61
65
|
}
|
|
66
|
+
if (r.projectAffinity) {
|
|
67
|
+
if (msg.at(-1) !== "(") {
|
|
68
|
+
msg += ", ";
|
|
69
|
+
}
|
|
70
|
+
msg += `raw=${r.projectAffinity.rawScoreKind}:${r.projectAffinity.rawScore.toFixed(3)}, base=${r.projectAffinity.baseScore.toFixed(3)}, affinity=${r.projectAffinity.affinityApplied.toFixed(3)}/${r.projectAffinity.affinityRequested.toFixed(3)}, auxiliary=${r.projectAffinity.combinedAuxiliaryApplied.toFixed(3)}/${r.projectAffinity.combinedAuxiliaryCap.toFixed(3)}, collection=${r.projectAffinity.collectionAlias}, root=${r.projectAffinity.rootAlias}, source=${r.projectAffinity.source}, final=${r.projectAffinity.finalScore.toFixed(3)}`;
|
|
71
|
+
}
|
|
62
72
|
msg += ")";
|
|
63
73
|
}
|
|
64
74
|
lines.push(`[explain] result ${r.rank}: ${r.docid} ${msg}`);
|
|
@@ -206,18 +216,49 @@ export function explainTimings(timings: StageTimingsInput): ExplainLine {
|
|
|
206
216
|
|
|
207
217
|
export function buildExplainResults(
|
|
208
218
|
candidates: RerankedCandidate[],
|
|
209
|
-
docidMap: Map<string, string
|
|
219
|
+
docidMap: Map<string, string>,
|
|
220
|
+
finalResults?: SearchResult[]
|
|
210
221
|
): ExplainResult[] {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
if (finalResults) {
|
|
223
|
+
return finalResults.slice(0, 20).map((result, index) => {
|
|
224
|
+
const planner = result[SEARCH_RESULT_PLANNER_METADATA];
|
|
225
|
+
const candidate = candidates.find(
|
|
226
|
+
(entry) =>
|
|
227
|
+
entry.mirrorHash === result.conversion?.mirrorHash &&
|
|
228
|
+
entry.seq === (planner?.retrievalSeq ?? planner?.seq)
|
|
229
|
+
);
|
|
230
|
+
return buildExplainResult(result.docid, result.score, index, candidate);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return candidates.slice(0, 20).map((candidate, index) => {
|
|
234
|
+
const key = `${candidate.mirrorHash}:${candidate.seq}`;
|
|
235
|
+
return buildExplainResult(
|
|
236
|
+
docidMap.get(key) ?? "#unknown",
|
|
237
|
+
candidate.blendedScore,
|
|
238
|
+
index,
|
|
239
|
+
candidate
|
|
240
|
+
);
|
|
222
241
|
});
|
|
223
242
|
}
|
|
243
|
+
|
|
244
|
+
function buildExplainResult(
|
|
245
|
+
docid: string,
|
|
246
|
+
score: number,
|
|
247
|
+
index: number,
|
|
248
|
+
candidate?: RerankedCandidate
|
|
249
|
+
): ExplainResult {
|
|
250
|
+
if (!candidate) {
|
|
251
|
+
return { rank: index + 1, docid, score };
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
rank: index + 1,
|
|
255
|
+
docid,
|
|
256
|
+
score,
|
|
257
|
+
fusionScore: candidate.fusionScore,
|
|
258
|
+
bm25Score:
|
|
259
|
+
candidate.bm25Rank !== null ? 1 / (60 + candidate.bm25Rank) : undefined,
|
|
260
|
+
vecScore:
|
|
261
|
+
candidate.vecRank !== null ? 1 / (60 + candidate.vecRank) : undefined,
|
|
262
|
+
rerankScore: candidate.rerankScore ?? undefined,
|
|
263
|
+
};
|
|
264
|
+
}
|
package/src/pipeline/hybrid.ts
CHANGED
|
@@ -41,6 +41,11 @@ import { evaluateDocumentChunkFilters } from "./filters";
|
|
|
41
41
|
import { type RankedInput, rrfFuse, toRankedInput } from "./fusion";
|
|
42
42
|
import { expandGraphCandidates } from "./graph-retrieval";
|
|
43
43
|
import { selectBestChunkForSteering } from "./intent";
|
|
44
|
+
import {
|
|
45
|
+
applyProjectAffinity,
|
|
46
|
+
getProjectAffinityMetadata,
|
|
47
|
+
hasProjectAffinity,
|
|
48
|
+
} from "./project-affinity";
|
|
44
49
|
import { detectQueryLanguage } from "./query-language";
|
|
45
50
|
import {
|
|
46
51
|
buildExpansionFromQueryModes,
|
|
@@ -301,6 +306,7 @@ export async function searchHybrid(
|
|
|
301
306
|
const runStartedAt = performance.now();
|
|
302
307
|
const { store, vectorIndex, embedPort, expandPort, rerankPort } = deps;
|
|
303
308
|
const pipelineConfig = deps.pipelineConfig ?? DEFAULT_PIPELINE_CONFIG;
|
|
309
|
+
const affinityActive = hasProjectAffinity(options.projectAffinity);
|
|
304
310
|
|
|
305
311
|
const limit = options.limit ?? 20;
|
|
306
312
|
const recencySort = shouldSortByRecency(query);
|
|
@@ -719,7 +725,7 @@ export async function searchHybrid(
|
|
|
719
725
|
// ─────────────────────────────────────────────────────────────────────────
|
|
720
726
|
const minScore = options.minScore ?? 0;
|
|
721
727
|
const filteredCandidates =
|
|
722
|
-
minScore > 0
|
|
728
|
+
minScore > 0 && !affinityActive
|
|
723
729
|
? rerankResult.candidates.filter((c) => c.blendedScore >= minScore)
|
|
724
730
|
: rerankResult.candidates;
|
|
725
731
|
|
|
@@ -875,7 +881,7 @@ export async function searchHybrid(
|
|
|
875
881
|
// Iterate until we have enough results (don't slice early - deduping may skip candidates)
|
|
876
882
|
for (const [candidateIndex, candidate] of filteredCandidates.entries()) {
|
|
877
883
|
// Stop when we have enough results
|
|
878
|
-
if (results.length >= assemblyLimit) {
|
|
884
|
+
if (!affinityActive && results.length >= assemblyLimit) {
|
|
879
885
|
break;
|
|
880
886
|
}
|
|
881
887
|
|
|
@@ -939,64 +945,76 @@ export async function searchHybrid(
|
|
|
939
945
|
}
|
|
940
946
|
|
|
941
947
|
for (const doc of candidateDocs) {
|
|
942
|
-
if (results.length >= assemblyLimit) break;
|
|
948
|
+
if (!affinityActive && results.length >= assemblyLimit) break;
|
|
943
949
|
const filterEval = evaluateDocumentChunkFilters(
|
|
944
950
|
query,
|
|
945
951
|
doc,
|
|
946
952
|
docChunks,
|
|
947
953
|
options
|
|
948
954
|
);
|
|
949
|
-
if (
|
|
955
|
+
if (
|
|
956
|
+
!filterEval.matches ||
|
|
957
|
+
(options.full && !affinityActive && seenDocids.has(doc.docid))
|
|
958
|
+
) {
|
|
950
959
|
continue;
|
|
951
960
|
}
|
|
952
961
|
const docidKey = `${candidate.mirrorHash}:${candidate.seq}`;
|
|
953
962
|
if (!docidMap.has(docidKey)) docidMap.set(docidKey, doc.docid);
|
|
954
963
|
const collectionPath = collectionPaths.get(doc.collection);
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
conversion: {
|
|
982
|
-
mirrorHash: candidate.mirrorHash,
|
|
983
|
-
converterId: doc.converterId ?? undefined,
|
|
984
|
-
converterVersion: doc.converterVersion ?? undefined,
|
|
985
|
-
},
|
|
964
|
+
if (options.full && !affinityActive) {
|
|
965
|
+
seenDocids.add(doc.docid);
|
|
966
|
+
}
|
|
967
|
+
const scoredResult = applyProjectAffinity(
|
|
968
|
+
{
|
|
969
|
+
docid: doc.docid,
|
|
970
|
+
score: candidate.blendedScore,
|
|
971
|
+
uri: doc.uri,
|
|
972
|
+
title: doc.title ?? undefined,
|
|
973
|
+
contentType: doc.contentType ?? undefined,
|
|
974
|
+
categories: doc.categories ?? undefined,
|
|
975
|
+
line: snippetChunk.startLine,
|
|
976
|
+
snippet,
|
|
977
|
+
snippetLanguage: chunk.language ?? undefined,
|
|
978
|
+
snippetRange,
|
|
979
|
+
source: {
|
|
980
|
+
relPath: doc.relPath,
|
|
981
|
+
absPath: collectionPath
|
|
982
|
+
? `${collectionPath}/${doc.relPath}`
|
|
983
|
+
: undefined,
|
|
984
|
+
mime: doc.sourceMime,
|
|
985
|
+
ext: doc.sourceExt,
|
|
986
|
+
modifiedAt: doc.sourceMtime,
|
|
987
|
+
documentDate: doc.frontmatterDate ?? undefined,
|
|
988
|
+
sizeBytes: doc.sourceSize,
|
|
989
|
+
sourceHash: doc.sourceHash,
|
|
986
990
|
},
|
|
987
|
-
{
|
|
988
|
-
retrievalRank: candidateIndex + 1,
|
|
991
|
+
conversion: {
|
|
989
992
|
mirrorHash: candidate.mirrorHash,
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
993
|
+
converterId: doc.converterId ?? undefined,
|
|
994
|
+
converterVersion: doc.converterVersion ?? undefined,
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
doc.collection,
|
|
998
|
+
options.projectAffinity,
|
|
999
|
+
{ kind: "hybrid_blended", score: candidate.blendedScore }
|
|
1000
|
+
);
|
|
1001
|
+
if (scoredResult.score < minScore) continue;
|
|
1002
|
+
results.push(
|
|
1003
|
+
attachSearchResultPlannerMetadata(scoredResult, {
|
|
1004
|
+
retrievalRank: candidateIndex + 1,
|
|
1005
|
+
mirrorHash: candidate.mirrorHash,
|
|
1006
|
+
seq: snippetChunk.seq,
|
|
1007
|
+
...(affinityActive && snippetChunk.seq !== candidate.seq
|
|
1008
|
+
? { retrievalSeq: candidate.seq }
|
|
1009
|
+
: {}),
|
|
1010
|
+
sources: [...candidate.sources].sort(),
|
|
1011
|
+
graphExpanded: candidate.sources.includes("graph"),
|
|
1012
|
+
startLine: snippetChunk.startLine,
|
|
1013
|
+
endLine: snippetChunk.endLine,
|
|
1014
|
+
passageHash: new Bun.CryptoHasher("sha256")
|
|
1015
|
+
.update(snippetChunk.text)
|
|
1016
|
+
.digest("hex"),
|
|
1017
|
+
})
|
|
1000
1018
|
);
|
|
1001
1019
|
}
|
|
1002
1020
|
}
|
|
@@ -1008,21 +1026,16 @@ export async function searchHybrid(
|
|
|
1008
1026
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1009
1027
|
// 6. Build explain data (if requested)
|
|
1010
1028
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1011
|
-
const explainData = options.explain
|
|
1012
|
-
? {
|
|
1013
|
-
lines: explainLines,
|
|
1014
|
-
results: buildExplainResults(
|
|
1015
|
-
filteredCandidates.slice(0, limit),
|
|
1016
|
-
docidMap
|
|
1017
|
-
),
|
|
1018
|
-
}
|
|
1019
|
-
: undefined;
|
|
1020
|
-
|
|
1021
1029
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1022
1030
|
// 7. Return results
|
|
1023
1031
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1032
|
+
const dedupedResults =
|
|
1033
|
+
options.full && affinityActive
|
|
1034
|
+
? dedupeFullResultsByDocid(results)
|
|
1035
|
+
: results;
|
|
1036
|
+
|
|
1024
1037
|
if (recencySort) {
|
|
1025
|
-
|
|
1038
|
+
dedupedResults.sort((a, b) => {
|
|
1026
1039
|
const aTs = resolveRecencyTimestamp(
|
|
1027
1040
|
a.source.documentDate,
|
|
1028
1041
|
a.source.modifiedAt
|
|
@@ -1036,9 +1049,26 @@ export async function searchHybrid(
|
|
|
1036
1049
|
}
|
|
1037
1050
|
return b.score - a.score;
|
|
1038
1051
|
});
|
|
1052
|
+
} else if (affinityActive) {
|
|
1053
|
+
dedupedResults.sort((a, b) => b.score - a.score);
|
|
1039
1054
|
}
|
|
1040
1055
|
|
|
1041
|
-
const finalResults =
|
|
1056
|
+
const finalResults = dedupedResults.slice(0, limit);
|
|
1057
|
+
const explainData = options.explain
|
|
1058
|
+
? {
|
|
1059
|
+
lines: explainLines,
|
|
1060
|
+
results: affinityActive
|
|
1061
|
+
? buildExplainResults(filteredCandidates, docidMap, finalResults).map(
|
|
1062
|
+
(result, index) => ({
|
|
1063
|
+
...result,
|
|
1064
|
+
projectAffinity: getProjectAffinityMetadata(
|
|
1065
|
+
finalResults[index]!
|
|
1066
|
+
),
|
|
1067
|
+
})
|
|
1068
|
+
)
|
|
1069
|
+
: buildExplainResults(filteredCandidates.slice(0, limit), docidMap),
|
|
1070
|
+
}
|
|
1071
|
+
: undefined;
|
|
1042
1072
|
await attachSearchResultContexts(store, finalResults);
|
|
1043
1073
|
|
|
1044
1074
|
const output: SearchResults = {
|
|
@@ -1143,3 +1173,14 @@ export async function searchHybrid(
|
|
|
1143
1173
|
}
|
|
1144
1174
|
return ok(output);
|
|
1145
1175
|
}
|
|
1176
|
+
|
|
1177
|
+
function dedupeFullResultsByDocid(results: SearchResult[]): SearchResult[] {
|
|
1178
|
+
const bestByDocid = new Map<string, SearchResult>();
|
|
1179
|
+
for (const result of results) {
|
|
1180
|
+
const existing = bestByDocid.get(result.docid);
|
|
1181
|
+
if (!existing || result.score > existing.score) {
|
|
1182
|
+
bestByDocid.set(result.docid, result);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return [...bestByDocid.values()];
|
|
1186
|
+
}
|