@gmickel/gno 1.25.1 → 1.27.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 +12 -5
- package/assets/skill/SKILL.md +37 -17
- package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.27.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +161 -6
- package/spec/db/schema.sql +1 -1
- package/spec/evals-agentic.md +17 -0
- package/spec/mcp.md +25 -6
- package/spec/output-schemas/ask.schema.json +3 -0
- package/spec/output-schemas/project-profile-apply.schema.json +209 -0
- package/spec/output-schemas/project-profile-command.schema.json +160 -0
- package/spec/output-schemas/query-diagnose.schema.json +68 -5
- package/spec/output-schemas/search-results.schema.json +87 -1
- package/spec/output-schemas/setup-profile-result.schema.json +87 -0
- package/spec/output-schemas/status.schema.json +24 -0
- package/spec/project-profile.schema.json +303 -0
- package/src/app/context-runtime-contract.ts +4 -1
- package/src/app/context-runtime-types.ts +2 -0
- package/src/app/context-runtime.ts +26 -0
- package/src/app/verified-ask.ts +6 -1
- package/src/cli/commands/ask.ts +8 -1
- package/src/cli/commands/collection/add.ts +39 -45
- package/src/cli/commands/collection/remove.ts +28 -28
- package/src/cli/commands/collection/rename.ts +55 -73
- package/src/cli/commands/context/add.ts +37 -26
- package/src/cli/commands/context/rm.ts +47 -20
- package/src/cli/commands/init.ts +55 -125
- package/src/cli/commands/models/use.ts +43 -38
- package/src/cli/commands/profile-apply.ts +334 -0
- package/src/cli/commands/profile.ts +409 -0
- package/src/cli/commands/query.ts +6 -3
- package/src/cli/commands/search.ts +6 -1
- package/src/cli/commands/setup-activation.ts +205 -54
- package/src/cli/commands/setup-profile.ts +223 -0
- package/src/cli/commands/setup.ts +3 -0
- package/src/cli/commands/status.ts +43 -7
- package/src/cli/program.ts +112 -9
- package/src/config/content-types.ts +82 -0
- package/src/config/index.ts +11 -0
- package/src/config/project-profile.ts +374 -0
- package/src/config/saver.ts +16 -7
- package/src/config/types.ts +53 -2
- package/src/core/config-mutation.ts +138 -76
- package/src/core/config-write-lock.ts +89 -0
- package/src/core/context-compiler.ts +38 -1
- package/src/core/context-identity.ts +16 -0
- package/src/core/context-resolver.ts +2 -12
- package/src/core/folder-setup-planning.ts +6 -21
- package/src/core/folder-setup.ts +30 -2
- package/src/core/path-rules.ts +53 -0
- package/src/core/project-affinity-surface.ts +102 -7
- package/src/core/project-profile-apply-state.ts +268 -0
- package/src/core/project-profile-apply-validation.ts +95 -0
- package/src/core/project-profile-apply.ts +408 -0
- package/src/core/project-profile-canonical.ts +71 -0
- package/src/core/project-profile-diff.ts +302 -0
- package/src/core/project-profile-discovery.ts +519 -0
- package/src/core/project-profile-file.ts +37 -0
- package/src/core/project-profile-parser.ts +98 -0
- package/src/core/project-profile.ts +490 -0
- package/src/core/retrieval-replay-candidate.ts +6 -1
- package/src/ingestion/sync-options.ts +6 -2
- package/src/ingestion/sync.ts +21 -29
- package/src/ingestion/types.ts +1 -1
- package/src/ingestion/walker.ts +85 -44
- package/src/llm/cache.ts +21 -0
- package/src/mcp/tools/ask.ts +1 -0
- package/src/mcp/tools/index.ts +4 -0
- package/src/mcp/tools/query.ts +4 -2
- package/src/mcp/tools/search.ts +3 -0
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/content-type-boost.ts +264 -0
- package/src/pipeline/diagnose.ts +46 -19
- package/src/pipeline/explain.ts +15 -2
- package/src/pipeline/hybrid.ts +170 -74
- package/src/pipeline/rerank.ts +45 -15
- package/src/pipeline/search.ts +29 -11
- package/src/pipeline/types.ts +13 -4
- package/src/pipeline/vsearch.ts +30 -10
- package/src/sdk/client.ts +19 -3
- package/src/sdk/index.ts +1 -0
- package/src/sdk/types.ts +21 -5
- package/src/serve/config-sync.ts +2 -2
- package/src/serve/resident-runtime.ts +1 -0
- package/src/serve/routes/api.ts +18 -3
- package/src/serve/status-model.ts +2 -0
- package/src/serve/status.ts +4 -0
- package/src/store/migrations/021-multi-context-identity.ts +37 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +86 -0
- package/src/store/types.ts +3 -2
- package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +0 -1
package/src/ingestion/walker.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import type { SkippedEntry, WalkConfig, WalkEntry, WalkerPort } from "./types";
|
|
21
21
|
|
|
22
22
|
import { SUPPORTED_EXTENSIONS } from "../converters/mime";
|
|
23
|
+
import { matchesCollectionExclusion } from "../core/path-rules";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Regex to detect dangerous patterns with parent directory traversal.
|
|
@@ -51,6 +52,70 @@ function validatePattern(pattern: string): string | null {
|
|
|
51
52
|
return null;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Split GNO's canonical whole-pattern union into independently scannable Bun
|
|
57
|
+
* globs. Bun.Glob.match() accepts a leading `{a,b}` union, but scan() does not.
|
|
58
|
+
* Nested braces remain part of each child glob; escaped outer commas become
|
|
59
|
+
* literal commas again before scanning.
|
|
60
|
+
*/
|
|
61
|
+
function scanPatterns(pattern: string): string[] {
|
|
62
|
+
if (!(pattern.startsWith("{") && pattern.endsWith("}"))) return [pattern];
|
|
63
|
+
|
|
64
|
+
const patterns: string[] = [];
|
|
65
|
+
let branch = "";
|
|
66
|
+
let depth = 0;
|
|
67
|
+
let bracketDepth = 0;
|
|
68
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
69
|
+
const character = pattern[index];
|
|
70
|
+
if (
|
|
71
|
+
character === "\\" &&
|
|
72
|
+
pattern[index + 1] === "," &&
|
|
73
|
+
depth === 1 &&
|
|
74
|
+
bracketDepth === 0
|
|
75
|
+
) {
|
|
76
|
+
branch += ",";
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (character === "[" && bracketDepth === 0) {
|
|
81
|
+
bracketDepth = 1;
|
|
82
|
+
branch += character;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (character === "]" && bracketDepth > 0) {
|
|
86
|
+
bracketDepth = 0;
|
|
87
|
+
branch += character;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (bracketDepth > 0) {
|
|
91
|
+
branch += character;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (character === "{") {
|
|
95
|
+
depth += 1;
|
|
96
|
+
if (depth > 1) branch += character;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (character === "}") {
|
|
100
|
+
depth -= 1;
|
|
101
|
+
if (depth < 0 || (depth === 0 && index !== pattern.length - 1)) {
|
|
102
|
+
return [pattern];
|
|
103
|
+
}
|
|
104
|
+
if (depth > 0) branch += character;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (character === "," && depth === 1) {
|
|
108
|
+
patterns.push(branch);
|
|
109
|
+
branch = "";
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
branch += character;
|
|
113
|
+
}
|
|
114
|
+
if (depth !== 0 || bracketDepth !== 0) return [pattern];
|
|
115
|
+
patterns.push(branch);
|
|
116
|
+
return patterns;
|
|
117
|
+
}
|
|
118
|
+
|
|
54
119
|
/**
|
|
55
120
|
* Compute safe relative path from root to file.
|
|
56
121
|
* Returns null if file is outside root (security check).
|
|
@@ -77,36 +142,6 @@ async function safeRelPath(
|
|
|
77
142
|
}
|
|
78
143
|
}
|
|
79
144
|
|
|
80
|
-
/**
|
|
81
|
-
* Check if a path matches any exclude pattern.
|
|
82
|
-
*
|
|
83
|
-
* Exclude semantics (component-based matching):
|
|
84
|
-
* - Patterns match against path components (directory/file names)
|
|
85
|
-
* - "node_modules" matches any path containing "node_modules" as a component
|
|
86
|
-
* - ".git" matches ".git" directory at any level
|
|
87
|
-
* - Patterns are NOT globs - they match exact component names
|
|
88
|
-
*
|
|
89
|
-
* Examples:
|
|
90
|
-
* - exclude: [".git"] matches "foo/.git/bar" but not "foo/.github/..."
|
|
91
|
-
* - exclude: ["dist"] matches "dist/bundle.js" and "src/dist/output.js"
|
|
92
|
-
*/
|
|
93
|
-
function matchesExclude(relPath: string, excludes: string[]): boolean {
|
|
94
|
-
const parts = relPath.split("/");
|
|
95
|
-
|
|
96
|
-
for (const pattern of excludes) {
|
|
97
|
-
// Check if any path component matches exactly
|
|
98
|
-
if (parts.includes(pattern)) {
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
// Check if path starts with pattern
|
|
102
|
-
if (relPath.startsWith(`${pattern}/`)) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
145
|
/**
|
|
111
146
|
* Check if a file extension matches the include list.
|
|
112
147
|
* Include list contains extensions like ".md" or "md" (normalized).
|
|
@@ -145,10 +180,12 @@ export class FileWalker implements WalkerPort {
|
|
|
145
180
|
const entries: WalkEntry[] = [];
|
|
146
181
|
const skipped: SkippedEntry[] = [];
|
|
147
182
|
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
183
|
+
const patterns = scanPatterns(config.pattern);
|
|
184
|
+
for (const pattern of patterns) {
|
|
185
|
+
const patternError = validatePattern(pattern);
|
|
186
|
+
if (patternError) {
|
|
187
|
+
throw new Error(`Invalid glob pattern: ${patternError}`);
|
|
188
|
+
}
|
|
152
189
|
}
|
|
153
190
|
|
|
154
191
|
// Resolve root to real path for consistent comparison
|
|
@@ -161,16 +198,20 @@ export class FileWalker implements WalkerPort {
|
|
|
161
198
|
return { entries: [], skipped: [] };
|
|
162
199
|
}
|
|
163
200
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
201
|
+
const matches = new Set<string>();
|
|
202
|
+
for (const pattern of patterns) {
|
|
203
|
+
const glob = new Bun.Glob(pattern);
|
|
204
|
+
for await (const match of glob.scan({
|
|
205
|
+
cwd: rootReal,
|
|
206
|
+
absolute: true,
|
|
207
|
+
onlyFiles: true,
|
|
208
|
+
followSymlinks: false,
|
|
209
|
+
})) {
|
|
210
|
+
matches.add(normalizePath(match));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
173
213
|
|
|
214
|
+
for (const absPath of [...matches].sort()) {
|
|
174
215
|
// Security: Compute safe relative path (validates file is within root)
|
|
175
216
|
const relPath = await safeRelPath(rootReal, absPath);
|
|
176
217
|
if (relPath === null) {
|
|
@@ -179,7 +220,7 @@ export class FileWalker implements WalkerPort {
|
|
|
179
220
|
}
|
|
180
221
|
|
|
181
222
|
// Check exclude patterns
|
|
182
|
-
if (
|
|
223
|
+
if (matchesCollectionExclusion(relPath, config.exclude)) {
|
|
183
224
|
skipped.push({
|
|
184
225
|
absPath,
|
|
185
226
|
relPath,
|
package/src/llm/cache.ts
CHANGED
|
@@ -527,6 +527,27 @@ export class ModelCache {
|
|
|
527
527
|
return cached !== null;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Read-only availability probe for inspection commands.
|
|
532
|
+
*
|
|
533
|
+
* Unlike isCached(), this never repairs manifest entries or removes invalid
|
|
534
|
+
* files. It is safe for commands whose contract forbids local mutation.
|
|
535
|
+
*/
|
|
536
|
+
async isCachedReadOnly(uri: string): Promise<boolean> {
|
|
537
|
+
const parsed = parseModelUri(uri);
|
|
538
|
+
if (!parsed.ok) return false;
|
|
539
|
+
if (parsed.value.scheme === "file") {
|
|
540
|
+
const validation = await validateGgufFile(parsed.value.file, uri, "user");
|
|
541
|
+
return validation.ok;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const manifest = await this.readManifestFromDisk();
|
|
545
|
+
const entry = manifest.models.find((model) => model.uri === uri);
|
|
546
|
+
if (!entry || !(await this.fileExists(entry.path))) return false;
|
|
547
|
+
const validation = await validateGgufFile(entry.path, uri, "cache");
|
|
548
|
+
return validation.ok;
|
|
549
|
+
}
|
|
550
|
+
|
|
530
551
|
/**
|
|
531
552
|
* Get cached/available path for a URI.
|
|
532
553
|
* For file: URIs, returns path if file exists.
|
package/src/mcp/tools/ask.ts
CHANGED
|
@@ -50,6 +50,7 @@ export const askInputSchema = z
|
|
|
50
50
|
graph: z.boolean().optional(),
|
|
51
51
|
noGraph: z.boolean().optional(),
|
|
52
52
|
noRerank: z.boolean().optional(),
|
|
53
|
+
explain: z.boolean().optional(),
|
|
53
54
|
maxAnswerTokens: z.number().int().positive().optional(),
|
|
54
55
|
contextBudgetTokens: z.number().int().positive().optional(),
|
|
55
56
|
contextBudgetBytes: z.number().int().positive().optional(),
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -562,6 +562,10 @@ export const queryInputSchema = z.object({
|
|
|
562
562
|
.boolean()
|
|
563
563
|
.optional()
|
|
564
564
|
.describe("Enable bounded one-hop graph neighbor expansion"),
|
|
565
|
+
explain: z
|
|
566
|
+
.boolean()
|
|
567
|
+
.optional()
|
|
568
|
+
.describe("Include deterministic stage and per-result scoring metadata"),
|
|
565
569
|
tagsAll: z.array(z.string()).optional().describe("Require ALL of these tags"),
|
|
566
570
|
tagsAny: z.array(z.string()).optional().describe("Require ANY of these tags"),
|
|
567
571
|
});
|
package/src/mcp/tools/query.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { ToolContext } from "../server";
|
|
|
22
22
|
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
23
23
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
24
24
|
import {
|
|
25
|
-
|
|
25
|
+
fingerprintContentTypeMetadataRules,
|
|
26
26
|
normalizeContentTypes,
|
|
27
27
|
} from "../../config";
|
|
28
28
|
import { resolveDepthPolicy } from "../../core/depth-policy";
|
|
@@ -71,6 +71,7 @@ interface QueryInput {
|
|
|
71
71
|
graph?: boolean;
|
|
72
72
|
tagsAll?: string[];
|
|
73
73
|
tagsAny?: string[];
|
|
74
|
+
explain?: boolean;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
interface QueryDiagnoseInput extends QueryInput {
|
|
@@ -260,6 +261,7 @@ export function handleQuery(
|
|
|
260
261
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
261
262
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
262
263
|
projectAffinity,
|
|
264
|
+
explain: args.explain,
|
|
263
265
|
};
|
|
264
266
|
|
|
265
267
|
try {
|
|
@@ -538,7 +540,7 @@ export function handleQueryDiagnose(
|
|
|
538
540
|
projectAffinity,
|
|
539
541
|
contentTypeRules,
|
|
540
542
|
contentTypeRulesFingerprint:
|
|
541
|
-
|
|
543
|
+
fingerprintContentTypeMetadataRules(contentTypeRules),
|
|
542
544
|
});
|
|
543
545
|
|
|
544
546
|
if (!result.ok) {
|
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 { normalizeContentTypes } from "../../config";
|
|
14
15
|
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
15
16
|
import {
|
|
16
17
|
finishRetrievalTraceAfterError,
|
|
@@ -133,6 +134,8 @@ export function handleSearch(
|
|
|
133
134
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
134
135
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
135
136
|
projectAffinity,
|
|
137
|
+
contentTypeRules: normalizeContentTypes(ctx.config.contentTypes ?? [])
|
|
138
|
+
.rules,
|
|
136
139
|
};
|
|
137
140
|
let traceSession: RetrievalTraceSession | undefined;
|
|
138
141
|
try {
|
package/src/mcp/tools/status.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { IndexStatus } from "../../store/types";
|
|
8
8
|
import type { ToolContext } from "../server";
|
|
9
9
|
|
|
10
|
+
import { buildContentTypeBoostStatus } from "../../config/content-types";
|
|
10
11
|
import { resolveModelUri } from "../../llm/registry";
|
|
11
12
|
import { createStandaloneResidentStatus } from "../../serve/resident-status";
|
|
12
13
|
import { runTool, type ToolResult } from "./index";
|
|
@@ -87,6 +88,9 @@ export function handleStatus(
|
|
|
87
88
|
return {
|
|
88
89
|
...result.value,
|
|
89
90
|
configPath: ctx.actualConfigPath,
|
|
91
|
+
contentTypeBoost: buildContentTypeBoostStatus(
|
|
92
|
+
ctx.config.contentTypes ?? []
|
|
93
|
+
),
|
|
90
94
|
resident:
|
|
91
95
|
ctx.getResidentStatus?.() ?? createStandaloneResidentStatus("stdio"),
|
|
92
96
|
};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded content-type scoring composed with trusted project affinity.
|
|
3
|
+
*
|
|
4
|
+
* @module src/pipeline/content-type-boost
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NormalizedContentTypeRule } from "../config/content-types";
|
|
8
|
+
import type { ProjectAffinityScoringInput } from "./project-affinity";
|
|
9
|
+
import type { SearchResult } from "./types";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
fingerprintContentTypeRules,
|
|
13
|
+
resolveContentTypeRule,
|
|
14
|
+
} from "../config/content-types";
|
|
15
|
+
import {
|
|
16
|
+
CONTENT_TYPE_SEARCH_BOOST_MAX,
|
|
17
|
+
CONTENT_TYPE_SEARCH_BOOST_MIN,
|
|
18
|
+
CONTENT_TYPE_SEARCH_BOOST_NEUTRAL,
|
|
19
|
+
} from "../config/types";
|
|
20
|
+
import {
|
|
21
|
+
applyAuxiliaryScore,
|
|
22
|
+
hasProjectAffinity,
|
|
23
|
+
SEARCH_RESULT_AFFINITY_METADATA,
|
|
24
|
+
scoreProjectAffinity,
|
|
25
|
+
type ProjectAffinityScoreMetadata,
|
|
26
|
+
} from "./project-affinity";
|
|
27
|
+
|
|
28
|
+
export const CONTENT_TYPE_MAX_CONTRIBUTION = 0.05;
|
|
29
|
+
|
|
30
|
+
export interface ContentTypeBoostScoreMetadata {
|
|
31
|
+
baseScore: number;
|
|
32
|
+
cappedContribution: number;
|
|
33
|
+
combinedAuxiliaryApplied: number;
|
|
34
|
+
combinedAuxiliaryCap: number;
|
|
35
|
+
combinedAuxiliaryRequested: number;
|
|
36
|
+
configuredFactor: number;
|
|
37
|
+
contentType: string;
|
|
38
|
+
finalScore: number;
|
|
39
|
+
rawContribution: number;
|
|
40
|
+
rawScore: number;
|
|
41
|
+
rawScoreKind: ProjectAffinityScoreMetadata["rawScoreKind"];
|
|
42
|
+
ruleSource: "configured-id" | "prefix";
|
|
43
|
+
rulesFingerprint: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AuxiliaryScoreResult {
|
|
47
|
+
contentTypeBoost?: ContentTypeBoostScoreMetadata;
|
|
48
|
+
projectAffinity: ProjectAffinityScoreMetadata;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const SEARCH_RESULT_CONTENT_TYPE_BOOST_METADATA = Symbol(
|
|
52
|
+
"gno.searchResultContentTypeBoostMetadata"
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const clamp = (value: number, min: number, max: number): number =>
|
|
56
|
+
Math.min(max, Math.max(min, value));
|
|
57
|
+
|
|
58
|
+
const rankingFingerprints = new WeakMap<
|
|
59
|
+
readonly NormalizedContentTypeRule[],
|
|
60
|
+
string
|
|
61
|
+
>();
|
|
62
|
+
|
|
63
|
+
const rankingFingerprint = (
|
|
64
|
+
rules: readonly NormalizedContentTypeRule[] | undefined
|
|
65
|
+
): string => {
|
|
66
|
+
if (!rules) return fingerprintContentTypeRules([]);
|
|
67
|
+
const cached = rankingFingerprints.get(rules);
|
|
68
|
+
if (cached) return cached;
|
|
69
|
+
const fingerprint = fingerprintContentTypeRules([...rules]);
|
|
70
|
+
rankingFingerprints.set(rules, fingerprint);
|
|
71
|
+
return fingerprint;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Map the supported factor range continuously onto the contribution range. */
|
|
75
|
+
export function contentTypeBoostContribution(factor: number): {
|
|
76
|
+
raw: number;
|
|
77
|
+
capped: number;
|
|
78
|
+
} {
|
|
79
|
+
const raw =
|
|
80
|
+
factor >= CONTENT_TYPE_SEARCH_BOOST_NEUTRAL
|
|
81
|
+
? ((factor - CONTENT_TYPE_SEARCH_BOOST_NEUTRAL) /
|
|
82
|
+
(CONTENT_TYPE_SEARCH_BOOST_MAX - CONTENT_TYPE_SEARCH_BOOST_NEUTRAL)) *
|
|
83
|
+
CONTENT_TYPE_MAX_CONTRIBUTION
|
|
84
|
+
: ((factor - CONTENT_TYPE_SEARCH_BOOST_NEUTRAL) /
|
|
85
|
+
(CONTENT_TYPE_SEARCH_BOOST_NEUTRAL - CONTENT_TYPE_SEARCH_BOOST_MIN)) *
|
|
86
|
+
CONTENT_TYPE_MAX_CONTRIBUTION;
|
|
87
|
+
return {
|
|
88
|
+
raw,
|
|
89
|
+
capped: clamp(
|
|
90
|
+
raw,
|
|
91
|
+
-CONTENT_TYPE_MAX_CONTRIBUTION,
|
|
92
|
+
CONTENT_TYPE_MAX_CONTRIBUTION
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function hasContentTypeBoost(
|
|
98
|
+
rules: readonly NormalizedContentTypeRule[] | undefined
|
|
99
|
+
): boolean {
|
|
100
|
+
return Boolean(
|
|
101
|
+
rules?.some(
|
|
102
|
+
(rule) => rule.searchBoost !== CONTENT_TYPE_SEARCH_BOOST_NEUTRAL
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function hasAuxiliaryRanking(
|
|
108
|
+
projectAffinity: ProjectAffinityScoringInput | undefined,
|
|
109
|
+
rules: readonly NormalizedContentTypeRule[] | undefined
|
|
110
|
+
): boolean {
|
|
111
|
+
return hasProjectAffinity(projectAffinity) || hasContentTypeBoost(rules);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function scoreContentTypeBoost(
|
|
115
|
+
baseScore: number,
|
|
116
|
+
contentType: string | undefined,
|
|
117
|
+
contentTypeSource: string | null | undefined,
|
|
118
|
+
relativePath: string,
|
|
119
|
+
collection: string,
|
|
120
|
+
rules: readonly NormalizedContentTypeRule[] | undefined,
|
|
121
|
+
projectAffinity: ProjectAffinityScoringInput | undefined,
|
|
122
|
+
raw: {
|
|
123
|
+
kind: ProjectAffinityScoreMetadata["rawScoreKind"];
|
|
124
|
+
score: number;
|
|
125
|
+
} = { kind: "normalized", score: baseScore }
|
|
126
|
+
): AuxiliaryScoreResult {
|
|
127
|
+
const configuredId =
|
|
128
|
+
contentTypeSource === "frontmatter-type" ||
|
|
129
|
+
contentTypeSource === "frontmatter"
|
|
130
|
+
? contentType
|
|
131
|
+
: undefined;
|
|
132
|
+
const resolution = resolveContentTypeRule(
|
|
133
|
+
configuredId,
|
|
134
|
+
relativePath,
|
|
135
|
+
rules ? [...rules] : []
|
|
136
|
+
);
|
|
137
|
+
const factor =
|
|
138
|
+
resolution?.rule.searchBoost ?? CONTENT_TYPE_SEARCH_BOOST_NEUTRAL;
|
|
139
|
+
const contribution = contentTypeBoostContribution(factor);
|
|
140
|
+
const projectScore = scoreProjectAffinity(
|
|
141
|
+
baseScore,
|
|
142
|
+
collection,
|
|
143
|
+
projectAffinity,
|
|
144
|
+
raw
|
|
145
|
+
);
|
|
146
|
+
const combined = applyAuxiliaryScore(baseScore, [
|
|
147
|
+
projectScore.affinityRequested,
|
|
148
|
+
contribution.capped,
|
|
149
|
+
]);
|
|
150
|
+
const compositeProjectScore: ProjectAffinityScoreMetadata = {
|
|
151
|
+
...projectScore,
|
|
152
|
+
combinedAuxiliaryApplied: combined.applied,
|
|
153
|
+
combinedAuxiliaryRequested: combined.requested,
|
|
154
|
+
finalBlendedScore: combined.finalScore,
|
|
155
|
+
finalScore: combined.finalScore,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (!resolution || factor === CONTENT_TYPE_SEARCH_BOOST_NEUTRAL) {
|
|
159
|
+
return { projectAffinity: compositeProjectScore };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
projectAffinity: compositeProjectScore,
|
|
164
|
+
contentTypeBoost: {
|
|
165
|
+
baseScore,
|
|
166
|
+
cappedContribution: contribution.capped,
|
|
167
|
+
combinedAuxiliaryApplied: compositeProjectScore.combinedAuxiliaryApplied,
|
|
168
|
+
combinedAuxiliaryCap: compositeProjectScore.combinedAuxiliaryCap,
|
|
169
|
+
combinedAuxiliaryRequested:
|
|
170
|
+
compositeProjectScore.combinedAuxiliaryRequested,
|
|
171
|
+
configuredFactor: factor,
|
|
172
|
+
contentType: resolution.rule.id,
|
|
173
|
+
finalScore: compositeProjectScore.finalScore,
|
|
174
|
+
rawContribution: contribution.raw,
|
|
175
|
+
rawScore: raw.score,
|
|
176
|
+
rawScoreKind: raw.kind,
|
|
177
|
+
ruleSource: resolution.source,
|
|
178
|
+
rulesFingerprint: rankingFingerprint(rules),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function attachAuxiliaryScoreMetadata(
|
|
184
|
+
result: SearchResult,
|
|
185
|
+
scored: AuxiliaryScoreResult,
|
|
186
|
+
finalScore: number,
|
|
187
|
+
includeProjectAffinity = false
|
|
188
|
+
): SearchResult {
|
|
189
|
+
const projectAffinity = {
|
|
190
|
+
...scored.projectAffinity,
|
|
191
|
+
finalBlendedScore: finalScore,
|
|
192
|
+
finalScore,
|
|
193
|
+
};
|
|
194
|
+
result.score = finalScore;
|
|
195
|
+
if (includeProjectAffinity) {
|
|
196
|
+
Object.defineProperty(result, SEARCH_RESULT_AFFINITY_METADATA, {
|
|
197
|
+
configurable: true,
|
|
198
|
+
enumerable: false,
|
|
199
|
+
value: projectAffinity,
|
|
200
|
+
writable: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (scored.contentTypeBoost) {
|
|
204
|
+
Object.defineProperty(result, SEARCH_RESULT_CONTENT_TYPE_BOOST_METADATA, {
|
|
205
|
+
configurable: true,
|
|
206
|
+
enumerable: false,
|
|
207
|
+
value: { ...scored.contentTypeBoost, finalScore },
|
|
208
|
+
writable: true,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function applyContentTypeBoost(
|
|
215
|
+
result: SearchResult,
|
|
216
|
+
collection: string,
|
|
217
|
+
rules: readonly NormalizedContentTypeRule[] | undefined,
|
|
218
|
+
projectAffinity: ProjectAffinityScoringInput | undefined,
|
|
219
|
+
contentTypeSource?: string | null,
|
|
220
|
+
raw?: {
|
|
221
|
+
kind: ProjectAffinityScoreMetadata["rawScoreKind"];
|
|
222
|
+
score: number;
|
|
223
|
+
}
|
|
224
|
+
): SearchResult {
|
|
225
|
+
const scored = scoreContentTypeBoost(
|
|
226
|
+
result.score,
|
|
227
|
+
result.contentType,
|
|
228
|
+
contentTypeSource,
|
|
229
|
+
result.source.relPath,
|
|
230
|
+
collection,
|
|
231
|
+
rules,
|
|
232
|
+
projectAffinity,
|
|
233
|
+
raw
|
|
234
|
+
);
|
|
235
|
+
const affinityActive = hasProjectAffinity(projectAffinity);
|
|
236
|
+
if (!(scored.contentTypeBoost || affinityActive)) return result;
|
|
237
|
+
return attachAuxiliaryScoreMetadata(
|
|
238
|
+
result,
|
|
239
|
+
scored,
|
|
240
|
+
scored.projectAffinity.finalScore,
|
|
241
|
+
affinityActive
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function getContentTypeBoostMetadata(
|
|
246
|
+
result: SearchResult
|
|
247
|
+
): ContentTypeBoostScoreMetadata | undefined {
|
|
248
|
+
return (
|
|
249
|
+
result as SearchResult & {
|
|
250
|
+
[SEARCH_RESULT_CONTENT_TYPE_BOOST_METADATA]?: ContentTypeBoostScoreMetadata;
|
|
251
|
+
}
|
|
252
|
+
)[SEARCH_RESULT_CONTENT_TYPE_BOOST_METADATA];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function sortByFinalScoreStable(results: SearchResult[]): void {
|
|
256
|
+
const originalRank = new Map(
|
|
257
|
+
results.map((result, index) => [result, index] as const)
|
|
258
|
+
);
|
|
259
|
+
results.sort(
|
|
260
|
+
(left, right) =>
|
|
261
|
+
right.score - left.score ||
|
|
262
|
+
(originalRank.get(left) ?? 0) - (originalRank.get(right) ?? 0)
|
|
263
|
+
);
|
|
264
|
+
}
|
package/src/pipeline/diagnose.ts
CHANGED
|
@@ -14,16 +14,23 @@ import type {
|
|
|
14
14
|
QueryDiagnoseTraceCandidate,
|
|
15
15
|
} from "./types";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
fingerprintContentTypeMetadataRules,
|
|
19
|
+
normalizeContentTypes,
|
|
20
|
+
} from "../config";
|
|
18
21
|
import { resolveDocRef } from "../core/ref-parser";
|
|
19
22
|
import { err, ok } from "../store/types";
|
|
23
|
+
import {
|
|
24
|
+
getContentTypeBoostMetadata,
|
|
25
|
+
scoreContentTypeBoost,
|
|
26
|
+
type ContentTypeBoostScoreMetadata,
|
|
27
|
+
} from "./content-type-boost";
|
|
20
28
|
import { evaluateQueryTargetFilters } from "./filters";
|
|
21
29
|
import { searchHybrid } from "./hybrid";
|
|
22
30
|
import {
|
|
23
31
|
getProjectAffinityMetadata,
|
|
24
32
|
type ProjectAffinityScoringInput,
|
|
25
33
|
type ProjectAffinityScoreMetadata,
|
|
26
|
-
scoreProjectAffinity,
|
|
27
34
|
} from "./project-affinity";
|
|
28
35
|
|
|
29
36
|
export type QueryDiagnoseTargetStatus =
|
|
@@ -52,7 +59,7 @@ export interface QueryDiagnoseStage {
|
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
export interface QueryDiagnoseResult {
|
|
55
|
-
schemaVersion: "1.0" | "1.1";
|
|
62
|
+
schemaVersion: "1.0" | "1.1" | "1.2";
|
|
56
63
|
query: string;
|
|
57
64
|
target: {
|
|
58
65
|
ref: string;
|
|
@@ -72,6 +79,7 @@ export interface QueryDiagnoseResult {
|
|
|
72
79
|
};
|
|
73
80
|
stages: QueryDiagnoseStage[];
|
|
74
81
|
affinity?: ProjectAffinityScoreMetadata;
|
|
82
|
+
contentTypeBoost?: ContentTypeBoostScoreMetadata;
|
|
75
83
|
chunk: {
|
|
76
84
|
seq: number | null;
|
|
77
85
|
startLine: number | null;
|
|
@@ -182,9 +190,12 @@ export async function diagnoseQueryTarget(
|
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
const doc = resolved.doc;
|
|
185
|
-
const rules =
|
|
193
|
+
const rules =
|
|
194
|
+
options.contentTypeRules ??
|
|
195
|
+
normalizeContentTypes(deps.config.contentTypes ?? []).rules;
|
|
186
196
|
const expectedFingerprint =
|
|
187
|
-
options.contentTypeRulesFingerprint ??
|
|
197
|
+
options.contentTypeRulesFingerprint ??
|
|
198
|
+
fingerprintContentTypeMetadataRules(rules);
|
|
188
199
|
const fingerprintMatches = doc.contentTypeRulesFingerprint
|
|
189
200
|
? doc.contentTypeRulesFingerprint === expectedFingerprint
|
|
190
201
|
: null;
|
|
@@ -302,16 +313,24 @@ export async function diagnoseQueryTarget(
|
|
|
302
313
|
(candidate) =>
|
|
303
314
|
candidate.mirrorHash === doc.mirrorHash && targetSeqs.has(candidate.seq)
|
|
304
315
|
);
|
|
316
|
+
const fallbackScore = lastMatched
|
|
317
|
+
? scoreContentTypeBoost(
|
|
318
|
+
lastMatched.score,
|
|
319
|
+
doc.contentType ?? undefined,
|
|
320
|
+
doc.contentTypeSource,
|
|
321
|
+
doc.relPath,
|
|
322
|
+
doc.collection,
|
|
323
|
+
rules,
|
|
324
|
+
options.projectAffinity,
|
|
325
|
+
{ kind: "hybrid_blended", score: lastMatched.score }
|
|
326
|
+
)
|
|
327
|
+
: undefined;
|
|
305
328
|
const affinity =
|
|
306
329
|
(targetResult ? getProjectAffinityMetadata(targetResult) : undefined) ??
|
|
307
|
-
(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
options.projectAffinity,
|
|
312
|
-
{ kind: "hybrid_blended", score: lastMatched.score }
|
|
313
|
-
)
|
|
314
|
-
: null);
|
|
330
|
+
(options.projectAffinity ? fallbackScore?.projectAffinity : undefined);
|
|
331
|
+
const contentTypeBoost =
|
|
332
|
+
(targetResult ? getContentTypeBoostMetadata(targetResult) : undefined) ??
|
|
333
|
+
fallbackScore?.contentTypeBoost;
|
|
315
334
|
|
|
316
335
|
const baseResult: QueryDiagnoseResult = {
|
|
317
336
|
...buildBaseResult(query, options.target, "diagnosed", doc, {
|
|
@@ -335,13 +354,21 @@ export async function diagnoseQueryTarget(
|
|
|
335
354
|
queryModes: searchResult.value.meta.queryModes,
|
|
336
355
|
},
|
|
337
356
|
};
|
|
338
|
-
|
|
357
|
+
const trustedAffinity =
|
|
339
358
|
affinity && hasTrustedProjectAffinityInput(options.projectAffinity)
|
|
340
|
-
?
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
359
|
+
? affinity
|
|
360
|
+
: undefined;
|
|
361
|
+
if (contentTypeBoost) {
|
|
362
|
+
return ok({
|
|
363
|
+
...baseResult,
|
|
364
|
+
schemaVersion: "1.2",
|
|
365
|
+
...(trustedAffinity ? { affinity: trustedAffinity } : {}),
|
|
366
|
+
contentTypeBoost,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return ok(
|
|
370
|
+
trustedAffinity
|
|
371
|
+
? { ...baseResult, schemaVersion: "1.1", affinity: trustedAffinity }
|
|
345
372
|
: baseResult
|
|
346
373
|
);
|
|
347
374
|
}
|