@mrclrchtr/supi-code-intelligence 6.1.0 → 6.2.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/README.md +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
- package/node_modules/@mrclrchtr/supi-lsp/README.md +21 -3
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
- package/node_modules/@mrclrchtr/supi-lsp/package.json +4 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +12 -7
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +0 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +6 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +2 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +5 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +2 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +17 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +40 -18
- package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +164 -30
- package/node_modules/@mrclrchtr/supi-lsp/src/config/lsp-settings.ts +9 -87
- package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +0 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +4 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +0 -29
- package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +7 -21
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-severity.ts +6 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +11 -14
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +0 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +0 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +29 -49
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -8
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +1 -9
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +9 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +26 -41
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +275 -99
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +4 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +59 -28
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +27 -19
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +26 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registration.ts +0 -17
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +83 -19
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +5 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/session/scanner.ts +24 -21
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +29 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +19 -40
- package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +13 -19
- package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +287 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +6 -18
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -5
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +1 -11
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +19 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +5 -23
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/operation-support.ts +3 -21
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +5 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +10 -20
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +7 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +9 -5
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +134 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-name.ts +96 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +11 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +39 -56
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +9 -7
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/js-binding-pattern.ts +26 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +2 -21
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-html-sql.ts +144 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline.ts +50 -37
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/scope.ts +138 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +0 -7
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +1 -14
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +0 -10
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +6 -8
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +34 -10
- package/package.json +5 -5
- package/src/analysis/capability/capability-warnings.ts +4 -22
- package/src/analysis/search/ast-scan-timing.ts +4 -2
- package/src/analysis/search/ast-scan.ts +1 -1
- package/src/api.ts +1 -0
- package/src/extension.ts +4 -2
- package/src/session/health-refresh.ts +24 -2
- package/src/session/health-types.ts +19 -0
- package/src/session/input/health-refactor.ts +16 -14
- package/src/session/refactor-types.ts +14 -1
- package/src/session/refactor-workflow.ts +21 -16
- package/src/substrate/lsp/lifecycle.ts +0 -3
- package/src/substrate/lsp/maintenance.ts +4 -5
- package/src/substrate/lsp/settings.ts +6 -9
- package/src/substrate/lsp/state.ts +0 -2
- package/src/substrate/workspace-provider-host.ts +2 -2
- package/src/tool/code_health/file-scope-markdown.ts +43 -0
- package/src/tool/code_health/markdown.ts +66 -66
- package/src/tool/code_health/refresh-outcome.ts +46 -0
- package/src/tool/code_health/refresh-status.ts +166 -23
- package/src/tool/code_health/result.ts +1 -0
- package/src/tool/code_health/tui.ts +18 -5
- package/src/tool/code_refactor_apply/guidance.ts +3 -2
- package/src/tool/code_refactor_plan/guidance.ts +1 -1
- package/src/tool/register.ts +5 -2
- package/src/tool/schemas.ts +21 -11
- package/node_modules/@mrclrchtr/supi-lsp/src/config/server-actions.ts +0 -59
- package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +0 -24
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +0 -132
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +0 -158
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +0 -167
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -42
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-registration.ts +0 -37
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/service-registry.ts +0 -30
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/structure.ts +0 -8
|
@@ -12,7 +12,7 @@ import type { GrammarId, SupportedExtension } from "./types.ts";
|
|
|
12
12
|
const sourceDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
|
|
14
14
|
/** Mapping from file extension to grammar identifier. */
|
|
15
|
-
const EXTENSION_GRAMMAR
|
|
15
|
+
const EXTENSION_GRAMMAR = {
|
|
16
16
|
".js": "javascript",
|
|
17
17
|
".jsx": "javascript",
|
|
18
18
|
".mjs": "javascript",
|
|
@@ -48,17 +48,12 @@ const EXTENSION_GRAMMAR: Record<string, GrammarId> = {
|
|
|
48
48
|
".xhtml": "html",
|
|
49
49
|
".r": "r",
|
|
50
50
|
".sql": "sql",
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const SUPPORTED_EXTENSIONS = new Set<string>(Object.keys(EXTENSION_GRAMMAR));
|
|
51
|
+
} satisfies Record<SupportedExtension, GrammarId>;
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
export function isSupportedFile(filePath: string): boolean {
|
|
57
|
-
return detectGrammar(filePath) !== undefined;
|
|
58
|
-
}
|
|
53
|
+
const SUPPORTED_EXTENSIONS = Object.keys(EXTENSION_GRAMMAR) as SupportedExtension[];
|
|
59
54
|
|
|
60
55
|
/** Return all supported file extensions (with leading dot). */
|
|
61
|
-
export function getSupportedExtensions():
|
|
56
|
+
export function getSupportedExtensions(): SupportedExtension[] {
|
|
62
57
|
return [...SUPPORTED_EXTENSIONS];
|
|
63
58
|
}
|
|
64
59
|
|
|
@@ -69,23 +64,10 @@ export function getSupportedExtensionGrammarEntries(): Array<
|
|
|
69
64
|
return Object.entries(EXTENSION_GRAMMAR) as Array<readonly [SupportedExtension, GrammarId]>;
|
|
70
65
|
}
|
|
71
66
|
|
|
72
|
-
/** Get the file extension if it's supported, otherwise undefined. */
|
|
73
|
-
export function getSupportedExtension(filePath: string): SupportedExtension | undefined {
|
|
74
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
75
|
-
return SUPPORTED_EXTENSIONS.has(ext) ? (ext as SupportedExtension) : undefined;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
67
|
/** Detect the grammar for a file. Returns undefined if unsupported. */
|
|
79
68
|
export function detectGrammar(filePath: string): GrammarId | undefined {
|
|
80
69
|
const ext = path.extname(filePath).toLowerCase();
|
|
81
|
-
return EXTENSION_GRAMMAR[ext];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const JS_TS_GRAMMARS: ReadonlySet<GrammarId> = new Set(["javascript", "typescript", "tsx"]);
|
|
85
|
-
|
|
86
|
-
/** Returns true if the grammar is one the JS/TS extractors understand. */
|
|
87
|
-
export function isJsTsGrammar(grammarId: GrammarId): boolean {
|
|
88
|
-
return JS_TS_GRAMMARS.has(grammarId);
|
|
70
|
+
return EXTENSION_GRAMMAR[ext as SupportedExtension];
|
|
89
71
|
}
|
|
90
72
|
|
|
91
73
|
/** Grammar WASM file names within the vendored resources directory. */
|
|
@@ -1,25 +1,7 @@
|
|
|
1
|
-
import { getSupportedExtensionGrammarEntries
|
|
1
|
+
import { getSupportedExtensionGrammarEntries } from "./language.ts";
|
|
2
2
|
import { supportsCallSitesGrammar } from "./tool/call-sites.ts";
|
|
3
3
|
import type { GrammarId, SupportedExtension } from "./types.ts";
|
|
4
4
|
|
|
5
|
-
const OUTLINE_GRAMMARS: ReadonlySet<GrammarId> = new Set([
|
|
6
|
-
"javascript",
|
|
7
|
-
"typescript",
|
|
8
|
-
"tsx",
|
|
9
|
-
"python",
|
|
10
|
-
"rust",
|
|
11
|
-
"go",
|
|
12
|
-
"c",
|
|
13
|
-
"cpp",
|
|
14
|
-
"java",
|
|
15
|
-
"kotlin",
|
|
16
|
-
"ruby",
|
|
17
|
-
"bash",
|
|
18
|
-
"html",
|
|
19
|
-
"r",
|
|
20
|
-
"sql",
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
5
|
/** Structural collection operations whose language support defines an AST Scan universe. */
|
|
24
6
|
const STRUCTURAL_SEARCH_OPERATIONS = ["outline", "imports", "exports", "call-sites"] as const;
|
|
25
7
|
|
|
@@ -49,10 +31,10 @@ export function supportsGrammarOperation(
|
|
|
49
31
|
case "call-sites":
|
|
50
32
|
return supportsCallSitesGrammar(grammar);
|
|
51
33
|
case "outline":
|
|
52
|
-
return
|
|
34
|
+
return true;
|
|
53
35
|
case "imports":
|
|
54
36
|
case "exports":
|
|
55
|
-
return
|
|
37
|
+
return grammar === "javascript" || grammar === "typescript" || grammar === "tsx";
|
|
56
38
|
default:
|
|
57
39
|
throw new TypeError(`Unsupported structural search operation: ${String(operation)}`);
|
|
58
40
|
}
|
|
@@ -35,31 +35,27 @@ export function createTreeSitterProvider(service: TreeSitterService): Structural
|
|
|
35
35
|
},
|
|
36
36
|
|
|
37
37
|
async exports(file, control?) {
|
|
38
|
-
const result =
|
|
38
|
+
const result = await service.exports(file, control);
|
|
39
39
|
return mapTreeSitterResult(result, mapExportRecords);
|
|
40
40
|
},
|
|
41
41
|
|
|
42
42
|
async outline(file, control?) {
|
|
43
|
-
const result =
|
|
43
|
+
const result = await service.outline(file, control);
|
|
44
44
|
return mapTreeSitterResult(result, mapOutlineItems);
|
|
45
45
|
},
|
|
46
46
|
|
|
47
47
|
async imports(file, control?) {
|
|
48
|
-
const result =
|
|
48
|
+
const result = await service.imports(file, control);
|
|
49
49
|
return mapTreeSitterResult(result, mapImportRecords);
|
|
50
50
|
},
|
|
51
51
|
|
|
52
52
|
async nodeAt(file, line, character, control?) {
|
|
53
|
-
const result = control
|
|
54
|
-
? await service.nodeAt(file, line, character, control)
|
|
55
|
-
: await service.nodeAt(file, line, character);
|
|
53
|
+
const result = await service.nodeAt(file, line, character, control);
|
|
56
54
|
return mapTreeSitterResult(result, mapNodeAtResult);
|
|
57
55
|
},
|
|
58
56
|
|
|
59
57
|
async callSites(file, control?) {
|
|
60
|
-
const result = control
|
|
61
|
-
? await service.callSites(file, control)
|
|
62
|
-
: await service.callSites(file);
|
|
58
|
+
const result = await service.callSites(file, control);
|
|
63
59
|
return mapTreeSitterResult(result, mapCallSites);
|
|
64
60
|
},
|
|
65
61
|
};
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import type { WorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
registerTreeSitterCapabilities,
|
|
8
|
-
unregisterTreeSitterCapabilities,
|
|
9
|
-
} from "./runtime-registration.ts";
|
|
10
|
-
import { clearSessionTreeSitterService, setSessionTreeSitterService } from "./service-registry.ts";
|
|
5
|
+
import { createTreeSitterProvider } from "../provider/tree-sitter-provider.ts";
|
|
6
|
+
import type { TreeSitterService, TreeSitterSession } from "../types.ts";
|
|
11
7
|
import { createTreeSitterSession } from "./session.ts";
|
|
12
8
|
|
|
13
9
|
/** Starting state before or after shutdown. */
|
|
14
|
-
|
|
10
|
+
type TsControllerState = TsControllerInitial | TsControllerReady | TsControllerUnavailable;
|
|
15
11
|
|
|
16
12
|
interface TsControllerInitial {
|
|
17
13
|
kind: "initial";
|
|
@@ -29,20 +25,20 @@ interface TsControllerUnavailable {
|
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
/** Result type from {@link TreeSitterRuntimeController.start}. */
|
|
32
|
-
|
|
28
|
+
type TsStartResult = { kind: "ready" } | { kind: "unavailable"; reason: string };
|
|
33
29
|
|
|
34
30
|
/** Pi-independent lifecycle for one shared workspace Structural Worker. */
|
|
35
31
|
export class TreeSitterRuntimeController {
|
|
36
32
|
readonly #cwd: string;
|
|
37
33
|
#state: TsControllerState = { kind: "initial" };
|
|
38
|
-
#runtime: WorkspaceRuntime
|
|
34
|
+
readonly #runtime: WorkspaceRuntime;
|
|
39
35
|
#pendingSession: TreeSitterSession | null = null;
|
|
40
36
|
#generation = 0;
|
|
41
37
|
#transition = Promise.resolve();
|
|
42
38
|
|
|
43
|
-
constructor(cwd: string, runtime
|
|
39
|
+
constructor(cwd: string, runtime: WorkspaceRuntime) {
|
|
44
40
|
this.#cwd = cwd;
|
|
45
|
-
this.#runtime = runtime
|
|
41
|
+
this.#runtime = runtime;
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
get cwd(): string {
|
|
@@ -53,14 +49,10 @@ export class TreeSitterRuntimeController {
|
|
|
53
49
|
return this.#state.kind;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
get service():
|
|
52
|
+
get service(): TreeSitterService | null {
|
|
57
53
|
return this.#state.kind === "ready" ? this.#state.service : null;
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
setRuntime(runtime: WorkspaceRuntime): void {
|
|
61
|
-
this.#runtime = runtime;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
56
|
/** Start and validate one Structural Worker before capability publication. */
|
|
65
57
|
start(): Promise<TsStartResult> {
|
|
66
58
|
const generation = ++this.#generation;
|
|
@@ -95,8 +87,7 @@ export class TreeSitterRuntimeController {
|
|
|
95
87
|
if (generation !== this.#generation || this.#pendingSession !== session) {
|
|
96
88
|
return supersededStart();
|
|
97
89
|
}
|
|
98
|
-
|
|
99
|
-
if (this.#runtime) registerTreeSitterCapabilities(this.#runtime, this.#cwd, session);
|
|
90
|
+
this.#runtime.registerStructural(this.#cwd, createTreeSitterProvider(session));
|
|
100
91
|
this.#pendingSession = null;
|
|
101
92
|
this.#state = { kind: "ready", session, service: session };
|
|
102
93
|
return { kind: "ready" };
|
|
@@ -119,8 +110,7 @@ export class TreeSitterRuntimeController {
|
|
|
119
110
|
}
|
|
120
111
|
|
|
121
112
|
#takeOwnedSessions(): Set<TreeSitterSession> {
|
|
122
|
-
|
|
123
|
-
clearSessionTreeSitterService(this.#cwd);
|
|
113
|
+
this.#runtime.clearStructural(this.#cwd);
|
|
124
114
|
const sessions = new Set<TreeSitterSession>();
|
|
125
115
|
if (this.#pendingSession) sessions.add(this.#pendingSession);
|
|
126
116
|
if (this.#state.kind === "ready") sessions.add(this.#state.session);
|
|
@@ -44,7 +44,13 @@ export function createTreeSitterService(client: StructuralWorkerClient): TreeSit
|
|
|
44
44
|
const control = typeof depthOrOptions === "string" ? undefined : depthOrOptions?.control;
|
|
45
45
|
return execute<CalleesAtResult>(
|
|
46
46
|
client,
|
|
47
|
-
{
|
|
47
|
+
{
|
|
48
|
+
operation: "calleesAt",
|
|
49
|
+
file,
|
|
50
|
+
line,
|
|
51
|
+
character,
|
|
52
|
+
...(depth === undefined ? {} : { depth }),
|
|
53
|
+
},
|
|
48
54
|
control,
|
|
49
55
|
);
|
|
50
56
|
},
|
|
@@ -4,8 +4,6 @@ import { Worker } from "node:worker_threads";
|
|
|
4
4
|
import {
|
|
5
5
|
type CodeRequestControl,
|
|
6
6
|
CodeRequestDeadlineError,
|
|
7
|
-
isCodeRequestDeadlineError,
|
|
8
|
-
isCodeRequestInterruption,
|
|
9
7
|
throwIfCodeRequestInterrupted,
|
|
10
8
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
11
9
|
import { isDebugOperationId, startDebugTimer } from "@mrclrchtr/supi-core/debug";
|
|
@@ -20,6 +18,7 @@ import {
|
|
|
20
18
|
STRUCTURAL_WORKER_PROTOCOL_VERSION,
|
|
21
19
|
type StructuralWorkerOperation,
|
|
22
20
|
type StructuralWorkerToParentMessage,
|
|
21
|
+
validateStructuralWorkerOperation,
|
|
23
22
|
validateWorkerToParentMessage,
|
|
24
23
|
} from "./structural-worker-protocol.ts";
|
|
25
24
|
|
|
@@ -122,6 +121,10 @@ export class StructuralWorkerClient {
|
|
|
122
121
|
input: StructuralWorkerOperation,
|
|
123
122
|
control?: CodeRequestControl,
|
|
124
123
|
): Promise<TreeSitterResult<T>> {
|
|
124
|
+
const validation = validateStructuralWorkerOperation(input);
|
|
125
|
+
if (validation.kind === "invalid") {
|
|
126
|
+
return { kind: "validation-error", message: validation.reason };
|
|
127
|
+
}
|
|
125
128
|
throwIfCodeRequestInterrupted(control);
|
|
126
129
|
if (this.#closed) return runtimeError("Structural Worker is shut down");
|
|
127
130
|
if (control?.operationId !== undefined && !isDebugOperationId(control.operationId)) {
|
|
@@ -308,6 +311,9 @@ export class StructuralWorkerClient {
|
|
|
308
311
|
const active = this.#active;
|
|
309
312
|
if (!active || active.id !== message.requestId)
|
|
310
313
|
return this.#protocolFailure("Unknown chunk request id");
|
|
314
|
+
if (active.finalSequence !== null) {
|
|
315
|
+
return this.#protocolFailure("Structural Worker sent a chunk after the final chunk");
|
|
316
|
+
}
|
|
311
317
|
if (active.awaitingAck !== null) {
|
|
312
318
|
return this.#protocolFailure("Structural Worker sent more than one unacknowledged chunk");
|
|
313
319
|
}
|
|
@@ -530,9 +536,7 @@ export class StructuralWorkerClient {
|
|
|
530
536
|
if (request.settled) return;
|
|
531
537
|
request.settled = true;
|
|
532
538
|
request.cleanupControl();
|
|
533
|
-
|
|
534
|
-
request.reject(error);
|
|
535
|
-
else request.reject(error);
|
|
539
|
+
request.reject(error);
|
|
536
540
|
}
|
|
537
541
|
}
|
|
538
542
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// biome-ignore-all lint/style/noExcessiveLinesPerFile: one private protocol module defines and validates the full closed contract
|
|
2
|
-
import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
|
|
3
2
|
import { isDebugOperationId } from "@mrclrchtr/supi-core/debug";
|
|
4
3
|
import type { TreeSitterResult } from "../types.ts";
|
|
5
4
|
import type { StructuralTimingEvent } from "./structural-timing.ts";
|
|
@@ -120,6 +119,45 @@ export type StructuralProtocolValidation<T> =
|
|
|
120
119
|
| { readonly kind: "valid"; readonly message: T }
|
|
121
120
|
| { readonly kind: "invalid"; readonly reason: string };
|
|
122
121
|
|
|
122
|
+
/** Validate one operation before it enters the Structural Worker mailbox. */
|
|
123
|
+
export function validateStructuralWorkerOperation(
|
|
124
|
+
value: unknown,
|
|
125
|
+
): StructuralProtocolValidation<StructuralWorkerOperation> {
|
|
126
|
+
if (!isRecord(value) || typeof value.operation !== "string") {
|
|
127
|
+
return invalid("Invalid structural operation");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
switch (value.operation) {
|
|
131
|
+
case "canParse":
|
|
132
|
+
case "outline":
|
|
133
|
+
case "imports":
|
|
134
|
+
case "exports":
|
|
135
|
+
case "callSites":
|
|
136
|
+
return hasExactKeys(value, ["operation", "file"]) && isFileValue(value.file)
|
|
137
|
+
? valid(value as unknown as StructuralWorkerOperation)
|
|
138
|
+
: invalid("Invalid structural operation");
|
|
139
|
+
case "query":
|
|
140
|
+
return hasExactKeys(value, ["operation", "file", "query"]) &&
|
|
141
|
+
isFileValue(value.file) &&
|
|
142
|
+
isQueryValue(value.query)
|
|
143
|
+
? valid(value as unknown as StructuralWorkerOperation)
|
|
144
|
+
: invalid("Invalid structural operation");
|
|
145
|
+
case "nodeAt":
|
|
146
|
+
return hasExactKeys(value, ["operation", "file", "line", "character"]) &&
|
|
147
|
+
isFileValue(value.file) &&
|
|
148
|
+
isPositiveInteger(value.line) &&
|
|
149
|
+
isPositiveInteger(value.character)
|
|
150
|
+
? valid(value as unknown as StructuralWorkerOperation)
|
|
151
|
+
: invalid("Invalid structural operation");
|
|
152
|
+
case "calleesAt":
|
|
153
|
+
return isCalleesAtOperation(value)
|
|
154
|
+
? valid(value as unknown as StructuralWorkerOperation)
|
|
155
|
+
: invalid("Invalid structural operation");
|
|
156
|
+
default:
|
|
157
|
+
return invalid("Invalid structural operation");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
123
161
|
/** Validate an untrusted request before the Structural Worker executes it. */
|
|
124
162
|
export function validateStructuralWorkerRequest(
|
|
125
163
|
value: unknown,
|
|
@@ -128,7 +166,7 @@ export function validateStructuralWorkerRequest(
|
|
|
128
166
|
const required = ["kind", "version", "generation", "requestId", "input", "cancellationFlag"];
|
|
129
167
|
const allowed = new Set([...required, "operationId", "deadline"]);
|
|
130
168
|
if (
|
|
131
|
-
required.some((key) => !(key
|
|
169
|
+
required.some((key) => !Object.hasOwn(value, key)) ||
|
|
132
170
|
Object.keys(value).some((key) => !allowed.has(key))
|
|
133
171
|
) {
|
|
134
172
|
return invalid("Invalid structural request envelope");
|
|
@@ -138,7 +176,6 @@ export function validateStructuralWorkerRequest(
|
|
|
138
176
|
value.version !== STRUCTURAL_WORKER_PROTOCOL_VERSION ||
|
|
139
177
|
!isPositiveInteger(value.generation) ||
|
|
140
178
|
!isRequestId(value.requestId) ||
|
|
141
|
-
!isRecord(value.input) ||
|
|
142
179
|
!(value.cancellationFlag instanceof SharedArrayBuffer) ||
|
|
143
180
|
value.cancellationFlag.byteLength !== Int32Array.BYTES_PER_ELEMENT ||
|
|
144
181
|
(value.deadline !== undefined &&
|
|
@@ -147,7 +184,71 @@ export function validateStructuralWorkerRequest(
|
|
|
147
184
|
) {
|
|
148
185
|
return invalid("Invalid structural request");
|
|
149
186
|
}
|
|
150
|
-
|
|
187
|
+
const operation = validateStructuralWorkerOperation(value.input);
|
|
188
|
+
return operation.kind === "valid"
|
|
189
|
+
? valid(value as unknown as StructuralWorkerRequestMessage)
|
|
190
|
+
: invalid(operation.reason);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Validate an untrusted cancellation envelope before it reaches the Worker state machine. */
|
|
194
|
+
export function validateStructuralWorkerCancel(
|
|
195
|
+
value: unknown,
|
|
196
|
+
): StructuralProtocolValidation<StructuralWorkerCancelMessage> {
|
|
197
|
+
if (!isRecord(value)) return invalid("Structural Worker cancel must be an object");
|
|
198
|
+
if (!hasExactKeys(value, ["kind", "version", "generation", "requestId"])) {
|
|
199
|
+
return invalid("Invalid structural cancel envelope");
|
|
200
|
+
}
|
|
201
|
+
if (
|
|
202
|
+
value.kind !== "cancel" ||
|
|
203
|
+
value.version !== STRUCTURAL_WORKER_PROTOCOL_VERSION ||
|
|
204
|
+
!isPositiveInteger(value.generation) ||
|
|
205
|
+
!isRequestId(value.requestId)
|
|
206
|
+
) {
|
|
207
|
+
return invalid("Invalid structural cancel");
|
|
208
|
+
}
|
|
209
|
+
return valid(value as unknown as StructuralWorkerCancelMessage);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Validate an untrusted chunk acknowledgement before it reaches the Worker state machine. */
|
|
213
|
+
export function validateStructuralWorkerChunkAck(
|
|
214
|
+
value: unknown,
|
|
215
|
+
): StructuralProtocolValidation<StructuralWorkerChunkAckMessage> {
|
|
216
|
+
if (!isRecord(value)) return invalid("Structural Worker chunk acknowledgement must be an object");
|
|
217
|
+
if (!hasExactKeys(value, ["kind", "version", "generation", "requestId", "sequence"])) {
|
|
218
|
+
return invalid("Invalid structural chunk acknowledgement envelope");
|
|
219
|
+
}
|
|
220
|
+
if (
|
|
221
|
+
value.kind !== "chunk-ack" ||
|
|
222
|
+
value.version !== STRUCTURAL_WORKER_PROTOCOL_VERSION ||
|
|
223
|
+
!isPositiveInteger(value.generation) ||
|
|
224
|
+
!isRequestId(value.requestId) ||
|
|
225
|
+
!isNonNegativeInteger(value.sequence)
|
|
226
|
+
) {
|
|
227
|
+
return invalid("Invalid structural chunk acknowledgement");
|
|
228
|
+
}
|
|
229
|
+
return valid(value as unknown as StructuralWorkerChunkAckMessage);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Validate any untrusted message sent from the parent to the Structural Worker. */
|
|
233
|
+
export function validateParentToStructuralWorkerMessage(
|
|
234
|
+
value: unknown,
|
|
235
|
+
): StructuralProtocolValidation<ParentToStructuralWorkerMessage> {
|
|
236
|
+
if (!isRecord(value)) return invalid("Structural Worker parent message must be an object");
|
|
237
|
+
switch (value.kind) {
|
|
238
|
+
case "request":
|
|
239
|
+
return validateStructuralWorkerRequest(value);
|
|
240
|
+
case "cancel":
|
|
241
|
+
return validateStructuralWorkerCancel(value);
|
|
242
|
+
case "chunk-ack":
|
|
243
|
+
return validateStructuralWorkerChunkAck(value);
|
|
244
|
+
default:
|
|
245
|
+
return invalid("Unknown Structural Worker parent message kind");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Read a generation only when the parent message has a safe positive integer shape. */
|
|
250
|
+
export function getStructuralWorkerMessageGeneration(value: unknown): number | undefined {
|
|
251
|
+
return isRecord(value) && isPositiveInteger(value.generation) ? value.generation : undefined;
|
|
151
252
|
}
|
|
152
253
|
|
|
153
254
|
/** Encode one complete structural result into fixed-size binary chunks. */
|
|
@@ -219,11 +320,6 @@ export function validateWorkerToParentMessage(
|
|
|
219
320
|
}
|
|
220
321
|
}
|
|
221
322
|
|
|
222
|
-
/** Build the exact request control represented by one Worker request. */
|
|
223
|
-
export interface StructuralWorkerRequestControl extends CodeRequestControl {
|
|
224
|
-
readonly cancellationFlag: Int32Array;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
323
|
function validateChunk(
|
|
228
324
|
value: Record<string, unknown>,
|
|
229
325
|
): StructuralProtocolValidation<StructuralWorkerChunkMessage> {
|
|
@@ -439,10 +535,39 @@ function isPositiveInteger(value: unknown): value is number {
|
|
|
439
535
|
return Number.isSafeInteger(value) && Number(value) > 0;
|
|
440
536
|
}
|
|
441
537
|
|
|
538
|
+
function isNonNegativeInteger(value: unknown): value is number {
|
|
539
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
540
|
+
}
|
|
541
|
+
|
|
442
542
|
function isRequestId(value: unknown): value is string {
|
|
443
543
|
return typeof value === "string" && value.length > 0 && value.length <= 128;
|
|
444
544
|
}
|
|
445
545
|
|
|
546
|
+
function isCalleesAtOperation(value: Record<string, unknown>): boolean {
|
|
547
|
+
const keysValid =
|
|
548
|
+
hasExactKeys(value, ["operation", "file", "line", "character"]) ||
|
|
549
|
+
hasExactKeys(value, ["operation", "file", "line", "character", "depth"]);
|
|
550
|
+
return (
|
|
551
|
+
keysValid &&
|
|
552
|
+
isFileValue(value.file) &&
|
|
553
|
+
isPositiveInteger(value.line) &&
|
|
554
|
+
isPositiveInteger(value.character) &&
|
|
555
|
+
(!("depth" in value) || value.depth === "direct" || value.depth === "deep")
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function isFileValue(value: unknown): value is string {
|
|
560
|
+
return isNonBlankString(value, 16_384);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function isQueryValue(value: unknown): value is string {
|
|
564
|
+
return isNonBlankString(value, 10_000);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function isNonBlankString(value: unknown, maxLength: number): value is string {
|
|
568
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength;
|
|
569
|
+
}
|
|
570
|
+
|
|
446
571
|
function valid<T>(message: T): StructuralProtocolValidation<T> {
|
|
447
572
|
return { kind: "valid", message };
|
|
448
573
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { GrammarId } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/** Normalize one grammar node that represents a call callee. */
|
|
4
|
+
export function normalizeCallName(text: string, grammarId: GrammarId, nodeType: string): string {
|
|
5
|
+
const normalized = text.trim().replace(/\s+/g, " ");
|
|
6
|
+
switch (grammarId) {
|
|
7
|
+
case "java":
|
|
8
|
+
return nodeType === "method_invocation"
|
|
9
|
+
? stripFinalInvocationArguments(normalized)
|
|
10
|
+
: normalized;
|
|
11
|
+
case "kotlin":
|
|
12
|
+
return nodeType === "call_expression" ? stripKotlinCallSuffix(normalized) : normalized;
|
|
13
|
+
case "ruby":
|
|
14
|
+
return nodeType === "call" ? stripRubyCallSuffix(normalized) : normalized;
|
|
15
|
+
default:
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stripKotlinCallSuffix(text: string): string {
|
|
21
|
+
const lambda = topLevelIndex(text, "{");
|
|
22
|
+
const withoutLambda = lambda >= 0 ? text.slice(0, lambda).trim() : text;
|
|
23
|
+
return stripFinalInvocationArguments(withoutLambda);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stripRubyCallSuffix(text: string): string {
|
|
27
|
+
const block = topLevelKeywordIndex(text, "do");
|
|
28
|
+
const withoutBlock = block >= 0 ? text.slice(0, block).trim() : text;
|
|
29
|
+
const bracedBlock = topLevelIndex(withoutBlock, "{");
|
|
30
|
+
const withoutSuffix = bracedBlock >= 0 ? withoutBlock.slice(0, bracedBlock).trim() : withoutBlock;
|
|
31
|
+
const withoutArguments = stripFinalInvocationArguments(withoutSuffix);
|
|
32
|
+
const whitespace = topLevelWhitespaceIndex(withoutArguments);
|
|
33
|
+
return whitespace >= 0 ? withoutArguments.slice(0, whitespace).trim() : withoutArguments;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stripFinalInvocationArguments(text: string): string {
|
|
37
|
+
if (!text.endsWith(")")) return text;
|
|
38
|
+
const openingIndex = findLastTopLevelOpening(text);
|
|
39
|
+
return openingIndex >= 0 ? text.slice(0, openingIndex).trim() : text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findLastTopLevelOpening(text: string): number {
|
|
43
|
+
let openingIndex = -1;
|
|
44
|
+
scanTopLevel(text, (character, index, depth) => {
|
|
45
|
+
if (character === "(" && depth === 0) openingIndex = index;
|
|
46
|
+
return false;
|
|
47
|
+
});
|
|
48
|
+
return openingIndex;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function topLevelKeywordIndex(text: string, keyword: string): number {
|
|
52
|
+
const marker = ` ${keyword}`;
|
|
53
|
+
const index = topLevelIndex(text, marker);
|
|
54
|
+
return index >= 0 ? index + 1 : -1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function topLevelIndex(text: string, marker: string): number {
|
|
58
|
+
return scanTopLevel(
|
|
59
|
+
text,
|
|
60
|
+
(_character, index, depth) => depth === 0 && text.startsWith(marker, index),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function topLevelWhitespaceIndex(text: string): number {
|
|
65
|
+
return scanTopLevel(text, (character, _index, depth) => depth === 0 && /\s/.test(character));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scanTopLevel(
|
|
69
|
+
text: string,
|
|
70
|
+
visit: (character: string, index: number, depth: number) => boolean,
|
|
71
|
+
): number {
|
|
72
|
+
let depth = 0;
|
|
73
|
+
let quote: string | null = null;
|
|
74
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
75
|
+
const character = text[index] ?? "";
|
|
76
|
+
if (quote !== null) {
|
|
77
|
+
if (character === quote && text[index - 1] !== "\\") quote = null;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (character === '"' || character === "'") {
|
|
81
|
+
quote = character;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (visit(character, index, depth)) return index;
|
|
85
|
+
depth = updateDepth(character, depth);
|
|
86
|
+
}
|
|
87
|
+
return -1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateDepth(character: string, depth: number): number {
|
|
91
|
+
if (character === "(" || character === "[" || character === "{") return depth + 1;
|
|
92
|
+
if (character === ")" || character === "]" || character === "}") {
|
|
93
|
+
return Math.max(0, depth - 1);
|
|
94
|
+
}
|
|
95
|
+
return depth;
|
|
96
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
|
|
|
4
4
|
import { detectGrammar } from "../language.ts";
|
|
5
5
|
import type { CallSiteMatch, GrammarId, TreeSitterResult } from "../types.ts";
|
|
6
6
|
import type { TreeSitterRuntime } from "../worker/runtime.ts";
|
|
7
|
+
import { normalizeCallName } from "./call-name.ts";
|
|
7
8
|
|
|
8
9
|
// ── Per-grammar call-site queries ─────────────────────────────────────
|
|
9
10
|
|
|
@@ -21,18 +22,16 @@ const CALL_SITE_QUERIES: Partial<Record<GrammarId, string>> = {
|
|
|
21
22
|
javascript: JS_TS_CALL_SITE_QUERY,
|
|
22
23
|
typescript: JS_TS_CALL_SITE_QUERY,
|
|
23
24
|
tsx: JS_TS_CALL_SITE_QUERY,
|
|
24
|
-
python:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"(call_expression . (simple_identifier) @call) (call_expression . (navigation_expression . (simple_identifier) @call))",
|
|
33
|
-
ruby: "(call method: (identifier) @call)",
|
|
25
|
+
python: "(call function: (_) @call)",
|
|
26
|
+
rust: "(call_expression function: (_) @call) (macro_invocation macro: (_) @call)",
|
|
27
|
+
go: "(call_expression function: (_) @call)",
|
|
28
|
+
c: "(call_expression function: (_) @call)",
|
|
29
|
+
cpp: "(call_expression function: (_) @call)",
|
|
30
|
+
java: "(method_invocation) @call (object_creation_expression type: (_) @call)",
|
|
31
|
+
kotlin: "(call_expression) @call",
|
|
32
|
+
ruby: "(call) @call",
|
|
34
33
|
bash: "(command name: (command_name) @call)",
|
|
35
|
-
r: "(call function: (
|
|
34
|
+
r: "(call function: (_) @call)",
|
|
36
35
|
};
|
|
37
36
|
|
|
38
37
|
/** Return whether the call-site extractor has a query for one grammar. */
|
|
@@ -78,7 +77,7 @@ export async function extractCallSites(
|
|
|
78
77
|
|
|
79
78
|
for (const capture of captures) {
|
|
80
79
|
// Normalize: trim whitespace and collapse internal whitespace to single space
|
|
81
|
-
const name = capture.text
|
|
80
|
+
const name = normalizeCallName(capture.text, grammarId, capture.nodeType);
|
|
82
81
|
if (name.length === 0) continue;
|
|
83
82
|
|
|
84
83
|
// Deduplicate by name + line
|