sift-light 1.0.0 → 1.0.2
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/CHANGELOG.md +39 -0
- package/README.md +11 -4
- package/README.zh-CN.md +11 -4
- package/package.json +1 -1
- package/plugins/sift-light/.claude-plugin/plugin.json +1 -1
- package/plugins/sift-light/.codex-plugin/plugin.json +1 -1
- package/plugins/sift-light/.mcp.json +1 -1
- package/plugins/sift-light/concept-worker.mjs +39 -12
- package/plugins/sift-light/kimi.plugin.json +2 -2
- package/plugins/sift-light/omp-extension.mjs +558 -83
- package/plugins/sift-light/package.json +1 -1
- package/src/analysis-limits.ts +1 -1
- package/src/analysis-store.ts +6 -2
- package/src/analysis-types.ts +10 -1
- package/src/concept-inference.ts +44 -11
- package/src/concept-model.ts +2 -2
- package/src/concept-search.ts +12 -2
- package/src/concept-worker.mjs +39 -12
- package/src/concept-worker.ts +1 -1
- package/src/config-reader.ts +11 -3
- package/src/evidence-service.ts +117 -13
- package/src/file-discovery.ts +111 -17
- package/src/hybrid-search.ts +13 -0
- package/src/index.ts +4 -10
- package/src/inspect.ts +3 -0
- package/src/language-capability-definitions.ts +9 -0
- package/src/mcp-model-output.ts +47 -1
- package/src/mcp-semantic-judge.ts +19 -8
- package/src/mcp-server.mjs +754 -247
- package/src/mcp-server.ts +11 -13
- package/src/mcp.ts +2 -0
- package/src/model-error.ts +6 -16
- package/src/omp-index.ts +4 -6
- package/src/prompt-guidelines.ts +5 -5
- package/src/python-roles.ts +247 -0
- package/src/redaction.ts +9 -2
- package/src/request-aliases.ts +48 -0
- package/src/request-contract-catalog.ts +24 -7
- package/src/request-contract.ts +60 -6
- package/src/request.ts +3 -1
- package/src/runtime.ts +2 -2
- package/src/semantic-judge.ts +17 -3
- package/src/service.ts +31 -1
- package/src/session-summary.ts +4 -4
- package/src/source-inspection.ts +27 -2
- package/src/tool-schema.ts +18 -18
- package/src/tui/layout.ts +2 -2
- package/src/tui/renderers.ts +2 -2
- package/src/types.ts +4 -0
package/src/file-discovery.ts
CHANGED
|
@@ -13,6 +13,42 @@ interface FileScore {
|
|
|
13
13
|
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
14
14
|
/** Bracket classes remain valid query text, so dynamic route names stay searchable. */
|
|
15
15
|
const FILE_QUERY_GLOB_WILDCARD = /[*?]/u;
|
|
16
|
+
/** Queries such as `*`, `**` or `.*` mean "every file" rather than filename text. */
|
|
17
|
+
const FILE_QUERY_MATCH_ALL = /^(?:[*/]+|\.\*|\.\+|\*\.\*)$/u;
|
|
18
|
+
/** Regex syntax that cannot be a simple glob; such a query is rejected with guidance. */
|
|
19
|
+
const FILE_QUERY_REGEX_SYNTAX = /[()|^$\\+]/u;
|
|
20
|
+
const IGNORED_MATCH_SAMPLES = 5;
|
|
21
|
+
|
|
22
|
+
interface NormalizedFileQuery {
|
|
23
|
+
query: string;
|
|
24
|
+
glob?: string;
|
|
25
|
+
note?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Filename discovery matches text. A wildcard-only query means "all files" and a
|
|
30
|
+
* plain glob such as `*.py` becomes an explicit glob filter; both are disclosed.
|
|
31
|
+
*/
|
|
32
|
+
function normalizeFileQuery(query: string, hasGlob: boolean): NormalizedFileQuery {
|
|
33
|
+
const trimmed = query.trim();
|
|
34
|
+
if (FILE_QUERY_MATCH_ALL.test(trimmed))
|
|
35
|
+
return {
|
|
36
|
+
query: "",
|
|
37
|
+
note: `Query ${JSON.stringify(query)} was treated as listing every file under path; omit query for the same result.`,
|
|
38
|
+
};
|
|
39
|
+
if (!FILE_QUERY_GLOB_WILDCARD.test(trimmed)) return { query };
|
|
40
|
+
if (!hasGlob && !FILE_QUERY_REGEX_SYNTAX.test(trimmed) && !/\s/u.test(trimmed)) {
|
|
41
|
+
const glob = trimmed.includes("/") ? trimmed : `**/${trimmed}`;
|
|
42
|
+
return {
|
|
43
|
+
query: "",
|
|
44
|
+
glob,
|
|
45
|
+
note: `Query ${JSON.stringify(query)} contains glob wildcards and was applied as glob ${JSON.stringify(glob)}; mode=files query otherwise matches filename text.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
throw new SiftLightError(
|
|
49
|
+
`File query ${JSON.stringify(query)} uses wildcard or regex syntax; mode=files matches filename and path text, not patterns. Omit query to retain every file under path, use glob for one name pattern, or run one files request per name.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
16
52
|
|
|
17
53
|
function subsequenceScore(text: string, query: string): number | undefined {
|
|
18
54
|
const characters = Array.from(graphemes.segment(text), (item) => item.segment);
|
|
@@ -70,26 +106,33 @@ export async function discoverFiles(
|
|
|
70
106
|
cwd: string,
|
|
71
107
|
signal?: AbortSignal,
|
|
72
108
|
): Promise<AnalysisResultSet> {
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
109
|
+
const rawQuery = input.query ?? "";
|
|
110
|
+
if (rawQuery.length > 256 || !rawQuery.isWellFormed() || /[\r\n\0]/.test(rawQuery))
|
|
75
111
|
throw new SiftLightError(
|
|
76
112
|
"File query must be well-formed single-line text of at most 256 characters",
|
|
77
113
|
);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
const requestedGlobs =
|
|
115
|
+
input.glob === undefined ? [] : Array.isArray(input.glob) ? input.glob : [input.glob];
|
|
116
|
+
const normalizedQuery = normalizeFileQuery(rawQuery, requestedGlobs.length > 0);
|
|
117
|
+
const query = normalizedQuery.query;
|
|
118
|
+
const request = normalizeRequest({
|
|
119
|
+
...input,
|
|
120
|
+
pattern: "",
|
|
121
|
+
...(normalizedQuery.glob ? { glob: [...requestedGlobs, normalizedQuery.glob] } : {}),
|
|
122
|
+
});
|
|
85
123
|
const policy = new SearchPathPolicy(cwd);
|
|
86
124
|
const discoveryRoot = request.path ?? ".";
|
|
87
125
|
const scoringRoot = await policy.resolveSearchTarget(discoveryRoot);
|
|
88
|
-
const
|
|
126
|
+
const includeIgnored = request.ignorePolicy === "include";
|
|
127
|
+
const listOptions = {
|
|
89
128
|
path: scoringRoot,
|
|
90
129
|
glob: request.glob,
|
|
91
130
|
exclude: request.exclude,
|
|
92
131
|
hidden: request.hidden,
|
|
132
|
+
};
|
|
133
|
+
const files = await listWorkspaceFiles(cwd, signal, {
|
|
134
|
+
...listOptions,
|
|
135
|
+
...(includeIgnored ? { ignore: false, ignoreParents: false } : {}),
|
|
93
136
|
});
|
|
94
137
|
const filtered = await filterPathsByModificationTime(
|
|
95
138
|
cwd,
|
|
@@ -98,10 +141,12 @@ export async function discoverFiles(
|
|
|
98
141
|
request.modifiedBeforeMs,
|
|
99
142
|
signal,
|
|
100
143
|
);
|
|
144
|
+
const rank = (path: string) =>
|
|
145
|
+
scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
|
|
101
146
|
const selected = filtered.paths
|
|
102
147
|
.flatMap((path) => {
|
|
103
|
-
const
|
|
104
|
-
return
|
|
148
|
+
const score = rank(path);
|
|
149
|
+
return score ? [{ path, ...score }] : [];
|
|
105
150
|
})
|
|
106
151
|
.toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
107
152
|
for (let offset = 0; offset < selected.length; offset += 16) {
|
|
@@ -111,15 +156,54 @@ export async function discoverFiles(
|
|
|
111
156
|
);
|
|
112
157
|
signal?.throwIfAborted();
|
|
113
158
|
}
|
|
159
|
+
const reasons = new Set([...files.reasons, ...filtered.reasons]);
|
|
160
|
+
if (normalizedQuery.note) reasons.add(normalizedQuery.note);
|
|
161
|
+
// Issue #97: ignore rules must never turn a filtered absence into a complete one.
|
|
162
|
+
let ignoredFiles = 0;
|
|
163
|
+
let ignoredMatches = 0;
|
|
164
|
+
let ignoredComparisonPartial = false;
|
|
165
|
+
if (!includeIgnored) {
|
|
166
|
+
const everything = await listWorkspaceFiles(cwd, signal, {
|
|
167
|
+
...listOptions,
|
|
168
|
+
ignore: false,
|
|
169
|
+
ignoreParents: false,
|
|
170
|
+
});
|
|
171
|
+
ignoredComparisonPartial = everything.partial;
|
|
172
|
+
const admitted = new Set(files.paths);
|
|
173
|
+
const ignored = everything.paths.filter((path) => !admitted.has(path));
|
|
174
|
+
ignoredFiles = ignored.length;
|
|
175
|
+
const ignoredCandidates = ignored
|
|
176
|
+
.flatMap((path) => {
|
|
177
|
+
const score = rank(path);
|
|
178
|
+
return score ? [{ path, ...score }] : [];
|
|
179
|
+
})
|
|
180
|
+
.toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
181
|
+
ignoredMatches = ignoredCandidates.length;
|
|
182
|
+
if (ignoredFiles > 0) {
|
|
183
|
+
const samples = ignoredCandidates
|
|
184
|
+
.slice(0, IGNORED_MATCH_SAMPLES)
|
|
185
|
+
.map((item) => `${JSON.stringify(item.path)} (${item.reason})`);
|
|
186
|
+
reasons.add(
|
|
187
|
+
ignoredMatches > 0
|
|
188
|
+
? `${String(ignoredFiles)} file(s) were excluded by ignore rules; ${String(ignoredMatches)} of them match this query${samples.length ? `, e.g. ${samples.join(", ")}` : ""}. Retry with ignorePolicy "include" to list them.`
|
|
189
|
+
: `${String(ignoredFiles)} file(s) were excluded by ignore rules; none of them match this query.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (ignoredComparisonPartial)
|
|
193
|
+
reasons.add(
|
|
194
|
+
"Ignored-file comparison was incomplete; the ignored-file count is a lower bound",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const enumerationPartial = files.partial || filtered.partial;
|
|
114
198
|
return {
|
|
115
199
|
kind: "files",
|
|
116
200
|
unit: "files",
|
|
117
|
-
partial:
|
|
118
|
-
reasons: [...
|
|
201
|
+
partial: enumerationPartial,
|
|
202
|
+
reasons: [...reasons],
|
|
119
203
|
items: selected.map((item) => ({
|
|
120
204
|
path: item.path,
|
|
121
205
|
line: 1,
|
|
122
|
-
label: `File candidate (${item.reason})`,
|
|
206
|
+
label: `File candidate (score ${String(item.score)}: ${item.reason})`,
|
|
123
207
|
details: {
|
|
124
208
|
kind: "file",
|
|
125
209
|
score: item.score,
|
|
@@ -127,8 +211,17 @@ export async function discoverFiles(
|
|
|
127
211
|
inspect: { mode: "inspect", path: item.path, line: 1 },
|
|
128
212
|
},
|
|
129
213
|
})),
|
|
130
|
-
coverage: {
|
|
131
|
-
|
|
214
|
+
coverage: {
|
|
215
|
+
fileEnumeration: enumerationPartial
|
|
216
|
+
? "partial"
|
|
217
|
+
: ignoredFiles > 0 || ignoredComparisonPartial
|
|
218
|
+
? "policy-filtered"
|
|
219
|
+
: "complete",
|
|
220
|
+
},
|
|
221
|
+
stats: {
|
|
222
|
+
filesEnumerated: files.paths.length,
|
|
223
|
+
...(includeIgnored ? {} : { ignoredFiles, ignoredMatches }),
|
|
224
|
+
},
|
|
132
225
|
scope: {
|
|
133
226
|
path: request.path ?? ".",
|
|
134
227
|
requestedPath: request.path ?? ".",
|
|
@@ -146,5 +239,6 @@ export async function discoverFiles(
|
|
|
146
239
|
: {}),
|
|
147
240
|
},
|
|
148
241
|
redact: input.redact ?? false,
|
|
242
|
+
...(input.limit !== undefined ? { pageSize: request.pageSize } : {}),
|
|
149
243
|
};
|
|
150
244
|
}
|
package/src/hybrid-search.ts
CHANGED
|
@@ -274,6 +274,14 @@ export async function combineHybridSearch(
|
|
|
274
274
|
const selectionReason = conceptCandidatesOmitted
|
|
275
275
|
? `Hybrid concept limit retained the top ${String(selectedConcept.length)} of ${String(eligibleConcept.length)} non-overlapping semantic candidates`
|
|
276
276
|
: undefined;
|
|
277
|
+
const firstScore = Number(
|
|
278
|
+
eligibleConcept[0]?.details?.rankingScore ?? eligibleConcept[0]?.details?.score,
|
|
279
|
+
);
|
|
280
|
+
const secondScore = Number(
|
|
281
|
+
eligibleConcept[1]?.details?.rankingScore ?? eligibleConcept[1]?.details?.score,
|
|
282
|
+
);
|
|
283
|
+
const closeRanking =
|
|
284
|
+
Number.isFinite(firstScore) && Number.isFinite(secondScore) && firstScore - secondScore < 0.01;
|
|
277
285
|
return {
|
|
278
286
|
kind: "hybrid",
|
|
279
287
|
unit: "evidence-items",
|
|
@@ -285,6 +293,11 @@ export async function combineHybridSearch(
|
|
|
285
293
|
...literal.reasons,
|
|
286
294
|
...execution.sourceGeneration.reasons,
|
|
287
295
|
...(selectionReason ? [selectionReason] : []),
|
|
296
|
+
...(closeRanking
|
|
297
|
+
? [
|
|
298
|
+
"Semantic ranks are close; verify the leading candidates with source inspection or literal terms.",
|
|
299
|
+
]
|
|
300
|
+
: []),
|
|
288
301
|
...(judgedConcept.semanticJudge?.reason ? [judgedConcept.semanticJudge.reason] : []),
|
|
289
302
|
],
|
|
290
303
|
filesRead: (concept.filesRead ?? 0) + access.filesRead,
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { SIFT_LIGHT_DESCRIPTION, siftLightSchema } from "./tool-schema.js";
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
4
|
-
normalizeSearchEnforcement,
|
|
5
|
-
readSiftLightConfig,
|
|
6
|
-
DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
7
|
-
type SiftLightConfig,
|
|
8
|
-
} from "./config.js";
|
|
3
|
+
import { normalizeSearchEnforcement, readSiftLightConfig, type SiftLightConfig } from "./config.js";
|
|
9
4
|
import { resolveContextBudget } from "./context-budget.js";
|
|
10
5
|
import { createRipgrepRunner } from "./rg.js";
|
|
11
6
|
import { createCtagsStructureProvider } from "./structure.js";
|
|
@@ -17,7 +12,7 @@ import type { SiftLightDetails } from "./types.js";
|
|
|
17
12
|
import { renderSiftLightCall, renderSiftLightResult } from "./tui/renderers.js";
|
|
18
13
|
import { registerPiSearchPolicy } from "./pi-search-policy.js";
|
|
19
14
|
import { modelErrorText } from "./model-error.js";
|
|
20
|
-
import {
|
|
15
|
+
import { createConfiguredSemanticJudgeIntegration } from "./semantic-judge.js";
|
|
21
16
|
|
|
22
17
|
const SIFT_LIGHT_LABEL = "sift-light";
|
|
23
18
|
|
|
@@ -27,13 +22,12 @@ export async function registerSiftLightExtension(
|
|
|
27
22
|
pi: ExtensionAPI,
|
|
28
23
|
config: SiftLightConfig,
|
|
29
24
|
): Promise<void> {
|
|
30
|
-
const semanticJudge =
|
|
31
|
-
config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
32
|
-
);
|
|
25
|
+
const semanticJudge = createConfiguredSemanticJudgeIntegration(config);
|
|
33
26
|
const runtime = new SiftLightRuntime(
|
|
34
27
|
new SiftLightService({
|
|
35
28
|
runRipgrep: createRipgrepRunner(),
|
|
36
29
|
structure: createCtagsStructureProvider(),
|
|
30
|
+
vectorSearchEnabled: config.vectorSearchEnabled === true,
|
|
37
31
|
semanticJudge,
|
|
38
32
|
}),
|
|
39
33
|
);
|
package/src/inspect.ts
CHANGED
|
@@ -78,6 +78,9 @@ export function resolveInspectionTarget(
|
|
|
78
78
|
line = retainedMatch.lineNumber;
|
|
79
79
|
}
|
|
80
80
|
if (!path) throw new SiftLightError("path is required when mode=inspect");
|
|
81
|
+
// A direct path without a line opens the file from its first line. Cursor
|
|
82
|
+
// inspection still needs the exact retained line, so it keeps failing closed.
|
|
83
|
+
if (line === undefined && input.cursor === undefined) line = 1;
|
|
81
84
|
if (line === undefined || !Number.isSafeInteger(line) || line < 1) {
|
|
82
85
|
throw new SiftLightError("line must be a positive integer when mode=inspect");
|
|
83
86
|
}
|
|
@@ -119,6 +119,15 @@ const PYTHON_LANGUAGE_CAPABILITIES: readonly LanguageCapabilitySpec[] = [
|
|
|
119
119
|
availability: "implemented",
|
|
120
120
|
load: "lazy",
|
|
121
121
|
},
|
|
122
|
+
{
|
|
123
|
+
id: "python-lexical.roles",
|
|
124
|
+
name: "roles",
|
|
125
|
+
provider: "bounded Python lexical role scanner",
|
|
126
|
+
providerKind: "builtin",
|
|
127
|
+
evidence: "syntax",
|
|
128
|
+
availability: "implemented",
|
|
129
|
+
load: "lazy",
|
|
130
|
+
},
|
|
122
131
|
];
|
|
123
132
|
|
|
124
133
|
export const DEFAULT_LANGUAGE_CAPABILITIES: readonly LanguageCapabilityDescriptor[] = [
|
package/src/mcp-model-output.ts
CHANGED
|
@@ -7,7 +7,48 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
7
7
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function compactSemanticMetadata(details: SiftLightDetails, analysis: AnalysisDetails): string[] {
|
|
11
|
+
const counts = analysis.counts;
|
|
12
|
+
const stats = analysis.stats;
|
|
13
|
+
const judge = analysis.semanticJudge;
|
|
14
|
+
const coverage = analysis.coverage;
|
|
15
|
+
return [
|
|
16
|
+
...(counts || stats
|
|
17
|
+
? [
|
|
18
|
+
`Search: ${String(counts?.filesAdmitted ?? stats?.filesAdmitted ?? 0)} files, ${String(stats?.passagesRanked ?? counts?.passagesQueued ?? 0)} passages; ${String(stats?.elapsedMs ?? 0)} ms; peak inference RSS ${String(stats?.inferencePeakRssBytes ?? 0)} bytes; cache ${String(stats?.conceptCacheHits ?? 0)} hits/${String(stats?.conceptCacheMisses ?? 0)} misses.`,
|
|
19
|
+
]
|
|
20
|
+
: []),
|
|
21
|
+
...(coverage
|
|
22
|
+
? [
|
|
23
|
+
`Coverage: ${Object.entries(coverage)
|
|
24
|
+
.map(([name, status]) => `${name}=${status}`)
|
|
25
|
+
.join(", ")}.`,
|
|
26
|
+
]
|
|
27
|
+
: []),
|
|
28
|
+
...(analysis.scope
|
|
29
|
+
? [`Scope: ${analysis.scope.path}; ignore=${analysis.scope.ignorePolicy}.`]
|
|
30
|
+
: []),
|
|
31
|
+
...(analysis.sourceGeneration
|
|
32
|
+
? [
|
|
33
|
+
`Source: ${analysis.sourceGeneration.verification}; ${String(analysis.sourceGeneration.filesUnavailable)} unavailable.`,
|
|
34
|
+
]
|
|
35
|
+
: []),
|
|
36
|
+
...(judge
|
|
37
|
+
? [
|
|
38
|
+
`Semantic judge: ${judge.status}; ${String(judge.judgedCandidates)}/${String(judge.candidatesConsidered)} judged, ${String(judge.candidatesUnjudged)} unjudged; batches ${String(judge.batchesCompleted)}/${String(judge.batchesAttempted)} completed; classes ${JSON.stringify(judge.classificationCounts)}${judge.reason ? `; ${judge.reason}` : ""}.`,
|
|
39
|
+
]
|
|
40
|
+
: []),
|
|
41
|
+
...(details.operation
|
|
42
|
+
? [`Operation: ${details.operation.state}; id=${details.operation.id}.`]
|
|
43
|
+
: []),
|
|
44
|
+
...analysis.reasons.map((reason) => `[${reason}]`),
|
|
45
|
+
...(details.redactionApplied ? ["[Display redaction applied.]"] : []),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
10
49
|
function compactMetadata(details: SiftLightDetails, analysis: AnalysisDetails): string[] {
|
|
50
|
+
if (analysis.kind === "concept" || analysis.kind === "hybrid")
|
|
51
|
+
return compactSemanticMetadata(details, analysis);
|
|
11
52
|
return [
|
|
12
53
|
...(analysis.statistics ? formatStatistics(analysis.statistics) : []),
|
|
13
54
|
analysis.counts ? `Counts: ${JSON.stringify(analysis.counts)}` : undefined,
|
|
@@ -46,7 +87,10 @@ function compactRows(analysis: AnalysisDetails): string[] {
|
|
|
46
87
|
rows.push(JSON.stringify(item.path));
|
|
47
88
|
previousPath = item.path;
|
|
48
89
|
}
|
|
49
|
-
const label =
|
|
90
|
+
const label =
|
|
91
|
+
(analysis.kind === "outline" && analysis.modelOutput) || analysis.kind === "files"
|
|
92
|
+
? item.label
|
|
93
|
+
: "metadata";
|
|
50
94
|
const semanticJudge = item.details?.semanticJudge;
|
|
51
95
|
const judgment =
|
|
52
96
|
isRecord(semanticJudge) &&
|
|
@@ -108,6 +152,7 @@ export function compactMcpModelText(result: SiftLightResult): string {
|
|
|
108
152
|
const inspect = compactInspectInstruction(analysis);
|
|
109
153
|
const nextRequest = distinctNextRequest(result.details, analysis);
|
|
110
154
|
const compact = [
|
|
155
|
+
...(result.details.requestNotes ?? []).map((note) => `[Request note: ${note}]`),
|
|
111
156
|
header,
|
|
112
157
|
...compactMetadata(result.details, analysis),
|
|
113
158
|
...compactRows(analysis),
|
|
@@ -115,6 +160,7 @@ export function compactMcpModelText(result: SiftLightResult): string {
|
|
|
115
160
|
...(nextRequest ? [`Next request: ${nextRequest}`] : []),
|
|
116
161
|
].join("\n");
|
|
117
162
|
const standard = result.text.replace(" Structured output retains per-item evidence details.", "");
|
|
163
|
+
// Both projections carry request notes, so the byte comparison stays fair.
|
|
118
164
|
if (analysis.semanticJudge) return compact;
|
|
119
165
|
return Buffer.byteLength(compact) < Buffer.byteLength(standard) ? compact : standard;
|
|
120
166
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_SIFT_LIGHT_CONFIG,
|
|
2
3
|
DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
3
4
|
readSiftLightConfigFile,
|
|
4
5
|
SIFT_LIGHT_CONFIG_ENV,
|
|
5
6
|
} from "./config-reader.js";
|
|
6
7
|
import {
|
|
7
8
|
createDisabledSemanticJudgeIntegration,
|
|
8
|
-
|
|
9
|
+
createConfiguredSemanticJudgeIntegration,
|
|
9
10
|
type SemanticJudgeIntegration,
|
|
10
11
|
} from "./semantic-judge.js";
|
|
11
12
|
|
|
@@ -19,17 +20,27 @@ function configuredPath(environment: NodeJS.ProcessEnv): string | undefined {
|
|
|
19
20
|
* The MCP process must receive the same config path explicitly in its own host
|
|
20
21
|
* environment; an absent path remains an observable, local-only disabled state.
|
|
21
22
|
*/
|
|
22
|
-
export async function
|
|
23
|
+
export async function createMcpSearchFeatures(
|
|
23
24
|
environment: NodeJS.ProcessEnv = process.env,
|
|
24
|
-
): Promise<SemanticJudgeIntegration> {
|
|
25
|
+
): Promise<{ semanticJudge: SemanticJudgeIntegration; vectorSearchEnabled: boolean }> {
|
|
25
26
|
const path = configuredPath(environment);
|
|
26
|
-
if (!path)
|
|
27
|
+
if (!path)
|
|
28
|
+
return {
|
|
29
|
+
semanticJudge: createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG),
|
|
30
|
+
vectorSearchEnabled: DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled === true,
|
|
31
|
+
};
|
|
27
32
|
|
|
28
33
|
const config = await readSiftLightConfigFile(path, { missing: "error" });
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
return {
|
|
35
|
+
semanticJudge: createConfiguredSemanticJudgeIntegration(config, environment),
|
|
36
|
+
vectorSearchEnabled: config.vectorSearchEnabled === true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function createMcpSemanticJudgeIntegration(
|
|
41
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
42
|
+
): Promise<SemanticJudgeIntegration> {
|
|
43
|
+
return (await createMcpSearchFeatures(environment)).semanticJudge;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
export function mcpSemanticJudgeConfigSource(
|