pi-vcc 0.4.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/demo.gif +0 -0
  4. package/flow/plans/20260515-1300/plan.md +206 -0
  5. package/index.ts +14 -0
  6. package/package.json +36 -0
  7. package/pi-vcc-config.schema.json +131 -0
  8. package/scripts/audit-sessions.ts +88 -0
  9. package/scripts/benchmark-real-sessions.ts +25 -0
  10. package/scripts/compare-before-after.ts +36 -0
  11. package/scripts/dump-branch-output.ts +20 -0
  12. package/src/commands/pi-vcc.ts +33 -0
  13. package/src/commands/vcc-recall.ts +65 -0
  14. package/src/core/brief.ts +381 -0
  15. package/src/core/build-sections.ts +87 -0
  16. package/src/core/content.ts +60 -0
  17. package/src/core/filter-noise.ts +42 -0
  18. package/src/core/format-recall.ts +27 -0
  19. package/src/core/format.ts +56 -0
  20. package/src/core/lineage.ts +26 -0
  21. package/src/core/load-messages.ts +63 -0
  22. package/src/core/normalize.ts +66 -0
  23. package/src/core/recall-scope.ts +14 -0
  24. package/src/core/render-entries.ts +68 -0
  25. package/src/core/report.ts +237 -0
  26. package/src/core/sanitize.ts +5 -0
  27. package/src/core/search-entries.ts +230 -0
  28. package/src/core/settings.ts +215 -0
  29. package/src/core/skill-collapse.ts +35 -0
  30. package/src/core/summarize.ts +159 -0
  31. package/src/core/tool-args.ts +14 -0
  32. package/src/details.ts +7 -0
  33. package/src/extract/commits.ts +69 -0
  34. package/src/extract/files.ts +80 -0
  35. package/src/extract/goals.ts +79 -0
  36. package/src/extract/preferences.ts +55 -0
  37. package/src/extract/references.ts +214 -0
  38. package/src/extract/signals.ts +145 -0
  39. package/src/hooks/before-compact.ts +405 -0
  40. package/src/sections.ts +14 -0
  41. package/src/tools/recall.ts +109 -0
  42. package/src/types.ts +14 -0
  43. package/tests/before-compact-hook.test.ts +181 -0
  44. package/tests/before-compact.test.ts +140 -0
  45. package/tests/brief.test.ts +206 -0
  46. package/tests/build-sections.test.ts +90 -0
  47. package/tests/compile.test.ts +110 -0
  48. package/tests/config-integration.test.ts +107 -0
  49. package/tests/content.test.ts +31 -0
  50. package/tests/edge-cases.test.ts +368 -0
  51. package/tests/extract-goals.test.ts +86 -0
  52. package/tests/extract-preferences.test.ts +30 -0
  53. package/tests/extract-references.test.ts +475 -0
  54. package/tests/extract-signals.test.ts +561 -0
  55. package/tests/filter-noise.test.ts +61 -0
  56. package/tests/fixtures.ts +61 -0
  57. package/tests/format-recall.test.ts +30 -0
  58. package/tests/format.test.ts +91 -0
  59. package/tests/lineage.test.ts +33 -0
  60. package/tests/load-messages.test.ts +51 -0
  61. package/tests/normalize.test.ts +97 -0
  62. package/tests/real-sessions.test.ts +38 -0
  63. package/tests/recall-expand.test.ts +15 -0
  64. package/tests/recall-scope.test.ts +32 -0
  65. package/tests/recall-tool-scope.test.ts +67 -0
  66. package/tests/render-entries.test.ts +62 -0
  67. package/tests/report.test.ts +44 -0
  68. package/tests/sanitize.test.ts +24 -0
  69. package/tests/search-entries.test.ts +144 -0
  70. package/tests/settings-scaffold.test.ts +120 -0
  71. package/tests/settings.test.ts +32 -0
  72. package/tests/support/load-session.ts +23 -0
  73. package/tests/support/real-sessions.ts +51 -0
  74. package/tsconfig.json +14 -0
  75. package/vitest.config.ts +7 -0
@@ -0,0 +1,561 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { extractSignals, formatSignals } from "../src/extract/signals";
3
+ import type { NormalizedBlock } from "../src/types";
4
+
5
+ // ─── extractSignals ────────────────────────────────────────────────
6
+
7
+ describe("extractSignals", () => {
8
+ // ── empty / no-match basics ──────────────────────────────────────
9
+
10
+ it("returns empty for no blocks", () => {
11
+ const s = extractSignals([]);
12
+ expect(s.constraints).toEqual([]);
13
+ expect(s.decisions).toEqual([]);
14
+ expect(s.statuses).toEqual([]);
15
+ });
16
+
17
+ it("returns empty when no user or assistant blocks", () => {
18
+ const blocks: NormalizedBlock[] = [
19
+ { kind: "tool_call", name: "bash", args: { command: "ls" } },
20
+ { kind: "tool_result", name: "bash", text: "file.ts", isError: false },
21
+ { kind: "thinking", text: "hmm", redacted: false },
22
+ ];
23
+ const s = extractSignals(blocks);
24
+ expect(s.constraints).toEqual([]);
25
+ expect(s.decisions).toEqual([]);
26
+ expect(s.statuses).toEqual([]);
27
+ });
28
+
29
+ // ── Constraints ──────────────────────────────────────────────────
30
+
31
+ describe("constraints", () => {
32
+ it("captures 'must not' constraint", () => {
33
+ const blocks: NormalizedBlock[] = [
34
+ { kind: "user", text: "You must not push to main directly" },
35
+ ];
36
+ const s = extractSignals(blocks);
37
+ expect(s.constraints.length).toBe(1);
38
+ expect(s.constraints[0]).toContain("must not push to main directly");
39
+ });
40
+
41
+ it("captures 'don't' constraint", () => {
42
+ const blocks: NormalizedBlock[] = [
43
+ { kind: "user", text: "Don't use any external dependencies" },
44
+ ];
45
+ const s = extractSignals(blocks);
46
+ expect(s.constraints.length).toBe(1);
47
+ expect(s.constraints[0]).toContain("Don't use any external dependencies");
48
+ });
49
+
50
+ it("captures 'do not' constraint", () => {
51
+ const blocks: NormalizedBlock[] = [
52
+ { kind: "user", text: "Do not commit to main branch" },
53
+ ];
54
+ const s = extractSignals(blocks);
55
+ expect(s.constraints.length).toBe(1);
56
+ expect(s.constraints[0]).toContain("Do not commit to main");
57
+ });
58
+
59
+ it("captures 'cannot' constraint", () => {
60
+ const blocks: NormalizedBlock[] = [
61
+ { kind: "user", text: "We cannot modify the public API" },
62
+ ];
63
+ const s = extractSignals(blocks);
64
+ expect(s.constraints.length).toBe(1);
65
+ expect(s.constraints[0]).toContain("cannot modify the public API");
66
+ });
67
+
68
+ it("captures 'forbidden' constraint", () => {
69
+ const blocks: NormalizedBlock[] = [
70
+ { kind: "user", text: "It is forbidden to write to /etc/config" },
71
+ ];
72
+ const s = extractSignals(blocks);
73
+ expect(s.constraints.length).toBe(1);
74
+ expect(s.constraints[0]).toContain("forbidden");
75
+ });
76
+
77
+ it("captures 'disallowed' constraint", () => {
78
+ const blocks: NormalizedBlock[] = [
79
+ { kind: "user", text: "Direct DB access is disallowed in production" },
80
+ ];
81
+ const s = extractSignals(blocks);
82
+ expect(s.constraints.length).toBe(1);
83
+ expect(s.constraints[0]).toContain("disallowed");
84
+ });
85
+
86
+ it("captures 'off-limits' constraint", () => {
87
+ const blocks: NormalizedBlock[] = [
88
+ { kind: "user", text: "The payment module is off-limits for now" },
89
+ ];
90
+ const s = extractSignals(blocks);
91
+ expect(s.constraints.length).toBe(1);
92
+ expect(s.constraints[0]).toContain("off-limits");
93
+ });
94
+
95
+ it("captures 'off limits' constraint (space variant)", () => {
96
+ const blocks: NormalizedBlock[] = [
97
+ { kind: "user", text: "That service is off limits for this sprint" },
98
+ ];
99
+ const s = extractSignals(blocks);
100
+ expect(s.constraints.length).toBe(1);
101
+ expect(s.constraints[0]).toContain("off limits");
102
+ });
103
+
104
+ it("captures 'out of scope' constraint", () => {
105
+ const blocks: NormalizedBlock[] = [
106
+ { kind: "user", text: "The admin panel is out of scope for this release" },
107
+ ];
108
+ const s = extractSignals(blocks);
109
+ expect(s.constraints.length).toBe(1);
110
+ expect(s.constraints[0]).toContain("out of scope");
111
+ });
112
+
113
+ it("captures 'excluded' constraint", () => {
114
+ const blocks: NormalizedBlock[] = [
115
+ { kind: "user", text: "Mobile views are excluded from this release" },
116
+ ];
117
+ const s = extractSignals(blocks);
118
+ expect(s.constraints.length).toBe(1);
119
+ expect(s.constraints[0]).toContain("excluded");
120
+ });
121
+
122
+ it("ignores constraint-like lines in tool_result blocks", () => {
123
+ const blocks: NormalizedBlock[] = [
124
+ { kind: "tool_result", name: "bash", text: "Error: cannot write to /etc", isError: true },
125
+ ];
126
+ const s = extractSignals(blocks);
127
+ expect(s.constraints).toEqual([]);
128
+ });
129
+
130
+ it("ignores constraint-like lines in tool_call blocks", () => {
131
+ const blocks: NormalizedBlock[] = [
132
+ { kind: "tool_call", name: "bash", args: { command: "echo 'do not touch this'" } },
133
+ ];
134
+ const s = extractSignals(blocks);
135
+ expect(s.constraints).toEqual([]);
136
+ });
137
+
138
+ it("ignores constraint-like lines in assistant blocks", () => {
139
+ const blocks: NormalizedBlock[] = [
140
+ { kind: "assistant", text: "I understand that we must not push to main" },
141
+ ];
142
+ const s = extractSignals(blocks);
143
+ // constraints only from user blocks
144
+ expect(s.constraints).toEqual([]);
145
+ });
146
+
147
+ it("rejects constraint lines shorter than 15 chars", () => {
148
+ const blocks: NormalizedBlock[] = [
149
+ { kind: "user", text: "do not do it" },
150
+ ];
151
+ const s = extractSignals(blocks);
152
+ expect(s.constraints).toEqual([]);
153
+ });
154
+
155
+ it("rejects constraint lines that are questions", () => {
156
+ const blocks: NormalizedBlock[] = [
157
+ { kind: "user", text: "Should we do not modify this file?" },
158
+ ];
159
+ const s = extractSignals(blocks);
160
+ expect(s.constraints).toEqual([]);
161
+ });
162
+ });
163
+
164
+ // ── Decisions ────────────────────────────────────────────────────
165
+
166
+ describe("decisions", () => {
167
+ it("captures 'decided' decision", () => {
168
+ const blocks: NormalizedBlock[] = [
169
+ { kind: "user", text: "We decided to use Redis for caching" },
170
+ ];
171
+ const s = extractSignals(blocks);
172
+ expect(s.decisions.length).toBe(1);
173
+ expect(s.decisions[0]).toContain("decided to use Redis");
174
+ });
175
+
176
+ it("captures \"let's use\" decision", () => {
177
+ const blocks: NormalizedBlock[] = [
178
+ { kind: "user", text: "Let's use approach B for the API layer" },
179
+ ];
180
+ const s = extractSignals(blocks);
181
+ expect(s.decisions.length).toBe(1);
182
+ expect(s.decisions[0]).toContain("use approach B");
183
+ });
184
+
185
+ it("captures 'going with' decision", () => {
186
+ const blocks: NormalizedBlock[] = [
187
+ { kind: "user", text: "Going with the microservice pattern for scaling" },
188
+ ];
189
+ const s = extractSignals(blocks);
190
+ expect(s.decisions.length).toBe(1);
191
+ expect(s.decisions[0]).toContain("Going with the microservice");
192
+ });
193
+
194
+ it("captures 'chose' decision", () => {
195
+ const blocks: NormalizedBlock[] = [
196
+ { kind: "user", text: "We chose SQLite for simplicity in this module" },
197
+ ];
198
+ const s = extractSignals(blocks);
199
+ expect(s.decisions.length).toBe(1);
200
+ expect(s.decisions[0]).toContain("chose SQLite");
201
+ });
202
+
203
+ it("captures \"we'll use\" decision", () => {
204
+ const blocks: NormalizedBlock[] = [
205
+ { kind: "user", text: "We'll use bun instead of node for this project" },
206
+ ];
207
+ const s = extractSignals(blocks);
208
+ expect(s.decisions.length).toBe(1);
209
+ expect(s.decisions[0]).toContain("use bun instead");
210
+ });
211
+
212
+ it("ignores decisions in tool_result blocks", () => {
213
+ const blocks: NormalizedBlock[] = [
214
+ { kind: "tool_result", name: "bash", text: "decided to use redis", isError: false },
215
+ ];
216
+ const s = extractSignals(blocks);
217
+ expect(s.decisions).toEqual([]);
218
+ });
219
+
220
+ it("ignores decisions in assistant blocks", () => {
221
+ const blocks: NormalizedBlock[] = [
222
+ { kind: "assistant", text: "I decided to refactor the module" },
223
+ ];
224
+ const s = extractSignals(blocks);
225
+ // decisions only from user blocks
226
+ expect(s.decisions).toEqual([]);
227
+ });
228
+
229
+ it("rejects decision lines shorter than 15 chars", () => {
230
+ const blocks: NormalizedBlock[] = [
231
+ { kind: "user", text: "chose Redis" },
232
+ ];
233
+ const s = extractSignals(blocks);
234
+ expect(s.decisions).toEqual([]);
235
+ });
236
+
237
+ it("rejects decision lines that are questions", () => {
238
+ const blocks: NormalizedBlock[] = [
239
+ { kind: "user", text: "Should we decided to use Redis here?" },
240
+ ];
241
+ const s = extractSignals(blocks);
242
+ expect(s.decisions).toEqual([]);
243
+ });
244
+ });
245
+
246
+ // ── Status markers ───────────────────────────────────────────────
247
+
248
+ describe("statuses", () => {
249
+ it("captures DONE status from user block", () => {
250
+ const blocks: NormalizedBlock[] = [
251
+ { kind: "user", text: "DONE — auth module migrated successfully" },
252
+ ];
253
+ const s = extractSignals(blocks);
254
+ expect(s.statuses.length).toBe(1);
255
+ expect(s.statuses[0]).toContain("DONE");
256
+ });
257
+
258
+ it("captures DONE status from assistant block", () => {
259
+ const blocks: NormalizedBlock[] = [
260
+ { kind: "assistant", text: "DONE — all tests passing now" },
261
+ ];
262
+ const s = extractSignals(blocks);
263
+ expect(s.statuses.length).toBe(1);
264
+ expect(s.statuses[0]).toContain("DONE");
265
+ });
266
+
267
+ it("captures TODO status", () => {
268
+ const blocks: NormalizedBlock[] = [
269
+ { kind: "user", text: "TODO: add integration tests for the auth flow" },
270
+ ];
271
+ const s = extractSignals(blocks);
272
+ expect(s.statuses.length).toBe(1);
273
+ expect(s.statuses[0]).toContain("TODO");
274
+ });
275
+
276
+ it("captures WIP status", () => {
277
+ const blocks: NormalizedBlock[] = [
278
+ { kind: "user", text: "WIP: still debugging the login issue" },
279
+ ];
280
+ const s = extractSignals(blocks);
281
+ expect(s.statuses.length).toBe(1);
282
+ expect(s.statuses[0]).toContain("WIP");
283
+ });
284
+
285
+ it("captures 'blocked' status", () => {
286
+ const blocks: NormalizedBlock[] = [
287
+ { kind: "user", text: "Blocked on upstream fix for the auth library" },
288
+ ];
289
+ const s = extractSignals(blocks);
290
+ expect(s.statuses.length).toBe(1);
291
+ expect(s.statuses[0]).toContain("Blocked");
292
+ });
293
+
294
+ it("captures 'resolved' status", () => {
295
+ const blocks: NormalizedBlock[] = [
296
+ { kind: "user", text: "Resolved: was a typo in the config file" },
297
+ ];
298
+ const s = extractSignals(blocks);
299
+ expect(s.statuses.length).toBe(1);
300
+ expect(s.statuses[0]).toContain("Resolved");
301
+ });
302
+
303
+ it("ignores status-like lines in tool_result blocks", () => {
304
+ const blocks: NormalizedBlock[] = [
305
+ { kind: "tool_result", name: "bash", text: "DONE: all files processed", isError: false },
306
+ ];
307
+ const s = extractSignals(blocks);
308
+ expect(s.statuses).toEqual([]);
309
+ });
310
+
311
+ it("ignores status-like lines in tool_call blocks", () => {
312
+ const blocks: NormalizedBlock[] = [
313
+ { kind: "tool_call", name: "bash", args: { command: "echo TODO: fix" } },
314
+ ];
315
+ const s = extractSignals(blocks);
316
+ expect(s.statuses).toEqual([]);
317
+ });
318
+
319
+ it("rejects status lines shorter than 15 chars", () => {
320
+ const blocks: NormalizedBlock[] = [
321
+ { kind: "user", text: "DONE" },
322
+ ];
323
+ const s = extractSignals(blocks);
324
+ expect(s.statuses).toEqual([]);
325
+ });
326
+
327
+ it("requires status marker to be prominent (start of line or after punctuation)", () => {
328
+ // Status buried mid-sentence should not match
329
+ const blocks: NormalizedBlock[] = [
330
+ { kind: "user", text: "The function returns DONE when the process finishes" },
331
+ ];
332
+ const s = extractSignals(blocks);
333
+ // "DONE" buried mid-sentence — should NOT match
334
+ expect(s.statuses).toEqual([]);
335
+ });
336
+
337
+ it("matches status at start of line", () => {
338
+ const blocks: NormalizedBlock[] = [
339
+ { kind: "user", text: "DONE migrating the database" },
340
+ ];
341
+ const s = extractSignals(blocks);
342
+ expect(s.statuses.length).toBe(1);
343
+ });
344
+
345
+ it("matches status after punctuation", () => {
346
+ const blocks: NormalizedBlock[] = [
347
+ { kind: "user", text: "Step 3 complete. DONE — the migration is finished" },
348
+ ];
349
+ const s = extractSignals(blocks);
350
+ expect(s.statuses.length).toBe(1);
351
+ });
352
+ });
353
+
354
+ // ── Dedup ────────────────────────────────────────────────────────
355
+
356
+ describe("deduplication", () => {
357
+ it("deduplicates identical constraints across blocks (case-insensitive)", () => {
358
+ const blocks: NormalizedBlock[] = [
359
+ { kind: "user", text: "Must not push to main directly" },
360
+ { kind: "assistant", text: "ok" },
361
+ { kind: "user", text: "must not push to main directly" },
362
+ ];
363
+ const s = extractSignals(blocks);
364
+ expect(s.constraints.length).toBe(1);
365
+ });
366
+
367
+ it("deduplicates identical decisions across blocks (case-insensitive)", () => {
368
+ const blocks: NormalizedBlock[] = [
369
+ { kind: "user", text: "Decided to use Redis for caching" },
370
+ { kind: "assistant", text: "ok" },
371
+ { kind: "user", text: "decided to use redis for caching" },
372
+ ];
373
+ const s = extractSignals(blocks);
374
+ expect(s.decisions.length).toBe(1);
375
+ });
376
+
377
+ it("deduplicates identical statuses across blocks (case-insensitive)", () => {
378
+ const blocks: NormalizedBlock[] = [
379
+ { kind: "user", text: "DONE — auth migrated" },
380
+ { kind: "assistant", text: "done — auth migrated" },
381
+ ];
382
+ const s = extractSignals(blocks);
383
+ expect(s.statuses.length).toBe(1);
384
+ });
385
+ });
386
+
387
+ // ── Caps ─────────────────────────────────────────────────────────
388
+
389
+ describe("caps", () => {
390
+ it("caps constraints at 5", () => {
391
+ const blocks: NormalizedBlock[] = Array.from({ length: 8 }, (_, i) => ({
392
+ kind: "user" as const,
393
+ text: `You must not do thing ${i} in the codebase at all`,
394
+ }));
395
+ const s = extractSignals(blocks);
396
+ expect(s.constraints.length).toBe(5);
397
+ });
398
+
399
+ it("caps decisions at 5", () => {
400
+ const blocks: NormalizedBlock[] = Array.from({ length: 8 }, (_, i) => ({
401
+ kind: "user" as const,
402
+ text: `We decided to use library ${i} for the backend implementation`,
403
+ }));
404
+ const s = extractSignals(blocks);
405
+ expect(s.decisions.length).toBe(5);
406
+ });
407
+
408
+ it("caps statuses at 5", () => {
409
+ const blocks: NormalizedBlock[] = Array.from({ length: 8 }, (_, i) => ({
410
+ kind: "user" as const,
411
+ text: `DONE — task ${i} completed and verified`,
412
+ }));
413
+ const s = extractSignals(blocks);
414
+ expect(s.statuses.length).toBe(5);
415
+ });
416
+ });
417
+
418
+ // ── Clip long lines ──────────────────────────────────────────────
419
+
420
+ describe("long lines", () => {
421
+ it("clips constraints longer than 200 chars", () => {
422
+ const longText = "Must not " + "a".repeat(250);
423
+ const blocks: NormalizedBlock[] = [
424
+ { kind: "user", text: longText },
425
+ ];
426
+ const s = extractSignals(blocks);
427
+ expect(s.constraints.length).toBe(1);
428
+ expect(s.constraints[0].length).toBeLessThanOrEqual(200);
429
+ });
430
+
431
+ it("clips decisions longer than 200 chars", () => {
432
+ const longText = "Decided " + "b".repeat(250);
433
+ const blocks: NormalizedBlock[] = [
434
+ { kind: "user", text: longText },
435
+ ];
436
+ const s = extractSignals(blocks);
437
+ expect(s.decisions.length).toBe(1);
438
+ expect(s.decisions[0].length).toBeLessThanOrEqual(200);
439
+ });
440
+
441
+ it("clips statuses longer than 200 chars", () => {
442
+ const longText = "DONE " + "c".repeat(250);
443
+ const blocks: NormalizedBlock[] = [
444
+ { kind: "user", text: longText },
445
+ ];
446
+ const s = extractSignals(blocks);
447
+ expect(s.statuses.length).toBe(1);
448
+ expect(s.statuses[0].length).toBeLessThanOrEqual(200);
449
+ });
450
+ });
451
+
452
+ // ── Multi-line blocks ────────────────────────────────────────────
453
+
454
+ describe("multi-line blocks", () => {
455
+ it("extracts multiple constraints from one block", () => {
456
+ const blocks: NormalizedBlock[] = [
457
+ {
458
+ kind: "user",
459
+ text: "You must not push to main directly\nAlso do not modify the release scripts",
460
+ },
461
+ ];
462
+ const s = extractSignals(blocks);
463
+ expect(s.constraints.length).toBe(2);
464
+ });
465
+
466
+ it("extracts constraint and decision from same block", () => {
467
+ const blocks: NormalizedBlock[] = [
468
+ {
469
+ kind: "user",
470
+ text: "We decided to use bun for runtime\nMust not use node under any circumstances",
471
+ },
472
+ ];
473
+ const s = extractSignals(blocks);
474
+ expect(s.decisions.length).toBe(1);
475
+ expect(s.constraints.length).toBe(1);
476
+ });
477
+ });
478
+
479
+ // ── Negative cases ───────────────────────────────────────────────
480
+
481
+ describe("negatives", () => {
482
+ it("does not extract signals from thinking blocks", () => {
483
+ const blocks: NormalizedBlock[] = [
484
+ { kind: "thinking", text: "I must not forget to check the types", redacted: false },
485
+ ];
486
+ const s = extractSignals(blocks);
487
+ expect(s.constraints).toEqual([]);
488
+ });
489
+
490
+ it("does not match constraint in question form", () => {
491
+ const blocks: NormalizedBlock[] = [
492
+ { kind: "user", text: "What if we must not push to main?" },
493
+ ];
494
+ const s = extractSignals(blocks);
495
+ expect(s.constraints).toEqual([]);
496
+ });
497
+
498
+ it("does not match short hypothetical", () => {
499
+ const blocks: NormalizedBlock[] = [
500
+ { kind: "user", text: "do not?" },
501
+ ];
502
+ const s = extractSignals(blocks);
503
+ expect(s.constraints).toEqual([]);
504
+ });
505
+ });
506
+ });
507
+
508
+ // ─── formatSignals ─────────────────────────────────────────────────
509
+
510
+ describe("formatSignals", () => {
511
+ it("returns empty array for empty signals", () => {
512
+ expect(formatSignals({ constraints: [], decisions: [], statuses: [] })).toEqual([]);
513
+ });
514
+
515
+ it("formats constraints with prefix", () => {
516
+ const result = formatSignals({
517
+ constraints: ["must not push to main"],
518
+ decisions: [],
519
+ statuses: [],
520
+ });
521
+ expect(result).toEqual(["Constraint: must not push to main"]);
522
+ });
523
+
524
+ it("formats decisions with prefix", () => {
525
+ const result = formatSignals({
526
+ constraints: [],
527
+ decisions: ["decided to use Redis"],
528
+ statuses: [],
529
+ });
530
+ expect(result).toEqual(["Decision: decided to use Redis"]);
531
+ });
532
+
533
+ it("formats statuses with prefix", () => {
534
+ const result = formatSignals({
535
+ constraints: [],
536
+ decisions: [],
537
+ statuses: ["DONE — auth migrated"],
538
+ });
539
+ expect(result).toEqual(["Status: DONE — auth migrated"]);
540
+ });
541
+
542
+ it("orders: constraints first, then decisions, then statuses", () => {
543
+ const result = formatSignals({
544
+ constraints: ["must not push"],
545
+ decisions: ["decided to use Redis"],
546
+ statuses: ["DONE — auth migrated"],
547
+ });
548
+ expect(result[0]).toContain("Constraint:");
549
+ expect(result[1]).toContain("Decision:");
550
+ expect(result[2]).toContain("Status:");
551
+ });
552
+
553
+ it("returns empty when all arrays empty", () => {
554
+ const result = formatSignals({
555
+ constraints: [],
556
+ decisions: [],
557
+ statuses: [],
558
+ });
559
+ expect(result).toEqual([]);
560
+ });
561
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { filterNoise } from "../src/core/filter-noise";
3
+ import type { NormalizedBlock } from "../src/types";
4
+
5
+ describe("filterNoise", () => {
6
+ it("removes thinking blocks", () => {
7
+ const blocks: NormalizedBlock[] = [
8
+ { kind: "thinking", text: "hmm", redacted: false },
9
+ { kind: "assistant", text: "hello" },
10
+ ];
11
+ expect(filterNoise(blocks)).toEqual([{ kind: "assistant", text: "hello" }]);
12
+ });
13
+
14
+ it("removes noise tool calls and results", () => {
15
+ const blocks: NormalizedBlock[] = [
16
+ { kind: "tool_call", name: "TodoWrite", args: {} },
17
+ { kind: "tool_result", name: "TodoWrite", text: "ok", isError: false },
18
+ { kind: "tool_call", name: "Read", args: { path: "x.ts" } },
19
+ ];
20
+ const result = filterNoise(blocks);
21
+ expect(result).toHaveLength(1);
22
+ expect(result[0]).toEqual({ kind: "tool_call", name: "Read", args: { path: "x.ts" } });
23
+ });
24
+
25
+ it("removes user blocks that are pure XML wrappers", () => {
26
+ const blocks: NormalizedBlock[] = [
27
+ { kind: "user", text: "<system-reminder>some noise</system-reminder>" },
28
+ { kind: "user", text: "Fix the bug" },
29
+ ];
30
+ const result = filterNoise(blocks);
31
+ expect(result).toHaveLength(1);
32
+ expect(result[0]).toEqual({ kind: "user", text: "Fix the bug" });
33
+ });
34
+
35
+ it("cleans XML wrappers from user text but keeps real content", () => {
36
+ const blocks: NormalizedBlock[] = [
37
+ { kind: "user", text: "<system-reminder>noise</system-reminder>\nFix the login" },
38
+ ];
39
+ const result = filterNoise(blocks);
40
+ expect(result).toHaveLength(1);
41
+ expect((result[0] as any).text).toBe("Fix the login");
42
+ });
43
+
44
+ it("removes known noise strings", () => {
45
+ const blocks: NormalizedBlock[] = [
46
+ { kind: "user", text: "Continue from where you left off." },
47
+ { kind: "user", text: "real task" },
48
+ ];
49
+ const result = filterNoise(blocks);
50
+ expect(result).toHaveLength(1);
51
+ expect((result[0] as any).text).toBe("real task");
52
+ });
53
+
54
+ it("preserves non-noise tool calls", () => {
55
+ const blocks: NormalizedBlock[] = [
56
+ { kind: "tool_call", name: "Edit", args: { path: "a.ts" } },
57
+ { kind: "tool_result", name: "Edit", text: "ok", isError: false },
58
+ ];
59
+ expect(filterNoise(blocks)).toHaveLength(2);
60
+ });
61
+ });
@@ -0,0 +1,61 @@
1
+ import type { Message } from "@mariozechner/pi-ai";
2
+
3
+ const ts = Date.now();
4
+ const assistBase = {
5
+ api: "messages" as any,
6
+ provider: "anthropic" as any,
7
+ model: "test",
8
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
9
+ timestamp: ts,
10
+ };
11
+
12
+ export const userMsg = (text: string): Message => ({
13
+ role: "user",
14
+ content: text,
15
+ timestamp: ts,
16
+ });
17
+
18
+ export const assistantText = (text: string): Message => ({
19
+ role: "assistant",
20
+ content: [{ type: "text", text }],
21
+ ...assistBase,
22
+ stopReason: "stop",
23
+ });
24
+
25
+ export const assistantWithThinking = (
26
+ text: string,
27
+ thinking: string,
28
+ ): Message => ({
29
+ role: "assistant",
30
+ content: [
31
+ { type: "thinking", thinking },
32
+ { type: "text", text },
33
+ ],
34
+ ...assistBase,
35
+ stopReason: "stop",
36
+ });
37
+
38
+ export const assistantWithToolCall = (
39
+ name: string,
40
+ args: Record<string, unknown>,
41
+ ): Message => ({
42
+ role: "assistant",
43
+ content: [{ type: "toolCall", id: "tc_1", name, arguments: args }],
44
+ ...assistBase,
45
+ stopReason: "toolUse",
46
+ });
47
+
48
+ export const toolResult = (
49
+ name: string,
50
+ text: string,
51
+ isError = false,
52
+ ): Message => ({
53
+ role: "toolResult",
54
+ toolCallId: "tc_1",
55
+ toolName: name,
56
+ content: [{ type: "text", text }],
57
+ isError,
58
+ timestamp: ts,
59
+ });
60
+
61
+