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.
Files changed (49) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +11 -4
  3. package/README.zh-CN.md +11 -4
  4. package/package.json +1 -1
  5. package/plugins/sift-light/.claude-plugin/plugin.json +1 -1
  6. package/plugins/sift-light/.codex-plugin/plugin.json +1 -1
  7. package/plugins/sift-light/.mcp.json +1 -1
  8. package/plugins/sift-light/concept-worker.mjs +39 -12
  9. package/plugins/sift-light/kimi.plugin.json +2 -2
  10. package/plugins/sift-light/omp-extension.mjs +558 -83
  11. package/plugins/sift-light/package.json +1 -1
  12. package/src/analysis-limits.ts +1 -1
  13. package/src/analysis-store.ts +6 -2
  14. package/src/analysis-types.ts +10 -1
  15. package/src/concept-inference.ts +44 -11
  16. package/src/concept-model.ts +2 -2
  17. package/src/concept-search.ts +12 -2
  18. package/src/concept-worker.mjs +39 -12
  19. package/src/concept-worker.ts +1 -1
  20. package/src/config-reader.ts +11 -3
  21. package/src/evidence-service.ts +117 -13
  22. package/src/file-discovery.ts +111 -17
  23. package/src/hybrid-search.ts +13 -0
  24. package/src/index.ts +4 -10
  25. package/src/inspect.ts +3 -0
  26. package/src/language-capability-definitions.ts +9 -0
  27. package/src/mcp-model-output.ts +47 -1
  28. package/src/mcp-semantic-judge.ts +19 -8
  29. package/src/mcp-server.mjs +754 -247
  30. package/src/mcp-server.ts +11 -13
  31. package/src/mcp.ts +2 -0
  32. package/src/model-error.ts +6 -16
  33. package/src/omp-index.ts +4 -6
  34. package/src/prompt-guidelines.ts +5 -5
  35. package/src/python-roles.ts +247 -0
  36. package/src/redaction.ts +9 -2
  37. package/src/request-aliases.ts +48 -0
  38. package/src/request-contract-catalog.ts +24 -7
  39. package/src/request-contract.ts +60 -6
  40. package/src/request.ts +3 -1
  41. package/src/runtime.ts +2 -2
  42. package/src/semantic-judge.ts +17 -3
  43. package/src/service.ts +31 -1
  44. package/src/session-summary.ts +4 -4
  45. package/src/source-inspection.ts +27 -2
  46. package/src/tool-schema.ts +18 -18
  47. package/src/tui/layout.ts +2 -2
  48. package/src/tui/renderers.ts +2 -2
  49. 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
- return `${prefix}"[REDACTED]"`;
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.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 = 1;
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("pattern is required when cursor is not provided");
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 = 2;
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
- rankingReason: "local multilingual E5 cosine similarity; relevance candidate, no binding or execution claim",
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?.score) - Number(a.details?.score) || a.path.localeCompare(b.path) || a.line - b.line);
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
- for (let index = offset;index < result.items.length && items.length < 30; index += 1) {
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 query = input.query ?? "";
7582
- if (query.length > 256 || !query.isWellFormed() || /[\r\n\0]/.test(query))
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
- if (FILE_QUERY_GLOB_WILDCARD.test(query))
7585
- throw new SiftLightError(`File query ${JSON.stringify(query)} uses glob wildcards; mode=files matches filename and path text, not glob patterns. Omit query to retain every file under path, or use glob to filter by name pattern.`);
7586
- const request = normalizeRequest({ ...input, pattern: "" });
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 files = await listWorkspaceFiles(cwd, signal, {
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 rank = scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
7599
- return rank ? [{ path, ...rank }] : [];
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: files.partial || filtered.partial,
7609
- reasons: [...new Set([...files.reasons, ...filtered.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: { fileEnumeration: files.partial || filtered.partial ? "partial" : "complete" },
7622
- stats: { filesEnumerated: files.paths.length },
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 (document2.utf8 && language && language !== "go") {
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: "Classify the candidate by what it actually does for the requested behavior. Judge the code excerpt, not just matching words.",
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: [...commonFields, "path", "line", "cursor", "matchIndex", "matchIndices", "targets"],
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: [...commonFields, "query", ...sourceFilters, "modifiedAfter", "modifiedBefore"],
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 rejects this field because its scope is fixed strict, and only redundant strict may be removed",
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 flatRule = `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.`;
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: `${reason}; choose the mode explicitly or remove the fields yourself without changing the requested scope. ${flatRule}`
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}; retry the exact nextRequest without repeating the original query.` : `${reason}; no semantics-preserving automatic request is available. ${flatRule} Preserve valid path, filters, redact and cursor fields when choosing the next request.`);
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 < 2 || terms.length > 3 || terms.some((term) => typeof term !== "string" || !term.trim() || /[\r\n\0]/.test(term)) || new Set(terms).size !== terms.length)
10458
- throw new SiftLightError("allOf requires 2\u20133 distinct, nonempty, single-line literal terms");
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 targets = this.#inspectionTargets(input, cwd);
10660
- return targets.some((target) => target.range !== undefined) ? inspectDocumentsMetadata(targets, access, this.#structure) : inspectDocuments(targets, access, this.#continuations, this.#structure);
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 matches");
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
- if (syntaxLanguage(file.document.path))
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 metadataOnly = item.details?.kind === "symbol" || item.details?.kind === "function";
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
- ...metadataOnly ? { range: item.range } : { absoluteFocus: item.range.start }
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(input, cwd, signal, options = {}) {
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 belong in hybrid/concept. Use wholeWord:true for a single-pattern whole-word search. exclude contains file globs, not content negation.`,
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: 2,
21024
+ minItems: 1,
20549
21025
  maxItems: 3,
20550
- description: `${fieldGuidance("allOf")}. 2-3 distinct terms must occur in one file (default) or one function.`
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 ambiguity. Cannot combine with allOf."
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 only: explicit detail-page match limit (max 100). Normally omit to preserve automatic summarization; analysis and inspect modes reject it.`
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: MAX_INSPECT_TARGETS,
20683
- description: "Inspect up to five visible match numbers together using the same cursor; mutually exclusive with matchIndex, path, line and targets."
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: MAX_INSPECT_TARGETS,
20691
- description: "Inspect known path/line locations together without a cursor. The complete batch shares one 16 KiB response budget."
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, message) {
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}. ${recovery.reason}`;
21233
+ Recovery action: ${recovery.action}.`;
20759
21234
  return `
20760
- Error details: ${serialized}
20761
- ${prefix}${next}`;
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 boundedMessage = error.message.toWellFormed().slice(0, 1024);
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: boundedMessage.length > 0 ? details.code : "E_REQUEST_CONTRACT_PAYLOAD",
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), "The request-contract error exceeded the bounded payload budget; correct the request explicitly.");
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 = createSemanticJudgeIntegration(resolvedConfig.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG);
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 and navigate code with bounded, verifiable evidence. Use pattern for content or mode=files with query for filenames.",
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(),