sift-light 1.0.0

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 (145) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/CONTRIBUTING.md +19 -0
  3. package/LICENSE +661 -0
  4. package/README.md +177 -0
  5. package/README.zh-CN.md +177 -0
  6. package/SECURITY.md +25 -0
  7. package/package.json +142 -0
  8. package/plugins/sift-light/.claude-plugin/plugin.json +10 -0
  9. package/plugins/sift-light/.codex-plugin/plugin.json +20 -0
  10. package/plugins/sift-light/.mcp.json +24 -0
  11. package/plugins/sift-light/LICENSE +661 -0
  12. package/plugins/sift-light/concept-worker.mjs +567 -0
  13. package/plugins/sift-light/hooks/THIRD_PARTY_NOTICES.txt +70 -0
  14. package/plugins/sift-light/hooks/hooks.json +16 -0
  15. package/plugins/sift-light/hooks/search-policy.mjs +4113 -0
  16. package/plugins/sift-light/hooks/tree-sitter-bash.wasm +0 -0
  17. package/plugins/sift-light/hooks/tree-sitter-powershell.wasm +0 -0
  18. package/plugins/sift-light/hooks/tree-sitter.wasm +0 -0
  19. package/plugins/sift-light/kimi.plugin.json +36 -0
  20. package/plugins/sift-light/mcp-server.mjs +1 -0
  21. package/plugins/sift-light/omp-extension.mjs +20914 -0
  22. package/plugins/sift-light/package.json +11 -0
  23. package/plugins/sift-light/syntax-worker.mjs +280 -0
  24. package/plugins/sift-light/syntax-worker.toml +2 -0
  25. package/src/analysis-evidence.ts +43 -0
  26. package/src/analysis-limits.ts +27 -0
  27. package/src/analysis-store.ts +559 -0
  28. package/src/analysis-term-pages.ts +73 -0
  29. package/src/analysis-types.ts +162 -0
  30. package/src/audit-search.ts +391 -0
  31. package/src/capped-lines.ts +112 -0
  32. package/src/concept-embedding-cache.ts +186 -0
  33. package/src/concept-inference.ts +218 -0
  34. package/src/concept-model.ts +96 -0
  35. package/src/concept-search.ts +577 -0
  36. package/src/concept-setup.ts +75 -0
  37. package/src/concept-source-generation.ts +268 -0
  38. package/src/concept-worker.mjs +567 -0
  39. package/src/concept-worker.ts +210 -0
  40. package/src/config-reader.ts +283 -0
  41. package/src/config.ts +37 -0
  42. package/src/context-budget.ts +26 -0
  43. package/src/discovery-errors.ts +2 -0
  44. package/src/errors.ts +95 -0
  45. package/src/evidence-candidates.ts +344 -0
  46. package/src/evidence-ranking.ts +48 -0
  47. package/src/evidence-service.ts +1047 -0
  48. package/src/evidence-validation.ts +466 -0
  49. package/src/evidence-validity.ts +33 -0
  50. package/src/excerpt.ts +53 -0
  51. package/src/file-discovery.ts +150 -0
  52. package/src/file-metadata-filter.ts +46 -0
  53. package/src/format.ts +407 -0
  54. package/src/git-diff.ts +195 -0
  55. package/src/git-process.ts +189 -0
  56. package/src/git-repository.ts +367 -0
  57. package/src/git-source.ts +385 -0
  58. package/src/historical-paths.ts +205 -0
  59. package/src/hybrid-search.ts +340 -0
  60. package/src/import-model.ts +611 -0
  61. package/src/import-navigation.ts +100 -0
  62. package/src/import-resolution.ts +240 -0
  63. package/src/index.ts +97 -0
  64. package/src/inspect-batch.ts +265 -0
  65. package/src/inspect.ts +271 -0
  66. package/src/language-capabilities.ts +157 -0
  67. package/src/language-capability-definitions.ts +204 -0
  68. package/src/literal-search.ts +25 -0
  69. package/src/mcp-cli.ts +21 -0
  70. package/src/mcp-model-output.ts +120 -0
  71. package/src/mcp-output.ts +9 -0
  72. package/src/mcp-semantic-judge.ts +39 -0
  73. package/src/mcp-server.mjs +14064 -0
  74. package/src/mcp-server.ts +157 -0
  75. package/src/mcp-stdio.ts +114 -0
  76. package/src/mcp.ts +631 -0
  77. package/src/model-error.ts +120 -0
  78. package/src/multi-term-search.ts +122 -0
  79. package/src/omp-index.ts +277 -0
  80. package/src/omp-marketplace-entry.ts +7 -0
  81. package/src/operation-lifecycle.ts +413 -0
  82. package/src/operation-output.ts +100 -0
  83. package/src/owned-parallel.ts +22 -0
  84. package/src/owned-process.ts +102 -0
  85. package/src/owned-task-queue.ts +38 -0
  86. package/src/package-version.ts +3 -0
  87. package/src/path-policy.ts +203 -0
  88. package/src/pi-search-policy.ts +33 -0
  89. package/src/prompt-guidelines.ts +54 -0
  90. package/src/python-outline.ts +231 -0
  91. package/src/record-value.ts +3 -0
  92. package/src/redaction.ts +95 -0
  93. package/src/request-contract-catalog.ts +215 -0
  94. package/src/request-contract-error.ts +96 -0
  95. package/src/request-contract.ts +481 -0
  96. package/src/request.ts +132 -0
  97. package/src/result-statistics.ts +178 -0
  98. package/src/rg.ts +574 -0
  99. package/src/ripgrep-diagnostics.ts +122 -0
  100. package/src/ripgrep-executable.ts +43 -0
  101. package/src/runtime.ts +53 -0
  102. package/src/scan-revisions.ts +148 -0
  103. package/src/script-runtime.ts +11 -0
  104. package/src/search-policy-commands.ts +170 -0
  105. package/src/search-policy-hook.ts +61 -0
  106. package/src/search-policy-recovery.ts +388 -0
  107. package/src/search-policy-shell.ts +280 -0
  108. package/src/search-policy.ts +102 -0
  109. package/src/search-retention.ts +77 -0
  110. package/src/semantic-judge-batches.ts +135 -0
  111. package/src/semantic-judge.ts +519 -0
  112. package/src/service.ts +945 -0
  113. package/src/session-summary.ts +100 -0
  114. package/src/snapshot-store.ts +203 -0
  115. package/src/source-access.ts +288 -0
  116. package/src/source-continuations.ts +168 -0
  117. package/src/source-document.ts +277 -0
  118. package/src/source-inspection.ts +731 -0
  119. package/src/source-pages.ts +112 -0
  120. package/src/source.ts +286 -0
  121. package/src/structural-search.ts +125 -0
  122. package/src/structure.ts +259 -0
  123. package/src/summary-previews.ts +96 -0
  124. package/src/summary.ts +79 -0
  125. package/src/syntax-facts.ts +464 -0
  126. package/src/syntax-fields.ts +161 -0
  127. package/src/syntax-search.ts +361 -0
  128. package/src/syntax-tree.ts +30 -0
  129. package/src/syntax-types.ts +72 -0
  130. package/src/syntax-worker.mjs +280 -0
  131. package/src/syntax-worker.toml +2 -0
  132. package/src/syntax-worker.ts +143 -0
  133. package/src/syntax.ts +259 -0
  134. package/src/test-navigation-facts.ts +261 -0
  135. package/src/test-navigation.ts +543 -0
  136. package/src/tool-schema.ts +321 -0
  137. package/src/tui/dashboard.ts +107 -0
  138. package/src/tui/layout.ts +291 -0
  139. package/src/tui/palette.ts +73 -0
  140. package/src/tui/presentation.ts +240 -0
  141. package/src/tui/renderers.ts +106 -0
  142. package/src/types.ts +368 -0
  143. package/src/validation-output.ts +148 -0
  144. package/src/validation-types.ts +48 -0
  145. package/src/workspace-files.ts +142 -0
@@ -0,0 +1,102 @@
1
+ import type { SearchKind, ShellLanguage } from "./search-policy-commands.js";
2
+ import { recoverShellSearch } from "./search-policy-recovery.js";
3
+ import { ShellSearchPolicy, type ShellSearchMatch } from "./search-policy-shell.js";
4
+
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.";
7
+ export const PI_REPLACED_SEARCH_TOOLS = new Set(["grep", "find"]);
8
+ export const PREFERRED_SEARCH_GUIDANCE =
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.";
10
+ const contentTools = new Set(["grep", "Grep", "SearchFileContent"]);
11
+ const fileTools = new Set(["find", "glob", "Glob", "GlobFile", "SearchFiles"]);
12
+ const shellTools = new Set([
13
+ "bash",
14
+ "Bash",
15
+ "powershell",
16
+ "PowerShell",
17
+ "Shell",
18
+ "exec_command",
19
+ "shell_command",
20
+ "shell",
21
+ ]);
22
+
23
+ export interface SearchPolicyDecision {
24
+ block: true;
25
+ reason: string;
26
+ }
27
+
28
+ function isInputRecord(input: unknown): input is Record<string, unknown> {
29
+ return typeof input === "object" && input !== null && !Array.isArray(input);
30
+ }
31
+
32
+ function recovery(kind: SearchKind): string {
33
+ return kind === "files"
34
+ ? '{"mode":"files","path":"<scope>"} (query is optional: omit it to list every file under path, supply it only to match known filename or path text, and use glob for name patterns)'
35
+ : '{"pattern":"<search text>","path":"<scope>","scope":"strict"}';
36
+ }
37
+
38
+ function blockedMatch(
39
+ command: string,
40
+ match: ShellSearchMatch,
41
+ workingDirectory: string | undefined,
42
+ ): SearchPolicyDecision {
43
+ const location =
44
+ match.nestedDepth > 0
45
+ ? `nested ${match.language} command #${match.commandIndex}`
46
+ : `${match.language} subcommand #${match.commandIndex}`;
47
+ const recovered = recoverShellSearch(command, match, workingDirectory);
48
+ const repair =
49
+ recovered.kind === "concrete"
50
+ ? `retry exactly once through sift-light (possibly MCP-prefixed) with ${recovered.request}`
51
+ : `an equivalent request was not generated because ${recovered.reason}; manually translate the search, then retry exactly once through sift-light (possibly MCP-prefixed) with ${recovery(match.kind)}`;
52
+ return {
53
+ block: true,
54
+ reason: `sift-light search policy blocked direct ${match.kind} search at ${location} (${match.command} …, bytes ${match.startByte}-${match.endByte}); the atomic shell call did not run. Split out non-search operations, then ${repair}. Do not include this denial in the retry, repeat it through another shell/script, or weaken the search. If sift-light is unavailable, report that connection error once without attempting another search.`,
55
+ };
56
+ }
57
+
58
+ function blockedTool(kind: SearchKind, toolName: string): SearchPolicyDecision {
59
+ return {
60
+ block: true,
61
+ reason: `sift-light search policy blocked direct ${kind} tool ${toolName}; it did not run. Retry exactly once through sift-light (possibly MCP-prefixed) with ${recovery(kind)}. Do not include this denial in the retry, use another search entry, or weaken the search. If sift-light is unavailable, report that connection error once without attempting another search.`,
62
+ };
63
+ }
64
+
65
+ export class SearchPolicy {
66
+ readonly #shell: ShellSearchPolicy;
67
+ constructor(assets: URL) {
68
+ this.#shell = new ShellSearchPolicy(assets);
69
+ }
70
+
71
+ async check(
72
+ toolName: string,
73
+ input: unknown,
74
+ signal?: AbortSignal,
75
+ ): Promise<SearchPolicyDecision | undefined> {
76
+ signal?.throwIfAborted();
77
+ if (contentTools.has(toolName)) return blockedTool("content", toolName);
78
+ if (fileTools.has(toolName)) return blockedTool("files", toolName);
79
+ if (!shellTools.has(toolName)) return undefined;
80
+ if (!isInputRecord(input)) throw new Error("Search policy expected a tool input object");
81
+ const fields = input;
82
+ const command = fields.command ?? fields.cmd;
83
+ if (typeof command !== "string")
84
+ throw new Error("Search policy expected a shell command string");
85
+ const shell = typeof fields.shell === "string" ? fields.shell : "";
86
+ const workingDirectoryValue = fields.workdir ?? fields.cwd;
87
+ const workingDirectory =
88
+ workingDirectoryValue === undefined
89
+ ? process.cwd()
90
+ : typeof workingDirectoryValue === "string" && workingDirectoryValue.length > 0
91
+ ? workingDirectoryValue
92
+ : undefined;
93
+ const language: ShellLanguage =
94
+ /powershell|pwsh/iu.test(`${toolName} ${shell}`) ||
95
+ (process.platform === "win32" && toolName !== "bash")
96
+ ? "powershell"
97
+ : "bash";
98
+ const match = await this.#shell.inspect(command, language);
99
+ signal?.throwIfAborted();
100
+ return match ? blockedMatch(command, match, workingDirectory) : undefined;
101
+ }
102
+ }
@@ -0,0 +1,77 @@
1
+ import { SiftLightError } from "./errors.js";
2
+ import {
3
+ MAX_SEARCH_STORAGE_BYTES,
4
+ MAX_STORED_OCCURRENCES,
5
+ type MatchRecord,
6
+ type SearchRetentionDetails,
7
+ } from "./types.js";
8
+
9
+ /** Accounting bounds serialized evidence and occurrence cardinality, not V8 heap bytes. */
10
+ export class SearchRetention {
11
+ #bytes = 0;
12
+ #metadataBytes = 0;
13
+ #metadataLimitReached = false;
14
+ #occurrences = 0;
15
+ readonly #reasons = new Set<string>();
16
+ readonly maxBytes: number;
17
+ readonly maxOccurrences: number;
18
+ constructor(maxBytes = MAX_SEARCH_STORAGE_BYTES, maxOccurrences = MAX_STORED_OCCURRENCES) {
19
+ this.maxBytes = maxBytes;
20
+ this.maxOccurrences = maxOccurrences;
21
+ for (const value of [maxBytes, maxOccurrences]) {
22
+ if (!Number.isSafeInteger(value) || value < 1)
23
+ throw new SiftLightError("Search retention limits must be positive safe integers");
24
+ }
25
+ }
26
+
27
+ file(displayPath: string, absolutePath: string): boolean {
28
+ if (this.#metadataLimitReached) return false;
29
+ // Reserve the count, revision and path metadata before admitting this file's matches.
30
+ const bytes = Buffer.byteLength(JSON.stringify([displayPath, absolutePath])) + 512;
31
+ const metadataLimit = Math.floor(this.maxBytes / 4);
32
+ if (this.#metadataBytes + bytes > metadataLimit) {
33
+ this.#metadataLimitReached = true;
34
+ this.#reasons.add(
35
+ `File-summary retention reached its ${String(metadataLimit)}-byte share of the search budget; file summaries are partial; narrow the path or filters`,
36
+ );
37
+ return false;
38
+ }
39
+ this.#bytes += bytes;
40
+ this.#metadataBytes += bytes;
41
+ return true;
42
+ }
43
+
44
+ canRetainOccurrences(count: number): boolean {
45
+ if (this.#occurrences + count <= this.maxOccurrences) return true;
46
+ this.#reasons.add(`Occurrence retention reached the ${String(this.maxOccurrences)} limit`);
47
+ return false;
48
+ }
49
+
50
+ retain(match: MatchRecord): boolean {
51
+ if (!this.canRetainOccurrences(match.occurrences.length)) return false;
52
+ const bytes = Buffer.byteLength(JSON.stringify(match)) + 1;
53
+ if (this.#bytes - this.#metadataBytes + bytes > this.maxBytes - Math.floor(this.maxBytes / 4)) {
54
+ this.#reasons.add(
55
+ `Search evidence retention reached the ${String(this.maxBytes)}-byte limit`,
56
+ );
57
+ return false;
58
+ }
59
+ this.#bytes += bytes;
60
+ this.#occurrences += match.occurrences.length;
61
+ return true;
62
+ }
63
+
64
+ noteLimit(reason: string): void {
65
+ this.#reasons.add(reason);
66
+ }
67
+
68
+ get details(): SearchRetentionDetails {
69
+ return {
70
+ accountedBytes: this.#bytes,
71
+ retainedOccurrences: this.#occurrences,
72
+ maxBytes: this.maxBytes,
73
+ maxOccurrences: this.maxOccurrences,
74
+ reasons: [...this.#reasons],
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,135 @@
1
+ import { SiftLightError } from "./errors.js";
2
+ import type {
3
+ SemanticJudgeCandidate,
4
+ SemanticJudgeResult,
5
+ SemanticJudgeRunner,
6
+ } from "./semantic-judge.js";
7
+
8
+ const MAX_SEMANTIC_JUDGE_BATCH_CANDIDATES = 8;
9
+ export const MAX_SEMANTIC_JUDGE_REQUEST_BYTES = 64 * 1024;
10
+
11
+ export class SemanticJudgePayloadTooLargeError extends SiftLightError {
12
+ constructor(message: string) {
13
+ super(message);
14
+ this.name = "SemanticJudgePayloadTooLargeError";
15
+ }
16
+ }
17
+
18
+ interface SemanticJudgeBatch {
19
+ startIndex: number;
20
+ candidates: SemanticJudgeCandidate[];
21
+ }
22
+
23
+ export interface SemanticJudgeBatchSuccess extends SemanticJudgeBatch {
24
+ result: SemanticJudgeResult;
25
+ }
26
+
27
+ export interface SemanticJudgeBatchFailure extends SemanticJudgeBatch {
28
+ error: unknown;
29
+ }
30
+
31
+ export interface SemanticJudgeBatchExecution {
32
+ successes: SemanticJudgeBatchSuccess[];
33
+ failures: SemanticJudgeBatchFailure[];
34
+ attempts: number;
35
+ splits: number;
36
+ }
37
+
38
+ export function createSemanticJudgeBatches(
39
+ candidates: readonly SemanticJudgeCandidate[],
40
+ serializedBytes: (candidates: readonly SemanticJudgeCandidate[]) => number,
41
+ ): SemanticJudgeBatch[] {
42
+ const batches: SemanticJudgeBatch[] = [];
43
+ let startIndex = 0;
44
+ let current: SemanticJudgeCandidate[] = [];
45
+ for (const [index, candidate] of candidates.entries()) {
46
+ const proposed = [...current, candidate];
47
+ const exceedsCount = proposed.length > MAX_SEMANTIC_JUDGE_BATCH_CANDIDATES;
48
+ const exceedsBytes = serializedBytes(proposed) > MAX_SEMANTIC_JUDGE_REQUEST_BYTES;
49
+ if (current.length > 0 && (exceedsCount || exceedsBytes)) {
50
+ batches.push({ startIndex, candidates: current });
51
+ startIndex = index;
52
+ current = [candidate];
53
+ } else {
54
+ current = proposed;
55
+ }
56
+ }
57
+ if (current.length > 0) batches.push({ startIndex, candidates: current });
58
+ return batches;
59
+ }
60
+
61
+ function combineBatchExecutions(
62
+ executions: readonly SemanticJudgeBatchExecution[],
63
+ ): SemanticJudgeBatchExecution {
64
+ return {
65
+ successes: executions.flatMap((execution) => execution.successes),
66
+ failures: executions.flatMap((execution) => execution.failures),
67
+ attempts: executions.reduce((total, execution) => total + execution.attempts, 0),
68
+ splits: executions.reduce((total, execution) => total + execution.splits, 0),
69
+ };
70
+ }
71
+
72
+ async function settledBatchExecutions(
73
+ operations: readonly Promise<SemanticJudgeBatchExecution>[],
74
+ ): Promise<SemanticJudgeBatchExecution[]> {
75
+ const settled = await Promise.allSettled(operations);
76
+ const executions: SemanticJudgeBatchExecution[] = [];
77
+ let hasRejection = false;
78
+ let rejection: unknown;
79
+ for (const result of settled) {
80
+ if (result.status === "rejected") {
81
+ if (!hasRejection) rejection = result.reason;
82
+ hasRejection = true;
83
+ } else executions.push(result.value);
84
+ }
85
+ if (hasRejection) throw rejection;
86
+ return executions;
87
+ }
88
+
89
+ async function judgeBatchWithSplit(
90
+ runner: SemanticJudgeRunner,
91
+ query: string,
92
+ batch: SemanticJudgeBatch,
93
+ signal?: AbortSignal,
94
+ ): Promise<SemanticJudgeBatchExecution> {
95
+ try {
96
+ const result = await runner(query, batch.candidates, signal);
97
+ return { successes: [{ ...batch, result }], failures: [], attempts: 1, splits: 0 };
98
+ } catch (error) {
99
+ if (signal?.aborted) throw error;
100
+ if (error instanceof SemanticJudgePayloadTooLargeError && batch.candidates.length > 1) {
101
+ const midpoint = Math.ceil(batch.candidates.length / 2);
102
+ const left = await judgeBatchWithSplit(
103
+ runner,
104
+ query,
105
+ { startIndex: batch.startIndex, candidates: batch.candidates.slice(0, midpoint) },
106
+ signal,
107
+ );
108
+ const right = await judgeBatchWithSplit(
109
+ runner,
110
+ query,
111
+ {
112
+ startIndex: batch.startIndex + midpoint,
113
+ candidates: batch.candidates.slice(midpoint),
114
+ },
115
+ signal,
116
+ );
117
+ const combined = combineBatchExecutions([left, right]);
118
+ return { ...combined, attempts: combined.attempts + 1, splits: combined.splits + 1 };
119
+ }
120
+ return { successes: [], failures: [{ ...batch, error }], attempts: 1, splits: 0 };
121
+ }
122
+ }
123
+
124
+ export async function judgeSemanticCandidateBatches(
125
+ runner: SemanticJudgeRunner,
126
+ query: string,
127
+ batches: readonly SemanticJudgeBatch[],
128
+ signal?: AbortSignal,
129
+ ): Promise<SemanticJudgeBatchExecution> {
130
+ return combineBatchExecutions(
131
+ await settledBatchExecutions(
132
+ batches.map((batch) => judgeBatchWithSplit(runner, query, batch, signal)),
133
+ ),
134
+ );
135
+ }