sift-light 1.0.1 → 1.0.3-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +5 -3
  3. package/README.zh-CN.md +5 -3
  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 +3 -2
  7. package/plugins/sift-light/.mcp.json +1 -1
  8. package/plugins/sift-light/kimi.plugin.json +2 -2
  9. package/plugins/sift-light/omp-extension.mjs +517 -72
  10. package/plugins/sift-light/package.json +1 -1
  11. package/plugins/sift-light/skills/local-search/SKILL.md +12 -0
  12. package/src/analysis-limits.ts +1 -1
  13. package/src/analysis-store.ts +16 -10
  14. package/src/analysis-term-pages.ts +2 -1
  15. package/src/analysis-types.ts +10 -1
  16. package/src/evidence-service.ts +118 -14
  17. package/src/file-discovery.ts +111 -17
  18. package/src/inspect.ts +3 -0
  19. package/src/language-capability-definitions.ts +9 -0
  20. package/src/mcp-model-output.ts +6 -1
  21. package/src/mcp-server.mjs +525 -77
  22. package/src/prompt-guidelines.ts +11 -5
  23. package/src/python-roles.ts +247 -0
  24. package/src/redaction.ts +9 -2
  25. package/src/request-aliases.ts +48 -0
  26. package/src/request-contract-catalog.ts +24 -7
  27. package/src/request-contract.ts +21 -3
  28. package/src/request.ts +3 -1
  29. package/src/result-statistics.ts +3 -3
  30. package/src/runtime.ts +2 -2
  31. package/src/service.ts +23 -1
  32. package/src/session-summary.ts +4 -4
  33. package/src/source-document.ts +4 -2
  34. package/src/source-inspection.ts +27 -2
  35. package/src/term-count-label.ts +6 -0
  36. package/src/tool-schema.ts +15 -15
  37. package/src/tui/layout.ts +2 -2
  38. package/src/tui/renderers.ts +2 -2
  39. package/src/types.ts +4 -0
@@ -229,6 +229,7 @@ var ESTIMATED_CHARACTERS_PER_TOKEN = 4;
229
229
  var DEFAULT_SUMMARY_FILE_LIMIT = 30;
230
230
  var MAX_SELECTED_PATHS = 20;
231
231
  var MAX_INSPECT_TARGETS = 5;
232
+ var MAX_INSPECT_REQUEST_TARGETS = 20;
232
233
  var MAX_DISPLAYED_OCCURRENCES = 20;
233
234
  var MAX_STORED_MATCHES = 50000;
234
235
  var MAX_STORED_OCCURRENCES = 200000;
@@ -808,6 +809,7 @@ var PRIVATE_KEY = /-----BEGIN ([^-\r\n]*PRIVATE KEY)-----[\s\S]*?-----END \1----
808
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]+)*)`;
809
810
  var SENSITIVE_ASSIGNMENT = new RegExp(String.raw`((?<![A-Za-z0-9_-])(?:["']?${SENSITIVE_NAME}["']?)\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}\r\n]+)`, "gi");
810
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;
811
813
  var TYPE_ONLY_VALUES = new Set([
812
814
  "boolean",
813
815
  "number",
@@ -827,10 +829,11 @@ function redactString(value) {
827
829
  });
828
830
  redacted = redacted.replace(SENSITIVE_ASSIGNMENT, (match, prefix, rawValue) => {
829
831
  const unquoted = rawValue.replace(/^["']|["']$/g, "").toLowerCase();
830
- if (TYPE_ONLY_VALUES.has(unquoted))
832
+ if (TYPE_ONLY_VALUES.has(unquoted) || CODE_EXPRESSION.test(rawValue))
831
833
  return match;
832
834
  count += 1;
833
- return `${prefix}"[REDACTED]"`;
835
+ const quote = rawValue.startsWith('"') || rawValue.startsWith("'") ? rawValue[0] : "";
836
+ return `${prefix}${quote}[REDACTED]${quote}`;
834
837
  });
835
838
  redacted = redacted.replace(SENSITIVE_TOKEN, () => {
836
839
  count += 1;
@@ -1819,7 +1822,7 @@ function createCtagsStructureProvider(options = {}) {
1819
1822
  // package.json
1820
1823
  var package_default = {
1821
1824
  name: "sift-light",
1822
- version: "1.0.1",
1825
+ version: "1.0.3-1",
1823
1826
  description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
1824
1827
  keywords: [
1825
1828
  "ai-agent",
@@ -2281,7 +2284,7 @@ function normalizeRequest(input) {
2281
2284
  throw new SiftLightError("ignorePolicy must be respect or include");
2282
2285
  const pattern = input.pattern;
2283
2286
  if (pattern === undefined) {
2284
- 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');
2285
2288
  }
2286
2289
  const path = input.path?.replace(/^@/, "");
2287
2290
  return {
@@ -2321,7 +2324,7 @@ var MAX_ANALYSIS_STORAGE_BYTES = 32 * 1024 * 1024;
2321
2324
  var ANALYSIS_METADATA_RESERVE_BYTES = 64 * 1024;
2322
2325
  var MAX_ANALYSIS_REASONS = 64;
2323
2326
  var MAX_ANALYSIS_REASON_BYTES = 4 * 1024;
2324
- var MIN_ANY_OF_TERMS = 2;
2327
+ var MIN_ANY_OF_TERMS = 1;
2325
2328
  var MAX_ANY_OF_TERMS = 8;
2326
2329
  var MAX_ANY_OF_TOTAL_TERMS = 64;
2327
2330
  var MAX_LITERAL_TERM_BYTES = 256;
@@ -3479,8 +3482,10 @@ async function readWorkspaceDocument(path, cwd, signal, expected, readBudget = M
3479
3482
  realpath3(cwd)
3480
3483
  ]);
3481
3484
  if (!canonical)
3482
- throw new SourceDocumentError("source-unavailable", "Source is unavailable");
3483
- const before = await getSourceRevision(absolute);
3485
+ throw new SourceDocumentError("source-unavailable", `Path not found: ${path}`);
3486
+ const before = await getSourceRevision(absolute, (error) => {
3487
+ throw error;
3488
+ });
3484
3489
  if (!before)
3485
3490
  throw new SourceDocumentError("source-unavailable", "Source is unavailable");
3486
3491
  if (expected && !sameSourceRevision(before, expected.revision)) {
@@ -4038,6 +4043,15 @@ var PYTHON_LANGUAGE_CAPABILITIES = [
4038
4043
  evidence: "syntax",
4039
4044
  availability: "implemented",
4040
4045
  load: "lazy"
4046
+ },
4047
+ {
4048
+ id: "python-lexical.roles",
4049
+ name: "roles",
4050
+ provider: "bounded Python lexical role scanner",
4051
+ providerKind: "builtin",
4052
+ evidence: "syntax",
4053
+ availability: "implemented",
4054
+ load: "lazy"
4041
4055
  }
4042
4056
  ];
4043
4057
  var DEFAULT_LANGUAGE_CAPABILITIES = [
@@ -5186,6 +5200,11 @@ var SEMANTIC_JUDGE_CLASSIFICATIONS = [
5186
5200
  ];
5187
5201
  var SEMANTIC_JUDGE_NON_PROOF_CLAIM = "semantic classification only; local static and runtime verification is not asserted";
5188
5202
 
5203
+ // src/term-count-label.ts
5204
+ function termCountLabel(term, index, redact = false) {
5205
+ return redact ? `condition #${String(index + 1)}` : `#${String(index + 1)} ${JSON.stringify(term)}`;
5206
+ }
5207
+
5189
5208
  // src/analysis-term-pages.ts
5190
5209
  var MAX_INLINE_TERM_COUNT_BYTES = 8 * 1024;
5191
5210
  function termCountRequest(id, offset, redact = false) {
@@ -5203,7 +5222,7 @@ function analysisTermPage(result, id, offset) {
5203
5222
  const term = all[index];
5204
5223
  if (!term)
5205
5224
  throw new Error("Term inventory index unavailable");
5206
- const label = `condition #${String(index + 1)}`;
5225
+ const label = termCountLabel(term.term, index, result.redact);
5207
5226
  const row = `${label}: ${String(term.retainedOccurrences)} retained occurrences`;
5208
5227
  const size = Buffer.byteLength(row) + 2;
5209
5228
  if (bytes + size > MAX_INLINE_TERM_COUNT_BYTES)
@@ -5339,8 +5358,8 @@ function analysisExtraGroups(counts, termCounts, items) {
5339
5358
  const classification = counts ? statisticsGroup("classification", Object.entries(counts).map(([label, count]) => ({ label, count }))) : undefined;
5340
5359
  if (classification)
5341
5360
  groups.push(classification);
5342
- const terms = termCounts ? statisticsGroup("conditions", termCounts.map((term, index) => ({
5343
- label: `condition #${String(index + 1)}`,
5361
+ const terms = termCounts ? statisticsGroup("conditions", termCounts.map((term) => ({
5362
+ label: term.term,
5344
5363
  count: term.retainedOccurrences
5345
5364
  }))) : undefined;
5346
5365
  if (terms)
@@ -5487,15 +5506,15 @@ function publicAnalysisItem(result, item, index, storedId, modelOutput) {
5487
5506
  return {
5488
5507
  path: item.path,
5489
5508
  line: item.line,
5490
- label: result.kind === "outline" && modelOutput ? item.label : publicAnalysisLabel(result),
5509
+ label: result.kind === "outline" && modelOutput || result.kind === "files" ? item.label : publicAnalysisLabel(result),
5491
5510
  index: index + 1,
5492
5511
  ...inspect ? { inspect } : {},
5493
5512
  ...publicDetails ? { details: publicDetails } : {}
5494
5513
  };
5495
5514
  }
5496
- function safeTermCounts(termCounts) {
5515
+ function safeTermCounts(termCounts, redact = false) {
5497
5516
  return termCounts?.map((entry, index) => ({
5498
- term: `condition #${String(index + 1)}`,
5517
+ term: termCountLabel(entry.term, index, redact),
5499
5518
  retainedOccurrences: entry.retainedOccurrences
5500
5519
  }));
5501
5520
  }
@@ -5617,21 +5636,22 @@ class AnalysisStore {
5617
5636
  throw new CursorError("Analysis item is outside the retained result");
5618
5637
  return structuredClone(item);
5619
5638
  }
5620
- page(cursor, modelOutput = false) {
5639
+ page(cursor, modelOutput = false, redact = false) {
5621
5640
  const { stored, offset, kind } = this.resolve(cursor);
5622
- const { result } = stored;
5641
+ const result = redact ? { ...stored.result, redact: true } : stored.result;
5623
5642
  if (kind === "analysis-terms")
5624
5643
  return analysisTermPage(result, stored.id, offset);
5625
5644
  const hybridPreview = kind === "analysis-hybrid";
5626
5645
  const hybridMatchesRequest = hybridPreview ? { cursor: `${stored.id}.analysis.0`, ...result.redact ? { redact: true } : {} } : undefined;
5627
- const pagedTerms = result.termCounts && Buffer.byteLength(JSON.stringify(result.termCounts)) > MAX_INLINE_TERM_COUNT_BYTES;
5628
- const inlineTerms = pagedTerms ? undefined : safeTermCounts(result.termCounts);
5646
+ const publicTerms = safeTermCounts(result.termCounts, result.redact);
5647
+ const pagedTerms = publicTerms && Buffer.byteLength(JSON.stringify(publicTerms)) > MAX_INLINE_TERM_COUNT_BYTES;
5648
+ const inlineTerms = pagedTerms ? undefined : publicTerms;
5629
5649
  const termsRequest = pagedTerms ? termCountRequest(stored.id, 0, result.redact) : undefined;
5630
5650
  const items = [];
5631
5651
  const sources = [];
5632
5652
  const sourceIds = new Map;
5633
5653
  const hybridInspectCursor = result.kind === "hybrid" ? `${stored.id}.analysis.0` : undefined;
5634
- const statistics = statisticsForItems(result.items, result.items.length, result.unit, analysisExtraGroups(result.counts, result.termCounts, result.items));
5654
+ const statistics = statisticsForItems(result.items, result.items.length, result.unit, analysisExtraGroups(result.counts, inlineTerms, result.items));
5635
5655
  const scope = result.scope ? ` Scope: ${result.scope.assertion === "project-wide" ? "project root" : "requested path"} ${JSON.stringify(result.scope.path)}${result.scope.expandedToProjectRoot ? `, expanded after ${JSON.stringify(result.scope.requestedPath)} had no matches` : ""}.${modificationTimeBoundsText(result.scope.modifiedAfterMs, result.scope.modifiedBeforeMs)}` : "";
5636
5656
  const coverage = result.coverage ? ` Coverage: ${JSON.stringify(result.coverage)}.` : "";
5637
5657
  const stats = result.stats ? ` Stats: ${JSON.stringify(result.stats)}.` : "";
@@ -5698,7 +5718,8 @@ Inspect: ${JSON.stringify(inspect)}` : ""}`;
5698
5718
  break;
5699
5719
  }
5700
5720
  } else {
5701
- for (let index = offset;index < result.items.length && items.length < 30; index += 1) {
5721
+ const pageSize = result.pageSize ?? 30;
5722
+ for (let index = offset;index < result.items.length && items.length < pageSize; index += 1) {
5702
5723
  if (!appendItem(index))
5703
5724
  break;
5704
5725
  }
@@ -5812,6 +5833,8 @@ function resolveInspectionTarget(input, cwd, snapshots) {
5812
5833
  }
5813
5834
  if (!path)
5814
5835
  throw new SiftLightError("path is required when mode=inspect");
5836
+ if (line === undefined && input.cursor === undefined)
5837
+ line = 1;
5815
5838
  if (line === undefined || !Number.isSafeInteger(line) || line < 1) {
5816
5839
  throw new SiftLightError("line must be a positive integer when mode=inspect");
5817
5840
  }
@@ -7540,6 +7563,28 @@ async function filterPathsByModificationTime(cwd, paths, modifiedAfterMs, modifi
7540
7563
  // src/file-discovery.ts
7541
7564
  var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
7542
7565
  var FILE_QUERY_GLOB_WILDCARD = /[*?]/u;
7566
+ var FILE_QUERY_MATCH_ALL = /^(?:[*/]+|\.\*|\.\+|\*\.\*)$/u;
7567
+ var FILE_QUERY_REGEX_SYNTAX = /[()|^$\\+]/u;
7568
+ var IGNORED_MATCH_SAMPLES = 5;
7569
+ function normalizeFileQuery(query, hasGlob) {
7570
+ const trimmed = query.trim();
7571
+ if (FILE_QUERY_MATCH_ALL.test(trimmed))
7572
+ return {
7573
+ query: "",
7574
+ note: `Query ${JSON.stringify(query)} was treated as listing every file under path; omit query for the same result.`
7575
+ };
7576
+ if (!FILE_QUERY_GLOB_WILDCARD.test(trimmed))
7577
+ return { query };
7578
+ if (!hasGlob && !FILE_QUERY_REGEX_SYNTAX.test(trimmed) && !/\s/u.test(trimmed)) {
7579
+ const glob = trimmed.includes("/") ? trimmed : `**/${trimmed}`;
7580
+ return {
7581
+ query: "",
7582
+ glob,
7583
+ note: `Query ${JSON.stringify(query)} contains glob wildcards and was applied as glob ${JSON.stringify(glob)}; mode=files query otherwise matches filename text.`
7584
+ };
7585
+ }
7586
+ 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.`);
7587
+ }
7543
7588
  function subsequenceScore(text, query) {
7544
7589
  const characters = Array.from(graphemes.segment(text), (item) => item.segment);
7545
7590
  const queryCharacters = Array.from(graphemes.segment(query), (item) => item.segment);
@@ -7590,39 +7635,79 @@ function pathRelativeToDiscoveryRoot(cwd, root, path) {
7590
7635
  return scoped || platformBasename(absolutePath);
7591
7636
  }
7592
7637
  async function discoverFiles(input, cwd, signal) {
7593
- const query = input.query ?? "";
7594
- if (query.length > 256 || !query.isWellFormed() || /[\r\n\0]/.test(query))
7638
+ const rawQuery = input.query ?? "";
7639
+ if (rawQuery.length > 256 || !rawQuery.isWellFormed() || /[\r\n\0]/.test(rawQuery))
7595
7640
  throw new SiftLightError("File query must be well-formed single-line text of at most 256 characters");
7596
- if (FILE_QUERY_GLOB_WILDCARD.test(query))
7597
- 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.`);
7598
- const request = normalizeRequest({ ...input, pattern: "" });
7641
+ const requestedGlobs = input.glob === undefined ? [] : Array.isArray(input.glob) ? input.glob : [input.glob];
7642
+ const normalizedQuery = normalizeFileQuery(rawQuery, requestedGlobs.length > 0);
7643
+ const query = normalizedQuery.query;
7644
+ const request = normalizeRequest({
7645
+ ...input,
7646
+ pattern: "",
7647
+ ...normalizedQuery.glob ? { glob: [...requestedGlobs, normalizedQuery.glob] } : {}
7648
+ });
7599
7649
  const policy = new SearchPathPolicy(cwd);
7600
7650
  const discoveryRoot = request.path ?? ".";
7601
7651
  const scoringRoot = await policy.resolveSearchTarget(discoveryRoot);
7602
- const files = await listWorkspaceFiles(cwd, signal, {
7652
+ const includeIgnored = request.ignorePolicy === "include";
7653
+ const listOptions = {
7603
7654
  path: scoringRoot,
7604
7655
  glob: request.glob,
7605
7656
  exclude: request.exclude,
7606
7657
  hidden: request.hidden
7658
+ };
7659
+ const files = await listWorkspaceFiles(cwd, signal, {
7660
+ ...listOptions,
7661
+ ...includeIgnored ? { ignore: false, ignoreParents: false } : {}
7607
7662
  });
7608
7663
  const filtered = await filterPathsByModificationTime(cwd, files.paths, request.modifiedAfterMs, request.modifiedBeforeMs, signal);
7664
+ const rank = (path) => scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
7609
7665
  const selected = filtered.paths.flatMap((path) => {
7610
- const rank = scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
7611
- return rank ? [{ path, ...rank }] : [];
7666
+ const score = rank(path);
7667
+ return score ? [{ path, ...score }] : [];
7612
7668
  }).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
7613
7669
  for (let offset = 0;offset < selected.length; offset += 16) {
7614
7670
  await Promise.all(selected.slice(offset, offset + 16).map((item) => policy.assertExistingPath(item.path)));
7615
7671
  signal?.throwIfAborted();
7616
7672
  }
7673
+ const reasons = new Set([...files.reasons, ...filtered.reasons]);
7674
+ if (normalizedQuery.note)
7675
+ reasons.add(normalizedQuery.note);
7676
+ let ignoredFiles = 0;
7677
+ let ignoredMatches = 0;
7678
+ let ignoredComparisonPartial = false;
7679
+ if (!includeIgnored) {
7680
+ const everything = await listWorkspaceFiles(cwd, signal, {
7681
+ ...listOptions,
7682
+ ignore: false,
7683
+ ignoreParents: false
7684
+ });
7685
+ ignoredComparisonPartial = everything.partial;
7686
+ const admitted = new Set(files.paths);
7687
+ const ignored = everything.paths.filter((path) => !admitted.has(path));
7688
+ ignoredFiles = ignored.length;
7689
+ const ignoredCandidates = ignored.flatMap((path) => {
7690
+ const score = rank(path);
7691
+ return score ? [{ path, ...score }] : [];
7692
+ }).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
7693
+ ignoredMatches = ignoredCandidates.length;
7694
+ if (ignoredFiles > 0) {
7695
+ const samples = ignoredCandidates.slice(0, IGNORED_MATCH_SAMPLES).map((item) => `${JSON.stringify(item.path)} (${item.reason})`);
7696
+ 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.`);
7697
+ }
7698
+ if (ignoredComparisonPartial)
7699
+ reasons.add("Ignored-file comparison was incomplete; the ignored-file count is a lower bound");
7700
+ }
7701
+ const enumerationPartial = files.partial || filtered.partial;
7617
7702
  return {
7618
7703
  kind: "files",
7619
7704
  unit: "files",
7620
- partial: files.partial || filtered.partial,
7621
- reasons: [...new Set([...files.reasons, ...filtered.reasons])],
7705
+ partial: enumerationPartial,
7706
+ reasons: [...reasons],
7622
7707
  items: selected.map((item) => ({
7623
7708
  path: item.path,
7624
7709
  line: 1,
7625
- label: `File candidate (${item.reason})`,
7710
+ label: `File candidate (score ${String(item.score)}: ${item.reason})`,
7626
7711
  details: {
7627
7712
  kind: "file",
7628
7713
  score: item.score,
@@ -7630,8 +7715,13 @@ async function discoverFiles(input, cwd, signal) {
7630
7715
  inspect: { mode: "inspect", path: item.path, line: 1 }
7631
7716
  }
7632
7717
  })),
7633
- coverage: { fileEnumeration: files.partial || filtered.partial ? "partial" : "complete" },
7634
- stats: { filesEnumerated: files.paths.length },
7718
+ coverage: {
7719
+ fileEnumeration: enumerationPartial ? "partial" : ignoredFiles > 0 || ignoredComparisonPartial ? "policy-filtered" : "complete"
7720
+ },
7721
+ stats: {
7722
+ filesEnumerated: files.paths.length,
7723
+ ...includeIgnored ? {} : { ignoredFiles, ignoredMatches }
7724
+ },
7635
7725
  scope: {
7636
7726
  path: request.path ?? ".",
7637
7727
  requestedPath: request.path ?? ".",
@@ -7644,7 +7734,8 @@ async function discoverFiles(input, cwd, signal) {
7644
7734
  ...request.modifiedAfterMs !== undefined ? { modifiedAfterMs: request.modifiedAfterMs } : {},
7645
7735
  ...request.modifiedBeforeMs !== undefined ? { modifiedBeforeMs: request.modifiedBeforeMs } : {}
7646
7736
  },
7647
- redact: input.redact ?? false
7737
+ redact: input.redact ?? false,
7738
+ ...input.limit !== undefined ? { pageSize: request.pageSize } : {}
7648
7739
  };
7649
7740
  }
7650
7741
 
@@ -7885,7 +7976,19 @@ async function prepare(target, access, structure) {
7885
7976
  let range = target.range;
7886
7977
  let details = { status: "no-symbol" };
7887
7978
  const language = syntaxLanguage(document2.path);
7888
- if (document2.utf8 && language && language !== "go") {
7979
+ if (target.range && target.structure) {
7980
+ document2.checkRange(target.range);
7981
+ const lines = {
7982
+ startLine: document2.lineAt(target.range.start),
7983
+ endLine: document2.lineAt(Math.max(target.range.start, target.range.end - 1))
7984
+ };
7985
+ range = document2.lineRange(lines.startLine, lines.endLine);
7986
+ details = {
7987
+ ...target.structure,
7988
+ range: lines,
7989
+ ...target.structure.symbol ? { symbol: { ...target.structure.symbol, range: lines } } : {}
7990
+ };
7991
+ } else if (document2.utf8 && language && language !== "go") {
7889
7992
  const syntax = await access.syntax(document2);
7890
7993
  details = {
7891
7994
  status: syntax.status === "ok" ? "no-symbol" : syntax.status === "unsupported" ? "provider-unavailable" : "parse-error",
@@ -7937,7 +8040,7 @@ async function prepare(target, access, structure) {
7937
8040
  details = { status: "provider-unavailable", ...language ? { language } : {} };
7938
8041
  }
7939
8042
  range ??= document2.lineRange(Math.max(1, target.line - 10), Math.min(document2.lineStarts.length, target.line + 10));
7940
- const boundary = target.range ? "requested-range" : details.status === "available" && details.range ? "syntax" : "line-window";
8043
+ const boundary = target.range ? target.structure ? "syntax" : "requested-range" : details.status === "available" && details.range ? "syntax" : "line-window";
7941
8044
  document2.checkRange(range);
7942
8045
  return { target, document: document2, range, structure: details, boundary, focus };
7943
8046
  }
@@ -8296,8 +8399,10 @@ ${preview.text}`);
8296
8399
  }
8297
8400
  };
8298
8401
  }
8299
- async function continueSource(cursor, access, continuations) {
8402
+ async function continueSource(cursor, access, continuations, expectedPath) {
8300
8403
  const state = continuations.resolve(cursor);
8404
+ if (expectedPath !== undefined && resolve18(access.cwd, expectedPath.replace(/^@/, "")) !== resolve18(access.cwd, state.source.path))
8405
+ throw new SiftLightError(`sourceCursor continues ${JSON.stringify(state.source.path)}, not ${JSON.stringify(expectedPath)}; copy the returned nextRequest exactly`);
8301
8406
  const document2 = await access.load(state.source.path, state.source);
8302
8407
  const page = sourcePage(document2, state.remaining, MAX_RESULT_BYTES - 1400);
8303
8408
  const next = continuations.advance(cursor, page.fragment);
@@ -9174,6 +9279,221 @@ function parsePythonOutline(document2) {
9174
9279
  });
9175
9280
  }
9176
9281
 
9282
+ // src/python-roles.ts
9283
+ var KEYWORDS = new Set([
9284
+ "and",
9285
+ "as",
9286
+ "assert",
9287
+ "async",
9288
+ "await",
9289
+ "case",
9290
+ "del",
9291
+ "elif",
9292
+ "else",
9293
+ "except",
9294
+ "for",
9295
+ "from",
9296
+ "global",
9297
+ "if",
9298
+ "import",
9299
+ "in",
9300
+ "is",
9301
+ "lambda",
9302
+ "match",
9303
+ "nonlocal",
9304
+ "not",
9305
+ "or",
9306
+ "raise",
9307
+ "return",
9308
+ "while",
9309
+ "with",
9310
+ "yield"
9311
+ ]);
9312
+ var STRING_PREFIX = /^(?:[rRbBuUfF]|[rR][bBfF]|[bBfF][rR])?$/u;
9313
+ var IDENTIFIER_START = /[\p{ID_Start}_]/u;
9314
+ var IDENTIFIER_PART = /[\p{ID_Continue}]/u;
9315
+ function lex(text) {
9316
+ const comments = [];
9317
+ const strings = [];
9318
+ const code = new Uint8Array(text.length).fill(1);
9319
+ const mark = (start2, end) => {
9320
+ code.fill(0, start2, end);
9321
+ };
9322
+ let index = 0;
9323
+ while (index < text.length) {
9324
+ const character = text[index];
9325
+ if (character === "#") {
9326
+ const newline = text.indexOf(`
9327
+ `, index);
9328
+ const end = newline < 0 ? text.length : newline;
9329
+ comments.push({ start: index, end });
9330
+ mark(index, end);
9331
+ index = end;
9332
+ continue;
9333
+ }
9334
+ if (character !== "'" && character !== '"') {
9335
+ index += 1;
9336
+ continue;
9337
+ }
9338
+ let prefixStart = index;
9339
+ while (prefixStart > 0 && /[rRbBuUfF]/u.test(text[prefixStart - 1] ?? ""))
9340
+ prefixStart -= 1;
9341
+ const prefix = text.slice(prefixStart, index);
9342
+ const beforePrefix = text[prefixStart - 1] ?? "";
9343
+ const start2 = STRING_PREFIX.test(prefix) && !IDENTIFIER_PART.test(beforePrefix) ? prefixStart : index;
9344
+ const triple = text.slice(index, index + 3) === character.repeat(3);
9345
+ const delimiter = triple ? character.repeat(3) : character;
9346
+ let cursor = index + delimiter.length;
9347
+ let end = text.length;
9348
+ while (cursor < text.length) {
9349
+ const current = text[cursor];
9350
+ if (current === "\\") {
9351
+ cursor += 2;
9352
+ continue;
9353
+ }
9354
+ if (!triple && current === `
9355
+ `) {
9356
+ end = cursor;
9357
+ break;
9358
+ }
9359
+ if (text.startsWith(delimiter, cursor)) {
9360
+ end = cursor + delimiter.length;
9361
+ break;
9362
+ }
9363
+ cursor += 1;
9364
+ }
9365
+ strings.push({ start: start2, end });
9366
+ mark(start2, end);
9367
+ index = end;
9368
+ }
9369
+ return { comments, strings, code };
9370
+ }
9371
+ function codeRanges(code) {
9372
+ const ranges = [];
9373
+ let start2 = -1;
9374
+ for (let index = 0;index <= code.length; index += 1) {
9375
+ const inCode = index < code.length && code[index] === 1;
9376
+ if (inCode && start2 < 0)
9377
+ start2 = index;
9378
+ else if (!inCode && start2 >= 0) {
9379
+ ranges.push({ start: start2, end: index });
9380
+ start2 = -1;
9381
+ }
9382
+ }
9383
+ return ranges;
9384
+ }
9385
+ function identifierEnd(text, start2) {
9386
+ let end = start2;
9387
+ while (end < text.length && IDENTIFIER_PART.test(text[end] ?? ""))
9388
+ end += 1;
9389
+ return end;
9390
+ }
9391
+ function skipSpaces(text, index, code) {
9392
+ let cursor = index;
9393
+ while (cursor < text.length && code[cursor] === 1 && /[ \t]/u.test(text[cursor] ?? ""))
9394
+ cursor += 1;
9395
+ return cursor;
9396
+ }
9397
+ function statementEnd(text, start2, code) {
9398
+ let depth = 0;
9399
+ for (let index = start2;index < text.length; index += 1) {
9400
+ if (code[index] !== 1)
9401
+ continue;
9402
+ const character = text[index];
9403
+ if (character === "(" || character === "[" || character === "{")
9404
+ depth += 1;
9405
+ else if (character === ")" || character === "]" || character === "}")
9406
+ depth = Math.max(0, depth - 1);
9407
+ else if (character === "\\" && text[index + 1] === `
9408
+ `)
9409
+ index += 1;
9410
+ else if (character === `
9411
+ ` && depth === 0)
9412
+ return index;
9413
+ }
9414
+ return text.length;
9415
+ }
9416
+ function pythonRoleAnalysis(document2) {
9417
+ const text = document2.text;
9418
+ const { comments, strings, code } = lex(text);
9419
+ const roles = [];
9420
+ const push = (start2, end, role, certainty, subkind) => {
9421
+ if (start2 < end)
9422
+ roles.push({ start: start2, end, role, certainty, node: 0, ...subkind ? { subkind } : {} });
9423
+ };
9424
+ for (const span of comments)
9425
+ push(span.start, span.end, "comment", "syntax");
9426
+ for (const span of strings)
9427
+ push(span.start, span.end, "string", "syntax");
9428
+ for (const span of codeRanges(code))
9429
+ push(span.start, span.end, "code", "syntax");
9430
+ let index = 0;
9431
+ let lineStart = true;
9432
+ while (index < text.length) {
9433
+ const character = text[index] ?? "";
9434
+ if (character === `
9435
+ `) {
9436
+ lineStart = true;
9437
+ index += 1;
9438
+ continue;
9439
+ }
9440
+ if (code[index] !== 1 || !IDENTIFIER_START.test(character)) {
9441
+ if (!/[ \t]/u.test(character))
9442
+ lineStart = false;
9443
+ index += 1;
9444
+ continue;
9445
+ }
9446
+ const previous = text[index - 1] ?? "";
9447
+ if (IDENTIFIER_PART.test(previous)) {
9448
+ index += 1;
9449
+ continue;
9450
+ }
9451
+ const end = identifierEnd(text, index);
9452
+ const word = text.slice(index, end);
9453
+ const atStatementStart = lineStart;
9454
+ lineStart = false;
9455
+ if (atStatementStart && (word === "import" || word === "from")) {
9456
+ const statement = statementEnd(text, index, code);
9457
+ const body2 = text.slice(index, statement);
9458
+ if (word === "import" || /\bimport\b/u.test(body2)) {
9459
+ push(index, statement, "import", "syntax", word === "from" ? "from-import" : "import");
9460
+ index = statement;
9461
+ continue;
9462
+ }
9463
+ }
9464
+ if (word === "def" || word === "class") {
9465
+ const nameStart = skipSpaces(text, end, code);
9466
+ if (IDENTIFIER_START.test(text[nameStart] ?? "")) {
9467
+ const nameEnd = identifierEnd(text, nameStart);
9468
+ push(nameStart, nameEnd, "declaration", "syntax", word === "def" ? "function" : "class");
9469
+ index = nameEnd;
9470
+ continue;
9471
+ }
9472
+ }
9473
+ let chainEnd = end;
9474
+ while (text[chainEnd] === "." && IDENTIFIER_START.test(text[chainEnd + 1] ?? "")) {
9475
+ chainEnd = identifierEnd(text, chainEnd + 1);
9476
+ }
9477
+ const open = skipSpaces(text, chainEnd, code);
9478
+ const lastSegment = text.slice(text.lastIndexOf(".", chainEnd - 1) + 1, chainEnd);
9479
+ const before = text.slice(Math.max(0, index - 4), index);
9480
+ if (text[open] === "(" && code[open] === 1 && !KEYWORDS.has(word) && !KEYWORDS.has(lastSegment) && !/\bdef\s+$|\bclass\s+$/u.test(before)) {
9481
+ push(index, open + 1, "call", "candidate", "lexical-call");
9482
+ }
9483
+ index = chainEnd;
9484
+ }
9485
+ roles.sort((a, b) => a.start - b.start || a.end - b.end || a.role.localeCompare(b.role));
9486
+ return {
9487
+ status: "ok",
9488
+ nodes: [],
9489
+ children: [],
9490
+ symbols: [],
9491
+ roles,
9492
+ diagnostics: [],
9493
+ limited: false
9494
+ };
9495
+ }
9496
+
9177
9497
  // src/hybrid-search.ts
9178
9498
  import { resolve as resolve20 } from "path";
9179
9499
 
@@ -10080,7 +10400,17 @@ var MODE_FIELDS_BY_MODE = {
10080
10400
  auto: ordinaryFields,
10081
10401
  summary: ordinaryFields,
10082
10402
  matches: ordinaryFields,
10083
- inspect: [...commonFields, "path", "line", "cursor", "matchIndex", "matchIndices", "targets"],
10403
+ inspect: [
10404
+ ...commonFields,
10405
+ "path",
10406
+ "paths",
10407
+ "line",
10408
+ "cursor",
10409
+ "matchIndex",
10410
+ "matchIndices",
10411
+ "targets",
10412
+ "scope"
10413
+ ],
10084
10414
  outline: [...commonFields, "path", "line", "symbol", "cursor", "matchIndex", "maxFilesToParse"],
10085
10415
  imports: [
10086
10416
  ...commonFields,
@@ -10100,7 +10430,16 @@ var MODE_FIELDS_BY_MODE = {
10100
10430
  "matchIndex",
10101
10431
  "maxFilesToParse"
10102
10432
  ],
10103
- files: [...commonFields, "query", ...sourceFilters, "modifiedAfter", "modifiedBefore"],
10433
+ files: [
10434
+ ...commonFields,
10435
+ "query",
10436
+ ...sourceFilters,
10437
+ "ignorePolicy",
10438
+ "limit",
10439
+ "scope",
10440
+ "modifiedAfter",
10441
+ "modifiedBefore"
10442
+ ],
10104
10443
  structure: [...commonFields, "pattern", ...sourceFilters, "maxFilesToParse"],
10105
10444
  concept: [...commonFields, "query", ...sourceFilters, "maxFilesToParse"],
10106
10445
  hybrid: [...commonFields, "query", ...sourceFilters, "conceptLimit", "maxFilesToParse"],
@@ -10109,9 +10448,7 @@ var MODE_FIELDS_BY_MODE = {
10109
10448
  await: ["mode", "operationId"],
10110
10449
  cancel: ["mode", "operationId"]
10111
10450
  };
10112
- var SAFE_DROP_FIELDS = {
10113
- files: ["scope"]
10114
- };
10451
+ var SAFE_DROP_FIELDS = {};
10115
10452
  var SUPPORTED_OUTLINE_EXTENSIONS = new Set(DEFAULT_LANGUAGE_CAPABILITIES.flatMap((descriptor) => descriptor.capabilities.some((capability) => capability.name === "outline") ? descriptor.extensions : []));
10116
10453
  function modeFields(mode) {
10117
10454
  return MODE_FIELDS_BY_MODE[mode];
@@ -10140,8 +10477,8 @@ var REQUEST_FIELD_GUIDANCE = {
10140
10477
  allOf: "allOf is case-sensitive exact-literal AND; omit pattern, anyOf, literal, ignoreCase, roles",
10141
10478
  context: "context is an output-context budget and is never silently dropped",
10142
10479
  limit: "limit is an output/page budget and is never silently dropped",
10143
- 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",
10144
- ignorePolicy: "respect keeps repository ignore rules; include searches ignored files but always excludes .git internals and protected paths",
10480
+ scope: "scope applies to ordinary content search; mode=files is always strict, so it accepts only the redundant scope=strict",
10481
+ 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",
10145
10482
  patterns: "audit accepts named exact-literal patterns and returns one closure receipt",
10146
10483
  maxFilesToParse: "concept/hybrid automatically batch the requested scope; this optional field sets an advanced hard file ceiling"
10147
10484
  };
@@ -10240,12 +10577,15 @@ function schemaError(input, field, reason) {
10240
10577
  function plainFilePatternRecovery(mode, input, invalid) {
10241
10578
  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);
10242
10579
  }
10580
+ function matchAllFilePatternRecovery(mode, input, invalid) {
10581
+ return mode === "files" && invalid.includes("pattern") && input.query === undefined && typeof input.pattern === "string" && /^(?:\.\*|\.\+|\*+)$/u.test(input.pattern.trim());
10582
+ }
10243
10583
  function safeNextRequest(input, mode, invalid) {
10244
10584
  if (input.redact === true && containsSensitiveText(input))
10245
10585
  return;
10246
10586
  const safe = new Set(SAFE_DROP_FIELDS[mode] ?? []);
10247
10587
  const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
10248
- if (plainFilePattern)
10588
+ if (plainFilePattern || matchAllFilePatternRecovery(mode, input, invalid))
10249
10589
  safe.add("pattern");
10250
10590
  if (invalid.some((field) => !safe.has(field)))
10251
10591
  return;
@@ -10291,7 +10631,7 @@ function fieldsError(input, mode, invalid, selectorIssues = []) {
10291
10631
  issues,
10292
10632
  recovery: nextRequest ? {
10293
10633
  action: "retry",
10294
- reason: plainFilePatternRecovery(mode, input, invalid) ? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved." : `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
10634
+ 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.`,
10295
10635
  nextRequest
10296
10636
  } : {
10297
10637
  action: "manual",
@@ -10447,7 +10787,7 @@ function validateRequestContract(input) {
10447
10787
  const allowed = new Set(MODE_FIELDS_BY_MODE[mode]);
10448
10788
  if (mode === "inspect" && raw.sourceCursor !== undefined) {
10449
10789
  allowed.clear();
10450
- for (const field of ["mode", "sourceCursor", "redact"])
10790
+ for (const field of ["mode", "sourceCursor", "redact", "path"])
10451
10791
  allowed.add(field);
10452
10792
  }
10453
10793
  if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof raw.cursor === "string" && raw.cursor.includes(".analysis")) {
@@ -10475,6 +10815,69 @@ import { realpath as realpath6 } from "fs/promises";
10475
10815
  function isEvidenceRequest(input) {
10476
10816
  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);
10477
10817
  }
10818
+ function pageInspectRequest(input) {
10819
+ if (input.scope === "expand")
10820
+ throw new SiftLightError("mode=inspect reads exact locations; scope=expand does not apply (omit scope)");
10821
+ const { scope: _scope, paths, ...rest } = input;
10822
+ let request = rest;
10823
+ if (paths !== undefined) {
10824
+ if (input.cursor !== undefined || input.targets !== undefined || input.matchIndices !== undefined || input.path !== undefined || input.line !== undefined || input.matchIndex !== undefined)
10825
+ throw new SiftLightError("mode=inspect paths opens files from line 1 and cannot be combined with cursor, targets, matchIndices, path, line or matchIndex");
10826
+ request = { ...rest, targets: paths.map((path) => ({ path, line: 1 })) };
10827
+ }
10828
+ const redact = input.redact ? { redact: true } : {};
10829
+ const requested = request.targets?.length ?? request.matchIndices?.length ?? 0;
10830
+ if (requested > MAX_INSPECT_REQUEST_TARGETS)
10831
+ throw new SiftLightError(`mode=inspect accepts at most ${String(MAX_INSPECT_REQUEST_TARGETS)} targets per request; ${String(MAX_INSPECT_TARGETS)} are inspected per response`);
10832
+ if (request.targets && request.targets.length > MAX_INSPECT_TARGETS)
10833
+ return {
10834
+ current: { ...request, targets: request.targets.slice(0, MAX_INSPECT_TARGETS) },
10835
+ remaining: {
10836
+ mode: "inspect",
10837
+ targets: request.targets.slice(MAX_INSPECT_TARGETS),
10838
+ ...redact
10839
+ }
10840
+ };
10841
+ if (request.matchIndices && request.matchIndices.length > MAX_INSPECT_TARGETS)
10842
+ return {
10843
+ current: { ...request, matchIndices: request.matchIndices.slice(0, MAX_INSPECT_TARGETS) },
10844
+ remaining: {
10845
+ mode: "inspect",
10846
+ ...request.cursor !== undefined ? { cursor: request.cursor } : {},
10847
+ matchIndices: request.matchIndices.slice(MAX_INSPECT_TARGETS),
10848
+ ...redact
10849
+ }
10850
+ };
10851
+ return { current: request };
10852
+ }
10853
+ function withRemainingInspection(result, remaining) {
10854
+ const count = remaining.targets?.length ?? remaining.matchIndices?.length ?? 0;
10855
+ return {
10856
+ text: `${result.text}
10857
+
10858
+ [${String(count)} more target(s) were not inspected; one response inspects ${String(MAX_INSPECT_TARGETS)}.]
10859
+ Next request: ${JSON.stringify(remaining)}`,
10860
+ details: {
10861
+ ...result.details,
10862
+ status: "partial",
10863
+ snapshotComplete: false,
10864
+ nextRequest: remaining
10865
+ }
10866
+ };
10867
+ }
10868
+ function analysisItemStructure(item) {
10869
+ const details = item.details ?? {};
10870
+ const language = typeof details.language === "string" ? details.language : undefined;
10871
+ const name2 = typeof details.name === "string" ? details.name : undefined;
10872
+ const scope = Array.isArray(details.scope) ? details.scope.filter((entry) => typeof entry === "string") : typeof details.scope === "string" ? [details.scope] : [];
10873
+ const kind = details.kind === "function" ? "function" : item.label.split(" ", 1)[0] ?? "symbol";
10874
+ return {
10875
+ status: "available",
10876
+ provider: language === "python" ? "python-outline" : "tree-sitter",
10877
+ ...language ? { language } : {},
10878
+ ...name2 ? { symbol: { name: name2, kind, scope, range: { startLine: item.line, endLine: item.line } } } : {}
10879
+ };
10880
+ }
10478
10881
  function maxFilesToParse(value, defaultValue = MAX_STRUCTURE_FILES) {
10479
10882
  const candidate = value ?? defaultValue;
10480
10883
  if (!Number.isSafeInteger(candidate) || candidate < 1 || candidate > MAX_CONFIGURABLE_STRUCTURE_FILES) {
@@ -10490,8 +10893,8 @@ function validateTerms(input) {
10490
10893
  }
10491
10894
  return;
10492
10895
  }
10493
- 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)
10494
- throw new SiftLightError("allOf requires 2\u20133 distinct, nonempty, single-line literal terms");
10896
+ 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)
10897
+ throw new SiftLightError("allOf requires 1\u20133 distinct, nonempty, single-line literal terms");
10495
10898
  if (input.pattern !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
10496
10899
  throw new SiftLightError("allOf is an explicit case-sensitive literal conjunction; omit pattern, roles, literal and ignoreCase");
10497
10900
  if (input.within !== undefined && input.within !== "file" && input.within !== "function")
@@ -10689,11 +11092,13 @@ class EvidenceService {
10689
11092
  throw new CursorError("A nonempty sourceCursor is required");
10690
11093
  if (input.mode !== "inspect")
10691
11094
  throw new SiftLightError("sourceCursor requires mode=inspect");
10692
- return continueSource(input.sourceCursor, access, this.#continuations);
11095
+ return continueSource(input.sourceCursor, access, this.#continuations, input.path);
10693
11096
  }
10694
11097
  if (input.mode === "inspect") {
10695
- const targets = this.#inspectionTargets(input, cwd);
10696
- return targets.some((target) => target.range !== undefined) ? inspectDocumentsMetadata(targets, access, this.#structure) : inspectDocuments(targets, access, this.#continuations, this.#structure);
11098
+ const { current, remaining } = pageInspectRequest(input);
11099
+ const targets = this.#inspectionTargets(current, cwd);
11100
+ const result = await inspectDocuments(targets, access, this.#continuations, this.#structure);
11101
+ return remaining ? withRemainingInspection(result, remaining) : result;
10697
11102
  }
10698
11103
  if (input.mode === "concept") {
10699
11104
  const execution = await this.#conceptSearch(input, access, options.onProgress);
@@ -10773,7 +11178,7 @@ class EvidenceService {
10773
11178
  this.#analyses.resolve(input.cursor);
10774
11179
  if (input.mode !== undefined && input.mode !== "matches" && input.mode !== "auto")
10775
11180
  throw new CursorError("Analysis cursor cannot continue in the requested mode", "E_CURSOR_WRONG_KIND");
10776
- return this.#analyses.page(input.cursor, options.modelOutput);
11181
+ return this.#analyses.page(input.cursor, options.modelOutput, input.redact);
10777
11182
  }
10778
11183
  if (input.mode === "outline" || input.mode === "imports" || input.mode === "tests")
10779
11184
  return this.#navigate(input, access, options);
@@ -10782,7 +11187,7 @@ class EvidenceService {
10782
11187
  if (input.pattern !== undefined || input.allOf !== undefined || input.within !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
10783
11188
  throw new SiftLightError("anyOf is an explicit case-sensitive literal union; omit pattern, allOf, within, roles, literal and ignoreCase");
10784
11189
  if (input.mode !== undefined && input.mode !== "auto" && input.mode !== "matches")
10785
- throw new SiftLightError("anyOf mode must be omitted, auto, or matches");
11190
+ throw new SiftLightError("anyOf mode must be omitted, auto, matches or summary");
10786
11191
  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));
10787
11192
  const { path: _inputPath, ...unscopedInput } = input;
10788
11193
  let chunkAccess = access;
@@ -10920,9 +11325,10 @@ class EvidenceService {
10920
11325
  if (item)
10921
11326
  result.items.push(item);
10922
11327
  } else if (terms || input.roles) {
10923
- if (syntaxLanguage(file.document.path))
11328
+ const pythonRoles = !terms && /\.py$/iu.test(file.document.path);
11329
+ if (pythonRoles || syntaxLanguage(file.document.path))
10924
11330
  syntaxCapableFiles += 1;
10925
- const syntax = await access.syntax(file.document);
11331
+ const syntax = pythonRoles ? pythonRoleAnalysis(file.document) : await access.syntax(file.document);
10926
11332
  const classified = terms ? findFunctionConjunctions(file.document, syntax, terms, input.changes?.scope === "lines" ? file.changedRanges : undefined) : filterRoleOccurrences(file.document, syntax, file.occurrences, input.roles ?? []);
10927
11333
  result.items.push(...classified.items);
10928
11334
  result.partial ||= classified.partial;
@@ -10999,12 +11405,12 @@ class EvidenceService {
10999
11405
  const item = this.#analyses.item(input.cursor, input.matchIndex);
11000
11406
  if (!item.source || !item.range)
11001
11407
  throw new CursorError("This analysis item has no verified source range");
11002
- const metadataOnly = item.details?.kind === "symbol" || item.details?.kind === "function";
11408
+ const bounded = item.details?.kind === "symbol" || item.details?.kind === "function";
11003
11409
  return {
11004
11410
  path: item.path,
11005
11411
  line: item.line,
11006
11412
  reference: item.source,
11007
- ...metadataOnly ? { range: item.range } : { absoluteFocus: item.range.start }
11413
+ ...bounded ? { range: item.range, structure: analysisItemStructure(item) } : { absoluteFocus: item.range.start }
11008
11414
  };
11009
11415
  }
11010
11416
  return {
@@ -11213,6 +11619,29 @@ import { resolve as resolve24 } from "path";
11213
11619
  // src/discovery-errors.ts
11214
11620
  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>"}';
11215
11621
 
11622
+ // src/request-aliases.ts
11623
+ var MODE_ALIASES = ["anyOf", "allOf"];
11624
+ function normalizeRequestAliases(request) {
11625
+ const notes = [];
11626
+ const { mode, ...rest } = request;
11627
+ let input = rest;
11628
+ if (mode === "anyOf" || mode === "allOf") {
11629
+ if (request[mode] === undefined)
11630
+ throw new SiftLightError(`mode=${mode} is not a mode; pass ${mode}:[...terms] and omit mode`);
11631
+ notes.push(`mode="${mode}" is not a mode; the ${mode} field alone selects this search.`);
11632
+ } else if (mode === "summary" && request.anyOf !== undefined) {
11633
+ notes.push("anyOf returns one analysis page whose header already carries per-file statistics; mode=summary was served by that page.");
11634
+ } else if (mode !== undefined) {
11635
+ input = { ...rest, mode };
11636
+ }
11637
+ if (input.sourceCursor !== undefined && input.line !== undefined) {
11638
+ const { line: _line, ...withoutLine } = input;
11639
+ input = withoutLine;
11640
+ notes.push("line is ignored with sourceCursor; the continuation selects its own source range.");
11641
+ }
11642
+ return { input, notes };
11643
+ }
11644
+
11216
11645
  // src/format.ts
11217
11646
  import { readFile as readFile3 } from "fs/promises";
11218
11647
  var RESULT_METADATA_RESERVE_BYTES = 1024;
@@ -12566,6 +12995,16 @@ class LanguageCapabilityCatalog {
12566
12995
  }
12567
12996
 
12568
12997
  // src/service.ts
12998
+ function withRequestNotes(result, notes) {
12999
+ if (notes.length === 0)
13000
+ return result;
13001
+ return {
13002
+ text: `${notes.map((note) => `[Request note: ${note}]`).join(`
13003
+ `)}
13004
+ ${result.text}`,
13005
+ details: { ...result.details, requestNotes: [...notes] }
13006
+ };
13007
+ }
12569
13008
  function filterList(value) {
12570
13009
  return value === undefined ? [] : Array.isArray(value) ? [...value] : [value];
12571
13010
  }
@@ -12794,7 +13233,11 @@ class SiftLightService {
12794
13233
  this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
12795
13234
  this.#evidence = new EvidenceService(this.#runRipgrep, this.#snapshots, options.structure, options.conceptSearch, options.semanticJudge);
12796
13235
  }
12797
- async search(input, cwd, signal, options = {}) {
13236
+ async search(request, cwd, signal, options = {}) {
13237
+ const { input, notes } = normalizeRequestAliases(request);
13238
+ return withRequestNotes(await this.#searchNormalized(input, cwd, signal, options), notes);
13239
+ }
13240
+ async#searchNormalized(input, cwd, signal, options) {
12798
13241
  validateRawSearchInput(input);
12799
13242
  validateRequestContract(input);
12800
13243
  if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
@@ -13199,16 +13642,18 @@ ${page.body}${rangeNote}${contextNote}${missingSelectionNote}
13199
13642
  }
13200
13643
 
13201
13644
  // src/prompt-guidelines.ts
13645
+ var SIFT_LIGHT_DISCOVERY_GUIDANCE = "Before reporting sift-light absent, check the complete runtime registry, including ALL_TOOLS deferred entries. Read mcp__sift_light__sift_light's declaration and call it via tools in functions.exec; use host tool search otherwise. Unexpanded does not mean absent; call failures are connection/capability failures.";
13202
13646
  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.";
13203
13647
  function siftLightPromptGuidelines(structuredOutput = true) {
13204
13648
  return [
13649
+ SIFT_LIGHT_DISCOVERY_GUIDANCE,
13205
13650
  `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.`,
13206
13651
  `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.`,
13207
13652
  `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.`,
13208
13653
  `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.`,
13209
13654
  `Mode="inspect" with a direct path/line or ordinary retained match returns bounded source windows and source-revision metadata. Some retained analysis selectors return only revision metadata; use direct path/line or the host read capability when source is needed. Do not use inspection merely to obtain a citation.`,
13210
13655
  `Use allOf:["term1","term2"] for explicit same-file literal AND. Add within:"function" only together with allOf to restrict that conjunction to one own-implementation JS/TS/TSX function; omit within for ordinary single-pattern searches. Use roles:["declaration"] or roles:["call"] with a single pattern for JS/TS/TSX/Go syntactic occurrence statistics.`,
13211
- `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.`,
13656
+ `Use anyOf:["term1","term2"] when every exact occurrence of 1-64 literals is needed in one version-bound result. It is case-sensitive, labels counts with input literals unless redact:true is requested, 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.`,
13212
13657
  `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.`,
13213
13658
  `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.`,
13214
13659
  `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.`,
@@ -20586,9 +21031,9 @@ var siftLightSchema = _Object_({
20586
21031
  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.`
20587
21032
  })),
20588
21033
  allOf: Optional(_Array_(String2({ maxLength: MAX_PATH_CHARACTERS }), {
20589
- minItems: 2,
21034
+ minItems: 1,
20590
21035
  maxItems: 3,
20591
- description: `${fieldGuidance("allOf")}. 2-3 distinct terms must occur in one file (default) or one function.`
21036
+ 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.`
20592
21037
  })),
20593
21038
  within: Optional(stringEnum(["file", "function"], {
20594
21039
  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."
@@ -20605,7 +21050,7 @@ var siftLightSchema = _Object_({
20605
21050
  "unknown"
20606
21051
  ]), {
20607
21052
  minItems: 1,
20608
- 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."
21053
+ 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."
20609
21054
  })),
20610
21055
  changes: Optional(_Object_({
20611
21056
  base: Optional(String2({
@@ -20638,7 +21083,7 @@ var siftLightSchema = _Object_({
20638
21083
  paths: Optional(_Array_(String2(), {
20639
21084
  minItems: 1,
20640
21085
  maxItems: MAX_SELECTED_PATHS,
20641
- description: "Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests."
21086
+ 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)."
20642
21087
  })),
20643
21088
  glob: Optional(Union([
20644
21089
  String2({ maxLength: MAX_PATH_CHARACTERS }),
@@ -20662,7 +21107,7 @@ var siftLightSchema = _Object_({
20662
21107
  })),
20663
21108
  hidden: Optional(Boolean2({ description: "Search hidden files (default true; .git is always excluded)." })),
20664
21109
  ignorePolicy: Optional(stringEnum(["respect", "include"], {
20665
- 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."
21110
+ 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."
20666
21111
  })),
20667
21112
  patterns: Optional(_Array_(_Object_({
20668
21113
  id: String2({ minLength: 1, maxLength: 64 }),
@@ -20707,29 +21152,29 @@ var siftLightSchema = _Object_({
20707
21152
  limit: Optional(Integer({
20708
21153
  minimum: 1,
20709
21154
  maximum: MAX_PAGE_SIZE,
20710
- 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.`
21155
+ 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.`
20711
21156
  })),
20712
- mode: Optional(stringEnum(SIFT_LIGHT_MODES, {
20713
- 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}`
21157
+ mode: Optional(stringEnum([...SIFT_LIGHT_MODES, ...MODE_ALIASES], {
21158
+ 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.`
20714
21159
  })),
20715
21160
  line: Optional(Number2({
20716
- description: "1-indexed source line for path inspection/navigation. Omit with matchIndex, matchIndices or targets."
21161
+ description: "1-indexed source line for path inspection/navigation; path-only inspect starts at line 1. Omit with matchIndex, matchIndices or targets."
20717
21162
  })),
20718
21163
  matchIndex: Optional(Number2({
20719
21164
  description: "1-based retained match index for cursor-scoped inspect; replaces path and line."
20720
21165
  })),
20721
21166
  matchIndices: Optional(_Array_(Integer({ minimum: 1 }), {
20722
21167
  minItems: 1,
20723
- maxItems: MAX_INSPECT_TARGETS,
20724
- description: "Inspect up to five visible match numbers together using the same cursor; mutually exclusive with matchIndex, path, line and targets."
21168
+ maxItems: MAX_INSPECT_REQUEST_TARGETS,
21169
+ 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.`
20725
21170
  })),
20726
21171
  targets: Optional(_Array_(_Object_({
20727
21172
  path: String2({ maxLength: MAX_PATH_CHARACTERS }),
20728
21173
  line: Integer({ minimum: 1 })
20729
21174
  }), {
20730
21175
  minItems: 1,
20731
- maxItems: MAX_INSPECT_TARGETS,
20732
- description: "Inspect known path/line locations together without a cursor. The complete batch shares one 16 KiB response budget."
21176
+ maxItems: MAX_INSPECT_REQUEST_TARGETS,
21177
+ 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.`
20733
21178
  })),
20734
21179
  cursor: Optional(String2({ description: "Opaque cursor from a previous stable search snapshot." })),
20735
21180
  operationId: Optional(String2({