sift-light 1.0.1 → 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.
@@ -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.2",
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;
@@ -4038,6 +4041,15 @@ var PYTHON_LANGUAGE_CAPABILITIES = [
4038
4041
  evidence: "syntax",
4039
4042
  availability: "implemented",
4040
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"
4041
4053
  }
4042
4054
  ];
4043
4055
  var DEFAULT_LANGUAGE_CAPABILITIES = [
@@ -5487,7 +5499,7 @@ function publicAnalysisItem(result, item, index, storedId, modelOutput) {
5487
5499
  return {
5488
5500
  path: item.path,
5489
5501
  line: item.line,
5490
- label: result.kind === "outline" && modelOutput ? item.label : publicAnalysisLabel(result),
5502
+ label: result.kind === "outline" && modelOutput || result.kind === "files" ? item.label : publicAnalysisLabel(result),
5491
5503
  index: index + 1,
5492
5504
  ...inspect ? { inspect } : {},
5493
5505
  ...publicDetails ? { details: publicDetails } : {}
@@ -5698,7 +5710,8 @@ Inspect: ${JSON.stringify(inspect)}` : ""}`;
5698
5710
  break;
5699
5711
  }
5700
5712
  } else {
5701
- 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) {
5702
5715
  if (!appendItem(index))
5703
5716
  break;
5704
5717
  }
@@ -5812,6 +5825,8 @@ function resolveInspectionTarget(input, cwd, snapshots) {
5812
5825
  }
5813
5826
  if (!path)
5814
5827
  throw new SiftLightError("path is required when mode=inspect");
5828
+ if (line === undefined && input.cursor === undefined)
5829
+ line = 1;
5815
5830
  if (line === undefined || !Number.isSafeInteger(line) || line < 1) {
5816
5831
  throw new SiftLightError("line must be a positive integer when mode=inspect");
5817
5832
  }
@@ -7540,6 +7555,28 @@ async function filterPathsByModificationTime(cwd, paths, modifiedAfterMs, modifi
7540
7555
  // src/file-discovery.ts
7541
7556
  var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
7542
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
+ }
7543
7580
  function subsequenceScore(text, query) {
7544
7581
  const characters = Array.from(graphemes.segment(text), (item) => item.segment);
7545
7582
  const queryCharacters = Array.from(graphemes.segment(query), (item) => item.segment);
@@ -7590,39 +7627,79 @@ function pathRelativeToDiscoveryRoot(cwd, root, path) {
7590
7627
  return scoped || platformBasename(absolutePath);
7591
7628
  }
7592
7629
  async function discoverFiles(input, cwd, signal) {
7593
- const query = input.query ?? "";
7594
- 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))
7595
7632
  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: "" });
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
+ });
7599
7641
  const policy = new SearchPathPolicy(cwd);
7600
7642
  const discoveryRoot = request.path ?? ".";
7601
7643
  const scoringRoot = await policy.resolveSearchTarget(discoveryRoot);
7602
- const files = await listWorkspaceFiles(cwd, signal, {
7644
+ const includeIgnored = request.ignorePolicy === "include";
7645
+ const listOptions = {
7603
7646
  path: scoringRoot,
7604
7647
  glob: request.glob,
7605
7648
  exclude: request.exclude,
7606
7649
  hidden: request.hidden
7650
+ };
7651
+ const files = await listWorkspaceFiles(cwd, signal, {
7652
+ ...listOptions,
7653
+ ...includeIgnored ? { ignore: false, ignoreParents: false } : {}
7607
7654
  });
7608
7655
  const filtered = await filterPathsByModificationTime(cwd, files.paths, request.modifiedAfterMs, request.modifiedBeforeMs, signal);
7656
+ const rank = (path) => scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
7609
7657
  const selected = filtered.paths.flatMap((path) => {
7610
- const rank = scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
7611
- return rank ? [{ path, ...rank }] : [];
7658
+ const score = rank(path);
7659
+ return score ? [{ path, ...score }] : [];
7612
7660
  }).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
7613
7661
  for (let offset = 0;offset < selected.length; offset += 16) {
7614
7662
  await Promise.all(selected.slice(offset, offset + 16).map((item) => policy.assertExistingPath(item.path)));
7615
7663
  signal?.throwIfAborted();
7616
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;
7617
7694
  return {
7618
7695
  kind: "files",
7619
7696
  unit: "files",
7620
- partial: files.partial || filtered.partial,
7621
- reasons: [...new Set([...files.reasons, ...filtered.reasons])],
7697
+ partial: enumerationPartial,
7698
+ reasons: [...reasons],
7622
7699
  items: selected.map((item) => ({
7623
7700
  path: item.path,
7624
7701
  line: 1,
7625
- label: `File candidate (${item.reason})`,
7702
+ label: `File candidate (score ${String(item.score)}: ${item.reason})`,
7626
7703
  details: {
7627
7704
  kind: "file",
7628
7705
  score: item.score,
@@ -7630,8 +7707,13 @@ async function discoverFiles(input, cwd, signal) {
7630
7707
  inspect: { mode: "inspect", path: item.path, line: 1 }
7631
7708
  }
7632
7709
  })),
7633
- coverage: { fileEnumeration: files.partial || filtered.partial ? "partial" : "complete" },
7634
- 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
+ },
7635
7717
  scope: {
7636
7718
  path: request.path ?? ".",
7637
7719
  requestedPath: request.path ?? ".",
@@ -7644,7 +7726,8 @@ async function discoverFiles(input, cwd, signal) {
7644
7726
  ...request.modifiedAfterMs !== undefined ? { modifiedAfterMs: request.modifiedAfterMs } : {},
7645
7727
  ...request.modifiedBeforeMs !== undefined ? { modifiedBeforeMs: request.modifiedBeforeMs } : {}
7646
7728
  },
7647
- redact: input.redact ?? false
7729
+ redact: input.redact ?? false,
7730
+ ...input.limit !== undefined ? { pageSize: request.pageSize } : {}
7648
7731
  };
7649
7732
  }
7650
7733
 
@@ -7885,7 +7968,19 @@ async function prepare(target, access, structure) {
7885
7968
  let range = target.range;
7886
7969
  let details = { status: "no-symbol" };
7887
7970
  const language = syntaxLanguage(document2.path);
7888
- 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") {
7889
7984
  const syntax = await access.syntax(document2);
7890
7985
  details = {
7891
7986
  status: syntax.status === "ok" ? "no-symbol" : syntax.status === "unsupported" ? "provider-unavailable" : "parse-error",
@@ -7937,7 +8032,7 @@ async function prepare(target, access, structure) {
7937
8032
  details = { status: "provider-unavailable", ...language ? { language } : {} };
7938
8033
  }
7939
8034
  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";
8035
+ const boundary = target.range ? target.structure ? "syntax" : "requested-range" : details.status === "available" && details.range ? "syntax" : "line-window";
7941
8036
  document2.checkRange(range);
7942
8037
  return { target, document: document2, range, structure: details, boundary, focus };
7943
8038
  }
@@ -8296,8 +8391,10 @@ ${preview.text}`);
8296
8391
  }
8297
8392
  };
8298
8393
  }
8299
- async function continueSource(cursor, access, continuations) {
8394
+ async function continueSource(cursor, access, continuations, expectedPath) {
8300
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`);
8301
8398
  const document2 = await access.load(state.source.path, state.source);
8302
8399
  const page = sourcePage(document2, state.remaining, MAX_RESULT_BYTES - 1400);
8303
8400
  const next = continuations.advance(cursor, page.fragment);
@@ -9174,6 +9271,221 @@ function parsePythonOutline(document2) {
9174
9271
  });
9175
9272
  }
9176
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
+
9177
9489
  // src/hybrid-search.ts
9178
9490
  import { resolve as resolve20 } from "path";
9179
9491
 
@@ -10080,7 +10392,17 @@ var MODE_FIELDS_BY_MODE = {
10080
10392
  auto: ordinaryFields,
10081
10393
  summary: ordinaryFields,
10082
10394
  matches: ordinaryFields,
10083
- 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
+ ],
10084
10406
  outline: [...commonFields, "path", "line", "symbol", "cursor", "matchIndex", "maxFilesToParse"],
10085
10407
  imports: [
10086
10408
  ...commonFields,
@@ -10100,7 +10422,16 @@ var MODE_FIELDS_BY_MODE = {
10100
10422
  "matchIndex",
10101
10423
  "maxFilesToParse"
10102
10424
  ],
10103
- 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
+ ],
10104
10435
  structure: [...commonFields, "pattern", ...sourceFilters, "maxFilesToParse"],
10105
10436
  concept: [...commonFields, "query", ...sourceFilters, "maxFilesToParse"],
10106
10437
  hybrid: [...commonFields, "query", ...sourceFilters, "conceptLimit", "maxFilesToParse"],
@@ -10109,9 +10440,7 @@ var MODE_FIELDS_BY_MODE = {
10109
10440
  await: ["mode", "operationId"],
10110
10441
  cancel: ["mode", "operationId"]
10111
10442
  };
10112
- var SAFE_DROP_FIELDS = {
10113
- files: ["scope"]
10114
- };
10443
+ var SAFE_DROP_FIELDS = {};
10115
10444
  var SUPPORTED_OUTLINE_EXTENSIONS = new Set(DEFAULT_LANGUAGE_CAPABILITIES.flatMap((descriptor) => descriptor.capabilities.some((capability) => capability.name === "outline") ? descriptor.extensions : []));
10116
10445
  function modeFields(mode) {
10117
10446
  return MODE_FIELDS_BY_MODE[mode];
@@ -10140,8 +10469,8 @@ var REQUEST_FIELD_GUIDANCE = {
10140
10469
  allOf: "allOf is case-sensitive exact-literal AND; omit pattern, anyOf, literal, ignoreCase, roles",
10141
10470
  context: "context is an output-context budget and is never silently dropped",
10142
10471
  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",
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",
10145
10474
  patterns: "audit accepts named exact-literal patterns and returns one closure receipt",
10146
10475
  maxFilesToParse: "concept/hybrid automatically batch the requested scope; this optional field sets an advanced hard file ceiling"
10147
10476
  };
@@ -10240,12 +10569,15 @@ function schemaError(input, field, reason) {
10240
10569
  function plainFilePatternRecovery(mode, input, invalid) {
10241
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);
10242
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
+ }
10243
10575
  function safeNextRequest(input, mode, invalid) {
10244
10576
  if (input.redact === true && containsSensitiveText(input))
10245
10577
  return;
10246
10578
  const safe = new Set(SAFE_DROP_FIELDS[mode] ?? []);
10247
10579
  const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
10248
- if (plainFilePattern)
10580
+ if (plainFilePattern || matchAllFilePatternRecovery(mode, input, invalid))
10249
10581
  safe.add("pattern");
10250
10582
  if (invalid.some((field) => !safe.has(field)))
10251
10583
  return;
@@ -10291,7 +10623,7 @@ function fieldsError(input, mode, invalid, selectorIssues = []) {
10291
10623
  issues,
10292
10624
  recovery: nextRequest ? {
10293
10625
  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.`,
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.`,
10295
10627
  nextRequest
10296
10628
  } : {
10297
10629
  action: "manual",
@@ -10447,7 +10779,7 @@ function validateRequestContract(input) {
10447
10779
  const allowed = new Set(MODE_FIELDS_BY_MODE[mode]);
10448
10780
  if (mode === "inspect" && raw.sourceCursor !== undefined) {
10449
10781
  allowed.clear();
10450
- for (const field of ["mode", "sourceCursor", "redact"])
10782
+ for (const field of ["mode", "sourceCursor", "redact", "path"])
10451
10783
  allowed.add(field);
10452
10784
  }
10453
10785
  if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof raw.cursor === "string" && raw.cursor.includes(".analysis")) {
@@ -10475,6 +10807,69 @@ import { realpath as realpath6 } from "fs/promises";
10475
10807
  function isEvidenceRequest(input) {
10476
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);
10477
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
+ }
10478
10873
  function maxFilesToParse(value, defaultValue = MAX_STRUCTURE_FILES) {
10479
10874
  const candidate = value ?? defaultValue;
10480
10875
  if (!Number.isSafeInteger(candidate) || candidate < 1 || candidate > MAX_CONFIGURABLE_STRUCTURE_FILES) {
@@ -10490,8 +10885,8 @@ function validateTerms(input) {
10490
10885
  }
10491
10886
  return;
10492
10887
  }
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");
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");
10495
10890
  if (input.pattern !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
10496
10891
  throw new SiftLightError("allOf is an explicit case-sensitive literal conjunction; omit pattern, roles, literal and ignoreCase");
10497
10892
  if (input.within !== undefined && input.within !== "file" && input.within !== "function")
@@ -10689,11 +11084,13 @@ class EvidenceService {
10689
11084
  throw new CursorError("A nonempty sourceCursor is required");
10690
11085
  if (input.mode !== "inspect")
10691
11086
  throw new SiftLightError("sourceCursor requires mode=inspect");
10692
- return continueSource(input.sourceCursor, access, this.#continuations);
11087
+ return continueSource(input.sourceCursor, access, this.#continuations, input.path);
10693
11088
  }
10694
11089
  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);
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;
10697
11094
  }
10698
11095
  if (input.mode === "concept") {
10699
11096
  const execution = await this.#conceptSearch(input, access, options.onProgress);
@@ -10782,7 +11179,7 @@ class EvidenceService {
10782
11179
  if (input.pattern !== undefined || input.allOf !== undefined || input.within !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
10783
11180
  throw new SiftLightError("anyOf is an explicit case-sensitive literal union; omit pattern, allOf, within, roles, literal and ignoreCase");
10784
11181
  if (input.mode !== undefined && input.mode !== "auto" && input.mode !== "matches")
10785
- throw new SiftLightError("anyOf mode must be omitted, auto, or matches");
11182
+ throw new SiftLightError("anyOf mode must be omitted, auto, matches or summary");
10786
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));
10787
11184
  const { path: _inputPath, ...unscopedInput } = input;
10788
11185
  let chunkAccess = access;
@@ -10920,9 +11317,10 @@ class EvidenceService {
10920
11317
  if (item)
10921
11318
  result.items.push(item);
10922
11319
  } else if (terms || input.roles) {
10923
- if (syntaxLanguage(file.document.path))
11320
+ const pythonRoles = !terms && /\.py$/iu.test(file.document.path);
11321
+ if (pythonRoles || syntaxLanguage(file.document.path))
10924
11322
  syntaxCapableFiles += 1;
10925
- const syntax = await access.syntax(file.document);
11323
+ const syntax = pythonRoles ? pythonRoleAnalysis(file.document) : await access.syntax(file.document);
10926
11324
  const classified = terms ? findFunctionConjunctions(file.document, syntax, terms, input.changes?.scope === "lines" ? file.changedRanges : undefined) : filterRoleOccurrences(file.document, syntax, file.occurrences, input.roles ?? []);
10927
11325
  result.items.push(...classified.items);
10928
11326
  result.partial ||= classified.partial;
@@ -10999,12 +11397,12 @@ class EvidenceService {
10999
11397
  const item = this.#analyses.item(input.cursor, input.matchIndex);
11000
11398
  if (!item.source || !item.range)
11001
11399
  throw new CursorError("This analysis item has no verified source range");
11002
- const metadataOnly = item.details?.kind === "symbol" || item.details?.kind === "function";
11400
+ const bounded = item.details?.kind === "symbol" || item.details?.kind === "function";
11003
11401
  return {
11004
11402
  path: item.path,
11005
11403
  line: item.line,
11006
11404
  reference: item.source,
11007
- ...metadataOnly ? { range: item.range } : { absoluteFocus: item.range.start }
11405
+ ...bounded ? { range: item.range, structure: analysisItemStructure(item) } : { absoluteFocus: item.range.start }
11008
11406
  };
11009
11407
  }
11010
11408
  return {
@@ -11213,6 +11611,29 @@ import { resolve as resolve24 } from "path";
11213
11611
  // src/discovery-errors.ts
11214
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>"}';
11215
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
+
11216
11637
  // src/format.ts
11217
11638
  import { readFile as readFile3 } from "fs/promises";
11218
11639
  var RESULT_METADATA_RESERVE_BYTES = 1024;
@@ -12566,6 +12987,16 @@ class LanguageCapabilityCatalog {
12566
12987
  }
12567
12988
 
12568
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
+ }
12569
13000
  function filterList(value) {
12570
13001
  return value === undefined ? [] : Array.isArray(value) ? [...value] : [value];
12571
13002
  }
@@ -12794,7 +13225,11 @@ class SiftLightService {
12794
13225
  this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
12795
13226
  this.#evidence = new EvidenceService(this.#runRipgrep, this.#snapshots, options.structure, options.conceptSearch, options.semanticJudge);
12796
13227
  }
12797
- 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) {
12798
13233
  validateRawSearchInput(input);
12799
13234
  validateRequestContract(input);
12800
13235
  if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
@@ -20586,9 +21021,9 @@ var siftLightSchema = _Object_({
20586
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.`
20587
21022
  })),
20588
21023
  allOf: Optional(_Array_(String2({ maxLength: MAX_PATH_CHARACTERS }), {
20589
- minItems: 2,
21024
+ minItems: 1,
20590
21025
  maxItems: 3,
20591
- 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.`
20592
21027
  })),
20593
21028
  within: Optional(stringEnum(["file", "function"], {
20594
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."
@@ -20605,7 +21040,7 @@ var siftLightSchema = _Object_({
20605
21040
  "unknown"
20606
21041
  ]), {
20607
21042
  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."
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."
20609
21044
  })),
20610
21045
  changes: Optional(_Object_({
20611
21046
  base: Optional(String2({
@@ -20638,7 +21073,7 @@ var siftLightSchema = _Object_({
20638
21073
  paths: Optional(_Array_(String2(), {
20639
21074
  minItems: 1,
20640
21075
  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."
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)."
20642
21077
  })),
20643
21078
  glob: Optional(Union([
20644
21079
  String2({ maxLength: MAX_PATH_CHARACTERS }),
@@ -20662,7 +21097,7 @@ var siftLightSchema = _Object_({
20662
21097
  })),
20663
21098
  hidden: Optional(Boolean2({ description: "Search hidden files (default true; .git is always excluded)." })),
20664
21099
  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."
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."
20666
21101
  })),
20667
21102
  patterns: Optional(_Array_(_Object_({
20668
21103
  id: String2({ minLength: 1, maxLength: 64 }),
@@ -20707,29 +21142,29 @@ var siftLightSchema = _Object_({
20707
21142
  limit: Optional(Integer({
20708
21143
  minimum: 1,
20709
21144
  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.`
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.`
20711
21146
  })),
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}`
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.`
20714
21149
  })),
20715
21150
  line: Optional(Number2({
20716
- 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."
20717
21152
  })),
20718
21153
  matchIndex: Optional(Number2({
20719
21154
  description: "1-based retained match index for cursor-scoped inspect; replaces path and line."
20720
21155
  })),
20721
21156
  matchIndices: Optional(_Array_(Integer({ minimum: 1 }), {
20722
21157
  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."
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.`
20725
21160
  })),
20726
21161
  targets: Optional(_Array_(_Object_({
20727
21162
  path: String2({ maxLength: MAX_PATH_CHARACTERS }),
20728
21163
  line: Integer({ minimum: 1 })
20729
21164
  }), {
20730
21165
  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."
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.`
20733
21168
  })),
20734
21169
  cursor: Optional(String2({ description: "Opaque cursor from a previous stable search snapshot." })),
20735
21170
  operationId: Optional(String2({