pi-plans 0.2.0 → 0.3.1
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 +90 -26
- package/agents/ref-analyst.md +18 -0
- package/index.ts +121 -9
- package/package.json +16 -1
- package/references/pi-planning-workflow.md +21 -6
- package/references/state-and-config.md +52 -5
- package/scripts/validate.ts +5 -0
- package/skills/plan-with-refs/SKILL.md +3 -3
- package/src/code-graph/commands.ts +483 -0
- package/src/code-graph/discovery.ts +118 -0
- package/src/code-graph/git.ts +108 -0
- package/src/code-graph/identity.ts +59 -0
- package/src/code-graph/indexer.ts +281 -0
- package/src/code-graph/materialize.ts +166 -0
- package/src/code-graph/mode.ts +28 -0
- package/src/code-graph/mutations.ts +160 -0
- package/src/code-graph/parser.ts +51 -0
- package/src/code-graph/parsers/javascript.ts +35 -0
- package/src/code-graph/parsers/python.ts +160 -0
- package/src/code-graph/parsers/tree-sitter.ts +316 -0
- package/src/code-graph/paths.ts +85 -0
- package/src/code-graph/prompts.ts +18 -0
- package/src/code-graph/resolver.ts +69 -0
- package/src/code-graph/runtime.ts +158 -0
- package/src/code-graph/schema.ts +135 -0
- package/src/code-graph/screening.ts +82 -0
- package/src/code-graph/store.ts +278 -0
- package/src/code-graph/summary.ts +435 -0
- package/src/code-graph/types.ts +163 -0
- package/src/compaction.ts +1125 -371
- package/src/config-command.ts +361 -0
- package/src/exec.ts +508 -693
- package/src/guard.ts +14 -1
- package/src/refine-prompts.ts +109 -0
- package/src/refine-ui-helpers.ts +71 -18
- package/src/refine-ui-state.ts +88 -22
- package/src/refine-ui.ts +210 -102
- package/src/state.ts +36 -7
- package/src/subagent.ts +164 -61
- package/src/termination-prompt.ts +22 -0
- package/tests/analyze-refs.test.ts +265 -0
- package/tests/ask-choice.test.ts +264 -0
- package/tests/autocomplete.test.ts +6 -1
- package/tests/code-graph-apply-action.test.ts +173 -0
- package/tests/code-graph-apply.test.ts +185 -0
- package/tests/code-graph-commands.test.ts +211 -0
- package/tests/code-graph-db.test.ts +166 -0
- package/tests/code-graph-discovery.test.ts +38 -0
- package/tests/code-graph-git.test.ts +94 -0
- package/tests/code-graph-index.test.ts +175 -0
- package/tests/code-graph-loop.e2e.test.ts +159 -0
- package/tests/code-graph-mutations.test.ts +117 -0
- package/tests/code-graph-parser.test.ts +85 -0
- package/tests/code-graph-rollback.test.ts +100 -0
- package/tests/code-graph-summary-batching.test.ts +518 -0
- package/tests/code-graph-summary.test.ts +148 -0
- package/tests/compaction.test.ts +371 -57
- package/tests/config-command.test.ts +263 -0
- package/tests/exec.test.ts +808 -241
- package/tests/fixtures/code-graph/sample.js +36 -0
- package/tests/fixtures/code-graph/sample.py +20 -0
- package/tests/fixtures/code-graph/sample.ts +15 -0
- package/tests/graph-aware-file-tools.test.ts +411 -0
- package/tests/guard.test.ts +27 -1
- package/tests/plans.test.ts +10 -0
- package/tests/refine-prompts.test.ts +101 -2
- package/tests/refine-ui.test.ts +371 -72
- package/tests/state.test.ts +32 -0
- package/tests/subagent.test.ts +48 -20
- package/tools/analyze-refs.ts +263 -0
- package/tools/ask-choice.ts +159 -11
- package/tools/code-graph.ts +277 -0
- package/tools/graph-aware-file-tools.ts +392 -0
- package/tools/plans.ts +97 -2
- package/tools/refine.ts +61 -15
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tree-sitter JSON helpers. Each language backend wraps a parser
|
|
3
|
+
* instance and converts the tree into our normalized function / render units.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
FunctionRecord,
|
|
8
|
+
ParseDiagnostic,
|
|
9
|
+
RenderUnit,
|
|
10
|
+
} from "../types.ts";
|
|
11
|
+
import { hashText, makeLocation, type ParserBackend, type ParsedFile } from "../parser.ts";
|
|
12
|
+
|
|
13
|
+
interface RawNode {
|
|
14
|
+
type: string;
|
|
15
|
+
startIndex: number;
|
|
16
|
+
endIndex: number;
|
|
17
|
+
startPosition: { row: number; column: number };
|
|
18
|
+
endPosition: { row: number; column: number };
|
|
19
|
+
namedChildren: RawNode[];
|
|
20
|
+
children: RawNode[];
|
|
21
|
+
text?: string;
|
|
22
|
+
parent?: RawNode;
|
|
23
|
+
isMissing?: boolean;
|
|
24
|
+
hasError?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Tree = {
|
|
28
|
+
rootNode: RawNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type Language = unknown;
|
|
32
|
+
|
|
33
|
+
interface ParserLike {
|
|
34
|
+
parse(input: string | Buffer): Tree;
|
|
35
|
+
setLanguage(language: Language): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TreeSitterBackendOptions {
|
|
39
|
+
ParserCtor: new () => ParserLike;
|
|
40
|
+
language: unknown;
|
|
41
|
+
languageId: FunctionRecord["kind"] extends string ? FunctionRecord["language"] : never;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const KINDS: Record<string, FunctionRecord["kind"]> = {
|
|
45
|
+
function_declaration: "declaration",
|
|
46
|
+
function: "declaration",
|
|
47
|
+
method_definition: "method",
|
|
48
|
+
generator_function_declaration: "generator",
|
|
49
|
+
arrow_function: "arrow",
|
|
50
|
+
function_expression: "expression",
|
|
51
|
+
lambda: "lambda",
|
|
52
|
+
async_function: "async",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function detectKind(node: RawNode): FunctionRecord["kind"] {
|
|
56
|
+
if (node.type === "method_definition") {
|
|
57
|
+
const accessor = (node.children ?? []).find((child) => child.type === "get" || child.type === "set");
|
|
58
|
+
if (accessor) return "accessor";
|
|
59
|
+
}
|
|
60
|
+
if (KINDS[node.type]) return KINDS[node.type];
|
|
61
|
+
if (node.type === "method_definition") return "method";
|
|
62
|
+
return "declaration";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectDiagnostics(node: RawNode, acc: ParseDiagnostic[]): void {
|
|
66
|
+
if (node.hasError || node.isMissing) {
|
|
67
|
+
acc.push({
|
|
68
|
+
message: node.isMissing ? `missing ${node.type}` : "syntax error",
|
|
69
|
+
severity: node.isMissing ? "missing" : "error",
|
|
70
|
+
startByte: node.startIndex,
|
|
71
|
+
endByte: node.endIndex,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
for (const child of node.children ?? []) collectDiagnostics(child, acc);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function classContainer(node: RawNode): string | undefined {
|
|
78
|
+
if (!node.parent) return undefined;
|
|
79
|
+
let p: RawNode | undefined = node.parent;
|
|
80
|
+
while (p) {
|
|
81
|
+
if (p.type === "class_declaration" || p.type === "class_definition" || p.type === "class" || p.type === "interface_declaration") {
|
|
82
|
+
return nameOfClass(p) ?? "<anonymous>";
|
|
83
|
+
}
|
|
84
|
+
p = p.parent;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function nameOfClass(node: RawNode): string | undefined {
|
|
90
|
+
const child = (node.children ?? []).find(
|
|
91
|
+
(c) => c.type === "identifier" || c.type === "name" || c.type === "type_identifier",
|
|
92
|
+
);
|
|
93
|
+
return child?.text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function functionName(node: RawNode): string | null {
|
|
97
|
+
const child = (node.children ?? []).find(
|
|
98
|
+
(c) => c.type === "identifier" || c.type === "name" || c.type === "property_identifier",
|
|
99
|
+
);
|
|
100
|
+
return child?.text ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function qualifiedName(node: RawNode, parentName: string | null, anonymousOrdinals?: Map<string, number>): string {
|
|
104
|
+
const named = functionName(node);
|
|
105
|
+
let id = named;
|
|
106
|
+
if (!id) {
|
|
107
|
+
const scope = parentName ?? classContainer(node) ?? "<module>";
|
|
108
|
+
const key = `${scope}\0${node.type}`;
|
|
109
|
+
const ordinal = (anonymousOrdinals?.get(key) ?? 0) + 1;
|
|
110
|
+
anonymousOrdinals?.set(key, ordinal);
|
|
111
|
+
id = `<anonymous:${node.type}#${ordinal}>`;
|
|
112
|
+
}
|
|
113
|
+
if (parentName) return `${parentName}.${id}`;
|
|
114
|
+
const container = classContainer(node);
|
|
115
|
+
return container ? `${container}.${id}` : id;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isOverloadSignatureNode(node: RawNode): boolean {
|
|
119
|
+
return (
|
|
120
|
+
node.type === "function_signature" ||
|
|
121
|
+
node.type === "method_signature" ||
|
|
122
|
+
node.type === "abstract_method_signature" ||
|
|
123
|
+
node.type === "declare_function"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function collectOverloadSignatures(node: RawNode, text: string, out: Map<string, string[]>): void {
|
|
128
|
+
if (isOverloadSignatureNode(node)) {
|
|
129
|
+
const name = functionName(node);
|
|
130
|
+
if (name) {
|
|
131
|
+
const base = qualifiedName(node, null);
|
|
132
|
+
const signatures = out.get(base) ?? [];
|
|
133
|
+
signatures.push(text.slice(node.startIndex, node.endIndex));
|
|
134
|
+
out.set(base, signatures);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const child of node.children ?? []) collectOverloadSignatures(child, text, out);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function locator(node: RawNode) {
|
|
141
|
+
return makeLocation(
|
|
142
|
+
node.startIndex,
|
|
143
|
+
node.endIndex,
|
|
144
|
+
node.startPosition.row + 1,
|
|
145
|
+
node.startPosition.column,
|
|
146
|
+
node.endPosition.row + 1,
|
|
147
|
+
node.endPosition.column,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export abstract class TreeSitterBackend implements ParserBackend {
|
|
152
|
+
abstract readonly language: FunctionRecord["language"];
|
|
153
|
+
protected readonly ParserCtor: new () => ParserLike;
|
|
154
|
+
protected readonly grammar: unknown;
|
|
155
|
+
|
|
156
|
+
constructor(opts: TreeSitterBackendOptions) {
|
|
157
|
+
this.ParserCtor = opts.ParserCtor;
|
|
158
|
+
this.grammar = opts.language;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected withParser<T>(fn: (parser: ParserLike) => T): T {
|
|
162
|
+
const parser = new this.ParserCtor();
|
|
163
|
+
parser.setLanguage(this.grammar);
|
|
164
|
+
return fn(parser);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
parse(source: Buffer | string): ParsedFile {
|
|
168
|
+
const text = typeof source === "string" ? source : source.toString("utf8");
|
|
169
|
+
const buf = Buffer.from(text, "utf8");
|
|
170
|
+
return this.withParser((parser) => {
|
|
171
|
+
const tree = parser.parse(text);
|
|
172
|
+
const diagnostics: ParseDiagnostic[] = [];
|
|
173
|
+
collectDiagnostics(tree.rootNode, diagnostics);
|
|
174
|
+
const overloads = new Map<string, string[]>();
|
|
175
|
+
collectOverloadSignatures(tree.rootNode, text, overloads);
|
|
176
|
+
const functions: FunctionRecord[] = [];
|
|
177
|
+
const renderUnits: RenderUnit[] = [
|
|
178
|
+
{
|
|
179
|
+
kind: "raw",
|
|
180
|
+
startByte: 0,
|
|
181
|
+
endByte: buf.length,
|
|
182
|
+
moveSupported: false,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
const anonymousOrdinals = new Map<string, number>();
|
|
186
|
+
this.collect(tree.rootNode, text, buf, functions, renderUnits, null, anonymousOrdinals);
|
|
187
|
+
for (const fn of functions) {
|
|
188
|
+
const signatures = overloads.get(fn.functionName);
|
|
189
|
+
if (signatures?.length) fn.overloadSignatures = signatures;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
language: this.language,
|
|
193
|
+
renderUnits,
|
|
194
|
+
functions,
|
|
195
|
+
diagnostics,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private collect(
|
|
201
|
+
node: RawNode,
|
|
202
|
+
text: string,
|
|
203
|
+
_buf: Buffer,
|
|
204
|
+
functions: FunctionRecord[],
|
|
205
|
+
renderUnits: RenderUnit[],
|
|
206
|
+
parentName: string | null,
|
|
207
|
+
anonymousOrdinals: Map<string, number>,
|
|
208
|
+
): void {
|
|
209
|
+
for (const child of node.children ?? []) {
|
|
210
|
+
this.visitNode(child, text, functions, renderUnits, parentName, anonymousOrdinals);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
protected visitNode(
|
|
215
|
+
node: RawNode,
|
|
216
|
+
text: string,
|
|
217
|
+
functions: FunctionRecord[],
|
|
218
|
+
renderUnits: RenderUnit[],
|
|
219
|
+
parentName: string | null,
|
|
220
|
+
anonymousOrdinals: Map<string, number>,
|
|
221
|
+
): void {
|
|
222
|
+
if (this.isCallable(node.type)) {
|
|
223
|
+
const name = functionName(node);
|
|
224
|
+
const qualified = qualifiedName(node, parentName, anonymousOrdinals);
|
|
225
|
+
const callableText = text.slice(node.startIndex, node.endIndex);
|
|
226
|
+
const container = classContainer(node);
|
|
227
|
+
functions.push({
|
|
228
|
+
fileDir: "",
|
|
229
|
+
fileName: "",
|
|
230
|
+
functionName: qualified,
|
|
231
|
+
language: this.language,
|
|
232
|
+
kind: detectKind(node),
|
|
233
|
+
fullCode: callableText,
|
|
234
|
+
fullCodeHash: hashText(callableText),
|
|
235
|
+
renderCode: callableText,
|
|
236
|
+
renderCodeHash: hashText(callableText),
|
|
237
|
+
parent: parentName ?? undefined,
|
|
238
|
+
container,
|
|
239
|
+
moveSupported: !!(name && container !== undefined ? true : false),
|
|
240
|
+
isPrimary: true,
|
|
241
|
+
overloadSignatures: undefined,
|
|
242
|
+
provenance: locator(node),
|
|
243
|
+
summary: null,
|
|
244
|
+
version: 1,
|
|
245
|
+
});
|
|
246
|
+
renderUnits.push({
|
|
247
|
+
kind: node.type === "method_definition" ? "method" : node.type === "arrow_function" ? "arrow" : "function",
|
|
248
|
+
startByte: node.startIndex,
|
|
249
|
+
endByte: node.endIndex,
|
|
250
|
+
label: qualified,
|
|
251
|
+
moveSupported: !!name,
|
|
252
|
+
children: this.collectNested(node, text, functions, renderUnits, qualified, anonymousOrdinals),
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this.collect(node, text, Buffer.from(text, "utf8"), functions, renderUnits, parentName, anonymousOrdinals);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
protected collectNested(
|
|
260
|
+
node: RawNode,
|
|
261
|
+
text: string,
|
|
262
|
+
functions: FunctionRecord[],
|
|
263
|
+
renderUnits: RenderUnit[],
|
|
264
|
+
parentName: string,
|
|
265
|
+
anonymousOrdinals: Map<string, number>,
|
|
266
|
+
): RenderUnit[] {
|
|
267
|
+
const nested: RenderUnit[] = [];
|
|
268
|
+
for (const child of node.children ?? []) {
|
|
269
|
+
if (this.isCallable(child.type)) {
|
|
270
|
+
const name = functionName(child);
|
|
271
|
+
const qualified = qualifiedName(child, parentName, anonymousOrdinals);
|
|
272
|
+
const callableText = text.slice(child.startIndex, child.endIndex);
|
|
273
|
+
const container = classContainer(child);
|
|
274
|
+
functions.push({
|
|
275
|
+
fileDir: "",
|
|
276
|
+
fileName: "",
|
|
277
|
+
functionName: qualified,
|
|
278
|
+
language: this.language,
|
|
279
|
+
kind: detectKind(child),
|
|
280
|
+
fullCode: callableText,
|
|
281
|
+
fullCodeHash: hashText(callableText),
|
|
282
|
+
renderCode: callableText,
|
|
283
|
+
renderCodeHash: hashText(callableText),
|
|
284
|
+
parent: parentName,
|
|
285
|
+
container,
|
|
286
|
+
moveSupported: !!name,
|
|
287
|
+
isPrimary: true,
|
|
288
|
+
provenance: locator(child),
|
|
289
|
+
summary: null,
|
|
290
|
+
version: 1,
|
|
291
|
+
});
|
|
292
|
+
nested.push({
|
|
293
|
+
kind: child.type === "method_definition" ? "method" : child.type === "arrow_function" ? "arrow" : "function",
|
|
294
|
+
startByte: child.startIndex,
|
|
295
|
+
endByte: child.endIndex,
|
|
296
|
+
label: qualified,
|
|
297
|
+
moveSupported: !!name,
|
|
298
|
+
});
|
|
299
|
+
} else if (child.children) {
|
|
300
|
+
for (const sub of child.children ?? []) this.visitNode(sub, text, functions, renderUnits, parentName, anonymousOrdinals);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return nested;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
protected isCallable(type: string): boolean {
|
|
307
|
+
return (
|
|
308
|
+
type === "function_declaration" ||
|
|
309
|
+
type === "method_definition" ||
|
|
310
|
+
type === "generator_function_declaration" ||
|
|
311
|
+
type === "arrow_function" ||
|
|
312
|
+
type === "function_expression" ||
|
|
313
|
+
type === "lambda"
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical worktree root and relative POSIX path helpers for the code-graph
|
|
3
|
+
* module. Strictly forbids absolute paths, `..`, and Windows separators.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { resolveStateRootOrNull } from "../state.ts";
|
|
9
|
+
|
|
10
|
+
export interface WorktreePaths {
|
|
11
|
+
worktreeRoot: string;
|
|
12
|
+
gitCommonDir: string;
|
|
13
|
+
stateRoot: string;
|
|
14
|
+
codeGraphDb: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class PathError extends Error {}
|
|
18
|
+
|
|
19
|
+
function runGit(cwd: string, args: string[]): { code: number; stdout: string } {
|
|
20
|
+
const env: Record<string, string | undefined> = { ...process.env };
|
|
21
|
+
for (const key of ["GIT_DIR", "GIT_COMMON_DIR", "GIT_WORK_TREE"]) delete env[key];
|
|
22
|
+
const result = spawnSync("git", args, { cwd, env, encoding: "utf8" });
|
|
23
|
+
return { code: result.status ?? 1, stdout: result.stdout ?? "" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveCanonicalWorktree(workdir: string): WorktreePaths {
|
|
27
|
+
const toplevel = runGit(workdir, ["rev-parse", "--show-toplevel"]);
|
|
28
|
+
if (toplevel.code !== 0) {
|
|
29
|
+
throw new PathError(`workdir is not inside a git work tree: ${workdir}`);
|
|
30
|
+
}
|
|
31
|
+
const common = runGit(workdir, ["rev-parse", "--git-common-dir"]);
|
|
32
|
+
if (common.code !== 0) {
|
|
33
|
+
throw new PathError(`git common dir unavailable for ${workdir}`);
|
|
34
|
+
}
|
|
35
|
+
const worktreeRoot = path.resolve(toplevel.stdout.trim());
|
|
36
|
+
const gitCommonDir = path.resolve(workdir, common.stdout.trim());
|
|
37
|
+
const stateRoot = path.join(gitCommonDir, "pi_plans");
|
|
38
|
+
if (!resolveStateRootOrNull(workdir)) {
|
|
39
|
+
throw new PathError(`pi-plans state missing; run a planning init first`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
worktreeRoot,
|
|
43
|
+
gitCommonDir,
|
|
44
|
+
stateRoot,
|
|
45
|
+
codeGraphDb: path.join(stateRoot, "code_graph.db"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizeRelative(worktreeRoot: string, target: string): { fileDir: string; fileName: string } {
|
|
50
|
+
const abs = path.resolve(target);
|
|
51
|
+
if (!abs.startsWith(worktreeRoot + path.sep) && abs !== worktreeRoot) {
|
|
52
|
+
throw new PathError(`path ${target} is outside worktree root ${worktreeRoot}`);
|
|
53
|
+
}
|
|
54
|
+
const rel = abs === worktreeRoot ? "." : path.relative(worktreeRoot, abs);
|
|
55
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
56
|
+
throw new PathError(`path ${target} escapes the worktree root`);
|
|
57
|
+
}
|
|
58
|
+
const posix = rel.split(path.sep).join("/");
|
|
59
|
+
const parts = posix.split("/");
|
|
60
|
+
const fileName = parts[parts.length - 1];
|
|
61
|
+
const fileDir = parts.length === 1 ? "." : parts.slice(0, -1).join("/");
|
|
62
|
+
return { fileDir, fileName };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isIgnoredDir(name: string): boolean {
|
|
66
|
+
const ignored = new Set([
|
|
67
|
+
"node_modules",
|
|
68
|
+
"dist",
|
|
69
|
+
"build",
|
|
70
|
+
".next",
|
|
71
|
+
".nuxt",
|
|
72
|
+
"out",
|
|
73
|
+
"coverage",
|
|
74
|
+
"target",
|
|
75
|
+
"venv",
|
|
76
|
+
".venv",
|
|
77
|
+
"__pycache__",
|
|
78
|
+
".pytest_cache",
|
|
79
|
+
".tox",
|
|
80
|
+
".mypy_cache",
|
|
81
|
+
]);
|
|
82
|
+
if (ignored.has(name)) return true;
|
|
83
|
+
if (name.startsWith(".") && name !== ".") return true;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph prompt blocks injected into planner/refiner/executor contexts,
|
|
3
|
+
* conditional on the graph mode. Enabled blocks are hard rules, not hints:
|
|
4
|
+
* indexed code files must be read at function level; `full` is the only
|
|
5
|
+
* whole-file exit.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function graphBlockForRefiner(enabled: boolean): string {
|
|
9
|
+
return enabled
|
|
10
|
+
? "Code graph: indexed code files MUST be read via the function digest or code_graph (screening, get-function) — no whole-file reads; full:true only as a last resort. Graph-aware read/edit are active."
|
|
11
|
+
: "Code graph disabled: use Read/grep/ls for code context.";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function graphBlockForExecutor(enabled: boolean): string {
|
|
15
|
+
return enabled
|
|
16
|
+
? "Code graph loop: indexed code files read as a function digest by default — never whole-file; drill in via offset/limit or code_graph get-function, full:true is the only whole-file exit. Edit via graph-aware edit (DB-first), then code_graph apply (its result includes the post-apply drift summary) → plans final-commit."
|
|
17
|
+
: "Code graph disabled: edit source files directly with edit/write.";
|
|
18
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative static call resolution. Captures call expressions of the form
|
|
3
|
+
* `name(...)` and `obj.method(...)` inside the same file; cross-file
|
|
4
|
+
* resolution is limited to relative imports and explicit module exports.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SourceLocation } from "./types.ts";
|
|
8
|
+
import type { DiscoveredFile } from "./discovery.ts";
|
|
9
|
+
|
|
10
|
+
export interface CallSite {
|
|
11
|
+
fromFunction: string;
|
|
12
|
+
calleeText: string;
|
|
13
|
+
kind: "call" | "definition" | "import";
|
|
14
|
+
resolution: "resolved" | "ambiguous" | "unresolved";
|
|
15
|
+
target?: { fileDir: string; fileName: string; functionName: string };
|
|
16
|
+
reason?: string;
|
|
17
|
+
provenance: SourceLocation;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CALL_PATTERN = /\b([a-zA-Z_$][\w$]*)(?:\.([a-zA-Z_$][\w$]*))?\s*\(/g;
|
|
21
|
+
|
|
22
|
+
export function resolveCalls(fromFunction: string, code: string, file: DiscoveredFile, out: CallSite[]): void {
|
|
23
|
+
for (const match of code.matchAll(CALL_PATTERN)) {
|
|
24
|
+
const head = match[1];
|
|
25
|
+
const member = match[2];
|
|
26
|
+
const start = match.index ?? 0;
|
|
27
|
+
const calleeText = match[0];
|
|
28
|
+
out.push({
|
|
29
|
+
fromFunction,
|
|
30
|
+
calleeText,
|
|
31
|
+
kind: "call",
|
|
32
|
+
resolution: "unresolved",
|
|
33
|
+
reason: "conservative resolution: same-file or relative import only",
|
|
34
|
+
provenance: locationOf(code, start, calleeText.length),
|
|
35
|
+
});
|
|
36
|
+
void head;
|
|
37
|
+
void member;
|
|
38
|
+
void file;
|
|
39
|
+
}
|
|
40
|
+
for (const match of code.matchAll(/^\s*(?:import|from)\s+([\w./-]+)/gm)) {
|
|
41
|
+
const start = match.index ?? 0;
|
|
42
|
+
out.push({
|
|
43
|
+
fromFunction,
|
|
44
|
+
calleeText: match[0],
|
|
45
|
+
kind: "import",
|
|
46
|
+
resolution: "unresolved",
|
|
47
|
+
reason: "import resolution deferred to module graph (not implemented in v1)",
|
|
48
|
+
provenance: locationOf(code, start, match[0].length),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function locationOf(code: string, byteStart: number, length: number): SourceLocation {
|
|
54
|
+
const before = code.slice(0, byteStart);
|
|
55
|
+
const startLine = before.split("\n").length;
|
|
56
|
+
const startColumn = before.length - before.lastIndexOf("\n");
|
|
57
|
+
const targetText = code.slice(byteStart, byteStart + length);
|
|
58
|
+
const endLine = startLine + targetText.split("\n").length - 1;
|
|
59
|
+
const endColumn =
|
|
60
|
+
targetText.split("\n").pop()?.length ?? startColumn;
|
|
61
|
+
return {
|
|
62
|
+
startByte: byteStart,
|
|
63
|
+
endByte: byteStart + length,
|
|
64
|
+
startLine,
|
|
65
|
+
startColumn,
|
|
66
|
+
endLine,
|
|
67
|
+
endColumn,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy runtime guards for the code-graph module. Imports happen on first use
|
|
3
|
+
* so that a missing parser dependency or unsupported Node version cannot
|
|
4
|
+
* prevent the rest of pi-plans from registering.
|
|
5
|
+
*
|
|
6
|
+
* The module never throws at import time; every consumer calls one of the
|
|
7
|
+
* `ensure*` helpers and inspects the resulting status object.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
export type RuntimeIssue =
|
|
13
|
+
| { kind: "node-version"; detail: string }
|
|
14
|
+
| { kind: "sqlite-import"; detail: string }
|
|
15
|
+
| { kind: "parser-import"; detail: string; package: string }
|
|
16
|
+
| { kind: "runtime-unsupported"; detail: string };
|
|
17
|
+
|
|
18
|
+
export interface RuntimeStatus {
|
|
19
|
+
nodeVersion: string;
|
|
20
|
+
nodeMajor: number;
|
|
21
|
+
hasExperimentalSqliteFlag: boolean;
|
|
22
|
+
graphEffectiveFloorMet: boolean;
|
|
23
|
+
parserAvailable: boolean;
|
|
24
|
+
sqliteAvailable: boolean;
|
|
25
|
+
bunDetected: boolean;
|
|
26
|
+
issues: RuntimeIssue[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MAJOR = Number(process.versions.node.split(".")[0]);
|
|
30
|
+
const MINOR = Number(process.versions.node.split(".")[1] ?? "0");
|
|
31
|
+
|
|
32
|
+
export const IS_BUN = typeof (process as { versions?: { bun?: string } }).versions?.bun === "string";
|
|
33
|
+
|
|
34
|
+
function flagHasExperimentalSqlite(): boolean {
|
|
35
|
+
try {
|
|
36
|
+
const out = execSync(process.execPath, ["--experimental-sqlite", "-e", "1"], { encoding: "utf8" });
|
|
37
|
+
return out === "1";
|
|
38
|
+
} catch {
|
|
39
|
+
try {
|
|
40
|
+
const out = execSync(process.execPath, ["-e", "process.features.sqlite ?? \"off\""], { encoding: "utf8" }).trim();
|
|
41
|
+
return out !== "off" && out !== "undefined" && out !== "\"off\"";
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function importSqlite(): Promise<unknown> {
|
|
49
|
+
return await import("node:sqlite");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function importParser(): Promise<unknown> {
|
|
53
|
+
const mod = await import("tree-sitter");
|
|
54
|
+
const js = await import("tree-sitter-javascript");
|
|
55
|
+
const ts = await import("tree-sitter-typescript");
|
|
56
|
+
const py = await import("tree-sitter-python");
|
|
57
|
+
return { default: mod.default ?? mod, js: js.default ?? js, ts: ts.default ?? ts, py: py.default ?? py };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function detectRuntimeStatus(): Promise<RuntimeStatus> {
|
|
61
|
+
const issues: RuntimeIssue[] = [];
|
|
62
|
+
if (IS_BUN) {
|
|
63
|
+
issues.push({
|
|
64
|
+
kind: "runtime-unsupported",
|
|
65
|
+
detail: "graph feature is Node-only; detected Bun runtime",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const nodeVersion = process.versions.node;
|
|
69
|
+
const graphEffectiveFloorMet = IS_BUN
|
|
70
|
+
? false
|
|
71
|
+
: (MAJOR === 22 && MINOR >= 13) || MAJOR > 22;
|
|
72
|
+
const hasFlag = flagHasExperimentalSqlite();
|
|
73
|
+
const sqliteAvailable = await importSqlite()
|
|
74
|
+
.then(() => true)
|
|
75
|
+
.catch((err: Error) => {
|
|
76
|
+
issues.push({ kind: "sqlite-import", detail: err.message });
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
if (!sqliteAvailable && !hasFlag && !IS_BUN) {
|
|
80
|
+
issues.push({
|
|
81
|
+
kind: "node-version",
|
|
82
|
+
detail: `node:sqlite requires Node >=22.13 or --experimental-sqlite; current ${nodeVersion}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const parserAvailable = await importParser()
|
|
86
|
+
.then(() => true)
|
|
87
|
+
.catch((err: Error) => {
|
|
88
|
+
issues.push({
|
|
89
|
+
kind: "parser-import",
|
|
90
|
+
package: "tree-sitter",
|
|
91
|
+
detail: err.message,
|
|
92
|
+
});
|
|
93
|
+
return false;
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
nodeVersion,
|
|
97
|
+
nodeMajor: MAJOR,
|
|
98
|
+
hasExperimentalSqliteFlag: hasFlag,
|
|
99
|
+
graphEffectiveFloorMet,
|
|
100
|
+
parserAvailable,
|
|
101
|
+
sqliteAvailable,
|
|
102
|
+
bunDetected: IS_BUN,
|
|
103
|
+
issues,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface GraphRuntime {
|
|
108
|
+
sqlite: typeof import("node:sqlite");
|
|
109
|
+
parser: {
|
|
110
|
+
Parser: typeof import("tree-sitter").default;
|
|
111
|
+
javascript: unknown;
|
|
112
|
+
typescript: unknown;
|
|
113
|
+
tsx: unknown;
|
|
114
|
+
python: unknown;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let cached: GraphRuntime | null = null;
|
|
119
|
+
|
|
120
|
+
export async function loadGraphRuntime(): Promise<{ runtime: GraphRuntime; status: RuntimeStatus }> {
|
|
121
|
+
if (cached) return { runtime: cached, status: await detectRuntimeStatus() };
|
|
122
|
+
const sqlite = await importSqlite();
|
|
123
|
+
const ParserMod = await import("tree-sitter");
|
|
124
|
+
const jsMod = await import("tree-sitter-javascript");
|
|
125
|
+
const tsMod = await import("tree-sitter-typescript");
|
|
126
|
+
const pyMod = await import("tree-sitter-python");
|
|
127
|
+
const Parser = (ParserMod as { default?: unknown }).default ?? ParserMod;
|
|
128
|
+
const runtime: GraphRuntime = {
|
|
129
|
+
sqlite: sqlite as typeof import("node:sqlite"),
|
|
130
|
+
parser: {
|
|
131
|
+
Parser: Parser as typeof import("tree-sitter").default,
|
|
132
|
+
javascript: (jsMod as { default?: unknown }).default ?? jsMod,
|
|
133
|
+
typescript: (tsMod as { default?: { typescript: unknown; tsx: unknown } }).default?.typescript ?? tsMod,
|
|
134
|
+
tsx: (tsMod as { default?: { tsx: unknown } }).default?.tsx ?? tsMod,
|
|
135
|
+
python: (pyMod as { default?: unknown }).default ?? pyMod,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
cached = runtime;
|
|
139
|
+
return { runtime, status: await detectRuntimeStatus() };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function describeRuntimeIssues(status: RuntimeStatus): string[] {
|
|
143
|
+
if (status.issues.length === 0) return [];
|
|
144
|
+
return status.issues.map((issue) => {
|
|
145
|
+
switch (issue.kind) {
|
|
146
|
+
case "node-version":
|
|
147
|
+
return `Node version: ${issue.detail}`;
|
|
148
|
+
case "sqlite-import":
|
|
149
|
+
return `node:sqlite import failed: ${issue.detail}`;
|
|
150
|
+
case "parser-import":
|
|
151
|
+
return `parser import failed (${issue.package}): ${issue.detail}`;
|
|
152
|
+
case "runtime-unsupported":
|
|
153
|
+
return `runtime: ${issue.detail}`;
|
|
154
|
+
default:
|
|
155
|
+
return "unknown runtime issue";
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|