pi-plans 0.2.0 → 0.3.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 (65) hide show
  1. package/README.md +74 -21
  2. package/index.ts +115 -9
  3. package/package.json +7 -1
  4. package/references/pi-planning-workflow.md +18 -3
  5. package/references/state-and-config.md +34 -2
  6. package/scripts/validate.ts +4 -0
  7. package/src/code-graph/commands.ts +437 -0
  8. package/src/code-graph/discovery.ts +118 -0
  9. package/src/code-graph/git.ts +108 -0
  10. package/src/code-graph/identity.ts +59 -0
  11. package/src/code-graph/indexer.ts +281 -0
  12. package/src/code-graph/materialize.ts +166 -0
  13. package/src/code-graph/mode.ts +28 -0
  14. package/src/code-graph/mutations.ts +160 -0
  15. package/src/code-graph/parser.ts +51 -0
  16. package/src/code-graph/parsers/javascript.ts +35 -0
  17. package/src/code-graph/parsers/python.ts +160 -0
  18. package/src/code-graph/parsers/tree-sitter.ts +316 -0
  19. package/src/code-graph/paths.ts +85 -0
  20. package/src/code-graph/prompts.ts +18 -0
  21. package/src/code-graph/resolver.ts +69 -0
  22. package/src/code-graph/runtime.ts +158 -0
  23. package/src/code-graph/schema.ts +135 -0
  24. package/src/code-graph/screening.ts +82 -0
  25. package/src/code-graph/store.ts +278 -0
  26. package/src/code-graph/summary.ts +435 -0
  27. package/src/code-graph/types.ts +163 -0
  28. package/src/compaction.ts +1125 -371
  29. package/src/config-command.ts +326 -0
  30. package/src/exec.ts +356 -686
  31. package/src/refine-prompts.ts +50 -0
  32. package/src/refine-ui-helpers.ts +71 -18
  33. package/src/refine-ui-state.ts +87 -21
  34. package/src/refine-ui.ts +210 -102
  35. package/src/state.ts +19 -6
  36. package/src/subagent.ts +163 -61
  37. package/tests/ask-choice.test.ts +263 -0
  38. package/tests/autocomplete.test.ts +6 -1
  39. package/tests/code-graph-apply.test.ts +185 -0
  40. package/tests/code-graph-commands.test.ts +211 -0
  41. package/tests/code-graph-db.test.ts +166 -0
  42. package/tests/code-graph-discovery.test.ts +38 -0
  43. package/tests/code-graph-git.test.ts +94 -0
  44. package/tests/code-graph-index.test.ts +175 -0
  45. package/tests/code-graph-loop.e2e.test.ts +159 -0
  46. package/tests/code-graph-mutations.test.ts +117 -0
  47. package/tests/code-graph-parser.test.ts +85 -0
  48. package/tests/code-graph-rollback.test.ts +100 -0
  49. package/tests/code-graph-summary-batching.test.ts +518 -0
  50. package/tests/code-graph-summary.test.ts +148 -0
  51. package/tests/compaction.test.ts +371 -57
  52. package/tests/config-command.test.ts +255 -0
  53. package/tests/exec.test.ts +665 -241
  54. package/tests/fixtures/code-graph/sample.js +36 -0
  55. package/tests/fixtures/code-graph/sample.py +20 -0
  56. package/tests/fixtures/code-graph/sample.ts +15 -0
  57. package/tests/graph-aware-file-tools.test.ts +411 -0
  58. package/tests/refine-prompts.test.ts +67 -2
  59. package/tests/refine-ui.test.ts +337 -72
  60. package/tests/subagent.test.ts +26 -20
  61. package/tools/ask-choice.ts +158 -11
  62. package/tools/code-graph.ts +254 -0
  63. package/tools/graph-aware-file-tools.ts +392 -0
  64. package/tools/plans.ts +84 -1
  65. package/tools/refine.ts +61 -15
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Parser backend interface. Each backend returns a list of render units plus a
3
+ * list of (callable) function records. Callable spans do not overlap with
4
+ * each other; overlapping spans are flattened through the parent linkage.
5
+ */
6
+
7
+ import type {
8
+ FunctionRecord,
9
+ Language,
10
+ ParseDiagnostic,
11
+ RenderUnit,
12
+ SourceLocation,
13
+ } from "./types.ts";
14
+
15
+ export interface ParsedFile {
16
+ language: Language;
17
+ renderUnits: RenderUnit[];
18
+ functions: FunctionRecord[];
19
+ diagnostics: ParseDiagnostic[];
20
+ }
21
+
22
+ export interface ParserBackend {
23
+ readonly language: Language;
24
+ parse(source: Buffer | string): ParsedFile;
25
+ }
26
+
27
+ export interface ParserContext {
28
+ ParserCtor: new () => unknown;
29
+ grammar: unknown;
30
+ }
31
+
32
+ export function makeLocation(
33
+ startByte: number,
34
+ endByte: number,
35
+ startLine: number,
36
+ startColumn: number,
37
+ endLine: number,
38
+ endColumn: number,
39
+ ): SourceLocation {
40
+ return { startByte, endByte, startLine, startColumn, endLine, endColumn };
41
+ }
42
+
43
+ export function hashText(text: string): string {
44
+ // Simple non-cryptographic hash, fast and stable for fingerprinting.
45
+ let h = 0x811c9dc5;
46
+ for (let i = 0; i < text.length; i++) {
47
+ h ^= text.charCodeAt(i);
48
+ h = Math.imul(h, 0x01000193);
49
+ }
50
+ return (h >>> 0).toString(16).padStart(8, "0");
51
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * JavaScript / ECMAScript backend. Implements JS function declaration,
3
+ * expression, arrow, method, generator and async functions.
4
+ */
5
+
6
+ import { TreeSitterBackend } from "./tree-sitter.ts";
7
+ import type { FunctionRecord, Language } from "../types.ts";
8
+
9
+ export interface JavaScriptGrammar {
10
+ default?: unknown;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ export class JavaScriptBackend extends TreeSitterBackend {
15
+ readonly language: Language = "javascript";
16
+ }
17
+
18
+ export class TypeScriptBackend extends TreeSitterBackend {
19
+ readonly language: Language = "typescript";
20
+ }
21
+
22
+ export class TsxBackend extends TreeSitterBackend {
23
+ readonly language: Language = "tsx";
24
+ }
25
+
26
+ export function makeBackend(
27
+ language: "javascript" | "typescript" | "tsx",
28
+ ParserCtor: new () => unknown,
29
+ grammar: unknown,
30
+ ): TreeSitterBackend {
31
+ const opts = { ParserCtor: ParserCtor as never, language: grammar };
32
+ if (language === "javascript") return new JavaScriptBackend(opts);
33
+ if (language === "typescript") return new TypeScriptBackend(opts);
34
+ return new TsxBackend(opts);
35
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Python backend. Recognizes `def`, `async def`, and `lambda` expressions,
3
+ * class methods, and decorators as parent-relative nested entries.
4
+ */
5
+
6
+ import type { ParserBackend, ParsedFile } from "../parser.ts";
7
+ import type { FunctionRecord, ParseDiagnostic, RenderUnit } from "../types.ts";
8
+ import { hashText, makeLocation } from "../parser.ts";
9
+
10
+ interface PythonNode {
11
+ type: string;
12
+ startIndex: number;
13
+ endIndex: number;
14
+ startPosition: { row: number; column: number };
15
+ endPosition: { row: number; column: number };
16
+ namedChildren: PythonNode[];
17
+ children: PythonNode[];
18
+ text?: string;
19
+ parent?: PythonNode;
20
+ isMissing?: boolean;
21
+ hasError?: boolean;
22
+ }
23
+
24
+ interface ParserLike {
25
+ parse(input: string | Buffer): { rootNode: PythonNode };
26
+ setLanguage(language: unknown): void;
27
+ }
28
+
29
+ export class PythonBackend implements ParserBackend {
30
+ readonly language = "python" as const;
31
+ private readonly ParserCtor: new () => ParserLike;
32
+ private readonly grammar: unknown;
33
+
34
+ constructor(ParserCtor: new () => unknown, grammar: unknown) {
35
+ this.ParserCtor = ParserCtor as never;
36
+ this.grammar = grammar;
37
+ }
38
+
39
+ parse(source: Buffer | string): ParsedFile {
40
+ const text = typeof source === "string" ? source : source.toString("utf8");
41
+ const buf = Buffer.from(text, "utf8");
42
+ const parser = new this.ParserCtor();
43
+ parser.setLanguage(this.grammar);
44
+ const tree = parser.parse(text);
45
+ const diagnostics: ParseDiagnostic[] = [];
46
+ const collectDiag = (node: PythonNode) => {
47
+ if (node.hasError || node.isMissing) {
48
+ diagnostics.push({
49
+ message: node.isMissing ? `missing ${node.type}` : "syntax error",
50
+ severity: node.isMissing ? "missing" : "error",
51
+ startByte: node.startIndex,
52
+ endByte: node.endIndex,
53
+ });
54
+ }
55
+ for (const child of node.namedChildren ?? []) collectDiag(child);
56
+ };
57
+ collectDiag(tree.rootNode);
58
+ const functions: FunctionRecord[] = [];
59
+ const renderUnits: RenderUnit[] = [
60
+ { kind: "raw", startByte: 0, endByte: buf.length, moveSupported: false },
61
+ ];
62
+ const anonymousOrdinals = new Map<string, number>();
63
+ const visit = (node: PythonNode, parentName: string | null) => {
64
+ if (node.type === "decorated_definition") {
65
+ for (const child of node.namedChildren ?? []) visit(child, parentName);
66
+ return;
67
+ }
68
+ if (node.type === "function_definition" || node.type === "lambda") {
69
+ const nameNode = (node.namedChildren ?? []).find(
70
+ (c) => c.type === "identifier" || c.type === "name",
71
+ );
72
+ let id = nameNode?.text;
73
+ if (!id) {
74
+ const scope = parentName ?? findClass(node) ?? "<module>";
75
+ const key = `${scope}\0${node.type}`;
76
+ const ordinal = (anonymousOrdinals.get(key) ?? 0) + 1;
77
+ anonymousOrdinals.set(key, ordinal);
78
+ id = `<anonymous:${node.type}#${ordinal}>`;
79
+ }
80
+ const qualified = parentName ? `${parentName}.${id}` : id;
81
+ const callableText = text.slice(node.startIndex, node.endIndex);
82
+ const container = findClass(node);
83
+ functions.push({
84
+ fileDir: "",
85
+ fileName: "",
86
+ functionName: qualified,
87
+ language: "python",
88
+ kind: node.type === "lambda" ? "lambda" : "declaration",
89
+ fullCode: callableText,
90
+ fullCodeHash: hashText(callableText),
91
+ renderCode: callableText,
92
+ renderCodeHash: hashText(callableText),
93
+ parent: parentName ?? undefined,
94
+ container,
95
+ moveSupported: node.type !== "lambda" && !!nameNode,
96
+ isPrimary: true,
97
+ provenance: locator(node),
98
+ summary: null,
99
+ version: 1,
100
+ });
101
+ renderUnits.push({
102
+ kind: node.type === "lambda" ? "lambda" : "function",
103
+ startByte: node.startIndex,
104
+ endByte: node.endIndex,
105
+ label: qualified,
106
+ moveSupported: node.type !== "lambda",
107
+ children: [],
108
+ });
109
+ return;
110
+ }
111
+ if (node.type === "class_definition") {
112
+ const classNameNode = (node.namedChildren ?? []).find(
113
+ (c) => c.type === "identifier" || c.type === "name",
114
+ );
115
+ const className = classNameNode?.text ?? "<anonymous>";
116
+ const children: RenderUnit[] = [];
117
+ for (const child of node.namedChildren ?? []) {
118
+ if (child.type === "block") {
119
+ for (const inner of child.namedChildren ?? []) visit(inner, className);
120
+ }
121
+ }
122
+ renderUnits.push({
123
+ kind: "raw",
124
+ startByte: node.startIndex,
125
+ endByte: node.endIndex,
126
+ label: className,
127
+ moveSupported: false,
128
+ children,
129
+ });
130
+ return;
131
+ }
132
+ for (const child of node.namedChildren ?? []) visit(child, parentName);
133
+ };
134
+ visit(tree.rootNode, null);
135
+ return { language: "python", renderUnits, functions, diagnostics };
136
+ }
137
+ }
138
+
139
+ function findClass(node: PythonNode): string | undefined {
140
+ let p: PythonNode | undefined = node.parent;
141
+ while (p) {
142
+ if (p.type === "class_definition") {
143
+ const nameNode = (p.namedChildren ?? []).find((c) => c.type === "identifier" || c.type === "name");
144
+ return nameNode?.text ?? "<anonymous>";
145
+ }
146
+ p = p.parent;
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ function locator(node: PythonNode) {
152
+ return makeLocation(
153
+ node.startIndex,
154
+ node.endIndex,
155
+ node.startPosition.row + 1,
156
+ node.startPosition.column,
157
+ node.endPosition.row + 1,
158
+ node.endPosition.column,
159
+ );
160
+ }
@@ -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 /apply-graph → /graph-drift → plans final-commit."
17
+ : "Code graph disabled: edit source files directly with edit/write.";
18
+ }