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.
@@ -7,7 +7,7 @@ import { URL as URL2 } from "node:url";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "sift-light",
10
- version: "1.0.1",
10
+ version: "1.0.2",
11
11
  description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
12
12
  keywords: [
13
13
  "ai-agent",
@@ -346,7 +346,7 @@ function compactRows(analysis) {
346
346
  rows.push(JSON.stringify(item.path));
347
347
  previousPath = item.path;
348
348
  }
349
- const label = analysis.kind === "outline" && analysis.modelOutput ? item.label : "metadata";
349
+ const label = analysis.kind === "outline" && analysis.modelOutput || analysis.kind === "files" ? item.label : "metadata";
350
350
  const semanticJudge = item.details?.semanticJudge;
351
351
  const judgment = isRecord(semanticJudge) && typeof semanticJudge.classification === "string" && typeof semanticJudge.probability === "number" && typeof semanticJudge.model === "string" ? ` Jev: ${semanticJudge.classification}; probability=${String(semanticJudge.probability)};${typeof semanticJudge.confidence === "number" ? ` confidence=${String(semanticJudge.confidence)};` : ""} model=${JSON.stringify(semanticJudge.model)}.` : "";
352
352
  rows.push(`#${String(item.index)} L${String(item.line)} ${label}${judgment}`);
@@ -386,6 +386,7 @@ function compactMcpModelText(result) {
386
386
  const inspect = compactInspectInstruction(analysis);
387
387
  const nextRequest = distinctNextRequest(result.details, analysis);
388
388
  const compact = [
389
+ ...(result.details.requestNotes ?? []).map((note) => `[Request note: ${note}]`),
389
390
  header,
390
391
  ...compactMetadata(result.details, analysis),
391
392
  ...compactRows(analysis),
@@ -417,7 +418,7 @@ var MAX_ANALYSIS_STORAGE_BYTES = 32 * 1024 * 1024;
417
418
  var ANALYSIS_METADATA_RESERVE_BYTES = 64 * 1024;
418
419
  var MAX_ANALYSIS_REASONS = 64;
419
420
  var MAX_ANALYSIS_REASON_BYTES = 4 * 1024;
420
- var MIN_ANY_OF_TERMS = 2;
421
+ var MIN_ANY_OF_TERMS = 1;
421
422
  var MAX_ANY_OF_TERMS = 8;
422
423
  var MAX_ANY_OF_TOTAL_TERMS = 64;
423
424
  var MAX_LITERAL_TERM_BYTES = 256;
@@ -437,6 +438,7 @@ var ESTIMATED_CHARACTERS_PER_TOKEN = 4;
437
438
  var DEFAULT_SUMMARY_FILE_LIMIT = 30;
438
439
  var MAX_SELECTED_PATHS = 20;
439
440
  var MAX_INSPECT_TARGETS = 5;
441
+ var MAX_INSPECT_REQUEST_TARGETS = 20;
440
442
  var MAX_DISPLAYED_OCCURRENCES = 20;
441
443
  var MAX_STORED_MATCHES = 50000;
442
444
  var MAX_STORED_OCCURRENCES = 200000;
@@ -458,6 +460,7 @@ var PRIVATE_KEY = /-----BEGIN ([^-\r\n]*PRIVATE KEY)-----[\s\S]*?-----END \1----
458
460
  var SENSITIVE_NAME = String.raw`(?:(?:[A-Za-z][A-Za-z0-9]*[_-])*(?:password|passwd|secret|token|api[_-]?key|access[_-]?(?:key|token)|secret[_-]?access[_-]?key|private[_-]?key|service[_-]?key)(?:[_-][A-Za-z0-9]+)*)`;
459
461
  var SENSITIVE_ASSIGNMENT = new RegExp(String.raw`((?<![A-Za-z0-9_-])(?:["']?${SENSITIVE_NAME}["']?)\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}\r\n]+)`, "gi");
460
462
  var SENSITIVE_TOKEN = /\b(?:sk|ghp|xox[baprs])[-_][A-Za-z0-9][A-Za-z0-9_-]{7,}\b/g;
463
+ var CODE_EXPRESSION = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\s*[([]/u;
461
464
  var TYPE_ONLY_VALUES = new Set([
462
465
  "boolean",
463
466
  "number",
@@ -477,10 +480,11 @@ function redactString(value) {
477
480
  });
478
481
  redacted = redacted.replace(SENSITIVE_ASSIGNMENT, (match, prefix, rawValue) => {
479
482
  const unquoted = rawValue.replace(/^["']|["']$/g, "").toLowerCase();
480
- if (TYPE_ONLY_VALUES.has(unquoted))
483
+ if (TYPE_ONLY_VALUES.has(unquoted) || CODE_EXPRESSION.test(rawValue))
481
484
  return match;
482
485
  count += 1;
483
- return `${prefix}"[REDACTED]"`;
486
+ const quote = rawValue.startsWith('"') || rawValue.startsWith("'") ? rawValue[0] : "";
487
+ return `${prefix}${quote}[REDACTED]${quote}`;
484
488
  });
485
489
  redacted = redacted.replace(SENSITIVE_TOKEN, () => {
486
490
  count += 1;
@@ -738,6 +742,15 @@ var PYTHON_LANGUAGE_CAPABILITIES = [
738
742
  evidence: "syntax",
739
743
  availability: "implemented",
740
744
  load: "lazy"
745
+ },
746
+ {
747
+ id: "python-lexical.roles",
748
+ name: "roles",
749
+ provider: "bounded Python lexical role scanner",
750
+ providerKind: "builtin",
751
+ evidence: "syntax",
752
+ availability: "implemented",
753
+ load: "lazy"
741
754
  }
742
755
  ];
743
756
  var DEFAULT_LANGUAGE_CAPABILITIES = [
@@ -865,7 +878,17 @@ var MODE_FIELDS_BY_MODE = {
865
878
  auto: ordinaryFields,
866
879
  summary: ordinaryFields,
867
880
  matches: ordinaryFields,
868
- inspect: [...commonFields, "path", "line", "cursor", "matchIndex", "matchIndices", "targets"],
881
+ inspect: [
882
+ ...commonFields,
883
+ "path",
884
+ "paths",
885
+ "line",
886
+ "cursor",
887
+ "matchIndex",
888
+ "matchIndices",
889
+ "targets",
890
+ "scope"
891
+ ],
869
892
  outline: [...commonFields, "path", "line", "symbol", "cursor", "matchIndex", "maxFilesToParse"],
870
893
  imports: [
871
894
  ...commonFields,
@@ -885,7 +908,16 @@ var MODE_FIELDS_BY_MODE = {
885
908
  "matchIndex",
886
909
  "maxFilesToParse"
887
910
  ],
888
- files: [...commonFields, "query", ...sourceFilters, "modifiedAfter", "modifiedBefore"],
911
+ files: [
912
+ ...commonFields,
913
+ "query",
914
+ ...sourceFilters,
915
+ "ignorePolicy",
916
+ "limit",
917
+ "scope",
918
+ "modifiedAfter",
919
+ "modifiedBefore"
920
+ ],
889
921
  structure: [...commonFields, "pattern", ...sourceFilters, "maxFilesToParse"],
890
922
  concept: [...commonFields, "query", ...sourceFilters, "maxFilesToParse"],
891
923
  hybrid: [...commonFields, "query", ...sourceFilters, "conceptLimit", "maxFilesToParse"],
@@ -894,9 +926,7 @@ var MODE_FIELDS_BY_MODE = {
894
926
  await: ["mode", "operationId"],
895
927
  cancel: ["mode", "operationId"]
896
928
  };
897
- var SAFE_DROP_FIELDS = {
898
- files: ["scope"]
899
- };
929
+ var SAFE_DROP_FIELDS = {};
900
930
  var SUPPORTED_OUTLINE_EXTENSIONS = new Set(DEFAULT_LANGUAGE_CAPABILITIES.flatMap((descriptor) => descriptor.capabilities.some((capability) => capability.name === "outline") ? descriptor.extensions : []));
901
931
  function modeFields(mode) {
902
932
  return MODE_FIELDS_BY_MODE[mode];
@@ -925,8 +955,8 @@ var REQUEST_FIELD_GUIDANCE = {
925
955
  allOf: "allOf is case-sensitive exact-literal AND; omit pattern, anyOf, literal, ignoreCase, roles",
926
956
  context: "context is an output-context budget and is never silently dropped",
927
957
  limit: "limit is an output/page budget and is never silently dropped",
928
- 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",
929
- ignorePolicy: "respect keeps repository ignore rules; include searches ignored files but always excludes .git internals and protected paths",
958
+ scope: "scope applies to ordinary content search; mode=files is always strict, so it accepts only the redundant scope=strict",
959
+ ignorePolicy: "respect keeps repository ignore rules and discloses ignored files; include also searches or lists ignored files but always excludes .git internals and protected paths",
930
960
  patterns: "audit accepts named exact-literal patterns and returns one closure receipt",
931
961
  maxFilesToParse: "concept/hybrid automatically batch the requested scope; this optional field sets an advanced hard file ceiling"
932
962
  };
@@ -1025,12 +1055,15 @@ function schemaError(input, field, reason) {
1025
1055
  function plainFilePatternRecovery(mode, input, invalid) {
1026
1056
  return mode === "files" && invalid.includes("pattern") && input.query === undefined && typeof input.pattern === "string" && input.pattern.trim().length > 0 && input.pattern.length <= 256 && input.pattern.isWellFormed() && !/[\\^$.*+?()[\]{}|\r\n\0]/u.test(input.pattern);
1027
1057
  }
1058
+ function matchAllFilePatternRecovery(mode, input, invalid) {
1059
+ return mode === "files" && invalid.includes("pattern") && input.query === undefined && typeof input.pattern === "string" && /^(?:\.\*|\.\+|\*+)$/u.test(input.pattern.trim());
1060
+ }
1028
1061
  function safeNextRequest(input, mode, invalid) {
1029
1062
  if (input.redact === true && containsSensitiveText(input))
1030
1063
  return;
1031
1064
  const safe = new Set(SAFE_DROP_FIELDS[mode] ?? []);
1032
1065
  const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
1033
- if (plainFilePattern)
1066
+ if (plainFilePattern || matchAllFilePatternRecovery(mode, input, invalid))
1034
1067
  safe.add("pattern");
1035
1068
  if (invalid.some((field) => !safe.has(field)))
1036
1069
  return;
@@ -1076,7 +1109,7 @@ function fieldsError(input, mode, invalid, selectorIssues = []) {
1076
1109
  issues,
1077
1110
  recovery: nextRequest ? {
1078
1111
  action: "retry",
1079
- 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.`,
1112
+ reason: plainFilePatternRecovery(mode, input, invalid) ? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved." : matchAllFilePatternRecovery(mode, input, invalid) ? "A match-all pattern lists every file; omitting query does that, so copy nextRequest; all filters are preserved." : `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
1080
1113
  nextRequest
1081
1114
  } : {
1082
1115
  action: "manual",
@@ -1232,7 +1265,7 @@ function validateRequestContract(input) {
1232
1265
  const allowed = new Set(MODE_FIELDS_BY_MODE[mode]);
1233
1266
  if (mode === "inspect" && raw.sourceCursor !== undefined) {
1234
1267
  allowed.clear();
1235
- for (const field of ["mode", "sourceCursor", "redact"])
1268
+ for (const field of ["mode", "sourceCursor", "redact", "path"])
1236
1269
  allowed.add(field);
1237
1270
  }
1238
1271
  if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof raw.cursor === "string" && raw.cursor.includes(".analysis")) {
@@ -2948,7 +2981,7 @@ function normalizeRequest(input) {
2948
2981
  throw new SiftLightError("ignorePolicy must be respect or include");
2949
2982
  const pattern = input.pattern;
2950
2983
  if (pattern === undefined) {
2951
- throw new SiftLightError("pattern is required when cursor is not provided");
2984
+ throw new SiftLightError('pattern is required when cursor is not provided; to list files use mode="files" (query optional), to read a known file use mode="inspect" with path');
2952
2985
  }
2953
2986
  const path = input.path?.replace(/^@/, "");
2954
2987
  return {
@@ -5872,7 +5905,7 @@ function publicAnalysisItem(result, item, index, storedId, modelOutput) {
5872
5905
  return {
5873
5906
  path: item.path,
5874
5907
  line: item.line,
5875
- label: result.kind === "outline" && modelOutput ? item.label : publicAnalysisLabel(result),
5908
+ label: result.kind === "outline" && modelOutput || result.kind === "files" ? item.label : publicAnalysisLabel(result),
5876
5909
  index: index + 1,
5877
5910
  ...inspect ? { inspect } : {},
5878
5911
  ...publicDetails ? { details: publicDetails } : {}
@@ -6083,7 +6116,8 @@ Inspect: ${JSON.stringify(inspect)}` : ""}`;
6083
6116
  break;
6084
6117
  }
6085
6118
  } else {
6086
- for (let index = offset;index < result.items.length && items.length < 30; index += 1) {
6119
+ const pageSize = result.pageSize ?? 30;
6120
+ for (let index = offset;index < result.items.length && items.length < pageSize; index += 1) {
6087
6121
  if (!appendItem(index))
6088
6122
  break;
6089
6123
  }
@@ -6197,6 +6231,8 @@ function resolveInspectionTarget(input, cwd, snapshots) {
6197
6231
  }
6198
6232
  if (!path)
6199
6233
  throw new SiftLightError("path is required when mode=inspect");
6234
+ if (line === undefined && input.cursor === undefined)
6235
+ line = 1;
6200
6236
  if (line === undefined || !Number.isSafeInteger(line) || line < 1) {
6201
6237
  throw new SiftLightError("line must be a positive integer when mode=inspect");
6202
6238
  }
@@ -7925,6 +7961,28 @@ async function filterPathsByModificationTime(cwd, paths, modifiedAfterMs, modifi
7925
7961
  // src/file-discovery.ts
7926
7962
  var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
7927
7963
  var FILE_QUERY_GLOB_WILDCARD = /[*?]/u;
7964
+ var FILE_QUERY_MATCH_ALL = /^(?:[*/]+|\.\*|\.\+|\*\.\*)$/u;
7965
+ var FILE_QUERY_REGEX_SYNTAX = /[()|^$\\+]/u;
7966
+ var IGNORED_MATCH_SAMPLES = 5;
7967
+ function normalizeFileQuery(query, hasGlob) {
7968
+ const trimmed = query.trim();
7969
+ if (FILE_QUERY_MATCH_ALL.test(trimmed))
7970
+ return {
7971
+ query: "",
7972
+ note: `Query ${JSON.stringify(query)} was treated as listing every file under path; omit query for the same result.`
7973
+ };
7974
+ if (!FILE_QUERY_GLOB_WILDCARD.test(trimmed))
7975
+ return { query };
7976
+ if (!hasGlob && !FILE_QUERY_REGEX_SYNTAX.test(trimmed) && !/\s/u.test(trimmed)) {
7977
+ const glob = trimmed.includes("/") ? trimmed : `**/${trimmed}`;
7978
+ return {
7979
+ query: "",
7980
+ glob,
7981
+ note: `Query ${JSON.stringify(query)} contains glob wildcards and was applied as glob ${JSON.stringify(glob)}; mode=files query otherwise matches filename text.`
7982
+ };
7983
+ }
7984
+ throw new SiftLightError(`File query ${JSON.stringify(query)} uses wildcard or regex syntax; mode=files matches filename and path text, not patterns. Omit query to retain every file under path, use glob for one name pattern, or run one files request per name.`);
7985
+ }
7928
7986
  function subsequenceScore(text, query) {
7929
7987
  const characters = Array.from(graphemes.segment(text), (item) => item.segment);
7930
7988
  const queryCharacters = Array.from(graphemes.segment(query), (item) => item.segment);
@@ -7975,39 +8033,79 @@ function pathRelativeToDiscoveryRoot(cwd, root, path) {
7975
8033
  return scoped || platformBasename(absolutePath);
7976
8034
  }
7977
8035
  async function discoverFiles(input, cwd, signal) {
7978
- const query = input.query ?? "";
7979
- if (query.length > 256 || !query.isWellFormed() || /[\r\n\0]/.test(query))
8036
+ const rawQuery = input.query ?? "";
8037
+ if (rawQuery.length > 256 || !rawQuery.isWellFormed() || /[\r\n\0]/.test(rawQuery))
7980
8038
  throw new SiftLightError("File query must be well-formed single-line text of at most 256 characters");
7981
- if (FILE_QUERY_GLOB_WILDCARD.test(query))
7982
- 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.`);
7983
- const request = normalizeRequest({ ...input, pattern: "" });
8039
+ const requestedGlobs = input.glob === undefined ? [] : Array.isArray(input.glob) ? input.glob : [input.glob];
8040
+ const normalizedQuery = normalizeFileQuery(rawQuery, requestedGlobs.length > 0);
8041
+ const query = normalizedQuery.query;
8042
+ const request = normalizeRequest({
8043
+ ...input,
8044
+ pattern: "",
8045
+ ...normalizedQuery.glob ? { glob: [...requestedGlobs, normalizedQuery.glob] } : {}
8046
+ });
7984
8047
  const policy = new SearchPathPolicy(cwd);
7985
8048
  const discoveryRoot = request.path ?? ".";
7986
8049
  const scoringRoot = await policy.resolveSearchTarget(discoveryRoot);
7987
- const files = await listWorkspaceFiles(cwd, signal, {
8050
+ const includeIgnored = request.ignorePolicy === "include";
8051
+ const listOptions = {
7988
8052
  path: scoringRoot,
7989
8053
  glob: request.glob,
7990
8054
  exclude: request.exclude,
7991
8055
  hidden: request.hidden
8056
+ };
8057
+ const files = await listWorkspaceFiles(cwd, signal, {
8058
+ ...listOptions,
8059
+ ...includeIgnored ? { ignore: false, ignoreParents: false } : {}
7992
8060
  });
7993
8061
  const filtered = await filterPathsByModificationTime(cwd, files.paths, request.modifiedAfterMs, request.modifiedBeforeMs, signal);
8062
+ const rank = (path) => scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
7994
8063
  const selected = filtered.paths.flatMap((path) => {
7995
- const rank = scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
7996
- return rank ? [{ path, ...rank }] : [];
8064
+ const score = rank(path);
8065
+ return score ? [{ path, ...score }] : [];
7997
8066
  }).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
7998
8067
  for (let offset = 0;offset < selected.length; offset += 16) {
7999
8068
  await Promise.all(selected.slice(offset, offset + 16).map((item) => policy.assertExistingPath(item.path)));
8000
8069
  signal?.throwIfAborted();
8001
8070
  }
8071
+ const reasons = new Set([...files.reasons, ...filtered.reasons]);
8072
+ if (normalizedQuery.note)
8073
+ reasons.add(normalizedQuery.note);
8074
+ let ignoredFiles = 0;
8075
+ let ignoredMatches = 0;
8076
+ let ignoredComparisonPartial = false;
8077
+ if (!includeIgnored) {
8078
+ const everything = await listWorkspaceFiles(cwd, signal, {
8079
+ ...listOptions,
8080
+ ignore: false,
8081
+ ignoreParents: false
8082
+ });
8083
+ ignoredComparisonPartial = everything.partial;
8084
+ const admitted = new Set(files.paths);
8085
+ const ignored = everything.paths.filter((path) => !admitted.has(path));
8086
+ ignoredFiles = ignored.length;
8087
+ const ignoredCandidates = ignored.flatMap((path) => {
8088
+ const score = rank(path);
8089
+ return score ? [{ path, ...score }] : [];
8090
+ }).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
8091
+ ignoredMatches = ignoredCandidates.length;
8092
+ if (ignoredFiles > 0) {
8093
+ const samples = ignoredCandidates.slice(0, IGNORED_MATCH_SAMPLES).map((item) => `${JSON.stringify(item.path)} (${item.reason})`);
8094
+ reasons.add(ignoredMatches > 0 ? `${String(ignoredFiles)} file(s) were excluded by ignore rules; ${String(ignoredMatches)} of them match this query${samples.length ? `, e.g. ${samples.join(", ")}` : ""}. Retry with ignorePolicy "include" to list them.` : `${String(ignoredFiles)} file(s) were excluded by ignore rules; none of them match this query.`);
8095
+ }
8096
+ if (ignoredComparisonPartial)
8097
+ reasons.add("Ignored-file comparison was incomplete; the ignored-file count is a lower bound");
8098
+ }
8099
+ const enumerationPartial = files.partial || filtered.partial;
8002
8100
  return {
8003
8101
  kind: "files",
8004
8102
  unit: "files",
8005
- partial: files.partial || filtered.partial,
8006
- reasons: [...new Set([...files.reasons, ...filtered.reasons])],
8103
+ partial: enumerationPartial,
8104
+ reasons: [...reasons],
8007
8105
  items: selected.map((item) => ({
8008
8106
  path: item.path,
8009
8107
  line: 1,
8010
- label: `File candidate (${item.reason})`,
8108
+ label: `File candidate (score ${String(item.score)}: ${item.reason})`,
8011
8109
  details: {
8012
8110
  kind: "file",
8013
8111
  score: item.score,
@@ -8015,8 +8113,13 @@ async function discoverFiles(input, cwd, signal) {
8015
8113
  inspect: { mode: "inspect", path: item.path, line: 1 }
8016
8114
  }
8017
8115
  })),
8018
- coverage: { fileEnumeration: files.partial || filtered.partial ? "partial" : "complete" },
8019
- stats: { filesEnumerated: files.paths.length },
8116
+ coverage: {
8117
+ fileEnumeration: enumerationPartial ? "partial" : ignoredFiles > 0 || ignoredComparisonPartial ? "policy-filtered" : "complete"
8118
+ },
8119
+ stats: {
8120
+ filesEnumerated: files.paths.length,
8121
+ ...includeIgnored ? {} : { ignoredFiles, ignoredMatches }
8122
+ },
8020
8123
  scope: {
8021
8124
  path: request.path ?? ".",
8022
8125
  requestedPath: request.path ?? ".",
@@ -8029,7 +8132,8 @@ async function discoverFiles(input, cwd, signal) {
8029
8132
  ...request.modifiedAfterMs !== undefined ? { modifiedAfterMs: request.modifiedAfterMs } : {},
8030
8133
  ...request.modifiedBeforeMs !== undefined ? { modifiedBeforeMs: request.modifiedBeforeMs } : {}
8031
8134
  },
8032
- redact: input.redact ?? false
8135
+ redact: input.redact ?? false,
8136
+ ...input.limit !== undefined ? { pageSize: request.pageSize } : {}
8033
8137
  };
8034
8138
  }
8035
8139
 
@@ -8270,7 +8374,19 @@ async function prepare(target, access, structure) {
8270
8374
  let range = target.range;
8271
8375
  let details = { status: "no-symbol" };
8272
8376
  const language = syntaxLanguage(document.path);
8273
- if (document.utf8 && language && language !== "go") {
8377
+ if (target.range && target.structure) {
8378
+ document.checkRange(target.range);
8379
+ const lines = {
8380
+ startLine: document.lineAt(target.range.start),
8381
+ endLine: document.lineAt(Math.max(target.range.start, target.range.end - 1))
8382
+ };
8383
+ range = document.lineRange(lines.startLine, lines.endLine);
8384
+ details = {
8385
+ ...target.structure,
8386
+ range: lines,
8387
+ ...target.structure.symbol ? { symbol: { ...target.structure.symbol, range: lines } } : {}
8388
+ };
8389
+ } else if (document.utf8 && language && language !== "go") {
8274
8390
  const syntax = await access.syntax(document);
8275
8391
  details = {
8276
8392
  status: syntax.status === "ok" ? "no-symbol" : syntax.status === "unsupported" ? "provider-unavailable" : "parse-error",
@@ -8322,7 +8438,7 @@ async function prepare(target, access, structure) {
8322
8438
  details = { status: "provider-unavailable", ...language ? { language } : {} };
8323
8439
  }
8324
8440
  range ??= document.lineRange(Math.max(1, target.line - 10), Math.min(document.lineStarts.length, target.line + 10));
8325
- const boundary = target.range ? "requested-range" : details.status === "available" && details.range ? "syntax" : "line-window";
8441
+ const boundary = target.range ? target.structure ? "syntax" : "requested-range" : details.status === "available" && details.range ? "syntax" : "line-window";
8326
8442
  document.checkRange(range);
8327
8443
  return { target, document, range, structure: details, boundary, focus };
8328
8444
  }
@@ -8681,8 +8797,10 @@ ${preview.text}`);
8681
8797
  }
8682
8798
  };
8683
8799
  }
8684
- async function continueSource(cursor, access, continuations) {
8800
+ async function continueSource(cursor, access, continuations, expectedPath) {
8685
8801
  const state = continuations.resolve(cursor);
8802
+ if (expectedPath !== undefined && resolve18(access.cwd, expectedPath.replace(/^@/, "")) !== resolve18(access.cwd, state.source.path))
8803
+ throw new SiftLightError(`sourceCursor continues ${JSON.stringify(state.source.path)}, not ${JSON.stringify(expectedPath)}; copy the returned nextRequest exactly`);
8686
8804
  const document = await access.load(state.source.path, state.source);
8687
8805
  const page = sourcePage(document, state.remaining, MAX_RESULT_BYTES - 1400);
8688
8806
  const next = continuations.advance(cursor, page.fragment);
@@ -9559,6 +9677,221 @@ function parsePythonOutline(document) {
9559
9677
  });
9560
9678
  }
9561
9679
 
9680
+ // src/python-roles.ts
9681
+ var KEYWORDS = new Set([
9682
+ "and",
9683
+ "as",
9684
+ "assert",
9685
+ "async",
9686
+ "await",
9687
+ "case",
9688
+ "del",
9689
+ "elif",
9690
+ "else",
9691
+ "except",
9692
+ "for",
9693
+ "from",
9694
+ "global",
9695
+ "if",
9696
+ "import",
9697
+ "in",
9698
+ "is",
9699
+ "lambda",
9700
+ "match",
9701
+ "nonlocal",
9702
+ "not",
9703
+ "or",
9704
+ "raise",
9705
+ "return",
9706
+ "while",
9707
+ "with",
9708
+ "yield"
9709
+ ]);
9710
+ var STRING_PREFIX = /^(?:[rRbBuUfF]|[rR][bBfF]|[bBfF][rR])?$/u;
9711
+ var IDENTIFIER_START = /[\p{ID_Start}_]/u;
9712
+ var IDENTIFIER_PART = /[\p{ID_Continue}]/u;
9713
+ function lex(text) {
9714
+ const comments = [];
9715
+ const strings = [];
9716
+ const code = new Uint8Array(text.length).fill(1);
9717
+ const mark = (start, end) => {
9718
+ code.fill(0, start, end);
9719
+ };
9720
+ let index = 0;
9721
+ while (index < text.length) {
9722
+ const character = text[index];
9723
+ if (character === "#") {
9724
+ const newline = text.indexOf(`
9725
+ `, index);
9726
+ const end = newline < 0 ? text.length : newline;
9727
+ comments.push({ start: index, end });
9728
+ mark(index, end);
9729
+ index = end;
9730
+ continue;
9731
+ }
9732
+ if (character !== "'" && character !== '"') {
9733
+ index += 1;
9734
+ continue;
9735
+ }
9736
+ let prefixStart = index;
9737
+ while (prefixStart > 0 && /[rRbBuUfF]/u.test(text[prefixStart - 1] ?? ""))
9738
+ prefixStart -= 1;
9739
+ const prefix = text.slice(prefixStart, index);
9740
+ const beforePrefix = text[prefixStart - 1] ?? "";
9741
+ const start = STRING_PREFIX.test(prefix) && !IDENTIFIER_PART.test(beforePrefix) ? prefixStart : index;
9742
+ const triple = text.slice(index, index + 3) === character.repeat(3);
9743
+ const delimiter = triple ? character.repeat(3) : character;
9744
+ let cursor = index + delimiter.length;
9745
+ let end = text.length;
9746
+ while (cursor < text.length) {
9747
+ const current = text[cursor];
9748
+ if (current === "\\") {
9749
+ cursor += 2;
9750
+ continue;
9751
+ }
9752
+ if (!triple && current === `
9753
+ `) {
9754
+ end = cursor;
9755
+ break;
9756
+ }
9757
+ if (text.startsWith(delimiter, cursor)) {
9758
+ end = cursor + delimiter.length;
9759
+ break;
9760
+ }
9761
+ cursor += 1;
9762
+ }
9763
+ strings.push({ start, end });
9764
+ mark(start, end);
9765
+ index = end;
9766
+ }
9767
+ return { comments, strings, code };
9768
+ }
9769
+ function codeRanges(code) {
9770
+ const ranges = [];
9771
+ let start = -1;
9772
+ for (let index = 0;index <= code.length; index += 1) {
9773
+ const inCode = index < code.length && code[index] === 1;
9774
+ if (inCode && start < 0)
9775
+ start = index;
9776
+ else if (!inCode && start >= 0) {
9777
+ ranges.push({ start, end: index });
9778
+ start = -1;
9779
+ }
9780
+ }
9781
+ return ranges;
9782
+ }
9783
+ function identifierEnd(text, start) {
9784
+ let end = start;
9785
+ while (end < text.length && IDENTIFIER_PART.test(text[end] ?? ""))
9786
+ end += 1;
9787
+ return end;
9788
+ }
9789
+ function skipSpaces(text, index, code) {
9790
+ let cursor = index;
9791
+ while (cursor < text.length && code[cursor] === 1 && /[ \t]/u.test(text[cursor] ?? ""))
9792
+ cursor += 1;
9793
+ return cursor;
9794
+ }
9795
+ function statementEnd(text, start, code) {
9796
+ let depth = 0;
9797
+ for (let index = start;index < text.length; index += 1) {
9798
+ if (code[index] !== 1)
9799
+ continue;
9800
+ const character = text[index];
9801
+ if (character === "(" || character === "[" || character === "{")
9802
+ depth += 1;
9803
+ else if (character === ")" || character === "]" || character === "}")
9804
+ depth = Math.max(0, depth - 1);
9805
+ else if (character === "\\" && text[index + 1] === `
9806
+ `)
9807
+ index += 1;
9808
+ else if (character === `
9809
+ ` && depth === 0)
9810
+ return index;
9811
+ }
9812
+ return text.length;
9813
+ }
9814
+ function pythonRoleAnalysis(document) {
9815
+ const text = document.text;
9816
+ const { comments, strings, code } = lex(text);
9817
+ const roles = [];
9818
+ const push = (start, end, role, certainty, subkind) => {
9819
+ if (start < end)
9820
+ roles.push({ start, end, role, certainty, node: 0, ...subkind ? { subkind } : {} });
9821
+ };
9822
+ for (const span of comments)
9823
+ push(span.start, span.end, "comment", "syntax");
9824
+ for (const span of strings)
9825
+ push(span.start, span.end, "string", "syntax");
9826
+ for (const span of codeRanges(code))
9827
+ push(span.start, span.end, "code", "syntax");
9828
+ let index = 0;
9829
+ let lineStart = true;
9830
+ while (index < text.length) {
9831
+ const character = text[index] ?? "";
9832
+ if (character === `
9833
+ `) {
9834
+ lineStart = true;
9835
+ index += 1;
9836
+ continue;
9837
+ }
9838
+ if (code[index] !== 1 || !IDENTIFIER_START.test(character)) {
9839
+ if (!/[ \t]/u.test(character))
9840
+ lineStart = false;
9841
+ index += 1;
9842
+ continue;
9843
+ }
9844
+ const previous = text[index - 1] ?? "";
9845
+ if (IDENTIFIER_PART.test(previous)) {
9846
+ index += 1;
9847
+ continue;
9848
+ }
9849
+ const end = identifierEnd(text, index);
9850
+ const word = text.slice(index, end);
9851
+ const atStatementStart = lineStart;
9852
+ lineStart = false;
9853
+ if (atStatementStart && (word === "import" || word === "from")) {
9854
+ const statement = statementEnd(text, index, code);
9855
+ const body = text.slice(index, statement);
9856
+ if (word === "import" || /\bimport\b/u.test(body)) {
9857
+ push(index, statement, "import", "syntax", word === "from" ? "from-import" : "import");
9858
+ index = statement;
9859
+ continue;
9860
+ }
9861
+ }
9862
+ if (word === "def" || word === "class") {
9863
+ const nameStart = skipSpaces(text, end, code);
9864
+ if (IDENTIFIER_START.test(text[nameStart] ?? "")) {
9865
+ const nameEnd = identifierEnd(text, nameStart);
9866
+ push(nameStart, nameEnd, "declaration", "syntax", word === "def" ? "function" : "class");
9867
+ index = nameEnd;
9868
+ continue;
9869
+ }
9870
+ }
9871
+ let chainEnd = end;
9872
+ while (text[chainEnd] === "." && IDENTIFIER_START.test(text[chainEnd + 1] ?? "")) {
9873
+ chainEnd = identifierEnd(text, chainEnd + 1);
9874
+ }
9875
+ const open = skipSpaces(text, chainEnd, code);
9876
+ const lastSegment = text.slice(text.lastIndexOf(".", chainEnd - 1) + 1, chainEnd);
9877
+ const before = text.slice(Math.max(0, index - 4), index);
9878
+ if (text[open] === "(" && code[open] === 1 && !KEYWORDS.has(word) && !KEYWORDS.has(lastSegment) && !/\bdef\s+$|\bclass\s+$/u.test(before)) {
9879
+ push(index, open + 1, "call", "candidate", "lexical-call");
9880
+ }
9881
+ index = chainEnd;
9882
+ }
9883
+ roles.sort((a, b) => a.start - b.start || a.end - b.end || a.role.localeCompare(b.role));
9884
+ return {
9885
+ status: "ok",
9886
+ nodes: [],
9887
+ children: [],
9888
+ symbols: [],
9889
+ roles,
9890
+ diagnostics: [],
9891
+ limited: false
9892
+ };
9893
+ }
9894
+
9562
9895
  // src/hybrid-search.ts
9563
9896
  import { resolve as resolve20 } from "node:path";
9564
9897
 
@@ -10506,6 +10839,69 @@ import { realpath as realpath6 } from "node:fs/promises";
10506
10839
  function isEvidenceRequest(input) {
10507
10840
  return input.mode === "concept" || input.mode === "hybrid" || input.mode === "structure" || input.mode === "files" || input.mode === "inspect" || input.mode === "outline" || input.mode === "imports" || input.mode === "tests" || input.mode === "validate" || input.sourceCursor !== undefined || input.anyOf !== undefined || input.allOf !== undefined || input.within !== undefined || input.roles !== undefined || input.changes !== undefined || input.symbol !== undefined || input.conceptLimit !== undefined || (input.cursor?.includes(".analysis") ?? false);
10508
10841
  }
10842
+ function pageInspectRequest(input) {
10843
+ if (input.scope === "expand")
10844
+ throw new SiftLightError("mode=inspect reads exact locations; scope=expand does not apply (omit scope)");
10845
+ const { scope: _scope, paths, ...rest } = input;
10846
+ let request = rest;
10847
+ if (paths !== undefined) {
10848
+ if (input.cursor !== undefined || input.targets !== undefined || input.matchIndices !== undefined || input.path !== undefined || input.line !== undefined || input.matchIndex !== undefined)
10849
+ throw new SiftLightError("mode=inspect paths opens files from line 1 and cannot be combined with cursor, targets, matchIndices, path, line or matchIndex");
10850
+ request = { ...rest, targets: paths.map((path) => ({ path, line: 1 })) };
10851
+ }
10852
+ const redact = input.redact ? { redact: true } : {};
10853
+ const requested = request.targets?.length ?? request.matchIndices?.length ?? 0;
10854
+ if (requested > MAX_INSPECT_REQUEST_TARGETS)
10855
+ throw new SiftLightError(`mode=inspect accepts at most ${String(MAX_INSPECT_REQUEST_TARGETS)} targets per request; ${String(MAX_INSPECT_TARGETS)} are inspected per response`);
10856
+ if (request.targets && request.targets.length > MAX_INSPECT_TARGETS)
10857
+ return {
10858
+ current: { ...request, targets: request.targets.slice(0, MAX_INSPECT_TARGETS) },
10859
+ remaining: {
10860
+ mode: "inspect",
10861
+ targets: request.targets.slice(MAX_INSPECT_TARGETS),
10862
+ ...redact
10863
+ }
10864
+ };
10865
+ if (request.matchIndices && request.matchIndices.length > MAX_INSPECT_TARGETS)
10866
+ return {
10867
+ current: { ...request, matchIndices: request.matchIndices.slice(0, MAX_INSPECT_TARGETS) },
10868
+ remaining: {
10869
+ mode: "inspect",
10870
+ ...request.cursor !== undefined ? { cursor: request.cursor } : {},
10871
+ matchIndices: request.matchIndices.slice(MAX_INSPECT_TARGETS),
10872
+ ...redact
10873
+ }
10874
+ };
10875
+ return { current: request };
10876
+ }
10877
+ function withRemainingInspection(result, remaining) {
10878
+ const count = remaining.targets?.length ?? remaining.matchIndices?.length ?? 0;
10879
+ return {
10880
+ text: `${result.text}
10881
+
10882
+ [${String(count)} more target(s) were not inspected; one response inspects ${String(MAX_INSPECT_TARGETS)}.]
10883
+ Next request: ${JSON.stringify(remaining)}`,
10884
+ details: {
10885
+ ...result.details,
10886
+ status: "partial",
10887
+ snapshotComplete: false,
10888
+ nextRequest: remaining
10889
+ }
10890
+ };
10891
+ }
10892
+ function analysisItemStructure(item) {
10893
+ const details = item.details ?? {};
10894
+ const language = typeof details.language === "string" ? details.language : undefined;
10895
+ const name = typeof details.name === "string" ? details.name : undefined;
10896
+ const scope = Array.isArray(details.scope) ? details.scope.filter((entry) => typeof entry === "string") : typeof details.scope === "string" ? [details.scope] : [];
10897
+ const kind = details.kind === "function" ? "function" : item.label.split(" ", 1)[0] ?? "symbol";
10898
+ return {
10899
+ status: "available",
10900
+ provider: language === "python" ? "python-outline" : "tree-sitter",
10901
+ ...language ? { language } : {},
10902
+ ...name ? { symbol: { name, kind, scope, range: { startLine: item.line, endLine: item.line } } } : {}
10903
+ };
10904
+ }
10509
10905
  function maxFilesToParse(value, defaultValue = MAX_STRUCTURE_FILES) {
10510
10906
  const candidate = value ?? defaultValue;
10511
10907
  if (!Number.isSafeInteger(candidate) || candidate < 1 || candidate > MAX_CONFIGURABLE_STRUCTURE_FILES) {
@@ -10521,8 +10917,8 @@ function validateTerms(input) {
10521
10917
  }
10522
10918
  return;
10523
10919
  }
10524
- 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)
10525
- throw new SiftLightError("allOf requires 2–3 distinct, nonempty, single-line literal terms");
10920
+ if (!Array.isArray(terms) || terms.length < 1 || terms.length > 3 || terms.some((term) => typeof term !== "string" || !term.trim() || /[\r\n\0]/.test(term)) || new Set(terms).size !== terms.length)
10921
+ throw new SiftLightError("allOf requires 1–3 distinct, nonempty, single-line literal terms");
10526
10922
  if (input.pattern !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
10527
10923
  throw new SiftLightError("allOf is an explicit case-sensitive literal conjunction; omit pattern, roles, literal and ignoreCase");
10528
10924
  if (input.within !== undefined && input.within !== "file" && input.within !== "function")
@@ -10720,11 +11116,13 @@ class EvidenceService {
10720
11116
  throw new CursorError("A nonempty sourceCursor is required");
10721
11117
  if (input.mode !== "inspect")
10722
11118
  throw new SiftLightError("sourceCursor requires mode=inspect");
10723
- return continueSource(input.sourceCursor, access, this.#continuations);
11119
+ return continueSource(input.sourceCursor, access, this.#continuations, input.path);
10724
11120
  }
10725
11121
  if (input.mode === "inspect") {
10726
- const targets = this.#inspectionTargets(input, cwd);
10727
- return targets.some((target) => target.range !== undefined) ? inspectDocumentsMetadata(targets, access, this.#structure) : inspectDocuments(targets, access, this.#continuations, this.#structure);
11122
+ const { current, remaining } = pageInspectRequest(input);
11123
+ const targets = this.#inspectionTargets(current, cwd);
11124
+ const result = await inspectDocuments(targets, access, this.#continuations, this.#structure);
11125
+ return remaining ? withRemainingInspection(result, remaining) : result;
10728
11126
  }
10729
11127
  if (input.mode === "concept") {
10730
11128
  const execution = await this.#conceptSearch(input, access, options.onProgress);
@@ -10813,7 +11211,7 @@ class EvidenceService {
10813
11211
  if (input.pattern !== undefined || input.allOf !== undefined || input.within !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
10814
11212
  throw new SiftLightError("anyOf is an explicit case-sensitive literal union; omit pattern, allOf, within, roles, literal and ignoreCase");
10815
11213
  if (input.mode !== undefined && input.mode !== "auto" && input.mode !== "matches")
10816
- throw new SiftLightError("anyOf mode must be omitted, auto, or matches");
11214
+ throw new SiftLightError("anyOf mode must be omitted, auto, matches or summary");
10817
11215
  const chunks = Array.from({ length: Math.ceil(anyOf.length / MAX_ANY_OF_TERMS) }, (_, index) => anyOf.slice(index * MAX_ANY_OF_TERMS, (index + 1) * MAX_ANY_OF_TERMS));
10818
11216
  const { path: _inputPath, ...unscopedInput } = input;
10819
11217
  let chunkAccess = access;
@@ -10951,9 +11349,10 @@ class EvidenceService {
10951
11349
  if (item)
10952
11350
  result.items.push(item);
10953
11351
  } else if (terms || input.roles) {
10954
- if (syntaxLanguage(file.document.path))
11352
+ const pythonRoles = !terms && /\.py$/iu.test(file.document.path);
11353
+ if (pythonRoles || syntaxLanguage(file.document.path))
10955
11354
  syntaxCapableFiles += 1;
10956
- const syntax = await access.syntax(file.document);
11355
+ const syntax = pythonRoles ? pythonRoleAnalysis(file.document) : await access.syntax(file.document);
10957
11356
  const classified = terms ? findFunctionConjunctions(file.document, syntax, terms, input.changes?.scope === "lines" ? file.changedRanges : undefined) : filterRoleOccurrences(file.document, syntax, file.occurrences, input.roles ?? []);
10958
11357
  result.items.push(...classified.items);
10959
11358
  result.partial ||= classified.partial;
@@ -11030,12 +11429,12 @@ class EvidenceService {
11030
11429
  const item = this.#analyses.item(input.cursor, input.matchIndex);
11031
11430
  if (!item.source || !item.range)
11032
11431
  throw new CursorError("This analysis item has no verified source range");
11033
- const metadataOnly = item.details?.kind === "symbol" || item.details?.kind === "function";
11432
+ const bounded = item.details?.kind === "symbol" || item.details?.kind === "function";
11034
11433
  return {
11035
11434
  path: item.path,
11036
11435
  line: item.line,
11037
11436
  reference: item.source,
11038
- ...metadataOnly ? { range: item.range } : { absoluteFocus: item.range.start }
11437
+ ...bounded ? { range: item.range, structure: analysisItemStructure(item) } : { absoluteFocus: item.range.start }
11039
11438
  };
11040
11439
  }
11041
11440
  return {
@@ -11244,6 +11643,29 @@ import { resolve as resolve24 } from "node:path";
11244
11643
  // src/discovery-errors.ts
11245
11644
  var DISCOVERY_MODE_REQUIRED_ERROR = 'query requires an explicit discovery mode: use mode=files for filename/path discovery or mode=concept for semantic discovery; for example {"mode":"files","query":"<filename-or-path>"}';
11246
11645
 
11646
+ // src/request-aliases.ts
11647
+ var MODE_ALIASES = ["anyOf", "allOf"];
11648
+ function normalizeRequestAliases(request) {
11649
+ const notes = [];
11650
+ const { mode, ...rest } = request;
11651
+ let input = rest;
11652
+ if (mode === "anyOf" || mode === "allOf") {
11653
+ if (request[mode] === undefined)
11654
+ throw new SiftLightError(`mode=${mode} is not a mode; pass ${mode}:[...terms] and omit mode`);
11655
+ notes.push(`mode="${mode}" is not a mode; the ${mode} field alone selects this search.`);
11656
+ } else if (mode === "summary" && request.anyOf !== undefined) {
11657
+ notes.push("anyOf returns one analysis page whose header already carries per-file statistics; mode=summary was served by that page.");
11658
+ } else if (mode !== undefined) {
11659
+ input = { ...rest, mode };
11660
+ }
11661
+ if (input.sourceCursor !== undefined && input.line !== undefined) {
11662
+ const { line: _line, ...withoutLine } = input;
11663
+ input = withoutLine;
11664
+ notes.push("line is ignored with sourceCursor; the continuation selects its own source range.");
11665
+ }
11666
+ return { input, notes };
11667
+ }
11668
+
11247
11669
  // src/format.ts
11248
11670
  import { readFile as readFile3 } from "node:fs/promises";
11249
11671
  var RESULT_METADATA_RESERVE_BYTES = 1024;
@@ -12597,6 +13019,16 @@ class LanguageCapabilityCatalog {
12597
13019
  }
12598
13020
 
12599
13021
  // src/service.ts
13022
+ function withRequestNotes(result, notes) {
13023
+ if (notes.length === 0)
13024
+ return result;
13025
+ return {
13026
+ text: `${notes.map((note) => `[Request note: ${note}]`).join(`
13027
+ `)}
13028
+ ${result.text}`,
13029
+ details: { ...result.details, requestNotes: [...notes] }
13030
+ };
13031
+ }
12600
13032
  function filterList(value) {
12601
13033
  return value === undefined ? [] : Array.isArray(value) ? [...value] : [value];
12602
13034
  }
@@ -12825,7 +13257,11 @@ class SiftLightService {
12825
13257
  this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
12826
13258
  this.#evidence = new EvidenceService(this.#runRipgrep, this.#snapshots, options.structure, options.conceptSearch, options.semanticJudge);
12827
13259
  }
12828
- async search(input, cwd, signal, options = {}) {
13260
+ async search(request, cwd, signal, options = {}) {
13261
+ const { input, notes } = normalizeRequestAliases(request);
13262
+ return withRequestNotes(await this.#searchNormalized(input, cwd, signal, options), notes);
13263
+ }
13264
+ async#searchNormalized(input, cwd, signal, options) {
12829
13265
  validateRawSearchInput(input);
12830
13266
  validateRequestContract(input);
12831
13267
  if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
@@ -13297,9 +13733,9 @@ var siftLightSchema = Type.Object({
13297
13733
  description: `${fieldGuidance("anyOf")}. ${String(MIN_ANY_OF_TERMS)}-${String(MAX_ANY_OF_TOTAL_TERMS)} distinct case-sensitive single-line terms, at most ${String(MAX_LITERAL_TERM_BYTES)} UTF-8 bytes each. Requests above ${String(MAX_ANY_OF_TERMS)} terms are split into version-checked chunks and merged. Returns every retained occurrence attributed to its term.`
13298
13734
  })),
13299
13735
  allOf: Type.Optional(Type.Array(Type.String({ maxLength: MAX_PATH_CHARACTERS }), {
13300
- minItems: 2,
13736
+ minItems: 1,
13301
13737
  maxItems: 3,
13302
- description: `${fieldGuidance("allOf")}. 2-3 distinct terms must occur in one file (default) or one function.`
13738
+ description: `${fieldGuidance("allOf")}. 1-3 distinct terms must occur in one file (default) or one function; one term lists the files or functions containing it.`
13303
13739
  })),
13304
13740
  within: Type.Optional(stringEnum(["file", "function"], {
13305
13741
  description: "Only valid with allOf; omit for ordinary single-pattern searches. function requires JS/TS/TSX and counts only that implementation's own code, excluding nested callbacks, strings/comments/types. Not proof of a shared execution path."
@@ -13316,7 +13752,7 @@ var siftLightSchema = Type.Object({
13316
13752
  "unknown"
13317
13753
  ]), {
13318
13754
  minItems: 1,
13319
- 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."
13755
+ description: "Filter each single-pattern occurrence by syntax role (JS/TS/TSX/Go parsed; Python lexical: comment, string, code, declaration, import, call candidates). Roles may be candidates, especially Go call/conversion and Python calls. Cannot combine with allOf."
13320
13756
  })),
13321
13757
  changes: Type.Optional(Type.Object({
13322
13758
  base: Type.Optional(Type.String({
@@ -13349,7 +13785,7 @@ var siftLightSchema = Type.Object({
13349
13785
  paths: Type.Optional(Type.Array(Type.String(), {
13350
13786
  minItems: 1,
13351
13787
  maxItems: MAX_SELECTED_PATHS,
13352
- description: "Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests."
13788
+ description: "Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests. With mode=inspect and no cursor, opens each file from line 1 (same as targets with line 1)."
13353
13789
  })),
13354
13790
  glob: Type.Optional(Type.Union([
13355
13791
  Type.String({ maxLength: MAX_PATH_CHARACTERS }),
@@ -13373,7 +13809,7 @@ var siftLightSchema = Type.Object({
13373
13809
  })),
13374
13810
  hidden: Type.Optional(Type.Boolean({ description: "Search hidden files (default true; .git is always excluded)." })),
13375
13811
  ignorePolicy: Type.Optional(stringEnum(["respect", "include"], {
13376
- description: "respect (default) honors ignore rules and reports policy-filtered coverage when files are omitted. include searches ignored files while still excluding .git internals and protected paths."
13812
+ description: "respect (default) honors ignore rules and reports policy-filtered coverage when files are omitted, including mode=files. include searches or lists ignored files while still excluding .git internals and protected paths."
13377
13813
  })),
13378
13814
  patterns: Type.Optional(Type.Array(Type.Object({
13379
13815
  id: Type.String({ minLength: 1, maxLength: 64 }),
@@ -13418,29 +13854,29 @@ var siftLightSchema = Type.Object({
13418
13854
  limit: Type.Optional(Type.Integer({
13419
13855
  minimum: 1,
13420
13856
  maximum: MAX_PAGE_SIZE,
13421
- 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.`
13857
+ description: `${fieldGuidance("limit")}. Ordinary search: explicit detail-page match limit (max 100); mode=files: files per page (default 30). Normally omit to preserve automatic summarization; other analysis and inspect modes reject it.`
13422
13858
  })),
13423
- mode: Type.Optional(stringEnum(SIFT_LIGHT_MODES, {
13424
- description: `Ordinary search defaults to auto; summary/matches request explicit pages. capabilities returns a compact names-only project inventory and per-language supported modes without loading providers. files uses query, structure uses an AST pattern, concept uses a required natural-language query, and hybrid uses one query for exact literal plus concept evidence in a single snapshot. validate rechecks saved search or analysis sources against their recorded origin. await waits for an existing long-running concept or hybrid operation without restarting it; cancel explicitly cancels one and waits for owned cleanup. Copy the returned nextRequest exactly and do not repeat the original query. Waiting is an operation state, not evidence. Validation details report the requested scope, comparison target, coverage, and freshness as current, stale, or unknown; partial coverage is retained during validation. inspect/outline/imports/tests retain their documented location selectors. Syntax results are static evidence; concept and related-test results remain candidates. ${MODE_CONTRACT_DESCRIPTION}`
13859
+ mode: Type.Optional(stringEnum([...SIFT_LIGHT_MODES, ...MODE_ALIASES], {
13860
+ description: `Ordinary search defaults to auto; summary/matches request explicit pages. capabilities returns a compact names-only project inventory and per-language supported modes without loading providers. files uses query, structure uses an AST pattern, concept uses a required natural-language query, and hybrid uses one query for exact literal plus concept evidence in a single snapshot. validate rechecks saved search or analysis sources against their recorded origin. await waits for an existing long-running concept or hybrid operation without restarting it; cancel explicitly cancels one and waits for owned cleanup. Copy the returned nextRequest exactly and do not repeat the original query. Waiting is an operation state, not evidence. Validation details report the requested scope, comparison target, coverage, and freshness as current, stale, or unknown; partial coverage is retained during validation. inspect/outline/imports/tests retain their documented location selectors. Syntax results are static evidence; concept and related-test results remain candidates. ${MODE_CONTRACT_DESCRIPTION} anyOf/allOf are field names, not modes; as mode values they are accepted aliases for omitting mode, disclosed in a request note.`
13425
13861
  })),
13426
13862
  line: Type.Optional(Type.Number({
13427
- description: "1-indexed source line for path inspection/navigation. Omit with matchIndex, matchIndices or targets."
13863
+ description: "1-indexed source line for path inspection/navigation; path-only inspect starts at line 1. Omit with matchIndex, matchIndices or targets."
13428
13864
  })),
13429
13865
  matchIndex: Type.Optional(Type.Number({
13430
13866
  description: "1-based retained match index for cursor-scoped inspect; replaces path and line."
13431
13867
  })),
13432
13868
  matchIndices: Type.Optional(Type.Array(Type.Integer({ minimum: 1 }), {
13433
13869
  minItems: 1,
13434
- maxItems: MAX_INSPECT_TARGETS,
13435
- description: "Inspect up to five visible match numbers together using the same cursor; mutually exclusive with matchIndex, path, line and targets."
13870
+ maxItems: MAX_INSPECT_REQUEST_TARGETS,
13871
+ description: `Inspect visible match numbers together using the same cursor; mutually exclusive with matchIndex, path, line and targets. One response inspects ${String(MAX_INSPECT_TARGETS)}; the rest are returned as an exact nextRequest.`
13436
13872
  })),
13437
13873
  targets: Type.Optional(Type.Array(Type.Object({
13438
13874
  path: Type.String({ maxLength: MAX_PATH_CHARACTERS }),
13439
13875
  line: Type.Integer({ minimum: 1 })
13440
13876
  }), {
13441
13877
  minItems: 1,
13442
- maxItems: MAX_INSPECT_TARGETS,
13443
- description: "Inspect known path/line locations together without a cursor. The complete batch shares one 16 KiB response budget."
13878
+ maxItems: MAX_INSPECT_REQUEST_TARGETS,
13879
+ description: `Inspect known path/line locations together without a cursor. One response inspects ${String(MAX_INSPECT_TARGETS)} within a shared 16 KiB budget; the rest are returned as an exact nextRequest.`
13444
13880
  })),
13445
13881
  cursor: Type.Optional(Type.String({ description: "Opaque cursor from a previous stable search snapshot." })),
13446
13882
  operationId: Type.Optional(Type.String({