@mrclrchtr/supi-code-intelligence 1.9.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +15 -7
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +7 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
  5. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +4 -0
  6. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +13 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +45 -0
  8. package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +5 -1
  9. package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +14 -3
  10. package/node_modules/@mrclrchtr/supi-core/README.md +7 -1
  11. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/README.md +7 -1
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +7 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +4 -0
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +13 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +45 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +5 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +14 -3
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +7 -1
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  23. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  24. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +139 -1
  25. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +7 -1
  26. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +7 -1
  27. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  28. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
  29. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +4 -0
  30. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +13 -0
  31. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +45 -0
  32. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +5 -1
  33. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +14 -3
  34. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +7 -1
  35. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  36. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  37. package/package.json +5 -5
  38. package/src/intent/types.ts +34 -0
  39. package/src/planner/planner.ts +82 -0
  40. package/src/presentation/markdown/refactor.ts +27 -0
  41. package/src/refactor/apply-workspace-edit.ts +162 -0
  42. package/src/refactor/safety.ts +154 -0
  43. package/src/tool/execute-affected.ts +22 -0
  44. package/src/tool/execute-brief.ts +9 -2
  45. package/src/tool/execute-refactor.ts +101 -0
  46. package/src/tool/execute-relations.ts +21 -3
  47. package/src/tool/guidance.ts +18 -11
  48. package/src/tool/tool-specs.ts +25 -0
@@ -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/presentation layers. */
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
- const provider = providerState.kind === "ready" ? providerState.provider : null;
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);
@@ -0,0 +1,101 @@
1
+ /**
2
+ * code_refactor tool execution.
3
+ *
4
+ * Routes through the planner to get the refactor-capable semantic provider,
5
+ * calls rename/codeActions, validates the returned edit, applies it,
6
+ * and returns the result.
7
+ */
8
+
9
+ import { getDefaultWorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
10
+ import { toLspPosition } from "@mrclrchtr/supi-lsp/api";
11
+ import { routeFor } from "../planner/planner.ts";
12
+ import { renderRefactorResult } from "../presentation/markdown/refactor.ts";
13
+ import { applyWorkspaceEdit } from "../refactor/apply-workspace-edit.ts";
14
+ import { validateEdit } from "../refactor/safety.ts";
15
+ import { normalizePath } from "../search-helpers.ts";
16
+ import type { CodeIntelResult } from "../types.ts";
17
+
18
+ export interface CodeRefactorToolParams {
19
+ operation: "rename";
20
+ file: string;
21
+ line: number;
22
+ character: number;
23
+ newName: string;
24
+ // Future: "code-action" operation could be added here
25
+ }
26
+
27
+ /**
28
+ * Execute the code_refactor tool.
29
+ */
30
+ export async function executeRefactorTool(
31
+ params: CodeRefactorToolParams,
32
+ ctx: { cwd: string },
33
+ ): Promise<CodeIntelResult> {
34
+ const route = routeFor(ctx.cwd, "code_refactor");
35
+ if (route.preferred === "unavailable") {
36
+ return {
37
+ content:
38
+ "**Error:** No refactor-capable provider is available for this workspace. Ensure an LSP server is configured and running.",
39
+ details: undefined,
40
+ };
41
+ }
42
+
43
+ const runtime = getDefaultWorkspaceRuntime();
44
+ const ws = runtime.getWorkspace(ctx.cwd);
45
+ const provider = ws.semantic.provider;
46
+
47
+ if (!provider?.rename) {
48
+ return {
49
+ content: "**Error:** The active semantic provider does not support rename operations.",
50
+ details: undefined,
51
+ };
52
+ }
53
+
54
+ const resolvedFile = normalizePath(params.file, ctx.cwd);
55
+
56
+ // Execute the rename operation with 0-based LSP coordinates
57
+ const refactorResult = await provider.rename(
58
+ resolvedFile,
59
+ toLspPosition(params.line, params.character),
60
+ params.newName,
61
+ );
62
+
63
+ if (refactorResult.kind === "unavailable") {
64
+ return {
65
+ content: `**Refactor unavailable:** ${refactorResult.reason}`,
66
+ details: undefined,
67
+ };
68
+ }
69
+
70
+ if (refactorResult.kind === "ambiguous") {
71
+ const candidates = refactorResult.candidates
72
+ .map((c, i) => {
73
+ const location = c.file ? `${c.file}${c.line != null ? `:${c.line}` : ""}` : "";
74
+ return `${i + 1}. ${c.description}${location ? ` (${location})` : ""}`;
75
+ })
76
+ .join("\n");
77
+ return {
78
+ content: `**Refactor ambiguous:** Multiple matching targets found. Please disambiguate:\n${candidates}`,
79
+ details: undefined,
80
+ };
81
+ }
82
+
83
+ // Validate the edit
84
+ const validation = validateEdit(refactorResult.edits);
85
+ if (!validation.safe) {
86
+ return {
87
+ content: `**Refactor safety check failed:** ${validation.reason}`,
88
+ details: undefined,
89
+ };
90
+ }
91
+
92
+ // Apply the edit
93
+ const applyResult = applyWorkspaceEdit(refactorResult.edits);
94
+ const content = renderRefactorResult({
95
+ result: applyResult,
96
+ operation: `rename "${params.newName}"`,
97
+ targetDescription: `${resolvedFile}:${params.line}:${params.character}`,
98
+ });
99
+
100
+ return { content, details: undefined };
101
+ }
@@ -1,3 +1,4 @@
1
+ import { routeFor } from "../planner/planner.ts";
1
2
  import type { CodeIntelResult } from "../types.ts";
2
3
  import type { RelationsInput } from "../use-case/generate-relations.ts";
3
4
  import { executeRelations } from "../use-case/generate-relations.ts";
@@ -16,7 +17,7 @@ export interface CodeRelationsToolParams {
16
17
  maxResults?: number;
17
18
  }
18
19
 
19
- /** Execute the public code_relations tool through the relations use-case. */
20
+ /** Execute the public code_relations tool through the planner-backed routing. */
20
21
  export async function executeRelationsTool(
21
22
  params: CodeRelationsToolParams,
22
23
  ctx: { cwd: string },
@@ -26,11 +27,11 @@ export async function executeRelationsTool(
26
27
  return { content: error, details: undefined };
27
28
  }
28
29
 
29
- // Semantic actions (callers, callees, implementations) require file or symbol
30
+ // Semantic actions (callers, implementations) or structural (callees) require file or symbol
30
31
  if (!params.file && !params.symbol) {
31
32
  return {
32
33
  content:
33
- "**Error:** Semantic actions require either anchored coordinates (`file`, `line`, `character`) or a `symbol` for discovery.",
34
+ "**Error:** Relations require either anchored coordinates (`file`, `line`, `character`) or a `symbol` for discovery.",
34
35
  details: {
35
36
  type: "search" as const,
36
37
  data: {
@@ -44,6 +45,23 @@ export async function executeRelationsTool(
44
45
  };
45
46
  }
46
47
 
48
+ const route = routeFor(ctx.cwd, "code_relations", params.kind);
49
+ if (route.preferred === "unavailable") {
50
+ return {
51
+ content: `**Error:** No ${params.kind === "callees" ? "structural" : "semantic"} analysis provider is available for this workspace. Use tree_sitter_callees or lsp_* tools directly if needed.`,
52
+ details: {
53
+ type: "search" as const,
54
+ data: {
55
+ confidence: "unavailable" as const,
56
+ scope: null,
57
+ candidateCount: 0,
58
+ omittedCount: 0,
59
+ nextQueries: ["Check LSP and tree-sitter configuration"],
60
+ },
61
+ },
62
+ };
63
+ }
64
+
47
65
  const input: RelationsInput = {
48
66
  kind: params.kind,
49
67
  path: params.path,
@@ -17,26 +17,33 @@ export type CodeIntelligenceToolPromptSurfaceMap = Record<
17
17
  CodeIntelligenceToolPromptSurface
18
18
  >;
19
19
 
20
- // ── Cross-family orchestration guidelines ──────────────────────────────
20
+ // ── Intent-first orchestration guidelines ─────────────────────────────
21
21
  //
22
- // These are appended to the appropriate code_* tools so the model sees a
23
- // coherent strategy for choosing between code_*, lsp_*, and tree_sitter_*
24
- // tools. They do not re-own substrate metadata — each of the other packages
25
- // describes and documents its own tools independently.
22
+ // These steer the model toward choosing tools by user intent rather than
23
+ // by implementation family. Substrate tools (lsp_*, tree_sitter_*) are
24
+ // described as expert follow-up surfaces, not the primary choice.
26
25
 
27
- const ORCHESTRATION_GUIDELINES: Record<CodeIntelligenceToolName, string[]> = {
26
+ const INTENT_GUIDELINES: Record<CodeIntelligenceToolName, string[]> = {
28
27
  code_brief: [
29
- "After code_brief, use lsp_hover/lsp_definition/lsp_references for semantic detail or tree_sitter_* for quick structure.",
28
+ "Use code_brief for prioritized orientation on a project, package, file, or symbol.",
29
+ "The planner selects the best provider (semantic or structural) automatically.",
30
+ "After code_brief, use lsp_hover/lsp_definition/lsp_references for deeper semantic detail or tree_sitter_* for quick structural context.",
30
31
  ],
31
32
  code_map: ["Use code_brief instead when you need prioritized guidance."],
32
33
  code_relations: [
33
- "Follow caller results with lsp_references/lsp_definition; use tree_sitter_callees for structural outgoing calls.",
34
+ "The planner routes callers/implementations to semantic (LSP) analysis and callees to structural (tree-sitter) analysis.",
35
+ "Follow caller results with lsp_references/lsp_definition for additional context; use tree_sitter_callees for structural outgoing calls as a debug surface.",
34
36
  ],
35
37
  code_affected: [
36
- "Use lsp_references instead when you need a plain reference list, not impact analysis.",
38
+ "Uses semantic evidence for blast-radius assessment. Does not fall back to heuristic text search.",
39
+ "Use lsp_references when you only need a plain reference list without impact analysis.",
37
40
  ],
38
41
  code_pattern: [
39
- "Use tree_sitter_query or lsp_hover/lsp_definition when you need structure or semantic precision.",
42
+ "The only code_* tool that uses heuristic/text search behavior. For structured or semantic precision, use tree_sitter_query or lsp_hover/lsp_definition instead.",
43
+ ],
44
+ code_refactor: [
45
+ "Uses semantic provider for precise rename/code-action operations with safety checks.",
46
+ "Does not fall back to heuristic text replacement — if the provider cannot produce precise edits, the tool reports unavailable.",
40
47
  ],
41
48
  };
42
49
 
@@ -49,7 +56,7 @@ export function buildCodeIntelligenceToolPromptSurfaces(): CodeIntelligenceToolP
49
56
  {
50
57
  description: spec.description,
51
58
  promptSnippet: spec.promptSnippet,
52
- promptGuidelines: [...spec.basePromptGuidelines, ...ORCHESTRATION_GUIDELINES[spec.name]],
59
+ promptGuidelines: [...spec.basePromptGuidelines, ...INTENT_GUIDELINES[spec.name]],
53
60
  } satisfies CodeIntelligenceToolPromptSurface,
54
61
  ]),
55
62
  ) as CodeIntelligenceToolPromptSurfaceMap;
@@ -5,6 +5,7 @@ import { executeAffectedTool } from "./execute-affected.ts";
5
5
  import { executeBriefTool } from "./execute-brief.ts";
6
6
  import { executeMapTool } from "./execute-map.ts";
7
7
  import { executePatternTool } from "./execute-pattern.ts";
8
+ import { executeRefactorTool } from "./execute-refactor.ts";
8
9
  import { executeRelationsTool } from "./execute-relations.ts";
9
10
 
10
11
  const PathParam = Type.String({ description: "Scope path" });
@@ -28,6 +29,7 @@ export const CODE_INTELLIGENCE_TOOL_NAMES = [
28
29
  "code_relations",
29
30
  "code_affected",
30
31
  "code_pattern",
32
+ "code_refactor",
31
33
  ] as const;
32
34
  export type CodeIntelligenceToolName = (typeof CODE_INTELLIGENCE_TOOL_NAMES)[number];
33
35
 
@@ -94,6 +96,17 @@ const CodePatternParameters = Type.Object(
94
96
  { additionalProperties: false },
95
97
  );
96
98
 
99
+ const CodeRefactorParameters = Type.Object(
100
+ {
101
+ operation: Type.String({ description: "Refactor operation: rename" }),
102
+ file: FileParam,
103
+ line: LineParam,
104
+ character: CharacterParam,
105
+ newName: Type.String({ description: "New name for rename operation" }),
106
+ },
107
+ { additionalProperties: false },
108
+ );
109
+
97
110
  export interface CodeIntelligenceToolDefinitionSpec {
98
111
  name: CodeIntelligenceToolName;
99
112
  label: string;
@@ -162,4 +175,16 @@ export const CODE_INTELLIGENCE_TOOL_SPECS = [
162
175
  run: (params, ctx) =>
163
176
  executePatternTool(params as Parameters<typeof executePatternTool>[0], ctx),
164
177
  },
178
+ {
179
+ name: "code_refactor",
180
+ label: "Code Refactor",
181
+ description: "Apply a precise semantic refactor (rename) with direct-apply safety checks.",
182
+ promptSnippet: "code_refactor — semantic refactor with direct apply",
183
+ basePromptGuidelines: [
184
+ "Use code_refactor for safe semantic rename operations with precise workspace edits.",
185
+ ],
186
+ parameters: CodeRefactorParameters,
187
+ run: (params, ctx) =>
188
+ executeRefactorTool(params as Parameters<typeof executeRefactorTool>[0], ctx),
189
+ },
165
190
  ] as const satisfies readonly CodeIntelligenceToolDefinitionSpec[];