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
|
@@ -71,6 +71,7 @@ var DEFAULT_SEMANTIC_JUDGE_CONFIG = {
|
|
|
71
71
|
var DEFAULT_SIFT_LIGHT_CONFIG = {
|
|
72
72
|
locale: "en",
|
|
73
73
|
enforceSearch: "hard",
|
|
74
|
+
vectorSearchEnabled: false,
|
|
74
75
|
semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG
|
|
75
76
|
};
|
|
76
77
|
function resolveSiftLightConfigPath(agentDirectory, environment = process.env) {
|
|
@@ -163,18 +164,22 @@ function parseConfig(value, path) {
|
|
|
163
164
|
if (!isRawSiftLightConfig(value)) {
|
|
164
165
|
throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
|
|
165
166
|
}
|
|
166
|
-
const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "semanticJudge"].includes(key));
|
|
167
|
+
const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "vectorSearchEnabled", "semanticJudge"].includes(key));
|
|
167
168
|
if (unknown.length > 0) {
|
|
168
|
-
throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch and semanticJudge are accepted`);
|
|
169
|
+
throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled and semanticJudge are accepted`);
|
|
169
170
|
}
|
|
170
|
-
const { locale, enforceSearch } = value;
|
|
171
|
+
const { locale, enforceSearch, vectorSearchEnabled } = value;
|
|
171
172
|
if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
|
|
172
173
|
throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
|
|
173
174
|
}
|
|
175
|
+
if (vectorSearchEnabled !== undefined && typeof vectorSearchEnabled !== "boolean") {
|
|
176
|
+
throw new Error(`Invalid sift-light config at ${path}: vectorSearchEnabled must be a boolean`);
|
|
177
|
+
}
|
|
174
178
|
const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
|
|
175
179
|
return {
|
|
176
180
|
locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
|
|
177
181
|
enforceSearch: enforcement,
|
|
182
|
+
vectorSearchEnabled: vectorSearchEnabled ?? DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled ?? false,
|
|
178
183
|
semanticJudge: parseSemanticJudge(value.semanticJudge, path)
|
|
179
184
|
};
|
|
180
185
|
}
|
|
@@ -224,6 +229,7 @@ var ESTIMATED_CHARACTERS_PER_TOKEN = 4;
|
|
|
224
229
|
var DEFAULT_SUMMARY_FILE_LIMIT = 30;
|
|
225
230
|
var MAX_SELECTED_PATHS = 20;
|
|
226
231
|
var MAX_INSPECT_TARGETS = 5;
|
|
232
|
+
var MAX_INSPECT_REQUEST_TARGETS = 20;
|
|
227
233
|
var MAX_DISPLAYED_OCCURRENCES = 20;
|
|
228
234
|
var MAX_STORED_MATCHES = 50000;
|
|
229
235
|
var MAX_STORED_OCCURRENCES = 200000;
|
|
@@ -803,6 +809,7 @@ var PRIVATE_KEY = /-----BEGIN ([^-\r\n]*PRIVATE KEY)-----[\s\S]*?-----END \1----
|
|
|
803
809
|
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]+)*)`;
|
|
804
810
|
var SENSITIVE_ASSIGNMENT = new RegExp(String.raw`((?<![A-Za-z0-9_-])(?:["']?${SENSITIVE_NAME}["']?)\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}\r\n]+)`, "gi");
|
|
805
811
|
var SENSITIVE_TOKEN = /\b(?:sk|ghp|xox[baprs])[-_][A-Za-z0-9][A-Za-z0-9_-]{7,}\b/g;
|
|
812
|
+
var CODE_EXPRESSION = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\s*[([]/u;
|
|
806
813
|
var TYPE_ONLY_VALUES = new Set([
|
|
807
814
|
"boolean",
|
|
808
815
|
"number",
|
|
@@ -822,10 +829,11 @@ function redactString(value) {
|
|
|
822
829
|
});
|
|
823
830
|
redacted = redacted.replace(SENSITIVE_ASSIGNMENT, (match, prefix, rawValue) => {
|
|
824
831
|
const unquoted = rawValue.replace(/^["']|["']$/g, "").toLowerCase();
|
|
825
|
-
if (TYPE_ONLY_VALUES.has(unquoted))
|
|
832
|
+
if (TYPE_ONLY_VALUES.has(unquoted) || CODE_EXPRESSION.test(rawValue))
|
|
826
833
|
return match;
|
|
827
834
|
count += 1;
|
|
828
|
-
|
|
835
|
+
const quote = rawValue.startsWith('"') || rawValue.startsWith("'") ? rawValue[0] : "";
|
|
836
|
+
return `${prefix}${quote}[REDACTED]${quote}`;
|
|
829
837
|
});
|
|
830
838
|
redacted = redacted.replace(SENSITIVE_TOKEN, () => {
|
|
831
839
|
count += 1;
|
|
@@ -1814,7 +1822,7 @@ function createCtagsStructureProvider(options = {}) {
|
|
|
1814
1822
|
// package.json
|
|
1815
1823
|
var package_default = {
|
|
1816
1824
|
name: "sift-light",
|
|
1817
|
-
version: "1.0.
|
|
1825
|
+
version: "1.0.2",
|
|
1818
1826
|
description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
|
|
1819
1827
|
keywords: [
|
|
1820
1828
|
"ai-agent",
|
|
@@ -2185,7 +2193,7 @@ var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
|
|
|
2185
2193
|
var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
|
|
2186
2194
|
var MAX_CONCEPT_CHARS = 1000;
|
|
2187
2195
|
var CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
|
|
2188
|
-
var CONCEPT_CACHE_VERSION =
|
|
2196
|
+
var CONCEPT_CACHE_VERSION = 2;
|
|
2189
2197
|
var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
2190
2198
|
var CONCEPT_TIMEOUT_MS = 10 * 60000;
|
|
2191
2199
|
var MIN_CONCEPT_TIMEOUT_MS = 1000;
|
|
@@ -2276,7 +2284,7 @@ function normalizeRequest(input) {
|
|
|
2276
2284
|
throw new SiftLightError("ignorePolicy must be respect or include");
|
|
2277
2285
|
const pattern = input.pattern;
|
|
2278
2286
|
if (pattern === undefined) {
|
|
2279
|
-
throw new SiftLightError(
|
|
2287
|
+
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');
|
|
2280
2288
|
}
|
|
2281
2289
|
const path = input.path?.replace(/^@/, "");
|
|
2282
2290
|
return {
|
|
@@ -2316,7 +2324,7 @@ var MAX_ANALYSIS_STORAGE_BYTES = 32 * 1024 * 1024;
|
|
|
2316
2324
|
var ANALYSIS_METADATA_RESERVE_BYTES = 64 * 1024;
|
|
2317
2325
|
var MAX_ANALYSIS_REASONS = 64;
|
|
2318
2326
|
var MAX_ANALYSIS_REASON_BYTES = 4 * 1024;
|
|
2319
|
-
var MIN_ANY_OF_TERMS =
|
|
2327
|
+
var MIN_ANY_OF_TERMS = 1;
|
|
2320
2328
|
var MAX_ANY_OF_TERMS = 8;
|
|
2321
2329
|
var MAX_ANY_OF_TOTAL_TERMS = 64;
|
|
2322
2330
|
var MAX_LITERAL_TERM_BYTES = 256;
|
|
@@ -4033,6 +4041,15 @@ var PYTHON_LANGUAGE_CAPABILITIES = [
|
|
|
4033
4041
|
evidence: "syntax",
|
|
4034
4042
|
availability: "implemented",
|
|
4035
4043
|
load: "lazy"
|
|
4044
|
+
},
|
|
4045
|
+
{
|
|
4046
|
+
id: "python-lexical.roles",
|
|
4047
|
+
name: "roles",
|
|
4048
|
+
provider: "bounded Python lexical role scanner",
|
|
4049
|
+
providerKind: "builtin",
|
|
4050
|
+
evidence: "syntax",
|
|
4051
|
+
availability: "implemented",
|
|
4052
|
+
load: "lazy"
|
|
4036
4053
|
}
|
|
4037
4054
|
];
|
|
4038
4055
|
var DEFAULT_LANGUAGE_CAPABILITIES = [
|
|
@@ -4687,6 +4704,11 @@ function scoreProfile(scores) {
|
|
|
4687
4704
|
spread: top - min
|
|
4688
4705
|
};
|
|
4689
4706
|
}
|
|
4707
|
+
function conceptRankingScore(cosine, passageLength) {
|
|
4708
|
+
const referenceLength = 500;
|
|
4709
|
+
const maximumCorrection = 0.02;
|
|
4710
|
+
return cosine - maximumCorrection * (1 - Math.sqrt(Math.min(passageLength / referenceLength, 1)));
|
|
4711
|
+
}
|
|
4690
4712
|
function passage(document2, start2) {
|
|
4691
4713
|
let end = Math.min(document2.text.length, start2 + MAX_CONCEPT_CHARS);
|
|
4692
4714
|
if (end < document2.text.length) {
|
|
@@ -4945,6 +4967,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
4945
4967
|
const similarity = inferred.scores[index];
|
|
4946
4968
|
if (similarity === undefined)
|
|
4947
4969
|
throw new Error("Missing concept similarity");
|
|
4970
|
+
const rankingScore = conceptRankingScore(similarity, item.text.length);
|
|
4948
4971
|
const evidence = rangeEvidence(item.document, item.range);
|
|
4949
4972
|
return {
|
|
4950
4973
|
path: item.document.path,
|
|
@@ -4957,7 +4980,8 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
4957
4980
|
kind: "concept-candidate",
|
|
4958
4981
|
certainty: "candidate",
|
|
4959
4982
|
score: similarity,
|
|
4960
|
-
|
|
4983
|
+
rankingScore,
|
|
4984
|
+
rankingReason: "local multilingual E5 cosine similarity with bounded short-passage rank correction; relevance candidate, no binding or execution claim",
|
|
4961
4985
|
model: CONCEPT_MODEL,
|
|
4962
4986
|
revision: CONCEPT_REVISION,
|
|
4963
4987
|
tokenTruncated: false,
|
|
@@ -4966,7 +4990,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
4966
4990
|
}
|
|
4967
4991
|
};
|
|
4968
4992
|
});
|
|
4969
|
-
const ranked = [...result.items, ...batchItems].toSorted((a, b) => Number(b.details?.
|
|
4993
|
+
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);
|
|
4970
4994
|
if (ranked.length > MAX_ANALYSIS_RESULTS)
|
|
4971
4995
|
retentionTruncated = true;
|
|
4972
4996
|
result.items = ranked.slice(0, MAX_ANALYSIS_RESULTS);
|
|
@@ -5475,7 +5499,7 @@ function publicAnalysisItem(result, item, index, storedId, modelOutput) {
|
|
|
5475
5499
|
return {
|
|
5476
5500
|
path: item.path,
|
|
5477
5501
|
line: item.line,
|
|
5478
|
-
label: result.kind === "outline" && modelOutput ? item.label : publicAnalysisLabel(result),
|
|
5502
|
+
label: result.kind === "outline" && modelOutput || result.kind === "files" ? item.label : publicAnalysisLabel(result),
|
|
5479
5503
|
index: index + 1,
|
|
5480
5504
|
...inspect ? { inspect } : {},
|
|
5481
5505
|
...publicDetails ? { details: publicDetails } : {}
|
|
@@ -5686,7 +5710,8 @@ Inspect: ${JSON.stringify(inspect)}` : ""}`;
|
|
|
5686
5710
|
break;
|
|
5687
5711
|
}
|
|
5688
5712
|
} else {
|
|
5689
|
-
|
|
5713
|
+
const pageSize = result.pageSize ?? 30;
|
|
5714
|
+
for (let index = offset;index < result.items.length && items.length < pageSize; index += 1) {
|
|
5690
5715
|
if (!appendItem(index))
|
|
5691
5716
|
break;
|
|
5692
5717
|
}
|
|
@@ -5800,6 +5825,8 @@ function resolveInspectionTarget(input, cwd, snapshots) {
|
|
|
5800
5825
|
}
|
|
5801
5826
|
if (!path)
|
|
5802
5827
|
throw new SiftLightError("path is required when mode=inspect");
|
|
5828
|
+
if (line === undefined && input.cursor === undefined)
|
|
5829
|
+
line = 1;
|
|
5803
5830
|
if (line === undefined || !Number.isSafeInteger(line) || line < 1) {
|
|
5804
5831
|
throw new SiftLightError("line must be a positive integer when mode=inspect");
|
|
5805
5832
|
}
|
|
@@ -7528,6 +7555,28 @@ async function filterPathsByModificationTime(cwd, paths, modifiedAfterMs, modifi
|
|
|
7528
7555
|
// src/file-discovery.ts
|
|
7529
7556
|
var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
7530
7557
|
var FILE_QUERY_GLOB_WILDCARD = /[*?]/u;
|
|
7558
|
+
var FILE_QUERY_MATCH_ALL = /^(?:[*/]+|\.\*|\.\+|\*\.\*)$/u;
|
|
7559
|
+
var FILE_QUERY_REGEX_SYNTAX = /[()|^$\\+]/u;
|
|
7560
|
+
var IGNORED_MATCH_SAMPLES = 5;
|
|
7561
|
+
function normalizeFileQuery(query, hasGlob) {
|
|
7562
|
+
const trimmed = query.trim();
|
|
7563
|
+
if (FILE_QUERY_MATCH_ALL.test(trimmed))
|
|
7564
|
+
return {
|
|
7565
|
+
query: "",
|
|
7566
|
+
note: `Query ${JSON.stringify(query)} was treated as listing every file under path; omit query for the same result.`
|
|
7567
|
+
};
|
|
7568
|
+
if (!FILE_QUERY_GLOB_WILDCARD.test(trimmed))
|
|
7569
|
+
return { query };
|
|
7570
|
+
if (!hasGlob && !FILE_QUERY_REGEX_SYNTAX.test(trimmed) && !/\s/u.test(trimmed)) {
|
|
7571
|
+
const glob = trimmed.includes("/") ? trimmed : `**/${trimmed}`;
|
|
7572
|
+
return {
|
|
7573
|
+
query: "",
|
|
7574
|
+
glob,
|
|
7575
|
+
note: `Query ${JSON.stringify(query)} contains glob wildcards and was applied as glob ${JSON.stringify(glob)}; mode=files query otherwise matches filename text.`
|
|
7576
|
+
};
|
|
7577
|
+
}
|
|
7578
|
+
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.`);
|
|
7579
|
+
}
|
|
7531
7580
|
function subsequenceScore(text, query) {
|
|
7532
7581
|
const characters = Array.from(graphemes.segment(text), (item) => item.segment);
|
|
7533
7582
|
const queryCharacters = Array.from(graphemes.segment(query), (item) => item.segment);
|
|
@@ -7578,39 +7627,79 @@ function pathRelativeToDiscoveryRoot(cwd, root, path) {
|
|
|
7578
7627
|
return scoped || platformBasename(absolutePath);
|
|
7579
7628
|
}
|
|
7580
7629
|
async function discoverFiles(input, cwd, signal) {
|
|
7581
|
-
const
|
|
7582
|
-
if (
|
|
7630
|
+
const rawQuery = input.query ?? "";
|
|
7631
|
+
if (rawQuery.length > 256 || !rawQuery.isWellFormed() || /[\r\n\0]/.test(rawQuery))
|
|
7583
7632
|
throw new SiftLightError("File query must be well-formed single-line text of at most 256 characters");
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
const
|
|
7633
|
+
const requestedGlobs = input.glob === undefined ? [] : Array.isArray(input.glob) ? input.glob : [input.glob];
|
|
7634
|
+
const normalizedQuery = normalizeFileQuery(rawQuery, requestedGlobs.length > 0);
|
|
7635
|
+
const query = normalizedQuery.query;
|
|
7636
|
+
const request = normalizeRequest({
|
|
7637
|
+
...input,
|
|
7638
|
+
pattern: "",
|
|
7639
|
+
...normalizedQuery.glob ? { glob: [...requestedGlobs, normalizedQuery.glob] } : {}
|
|
7640
|
+
});
|
|
7587
7641
|
const policy = new SearchPathPolicy(cwd);
|
|
7588
7642
|
const discoveryRoot = request.path ?? ".";
|
|
7589
7643
|
const scoringRoot = await policy.resolveSearchTarget(discoveryRoot);
|
|
7590
|
-
const
|
|
7644
|
+
const includeIgnored = request.ignorePolicy === "include";
|
|
7645
|
+
const listOptions = {
|
|
7591
7646
|
path: scoringRoot,
|
|
7592
7647
|
glob: request.glob,
|
|
7593
7648
|
exclude: request.exclude,
|
|
7594
7649
|
hidden: request.hidden
|
|
7650
|
+
};
|
|
7651
|
+
const files = await listWorkspaceFiles(cwd, signal, {
|
|
7652
|
+
...listOptions,
|
|
7653
|
+
...includeIgnored ? { ignore: false, ignoreParents: false } : {}
|
|
7595
7654
|
});
|
|
7596
7655
|
const filtered = await filterPathsByModificationTime(cwd, files.paths, request.modifiedAfterMs, request.modifiedBeforeMs, signal);
|
|
7656
|
+
const rank = (path) => scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
|
|
7597
7657
|
const selected = filtered.paths.flatMap((path) => {
|
|
7598
|
-
const
|
|
7599
|
-
return
|
|
7658
|
+
const score = rank(path);
|
|
7659
|
+
return score ? [{ path, ...score }] : [];
|
|
7600
7660
|
}).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
7601
7661
|
for (let offset = 0;offset < selected.length; offset += 16) {
|
|
7602
7662
|
await Promise.all(selected.slice(offset, offset + 16).map((item) => policy.assertExistingPath(item.path)));
|
|
7603
7663
|
signal?.throwIfAborted();
|
|
7604
7664
|
}
|
|
7665
|
+
const reasons = new Set([...files.reasons, ...filtered.reasons]);
|
|
7666
|
+
if (normalizedQuery.note)
|
|
7667
|
+
reasons.add(normalizedQuery.note);
|
|
7668
|
+
let ignoredFiles = 0;
|
|
7669
|
+
let ignoredMatches = 0;
|
|
7670
|
+
let ignoredComparisonPartial = false;
|
|
7671
|
+
if (!includeIgnored) {
|
|
7672
|
+
const everything = await listWorkspaceFiles(cwd, signal, {
|
|
7673
|
+
...listOptions,
|
|
7674
|
+
ignore: false,
|
|
7675
|
+
ignoreParents: false
|
|
7676
|
+
});
|
|
7677
|
+
ignoredComparisonPartial = everything.partial;
|
|
7678
|
+
const admitted = new Set(files.paths);
|
|
7679
|
+
const ignored = everything.paths.filter((path) => !admitted.has(path));
|
|
7680
|
+
ignoredFiles = ignored.length;
|
|
7681
|
+
const ignoredCandidates = ignored.flatMap((path) => {
|
|
7682
|
+
const score = rank(path);
|
|
7683
|
+
return score ? [{ path, ...score }] : [];
|
|
7684
|
+
}).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
7685
|
+
ignoredMatches = ignoredCandidates.length;
|
|
7686
|
+
if (ignoredFiles > 0) {
|
|
7687
|
+
const samples = ignoredCandidates.slice(0, IGNORED_MATCH_SAMPLES).map((item) => `${JSON.stringify(item.path)} (${item.reason})`);
|
|
7688
|
+
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.`);
|
|
7689
|
+
}
|
|
7690
|
+
if (ignoredComparisonPartial)
|
|
7691
|
+
reasons.add("Ignored-file comparison was incomplete; the ignored-file count is a lower bound");
|
|
7692
|
+
}
|
|
7693
|
+
const enumerationPartial = files.partial || filtered.partial;
|
|
7605
7694
|
return {
|
|
7606
7695
|
kind: "files",
|
|
7607
7696
|
unit: "files",
|
|
7608
|
-
partial:
|
|
7609
|
-
reasons: [...
|
|
7697
|
+
partial: enumerationPartial,
|
|
7698
|
+
reasons: [...reasons],
|
|
7610
7699
|
items: selected.map((item) => ({
|
|
7611
7700
|
path: item.path,
|
|
7612
7701
|
line: 1,
|
|
7613
|
-
label: `File candidate (${item.reason})`,
|
|
7702
|
+
label: `File candidate (score ${String(item.score)}: ${item.reason})`,
|
|
7614
7703
|
details: {
|
|
7615
7704
|
kind: "file",
|
|
7616
7705
|
score: item.score,
|
|
@@ -7618,8 +7707,13 @@ async function discoverFiles(input, cwd, signal) {
|
|
|
7618
7707
|
inspect: { mode: "inspect", path: item.path, line: 1 }
|
|
7619
7708
|
}
|
|
7620
7709
|
})),
|
|
7621
|
-
coverage: {
|
|
7622
|
-
|
|
7710
|
+
coverage: {
|
|
7711
|
+
fileEnumeration: enumerationPartial ? "partial" : ignoredFiles > 0 || ignoredComparisonPartial ? "policy-filtered" : "complete"
|
|
7712
|
+
},
|
|
7713
|
+
stats: {
|
|
7714
|
+
filesEnumerated: files.paths.length,
|
|
7715
|
+
...includeIgnored ? {} : { ignoredFiles, ignoredMatches }
|
|
7716
|
+
},
|
|
7623
7717
|
scope: {
|
|
7624
7718
|
path: request.path ?? ".",
|
|
7625
7719
|
requestedPath: request.path ?? ".",
|
|
@@ -7632,7 +7726,8 @@ async function discoverFiles(input, cwd, signal) {
|
|
|
7632
7726
|
...request.modifiedAfterMs !== undefined ? { modifiedAfterMs: request.modifiedAfterMs } : {},
|
|
7633
7727
|
...request.modifiedBeforeMs !== undefined ? { modifiedBeforeMs: request.modifiedBeforeMs } : {}
|
|
7634
7728
|
},
|
|
7635
|
-
redact: input.redact ?? false
|
|
7729
|
+
redact: input.redact ?? false,
|
|
7730
|
+
...input.limit !== undefined ? { pageSize: request.pageSize } : {}
|
|
7636
7731
|
};
|
|
7637
7732
|
}
|
|
7638
7733
|
|
|
@@ -7873,7 +7968,19 @@ async function prepare(target, access, structure) {
|
|
|
7873
7968
|
let range = target.range;
|
|
7874
7969
|
let details = { status: "no-symbol" };
|
|
7875
7970
|
const language = syntaxLanguage(document2.path);
|
|
7876
|
-
if (
|
|
7971
|
+
if (target.range && target.structure) {
|
|
7972
|
+
document2.checkRange(target.range);
|
|
7973
|
+
const lines = {
|
|
7974
|
+
startLine: document2.lineAt(target.range.start),
|
|
7975
|
+
endLine: document2.lineAt(Math.max(target.range.start, target.range.end - 1))
|
|
7976
|
+
};
|
|
7977
|
+
range = document2.lineRange(lines.startLine, lines.endLine);
|
|
7978
|
+
details = {
|
|
7979
|
+
...target.structure,
|
|
7980
|
+
range: lines,
|
|
7981
|
+
...target.structure.symbol ? { symbol: { ...target.structure.symbol, range: lines } } : {}
|
|
7982
|
+
};
|
|
7983
|
+
} else if (document2.utf8 && language && language !== "go") {
|
|
7877
7984
|
const syntax = await access.syntax(document2);
|
|
7878
7985
|
details = {
|
|
7879
7986
|
status: syntax.status === "ok" ? "no-symbol" : syntax.status === "unsupported" ? "provider-unavailable" : "parse-error",
|
|
@@ -7925,7 +8032,7 @@ async function prepare(target, access, structure) {
|
|
|
7925
8032
|
details = { status: "provider-unavailable", ...language ? { language } : {} };
|
|
7926
8033
|
}
|
|
7927
8034
|
range ??= document2.lineRange(Math.max(1, target.line - 10), Math.min(document2.lineStarts.length, target.line + 10));
|
|
7928
|
-
const boundary = target.range ? "requested-range" : details.status === "available" && details.range ? "syntax" : "line-window";
|
|
8035
|
+
const boundary = target.range ? target.structure ? "syntax" : "requested-range" : details.status === "available" && details.range ? "syntax" : "line-window";
|
|
7929
8036
|
document2.checkRange(range);
|
|
7930
8037
|
return { target, document: document2, range, structure: details, boundary, focus };
|
|
7931
8038
|
}
|
|
@@ -8284,8 +8391,10 @@ ${preview.text}`);
|
|
|
8284
8391
|
}
|
|
8285
8392
|
};
|
|
8286
8393
|
}
|
|
8287
|
-
async function continueSource(cursor, access, continuations) {
|
|
8394
|
+
async function continueSource(cursor, access, continuations, expectedPath) {
|
|
8288
8395
|
const state = continuations.resolve(cursor);
|
|
8396
|
+
if (expectedPath !== undefined && resolve18(access.cwd, expectedPath.replace(/^@/, "")) !== resolve18(access.cwd, state.source.path))
|
|
8397
|
+
throw new SiftLightError(`sourceCursor continues ${JSON.stringify(state.source.path)}, not ${JSON.stringify(expectedPath)}; copy the returned nextRequest exactly`);
|
|
8289
8398
|
const document2 = await access.load(state.source.path, state.source);
|
|
8290
8399
|
const page = sourcePage(document2, state.remaining, MAX_RESULT_BYTES - 1400);
|
|
8291
8400
|
const next = continuations.advance(cursor, page.fragment);
|
|
@@ -9162,6 +9271,221 @@ function parsePythonOutline(document2) {
|
|
|
9162
9271
|
});
|
|
9163
9272
|
}
|
|
9164
9273
|
|
|
9274
|
+
// src/python-roles.ts
|
|
9275
|
+
var KEYWORDS = new Set([
|
|
9276
|
+
"and",
|
|
9277
|
+
"as",
|
|
9278
|
+
"assert",
|
|
9279
|
+
"async",
|
|
9280
|
+
"await",
|
|
9281
|
+
"case",
|
|
9282
|
+
"del",
|
|
9283
|
+
"elif",
|
|
9284
|
+
"else",
|
|
9285
|
+
"except",
|
|
9286
|
+
"for",
|
|
9287
|
+
"from",
|
|
9288
|
+
"global",
|
|
9289
|
+
"if",
|
|
9290
|
+
"import",
|
|
9291
|
+
"in",
|
|
9292
|
+
"is",
|
|
9293
|
+
"lambda",
|
|
9294
|
+
"match",
|
|
9295
|
+
"nonlocal",
|
|
9296
|
+
"not",
|
|
9297
|
+
"or",
|
|
9298
|
+
"raise",
|
|
9299
|
+
"return",
|
|
9300
|
+
"while",
|
|
9301
|
+
"with",
|
|
9302
|
+
"yield"
|
|
9303
|
+
]);
|
|
9304
|
+
var STRING_PREFIX = /^(?:[rRbBuUfF]|[rR][bBfF]|[bBfF][rR])?$/u;
|
|
9305
|
+
var IDENTIFIER_START = /[\p{ID_Start}_]/u;
|
|
9306
|
+
var IDENTIFIER_PART = /[\p{ID_Continue}]/u;
|
|
9307
|
+
function lex(text) {
|
|
9308
|
+
const comments = [];
|
|
9309
|
+
const strings = [];
|
|
9310
|
+
const code = new Uint8Array(text.length).fill(1);
|
|
9311
|
+
const mark = (start2, end) => {
|
|
9312
|
+
code.fill(0, start2, end);
|
|
9313
|
+
};
|
|
9314
|
+
let index = 0;
|
|
9315
|
+
while (index < text.length) {
|
|
9316
|
+
const character = text[index];
|
|
9317
|
+
if (character === "#") {
|
|
9318
|
+
const newline = text.indexOf(`
|
|
9319
|
+
`, index);
|
|
9320
|
+
const end = newline < 0 ? text.length : newline;
|
|
9321
|
+
comments.push({ start: index, end });
|
|
9322
|
+
mark(index, end);
|
|
9323
|
+
index = end;
|
|
9324
|
+
continue;
|
|
9325
|
+
}
|
|
9326
|
+
if (character !== "'" && character !== '"') {
|
|
9327
|
+
index += 1;
|
|
9328
|
+
continue;
|
|
9329
|
+
}
|
|
9330
|
+
let prefixStart = index;
|
|
9331
|
+
while (prefixStart > 0 && /[rRbBuUfF]/u.test(text[prefixStart - 1] ?? ""))
|
|
9332
|
+
prefixStart -= 1;
|
|
9333
|
+
const prefix = text.slice(prefixStart, index);
|
|
9334
|
+
const beforePrefix = text[prefixStart - 1] ?? "";
|
|
9335
|
+
const start2 = STRING_PREFIX.test(prefix) && !IDENTIFIER_PART.test(beforePrefix) ? prefixStart : index;
|
|
9336
|
+
const triple = text.slice(index, index + 3) === character.repeat(3);
|
|
9337
|
+
const delimiter = triple ? character.repeat(3) : character;
|
|
9338
|
+
let cursor = index + delimiter.length;
|
|
9339
|
+
let end = text.length;
|
|
9340
|
+
while (cursor < text.length) {
|
|
9341
|
+
const current = text[cursor];
|
|
9342
|
+
if (current === "\\") {
|
|
9343
|
+
cursor += 2;
|
|
9344
|
+
continue;
|
|
9345
|
+
}
|
|
9346
|
+
if (!triple && current === `
|
|
9347
|
+
`) {
|
|
9348
|
+
end = cursor;
|
|
9349
|
+
break;
|
|
9350
|
+
}
|
|
9351
|
+
if (text.startsWith(delimiter, cursor)) {
|
|
9352
|
+
end = cursor + delimiter.length;
|
|
9353
|
+
break;
|
|
9354
|
+
}
|
|
9355
|
+
cursor += 1;
|
|
9356
|
+
}
|
|
9357
|
+
strings.push({ start: start2, end });
|
|
9358
|
+
mark(start2, end);
|
|
9359
|
+
index = end;
|
|
9360
|
+
}
|
|
9361
|
+
return { comments, strings, code };
|
|
9362
|
+
}
|
|
9363
|
+
function codeRanges(code) {
|
|
9364
|
+
const ranges = [];
|
|
9365
|
+
let start2 = -1;
|
|
9366
|
+
for (let index = 0;index <= code.length; index += 1) {
|
|
9367
|
+
const inCode = index < code.length && code[index] === 1;
|
|
9368
|
+
if (inCode && start2 < 0)
|
|
9369
|
+
start2 = index;
|
|
9370
|
+
else if (!inCode && start2 >= 0) {
|
|
9371
|
+
ranges.push({ start: start2, end: index });
|
|
9372
|
+
start2 = -1;
|
|
9373
|
+
}
|
|
9374
|
+
}
|
|
9375
|
+
return ranges;
|
|
9376
|
+
}
|
|
9377
|
+
function identifierEnd(text, start2) {
|
|
9378
|
+
let end = start2;
|
|
9379
|
+
while (end < text.length && IDENTIFIER_PART.test(text[end] ?? ""))
|
|
9380
|
+
end += 1;
|
|
9381
|
+
return end;
|
|
9382
|
+
}
|
|
9383
|
+
function skipSpaces(text, index, code) {
|
|
9384
|
+
let cursor = index;
|
|
9385
|
+
while (cursor < text.length && code[cursor] === 1 && /[ \t]/u.test(text[cursor] ?? ""))
|
|
9386
|
+
cursor += 1;
|
|
9387
|
+
return cursor;
|
|
9388
|
+
}
|
|
9389
|
+
function statementEnd(text, start2, code) {
|
|
9390
|
+
let depth = 0;
|
|
9391
|
+
for (let index = start2;index < text.length; index += 1) {
|
|
9392
|
+
if (code[index] !== 1)
|
|
9393
|
+
continue;
|
|
9394
|
+
const character = text[index];
|
|
9395
|
+
if (character === "(" || character === "[" || character === "{")
|
|
9396
|
+
depth += 1;
|
|
9397
|
+
else if (character === ")" || character === "]" || character === "}")
|
|
9398
|
+
depth = Math.max(0, depth - 1);
|
|
9399
|
+
else if (character === "\\" && text[index + 1] === `
|
|
9400
|
+
`)
|
|
9401
|
+
index += 1;
|
|
9402
|
+
else if (character === `
|
|
9403
|
+
` && depth === 0)
|
|
9404
|
+
return index;
|
|
9405
|
+
}
|
|
9406
|
+
return text.length;
|
|
9407
|
+
}
|
|
9408
|
+
function pythonRoleAnalysis(document2) {
|
|
9409
|
+
const text = document2.text;
|
|
9410
|
+
const { comments, strings, code } = lex(text);
|
|
9411
|
+
const roles = [];
|
|
9412
|
+
const push = (start2, end, role, certainty, subkind) => {
|
|
9413
|
+
if (start2 < end)
|
|
9414
|
+
roles.push({ start: start2, end, role, certainty, node: 0, ...subkind ? { subkind } : {} });
|
|
9415
|
+
};
|
|
9416
|
+
for (const span of comments)
|
|
9417
|
+
push(span.start, span.end, "comment", "syntax");
|
|
9418
|
+
for (const span of strings)
|
|
9419
|
+
push(span.start, span.end, "string", "syntax");
|
|
9420
|
+
for (const span of codeRanges(code))
|
|
9421
|
+
push(span.start, span.end, "code", "syntax");
|
|
9422
|
+
let index = 0;
|
|
9423
|
+
let lineStart = true;
|
|
9424
|
+
while (index < text.length) {
|
|
9425
|
+
const character = text[index] ?? "";
|
|
9426
|
+
if (character === `
|
|
9427
|
+
`) {
|
|
9428
|
+
lineStart = true;
|
|
9429
|
+
index += 1;
|
|
9430
|
+
continue;
|
|
9431
|
+
}
|
|
9432
|
+
if (code[index] !== 1 || !IDENTIFIER_START.test(character)) {
|
|
9433
|
+
if (!/[ \t]/u.test(character))
|
|
9434
|
+
lineStart = false;
|
|
9435
|
+
index += 1;
|
|
9436
|
+
continue;
|
|
9437
|
+
}
|
|
9438
|
+
const previous = text[index - 1] ?? "";
|
|
9439
|
+
if (IDENTIFIER_PART.test(previous)) {
|
|
9440
|
+
index += 1;
|
|
9441
|
+
continue;
|
|
9442
|
+
}
|
|
9443
|
+
const end = identifierEnd(text, index);
|
|
9444
|
+
const word = text.slice(index, end);
|
|
9445
|
+
const atStatementStart = lineStart;
|
|
9446
|
+
lineStart = false;
|
|
9447
|
+
if (atStatementStart && (word === "import" || word === "from")) {
|
|
9448
|
+
const statement = statementEnd(text, index, code);
|
|
9449
|
+
const body2 = text.slice(index, statement);
|
|
9450
|
+
if (word === "import" || /\bimport\b/u.test(body2)) {
|
|
9451
|
+
push(index, statement, "import", "syntax", word === "from" ? "from-import" : "import");
|
|
9452
|
+
index = statement;
|
|
9453
|
+
continue;
|
|
9454
|
+
}
|
|
9455
|
+
}
|
|
9456
|
+
if (word === "def" || word === "class") {
|
|
9457
|
+
const nameStart = skipSpaces(text, end, code);
|
|
9458
|
+
if (IDENTIFIER_START.test(text[nameStart] ?? "")) {
|
|
9459
|
+
const nameEnd = identifierEnd(text, nameStart);
|
|
9460
|
+
push(nameStart, nameEnd, "declaration", "syntax", word === "def" ? "function" : "class");
|
|
9461
|
+
index = nameEnd;
|
|
9462
|
+
continue;
|
|
9463
|
+
}
|
|
9464
|
+
}
|
|
9465
|
+
let chainEnd = end;
|
|
9466
|
+
while (text[chainEnd] === "." && IDENTIFIER_START.test(text[chainEnd + 1] ?? "")) {
|
|
9467
|
+
chainEnd = identifierEnd(text, chainEnd + 1);
|
|
9468
|
+
}
|
|
9469
|
+
const open = skipSpaces(text, chainEnd, code);
|
|
9470
|
+
const lastSegment = text.slice(text.lastIndexOf(".", chainEnd - 1) + 1, chainEnd);
|
|
9471
|
+
const before = text.slice(Math.max(0, index - 4), index);
|
|
9472
|
+
if (text[open] === "(" && code[open] === 1 && !KEYWORDS.has(word) && !KEYWORDS.has(lastSegment) && !/\bdef\s+$|\bclass\s+$/u.test(before)) {
|
|
9473
|
+
push(index, open + 1, "call", "candidate", "lexical-call");
|
|
9474
|
+
}
|
|
9475
|
+
index = chainEnd;
|
|
9476
|
+
}
|
|
9477
|
+
roles.sort((a, b) => a.start - b.start || a.end - b.end || a.role.localeCompare(b.role));
|
|
9478
|
+
return {
|
|
9479
|
+
status: "ok",
|
|
9480
|
+
nodes: [],
|
|
9481
|
+
children: [],
|
|
9482
|
+
symbols: [],
|
|
9483
|
+
roles,
|
|
9484
|
+
diagnostics: [],
|
|
9485
|
+
limited: false
|
|
9486
|
+
};
|
|
9487
|
+
}
|
|
9488
|
+
|
|
9165
9489
|
// src/hybrid-search.ts
|
|
9166
9490
|
import { resolve as resolve20 } from "path";
|
|
9167
9491
|
|
|
@@ -9312,7 +9636,7 @@ function requestBody(query, candidates, model) {
|
|
|
9312
9636
|
for (const candidate of candidates) {
|
|
9313
9637
|
questions[candidate.id] = {
|
|
9314
9638
|
type: "choice",
|
|
9315
|
-
instructions:
|
|
9639
|
+
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.`,
|
|
9316
9640
|
criteria: {
|
|
9317
9641
|
"implementation-candidate": "The excerpt appears to implement the requested behavior or its core decision/side effect.",
|
|
9318
9642
|
"caller-candidate": "The excerpt invokes or wires an implementation but does not implement the behavior itself.",
|
|
@@ -9475,6 +9799,10 @@ function createSemanticJudgeIntegration(config, environment = process.env, fetch
|
|
|
9475
9799
|
}
|
|
9476
9800
|
return { config, runner: createJevRunner(config, key, fetcher) };
|
|
9477
9801
|
}
|
|
9802
|
+
function createConfiguredSemanticJudgeIntegration(config, environment = process.env) {
|
|
9803
|
+
const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
|
|
9804
|
+
return config.vectorSearchEnabled === true ? createSemanticJudgeIntegration(judge, environment) : createDisabledSemanticJudgeIntegration(judge);
|
|
9805
|
+
}
|
|
9478
9806
|
function baseDetails(config) {
|
|
9479
9807
|
return {
|
|
9480
9808
|
enabled: config.enabled,
|
|
@@ -9781,6 +10109,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
|
|
|
9781
10109
|
const deduplicationCoverage = scan.snapshotComplete && literal.sourceCoverage === "complete" && conceptSourceCoverage === "complete" ? "complete" : "partial";
|
|
9782
10110
|
const partial = !scan.snapshotComplete || concept.partial || conceptCoverage === "skipped" || conceptSourceCoverage === "partial" || literal.sourceCoverage === "partial" || deduplicationCoverage === "partial" || judgedConcept.semanticJudge?.status === "failed" || judgedConcept.semanticJudge?.status === "partial";
|
|
9783
10111
|
const selectionReason = conceptCandidatesOmitted ? `Hybrid concept limit retained the top ${String(selectedConcept.length)} of ${String(eligibleConcept.length)} non-overlapping semantic candidates` : undefined;
|
|
10112
|
+
const firstScore = Number(eligibleConcept[0]?.details?.rankingScore ?? eligibleConcept[0]?.details?.score);
|
|
10113
|
+
const secondScore = Number(eligibleConcept[1]?.details?.rankingScore ?? eligibleConcept[1]?.details?.score);
|
|
10114
|
+
const closeRanking = Number.isFinite(firstScore) && Number.isFinite(secondScore) && firstScore - secondScore < 0.01;
|
|
9784
10115
|
return {
|
|
9785
10116
|
kind: "hybrid",
|
|
9786
10117
|
unit: "evidence-items",
|
|
@@ -9792,6 +10123,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
|
|
|
9792
10123
|
...literal.reasons,
|
|
9793
10124
|
...execution.sourceGeneration.reasons,
|
|
9794
10125
|
...selectionReason ? [selectionReason] : [],
|
|
10126
|
+
...closeRanking ? [
|
|
10127
|
+
"Semantic ranks are close; verify the leading candidates with source inspection or literal terms."
|
|
10128
|
+
] : [],
|
|
9795
10129
|
...judgedConcept.semanticJudge?.reason ? [judgedConcept.semanticJudge.reason] : []
|
|
9796
10130
|
],
|
|
9797
10131
|
filesRead: (concept.filesRead ?? 0) + access.filesRead,
|
|
@@ -10058,7 +10392,17 @@ var MODE_FIELDS_BY_MODE = {
|
|
|
10058
10392
|
auto: ordinaryFields,
|
|
10059
10393
|
summary: ordinaryFields,
|
|
10060
10394
|
matches: ordinaryFields,
|
|
10061
|
-
inspect: [
|
|
10395
|
+
inspect: [
|
|
10396
|
+
...commonFields,
|
|
10397
|
+
"path",
|
|
10398
|
+
"paths",
|
|
10399
|
+
"line",
|
|
10400
|
+
"cursor",
|
|
10401
|
+
"matchIndex",
|
|
10402
|
+
"matchIndices",
|
|
10403
|
+
"targets",
|
|
10404
|
+
"scope"
|
|
10405
|
+
],
|
|
10062
10406
|
outline: [...commonFields, "path", "line", "symbol", "cursor", "matchIndex", "maxFilesToParse"],
|
|
10063
10407
|
imports: [
|
|
10064
10408
|
...commonFields,
|
|
@@ -10078,7 +10422,16 @@ var MODE_FIELDS_BY_MODE = {
|
|
|
10078
10422
|
"matchIndex",
|
|
10079
10423
|
"maxFilesToParse"
|
|
10080
10424
|
],
|
|
10081
|
-
files: [
|
|
10425
|
+
files: [
|
|
10426
|
+
...commonFields,
|
|
10427
|
+
"query",
|
|
10428
|
+
...sourceFilters,
|
|
10429
|
+
"ignorePolicy",
|
|
10430
|
+
"limit",
|
|
10431
|
+
"scope",
|
|
10432
|
+
"modifiedAfter",
|
|
10433
|
+
"modifiedBefore"
|
|
10434
|
+
],
|
|
10082
10435
|
structure: [...commonFields, "pattern", ...sourceFilters, "maxFilesToParse"],
|
|
10083
10436
|
concept: [...commonFields, "query", ...sourceFilters, "maxFilesToParse"],
|
|
10084
10437
|
hybrid: [...commonFields, "query", ...sourceFilters, "conceptLimit", "maxFilesToParse"],
|
|
@@ -10087,9 +10440,7 @@ var MODE_FIELDS_BY_MODE = {
|
|
|
10087
10440
|
await: ["mode", "operationId"],
|
|
10088
10441
|
cancel: ["mode", "operationId"]
|
|
10089
10442
|
};
|
|
10090
|
-
var SAFE_DROP_FIELDS = {
|
|
10091
|
-
files: ["scope"]
|
|
10092
|
-
};
|
|
10443
|
+
var SAFE_DROP_FIELDS = {};
|
|
10093
10444
|
var SUPPORTED_OUTLINE_EXTENSIONS = new Set(DEFAULT_LANGUAGE_CAPABILITIES.flatMap((descriptor) => descriptor.capabilities.some((capability) => capability.name === "outline") ? descriptor.extensions : []));
|
|
10094
10445
|
function modeFields(mode) {
|
|
10095
10446
|
return MODE_FIELDS_BY_MODE[mode];
|
|
@@ -10118,8 +10469,8 @@ var REQUEST_FIELD_GUIDANCE = {
|
|
|
10118
10469
|
allOf: "allOf is case-sensitive exact-literal AND; omit pattern, anyOf, literal, ignoreCase, roles",
|
|
10119
10470
|
context: "context is an output-context budget and is never silently dropped",
|
|
10120
10471
|
limit: "limit is an output/page budget and is never silently dropped",
|
|
10121
|
-
scope: "scope applies to ordinary content search; mode=files
|
|
10122
|
-
ignorePolicy: "respect keeps repository ignore rules; include searches ignored files but always excludes .git internals and protected paths",
|
|
10472
|
+
scope: "scope applies to ordinary content search; mode=files is always strict, so it accepts only the redundant scope=strict",
|
|
10473
|
+
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",
|
|
10123
10474
|
patterns: "audit accepts named exact-literal patterns and returns one closure receipt",
|
|
10124
10475
|
maxFilesToParse: "concept/hybrid automatically batch the requested scope; this optional field sets an advanced hard file ceiling"
|
|
10125
10476
|
};
|
|
@@ -10215,15 +10566,26 @@ function schemaError(input, field, reason) {
|
|
|
10215
10566
|
recovery: { action: "manual", reason }
|
|
10216
10567
|
}, `${field} is invalid: ${reason}`);
|
|
10217
10568
|
}
|
|
10569
|
+
function plainFilePatternRecovery(mode, input, invalid) {
|
|
10570
|
+
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);
|
|
10571
|
+
}
|
|
10572
|
+
function matchAllFilePatternRecovery(mode, input, invalid) {
|
|
10573
|
+
return mode === "files" && invalid.includes("pattern") && input.query === undefined && typeof input.pattern === "string" && /^(?:\.\*|\.\+|\*+)$/u.test(input.pattern.trim());
|
|
10574
|
+
}
|
|
10218
10575
|
function safeNextRequest(input, mode, invalid) {
|
|
10219
10576
|
if (input.redact === true && containsSensitiveText(input))
|
|
10220
10577
|
return;
|
|
10221
10578
|
const safe = new Set(SAFE_DROP_FIELDS[mode] ?? []);
|
|
10579
|
+
const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
|
|
10580
|
+
if (plainFilePattern || matchAllFilePatternRecovery(mode, input, invalid))
|
|
10581
|
+
safe.add("pattern");
|
|
10222
10582
|
if (invalid.some((field) => !safe.has(field)))
|
|
10223
10583
|
return;
|
|
10224
10584
|
const next = { ...input };
|
|
10225
10585
|
for (const field of invalid)
|
|
10226
10586
|
delete next[field];
|
|
10587
|
+
if (plainFilePattern)
|
|
10588
|
+
next.query = input.pattern;
|
|
10227
10589
|
if (selectorIssuesFor(next, mode).length > 0)
|
|
10228
10590
|
return;
|
|
10229
10591
|
try {
|
|
@@ -10252,20 +10614,26 @@ function fieldsError(input, mode, invalid, selectorIssues = []) {
|
|
|
10252
10614
|
});
|
|
10253
10615
|
const visibleFields = visibleInvalid.map((field) => boundedField(field)).join(", ");
|
|
10254
10616
|
const reason = omitted > 0 ? `mode=${mode} does not accept ${visibleFields} and ${String(omitted)} additional field(s)` : `mode=${mode} does not accept ${visibleFields}`;
|
|
10255
|
-
const
|
|
10617
|
+
const nestedModeObject = invalid.includes(mode);
|
|
10618
|
+
const flatRule = nestedModeObject ? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.` : "";
|
|
10619
|
+
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." : "";
|
|
10256
10620
|
return new RequestContractError({
|
|
10257
10621
|
code: "E_MODE_FIELDS",
|
|
10258
10622
|
mode,
|
|
10259
10623
|
issues,
|
|
10260
10624
|
recovery: nextRequest ? {
|
|
10261
10625
|
action: "retry",
|
|
10262
|
-
reason: `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
|
|
10626
|
+
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.`,
|
|
10263
10627
|
nextRequest
|
|
10264
10628
|
} : {
|
|
10265
10629
|
action: "manual",
|
|
10266
|
-
reason:
|
|
10630
|
+
reason: [
|
|
10631
|
+
reason,
|
|
10632
|
+
filesPatternRule || "Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
|
|
10633
|
+
flatRule
|
|
10634
|
+
].filter(Boolean).join(" ")
|
|
10267
10635
|
}
|
|
10268
|
-
}, nextRequest ? `${reason};
|
|
10636
|
+
}, 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."}`);
|
|
10269
10637
|
}
|
|
10270
10638
|
function selectorIssuesFor(input, mode) {
|
|
10271
10639
|
if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof input.cursor === "string" && input.cursor.trim().length > 0 && !input.cursor.includes(".analysis")) {
|
|
@@ -10411,7 +10779,7 @@ function validateRequestContract(input) {
|
|
|
10411
10779
|
const allowed = new Set(MODE_FIELDS_BY_MODE[mode]);
|
|
10412
10780
|
if (mode === "inspect" && raw.sourceCursor !== undefined) {
|
|
10413
10781
|
allowed.clear();
|
|
10414
|
-
for (const field of ["mode", "sourceCursor", "redact"])
|
|
10782
|
+
for (const field of ["mode", "sourceCursor", "redact", "path"])
|
|
10415
10783
|
allowed.add(field);
|
|
10416
10784
|
}
|
|
10417
10785
|
if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof raw.cursor === "string" && raw.cursor.includes(".analysis")) {
|
|
@@ -10439,6 +10807,69 @@ import { realpath as realpath6 } from "fs/promises";
|
|
|
10439
10807
|
function isEvidenceRequest(input) {
|
|
10440
10808
|
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);
|
|
10441
10809
|
}
|
|
10810
|
+
function pageInspectRequest(input) {
|
|
10811
|
+
if (input.scope === "expand")
|
|
10812
|
+
throw new SiftLightError("mode=inspect reads exact locations; scope=expand does not apply (omit scope)");
|
|
10813
|
+
const { scope: _scope, paths, ...rest } = input;
|
|
10814
|
+
let request = rest;
|
|
10815
|
+
if (paths !== undefined) {
|
|
10816
|
+
if (input.cursor !== undefined || input.targets !== undefined || input.matchIndices !== undefined || input.path !== undefined || input.line !== undefined || input.matchIndex !== undefined)
|
|
10817
|
+
throw new SiftLightError("mode=inspect paths opens files from line 1 and cannot be combined with cursor, targets, matchIndices, path, line or matchIndex");
|
|
10818
|
+
request = { ...rest, targets: paths.map((path) => ({ path, line: 1 })) };
|
|
10819
|
+
}
|
|
10820
|
+
const redact = input.redact ? { redact: true } : {};
|
|
10821
|
+
const requested = request.targets?.length ?? request.matchIndices?.length ?? 0;
|
|
10822
|
+
if (requested > MAX_INSPECT_REQUEST_TARGETS)
|
|
10823
|
+
throw new SiftLightError(`mode=inspect accepts at most ${String(MAX_INSPECT_REQUEST_TARGETS)} targets per request; ${String(MAX_INSPECT_TARGETS)} are inspected per response`);
|
|
10824
|
+
if (request.targets && request.targets.length > MAX_INSPECT_TARGETS)
|
|
10825
|
+
return {
|
|
10826
|
+
current: { ...request, targets: request.targets.slice(0, MAX_INSPECT_TARGETS) },
|
|
10827
|
+
remaining: {
|
|
10828
|
+
mode: "inspect",
|
|
10829
|
+
targets: request.targets.slice(MAX_INSPECT_TARGETS),
|
|
10830
|
+
...redact
|
|
10831
|
+
}
|
|
10832
|
+
};
|
|
10833
|
+
if (request.matchIndices && request.matchIndices.length > MAX_INSPECT_TARGETS)
|
|
10834
|
+
return {
|
|
10835
|
+
current: { ...request, matchIndices: request.matchIndices.slice(0, MAX_INSPECT_TARGETS) },
|
|
10836
|
+
remaining: {
|
|
10837
|
+
mode: "inspect",
|
|
10838
|
+
...request.cursor !== undefined ? { cursor: request.cursor } : {},
|
|
10839
|
+
matchIndices: request.matchIndices.slice(MAX_INSPECT_TARGETS),
|
|
10840
|
+
...redact
|
|
10841
|
+
}
|
|
10842
|
+
};
|
|
10843
|
+
return { current: request };
|
|
10844
|
+
}
|
|
10845
|
+
function withRemainingInspection(result, remaining) {
|
|
10846
|
+
const count = remaining.targets?.length ?? remaining.matchIndices?.length ?? 0;
|
|
10847
|
+
return {
|
|
10848
|
+
text: `${result.text}
|
|
10849
|
+
|
|
10850
|
+
[${String(count)} more target(s) were not inspected; one response inspects ${String(MAX_INSPECT_TARGETS)}.]
|
|
10851
|
+
Next request: ${JSON.stringify(remaining)}`,
|
|
10852
|
+
details: {
|
|
10853
|
+
...result.details,
|
|
10854
|
+
status: "partial",
|
|
10855
|
+
snapshotComplete: false,
|
|
10856
|
+
nextRequest: remaining
|
|
10857
|
+
}
|
|
10858
|
+
};
|
|
10859
|
+
}
|
|
10860
|
+
function analysisItemStructure(item) {
|
|
10861
|
+
const details = item.details ?? {};
|
|
10862
|
+
const language = typeof details.language === "string" ? details.language : undefined;
|
|
10863
|
+
const name2 = typeof details.name === "string" ? details.name : undefined;
|
|
10864
|
+
const scope = Array.isArray(details.scope) ? details.scope.filter((entry) => typeof entry === "string") : typeof details.scope === "string" ? [details.scope] : [];
|
|
10865
|
+
const kind = details.kind === "function" ? "function" : item.label.split(" ", 1)[0] ?? "symbol";
|
|
10866
|
+
return {
|
|
10867
|
+
status: "available",
|
|
10868
|
+
provider: language === "python" ? "python-outline" : "tree-sitter",
|
|
10869
|
+
...language ? { language } : {},
|
|
10870
|
+
...name2 ? { symbol: { name: name2, kind, scope, range: { startLine: item.line, endLine: item.line } } } : {}
|
|
10871
|
+
};
|
|
10872
|
+
}
|
|
10442
10873
|
function maxFilesToParse(value, defaultValue = MAX_STRUCTURE_FILES) {
|
|
10443
10874
|
const candidate = value ?? defaultValue;
|
|
10444
10875
|
if (!Number.isSafeInteger(candidate) || candidate < 1 || candidate > MAX_CONFIGURABLE_STRUCTURE_FILES) {
|
|
@@ -10454,8 +10885,8 @@ function validateTerms(input) {
|
|
|
10454
10885
|
}
|
|
10455
10886
|
return;
|
|
10456
10887
|
}
|
|
10457
|
-
if (!Array.isArray(terms) || terms.length <
|
|
10458
|
-
throw new SiftLightError("allOf requires
|
|
10888
|
+
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)
|
|
10889
|
+
throw new SiftLightError("allOf requires 1\u20133 distinct, nonempty, single-line literal terms");
|
|
10459
10890
|
if (input.pattern !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
|
|
10460
10891
|
throw new SiftLightError("allOf is an explicit case-sensitive literal conjunction; omit pattern, roles, literal and ignoreCase");
|
|
10461
10892
|
if (input.within !== undefined && input.within !== "file" && input.within !== "function")
|
|
@@ -10653,11 +11084,13 @@ class EvidenceService {
|
|
|
10653
11084
|
throw new CursorError("A nonempty sourceCursor is required");
|
|
10654
11085
|
if (input.mode !== "inspect")
|
|
10655
11086
|
throw new SiftLightError("sourceCursor requires mode=inspect");
|
|
10656
|
-
return continueSource(input.sourceCursor, access, this.#continuations);
|
|
11087
|
+
return continueSource(input.sourceCursor, access, this.#continuations, input.path);
|
|
10657
11088
|
}
|
|
10658
11089
|
if (input.mode === "inspect") {
|
|
10659
|
-
const
|
|
10660
|
-
|
|
11090
|
+
const { current, remaining } = pageInspectRequest(input);
|
|
11091
|
+
const targets = this.#inspectionTargets(current, cwd);
|
|
11092
|
+
const result = await inspectDocuments(targets, access, this.#continuations, this.#structure);
|
|
11093
|
+
return remaining ? withRemainingInspection(result, remaining) : result;
|
|
10661
11094
|
}
|
|
10662
11095
|
if (input.mode === "concept") {
|
|
10663
11096
|
const execution = await this.#conceptSearch(input, access, options.onProgress);
|
|
@@ -10746,7 +11179,7 @@ class EvidenceService {
|
|
|
10746
11179
|
if (input.pattern !== undefined || input.allOf !== undefined || input.within !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
|
|
10747
11180
|
throw new SiftLightError("anyOf is an explicit case-sensitive literal union; omit pattern, allOf, within, roles, literal and ignoreCase");
|
|
10748
11181
|
if (input.mode !== undefined && input.mode !== "auto" && input.mode !== "matches")
|
|
10749
|
-
throw new SiftLightError("anyOf mode must be omitted, auto, or
|
|
11182
|
+
throw new SiftLightError("anyOf mode must be omitted, auto, matches or summary");
|
|
10750
11183
|
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));
|
|
10751
11184
|
const { path: _inputPath, ...unscopedInput } = input;
|
|
10752
11185
|
let chunkAccess = access;
|
|
@@ -10884,9 +11317,10 @@ class EvidenceService {
|
|
|
10884
11317
|
if (item)
|
|
10885
11318
|
result.items.push(item);
|
|
10886
11319
|
} else if (terms || input.roles) {
|
|
10887
|
-
|
|
11320
|
+
const pythonRoles = !terms && /\.py$/iu.test(file.document.path);
|
|
11321
|
+
if (pythonRoles || syntaxLanguage(file.document.path))
|
|
10888
11322
|
syntaxCapableFiles += 1;
|
|
10889
|
-
const syntax = await access.syntax(file.document);
|
|
11323
|
+
const syntax = pythonRoles ? pythonRoleAnalysis(file.document) : await access.syntax(file.document);
|
|
10890
11324
|
const classified = terms ? findFunctionConjunctions(file.document, syntax, terms, input.changes?.scope === "lines" ? file.changedRanges : undefined) : filterRoleOccurrences(file.document, syntax, file.occurrences, input.roles ?? []);
|
|
10891
11325
|
result.items.push(...classified.items);
|
|
10892
11326
|
result.partial ||= classified.partial;
|
|
@@ -10963,12 +11397,12 @@ class EvidenceService {
|
|
|
10963
11397
|
const item = this.#analyses.item(input.cursor, input.matchIndex);
|
|
10964
11398
|
if (!item.source || !item.range)
|
|
10965
11399
|
throw new CursorError("This analysis item has no verified source range");
|
|
10966
|
-
const
|
|
11400
|
+
const bounded = item.details?.kind === "symbol" || item.details?.kind === "function";
|
|
10967
11401
|
return {
|
|
10968
11402
|
path: item.path,
|
|
10969
11403
|
line: item.line,
|
|
10970
11404
|
reference: item.source,
|
|
10971
|
-
...
|
|
11405
|
+
...bounded ? { range: item.range, structure: analysisItemStructure(item) } : { absoluteFocus: item.range.start }
|
|
10972
11406
|
};
|
|
10973
11407
|
}
|
|
10974
11408
|
return {
|
|
@@ -11177,6 +11611,29 @@ import { resolve as resolve24 } from "path";
|
|
|
11177
11611
|
// src/discovery-errors.ts
|
|
11178
11612
|
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>"}';
|
|
11179
11613
|
|
|
11614
|
+
// src/request-aliases.ts
|
|
11615
|
+
var MODE_ALIASES = ["anyOf", "allOf"];
|
|
11616
|
+
function normalizeRequestAliases(request) {
|
|
11617
|
+
const notes = [];
|
|
11618
|
+
const { mode, ...rest } = request;
|
|
11619
|
+
let input = rest;
|
|
11620
|
+
if (mode === "anyOf" || mode === "allOf") {
|
|
11621
|
+
if (request[mode] === undefined)
|
|
11622
|
+
throw new SiftLightError(`mode=${mode} is not a mode; pass ${mode}:[...terms] and omit mode`);
|
|
11623
|
+
notes.push(`mode="${mode}" is not a mode; the ${mode} field alone selects this search.`);
|
|
11624
|
+
} else if (mode === "summary" && request.anyOf !== undefined) {
|
|
11625
|
+
notes.push("anyOf returns one analysis page whose header already carries per-file statistics; mode=summary was served by that page.");
|
|
11626
|
+
} else if (mode !== undefined) {
|
|
11627
|
+
input = { ...rest, mode };
|
|
11628
|
+
}
|
|
11629
|
+
if (input.sourceCursor !== undefined && input.line !== undefined) {
|
|
11630
|
+
const { line: _line, ...withoutLine } = input;
|
|
11631
|
+
input = withoutLine;
|
|
11632
|
+
notes.push("line is ignored with sourceCursor; the continuation selects its own source range.");
|
|
11633
|
+
}
|
|
11634
|
+
return { input, notes };
|
|
11635
|
+
}
|
|
11636
|
+
|
|
11180
11637
|
// src/format.ts
|
|
11181
11638
|
import { readFile as readFile3 } from "fs/promises";
|
|
11182
11639
|
var RESULT_METADATA_RESERVE_BYTES = 1024;
|
|
@@ -12530,6 +12987,16 @@ class LanguageCapabilityCatalog {
|
|
|
12530
12987
|
}
|
|
12531
12988
|
|
|
12532
12989
|
// src/service.ts
|
|
12990
|
+
function withRequestNotes(result, notes) {
|
|
12991
|
+
if (notes.length === 0)
|
|
12992
|
+
return result;
|
|
12993
|
+
return {
|
|
12994
|
+
text: `${notes.map((note) => `[Request note: ${note}]`).join(`
|
|
12995
|
+
`)}
|
|
12996
|
+
${result.text}`,
|
|
12997
|
+
details: { ...result.details, requestNotes: [...notes] }
|
|
12998
|
+
};
|
|
12999
|
+
}
|
|
12533
13000
|
function filterList(value) {
|
|
12534
13001
|
return value === undefined ? [] : Array.isArray(value) ? [...value] : [value];
|
|
12535
13002
|
}
|
|
@@ -12743,6 +13210,7 @@ class SiftLightService {
|
|
|
12743
13210
|
#runRipgrep;
|
|
12744
13211
|
#snapshots;
|
|
12745
13212
|
#summaryFileLimit;
|
|
13213
|
+
#vectorSearchEnabled;
|
|
12746
13214
|
#capabilities = new LanguageCapabilityCatalog;
|
|
12747
13215
|
#evidence;
|
|
12748
13216
|
#lifecycle = new AbortController;
|
|
@@ -12753,12 +13221,20 @@ class SiftLightService {
|
|
|
12753
13221
|
this.#runRipgrep = options.runRipgrep;
|
|
12754
13222
|
this.#snapshots = options.snapshots ?? new SnapshotStore;
|
|
12755
13223
|
this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
|
|
13224
|
+
this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
|
|
12756
13225
|
this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
|
|
12757
13226
|
this.#evidence = new EvidenceService(this.#runRipgrep, this.#snapshots, options.structure, options.conceptSearch, options.semanticJudge);
|
|
12758
13227
|
}
|
|
12759
|
-
async search(
|
|
13228
|
+
async search(request, cwd, signal, options = {}) {
|
|
13229
|
+
const { input, notes } = normalizeRequestAliases(request);
|
|
13230
|
+
return withRequestNotes(await this.#searchNormalized(input, cwd, signal, options), notes);
|
|
13231
|
+
}
|
|
13232
|
+
async#searchNormalized(input, cwd, signal, options) {
|
|
12760
13233
|
validateRawSearchInput(input);
|
|
12761
13234
|
validateRequestContract(input);
|
|
13235
|
+
if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
|
|
13236
|
+
throw new SiftLightError(`${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`);
|
|
13237
|
+
}
|
|
12762
13238
|
let request;
|
|
12763
13239
|
if (input.mode === "await" || input.mode === "cancel") {
|
|
12764
13240
|
request = this.#operationCommand(input, cwd, signal);
|
|
@@ -13161,7 +13637,7 @@ ${page.body}${rangeNote}${contextNote}${missingSelectionNote}
|
|
|
13161
13637
|
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.";
|
|
13162
13638
|
function siftLightPromptGuidelines(structuredOutput = true) {
|
|
13163
13639
|
return [
|
|
13164
|
-
`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.`,
|
|
13640
|
+
`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.`,
|
|
13165
13641
|
`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.`,
|
|
13166
13642
|
`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.`,
|
|
13167
13643
|
`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.`,
|
|
@@ -13170,7 +13646,7 @@ function siftLightPromptGuidelines(structuredOutput = true) {
|
|
|
13170
13646
|
`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.`,
|
|
13171
13647
|
`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.`,
|
|
13172
13648
|
`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.`,
|
|
13173
|
-
`Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; business concepts
|
|
13649
|
+
`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.`,
|
|
13174
13650
|
`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.`,
|
|
13175
13651
|
`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.`,
|
|
13176
13652
|
`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.`,
|
|
@@ -20526,12 +21002,12 @@ function stringEnum(values, options) {
|
|
|
20526
21002
|
...options?.description ? { description: options.description } : {}
|
|
20527
21003
|
});
|
|
20528
21004
|
}
|
|
20529
|
-
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}`;
|
|
20530
|
-
var SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
21005
|
+
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}`;
|
|
21006
|
+
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.`;
|
|
20531
21007
|
var siftLightSchema = _Object_({
|
|
20532
21008
|
query: Optional(String2({
|
|
20533
21009
|
maxLength: 256,
|
|
20534
|
-
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.`
|
|
21010
|
+
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.`
|
|
20535
21011
|
})),
|
|
20536
21012
|
scope: Optional(stringEnum(["strict", "expand"], {
|
|
20537
21013
|
description: `${fieldGuidance("scope")}; expand (default) retries ordinary content search from project cwd. Applies to ordinary, multi-term and role searches.`
|
|
@@ -20545,9 +21021,9 @@ var siftLightSchema = _Object_({
|
|
|
20545
21021
|
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.`
|
|
20546
21022
|
})),
|
|
20547
21023
|
allOf: Optional(_Array_(String2({ maxLength: MAX_PATH_CHARACTERS }), {
|
|
20548
|
-
minItems:
|
|
21024
|
+
minItems: 1,
|
|
20549
21025
|
maxItems: 3,
|
|
20550
|
-
description: `${fieldGuidance("allOf")}.
|
|
21026
|
+
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.`
|
|
20551
21027
|
})),
|
|
20552
21028
|
within: Optional(stringEnum(["file", "function"], {
|
|
20553
21029
|
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."
|
|
@@ -20564,7 +21040,7 @@ var siftLightSchema = _Object_({
|
|
|
20564
21040
|
"unknown"
|
|
20565
21041
|
]), {
|
|
20566
21042
|
minItems: 1,
|
|
20567
|
-
description: "Filter each single-pattern occurrence by syntax role (JS/TS/TSX/Go). Roles may be candidates, especially Go call/conversion
|
|
21043
|
+
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."
|
|
20568
21044
|
})),
|
|
20569
21045
|
changes: Optional(_Object_({
|
|
20570
21046
|
base: Optional(String2({
|
|
@@ -20597,7 +21073,7 @@ var siftLightSchema = _Object_({
|
|
|
20597
21073
|
paths: Optional(_Array_(String2(), {
|
|
20598
21074
|
minItems: 1,
|
|
20599
21075
|
maxItems: MAX_SELECTED_PATHS,
|
|
20600
|
-
description: "Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests."
|
|
21076
|
+
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)."
|
|
20601
21077
|
})),
|
|
20602
21078
|
glob: Optional(Union([
|
|
20603
21079
|
String2({ maxLength: MAX_PATH_CHARACTERS }),
|
|
@@ -20621,7 +21097,7 @@ var siftLightSchema = _Object_({
|
|
|
20621
21097
|
})),
|
|
20622
21098
|
hidden: Optional(Boolean2({ description: "Search hidden files (default true; .git is always excluded)." })),
|
|
20623
21099
|
ignorePolicy: Optional(stringEnum(["respect", "include"], {
|
|
20624
|
-
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."
|
|
21100
|
+
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."
|
|
20625
21101
|
})),
|
|
20626
21102
|
patterns: Optional(_Array_(_Object_({
|
|
20627
21103
|
id: String2({ minLength: 1, maxLength: 64 }),
|
|
@@ -20666,29 +21142,29 @@ var siftLightSchema = _Object_({
|
|
|
20666
21142
|
limit: Optional(Integer({
|
|
20667
21143
|
minimum: 1,
|
|
20668
21144
|
maximum: MAX_PAGE_SIZE,
|
|
20669
|
-
description: `${fieldGuidance("limit")}. Ordinary search
|
|
21145
|
+
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.`
|
|
20670
21146
|
})),
|
|
20671
|
-
mode: Optional(stringEnum(SIFT_LIGHT_MODES, {
|
|
20672
|
-
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}
|
|
21147
|
+
mode: Optional(stringEnum([...SIFT_LIGHT_MODES, ...MODE_ALIASES], {
|
|
21148
|
+
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.`
|
|
20673
21149
|
})),
|
|
20674
21150
|
line: Optional(Number2({
|
|
20675
|
-
description: "1-indexed source line for path inspection/navigation. Omit with matchIndex, matchIndices or targets."
|
|
21151
|
+
description: "1-indexed source line for path inspection/navigation; path-only inspect starts at line 1. Omit with matchIndex, matchIndices or targets."
|
|
20676
21152
|
})),
|
|
20677
21153
|
matchIndex: Optional(Number2({
|
|
20678
21154
|
description: "1-based retained match index for cursor-scoped inspect; replaces path and line."
|
|
20679
21155
|
})),
|
|
20680
21156
|
matchIndices: Optional(_Array_(Integer({ minimum: 1 }), {
|
|
20681
21157
|
minItems: 1,
|
|
20682
|
-
maxItems:
|
|
20683
|
-
description:
|
|
21158
|
+
maxItems: MAX_INSPECT_REQUEST_TARGETS,
|
|
21159
|
+
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.`
|
|
20684
21160
|
})),
|
|
20685
21161
|
targets: Optional(_Array_(_Object_({
|
|
20686
21162
|
path: String2({ maxLength: MAX_PATH_CHARACTERS }),
|
|
20687
21163
|
line: Integer({ minimum: 1 })
|
|
20688
21164
|
}), {
|
|
20689
21165
|
minItems: 1,
|
|
20690
|
-
maxItems:
|
|
20691
|
-
description:
|
|
21166
|
+
maxItems: MAX_INSPECT_REQUEST_TARGETS,
|
|
21167
|
+
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.`
|
|
20692
21168
|
})),
|
|
20693
21169
|
cursor: Optional(String2({ description: "Opaque cursor from a previous stable search snapshot." })),
|
|
20694
21170
|
operationId: Optional(String2({
|
|
@@ -20750,25 +21226,23 @@ function modelErrorText(error) {
|
|
|
20750
21226
|
function requestContractErrorText(error) {
|
|
20751
21227
|
return requestContractProjection(error).text;
|
|
20752
21228
|
}
|
|
20753
|
-
function projectRequestContract(projected, serialized
|
|
20754
|
-
const prefix = `sift-light failed: request rejected [${projected.code}]: ${message}`;
|
|
21229
|
+
function projectRequestContract(projected, serialized) {
|
|
20755
21230
|
const recovery = projected.recovery;
|
|
20756
21231
|
const next = recovery.nextRequest ? `
|
|
20757
21232
|
Copy the nested recovery.nextRequest object unchanged; do not repeat the original query.` : `
|
|
20758
|
-
Recovery action: ${recovery.action}
|
|
21233
|
+
Recovery action: ${recovery.action}.`;
|
|
20759
21234
|
return `
|
|
20760
|
-
|
|
20761
|
-
${
|
|
21235
|
+
sift-light failed: request rejected [${projected.code}].
|
|
21236
|
+
Error details: ${serialized}${next}`;
|
|
20762
21237
|
}
|
|
20763
21238
|
function requestContractProjection(error) {
|
|
20764
21239
|
const details = boundedRequestContractDetails(error.details);
|
|
20765
21240
|
const serializedDetails = JSON.stringify(details);
|
|
20766
|
-
const
|
|
20767
|
-
const result = projectRequestContract(details, serializedDetails, boundedMessage);
|
|
21241
|
+
const result = projectRequestContract(details, serializedDetails);
|
|
20768
21242
|
if (Buffer.byteLength(result) <= MAX_REQUEST_RECOVERY_BYTES)
|
|
20769
21243
|
return { details, text: result };
|
|
20770
21244
|
const compact = {
|
|
20771
|
-
code:
|
|
21245
|
+
code: details.code,
|
|
20772
21246
|
...details.mode ? { mode: details.mode } : {},
|
|
20773
21247
|
issues: [
|
|
20774
21248
|
{
|
|
@@ -20781,7 +21255,7 @@ function requestContractProjection(error) {
|
|
|
20781
21255
|
reason: "Exact recovery was omitted; correct the request explicitly."
|
|
20782
21256
|
}
|
|
20783
21257
|
};
|
|
20784
|
-
const compactText = projectRequestContract(compact, JSON.stringify(compact)
|
|
21258
|
+
const compactText = projectRequestContract(compact, JSON.stringify(compact));
|
|
20785
21259
|
return { details: compact, text: compactText };
|
|
20786
21260
|
}
|
|
20787
21261
|
|
|
@@ -20839,10 +21313,11 @@ async function registerOmpSiftLightExtension(pi, searchPolicyAssets = new URL(".
|
|
|
20839
21313
|
const resolvedConfig = config ?? await readSiftLightConfigFile(resolveSiftLightConfigPath(ompAgentDir()), {
|
|
20840
21314
|
missing: process.env[SIFT_LIGHT_CONFIG_ENV]?.trim() ? "error" : "defaults"
|
|
20841
21315
|
});
|
|
20842
|
-
const semanticJudge =
|
|
21316
|
+
const semanticJudge = createConfiguredSemanticJudgeIntegration(resolvedConfig);
|
|
20843
21317
|
const runtime = new SiftLightRuntime(new SiftLightService({
|
|
20844
21318
|
runRipgrep: createRipgrepRunner(),
|
|
20845
21319
|
structure: createCtagsStructureProvider(),
|
|
21320
|
+
vectorSearchEnabled: resolvedConfig.vectorSearchEnabled === true,
|
|
20846
21321
|
semanticJudge
|
|
20847
21322
|
}));
|
|
20848
21323
|
const { locale } = resolvedConfig;
|
|
@@ -20861,7 +21336,7 @@ async function registerOmpSiftLightExtension(pi, searchPolicyAssets = new URL(".
|
|
|
20861
21336
|
pi.registerTool({
|
|
20862
21337
|
name: SIFT_LIGHT_LABEL,
|
|
20863
21338
|
label: SIFT_LIGHT_LABEL,
|
|
20864
|
-
description: "Search
|
|
21339
|
+
description: "Search code with bounded evidence. Routine searches use pattern for fast exact content or mode=files with query for filenames; concept/hybrid require vectorSearchEnabled:true in sift-light.json.",
|
|
20865
21340
|
approval: "read",
|
|
20866
21341
|
promptSnippet: "Search file contents without flooding context",
|
|
20867
21342
|
promptGuidelines: siftLightPromptGuidelines(),
|