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,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM screening pipeline for code-graph. Records explicit consent and
|
|
3
|
+
* gracefully degrades to pending/declined summaries when the host has no UI
|
|
4
|
+
* or no model.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { hashText } from "./parser.ts";
|
|
10
|
+
import { Store } from "./store.ts";
|
|
11
|
+
import type { FunctionRecord, SummaryRecord } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
export interface CompletionRequest {
|
|
14
|
+
messages: Array<{ role: "user"; content: string }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CompletionHandle {
|
|
18
|
+
complete: (request: CompletionRequest) => Promise<{
|
|
19
|
+
content: Array<{ type: "text"; text: string }>;
|
|
20
|
+
stopReason?: string;
|
|
21
|
+
}>;
|
|
22
|
+
model?: () => { provider?: string; id?: string; api?: string; reasoning?: boolean };
|
|
23
|
+
thinkingLevel?: () => string | undefined;
|
|
24
|
+
hasUI?: boolean;
|
|
25
|
+
confirm?: (title: string, body: string) => Promise<boolean>;
|
|
26
|
+
notify?: (message: string, kind?: "info" | "warning" | "error") => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SummaryOptions {
|
|
30
|
+
store: Store;
|
|
31
|
+
ctx: CompletionHandle;
|
|
32
|
+
batchTokens?: number;
|
|
33
|
+
skipConsent?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SummaryReport {
|
|
37
|
+
processed: number;
|
|
38
|
+
ok: number;
|
|
39
|
+
failed: number;
|
|
40
|
+
declined: number;
|
|
41
|
+
batches: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SUMMARY_SCHEMA = {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
description: { type: "string", maxLength: 280 },
|
|
48
|
+
inputs: { type: "array", items: { type: "string", maxLength: 80 }, maxItems: 8 },
|
|
49
|
+
outputs: { type: "array", items: { type: "string", maxLength: 80 }, maxItems: 8 },
|
|
50
|
+
},
|
|
51
|
+
required: ["description", "inputs", "outputs"],
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
/** Conservative default prompt budget per completion call. Provider token
|
|
56
|
+
* counts vary; character-based estimates here intentionally bias low so the
|
|
57
|
+
* system cannot exceed typical context windows even with prompt overhead. */
|
|
58
|
+
const DEFAULT_BATCH_TOKENS = 8_000;
|
|
59
|
+
/** Characters-per-token ratio used for the conservative estimate. */
|
|
60
|
+
const CHARS_PER_TOKEN = 4;
|
|
61
|
+
/** Per-entry overhead added to the estimate (path, name, separator). */
|
|
62
|
+
const PER_ENTRY_OVERHEAD_TOKENS = 32;
|
|
63
|
+
/** Maximum allowed characters for a persisted `summary_error` value. */
|
|
64
|
+
const SUMMARY_ERROR_MAX_LENGTH = 240;
|
|
65
|
+
|
|
66
|
+
const SYSTEM_PROMPT = `You summarize code functions in structured JSON. Each input block starts with a "ref:" line. For each input return exactly one JSON object matching {ref: string, description: string, inputs: string[], outputs: string[]} where ref is the exact ref line value copied verbatim, description <= 280 chars, and arrays of short strings (<= 80 chars, <= 8 entries). Do not include any explanation or additional fields. Output one JSON object per input.`;
|
|
67
|
+
|
|
68
|
+
/** Opaque alignment key echoed back by the model. Built in ONE place so
|
|
69
|
+
* prompt, alignment, and DB writes can never disagree. */
|
|
70
|
+
export function buildRef(fileDir: string, fileName: string, functionName: string): string {
|
|
71
|
+
return `${fileDir}/${fileName}::${functionName}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PendingSummary {
|
|
75
|
+
fileDir: string;
|
|
76
|
+
fileName: string;
|
|
77
|
+
functionName: string;
|
|
78
|
+
fullCodeHash: string;
|
|
79
|
+
language: string;
|
|
80
|
+
fullCode: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function pendingFunctions(store: Store): PendingSummary[] {
|
|
84
|
+
const rows = store
|
|
85
|
+
.read(() =>
|
|
86
|
+
store.db
|
|
87
|
+
.prepare(
|
|
88
|
+
`SELECT file_dir, file_name, function_name, full_code_hash, language, full_code
|
|
89
|
+
FROM functions
|
|
90
|
+
WHERE summary_status IS NULL OR summary_status = 'pending' OR summary_status = 'failed'`,
|
|
91
|
+
)
|
|
92
|
+
.all(),
|
|
93
|
+
) as Array<{ file_dir: string; file_name: string; function_name: string; full_code_hash: string; language: string; full_code: string }>;
|
|
94
|
+
return rows.map((row) => ({
|
|
95
|
+
fileDir: row.file_dir,
|
|
96
|
+
fileName: row.file_name,
|
|
97
|
+
functionName: row.function_name,
|
|
98
|
+
fullCodeHash: row.full_code_hash,
|
|
99
|
+
language: row.language,
|
|
100
|
+
fullCode: row.full_code,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cacheKey(input: PendingSummary): string {
|
|
105
|
+
return `${input.language}:${input.fullCodeHash}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface CacheEntry {
|
|
109
|
+
summary: SummaryRecord;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const cache = new Map<string, CacheEntry>();
|
|
113
|
+
|
|
114
|
+
function boundedLength(value: string, max: number): string {
|
|
115
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function boundedErrorMessage(message: string): string {
|
|
119
|
+
const trimmed = message.replace(/[\r\n\t]+/g, " ").trim();
|
|
120
|
+
return boundedLength(trimmed, SUMMARY_ERROR_MAX_LENGTH);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function estimateEntryTokens(entry: PendingSummary): number {
|
|
124
|
+
const header = `${entry.fileDir}/${entry.fileName}::${entry.functionName}\n`;
|
|
125
|
+
const chars = header.length + entry.fullCode.length;
|
|
126
|
+
return Math.ceil(chars / CHARS_PER_TOKEN) + PER_ENTRY_OVERHEAD_TOKENS;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Greedy, deterministic batch builder. Each batch keeps a running token
|
|
130
|
+
* estimate; an entry that does not fit becomes the start of the next batch.
|
|
131
|
+
* A single oversized entry still forms its own batch (no batching progress
|
|
132
|
+
* must silently drop or split an entry). */
|
|
133
|
+
export function buildBatches(pending: PendingSummary[], batchTokens: number): PendingSummary[][] {
|
|
134
|
+
const limit = Math.max(1, batchTokens | 0);
|
|
135
|
+
const batches: PendingSummary[][] = [];
|
|
136
|
+
let current: PendingSummary[] = [];
|
|
137
|
+
let currentTokens = 0;
|
|
138
|
+
for (const entry of pending) {
|
|
139
|
+
const entryTokens = estimateEntryTokens(entry);
|
|
140
|
+
if (current.length === 0) {
|
|
141
|
+
current = [entry];
|
|
142
|
+
currentTokens = entryTokens;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (currentTokens + entryTokens <= limit) {
|
|
146
|
+
current.push(entry);
|
|
147
|
+
currentTokens += entryTokens;
|
|
148
|
+
} else {
|
|
149
|
+
batches.push(current);
|
|
150
|
+
current = [entry];
|
|
151
|
+
currentTokens = entryTokens;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (current.length > 0) batches.push(current);
|
|
155
|
+
return batches;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validate(record: unknown): SummaryRecord | null {
|
|
159
|
+
if (!record || typeof record !== "object") return null;
|
|
160
|
+
const obj = record as Record<string, unknown>;
|
|
161
|
+
if (typeof obj.description !== "string") return null;
|
|
162
|
+
if (!Array.isArray(obj.inputs) || !Array.isArray(obj.outputs)) return null;
|
|
163
|
+
if (!obj.inputs.every((s) => typeof s === "string")) return null;
|
|
164
|
+
if (!obj.outputs.every((s) => typeof s === "string")) return null;
|
|
165
|
+
const description = boundedLength(obj.description, 280);
|
|
166
|
+
const inputs = (obj.inputs as string[]).slice(0, 8).map((s) => boundedLength(s, 80));
|
|
167
|
+
const outputs = (obj.outputs as string[]).slice(0, 8).map((s) => boundedLength(s, 80));
|
|
168
|
+
return {
|
|
169
|
+
description,
|
|
170
|
+
inputs,
|
|
171
|
+
outputs,
|
|
172
|
+
status: "ok",
|
|
173
|
+
schemaVersion: 1,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Quote-aware balanced-brace scanner: extracts top-level {...} object
|
|
178
|
+
* substrings from raw model output, tolerating pretty-printed objects that
|
|
179
|
+
* span lines, multiple objects on one line, and garbage between objects.
|
|
180
|
+
* A truncated final object is dropped (never mis-parsed). Strings and
|
|
181
|
+
* escapes are skipped so braces inside literals cannot split an object. */
|
|
182
|
+
export function parseSummaryObjects(raw: string): unknown[] {
|
|
183
|
+
const objects: unknown[] = [];
|
|
184
|
+
let depth = 0;
|
|
185
|
+
let start = -1;
|
|
186
|
+
let inString = false;
|
|
187
|
+
let escaped = false;
|
|
188
|
+
for (let i = 0; i < raw.length; i++) {
|
|
189
|
+
const ch = raw[i]!;
|
|
190
|
+
if (inString) {
|
|
191
|
+
if (escaped) escaped = false;
|
|
192
|
+
else if (ch === "\\") escaped = true;
|
|
193
|
+
else if (ch === '"') inString = false;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (ch === '"') {
|
|
197
|
+
inString = true;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (ch === "{") {
|
|
201
|
+
if (depth === 0) start = i;
|
|
202
|
+
depth++;
|
|
203
|
+
} else if (ch === "}") {
|
|
204
|
+
if (depth > 0) {
|
|
205
|
+
depth--;
|
|
206
|
+
if (depth === 0 && start >= 0) {
|
|
207
|
+
try {
|
|
208
|
+
objects.push(JSON.parse(raw.slice(start, i + 1)));
|
|
209
|
+
} catch {
|
|
210
|
+
/* malformed object: skip */
|
|
211
|
+
}
|
|
212
|
+
start = -1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return objects;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface AlignOutcome {
|
|
221
|
+
/** Per-input-function resolution, aligned with `updates` order. */
|
|
222
|
+
aligned: Array<{ entry: PendingSummary; record: unknown } | { entry: PendingSummary; record: null }>;
|
|
223
|
+
/** true when order fallback was used (zero ref-carrying records, count equal). */
|
|
224
|
+
orderFallback: boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Align parsed records back to batch functions by their echoed `ref`.
|
|
228
|
+
* Duplicates: first wins. Unknown refs are dropped (uncounted). When NO
|
|
229
|
+
* record carries a usable ref AND counts match exactly, fall back to order
|
|
230
|
+
* alignment (the pre-ref contract) so legacy responses keep working. */
|
|
231
|
+
export function alignByRef(updates: PendingSummary[], records: unknown[]): AlignOutcome {
|
|
232
|
+
const byRef = new Map<string, unknown>();
|
|
233
|
+
let refRecords = 0;
|
|
234
|
+
for (const record of records) {
|
|
235
|
+
const ref = record && typeof record === "object" && typeof (record as Record<string, unknown>).ref === "string"
|
|
236
|
+
? (record as Record<string, unknown>).ref
|
|
237
|
+
: null;
|
|
238
|
+
if (ref === null) continue;
|
|
239
|
+
refRecords++;
|
|
240
|
+
if (!byRef.has(ref)) byRef.set(ref, record);
|
|
241
|
+
}
|
|
242
|
+
const orderFallback = refRecords === 0 && records.length === updates.length;
|
|
243
|
+
const aligned: AlignOutcome["aligned"] = updates.map((entry, index) => {
|
|
244
|
+
if (orderFallback) return { entry, record: records[index] ?? null };
|
|
245
|
+
return { entry, record: byRef.get(buildRef(entry.fileDir, entry.fileName, entry.functionName)) ?? null };
|
|
246
|
+
});
|
|
247
|
+
return { aligned, orderFallback };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function effectiveEffort(ctx: CompletionHandle): string | undefined {
|
|
251
|
+
const model = ctx.model?.();
|
|
252
|
+
if (!model) return undefined;
|
|
253
|
+
const api = model.api ?? "";
|
|
254
|
+
if (api.includes("openai-completions") || api.includes("openai-responses") || api.includes("anthropic")) {
|
|
255
|
+
return "low";
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function userConsent(opts: SummaryOptions, pending: PendingSummary[]): Promise<boolean> {
|
|
261
|
+
if (opts.skipConsent) return true;
|
|
262
|
+
if (!opts.ctx.hasUI) return false;
|
|
263
|
+
if (!opts.ctx.confirm) return false;
|
|
264
|
+
const model = opts.ctx.model?.();
|
|
265
|
+
const body = [
|
|
266
|
+
`Functions awaiting summary: ${pending.length}`,
|
|
267
|
+
`Model: ${model ? `${model.provider ?? "?"}/${model.id ?? "?"}` : "unknown"}`,
|
|
268
|
+
`Thinking level: ${opts.ctx.thinkingLevel?.() ?? "default"}`,
|
|
269
|
+
`Reasoning capability: ${model?.reasoning ? "yes" : "no"}`,
|
|
270
|
+
`Send source code for each function to the current model?`,
|
|
271
|
+
].join("\n");
|
|
272
|
+
return await opts.ctx.confirm("Generate code summaries with LLM?", body);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function generateSummaries(opts: SummaryOptions): Promise<SummaryReport> {
|
|
276
|
+
const pending = pendingFunctions(opts.store);
|
|
277
|
+
if (pending.length === 0) {
|
|
278
|
+
return { processed: 0, ok: 0, failed: 0, declined: 0, batches: 0 };
|
|
279
|
+
}
|
|
280
|
+
const consent = await userConsent(opts, pending);
|
|
281
|
+
if (!consent) {
|
|
282
|
+
markDeclined(opts.store, pending);
|
|
283
|
+
return { processed: pending.length, ok: 0, failed: 0, declined: pending.length, batches: 0 };
|
|
284
|
+
}
|
|
285
|
+
const effort = effectiveEffort(opts.ctx);
|
|
286
|
+
const batchTokens = opts.batchTokens ?? DEFAULT_BATCH_TOKENS;
|
|
287
|
+
const batches = buildBatches(pending, batchTokens);
|
|
288
|
+
const report: SummaryReport = {
|
|
289
|
+
processed: 0,
|
|
290
|
+
ok: 0,
|
|
291
|
+
failed: 0,
|
|
292
|
+
declined: 0,
|
|
293
|
+
batches: batches.length,
|
|
294
|
+
};
|
|
295
|
+
for (let index = 0; index < batches.length; index++) {
|
|
296
|
+
const updates = batches[index];
|
|
297
|
+
opts.ctx.notify?.(
|
|
298
|
+
`code-graph summary batch ${index + 1}/${batches.length} (${updates.length} function(s))`,
|
|
299
|
+
"info",
|
|
300
|
+
);
|
|
301
|
+
const prompts = updates
|
|
302
|
+
.map((entry) => `ref: ${buildRef(entry.fileDir, entry.fileName, entry.functionName)}\n${entry.fullCode}`)
|
|
303
|
+
.join("\n---\n");
|
|
304
|
+
try {
|
|
305
|
+
const response = await opts.ctx.complete({
|
|
306
|
+
messages: [{ role: "user", content: `${SYSTEM_PROMPT}\n\n${prompts}` }],
|
|
307
|
+
});
|
|
308
|
+
const texts = response.content
|
|
309
|
+
.filter((part) => part.type === "text")
|
|
310
|
+
.map((part) => part.text)
|
|
311
|
+
.join("\n");
|
|
312
|
+
const records = parseSummaryObjects(texts);
|
|
313
|
+
const { aligned, orderFallback } = alignByRef(updates, records);
|
|
314
|
+
if (orderFallback) {
|
|
315
|
+
opts.ctx.notify?.("code-graph summary: no refs echoed; aligned by order (legacy response shape)", "info");
|
|
316
|
+
}
|
|
317
|
+
const applied = applyAligned(opts.store, aligned, effort);
|
|
318
|
+
report.processed += applied.processed;
|
|
319
|
+
report.ok += applied.ok;
|
|
320
|
+
report.failed += applied.failed;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
const message = boundedErrorMessage((error as Error).message || "completion failed");
|
|
323
|
+
markFailed(opts.store, updates, message);
|
|
324
|
+
report.processed += updates.length;
|
|
325
|
+
report.failed += updates.length;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return report;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function markDeclined(store: Store, pending: PendingSummary[]): void {
|
|
332
|
+
const stmt = store.prepare(
|
|
333
|
+
"update_declined",
|
|
334
|
+
`UPDATE functions SET summary_status = 'declined', summary_updated_at = ?,
|
|
335
|
+
summary_description = NULL, summary_inputs = NULL, summary_outputs = NULL
|
|
336
|
+
WHERE file_dir = ? AND file_name = ? AND function_name = ?`,
|
|
337
|
+
);
|
|
338
|
+
store.tx(() => {
|
|
339
|
+
const now = new Date().toISOString();
|
|
340
|
+
for (const entry of pending) {
|
|
341
|
+
stmt.run(now, entry.fileDir, entry.fileName, entry.functionName);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function markFailed(store: Store, pending: PendingSummary[], message: string): void {
|
|
347
|
+
const stmt = store.prepare(
|
|
348
|
+
"update_failed",
|
|
349
|
+
`UPDATE functions SET summary_status = 'failed', summary_error = ?, summary_updated_at = ?
|
|
350
|
+
WHERE file_dir = ? AND file_name = ? AND function_name = ?`,
|
|
351
|
+
);
|
|
352
|
+
const text = boundedErrorMessage(message);
|
|
353
|
+
store.tx(() => {
|
|
354
|
+
const now = new Date().toISOString();
|
|
355
|
+
for (const entry of pending) {
|
|
356
|
+
stmt.run(text, now, entry.fileDir, entry.fileName, entry.functionName);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function applyAligned(
|
|
362
|
+
store: Store,
|
|
363
|
+
aligned: AlignOutcome["aligned"],
|
|
364
|
+
effort: string | undefined,
|
|
365
|
+
): { processed: number; ok: number; failed: number } {
|
|
366
|
+
const stmt = store.prepare(
|
|
367
|
+
"update_summary",
|
|
368
|
+
`UPDATE functions SET
|
|
369
|
+
summary_description = ?,
|
|
370
|
+
summary_inputs = ?,
|
|
371
|
+
summary_outputs = ?,
|
|
372
|
+
summary_status = ?,
|
|
373
|
+
summary_model = ?,
|
|
374
|
+
summary_schema_version = ?,
|
|
375
|
+
summary_effective_effort = ?,
|
|
376
|
+
summary_error = NULL,
|
|
377
|
+
summary_updated_at = ?
|
|
378
|
+
WHERE file_dir = ? AND file_name = ? AND function_name = ?`,
|
|
379
|
+
);
|
|
380
|
+
const failStmt = store.prepare(
|
|
381
|
+
"update_failed_single",
|
|
382
|
+
`UPDATE functions SET summary_status = 'failed', summary_error = ?, summary_updated_at = ?
|
|
383
|
+
WHERE file_dir = ? AND file_name = ? AND function_name = ?`,
|
|
384
|
+
);
|
|
385
|
+
let ok = 0;
|
|
386
|
+
let failed = 0;
|
|
387
|
+
store.tx(() => {
|
|
388
|
+
const now = new Date().toISOString();
|
|
389
|
+
for (const slot of aligned) {
|
|
390
|
+
const validated = slot.record === null ? null : validate(slot.record);
|
|
391
|
+
if (!validated) {
|
|
392
|
+
const ref = buildRef(slot.entry.fileDir, slot.entry.fileName, slot.entry.functionName);
|
|
393
|
+
const reason = slot.record === null
|
|
394
|
+
? `no aligned summary record for ${ref} (missing/unknown ref or unparseable object)`
|
|
395
|
+
: `invalid summary fields for ${ref}`;
|
|
396
|
+
failStmt.run(boundedErrorMessage(reason), now, slot.entry.fileDir, slot.entry.fileName, slot.entry.functionName);
|
|
397
|
+
failed++;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
stmt.run(
|
|
401
|
+
validated.description,
|
|
402
|
+
JSON.stringify(validated.inputs),
|
|
403
|
+
JSON.stringify(validated.outputs),
|
|
404
|
+
"ok",
|
|
405
|
+
"(current-model)",
|
|
406
|
+
1,
|
|
407
|
+
effort ?? null,
|
|
408
|
+
now,
|
|
409
|
+
slot.entry.fileDir,
|
|
410
|
+
slot.entry.fileName,
|
|
411
|
+
slot.entry.functionName,
|
|
412
|
+
);
|
|
413
|
+
cache.set(cacheKey(slot.entry), { summary: validated });
|
|
414
|
+
ok++;
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
return { processed: aligned.length, ok, failed };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function clearSummaryCache(): void {
|
|
421
|
+
cache.clear();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function summaryCacheStats(): { size: number } {
|
|
425
|
+
return { size: cache.size };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export { SUMMARY_SCHEMA };
|
|
429
|
+
|
|
430
|
+
// Minimal smoke: ensures the schema object remains usable as a JSON Schema
|
|
431
|
+
// description for tests and documentation.
|
|
432
|
+
if (process.env.PI_PLANS_GRAPH_DUMP_SCHEMA === "1") {
|
|
433
|
+
const dumpPath = path.join(fs.realpathSync("."), "code-graph-summary.schema.json");
|
|
434
|
+
fs.writeFileSync(dumpPath, JSON.stringify(SUMMARY_SCHEMA, null, 2));
|
|
435
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public type definitions shared across the code-graph module. Kept small and
|
|
3
|
+
* dependency-free so all sibling modules can import them safely.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type Language = "javascript" | "typescript" | "tsx" | "python";
|
|
7
|
+
|
|
8
|
+
export interface SourceLocation {
|
|
9
|
+
startByte: number;
|
|
10
|
+
endByte: number;
|
|
11
|
+
startLine: number;
|
|
12
|
+
startColumn: number;
|
|
13
|
+
endLine: number;
|
|
14
|
+
endColumn: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ParseDiagnostic {
|
|
18
|
+
message: string;
|
|
19
|
+
severity: "error" | "warning" | "missing";
|
|
20
|
+
startByte?: number;
|
|
21
|
+
endByte?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type RenderUnitKind =
|
|
25
|
+
| "raw"
|
|
26
|
+
| "function"
|
|
27
|
+
| "method"
|
|
28
|
+
| "arrow"
|
|
29
|
+
| "lambda"
|
|
30
|
+
| "expression"
|
|
31
|
+
| "decorator"
|
|
32
|
+
| "docstring"
|
|
33
|
+
| "unsupported";
|
|
34
|
+
|
|
35
|
+
export interface RenderUnit {
|
|
36
|
+
kind: RenderUnitKind;
|
|
37
|
+
/** Byte offsets into the source snapshot (UTF-8 bytes). */
|
|
38
|
+
startByte: number;
|
|
39
|
+
endByte: number;
|
|
40
|
+
/** Optional identifier for chunks (function name, expression handle, etc.). */
|
|
41
|
+
label?: string;
|
|
42
|
+
/** Nested children rendered by the same backend. */
|
|
43
|
+
children?: RenderUnit[];
|
|
44
|
+
/** Whether this unit may be safely moved between manifest entries. */
|
|
45
|
+
moveSupported: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FunctionRecord {
|
|
49
|
+
fileDir: string;
|
|
50
|
+
fileName: string;
|
|
51
|
+
functionName: string;
|
|
52
|
+
language: Language;
|
|
53
|
+
kind: "declaration" | "expression" | "arrow" | "method" | "async" | "generator" | "lambda" | "accessor" | "unsupported";
|
|
54
|
+
/** UTF-8 text of the callable body (best-effort, may equal render code). */
|
|
55
|
+
fullCode: string;
|
|
56
|
+
fullCodeHash: string;
|
|
57
|
+
/** Text used by the materializer to reconstruct the file (render unit). */
|
|
58
|
+
renderCode: string;
|
|
59
|
+
renderCodeHash: string;
|
|
60
|
+
/** Optional human-readable parent identifier (class name, container). */
|
|
61
|
+
parent?: string;
|
|
62
|
+
/** Optional container group (class body, module). */
|
|
63
|
+
container?: string;
|
|
64
|
+
/** Whether the function entry may be moved between manifest positions. */
|
|
65
|
+
moveSupported: boolean;
|
|
66
|
+
/** Whether this record is a primary function (true) or merged overload signature (false). */
|
|
67
|
+
isPrimary: boolean;
|
|
68
|
+
/** Optional list of overload signatures merged into this entry (TypeScript). */
|
|
69
|
+
overloadSignatures?: string[];
|
|
70
|
+
/** UTF-8 byte span of the callable (provenance). */
|
|
71
|
+
provenance: SourceLocation;
|
|
72
|
+
summary: SummaryRecord | null;
|
|
73
|
+
version: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SummaryRecord {
|
|
77
|
+
description: string;
|
|
78
|
+
inputs: string[];
|
|
79
|
+
outputs: string[];
|
|
80
|
+
status: "ok" | "pending" | "declined" | "failed";
|
|
81
|
+
model?: string;
|
|
82
|
+
schemaVersion: number;
|
|
83
|
+
effectiveEffort?: string;
|
|
84
|
+
errorMessage?: string;
|
|
85
|
+
updatedAt?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface FileEntry {
|
|
89
|
+
id: number;
|
|
90
|
+
kind: "raw" | "function" | "decorator" | "docstring" | "trailing";
|
|
91
|
+
/** Order within the file manifest. */
|
|
92
|
+
ordinal: number;
|
|
93
|
+
/** Optional reference to a function row (`(file_dir,file_name,function_name)`). */
|
|
94
|
+
functionName?: string;
|
|
95
|
+
/** Byte offsets into the file source snapshot. */
|
|
96
|
+
startByte: number;
|
|
97
|
+
endByte: number;
|
|
98
|
+
text: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface FileRecord {
|
|
102
|
+
fileDir: string;
|
|
103
|
+
fileName: string;
|
|
104
|
+
language: Language;
|
|
105
|
+
sourceHash: string;
|
|
106
|
+
sourceText: string;
|
|
107
|
+
entries: FileEntry[];
|
|
108
|
+
updatedAt: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type EdgeKind = "call" | "definition" | "import";
|
|
112
|
+
export type EdgeResolution = "resolved" | "ambiguous" | "unresolved";
|
|
113
|
+
|
|
114
|
+
export interface CallEdge {
|
|
115
|
+
id: number;
|
|
116
|
+
fromFileDir: string;
|
|
117
|
+
fromFileName: string;
|
|
118
|
+
fromFunction: string;
|
|
119
|
+
toFileDir?: string;
|
|
120
|
+
toFileName?: string;
|
|
121
|
+
toFunction?: string;
|
|
122
|
+
toCalleeText: string;
|
|
123
|
+
kind: EdgeKind;
|
|
124
|
+
resolution: EdgeResolution;
|
|
125
|
+
reason?: string;
|
|
126
|
+
provenance: SourceLocation;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface GraphMeta {
|
|
130
|
+
schemaVersion: number;
|
|
131
|
+
worktreeRoot: string;
|
|
132
|
+
gitCommonDir: string;
|
|
133
|
+
parserVersions: Record<string, string>;
|
|
134
|
+
updatedAt: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface CodeGraphSnapshot {
|
|
138
|
+
id: number;
|
|
139
|
+
headCommit: string;
|
|
140
|
+
uncommittedPaths: string[];
|
|
141
|
+
recordedAt: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface GraphContext {
|
|
145
|
+
worktreeRoot: string;
|
|
146
|
+
gitCommonDir: string;
|
|
147
|
+
dbPath: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface FunctionScreening {
|
|
151
|
+
fileDir: string;
|
|
152
|
+
fileName: string;
|
|
153
|
+
functionName: string;
|
|
154
|
+
language: Language;
|
|
155
|
+
kind: FunctionRecord["kind"];
|
|
156
|
+
description: string | null;
|
|
157
|
+
inputs: string[] | null;
|
|
158
|
+
outputs: string[] | null;
|
|
159
|
+
version: number;
|
|
160
|
+
summaryStatus: SummaryRecord["status"] | null;
|
|
161
|
+
inLinks: Array<{ fileDir: string; fileName: string; functionName: string }>;
|
|
162
|
+
outLinks: Array<{ fileDir: string; fileName: string; functionName: string }>;
|
|
163
|
+
}
|