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
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { relative, resolve } from "node:path";
|
|
4
|
+
import { MAX_STRUCTURE_BYTES, MAX_STRUCTURE_FILES } from "./analysis-limits.js";
|
|
5
|
+
import { abortError, SiftLightError } from "./errors.js";
|
|
6
|
+
import {
|
|
7
|
+
SourceDocumentError,
|
|
8
|
+
type ByteRange,
|
|
9
|
+
type SourceDocument,
|
|
10
|
+
type SourceReference,
|
|
11
|
+
} from "./source-document.js";
|
|
12
|
+
import { isPathInsideRoot } from "./path-policy.js";
|
|
13
|
+
import { syntaxField } from "./syntax-tree.js";
|
|
14
|
+
import type { SyntaxAnalysis } from "./syntax-types.js";
|
|
15
|
+
|
|
16
|
+
export interface NavigationHost {
|
|
17
|
+
cwd: string;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
normalizePath?(this: void, path: string): string;
|
|
20
|
+
load(this: void, path: string, expected?: SourceReference): Promise<SourceDocument>;
|
|
21
|
+
syntax(document: SourceDocument): Promise<SyntaxAnalysis>;
|
|
22
|
+
releaseSyntax?(document: SourceDocument): void;
|
|
23
|
+
listFiles(): Promise<string[] | { paths: string[]; partial: boolean; reasons: string[] }>;
|
|
24
|
+
maxFilesToParse?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface NavigationInput {
|
|
28
|
+
path: string;
|
|
29
|
+
line?: number;
|
|
30
|
+
symbol?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface NavigationItem {
|
|
33
|
+
path: string;
|
|
34
|
+
line: number;
|
|
35
|
+
label: string;
|
|
36
|
+
excerpt?: string;
|
|
37
|
+
source: SourceReference;
|
|
38
|
+
range: ByteRange;
|
|
39
|
+
details: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
export interface NavigationResult {
|
|
42
|
+
items: NavigationItem[];
|
|
43
|
+
partial: boolean;
|
|
44
|
+
reasons: string[];
|
|
45
|
+
filesRead: number;
|
|
46
|
+
bytesRead: number;
|
|
47
|
+
counts?: Record<string, number>;
|
|
48
|
+
stats?: {
|
|
49
|
+
filesParsed: number;
|
|
50
|
+
filesSkipped: number;
|
|
51
|
+
parseMs: number;
|
|
52
|
+
budgetExhausted: boolean;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ImportBinding {
|
|
57
|
+
statement: number;
|
|
58
|
+
node: number;
|
|
59
|
+
source: string | undefined;
|
|
60
|
+
local?: string;
|
|
61
|
+
imported: string;
|
|
62
|
+
kind: "named" | "default" | "namespace" | "side-effect";
|
|
63
|
+
typeOnly: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface ExportBinding {
|
|
66
|
+
statement: number;
|
|
67
|
+
node: number;
|
|
68
|
+
exported: string;
|
|
69
|
+
local?: string;
|
|
70
|
+
source?: string;
|
|
71
|
+
definition?: number;
|
|
72
|
+
kind: "named" | "default" | "namespace" | "star";
|
|
73
|
+
}
|
|
74
|
+
export interface ModuleFacts {
|
|
75
|
+
document: SourceDocument;
|
|
76
|
+
syntax: SyntaxAnalysis;
|
|
77
|
+
imports: ImportBinding[];
|
|
78
|
+
exports: ExportBinding[];
|
|
79
|
+
declarations: Map<string, number[]>;
|
|
80
|
+
locations: Map<number, { start: number; end: number; kind: string }>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class NavigationFailure extends Error {
|
|
84
|
+
readonly reason: string;
|
|
85
|
+
constructor(reason: string) {
|
|
86
|
+
super(reason);
|
|
87
|
+
this.reason = reason;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function navigationPath(path: string): string {
|
|
92
|
+
const normalized = posix.normalize(path.replaceAll("\\", "/"));
|
|
93
|
+
if (
|
|
94
|
+
normalized === ".." ||
|
|
95
|
+
normalized.startsWith("../") ||
|
|
96
|
+
normalized.startsWith("/") ||
|
|
97
|
+
/^[A-Za-z]:/.test(normalized)
|
|
98
|
+
) {
|
|
99
|
+
throw new SiftLightError("Navigation paths must stay inside the workspace");
|
|
100
|
+
}
|
|
101
|
+
return normalized.replace(/^\.\//, "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function nodeText(
|
|
105
|
+
facts: Pick<ModuleFacts, "document" | "syntax" | "locations">,
|
|
106
|
+
node: number | undefined,
|
|
107
|
+
): string | undefined {
|
|
108
|
+
const value =
|
|
109
|
+
node === undefined ? undefined : (facts.syntax.nodes[node] ?? facts.locations.get(node));
|
|
110
|
+
return value ? facts.document.text.slice(value.start, value.end) : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Decode literal syntax without evaluating any source code. */
|
|
114
|
+
export function literalText(raw: string | undefined): string | undefined {
|
|
115
|
+
if (!raw || raw.length < 2) return undefined;
|
|
116
|
+
const quote = raw[0];
|
|
117
|
+
if (
|
|
118
|
+
(quote !== "'" && quote !== '"' && quote !== "`") ||
|
|
119
|
+
raw.at(-1) !== quote ||
|
|
120
|
+
(quote === "`" && raw.includes("${"))
|
|
121
|
+
)
|
|
122
|
+
return undefined;
|
|
123
|
+
const body = raw.slice(1, -1);
|
|
124
|
+
let result = "";
|
|
125
|
+
for (let index = 0; index < body.length; index++) {
|
|
126
|
+
const character = body[index];
|
|
127
|
+
if (character !== "\\") {
|
|
128
|
+
result += character;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const escaped = body[++index];
|
|
132
|
+
if (escaped === undefined) return undefined;
|
|
133
|
+
if (escaped === "\n") continue;
|
|
134
|
+
if (escaped === "\r") {
|
|
135
|
+
if (body[index + 1] === "\n") index++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const simple: Record<string, string> = {
|
|
139
|
+
n: "\n",
|
|
140
|
+
r: "\r",
|
|
141
|
+
t: "\t",
|
|
142
|
+
b: "\b",
|
|
143
|
+
f: "\f",
|
|
144
|
+
v: "\v",
|
|
145
|
+
"0": "\0",
|
|
146
|
+
};
|
|
147
|
+
if (escaped in simple) {
|
|
148
|
+
if (escaped === "0" && /[0-9]/.test(body[index + 1] ?? "")) return undefined;
|
|
149
|
+
result += simple[escaped];
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (escaped === "x" || escaped === "u") {
|
|
153
|
+
const brace = escaped === "u" && body[index + 1] === "{";
|
|
154
|
+
const end = brace ? body.indexOf("}", index + 2) : index + (escaped === "x" ? 2 : 4) + 1;
|
|
155
|
+
if (end < 0) return undefined;
|
|
156
|
+
const digits = body.slice(index + (brace ? 2 : 1), end);
|
|
157
|
+
if (!/^[\da-fA-F]+$/.test(digits) || (!brace && digits.length !== (escaped === "x" ? 2 : 4)))
|
|
158
|
+
return undefined;
|
|
159
|
+
const point = Number.parseInt(digits, 16);
|
|
160
|
+
if (point > 0x10ffff) return undefined;
|
|
161
|
+
result += String.fromCodePoint(point);
|
|
162
|
+
index = brace ? end : end - 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (/[1-9]/.test(escaped)) return undefined;
|
|
166
|
+
result += escaped;
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function moduleRange(facts: ModuleFacts, node: number): ByteRange {
|
|
172
|
+
const value = facts.syntax.nodes[node] ?? facts.locations.get(node);
|
|
173
|
+
if (!value) throw new Error("Missing syntax node");
|
|
174
|
+
return {
|
|
175
|
+
start: facts.document.toByteOffset(value.start),
|
|
176
|
+
end: facts.document.toByteOffset(value.end),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function nodeLine(facts: ModuleFacts, node: number): number {
|
|
181
|
+
return facts.document.lineAt(moduleRange(facts, node).start);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function descendants(syntax: SyntaxAnalysis, node: number, kind: string): number[] {
|
|
185
|
+
const result: number[] = [];
|
|
186
|
+
const pending = [...(syntax.children[node] ?? [])];
|
|
187
|
+
while (pending.length) {
|
|
188
|
+
const current = pending.pop();
|
|
189
|
+
if (current === undefined) break;
|
|
190
|
+
if (syntax.nodes[current]?.kind === kind) result.push(current);
|
|
191
|
+
else pending.push(...(syntax.children[current] ?? []));
|
|
192
|
+
}
|
|
193
|
+
return result.toSorted((a, b) => a - b);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function topLevel(syntax: SyntaxAnalysis, node: number): boolean {
|
|
197
|
+
let parent = syntax.nodes[node]?.parent;
|
|
198
|
+
while (parent !== null && parent !== undefined) {
|
|
199
|
+
const kind = syntax.nodes[parent]?.kind;
|
|
200
|
+
if (kind === "program") return true;
|
|
201
|
+
if (
|
|
202
|
+
kind !== "export_statement" &&
|
|
203
|
+
kind !== "lexical_declaration" &&
|
|
204
|
+
kind !== "variable_declaration" &&
|
|
205
|
+
kind !== "ambient_declaration"
|
|
206
|
+
)
|
|
207
|
+
return false;
|
|
208
|
+
parent = syntax.nodes[parent]?.parent;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function declaredNames(facts: ModuleFacts, root: number): string[] {
|
|
214
|
+
const names: string[] = [];
|
|
215
|
+
const pending = [root];
|
|
216
|
+
while (pending.length) {
|
|
217
|
+
const id = pending.pop();
|
|
218
|
+
if (id === undefined) break;
|
|
219
|
+
const node = facts.syntax.nodes[id];
|
|
220
|
+
if (!node || node.field === "type" || node.field === "right" || node.field === "key") continue;
|
|
221
|
+
if (node.kind === "identifier" || node.kind === "shorthand_property_identifier_pattern") {
|
|
222
|
+
const name = nodeText(facts, id);
|
|
223
|
+
if (name) names.push(name);
|
|
224
|
+
} else pending.push(...(facts.syntax.children[id] ?? []));
|
|
225
|
+
}
|
|
226
|
+
return names;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function collectModuleFacts(document: SourceDocument, syntax: SyntaxAnalysis): ModuleFacts {
|
|
230
|
+
const facts: ModuleFacts = {
|
|
231
|
+
document,
|
|
232
|
+
syntax,
|
|
233
|
+
imports: [],
|
|
234
|
+
exports: [],
|
|
235
|
+
declarations: new Map(),
|
|
236
|
+
locations: new Map(),
|
|
237
|
+
};
|
|
238
|
+
for (const symbol of syntax.symbols) {
|
|
239
|
+
if (!topLevel(syntax, symbol.node)) continue;
|
|
240
|
+
// Expressions are owned by their variable binding; they do not declare a second export.
|
|
241
|
+
if (
|
|
242
|
+
[
|
|
243
|
+
"arrow_function",
|
|
244
|
+
"function_expression",
|
|
245
|
+
"generator_function",
|
|
246
|
+
"class",
|
|
247
|
+
"variable_declarator",
|
|
248
|
+
].includes(symbol.kind)
|
|
249
|
+
)
|
|
250
|
+
continue;
|
|
251
|
+
const existing = facts.declarations.get(symbol.name) ?? [];
|
|
252
|
+
if (!existing.includes(symbol.node)) existing.push(symbol.node);
|
|
253
|
+
facts.declarations.set(symbol.name, existing);
|
|
254
|
+
}
|
|
255
|
+
for (let id = 0; id < syntax.nodes.length; id++) {
|
|
256
|
+
if (syntax.nodes[id]?.kind !== "variable_declarator" || !topLevel(syntax, id)) continue;
|
|
257
|
+
const binding = syntaxField(syntax, id, "name");
|
|
258
|
+
if (binding === undefined) continue;
|
|
259
|
+
for (const name of declaredNames(facts, binding)) {
|
|
260
|
+
const existing = facts.declarations.get(name) ?? [];
|
|
261
|
+
if (!existing.includes(id)) existing.push(id);
|
|
262
|
+
facts.declarations.set(name, existing);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const [name, declarations] of facts.declarations) {
|
|
266
|
+
const implementations = declarations.filter((id) =>
|
|
267
|
+
["function_declaration", "generator_function_declaration"].includes(
|
|
268
|
+
syntax.nodes[id]?.kind ?? "",
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
if (
|
|
272
|
+
implementations.length === 1 &&
|
|
273
|
+
declarations.every(
|
|
274
|
+
(id) => id === implementations[0] || syntax.nodes[id]?.kind === "function_signature",
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
facts.declarations.set(name, implementations);
|
|
278
|
+
}
|
|
279
|
+
for (const statement of syntax.children[0] ?? []) {
|
|
280
|
+
const node = syntax.nodes[statement];
|
|
281
|
+
if (!node) continue;
|
|
282
|
+
const source = literalText(nodeText(facts, syntaxField(syntax, statement, "source")));
|
|
283
|
+
const children = syntax.children[statement] ?? [];
|
|
284
|
+
if (node.kind === "import_statement") {
|
|
285
|
+
const clause = children.find((child) => syntax.nodes[child]?.kind === "import_clause");
|
|
286
|
+
const typeOnly = children.some((child) => syntax.nodes[child]?.kind === "type");
|
|
287
|
+
if (clause === undefined) {
|
|
288
|
+
facts.imports.push({
|
|
289
|
+
statement,
|
|
290
|
+
node: statement,
|
|
291
|
+
source,
|
|
292
|
+
imported: "*",
|
|
293
|
+
kind: "side-effect",
|
|
294
|
+
typeOnly,
|
|
295
|
+
});
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
for (const child of syntax.children[clause] ?? []) {
|
|
299
|
+
const kind = syntax.nodes[child]?.kind;
|
|
300
|
+
if (kind === "identifier")
|
|
301
|
+
facts.imports.push({
|
|
302
|
+
statement,
|
|
303
|
+
node: child,
|
|
304
|
+
source,
|
|
305
|
+
local: nodeText(facts, child) ?? "",
|
|
306
|
+
imported: "default",
|
|
307
|
+
kind: "default",
|
|
308
|
+
typeOnly,
|
|
309
|
+
});
|
|
310
|
+
if (kind === "namespace_import") {
|
|
311
|
+
const local = (syntax.children[child] ?? []).find(
|
|
312
|
+
(part) => syntax.nodes[part]?.kind === "identifier",
|
|
313
|
+
);
|
|
314
|
+
facts.imports.push({
|
|
315
|
+
statement,
|
|
316
|
+
node: child,
|
|
317
|
+
source,
|
|
318
|
+
local: nodeText(facts, local) ?? "",
|
|
319
|
+
imported: "*",
|
|
320
|
+
kind: "namespace",
|
|
321
|
+
typeOnly,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const specifier of descendants(syntax, clause, "import_specifier")) {
|
|
326
|
+
const name = nodeText(facts, syntaxField(syntax, specifier, "name"));
|
|
327
|
+
const imported = literalText(name) ?? name;
|
|
328
|
+
if (imported === undefined) continue;
|
|
329
|
+
const local = nodeText(facts, syntaxField(syntax, specifier, "alias")) ?? imported;
|
|
330
|
+
facts.imports.push({
|
|
331
|
+
statement,
|
|
332
|
+
node: specifier,
|
|
333
|
+
source,
|
|
334
|
+
imported,
|
|
335
|
+
local,
|
|
336
|
+
kind: "named",
|
|
337
|
+
typeOnly:
|
|
338
|
+
typeOnly ||
|
|
339
|
+
(syntax.children[specifier] ?? []).some((part) => syntax.nodes[part]?.kind === "type"),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (node.kind !== "export_statement") continue;
|
|
344
|
+
const isDefault = children.some((child) => syntax.nodes[child]?.kind === "default");
|
|
345
|
+
const declaration = syntaxField(syntax, statement, "declaration");
|
|
346
|
+
const value = syntaxField(syntax, statement, "value");
|
|
347
|
+
if (isDefault && (declaration !== undefined || value !== undefined)) {
|
|
348
|
+
const definition = declaration ?? value;
|
|
349
|
+
if (definition !== undefined)
|
|
350
|
+
facts.exports.push({
|
|
351
|
+
statement,
|
|
352
|
+
node: statement,
|
|
353
|
+
exported: "default",
|
|
354
|
+
definition,
|
|
355
|
+
kind: "default",
|
|
356
|
+
});
|
|
357
|
+
} else if (declaration !== undefined) {
|
|
358
|
+
for (const [name, declarations] of facts.declarations) {
|
|
359
|
+
for (const definition of declarations) {
|
|
360
|
+
const current = syntax.nodes[definition];
|
|
361
|
+
const container = syntax.nodes[declaration];
|
|
362
|
+
if (
|
|
363
|
+
current &&
|
|
364
|
+
container &&
|
|
365
|
+
current.start >= container.start &&
|
|
366
|
+
current.end <= container.end
|
|
367
|
+
)
|
|
368
|
+
facts.exports.push({
|
|
369
|
+
statement,
|
|
370
|
+
node: definition,
|
|
371
|
+
exported: name,
|
|
372
|
+
local: name,
|
|
373
|
+
definition,
|
|
374
|
+
kind: "named",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (const specifier of descendants(syntax, statement, "export_specifier")) {
|
|
380
|
+
const raw = nodeText(facts, syntaxField(syntax, specifier, "name"));
|
|
381
|
+
const local = literalText(raw) ?? raw;
|
|
382
|
+
if (local === undefined) continue;
|
|
383
|
+
const alias = nodeText(facts, syntaxField(syntax, specifier, "alias"));
|
|
384
|
+
const exported = literalText(alias) ?? alias ?? local;
|
|
385
|
+
facts.exports.push({
|
|
386
|
+
statement,
|
|
387
|
+
node: specifier,
|
|
388
|
+
exported,
|
|
389
|
+
local,
|
|
390
|
+
...(source !== undefined ? { source } : {}),
|
|
391
|
+
kind: "named",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const namespace = children.find((child) => syntax.nodes[child]?.kind === "namespace_export");
|
|
395
|
+
if (namespace !== undefined) {
|
|
396
|
+
const name = (syntax.children[namespace] ?? []).find(
|
|
397
|
+
(child) => syntax.nodes[child]?.kind === "identifier",
|
|
398
|
+
);
|
|
399
|
+
facts.exports.push({
|
|
400
|
+
statement,
|
|
401
|
+
node: namespace,
|
|
402
|
+
exported: nodeText(facts, name) ?? "*",
|
|
403
|
+
...(source !== undefined ? { source } : {}),
|
|
404
|
+
kind: "namespace",
|
|
405
|
+
});
|
|
406
|
+
} else if (children.some((child) => syntax.nodes[child]?.kind === "*")) {
|
|
407
|
+
facts.exports.push({
|
|
408
|
+
statement,
|
|
409
|
+
node: statement,
|
|
410
|
+
exported: "*",
|
|
411
|
+
...(source !== undefined ? { source } : {}),
|
|
412
|
+
kind: "star",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const retained = new Set([
|
|
417
|
+
0,
|
|
418
|
+
...syntax.symbols.map((symbol) => symbol.node),
|
|
419
|
+
...[...facts.declarations.values()].flat(),
|
|
420
|
+
...facts.imports.flatMap((binding) => [binding.node, binding.statement]),
|
|
421
|
+
...facts.exports.flatMap((binding) => [
|
|
422
|
+
binding.node,
|
|
423
|
+
binding.statement,
|
|
424
|
+
...(binding.definition === undefined ? [] : [binding.definition]),
|
|
425
|
+
]),
|
|
426
|
+
]);
|
|
427
|
+
for (const id of retained) {
|
|
428
|
+
const node = syntax.nodes[id];
|
|
429
|
+
if (node) facts.locations.set(id, { start: node.start, end: node.end, kind: node.kind });
|
|
430
|
+
}
|
|
431
|
+
return facts;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function navigationError(error: unknown): string | undefined {
|
|
435
|
+
if (
|
|
436
|
+
typeof error === "object" &&
|
|
437
|
+
error !== null &&
|
|
438
|
+
"reason" in error &&
|
|
439
|
+
error.reason === "structural-read-budget-exhausted"
|
|
440
|
+
)
|
|
441
|
+
return error.reason;
|
|
442
|
+
if (error instanceof NavigationFailure) return error.reason;
|
|
443
|
+
if (error instanceof SourceDocumentError) return error.reason;
|
|
444
|
+
if (
|
|
445
|
+
typeof error === "object" &&
|
|
446
|
+
error !== null &&
|
|
447
|
+
"code" in error &&
|
|
448
|
+
(error.code === "ENOENT" ||
|
|
449
|
+
error.code === "ENOTDIR" ||
|
|
450
|
+
error.code === "EISDIR" ||
|
|
451
|
+
error.code === "EACCES")
|
|
452
|
+
)
|
|
453
|
+
return "source-unavailable";
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Current-request facts only; serialized results never contain these trees. */
|
|
458
|
+
export class NavigationContext {
|
|
459
|
+
readonly host: NavigationHost;
|
|
460
|
+
readonly modules = new Map<string, ModuleFacts>();
|
|
461
|
+
readonly documents = new Map<string, SourceDocument>();
|
|
462
|
+
readonly reasons = new Set<string>();
|
|
463
|
+
bytesRead = 0;
|
|
464
|
+
#files?: Set<string>;
|
|
465
|
+
readonly #pathAliases = new Map<string, string>();
|
|
466
|
+
#fileLimit: number;
|
|
467
|
+
#failures = new Map<string, string>();
|
|
468
|
+
#attempted = new Set<string>();
|
|
469
|
+
constructor(host: NavigationHost, fileLimit = host.maxFilesToParse ?? MAX_STRUCTURE_FILES) {
|
|
470
|
+
this.host = host;
|
|
471
|
+
this.#fileLimit = fileLimit;
|
|
472
|
+
}
|
|
473
|
+
checkAbort(): void {
|
|
474
|
+
if (this.host.signal?.aborted) throw abortError();
|
|
475
|
+
}
|
|
476
|
+
normalizePath(path: string): string {
|
|
477
|
+
const normalized = this.host.normalizePath?.(path) ?? navigationPath(path);
|
|
478
|
+
return this.#pathAliases.get(normalized) ?? normalized;
|
|
479
|
+
}
|
|
480
|
+
async files(): Promise<Set<string>> {
|
|
481
|
+
this.checkAbort();
|
|
482
|
+
if (this.#files) return this.#files;
|
|
483
|
+
const listed = await this.host.listFiles();
|
|
484
|
+
if (!Array.isArray(listed) && listed.partial)
|
|
485
|
+
for (const reason of listed.reasons) this.reasons.add(reason);
|
|
486
|
+
const paths = new Set<string>();
|
|
487
|
+
const canonicalCwd = await realpath(this.host.cwd).catch(() => undefined);
|
|
488
|
+
const normalizedPaths = (Array.isArray(listed) ? listed : listed.paths).map(
|
|
489
|
+
(path) => this.host.normalizePath?.(path) ?? navigationPath(path),
|
|
490
|
+
);
|
|
491
|
+
const canonicalPaths = await Promise.all(
|
|
492
|
+
normalizedPaths.map(async (normalized) => {
|
|
493
|
+
try {
|
|
494
|
+
const target = await realpath(resolve(this.host.cwd, normalized));
|
|
495
|
+
const canonical =
|
|
496
|
+
canonicalCwd !== undefined && isPathInsideRoot(target, canonicalCwd)
|
|
497
|
+
? relative(canonicalCwd, target).replaceAll("\\", "/")
|
|
498
|
+
: target.replaceAll("\\", "/");
|
|
499
|
+
return [normalized, canonical] as const;
|
|
500
|
+
} catch {
|
|
501
|
+
// Retain the verified enumeration path when its target disappears during discovery.
|
|
502
|
+
return [normalized, normalized] as const;
|
|
503
|
+
}
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
for (const [normalized, canonical] of canonicalPaths) {
|
|
507
|
+
this.#pathAliases.set(normalized, canonical);
|
|
508
|
+
this.#pathAliases.set(canonical, canonical);
|
|
509
|
+
paths.add(canonical);
|
|
510
|
+
}
|
|
511
|
+
this.#files = paths;
|
|
512
|
+
return this.#files;
|
|
513
|
+
}
|
|
514
|
+
async module(path: string, retainSyntax = false): Promise<ModuleFacts> {
|
|
515
|
+
this.checkAbort();
|
|
516
|
+
path = this.normalizePath(path);
|
|
517
|
+
const cached = this.modules.get(path);
|
|
518
|
+
if (cached) {
|
|
519
|
+
if (retainSyntax && cached.syntax.nodes.length === 0) {
|
|
520
|
+
let retained = false;
|
|
521
|
+
try {
|
|
522
|
+
cached.syntax = await this.host.syntax(cached.document);
|
|
523
|
+
if (cached.syntax.status !== "ok")
|
|
524
|
+
throw new NavigationFailure(`syntax-${cached.syntax.status}`);
|
|
525
|
+
retained = true;
|
|
526
|
+
} finally {
|
|
527
|
+
if (!retained) this.release(cached);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return cached;
|
|
531
|
+
}
|
|
532
|
+
const failed = this.#failures.get(path);
|
|
533
|
+
if (failed) throw new NavigationFailure(failed);
|
|
534
|
+
if (!this.#attempted.has(path) && this.#attempted.size >= this.#fileLimit)
|
|
535
|
+
throw new NavigationFailure("file-budget-exhausted");
|
|
536
|
+
this.#attempted.add(path);
|
|
537
|
+
const document = await this.host.load(path);
|
|
538
|
+
this.documents.set(path, document);
|
|
539
|
+
let facts: ModuleFacts | undefined;
|
|
540
|
+
let retained = false;
|
|
541
|
+
try {
|
|
542
|
+
if (document.reference.origin.kind !== "worktree")
|
|
543
|
+
throw new NavigationFailure("historical-navigation-unsupported");
|
|
544
|
+
if (!document.utf8) throw new NavigationFailure("encoding");
|
|
545
|
+
if (this.bytesRead + document.bytes.length > MAX_STRUCTURE_BYTES)
|
|
546
|
+
throw new NavigationFailure("byte-budget-exhausted");
|
|
547
|
+
this.bytesRead += document.bytes.length;
|
|
548
|
+
const syntax = await this.host.syntax(document);
|
|
549
|
+
if (syntax.language === "go" || syntax.status !== "ok") {
|
|
550
|
+
const reason =
|
|
551
|
+
syntax.language === "go" ? "language-unsupported" : `syntax-${syntax.status}`;
|
|
552
|
+
this.#failures.set(path, reason);
|
|
553
|
+
throw new NavigationFailure(reason);
|
|
554
|
+
}
|
|
555
|
+
facts = collectModuleFacts(document, syntax);
|
|
556
|
+
this.modules.set(path, facts);
|
|
557
|
+
retained = retainSyntax;
|
|
558
|
+
return facts;
|
|
559
|
+
} finally {
|
|
560
|
+
if (!retained) {
|
|
561
|
+
if (facts) this.release(facts);
|
|
562
|
+
else this.host.releaseSyntax?.(document);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
release(facts: ModuleFacts): void {
|
|
567
|
+
this.host.releaseSyntax?.(facts.document);
|
|
568
|
+
const { language, status, limited } = facts.syntax;
|
|
569
|
+
facts.syntax = {
|
|
570
|
+
...(language ? { language } : {}),
|
|
571
|
+
status,
|
|
572
|
+
limited,
|
|
573
|
+
nodes: [],
|
|
574
|
+
children: [],
|
|
575
|
+
symbols: [],
|
|
576
|
+
roles: [],
|
|
577
|
+
diagnostics: [],
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
async verify(): Promise<Map<string, string>> {
|
|
581
|
+
const invalid = new Map<string, string>();
|
|
582
|
+
let exhausted = false;
|
|
583
|
+
for (const [path, document] of this.documents) {
|
|
584
|
+
this.checkAbort();
|
|
585
|
+
if (exhausted) {
|
|
586
|
+
invalid.set(path, "structural-read-budget-exhausted");
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
// oxlint-disable-next-line no-await-in-loop -- version rechecks consume one shared read budget.
|
|
591
|
+
await this.host.load(path, document.reference);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
const reason = navigationError(error);
|
|
594
|
+
if (!reason) throw error;
|
|
595
|
+
invalid.set(path, reason);
|
|
596
|
+
this.reasons.add(`${path}: ${reason}`);
|
|
597
|
+
exhausted = reason === "structural-read-budget-exhausted";
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return invalid;
|
|
601
|
+
}
|
|
602
|
+
result(items: NavigationItem[]): NavigationResult {
|
|
603
|
+
return {
|
|
604
|
+
items,
|
|
605
|
+
partial: this.reasons.size > 0,
|
|
606
|
+
reasons: [...this.reasons],
|
|
607
|
+
filesRead: this.#attempted.size,
|
|
608
|
+
bytesRead: this.bytesRead,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { MAX_IMPORT_FILES } from "./analysis-limits.js";
|
|
2
|
+
import { SiftLightError } from "./errors.js";
|
|
3
|
+
import {
|
|
4
|
+
NavigationContext,
|
|
5
|
+
moduleRange,
|
|
6
|
+
navigationError,
|
|
7
|
+
nodeLine,
|
|
8
|
+
type NavigationHost,
|
|
9
|
+
type NavigationInput,
|
|
10
|
+
type NavigationItem,
|
|
11
|
+
type NavigationResult,
|
|
12
|
+
} from "./import-model.js";
|
|
13
|
+
import {
|
|
14
|
+
STATIC_MODULE_RESOLUTION,
|
|
15
|
+
importStatementExcerpt,
|
|
16
|
+
traceImport,
|
|
17
|
+
tracePaths,
|
|
18
|
+
} from "./import-resolution.js";
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
NavigationHost,
|
|
22
|
+
NavigationInput,
|
|
23
|
+
NavigationItem,
|
|
24
|
+
NavigationResult,
|
|
25
|
+
} from "./import-model.js";
|
|
26
|
+
|
|
27
|
+
/** Return only static import/re-export evidence; this is not a call graph. */
|
|
28
|
+
export async function navigateImports(
|
|
29
|
+
host: NavigationHost,
|
|
30
|
+
input: NavigationInput,
|
|
31
|
+
): Promise<NavigationResult> {
|
|
32
|
+
if (input.line !== undefined && (!Number.isSafeInteger(input.line) || input.line < 1))
|
|
33
|
+
throw new SiftLightError("Navigation line must be a positive integer");
|
|
34
|
+
if (input.symbol !== undefined && input.symbol.trim().length === 0)
|
|
35
|
+
throw new SiftLightError("Navigation symbol must be nonempty");
|
|
36
|
+
const context = new NavigationContext(host, MAX_IMPORT_FILES);
|
|
37
|
+
let facts;
|
|
38
|
+
try {
|
|
39
|
+
facts = await context.module(input.path);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const reason = navigationError(error);
|
|
42
|
+
if (!reason) throw error;
|
|
43
|
+
context.reasons.add(reason);
|
|
44
|
+
return context.result([]);
|
|
45
|
+
}
|
|
46
|
+
const entries = [...facts.imports, ...facts.exports].filter((binding) => {
|
|
47
|
+
if (input.line !== undefined) {
|
|
48
|
+
const range = moduleRange(facts, binding.statement);
|
|
49
|
+
if (
|
|
50
|
+
input.line < facts.document.lineAt(range.start) ||
|
|
51
|
+
input.line > facts.document.lineAt(Math.max(range.start, range.end - 1))
|
|
52
|
+
)
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return (
|
|
56
|
+
input.symbol === undefined ||
|
|
57
|
+
binding.local === input.symbol ||
|
|
58
|
+
("imported" in binding
|
|
59
|
+
? binding.imported === input.symbol
|
|
60
|
+
: binding.exported === input.symbol)
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
const items: NavigationItem[] = [];
|
|
64
|
+
const affected: string[][] = [];
|
|
65
|
+
for (const binding of entries) {
|
|
66
|
+
context.checkAbort();
|
|
67
|
+
// oxlint-disable-next-line no-await-in-loop -- every path consumes the same bounded source context.
|
|
68
|
+
const trace = await traceImport(context, facts, binding);
|
|
69
|
+
const name = "imported" in binding ? (binding.local ?? binding.imported) : binding.exported;
|
|
70
|
+
items.push({
|
|
71
|
+
path: facts.document.path,
|
|
72
|
+
line: nodeLine(facts, binding.statement),
|
|
73
|
+
source: facts.document.reference,
|
|
74
|
+
range: moduleRange(facts, binding.statement),
|
|
75
|
+
excerpt: importStatementExcerpt(facts, binding.statement),
|
|
76
|
+
label: `Static import/re-export path: ${name} (${trace.status}${trace.reason ? `: ${trace.reason}` : ""})`,
|
|
77
|
+
details: { kind: "import", ...trace, resolutionPolicy: STATIC_MODULE_RESOLUTION },
|
|
78
|
+
});
|
|
79
|
+
affected.push([facts.document.path, ...tracePaths(trace)]);
|
|
80
|
+
if (trace.status === "unresolved") context.reasons.add(trace.reason ?? "import-unresolved");
|
|
81
|
+
}
|
|
82
|
+
if (entries.length === 0) context.reasons.add("no-static-import-export-at-target");
|
|
83
|
+
const invalid = await context.verify();
|
|
84
|
+
for (let index = 0; index < items.length; index++) {
|
|
85
|
+
const item = items[index];
|
|
86
|
+
const reason = affected[index]
|
|
87
|
+
?.map((path) => invalid.get(path))
|
|
88
|
+
.find((value) => value !== undefined);
|
|
89
|
+
if (!item || reason === undefined) continue;
|
|
90
|
+
item.label = `Static import/re-export path invalidated: ${reason}`;
|
|
91
|
+
item.details = {
|
|
92
|
+
...item.details,
|
|
93
|
+
status: "unresolved",
|
|
94
|
+
reason,
|
|
95
|
+
destination: undefined,
|
|
96
|
+
module: undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return context.result(items);
|
|
100
|
+
}
|