@mrclrchtr/supi-code-intelligence 1.9.1 → 1.11.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 +15 -7
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +7 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +112 -6
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +20 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +108 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +66 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +4 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +13 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +45 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +5 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +14 -3
- package/node_modules/@mrclrchtr/supi-core/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +7 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +112 -6
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +20 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +108 -0
- package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +66 -0
- package/node_modules/@mrclrchtr/supi-lsp/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +7 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +112 -6
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +20 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +108 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +66 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +4 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +13 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +45 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +5 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +14 -3
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +7 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +112 -6
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +20 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +108 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +66 -0
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +139 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +7 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +112 -6
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +20 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +108 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +66 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +4 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +13 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +45 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +5 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +14 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +7 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +112 -6
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +20 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +108 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +66 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
- package/package.json +5 -5
- package/src/intent/types.ts +34 -0
- package/src/planner/planner.ts +82 -0
- package/src/presentation/markdown/refactor.ts +27 -0
- package/src/refactor/apply-workspace-edit.ts +162 -0
- package/src/refactor/safety.ts +154 -0
- package/src/tool/execute-affected.ts +22 -0
- package/src/tool/execute-brief.ts +9 -2
- package/src/tool/execute-refactor.ts +101 -0
- package/src/tool/execute-relations.ts +21 -3
- package/src/tool/guidance.ts +18 -11
- package/src/tool/tool-specs.ts +25 -0
|
@@ -8,9 +8,11 @@ import type {
|
|
|
8
8
|
AgentToolResult,
|
|
9
9
|
AgentToolUpdateCallback,
|
|
10
10
|
ExtensionAPI,
|
|
11
|
+
ExtensionCommandContext,
|
|
11
12
|
ExtensionContext,
|
|
12
13
|
} from "@earendil-works/pi-coding-agent";
|
|
13
14
|
import { type TSchema, Type } from "typebox";
|
|
15
|
+
import { ProgressWidget, type WidgetProgress } from "./progress-widget.ts";
|
|
14
16
|
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Types
|
|
@@ -114,3 +116,67 @@ export const SymbolParam = Type.String({
|
|
|
114
116
|
|
|
115
117
|
/** Maximum results to return. */
|
|
116
118
|
export const MaxResultsParam = Type.Number({ description: "Maximum results to return" });
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Progress widget runner
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Run an async operation with a live TUI progress widget.
|
|
126
|
+
*
|
|
127
|
+
* Automatically manages:
|
|
128
|
+
* - The {@link ProgressWidget} lifecycle
|
|
129
|
+
* - `supi:working:start` / `supi:working:end` events for tab-spinner integration
|
|
130
|
+
* - Abort signal handling
|
|
131
|
+
* - Error catching (returns `null` on failure)
|
|
132
|
+
*
|
|
133
|
+
* Falls back to running without a widget when `ctx.hasUI` is false.
|
|
134
|
+
*
|
|
135
|
+
* @param pi - The extension API (for event emission).
|
|
136
|
+
* @param ctx - The command context (for UI access and hasUI check).
|
|
137
|
+
* @param title - The progress widget title.
|
|
138
|
+
* @param runner - Async function that receives (signal, onProgress).
|
|
139
|
+
* @returns The runner result, or `null` on cancel/error.
|
|
140
|
+
*/
|
|
141
|
+
export async function runWithProgressWidget<T>(
|
|
142
|
+
pi: ExtensionAPI,
|
|
143
|
+
ctx: ExtensionCommandContext,
|
|
144
|
+
title: string,
|
|
145
|
+
runner: (signal: AbortSignal, onProgress: (p: WidgetProgress) => void) => Promise<T>,
|
|
146
|
+
): Promise<T | null> {
|
|
147
|
+
if (!ctx.hasUI) {
|
|
148
|
+
// No UI — run without progress widget but still emit working events
|
|
149
|
+
pi.events.emit("supi:working:start", { source: "supi-core" });
|
|
150
|
+
try {
|
|
151
|
+
return await runner(new AbortController().signal, () => {});
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
} finally {
|
|
155
|
+
pi.events.emit("supi:working:end", { source: "supi-core" });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return ctx.ui.custom<T | null>((tui, theme, _kb, done) => {
|
|
160
|
+
const widget = new ProgressWidget(tui, theme, title);
|
|
161
|
+
let finished = false;
|
|
162
|
+
|
|
163
|
+
const finish = (result: T | null) => {
|
|
164
|
+
if (finished) return;
|
|
165
|
+
finished = true;
|
|
166
|
+
pi.events.emit("supi:working:end", { source: "supi-core" });
|
|
167
|
+
widget.dispose();
|
|
168
|
+
done(result);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
widget.onAbort = () => {
|
|
172
|
+
// Widget handles abort signal; runner resolves with cancel/error.
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
pi.events.emit("supi:working:start", { source: "supi-core" });
|
|
176
|
+
runner(widget.signal, (progress) => widget.updateProgress(progress))
|
|
177
|
+
.then((result) => finish(result))
|
|
178
|
+
.catch(() => finish(null));
|
|
179
|
+
|
|
180
|
+
return widget;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-tree-sitter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "SuPi Tree-sitter extension — structural AST analysis for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"web-tree-sitter": "^0.26.8",
|
|
27
|
-
"@mrclrchtr/supi-code-runtime": "1.
|
|
28
|
-
"@mrclrchtr/supi-core": "1.
|
|
27
|
+
"@mrclrchtr/supi-code-runtime": "1.11.0",
|
|
28
|
+
"@mrclrchtr/supi-core": "1.11.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@earendil-works/pi-ai": "*",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-code-intelligence",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "SuPi Code Intelligence extension — architecture briefs, caller/callee analysis, impact assessment, and pattern search for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
"src/**/*.ts"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@mrclrchtr/supi-
|
|
23
|
-
"@mrclrchtr/supi-core": "1.
|
|
24
|
-
"@mrclrchtr/supi-
|
|
25
|
-
"@mrclrchtr/supi-tree-sitter": "1.
|
|
22
|
+
"@mrclrchtr/supi-code-runtime": "1.11.0",
|
|
23
|
+
"@mrclrchtr/supi-core": "1.11.0",
|
|
24
|
+
"@mrclrchtr/supi-lsp": "1.11.0",
|
|
25
|
+
"@mrclrchtr/supi-tree-sitter": "1.11.0"
|
|
26
26
|
},
|
|
27
27
|
"bundledDependencies": [
|
|
28
28
|
"@mrclrchtr/supi-code-runtime",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized intent and routing types for the code-intelligence planner.
|
|
3
|
+
*/
|
|
4
|
+
import type { CodeIntelligenceToolName, CodeRelationsKind } from "../tool/tool-specs.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A route describes how the planner recommends handling a tool intent.
|
|
8
|
+
*/
|
|
9
|
+
export interface PlannerRoute {
|
|
10
|
+
/** Whether a semantic (LSP-backed) provider is available */
|
|
11
|
+
semanticAvailable: boolean;
|
|
12
|
+
/** Whether a structural (tree-sitter-backed) provider is available */
|
|
13
|
+
structuralAvailable: boolean;
|
|
14
|
+
/** Whether precise refactoring is available */
|
|
15
|
+
refactorAvailable: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The preferred execution strategy for this intent.
|
|
18
|
+
* - `semantic`: use LSP first
|
|
19
|
+
* - `structural`: use tree-sitter first
|
|
20
|
+
* - `search`: use explicit text/heuristic search
|
|
21
|
+
* - `unavailable`: no capability can satisfy this intent
|
|
22
|
+
*/
|
|
23
|
+
preferred: "semantic" | "structural" | "search" | "unavailable";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A resolved intents for a tool execution request.
|
|
28
|
+
* Provides the normalized target and routing info together.
|
|
29
|
+
*/
|
|
30
|
+
export interface ResolvedIntent {
|
|
31
|
+
tool: CodeIntelligenceToolName;
|
|
32
|
+
relationsKind?: CodeRelationsKind;
|
|
33
|
+
route: PlannerRoute;
|
|
34
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central planner for code-intelligence tool routing.
|
|
3
|
+
*
|
|
4
|
+
* Reads capability state from the shared workspace broker and returns
|
|
5
|
+
* routing decisions for each tool intent. Keeps the execution strategy
|
|
6
|
+
* explicit and centralized instead of scattering capability checks
|
|
7
|
+
* across per-tool execute files.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getDefaultWorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
|
|
11
|
+
import type { PlannerRoute } from "../intent/types.ts";
|
|
12
|
+
import type { CodeIntelligenceToolName, CodeRelationsKind } from "../tool/tool-specs.ts";
|
|
13
|
+
|
|
14
|
+
interface RouteAvailability {
|
|
15
|
+
semanticAvailable: boolean;
|
|
16
|
+
structuralAvailable: boolean;
|
|
17
|
+
refactorAvailable: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readAvailability(cwd: string): RouteAvailability {
|
|
21
|
+
const runtime = getDefaultWorkspaceRuntime();
|
|
22
|
+
const workspace = runtime.getWorkspace(cwd);
|
|
23
|
+
return {
|
|
24
|
+
semanticAvailable:
|
|
25
|
+
workspace.semantic.state.kind === "ready" && workspace.semantic.provider !== null,
|
|
26
|
+
structuralAvailable:
|
|
27
|
+
workspace.structural.state.kind === "ready" && workspace.structural.provider !== null,
|
|
28
|
+
refactorAvailable: workspace.semantic.refactorAvailable,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function withPreferred(
|
|
33
|
+
availability: RouteAvailability,
|
|
34
|
+
preferred: PlannerRoute["preferred"],
|
|
35
|
+
): PlannerRoute {
|
|
36
|
+
return { ...availability, preferred };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function semanticOnly(availability: RouteAvailability): PlannerRoute["preferred"] {
|
|
40
|
+
return availability.semanticAvailable ? "semantic" : "unavailable";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function structuralOnly(availability: RouteAvailability): PlannerRoute["preferred"] {
|
|
44
|
+
return availability.structuralAvailable ? "structural" : "unavailable";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function briefPreferred(availability: RouteAvailability): PlannerRoute["preferred"] {
|
|
48
|
+
if (availability.semanticAvailable) return "semantic";
|
|
49
|
+
if (availability.structuralAvailable) return "structural";
|
|
50
|
+
return "unavailable";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the routing decision for a tool intent in a workspace.
|
|
55
|
+
*/
|
|
56
|
+
export function routeFor(
|
|
57
|
+
cwd: string,
|
|
58
|
+
tool: CodeIntelligenceToolName,
|
|
59
|
+
relationsKind?: CodeRelationsKind,
|
|
60
|
+
): PlannerRoute {
|
|
61
|
+
const availability = readAvailability(cwd);
|
|
62
|
+
|
|
63
|
+
if (tool === "code_pattern") {
|
|
64
|
+
return withPreferred(availability, "search");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (tool === "code_relations") {
|
|
68
|
+
const preferred =
|
|
69
|
+
relationsKind === "callees" ? structuralOnly(availability) : semanticOnly(availability);
|
|
70
|
+
return withPreferred(availability, preferred);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (tool === "code_affected") {
|
|
74
|
+
return withPreferred(availability, semanticOnly(availability));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (tool === "code_refactor") {
|
|
78
|
+
return withPreferred(availability, availability.refactorAvailable ? "semantic" : "unavailable");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return withPreferred(availability, briefPreferred(availability));
|
|
82
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown renderer for refactor results.
|
|
3
|
+
*/
|
|
4
|
+
import type { ApplyResult } from "../../refactor/apply-workspace-edit.ts";
|
|
5
|
+
|
|
6
|
+
export interface RefactorRenderInput {
|
|
7
|
+
result: ApplyResult;
|
|
8
|
+
operation: string;
|
|
9
|
+
targetDescription: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Render a refactor result as human-readable markdown.
|
|
14
|
+
*/
|
|
15
|
+
export function renderRefactorResult(input: RefactorRenderInput): string {
|
|
16
|
+
const { result, operation, targetDescription } = input;
|
|
17
|
+
|
|
18
|
+
if (result.kind === "error") {
|
|
19
|
+
return `**Refactor failed:** ${result.reason}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
`**Refactor applied:** ${operation} on ${targetDescription}`,
|
|
24
|
+
`- Files changed: ${result.filesChanged}`,
|
|
25
|
+
`- Total edits: ${result.totalEdits}`,
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct-apply file mutation path for precise workspace edits.
|
|
3
|
+
*
|
|
4
|
+
* Writes edits atomically per-file: all transformed content is precomputed
|
|
5
|
+
* in memory before any file is written, so a mid-apply failure never
|
|
6
|
+
* leaves the workspace half-renamed.
|
|
7
|
+
*
|
|
8
|
+
* Edits are sorted by descending absolute offset (line + character)
|
|
9
|
+
* so same-line edits are applied right-to-left regardless of order.
|
|
10
|
+
*
|
|
11
|
+
* Only called after safety validation has passed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import type { FileEdit, WorkspaceEdit } from "@mrclrchtr/supi-code-runtime/api";
|
|
16
|
+
import { validateEditAgainstFiles } from "./safety.ts";
|
|
17
|
+
|
|
18
|
+
export type ApplyResult =
|
|
19
|
+
| { kind: "applied"; filesChanged: number; totalEdits: number }
|
|
20
|
+
| { kind: "error"; reason: string };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Apply a validated WorkspaceEdit to the filesystem.
|
|
24
|
+
*
|
|
25
|
+
* Precomputes every file's new content in memory first, then commits
|
|
26
|
+
* all writes. If a commit fails after some files were already written,
|
|
27
|
+
* the function rolls those files back to their original contents.
|
|
28
|
+
*/
|
|
29
|
+
export function applyWorkspaceEdit(edit: WorkspaceEdit): ApplyResult {
|
|
30
|
+
const validation = validateEditAgainstFiles(edit);
|
|
31
|
+
if (!validation.safe) {
|
|
32
|
+
return { kind: "error", reason: validation.reason };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const grouped = groupEditsByFile(edit.edits);
|
|
36
|
+
const originalContents = readOriginalContents(grouped);
|
|
37
|
+
if (originalContents.kind === "error") return originalContents;
|
|
38
|
+
|
|
39
|
+
const transformedContents = buildTransformedContents(grouped, originalContents.contents);
|
|
40
|
+
return commitTransformedContents(
|
|
41
|
+
transformedContents,
|
|
42
|
+
originalContents.contents,
|
|
43
|
+
edit.edits.length,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function groupEditsByFile(edits: FileEdit[]): Map<string, FileEdit[]> {
|
|
48
|
+
const grouped = new Map<string, FileEdit[]>();
|
|
49
|
+
for (const fileEdit of edits) {
|
|
50
|
+
const group = grouped.get(fileEdit.file) ?? [];
|
|
51
|
+
group.push(fileEdit);
|
|
52
|
+
grouped.set(fileEdit.file, group);
|
|
53
|
+
}
|
|
54
|
+
return grouped;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readOriginalContents(
|
|
58
|
+
grouped: Map<string, FileEdit[]>,
|
|
59
|
+
): { kind: "ok"; contents: Map<string, string> } | { kind: "error"; reason: string } {
|
|
60
|
+
const contents = new Map<string, string>();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
for (const file of [...grouped.keys()].sort()) {
|
|
64
|
+
contents.set(file, readFileSync(file, "utf-8"));
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return { kind: "error", reason: toErrorMessage(error) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { kind: "ok", contents };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildTransformedContents(
|
|
74
|
+
grouped: Map<string, FileEdit[]>,
|
|
75
|
+
originalContents: Map<string, string>,
|
|
76
|
+
): Map<string, string> {
|
|
77
|
+
const transformed = new Map<string, string>();
|
|
78
|
+
|
|
79
|
+
for (const [file, edits] of grouped) {
|
|
80
|
+
const originalContent = originalContents.get(file) ?? "";
|
|
81
|
+
transformed.set(file, applyEditsToContent(originalContent, edits));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return transformed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function applyEditsToContent(content: string, edits: FileEdit[]): string {
|
|
88
|
+
const lines = content.split("\n");
|
|
89
|
+
const sortedEdits = [...edits].sort(
|
|
90
|
+
(left, right) =>
|
|
91
|
+
absoluteOffset(right.range.start.line, right.range.start.character) -
|
|
92
|
+
absoluteOffset(left.range.start.line, left.range.start.character),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
let updated = content;
|
|
96
|
+
for (const fileEdit of sortedEdits) {
|
|
97
|
+
const startOffset = toOffset(lines, fileEdit.range.start.line, fileEdit.range.start.character);
|
|
98
|
+
const endOffset = toOffset(lines, fileEdit.range.end.line, fileEdit.range.end.character);
|
|
99
|
+
updated = updated.slice(0, startOffset) + fileEdit.newText + updated.slice(endOffset);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return updated;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function commitTransformedContents(
|
|
106
|
+
transformedContents: Map<string, string>,
|
|
107
|
+
originalContents: Map<string, string>,
|
|
108
|
+
totalEdits: number,
|
|
109
|
+
): ApplyResult {
|
|
110
|
+
const writtenFiles: string[] = [];
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
for (const file of [...transformedContents.keys()].sort()) {
|
|
114
|
+
writeFileSync(file, transformedContents.get(file) ?? "", "utf-8");
|
|
115
|
+
writtenFiles.push(file);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const rollbackError = rollbackWrittenFiles(writtenFiles, originalContents);
|
|
119
|
+
return {
|
|
120
|
+
kind: "error",
|
|
121
|
+
reason: rollbackError
|
|
122
|
+
? `${toErrorMessage(error)} (rollback failed: ${rollbackError})`
|
|
123
|
+
: toErrorMessage(error),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
kind: "applied",
|
|
129
|
+
filesChanged: transformedContents.size,
|
|
130
|
+
totalEdits,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function rollbackWrittenFiles(
|
|
135
|
+
writtenFiles: string[],
|
|
136
|
+
originalContents: Map<string, string>,
|
|
137
|
+
): string | null {
|
|
138
|
+
try {
|
|
139
|
+
for (const file of writtenFiles.reverse()) {
|
|
140
|
+
writeFileSync(file, originalContents.get(file) ?? "", "utf-8");
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return toErrorMessage(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function absoluteOffset(line: number, character: number): number {
|
|
149
|
+
return line * 1_000_000 + character;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function toOffset(lines: string[], line: number, character: number): number {
|
|
153
|
+
let offset = 0;
|
|
154
|
+
for (let index = 0; index < line && index < lines.length; index++) {
|
|
155
|
+
offset += lines[index].length + 1;
|
|
156
|
+
}
|
|
157
|
+
return offset + character;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toErrorMessage(error: unknown): string {
|
|
161
|
+
return error instanceof Error ? error.message : String(error);
|
|
162
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit validation safety checks for the code_refactor apply path.
|
|
3
|
+
*
|
|
4
|
+
* Rejects empty edits, invalid ranges, and out-of-bounds changes
|
|
5
|
+
* before they reach the filesystem.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import type { FileEdit, WorkspaceEdit } from "@mrclrchtr/supi-code-runtime/api";
|
|
10
|
+
|
|
11
|
+
export type ValidationResult = { safe: true } | { safe: false; reason: string };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate a WorkspaceEdit before applying it.
|
|
15
|
+
*
|
|
16
|
+
* Rejects:
|
|
17
|
+
* - empty edit sets
|
|
18
|
+
* - edits with negative line/character ranges
|
|
19
|
+
* - edits with end position before start position
|
|
20
|
+
* - overlapping edits on the same file
|
|
21
|
+
*/
|
|
22
|
+
export function validateEdit(edit: WorkspaceEdit): ValidationResult {
|
|
23
|
+
if (!edit.edits || edit.edits.length === 0) {
|
|
24
|
+
return { safe: false, reason: "Edit set is empty" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const fe of edit.edits) {
|
|
28
|
+
if (fe.range.start.line < 0 || fe.range.end.line < 0) {
|
|
29
|
+
return { safe: false, reason: `Invalid range in edit for file "${fe.file}": negative line` };
|
|
30
|
+
}
|
|
31
|
+
if (fe.range.start.character < 0 || fe.range.end.character < 0) {
|
|
32
|
+
return {
|
|
33
|
+
safe: false,
|
|
34
|
+
reason: `Invalid range in edit for file "${fe.file}": negative character`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
fe.range.end.line < fe.range.start.line ||
|
|
40
|
+
(fe.range.end.line === fe.range.start.line &&
|
|
41
|
+
fe.range.end.character < fe.range.start.character)
|
|
42
|
+
) {
|
|
43
|
+
return { safe: false, reason: `Edit for file "${fe.file}" has end before start` };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (hasOverlappingEdits(edit.edits)) {
|
|
48
|
+
return { safe: false, reason: "Overlapping edits in one or more files" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { safe: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate edit ranges against the current file contents.
|
|
56
|
+
*
|
|
57
|
+
* Rejects:
|
|
58
|
+
* - unreadable files
|
|
59
|
+
* - line indices beyond file length
|
|
60
|
+
* - character indices beyond the referenced line length
|
|
61
|
+
*/
|
|
62
|
+
export function validateEditAgainstFiles(edit: WorkspaceEdit): ValidationResult {
|
|
63
|
+
const baseValidation = validateEdit(edit);
|
|
64
|
+
if (!baseValidation.safe) return baseValidation;
|
|
65
|
+
|
|
66
|
+
const grouped = groupByFile(edit.edits);
|
|
67
|
+
for (const [file, fileEdits] of grouped) {
|
|
68
|
+
let content: string;
|
|
69
|
+
try {
|
|
70
|
+
content = readFileSync(file, "utf-8");
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return {
|
|
73
|
+
safe: false,
|
|
74
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const lines = content.split("\n");
|
|
79
|
+
for (const fileEdit of fileEdits) {
|
|
80
|
+
const lineValidation = validateLineBounds(file, fileEdit, lines);
|
|
81
|
+
if (!lineValidation.safe) return lineValidation;
|
|
82
|
+
|
|
83
|
+
const characterValidation = validateCharacterBounds(file, fileEdit, lines);
|
|
84
|
+
if (!characterValidation.safe) return characterValidation;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { safe: true };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateLineBounds(file: string, edit: FileEdit, lines: string[]): ValidationResult {
|
|
92
|
+
const maxLine = lines.length - 1;
|
|
93
|
+
if (edit.range.start.line > maxLine || edit.range.end.line > maxLine) {
|
|
94
|
+
return {
|
|
95
|
+
safe: false,
|
|
96
|
+
reason: `Edit in file "${file}" references line ${Math.max(edit.range.start.line, edit.range.end.line)}, but the file has only ${lines.length} line${lines.length === 1 ? "" : "s"}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { safe: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateCharacterBounds(file: string, edit: FileEdit, lines: string[]): ValidationResult {
|
|
103
|
+
const startLineLength = lines[edit.range.start.line]?.length ?? 0;
|
|
104
|
+
if (edit.range.start.character > startLineLength) {
|
|
105
|
+
return {
|
|
106
|
+
safe: false,
|
|
107
|
+
reason: `Edit in file "${file}" references character ${edit.range.start.character} on line ${edit.range.start.line}, but that line is only ${startLineLength} character${startLineLength === 1 ? "" : "s"} long`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const endLineLength = lines[edit.range.end.line]?.length ?? 0;
|
|
112
|
+
if (edit.range.end.character > endLineLength) {
|
|
113
|
+
return {
|
|
114
|
+
safe: false,
|
|
115
|
+
reason: `Edit in file "${file}" references character ${edit.range.end.character} on line ${edit.range.end.line}, but that line is only ${endLineLength} character${endLineLength === 1 ? "" : "s"} long`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { safe: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasOverlappingEdits(edits: FileEdit[]): boolean {
|
|
123
|
+
const fileGroups = groupByFile(edits);
|
|
124
|
+
for (const [, fileEdits] of fileGroups) {
|
|
125
|
+
const sorted = [...fileEdits].sort(
|
|
126
|
+
(a, b) =>
|
|
127
|
+
absoluteOffset(a.range.start.line, a.range.start.character) -
|
|
128
|
+
absoluteOffset(b.range.start.line, b.range.start.character),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
for (let index = 1; index < sorted.length; index++) {
|
|
132
|
+
const previous = sorted[index - 1];
|
|
133
|
+
const current = sorted[index];
|
|
134
|
+
const previousEnd = absoluteOffset(previous.range.end.line, previous.range.end.character);
|
|
135
|
+
const currentStart = absoluteOffset(current.range.start.line, current.range.start.character);
|
|
136
|
+
if (currentStart < previousEnd) return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function absoluteOffset(line: number, character: number): number {
|
|
143
|
+
return line * 1_000_000 + character;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function groupByFile(edits: FileEdit[]): Map<string, FileEdit[]> {
|
|
147
|
+
const groups = new Map<string, FileEdit[]>();
|
|
148
|
+
for (const fileEdit of edits) {
|
|
149
|
+
const group = groups.get(fileEdit.file) ?? [];
|
|
150
|
+
group.push(fileEdit);
|
|
151
|
+
groups.set(fileEdit.file, group);
|
|
152
|
+
}
|
|
153
|
+
return groups;
|
|
154
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { routeFor } from "../planner/planner.ts";
|
|
1
2
|
import type { CodeIntelResult } from "../types.ts";
|
|
2
3
|
import { executeAffected } from "../use-case/generate-affected.ts";
|
|
3
4
|
import { getCodeProvider } from "../workspace/request-context.ts";
|
|
@@ -43,6 +44,27 @@ export async function executeAffectedTool(
|
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
const route = routeFor(ctx.cwd, "code_affected");
|
|
48
|
+
if (route.preferred === "unavailable") {
|
|
49
|
+
return {
|
|
50
|
+
content:
|
|
51
|
+
"**Error:** No semantic analysis provider is available for this workspace. Use lsp_* tools directly if needed.",
|
|
52
|
+
details: {
|
|
53
|
+
type: "affected" as const,
|
|
54
|
+
data: {
|
|
55
|
+
confidence: "unavailable" as const,
|
|
56
|
+
directCount: 0,
|
|
57
|
+
downstreamCount: 0,
|
|
58
|
+
riskLevel: "low" as const,
|
|
59
|
+
checkNext: [],
|
|
60
|
+
likelyTests: [],
|
|
61
|
+
omittedCount: 0,
|
|
62
|
+
nextQueries: ["Check LSP configuration for this workspace"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
const providerState = getCodeProvider(ctx.cwd);
|
|
47
69
|
const provider = providerState.kind === "ready" ? providerState.provider : null;
|
|
48
70
|
return executeAffected(params, { cwd: ctx.cwd, provider });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildArchitectureModel } from "../model.ts";
|
|
2
|
+
import { routeFor } from "../planner/planner.ts";
|
|
2
3
|
import type { CodeIntelResult } from "../types.ts";
|
|
3
4
|
import { executeBrief } from "../use-case/generate-brief.ts";
|
|
4
5
|
import type { BriefInput } from "../use-case/types.ts";
|
|
@@ -14,7 +15,7 @@ export interface CodeBriefToolParams {
|
|
|
14
15
|
maxResults?: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
/** Execute the public code_brief tool through the use-case
|
|
18
|
+
/** Execute the public code_brief tool through the planner-backed use-case layers. */
|
|
18
19
|
export async function executeBriefTool(
|
|
19
20
|
params: CodeBriefToolParams,
|
|
20
21
|
ctx: { cwd: string },
|
|
@@ -24,8 +25,14 @@ export async function executeBriefTool(
|
|
|
24
25
|
return { content: error, details: undefined };
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
const route = routeFor(ctx.cwd, "code_brief");
|
|
27
29
|
const providerState = getCodeProvider(ctx.cwd);
|
|
28
|
-
|
|
30
|
+
let provider = providerState.kind === "ready" ? providerState.provider : null;
|
|
31
|
+
|
|
32
|
+
if (route.preferred === "unavailable") {
|
|
33
|
+
// code_brief can still work with model-only data even without providers
|
|
34
|
+
provider = null;
|
|
35
|
+
}
|
|
29
36
|
const model = await buildArchitectureModel(ctx.cwd);
|
|
30
37
|
|
|
31
38
|
const input: BriefInput = determineInput(params);
|