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/mcp-server.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { URL as URL2 } from "node:url";
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "sift-light",
|
|
10
|
-
version: "1.0.
|
|
10
|
+
version: "1.0.2",
|
|
11
11
|
description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
|
|
12
12
|
keywords: [
|
|
13
13
|
"ai-agent",
|
|
@@ -292,7 +292,33 @@ function analysisExtraGroups(counts, termCounts, items) {
|
|
|
292
292
|
function isRecord(value) {
|
|
293
293
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
294
294
|
}
|
|
295
|
+
function compactSemanticMetadata(details, analysis) {
|
|
296
|
+
const counts = analysis.counts;
|
|
297
|
+
const stats = analysis.stats;
|
|
298
|
+
const judge = analysis.semanticJudge;
|
|
299
|
+
const coverage = analysis.coverage;
|
|
300
|
+
return [
|
|
301
|
+
...counts || stats ? [
|
|
302
|
+
`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.`
|
|
303
|
+
] : [],
|
|
304
|
+
...coverage ? [
|
|
305
|
+
`Coverage: ${Object.entries(coverage).map(([name, status]) => `${name}=${status}`).join(", ")}.`
|
|
306
|
+
] : [],
|
|
307
|
+
...analysis.scope ? [`Scope: ${analysis.scope.path}; ignore=${analysis.scope.ignorePolicy}.`] : [],
|
|
308
|
+
...analysis.sourceGeneration ? [
|
|
309
|
+
`Source: ${analysis.sourceGeneration.verification}; ${String(analysis.sourceGeneration.filesUnavailable)} unavailable.`
|
|
310
|
+
] : [],
|
|
311
|
+
...judge ? [
|
|
312
|
+
`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}` : ""}.`
|
|
313
|
+
] : [],
|
|
314
|
+
...details.operation ? [`Operation: ${details.operation.state}; id=${details.operation.id}.`] : [],
|
|
315
|
+
...analysis.reasons.map((reason) => `[${reason}]`),
|
|
316
|
+
...details.redactionApplied ? ["[Display redaction applied.]"] : []
|
|
317
|
+
];
|
|
318
|
+
}
|
|
295
319
|
function compactMetadata(details, analysis) {
|
|
320
|
+
if (analysis.kind === "concept" || analysis.kind === "hybrid")
|
|
321
|
+
return compactSemanticMetadata(details, analysis);
|
|
296
322
|
return [
|
|
297
323
|
...analysis.statistics ? formatStatistics(analysis.statistics) : [],
|
|
298
324
|
analysis.counts ? `Counts: ${JSON.stringify(analysis.counts)}` : undefined,
|
|
@@ -320,7 +346,7 @@ function compactRows(analysis) {
|
|
|
320
346
|
rows.push(JSON.stringify(item.path));
|
|
321
347
|
previousPath = item.path;
|
|
322
348
|
}
|
|
323
|
-
const label = analysis.kind === "outline" && analysis.modelOutput ? item.label : "metadata";
|
|
349
|
+
const label = analysis.kind === "outline" && analysis.modelOutput || analysis.kind === "files" ? item.label : "metadata";
|
|
324
350
|
const semanticJudge = item.details?.semanticJudge;
|
|
325
351
|
const judgment = isRecord(semanticJudge) && typeof semanticJudge.classification === "string" && typeof semanticJudge.probability === "number" && typeof semanticJudge.model === "string" ? ` Jev: ${semanticJudge.classification}; probability=${String(semanticJudge.probability)};${typeof semanticJudge.confidence === "number" ? ` confidence=${String(semanticJudge.confidence)};` : ""} model=${JSON.stringify(semanticJudge.model)}.` : "";
|
|
326
352
|
rows.push(`#${String(item.index)} L${String(item.line)} ${label}${judgment}`);
|
|
@@ -360,6 +386,7 @@ function compactMcpModelText(result) {
|
|
|
360
386
|
const inspect = compactInspectInstruction(analysis);
|
|
361
387
|
const nextRequest = distinctNextRequest(result.details, analysis);
|
|
362
388
|
const compact = [
|
|
389
|
+
...(result.details.requestNotes ?? []).map((note) => `[Request note: ${note}]`),
|
|
363
390
|
header,
|
|
364
391
|
...compactMetadata(result.details, analysis),
|
|
365
392
|
...compactRows(analysis),
|
|
@@ -391,7 +418,7 @@ var MAX_ANALYSIS_STORAGE_BYTES = 32 * 1024 * 1024;
|
|
|
391
418
|
var ANALYSIS_METADATA_RESERVE_BYTES = 64 * 1024;
|
|
392
419
|
var MAX_ANALYSIS_REASONS = 64;
|
|
393
420
|
var MAX_ANALYSIS_REASON_BYTES = 4 * 1024;
|
|
394
|
-
var MIN_ANY_OF_TERMS =
|
|
421
|
+
var MIN_ANY_OF_TERMS = 1;
|
|
395
422
|
var MAX_ANY_OF_TERMS = 8;
|
|
396
423
|
var MAX_ANY_OF_TOTAL_TERMS = 64;
|
|
397
424
|
var MAX_LITERAL_TERM_BYTES = 256;
|
|
@@ -411,6 +438,7 @@ var ESTIMATED_CHARACTERS_PER_TOKEN = 4;
|
|
|
411
438
|
var DEFAULT_SUMMARY_FILE_LIMIT = 30;
|
|
412
439
|
var MAX_SELECTED_PATHS = 20;
|
|
413
440
|
var MAX_INSPECT_TARGETS = 5;
|
|
441
|
+
var MAX_INSPECT_REQUEST_TARGETS = 20;
|
|
414
442
|
var MAX_DISPLAYED_OCCURRENCES = 20;
|
|
415
443
|
var MAX_STORED_MATCHES = 50000;
|
|
416
444
|
var MAX_STORED_OCCURRENCES = 200000;
|
|
@@ -432,6 +460,7 @@ var PRIVATE_KEY = /-----BEGIN ([^-\r\n]*PRIVATE KEY)-----[\s\S]*?-----END \1----
|
|
|
432
460
|
var SENSITIVE_NAME = String.raw`(?:(?:[A-Za-z][A-Za-z0-9]*[_-])*(?:password|passwd|secret|token|api[_-]?key|access[_-]?(?:key|token)|secret[_-]?access[_-]?key|private[_-]?key|service[_-]?key)(?:[_-][A-Za-z0-9]+)*)`;
|
|
433
461
|
var SENSITIVE_ASSIGNMENT = new RegExp(String.raw`((?<![A-Za-z0-9_-])(?:["']?${SENSITIVE_NAME}["']?)\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}\r\n]+)`, "gi");
|
|
434
462
|
var SENSITIVE_TOKEN = /\b(?:sk|ghp|xox[baprs])[-_][A-Za-z0-9][A-Za-z0-9_-]{7,}\b/g;
|
|
463
|
+
var CODE_EXPRESSION = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\s*[([]/u;
|
|
435
464
|
var TYPE_ONLY_VALUES = new Set([
|
|
436
465
|
"boolean",
|
|
437
466
|
"number",
|
|
@@ -451,10 +480,11 @@ function redactString(value) {
|
|
|
451
480
|
});
|
|
452
481
|
redacted = redacted.replace(SENSITIVE_ASSIGNMENT, (match, prefix, rawValue) => {
|
|
453
482
|
const unquoted = rawValue.replace(/^["']|["']$/g, "").toLowerCase();
|
|
454
|
-
if (TYPE_ONLY_VALUES.has(unquoted))
|
|
483
|
+
if (TYPE_ONLY_VALUES.has(unquoted) || CODE_EXPRESSION.test(rawValue))
|
|
455
484
|
return match;
|
|
456
485
|
count += 1;
|
|
457
|
-
|
|
486
|
+
const quote = rawValue.startsWith('"') || rawValue.startsWith("'") ? rawValue[0] : "";
|
|
487
|
+
return `${prefix}${quote}[REDACTED]${quote}`;
|
|
458
488
|
});
|
|
459
489
|
redacted = redacted.replace(SENSITIVE_TOKEN, () => {
|
|
460
490
|
count += 1;
|
|
@@ -712,6 +742,15 @@ var PYTHON_LANGUAGE_CAPABILITIES = [
|
|
|
712
742
|
evidence: "syntax",
|
|
713
743
|
availability: "implemented",
|
|
714
744
|
load: "lazy"
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
id: "python-lexical.roles",
|
|
748
|
+
name: "roles",
|
|
749
|
+
provider: "bounded Python lexical role scanner",
|
|
750
|
+
providerKind: "builtin",
|
|
751
|
+
evidence: "syntax",
|
|
752
|
+
availability: "implemented",
|
|
753
|
+
load: "lazy"
|
|
715
754
|
}
|
|
716
755
|
];
|
|
717
756
|
var DEFAULT_LANGUAGE_CAPABILITIES = [
|
|
@@ -839,7 +878,17 @@ var MODE_FIELDS_BY_MODE = {
|
|
|
839
878
|
auto: ordinaryFields,
|
|
840
879
|
summary: ordinaryFields,
|
|
841
880
|
matches: ordinaryFields,
|
|
842
|
-
inspect: [
|
|
881
|
+
inspect: [
|
|
882
|
+
...commonFields,
|
|
883
|
+
"path",
|
|
884
|
+
"paths",
|
|
885
|
+
"line",
|
|
886
|
+
"cursor",
|
|
887
|
+
"matchIndex",
|
|
888
|
+
"matchIndices",
|
|
889
|
+
"targets",
|
|
890
|
+
"scope"
|
|
891
|
+
],
|
|
843
892
|
outline: [...commonFields, "path", "line", "symbol", "cursor", "matchIndex", "maxFilesToParse"],
|
|
844
893
|
imports: [
|
|
845
894
|
...commonFields,
|
|
@@ -859,7 +908,16 @@ var MODE_FIELDS_BY_MODE = {
|
|
|
859
908
|
"matchIndex",
|
|
860
909
|
"maxFilesToParse"
|
|
861
910
|
],
|
|
862
|
-
files: [
|
|
911
|
+
files: [
|
|
912
|
+
...commonFields,
|
|
913
|
+
"query",
|
|
914
|
+
...sourceFilters,
|
|
915
|
+
"ignorePolicy",
|
|
916
|
+
"limit",
|
|
917
|
+
"scope",
|
|
918
|
+
"modifiedAfter",
|
|
919
|
+
"modifiedBefore"
|
|
920
|
+
],
|
|
863
921
|
structure: [...commonFields, "pattern", ...sourceFilters, "maxFilesToParse"],
|
|
864
922
|
concept: [...commonFields, "query", ...sourceFilters, "maxFilesToParse"],
|
|
865
923
|
hybrid: [...commonFields, "query", ...sourceFilters, "conceptLimit", "maxFilesToParse"],
|
|
@@ -868,9 +926,7 @@ var MODE_FIELDS_BY_MODE = {
|
|
|
868
926
|
await: ["mode", "operationId"],
|
|
869
927
|
cancel: ["mode", "operationId"]
|
|
870
928
|
};
|
|
871
|
-
var SAFE_DROP_FIELDS = {
|
|
872
|
-
files: ["scope"]
|
|
873
|
-
};
|
|
929
|
+
var SAFE_DROP_FIELDS = {};
|
|
874
930
|
var SUPPORTED_OUTLINE_EXTENSIONS = new Set(DEFAULT_LANGUAGE_CAPABILITIES.flatMap((descriptor) => descriptor.capabilities.some((capability) => capability.name === "outline") ? descriptor.extensions : []));
|
|
875
931
|
function modeFields(mode) {
|
|
876
932
|
return MODE_FIELDS_BY_MODE[mode];
|
|
@@ -899,8 +955,8 @@ var REQUEST_FIELD_GUIDANCE = {
|
|
|
899
955
|
allOf: "allOf is case-sensitive exact-literal AND; omit pattern, anyOf, literal, ignoreCase, roles",
|
|
900
956
|
context: "context is an output-context budget and is never silently dropped",
|
|
901
957
|
limit: "limit is an output/page budget and is never silently dropped",
|
|
902
|
-
scope: "scope applies to ordinary content search; mode=files
|
|
903
|
-
ignorePolicy: "respect keeps repository ignore rules; include searches ignored files but always excludes .git internals and protected paths",
|
|
958
|
+
scope: "scope applies to ordinary content search; mode=files is always strict, so it accepts only the redundant scope=strict",
|
|
959
|
+
ignorePolicy: "respect keeps repository ignore rules and discloses ignored files; include also searches or lists ignored files but always excludes .git internals and protected paths",
|
|
904
960
|
patterns: "audit accepts named exact-literal patterns and returns one closure receipt",
|
|
905
961
|
maxFilesToParse: "concept/hybrid automatically batch the requested scope; this optional field sets an advanced hard file ceiling"
|
|
906
962
|
};
|
|
@@ -996,15 +1052,26 @@ function schemaError(input, field, reason) {
|
|
|
996
1052
|
recovery: { action: "manual", reason }
|
|
997
1053
|
}, `${field} is invalid: ${reason}`);
|
|
998
1054
|
}
|
|
1055
|
+
function plainFilePatternRecovery(mode, input, invalid) {
|
|
1056
|
+
return mode === "files" && invalid.includes("pattern") && input.query === undefined && typeof input.pattern === "string" && input.pattern.trim().length > 0 && input.pattern.length <= 256 && input.pattern.isWellFormed() && !/[\\^$.*+?()[\]{}|\r\n\0]/u.test(input.pattern);
|
|
1057
|
+
}
|
|
1058
|
+
function matchAllFilePatternRecovery(mode, input, invalid) {
|
|
1059
|
+
return mode === "files" && invalid.includes("pattern") && input.query === undefined && typeof input.pattern === "string" && /^(?:\.\*|\.\+|\*+)$/u.test(input.pattern.trim());
|
|
1060
|
+
}
|
|
999
1061
|
function safeNextRequest(input, mode, invalid) {
|
|
1000
1062
|
if (input.redact === true && containsSensitiveText(input))
|
|
1001
1063
|
return;
|
|
1002
1064
|
const safe = new Set(SAFE_DROP_FIELDS[mode] ?? []);
|
|
1065
|
+
const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
|
|
1066
|
+
if (plainFilePattern || matchAllFilePatternRecovery(mode, input, invalid))
|
|
1067
|
+
safe.add("pattern");
|
|
1003
1068
|
if (invalid.some((field) => !safe.has(field)))
|
|
1004
1069
|
return;
|
|
1005
1070
|
const next = { ...input };
|
|
1006
1071
|
for (const field of invalid)
|
|
1007
1072
|
delete next[field];
|
|
1073
|
+
if (plainFilePattern)
|
|
1074
|
+
next.query = input.pattern;
|
|
1008
1075
|
if (selectorIssuesFor(next, mode).length > 0)
|
|
1009
1076
|
return;
|
|
1010
1077
|
try {
|
|
@@ -1033,20 +1100,26 @@ function fieldsError(input, mode, invalid, selectorIssues = []) {
|
|
|
1033
1100
|
});
|
|
1034
1101
|
const visibleFields = visibleInvalid.map((field) => boundedField(field)).join(", ");
|
|
1035
1102
|
const reason = omitted > 0 ? `mode=${mode} does not accept ${visibleFields} and ${String(omitted)} additional field(s)` : `mode=${mode} does not accept ${visibleFields}`;
|
|
1036
|
-
const
|
|
1103
|
+
const nestedModeObject = invalid.includes(mode);
|
|
1104
|
+
const flatRule = nestedModeObject ? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.` : "";
|
|
1105
|
+
const filesPatternRule = mode === "files" && invalid.includes("pattern") ? "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." : "";
|
|
1037
1106
|
return new RequestContractError({
|
|
1038
1107
|
code: "E_MODE_FIELDS",
|
|
1039
1108
|
mode,
|
|
1040
1109
|
issues,
|
|
1041
1110
|
recovery: nextRequest ? {
|
|
1042
1111
|
action: "retry",
|
|
1043
|
-
reason: `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
|
|
1112
|
+
reason: plainFilePatternRecovery(mode, input, invalid) ? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved." : matchAllFilePatternRecovery(mode, input, invalid) ? "A match-all pattern lists every file; omitting query does that, so copy nextRequest; all filters are preserved." : `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
|
|
1044
1113
|
nextRequest
|
|
1045
1114
|
} : {
|
|
1046
1115
|
action: "manual",
|
|
1047
|
-
reason:
|
|
1116
|
+
reason: [
|
|
1117
|
+
reason,
|
|
1118
|
+
filesPatternRule || "Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
|
|
1119
|
+
flatRule
|
|
1120
|
+
].filter(Boolean).join(" ")
|
|
1048
1121
|
}
|
|
1049
|
-
}, nextRequest ? `${reason};
|
|
1122
|
+
}, nextRequest ? `${reason}; ${plainFilePatternRecovery(mode, input, invalid) ? "move plain filename text to query" : "retry the exact nextRequest"}.` : `${reason}; no semantics-preserving automatic request is available. ${filesPatternRule || flatRule || "Check the mode's accepted fields."}`);
|
|
1050
1123
|
}
|
|
1051
1124
|
function selectorIssuesFor(input, mode) {
|
|
1052
1125
|
if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof input.cursor === "string" && input.cursor.trim().length > 0 && !input.cursor.includes(".analysis")) {
|
|
@@ -1192,7 +1265,7 @@ function validateRequestContract(input) {
|
|
|
1192
1265
|
const allowed = new Set(MODE_FIELDS_BY_MODE[mode]);
|
|
1193
1266
|
if (mode === "inspect" && raw.sourceCursor !== undefined) {
|
|
1194
1267
|
allowed.clear();
|
|
1195
|
-
for (const field of ["mode", "sourceCursor", "redact"])
|
|
1268
|
+
for (const field of ["mode", "sourceCursor", "redact", "path"])
|
|
1196
1269
|
allowed.add(field);
|
|
1197
1270
|
}
|
|
1198
1271
|
if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof raw.cursor === "string" && raw.cursor.includes(".analysis")) {
|
|
@@ -1282,25 +1355,23 @@ function modelErrorText(error) {
|
|
|
1282
1355
|
function requestContractErrorText(error) {
|
|
1283
1356
|
return requestContractProjection(error).text;
|
|
1284
1357
|
}
|
|
1285
|
-
function projectRequestContract(projected, serialized
|
|
1286
|
-
const prefix = `sift-light failed: request rejected [${projected.code}]: ${message}`;
|
|
1358
|
+
function projectRequestContract(projected, serialized) {
|
|
1287
1359
|
const recovery = projected.recovery;
|
|
1288
1360
|
const next = recovery.nextRequest ? `
|
|
1289
1361
|
Copy the nested recovery.nextRequest object unchanged; do not repeat the original query.` : `
|
|
1290
|
-
Recovery action: ${recovery.action}
|
|
1362
|
+
Recovery action: ${recovery.action}.`;
|
|
1291
1363
|
return `
|
|
1292
|
-
|
|
1293
|
-
${
|
|
1364
|
+
sift-light failed: request rejected [${projected.code}].
|
|
1365
|
+
Error details: ${serialized}${next}`;
|
|
1294
1366
|
}
|
|
1295
1367
|
function requestContractProjection(error) {
|
|
1296
1368
|
const details = boundedRequestContractDetails(error.details);
|
|
1297
1369
|
const serializedDetails = JSON.stringify(details);
|
|
1298
|
-
const
|
|
1299
|
-
const result = projectRequestContract(details, serializedDetails, boundedMessage);
|
|
1370
|
+
const result = projectRequestContract(details, serializedDetails);
|
|
1300
1371
|
if (Buffer.byteLength(result) <= MAX_REQUEST_RECOVERY_BYTES)
|
|
1301
1372
|
return { details, text: result };
|
|
1302
1373
|
const compact = {
|
|
1303
|
-
code:
|
|
1374
|
+
code: details.code,
|
|
1304
1375
|
...details.mode ? { mode: details.mode } : {},
|
|
1305
1376
|
issues: [
|
|
1306
1377
|
{
|
|
@@ -1313,7 +1384,7 @@ function requestContractProjection(error) {
|
|
|
1313
1384
|
reason: "Exact recovery was omitted; correct the request explicitly."
|
|
1314
1385
|
}
|
|
1315
1386
|
};
|
|
1316
|
-
const compactText = projectRequestContract(compact, JSON.stringify(compact)
|
|
1387
|
+
const compactText = projectRequestContract(compact, JSON.stringify(compact));
|
|
1317
1388
|
return { details: compact, text: compactText };
|
|
1318
1389
|
}
|
|
1319
1390
|
|
|
@@ -2819,7 +2890,7 @@ var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
|
|
|
2819
2890
|
var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
|
|
2820
2891
|
var MAX_CONCEPT_CHARS = 1000;
|
|
2821
2892
|
var CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
|
|
2822
|
-
var CONCEPT_CACHE_VERSION =
|
|
2893
|
+
var CONCEPT_CACHE_VERSION = 2;
|
|
2823
2894
|
var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
2824
2895
|
var CONCEPT_TIMEOUT_MS = 10 * 60000;
|
|
2825
2896
|
var MIN_CONCEPT_TIMEOUT_MS = 1000;
|
|
@@ -2910,7 +2981,7 @@ function normalizeRequest(input) {
|
|
|
2910
2981
|
throw new SiftLightError("ignorePolicy must be respect or include");
|
|
2911
2982
|
const pattern = input.pattern;
|
|
2912
2983
|
if (pattern === undefined) {
|
|
2913
|
-
throw new SiftLightError(
|
|
2984
|
+
throw new SiftLightError('pattern is required when cursor is not provided; to list files use mode="files" (query optional), to read a known file use mode="inspect" with path');
|
|
2914
2985
|
}
|
|
2915
2986
|
const path = input.path?.replace(/^@/, "");
|
|
2916
2987
|
return {
|
|
@@ -5142,6 +5213,11 @@ function scoreProfile(scores) {
|
|
|
5142
5213
|
spread: top - min
|
|
5143
5214
|
};
|
|
5144
5215
|
}
|
|
5216
|
+
function conceptRankingScore(cosine, passageLength) {
|
|
5217
|
+
const referenceLength = 500;
|
|
5218
|
+
const maximumCorrection = 0.02;
|
|
5219
|
+
return cosine - maximumCorrection * (1 - Math.sqrt(Math.min(passageLength / referenceLength, 1)));
|
|
5220
|
+
}
|
|
5145
5221
|
function passage(document, start) {
|
|
5146
5222
|
let end = Math.min(document.text.length, start + MAX_CONCEPT_CHARS);
|
|
5147
5223
|
if (end < document.text.length) {
|
|
@@ -5400,6 +5476,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
5400
5476
|
const similarity = inferred.scores[index];
|
|
5401
5477
|
if (similarity === undefined)
|
|
5402
5478
|
throw new Error("Missing concept similarity");
|
|
5479
|
+
const rankingScore = conceptRankingScore(similarity, item.text.length);
|
|
5403
5480
|
const evidence = rangeEvidence(item.document, item.range);
|
|
5404
5481
|
return {
|
|
5405
5482
|
path: item.document.path,
|
|
@@ -5412,7 +5489,8 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
5412
5489
|
kind: "concept-candidate",
|
|
5413
5490
|
certainty: "candidate",
|
|
5414
5491
|
score: similarity,
|
|
5415
|
-
|
|
5492
|
+
rankingScore,
|
|
5493
|
+
rankingReason: "local multilingual E5 cosine similarity with bounded short-passage rank correction; relevance candidate, no binding or execution claim",
|
|
5416
5494
|
model: CONCEPT_MODEL,
|
|
5417
5495
|
revision: CONCEPT_REVISION,
|
|
5418
5496
|
tokenTruncated: false,
|
|
@@ -5421,7 +5499,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
5421
5499
|
}
|
|
5422
5500
|
};
|
|
5423
5501
|
});
|
|
5424
|
-
const ranked = [...result.items, ...batchItems].toSorted((a, b) => Number(b.details?.
|
|
5502
|
+
const ranked = [...result.items, ...batchItems].toSorted((a, b) => Number(b.details?.rankingScore) - Number(a.details?.rankingScore) || a.path.localeCompare(b.path) || a.line - b.line);
|
|
5425
5503
|
if (ranked.length > MAX_ANALYSIS_RESULTS)
|
|
5426
5504
|
retentionTruncated = true;
|
|
5427
5505
|
result.items = ranked.slice(0, MAX_ANALYSIS_RESULTS);
|
|
@@ -5827,7 +5905,7 @@ function publicAnalysisItem(result, item, index, storedId, modelOutput) {
|
|
|
5827
5905
|
return {
|
|
5828
5906
|
path: item.path,
|
|
5829
5907
|
line: item.line,
|
|
5830
|
-
label: result.kind === "outline" && modelOutput ? item.label : publicAnalysisLabel(result),
|
|
5908
|
+
label: result.kind === "outline" && modelOutput || result.kind === "files" ? item.label : publicAnalysisLabel(result),
|
|
5831
5909
|
index: index + 1,
|
|
5832
5910
|
...inspect ? { inspect } : {},
|
|
5833
5911
|
...publicDetails ? { details: publicDetails } : {}
|
|
@@ -6038,7 +6116,8 @@ Inspect: ${JSON.stringify(inspect)}` : ""}`;
|
|
|
6038
6116
|
break;
|
|
6039
6117
|
}
|
|
6040
6118
|
} else {
|
|
6041
|
-
|
|
6119
|
+
const pageSize = result.pageSize ?? 30;
|
|
6120
|
+
for (let index = offset;index < result.items.length && items.length < pageSize; index += 1) {
|
|
6042
6121
|
if (!appendItem(index))
|
|
6043
6122
|
break;
|
|
6044
6123
|
}
|
|
@@ -6152,6 +6231,8 @@ function resolveInspectionTarget(input, cwd, snapshots) {
|
|
|
6152
6231
|
}
|
|
6153
6232
|
if (!path)
|
|
6154
6233
|
throw new SiftLightError("path is required when mode=inspect");
|
|
6234
|
+
if (line === undefined && input.cursor === undefined)
|
|
6235
|
+
line = 1;
|
|
6155
6236
|
if (line === undefined || !Number.isSafeInteger(line) || line < 1) {
|
|
6156
6237
|
throw new SiftLightError("line must be a positive integer when mode=inspect");
|
|
6157
6238
|
}
|
|
@@ -7880,6 +7961,28 @@ async function filterPathsByModificationTime(cwd, paths, modifiedAfterMs, modifi
|
|
|
7880
7961
|
// src/file-discovery.ts
|
|
7881
7962
|
var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
7882
7963
|
var FILE_QUERY_GLOB_WILDCARD = /[*?]/u;
|
|
7964
|
+
var FILE_QUERY_MATCH_ALL = /^(?:[*/]+|\.\*|\.\+|\*\.\*)$/u;
|
|
7965
|
+
var FILE_QUERY_REGEX_SYNTAX = /[()|^$\\+]/u;
|
|
7966
|
+
var IGNORED_MATCH_SAMPLES = 5;
|
|
7967
|
+
function normalizeFileQuery(query, hasGlob) {
|
|
7968
|
+
const trimmed = query.trim();
|
|
7969
|
+
if (FILE_QUERY_MATCH_ALL.test(trimmed))
|
|
7970
|
+
return {
|
|
7971
|
+
query: "",
|
|
7972
|
+
note: `Query ${JSON.stringify(query)} was treated as listing every file under path; omit query for the same result.`
|
|
7973
|
+
};
|
|
7974
|
+
if (!FILE_QUERY_GLOB_WILDCARD.test(trimmed))
|
|
7975
|
+
return { query };
|
|
7976
|
+
if (!hasGlob && !FILE_QUERY_REGEX_SYNTAX.test(trimmed) && !/\s/u.test(trimmed)) {
|
|
7977
|
+
const glob = trimmed.includes("/") ? trimmed : `**/${trimmed}`;
|
|
7978
|
+
return {
|
|
7979
|
+
query: "",
|
|
7980
|
+
glob,
|
|
7981
|
+
note: `Query ${JSON.stringify(query)} contains glob wildcards and was applied as glob ${JSON.stringify(glob)}; mode=files query otherwise matches filename text.`
|
|
7982
|
+
};
|
|
7983
|
+
}
|
|
7984
|
+
throw new SiftLightError(`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.`);
|
|
7985
|
+
}
|
|
7883
7986
|
function subsequenceScore(text, query) {
|
|
7884
7987
|
const characters = Array.from(graphemes.segment(text), (item) => item.segment);
|
|
7885
7988
|
const queryCharacters = Array.from(graphemes.segment(query), (item) => item.segment);
|
|
@@ -7930,39 +8033,79 @@ function pathRelativeToDiscoveryRoot(cwd, root, path) {
|
|
|
7930
8033
|
return scoped || platformBasename(absolutePath);
|
|
7931
8034
|
}
|
|
7932
8035
|
async function discoverFiles(input, cwd, signal) {
|
|
7933
|
-
const
|
|
7934
|
-
if (
|
|
8036
|
+
const rawQuery = input.query ?? "";
|
|
8037
|
+
if (rawQuery.length > 256 || !rawQuery.isWellFormed() || /[\r\n\0]/.test(rawQuery))
|
|
7935
8038
|
throw new SiftLightError("File query must be well-formed single-line text of at most 256 characters");
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
const
|
|
8039
|
+
const requestedGlobs = input.glob === undefined ? [] : Array.isArray(input.glob) ? input.glob : [input.glob];
|
|
8040
|
+
const normalizedQuery = normalizeFileQuery(rawQuery, requestedGlobs.length > 0);
|
|
8041
|
+
const query = normalizedQuery.query;
|
|
8042
|
+
const request = normalizeRequest({
|
|
8043
|
+
...input,
|
|
8044
|
+
pattern: "",
|
|
8045
|
+
...normalizedQuery.glob ? { glob: [...requestedGlobs, normalizedQuery.glob] } : {}
|
|
8046
|
+
});
|
|
7939
8047
|
const policy = new SearchPathPolicy(cwd);
|
|
7940
8048
|
const discoveryRoot = request.path ?? ".";
|
|
7941
8049
|
const scoringRoot = await policy.resolveSearchTarget(discoveryRoot);
|
|
7942
|
-
const
|
|
8050
|
+
const includeIgnored = request.ignorePolicy === "include";
|
|
8051
|
+
const listOptions = {
|
|
7943
8052
|
path: scoringRoot,
|
|
7944
8053
|
glob: request.glob,
|
|
7945
8054
|
exclude: request.exclude,
|
|
7946
8055
|
hidden: request.hidden
|
|
8056
|
+
};
|
|
8057
|
+
const files = await listWorkspaceFiles(cwd, signal, {
|
|
8058
|
+
...listOptions,
|
|
8059
|
+
...includeIgnored ? { ignore: false, ignoreParents: false } : {}
|
|
7947
8060
|
});
|
|
7948
8061
|
const filtered = await filterPathsByModificationTime(cwd, files.paths, request.modifiedAfterMs, request.modifiedBeforeMs, signal);
|
|
8062
|
+
const rank = (path) => scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
|
|
7949
8063
|
const selected = filtered.paths.flatMap((path) => {
|
|
7950
|
-
const
|
|
7951
|
-
return
|
|
8064
|
+
const score = rank(path);
|
|
8065
|
+
return score ? [{ path, ...score }] : [];
|
|
7952
8066
|
}).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
7953
8067
|
for (let offset = 0;offset < selected.length; offset += 16) {
|
|
7954
8068
|
await Promise.all(selected.slice(offset, offset + 16).map((item) => policy.assertExistingPath(item.path)));
|
|
7955
8069
|
signal?.throwIfAborted();
|
|
7956
8070
|
}
|
|
8071
|
+
const reasons = new Set([...files.reasons, ...filtered.reasons]);
|
|
8072
|
+
if (normalizedQuery.note)
|
|
8073
|
+
reasons.add(normalizedQuery.note);
|
|
8074
|
+
let ignoredFiles = 0;
|
|
8075
|
+
let ignoredMatches = 0;
|
|
8076
|
+
let ignoredComparisonPartial = false;
|
|
8077
|
+
if (!includeIgnored) {
|
|
8078
|
+
const everything = await listWorkspaceFiles(cwd, signal, {
|
|
8079
|
+
...listOptions,
|
|
8080
|
+
ignore: false,
|
|
8081
|
+
ignoreParents: false
|
|
8082
|
+
});
|
|
8083
|
+
ignoredComparisonPartial = everything.partial;
|
|
8084
|
+
const admitted = new Set(files.paths);
|
|
8085
|
+
const ignored = everything.paths.filter((path) => !admitted.has(path));
|
|
8086
|
+
ignoredFiles = ignored.length;
|
|
8087
|
+
const ignoredCandidates = ignored.flatMap((path) => {
|
|
8088
|
+
const score = rank(path);
|
|
8089
|
+
return score ? [{ path, ...score }] : [];
|
|
8090
|
+
}).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
8091
|
+
ignoredMatches = ignoredCandidates.length;
|
|
8092
|
+
if (ignoredFiles > 0) {
|
|
8093
|
+
const samples = ignoredCandidates.slice(0, IGNORED_MATCH_SAMPLES).map((item) => `${JSON.stringify(item.path)} (${item.reason})`);
|
|
8094
|
+
reasons.add(ignoredMatches > 0 ? `${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.` : `${String(ignoredFiles)} file(s) were excluded by ignore rules; none of them match this query.`);
|
|
8095
|
+
}
|
|
8096
|
+
if (ignoredComparisonPartial)
|
|
8097
|
+
reasons.add("Ignored-file comparison was incomplete; the ignored-file count is a lower bound");
|
|
8098
|
+
}
|
|
8099
|
+
const enumerationPartial = files.partial || filtered.partial;
|
|
7957
8100
|
return {
|
|
7958
8101
|
kind: "files",
|
|
7959
8102
|
unit: "files",
|
|
7960
|
-
partial:
|
|
7961
|
-
reasons: [...
|
|
8103
|
+
partial: enumerationPartial,
|
|
8104
|
+
reasons: [...reasons],
|
|
7962
8105
|
items: selected.map((item) => ({
|
|
7963
8106
|
path: item.path,
|
|
7964
8107
|
line: 1,
|
|
7965
|
-
label: `File candidate (${item.reason})`,
|
|
8108
|
+
label: `File candidate (score ${String(item.score)}: ${item.reason})`,
|
|
7966
8109
|
details: {
|
|
7967
8110
|
kind: "file",
|
|
7968
8111
|
score: item.score,
|
|
@@ -7970,8 +8113,13 @@ async function discoverFiles(input, cwd, signal) {
|
|
|
7970
8113
|
inspect: { mode: "inspect", path: item.path, line: 1 }
|
|
7971
8114
|
}
|
|
7972
8115
|
})),
|
|
7973
|
-
coverage: {
|
|
7974
|
-
|
|
8116
|
+
coverage: {
|
|
8117
|
+
fileEnumeration: enumerationPartial ? "partial" : ignoredFiles > 0 || ignoredComparisonPartial ? "policy-filtered" : "complete"
|
|
8118
|
+
},
|
|
8119
|
+
stats: {
|
|
8120
|
+
filesEnumerated: files.paths.length,
|
|
8121
|
+
...includeIgnored ? {} : { ignoredFiles, ignoredMatches }
|
|
8122
|
+
},
|
|
7975
8123
|
scope: {
|
|
7976
8124
|
path: request.path ?? ".",
|
|
7977
8125
|
requestedPath: request.path ?? ".",
|
|
@@ -7984,7 +8132,8 @@ async function discoverFiles(input, cwd, signal) {
|
|
|
7984
8132
|
...request.modifiedAfterMs !== undefined ? { modifiedAfterMs: request.modifiedAfterMs } : {},
|
|
7985
8133
|
...request.modifiedBeforeMs !== undefined ? { modifiedBeforeMs: request.modifiedBeforeMs } : {}
|
|
7986
8134
|
},
|
|
7987
|
-
redact: input.redact ?? false
|
|
8135
|
+
redact: input.redact ?? false,
|
|
8136
|
+
...input.limit !== undefined ? { pageSize: request.pageSize } : {}
|
|
7988
8137
|
};
|
|
7989
8138
|
}
|
|
7990
8139
|
|
|
@@ -8225,7 +8374,19 @@ async function prepare(target, access, structure) {
|
|
|
8225
8374
|
let range = target.range;
|
|
8226
8375
|
let details = { status: "no-symbol" };
|
|
8227
8376
|
const language = syntaxLanguage(document.path);
|
|
8228
|
-
if (
|
|
8377
|
+
if (target.range && target.structure) {
|
|
8378
|
+
document.checkRange(target.range);
|
|
8379
|
+
const lines = {
|
|
8380
|
+
startLine: document.lineAt(target.range.start),
|
|
8381
|
+
endLine: document.lineAt(Math.max(target.range.start, target.range.end - 1))
|
|
8382
|
+
};
|
|
8383
|
+
range = document.lineRange(lines.startLine, lines.endLine);
|
|
8384
|
+
details = {
|
|
8385
|
+
...target.structure,
|
|
8386
|
+
range: lines,
|
|
8387
|
+
...target.structure.symbol ? { symbol: { ...target.structure.symbol, range: lines } } : {}
|
|
8388
|
+
};
|
|
8389
|
+
} else if (document.utf8 && language && language !== "go") {
|
|
8229
8390
|
const syntax = await access.syntax(document);
|
|
8230
8391
|
details = {
|
|
8231
8392
|
status: syntax.status === "ok" ? "no-symbol" : syntax.status === "unsupported" ? "provider-unavailable" : "parse-error",
|
|
@@ -8277,7 +8438,7 @@ async function prepare(target, access, structure) {
|
|
|
8277
8438
|
details = { status: "provider-unavailable", ...language ? { language } : {} };
|
|
8278
8439
|
}
|
|
8279
8440
|
range ??= document.lineRange(Math.max(1, target.line - 10), Math.min(document.lineStarts.length, target.line + 10));
|
|
8280
|
-
const boundary = target.range ? "requested-range" : details.status === "available" && details.range ? "syntax" : "line-window";
|
|
8441
|
+
const boundary = target.range ? target.structure ? "syntax" : "requested-range" : details.status === "available" && details.range ? "syntax" : "line-window";
|
|
8281
8442
|
document.checkRange(range);
|
|
8282
8443
|
return { target, document, range, structure: details, boundary, focus };
|
|
8283
8444
|
}
|
|
@@ -8636,8 +8797,10 @@ ${preview.text}`);
|
|
|
8636
8797
|
}
|
|
8637
8798
|
};
|
|
8638
8799
|
}
|
|
8639
|
-
async function continueSource(cursor, access, continuations) {
|
|
8800
|
+
async function continueSource(cursor, access, continuations, expectedPath) {
|
|
8640
8801
|
const state = continuations.resolve(cursor);
|
|
8802
|
+
if (expectedPath !== undefined && resolve18(access.cwd, expectedPath.replace(/^@/, "")) !== resolve18(access.cwd, state.source.path))
|
|
8803
|
+
throw new SiftLightError(`sourceCursor continues ${JSON.stringify(state.source.path)}, not ${JSON.stringify(expectedPath)}; copy the returned nextRequest exactly`);
|
|
8641
8804
|
const document = await access.load(state.source.path, state.source);
|
|
8642
8805
|
const page = sourcePage(document, state.remaining, MAX_RESULT_BYTES - 1400);
|
|
8643
8806
|
const next = continuations.advance(cursor, page.fragment);
|
|
@@ -9514,9 +9677,378 @@ function parsePythonOutline(document) {
|
|
|
9514
9677
|
});
|
|
9515
9678
|
}
|
|
9516
9679
|
|
|
9680
|
+
// src/python-roles.ts
|
|
9681
|
+
var KEYWORDS = new Set([
|
|
9682
|
+
"and",
|
|
9683
|
+
"as",
|
|
9684
|
+
"assert",
|
|
9685
|
+
"async",
|
|
9686
|
+
"await",
|
|
9687
|
+
"case",
|
|
9688
|
+
"del",
|
|
9689
|
+
"elif",
|
|
9690
|
+
"else",
|
|
9691
|
+
"except",
|
|
9692
|
+
"for",
|
|
9693
|
+
"from",
|
|
9694
|
+
"global",
|
|
9695
|
+
"if",
|
|
9696
|
+
"import",
|
|
9697
|
+
"in",
|
|
9698
|
+
"is",
|
|
9699
|
+
"lambda",
|
|
9700
|
+
"match",
|
|
9701
|
+
"nonlocal",
|
|
9702
|
+
"not",
|
|
9703
|
+
"or",
|
|
9704
|
+
"raise",
|
|
9705
|
+
"return",
|
|
9706
|
+
"while",
|
|
9707
|
+
"with",
|
|
9708
|
+
"yield"
|
|
9709
|
+
]);
|
|
9710
|
+
var STRING_PREFIX = /^(?:[rRbBuUfF]|[rR][bBfF]|[bBfF][rR])?$/u;
|
|
9711
|
+
var IDENTIFIER_START = /[\p{ID_Start}_]/u;
|
|
9712
|
+
var IDENTIFIER_PART = /[\p{ID_Continue}]/u;
|
|
9713
|
+
function lex(text) {
|
|
9714
|
+
const comments = [];
|
|
9715
|
+
const strings = [];
|
|
9716
|
+
const code = new Uint8Array(text.length).fill(1);
|
|
9717
|
+
const mark = (start, end) => {
|
|
9718
|
+
code.fill(0, start, end);
|
|
9719
|
+
};
|
|
9720
|
+
let index = 0;
|
|
9721
|
+
while (index < text.length) {
|
|
9722
|
+
const character = text[index];
|
|
9723
|
+
if (character === "#") {
|
|
9724
|
+
const newline = text.indexOf(`
|
|
9725
|
+
`, index);
|
|
9726
|
+
const end = newline < 0 ? text.length : newline;
|
|
9727
|
+
comments.push({ start: index, end });
|
|
9728
|
+
mark(index, end);
|
|
9729
|
+
index = end;
|
|
9730
|
+
continue;
|
|
9731
|
+
}
|
|
9732
|
+
if (character !== "'" && character !== '"') {
|
|
9733
|
+
index += 1;
|
|
9734
|
+
continue;
|
|
9735
|
+
}
|
|
9736
|
+
let prefixStart = index;
|
|
9737
|
+
while (prefixStart > 0 && /[rRbBuUfF]/u.test(text[prefixStart - 1] ?? ""))
|
|
9738
|
+
prefixStart -= 1;
|
|
9739
|
+
const prefix = text.slice(prefixStart, index);
|
|
9740
|
+
const beforePrefix = text[prefixStart - 1] ?? "";
|
|
9741
|
+
const start = STRING_PREFIX.test(prefix) && !IDENTIFIER_PART.test(beforePrefix) ? prefixStart : index;
|
|
9742
|
+
const triple = text.slice(index, index + 3) === character.repeat(3);
|
|
9743
|
+
const delimiter = triple ? character.repeat(3) : character;
|
|
9744
|
+
let cursor = index + delimiter.length;
|
|
9745
|
+
let end = text.length;
|
|
9746
|
+
while (cursor < text.length) {
|
|
9747
|
+
const current = text[cursor];
|
|
9748
|
+
if (current === "\\") {
|
|
9749
|
+
cursor += 2;
|
|
9750
|
+
continue;
|
|
9751
|
+
}
|
|
9752
|
+
if (!triple && current === `
|
|
9753
|
+
`) {
|
|
9754
|
+
end = cursor;
|
|
9755
|
+
break;
|
|
9756
|
+
}
|
|
9757
|
+
if (text.startsWith(delimiter, cursor)) {
|
|
9758
|
+
end = cursor + delimiter.length;
|
|
9759
|
+
break;
|
|
9760
|
+
}
|
|
9761
|
+
cursor += 1;
|
|
9762
|
+
}
|
|
9763
|
+
strings.push({ start, end });
|
|
9764
|
+
mark(start, end);
|
|
9765
|
+
index = end;
|
|
9766
|
+
}
|
|
9767
|
+
return { comments, strings, code };
|
|
9768
|
+
}
|
|
9769
|
+
function codeRanges(code) {
|
|
9770
|
+
const ranges = [];
|
|
9771
|
+
let start = -1;
|
|
9772
|
+
for (let index = 0;index <= code.length; index += 1) {
|
|
9773
|
+
const inCode = index < code.length && code[index] === 1;
|
|
9774
|
+
if (inCode && start < 0)
|
|
9775
|
+
start = index;
|
|
9776
|
+
else if (!inCode && start >= 0) {
|
|
9777
|
+
ranges.push({ start, end: index });
|
|
9778
|
+
start = -1;
|
|
9779
|
+
}
|
|
9780
|
+
}
|
|
9781
|
+
return ranges;
|
|
9782
|
+
}
|
|
9783
|
+
function identifierEnd(text, start) {
|
|
9784
|
+
let end = start;
|
|
9785
|
+
while (end < text.length && IDENTIFIER_PART.test(text[end] ?? ""))
|
|
9786
|
+
end += 1;
|
|
9787
|
+
return end;
|
|
9788
|
+
}
|
|
9789
|
+
function skipSpaces(text, index, code) {
|
|
9790
|
+
let cursor = index;
|
|
9791
|
+
while (cursor < text.length && code[cursor] === 1 && /[ \t]/u.test(text[cursor] ?? ""))
|
|
9792
|
+
cursor += 1;
|
|
9793
|
+
return cursor;
|
|
9794
|
+
}
|
|
9795
|
+
function statementEnd(text, start, code) {
|
|
9796
|
+
let depth = 0;
|
|
9797
|
+
for (let index = start;index < text.length; index += 1) {
|
|
9798
|
+
if (code[index] !== 1)
|
|
9799
|
+
continue;
|
|
9800
|
+
const character = text[index];
|
|
9801
|
+
if (character === "(" || character === "[" || character === "{")
|
|
9802
|
+
depth += 1;
|
|
9803
|
+
else if (character === ")" || character === "]" || character === "}")
|
|
9804
|
+
depth = Math.max(0, depth - 1);
|
|
9805
|
+
else if (character === "\\" && text[index + 1] === `
|
|
9806
|
+
`)
|
|
9807
|
+
index += 1;
|
|
9808
|
+
else if (character === `
|
|
9809
|
+
` && depth === 0)
|
|
9810
|
+
return index;
|
|
9811
|
+
}
|
|
9812
|
+
return text.length;
|
|
9813
|
+
}
|
|
9814
|
+
function pythonRoleAnalysis(document) {
|
|
9815
|
+
const text = document.text;
|
|
9816
|
+
const { comments, strings, code } = lex(text);
|
|
9817
|
+
const roles = [];
|
|
9818
|
+
const push = (start, end, role, certainty, subkind) => {
|
|
9819
|
+
if (start < end)
|
|
9820
|
+
roles.push({ start, end, role, certainty, node: 0, ...subkind ? { subkind } : {} });
|
|
9821
|
+
};
|
|
9822
|
+
for (const span of comments)
|
|
9823
|
+
push(span.start, span.end, "comment", "syntax");
|
|
9824
|
+
for (const span of strings)
|
|
9825
|
+
push(span.start, span.end, "string", "syntax");
|
|
9826
|
+
for (const span of codeRanges(code))
|
|
9827
|
+
push(span.start, span.end, "code", "syntax");
|
|
9828
|
+
let index = 0;
|
|
9829
|
+
let lineStart = true;
|
|
9830
|
+
while (index < text.length) {
|
|
9831
|
+
const character = text[index] ?? "";
|
|
9832
|
+
if (character === `
|
|
9833
|
+
`) {
|
|
9834
|
+
lineStart = true;
|
|
9835
|
+
index += 1;
|
|
9836
|
+
continue;
|
|
9837
|
+
}
|
|
9838
|
+
if (code[index] !== 1 || !IDENTIFIER_START.test(character)) {
|
|
9839
|
+
if (!/[ \t]/u.test(character))
|
|
9840
|
+
lineStart = false;
|
|
9841
|
+
index += 1;
|
|
9842
|
+
continue;
|
|
9843
|
+
}
|
|
9844
|
+
const previous = text[index - 1] ?? "";
|
|
9845
|
+
if (IDENTIFIER_PART.test(previous)) {
|
|
9846
|
+
index += 1;
|
|
9847
|
+
continue;
|
|
9848
|
+
}
|
|
9849
|
+
const end = identifierEnd(text, index);
|
|
9850
|
+
const word = text.slice(index, end);
|
|
9851
|
+
const atStatementStart = lineStart;
|
|
9852
|
+
lineStart = false;
|
|
9853
|
+
if (atStatementStart && (word === "import" || word === "from")) {
|
|
9854
|
+
const statement = statementEnd(text, index, code);
|
|
9855
|
+
const body = text.slice(index, statement);
|
|
9856
|
+
if (word === "import" || /\bimport\b/u.test(body)) {
|
|
9857
|
+
push(index, statement, "import", "syntax", word === "from" ? "from-import" : "import");
|
|
9858
|
+
index = statement;
|
|
9859
|
+
continue;
|
|
9860
|
+
}
|
|
9861
|
+
}
|
|
9862
|
+
if (word === "def" || word === "class") {
|
|
9863
|
+
const nameStart = skipSpaces(text, end, code);
|
|
9864
|
+
if (IDENTIFIER_START.test(text[nameStart] ?? "")) {
|
|
9865
|
+
const nameEnd = identifierEnd(text, nameStart);
|
|
9866
|
+
push(nameStart, nameEnd, "declaration", "syntax", word === "def" ? "function" : "class");
|
|
9867
|
+
index = nameEnd;
|
|
9868
|
+
continue;
|
|
9869
|
+
}
|
|
9870
|
+
}
|
|
9871
|
+
let chainEnd = end;
|
|
9872
|
+
while (text[chainEnd] === "." && IDENTIFIER_START.test(text[chainEnd + 1] ?? "")) {
|
|
9873
|
+
chainEnd = identifierEnd(text, chainEnd + 1);
|
|
9874
|
+
}
|
|
9875
|
+
const open = skipSpaces(text, chainEnd, code);
|
|
9876
|
+
const lastSegment = text.slice(text.lastIndexOf(".", chainEnd - 1) + 1, chainEnd);
|
|
9877
|
+
const before = text.slice(Math.max(0, index - 4), index);
|
|
9878
|
+
if (text[open] === "(" && code[open] === 1 && !KEYWORDS.has(word) && !KEYWORDS.has(lastSegment) && !/\bdef\s+$|\bclass\s+$/u.test(before)) {
|
|
9879
|
+
push(index, open + 1, "call", "candidate", "lexical-call");
|
|
9880
|
+
}
|
|
9881
|
+
index = chainEnd;
|
|
9882
|
+
}
|
|
9883
|
+
roles.sort((a, b) => a.start - b.start || a.end - b.end || a.role.localeCompare(b.role));
|
|
9884
|
+
return {
|
|
9885
|
+
status: "ok",
|
|
9886
|
+
nodes: [],
|
|
9887
|
+
children: [],
|
|
9888
|
+
symbols: [],
|
|
9889
|
+
roles,
|
|
9890
|
+
diagnostics: [],
|
|
9891
|
+
limited: false
|
|
9892
|
+
};
|
|
9893
|
+
}
|
|
9894
|
+
|
|
9517
9895
|
// src/hybrid-search.ts
|
|
9518
9896
|
import { resolve as resolve20 } from "node:path";
|
|
9519
9897
|
|
|
9898
|
+
// src/config-reader.ts
|
|
9899
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
9900
|
+
var SIFT_LIGHT_CONFIG_ENV = "SIFT_LIGHT_CONFIG";
|
|
9901
|
+
var SEMANTIC_JUDGE_API_KEY_ENVS = ["TYPESAFE_API_KEY", "SIFT_LIGHT_JEV_API_KEY"];
|
|
9902
|
+
var DEFAULT_SEMANTIC_JUDGE_CONFIG = {
|
|
9903
|
+
enabled: false,
|
|
9904
|
+
provider: "jev",
|
|
9905
|
+
endpoint: "https://api.typesafe.ai/v1/systemone",
|
|
9906
|
+
apiKeyEnv: SEMANTIC_JUDGE_API_KEY_ENVS[0],
|
|
9907
|
+
model: "jev-latest",
|
|
9908
|
+
timeoutMs: 120000,
|
|
9909
|
+
maxCandidates: 20,
|
|
9910
|
+
maxRetries: 2
|
|
9911
|
+
};
|
|
9912
|
+
var DEFAULT_SIFT_LIGHT_CONFIG = {
|
|
9913
|
+
locale: "en",
|
|
9914
|
+
enforceSearch: "hard",
|
|
9915
|
+
vectorSearchEnabled: false,
|
|
9916
|
+
semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG
|
|
9917
|
+
};
|
|
9918
|
+
function hasErrorCode(error, codes) {
|
|
9919
|
+
return error instanceof Error && "code" in error && codes.includes(String(error.code));
|
|
9920
|
+
}
|
|
9921
|
+
function isMissingFile(error) {
|
|
9922
|
+
return hasErrorCode(error, ["ENOENT"]);
|
|
9923
|
+
}
|
|
9924
|
+
function isRawSiftLightConfig(value) {
|
|
9925
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9926
|
+
}
|
|
9927
|
+
function isRawSemanticJudgeConfig(value) {
|
|
9928
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9929
|
+
}
|
|
9930
|
+
function boundedInteger2(value, fallback, minimum, maximum, field) {
|
|
9931
|
+
const candidate = value ?? fallback;
|
|
9932
|
+
if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < minimum || candidate > maximum) {
|
|
9933
|
+
throw new Error(`Invalid sift-light ${field}: expected an integer from ${String(minimum)} through ${String(maximum)}`);
|
|
9934
|
+
}
|
|
9935
|
+
return candidate;
|
|
9936
|
+
}
|
|
9937
|
+
function parseSemanticJudge(value, path) {
|
|
9938
|
+
if (value === undefined)
|
|
9939
|
+
return { ...DEFAULT_SEMANTIC_JUDGE_CONFIG };
|
|
9940
|
+
if (!isRawSemanticJudgeConfig(value)) {
|
|
9941
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge must be an object`);
|
|
9942
|
+
}
|
|
9943
|
+
const unknown = Object.keys(value).filter((key) => ![
|
|
9944
|
+
"enabled",
|
|
9945
|
+
"provider",
|
|
9946
|
+
"endpoint",
|
|
9947
|
+
"apiKeyEnv",
|
|
9948
|
+
"model",
|
|
9949
|
+
"timeoutMs",
|
|
9950
|
+
"maxCandidates",
|
|
9951
|
+
"maxRetries"
|
|
9952
|
+
].includes(key));
|
|
9953
|
+
if (unknown.length > 0) {
|
|
9954
|
+
throw new Error(`Invalid sift-light config at ${path}: unsupported semanticJudge fields; accepted fields are enabled, provider, endpoint, apiKeyEnv, model, timeoutMs, maxCandidates and maxRetries`);
|
|
9955
|
+
}
|
|
9956
|
+
const enabled = value.enabled ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.enabled;
|
|
9957
|
+
if (typeof enabled !== "boolean") {
|
|
9958
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.enabled must be a boolean`);
|
|
9959
|
+
}
|
|
9960
|
+
const provider = value.provider ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.provider;
|
|
9961
|
+
if (provider !== "jev") {
|
|
9962
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.provider must be "jev"`);
|
|
9963
|
+
}
|
|
9964
|
+
const endpoint = value.endpoint ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.endpoint;
|
|
9965
|
+
if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.length > 2048) {
|
|
9966
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a nonempty URL`);
|
|
9967
|
+
}
|
|
9968
|
+
let parsedEndpoint;
|
|
9969
|
+
try {
|
|
9970
|
+
parsedEndpoint = new URL(endpoint);
|
|
9971
|
+
} catch (error) {
|
|
9972
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a URL`, {
|
|
9973
|
+
cause: error
|
|
9974
|
+
});
|
|
9975
|
+
}
|
|
9976
|
+
const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
9977
|
+
const secureEndpoint = parsedEndpoint.protocol === "https:" || parsedEndpoint.protocol === "http:" && loopbackHosts.has(parsedEndpoint.hostname);
|
|
9978
|
+
if (!secureEndpoint || parsedEndpoint.username || parsedEndpoint.password) {
|
|
9979
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must use HTTPS, except that HTTP is allowed for localhost loopback development; URL credentials are not allowed`);
|
|
9980
|
+
}
|
|
9981
|
+
const apiKeyEnv = value.apiKeyEnv ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.apiKeyEnv;
|
|
9982
|
+
if (typeof apiKeyEnv !== "string" || !/^[A-Z][A-Z0-9_]{0,127}$/u.test(apiKeyEnv)) {
|
|
9983
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.apiKeyEnv must be an uppercase environment variable name`);
|
|
9984
|
+
}
|
|
9985
|
+
const model = value.model ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.model;
|
|
9986
|
+
if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
|
|
9987
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.model must be bounded single-line text`);
|
|
9988
|
+
}
|
|
9989
|
+
return {
|
|
9990
|
+
enabled,
|
|
9991
|
+
provider,
|
|
9992
|
+
endpoint,
|
|
9993
|
+
apiKeyEnv,
|
|
9994
|
+
model,
|
|
9995
|
+
timeoutMs: boundedInteger2(value.timeoutMs, DEFAULT_SEMANTIC_JUDGE_CONFIG.timeoutMs, 1000, 1200000, `config at ${path}: semanticJudge.timeoutMs`),
|
|
9996
|
+
maxCandidates: boundedInteger2(value.maxCandidates, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxCandidates, 1, 20, `config at ${path}: semanticJudge.maxCandidates`),
|
|
9997
|
+
maxRetries: boundedInteger2(value.maxRetries, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxRetries, 0, 5, `config at ${path}: semanticJudge.maxRetries`)
|
|
9998
|
+
};
|
|
9999
|
+
}
|
|
10000
|
+
function parseConfig(value, path) {
|
|
10001
|
+
if (!isRawSiftLightConfig(value)) {
|
|
10002
|
+
throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
|
|
10003
|
+
}
|
|
10004
|
+
const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "vectorSearchEnabled", "semanticJudge"].includes(key));
|
|
10005
|
+
if (unknown.length > 0) {
|
|
10006
|
+
throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled and semanticJudge are accepted`);
|
|
10007
|
+
}
|
|
10008
|
+
const { locale, enforceSearch, vectorSearchEnabled } = value;
|
|
10009
|
+
if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
|
|
10010
|
+
throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
|
|
10011
|
+
}
|
|
10012
|
+
if (vectorSearchEnabled !== undefined && typeof vectorSearchEnabled !== "boolean") {
|
|
10013
|
+
throw new Error(`Invalid sift-light config at ${path}: vectorSearchEnabled must be a boolean`);
|
|
10014
|
+
}
|
|
10015
|
+
const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
|
|
10016
|
+
return {
|
|
10017
|
+
locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
|
|
10018
|
+
enforceSearch: enforcement,
|
|
10019
|
+
vectorSearchEnabled: vectorSearchEnabled ?? DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled ?? false,
|
|
10020
|
+
semanticJudge: parseSemanticJudge(value.semanticJudge, path)
|
|
10021
|
+
};
|
|
10022
|
+
}
|
|
10023
|
+
function normalizeSearchEnforcement(value, source) {
|
|
10024
|
+
if (value === undefined || value === "hard")
|
|
10025
|
+
return "hard";
|
|
10026
|
+
if (value === "prefer")
|
|
10027
|
+
return "prefer";
|
|
10028
|
+
if (value === "off")
|
|
10029
|
+
return "off";
|
|
10030
|
+
throw new Error(`Invalid sift-light ${source}: enforceSearch must be "hard", "prefer", or "off"`);
|
|
10031
|
+
}
|
|
10032
|
+
async function readSiftLightConfigFile(path, options = {}) {
|
|
10033
|
+
try {
|
|
10034
|
+
const content = await readFile2(path, "utf8");
|
|
10035
|
+
return parseConfig(JSON.parse(content), path);
|
|
10036
|
+
} catch (error) {
|
|
10037
|
+
if (isMissingFile(error)) {
|
|
10038
|
+
if (options.missing === "error") {
|
|
10039
|
+
throw new Error(`sift-light config was not found at ${path}; create it or unset ${SIFT_LIGHT_CONFIG_ENV}`, { cause: error });
|
|
10040
|
+
}
|
|
10041
|
+
return { ...DEFAULT_SIFT_LIGHT_CONFIG };
|
|
10042
|
+
}
|
|
10043
|
+
if (error instanceof SyntaxError) {
|
|
10044
|
+
throw new Error(`Invalid sift-light config at ${path}: ${error.message}`, {
|
|
10045
|
+
cause: error
|
|
10046
|
+
});
|
|
10047
|
+
}
|
|
10048
|
+
throw error;
|
|
10049
|
+
}
|
|
10050
|
+
}
|
|
10051
|
+
|
|
9520
10052
|
// src/semantic-judge-batches.ts
|
|
9521
10053
|
var MAX_SEMANTIC_JUDGE_BATCH_CANDIDATES = 8;
|
|
9522
10054
|
var MAX_SEMANTIC_JUDGE_REQUEST_BYTES = 64 * 1024;
|
|
@@ -9664,7 +10196,7 @@ function requestBody(query, candidates, model) {
|
|
|
9664
10196
|
for (const candidate of candidates) {
|
|
9665
10197
|
questions[candidate.id] = {
|
|
9666
10198
|
type: "choice",
|
|
9667
|
-
instructions:
|
|
10199
|
+
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.`,
|
|
9668
10200
|
criteria: {
|
|
9669
10201
|
"implementation-candidate": "The excerpt appears to implement the requested behavior or its core decision/side effect.",
|
|
9670
10202
|
"caller-candidate": "The excerpt invokes or wires an implementation but does not implement the behavior itself.",
|
|
@@ -9827,6 +10359,10 @@ function createSemanticJudgeIntegration(config, environment = process.env, fetch
|
|
|
9827
10359
|
}
|
|
9828
10360
|
return { config, runner: createJevRunner(config, key, fetcher) };
|
|
9829
10361
|
}
|
|
10362
|
+
function createConfiguredSemanticJudgeIntegration(config, environment = process.env) {
|
|
10363
|
+
const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
|
|
10364
|
+
return config.vectorSearchEnabled === true ? createSemanticJudgeIntegration(judge, environment) : createDisabledSemanticJudgeIntegration(judge);
|
|
10365
|
+
}
|
|
9830
10366
|
function baseDetails(config) {
|
|
9831
10367
|
return {
|
|
9832
10368
|
enabled: config.enabled,
|
|
@@ -10133,6 +10669,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
|
|
|
10133
10669
|
const deduplicationCoverage = scan.snapshotComplete && literal.sourceCoverage === "complete" && conceptSourceCoverage === "complete" ? "complete" : "partial";
|
|
10134
10670
|
const partial = !scan.snapshotComplete || concept.partial || conceptCoverage === "skipped" || conceptSourceCoverage === "partial" || literal.sourceCoverage === "partial" || deduplicationCoverage === "partial" || judgedConcept.semanticJudge?.status === "failed" || judgedConcept.semanticJudge?.status === "partial";
|
|
10135
10671
|
const selectionReason = conceptCandidatesOmitted ? `Hybrid concept limit retained the top ${String(selectedConcept.length)} of ${String(eligibleConcept.length)} non-overlapping semantic candidates` : undefined;
|
|
10672
|
+
const firstScore = Number(eligibleConcept[0]?.details?.rankingScore ?? eligibleConcept[0]?.details?.score);
|
|
10673
|
+
const secondScore = Number(eligibleConcept[1]?.details?.rankingScore ?? eligibleConcept[1]?.details?.score);
|
|
10674
|
+
const closeRanking = Number.isFinite(firstScore) && Number.isFinite(secondScore) && firstScore - secondScore < 0.01;
|
|
10136
10675
|
return {
|
|
10137
10676
|
kind: "hybrid",
|
|
10138
10677
|
unit: "evidence-items",
|
|
@@ -10144,6 +10683,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
|
|
|
10144
10683
|
...literal.reasons,
|
|
10145
10684
|
...execution.sourceGeneration.reasons,
|
|
10146
10685
|
...selectionReason ? [selectionReason] : [],
|
|
10686
|
+
...closeRanking ? [
|
|
10687
|
+
"Semantic ranks are close; verify the leading candidates with source inspection or literal terms."
|
|
10688
|
+
] : [],
|
|
10147
10689
|
...judgedConcept.semanticJudge?.reason ? [judgedConcept.semanticJudge.reason] : []
|
|
10148
10690
|
],
|
|
10149
10691
|
filesRead: (concept.filesRead ?? 0) + access.filesRead,
|
|
@@ -10297,6 +10839,69 @@ import { realpath as realpath6 } from "node:fs/promises";
|
|
|
10297
10839
|
function isEvidenceRequest(input) {
|
|
10298
10840
|
return input.mode === "concept" || input.mode === "hybrid" || input.mode === "structure" || input.mode === "files" || input.mode === "inspect" || input.mode === "outline" || input.mode === "imports" || input.mode === "tests" || input.mode === "validate" || input.sourceCursor !== undefined || input.anyOf !== undefined || input.allOf !== undefined || input.within !== undefined || input.roles !== undefined || input.changes !== undefined || input.symbol !== undefined || input.conceptLimit !== undefined || (input.cursor?.includes(".analysis") ?? false);
|
|
10299
10841
|
}
|
|
10842
|
+
function pageInspectRequest(input) {
|
|
10843
|
+
if (input.scope === "expand")
|
|
10844
|
+
throw new SiftLightError("mode=inspect reads exact locations; scope=expand does not apply (omit scope)");
|
|
10845
|
+
const { scope: _scope, paths, ...rest } = input;
|
|
10846
|
+
let request = rest;
|
|
10847
|
+
if (paths !== undefined) {
|
|
10848
|
+
if (input.cursor !== undefined || input.targets !== undefined || input.matchIndices !== undefined || input.path !== undefined || input.line !== undefined || input.matchIndex !== undefined)
|
|
10849
|
+
throw new SiftLightError("mode=inspect paths opens files from line 1 and cannot be combined with cursor, targets, matchIndices, path, line or matchIndex");
|
|
10850
|
+
request = { ...rest, targets: paths.map((path) => ({ path, line: 1 })) };
|
|
10851
|
+
}
|
|
10852
|
+
const redact = input.redact ? { redact: true } : {};
|
|
10853
|
+
const requested = request.targets?.length ?? request.matchIndices?.length ?? 0;
|
|
10854
|
+
if (requested > MAX_INSPECT_REQUEST_TARGETS)
|
|
10855
|
+
throw new SiftLightError(`mode=inspect accepts at most ${String(MAX_INSPECT_REQUEST_TARGETS)} targets per request; ${String(MAX_INSPECT_TARGETS)} are inspected per response`);
|
|
10856
|
+
if (request.targets && request.targets.length > MAX_INSPECT_TARGETS)
|
|
10857
|
+
return {
|
|
10858
|
+
current: { ...request, targets: request.targets.slice(0, MAX_INSPECT_TARGETS) },
|
|
10859
|
+
remaining: {
|
|
10860
|
+
mode: "inspect",
|
|
10861
|
+
targets: request.targets.slice(MAX_INSPECT_TARGETS),
|
|
10862
|
+
...redact
|
|
10863
|
+
}
|
|
10864
|
+
};
|
|
10865
|
+
if (request.matchIndices && request.matchIndices.length > MAX_INSPECT_TARGETS)
|
|
10866
|
+
return {
|
|
10867
|
+
current: { ...request, matchIndices: request.matchIndices.slice(0, MAX_INSPECT_TARGETS) },
|
|
10868
|
+
remaining: {
|
|
10869
|
+
mode: "inspect",
|
|
10870
|
+
...request.cursor !== undefined ? { cursor: request.cursor } : {},
|
|
10871
|
+
matchIndices: request.matchIndices.slice(MAX_INSPECT_TARGETS),
|
|
10872
|
+
...redact
|
|
10873
|
+
}
|
|
10874
|
+
};
|
|
10875
|
+
return { current: request };
|
|
10876
|
+
}
|
|
10877
|
+
function withRemainingInspection(result, remaining) {
|
|
10878
|
+
const count = remaining.targets?.length ?? remaining.matchIndices?.length ?? 0;
|
|
10879
|
+
return {
|
|
10880
|
+
text: `${result.text}
|
|
10881
|
+
|
|
10882
|
+
[${String(count)} more target(s) were not inspected; one response inspects ${String(MAX_INSPECT_TARGETS)}.]
|
|
10883
|
+
Next request: ${JSON.stringify(remaining)}`,
|
|
10884
|
+
details: {
|
|
10885
|
+
...result.details,
|
|
10886
|
+
status: "partial",
|
|
10887
|
+
snapshotComplete: false,
|
|
10888
|
+
nextRequest: remaining
|
|
10889
|
+
}
|
|
10890
|
+
};
|
|
10891
|
+
}
|
|
10892
|
+
function analysisItemStructure(item) {
|
|
10893
|
+
const details = item.details ?? {};
|
|
10894
|
+
const language = typeof details.language === "string" ? details.language : undefined;
|
|
10895
|
+
const name = typeof details.name === "string" ? details.name : undefined;
|
|
10896
|
+
const scope = Array.isArray(details.scope) ? details.scope.filter((entry) => typeof entry === "string") : typeof details.scope === "string" ? [details.scope] : [];
|
|
10897
|
+
const kind = details.kind === "function" ? "function" : item.label.split(" ", 1)[0] ?? "symbol";
|
|
10898
|
+
return {
|
|
10899
|
+
status: "available",
|
|
10900
|
+
provider: language === "python" ? "python-outline" : "tree-sitter",
|
|
10901
|
+
...language ? { language } : {},
|
|
10902
|
+
...name ? { symbol: { name, kind, scope, range: { startLine: item.line, endLine: item.line } } } : {}
|
|
10903
|
+
};
|
|
10904
|
+
}
|
|
10300
10905
|
function maxFilesToParse(value, defaultValue = MAX_STRUCTURE_FILES) {
|
|
10301
10906
|
const candidate = value ?? defaultValue;
|
|
10302
10907
|
if (!Number.isSafeInteger(candidate) || candidate < 1 || candidate > MAX_CONFIGURABLE_STRUCTURE_FILES) {
|
|
@@ -10312,8 +10917,8 @@ function validateTerms(input) {
|
|
|
10312
10917
|
}
|
|
10313
10918
|
return;
|
|
10314
10919
|
}
|
|
10315
|
-
if (!Array.isArray(terms) || terms.length <
|
|
10316
|
-
throw new SiftLightError("allOf requires
|
|
10920
|
+
if (!Array.isArray(terms) || terms.length < 1 || terms.length > 3 || terms.some((term) => typeof term !== "string" || !term.trim() || /[\r\n\0]/.test(term)) || new Set(terms).size !== terms.length)
|
|
10921
|
+
throw new SiftLightError("allOf requires 1–3 distinct, nonempty, single-line literal terms");
|
|
10317
10922
|
if (input.pattern !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
|
|
10318
10923
|
throw new SiftLightError("allOf is an explicit case-sensitive literal conjunction; omit pattern, roles, literal and ignoreCase");
|
|
10319
10924
|
if (input.within !== undefined && input.within !== "file" && input.within !== "function")
|
|
@@ -10511,11 +11116,13 @@ class EvidenceService {
|
|
|
10511
11116
|
throw new CursorError("A nonempty sourceCursor is required");
|
|
10512
11117
|
if (input.mode !== "inspect")
|
|
10513
11118
|
throw new SiftLightError("sourceCursor requires mode=inspect");
|
|
10514
|
-
return continueSource(input.sourceCursor, access, this.#continuations);
|
|
11119
|
+
return continueSource(input.sourceCursor, access, this.#continuations, input.path);
|
|
10515
11120
|
}
|
|
10516
11121
|
if (input.mode === "inspect") {
|
|
10517
|
-
const
|
|
10518
|
-
|
|
11122
|
+
const { current, remaining } = pageInspectRequest(input);
|
|
11123
|
+
const targets = this.#inspectionTargets(current, cwd);
|
|
11124
|
+
const result = await inspectDocuments(targets, access, this.#continuations, this.#structure);
|
|
11125
|
+
return remaining ? withRemainingInspection(result, remaining) : result;
|
|
10519
11126
|
}
|
|
10520
11127
|
if (input.mode === "concept") {
|
|
10521
11128
|
const execution = await this.#conceptSearch(input, access, options.onProgress);
|
|
@@ -10604,7 +11211,7 @@ class EvidenceService {
|
|
|
10604
11211
|
if (input.pattern !== undefined || input.allOf !== undefined || input.within !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
|
|
10605
11212
|
throw new SiftLightError("anyOf is an explicit case-sensitive literal union; omit pattern, allOf, within, roles, literal and ignoreCase");
|
|
10606
11213
|
if (input.mode !== undefined && input.mode !== "auto" && input.mode !== "matches")
|
|
10607
|
-
throw new SiftLightError("anyOf mode must be omitted, auto, or
|
|
11214
|
+
throw new SiftLightError("anyOf mode must be omitted, auto, matches or summary");
|
|
10608
11215
|
const chunks = Array.from({ length: Math.ceil(anyOf.length / MAX_ANY_OF_TERMS) }, (_, index) => anyOf.slice(index * MAX_ANY_OF_TERMS, (index + 1) * MAX_ANY_OF_TERMS));
|
|
10609
11216
|
const { path: _inputPath, ...unscopedInput } = input;
|
|
10610
11217
|
let chunkAccess = access;
|
|
@@ -10742,9 +11349,10 @@ class EvidenceService {
|
|
|
10742
11349
|
if (item)
|
|
10743
11350
|
result.items.push(item);
|
|
10744
11351
|
} else if (terms || input.roles) {
|
|
10745
|
-
|
|
11352
|
+
const pythonRoles = !terms && /\.py$/iu.test(file.document.path);
|
|
11353
|
+
if (pythonRoles || syntaxLanguage(file.document.path))
|
|
10746
11354
|
syntaxCapableFiles += 1;
|
|
10747
|
-
const syntax = await access.syntax(file.document);
|
|
11355
|
+
const syntax = pythonRoles ? pythonRoleAnalysis(file.document) : await access.syntax(file.document);
|
|
10748
11356
|
const classified = terms ? findFunctionConjunctions(file.document, syntax, terms, input.changes?.scope === "lines" ? file.changedRanges : undefined) : filterRoleOccurrences(file.document, syntax, file.occurrences, input.roles ?? []);
|
|
10749
11357
|
result.items.push(...classified.items);
|
|
10750
11358
|
result.partial ||= classified.partial;
|
|
@@ -10821,12 +11429,12 @@ class EvidenceService {
|
|
|
10821
11429
|
const item = this.#analyses.item(input.cursor, input.matchIndex);
|
|
10822
11430
|
if (!item.source || !item.range)
|
|
10823
11431
|
throw new CursorError("This analysis item has no verified source range");
|
|
10824
|
-
const
|
|
11432
|
+
const bounded = item.details?.kind === "symbol" || item.details?.kind === "function";
|
|
10825
11433
|
return {
|
|
10826
11434
|
path: item.path,
|
|
10827
11435
|
line: item.line,
|
|
10828
11436
|
reference: item.source,
|
|
10829
|
-
...
|
|
11437
|
+
...bounded ? { range: item.range, structure: analysisItemStructure(item) } : { absoluteFocus: item.range.start }
|
|
10830
11438
|
};
|
|
10831
11439
|
}
|
|
10832
11440
|
return {
|
|
@@ -11035,8 +11643,31 @@ import { resolve as resolve24 } from "node:path";
|
|
|
11035
11643
|
// src/discovery-errors.ts
|
|
11036
11644
|
var DISCOVERY_MODE_REQUIRED_ERROR = 'query requires an explicit discovery mode: use mode=files for filename/path discovery or mode=concept for semantic discovery; for example {"mode":"files","query":"<filename-or-path>"}';
|
|
11037
11645
|
|
|
11646
|
+
// src/request-aliases.ts
|
|
11647
|
+
var MODE_ALIASES = ["anyOf", "allOf"];
|
|
11648
|
+
function normalizeRequestAliases(request) {
|
|
11649
|
+
const notes = [];
|
|
11650
|
+
const { mode, ...rest } = request;
|
|
11651
|
+
let input = rest;
|
|
11652
|
+
if (mode === "anyOf" || mode === "allOf") {
|
|
11653
|
+
if (request[mode] === undefined)
|
|
11654
|
+
throw new SiftLightError(`mode=${mode} is not a mode; pass ${mode}:[...terms] and omit mode`);
|
|
11655
|
+
notes.push(`mode="${mode}" is not a mode; the ${mode} field alone selects this search.`);
|
|
11656
|
+
} else if (mode === "summary" && request.anyOf !== undefined) {
|
|
11657
|
+
notes.push("anyOf returns one analysis page whose header already carries per-file statistics; mode=summary was served by that page.");
|
|
11658
|
+
} else if (mode !== undefined) {
|
|
11659
|
+
input = { ...rest, mode };
|
|
11660
|
+
}
|
|
11661
|
+
if (input.sourceCursor !== undefined && input.line !== undefined) {
|
|
11662
|
+
const { line: _line, ...withoutLine } = input;
|
|
11663
|
+
input = withoutLine;
|
|
11664
|
+
notes.push("line is ignored with sourceCursor; the continuation selects its own source range.");
|
|
11665
|
+
}
|
|
11666
|
+
return { input, notes };
|
|
11667
|
+
}
|
|
11668
|
+
|
|
11038
11669
|
// src/format.ts
|
|
11039
|
-
import { readFile as
|
|
11670
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
11040
11671
|
var RESULT_METADATA_RESERVE_BYTES = 1024;
|
|
11041
11672
|
var RESULT_METADATA_RESERVE_CHARACTERS = 512;
|
|
11042
11673
|
|
|
@@ -11170,7 +11801,7 @@ async function loadContextLines(match, expectedRevision, cache, signal) {
|
|
|
11170
11801
|
cache.set(match.absolutePath, changed);
|
|
11171
11802
|
return changed;
|
|
11172
11803
|
}
|
|
11173
|
-
const content = await
|
|
11804
|
+
const content = await readFile3(match.absolutePath, { encoding: "utf8", signal });
|
|
11174
11805
|
const afterRevision = await getSourceRevision(match.absolutePath);
|
|
11175
11806
|
if (!afterRevision || !sameSourceRevision(expectedRevision, afterRevision)) {
|
|
11176
11807
|
const changed = { status: "changed" };
|
|
@@ -12388,6 +13019,16 @@ class LanguageCapabilityCatalog {
|
|
|
12388
13019
|
}
|
|
12389
13020
|
|
|
12390
13021
|
// src/service.ts
|
|
13022
|
+
function withRequestNotes(result, notes) {
|
|
13023
|
+
if (notes.length === 0)
|
|
13024
|
+
return result;
|
|
13025
|
+
return {
|
|
13026
|
+
text: `${notes.map((note) => `[Request note: ${note}]`).join(`
|
|
13027
|
+
`)}
|
|
13028
|
+
${result.text}`,
|
|
13029
|
+
details: { ...result.details, requestNotes: [...notes] }
|
|
13030
|
+
};
|
|
13031
|
+
}
|
|
12391
13032
|
function filterList(value) {
|
|
12392
13033
|
return value === undefined ? [] : Array.isArray(value) ? [...value] : [value];
|
|
12393
13034
|
}
|
|
@@ -12601,6 +13242,7 @@ class SiftLightService {
|
|
|
12601
13242
|
#runRipgrep;
|
|
12602
13243
|
#snapshots;
|
|
12603
13244
|
#summaryFileLimit;
|
|
13245
|
+
#vectorSearchEnabled;
|
|
12604
13246
|
#capabilities = new LanguageCapabilityCatalog;
|
|
12605
13247
|
#evidence;
|
|
12606
13248
|
#lifecycle = new AbortController;
|
|
@@ -12611,12 +13253,20 @@ class SiftLightService {
|
|
|
12611
13253
|
this.#runRipgrep = options.runRipgrep;
|
|
12612
13254
|
this.#snapshots = options.snapshots ?? new SnapshotStore;
|
|
12613
13255
|
this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
|
|
13256
|
+
this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
|
|
12614
13257
|
this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
|
|
12615
13258
|
this.#evidence = new EvidenceService(this.#runRipgrep, this.#snapshots, options.structure, options.conceptSearch, options.semanticJudge);
|
|
12616
13259
|
}
|
|
12617
|
-
async search(
|
|
13260
|
+
async search(request, cwd, signal, options = {}) {
|
|
13261
|
+
const { input, notes } = normalizeRequestAliases(request);
|
|
13262
|
+
return withRequestNotes(await this.#searchNormalized(input, cwd, signal, options), notes);
|
|
13263
|
+
}
|
|
13264
|
+
async#searchNormalized(input, cwd, signal, options) {
|
|
12618
13265
|
validateRawSearchInput(input);
|
|
12619
13266
|
validateRequestContract(input);
|
|
13267
|
+
if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
|
|
13268
|
+
throw new SiftLightError(`${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`);
|
|
13269
|
+
}
|
|
12620
13270
|
let request;
|
|
12621
13271
|
if (input.mode === "await" || input.mode === "cancel") {
|
|
12622
13272
|
request = this.#operationCommand(input, cwd, signal);
|
|
@@ -13015,160 +13665,11 @@ ${page.body}${rangeNote}${contextNote}${missingSelectionNote}
|
|
|
13015
13665
|
}
|
|
13016
13666
|
}
|
|
13017
13667
|
|
|
13018
|
-
// src/config-reader.ts
|
|
13019
|
-
import { readFile as readFile3 } from "node:fs/promises";
|
|
13020
|
-
var SIFT_LIGHT_CONFIG_ENV = "SIFT_LIGHT_CONFIG";
|
|
13021
|
-
var SEMANTIC_JUDGE_API_KEY_ENVS = ["TYPESAFE_API_KEY", "SIFT_LIGHT_JEV_API_KEY"];
|
|
13022
|
-
var DEFAULT_SEMANTIC_JUDGE_CONFIG = {
|
|
13023
|
-
enabled: false,
|
|
13024
|
-
provider: "jev",
|
|
13025
|
-
endpoint: "https://api.typesafe.ai/v1/systemone",
|
|
13026
|
-
apiKeyEnv: SEMANTIC_JUDGE_API_KEY_ENVS[0],
|
|
13027
|
-
model: "jev-latest",
|
|
13028
|
-
timeoutMs: 120000,
|
|
13029
|
-
maxCandidates: 20,
|
|
13030
|
-
maxRetries: 2
|
|
13031
|
-
};
|
|
13032
|
-
var DEFAULT_SIFT_LIGHT_CONFIG = {
|
|
13033
|
-
locale: "en",
|
|
13034
|
-
enforceSearch: "hard",
|
|
13035
|
-
semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG
|
|
13036
|
-
};
|
|
13037
|
-
function hasErrorCode(error, codes) {
|
|
13038
|
-
return error instanceof Error && "code" in error && codes.includes(String(error.code));
|
|
13039
|
-
}
|
|
13040
|
-
function isMissingFile(error) {
|
|
13041
|
-
return hasErrorCode(error, ["ENOENT"]);
|
|
13042
|
-
}
|
|
13043
|
-
function isRawSiftLightConfig(value) {
|
|
13044
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13045
|
-
}
|
|
13046
|
-
function isRawSemanticJudgeConfig(value) {
|
|
13047
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13048
|
-
}
|
|
13049
|
-
function boundedInteger2(value, fallback, minimum, maximum, field) {
|
|
13050
|
-
const candidate = value ?? fallback;
|
|
13051
|
-
if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < minimum || candidate > maximum) {
|
|
13052
|
-
throw new Error(`Invalid sift-light ${field}: expected an integer from ${String(minimum)} through ${String(maximum)}`);
|
|
13053
|
-
}
|
|
13054
|
-
return candidate;
|
|
13055
|
-
}
|
|
13056
|
-
function parseSemanticJudge(value, path) {
|
|
13057
|
-
if (value === undefined)
|
|
13058
|
-
return { ...DEFAULT_SEMANTIC_JUDGE_CONFIG };
|
|
13059
|
-
if (!isRawSemanticJudgeConfig(value)) {
|
|
13060
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge must be an object`);
|
|
13061
|
-
}
|
|
13062
|
-
const unknown = Object.keys(value).filter((key) => ![
|
|
13063
|
-
"enabled",
|
|
13064
|
-
"provider",
|
|
13065
|
-
"endpoint",
|
|
13066
|
-
"apiKeyEnv",
|
|
13067
|
-
"model",
|
|
13068
|
-
"timeoutMs",
|
|
13069
|
-
"maxCandidates",
|
|
13070
|
-
"maxRetries"
|
|
13071
|
-
].includes(key));
|
|
13072
|
-
if (unknown.length > 0) {
|
|
13073
|
-
throw new Error(`Invalid sift-light config at ${path}: unsupported semanticJudge fields; accepted fields are enabled, provider, endpoint, apiKeyEnv, model, timeoutMs, maxCandidates and maxRetries`);
|
|
13074
|
-
}
|
|
13075
|
-
const enabled = value.enabled ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.enabled;
|
|
13076
|
-
if (typeof enabled !== "boolean") {
|
|
13077
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.enabled must be a boolean`);
|
|
13078
|
-
}
|
|
13079
|
-
const provider = value.provider ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.provider;
|
|
13080
|
-
if (provider !== "jev") {
|
|
13081
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.provider must be "jev"`);
|
|
13082
|
-
}
|
|
13083
|
-
const endpoint = value.endpoint ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.endpoint;
|
|
13084
|
-
if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.length > 2048) {
|
|
13085
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a nonempty URL`);
|
|
13086
|
-
}
|
|
13087
|
-
let parsedEndpoint;
|
|
13088
|
-
try {
|
|
13089
|
-
parsedEndpoint = new URL(endpoint);
|
|
13090
|
-
} catch (error) {
|
|
13091
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a URL`, {
|
|
13092
|
-
cause: error
|
|
13093
|
-
});
|
|
13094
|
-
}
|
|
13095
|
-
const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
13096
|
-
const secureEndpoint = parsedEndpoint.protocol === "https:" || parsedEndpoint.protocol === "http:" && loopbackHosts.has(parsedEndpoint.hostname);
|
|
13097
|
-
if (!secureEndpoint || parsedEndpoint.username || parsedEndpoint.password) {
|
|
13098
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must use HTTPS, except that HTTP is allowed for localhost loopback development; URL credentials are not allowed`);
|
|
13099
|
-
}
|
|
13100
|
-
const apiKeyEnv = value.apiKeyEnv ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.apiKeyEnv;
|
|
13101
|
-
if (typeof apiKeyEnv !== "string" || !/^[A-Z][A-Z0-9_]{0,127}$/u.test(apiKeyEnv)) {
|
|
13102
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.apiKeyEnv must be an uppercase environment variable name`);
|
|
13103
|
-
}
|
|
13104
|
-
const model = value.model ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.model;
|
|
13105
|
-
if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
|
|
13106
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.model must be bounded single-line text`);
|
|
13107
|
-
}
|
|
13108
|
-
return {
|
|
13109
|
-
enabled,
|
|
13110
|
-
provider,
|
|
13111
|
-
endpoint,
|
|
13112
|
-
apiKeyEnv,
|
|
13113
|
-
model,
|
|
13114
|
-
timeoutMs: boundedInteger2(value.timeoutMs, DEFAULT_SEMANTIC_JUDGE_CONFIG.timeoutMs, 1000, 1200000, `config at ${path}: semanticJudge.timeoutMs`),
|
|
13115
|
-
maxCandidates: boundedInteger2(value.maxCandidates, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxCandidates, 1, 20, `config at ${path}: semanticJudge.maxCandidates`),
|
|
13116
|
-
maxRetries: boundedInteger2(value.maxRetries, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxRetries, 0, 5, `config at ${path}: semanticJudge.maxRetries`)
|
|
13117
|
-
};
|
|
13118
|
-
}
|
|
13119
|
-
function parseConfig(value, path) {
|
|
13120
|
-
if (!isRawSiftLightConfig(value)) {
|
|
13121
|
-
throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
|
|
13122
|
-
}
|
|
13123
|
-
const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "semanticJudge"].includes(key));
|
|
13124
|
-
if (unknown.length > 0) {
|
|
13125
|
-
throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch and semanticJudge are accepted`);
|
|
13126
|
-
}
|
|
13127
|
-
const { locale, enforceSearch } = value;
|
|
13128
|
-
if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
|
|
13129
|
-
throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
|
|
13130
|
-
}
|
|
13131
|
-
const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
|
|
13132
|
-
return {
|
|
13133
|
-
locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
|
|
13134
|
-
enforceSearch: enforcement,
|
|
13135
|
-
semanticJudge: parseSemanticJudge(value.semanticJudge, path)
|
|
13136
|
-
};
|
|
13137
|
-
}
|
|
13138
|
-
function normalizeSearchEnforcement(value, source) {
|
|
13139
|
-
if (value === undefined || value === "hard")
|
|
13140
|
-
return "hard";
|
|
13141
|
-
if (value === "prefer")
|
|
13142
|
-
return "prefer";
|
|
13143
|
-
if (value === "off")
|
|
13144
|
-
return "off";
|
|
13145
|
-
throw new Error(`Invalid sift-light ${source}: enforceSearch must be "hard", "prefer", or "off"`);
|
|
13146
|
-
}
|
|
13147
|
-
async function readSiftLightConfigFile(path, options = {}) {
|
|
13148
|
-
try {
|
|
13149
|
-
const content = await readFile3(path, "utf8");
|
|
13150
|
-
return parseConfig(JSON.parse(content), path);
|
|
13151
|
-
} catch (error) {
|
|
13152
|
-
if (isMissingFile(error)) {
|
|
13153
|
-
if (options.missing === "error") {
|
|
13154
|
-
throw new Error(`sift-light config was not found at ${path}; create it or unset ${SIFT_LIGHT_CONFIG_ENV}`, { cause: error });
|
|
13155
|
-
}
|
|
13156
|
-
return { ...DEFAULT_SIFT_LIGHT_CONFIG };
|
|
13157
|
-
}
|
|
13158
|
-
if (error instanceof SyntaxError) {
|
|
13159
|
-
throw new Error(`Invalid sift-light config at ${path}: ${error.message}`, {
|
|
13160
|
-
cause: error
|
|
13161
|
-
});
|
|
13162
|
-
}
|
|
13163
|
-
throw error;
|
|
13164
|
-
}
|
|
13165
|
-
}
|
|
13166
|
-
|
|
13167
13668
|
// src/prompt-guidelines.ts
|
|
13168
13669
|
var SOURCE_OUTPUT_GUIDANCE = "Auto/summary text may include bounded source excerpts; ordinary matches text is metadata-only. Inspect may return bounded source windows covering an entire small file. Analysis text may include semantic passages; structured details may retain excerpts, names and signatures. Follow output limits, coverage and continuations.";
|
|
13169
13670
|
function siftLightPromptGuidelines(structuredOutput = true) {
|
|
13170
13671
|
return [
|
|
13171
|
-
`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.`,
|
|
13672
|
+
`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.`,
|
|
13172
13673
|
`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.`,
|
|
13173
13674
|
`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.`,
|
|
13174
13675
|
`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.`,
|
|
@@ -13177,7 +13678,7 @@ function siftLightPromptGuidelines(structuredOutput = true) {
|
|
|
13177
13678
|
`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.`,
|
|
13178
13679
|
`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.`,
|
|
13179
13680
|
`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.`,
|
|
13180
|
-
`Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; business concepts
|
|
13681
|
+
`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.`,
|
|
13181
13682
|
`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.`,
|
|
13182
13683
|
`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.`,
|
|
13183
13684
|
`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.`,
|
|
@@ -13188,9 +13689,9 @@ function siftLightPromptGuidelines(structuredOutput = true) {
|
|
|
13188
13689
|
}
|
|
13189
13690
|
function siftLightModelGuidelines() {
|
|
13190
13691
|
return [
|
|
13191
|
-
`Search with pattern and optional path. ${SOURCE_OUTPUT_GUIDANCE}
|
|
13192
|
-
`Use ranked paths and
|
|
13193
|
-
`Modes:
|
|
13692
|
+
`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.`,
|
|
13693
|
+
`Use ranked paths and counts; Cursor continuation pages retained results. Use mode="inspect" for exact source before editing.`,
|
|
13694
|
+
`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.`,
|
|
13194
13695
|
`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.`
|
|
13195
13696
|
];
|
|
13196
13697
|
}
|
|
@@ -13213,12 +13714,12 @@ function stringEnum(values, options) {
|
|
|
13213
13714
|
...options?.description ? { description: options.description } : {}
|
|
13214
13715
|
});
|
|
13215
13716
|
}
|
|
13216
|
-
var 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}`;
|
|
13217
|
-
var SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
13717
|
+
var 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}`;
|
|
13718
|
+
var 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.`;
|
|
13218
13719
|
var siftLightSchema = Type.Object({
|
|
13219
13720
|
query: Type.Optional(Type.String({
|
|
13220
13721
|
maxLength: 256,
|
|
13221
|
-
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.`
|
|
13722
|
+
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.`
|
|
13222
13723
|
})),
|
|
13223
13724
|
scope: Type.Optional(stringEnum(["strict", "expand"], {
|
|
13224
13725
|
description: `${fieldGuidance("scope")}; expand (default) retries ordinary content search from project cwd. Applies to ordinary, multi-term and role searches.`
|
|
@@ -13232,9 +13733,9 @@ var siftLightSchema = Type.Object({
|
|
|
13232
13733
|
description: `${fieldGuidance("anyOf")}. ${String(MIN_ANY_OF_TERMS)}-${String(MAX_ANY_OF_TOTAL_TERMS)} distinct case-sensitive single-line terms, at most ${String(MAX_LITERAL_TERM_BYTES)} UTF-8 bytes each. Requests above ${String(MAX_ANY_OF_TERMS)} terms are split into version-checked chunks and merged. Returns every retained occurrence attributed to its term.`
|
|
13233
13734
|
})),
|
|
13234
13735
|
allOf: Type.Optional(Type.Array(Type.String({ maxLength: MAX_PATH_CHARACTERS }), {
|
|
13235
|
-
minItems:
|
|
13736
|
+
minItems: 1,
|
|
13236
13737
|
maxItems: 3,
|
|
13237
|
-
description: `${fieldGuidance("allOf")}.
|
|
13738
|
+
description: `${fieldGuidance("allOf")}. 1-3 distinct terms must occur in one file (default) or one function; one term lists the files or functions containing it.`
|
|
13238
13739
|
})),
|
|
13239
13740
|
within: Type.Optional(stringEnum(["file", "function"], {
|
|
13240
13741
|
description: "Only valid with allOf; omit for ordinary single-pattern searches. function requires JS/TS/TSX and counts only that implementation's own code, excluding nested callbacks, strings/comments/types. Not proof of a shared execution path."
|
|
@@ -13251,7 +13752,7 @@ var siftLightSchema = Type.Object({
|
|
|
13251
13752
|
"unknown"
|
|
13252
13753
|
]), {
|
|
13253
13754
|
minItems: 1,
|
|
13254
|
-
description: "Filter each single-pattern occurrence by syntax role (JS/TS/TSX/Go). Roles may be candidates, especially Go call/conversion
|
|
13755
|
+
description: "Filter each single-pattern occurrence by syntax role (JS/TS/TSX/Go parsed; Python lexical: comment, string, code, declaration, import, call candidates). Roles may be candidates, especially Go call/conversion and Python calls. Cannot combine with allOf."
|
|
13255
13756
|
})),
|
|
13256
13757
|
changes: Type.Optional(Type.Object({
|
|
13257
13758
|
base: Type.Optional(Type.String({
|
|
@@ -13284,7 +13785,7 @@ var siftLightSchema = Type.Object({
|
|
|
13284
13785
|
paths: Type.Optional(Type.Array(Type.String(), {
|
|
13285
13786
|
minItems: 1,
|
|
13286
13787
|
maxItems: MAX_SELECTED_PATHS,
|
|
13287
|
-
description: "Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests."
|
|
13788
|
+
description: "Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests. With mode=inspect and no cursor, opens each file from line 1 (same as targets with line 1)."
|
|
13288
13789
|
})),
|
|
13289
13790
|
glob: Type.Optional(Type.Union([
|
|
13290
13791
|
Type.String({ maxLength: MAX_PATH_CHARACTERS }),
|
|
@@ -13308,7 +13809,7 @@ var siftLightSchema = Type.Object({
|
|
|
13308
13809
|
})),
|
|
13309
13810
|
hidden: Type.Optional(Type.Boolean({ description: "Search hidden files (default true; .git is always excluded)." })),
|
|
13310
13811
|
ignorePolicy: Type.Optional(stringEnum(["respect", "include"], {
|
|
13311
|
-
description: "respect (default) honors ignore rules and reports policy-filtered coverage when files are omitted. include searches ignored files while still excluding .git internals and protected paths."
|
|
13812
|
+
description: "respect (default) honors ignore rules and reports policy-filtered coverage when files are omitted, including mode=files. include searches or lists ignored files while still excluding .git internals and protected paths."
|
|
13312
13813
|
})),
|
|
13313
13814
|
patterns: Type.Optional(Type.Array(Type.Object({
|
|
13314
13815
|
id: Type.String({ minLength: 1, maxLength: 64 }),
|
|
@@ -13353,29 +13854,29 @@ var siftLightSchema = Type.Object({
|
|
|
13353
13854
|
limit: Type.Optional(Type.Integer({
|
|
13354
13855
|
minimum: 1,
|
|
13355
13856
|
maximum: MAX_PAGE_SIZE,
|
|
13356
|
-
description: `${fieldGuidance("limit")}. Ordinary search
|
|
13857
|
+
description: `${fieldGuidance("limit")}. Ordinary search: explicit detail-page match limit (max 100); mode=files: files per page (default 30). Normally omit to preserve automatic summarization; other analysis and inspect modes reject it.`
|
|
13357
13858
|
})),
|
|
13358
|
-
mode: Type.Optional(stringEnum(SIFT_LIGHT_MODES, {
|
|
13359
|
-
description: `Ordinary search defaults to auto; summary/matches request explicit pages. capabilities returns a compact names-only project inventory and per-language supported modes without loading providers. files uses query, structure uses an AST pattern, concept uses a required natural-language query, and hybrid uses one query for exact literal plus concept evidence in a single snapshot. validate rechecks saved search or analysis sources against their recorded origin. await waits for an existing long-running concept or hybrid operation without restarting it; cancel explicitly cancels one and waits for owned cleanup. Copy the returned nextRequest exactly and do not repeat the original query. Waiting is an operation state, not evidence. Validation details report the requested scope, comparison target, coverage, and freshness as current, stale, or unknown; partial coverage is retained during validation. inspect/outline/imports/tests retain their documented location selectors. Syntax results are static evidence; concept and related-test results remain candidates. ${MODE_CONTRACT_DESCRIPTION}
|
|
13859
|
+
mode: Type.Optional(stringEnum([...SIFT_LIGHT_MODES, ...MODE_ALIASES], {
|
|
13860
|
+
description: `Ordinary search defaults to auto; summary/matches request explicit pages. capabilities returns a compact names-only project inventory and per-language supported modes without loading providers. files uses query, structure uses an AST pattern, concept uses a required natural-language query, and hybrid uses one query for exact literal plus concept evidence in a single snapshot. validate rechecks saved search or analysis sources against their recorded origin. await waits for an existing long-running concept or hybrid operation without restarting it; cancel explicitly cancels one and waits for owned cleanup. Copy the returned nextRequest exactly and do not repeat the original query. Waiting is an operation state, not evidence. Validation details report the requested scope, comparison target, coverage, and freshness as current, stale, or unknown; partial coverage is retained during validation. inspect/outline/imports/tests retain their documented location selectors. Syntax results are static evidence; concept and related-test results remain candidates. ${MODE_CONTRACT_DESCRIPTION} anyOf/allOf are field names, not modes; as mode values they are accepted aliases for omitting mode, disclosed in a request note.`
|
|
13360
13861
|
})),
|
|
13361
13862
|
line: Type.Optional(Type.Number({
|
|
13362
|
-
description: "1-indexed source line for path inspection/navigation. Omit with matchIndex, matchIndices or targets."
|
|
13863
|
+
description: "1-indexed source line for path inspection/navigation; path-only inspect starts at line 1. Omit with matchIndex, matchIndices or targets."
|
|
13363
13864
|
})),
|
|
13364
13865
|
matchIndex: Type.Optional(Type.Number({
|
|
13365
13866
|
description: "1-based retained match index for cursor-scoped inspect; replaces path and line."
|
|
13366
13867
|
})),
|
|
13367
13868
|
matchIndices: Type.Optional(Type.Array(Type.Integer({ minimum: 1 }), {
|
|
13368
13869
|
minItems: 1,
|
|
13369
|
-
maxItems:
|
|
13370
|
-
description:
|
|
13870
|
+
maxItems: MAX_INSPECT_REQUEST_TARGETS,
|
|
13871
|
+
description: `Inspect visible match numbers together using the same cursor; mutually exclusive with matchIndex, path, line and targets. One response inspects ${String(MAX_INSPECT_TARGETS)}; the rest are returned as an exact nextRequest.`
|
|
13371
13872
|
})),
|
|
13372
13873
|
targets: Type.Optional(Type.Array(Type.Object({
|
|
13373
13874
|
path: Type.String({ maxLength: MAX_PATH_CHARACTERS }),
|
|
13374
13875
|
line: Type.Integer({ minimum: 1 })
|
|
13375
13876
|
}), {
|
|
13376
13877
|
minItems: 1,
|
|
13377
|
-
maxItems:
|
|
13378
|
-
description:
|
|
13878
|
+
maxItems: MAX_INSPECT_REQUEST_TARGETS,
|
|
13879
|
+
description: `Inspect known path/line locations together without a cursor. One response inspects ${String(MAX_INSPECT_TARGETS)} within a shared 16 KiB budget; the rest are returned as an exact nextRequest.`
|
|
13379
13880
|
})),
|
|
13380
13881
|
cursor: Type.Optional(Type.String({ description: "Opaque cursor from a previous stable search snapshot." })),
|
|
13381
13882
|
operationId: Type.Optional(Type.String({
|
|
@@ -13421,11 +13922,12 @@ function siftLightTool(outputMode) {
|
|
|
13421
13922
|
tool.outputSchema = SIFT_LIGHT_OUTPUT_SCHEMA;
|
|
13422
13923
|
return tool;
|
|
13423
13924
|
}
|
|
13424
|
-
function createDefaultSiftLightMcpService(semanticJudge) {
|
|
13925
|
+
function createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled = false) {
|
|
13425
13926
|
const resolvedSemanticJudge = semanticJudge ?? createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG);
|
|
13426
13927
|
return new SiftLightService({
|
|
13427
13928
|
runRipgrep: createRipgrepRunner(),
|
|
13428
13929
|
structure: createCtagsStructureProvider(),
|
|
13930
|
+
vectorSearchEnabled,
|
|
13429
13931
|
semanticJudge: resolvedSemanticJudge
|
|
13430
13932
|
});
|
|
13431
13933
|
}
|
|
@@ -13848,12 +14350,18 @@ function configuredPath(environment) {
|
|
|
13848
14350
|
const value = environment[SIFT_LIGHT_CONFIG_ENV]?.trim();
|
|
13849
14351
|
return value || undefined;
|
|
13850
14352
|
}
|
|
13851
|
-
async function
|
|
14353
|
+
async function createMcpSearchFeatures(environment = process.env) {
|
|
13852
14354
|
const path = configuredPath(environment);
|
|
13853
14355
|
if (!path)
|
|
13854
|
-
return
|
|
14356
|
+
return {
|
|
14357
|
+
semanticJudge: createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG),
|
|
14358
|
+
vectorSearchEnabled: DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled === true
|
|
14359
|
+
};
|
|
13855
14360
|
const config = await readSiftLightConfigFile(path, { missing: "error" });
|
|
13856
|
-
return
|
|
14361
|
+
return {
|
|
14362
|
+
semanticJudge: createConfiguredSemanticJudgeIntegration(config, environment),
|
|
14363
|
+
vectorSearchEnabled: config.vectorSearchEnabled === true
|
|
14364
|
+
};
|
|
13857
14365
|
}
|
|
13858
14366
|
function mcpSemanticJudgeConfigSource(environment = process.env) {
|
|
13859
14367
|
return configuredPath(environment) ? "explicit-config" : "not-configured";
|
|
@@ -13967,20 +14475,17 @@ function environmentInteger(name, fallback, minimum, maximum) {
|
|
|
13967
14475
|
function allowedOrigins() {
|
|
13968
14476
|
return (process.env.SIFT_LIGHT_MCP_ALLOWED_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
13969
14477
|
}
|
|
13970
|
-
async function configuredSemanticJudge() {
|
|
13971
|
-
return createMcpSemanticJudgeIntegration();
|
|
13972
|
-
}
|
|
13973
14478
|
function logSemanticJudgeStatus(integration) {
|
|
13974
14479
|
const status = integration.config.enabled ? "enabled" : "disabled";
|
|
13975
14480
|
process.stderr.write(`sift-light MCP semantic judge: ${status}; source=${mcpSemanticJudgeConfigSource()}
|
|
13976
14481
|
`);
|
|
13977
14482
|
}
|
|
13978
|
-
async function runHttpServer(outputMode, semanticJudge) {
|
|
14483
|
+
async function runHttpServer(outputMode, semanticJudge, vectorSearchEnabled) {
|
|
13979
14484
|
const running = await startSiftLightMcpServer({
|
|
13980
14485
|
cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.cwd(),
|
|
13981
14486
|
host: process.env.SIFT_LIGHT_MCP_HOST ?? DEFAULT_MCP_HOST,
|
|
13982
14487
|
port: environmentInteger("SIFT_LIGHT_MCP_PORT", DEFAULT_MCP_PORT, 0, 65535),
|
|
13983
|
-
createService: () => createDefaultSiftLightMcpService(semanticJudge),
|
|
14488
|
+
createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled),
|
|
13984
14489
|
maxSessions: environmentInteger("SIFT_LIGHT_MCP_MAX_SESSIONS", DEFAULT_MCP_MAX_SESSIONS, 1, Number.MAX_SAFE_INTEGER),
|
|
13985
14490
|
sessionIdleTimeoutMs: environmentInteger("SIFT_LIGHT_MCP_SESSION_IDLE_MS", DEFAULT_MCP_SESSION_IDLE_TIMEOUT_MS, 1, Number.MAX_SAFE_INTEGER),
|
|
13986
14491
|
allowedOrigins: allowedOrigins(),
|
|
@@ -14014,11 +14519,11 @@ async function runHttpServer(outputMode, semanticJudge) {
|
|
|
14014
14519
|
process.once("SIGINT", shutdown);
|
|
14015
14520
|
process.once("SIGTERM", shutdown);
|
|
14016
14521
|
}
|
|
14017
|
-
async function runStdioServer(outputMode, semanticJudge) {
|
|
14522
|
+
async function runStdioServer(outputMode, semanticJudge, vectorSearchEnabled) {
|
|
14018
14523
|
const running = await startSiftLightMcpStdioServer({
|
|
14019
14524
|
cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
|
|
14020
14525
|
outputMode,
|
|
14021
|
-
createService: () => createDefaultSiftLightMcpService(semanticJudge)
|
|
14526
|
+
createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled)
|
|
14022
14527
|
});
|
|
14023
14528
|
process.stderr.write(`sift-light MCP serving one local client over stdio
|
|
14024
14529
|
`);
|
|
@@ -14047,13 +14552,15 @@ async function main() {
|
|
|
14047
14552
|
return;
|
|
14048
14553
|
}
|
|
14049
14554
|
const outputMode = parseSiftLightMcpOutputMode(process.env.SIFT_LIGHT_MCP_OUTPUT_MODE);
|
|
14050
|
-
const semanticJudge = await
|
|
14555
|
+
const { semanticJudge, vectorSearchEnabled } = await createMcpSearchFeatures();
|
|
14051
14556
|
logSemanticJudgeStatus(semanticJudge);
|
|
14557
|
+
process.stderr.write(`sift-light MCP vector search: ${vectorSearchEnabled ? "enabled" : "disabled"}
|
|
14558
|
+
`);
|
|
14052
14559
|
if (transport === "stdio") {
|
|
14053
|
-
await runStdioServer(outputMode, semanticJudge);
|
|
14560
|
+
await runStdioServer(outputMode, semanticJudge, vectorSearchEnabled);
|
|
14054
14561
|
return;
|
|
14055
14562
|
}
|
|
14056
|
-
await runHttpServer(outputMode, semanticJudge);
|
|
14563
|
+
await runHttpServer(outputMode, semanticJudge, vectorSearchEnabled);
|
|
14057
14564
|
}
|
|
14058
14565
|
try {
|
|
14059
14566
|
await main();
|