@gmickel/gno 1.26.0 → 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 +2 -1
- package/assets/skill/SKILL.md +8 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.26.0.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 +15 -2
- 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/query-diagnose.schema.json +61 -4
- package/spec/output-schemas/search-results.schema.json +87 -1
- package/spec/output-schemas/status.schema.json +24 -0
- package/spec/project-profile.schema.json +3 -1
- 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/query.ts +6 -3
- package/src/cli/commands/search.ts +6 -1
- package/src/cli/commands/status.ts +43 -7
- package/src/cli/program.ts +2 -0
- package/src/config/content-types.ts +82 -0
- package/src/config/index.ts +8 -0
- package/src/config/project-profile.ts +8 -1
- package/src/config/types.ts +11 -2
- package/src/core/context-compiler.ts +38 -1
- 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/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/routes/api.ts +17 -3
- package/src/serve/status-model.ts +2 -0
- package/src/serve/status.ts +4 -0
- package/src/store/sqlite/adapter.ts +3 -0
- package/src/store/types.ts +3 -2
- package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +0 -1
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from "../../core/retrieval-trace-session";
|
|
13
13
|
import type { SearchOptions, SearchResults } from "../../pipeline/types";
|
|
14
14
|
|
|
15
|
+
import { normalizeContentTypes } from "../../config";
|
|
15
16
|
import { resolveCliProjectAffinity } from "../../core/project-affinity-surface";
|
|
16
17
|
import {
|
|
17
18
|
finishRetrievalTraceAfterError,
|
|
@@ -28,7 +29,10 @@ import { decorateSearchResultsForIndex, initStore } from "./shared";
|
|
|
28
29
|
// Types
|
|
29
30
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
31
|
|
|
31
|
-
export type SearchCommandOptions = Omit<
|
|
32
|
+
export type SearchCommandOptions = Omit<
|
|
33
|
+
SearchOptions,
|
|
34
|
+
"contentTypeRules" | "projectAffinity"
|
|
35
|
+
> &
|
|
32
36
|
CliProjectAffinityRequest & {
|
|
33
37
|
/** Override config path */
|
|
34
38
|
configPath?: string;
|
|
@@ -121,6 +125,7 @@ export async function search(
|
|
|
121
125
|
...searchOptions,
|
|
122
126
|
limit,
|
|
123
127
|
projectAffinity,
|
|
128
|
+
contentTypeRules: normalizeContentTypes(config.contentTypes ?? []).rules,
|
|
124
129
|
traceSession,
|
|
125
130
|
});
|
|
126
131
|
|
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
* @module src/cli/commands/status
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ContentTypeBoostStatus } from "../../config/content-types";
|
|
8
9
|
import type { ActivationStatus } from "../../core/activation-status";
|
|
9
10
|
import type { IndexStatus } from "../../store/types";
|
|
10
11
|
|
|
11
12
|
import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
buildContentTypeBoostStatus,
|
|
15
|
+
getConfigPaths,
|
|
16
|
+
isInitialized,
|
|
17
|
+
loadConfig,
|
|
18
|
+
} from "../../config";
|
|
13
19
|
import { isConnectorActivationComplete } from "../../core/activation-connector-health";
|
|
14
20
|
import { buildActivationStatus } from "../../core/activation-status";
|
|
15
21
|
import { ModelCache } from "../../llm/cache";
|
|
@@ -36,7 +42,12 @@ export interface StatusOptions {
|
|
|
36
42
|
* Result of status command.
|
|
37
43
|
*/
|
|
38
44
|
export type StatusResult =
|
|
39
|
-
| {
|
|
45
|
+
| {
|
|
46
|
+
success: true;
|
|
47
|
+
status: IndexStatus;
|
|
48
|
+
activation: ActivationStatus;
|
|
49
|
+
contentTypeBoost: ContentTypeBoostStatus;
|
|
50
|
+
}
|
|
40
51
|
| { success: false; error: string };
|
|
41
52
|
|
|
42
53
|
function connectorProjectionLine(activation: ActivationStatus): string | null {
|
|
@@ -63,7 +74,8 @@ function isStatusHealthy(
|
|
|
63
74
|
*/
|
|
64
75
|
function formatTerminal(
|
|
65
76
|
indexStatus: IndexStatus,
|
|
66
|
-
activation: ActivationStatus
|
|
77
|
+
activation: ActivationStatus,
|
|
78
|
+
contentTypeBoost: ContentTypeBoostStatus
|
|
67
79
|
): string {
|
|
68
80
|
const lines: string[] = [];
|
|
69
81
|
|
|
@@ -101,6 +113,11 @@ function formatTerminal(
|
|
|
101
113
|
lines.push(`Last updated: ${indexStatus.lastUpdatedAt}`);
|
|
102
114
|
}
|
|
103
115
|
|
|
116
|
+
lines.push(
|
|
117
|
+
`Content-type boosts: ${contentTypeBoost.rules.length ? contentTypeBoost.rules.map((rule) => `${rule.id}=${rule.searchBoost}`).join(", ") : "none"}`
|
|
118
|
+
);
|
|
119
|
+
lines.push(`Ranking fingerprint: ${contentTypeBoost.rulesFingerprint}`);
|
|
120
|
+
|
|
104
121
|
lines.push(
|
|
105
122
|
`Health: ${isStatusHealthy(indexStatus, activation) ? "OK" : "DEGRADED"}`
|
|
106
123
|
);
|
|
@@ -138,7 +155,8 @@ function formatTerminal(
|
|
|
138
155
|
*/
|
|
139
156
|
function formatMarkdown(
|
|
140
157
|
indexStatus: IndexStatus,
|
|
141
|
-
activation: ActivationStatus
|
|
158
|
+
activation: ActivationStatus,
|
|
159
|
+
contentTypeBoost: ContentTypeBoostStatus
|
|
142
160
|
): string {
|
|
143
161
|
const lines: string[] = [];
|
|
144
162
|
|
|
@@ -174,6 +192,10 @@ function formatMarkdown(
|
|
|
174
192
|
if (indexStatus.lastUpdatedAt) {
|
|
175
193
|
lines.push(`- **Last updated**: ${indexStatus.lastUpdatedAt}`);
|
|
176
194
|
}
|
|
195
|
+
lines.push(
|
|
196
|
+
`- **Content-type boosts**: ${contentTypeBoost.rules.length ? contentTypeBoost.rules.map((rule) => `${rule.id}=${rule.searchBoost}`).join(", ") : "none"}`
|
|
197
|
+
);
|
|
198
|
+
lines.push(`- **Ranking fingerprint**: ${contentTypeBoost.rulesFingerprint}`);
|
|
177
199
|
|
|
178
200
|
lines.push("");
|
|
179
201
|
lines.push("## Lexical activation");
|
|
@@ -254,7 +276,12 @@ export async function status(
|
|
|
254
276
|
}
|
|
255
277
|
);
|
|
256
278
|
|
|
257
|
-
return {
|
|
279
|
+
return {
|
|
280
|
+
success: true,
|
|
281
|
+
status: statusResult.value,
|
|
282
|
+
activation,
|
|
283
|
+
contentTypeBoost: buildContentTypeBoostStatus(config.contentTypes ?? []),
|
|
284
|
+
};
|
|
258
285
|
} finally {
|
|
259
286
|
await store.close();
|
|
260
287
|
}
|
|
@@ -294,6 +321,7 @@ export function formatStatus(
|
|
|
294
321
|
embeddingBacklog: s.embeddingBacklog,
|
|
295
322
|
lastUpdated: s.lastUpdatedAt,
|
|
296
323
|
healthy: isStatusHealthy(s, result.activation),
|
|
324
|
+
contentTypeBoost: result.contentTypeBoost,
|
|
297
325
|
activation: result.activation,
|
|
298
326
|
},
|
|
299
327
|
null,
|
|
@@ -302,8 +330,16 @@ export function formatStatus(
|
|
|
302
330
|
}
|
|
303
331
|
|
|
304
332
|
if (options.md) {
|
|
305
|
-
return formatMarkdown(
|
|
333
|
+
return formatMarkdown(
|
|
334
|
+
result.status,
|
|
335
|
+
result.activation,
|
|
336
|
+
result.contentTypeBoost
|
|
337
|
+
);
|
|
306
338
|
}
|
|
307
339
|
|
|
308
|
-
return formatTerminal(
|
|
340
|
+
return formatTerminal(
|
|
341
|
+
result.status,
|
|
342
|
+
result.activation,
|
|
343
|
+
result.contentTypeBoost
|
|
344
|
+
);
|
|
309
345
|
}
|
package/src/cli/program.ts
CHANGED
|
@@ -1151,6 +1151,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
1151
1151
|
.option("--context-budget-bytes <num>", "verified Context byte budget")
|
|
1152
1152
|
.option("--min-score <score>", "minimum retrieval score (0-1)")
|
|
1153
1153
|
.option("--graph", "include bounded graph expansion")
|
|
1154
|
+
.option("--explain", "include retrieval scoring explanation")
|
|
1154
1155
|
.option(
|
|
1155
1156
|
"--project-root <path>",
|
|
1156
1157
|
"trusted project root (repeatable; replaces cwd affinity)",
|
|
@@ -1259,6 +1260,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
1259
1260
|
noExpand: depthPolicy.noExpand,
|
|
1260
1261
|
noRerank: depthPolicy.noRerank,
|
|
1261
1262
|
candidateLimit: depthPolicy.candidateLimit,
|
|
1263
|
+
explain: Boolean(cmdOpts.explain),
|
|
1262
1264
|
// Per spec: --answer defaults to false, --no-answer forces retrieval-only
|
|
1263
1265
|
// Commander creates separate cmdOpts.noAnswer for --no-answer flag
|
|
1264
1266
|
answer: Boolean(cmdOpts.answer),
|
|
@@ -8,6 +8,7 @@ import type { NotePresetId } from "../core/note-presets";
|
|
|
8
8
|
import type { Config, ContentTypeConfig } from "./types";
|
|
9
9
|
|
|
10
10
|
import { NOTE_PRESETS } from "../core/note-presets";
|
|
11
|
+
import { CONTENT_TYPE_SEARCH_BOOST_NEUTRAL } from "./types";
|
|
11
12
|
|
|
12
13
|
export type ConfigWarningCode =
|
|
13
14
|
| "UNKNOWN_CONTENT_TYPE_PRESET"
|
|
@@ -21,6 +22,7 @@ export interface ConfigWarning {
|
|
|
21
22
|
|
|
22
23
|
export interface NormalizedContentTypeRule extends ContentTypeConfig {
|
|
23
24
|
preset: NotePresetId;
|
|
25
|
+
searchBoost: number;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface ContentTypeNormalizationResult {
|
|
@@ -33,6 +35,14 @@ export interface ConfigNormalizationResult {
|
|
|
33
35
|
warnings: ConfigWarning[];
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
export interface ContentTypeBoostStatus {
|
|
39
|
+
rulesFingerprint: string;
|
|
40
|
+
rules: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
searchBoost: number;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
const NOTE_PRESET_IDS = new Set<NotePresetId>(
|
|
37
47
|
NOTE_PRESETS.map((preset) => preset.id)
|
|
38
48
|
);
|
|
@@ -48,6 +58,10 @@ function isNotePresetId(value: string): value is NotePresetId {
|
|
|
48
58
|
return NOTE_PRESET_IDS.has(value as NotePresetId);
|
|
49
59
|
}
|
|
50
60
|
|
|
61
|
+
function normalizeSearchBoost(value: number | undefined): number {
|
|
62
|
+
return value ?? CONTENT_TYPE_SEARCH_BOOST_NEUTRAL;
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
export function normalizeContentTypes(
|
|
52
66
|
contentTypes: ContentTypeConfig[]
|
|
53
67
|
): ContentTypeNormalizationResult {
|
|
@@ -89,6 +103,7 @@ export function normalizeContentTypes(
|
|
|
89
103
|
graphHints: contentType.graphHints
|
|
90
104
|
? contentType.graphHints.map(normalizeGraphHint).filter(Boolean)
|
|
91
105
|
: undefined,
|
|
106
|
+
searchBoost: normalizeSearchBoost(contentType.searchBoost),
|
|
92
107
|
});
|
|
93
108
|
continue;
|
|
94
109
|
}
|
|
@@ -100,6 +115,7 @@ export function normalizeContentTypes(
|
|
|
100
115
|
graphHints: contentType.graphHints
|
|
101
116
|
? contentType.graphHints.map(normalizeGraphHint).filter(Boolean)
|
|
102
117
|
: undefined,
|
|
118
|
+
searchBoost: normalizeSearchBoost(contentType.searchBoost),
|
|
103
119
|
});
|
|
104
120
|
}
|
|
105
121
|
|
|
@@ -112,6 +128,34 @@ export function normalizeContentTypes(
|
|
|
112
128
|
return { rules, warnings };
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
export interface ContentTypeRuleResolution {
|
|
132
|
+
rule: NormalizedContentTypeRule;
|
|
133
|
+
source: "configured-id" | "prefix";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve one canonical configured rule. A configured frontmatter type wins;
|
|
138
|
+
* otherwise normalized rule order provides longest-prefix matching. Arbitrary
|
|
139
|
+
* category text is intentionally not an input and boosts never stack.
|
|
140
|
+
*/
|
|
141
|
+
export function resolveContentTypeRule(
|
|
142
|
+
configuredId: string | undefined,
|
|
143
|
+
relativePath: string,
|
|
144
|
+
rules: NormalizedContentTypeRule[]
|
|
145
|
+
): ContentTypeRuleResolution | undefined {
|
|
146
|
+
if (configuredId) {
|
|
147
|
+
const configuredRule = rules.find((rule) => rule.id === configuredId);
|
|
148
|
+
if (configuredRule) {
|
|
149
|
+
return { rule: configuredRule, source: "configured-id" };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const prefixRule = rules.find((rule) =>
|
|
154
|
+
rule.prefixes.some((prefix) => relativePath.startsWith(prefix))
|
|
155
|
+
);
|
|
156
|
+
return prefixRule ? { rule: prefixRule, source: "prefix" } : undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
115
159
|
export function normalizeConfigContentTypes(
|
|
116
160
|
config: Config
|
|
117
161
|
): ConfigNormalizationResult {
|
|
@@ -127,6 +171,44 @@ export function normalizeConfigContentTypes(
|
|
|
127
171
|
|
|
128
172
|
export function fingerprintContentTypeRules(
|
|
129
173
|
rules: NormalizedContentTypeRule[]
|
|
174
|
+
): string {
|
|
175
|
+
const canonical = rules.map((rule) => {
|
|
176
|
+
const base = {
|
|
177
|
+
id: rule.id,
|
|
178
|
+
preset: rule.preset,
|
|
179
|
+
prefixes: rule.prefixes,
|
|
180
|
+
graphHints: rule.graphHints ?? [],
|
|
181
|
+
};
|
|
182
|
+
return rule.searchBoost === CONTENT_TYPE_SEARCH_BOOST_NEUTRAL
|
|
183
|
+
? base
|
|
184
|
+
: { ...base, searchBoost: rule.searchBoost };
|
|
185
|
+
});
|
|
186
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
187
|
+
hasher.update(JSON.stringify(canonical));
|
|
188
|
+
return hasher.digest("hex");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Redacted live-ranking projection for status surfaces. Prefixes remain local
|
|
193
|
+
* configuration details; rule IDs and factors are enough to audit behavior.
|
|
194
|
+
*/
|
|
195
|
+
export function buildContentTypeBoostStatus(
|
|
196
|
+
contentTypes: ContentTypeConfig[]
|
|
197
|
+
): ContentTypeBoostStatus {
|
|
198
|
+
const { rules } = normalizeContentTypes(contentTypes);
|
|
199
|
+
return {
|
|
200
|
+
rulesFingerprint: fingerprintContentTypeRules(rules),
|
|
201
|
+
rules: rules.map(({ id, searchBoost }) => ({ id, searchBoost })),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Fingerprint only fields that derive persisted document metadata.
|
|
207
|
+
* Search boosts are evaluated from live config at query time, so changing one
|
|
208
|
+
* must not force content conversion or vector rebuilds.
|
|
209
|
+
*/
|
|
210
|
+
export function fingerprintContentTypeMetadataRules(
|
|
211
|
+
rules: NormalizedContentTypeRule[]
|
|
130
212
|
): string {
|
|
131
213
|
const canonical = rules.map((rule) => ({
|
|
132
214
|
id: rule.id,
|
package/src/config/index.ts
CHANGED
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
export { createDefaultConfig } from "./defaults";
|
|
8
8
|
export {
|
|
9
|
+
type ContentTypeRuleResolution,
|
|
10
|
+
type ContentTypeBoostStatus,
|
|
9
11
|
type ConfigWarning,
|
|
12
|
+
buildContentTypeBoostStatus,
|
|
13
|
+
fingerprintContentTypeMetadataRules,
|
|
10
14
|
fingerprintContentTypeRules,
|
|
11
15
|
formatConfigWarning,
|
|
12
16
|
formatConfigWarnings,
|
|
13
17
|
normalizeConfigContentTypes,
|
|
14
18
|
normalizeContentTypes,
|
|
15
19
|
type NormalizedContentTypeRule,
|
|
20
|
+
resolveContentTypeRule,
|
|
16
21
|
writeConfigWarningsToStderr,
|
|
17
22
|
} from "./content-types";
|
|
18
23
|
// Loading
|
|
@@ -47,6 +52,9 @@ export {
|
|
|
47
52
|
// Types and schemas
|
|
48
53
|
export {
|
|
49
54
|
CONFIG_VERSION,
|
|
55
|
+
CONTENT_TYPE_SEARCH_BOOST_MAX,
|
|
56
|
+
CONTENT_TYPE_SEARCH_BOOST_MIN,
|
|
57
|
+
CONTENT_TYPE_SEARCH_BOOST_NEUTRAL,
|
|
50
58
|
type Collection,
|
|
51
59
|
CollectionSchema,
|
|
52
60
|
type Config,
|
|
@@ -3,6 +3,8 @@ import { z } from "zod";
|
|
|
3
3
|
import { NOTE_PRESETS } from "../core/note-presets";
|
|
4
4
|
import { hasLikelySecretPath } from "../core/path-rules";
|
|
5
5
|
import {
|
|
6
|
+
CONTENT_TYPE_SEARCH_BOOST_MAX,
|
|
7
|
+
CONTENT_TYPE_SEARCH_BOOST_MIN,
|
|
6
8
|
isValidLanguageHint,
|
|
7
9
|
PROJECT_AFFINITY_MAX_CONTRIBUTION,
|
|
8
10
|
} from "./types";
|
|
@@ -284,7 +286,12 @@ export const ProjectProfileContentTypeSchema = z
|
|
|
284
286
|
}
|
|
285
287
|
})
|
|
286
288
|
.optional(),
|
|
287
|
-
searchBoost: z
|
|
289
|
+
searchBoost: z
|
|
290
|
+
.number()
|
|
291
|
+
.finite()
|
|
292
|
+
.min(CONTENT_TYPE_SEARCH_BOOST_MIN)
|
|
293
|
+
.max(CONTENT_TYPE_SEARCH_BOOST_MAX)
|
|
294
|
+
.optional(),
|
|
288
295
|
temporal: z.boolean().optional(),
|
|
289
296
|
})
|
|
290
297
|
.strict();
|
package/src/config/types.ts
CHANGED
|
@@ -385,6 +385,10 @@ export const CONTENT_TYPE_GRAPH_HINTS = [
|
|
|
385
385
|
|
|
386
386
|
export type ContentTypeGraphHint = (typeof CONTENT_TYPE_GRAPH_HINTS)[number];
|
|
387
387
|
|
|
388
|
+
export const CONTENT_TYPE_SEARCH_BOOST_MIN = 0.5;
|
|
389
|
+
export const CONTENT_TYPE_SEARCH_BOOST_NEUTRAL = 1;
|
|
390
|
+
export const CONTENT_TYPE_SEARCH_BOOST_MAX = 2;
|
|
391
|
+
|
|
388
392
|
export const ContentTypeSchema = z.object({
|
|
389
393
|
/** Stable content type identifier */
|
|
390
394
|
id: z.string().min(1),
|
|
@@ -394,8 +398,13 @@ export const ContentTypeSchema = z.object({
|
|
|
394
398
|
preset: z.string().min(1),
|
|
395
399
|
/** Reserved for fn-84 typed graph hints; accepted but no-op in fn-83 */
|
|
396
400
|
graphHints: z.array(z.string().min(1)).optional(),
|
|
397
|
-
/**
|
|
398
|
-
searchBoost: z
|
|
401
|
+
/** Bounded soft ranking factor; normalized to 1.0 when omitted */
|
|
402
|
+
searchBoost: z
|
|
403
|
+
.number()
|
|
404
|
+
.finite()
|
|
405
|
+
.min(CONTENT_TYPE_SEARCH_BOOST_MIN)
|
|
406
|
+
.max(CONTENT_TYPE_SEARCH_BOOST_MAX)
|
|
407
|
+
.optional(),
|
|
399
408
|
/** Marks time-oriented content types; accepted but no-op in fn-83 */
|
|
400
409
|
temporal: z.boolean().optional(),
|
|
401
410
|
});
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
HybridSearchOptions,
|
|
12
12
|
QueryModeInput,
|
|
13
13
|
SearchMeta,
|
|
14
|
+
SearchExplain,
|
|
14
15
|
SearchResult,
|
|
15
16
|
SearchResults,
|
|
16
17
|
} from "../pipeline/types";
|
|
@@ -113,6 +114,8 @@ export interface ContextCompilerInput {
|
|
|
113
114
|
limits: ContextBudgetLimits;
|
|
114
115
|
/** One caller-owned, successfully loaded context snapshot for this plan. */
|
|
115
116
|
contextSnapshot: ContextRow[];
|
|
117
|
+
/** Internal non-canonical retrieval explanation request. */
|
|
118
|
+
explain?: boolean;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
export interface ContextRetrievalPlan {
|
|
@@ -159,6 +162,8 @@ export interface ContextEvidencePlan<
|
|
|
159
162
|
uriPrefix: string | null;
|
|
160
163
|
retrieval: ContextRetrievalPlan;
|
|
161
164
|
configuredContexts: ContextConfiguredGuidance[];
|
|
165
|
+
/** Non-canonical scoring sidecar; never included in Capsule projection. */
|
|
166
|
+
explain?: SearchExplain;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
const compareCodeUnits = (left: string, right: string): number => {
|
|
@@ -365,6 +370,7 @@ export const planContextEvidence = async <T, P>(
|
|
|
365
370
|
limit: resultLimit === undefined ? undefined : Math.max(1, resultLimit),
|
|
366
371
|
candidateLimit:
|
|
367
372
|
rerankLimit === undefined ? undefined : Math.max(1, rerankLimit),
|
|
373
|
+
explain: input.explain,
|
|
368
374
|
})
|
|
369
375
|
);
|
|
370
376
|
}
|
|
@@ -393,6 +399,33 @@ export const planContextEvidence = async <T, P>(
|
|
|
393
399
|
.slice(0, input.limit ?? decoratedResults.length)
|
|
394
400
|
.map(({ result }) => result)
|
|
395
401
|
.sort(compareSearchResults);
|
|
402
|
+
const explainByResult = new Map<string, SearchExplain["results"][number]>();
|
|
403
|
+
for (const response of responses) {
|
|
404
|
+
for (const [index, result] of response.results.entries()) {
|
|
405
|
+
const detail = response.meta.explain?.results[index];
|
|
406
|
+
if (detail) explainByResult.set(`${result.docid}\0${result.uri}`, detail);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const explain = input.explain
|
|
410
|
+
? {
|
|
411
|
+
lines: responses.flatMap(
|
|
412
|
+
(response) => response.meta.explain?.lines ?? []
|
|
413
|
+
),
|
|
414
|
+
results: results.flatMap((result) => {
|
|
415
|
+
const detail = explainByResult.get(`${result.docid}\0${result.uri}`);
|
|
416
|
+
const retrievalRank = plannerMeta(result)?.retrievalRank;
|
|
417
|
+
return detail
|
|
418
|
+
? [
|
|
419
|
+
{
|
|
420
|
+
...detail,
|
|
421
|
+
rank: retrievalRank ?? detail.rank,
|
|
422
|
+
score: result.score,
|
|
423
|
+
},
|
|
424
|
+
]
|
|
425
|
+
: [];
|
|
426
|
+
}),
|
|
427
|
+
}
|
|
428
|
+
: undefined;
|
|
396
429
|
const uriPrefix =
|
|
397
430
|
input.uriPrefix === null || input.uriPrefix === undefined
|
|
398
431
|
? null
|
|
@@ -528,5 +561,9 @@ export const planContextEvidence = async <T, P>(
|
|
|
528
561
|
projectCanonical: (state) =>
|
|
529
562
|
deps.projectCanonical({ ...baseDraft, selection: state }),
|
|
530
563
|
});
|
|
531
|
-
return {
|
|
564
|
+
return {
|
|
565
|
+
...baseDraft,
|
|
566
|
+
...selection,
|
|
567
|
+
...(explain ? { explain } : {}),
|
|
568
|
+
};
|
|
532
569
|
};
|
|
@@ -12,6 +12,7 @@ import type { RetrievalQrelsCase } from "./retrieval-qrels";
|
|
|
12
12
|
import type { RetrievalReplayCandidate } from "./retrieval-replay-types";
|
|
13
13
|
|
|
14
14
|
import { parseUri } from "../app/constants";
|
|
15
|
+
import { normalizeContentTypes } from "../config";
|
|
15
16
|
import { searchHybrid } from "../pipeline/hybrid";
|
|
16
17
|
import { searchBm25 } from "../pipeline/search";
|
|
17
18
|
import { SEARCH_RESULTS_TRACE_METADATA } from "../pipeline/types";
|
|
@@ -145,7 +146,11 @@ const runCandidateOnce = async (
|
|
|
145
146
|
options: HybridSearchOptions
|
|
146
147
|
): Promise<StoreResult<SearchResults>> => {
|
|
147
148
|
if (candidate.type === "bm25") {
|
|
148
|
-
return searchBm25(deps.store, source.query.text,
|
|
149
|
+
return searchBm25(deps.store, source.query.text, {
|
|
150
|
+
...options,
|
|
151
|
+
contentTypeRules: normalizeContentTypes(deps.config.contentTypes ?? [])
|
|
152
|
+
.rules,
|
|
153
|
+
});
|
|
149
154
|
}
|
|
150
155
|
if (candidate.type === "vector") {
|
|
151
156
|
if (!(deps.vectorIndex && deps.embedPort)) {
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
import type { Config, NormalizedContentTypeRule } from "../config";
|
|
8
8
|
import type { SyncOptions } from "./types";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
fingerprintContentTypeMetadataRules,
|
|
12
|
+
normalizeContentTypes,
|
|
13
|
+
} from "../config";
|
|
11
14
|
|
|
12
15
|
export function resolveContentTypeRules(
|
|
13
16
|
config?: Pick<Config, "contentTypes">
|
|
@@ -24,6 +27,7 @@ export function withContentTypeRules(
|
|
|
24
27
|
...options,
|
|
25
28
|
contentTypeRules: rules,
|
|
26
29
|
contentTypeRulesFingerprint:
|
|
27
|
-
options.contentTypeRulesFingerprint ??
|
|
30
|
+
options.contentTypeRulesFingerprint ??
|
|
31
|
+
fingerprintContentTypeMetadataRules(rules),
|
|
28
32
|
};
|
|
29
33
|
}
|
package/src/ingestion/sync.ts
CHANGED
|
@@ -33,7 +33,10 @@ import type {
|
|
|
33
33
|
WalkerPort,
|
|
34
34
|
} from "./types";
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
fingerprintContentTypeMetadataRules,
|
|
38
|
+
resolveContentTypeRule,
|
|
39
|
+
} from "../config";
|
|
37
40
|
import { getDefaultMimeDetector, type MimeDetector } from "../converters/mime";
|
|
38
41
|
import {
|
|
39
42
|
type ConversionPipeline,
|
|
@@ -80,7 +83,8 @@ const MAX_CONCURRENCY = 16;
|
|
|
80
83
|
* Documents with ingestVersion < INGEST_VERSION will be re-processed.
|
|
81
84
|
*/
|
|
82
85
|
export const INGEST_VERSION = 6;
|
|
83
|
-
const EMPTY_CONTENT_TYPE_RULES_FINGERPRINT =
|
|
86
|
+
const EMPTY_CONTENT_TYPE_RULES_FINGERPRINT =
|
|
87
|
+
fingerprintContentTypeMetadataRules([]);
|
|
84
88
|
const RELATION_EDGE_TYPE_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
85
89
|
const PROJECTION_YIELD_INTERVAL = 25;
|
|
86
90
|
const NON_RETRYABLE_CONVERSION_ERROR_CODES = new Set([
|
|
@@ -401,18 +405,6 @@ function parseCategories(input: unknown): string[] {
|
|
|
401
405
|
return [];
|
|
402
406
|
}
|
|
403
407
|
|
|
404
|
-
function matchPrefixContentType(
|
|
405
|
-
relPath: string,
|
|
406
|
-
rules: NormalizedContentTypeRule[]
|
|
407
|
-
): string | undefined {
|
|
408
|
-
for (const rule of rules) {
|
|
409
|
-
if (rule.prefixes.some((prefix) => relPath.startsWith(prefix))) {
|
|
410
|
-
return rule.id;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return undefined;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
408
|
export function extractDocumentMetadata(
|
|
417
409
|
markdown: string,
|
|
418
410
|
relPath: string,
|
|
@@ -421,23 +413,23 @@ export function extractDocumentMetadata(
|
|
|
421
413
|
): DocumentMetadata {
|
|
422
414
|
const parsed = parseFrontmatter(markdown);
|
|
423
415
|
const metadata = parsed.metadata;
|
|
424
|
-
const typedRules = new Map(contentTypeRules.map((rule) => [rule.id, rule]));
|
|
425
416
|
const rawFrontmatterType =
|
|
426
417
|
typeof metadata.type === "string"
|
|
427
418
|
? normalizeFrontmatterScalar(metadata.type)
|
|
428
419
|
: "";
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
420
|
+
const configuredRule = resolveContentTypeRule(
|
|
421
|
+
rawFrontmatterType,
|
|
422
|
+
relPath,
|
|
423
|
+
contentTypeRules
|
|
424
|
+
);
|
|
434
425
|
const inferred = inferPathContentType(relPath, ext);
|
|
435
|
-
const contentType =
|
|
436
|
-
const contentTypeSource: ContentTypeSource =
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
426
|
+
const contentType = configuredRule?.rule.id ?? inferred.contentType;
|
|
427
|
+
const contentTypeSource: ContentTypeSource =
|
|
428
|
+
configuredRule?.source === "configured-id"
|
|
429
|
+
? "frontmatter-type"
|
|
430
|
+
: configuredRule?.source === "prefix"
|
|
431
|
+
? "prefix"
|
|
432
|
+
: inferred.source;
|
|
441
433
|
const categories = new Set<string>([contentType]);
|
|
442
434
|
|
|
443
435
|
const fmCategories = parseCategories(
|
|
@@ -679,7 +671,7 @@ export class SyncService {
|
|
|
679
671
|
const contentTypeRules = options.contentTypeRules ?? [];
|
|
680
672
|
const contentTypeRulesFingerprint =
|
|
681
673
|
options.contentTypeRulesFingerprint ??
|
|
682
|
-
|
|
674
|
+
fingerprintContentTypeMetadataRules(contentTypeRules);
|
|
683
675
|
|
|
684
676
|
// 4. Check existing doc for skip/repair decision
|
|
685
677
|
const existingResult = await store.getDocument(
|
|
@@ -1066,7 +1058,7 @@ export class SyncService {
|
|
|
1066
1058
|
contentTypeRules: options.contentTypeRules ?? [],
|
|
1067
1059
|
contentTypeRulesFingerprint:
|
|
1068
1060
|
options.contentTypeRulesFingerprint ??
|
|
1069
|
-
|
|
1061
|
+
fingerprintContentTypeMetadataRules(options.contentTypeRules ?? []),
|
|
1070
1062
|
};
|
|
1071
1063
|
const results: FileSyncResult[] = [];
|
|
1072
1064
|
const projectionSourceIds = new Set<number>();
|
|
@@ -1411,7 +1403,7 @@ export class SyncService {
|
|
|
1411
1403
|
contentTypeRules: options.contentTypeRules ?? [],
|
|
1412
1404
|
contentTypeRulesFingerprint:
|
|
1413
1405
|
options.contentTypeRulesFingerprint ??
|
|
1414
|
-
|
|
1406
|
+
fingerprintContentTypeMetadataRules(options.contentTypeRules ?? []),
|
|
1415
1407
|
};
|
|
1416
1408
|
const errors: Array<{ relPath: string; code: string; message: string }> =
|
|
1417
1409
|
[];
|
package/src/ingestion/types.ts
CHANGED
|
@@ -135,7 +135,7 @@ export interface SyncOptions {
|
|
|
135
135
|
concurrency?: number;
|
|
136
136
|
/** Normalized content type rules from config.contentTypes. */
|
|
137
137
|
contentTypeRules?: NormalizedContentTypeRule[];
|
|
138
|
-
/** Stable hash of
|
|
138
|
+
/** Stable hash of metadata-affecting rules, used for re-derivation. */
|
|
139
139
|
contentTypeRulesFingerprint?: string;
|
|
140
140
|
/** Internal orchestration flag: defer graph projection to an outer sync. */
|
|
141
141
|
projectTypedEdges?: boolean;
|
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 {
|