@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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded auxiliary scoring for trusted project affinity.
|
|
3
|
+
*
|
|
4
|
+
* @module src/pipeline/project-affinity
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ProjectAffinityMatch,
|
|
9
|
+
ProjectAffinityResolution,
|
|
10
|
+
} from "../core/project-affinity";
|
|
11
|
+
import type { SearchResult } from "./types";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
AUXILIARY_RANKING_MAX_CONTRIBUTION,
|
|
15
|
+
PROJECT_AFFINITY_MAX_CONTRIBUTION,
|
|
16
|
+
} from "../config/types";
|
|
17
|
+
|
|
18
|
+
export interface ProjectAffinityScoringInput {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
contribution?: number;
|
|
21
|
+
resolution: ProjectAffinityResolution;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProjectAffinityScoreMetadata {
|
|
25
|
+
affinityAdjustedScore: number;
|
|
26
|
+
affinityApplied: number;
|
|
27
|
+
affinityRequested: number;
|
|
28
|
+
affinityWeight: number;
|
|
29
|
+
baseScore: number;
|
|
30
|
+
collectionAlias: string | null;
|
|
31
|
+
combinedAuxiliaryApplied: number;
|
|
32
|
+
combinedAuxiliaryCap: number;
|
|
33
|
+
combinedAuxiliaryRequested: number;
|
|
34
|
+
finalBlendedScore: number;
|
|
35
|
+
finalScore: number;
|
|
36
|
+
matched: boolean;
|
|
37
|
+
rawScore: number;
|
|
38
|
+
rawScoreKind: "bm25" | "hybrid_blended" | "normalized" | "vector_distance";
|
|
39
|
+
rootAlias: string | null;
|
|
40
|
+
source: ProjectAffinityMatch["source"] | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const SEARCH_RESULT_AFFINITY_METADATA = Symbol(
|
|
44
|
+
"gno.searchResultAffinityMetadata"
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const clamp = (value: number, min: number, max: number): number =>
|
|
48
|
+
Math.min(max, Math.max(min, value));
|
|
49
|
+
|
|
50
|
+
export function applyAuxiliaryScore(
|
|
51
|
+
baseScore: number,
|
|
52
|
+
contributions: readonly number[]
|
|
53
|
+
): {
|
|
54
|
+
applied: number;
|
|
55
|
+
finalScore: number;
|
|
56
|
+
requested: number;
|
|
57
|
+
} {
|
|
58
|
+
const requested = [...contributions]
|
|
59
|
+
.sort((left, right) => left - right)
|
|
60
|
+
.reduce((total, contribution) => total + contribution, 0);
|
|
61
|
+
const applied = clamp(
|
|
62
|
+
requested,
|
|
63
|
+
-AUXILIARY_RANKING_MAX_CONTRIBUTION,
|
|
64
|
+
AUXILIARY_RANKING_MAX_CONTRIBUTION
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
requested,
|
|
68
|
+
applied,
|
|
69
|
+
finalScore: clamp(baseScore + applied, 0, 1),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const matchingCollection = (
|
|
74
|
+
input: ProjectAffinityScoringInput | undefined,
|
|
75
|
+
collection: string
|
|
76
|
+
): ProjectAffinityMatch | undefined => {
|
|
77
|
+
if (input?.enabled === false) return undefined;
|
|
78
|
+
return input?.resolution.matches.find(
|
|
79
|
+
(match) => match.collection === collection
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function scoreProjectAffinity(
|
|
84
|
+
baseScore: number,
|
|
85
|
+
collection: string,
|
|
86
|
+
input: ProjectAffinityScoringInput | undefined,
|
|
87
|
+
raw: {
|
|
88
|
+
kind: ProjectAffinityScoreMetadata["rawScoreKind"];
|
|
89
|
+
score: number;
|
|
90
|
+
} = { kind: "normalized", score: baseScore }
|
|
91
|
+
): ProjectAffinityScoreMetadata {
|
|
92
|
+
const match = matchingCollection(input, collection);
|
|
93
|
+
const configuredWeight = clamp(
|
|
94
|
+
input?.contribution ?? PROJECT_AFFINITY_MAX_CONTRIBUTION,
|
|
95
|
+
0,
|
|
96
|
+
PROJECT_AFFINITY_MAX_CONTRIBUTION
|
|
97
|
+
);
|
|
98
|
+
const affinityRequested = match ? configuredWeight : 0;
|
|
99
|
+
const auxiliary = applyAuxiliaryScore(baseScore, [affinityRequested]);
|
|
100
|
+
const affinityApplied = auxiliary.finalScore - baseScore;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
affinityAdjustedScore: auxiliary.finalScore,
|
|
104
|
+
affinityApplied,
|
|
105
|
+
affinityRequested,
|
|
106
|
+
affinityWeight: configuredWeight,
|
|
107
|
+
baseScore,
|
|
108
|
+
collectionAlias: match?.collectionAlias ?? null,
|
|
109
|
+
combinedAuxiliaryApplied: auxiliary.applied,
|
|
110
|
+
combinedAuxiliaryCap: AUXILIARY_RANKING_MAX_CONTRIBUTION,
|
|
111
|
+
combinedAuxiliaryRequested: auxiliary.requested,
|
|
112
|
+
finalBlendedScore: auxiliary.finalScore,
|
|
113
|
+
finalScore: auxiliary.finalScore,
|
|
114
|
+
matched: Boolean(match),
|
|
115
|
+
rawScore: raw.score,
|
|
116
|
+
rawScoreKind: raw.kind,
|
|
117
|
+
rootAlias: match?.rootAlias ?? null,
|
|
118
|
+
source: match?.source ?? null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function hasProjectAffinity(
|
|
123
|
+
input: ProjectAffinityScoringInput | undefined
|
|
124
|
+
): boolean {
|
|
125
|
+
return (
|
|
126
|
+
Boolean(input) &&
|
|
127
|
+
input?.enabled !== false &&
|
|
128
|
+
(input?.contribution ?? PROJECT_AFFINITY_MAX_CONTRIBUTION) > 0 &&
|
|
129
|
+
(input?.resolution.matches.length ?? 0) !== 0
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function applyProjectAffinity(
|
|
134
|
+
result: SearchResult,
|
|
135
|
+
collection: string,
|
|
136
|
+
input: ProjectAffinityScoringInput | undefined,
|
|
137
|
+
raw?: {
|
|
138
|
+
kind: ProjectAffinityScoreMetadata["rawScoreKind"];
|
|
139
|
+
score: number;
|
|
140
|
+
}
|
|
141
|
+
): SearchResult {
|
|
142
|
+
if (!hasProjectAffinity(input)) return result;
|
|
143
|
+
const metadata = scoreProjectAffinity(result.score, collection, input, raw);
|
|
144
|
+
result.score = metadata.finalScore;
|
|
145
|
+
Object.defineProperty(result, SEARCH_RESULT_AFFINITY_METADATA, {
|
|
146
|
+
configurable: true,
|
|
147
|
+
enumerable: false,
|
|
148
|
+
value: metadata,
|
|
149
|
+
writable: true,
|
|
150
|
+
});
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getProjectAffinityMetadata(
|
|
155
|
+
result: SearchResult
|
|
156
|
+
): ProjectAffinityScoreMetadata | undefined {
|
|
157
|
+
return (
|
|
158
|
+
result as SearchResult & {
|
|
159
|
+
[SEARCH_RESULT_AFFINITY_METADATA]?: ProjectAffinityScoreMetadata;
|
|
160
|
+
}
|
|
161
|
+
)[SEARCH_RESULT_AFFINITY_METADATA];
|
|
162
|
+
}
|
package/src/pipeline/search.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { err, ok } from "../store/types";
|
|
|
20
20
|
import { createChunkLookup } from "./chunk-lookup";
|
|
21
21
|
import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
|
|
22
22
|
import { selectBestChunkForSteering } from "./intent";
|
|
23
|
+
import { applyProjectAffinity, hasProjectAffinity } from "./project-affinity";
|
|
23
24
|
import { detectQueryLanguage } from "./query-language";
|
|
24
25
|
import { attachSearchResultContexts } from "./result-context";
|
|
25
26
|
import {
|
|
@@ -160,8 +161,9 @@ export async function searchBm25(
|
|
|
160
161
|
const traceStartedAt = options.traceSession ? performance.now() : 0;
|
|
161
162
|
const limit = options.limit ?? 20;
|
|
162
163
|
const minScore = options.minScore ?? 0;
|
|
164
|
+
const affinityActive = hasProjectAffinity(options.projectAffinity);
|
|
163
165
|
const recencySort = shouldSortByRecency(query);
|
|
164
|
-
const retrievalLimit = recencySort ? limit * 3 : limit;
|
|
166
|
+
const retrievalLimit = recencySort || affinityActive ? limit * 3 : limit;
|
|
165
167
|
const temporalRange = resolveTemporalRange(
|
|
166
168
|
query,
|
|
167
169
|
options.since,
|
|
@@ -208,6 +210,10 @@ export async function searchBm25(
|
|
|
208
210
|
|
|
209
211
|
// Build results
|
|
210
212
|
const results: SearchResult[] = [];
|
|
213
|
+
const scoringByResult = new WeakMap<
|
|
214
|
+
SearchResult,
|
|
215
|
+
{ collection: string; rawScore: number }
|
|
216
|
+
>();
|
|
211
217
|
|
|
212
218
|
// Pre-fetch all chunks in one batch query (eliminates N+1)
|
|
213
219
|
const uniqueHashes = [
|
|
@@ -229,6 +235,11 @@ export async function searchBm25(
|
|
|
229
235
|
string,
|
|
230
236
|
{ fts: FtsResult; chunk: ChunkRow | null; score: number }
|
|
231
237
|
>();
|
|
238
|
+
const fullAffinityEntries: {
|
|
239
|
+
fts: FtsResult;
|
|
240
|
+
chunk: ChunkRow | null;
|
|
241
|
+
score: number;
|
|
242
|
+
}[] = [];
|
|
232
243
|
|
|
233
244
|
for (const fts of ftsResult.value) {
|
|
234
245
|
// Dedup by uri+seq - eliminates rows from mirror_hash join fan-out
|
|
@@ -278,6 +289,10 @@ export async function searchBm25(
|
|
|
278
289
|
// For --full, de-dupe by docid (keep best scoring chunk per doc)
|
|
279
290
|
// Raw BM25: smaller (more negative) is better
|
|
280
291
|
if (options.full) {
|
|
292
|
+
if (affinityActive) {
|
|
293
|
+
fullAffinityEntries.push({ fts, chunk, score: fts.score });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
281
296
|
const docid = fts.docid ?? "";
|
|
282
297
|
const existing = bestByDocid.get(docid);
|
|
283
298
|
if (!existing || fts.score < existing.score) {
|
|
@@ -289,16 +304,22 @@ export async function searchBm25(
|
|
|
289
304
|
const collectionPath = fts.collection
|
|
290
305
|
? collectionPaths.get(fts.collection)
|
|
291
306
|
: undefined;
|
|
292
|
-
|
|
293
|
-
|
|
307
|
+
const result = buildSearchResult({ fts, chunk, collectionPath, options });
|
|
308
|
+
if (fts.collection) {
|
|
309
|
+
scoringByResult.set(result, {
|
|
310
|
+
collection: fts.collection,
|
|
311
|
+
rawScore: fts.score,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
results.push(result);
|
|
294
315
|
}
|
|
295
316
|
|
|
296
317
|
// For --full, fetch full content and build results
|
|
297
318
|
if (options.full) {
|
|
298
319
|
// Sort by raw BM25 score (smaller = better) before building results
|
|
299
|
-
const sortedEntries =
|
|
300
|
-
|
|
301
|
-
);
|
|
320
|
+
const sortedEntries = (
|
|
321
|
+
affinityActive ? fullAffinityEntries : [...bestByDocid.values()]
|
|
322
|
+
).sort((a, b) => a.score - b.score);
|
|
302
323
|
const fullContentResult = await getContentBatch(
|
|
303
324
|
store,
|
|
304
325
|
sortedEntries
|
|
@@ -317,18 +338,50 @@ export async function searchBm25(
|
|
|
317
338
|
const collectionPath = fts.collection
|
|
318
339
|
? collectionPaths.get(fts.collection)
|
|
319
340
|
: undefined;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
341
|
+
const result = buildSearchResult({
|
|
342
|
+
fts,
|
|
343
|
+
chunk,
|
|
344
|
+
collectionPath,
|
|
345
|
+
options,
|
|
346
|
+
fullContent,
|
|
347
|
+
});
|
|
348
|
+
if (fts.collection) {
|
|
349
|
+
scoringByResult.set(result, {
|
|
350
|
+
collection: fts.collection,
|
|
351
|
+
rawScore: fts.score,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
results.push(result);
|
|
323
355
|
}
|
|
324
356
|
}
|
|
325
357
|
|
|
326
358
|
// Normalize scores to 0-1 range (batch min-max)
|
|
327
359
|
normalizeBm25Scores(results);
|
|
328
360
|
|
|
361
|
+
if (affinityActive) {
|
|
362
|
+
for (const result of results) {
|
|
363
|
+
const scoring = scoringByResult.get(result);
|
|
364
|
+
if (scoring) {
|
|
365
|
+
applyProjectAffinity(
|
|
366
|
+
result,
|
|
367
|
+
scoring.collection,
|
|
368
|
+
options.projectAffinity,
|
|
369
|
+
{ kind: "bm25", score: scoring.rawScore }
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const dedupedResults =
|
|
376
|
+
options.full && affinityActive
|
|
377
|
+
? dedupeFullResultsByDocid(results)
|
|
378
|
+
: results;
|
|
379
|
+
|
|
329
380
|
// Apply minScore filter after normalization
|
|
330
381
|
const filteredResults =
|
|
331
|
-
minScore > 0
|
|
382
|
+
minScore > 0
|
|
383
|
+
? dedupedResults.filter((r) => r.score >= minScore)
|
|
384
|
+
: dedupedResults;
|
|
332
385
|
|
|
333
386
|
if (recencySort) {
|
|
334
387
|
filteredResults.sort((a, b) => {
|
|
@@ -345,6 +398,8 @@ export async function searchBm25(
|
|
|
345
398
|
}
|
|
346
399
|
return b.score - a.score;
|
|
347
400
|
});
|
|
401
|
+
} else if (affinityActive) {
|
|
402
|
+
filteredResults.sort((a, b) => b.score - a.score);
|
|
348
403
|
}
|
|
349
404
|
|
|
350
405
|
const finalResults = filteredResults.slice(0, limit);
|
|
@@ -384,3 +439,14 @@ export async function searchBm25(
|
|
|
384
439
|
}
|
|
385
440
|
return ok(output);
|
|
386
441
|
}
|
|
442
|
+
|
|
443
|
+
function dedupeFullResultsByDocid(results: SearchResult[]): SearchResult[] {
|
|
444
|
+
const bestByDocid = new Map<string, SearchResult>();
|
|
445
|
+
for (const result of results) {
|
|
446
|
+
const existing = bestByDocid.get(result.docid);
|
|
447
|
+
if (!existing || result.score > existing.score) {
|
|
448
|
+
bestByDocid.set(result.docid, result);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return [...bestByDocid.values()];
|
|
452
|
+
}
|
package/src/pipeline/types.ts
CHANGED
|
@@ -13,6 +13,10 @@ import type { RetrievalTraceSession } from "../core/retrieval-trace-session";
|
|
|
13
13
|
import type { StoreResult } from "../store/types";
|
|
14
14
|
import type { ClaimVerificationResult } from "./claim-verification";
|
|
15
15
|
import type { SemanticVerificationCapability } from "./claim-verifier";
|
|
16
|
+
import type {
|
|
17
|
+
ProjectAffinityScoreMetadata,
|
|
18
|
+
ProjectAffinityScoringInput,
|
|
19
|
+
} from "./project-affinity";
|
|
16
20
|
|
|
17
21
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
22
|
// Search Result Types
|
|
@@ -53,6 +57,8 @@ export interface SearchResultPlannerMetadata {
|
|
|
53
57
|
retrievalRank: number;
|
|
54
58
|
mirrorHash: string;
|
|
55
59
|
seq: number;
|
|
60
|
+
/** Original retrieval seq when intent steering selects another snippet. */
|
|
61
|
+
retrievalSeq?: number;
|
|
56
62
|
sources: FusionSource[];
|
|
57
63
|
graphExpanded: boolean;
|
|
58
64
|
/** Exact canonical chunk coordinates, retained even for full-content output. */
|
|
@@ -164,6 +170,8 @@ export interface SearchResults {
|
|
|
164
170
|
export interface SearchOptions {
|
|
165
171
|
/** Internal receipt seam; never serialized or included in public schemas. */
|
|
166
172
|
traceSession?: RetrievalTraceSession;
|
|
173
|
+
/** Trusted, already-resolved project affinity; never accepts raw roots. */
|
|
174
|
+
projectAffinity?: ProjectAffinityScoringInput;
|
|
167
175
|
/** Max results */
|
|
168
176
|
limit?: number;
|
|
169
177
|
/** Min score threshold (0-1) */
|
|
@@ -536,4 +544,5 @@ export interface ExplainResult {
|
|
|
536
544
|
bm25Score?: number;
|
|
537
545
|
vecScore?: number;
|
|
538
546
|
rerankScore?: number;
|
|
547
|
+
projectAffinity?: ProjectAffinityScoreMetadata;
|
|
539
548
|
}
|
package/src/pipeline/vsearch.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { createChunkLookup } from "./chunk-lookup";
|
|
|
17
17
|
import { formatQueryForEmbedding } from "./contextual";
|
|
18
18
|
import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
|
|
19
19
|
import { selectBestChunkForSteering } from "./intent";
|
|
20
|
+
import { applyProjectAffinity, hasProjectAffinity } from "./project-affinity";
|
|
20
21
|
import { detectQueryLanguage } from "./query-language";
|
|
21
22
|
import { attachSearchResultContexts } from "./result-context";
|
|
22
23
|
import {
|
|
@@ -82,8 +83,9 @@ export async function searchVectorWithEmbedding(
|
|
|
82
83
|
const { store, vectorIndex } = deps;
|
|
83
84
|
const limit = options.limit ?? 20;
|
|
84
85
|
const minScore = options.minScore ?? 0;
|
|
86
|
+
const affinityActive = hasProjectAffinity(options.projectAffinity);
|
|
85
87
|
const recencySort = shouldSortByRecency(query);
|
|
86
|
-
const retrievalLimit = recencySort ? limit * 3 : limit;
|
|
88
|
+
const retrievalLimit = recencySort || affinityActive ? limit * 3 : limit;
|
|
87
89
|
const temporalRange = resolveTemporalRange(
|
|
88
90
|
query,
|
|
89
91
|
options.since,
|
|
@@ -104,7 +106,7 @@ export async function searchVectorWithEmbedding(
|
|
|
104
106
|
queryEmbedding,
|
|
105
107
|
retrievalLimit,
|
|
106
108
|
{
|
|
107
|
-
minScore,
|
|
109
|
+
minScore: affinityActive ? undefined : minScore,
|
|
108
110
|
allowedMirrorHashes: options.retrievalScope?.allowedMirrorHashes,
|
|
109
111
|
}
|
|
110
112
|
);
|
|
@@ -126,7 +128,7 @@ export async function searchVectorWithEmbedding(
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
// Cache docs to avoid N+1 queries (filtered by collection and tags)
|
|
129
|
-
const
|
|
131
|
+
const docsByMirrorHash = await buildDocumentMap(store, {
|
|
130
132
|
collection: options.collection,
|
|
131
133
|
relPathPrefix: options.retrievalScope?.relPathPrefix,
|
|
132
134
|
tagsAll: options.tagsAll,
|
|
@@ -152,12 +154,18 @@ export async function searchVectorWithEmbedding(
|
|
|
152
154
|
// For --full, track best score per docid to de-dupe
|
|
153
155
|
const bestByDocid = new Map<
|
|
154
156
|
string,
|
|
155
|
-
{
|
|
157
|
+
{
|
|
158
|
+
doc: DocumentInfo;
|
|
159
|
+
chunk: ChunkInfo;
|
|
160
|
+
rankingScore: number;
|
|
161
|
+
rawDistance: number;
|
|
162
|
+
score: number;
|
|
163
|
+
}
|
|
156
164
|
>();
|
|
157
165
|
|
|
158
166
|
for (const vec of vecResults) {
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
167
|
+
const baseScore = normalizeVectorScore(vec.distance);
|
|
168
|
+
if (!affinityActive && baseScore < minScore) {
|
|
161
169
|
continue;
|
|
162
170
|
}
|
|
163
171
|
|
|
@@ -184,56 +192,36 @@ export async function searchVectorWithEmbedding(
|
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
// Get document (cached)
|
|
187
|
-
const
|
|
188
|
-
if (!
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const excluded =
|
|
193
|
-
matchesExcludedText(
|
|
194
|
-
[
|
|
195
|
-
doc.title ?? "",
|
|
196
|
-
doc.relPath,
|
|
197
|
-
doc.author ?? "",
|
|
198
|
-
doc.contentType ?? "",
|
|
199
|
-
...(doc.categories ?? []),
|
|
200
|
-
],
|
|
201
|
-
options.exclude
|
|
202
|
-
) ||
|
|
203
|
-
matchesExcludedChunks(
|
|
204
|
-
chunksMap.get(vec.mirrorHash) ?? [],
|
|
205
|
-
options.exclude
|
|
206
|
-
);
|
|
207
|
-
if (excluded) {
|
|
195
|
+
const matchingDocs = docsByMirrorHash.get(vec.mirrorHash);
|
|
196
|
+
if (!matchingDocs || matchingDocs.length === 0) {
|
|
208
197
|
continue;
|
|
209
198
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
199
|
+
const docs = affinityActive ? matchingDocs : [matchingDocs.at(-1)!];
|
|
200
|
+
for (const doc of docs) {
|
|
201
|
+
const collectionPath = collectionPaths.get(doc.collection);
|
|
202
|
+
const excluded =
|
|
203
|
+
matchesExcludedText(
|
|
204
|
+
[
|
|
205
|
+
doc.title ?? "",
|
|
206
|
+
doc.relPath,
|
|
207
|
+
doc.author ?? "",
|
|
208
|
+
doc.contentType ?? "",
|
|
209
|
+
...(doc.categories ?? []),
|
|
210
|
+
],
|
|
211
|
+
options.exclude
|
|
212
|
+
) ||
|
|
213
|
+
matchesExcludedChunks(
|
|
214
|
+
chunksMap.get(vec.mirrorHash) ?? [],
|
|
215
|
+
options.exclude
|
|
216
|
+
);
|
|
217
|
+
if (excluded) {
|
|
218
|
+
continue;
|
|
226
219
|
}
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
220
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
results.push(
|
|
233
|
-
attachSearchResultPlannerMetadata(
|
|
221
|
+
const scoredResult = applyProjectAffinity(
|
|
234
222
|
{
|
|
235
223
|
docid: doc.docid,
|
|
236
|
-
score,
|
|
224
|
+
score: baseScore,
|
|
237
225
|
uri: doc.uri,
|
|
238
226
|
title: doc.title ?? undefined,
|
|
239
227
|
contentType: doc.contentType ?? undefined,
|
|
@@ -265,7 +253,35 @@ export async function searchVectorWithEmbedding(
|
|
|
265
253
|
}
|
|
266
254
|
: undefined,
|
|
267
255
|
},
|
|
268
|
-
|
|
256
|
+
doc.collection,
|
|
257
|
+
options.projectAffinity,
|
|
258
|
+
{ kind: "vector_distance", score: vec.distance }
|
|
259
|
+
);
|
|
260
|
+
if (scoredResult.score < minScore) continue;
|
|
261
|
+
|
|
262
|
+
// For --full, de-dupe by docid (keep best scoring chunk per doc)
|
|
263
|
+
if (options.full) {
|
|
264
|
+
const existing = bestByDocid.get(doc.docid);
|
|
265
|
+
if (!existing || scoredResult.score > existing.rankingScore) {
|
|
266
|
+
bestByDocid.set(doc.docid, {
|
|
267
|
+
doc,
|
|
268
|
+
chunk: {
|
|
269
|
+
text: chunk.text,
|
|
270
|
+
language: chunk.language,
|
|
271
|
+
startLine: chunk.startLine,
|
|
272
|
+
endLine: chunk.endLine,
|
|
273
|
+
seq: chunk.seq,
|
|
274
|
+
},
|
|
275
|
+
rankingScore: scoredResult.score,
|
|
276
|
+
rawDistance: vec.distance,
|
|
277
|
+
score: baseScore,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
results.push(
|
|
284
|
+
attachSearchResultPlannerMetadata(scoredResult, {
|
|
269
285
|
retrievalRank: 0,
|
|
270
286
|
mirrorHash: vec.mirrorHash,
|
|
271
287
|
seq: chunk.seq,
|
|
@@ -276,9 +292,9 @@ export async function searchVectorWithEmbedding(
|
|
|
276
292
|
passageHash: new Bun.CryptoHasher("sha256")
|
|
277
293
|
.update(chunk.text)
|
|
278
294
|
.digest("hex"),
|
|
279
|
-
}
|
|
280
|
-
)
|
|
281
|
-
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
}
|
|
282
298
|
}
|
|
283
299
|
|
|
284
300
|
// For --full, fetch full content and build results
|
|
@@ -294,47 +310,52 @@ export async function searchVectorWithEmbedding(
|
|
|
294
310
|
}
|
|
295
311
|
const fullContentByHash = fullContentResult.value;
|
|
296
312
|
|
|
297
|
-
for (const { doc, chunk, score } of bestByDocid.values()) {
|
|
313
|
+
for (const { doc, chunk, rawDistance, score } of bestByDocid.values()) {
|
|
298
314
|
const fullContent = doc.mirrorHash
|
|
299
315
|
? fullContentByHash.get(doc.mirrorHash)
|
|
300
316
|
: undefined;
|
|
301
317
|
|
|
302
318
|
const collectionPath = collectionPaths.get(doc.collection);
|
|
303
319
|
|
|
304
|
-
const result
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
const result = applyProjectAffinity(
|
|
321
|
+
{
|
|
322
|
+
docid: doc.docid,
|
|
323
|
+
score,
|
|
324
|
+
uri: doc.uri,
|
|
325
|
+
title: doc.title ?? undefined,
|
|
326
|
+
contentType: doc.contentType ?? undefined,
|
|
327
|
+
categories: doc.categories ?? undefined,
|
|
328
|
+
line: chunk.startLine,
|
|
329
|
+
snippet: fullContent ?? chunk.text,
|
|
330
|
+
snippetLanguage: chunk.language ?? undefined,
|
|
331
|
+
// --full: no snippetRange (full doc content)
|
|
332
|
+
snippetRange: fullContent
|
|
333
|
+
? undefined
|
|
334
|
+
: { startLine: chunk.startLine, endLine: chunk.endLine },
|
|
335
|
+
source: {
|
|
336
|
+
relPath: doc.relPath,
|
|
337
|
+
absPath: collectionPath
|
|
338
|
+
? `${collectionPath}/${doc.relPath}`
|
|
339
|
+
: undefined,
|
|
340
|
+
mime: doc.sourceMime,
|
|
341
|
+
ext: doc.sourceExt,
|
|
342
|
+
modifiedAt: doc.sourceMtime,
|
|
343
|
+
documentDate: doc.frontmatterDate ?? undefined,
|
|
344
|
+
sizeBytes: doc.sourceSize,
|
|
345
|
+
sourceHash: doc.sourceHash,
|
|
346
|
+
},
|
|
347
|
+
conversion: doc.mirrorHash
|
|
348
|
+
? {
|
|
349
|
+
mirrorHash: doc.mirrorHash,
|
|
350
|
+
converterId: doc.converterId ?? undefined,
|
|
351
|
+
converterVersion: doc.converterVersion ?? undefined,
|
|
352
|
+
}
|
|
322
353
|
: undefined,
|
|
323
|
-
mime: doc.sourceMime,
|
|
324
|
-
ext: doc.sourceExt,
|
|
325
|
-
modifiedAt: doc.sourceMtime,
|
|
326
|
-
documentDate: doc.frontmatterDate ?? undefined,
|
|
327
|
-
sizeBytes: doc.sourceSize,
|
|
328
|
-
sourceHash: doc.sourceHash,
|
|
329
354
|
},
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
converterVersion: doc.converterVersion ?? undefined,
|
|
335
|
-
}
|
|
336
|
-
: undefined,
|
|
337
|
-
};
|
|
355
|
+
doc.collection,
|
|
356
|
+
options.projectAffinity,
|
|
357
|
+
{ kind: "vector_distance", score: rawDistance }
|
|
358
|
+
);
|
|
338
359
|
results.push(
|
|
339
360
|
doc.mirrorHash
|
|
340
361
|
? attachSearchResultPlannerMetadata(result, {
|
|
@@ -369,6 +390,8 @@ export async function searchVectorWithEmbedding(
|
|
|
369
390
|
}
|
|
370
391
|
return b.score - a.score;
|
|
371
392
|
});
|
|
393
|
+
} else if (affinityActive) {
|
|
394
|
+
results.sort((a, b) => b.score - a.score);
|
|
372
395
|
}
|
|
373
396
|
|
|
374
397
|
const finalResults = results.slice(0, limit);
|
|
@@ -507,8 +530,8 @@ function matchesCategoryFilter(
|
|
|
507
530
|
async function buildDocumentMap(
|
|
508
531
|
store: StorePort,
|
|
509
532
|
options: DocumentMapOptions = {}
|
|
510
|
-
): Promise<Map<string, DocumentInfo>> {
|
|
511
|
-
const result = new Map<string, DocumentInfo>();
|
|
533
|
+
): Promise<Map<string, DocumentInfo[]>> {
|
|
534
|
+
const result = new Map<string, DocumentInfo[]>();
|
|
512
535
|
|
|
513
536
|
if (options.mirrorHashes && options.mirrorHashes.length === 0) {
|
|
514
537
|
return result;
|
|
@@ -594,7 +617,7 @@ async function buildDocumentMap(
|
|
|
594
617
|
continue;
|
|
595
618
|
}
|
|
596
619
|
|
|
597
|
-
|
|
620
|
+
const documentInfo: DocumentInfo = {
|
|
598
621
|
docid: doc.docid,
|
|
599
622
|
uri: doc.uri,
|
|
600
623
|
title: doc.title,
|
|
@@ -612,7 +635,10 @@ async function buildDocumentMap(
|
|
|
612
635
|
mirrorHash: doc.mirrorHash,
|
|
613
636
|
converterId: doc.converterId,
|
|
614
637
|
converterVersion: doc.converterVersion,
|
|
615
|
-
}
|
|
638
|
+
};
|
|
639
|
+
const matchingDocuments = result.get(doc.mirrorHash!) ?? [];
|
|
640
|
+
matchingDocuments.push(documentInfo);
|
|
641
|
+
result.set(doc.mirrorHash!, matchingDocuments);
|
|
616
642
|
}
|
|
617
643
|
|
|
618
644
|
return result;
|