sift-light 1.0.3-2 → 1.0.3-4

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.
@@ -0,0 +1,174 @@
1
+ import { executableName, type ShellLanguage } from "./search-policy-commands.js";
2
+
3
+ // Only options whose operands cannot name an input file are accepted here. Unknown
4
+ // options fail closed, since grep variants and PowerShell aliases can add file inputs.
5
+ const GREP_SWITCHES = new Set("abcEFGHhiILlnoPqsTUuvwxyzZ".split(""));
6
+ const GREP_VALUE_OPTIONS = new Set(["A", "B", "C", "m"]);
7
+ const GREP_LONG_SWITCHES = new Set([
8
+ "--basic-regexp",
9
+ "--extended-regexp",
10
+ "--fixed-strings",
11
+ "--perl-regexp",
12
+ "--ignore-case",
13
+ "--no-ignore-case",
14
+ "--invert-match",
15
+ "--word-regexp",
16
+ "--line-regexp",
17
+ "--count",
18
+ "--files-with-matches",
19
+ "--files-without-match",
20
+ "--only-matching",
21
+ "--quiet",
22
+ "--silent",
23
+ "--no-messages",
24
+ "--byte-offset",
25
+ "--line-number",
26
+ "--with-filename",
27
+ "--no-filename",
28
+ "--initial-tab",
29
+ "--null",
30
+ "--null-data",
31
+ "--text",
32
+ "--binary",
33
+ "--line-buffered",
34
+ "--no-group-separator",
35
+ ]);
36
+ const GREP_LONG_VALUES = new Set([
37
+ "--after-context",
38
+ "--before-context",
39
+ "--context",
40
+ "--max-count",
41
+ "--binary-files",
42
+ "--label",
43
+ "--group-separator",
44
+ "--include",
45
+ "--exclude",
46
+ "--exclude-dir",
47
+ ]);
48
+
49
+ function isStdinOnlyGrep(args: readonly (string | null)[]): boolean {
50
+ const positionals: string[] = [];
51
+ let hasPatternOption = false;
52
+ let optionsEnded = false;
53
+ for (let index = 0; index < args.length; index += 1) {
54
+ const arg = args[index];
55
+ if (arg === null || arg === undefined) return false;
56
+ // Some grep implementations (and GNU grep with POSIXLY_CORRECT) stop option
57
+ // parsing at the first operand. Later option-looking words can be file names.
58
+ if (positionals.length > 0 && arg !== "-") return false;
59
+ if (!optionsEnded && arg === "--") {
60
+ optionsEnded = true;
61
+ continue;
62
+ }
63
+ if (!optionsEnded && arg.startsWith("--")) {
64
+ const separator = arg.indexOf("=");
65
+ const option = separator < 0 ? arg : arg.slice(0, separator);
66
+ const attached = separator < 0 ? undefined : arg.slice(separator + 1);
67
+ if (option === "--regexp") {
68
+ hasPatternOption = true;
69
+ } else if (option === "--color" || option === "--colour") {
70
+ continue;
71
+ } else if (GREP_LONG_SWITCHES.has(option) && attached === undefined) {
72
+ continue;
73
+ } else if (!GREP_LONG_VALUES.has(option)) {
74
+ return false;
75
+ }
76
+ if (attached === undefined) {
77
+ index += 1;
78
+ if (args[index] === null || args[index] === undefined) return false;
79
+ }
80
+ continue;
81
+ }
82
+ if (!optionsEnded && arg.startsWith("-") && arg !== "-") {
83
+ for (let optionIndex = 1; optionIndex < arg.length; optionIndex += 1) {
84
+ const option = arg[optionIndex];
85
+ if (option === "e") {
86
+ hasPatternOption = true;
87
+ } else if (option !== undefined && GREP_SWITCHES.has(option)) {
88
+ continue;
89
+ } else if (option === undefined || !GREP_VALUE_OPTIONS.has(option)) {
90
+ return false;
91
+ }
92
+ if (optionIndex === arg.length - 1) {
93
+ index += 1;
94
+ if (args[index] === null || args[index] === undefined) return false;
95
+ }
96
+ break;
97
+ }
98
+ continue;
99
+ }
100
+ positionals.push(arg);
101
+ }
102
+ return hasPatternOption
103
+ ? positionals.every((operand) => operand === "-")
104
+ : positionals.length > 0 && positionals.slice(1).every((operand) => operand === "-");
105
+ }
106
+
107
+ const SELECT_STRING_SWITCHES = new Set([
108
+ "simplematch",
109
+ "casesensitive",
110
+ "quiet",
111
+ "list",
112
+ "noemphasis",
113
+ "notmatch",
114
+ "allmatches",
115
+ "raw",
116
+ "verbose",
117
+ "debug",
118
+ ]);
119
+ const SELECT_STRING_VALUES = new Set([
120
+ "culture",
121
+ "encoding",
122
+ "context",
123
+ "erroraction",
124
+ "warningaction",
125
+ "informationaction",
126
+ "outvariable",
127
+ "pipelinevariable",
128
+ ]);
129
+
130
+ function isPipelineOnlySelectString(
131
+ args: readonly (string | null)[],
132
+ namedParameters: readonly boolean[],
133
+ ): boolean {
134
+ let hasPattern = false;
135
+ for (let index = 0; index < args.length; index += 1) {
136
+ const arg = args[index];
137
+ if (arg === null || arg === undefined) return false;
138
+ if (!namedParameters[index]) {
139
+ if (hasPattern) return false; // Further positional values can bind to -Path.
140
+ hasPattern = true;
141
+ continue;
142
+ }
143
+ const separator = arg.indexOf(":");
144
+ const option = (separator < 0 ? arg.slice(1) : arg.slice(1, separator)).toLowerCase();
145
+ if (SELECT_STRING_SWITCHES.has(option) && separator < 0) continue;
146
+ if (option !== "pattern" && !SELECT_STRING_VALUES.has(option)) return false;
147
+ if (option === "pattern") {
148
+ if (hasPattern) return false;
149
+ hasPattern = true;
150
+ }
151
+ if (separator < 0) {
152
+ index += 1;
153
+ if (args[index] === null || args[index] === undefined) return false;
154
+ }
155
+ }
156
+ return hasPattern;
157
+ }
158
+
159
+ /** The exception is for filters consuming the pipeline, never for file operands. */
160
+ export function isStdinOnlySearchFilter(
161
+ words: readonly (string | null)[],
162
+ language: ShellLanguage,
163
+ namedParameters: readonly boolean[],
164
+ ): boolean {
165
+ const executable = words[0];
166
+ if (executable === null || executable === undefined) return false;
167
+ const name = executableName(executable);
168
+ if (language === "powershell")
169
+ return (
170
+ ["select-string", "sls"].includes(name.toLowerCase()) &&
171
+ isPipelineOnlySelectString(words.slice(1), namedParameters.slice(1))
172
+ );
173
+ return ["grep", "egrep", "fgrep"].includes(name) && isStdinOnlyGrep(words.slice(1));
174
+ }
@@ -3,7 +3,7 @@ import { recoverShellSearch } from "./search-policy-recovery.js";
3
3
  import { ShellSearchPolicy, type ShellSearchMatch } from "./search-policy-shell.js";
4
4
 
5
5
  export const SEARCH_POLICY_GUIDANCE =
6
- "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; filtering output from an unrelated producer at a pipeline tail remains available. Use pattern for contents or mode=files with query for filenames. Keep read/edit/write, tests and builds available. After a denial, call sift-light once with the stated repair; do not paste the denial into the request, repeat the blocked call, use another shell/custom script, or weaken the search mode.";
6
+ "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; recognized stdin-only filtering of output from an unrelated producer remains available at any pipeline position. File operands, recursive search options, file-sourced patterns and input redirection are not filter exceptions. Use pattern for contents or mode=files with query for filenames. Keep read/edit/write, tests and builds available. After a denial, call sift-light once with the stated repair; do not paste the denial into the request, repeat the blocked call, use another shell/custom script, or weaken the search mode.";
7
7
  export const PI_REPLACED_SEARCH_TOOLS = new Set(["grep", "find"]);
8
8
  export const PREFERRED_SEARCH_GUIDANCE =
9
9
  "Prefer sift-light for local content and filename searches because it provides bounded evidence, coverage and continuation details. Conventional search entries remain available in advisory mode.";
package/src/service.ts CHANGED
@@ -598,7 +598,11 @@ export class SiftLightService {
598
598
 
599
599
  const request = normalizeRequest(input);
600
600
  let scan = await this.#runRipgrep(request, cwd, signal);
601
- if (scan.totalMatches === 0 && request.path !== undefined && request.scope !== "strict") {
601
+ if (
602
+ scan.totalMatches === 0 &&
603
+ request.path !== undefined &&
604
+ (await new SearchPathPolicy(cwd).allowsScopeExpansion(request.path, request.scope))
605
+ ) {
602
606
  const { path: requestedPath, ...projectRequest } = request;
603
607
  scan = await this.#runRipgrep(
604
608
  { ...projectRequest, expandedFromPath: requestedPath },
@@ -55,6 +55,7 @@ export async function listWorkspaceFiles(
55
55
  const absolutePath = resolve(cwd, options.path ?? ".");
56
56
  const policy = new SearchPathPolicy(cwd);
57
57
  const searchPath = await policy.resolveSearchTarget(absolutePath);
58
+ const ripgrepCwd = await policy.ripgrepWorkingDirectory(searchPath);
58
59
  const maxFiles = options.maxFiles ?? MAX_SOURCE_REVISION_FILES;
59
60
  if (!Number.isSafeInteger(maxFiles) || maxFiles < 1)
60
61
  throw new SiftLightError("Candidate file limit must be a positive integer");
@@ -81,7 +82,7 @@ export async function listWorkspaceFiles(
81
82
  "--",
82
83
  searchPath,
83
84
  ],
84
- cwd,
85
+ cwd: ripgrepCwd,
85
86
  ...(signal ? { signal } : {}),
86
87
  },
87
88
  async (stdout) => {