sift-light 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +11 -4
  3. package/README.zh-CN.md +11 -4
  4. package/package.json +1 -1
  5. package/plugins/sift-light/.claude-plugin/plugin.json +1 -1
  6. package/plugins/sift-light/.codex-plugin/plugin.json +1 -1
  7. package/plugins/sift-light/.mcp.json +1 -1
  8. package/plugins/sift-light/concept-worker.mjs +39 -12
  9. package/plugins/sift-light/kimi.plugin.json +2 -2
  10. package/plugins/sift-light/omp-extension.mjs +558 -83
  11. package/plugins/sift-light/package.json +1 -1
  12. package/src/analysis-limits.ts +1 -1
  13. package/src/analysis-store.ts +6 -2
  14. package/src/analysis-types.ts +10 -1
  15. package/src/concept-inference.ts +44 -11
  16. package/src/concept-model.ts +2 -2
  17. package/src/concept-search.ts +12 -2
  18. package/src/concept-worker.mjs +39 -12
  19. package/src/concept-worker.ts +1 -1
  20. package/src/config-reader.ts +11 -3
  21. package/src/evidence-service.ts +117 -13
  22. package/src/file-discovery.ts +111 -17
  23. package/src/hybrid-search.ts +13 -0
  24. package/src/index.ts +4 -10
  25. package/src/inspect.ts +3 -0
  26. package/src/language-capability-definitions.ts +9 -0
  27. package/src/mcp-model-output.ts +47 -1
  28. package/src/mcp-semantic-judge.ts +19 -8
  29. package/src/mcp-server.mjs +754 -247
  30. package/src/mcp-server.ts +11 -13
  31. package/src/mcp.ts +2 -0
  32. package/src/model-error.ts +6 -16
  33. package/src/omp-index.ts +4 -6
  34. package/src/prompt-guidelines.ts +5 -5
  35. package/src/python-roles.ts +247 -0
  36. package/src/redaction.ts +9 -2
  37. package/src/request-aliases.ts +48 -0
  38. package/src/request-contract-catalog.ts +24 -7
  39. package/src/request-contract.ts +60 -6
  40. package/src/request.ts +3 -1
  41. package/src/runtime.ts +2 -2
  42. package/src/semantic-judge.ts +17 -3
  43. package/src/service.ts +31 -1
  44. package/src/session-summary.ts +4 -4
  45. package/src/source-inspection.ts +27 -2
  46. package/src/tool-schema.ts +18 -18
  47. package/src/tui/layout.ts +2 -2
  48. package/src/tui/renderers.ts +2 -2
  49. package/src/types.ts +4 -0
@@ -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.0",
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",
@@ -292,7 +292,33 @@ function analysisExtraGroups(counts, termCounts, items) {
292
292
  function isRecord(value) {
293
293
  return typeof value === "object" && value !== null && !Array.isArray(value);
294
294
  }
295
+ function compactSemanticMetadata(details, analysis) {
296
+ const counts = analysis.counts;
297
+ const stats = analysis.stats;
298
+ const judge = analysis.semanticJudge;
299
+ const coverage = analysis.coverage;
300
+ return [
301
+ ...counts || stats ? [
302
+ `Search: ${String(counts?.filesAdmitted ?? stats?.filesAdmitted ?? 0)} files, ${String(stats?.passagesRanked ?? counts?.passagesQueued ?? 0)} passages; ${String(stats?.elapsedMs ?? 0)} ms; peak inference RSS ${String(stats?.inferencePeakRssBytes ?? 0)} bytes; cache ${String(stats?.conceptCacheHits ?? 0)} hits/${String(stats?.conceptCacheMisses ?? 0)} misses.`
303
+ ] : [],
304
+ ...coverage ? [
305
+ `Coverage: ${Object.entries(coverage).map(([name, status]) => `${name}=${status}`).join(", ")}.`
306
+ ] : [],
307
+ ...analysis.scope ? [`Scope: ${analysis.scope.path}; ignore=${analysis.scope.ignorePolicy}.`] : [],
308
+ ...analysis.sourceGeneration ? [
309
+ `Source: ${analysis.sourceGeneration.verification}; ${String(analysis.sourceGeneration.filesUnavailable)} unavailable.`
310
+ ] : [],
311
+ ...judge ? [
312
+ `Semantic judge: ${judge.status}; ${String(judge.judgedCandidates)}/${String(judge.candidatesConsidered)} judged, ${String(judge.candidatesUnjudged)} unjudged; batches ${String(judge.batchesCompleted)}/${String(judge.batchesAttempted)} completed; classes ${JSON.stringify(judge.classificationCounts)}${judge.reason ? `; ${judge.reason}` : ""}.`
313
+ ] : [],
314
+ ...details.operation ? [`Operation: ${details.operation.state}; id=${details.operation.id}.`] : [],
315
+ ...analysis.reasons.map((reason) => `[${reason}]`),
316
+ ...details.redactionApplied ? ["[Display redaction applied.]"] : []
317
+ ];
318
+ }
295
319
  function compactMetadata(details, analysis) {
320
+ if (analysis.kind === "concept" || analysis.kind === "hybrid")
321
+ return compactSemanticMetadata(details, analysis);
296
322
  return [
297
323
  ...analysis.statistics ? formatStatistics(analysis.statistics) : [],
298
324
  analysis.counts ? `Counts: ${JSON.stringify(analysis.counts)}` : undefined,
@@ -320,7 +346,7 @@ function compactRows(analysis) {
320
346
  rows.push(JSON.stringify(item.path));
321
347
  previousPath = item.path;
322
348
  }
323
- const label = analysis.kind === "outline" && analysis.modelOutput ? item.label : "metadata";
349
+ const label = analysis.kind === "outline" && analysis.modelOutput || analysis.kind === "files" ? item.label : "metadata";
324
350
  const semanticJudge = item.details?.semanticJudge;
325
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)}.` : "";
326
352
  rows.push(`#${String(item.index)} L${String(item.line)} ${label}${judgment}`);
@@ -360,6 +386,7 @@ function compactMcpModelText(result) {
360
386
  const inspect = compactInspectInstruction(analysis);
361
387
  const nextRequest = distinctNextRequest(result.details, analysis);
362
388
  const compact = [
389
+ ...(result.details.requestNotes ?? []).map((note) => `[Request note: ${note}]`),
363
390
  header,
364
391
  ...compactMetadata(result.details, analysis),
365
392
  ...compactRows(analysis),
@@ -391,7 +418,7 @@ var MAX_ANALYSIS_STORAGE_BYTES = 32 * 1024 * 1024;
391
418
  var ANALYSIS_METADATA_RESERVE_BYTES = 64 * 1024;
392
419
  var MAX_ANALYSIS_REASONS = 64;
393
420
  var MAX_ANALYSIS_REASON_BYTES = 4 * 1024;
394
- var MIN_ANY_OF_TERMS = 2;
421
+ var MIN_ANY_OF_TERMS = 1;
395
422
  var MAX_ANY_OF_TERMS = 8;
396
423
  var MAX_ANY_OF_TOTAL_TERMS = 64;
397
424
  var MAX_LITERAL_TERM_BYTES = 256;
@@ -411,6 +438,7 @@ var ESTIMATED_CHARACTERS_PER_TOKEN = 4;
411
438
  var DEFAULT_SUMMARY_FILE_LIMIT = 30;
412
439
  var MAX_SELECTED_PATHS = 20;
413
440
  var MAX_INSPECT_TARGETS = 5;
441
+ var MAX_INSPECT_REQUEST_TARGETS = 20;
414
442
  var MAX_DISPLAYED_OCCURRENCES = 20;
415
443
  var MAX_STORED_MATCHES = 50000;
416
444
  var MAX_STORED_OCCURRENCES = 200000;
@@ -432,6 +460,7 @@ var PRIVATE_KEY = /-----BEGIN ([^-\r\n]*PRIVATE KEY)-----[\s\S]*?-----END \1----
432
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]+)*)`;
433
461
  var SENSITIVE_ASSIGNMENT = new RegExp(String.raw`((?<![A-Za-z0-9_-])(?:["']?${SENSITIVE_NAME}["']?)\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}\r\n]+)`, "gi");
434
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;
435
464
  var TYPE_ONLY_VALUES = new Set([
436
465
  "boolean",
437
466
  "number",
@@ -451,10 +480,11 @@ function redactString(value) {
451
480
  });
452
481
  redacted = redacted.replace(SENSITIVE_ASSIGNMENT, (match, prefix, rawValue) => {
453
482
  const unquoted = rawValue.replace(/^["']|["']$/g, "").toLowerCase();
454
- if (TYPE_ONLY_VALUES.has(unquoted))
483
+ if (TYPE_ONLY_VALUES.has(unquoted) || CODE_EXPRESSION.test(rawValue))
455
484
  return match;
456
485
  count += 1;
457
- return `${prefix}"[REDACTED]"`;
486
+ const quote = rawValue.startsWith('"') || rawValue.startsWith("'") ? rawValue[0] : "";
487
+ return `${prefix}${quote}[REDACTED]${quote}`;
458
488
  });
459
489
  redacted = redacted.replace(SENSITIVE_TOKEN, () => {
460
490
  count += 1;
@@ -712,6 +742,15 @@ var PYTHON_LANGUAGE_CAPABILITIES = [
712
742
  evidence: "syntax",
713
743
  availability: "implemented",
714
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"
715
754
  }
716
755
  ];
717
756
  var DEFAULT_LANGUAGE_CAPABILITIES = [
@@ -839,7 +878,17 @@ var MODE_FIELDS_BY_MODE = {
839
878
  auto: ordinaryFields,
840
879
  summary: ordinaryFields,
841
880
  matches: ordinaryFields,
842
- 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
+ ],
843
892
  outline: [...commonFields, "path", "line", "symbol", "cursor", "matchIndex", "maxFilesToParse"],
844
893
  imports: [
845
894
  ...commonFields,
@@ -859,7 +908,16 @@ var MODE_FIELDS_BY_MODE = {
859
908
  "matchIndex",
860
909
  "maxFilesToParse"
861
910
  ],
862
- 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
+ ],
863
921
  structure: [...commonFields, "pattern", ...sourceFilters, "maxFilesToParse"],
864
922
  concept: [...commonFields, "query", ...sourceFilters, "maxFilesToParse"],
865
923
  hybrid: [...commonFields, "query", ...sourceFilters, "conceptLimit", "maxFilesToParse"],
@@ -868,9 +926,7 @@ var MODE_FIELDS_BY_MODE = {
868
926
  await: ["mode", "operationId"],
869
927
  cancel: ["mode", "operationId"]
870
928
  };
871
- var SAFE_DROP_FIELDS = {
872
- files: ["scope"]
873
- };
929
+ var SAFE_DROP_FIELDS = {};
874
930
  var SUPPORTED_OUTLINE_EXTENSIONS = new Set(DEFAULT_LANGUAGE_CAPABILITIES.flatMap((descriptor) => descriptor.capabilities.some((capability) => capability.name === "outline") ? descriptor.extensions : []));
875
931
  function modeFields(mode) {
876
932
  return MODE_FIELDS_BY_MODE[mode];
@@ -899,8 +955,8 @@ var REQUEST_FIELD_GUIDANCE = {
899
955
  allOf: "allOf is case-sensitive exact-literal AND; omit pattern, anyOf, literal, ignoreCase, roles",
900
956
  context: "context is an output-context budget and is never silently dropped",
901
957
  limit: "limit is an output/page budget and is never silently dropped",
902
- 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",
903
- 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",
904
960
  patterns: "audit accepts named exact-literal patterns and returns one closure receipt",
905
961
  maxFilesToParse: "concept/hybrid automatically batch the requested scope; this optional field sets an advanced hard file ceiling"
906
962
  };
@@ -996,15 +1052,26 @@ function schemaError(input, field, reason) {
996
1052
  recovery: { action: "manual", reason }
997
1053
  }, `${field} is invalid: ${reason}`);
998
1054
  }
1055
+ function plainFilePatternRecovery(mode, input, invalid) {
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);
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
+ }
999
1061
  function safeNextRequest(input, mode, invalid) {
1000
1062
  if (input.redact === true && containsSensitiveText(input))
1001
1063
  return;
1002
1064
  const safe = new Set(SAFE_DROP_FIELDS[mode] ?? []);
1065
+ const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
1066
+ if (plainFilePattern || matchAllFilePatternRecovery(mode, input, invalid))
1067
+ safe.add("pattern");
1003
1068
  if (invalid.some((field) => !safe.has(field)))
1004
1069
  return;
1005
1070
  const next = { ...input };
1006
1071
  for (const field of invalid)
1007
1072
  delete next[field];
1073
+ if (plainFilePattern)
1074
+ next.query = input.pattern;
1008
1075
  if (selectorIssuesFor(next, mode).length > 0)
1009
1076
  return;
1010
1077
  try {
@@ -1033,20 +1100,26 @@ function fieldsError(input, mode, invalid, selectorIssues = []) {
1033
1100
  });
1034
1101
  const visibleFields = visibleInvalid.map((field) => boundedField(field)).join(", ");
1035
1102
  const reason = omitted > 0 ? `mode=${mode} does not accept ${visibleFields} and ${String(omitted)} additional field(s)` : `mode=${mode} does not accept ${visibleFields}`;
1036
- const flatRule = `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.`;
1103
+ const nestedModeObject = invalid.includes(mode);
1104
+ const flatRule = nestedModeObject ? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.` : "";
1105
+ const filesPatternRule = mode === "files" && invalid.includes("pattern") ? "For filename or path discovery, put the literal name text in query; pattern is a content-search regex, so check any regex syntax before copying it." : "";
1037
1106
  return new RequestContractError({
1038
1107
  code: "E_MODE_FIELDS",
1039
1108
  mode,
1040
1109
  issues,
1041
1110
  recovery: nextRequest ? {
1042
1111
  action: "retry",
1043
- reason: `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.`,
1044
1113
  nextRequest
1045
1114
  } : {
1046
1115
  action: "manual",
1047
- reason: `${reason}; choose the mode explicitly or remove the fields yourself without changing the requested scope. ${flatRule}`
1116
+ reason: [
1117
+ reason,
1118
+ filesPatternRule || "Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
1119
+ flatRule
1120
+ ].filter(Boolean).join(" ")
1048
1121
  }
1049
- }, nextRequest ? `${reason}; retry the exact nextRequest without repeating the original query.` : `${reason}; no semantics-preserving automatic request is available. ${flatRule} Preserve valid path, filters, redact and cursor fields when choosing the next request.`);
1122
+ }, nextRequest ? `${reason}; ${plainFilePatternRecovery(mode, input, invalid) ? "move plain filename text to query" : "retry the exact nextRequest"}.` : `${reason}; no semantics-preserving automatic request is available. ${filesPatternRule || flatRule || "Check the mode's accepted fields."}`);
1050
1123
  }
1051
1124
  function selectorIssuesFor(input, mode) {
1052
1125
  if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof input.cursor === "string" && input.cursor.trim().length > 0 && !input.cursor.includes(".analysis")) {
@@ -1192,7 +1265,7 @@ function validateRequestContract(input) {
1192
1265
  const allowed = new Set(MODE_FIELDS_BY_MODE[mode]);
1193
1266
  if (mode === "inspect" && raw.sourceCursor !== undefined) {
1194
1267
  allowed.clear();
1195
- for (const field of ["mode", "sourceCursor", "redact"])
1268
+ for (const field of ["mode", "sourceCursor", "redact", "path"])
1196
1269
  allowed.add(field);
1197
1270
  }
1198
1271
  if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof raw.cursor === "string" && raw.cursor.includes(".analysis")) {
@@ -1282,25 +1355,23 @@ function modelErrorText(error) {
1282
1355
  function requestContractErrorText(error) {
1283
1356
  return requestContractProjection(error).text;
1284
1357
  }
1285
- function projectRequestContract(projected, serialized, message) {
1286
- const prefix = `sift-light failed: request rejected [${projected.code}]: ${message}`;
1358
+ function projectRequestContract(projected, serialized) {
1287
1359
  const recovery = projected.recovery;
1288
1360
  const next = recovery.nextRequest ? `
1289
1361
  Copy the nested recovery.nextRequest object unchanged; do not repeat the original query.` : `
1290
- Recovery action: ${recovery.action}. ${recovery.reason}`;
1362
+ Recovery action: ${recovery.action}.`;
1291
1363
  return `
1292
- Error details: ${serialized}
1293
- ${prefix}${next}`;
1364
+ sift-light failed: request rejected [${projected.code}].
1365
+ Error details: ${serialized}${next}`;
1294
1366
  }
1295
1367
  function requestContractProjection(error) {
1296
1368
  const details = boundedRequestContractDetails(error.details);
1297
1369
  const serializedDetails = JSON.stringify(details);
1298
- const boundedMessage = error.message.toWellFormed().slice(0, 1024);
1299
- const result = projectRequestContract(details, serializedDetails, boundedMessage);
1370
+ const result = projectRequestContract(details, serializedDetails);
1300
1371
  if (Buffer.byteLength(result) <= MAX_REQUEST_RECOVERY_BYTES)
1301
1372
  return { details, text: result };
1302
1373
  const compact = {
1303
- code: boundedMessage.length > 0 ? details.code : "E_REQUEST_CONTRACT_PAYLOAD",
1374
+ code: details.code,
1304
1375
  ...details.mode ? { mode: details.mode } : {},
1305
1376
  issues: [
1306
1377
  {
@@ -1313,7 +1384,7 @@ function requestContractProjection(error) {
1313
1384
  reason: "Exact recovery was omitted; correct the request explicitly."
1314
1385
  }
1315
1386
  };
1316
- const compactText = projectRequestContract(compact, JSON.stringify(compact), "The request-contract error exceeded the bounded payload budget; correct the request explicitly.");
1387
+ const compactText = projectRequestContract(compact, JSON.stringify(compact));
1317
1388
  return { details: compact, text: compactText };
1318
1389
  }
1319
1390
 
@@ -2819,7 +2890,7 @@ var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
2819
2890
  var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
2820
2891
  var MAX_CONCEPT_CHARS = 1000;
2821
2892
  var CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
2822
- var CONCEPT_CACHE_VERSION = 1;
2893
+ var CONCEPT_CACHE_VERSION = 2;
2823
2894
  var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
2824
2895
  var CONCEPT_TIMEOUT_MS = 10 * 60000;
2825
2896
  var MIN_CONCEPT_TIMEOUT_MS = 1000;
@@ -2910,7 +2981,7 @@ function normalizeRequest(input) {
2910
2981
  throw new SiftLightError("ignorePolicy must be respect or include");
2911
2982
  const pattern = input.pattern;
2912
2983
  if (pattern === undefined) {
2913
- 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');
2914
2985
  }
2915
2986
  const path = input.path?.replace(/^@/, "");
2916
2987
  return {
@@ -5142,6 +5213,11 @@ function scoreProfile(scores) {
5142
5213
  spread: top - min
5143
5214
  };
5144
5215
  }
5216
+ function conceptRankingScore(cosine, passageLength) {
5217
+ const referenceLength = 500;
5218
+ const maximumCorrection = 0.02;
5219
+ return cosine - maximumCorrection * (1 - Math.sqrt(Math.min(passageLength / referenceLength, 1)));
5220
+ }
5145
5221
  function passage(document, start) {
5146
5222
  let end = Math.min(document.text.length, start + MAX_CONCEPT_CHARS);
5147
5223
  if (end < document.text.length) {
@@ -5400,6 +5476,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
5400
5476
  const similarity = inferred.scores[index];
5401
5477
  if (similarity === undefined)
5402
5478
  throw new Error("Missing concept similarity");
5479
+ const rankingScore = conceptRankingScore(similarity, item.text.length);
5403
5480
  const evidence = rangeEvidence(item.document, item.range);
5404
5481
  return {
5405
5482
  path: item.document.path,
@@ -5412,7 +5489,8 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
5412
5489
  kind: "concept-candidate",
5413
5490
  certainty: "candidate",
5414
5491
  score: similarity,
5415
- rankingReason: "local multilingual E5 cosine similarity; relevance candidate, no binding or execution claim",
5492
+ rankingScore,
5493
+ rankingReason: "local multilingual E5 cosine similarity with bounded short-passage rank correction; relevance candidate, no binding or execution claim",
5416
5494
  model: CONCEPT_MODEL,
5417
5495
  revision: CONCEPT_REVISION,
5418
5496
  tokenTruncated: false,
@@ -5421,7 +5499,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
5421
5499
  }
5422
5500
  };
5423
5501
  });
5424
- const ranked = [...result.items, ...batchItems].toSorted((a, b) => Number(b.details?.score) - Number(a.details?.score) || a.path.localeCompare(b.path) || a.line - b.line);
5502
+ const ranked = [...result.items, ...batchItems].toSorted((a, b) => Number(b.details?.rankingScore) - Number(a.details?.rankingScore) || a.path.localeCompare(b.path) || a.line - b.line);
5425
5503
  if (ranked.length > MAX_ANALYSIS_RESULTS)
5426
5504
  retentionTruncated = true;
5427
5505
  result.items = ranked.slice(0, MAX_ANALYSIS_RESULTS);
@@ -5827,7 +5905,7 @@ function publicAnalysisItem(result, item, index, storedId, modelOutput) {
5827
5905
  return {
5828
5906
  path: item.path,
5829
5907
  line: item.line,
5830
- label: result.kind === "outline" && modelOutput ? item.label : publicAnalysisLabel(result),
5908
+ label: result.kind === "outline" && modelOutput || result.kind === "files" ? item.label : publicAnalysisLabel(result),
5831
5909
  index: index + 1,
5832
5910
  ...inspect ? { inspect } : {},
5833
5911
  ...publicDetails ? { details: publicDetails } : {}
@@ -6038,7 +6116,8 @@ Inspect: ${JSON.stringify(inspect)}` : ""}`;
6038
6116
  break;
6039
6117
  }
6040
6118
  } else {
6041
- 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) {
6042
6121
  if (!appendItem(index))
6043
6122
  break;
6044
6123
  }
@@ -6152,6 +6231,8 @@ function resolveInspectionTarget(input, cwd, snapshots) {
6152
6231
  }
6153
6232
  if (!path)
6154
6233
  throw new SiftLightError("path is required when mode=inspect");
6234
+ if (line === undefined && input.cursor === undefined)
6235
+ line = 1;
6155
6236
  if (line === undefined || !Number.isSafeInteger(line) || line < 1) {
6156
6237
  throw new SiftLightError("line must be a positive integer when mode=inspect");
6157
6238
  }
@@ -7880,6 +7961,28 @@ async function filterPathsByModificationTime(cwd, paths, modifiedAfterMs, modifi
7880
7961
  // src/file-discovery.ts
7881
7962
  var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
7882
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
+ }
7883
7986
  function subsequenceScore(text, query) {
7884
7987
  const characters = Array.from(graphemes.segment(text), (item) => item.segment);
7885
7988
  const queryCharacters = Array.from(graphemes.segment(query), (item) => item.segment);
@@ -7930,39 +8033,79 @@ function pathRelativeToDiscoveryRoot(cwd, root, path) {
7930
8033
  return scoped || platformBasename(absolutePath);
7931
8034
  }
7932
8035
  async function discoverFiles(input, cwd, signal) {
7933
- const query = input.query ?? "";
7934
- 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))
7935
8038
  throw new SiftLightError("File query must be well-formed single-line text of at most 256 characters");
7936
- if (FILE_QUERY_GLOB_WILDCARD.test(query))
7937
- 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.`);
7938
- 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
+ });
7939
8047
  const policy = new SearchPathPolicy(cwd);
7940
8048
  const discoveryRoot = request.path ?? ".";
7941
8049
  const scoringRoot = await policy.resolveSearchTarget(discoveryRoot);
7942
- const files = await listWorkspaceFiles(cwd, signal, {
8050
+ const includeIgnored = request.ignorePolicy === "include";
8051
+ const listOptions = {
7943
8052
  path: scoringRoot,
7944
8053
  glob: request.glob,
7945
8054
  exclude: request.exclude,
7946
8055
  hidden: request.hidden
8056
+ };
8057
+ const files = await listWorkspaceFiles(cwd, signal, {
8058
+ ...listOptions,
8059
+ ...includeIgnored ? { ignore: false, ignoreParents: false } : {}
7947
8060
  });
7948
8061
  const filtered = await filterPathsByModificationTime(cwd, files.paths, request.modifiedAfterMs, request.modifiedBeforeMs, signal);
8062
+ const rank = (path) => scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
7949
8063
  const selected = filtered.paths.flatMap((path) => {
7950
- const rank = scoreFilePath(pathRelativeToDiscoveryRoot(cwd, scoringRoot, path), query);
7951
- return rank ? [{ path, ...rank }] : [];
8064
+ const score = rank(path);
8065
+ return score ? [{ path, ...score }] : [];
7952
8066
  }).toSorted((left, right) => right.score - left.score || left.path.localeCompare(right.path));
7953
8067
  for (let offset = 0;offset < selected.length; offset += 16) {
7954
8068
  await Promise.all(selected.slice(offset, offset + 16).map((item) => policy.assertExistingPath(item.path)));
7955
8069
  signal?.throwIfAborted();
7956
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;
7957
8100
  return {
7958
8101
  kind: "files",
7959
8102
  unit: "files",
7960
- partial: files.partial || filtered.partial,
7961
- reasons: [...new Set([...files.reasons, ...filtered.reasons])],
8103
+ partial: enumerationPartial,
8104
+ reasons: [...reasons],
7962
8105
  items: selected.map((item) => ({
7963
8106
  path: item.path,
7964
8107
  line: 1,
7965
- label: `File candidate (${item.reason})`,
8108
+ label: `File candidate (score ${String(item.score)}: ${item.reason})`,
7966
8109
  details: {
7967
8110
  kind: "file",
7968
8111
  score: item.score,
@@ -7970,8 +8113,13 @@ async function discoverFiles(input, cwd, signal) {
7970
8113
  inspect: { mode: "inspect", path: item.path, line: 1 }
7971
8114
  }
7972
8115
  })),
7973
- coverage: { fileEnumeration: files.partial || filtered.partial ? "partial" : "complete" },
7974
- 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
+ },
7975
8123
  scope: {
7976
8124
  path: request.path ?? ".",
7977
8125
  requestedPath: request.path ?? ".",
@@ -7984,7 +8132,8 @@ async function discoverFiles(input, cwd, signal) {
7984
8132
  ...request.modifiedAfterMs !== undefined ? { modifiedAfterMs: request.modifiedAfterMs } : {},
7985
8133
  ...request.modifiedBeforeMs !== undefined ? { modifiedBeforeMs: request.modifiedBeforeMs } : {}
7986
8134
  },
7987
- redact: input.redact ?? false
8135
+ redact: input.redact ?? false,
8136
+ ...input.limit !== undefined ? { pageSize: request.pageSize } : {}
7988
8137
  };
7989
8138
  }
7990
8139
 
@@ -8225,7 +8374,19 @@ async function prepare(target, access, structure) {
8225
8374
  let range = target.range;
8226
8375
  let details = { status: "no-symbol" };
8227
8376
  const language = syntaxLanguage(document.path);
8228
- 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") {
8229
8390
  const syntax = await access.syntax(document);
8230
8391
  details = {
8231
8392
  status: syntax.status === "ok" ? "no-symbol" : syntax.status === "unsupported" ? "provider-unavailable" : "parse-error",
@@ -8277,7 +8438,7 @@ async function prepare(target, access, structure) {
8277
8438
  details = { status: "provider-unavailable", ...language ? { language } : {} };
8278
8439
  }
8279
8440
  range ??= document.lineRange(Math.max(1, target.line - 10), Math.min(document.lineStarts.length, target.line + 10));
8280
- 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";
8281
8442
  document.checkRange(range);
8282
8443
  return { target, document, range, structure: details, boundary, focus };
8283
8444
  }
@@ -8636,8 +8797,10 @@ ${preview.text}`);
8636
8797
  }
8637
8798
  };
8638
8799
  }
8639
- async function continueSource(cursor, access, continuations) {
8800
+ async function continueSource(cursor, access, continuations, expectedPath) {
8640
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`);
8641
8804
  const document = await access.load(state.source.path, state.source);
8642
8805
  const page = sourcePage(document, state.remaining, MAX_RESULT_BYTES - 1400);
8643
8806
  const next = continuations.advance(cursor, page.fragment);
@@ -9514,9 +9677,378 @@ function parsePythonOutline(document) {
9514
9677
  });
9515
9678
  }
9516
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
+
9517
9895
  // src/hybrid-search.ts
9518
9896
  import { resolve as resolve20 } from "node:path";
9519
9897
 
9898
+ // src/config-reader.ts
9899
+ import { readFile as readFile2 } from "node:fs/promises";
9900
+ var SIFT_LIGHT_CONFIG_ENV = "SIFT_LIGHT_CONFIG";
9901
+ var SEMANTIC_JUDGE_API_KEY_ENVS = ["TYPESAFE_API_KEY", "SIFT_LIGHT_JEV_API_KEY"];
9902
+ var DEFAULT_SEMANTIC_JUDGE_CONFIG = {
9903
+ enabled: false,
9904
+ provider: "jev",
9905
+ endpoint: "https://api.typesafe.ai/v1/systemone",
9906
+ apiKeyEnv: SEMANTIC_JUDGE_API_KEY_ENVS[0],
9907
+ model: "jev-latest",
9908
+ timeoutMs: 120000,
9909
+ maxCandidates: 20,
9910
+ maxRetries: 2
9911
+ };
9912
+ var DEFAULT_SIFT_LIGHT_CONFIG = {
9913
+ locale: "en",
9914
+ enforceSearch: "hard",
9915
+ vectorSearchEnabled: false,
9916
+ semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG
9917
+ };
9918
+ function hasErrorCode(error, codes) {
9919
+ return error instanceof Error && "code" in error && codes.includes(String(error.code));
9920
+ }
9921
+ function isMissingFile(error) {
9922
+ return hasErrorCode(error, ["ENOENT"]);
9923
+ }
9924
+ function isRawSiftLightConfig(value) {
9925
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9926
+ }
9927
+ function isRawSemanticJudgeConfig(value) {
9928
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9929
+ }
9930
+ function boundedInteger2(value, fallback, minimum, maximum, field) {
9931
+ const candidate = value ?? fallback;
9932
+ if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < minimum || candidate > maximum) {
9933
+ throw new Error(`Invalid sift-light ${field}: expected an integer from ${String(minimum)} through ${String(maximum)}`);
9934
+ }
9935
+ return candidate;
9936
+ }
9937
+ function parseSemanticJudge(value, path) {
9938
+ if (value === undefined)
9939
+ return { ...DEFAULT_SEMANTIC_JUDGE_CONFIG };
9940
+ if (!isRawSemanticJudgeConfig(value)) {
9941
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge must be an object`);
9942
+ }
9943
+ const unknown = Object.keys(value).filter((key) => ![
9944
+ "enabled",
9945
+ "provider",
9946
+ "endpoint",
9947
+ "apiKeyEnv",
9948
+ "model",
9949
+ "timeoutMs",
9950
+ "maxCandidates",
9951
+ "maxRetries"
9952
+ ].includes(key));
9953
+ if (unknown.length > 0) {
9954
+ throw new Error(`Invalid sift-light config at ${path}: unsupported semanticJudge fields; accepted fields are enabled, provider, endpoint, apiKeyEnv, model, timeoutMs, maxCandidates and maxRetries`);
9955
+ }
9956
+ const enabled = value.enabled ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.enabled;
9957
+ if (typeof enabled !== "boolean") {
9958
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.enabled must be a boolean`);
9959
+ }
9960
+ const provider = value.provider ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.provider;
9961
+ if (provider !== "jev") {
9962
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.provider must be "jev"`);
9963
+ }
9964
+ const endpoint = value.endpoint ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.endpoint;
9965
+ if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.length > 2048) {
9966
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a nonempty URL`);
9967
+ }
9968
+ let parsedEndpoint;
9969
+ try {
9970
+ parsedEndpoint = new URL(endpoint);
9971
+ } catch (error) {
9972
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a URL`, {
9973
+ cause: error
9974
+ });
9975
+ }
9976
+ const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
9977
+ const secureEndpoint = parsedEndpoint.protocol === "https:" || parsedEndpoint.protocol === "http:" && loopbackHosts.has(parsedEndpoint.hostname);
9978
+ if (!secureEndpoint || parsedEndpoint.username || parsedEndpoint.password) {
9979
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must use HTTPS, except that HTTP is allowed for localhost loopback development; URL credentials are not allowed`);
9980
+ }
9981
+ const apiKeyEnv = value.apiKeyEnv ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.apiKeyEnv;
9982
+ if (typeof apiKeyEnv !== "string" || !/^[A-Z][A-Z0-9_]{0,127}$/u.test(apiKeyEnv)) {
9983
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.apiKeyEnv must be an uppercase environment variable name`);
9984
+ }
9985
+ const model = value.model ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.model;
9986
+ if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
9987
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.model must be bounded single-line text`);
9988
+ }
9989
+ return {
9990
+ enabled,
9991
+ provider,
9992
+ endpoint,
9993
+ apiKeyEnv,
9994
+ model,
9995
+ timeoutMs: boundedInteger2(value.timeoutMs, DEFAULT_SEMANTIC_JUDGE_CONFIG.timeoutMs, 1000, 1200000, `config at ${path}: semanticJudge.timeoutMs`),
9996
+ maxCandidates: boundedInteger2(value.maxCandidates, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxCandidates, 1, 20, `config at ${path}: semanticJudge.maxCandidates`),
9997
+ maxRetries: boundedInteger2(value.maxRetries, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxRetries, 0, 5, `config at ${path}: semanticJudge.maxRetries`)
9998
+ };
9999
+ }
10000
+ function parseConfig(value, path) {
10001
+ if (!isRawSiftLightConfig(value)) {
10002
+ throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
10003
+ }
10004
+ const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "vectorSearchEnabled", "semanticJudge"].includes(key));
10005
+ if (unknown.length > 0) {
10006
+ throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled and semanticJudge are accepted`);
10007
+ }
10008
+ const { locale, enforceSearch, vectorSearchEnabled } = value;
10009
+ if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
10010
+ throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
10011
+ }
10012
+ if (vectorSearchEnabled !== undefined && typeof vectorSearchEnabled !== "boolean") {
10013
+ throw new Error(`Invalid sift-light config at ${path}: vectorSearchEnabled must be a boolean`);
10014
+ }
10015
+ const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
10016
+ return {
10017
+ locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
10018
+ enforceSearch: enforcement,
10019
+ vectorSearchEnabled: vectorSearchEnabled ?? DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled ?? false,
10020
+ semanticJudge: parseSemanticJudge(value.semanticJudge, path)
10021
+ };
10022
+ }
10023
+ function normalizeSearchEnforcement(value, source) {
10024
+ if (value === undefined || value === "hard")
10025
+ return "hard";
10026
+ if (value === "prefer")
10027
+ return "prefer";
10028
+ if (value === "off")
10029
+ return "off";
10030
+ throw new Error(`Invalid sift-light ${source}: enforceSearch must be "hard", "prefer", or "off"`);
10031
+ }
10032
+ async function readSiftLightConfigFile(path, options = {}) {
10033
+ try {
10034
+ const content = await readFile2(path, "utf8");
10035
+ return parseConfig(JSON.parse(content), path);
10036
+ } catch (error) {
10037
+ if (isMissingFile(error)) {
10038
+ if (options.missing === "error") {
10039
+ throw new Error(`sift-light config was not found at ${path}; create it or unset ${SIFT_LIGHT_CONFIG_ENV}`, { cause: error });
10040
+ }
10041
+ return { ...DEFAULT_SIFT_LIGHT_CONFIG };
10042
+ }
10043
+ if (error instanceof SyntaxError) {
10044
+ throw new Error(`Invalid sift-light config at ${path}: ${error.message}`, {
10045
+ cause: error
10046
+ });
10047
+ }
10048
+ throw error;
10049
+ }
10050
+ }
10051
+
9520
10052
  // src/semantic-judge-batches.ts
9521
10053
  var MAX_SEMANTIC_JUDGE_BATCH_CANDIDATES = 8;
9522
10054
  var MAX_SEMANTIC_JUDGE_REQUEST_BYTES = 64 * 1024;
@@ -9664,7 +10196,7 @@ function requestBody(query, candidates, model) {
9664
10196
  for (const candidate of candidates) {
9665
10197
  questions[candidate.id] = {
9666
10198
  type: "choice",
9667
- instructions: "Classify the candidate by what it actually does for the requested behavior. Judge the code excerpt, not just matching words.",
10199
+ instructions: `Classify only candidate ${candidate.id} in state.candidates for state.query. Judge that candidate's excerpt by its actual behavior, not by matching words or the other candidates.`,
9668
10200
  criteria: {
9669
10201
  "implementation-candidate": "The excerpt appears to implement the requested behavior or its core decision/side effect.",
9670
10202
  "caller-candidate": "The excerpt invokes or wires an implementation but does not implement the behavior itself.",
@@ -9827,6 +10359,10 @@ function createSemanticJudgeIntegration(config, environment = process.env, fetch
9827
10359
  }
9828
10360
  return { config, runner: createJevRunner(config, key, fetcher) };
9829
10361
  }
10362
+ function createConfiguredSemanticJudgeIntegration(config, environment = process.env) {
10363
+ const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
10364
+ return config.vectorSearchEnabled === true ? createSemanticJudgeIntegration(judge, environment) : createDisabledSemanticJudgeIntegration(judge);
10365
+ }
9830
10366
  function baseDetails(config) {
9831
10367
  return {
9832
10368
  enabled: config.enabled,
@@ -10133,6 +10669,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
10133
10669
  const deduplicationCoverage = scan.snapshotComplete && literal.sourceCoverage === "complete" && conceptSourceCoverage === "complete" ? "complete" : "partial";
10134
10670
  const partial = !scan.snapshotComplete || concept.partial || conceptCoverage === "skipped" || conceptSourceCoverage === "partial" || literal.sourceCoverage === "partial" || deduplicationCoverage === "partial" || judgedConcept.semanticJudge?.status === "failed" || judgedConcept.semanticJudge?.status === "partial";
10135
10671
  const selectionReason = conceptCandidatesOmitted ? `Hybrid concept limit retained the top ${String(selectedConcept.length)} of ${String(eligibleConcept.length)} non-overlapping semantic candidates` : undefined;
10672
+ const firstScore = Number(eligibleConcept[0]?.details?.rankingScore ?? eligibleConcept[0]?.details?.score);
10673
+ const secondScore = Number(eligibleConcept[1]?.details?.rankingScore ?? eligibleConcept[1]?.details?.score);
10674
+ const closeRanking = Number.isFinite(firstScore) && Number.isFinite(secondScore) && firstScore - secondScore < 0.01;
10136
10675
  return {
10137
10676
  kind: "hybrid",
10138
10677
  unit: "evidence-items",
@@ -10144,6 +10683,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
10144
10683
  ...literal.reasons,
10145
10684
  ...execution.sourceGeneration.reasons,
10146
10685
  ...selectionReason ? [selectionReason] : [],
10686
+ ...closeRanking ? [
10687
+ "Semantic ranks are close; verify the leading candidates with source inspection or literal terms."
10688
+ ] : [],
10147
10689
  ...judgedConcept.semanticJudge?.reason ? [judgedConcept.semanticJudge.reason] : []
10148
10690
  ],
10149
10691
  filesRead: (concept.filesRead ?? 0) + access.filesRead,
@@ -10297,6 +10839,69 @@ import { realpath as realpath6 } from "node:fs/promises";
10297
10839
  function isEvidenceRequest(input) {
10298
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);
10299
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
+ }
10300
10905
  function maxFilesToParse(value, defaultValue = MAX_STRUCTURE_FILES) {
10301
10906
  const candidate = value ?? defaultValue;
10302
10907
  if (!Number.isSafeInteger(candidate) || candidate < 1 || candidate > MAX_CONFIGURABLE_STRUCTURE_FILES) {
@@ -10312,8 +10917,8 @@ function validateTerms(input) {
10312
10917
  }
10313
10918
  return;
10314
10919
  }
10315
- 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)
10316
- 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");
10317
10922
  if (input.pattern !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
10318
10923
  throw new SiftLightError("allOf is an explicit case-sensitive literal conjunction; omit pattern, roles, literal and ignoreCase");
10319
10924
  if (input.within !== undefined && input.within !== "file" && input.within !== "function")
@@ -10511,11 +11116,13 @@ class EvidenceService {
10511
11116
  throw new CursorError("A nonempty sourceCursor is required");
10512
11117
  if (input.mode !== "inspect")
10513
11118
  throw new SiftLightError("sourceCursor requires mode=inspect");
10514
- return continueSource(input.sourceCursor, access, this.#continuations);
11119
+ return continueSource(input.sourceCursor, access, this.#continuations, input.path);
10515
11120
  }
10516
11121
  if (input.mode === "inspect") {
10517
- const targets = this.#inspectionTargets(input, cwd);
10518
- 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;
10519
11126
  }
10520
11127
  if (input.mode === "concept") {
10521
11128
  const execution = await this.#conceptSearch(input, access, options.onProgress);
@@ -10604,7 +11211,7 @@ class EvidenceService {
10604
11211
  if (input.pattern !== undefined || input.allOf !== undefined || input.within !== undefined || input.roles !== undefined || input.literal !== undefined || input.ignoreCase !== undefined || input.wholeWord !== undefined)
10605
11212
  throw new SiftLightError("anyOf is an explicit case-sensitive literal union; omit pattern, allOf, within, roles, literal and ignoreCase");
10606
11213
  if (input.mode !== undefined && input.mode !== "auto" && input.mode !== "matches")
10607
- throw new SiftLightError("anyOf mode must be omitted, auto, or matches");
11214
+ throw new SiftLightError("anyOf mode must be omitted, auto, matches or summary");
10608
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));
10609
11216
  const { path: _inputPath, ...unscopedInput } = input;
10610
11217
  let chunkAccess = access;
@@ -10742,9 +11349,10 @@ class EvidenceService {
10742
11349
  if (item)
10743
11350
  result.items.push(item);
10744
11351
  } else if (terms || input.roles) {
10745
- if (syntaxLanguage(file.document.path))
11352
+ const pythonRoles = !terms && /\.py$/iu.test(file.document.path);
11353
+ if (pythonRoles || syntaxLanguage(file.document.path))
10746
11354
  syntaxCapableFiles += 1;
10747
- const syntax = await access.syntax(file.document);
11355
+ const syntax = pythonRoles ? pythonRoleAnalysis(file.document) : await access.syntax(file.document);
10748
11356
  const classified = terms ? findFunctionConjunctions(file.document, syntax, terms, input.changes?.scope === "lines" ? file.changedRanges : undefined) : filterRoleOccurrences(file.document, syntax, file.occurrences, input.roles ?? []);
10749
11357
  result.items.push(...classified.items);
10750
11358
  result.partial ||= classified.partial;
@@ -10821,12 +11429,12 @@ class EvidenceService {
10821
11429
  const item = this.#analyses.item(input.cursor, input.matchIndex);
10822
11430
  if (!item.source || !item.range)
10823
11431
  throw new CursorError("This analysis item has no verified source range");
10824
- const metadataOnly = item.details?.kind === "symbol" || item.details?.kind === "function";
11432
+ const bounded = item.details?.kind === "symbol" || item.details?.kind === "function";
10825
11433
  return {
10826
11434
  path: item.path,
10827
11435
  line: item.line,
10828
11436
  reference: item.source,
10829
- ...metadataOnly ? { range: item.range } : { absoluteFocus: item.range.start }
11437
+ ...bounded ? { range: item.range, structure: analysisItemStructure(item) } : { absoluteFocus: item.range.start }
10830
11438
  };
10831
11439
  }
10832
11440
  return {
@@ -11035,8 +11643,31 @@ import { resolve as resolve24 } from "node:path";
11035
11643
  // src/discovery-errors.ts
11036
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>"}';
11037
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
+
11038
11669
  // src/format.ts
11039
- import { readFile as readFile2 } from "node:fs/promises";
11670
+ import { readFile as readFile3 } from "node:fs/promises";
11040
11671
  var RESULT_METADATA_RESERVE_BYTES = 1024;
11041
11672
  var RESULT_METADATA_RESERVE_CHARACTERS = 512;
11042
11673
 
@@ -11170,7 +11801,7 @@ async function loadContextLines(match, expectedRevision, cache, signal) {
11170
11801
  cache.set(match.absolutePath, changed);
11171
11802
  return changed;
11172
11803
  }
11173
- const content = await readFile2(match.absolutePath, { encoding: "utf8", signal });
11804
+ const content = await readFile3(match.absolutePath, { encoding: "utf8", signal });
11174
11805
  const afterRevision = await getSourceRevision(match.absolutePath);
11175
11806
  if (!afterRevision || !sameSourceRevision(expectedRevision, afterRevision)) {
11176
11807
  const changed = { status: "changed" };
@@ -12388,6 +13019,16 @@ class LanguageCapabilityCatalog {
12388
13019
  }
12389
13020
 
12390
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
+ }
12391
13032
  function filterList(value) {
12392
13033
  return value === undefined ? [] : Array.isArray(value) ? [...value] : [value];
12393
13034
  }
@@ -12601,6 +13242,7 @@ class SiftLightService {
12601
13242
  #runRipgrep;
12602
13243
  #snapshots;
12603
13244
  #summaryFileLimit;
13245
+ #vectorSearchEnabled;
12604
13246
  #capabilities = new LanguageCapabilityCatalog;
12605
13247
  #evidence;
12606
13248
  #lifecycle = new AbortController;
@@ -12611,12 +13253,20 @@ class SiftLightService {
12611
13253
  this.#runRipgrep = options.runRipgrep;
12612
13254
  this.#snapshots = options.snapshots ?? new SnapshotStore;
12613
13255
  this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
13256
+ this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
12614
13257
  this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
12615
13258
  this.#evidence = new EvidenceService(this.#runRipgrep, this.#snapshots, options.structure, options.conceptSearch, options.semanticJudge);
12616
13259
  }
12617
- 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) {
12618
13265
  validateRawSearchInput(input);
12619
13266
  validateRequestContract(input);
13267
+ if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
13268
+ throw new SiftLightError(`${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`);
13269
+ }
12620
13270
  let request;
12621
13271
  if (input.mode === "await" || input.mode === "cancel") {
12622
13272
  request = this.#operationCommand(input, cwd, signal);
@@ -13015,160 +13665,11 @@ ${page.body}${rangeNote}${contextNote}${missingSelectionNote}
13015
13665
  }
13016
13666
  }
13017
13667
 
13018
- // src/config-reader.ts
13019
- import { readFile as readFile3 } from "node:fs/promises";
13020
- var SIFT_LIGHT_CONFIG_ENV = "SIFT_LIGHT_CONFIG";
13021
- var SEMANTIC_JUDGE_API_KEY_ENVS = ["TYPESAFE_API_KEY", "SIFT_LIGHT_JEV_API_KEY"];
13022
- var DEFAULT_SEMANTIC_JUDGE_CONFIG = {
13023
- enabled: false,
13024
- provider: "jev",
13025
- endpoint: "https://api.typesafe.ai/v1/systemone",
13026
- apiKeyEnv: SEMANTIC_JUDGE_API_KEY_ENVS[0],
13027
- model: "jev-latest",
13028
- timeoutMs: 120000,
13029
- maxCandidates: 20,
13030
- maxRetries: 2
13031
- };
13032
- var DEFAULT_SIFT_LIGHT_CONFIG = {
13033
- locale: "en",
13034
- enforceSearch: "hard",
13035
- semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG
13036
- };
13037
- function hasErrorCode(error, codes) {
13038
- return error instanceof Error && "code" in error && codes.includes(String(error.code));
13039
- }
13040
- function isMissingFile(error) {
13041
- return hasErrorCode(error, ["ENOENT"]);
13042
- }
13043
- function isRawSiftLightConfig(value) {
13044
- return typeof value === "object" && value !== null && !Array.isArray(value);
13045
- }
13046
- function isRawSemanticJudgeConfig(value) {
13047
- return typeof value === "object" && value !== null && !Array.isArray(value);
13048
- }
13049
- function boundedInteger2(value, fallback, minimum, maximum, field) {
13050
- const candidate = value ?? fallback;
13051
- if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < minimum || candidate > maximum) {
13052
- throw new Error(`Invalid sift-light ${field}: expected an integer from ${String(minimum)} through ${String(maximum)}`);
13053
- }
13054
- return candidate;
13055
- }
13056
- function parseSemanticJudge(value, path) {
13057
- if (value === undefined)
13058
- return { ...DEFAULT_SEMANTIC_JUDGE_CONFIG };
13059
- if (!isRawSemanticJudgeConfig(value)) {
13060
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge must be an object`);
13061
- }
13062
- const unknown = Object.keys(value).filter((key) => ![
13063
- "enabled",
13064
- "provider",
13065
- "endpoint",
13066
- "apiKeyEnv",
13067
- "model",
13068
- "timeoutMs",
13069
- "maxCandidates",
13070
- "maxRetries"
13071
- ].includes(key));
13072
- if (unknown.length > 0) {
13073
- throw new Error(`Invalid sift-light config at ${path}: unsupported semanticJudge fields; accepted fields are enabled, provider, endpoint, apiKeyEnv, model, timeoutMs, maxCandidates and maxRetries`);
13074
- }
13075
- const enabled = value.enabled ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.enabled;
13076
- if (typeof enabled !== "boolean") {
13077
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.enabled must be a boolean`);
13078
- }
13079
- const provider = value.provider ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.provider;
13080
- if (provider !== "jev") {
13081
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.provider must be "jev"`);
13082
- }
13083
- const endpoint = value.endpoint ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.endpoint;
13084
- if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.length > 2048) {
13085
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a nonempty URL`);
13086
- }
13087
- let parsedEndpoint;
13088
- try {
13089
- parsedEndpoint = new URL(endpoint);
13090
- } catch (error) {
13091
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a URL`, {
13092
- cause: error
13093
- });
13094
- }
13095
- const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
13096
- const secureEndpoint = parsedEndpoint.protocol === "https:" || parsedEndpoint.protocol === "http:" && loopbackHosts.has(parsedEndpoint.hostname);
13097
- if (!secureEndpoint || parsedEndpoint.username || parsedEndpoint.password) {
13098
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must use HTTPS, except that HTTP is allowed for localhost loopback development; URL credentials are not allowed`);
13099
- }
13100
- const apiKeyEnv = value.apiKeyEnv ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.apiKeyEnv;
13101
- if (typeof apiKeyEnv !== "string" || !/^[A-Z][A-Z0-9_]{0,127}$/u.test(apiKeyEnv)) {
13102
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.apiKeyEnv must be an uppercase environment variable name`);
13103
- }
13104
- const model = value.model ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.model;
13105
- if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
13106
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.model must be bounded single-line text`);
13107
- }
13108
- return {
13109
- enabled,
13110
- provider,
13111
- endpoint,
13112
- apiKeyEnv,
13113
- model,
13114
- timeoutMs: boundedInteger2(value.timeoutMs, DEFAULT_SEMANTIC_JUDGE_CONFIG.timeoutMs, 1000, 1200000, `config at ${path}: semanticJudge.timeoutMs`),
13115
- maxCandidates: boundedInteger2(value.maxCandidates, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxCandidates, 1, 20, `config at ${path}: semanticJudge.maxCandidates`),
13116
- maxRetries: boundedInteger2(value.maxRetries, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxRetries, 0, 5, `config at ${path}: semanticJudge.maxRetries`)
13117
- };
13118
- }
13119
- function parseConfig(value, path) {
13120
- if (!isRawSiftLightConfig(value)) {
13121
- throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
13122
- }
13123
- const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "semanticJudge"].includes(key));
13124
- if (unknown.length > 0) {
13125
- throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch and semanticJudge are accepted`);
13126
- }
13127
- const { locale, enforceSearch } = value;
13128
- if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
13129
- throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
13130
- }
13131
- const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
13132
- return {
13133
- locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
13134
- enforceSearch: enforcement,
13135
- semanticJudge: parseSemanticJudge(value.semanticJudge, path)
13136
- };
13137
- }
13138
- function normalizeSearchEnforcement(value, source) {
13139
- if (value === undefined || value === "hard")
13140
- return "hard";
13141
- if (value === "prefer")
13142
- return "prefer";
13143
- if (value === "off")
13144
- return "off";
13145
- throw new Error(`Invalid sift-light ${source}: enforceSearch must be "hard", "prefer", or "off"`);
13146
- }
13147
- async function readSiftLightConfigFile(path, options = {}) {
13148
- try {
13149
- const content = await readFile3(path, "utf8");
13150
- return parseConfig(JSON.parse(content), path);
13151
- } catch (error) {
13152
- if (isMissingFile(error)) {
13153
- if (options.missing === "error") {
13154
- throw new Error(`sift-light config was not found at ${path}; create it or unset ${SIFT_LIGHT_CONFIG_ENV}`, { cause: error });
13155
- }
13156
- return { ...DEFAULT_SIFT_LIGHT_CONFIG };
13157
- }
13158
- if (error instanceof SyntaxError) {
13159
- throw new Error(`Invalid sift-light config at ${path}: ${error.message}`, {
13160
- cause: error
13161
- });
13162
- }
13163
- throw error;
13164
- }
13165
- }
13166
-
13167
13668
  // src/prompt-guidelines.ts
13168
13669
  var SOURCE_OUTPUT_GUIDANCE = "Auto/summary text may include bounded source excerpts; ordinary matches text is metadata-only. Inspect may return bounded source windows covering an entire small file. Analysis text may include semantic passages; structured details may retain excerpts, names and signatures. Follow output limits, coverage and continuations.";
13169
13670
  function siftLightPromptGuidelines(structuredOutput = true) {
13170
13671
  return [
13171
- `Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
13672
+ `Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} For routine development searches, start with fast exact content, filename or applicable structural modes when the request has a usable name, symbol, error text or other literal clue. Omitted mode is ordinary exact search and never loads the local embedding model. Vector search is disabled by default; concept/hybrid require vectorSearchEnabled:true in sift-light.json and an installed model. They can take tens of seconds on an uncached scope, so select them only when semantic recall is needed. Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
13172
13673
  `An omitted path searches the project cwd. Use scope:"strict" for a question restricted to one path; otherwise, if an explicit subpath has zero matches, ordinary and content-analysis searches retry from cwd and return project-wide counts with an expansion notice. Explicit absolute paths and .. traversal can search outside cwd, except protected external system areas and .git internals. Git changes mode remains cwd-scoped.`,
13173
13674
  `Search output includes counts, categories, ranked paths, coverage and continuation metadata. Source excerpts may contain the searched text. Use mode="inspect" or the host read capability when exact source is required for an edit or verification.`,
13174
13675
  `Use file and directory distributions to choose evidence. Reuse the visible cursor with path or paths for match metadata; mode="summary" pages the remaining file statistics. Match counts are not relevance scores.`,
@@ -13177,7 +13678,7 @@ function siftLightPromptGuidelines(structuredOutput = true) {
13177
13678
  `Use anyOf:["term1","term2"] when every exact occurrence of 2-64 literals is needed in one version-bound result. It is case-sensitive, reports anonymized condition counts, and runs requests above eight terms as bounded parallel chunks. Large condition inventories have separate continuation pages; copy those requests to retrieve the complete counts.`,
13178
13679
  `For a changed-code question, add changes:{base:"HEAD",scope:"lines",side:"new"}; omit target for the working tree, use side:"old" for deleted-side statistics. Copy returned continuation requests to preserve source versions.`,
13179
13680
  `Use mode:"capabilities" when the language or requested operation is unclear to get a compact lazy inventory. Use mode:"outline" with a concrete source file path for symbol counts and locations, mode:"imports" for static relationships, and mode:"tests" for related-test candidates. Their text pages summarize metadata; structured details can retain source evidence.`,
13180
- `Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; business concepts belong in hybrid/concept. Use wholeWord:true for a single-pattern whole-word search. exclude contains file globs, not content negation.`,
13681
+ `Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; use hybrid/concept only for business concepts that cannot be located by a literal clue. Use wholeWord:true for a single-pattern whole-word search. exclude contains file globs, not content negation.`,
13181
13682
  `Use mode:"structure" only for JS/TS/TSX/Go, with a required nonempty ast-grep pattern such as "compare($X, $X)" or "send()" for code shapes across whitespace; the text page reports structural counts and locations; structured details can retain matched source evidence.`,
13182
13683
  `Use mode:"concept" plus a natural-language query when names are unknown. It runs a pinned local multilingual model only after explicit installation; no search downloads weights or sends code to a remote model. Results expose candidate counts, score statistics, paths and the bounded ranked passage behind each candidate. Similarity scores identify candidates, not correctness.`,
13183
13684
  `Use mode:"hybrid" plus query when wording may differ from the source. It reports exact and semantic counts separately, removes overlap, and retains one pageable evidence snapshot. conceptLimit changes only the semantic candidate count; retained candidates keep their bounded source passages.`,
@@ -13188,9 +13689,9 @@ function siftLightPromptGuidelines(structuredOutput = true) {
13188
13689
  }
13189
13690
  function siftLightModelGuidelines() {
13190
13691
  return [
13191
- `Search with pattern and optional path. ${SOURCE_OUTPUT_GUIDANCE} Omit mode/limit for automatic detail/summary selection. Compact model analysis pages may defer source excerpts to inspect.`,
13192
- `Use ranked paths and condition counts to choose evidence. Cursor continuation pages the retained snapshot; summary file selection returns ordinary match metadata. Use mode="inspect" when an edit requires exact source.`,
13193
- `Modes: capabilities for language prerequisites; files+query for filenames; anyOf/allOf for literal condition statistics; outline/imports/tests for structural and relationship summaries; structure for AST match statistics; concept/hybrid for semantic candidate statistics. Similarity and static links are not proof.`,
13692
+ `Search with pattern and optional path. ${SOURCE_OUTPUT_GUIDANCE} Default search is exact and model-free. concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model; uncached runs may take tens of seconds. Omit mode/limit for auto pages.`,
13693
+ `Use ranked paths and counts; Cursor continuation pages retained results. Use mode="inspect" for exact source before editing.`,
13694
+ `Modes: files+query for filenames; anyOf/allOf for literals; outline/imports/tests for static code; structure for AST; concept/hybrid for semantic candidates. Similarity is not proof.`,
13194
13695
  `On rejection keep the strongest applicable mode and apply the stated repair once. Do not repeat the rejected request or include its error. Only explicit capability-unavailable permits a visibly partial alternative.`
13195
13696
  ];
13196
13697
  }
@@ -13213,12 +13714,12 @@ function stringEnum(values, options) {
13213
13714
  ...options?.description ? { description: options.description } : {}
13214
13715
  });
13215
13716
  }
13216
- var SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
13217
- var SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
13717
+ var SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Routine searches should use exact content, filenames or applicable structural modes first. Omitted mode is ordinary exact search and does not load the embedding model; concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model and may take tens of seconds on an uncached scope. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
13718
+ var SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. Default exact search is model-free; concept/hybrid require vectorSearchEnabled:true in sift-light.json and a model. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
13218
13719
  var siftLightSchema = Type.Object({
13219
13720
  query: Type.Optional(Type.String({
13220
13721
  maxLength: 256,
13221
- description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`
13722
+ description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require vectorSearchEnabled:true in sift-light.json and an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`
13222
13723
  })),
13223
13724
  scope: Type.Optional(stringEnum(["strict", "expand"], {
13224
13725
  description: `${fieldGuidance("scope")}; expand (default) retries ordinary content search from project cwd. Applies to ordinary, multi-term and role searches.`
@@ -13232,9 +13733,9 @@ var siftLightSchema = Type.Object({
13232
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.`
13233
13734
  })),
13234
13735
  allOf: Type.Optional(Type.Array(Type.String({ maxLength: MAX_PATH_CHARACTERS }), {
13235
- minItems: 2,
13736
+ minItems: 1,
13236
13737
  maxItems: 3,
13237
- 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.`
13238
13739
  })),
13239
13740
  within: Type.Optional(stringEnum(["file", "function"], {
13240
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."
@@ -13251,7 +13752,7 @@ var siftLightSchema = Type.Object({
13251
13752
  "unknown"
13252
13753
  ]), {
13253
13754
  minItems: 1,
13254
- 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."
13255
13756
  })),
13256
13757
  changes: Type.Optional(Type.Object({
13257
13758
  base: Type.Optional(Type.String({
@@ -13284,7 +13785,7 @@ var siftLightSchema = Type.Object({
13284
13785
  paths: Type.Optional(Type.Array(Type.String(), {
13285
13786
  minItems: 1,
13286
13787
  maxItems: MAX_SELECTED_PATHS,
13287
- 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)."
13288
13789
  })),
13289
13790
  glob: Type.Optional(Type.Union([
13290
13791
  Type.String({ maxLength: MAX_PATH_CHARACTERS }),
@@ -13308,7 +13809,7 @@ var siftLightSchema = Type.Object({
13308
13809
  })),
13309
13810
  hidden: Type.Optional(Type.Boolean({ description: "Search hidden files (default true; .git is always excluded)." })),
13310
13811
  ignorePolicy: Type.Optional(stringEnum(["respect", "include"], {
13311
- 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."
13312
13813
  })),
13313
13814
  patterns: Type.Optional(Type.Array(Type.Object({
13314
13815
  id: Type.String({ minLength: 1, maxLength: 64 }),
@@ -13353,29 +13854,29 @@ var siftLightSchema = Type.Object({
13353
13854
  limit: Type.Optional(Type.Integer({
13354
13855
  minimum: 1,
13355
13856
  maximum: MAX_PAGE_SIZE,
13356
- 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.`
13357
13858
  })),
13358
- mode: Type.Optional(stringEnum(SIFT_LIGHT_MODES, {
13359
- 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.`
13360
13861
  })),
13361
13862
  line: Type.Optional(Type.Number({
13362
- 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."
13363
13864
  })),
13364
13865
  matchIndex: Type.Optional(Type.Number({
13365
13866
  description: "1-based retained match index for cursor-scoped inspect; replaces path and line."
13366
13867
  })),
13367
13868
  matchIndices: Type.Optional(Type.Array(Type.Integer({ minimum: 1 }), {
13368
13869
  minItems: 1,
13369
- maxItems: MAX_INSPECT_TARGETS,
13370
- 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.`
13371
13872
  })),
13372
13873
  targets: Type.Optional(Type.Array(Type.Object({
13373
13874
  path: Type.String({ maxLength: MAX_PATH_CHARACTERS }),
13374
13875
  line: Type.Integer({ minimum: 1 })
13375
13876
  }), {
13376
13877
  minItems: 1,
13377
- maxItems: MAX_INSPECT_TARGETS,
13378
- 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.`
13379
13880
  })),
13380
13881
  cursor: Type.Optional(Type.String({ description: "Opaque cursor from a previous stable search snapshot." })),
13381
13882
  operationId: Type.Optional(Type.String({
@@ -13421,11 +13922,12 @@ function siftLightTool(outputMode) {
13421
13922
  tool.outputSchema = SIFT_LIGHT_OUTPUT_SCHEMA;
13422
13923
  return tool;
13423
13924
  }
13424
- function createDefaultSiftLightMcpService(semanticJudge) {
13925
+ function createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled = false) {
13425
13926
  const resolvedSemanticJudge = semanticJudge ?? createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG);
13426
13927
  return new SiftLightService({
13427
13928
  runRipgrep: createRipgrepRunner(),
13428
13929
  structure: createCtagsStructureProvider(),
13930
+ vectorSearchEnabled,
13429
13931
  semanticJudge: resolvedSemanticJudge
13430
13932
  });
13431
13933
  }
@@ -13848,12 +14350,18 @@ function configuredPath(environment) {
13848
14350
  const value = environment[SIFT_LIGHT_CONFIG_ENV]?.trim();
13849
14351
  return value || undefined;
13850
14352
  }
13851
- async function createMcpSemanticJudgeIntegration(environment = process.env) {
14353
+ async function createMcpSearchFeatures(environment = process.env) {
13852
14354
  const path = configuredPath(environment);
13853
14355
  if (!path)
13854
- return createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG);
14356
+ return {
14357
+ semanticJudge: createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG),
14358
+ vectorSearchEnabled: DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled === true
14359
+ };
13855
14360
  const config = await readSiftLightConfigFile(path, { missing: "error" });
13856
- return createSemanticJudgeIntegration(config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG, environment);
14361
+ return {
14362
+ semanticJudge: createConfiguredSemanticJudgeIntegration(config, environment),
14363
+ vectorSearchEnabled: config.vectorSearchEnabled === true
14364
+ };
13857
14365
  }
13858
14366
  function mcpSemanticJudgeConfigSource(environment = process.env) {
13859
14367
  return configuredPath(environment) ? "explicit-config" : "not-configured";
@@ -13967,20 +14475,17 @@ function environmentInteger(name, fallback, minimum, maximum) {
13967
14475
  function allowedOrigins() {
13968
14476
  return (process.env.SIFT_LIGHT_MCP_ALLOWED_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
13969
14477
  }
13970
- async function configuredSemanticJudge() {
13971
- return createMcpSemanticJudgeIntegration();
13972
- }
13973
14478
  function logSemanticJudgeStatus(integration) {
13974
14479
  const status = integration.config.enabled ? "enabled" : "disabled";
13975
14480
  process.stderr.write(`sift-light MCP semantic judge: ${status}; source=${mcpSemanticJudgeConfigSource()}
13976
14481
  `);
13977
14482
  }
13978
- async function runHttpServer(outputMode, semanticJudge) {
14483
+ async function runHttpServer(outputMode, semanticJudge, vectorSearchEnabled) {
13979
14484
  const running = await startSiftLightMcpServer({
13980
14485
  cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.cwd(),
13981
14486
  host: process.env.SIFT_LIGHT_MCP_HOST ?? DEFAULT_MCP_HOST,
13982
14487
  port: environmentInteger("SIFT_LIGHT_MCP_PORT", DEFAULT_MCP_PORT, 0, 65535),
13983
- createService: () => createDefaultSiftLightMcpService(semanticJudge),
14488
+ createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled),
13984
14489
  maxSessions: environmentInteger("SIFT_LIGHT_MCP_MAX_SESSIONS", DEFAULT_MCP_MAX_SESSIONS, 1, Number.MAX_SAFE_INTEGER),
13985
14490
  sessionIdleTimeoutMs: environmentInteger("SIFT_LIGHT_MCP_SESSION_IDLE_MS", DEFAULT_MCP_SESSION_IDLE_TIMEOUT_MS, 1, Number.MAX_SAFE_INTEGER),
13986
14491
  allowedOrigins: allowedOrigins(),
@@ -14014,11 +14519,11 @@ async function runHttpServer(outputMode, semanticJudge) {
14014
14519
  process.once("SIGINT", shutdown);
14015
14520
  process.once("SIGTERM", shutdown);
14016
14521
  }
14017
- async function runStdioServer(outputMode, semanticJudge) {
14522
+ async function runStdioServer(outputMode, semanticJudge, vectorSearchEnabled) {
14018
14523
  const running = await startSiftLightMcpStdioServer({
14019
14524
  cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
14020
14525
  outputMode,
14021
- createService: () => createDefaultSiftLightMcpService(semanticJudge)
14526
+ createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled)
14022
14527
  });
14023
14528
  process.stderr.write(`sift-light MCP serving one local client over stdio
14024
14529
  `);
@@ -14047,13 +14552,15 @@ async function main() {
14047
14552
  return;
14048
14553
  }
14049
14554
  const outputMode = parseSiftLightMcpOutputMode(process.env.SIFT_LIGHT_MCP_OUTPUT_MODE);
14050
- const semanticJudge = await configuredSemanticJudge();
14555
+ const { semanticJudge, vectorSearchEnabled } = await createMcpSearchFeatures();
14051
14556
  logSemanticJudgeStatus(semanticJudge);
14557
+ process.stderr.write(`sift-light MCP vector search: ${vectorSearchEnabled ? "enabled" : "disabled"}
14558
+ `);
14052
14559
  if (transport === "stdio") {
14053
- await runStdioServer(outputMode, semanticJudge);
14560
+ await runStdioServer(outputMode, semanticJudge, vectorSearchEnabled);
14054
14561
  return;
14055
14562
  }
14056
- await runHttpServer(outputMode, semanticJudge);
14563
+ await runHttpServer(outputMode, semanticJudge, vectorSearchEnabled);
14057
14564
  }
14058
14565
  try {
14059
14566
  await main();