sift-light 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.1",
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,
@@ -996,15 +1022,23 @@ function schemaError(input, field, reason) {
996
1022
  recovery: { action: "manual", reason }
997
1023
  }, `${field} is invalid: ${reason}`);
998
1024
  }
1025
+ function plainFilePatternRecovery(mode, input, invalid) {
1026
+ return mode === "files" && invalid.includes("pattern") && input.query === undefined && typeof input.pattern === "string" && input.pattern.trim().length > 0 && input.pattern.length <= 256 && input.pattern.isWellFormed() && !/[\\^$.*+?()[\]{}|\r\n\0]/u.test(input.pattern);
1027
+ }
999
1028
  function safeNextRequest(input, mode, invalid) {
1000
1029
  if (input.redact === true && containsSensitiveText(input))
1001
1030
  return;
1002
1031
  const safe = new Set(SAFE_DROP_FIELDS[mode] ?? []);
1032
+ const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
1033
+ if (plainFilePattern)
1034
+ safe.add("pattern");
1003
1035
  if (invalid.some((field) => !safe.has(field)))
1004
1036
  return;
1005
1037
  const next = { ...input };
1006
1038
  for (const field of invalid)
1007
1039
  delete next[field];
1040
+ if (plainFilePattern)
1041
+ next.query = input.pattern;
1008
1042
  if (selectorIssuesFor(next, mode).length > 0)
1009
1043
  return;
1010
1044
  try {
@@ -1033,20 +1067,26 @@ function fieldsError(input, mode, invalid, selectorIssues = []) {
1033
1067
  });
1034
1068
  const visibleFields = visibleInvalid.map((field) => boundedField(field)).join(", ");
1035
1069
  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(", ")}.`;
1070
+ const nestedModeObject = invalid.includes(mode);
1071
+ const flatRule = nestedModeObject ? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.` : "";
1072
+ 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
1073
  return new RequestContractError({
1038
1074
  code: "E_MODE_FIELDS",
1039
1075
  mode,
1040
1076
  issues,
1041
1077
  recovery: nextRequest ? {
1042
1078
  action: "retry",
1043
- reason: `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
1079
+ reason: plainFilePatternRecovery(mode, input, invalid) ? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved." : `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
1044
1080
  nextRequest
1045
1081
  } : {
1046
1082
  action: "manual",
1047
- reason: `${reason}; choose the mode explicitly or remove the fields yourself without changing the requested scope. ${flatRule}`
1083
+ reason: [
1084
+ reason,
1085
+ filesPatternRule || "Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
1086
+ flatRule
1087
+ ].filter(Boolean).join(" ")
1048
1088
  }
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.`);
1089
+ }, 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
1090
  }
1051
1091
  function selectorIssuesFor(input, mode) {
1052
1092
  if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof input.cursor === "string" && input.cursor.trim().length > 0 && !input.cursor.includes(".analysis")) {
@@ -1282,25 +1322,23 @@ function modelErrorText(error) {
1282
1322
  function requestContractErrorText(error) {
1283
1323
  return requestContractProjection(error).text;
1284
1324
  }
1285
- function projectRequestContract(projected, serialized, message) {
1286
- const prefix = `sift-light failed: request rejected [${projected.code}]: ${message}`;
1325
+ function projectRequestContract(projected, serialized) {
1287
1326
  const recovery = projected.recovery;
1288
1327
  const next = recovery.nextRequest ? `
1289
1328
  Copy the nested recovery.nextRequest object unchanged; do not repeat the original query.` : `
1290
- Recovery action: ${recovery.action}. ${recovery.reason}`;
1329
+ Recovery action: ${recovery.action}.`;
1291
1330
  return `
1292
- Error details: ${serialized}
1293
- ${prefix}${next}`;
1331
+ sift-light failed: request rejected [${projected.code}].
1332
+ Error details: ${serialized}${next}`;
1294
1333
  }
1295
1334
  function requestContractProjection(error) {
1296
1335
  const details = boundedRequestContractDetails(error.details);
1297
1336
  const serializedDetails = JSON.stringify(details);
1298
- const boundedMessage = error.message.toWellFormed().slice(0, 1024);
1299
- const result = projectRequestContract(details, serializedDetails, boundedMessage);
1337
+ const result = projectRequestContract(details, serializedDetails);
1300
1338
  if (Buffer.byteLength(result) <= MAX_REQUEST_RECOVERY_BYTES)
1301
1339
  return { details, text: result };
1302
1340
  const compact = {
1303
- code: boundedMessage.length > 0 ? details.code : "E_REQUEST_CONTRACT_PAYLOAD",
1341
+ code: details.code,
1304
1342
  ...details.mode ? { mode: details.mode } : {},
1305
1343
  issues: [
1306
1344
  {
@@ -1313,7 +1351,7 @@ function requestContractProjection(error) {
1313
1351
  reason: "Exact recovery was omitted; correct the request explicitly."
1314
1352
  }
1315
1353
  };
1316
- const compactText = projectRequestContract(compact, JSON.stringify(compact), "The request-contract error exceeded the bounded payload budget; correct the request explicitly.");
1354
+ const compactText = projectRequestContract(compact, JSON.stringify(compact));
1317
1355
  return { details: compact, text: compactText };
1318
1356
  }
1319
1357
 
@@ -2819,7 +2857,7 @@ var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
2819
2857
  var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
2820
2858
  var MAX_CONCEPT_CHARS = 1000;
2821
2859
  var CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
2822
- var CONCEPT_CACHE_VERSION = 1;
2860
+ var CONCEPT_CACHE_VERSION = 2;
2823
2861
  var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
2824
2862
  var CONCEPT_TIMEOUT_MS = 10 * 60000;
2825
2863
  var MIN_CONCEPT_TIMEOUT_MS = 1000;
@@ -5142,6 +5180,11 @@ function scoreProfile(scores) {
5142
5180
  spread: top - min
5143
5181
  };
5144
5182
  }
5183
+ function conceptRankingScore(cosine, passageLength) {
5184
+ const referenceLength = 500;
5185
+ const maximumCorrection = 0.02;
5186
+ return cosine - maximumCorrection * (1 - Math.sqrt(Math.min(passageLength / referenceLength, 1)));
5187
+ }
5145
5188
  function passage(document, start) {
5146
5189
  let end = Math.min(document.text.length, start + MAX_CONCEPT_CHARS);
5147
5190
  if (end < document.text.length) {
@@ -5400,6 +5443,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
5400
5443
  const similarity = inferred.scores[index];
5401
5444
  if (similarity === undefined)
5402
5445
  throw new Error("Missing concept similarity");
5446
+ const rankingScore = conceptRankingScore(similarity, item.text.length);
5403
5447
  const evidence = rangeEvidence(item.document, item.range);
5404
5448
  return {
5405
5449
  path: item.document.path,
@@ -5412,7 +5456,8 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
5412
5456
  kind: "concept-candidate",
5413
5457
  certainty: "candidate",
5414
5458
  score: similarity,
5415
- rankingReason: "local multilingual E5 cosine similarity; relevance candidate, no binding or execution claim",
5459
+ rankingScore,
5460
+ rankingReason: "local multilingual E5 cosine similarity with bounded short-passage rank correction; relevance candidate, no binding or execution claim",
5416
5461
  model: CONCEPT_MODEL,
5417
5462
  revision: CONCEPT_REVISION,
5418
5463
  tokenTruncated: false,
@@ -5421,7 +5466,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
5421
5466
  }
5422
5467
  };
5423
5468
  });
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);
5469
+ 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
5470
  if (ranked.length > MAX_ANALYSIS_RESULTS)
5426
5471
  retentionTruncated = true;
5427
5472
  result.items = ranked.slice(0, MAX_ANALYSIS_RESULTS);
@@ -9517,6 +9562,160 @@ function parsePythonOutline(document) {
9517
9562
  // src/hybrid-search.ts
9518
9563
  import { resolve as resolve20 } from "node:path";
9519
9564
 
9565
+ // src/config-reader.ts
9566
+ import { readFile as readFile2 } from "node:fs/promises";
9567
+ var SIFT_LIGHT_CONFIG_ENV = "SIFT_LIGHT_CONFIG";
9568
+ var SEMANTIC_JUDGE_API_KEY_ENVS = ["TYPESAFE_API_KEY", "SIFT_LIGHT_JEV_API_KEY"];
9569
+ var DEFAULT_SEMANTIC_JUDGE_CONFIG = {
9570
+ enabled: false,
9571
+ provider: "jev",
9572
+ endpoint: "https://api.typesafe.ai/v1/systemone",
9573
+ apiKeyEnv: SEMANTIC_JUDGE_API_KEY_ENVS[0],
9574
+ model: "jev-latest",
9575
+ timeoutMs: 120000,
9576
+ maxCandidates: 20,
9577
+ maxRetries: 2
9578
+ };
9579
+ var DEFAULT_SIFT_LIGHT_CONFIG = {
9580
+ locale: "en",
9581
+ enforceSearch: "hard",
9582
+ vectorSearchEnabled: false,
9583
+ semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG
9584
+ };
9585
+ function hasErrorCode(error, codes) {
9586
+ return error instanceof Error && "code" in error && codes.includes(String(error.code));
9587
+ }
9588
+ function isMissingFile(error) {
9589
+ return hasErrorCode(error, ["ENOENT"]);
9590
+ }
9591
+ function isRawSiftLightConfig(value) {
9592
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9593
+ }
9594
+ function isRawSemanticJudgeConfig(value) {
9595
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9596
+ }
9597
+ function boundedInteger2(value, fallback, minimum, maximum, field) {
9598
+ const candidate = value ?? fallback;
9599
+ if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < minimum || candidate > maximum) {
9600
+ throw new Error(`Invalid sift-light ${field}: expected an integer from ${String(minimum)} through ${String(maximum)}`);
9601
+ }
9602
+ return candidate;
9603
+ }
9604
+ function parseSemanticJudge(value, path) {
9605
+ if (value === undefined)
9606
+ return { ...DEFAULT_SEMANTIC_JUDGE_CONFIG };
9607
+ if (!isRawSemanticJudgeConfig(value)) {
9608
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge must be an object`);
9609
+ }
9610
+ const unknown = Object.keys(value).filter((key) => ![
9611
+ "enabled",
9612
+ "provider",
9613
+ "endpoint",
9614
+ "apiKeyEnv",
9615
+ "model",
9616
+ "timeoutMs",
9617
+ "maxCandidates",
9618
+ "maxRetries"
9619
+ ].includes(key));
9620
+ if (unknown.length > 0) {
9621
+ throw new Error(`Invalid sift-light config at ${path}: unsupported semanticJudge fields; accepted fields are enabled, provider, endpoint, apiKeyEnv, model, timeoutMs, maxCandidates and maxRetries`);
9622
+ }
9623
+ const enabled = value.enabled ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.enabled;
9624
+ if (typeof enabled !== "boolean") {
9625
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.enabled must be a boolean`);
9626
+ }
9627
+ const provider = value.provider ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.provider;
9628
+ if (provider !== "jev") {
9629
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.provider must be "jev"`);
9630
+ }
9631
+ const endpoint = value.endpoint ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.endpoint;
9632
+ if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.length > 2048) {
9633
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a nonempty URL`);
9634
+ }
9635
+ let parsedEndpoint;
9636
+ try {
9637
+ parsedEndpoint = new URL(endpoint);
9638
+ } catch (error) {
9639
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a URL`, {
9640
+ cause: error
9641
+ });
9642
+ }
9643
+ const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
9644
+ const secureEndpoint = parsedEndpoint.protocol === "https:" || parsedEndpoint.protocol === "http:" && loopbackHosts.has(parsedEndpoint.hostname);
9645
+ if (!secureEndpoint || parsedEndpoint.username || parsedEndpoint.password) {
9646
+ 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`);
9647
+ }
9648
+ const apiKeyEnv = value.apiKeyEnv ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.apiKeyEnv;
9649
+ if (typeof apiKeyEnv !== "string" || !/^[A-Z][A-Z0-9_]{0,127}$/u.test(apiKeyEnv)) {
9650
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.apiKeyEnv must be an uppercase environment variable name`);
9651
+ }
9652
+ const model = value.model ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.model;
9653
+ if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
9654
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.model must be bounded single-line text`);
9655
+ }
9656
+ return {
9657
+ enabled,
9658
+ provider,
9659
+ endpoint,
9660
+ apiKeyEnv,
9661
+ model,
9662
+ timeoutMs: boundedInteger2(value.timeoutMs, DEFAULT_SEMANTIC_JUDGE_CONFIG.timeoutMs, 1000, 1200000, `config at ${path}: semanticJudge.timeoutMs`),
9663
+ maxCandidates: boundedInteger2(value.maxCandidates, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxCandidates, 1, 20, `config at ${path}: semanticJudge.maxCandidates`),
9664
+ maxRetries: boundedInteger2(value.maxRetries, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxRetries, 0, 5, `config at ${path}: semanticJudge.maxRetries`)
9665
+ };
9666
+ }
9667
+ function parseConfig(value, path) {
9668
+ if (!isRawSiftLightConfig(value)) {
9669
+ throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
9670
+ }
9671
+ const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "vectorSearchEnabled", "semanticJudge"].includes(key));
9672
+ if (unknown.length > 0) {
9673
+ throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled and semanticJudge are accepted`);
9674
+ }
9675
+ const { locale, enforceSearch, vectorSearchEnabled } = value;
9676
+ if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
9677
+ throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
9678
+ }
9679
+ if (vectorSearchEnabled !== undefined && typeof vectorSearchEnabled !== "boolean") {
9680
+ throw new Error(`Invalid sift-light config at ${path}: vectorSearchEnabled must be a boolean`);
9681
+ }
9682
+ const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
9683
+ return {
9684
+ locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
9685
+ enforceSearch: enforcement,
9686
+ vectorSearchEnabled: vectorSearchEnabled ?? DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled ?? false,
9687
+ semanticJudge: parseSemanticJudge(value.semanticJudge, path)
9688
+ };
9689
+ }
9690
+ function normalizeSearchEnforcement(value, source) {
9691
+ if (value === undefined || value === "hard")
9692
+ return "hard";
9693
+ if (value === "prefer")
9694
+ return "prefer";
9695
+ if (value === "off")
9696
+ return "off";
9697
+ throw new Error(`Invalid sift-light ${source}: enforceSearch must be "hard", "prefer", or "off"`);
9698
+ }
9699
+ async function readSiftLightConfigFile(path, options = {}) {
9700
+ try {
9701
+ const content = await readFile2(path, "utf8");
9702
+ return parseConfig(JSON.parse(content), path);
9703
+ } catch (error) {
9704
+ if (isMissingFile(error)) {
9705
+ if (options.missing === "error") {
9706
+ throw new Error(`sift-light config was not found at ${path}; create it or unset ${SIFT_LIGHT_CONFIG_ENV}`, { cause: error });
9707
+ }
9708
+ return { ...DEFAULT_SIFT_LIGHT_CONFIG };
9709
+ }
9710
+ if (error instanceof SyntaxError) {
9711
+ throw new Error(`Invalid sift-light config at ${path}: ${error.message}`, {
9712
+ cause: error
9713
+ });
9714
+ }
9715
+ throw error;
9716
+ }
9717
+ }
9718
+
9520
9719
  // src/semantic-judge-batches.ts
9521
9720
  var MAX_SEMANTIC_JUDGE_BATCH_CANDIDATES = 8;
9522
9721
  var MAX_SEMANTIC_JUDGE_REQUEST_BYTES = 64 * 1024;
@@ -9664,7 +9863,7 @@ function requestBody(query, candidates, model) {
9664
9863
  for (const candidate of candidates) {
9665
9864
  questions[candidate.id] = {
9666
9865
  type: "choice",
9667
- instructions: "Classify the candidate by what it actually does for the requested behavior. Judge the code excerpt, not just matching words.",
9866
+ 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
9867
  criteria: {
9669
9868
  "implementation-candidate": "The excerpt appears to implement the requested behavior or its core decision/side effect.",
9670
9869
  "caller-candidate": "The excerpt invokes or wires an implementation but does not implement the behavior itself.",
@@ -9827,6 +10026,10 @@ function createSemanticJudgeIntegration(config, environment = process.env, fetch
9827
10026
  }
9828
10027
  return { config, runner: createJevRunner(config, key, fetcher) };
9829
10028
  }
10029
+ function createConfiguredSemanticJudgeIntegration(config, environment = process.env) {
10030
+ const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
10031
+ return config.vectorSearchEnabled === true ? createSemanticJudgeIntegration(judge, environment) : createDisabledSemanticJudgeIntegration(judge);
10032
+ }
9830
10033
  function baseDetails(config) {
9831
10034
  return {
9832
10035
  enabled: config.enabled,
@@ -10133,6 +10336,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
10133
10336
  const deduplicationCoverage = scan.snapshotComplete && literal.sourceCoverage === "complete" && conceptSourceCoverage === "complete" ? "complete" : "partial";
10134
10337
  const partial = !scan.snapshotComplete || concept.partial || conceptCoverage === "skipped" || conceptSourceCoverage === "partial" || literal.sourceCoverage === "partial" || deduplicationCoverage === "partial" || judgedConcept.semanticJudge?.status === "failed" || judgedConcept.semanticJudge?.status === "partial";
10135
10338
  const selectionReason = conceptCandidatesOmitted ? `Hybrid concept limit retained the top ${String(selectedConcept.length)} of ${String(eligibleConcept.length)} non-overlapping semantic candidates` : undefined;
10339
+ const firstScore = Number(eligibleConcept[0]?.details?.rankingScore ?? eligibleConcept[0]?.details?.score);
10340
+ const secondScore = Number(eligibleConcept[1]?.details?.rankingScore ?? eligibleConcept[1]?.details?.score);
10341
+ const closeRanking = Number.isFinite(firstScore) && Number.isFinite(secondScore) && firstScore - secondScore < 0.01;
10136
10342
  return {
10137
10343
  kind: "hybrid",
10138
10344
  unit: "evidence-items",
@@ -10144,6 +10350,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
10144
10350
  ...literal.reasons,
10145
10351
  ...execution.sourceGeneration.reasons,
10146
10352
  ...selectionReason ? [selectionReason] : [],
10353
+ ...closeRanking ? [
10354
+ "Semantic ranks are close; verify the leading candidates with source inspection or literal terms."
10355
+ ] : [],
10147
10356
  ...judgedConcept.semanticJudge?.reason ? [judgedConcept.semanticJudge.reason] : []
10148
10357
  ],
10149
10358
  filesRead: (concept.filesRead ?? 0) + access.filesRead,
@@ -11036,7 +11245,7 @@ import { resolve as resolve24 } from "node:path";
11036
11245
  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
11246
 
11038
11247
  // src/format.ts
11039
- import { readFile as readFile2 } from "node:fs/promises";
11248
+ import { readFile as readFile3 } from "node:fs/promises";
11040
11249
  var RESULT_METADATA_RESERVE_BYTES = 1024;
11041
11250
  var RESULT_METADATA_RESERVE_CHARACTERS = 512;
11042
11251
 
@@ -11170,7 +11379,7 @@ async function loadContextLines(match, expectedRevision, cache, signal) {
11170
11379
  cache.set(match.absolutePath, changed);
11171
11380
  return changed;
11172
11381
  }
11173
- const content = await readFile2(match.absolutePath, { encoding: "utf8", signal });
11382
+ const content = await readFile3(match.absolutePath, { encoding: "utf8", signal });
11174
11383
  const afterRevision = await getSourceRevision(match.absolutePath);
11175
11384
  if (!afterRevision || !sameSourceRevision(expectedRevision, afterRevision)) {
11176
11385
  const changed = { status: "changed" };
@@ -12601,6 +12810,7 @@ class SiftLightService {
12601
12810
  #runRipgrep;
12602
12811
  #snapshots;
12603
12812
  #summaryFileLimit;
12813
+ #vectorSearchEnabled;
12604
12814
  #capabilities = new LanguageCapabilityCatalog;
12605
12815
  #evidence;
12606
12816
  #lifecycle = new AbortController;
@@ -12611,12 +12821,16 @@ class SiftLightService {
12611
12821
  this.#runRipgrep = options.runRipgrep;
12612
12822
  this.#snapshots = options.snapshots ?? new SnapshotStore;
12613
12823
  this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
12824
+ this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
12614
12825
  this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
12615
12826
  this.#evidence = new EvidenceService(this.#runRipgrep, this.#snapshots, options.structure, options.conceptSearch, options.semanticJudge);
12616
12827
  }
12617
12828
  async search(input, cwd, signal, options = {}) {
12618
12829
  validateRawSearchInput(input);
12619
12830
  validateRequestContract(input);
12831
+ if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
12832
+ throw new SiftLightError(`${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`);
12833
+ }
12620
12834
  let request;
12621
12835
  if (input.mode === "await" || input.mode === "cancel") {
12622
12836
  request = this.#operationCommand(input, cwd, signal);
@@ -13015,160 +13229,11 @@ ${page.body}${rangeNote}${contextNote}${missingSelectionNote}
13015
13229
  }
13016
13230
  }
13017
13231
 
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
13232
  // src/prompt-guidelines.ts
13168
13233
  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
13234
  function siftLightPromptGuidelines(structuredOutput = true) {
13170
13235
  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.`,
13236
+ `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
13237
  `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
13238
  `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
13239
  `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 +13242,7 @@ function siftLightPromptGuidelines(structuredOutput = true) {
13177
13242
  `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
13243
  `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
13244
  `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.`,
13245
+ `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
13246
  `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
13247
  `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
13248
  `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 +13253,9 @@ function siftLightPromptGuidelines(structuredOutput = true) {
13188
13253
  }
13189
13254
  function siftLightModelGuidelines() {
13190
13255
  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.`,
13256
+ `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.`,
13257
+ `Use ranked paths and counts; Cursor continuation pages retained results. Use mode="inspect" for exact source before editing.`,
13258
+ `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
13259
  `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
13260
  ];
13196
13261
  }
@@ -13213,12 +13278,12 @@ function stringEnum(values, options) {
13213
13278
  ...options?.description ? { description: options.description } : {}
13214
13279
  });
13215
13280
  }
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.`;
13281
+ 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}`;
13282
+ 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
13283
  var siftLightSchema = Type.Object({
13219
13284
  query: Type.Optional(Type.String({
13220
13285
  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.`
13286
+ 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
13287
  })),
13223
13288
  scope: Type.Optional(stringEnum(["strict", "expand"], {
13224
13289
  description: `${fieldGuidance("scope")}; expand (default) retries ordinary content search from project cwd. Applies to ordinary, multi-term and role searches.`
@@ -13421,11 +13486,12 @@ function siftLightTool(outputMode) {
13421
13486
  tool.outputSchema = SIFT_LIGHT_OUTPUT_SCHEMA;
13422
13487
  return tool;
13423
13488
  }
13424
- function createDefaultSiftLightMcpService(semanticJudge) {
13489
+ function createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled = false) {
13425
13490
  const resolvedSemanticJudge = semanticJudge ?? createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG);
13426
13491
  return new SiftLightService({
13427
13492
  runRipgrep: createRipgrepRunner(),
13428
13493
  structure: createCtagsStructureProvider(),
13494
+ vectorSearchEnabled,
13429
13495
  semanticJudge: resolvedSemanticJudge
13430
13496
  });
13431
13497
  }
@@ -13848,12 +13914,18 @@ function configuredPath(environment) {
13848
13914
  const value = environment[SIFT_LIGHT_CONFIG_ENV]?.trim();
13849
13915
  return value || undefined;
13850
13916
  }
13851
- async function createMcpSemanticJudgeIntegration(environment = process.env) {
13917
+ async function createMcpSearchFeatures(environment = process.env) {
13852
13918
  const path = configuredPath(environment);
13853
13919
  if (!path)
13854
- return createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG);
13920
+ return {
13921
+ semanticJudge: createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG),
13922
+ vectorSearchEnabled: DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled === true
13923
+ };
13855
13924
  const config = await readSiftLightConfigFile(path, { missing: "error" });
13856
- return createSemanticJudgeIntegration(config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG, environment);
13925
+ return {
13926
+ semanticJudge: createConfiguredSemanticJudgeIntegration(config, environment),
13927
+ vectorSearchEnabled: config.vectorSearchEnabled === true
13928
+ };
13857
13929
  }
13858
13930
  function mcpSemanticJudgeConfigSource(environment = process.env) {
13859
13931
  return configuredPath(environment) ? "explicit-config" : "not-configured";
@@ -13967,20 +14039,17 @@ function environmentInteger(name, fallback, minimum, maximum) {
13967
14039
  function allowedOrigins() {
13968
14040
  return (process.env.SIFT_LIGHT_MCP_ALLOWED_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
13969
14041
  }
13970
- async function configuredSemanticJudge() {
13971
- return createMcpSemanticJudgeIntegration();
13972
- }
13973
14042
  function logSemanticJudgeStatus(integration) {
13974
14043
  const status = integration.config.enabled ? "enabled" : "disabled";
13975
14044
  process.stderr.write(`sift-light MCP semantic judge: ${status}; source=${mcpSemanticJudgeConfigSource()}
13976
14045
  `);
13977
14046
  }
13978
- async function runHttpServer(outputMode, semanticJudge) {
14047
+ async function runHttpServer(outputMode, semanticJudge, vectorSearchEnabled) {
13979
14048
  const running = await startSiftLightMcpServer({
13980
14049
  cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.cwd(),
13981
14050
  host: process.env.SIFT_LIGHT_MCP_HOST ?? DEFAULT_MCP_HOST,
13982
14051
  port: environmentInteger("SIFT_LIGHT_MCP_PORT", DEFAULT_MCP_PORT, 0, 65535),
13983
- createService: () => createDefaultSiftLightMcpService(semanticJudge),
14052
+ createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled),
13984
14053
  maxSessions: environmentInteger("SIFT_LIGHT_MCP_MAX_SESSIONS", DEFAULT_MCP_MAX_SESSIONS, 1, Number.MAX_SAFE_INTEGER),
13985
14054
  sessionIdleTimeoutMs: environmentInteger("SIFT_LIGHT_MCP_SESSION_IDLE_MS", DEFAULT_MCP_SESSION_IDLE_TIMEOUT_MS, 1, Number.MAX_SAFE_INTEGER),
13986
14055
  allowedOrigins: allowedOrigins(),
@@ -14014,11 +14083,11 @@ async function runHttpServer(outputMode, semanticJudge) {
14014
14083
  process.once("SIGINT", shutdown);
14015
14084
  process.once("SIGTERM", shutdown);
14016
14085
  }
14017
- async function runStdioServer(outputMode, semanticJudge) {
14086
+ async function runStdioServer(outputMode, semanticJudge, vectorSearchEnabled) {
14018
14087
  const running = await startSiftLightMcpStdioServer({
14019
14088
  cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
14020
14089
  outputMode,
14021
- createService: () => createDefaultSiftLightMcpService(semanticJudge)
14090
+ createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled)
14022
14091
  });
14023
14092
  process.stderr.write(`sift-light MCP serving one local client over stdio
14024
14093
  `);
@@ -14047,13 +14116,15 @@ async function main() {
14047
14116
  return;
14048
14117
  }
14049
14118
  const outputMode = parseSiftLightMcpOutputMode(process.env.SIFT_LIGHT_MCP_OUTPUT_MODE);
14050
- const semanticJudge = await configuredSemanticJudge();
14119
+ const { semanticJudge, vectorSearchEnabled } = await createMcpSearchFeatures();
14051
14120
  logSemanticJudgeStatus(semanticJudge);
14121
+ process.stderr.write(`sift-light MCP vector search: ${vectorSearchEnabled ? "enabled" : "disabled"}
14122
+ `);
14052
14123
  if (transport === "stdio") {
14053
- await runStdioServer(outputMode, semanticJudge);
14124
+ await runStdioServer(outputMode, semanticJudge, vectorSearchEnabled);
14054
14125
  return;
14055
14126
  }
14056
- await runHttpServer(outputMode, semanticJudge);
14127
+ await runHttpServer(outputMode, semanticJudge, vectorSearchEnabled);
14057
14128
  }
14058
14129
  try {
14059
14130
  await main();