sift-light 1.0.0 → 1.0.1
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 +19 -0
- package/README.md +8 -1
- package/README.zh-CN.md +8 -1
- 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 +68 -28
- package/plugins/sift-light/package.json +1 -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/hybrid-search.ts +13 -0
- package/src/index.ts +4 -10
- package/src/mcp-model-output.ts +41 -0
- package/src/mcp-semantic-judge.ts +19 -8
- package/src/mcp-server.mjs +262 -191
- 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/request-contract.ts +41 -5
- package/src/semantic-judge.ts +17 -3
- package/src/service.ts +8 -0
- package/src/tool-schema.ts +3 -3
package/src/mcp-server.ts
CHANGED
|
@@ -8,10 +8,7 @@ import {
|
|
|
8
8
|
createDefaultSiftLightMcpService,
|
|
9
9
|
startSiftLightMcpServer,
|
|
10
10
|
} from "./mcp.js";
|
|
11
|
-
import {
|
|
12
|
-
createMcpSemanticJudgeIntegration,
|
|
13
|
-
mcpSemanticJudgeConfigSource,
|
|
14
|
-
} from "./mcp-semantic-judge.js";
|
|
11
|
+
import { createMcpSearchFeatures, mcpSemanticJudgeConfigSource } from "./mcp-semantic-judge.js";
|
|
15
12
|
import type { SemanticJudgeIntegration } from "./semantic-judge.js";
|
|
16
13
|
import { parseSiftLightMcpTransport, SIFT_LIGHT_MCP_USAGE } from "./mcp-cli.js";
|
|
17
14
|
import { parseSiftLightMcpOutputMode, type SiftLightMcpOutputMode } from "./mcp-output.js";
|
|
@@ -41,10 +38,6 @@ function allowedOrigins(): string[] {
|
|
|
41
38
|
.filter((origin) => origin.length > 0);
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
async function configuredSemanticJudge(): Promise<SemanticJudgeIntegration> {
|
|
45
|
-
return createMcpSemanticJudgeIntegration();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
41
|
function logSemanticJudgeStatus(integration: SemanticJudgeIntegration): void {
|
|
49
42
|
const status = integration.config.enabled ? "enabled" : "disabled";
|
|
50
43
|
process.stderr.write(
|
|
@@ -55,12 +48,13 @@ function logSemanticJudgeStatus(integration: SemanticJudgeIntegration): void {
|
|
|
55
48
|
async function runHttpServer(
|
|
56
49
|
outputMode: SiftLightMcpOutputMode,
|
|
57
50
|
semanticJudge: SemanticJudgeIntegration | undefined,
|
|
51
|
+
vectorSearchEnabled: boolean,
|
|
58
52
|
): Promise<void> {
|
|
59
53
|
const running = await startSiftLightMcpServer({
|
|
60
54
|
cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.cwd(),
|
|
61
55
|
host: process.env.SIFT_LIGHT_MCP_HOST ?? DEFAULT_MCP_HOST,
|
|
62
56
|
port: environmentInteger("SIFT_LIGHT_MCP_PORT", DEFAULT_MCP_PORT, 0, 65_535),
|
|
63
|
-
createService: () => createDefaultSiftLightMcpService(semanticJudge),
|
|
57
|
+
createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled),
|
|
64
58
|
maxSessions: environmentInteger(
|
|
65
59
|
"SIFT_LIGHT_MCP_MAX_SESSIONS",
|
|
66
60
|
DEFAULT_MCP_MAX_SESSIONS,
|
|
@@ -108,11 +102,12 @@ async function runHttpServer(
|
|
|
108
102
|
async function runStdioServer(
|
|
109
103
|
outputMode: SiftLightMcpOutputMode,
|
|
110
104
|
semanticJudge: SemanticJudgeIntegration | undefined,
|
|
105
|
+
vectorSearchEnabled: boolean,
|
|
111
106
|
): Promise<void> {
|
|
112
107
|
const running = await startSiftLightMcpStdioServer({
|
|
113
108
|
cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
|
|
114
109
|
outputMode,
|
|
115
|
-
createService: () => createDefaultSiftLightMcpService(semanticJudge),
|
|
110
|
+
createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled),
|
|
116
111
|
});
|
|
117
112
|
process.stderr.write("sift-light MCP serving one local client over stdio\n");
|
|
118
113
|
process.stderr.write(`sift-light MCP working directory: ${running.cwd}\n`);
|
|
@@ -140,13 +135,16 @@ async function main(): Promise<void> {
|
|
|
140
135
|
return;
|
|
141
136
|
}
|
|
142
137
|
const outputMode = parseSiftLightMcpOutputMode(process.env.SIFT_LIGHT_MCP_OUTPUT_MODE);
|
|
143
|
-
const semanticJudge = await
|
|
138
|
+
const { semanticJudge, vectorSearchEnabled } = await createMcpSearchFeatures();
|
|
144
139
|
logSemanticJudgeStatus(semanticJudge);
|
|
140
|
+
process.stderr.write(
|
|
141
|
+
`sift-light MCP vector search: ${vectorSearchEnabled ? "enabled" : "disabled"}\n`,
|
|
142
|
+
);
|
|
145
143
|
if (transport === "stdio") {
|
|
146
|
-
await runStdioServer(outputMode, semanticJudge);
|
|
144
|
+
await runStdioServer(outputMode, semanticJudge, vectorSearchEnabled);
|
|
147
145
|
return;
|
|
148
146
|
}
|
|
149
|
-
await runHttpServer(outputMode, semanticJudge);
|
|
147
|
+
await runHttpServer(outputMode, semanticJudge, vectorSearchEnabled);
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
try {
|
package/src/mcp.ts
CHANGED
|
@@ -93,12 +93,14 @@ export interface SiftLightMcpService {
|
|
|
93
93
|
|
|
94
94
|
export function createDefaultSiftLightMcpService(
|
|
95
95
|
semanticJudge?: SemanticJudgeIntegration,
|
|
96
|
+
vectorSearchEnabled = false,
|
|
96
97
|
): SiftLightMcpService {
|
|
97
98
|
const resolvedSemanticJudge =
|
|
98
99
|
semanticJudge ?? createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG);
|
|
99
100
|
return new SiftLightService({
|
|
100
101
|
runRipgrep: createRipgrepRunner(),
|
|
101
102
|
structure: createCtagsStructureProvider(),
|
|
103
|
+
vectorSearchEnabled,
|
|
102
104
|
semanticJudge: resolvedSemanticJudge,
|
|
103
105
|
});
|
|
104
106
|
}
|
package/src/model-error.ts
CHANGED
|
@@ -73,19 +73,14 @@ export interface RequestContractProjection {
|
|
|
73
73
|
text: string;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function projectRequestContract(
|
|
77
|
-
projected: RequestContractDetails,
|
|
78
|
-
serialized: string,
|
|
79
|
-
message: string,
|
|
80
|
-
): string {
|
|
81
|
-
const prefix = `sift-light failed: request rejected [${projected.code}]: ${message}`;
|
|
76
|
+
function projectRequestContract(projected: RequestContractDetails, serialized: string): string {
|
|
82
77
|
const recovery = projected.recovery;
|
|
83
78
|
const next = recovery.nextRequest
|
|
84
79
|
? "\nCopy the nested recovery.nextRequest object unchanged; do not repeat the original query."
|
|
85
|
-
: `\nRecovery action: ${recovery.action}
|
|
80
|
+
: `\nRecovery action: ${recovery.action}.`;
|
|
86
81
|
// Put the machine payload first so simple host adapters that locate the
|
|
87
82
|
// first `nextRequest` marker see the exact nested request object.
|
|
88
|
-
return `\nError details: ${serialized}
|
|
83
|
+
return `\nsift-light failed: request rejected [${projected.code}].\nError details: ${serialized}${next}`;
|
|
89
84
|
}
|
|
90
85
|
|
|
91
86
|
/** Build the one bounded contract projection consumed by structured and text hosts. */
|
|
@@ -94,11 +89,10 @@ export function requestContractProjection(
|
|
|
94
89
|
): RequestContractProjection {
|
|
95
90
|
const details = boundedRequestContractDetails(error.details);
|
|
96
91
|
const serializedDetails = JSON.stringify(details);
|
|
97
|
-
const
|
|
98
|
-
const result = projectRequestContract(details, serializedDetails, boundedMessage);
|
|
92
|
+
const result = projectRequestContract(details, serializedDetails);
|
|
99
93
|
if (Buffer.byteLength(result) <= MAX_REQUEST_RECOVERY_BYTES) return { details, text: result };
|
|
100
94
|
const compact: RequestContractDetails = {
|
|
101
|
-
code:
|
|
95
|
+
code: details.code,
|
|
102
96
|
...(details.mode ? { mode: details.mode } : {}),
|
|
103
97
|
issues: [
|
|
104
98
|
{
|
|
@@ -111,10 +105,6 @@ export function requestContractProjection(
|
|
|
111
105
|
reason: "Exact recovery was omitted; correct the request explicitly.",
|
|
112
106
|
},
|
|
113
107
|
};
|
|
114
|
-
const compactText = projectRequestContract(
|
|
115
|
-
compact,
|
|
116
|
-
JSON.stringify(compact),
|
|
117
|
-
"The request-contract error exceeded the bounded payload budget; correct the request explicitly.",
|
|
118
|
-
);
|
|
108
|
+
const compactText = projectRequestContract(compact, JSON.stringify(compact));
|
|
119
109
|
return { details: compact, text: compactText };
|
|
120
110
|
}
|
package/src/omp-index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import {
|
|
4
|
-
DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
5
4
|
normalizeSearchEnforcement,
|
|
6
5
|
readSiftLightConfigFile,
|
|
7
6
|
resolveSiftLightConfigPath,
|
|
@@ -27,7 +26,7 @@ import {
|
|
|
27
26
|
} from "./search-policy.js";
|
|
28
27
|
import { siftLightSchema } from "./tool-schema.js";
|
|
29
28
|
import { modelErrorText } from "./model-error.js";
|
|
30
|
-
import {
|
|
29
|
+
import { createConfiguredSemanticJudgeIntegration } from "./semantic-judge.js";
|
|
31
30
|
|
|
32
31
|
const SIFT_LIGHT_LABEL = "sift-light";
|
|
33
32
|
const OMP_REPLACED_SEARCH_TOOLS = new Set(["grep", "glob"]);
|
|
@@ -188,13 +187,12 @@ export async function registerOmpSiftLightExtension(
|
|
|
188
187
|
(await readSiftLightConfigFile(resolveSiftLightConfigPath(ompAgentDir()), {
|
|
189
188
|
missing: process.env[SIFT_LIGHT_CONFIG_ENV]?.trim() ? "error" : "defaults",
|
|
190
189
|
}));
|
|
191
|
-
const semanticJudge =
|
|
192
|
-
resolvedConfig.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
193
|
-
);
|
|
190
|
+
const semanticJudge = createConfiguredSemanticJudgeIntegration(resolvedConfig);
|
|
194
191
|
const runtime = new SiftLightRuntime(
|
|
195
192
|
new SiftLightService({
|
|
196
193
|
runRipgrep: createRipgrepRunner(),
|
|
197
194
|
structure: createCtagsStructureProvider(),
|
|
195
|
+
vectorSearchEnabled: resolvedConfig.vectorSearchEnabled === true,
|
|
198
196
|
semanticJudge,
|
|
199
197
|
}),
|
|
200
198
|
);
|
|
@@ -218,7 +216,7 @@ export async function registerOmpSiftLightExtension(
|
|
|
218
216
|
name: SIFT_LIGHT_LABEL,
|
|
219
217
|
label: SIFT_LIGHT_LABEL,
|
|
220
218
|
description:
|
|
221
|
-
"Search
|
|
219
|
+
"Search code with bounded evidence. Routine searches use pattern for fast exact content or mode=files with query for filenames; concept/hybrid require vectorSearchEnabled:true in sift-light.json.",
|
|
222
220
|
approval: "read",
|
|
223
221
|
promptSnippet: "Search file contents without flooding context",
|
|
224
222
|
promptGuidelines: siftLightPromptGuidelines(),
|
package/src/prompt-guidelines.ts
CHANGED
|
@@ -5,7 +5,7 @@ const SOURCE_OUTPUT_GUIDANCE =
|
|
|
5
5
|
|
|
6
6
|
export function siftLightPromptGuidelines(structuredOutput = true): string[] {
|
|
7
7
|
return [
|
|
8
|
-
`Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
|
|
8
|
+
`Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} For routine development searches, start with fast exact content, filename or applicable structural modes when the request has a usable name, symbol, error text or other literal clue. Omitted mode is ordinary exact search and never loads the local embedding model. Vector search is disabled by default; concept/hybrid require vectorSearchEnabled:true in sift-light.json and an installed model. They can take tens of seconds on an uncached scope, so select them only when semantic recall is needed. Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
|
|
9
9
|
`An omitted path searches the project cwd. Use scope:"strict" for a question restricted to one path; otherwise, if an explicit subpath has zero matches, ordinary and content-analysis searches retry from cwd and return project-wide counts with an expansion notice. Explicit absolute paths and .. traversal can search outside cwd, except protected external system areas and .git internals. Git changes mode remains cwd-scoped.`,
|
|
10
10
|
`Search output includes counts, categories, ranked paths, coverage and continuation metadata. Source excerpts may contain the searched text. Use mode="inspect" or the host read capability when exact source is required for an edit or verification.`,
|
|
11
11
|
`Use file and directory distributions to choose evidence. Reuse the visible cursor with path or paths for match metadata; mode="summary" pages the remaining file statistics. Match counts are not relevance scores.`,
|
|
@@ -14,7 +14,7 @@ export function siftLightPromptGuidelines(structuredOutput = true): string[] {
|
|
|
14
14
|
`Use anyOf:["term1","term2"] when every exact occurrence of 2-64 literals is needed in one version-bound result. It is case-sensitive, reports anonymized condition counts, and runs requests above eight terms as bounded parallel chunks. Large condition inventories have separate continuation pages; copy those requests to retrieve the complete counts.`,
|
|
15
15
|
`For a changed-code question, add changes:{base:"HEAD",scope:"lines",side:"new"}; omit target for the working tree, use side:"old" for deleted-side statistics. Copy returned continuation requests to preserve source versions.`,
|
|
16
16
|
`Use mode:"capabilities" when the language or requested operation is unclear to get a compact lazy inventory. Use mode:"outline" with a concrete source file path for symbol counts and locations, mode:"imports" for static relationships, and mode:"tests" for related-test candidates. Their text pages summarize metadata; structured details can retain source evidence.`,
|
|
17
|
-
`Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; business concepts
|
|
17
|
+
`Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; use hybrid/concept only for business concepts that cannot be located by a literal clue. Use wholeWord:true for a single-pattern whole-word search. exclude contains file globs, not content negation.`,
|
|
18
18
|
`Use mode:"structure" only for JS/TS/TSX/Go, with a required nonempty ast-grep pattern such as "compare($X, $X)" or "send()" for code shapes across whitespace; the text page reports structural counts and locations; structured details can retain matched source evidence.`,
|
|
19
19
|
`Use mode:"concept" plus a natural-language query when names are unknown. It runs a pinned local multilingual model only after explicit installation; no search downloads weights or sends code to a remote model. Results expose candidate counts, score statistics, paths and the bounded ranked passage behind each candidate. Similarity scores identify candidates, not correctness.`,
|
|
20
20
|
`Use mode:"hybrid" plus query when wording may differ from the source. It reports exact and semantic counts separately, removes overlap, and retains one pageable evidence snapshot. conceptLimit changes only the semantic candidate count; retained candidates keep their bounded source passages.`,
|
|
@@ -28,9 +28,9 @@ export function siftLightPromptGuidelines(structuredOutput = true): string[] {
|
|
|
28
28
|
|
|
29
29
|
function siftLightModelGuidelines(): string[] {
|
|
30
30
|
return [
|
|
31
|
-
`Search with pattern and optional path. ${SOURCE_OUTPUT_GUIDANCE}
|
|
32
|
-
`Use ranked paths and
|
|
33
|
-
`Modes:
|
|
31
|
+
`Search with pattern and optional path. ${SOURCE_OUTPUT_GUIDANCE} Default search is exact and model-free. concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model; uncached runs may take tens of seconds. Omit mode/limit for auto pages.`,
|
|
32
|
+
`Use ranked paths and counts; Cursor continuation pages retained results. Use mode="inspect" for exact source before editing.`,
|
|
33
|
+
`Modes: files+query for filenames; anyOf/allOf for literals; outline/imports/tests for static code; structure for AST; concept/hybrid for semantic candidates. Similarity is not proof.`,
|
|
34
34
|
`On rejection keep the strongest applicable mode and apply the stated repair once. Do not repeat the rejected request or include its error. Only explicit capability-unavailable permits a visibly partial alternative.`,
|
|
35
35
|
];
|
|
36
36
|
}
|
package/src/request-contract.ts
CHANGED
|
@@ -115,6 +115,23 @@ function schemaError(
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function plainFilePatternRecovery(
|
|
119
|
+
mode: SiftLightMode,
|
|
120
|
+
input: Record<string, unknown>,
|
|
121
|
+
invalid: readonly string[],
|
|
122
|
+
): boolean {
|
|
123
|
+
return (
|
|
124
|
+
mode === "files" &&
|
|
125
|
+
invalid.includes("pattern") &&
|
|
126
|
+
input.query === undefined &&
|
|
127
|
+
typeof input.pattern === "string" &&
|
|
128
|
+
input.pattern.trim().length > 0 &&
|
|
129
|
+
input.pattern.length <= 256 &&
|
|
130
|
+
input.pattern.isWellFormed() &&
|
|
131
|
+
!/[\\^$.*+?()[\]{}|\r\n\0]/u.test(input.pattern)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
118
135
|
function safeNextRequest(
|
|
119
136
|
input: Record<string, unknown>,
|
|
120
137
|
mode: SiftLightMode,
|
|
@@ -122,9 +139,12 @@ function safeNextRequest(
|
|
|
122
139
|
): Record<string, unknown> | undefined {
|
|
123
140
|
if (input.redact === true && containsSensitiveText(input)) return undefined;
|
|
124
141
|
const safe = new Set<string>(SAFE_DROP_FIELDS[mode] ?? []);
|
|
142
|
+
const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
|
|
143
|
+
if (plainFilePattern) safe.add("pattern");
|
|
125
144
|
if (invalid.some((field) => !safe.has(field))) return undefined;
|
|
126
145
|
const next: Record<string, unknown> = { ...input };
|
|
127
146
|
for (const field of invalid) delete next[field];
|
|
147
|
+
if (plainFilePattern) next.query = input.pattern;
|
|
128
148
|
if (selectorIssuesFor(next, mode).length > 0) return undefined;
|
|
129
149
|
// A copied request must remain within the transport's bounded line budget.
|
|
130
150
|
try {
|
|
@@ -165,7 +185,14 @@ function fieldsError(
|
|
|
165
185
|
// the signature of arguments shaped as a per-mode object, which the flat
|
|
166
186
|
// schema never advertises. Naming the accepted fields replaces an otherwise
|
|
167
187
|
// puzzling rejection with the rule and the valid names.
|
|
168
|
-
const
|
|
188
|
+
const nestedModeObject = invalid.includes(mode);
|
|
189
|
+
const flatRule = nestedModeObject
|
|
190
|
+
? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.`
|
|
191
|
+
: "";
|
|
192
|
+
const filesPatternRule =
|
|
193
|
+
mode === "files" && invalid.includes("pattern")
|
|
194
|
+
? "For filename or path discovery, put the literal name text in query; pattern is a content-search regex, so check any regex syntax before copying it."
|
|
195
|
+
: "";
|
|
169
196
|
return new RequestContractError(
|
|
170
197
|
{
|
|
171
198
|
code: "E_MODE_FIELDS",
|
|
@@ -174,17 +201,26 @@ function fieldsError(
|
|
|
174
201
|
recovery: nextRequest
|
|
175
202
|
? {
|
|
176
203
|
action: "retry",
|
|
177
|
-
reason:
|
|
204
|
+
reason: plainFilePatternRecovery(mode, input, invalid)
|
|
205
|
+
? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved."
|
|
206
|
+
: `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
|
|
178
207
|
nextRequest,
|
|
179
208
|
}
|
|
180
209
|
: {
|
|
181
210
|
action: "manual",
|
|
182
|
-
reason:
|
|
211
|
+
reason: [
|
|
212
|
+
reason,
|
|
213
|
+
filesPatternRule ||
|
|
214
|
+
"Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
|
|
215
|
+
flatRule,
|
|
216
|
+
]
|
|
217
|
+
.filter(Boolean)
|
|
218
|
+
.join(" "),
|
|
183
219
|
},
|
|
184
220
|
},
|
|
185
221
|
nextRequest
|
|
186
|
-
? `${reason};
|
|
187
|
-
: `${reason}; no semantics-preserving automatic request is available. ${
|
|
222
|
+
? `${reason}; ${plainFilePatternRecovery(mode, input, invalid) ? "move plain filename text to query" : "retry the exact nextRequest"}.`
|
|
223
|
+
: `${reason}; no semantics-preserving automatic request is available. ${filesPatternRule || flatRule || "Check the mode's accepted fields."}`,
|
|
188
224
|
);
|
|
189
225
|
}
|
|
190
226
|
|
package/src/semantic-judge.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
3
|
+
type SemanticJudgeConfig,
|
|
4
|
+
type SiftLightConfig,
|
|
5
|
+
} from "./config-reader.js";
|
|
2
6
|
import {
|
|
3
7
|
SEMANTIC_JUDGE_CLASSIFICATIONS,
|
|
4
8
|
SEMANTIC_JUDGE_NON_PROOF_CLAIM,
|
|
@@ -134,8 +138,7 @@ function requestBody(
|
|
|
134
138
|
for (const candidate of candidates) {
|
|
135
139
|
questions[candidate.id] = {
|
|
136
140
|
type: "choice",
|
|
137
|
-
instructions:
|
|
138
|
-
"Classify the candidate by what it actually does for the requested behavior. Judge the code excerpt, not just matching words.",
|
|
141
|
+
instructions: `Classify only candidate ${candidate.id} in state.candidates for state.query. Judge that candidate's excerpt by its actual behavior, not by matching words or the other candidates.`,
|
|
139
142
|
criteria: {
|
|
140
143
|
"implementation-candidate":
|
|
141
144
|
"The excerpt appears to implement the requested behavior or its core decision/side effect.",
|
|
@@ -374,6 +377,17 @@ export function createSemanticJudgeIntegration(
|
|
|
374
377
|
}
|
|
375
378
|
return { config, runner: createJevRunner(config, key, fetcher) };
|
|
376
379
|
}
|
|
380
|
+
|
|
381
|
+
export function createConfiguredSemanticJudgeIntegration(
|
|
382
|
+
config: SiftLightConfig,
|
|
383
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
384
|
+
): SemanticJudgeIntegration {
|
|
385
|
+
const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
|
|
386
|
+
return config.vectorSearchEnabled === true
|
|
387
|
+
? createSemanticJudgeIntegration(judge, environment)
|
|
388
|
+
: createDisabledSemanticJudgeIntegration(judge);
|
|
389
|
+
}
|
|
390
|
+
|
|
377
391
|
function baseDetails(config: SemanticJudgeConfig): SemanticJudgeDetails {
|
|
378
392
|
return {
|
|
379
393
|
enabled: config.enabled,
|
package/src/service.ts
CHANGED
|
@@ -87,6 +87,7 @@ export interface SiftLightServiceOptions {
|
|
|
87
87
|
snapshots?: SnapshotStore;
|
|
88
88
|
summaryFileLimit?: number;
|
|
89
89
|
structure?: CodeStructureProvider;
|
|
90
|
+
vectorSearchEnabled?: boolean;
|
|
90
91
|
conceptSearch?: ConceptSearchRunner;
|
|
91
92
|
semanticJudge?: SemanticJudgeIntegration;
|
|
92
93
|
}
|
|
@@ -367,6 +368,7 @@ export class SiftLightService {
|
|
|
367
368
|
readonly #runRipgrep: RipgrepRunner;
|
|
368
369
|
readonly #snapshots: SnapshotStore;
|
|
369
370
|
readonly #summaryFileLimit: number;
|
|
371
|
+
readonly #vectorSearchEnabled: boolean;
|
|
370
372
|
readonly #capabilities = new LanguageCapabilityCatalog();
|
|
371
373
|
readonly #evidence: EvidenceService;
|
|
372
374
|
#lifecycle = new AbortController();
|
|
@@ -378,6 +380,7 @@ export class SiftLightService {
|
|
|
378
380
|
this.#runRipgrep = options.runRipgrep;
|
|
379
381
|
this.#snapshots = options.snapshots ?? new SnapshotStore();
|
|
380
382
|
this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
|
|
383
|
+
this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
|
|
381
384
|
this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
|
|
382
385
|
this.#evidence = new EvidenceService(
|
|
383
386
|
this.#runRipgrep,
|
|
@@ -396,6 +399,11 @@ export class SiftLightService {
|
|
|
396
399
|
): Promise<SiftLightResult> {
|
|
397
400
|
validateRawSearchInput(input);
|
|
398
401
|
validateRequestContract(input);
|
|
402
|
+
if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
|
|
403
|
+
throw new SiftLightError(
|
|
404
|
+
`${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
399
407
|
let request: Promise<SiftLightResult>;
|
|
400
408
|
if (input.mode === "await" || input.mode === "cancel") {
|
|
401
409
|
request = this.#operationCommand(input, cwd, signal);
|
package/src/tool-schema.ts
CHANGED
|
@@ -37,15 +37,15 @@ function stringEnum<const Values extends readonly string[]>(
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export const SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
|
|
40
|
+
export const SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Routine searches should use exact content, filenames or applicable structural modes first. Omitted mode is ordinary exact search and does not load the embedding model; concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model and may take tens of seconds on an uncached scope. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
|
|
41
41
|
|
|
42
|
-
export const SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
42
|
+
export const SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. Default exact search is model-free; concept/hybrid require vectorSearchEnabled:true in sift-light.json and a model. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
43
43
|
|
|
44
44
|
export const siftLightSchema = Type.Object({
|
|
45
45
|
query: Type.Optional(
|
|
46
46
|
Type.String({
|
|
47
47
|
maxLength: 256,
|
|
48
|
-
description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`,
|
|
48
|
+
description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require vectorSearchEnabled:true in sift-light.json and an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`,
|
|
49
49
|
}),
|
|
50
50
|
),
|
|
51
51
|
scope: Type.Optional(
|