@monotykamary/pi-vcc 0.3.16

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 (76) hide show
  1. package/README.md +688 -0
  2. package/__tests__/helpers.ts +135 -0
  3. package/__tests__/root-causes.test.ts +858 -0
  4. package/demo.gif +0 -0
  5. package/index.ts +21 -0
  6. package/knip.json +4 -0
  7. package/npm-shrinkwrap.json +3001 -0
  8. package/package.json +36 -0
  9. package/scripts/audit-sessions.ts +88 -0
  10. package/scripts/benchmark-real-sessions.ts +25 -0
  11. package/scripts/compare-before-after.ts +36 -0
  12. package/scripts/dump-branch-output.ts +20 -0
  13. package/src/commands/pi-vcc.ts +36 -0
  14. package/src/commands/vcc-recall.ts +65 -0
  15. package/src/core/brief.ts +497 -0
  16. package/src/core/build-sections.ts +431 -0
  17. package/src/core/content.ts +71 -0
  18. package/src/core/filter-noise.ts +42 -0
  19. package/src/core/format-recall.ts +177 -0
  20. package/src/core/format.ts +98 -0
  21. package/src/core/invisible-continue.ts +172 -0
  22. package/src/core/lineage.ts +26 -0
  23. package/src/core/load-messages.ts +40 -0
  24. package/src/core/normalize.ts +73 -0
  25. package/src/core/recall-scope.ts +49 -0
  26. package/src/core/render-entries.ts +58 -0
  27. package/src/core/report.ts +237 -0
  28. package/src/core/sanitize.ts +5 -0
  29. package/src/core/search-entries.ts +375 -0
  30. package/src/core/settings.ts +190 -0
  31. package/src/core/skill-collapse.ts +35 -0
  32. package/src/core/summarize.ts +179 -0
  33. package/src/core/tool-args.ts +14 -0
  34. package/src/details.ts +19 -0
  35. package/src/extract/commits.ts +69 -0
  36. package/src/extract/goals.ts +79 -0
  37. package/src/extract/preferences.ts +55 -0
  38. package/src/extract/shared-symbols.ts +357 -0
  39. package/src/hooks/before-compact.ts +564 -0
  40. package/src/hooks/proactive-threshold.ts +131 -0
  41. package/src/sections.ts +23 -0
  42. package/src/tools/recall.ts +204 -0
  43. package/src/types.ts +20 -0
  44. package/tests/australian-government.test.ts +479 -0
  45. package/tests/before-compact-hook.test.ts +185 -0
  46. package/tests/before-compact.test.ts +227 -0
  47. package/tests/brief.test.ts +215 -0
  48. package/tests/build-sections.test.ts +59 -0
  49. package/tests/compile.test.ts +89 -0
  50. package/tests/content.test.ts +66 -0
  51. package/tests/deep-extraction.test.ts +313 -0
  52. package/tests/extract-goals.test.ts +86 -0
  53. package/tests/extract-preferences.test.ts +30 -0
  54. package/tests/filter-noise.test.ts +61 -0
  55. package/tests/fixtures.ts +61 -0
  56. package/tests/format-recall.test.ts +87 -0
  57. package/tests/format.test.ts +73 -0
  58. package/tests/government-domain.test.ts +542 -0
  59. package/tests/lineage.test.ts +33 -0
  60. package/tests/load-messages.test.ts +49 -0
  61. package/tests/model-threshold-hook.test.ts +377 -0
  62. package/tests/model-threshold.test.ts +223 -0
  63. package/tests/normalize.test.ts +104 -0
  64. package/tests/proactive-threshold.test.ts +490 -0
  65. package/tests/real-sessions.test.ts +38 -0
  66. package/tests/recall-expand.test.ts +15 -0
  67. package/tests/recall-scope.test.ts +32 -0
  68. package/tests/recall-tool-scope.test.ts +67 -0
  69. package/tests/render-entries.test.ts +92 -0
  70. package/tests/report.test.ts +58 -0
  71. package/tests/sanitize.test.ts +24 -0
  72. package/tests/search-entries.test.ts +233 -0
  73. package/tests/support/load-session.ts +23 -0
  74. package/tests/support/real-sessions.ts +51 -0
  75. package/tests/threshold-integration.test.ts +359 -0
  76. package/tsconfig.json +20 -0
@@ -0,0 +1,357 @@
1
+ import type { NormalizedBlock, ToolResultIndex } from "../types";
2
+ import { extractPath } from "../core/tool-args";
3
+
4
+ const FILE_WRITE_TOOLS = new Set([
5
+ "Edit", "Write", "edit", "write", "edit_file", "write_file",
6
+ "MultiEdit",
7
+ ]);
8
+
9
+ const FILE_READ_TOOLS = new Set([
10
+ "Read", "read", "read_file", "View",
11
+ ]);
12
+
13
+ const FILE_CREATE_TOOLS = new Set([
14
+ "Write", "write", "write_file",
15
+ ]);
16
+
17
+ // Declaration regexes — consolidated from files.ts, symbol-changes.ts, type-catalog.ts
18
+ // Order: more specific patterns first, generic fallbacks last.
19
+
20
+ const TS_EXPORT_DECL_RE =
21
+ /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|class|type|interface|const|let|enum)\s+(\w+)/;
22
+ const TS_TYPE_DECL_RE =
23
+ /^\s*(?:export\s+)?(?:type|interface)\s+(\w+)/;
24
+ const TS_EXPORT_SIG_RE =
25
+ /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|class|type|interface|const|let|enum)\s+\w+[^;{]*[;{]?/;
26
+
27
+ const RUST_DECL_RE =
28
+ /^\s*(?:pub(?:\s*\([^)]*\))?\s+)?(?:fn|struct|enum|trait|type|const|union|var)\s+(\w+)/;
29
+ const RUST_IMPL_RE =
30
+ /^\s*(?:pub(?:\s*\([^)]*\))?\s+)?impl\s+(?:<[^>]+>\s+)?(\w+)(?:\s+for\s+(\w+))?/;
31
+ const RUST_SIG_RE =
32
+ /^\s*pub\s+(?:async\s+)?(?:fn|struct|enum|trait|type)\s+\w+/;
33
+
34
+ const ELIXIR_DEF_RE =
35
+ /^\s*def(?:p|macro|macrop|guard|guardp)?\s+(\w+)/;
36
+ const ELIXIR_MODULE_RE =
37
+ /^\s*defmodule\s+(\w+)/;
38
+ const ELIXIR_SPECIAL_RE =
39
+ /^\s*def(?:struct|protocol|impl)\s+(\w+)/;
40
+
41
+ const JAVA_TYPE_RE =
42
+ /^\s*(?:(?:public|private|protected)\s+)?(?:abstract\s+|static\s+|final\s+|sealed\s+)?(?:class|interface|enum|@interface|record)\s+(\w+)/;
43
+ const JAVA_METHOD_RE =
44
+ /^\s*(?:public|protected)\s+(?:static\s+|abstract\s+|final\s+)?(?:\S+(?:\s*\[\])?\s+)(\w+)\s*\(/;
45
+
46
+ const C_TYPE_RE =
47
+ /^\s*(?:typedef\s+)?(?:struct|class|enum|union)\s+(\w+)/;
48
+ const C_FUNC_RE =
49
+ /^\s*(?!func\b)(?:(?:static|extern|inline|virtual)\s+)?[\w][\w:*&\s]*?(\b\w+)\s*\(/;
50
+
51
+ const RUBY_DEF_RE =
52
+ /^\s*def\s+(?:self\.)?(\w+)/;
53
+ const RUBY_TYPE_RE =
54
+ /^\s*(?:class|module)\s+(\w+)/;
55
+
56
+ const PY_DECL_RE =
57
+ /^\s*(?:async\s+)?def\s+(\w+)|^\s*class\s+(\w+)/;
58
+ const PY_SIG_RE =
59
+ /^\s*(?:async\s+)?(?:def|class)\s+\w+\s*(?:\([^)]*\))?/;
60
+
61
+ const GO_DECL_RE =
62
+ /^\s*func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)/;
63
+ const GO_SIG_RE =
64
+ /^\s*func\s+(?:\(\w+\s+\*?\w+\)\s+)?\w+\s*(?:\([^)]*\))?\s*(?:\([^)]*\))?/;
65
+
66
+ interface SymbolInfo {
67
+ /** Simple declaration name (used by files.ts, symbol-changes.ts) */
68
+ name: string;
69
+ /** Declaration kind */
70
+ kind: "function" | "type" | "class" | "variable" | "unknown";
71
+ /** Full signature line (used by type-catalog.ts) */
72
+ signature?: string;
73
+ }
74
+
75
+ interface ToolCallSymbols {
76
+ /** Symbols found in the tool_result text */
77
+ resultSymbols: SymbolInfo[];
78
+ /** Symbols found in Edit/Write args (newText/content) */
79
+ argSymbols: SymbolInfo[];
80
+ }
81
+
82
+ // Fast screening regex: rejects lines that can't start any declaration.
83
+ // Avoids running the full 15-regex cascade on body code / comments / blank lines.
84
+ const DECL_SCREEN_RE =
85
+ /^\s*(?:export|pub|func|def|class|type|interface|async|abstract|static|public|private|protected|struct|enum|trait|impl|module|const|fn|sealed|record|typedef|union|virtual|extern|inline)/;
86
+
87
+ const parseDeclName = (line: string): { name: string; kind: SymbolInfo["kind"] } | null => {
88
+ // Quick reject: lines that can't start any declaration keyword
89
+ if (!DECL_SCREEN_RE.test(line)) return null;
90
+
91
+ let m = line.match(TS_EXPORT_DECL_RE);
92
+ if (m) {
93
+ const kind = line.includes("function") ? "function"
94
+ : line.includes("class") ? "class"
95
+ : line.includes("type") ? "type"
96
+ : line.includes("interface") ? "type"
97
+ : line.includes("enum") ? "variable"
98
+ : "variable";
99
+ return { name: m[1], kind };
100
+ }
101
+ m = line.match(TS_TYPE_DECL_RE);
102
+ if (m) return { name: m[1], kind: "type" };
103
+ m = line.match(RUST_DECL_RE);
104
+ if (m) return { name: m[1], kind: "function" };
105
+ m = line.match(RUST_IMPL_RE);
106
+ if (m) return { name: m[1], kind: "class" };
107
+ m = line.match(ELIXIR_MODULE_RE);
108
+ if (m) return { name: m[1], kind: "class" };
109
+ m = line.match(ELIXIR_SPECIAL_RE);
110
+ if (m) return { name: m[1], kind: "class" };
111
+ m = line.match(ELIXIR_DEF_RE);
112
+ if (m) return { name: m[1], kind: "function" };
113
+ m = line.match(JAVA_TYPE_RE);
114
+ if (m) return { name: m[1], kind: "class" };
115
+ m = line.match(JAVA_METHOD_RE);
116
+ if (m) return { name: m[1], kind: "function" };
117
+ m = line.match(C_TYPE_RE);
118
+ if (m) return { name: m[1], kind: "class" };
119
+ m = line.match(C_FUNC_RE);
120
+ if (m) return { name: m[1], kind: "function" };
121
+ m = line.match(RUBY_TYPE_RE);
122
+ if (m) return { name: m[1], kind: "class" };
123
+ m = line.match(RUBY_DEF_RE);
124
+ if (m) return { name: m[1], kind: "function" };
125
+ m = line.match(PY_DECL_RE);
126
+ if (m) return { name: m[1] || m[2], kind: m[2] ? "class" : "function" };
127
+ m = line.match(GO_DECL_RE);
128
+ if (m && m[1][0] === m[1][0].toUpperCase()) return { name: m[1], kind: "function" };
129
+ return null;
130
+ };
131
+
132
+ const parseSignature = (line: string): string | null => {
133
+ if (TS_EXPORT_SIG_RE.test(line)) return line.trim();
134
+ if (PY_SIG_RE.test(line) && !line.trim().startsWith("def _") && !line.trim().startsWith("class _")) return line.trim();
135
+ if (GO_SIG_RE.test(line)) {
136
+ const nameMatch = line.match(/func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)/);
137
+ if (nameMatch && nameMatch[1] && nameMatch[1][0] === nameMatch[1][0].toUpperCase()) return line.trim();
138
+ }
139
+ if (RUST_SIG_RE.test(line)) return line.trim();
140
+ return null;
141
+ };
142
+
143
+ /**
144
+ * Line-by-line iteration over text using indexOf("\n") instead of split().
145
+ * Avoids allocating an intermediate string array — for a 300-line, 12KB
146
+ * tool result this saves ~30μs/scan vs split(). Over 600 tool results
147
+ * in a large session, that's ~18ms reclaimed.
148
+ */
149
+ const eachLine = function* (text: string, maxLines: number): Generator<string> {
150
+ let pos = 0;
151
+ let count = 0;
152
+ const len = text.length;
153
+ while (pos < len && count < maxLines) {
154
+ const nl = text.indexOf("\n", pos);
155
+ if (nl === -1) {
156
+ yield text.slice(pos);
157
+ return;
158
+ }
159
+ yield text.slice(pos, nl);
160
+ pos = nl + 1;
161
+ count++;
162
+ }
163
+ };
164
+
165
+ const extractSymbolsFromText = (text: string, maxLines: number, includeSigs: boolean): SymbolInfo[] => {
166
+ const names: SymbolInfo[] = [];
167
+ const seen = new Set<string>();
168
+ for (const line of eachLine(text, maxLines)) {
169
+ const decl = parseDeclName(line);
170
+ if (decl && !seen.has(decl.name)) {
171
+ seen.add(decl.name);
172
+ const sig = includeSigs ? parseSignature(line) : undefined;
173
+ names.push({ name: decl.name, kind: decl.kind, signature: sig ?? undefined });
174
+ }
175
+ }
176
+ return names;
177
+ };
178
+
179
+ interface FileActivity {
180
+ read: Set<string>;
181
+ modified: Set<string>;
182
+ created: Set<string>;
183
+ symbols: Map<string, string[]>;
184
+ }
185
+
186
+ interface ExportSig {
187
+ file: string;
188
+ signatures: string[];
189
+ modified: boolean;
190
+ }
191
+
192
+ export interface SymbolRef {
193
+ name: string;
194
+ file: string;
195
+ kind: "function" | "type" | "class" | "variable" | "unknown";
196
+ access: "modified" | "read";
197
+ }
198
+
199
+ export interface UnifiedExtractResult {
200
+ fileActivity: FileActivity;
201
+ typeCatalog: ExportSig[];
202
+ symbolChanges: SymbolRef[];
203
+ }
204
+
205
+ /**
206
+ * Single-pass extraction of all file/symbol information from tool calls.
207
+ *
208
+ * Replaces the triple-redundant scan performed by:
209
+ * - extractFiles() — path collection + 200-line symbol scan
210
+ * - extractSymbolChanges() — 300-line symbol scan
211
+ * - extractTypeCatalog() — 150-line signature scan
212
+ *
213
+ * All three scanned the same tool_result content with overlapping regex
214
+ * patterns. This unified pass does it once and returns all three datasets.
215
+ */
216
+ export const extractFileAndSymbolData = (
217
+ blocks: NormalizedBlock[],
218
+ tri?: ToolResultIndex,
219
+ ): UnifiedExtractResult => {
220
+ const read = new Set<string>();
221
+ const modified = new Set<string>();
222
+ const created = new Set<string>();
223
+ // Parallel dedup set for symbols Map — avoids O(n) Array.includes()
224
+ // on symbol arrays that can grow to 200+ entries per file.
225
+ const symbols = new Map<string, string[]>();
226
+ const symbolsSeen = new Map<string, Set<string>>();
227
+ const symbolRefs: SymbolRef[] = [];
228
+ const refSeen = new Set<string>();
229
+
230
+ // Type catalog state
231
+ const fileSigs = new Map<string, { sigs: string[]; modified: boolean }>();
232
+ const fileOrder: string[] = [];
233
+
234
+ for (let i = 0; i < blocks.length; i++) {
235
+ const b = blocks[i];
236
+ if (b.kind !== "tool_call") continue;
237
+ const p = extractPath(b.args);
238
+ if (!p) continue;
239
+
240
+ const isRead = FILE_READ_TOOLS.has(b.name);
241
+ const isWrite = FILE_WRITE_TOOLS.has(b.name);
242
+ const isCreate = FILE_CREATE_TOOLS.has(b.name);
243
+
244
+ if (isRead) read.add(p);
245
+ if (isWrite) modified.add(p);
246
+ if (isCreate) created.add(p);
247
+
248
+ // Extract symbols from Edit/Write args
249
+ if (isWrite) {
250
+ const newText = (b.args.newText ?? b.args.new_text ?? b.args.content ?? "") as string;
251
+ if (newText && typeof newText === "string") {
252
+ const syms = extractSymbolsFromText(newText, 100, true);
253
+
254
+ // File activity symbols
255
+ let seen = symbolsSeen.get(p);
256
+ if (!seen) { seen = new Set(); symbolsSeen.set(p, seen); }
257
+ if (!symbols.has(p)) symbols.set(p, []);
258
+ const existing = symbols.get(p)!;
259
+ for (const s of syms) {
260
+ if (!seen.has(s.name)) { seen.add(s.name); existing.push(s.name); }
261
+ }
262
+
263
+ // Type catalog
264
+ if (!fileSigs.has(p)) {
265
+ fileSigs.set(p, { sigs: [], modified: true });
266
+ fileOrder.push(p);
267
+ } else {
268
+ fileSigs.get(p)!.modified = true;
269
+ }
270
+ const sigs = fileSigs.get(p)!.sigs;
271
+ for (const s of syms) {
272
+ if (s.signature && !sigs.includes(s.signature)) sigs.push(s.signature);
273
+ }
274
+
275
+ // Symbol changes
276
+ const access = "modified" as const;
277
+ for (const s of syms) {
278
+ const key = `${s.name}@${p}`;
279
+ if (!refSeen.has(key)) {
280
+ refSeen.add(key);
281
+ symbolRefs.push({ name: s.name, file: p, kind: s.kind, access });
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ // Extract from tool_result (shared look-ahead via index)
288
+ if (isRead || isWrite) {
289
+ const r = tri ? tri.get(i) : (() => {
290
+ for (let j = i + 1; j < Math.min(blocks.length, i + 4); j++) {
291
+ const b2 = blocks[j];
292
+ if (b2.kind === "tool_result") return b2 as Extract<NormalizedBlock, { kind: "tool_result" }>;
293
+ }
294
+ return null;
295
+ })();
296
+
297
+ if (r && r.text && !r.isError) {
298
+ const resultText = r.text;
299
+
300
+ // Parse symbols once — 200 lines covers all three extractors' needs
301
+ const syms = extractSymbolsFromText(resultText, 200, true);
302
+
303
+ // File activity symbols
304
+ let seen = symbolsSeen.get(p);
305
+ if (!seen) { seen = new Set(); symbolsSeen.set(p, seen); }
306
+ if (!symbols.has(p)) symbols.set(p, []);
307
+ const existing = symbols.get(p)!;
308
+ for (const s of syms) {
309
+ if (!seen.has(s.name)) { seen.add(s.name); existing.push(s.name); }
310
+ }
311
+
312
+ // Type catalog (Read results)
313
+ if (isRead) {
314
+ if (!fileSigs.has(p)) {
315
+ fileSigs.set(p, { sigs: [], modified: false });
316
+ fileOrder.push(p);
317
+ }
318
+ const sigs = fileSigs.get(p)!.sigs;
319
+ for (const s of syms) {
320
+ if (s.signature && !sigs.includes(s.signature)) sigs.push(s.signature);
321
+ }
322
+ }
323
+
324
+ // Symbol changes
325
+ const access = isWrite ? "modified" as const : "read" as const;
326
+ for (const s of syms) {
327
+ const key = `${s.name}@${p}`;
328
+ if (!refSeen.has(key)) {
329
+ refSeen.add(key);
330
+ symbolRefs.push({ name: s.name, file: p, kind: s.kind, access });
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ // Dedup: if already Modified, drop from Created
338
+ for (const p of modified) created.delete(p);
339
+
340
+ // Build type catalog — modified files first, then read files
341
+ const modifiedSigs: ExportSig[] = [];
342
+ const readSigs: ExportSig[] = [];
343
+ for (const file of fileOrder) {
344
+ const entry = fileSigs.get(file)!;
345
+ if (entry.sigs.length === 0) continue;
346
+ const esig: ExportSig = { file, signatures: entry.sigs.slice(0, 8), modified: entry.modified };
347
+ if (esig.modified) modifiedSigs.push(esig);
348
+ else readSigs.push(esig);
349
+ }
350
+ const typeCatalog = [...modifiedSigs, ...readSigs].slice(0, 12);
351
+
352
+ return {
353
+ fileActivity: { read, modified, created, symbols },
354
+ typeCatalog,
355
+ symbolChanges: symbolRefs,
356
+ };
357
+ };