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.
- package/CHANGELOG.md +0 -0
- package/CONTRIBUTING.md +19 -0
- package/LICENSE +661 -0
- package/README.md +177 -0
- package/README.zh-CN.md +177 -0
- package/SECURITY.md +25 -0
- package/package.json +142 -0
- package/plugins/sift-light/.claude-plugin/plugin.json +10 -0
- package/plugins/sift-light/.codex-plugin/plugin.json +20 -0
- package/plugins/sift-light/.mcp.json +24 -0
- package/plugins/sift-light/LICENSE +661 -0
- package/plugins/sift-light/concept-worker.mjs +567 -0
- package/plugins/sift-light/hooks/THIRD_PARTY_NOTICES.txt +70 -0
- package/plugins/sift-light/hooks/hooks.json +16 -0
- package/plugins/sift-light/hooks/search-policy.mjs +4113 -0
- package/plugins/sift-light/hooks/tree-sitter-bash.wasm +0 -0
- package/plugins/sift-light/hooks/tree-sitter-powershell.wasm +0 -0
- package/plugins/sift-light/hooks/tree-sitter.wasm +0 -0
- package/plugins/sift-light/kimi.plugin.json +36 -0
- package/plugins/sift-light/mcp-server.mjs +1 -0
- package/plugins/sift-light/omp-extension.mjs +20914 -0
- package/plugins/sift-light/package.json +11 -0
- package/plugins/sift-light/syntax-worker.mjs +280 -0
- package/plugins/sift-light/syntax-worker.toml +2 -0
- package/src/analysis-evidence.ts +43 -0
- package/src/analysis-limits.ts +27 -0
- package/src/analysis-store.ts +559 -0
- package/src/analysis-term-pages.ts +73 -0
- package/src/analysis-types.ts +162 -0
- package/src/audit-search.ts +391 -0
- package/src/capped-lines.ts +112 -0
- package/src/concept-embedding-cache.ts +186 -0
- package/src/concept-inference.ts +218 -0
- package/src/concept-model.ts +96 -0
- package/src/concept-search.ts +577 -0
- package/src/concept-setup.ts +75 -0
- package/src/concept-source-generation.ts +268 -0
- package/src/concept-worker.mjs +567 -0
- package/src/concept-worker.ts +210 -0
- package/src/config-reader.ts +283 -0
- package/src/config.ts +37 -0
- package/src/context-budget.ts +26 -0
- package/src/discovery-errors.ts +2 -0
- package/src/errors.ts +95 -0
- package/src/evidence-candidates.ts +344 -0
- package/src/evidence-ranking.ts +48 -0
- package/src/evidence-service.ts +1047 -0
- package/src/evidence-validation.ts +466 -0
- package/src/evidence-validity.ts +33 -0
- package/src/excerpt.ts +53 -0
- package/src/file-discovery.ts +150 -0
- package/src/file-metadata-filter.ts +46 -0
- package/src/format.ts +407 -0
- package/src/git-diff.ts +195 -0
- package/src/git-process.ts +189 -0
- package/src/git-repository.ts +367 -0
- package/src/git-source.ts +385 -0
- package/src/historical-paths.ts +205 -0
- package/src/hybrid-search.ts +340 -0
- package/src/import-model.ts +611 -0
- package/src/import-navigation.ts +100 -0
- package/src/import-resolution.ts +240 -0
- package/src/index.ts +97 -0
- package/src/inspect-batch.ts +265 -0
- package/src/inspect.ts +271 -0
- package/src/language-capabilities.ts +157 -0
- package/src/language-capability-definitions.ts +204 -0
- package/src/literal-search.ts +25 -0
- package/src/mcp-cli.ts +21 -0
- package/src/mcp-model-output.ts +120 -0
- package/src/mcp-output.ts +9 -0
- package/src/mcp-semantic-judge.ts +39 -0
- package/src/mcp-server.mjs +14064 -0
- package/src/mcp-server.ts +157 -0
- package/src/mcp-stdio.ts +114 -0
- package/src/mcp.ts +631 -0
- package/src/model-error.ts +120 -0
- package/src/multi-term-search.ts +122 -0
- package/src/omp-index.ts +277 -0
- package/src/omp-marketplace-entry.ts +7 -0
- package/src/operation-lifecycle.ts +413 -0
- package/src/operation-output.ts +100 -0
- package/src/owned-parallel.ts +22 -0
- package/src/owned-process.ts +102 -0
- package/src/owned-task-queue.ts +38 -0
- package/src/package-version.ts +3 -0
- package/src/path-policy.ts +203 -0
- package/src/pi-search-policy.ts +33 -0
- package/src/prompt-guidelines.ts +54 -0
- package/src/python-outline.ts +231 -0
- package/src/record-value.ts +3 -0
- package/src/redaction.ts +95 -0
- package/src/request-contract-catalog.ts +215 -0
- package/src/request-contract-error.ts +96 -0
- package/src/request-contract.ts +481 -0
- package/src/request.ts +132 -0
- package/src/result-statistics.ts +178 -0
- package/src/rg.ts +574 -0
- package/src/ripgrep-diagnostics.ts +122 -0
- package/src/ripgrep-executable.ts +43 -0
- package/src/runtime.ts +53 -0
- package/src/scan-revisions.ts +148 -0
- package/src/script-runtime.ts +11 -0
- package/src/search-policy-commands.ts +170 -0
- package/src/search-policy-hook.ts +61 -0
- package/src/search-policy-recovery.ts +388 -0
- package/src/search-policy-shell.ts +280 -0
- package/src/search-policy.ts +102 -0
- package/src/search-retention.ts +77 -0
- package/src/semantic-judge-batches.ts +135 -0
- package/src/semantic-judge.ts +519 -0
- package/src/service.ts +945 -0
- package/src/session-summary.ts +100 -0
- package/src/snapshot-store.ts +203 -0
- package/src/source-access.ts +288 -0
- package/src/source-continuations.ts +168 -0
- package/src/source-document.ts +277 -0
- package/src/source-inspection.ts +731 -0
- package/src/source-pages.ts +112 -0
- package/src/source.ts +286 -0
- package/src/structural-search.ts +125 -0
- package/src/structure.ts +259 -0
- package/src/summary-previews.ts +96 -0
- package/src/summary.ts +79 -0
- package/src/syntax-facts.ts +464 -0
- package/src/syntax-fields.ts +161 -0
- package/src/syntax-search.ts +361 -0
- package/src/syntax-tree.ts +30 -0
- package/src/syntax-types.ts +72 -0
- package/src/syntax-worker.mjs +280 -0
- package/src/syntax-worker.toml +2 -0
- package/src/syntax-worker.ts +143 -0
- package/src/syntax.ts +259 -0
- package/src/test-navigation-facts.ts +261 -0
- package/src/test-navigation.ts +543 -0
- package/src/tool-schema.ts +321 -0
- package/src/tui/dashboard.ts +107 -0
- package/src/tui/layout.ts +291 -0
- package/src/tui/palette.ts +73 -0
- package/src/tui/presentation.ts +240 -0
- package/src/tui/renderers.ts +106 -0
- package/src/types.ts +368 -0
- package/src/validation-output.ts +148 -0
- package/src/validation-types.ts +48 -0
- package/src/workspace-files.ts +142 -0
package/src/inspect.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { abortError, CursorError, SiftLightError } from "./errors.js";
|
|
3
|
+
import { SearchPathPolicy } from "./path-policy.js";
|
|
4
|
+
import type { CodeStructureProvider, StructureInspection } from "./structure.js";
|
|
5
|
+
import {
|
|
6
|
+
assertExistingSearchPath,
|
|
7
|
+
getSourceRevision,
|
|
8
|
+
readSourceRange,
|
|
9
|
+
sameSourceRevision,
|
|
10
|
+
SourceBudgetTooSmallError,
|
|
11
|
+
SourceTooLargeError,
|
|
12
|
+
type SourceRangeRead,
|
|
13
|
+
} from "./source.js";
|
|
14
|
+
import { SnapshotStore } from "./snapshot-store.js";
|
|
15
|
+
import {
|
|
16
|
+
MAX_LINE_CHARACTERS,
|
|
17
|
+
MAX_RESULT_BYTES,
|
|
18
|
+
type MatchRecord,
|
|
19
|
+
type SiftLightDetails,
|
|
20
|
+
type SiftLightResult,
|
|
21
|
+
type SourceExcerptDetails,
|
|
22
|
+
type SourceRevision,
|
|
23
|
+
type StructureDetails,
|
|
24
|
+
} from "./types.js";
|
|
25
|
+
|
|
26
|
+
export interface InspectInput {
|
|
27
|
+
path?: string;
|
|
28
|
+
line?: number;
|
|
29
|
+
cursor?: string;
|
|
30
|
+
matchIndex?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface InspectOptions {
|
|
34
|
+
snapshots: SnapshotStore;
|
|
35
|
+
structure?: CodeStructureProvider;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface InspectionTarget {
|
|
39
|
+
path: string;
|
|
40
|
+
absolutePath: string;
|
|
41
|
+
line: number;
|
|
42
|
+
retainedMatch?: MatchRecord;
|
|
43
|
+
expectedRevision?: SourceRevision;
|
|
44
|
+
unverified: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface InspectionEvidence {
|
|
48
|
+
target: InspectionTarget;
|
|
49
|
+
structure: StructureDetails;
|
|
50
|
+
source?: SourceRangeRead;
|
|
51
|
+
revision?: SourceRevision;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveInspectionTarget(
|
|
55
|
+
input: InspectInput,
|
|
56
|
+
cwd: string,
|
|
57
|
+
snapshots: SnapshotStore,
|
|
58
|
+
): InspectionTarget {
|
|
59
|
+
let path = input.path?.replace(/^@/, "");
|
|
60
|
+
let line = input.line;
|
|
61
|
+
let retainedMatch: MatchRecord | undefined;
|
|
62
|
+
if (input.matchIndex !== undefined) {
|
|
63
|
+
if (!input.cursor) throw new SiftLightError("matchIndex requires a cursor when mode=inspect");
|
|
64
|
+
if (input.path !== undefined || input.line !== undefined) {
|
|
65
|
+
throw new SiftLightError("matchIndex replaces path and line when mode=inspect");
|
|
66
|
+
}
|
|
67
|
+
if (!Number.isSafeInteger(input.matchIndex) || input.matchIndex < 1) {
|
|
68
|
+
throw new SiftLightError("matchIndex must be a positive integer when mode=inspect");
|
|
69
|
+
}
|
|
70
|
+
const { snapshot } = snapshots.resolve(input.cursor);
|
|
71
|
+
retainedMatch = snapshot.matches[input.matchIndex - 1];
|
|
72
|
+
if (!retainedMatch) {
|
|
73
|
+
throw new CursorError(
|
|
74
|
+
`matchIndex is ${snapshot.snapshotComplete ? "outside this snapshot" : "not retained in this partial snapshot"}.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
path = retainedMatch.displayPath;
|
|
78
|
+
line = retainedMatch.lineNumber;
|
|
79
|
+
}
|
|
80
|
+
if (!path) throw new SiftLightError("path is required when mode=inspect");
|
|
81
|
+
if (line === undefined || !Number.isSafeInteger(line) || line < 1) {
|
|
82
|
+
throw new SiftLightError("line must be a positive integer when mode=inspect");
|
|
83
|
+
}
|
|
84
|
+
const absolutePath = retainedMatch?.absolutePath ?? resolve(cwd, path);
|
|
85
|
+
new SearchPathPolicy(cwd).assertPath(absolutePath);
|
|
86
|
+
let expectedRevision: SourceRevision | undefined;
|
|
87
|
+
if (input.cursor) {
|
|
88
|
+
const { snapshot } = snapshots.resolve(input.cursor);
|
|
89
|
+
retainedMatch ??= snapshot.matches.find(
|
|
90
|
+
(match) => match.absolutePath === absolutePath && match.lineNumber === line,
|
|
91
|
+
);
|
|
92
|
+
if (!retainedMatch) {
|
|
93
|
+
throw new CursorError("The requested line is not a retained match in this snapshot.");
|
|
94
|
+
}
|
|
95
|
+
expectedRevision = snapshot.sourceRevisions.get(absolutePath);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
path,
|
|
99
|
+
absolutePath,
|
|
100
|
+
line,
|
|
101
|
+
unverified: input.cursor !== undefined && expectedRevision === undefined,
|
|
102
|
+
...(retainedMatch ? { retainedMatch } : {}),
|
|
103
|
+
...(expectedRevision ? { expectedRevision } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function sourceExcerptDetails(source: SourceRangeRead): SourceExcerptDetails {
|
|
108
|
+
return {
|
|
109
|
+
range: { startLine: source.startLine, endLine: source.endLine },
|
|
110
|
+
omittedBefore: source.omittedBefore,
|
|
111
|
+
omittedAfter: source.omittedAfter,
|
|
112
|
+
truncatedLines: source.truncatedLines,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function sourceTruncationText(source: SourceRangeRead): string {
|
|
117
|
+
const range = source.truncated
|
|
118
|
+
? `\n[source range centered on target; omitted ${String(source.omittedBefore)} lines before and ${String(source.omittedAfter)} lines after]`
|
|
119
|
+
: "";
|
|
120
|
+
const lines =
|
|
121
|
+
source.truncatedLines.length > 0
|
|
122
|
+
? `\n[source line excerpts truncated: ${source.truncatedLines.join(", ")}; maximum ${String(MAX_LINE_CHARACTERS)} source characters per line]`
|
|
123
|
+
: "";
|
|
124
|
+
return range + lines;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function inspectionDescription(evidence: InspectionEvidence): string {
|
|
128
|
+
const symbol = evidence.structure.symbol;
|
|
129
|
+
return symbol
|
|
130
|
+
? `${symbol.scope.length > 0 ? `${symbol.scope.join(".")}.` : ""}${symbol.name} (${symbol.kind}) lines ${symbol.range.startLine}-${symbol.range.endLine}`
|
|
131
|
+
: `No enclosing symbol found for line ${String(evidence.target.line)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function unavailableEvidence(
|
|
135
|
+
target: InspectionTarget,
|
|
136
|
+
status: StructureDetails["status"],
|
|
137
|
+
provider?: string,
|
|
138
|
+
): InspectionEvidence {
|
|
139
|
+
return { target, structure: { status, ...(provider ? { provider } : {}) } };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isUnavailableSourceError(error: unknown): boolean {
|
|
143
|
+
return (
|
|
144
|
+
error instanceof Error &&
|
|
145
|
+
"code" in error &&
|
|
146
|
+
["ENOENT", "EACCES", "EPERM", "ENOTDIR", "EISDIR"].includes(String(error.code))
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function inspectSourceEvidence(
|
|
151
|
+
target: InspectionTarget,
|
|
152
|
+
cwd: string,
|
|
153
|
+
signal: AbortSignal | undefined,
|
|
154
|
+
options: InspectOptions,
|
|
155
|
+
maxSourceBytes?: number,
|
|
156
|
+
): Promise<InspectionEvidence> {
|
|
157
|
+
if (signal?.aborted) throw abortError();
|
|
158
|
+
try {
|
|
159
|
+
await assertExistingSearchPath(target.absolutePath, cwd);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (signal?.aborted) throw abortError();
|
|
162
|
+
if (isUnavailableSourceError(error)) return unavailableEvidence(target, "source-unavailable");
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
if (signal?.aborted) throw abortError();
|
|
166
|
+
if (target.unverified) return unavailableEvidence(target, "source-unavailable");
|
|
167
|
+
let inspection: StructureInspection;
|
|
168
|
+
if (options.structure) {
|
|
169
|
+
inspection = await options.structure.inspect(
|
|
170
|
+
{
|
|
171
|
+
absolutePath: target.absolutePath,
|
|
172
|
+
cwd,
|
|
173
|
+
line: target.line,
|
|
174
|
+
...(target.expectedRevision ? { expectedRevision: target.expectedRevision } : {}),
|
|
175
|
+
},
|
|
176
|
+
signal,
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
const currentRevision = await getSourceRevision(target.absolutePath);
|
|
180
|
+
inspection = {
|
|
181
|
+
details: { status: "provider-unavailable" },
|
|
182
|
+
...(currentRevision ? { currentRevision } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const { details: structure } = inspection;
|
|
186
|
+
if (signal?.aborted) throw abortError();
|
|
187
|
+
if (["source-changed", "source-unavailable", "file-too-large"].includes(structure.status)) {
|
|
188
|
+
return { target, structure };
|
|
189
|
+
}
|
|
190
|
+
const revision = inspection.currentRevision ?? (await getSourceRevision(target.absolutePath));
|
|
191
|
+
if (!revision) return unavailableEvidence(target, "source-unavailable", structure.provider);
|
|
192
|
+
if (target.expectedRevision && !sameSourceRevision(target.expectedRevision, revision)) {
|
|
193
|
+
return unavailableEvidence(target, "source-changed", structure.provider);
|
|
194
|
+
}
|
|
195
|
+
const evidence: InspectionEvidence = { target, structure, revision };
|
|
196
|
+
const headerBytes = Buffer.byteLength(
|
|
197
|
+
`${target.path}:${String(target.line)}\n${inspectionDescription(evidence)}\n\n[structure: ${structure.status}${structure.provider ? ` via ${structure.provider}` : ""}${structure.reason ? `; ${structure.reason}` : ""}]`,
|
|
198
|
+
);
|
|
199
|
+
const maxBytes = Math.min(
|
|
200
|
+
maxSourceBytes ?? MAX_RESULT_BYTES,
|
|
201
|
+
MAX_RESULT_BYTES - headerBytes - 1024,
|
|
202
|
+
);
|
|
203
|
+
if (maxBytes <= 0) throw new SourceBudgetTooSmallError();
|
|
204
|
+
const range = structure.range ?? {
|
|
205
|
+
startLine: Math.max(1, target.line - 10),
|
|
206
|
+
endLine: target.line + 10,
|
|
207
|
+
};
|
|
208
|
+
try {
|
|
209
|
+
evidence.source = await readSourceRange(
|
|
210
|
+
target.absolutePath,
|
|
211
|
+
range.startLine,
|
|
212
|
+
range.endLine,
|
|
213
|
+
signal,
|
|
214
|
+
target.line,
|
|
215
|
+
{
|
|
216
|
+
maxBytes,
|
|
217
|
+
...(target.retainedMatch?.occurrences[0]
|
|
218
|
+
? { focus: target.retainedMatch.occurrences[0] }
|
|
219
|
+
: {}),
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (signal?.aborted || (error instanceof Error && error.name === "AbortError"))
|
|
224
|
+
throw abortError();
|
|
225
|
+
if (error instanceof SourceTooLargeError)
|
|
226
|
+
return unavailableEvidence(target, "file-too-large", structure.provider);
|
|
227
|
+
if (isUnavailableSourceError(error)) {
|
|
228
|
+
return unavailableEvidence(target, "source-unavailable", structure.provider);
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
const finalRevision = await getSourceRevision(target.absolutePath);
|
|
233
|
+
if (signal?.aborted) throw abortError();
|
|
234
|
+
if (!finalRevision || !sameSourceRevision(revision, finalRevision)) {
|
|
235
|
+
return unavailableEvidence(target, "source-changed", structure.provider);
|
|
236
|
+
}
|
|
237
|
+
return evidence;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function inspectSource(
|
|
241
|
+
input: InspectInput,
|
|
242
|
+
cwd: string,
|
|
243
|
+
signal: AbortSignal | undefined,
|
|
244
|
+
options: InspectOptions,
|
|
245
|
+
): Promise<SiftLightResult> {
|
|
246
|
+
const target = resolveInspectionTarget(input, cwd, options.snapshots);
|
|
247
|
+
const evidence = await inspectSourceEvidence(target, cwd, signal, options);
|
|
248
|
+
const { source, structure } = evidence;
|
|
249
|
+
const details: SiftLightDetails = {
|
|
250
|
+
version: 1,
|
|
251
|
+
mode: "inspect",
|
|
252
|
+
status: source ? "complete" : "partial",
|
|
253
|
+
totalMatches: 0,
|
|
254
|
+
storedMatches: 0,
|
|
255
|
+
totalFiles: 1,
|
|
256
|
+
returnedMatches: source?.lines.length ?? 0,
|
|
257
|
+
snapshotComplete: source !== undefined,
|
|
258
|
+
structure,
|
|
259
|
+
...(source ? { source: sourceExcerptDetails(source) } : {}),
|
|
260
|
+
...(source && source.truncatedLines.length > 0
|
|
261
|
+
? { lineContentTruncated: source.truncatedLines.length }
|
|
262
|
+
: {}),
|
|
263
|
+
};
|
|
264
|
+
const status = `[structure: ${structure.status}${structure.provider ? ` via ${structure.provider}` : ""}${structure.reason ? `; ${structure.reason}` : ""}${!source && target.retainedMatch ? "; refresh the search before inspecting this match" : ""}]`;
|
|
265
|
+
const text = source
|
|
266
|
+
? `${target.path}:${String(target.line)}\n${inspectionDescription(evidence)}\n\n${source.text}${sourceTruncationText(source)}\n\n${status}`
|
|
267
|
+
: `${target.path}:${String(target.line)}\n\n${status}`;
|
|
268
|
+
if (Buffer.byteLength(text) > MAX_RESULT_BYTES)
|
|
269
|
+
throw new SiftLightError("Inspection metadata exceeds the result byte budget");
|
|
270
|
+
return { text, details };
|
|
271
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { listWorkspaceFiles } from "./workspace-files.js";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_LANGUAGE_CAPABILITIES,
|
|
5
|
+
DEFAULT_NEUTRAL_CAPABILITIES,
|
|
6
|
+
descriptorForPath,
|
|
7
|
+
type CapabilityAvailability,
|
|
8
|
+
type LanguageCapability,
|
|
9
|
+
type LanguageCapabilityDescriptor,
|
|
10
|
+
type LanguageCapabilitySpec,
|
|
11
|
+
type LanguageId,
|
|
12
|
+
type NeutralCapabilitySpec,
|
|
13
|
+
} from "./language-capability-definitions.js";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
CapabilityAvailability,
|
|
17
|
+
LanguageCapability,
|
|
18
|
+
LanguageCapabilityDescriptor,
|
|
19
|
+
LanguageCapabilitySpec,
|
|
20
|
+
LanguageId,
|
|
21
|
+
NeutralCapabilitySpec,
|
|
22
|
+
} from "./language-capability-definitions.js";
|
|
23
|
+
export {
|
|
24
|
+
DEFAULT_LANGUAGE_CAPABILITIES,
|
|
25
|
+
DEFAULT_NEUTRAL_CAPABILITIES,
|
|
26
|
+
} from "./language-capability-definitions.js";
|
|
27
|
+
|
|
28
|
+
export interface LanguageCapabilityEntry {
|
|
29
|
+
readonly language: LanguageId;
|
|
30
|
+
readonly files: number;
|
|
31
|
+
readonly extensions: readonly string[];
|
|
32
|
+
/** Stable ids are resolved through inventory.capabilityDefinitions. */
|
|
33
|
+
readonly capabilities: readonly { id: string; name: LanguageCapability }[];
|
|
34
|
+
readonly availability: CapabilityAvailability;
|
|
35
|
+
/** Capability discovery never probes external tools or project indexes. */
|
|
36
|
+
readonly readiness: "not-probed";
|
|
37
|
+
readonly evidence: "inventory" | "target-extension";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LanguageCapabilityRequest {
|
|
41
|
+
readonly cwd: string;
|
|
42
|
+
readonly path?: string;
|
|
43
|
+
readonly glob?: readonly string[];
|
|
44
|
+
readonly exclude?: readonly string[];
|
|
45
|
+
readonly hidden?: boolean;
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LanguageCapabilityInventory {
|
|
50
|
+
/** Root of the bounded names-only inventory, not a parsed project root. */
|
|
51
|
+
readonly root: string;
|
|
52
|
+
readonly path?: string;
|
|
53
|
+
readonly partial: boolean;
|
|
54
|
+
readonly reasons: readonly string[];
|
|
55
|
+
readonly languages: readonly LanguageCapabilityEntry[];
|
|
56
|
+
/** Deduplicated implementation facts, referenced by capability id above. */
|
|
57
|
+
readonly capabilityDefinitions: readonly LanguageCapabilitySpec[];
|
|
58
|
+
readonly neutral: readonly NeutralCapabilitySpec[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeLists(values: readonly string[] | undefined): string[] {
|
|
62
|
+
return values === undefined ? [] : [...values];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function languageAvailability(
|
|
66
|
+
capabilities: readonly LanguageCapabilitySpec[],
|
|
67
|
+
): CapabilityAvailability {
|
|
68
|
+
if (capabilities.length === 0) return "unavailable";
|
|
69
|
+
if (capabilities.some((capability) => capability.availability === "conditional"))
|
|
70
|
+
return "conditional";
|
|
71
|
+
return "implemented";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lightweight project capability discovery. It enumerates names only and
|
|
76
|
+
* never opens a parser, compiler, model worker or language server.
|
|
77
|
+
*/
|
|
78
|
+
export class LanguageCapabilityCatalog {
|
|
79
|
+
readonly #descriptors: readonly LanguageCapabilityDescriptor[];
|
|
80
|
+
readonly #neutral: readonly NeutralCapabilitySpec[];
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
descriptors: readonly LanguageCapabilityDescriptor[] = DEFAULT_LANGUAGE_CAPABILITIES,
|
|
84
|
+
neutral: readonly NeutralCapabilitySpec[] = DEFAULT_NEUTRAL_CAPABILITIES,
|
|
85
|
+
) {
|
|
86
|
+
this.#descriptors = [...descriptors];
|
|
87
|
+
this.#neutral = [...neutral];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
descriptor(language: LanguageId): LanguageCapabilityDescriptor | undefined {
|
|
91
|
+
return this.#descriptors.find((item) => item.language === language);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
descriptorForPath(path: string): LanguageCapabilityDescriptor | undefined {
|
|
95
|
+
return descriptorForPath(this.#descriptors, path);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async inspect(request: LanguageCapabilityRequest): Promise<LanguageCapabilityInventory> {
|
|
99
|
+
const path = request.path?.replace(/^@/, "");
|
|
100
|
+
const files = await listWorkspaceFiles(request.cwd, request.signal, {
|
|
101
|
+
...(path ? { path } : {}),
|
|
102
|
+
glob: normalizeLists(request.glob),
|
|
103
|
+
exclude: normalizeLists(request.exclude),
|
|
104
|
+
hidden: request.hidden ?? true,
|
|
105
|
+
});
|
|
106
|
+
const counts = new Map<LanguageId, number>();
|
|
107
|
+
for (const file of files.paths) {
|
|
108
|
+
const descriptor = this.descriptorForPath(file);
|
|
109
|
+
if (descriptor) counts.set(descriptor.language, (counts.get(descriptor.language) ?? 0) + 1);
|
|
110
|
+
}
|
|
111
|
+
const entries = [...counts.entries()].flatMap(([language, count]) => {
|
|
112
|
+
const descriptor = this.descriptor(language);
|
|
113
|
+
if (!descriptor) return [];
|
|
114
|
+
return [this.#entry(descriptor, count, "inventory")];
|
|
115
|
+
});
|
|
116
|
+
const target = path ? this.descriptorForPath(path) : undefined;
|
|
117
|
+
if (target && !counts.has(target.language))
|
|
118
|
+
entries.push(this.#entry(target, 0, "target-extension"));
|
|
119
|
+
entries.sort((left, right) => left.language.localeCompare(right.language));
|
|
120
|
+
const capabilityDefinitions = [
|
|
121
|
+
...new Map(
|
|
122
|
+
entries
|
|
123
|
+
.flatMap((entry) => this.descriptor(entry.language)?.capabilities ?? [])
|
|
124
|
+
.map((capability) => [capability.id, capability] as const),
|
|
125
|
+
).values(),
|
|
126
|
+
];
|
|
127
|
+
return {
|
|
128
|
+
root: resolve(request.cwd),
|
|
129
|
+
...(path ? { path } : {}),
|
|
130
|
+
partial: files.partial,
|
|
131
|
+
reasons: files.reasons,
|
|
132
|
+
languages: entries,
|
|
133
|
+
capabilityDefinitions,
|
|
134
|
+
neutral: this.#neutral,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#entry(
|
|
139
|
+
descriptor: LanguageCapabilityDescriptor,
|
|
140
|
+
files: number,
|
|
141
|
+
evidence: LanguageCapabilityEntry["evidence"],
|
|
142
|
+
): LanguageCapabilityEntry {
|
|
143
|
+
return {
|
|
144
|
+
language: descriptor.language,
|
|
145
|
+
files,
|
|
146
|
+
extensions: descriptor.extensions,
|
|
147
|
+
capabilities: descriptor.capabilities.map(({ id, name }) => ({ id, name })),
|
|
148
|
+
availability: languageAvailability(descriptor.capabilities),
|
|
149
|
+
readiness: "not-probed",
|
|
150
|
+
evidence,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function languageCapabilityForPath(path: string): LanguageCapabilityDescriptor | undefined {
|
|
156
|
+
return descriptorForPath(DEFAULT_LANGUAGE_CAPABILITIES, path);
|
|
157
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
|
|
3
|
+
export type LanguageId = "javascript" | "typescript" | "tsx" | "go" | "python" | "swift";
|
|
4
|
+
|
|
5
|
+
export type LanguageCapability = "outline" | "structure" | "roles" | "imports" | "tests";
|
|
6
|
+
|
|
7
|
+
export type CapabilityEvidence = "text" | "syntax" | "compiler" | "external" | "model";
|
|
8
|
+
export type CapabilityAvailability = "implemented" | "conditional" | "unavailable";
|
|
9
|
+
export type CapabilityProviderKind = "builtin" | "external" | "model";
|
|
10
|
+
|
|
11
|
+
/** One capability declaration is the source of provider, evidence and loading facts. */
|
|
12
|
+
export interface LanguageCapabilitySpec {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly name: LanguageCapability;
|
|
15
|
+
readonly provider: string;
|
|
16
|
+
readonly providerKind: CapabilityProviderKind;
|
|
17
|
+
readonly evidence: CapabilityEvidence;
|
|
18
|
+
readonly availability: CapabilityAvailability;
|
|
19
|
+
/** Runtime parser/compiler/model resource activation, not JavaScript module import timing. */
|
|
20
|
+
readonly load: "lazy";
|
|
21
|
+
readonly prerequisites?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LanguageCapabilityDescriptor {
|
|
25
|
+
readonly language: LanguageId;
|
|
26
|
+
readonly extensions: readonly string[];
|
|
27
|
+
readonly capabilities: readonly LanguageCapabilitySpec[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type NeutralCapabilityName = "content" | "files" | "concept" | "hybrid";
|
|
31
|
+
|
|
32
|
+
export interface NeutralCapabilitySpec {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly name: NeutralCapabilityName;
|
|
35
|
+
readonly provider: string;
|
|
36
|
+
readonly providerKind: CapabilityProviderKind;
|
|
37
|
+
readonly evidence: CapabilityEvidence;
|
|
38
|
+
readonly availability: CapabilityAvailability;
|
|
39
|
+
readonly load: "none" | "lazy";
|
|
40
|
+
readonly prerequisites?: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const TYPESCRIPT_LANGUAGE_CAPABILITIES: readonly LanguageCapabilitySpec[] = [
|
|
44
|
+
{
|
|
45
|
+
id: "ast-grep.outline",
|
|
46
|
+
name: "outline",
|
|
47
|
+
provider: "ast-grep",
|
|
48
|
+
providerKind: "builtin",
|
|
49
|
+
evidence: "syntax",
|
|
50
|
+
availability: "implemented",
|
|
51
|
+
load: "lazy",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "ast-grep.structure",
|
|
55
|
+
name: "structure",
|
|
56
|
+
provider: "ast-grep",
|
|
57
|
+
providerKind: "builtin",
|
|
58
|
+
evidence: "syntax",
|
|
59
|
+
availability: "implemented",
|
|
60
|
+
load: "lazy",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "ast-grep.roles",
|
|
64
|
+
name: "roles",
|
|
65
|
+
provider: "ast-grep",
|
|
66
|
+
providerKind: "builtin",
|
|
67
|
+
evidence: "syntax",
|
|
68
|
+
availability: "implemented",
|
|
69
|
+
load: "lazy",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "static-navigation.imports",
|
|
73
|
+
name: "imports",
|
|
74
|
+
provider: "bounded static module resolver",
|
|
75
|
+
providerKind: "builtin",
|
|
76
|
+
evidence: "syntax",
|
|
77
|
+
availability: "implemented",
|
|
78
|
+
load: "lazy",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "static-navigation.tests",
|
|
82
|
+
name: "tests",
|
|
83
|
+
provider: "bounded static module resolver",
|
|
84
|
+
providerKind: "builtin",
|
|
85
|
+
evidence: "syntax",
|
|
86
|
+
availability: "implemented",
|
|
87
|
+
load: "lazy",
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const GO_LANGUAGE_CAPABILITIES: readonly LanguageCapabilitySpec[] = [
|
|
92
|
+
{
|
|
93
|
+
id: "ast-grep-go.structure",
|
|
94
|
+
name: "structure",
|
|
95
|
+
provider: "ast-grep lang-go",
|
|
96
|
+
providerKind: "builtin",
|
|
97
|
+
evidence: "syntax",
|
|
98
|
+
availability: "implemented",
|
|
99
|
+
load: "lazy",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "ast-grep-go.roles",
|
|
103
|
+
name: "roles",
|
|
104
|
+
provider: "ast-grep lang-go",
|
|
105
|
+
providerKind: "builtin",
|
|
106
|
+
evidence: "syntax",
|
|
107
|
+
availability: "implemented",
|
|
108
|
+
load: "lazy",
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const PYTHON_LANGUAGE_CAPABILITIES: readonly LanguageCapabilitySpec[] = [
|
|
113
|
+
{
|
|
114
|
+
id: "python-outline.outline",
|
|
115
|
+
name: "outline",
|
|
116
|
+
provider: "bounded Python outline parser",
|
|
117
|
+
providerKind: "builtin",
|
|
118
|
+
evidence: "syntax",
|
|
119
|
+
availability: "implemented",
|
|
120
|
+
load: "lazy",
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
export const DEFAULT_LANGUAGE_CAPABILITIES: readonly LanguageCapabilityDescriptor[] = [
|
|
125
|
+
{
|
|
126
|
+
language: "javascript",
|
|
127
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
128
|
+
capabilities: TYPESCRIPT_LANGUAGE_CAPABILITIES,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
language: "typescript",
|
|
132
|
+
extensions: [".ts", ".mts", ".cts"],
|
|
133
|
+
capabilities: TYPESCRIPT_LANGUAGE_CAPABILITIES,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
language: "tsx",
|
|
137
|
+
extensions: [".tsx"],
|
|
138
|
+
capabilities: TYPESCRIPT_LANGUAGE_CAPABILITIES,
|
|
139
|
+
},
|
|
140
|
+
{ language: "go", extensions: [".go"], capabilities: GO_LANGUAGE_CAPABILITIES },
|
|
141
|
+
{ language: "python", extensions: [".py"], capabilities: PYTHON_LANGUAGE_CAPABILITIES },
|
|
142
|
+
{ language: "swift", extensions: [".swift"], capabilities: [] },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
export const DEFAULT_NEUTRAL_CAPABILITIES: readonly NeutralCapabilitySpec[] = [
|
|
146
|
+
{
|
|
147
|
+
id: "neutral.content",
|
|
148
|
+
name: "content",
|
|
149
|
+
provider: "ripgrep",
|
|
150
|
+
providerKind: "builtin",
|
|
151
|
+
evidence: "text",
|
|
152
|
+
availability: "implemented",
|
|
153
|
+
load: "none",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "neutral.files",
|
|
157
|
+
name: "files",
|
|
158
|
+
provider: "ripgrep --files",
|
|
159
|
+
providerKind: "builtin",
|
|
160
|
+
evidence: "text",
|
|
161
|
+
availability: "implemented",
|
|
162
|
+
load: "none",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "neutral.concept",
|
|
166
|
+
name: "concept",
|
|
167
|
+
provider: "local multilingual-e5 model",
|
|
168
|
+
providerKind: "model",
|
|
169
|
+
evidence: "model",
|
|
170
|
+
availability: "conditional",
|
|
171
|
+
load: "lazy",
|
|
172
|
+
prerequisites: ["verified local model assets"],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "neutral.hybrid",
|
|
176
|
+
name: "hybrid",
|
|
177
|
+
provider: "ripgrep + local multilingual-e5 model",
|
|
178
|
+
providerKind: "model",
|
|
179
|
+
evidence: "model",
|
|
180
|
+
availability: "conditional",
|
|
181
|
+
load: "lazy",
|
|
182
|
+
prerequisites: ["verified local model assets"],
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
export function descriptorForPath(
|
|
187
|
+
descriptors: readonly LanguageCapabilityDescriptor[],
|
|
188
|
+
path: string,
|
|
189
|
+
): LanguageCapabilityDescriptor | undefined {
|
|
190
|
+
const extension = extname(path).toLowerCase();
|
|
191
|
+
return descriptors.find((item) => item.extensions.includes(extension));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function languageForPath(
|
|
195
|
+
descriptors: readonly LanguageCapabilityDescriptor[],
|
|
196
|
+
path: string,
|
|
197
|
+
): LanguageId | undefined {
|
|
198
|
+
return descriptorForPath(descriptors, path)?.language;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function outlineExtension(path: string): string | undefined {
|
|
202
|
+
const extension = extname(path).toLowerCase();
|
|
203
|
+
return extension || undefined;
|
|
204
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ByteRange, SourceDocument } from "./source-document.js";
|
|
2
|
+
|
|
3
|
+
export function escapeRegexLiteral(term: string): string {
|
|
4
|
+
return term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Finds non-overlapping occurrences of one exact byte sequence. Different terms are scanned independently. */
|
|
8
|
+
export function literalOccurrences(
|
|
9
|
+
document: SourceDocument,
|
|
10
|
+
term: string,
|
|
11
|
+
allowed?: readonly ByteRange[],
|
|
12
|
+
): ByteRange[] {
|
|
13
|
+
const needle = Buffer.from(term);
|
|
14
|
+
const found: ByteRange[] = [];
|
|
15
|
+
for (
|
|
16
|
+
let start = document.bytes.indexOf(needle);
|
|
17
|
+
start >= 0;
|
|
18
|
+
start = document.bytes.indexOf(needle, start + Math.max(1, needle.length))
|
|
19
|
+
) {
|
|
20
|
+
const range = { start, end: start + needle.length };
|
|
21
|
+
if (!allowed || allowed.some((part) => part.start <= range.start && range.end <= part.end))
|
|
22
|
+
found.push(range);
|
|
23
|
+
}
|
|
24
|
+
return found;
|
|
25
|
+
}
|
package/src/mcp-cli.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SiftLightMcpTransport = "http" | "stdio";
|
|
2
|
+
|
|
3
|
+
export const SIFT_LIGHT_MCP_USAGE = `Usage: sift-light-mcp [--http | --stdio]
|
|
4
|
+
|
|
5
|
+
Transports:
|
|
6
|
+
--http Start the Streamable HTTP server (default)
|
|
7
|
+
--stdio Serve one local MCP client over stdin/stdout
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
export function parseSiftLightMcpTransport(
|
|
11
|
+
arguments_: readonly string[],
|
|
12
|
+
): SiftLightMcpTransport | "help" {
|
|
13
|
+
if (arguments_.length === 0 || (arguments_.length === 1 && arguments_[0] === "--http")) {
|
|
14
|
+
return "http";
|
|
15
|
+
}
|
|
16
|
+
if (arguments_.length === 1 && arguments_[0] === "--stdio") return "stdio";
|
|
17
|
+
if (arguments_.length === 1 && (arguments_[0] === "--help" || arguments_[0] === "-h")) {
|
|
18
|
+
return "help";
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Unknown arguments: ${arguments_.join(" ")}\n${SIFT_LIGHT_MCP_USAGE}`);
|
|
21
|
+
}
|