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
@@ -115,6 +115,38 @@ function schemaError(
115
115
  );
116
116
  }
117
117
 
118
+ function plainFilePatternRecovery(
119
+ mode: SiftLightMode,
120
+ input: Record<string, unknown>,
121
+ invalid: readonly string[],
122
+ ): boolean {
123
+ return (
124
+ mode === "files" &&
125
+ invalid.includes("pattern") &&
126
+ input.query === undefined &&
127
+ typeof input.pattern === "string" &&
128
+ input.pattern.trim().length > 0 &&
129
+ input.pattern.length <= 256 &&
130
+ input.pattern.isWellFormed() &&
131
+ !/[\\^$.*+?()[\]{}|\r\n\0]/u.test(input.pattern)
132
+ );
133
+ }
134
+
135
+ /** `pattern: ".*"` in files mode means "every file": the exact repair omits it. */
136
+ function matchAllFilePatternRecovery(
137
+ mode: SiftLightMode,
138
+ input: Record<string, unknown>,
139
+ invalid: readonly string[],
140
+ ): boolean {
141
+ return (
142
+ mode === "files" &&
143
+ invalid.includes("pattern") &&
144
+ input.query === undefined &&
145
+ typeof input.pattern === "string" &&
146
+ /^(?:\.\*|\.\+|\*+)$/u.test(input.pattern.trim())
147
+ );
148
+ }
149
+
118
150
  function safeNextRequest(
119
151
  input: Record<string, unknown>,
120
152
  mode: SiftLightMode,
@@ -122,9 +154,12 @@ function safeNextRequest(
122
154
  ): Record<string, unknown> | undefined {
123
155
  if (input.redact === true && containsSensitiveText(input)) return undefined;
124
156
  const safe = new Set<string>(SAFE_DROP_FIELDS[mode] ?? []);
157
+ const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
158
+ if (plainFilePattern || matchAllFilePatternRecovery(mode, input, invalid)) safe.add("pattern");
125
159
  if (invalid.some((field) => !safe.has(field))) return undefined;
126
160
  const next: Record<string, unknown> = { ...input };
127
161
  for (const field of invalid) delete next[field];
162
+ if (plainFilePattern) next.query = input.pattern;
128
163
  if (selectorIssuesFor(next, mode).length > 0) return undefined;
129
164
  // A copied request must remain within the transport's bounded line budget.
130
165
  try {
@@ -165,7 +200,14 @@ function fieldsError(
165
200
  // the signature of arguments shaped as a per-mode object, which the flat
166
201
  // schema never advertises. Naming the accepted fields replaces an otherwise
167
202
  // puzzling rejection with the rule and the valid names.
168
- const flatRule = `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.`;
203
+ const nestedModeObject = invalid.includes(mode);
204
+ const flatRule = nestedModeObject
205
+ ? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.`
206
+ : "";
207
+ const filesPatternRule =
208
+ mode === "files" && invalid.includes("pattern")
209
+ ? "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."
210
+ : "";
169
211
  return new RequestContractError(
170
212
  {
171
213
  code: "E_MODE_FIELDS",
@@ -174,17 +216,28 @@ function fieldsError(
174
216
  recovery: nextRequest
175
217
  ? {
176
218
  action: "retry",
177
- reason: `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
219
+ reason: plainFilePatternRecovery(mode, input, invalid)
220
+ ? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved."
221
+ : matchAllFilePatternRecovery(mode, input, invalid)
222
+ ? "A match-all pattern lists every file; omitting query does that, so copy nextRequest; all filters are preserved."
223
+ : `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
178
224
  nextRequest,
179
225
  }
180
226
  : {
181
227
  action: "manual",
182
- reason: `${reason}; choose the mode explicitly or remove the fields yourself without changing the requested scope. ${flatRule}`,
228
+ reason: [
229
+ reason,
230
+ filesPatternRule ||
231
+ "Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
232
+ flatRule,
233
+ ]
234
+ .filter(Boolean)
235
+ .join(" "),
183
236
  },
184
237
  },
185
238
  nextRequest
186
- ? `${reason}; retry the exact nextRequest without repeating the original query.`
187
- : `${reason}; no semantics-preserving automatic request is available. ${flatRule} Preserve valid path, filters, redact and cursor fields when choosing the next request.`,
239
+ ? `${reason}; ${plainFilePatternRecovery(mode, input, invalid) ? "move plain filename text to query" : "retry the exact nextRequest"}.`
240
+ : `${reason}; no semantics-preserving automatic request is available. ${filesPatternRule || flatRule || "Check the mode's accepted fields."}`,
188
241
  );
189
242
  }
190
243
 
@@ -422,7 +475,8 @@ export function validateRequestContract(input: SiftLightInput): void {
422
475
  const allowed = new Set(MODE_FIELDS_BY_MODE[mode]);
423
476
  if (mode === "inspect" && raw.sourceCursor !== undefined) {
424
477
  allowed.clear();
425
- for (const field of ["mode", "sourceCursor", "redact"] as const) allowed.add(field);
478
+ // path is checked against the continuation's own source; it cannot redirect it.
479
+ for (const field of ["mode", "sourceCursor", "redact", "path"] as const) allowed.add(field);
426
480
  }
427
481
  if (
428
482
  (mode === "auto" || mode === "summary" || mode === "matches") &&
package/src/request.ts CHANGED
@@ -108,7 +108,9 @@ export function normalizeRequest(input: RawSearchInput): SearchRequest {
108
108
  throw new SiftLightError("ignorePolicy must be respect or include");
109
109
  const pattern = input.pattern;
110
110
  if (pattern === undefined) {
111
- throw new SiftLightError("pattern is required when cursor is not provided");
111
+ throw new SiftLightError(
112
+ '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',
113
+ );
112
114
  }
113
115
 
114
116
  const path = input.path?.replace(/^@/, "");
package/src/runtime.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { SiftLightLocale } from "./config.js";
2
- import type { SiftLightInput, SiftLightSearchOptions, SiftLightService } from "./service.js";
2
+ import type { SiftLightRequest, SiftLightSearchOptions, SiftLightService } from "./service.js";
3
3
  import { type SessionSummarySnapshot, SessionSummary } from "./session-summary.js";
4
4
  import type { ContextBudget, SiftLightResult } from "./types.js";
5
5
 
@@ -12,7 +12,7 @@ export class SiftLightRuntime {
12
12
  }
13
13
 
14
14
  async search(
15
- input: SiftLightInput,
15
+ input: SiftLightRequest,
16
16
  cwd: string,
17
17
  signal?: AbortSignal,
18
18
  contextBudget?: ContextBudget,
@@ -1,4 +1,8 @@
1
- import type { SemanticJudgeConfig } from "./config-reader.js";
1
+ import {
2
+ DEFAULT_SEMANTIC_JUDGE_CONFIG,
3
+ type SemanticJudgeConfig,
4
+ type SiftLightConfig,
5
+ } from "./config-reader.js";
2
6
  import {
3
7
  SEMANTIC_JUDGE_CLASSIFICATIONS,
4
8
  SEMANTIC_JUDGE_NON_PROOF_CLAIM,
@@ -134,8 +138,7 @@ function requestBody(
134
138
  for (const candidate of candidates) {
135
139
  questions[candidate.id] = {
136
140
  type: "choice",
137
- instructions:
138
- "Classify the candidate by what it actually does for the requested behavior. Judge the code excerpt, not just matching words.",
141
+ 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.`,
139
142
  criteria: {
140
143
  "implementation-candidate":
141
144
  "The excerpt appears to implement the requested behavior or its core decision/side effect.",
@@ -374,6 +377,17 @@ export function createSemanticJudgeIntegration(
374
377
  }
375
378
  return { config, runner: createJevRunner(config, key, fetcher) };
376
379
  }
380
+
381
+ export function createConfiguredSemanticJudgeIntegration(
382
+ config: SiftLightConfig,
383
+ environment: NodeJS.ProcessEnv = process.env,
384
+ ): SemanticJudgeIntegration {
385
+ const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
386
+ return config.vectorSearchEnabled === true
387
+ ? createSemanticJudgeIntegration(judge, environment)
388
+ : createDisabledSemanticJudgeIntegration(judge);
389
+ }
390
+
377
391
  function baseDetails(config: SemanticJudgeConfig): SemanticJudgeDetails {
378
392
  return {
379
393
  enabled: config.enabled,
package/src/service.ts CHANGED
@@ -7,6 +7,18 @@ import { resolve } from "node:path";
7
7
  import { CursorError, SiftLightError } from "./errors.js";
8
8
  import { DISCOVERY_MODE_REQUIRED_ERROR } from "./discovery-errors.js";
9
9
  import { validateRequestContract } from "./request-contract.js";
10
+ import { normalizeRequestAliases, type SiftLightRequest } from "./request-aliases.js";
11
+
12
+ export type { SiftLightRequest } from "./request-aliases.js";
13
+
14
+ /** Normalizations are part of the evidence: show them in text and details. */
15
+ function withRequestNotes(result: SiftLightResult, notes: readonly string[]): SiftLightResult {
16
+ if (notes.length === 0) return result;
17
+ return {
18
+ text: `${notes.map((note) => `[Request note: ${note}]`).join("\n")}\n${result.text}`,
19
+ details: { ...result.details, requestNotes: [...notes] },
20
+ };
21
+ }
10
22
  import {
11
23
  formatMatchMetadataPage,
12
24
  formatMatchPage,
@@ -87,6 +99,7 @@ export interface SiftLightServiceOptions {
87
99
  snapshots?: SnapshotStore;
88
100
  summaryFileLimit?: number;
89
101
  structure?: CodeStructureProvider;
102
+ vectorSearchEnabled?: boolean;
90
103
  conceptSearch?: ConceptSearchRunner;
91
104
  semanticJudge?: SemanticJudgeIntegration;
92
105
  }
@@ -367,6 +380,7 @@ export class SiftLightService {
367
380
  readonly #runRipgrep: RipgrepRunner;
368
381
  readonly #snapshots: SnapshotStore;
369
382
  readonly #summaryFileLimit: number;
383
+ readonly #vectorSearchEnabled: boolean;
370
384
  readonly #capabilities = new LanguageCapabilityCatalog();
371
385
  readonly #evidence: EvidenceService;
372
386
  #lifecycle = new AbortController();
@@ -378,6 +392,7 @@ export class SiftLightService {
378
392
  this.#runRipgrep = options.runRipgrep;
379
393
  this.#snapshots = options.snapshots ?? new SnapshotStore();
380
394
  this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
395
+ this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
381
396
  this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
382
397
  this.#evidence = new EvidenceService(
383
398
  this.#runRipgrep,
@@ -389,13 +404,28 @@ export class SiftLightService {
389
404
  }
390
405
 
391
406
  async search(
392
- input: SiftLightInput,
407
+ request: SiftLightRequest,
393
408
  cwd: string,
394
409
  signal?: AbortSignal,
395
410
  options: SiftLightSearchOptions = {},
411
+ ): Promise<SiftLightResult> {
412
+ const { input, notes } = normalizeRequestAliases(request);
413
+ return withRequestNotes(await this.#searchNormalized(input, cwd, signal, options), notes);
414
+ }
415
+
416
+ async #searchNormalized(
417
+ input: SiftLightInput,
418
+ cwd: string,
419
+ signal: AbortSignal | undefined,
420
+ options: SiftLightSearchOptions,
396
421
  ): Promise<SiftLightResult> {
397
422
  validateRawSearchInput(input);
398
423
  validateRequestContract(input);
424
+ if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
425
+ throw new SiftLightError(
426
+ `${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`,
427
+ );
428
+ }
399
429
  let request: Promise<SiftLightResult>;
400
430
  if (input.mode === "await" || input.mode === "cancel") {
401
431
  request = this.#operationCommand(input, cwd, signal);
@@ -1,6 +1,6 @@
1
1
  import type { SiftLightLocale } from "./config.js";
2
2
  import { SIFT_LIGHT_VERSION } from "./package-version.js";
3
- import type { SiftLightInput } from "./service.js";
3
+ import type { SiftLightRequest } from "./service.js";
4
4
  import type { SiftLightResult } from "./types.js";
5
5
 
6
6
  export const SESSION_STATUS_KEY = "sift-light_session";
@@ -13,7 +13,7 @@ export interface SessionSummarySnapshot {
13
13
  failedCalls: number;
14
14
  }
15
15
 
16
- function isNewQuery(input: SiftLightInput): boolean {
16
+ function isNewQuery(input: SiftLightRequest): boolean {
17
17
  return (
18
18
  input.cursor === undefined &&
19
19
  input.sourceCursor === undefined &&
@@ -21,7 +21,7 @@ function isNewQuery(input: SiftLightInput): boolean {
21
21
  );
22
22
  }
23
23
 
24
- function wasAutomaticallyOrganized(input: SiftLightInput, result: SiftLightResult): boolean {
24
+ function wasAutomaticallyOrganized(input: SiftLightRequest, result: SiftLightResult): boolean {
25
25
  const autoMode = input.mode === undefined || input.mode === "auto";
26
26
  return autoMode && input.limit === undefined && result.details.summaryFilesShown !== undefined;
27
27
  }
@@ -59,7 +59,7 @@ export class SessionSummary {
59
59
  failedCalls: 0,
60
60
  };
61
61
 
62
- record(input: SiftLightInput, result: SiftLightResult): void {
62
+ record(input: SiftLightRequest, result: SiftLightResult): void {
63
63
  if (!isNewQuery(input)) return;
64
64
  this.#snapshot.queries += 1;
65
65
  if (result.details.status === "complete") this.#snapshot.completeQueries += 1;
@@ -33,6 +33,8 @@ export interface SourceInspectionTarget {
33
33
  /** Raw source byte offset; retained-match focus is line-relative. */
34
34
  absoluteFocus?: number;
35
35
  focus?: number;
36
+ /** Structure already proven by the analysis that produced `range`. */
37
+ structure?: StructureDetails;
36
38
  expectedRevision?: SourceRevision;
37
39
  unverified?: boolean;
38
40
  retry?: SiftLightInput;
@@ -121,7 +123,20 @@ async function prepare(
121
123
  let range = target.range;
122
124
  let details: StructureDetails = { status: "no-symbol" };
123
125
  const language = syntaxLanguage(document.path);
124
- if (document.utf8 && language && language !== "go") {
126
+ if (target.range && target.structure) {
127
+ document.checkRange(target.range);
128
+ const lines = {
129
+ startLine: document.lineAt(target.range.start),
130
+ endLine: document.lineAt(Math.max(target.range.start, target.range.end - 1)),
131
+ };
132
+ // Whole lines keep leading modifiers such as `export` or decorators readable.
133
+ range = document.lineRange(lines.startLine, lines.endLine);
134
+ details = {
135
+ ...target.structure,
136
+ range: lines,
137
+ ...(target.structure.symbol ? { symbol: { ...target.structure.symbol, range: lines } } : {}),
138
+ };
139
+ } else if (document.utf8 && language && language !== "go") {
125
140
  const syntax = await access.syntax(document);
126
141
  details = {
127
142
  status:
@@ -205,7 +220,9 @@ async function prepare(
205
220
  Math.min(document.lineStarts.length, target.line + 10),
206
221
  );
207
222
  const boundary: Exclude<SourceBoundary, "mixed"> = target.range
208
- ? "requested-range"
223
+ ? target.structure
224
+ ? "syntax"
225
+ : "requested-range"
209
226
  : details.status === "available" && details.range
210
227
  ? "syntax"
211
228
  : "line-window";
@@ -692,8 +709,16 @@ export async function continueSource(
692
709
  cursor: string,
693
710
  access: SourceAccess,
694
711
  continuations: SourceContinuations,
712
+ expectedPath?: string,
695
713
  ): Promise<SiftLightResult> {
696
714
  const state = continuations.resolve(cursor);
715
+ if (
716
+ expectedPath !== undefined &&
717
+ resolve(access.cwd, expectedPath.replace(/^@/, "")) !== resolve(access.cwd, state.source.path)
718
+ )
719
+ throw new SiftLightError(
720
+ `sourceCursor continues ${JSON.stringify(state.source.path)}, not ${JSON.stringify(expectedPath)}; copy the returned nextRequest exactly`,
721
+ );
697
722
  const document = await access.load(state.source.path, state.source);
698
723
  const page = sourcePage(document, state.remaining, MAX_RESULT_BYTES - 1400);
699
724
  const next = continuations.advance(cursor, page.fragment);
@@ -10,6 +10,7 @@ import {
10
10
  } from "./analysis-limits.js";
11
11
  import {
12
12
  MAX_CONTEXT_LINES,
13
+ MAX_INSPECT_REQUEST_TARGETS,
13
14
  MAX_INSPECT_TARGETS,
14
15
  MAX_PAGE_SIZE,
15
16
  MAX_SELECTED_PATHS,
@@ -25,6 +26,7 @@ import {
25
26
  SIFT_LIGHT_MODES,
26
27
  fieldGuidance,
27
28
  } from "./request-contract.js";
29
+ import { MODE_ALIASES } from "./request-aliases.js";
28
30
 
29
31
  function stringEnum<const Values extends readonly string[]>(
30
32
  values: Values,
@@ -37,15 +39,15 @@ function stringEnum<const Values extends readonly string[]>(
37
39
  });
38
40
  }
39
41
 
40
- export const 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}`;
42
+ export const 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}`;
41
43
 
42
- export const SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
44
+ export const 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.`;
43
45
 
44
46
  export const siftLightSchema = Type.Object({
45
47
  query: Type.Optional(
46
48
  Type.String({
47
49
  maxLength: 256,
48
- 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.`,
50
+ 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.`,
49
51
  }),
50
52
  ),
51
53
  scope: Type.Optional(
@@ -68,9 +70,9 @@ export const siftLightSchema = Type.Object({
68
70
  ),
69
71
  allOf: Type.Optional(
70
72
  Type.Array(Type.String({ maxLength: MAX_PATH_CHARACTERS }), {
71
- minItems: 2,
73
+ minItems: 1,
72
74
  maxItems: 3,
73
- description: `${fieldGuidance("allOf")}. 2-3 distinct terms must occur in one file (default) or one function.`,
75
+ 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.`,
74
76
  }),
75
77
  ),
76
78
  within: Type.Optional(
@@ -95,7 +97,7 @@ export const siftLightSchema = Type.Object({
95
97
  {
96
98
  minItems: 1,
97
99
  description:
98
- "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.",
100
+ "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.",
99
101
  },
100
102
  ),
101
103
  ),
@@ -151,7 +153,7 @@ export const siftLightSchema = Type.Object({
151
153
  minItems: 1,
152
154
  maxItems: MAX_SELECTED_PATHS,
153
155
  description:
154
- "Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests.",
156
+ "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).",
155
157
  }),
156
158
  ),
157
159
  glob: Type.Optional(
@@ -193,7 +195,7 @@ export const siftLightSchema = Type.Object({
193
195
  ignorePolicy: Type.Optional(
194
196
  stringEnum(["respect", "include"] as const, {
195
197
  description:
196
- "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.",
198
+ "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.",
197
199
  }),
198
200
  ),
199
201
  patterns: Type.Optional(
@@ -264,19 +266,19 @@ export const siftLightSchema = Type.Object({
264
266
  Type.Integer({
265
267
  minimum: 1,
266
268
  maximum: MAX_PAGE_SIZE,
267
- 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.`,
269
+ 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.`,
268
270
  }),
269
271
  ),
270
272
  mode: Type.Optional(
271
- stringEnum(SIFT_LIGHT_MODES, {
272
- 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}`,
273
+ stringEnum([...SIFT_LIGHT_MODES, ...MODE_ALIASES], {
274
+ 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.`,
273
275
  }),
274
276
  ),
275
277
 
276
278
  line: Type.Optional(
277
279
  Type.Number({
278
280
  description:
279
- "1-indexed source line for path inspection/navigation. Omit with matchIndex, matchIndices or targets.",
281
+ "1-indexed source line for path inspection/navigation; path-only inspect starts at line 1. Omit with matchIndex, matchIndices or targets.",
280
282
  }),
281
283
  ),
282
284
  matchIndex: Type.Optional(
@@ -288,9 +290,8 @@ export const siftLightSchema = Type.Object({
288
290
  matchIndices: Type.Optional(
289
291
  Type.Array(Type.Integer({ minimum: 1 }), {
290
292
  minItems: 1,
291
- maxItems: MAX_INSPECT_TARGETS,
292
- description:
293
- "Inspect up to five visible match numbers together using the same cursor; mutually exclusive with matchIndex, path, line and targets.",
293
+ maxItems: MAX_INSPECT_REQUEST_TARGETS,
294
+ 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.`,
294
295
  }),
295
296
  ),
296
297
  targets: Type.Optional(
@@ -301,9 +302,8 @@ export const siftLightSchema = Type.Object({
301
302
  }),
302
303
  {
303
304
  minItems: 1,
304
- maxItems: MAX_INSPECT_TARGETS,
305
- description:
306
- "Inspect known path/line locations together without a cursor. The complete batch shares one 16 KiB response budget.",
305
+ maxItems: MAX_INSPECT_REQUEST_TARGETS,
306
+ 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.`,
307
307
  },
308
308
  ),
309
309
  ),
package/src/tui/layout.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import type { SiftLightLocale } from "../config.js";
3
- import type { SiftLightInput } from "../service.js";
3
+ import type { SiftLightRequest } from "../service.js";
4
4
  import type { Dashboard } from "./dashboard.js";
5
5
 
6
6
  import { candy, type SiftLightTheme } from "./palette.js";
@@ -254,7 +254,7 @@ export function renderDashboard(
254
254
  }
255
255
 
256
256
  export function renderSiftLightCallLines(
257
- input: SiftLightInput,
257
+ input: SiftLightRequest,
258
258
  locale: SiftLightLocale,
259
259
  theme: SiftLightTheme,
260
260
  width: number,
@@ -1,6 +1,6 @@
1
1
  import { type Component } from "@earendil-works/pi-tui";
2
2
  import type { SiftLightLocale } from "../config.js";
3
- import type { SiftLightInput } from "../service.js";
3
+ import type { SiftLightRequest } from "../service.js";
4
4
  import type { SiftLightDetails } from "../types.js";
5
5
  import { dashboard } from "./dashboard.js";
6
6
  import { fit, renderDashboard, renderSiftLightCallLines, type SiftLightTheme } from "./layout.js";
@@ -47,7 +47,7 @@ function failure(text: string, locale: SiftLightLocale): string {
47
47
  }
48
48
 
49
49
  export function renderSiftLightCall(
50
- input: SiftLightInput,
50
+ input: SiftLightRequest,
51
51
  locale: SiftLightLocale,
52
52
  theme: SiftLightTheme,
53
53
  ): Component {
package/src/types.ts CHANGED
@@ -23,6 +23,8 @@ export const ESTIMATED_CHARACTERS_PER_TOKEN = 4;
23
23
  export const DEFAULT_SUMMARY_FILE_LIMIT = 30;
24
24
  export const MAX_SELECTED_PATHS = 20;
25
25
  export const MAX_INSPECT_TARGETS = 5;
26
+ /** A request may name more targets; they are served in pages of MAX_INSPECT_TARGETS. */
27
+ export const MAX_INSPECT_REQUEST_TARGETS = 20;
26
28
  export const MAX_DISPLAYED_OCCURRENCES = 20;
27
29
  export const MAX_STORED_MATCHES = 50_000;
28
30
  export const MAX_STORED_OCCURRENCES = 200_000;
@@ -310,6 +312,8 @@ export interface SiftLightDetails {
310
312
  source?: SourceExcerptDetails;
311
313
  inspections?: InspectBatchItemDetails[];
312
314
  scope?: SearchScopeDetails;
315
+ /** Disclosed request normalizations, such as a field name sent as mode. */
316
+ requestNotes?: string[];
313
317
  redactedCount?: number;
314
318
  redactionRequested?: boolean;
315
319
  redactionApplied?: boolean;