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,110 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { compile } from "../src/core/summarize";
3
+ import {
4
+ userMsg,
5
+ assistantText,
6
+ assistantWithToolCall,
7
+ toolResult,
8
+ } from "./fixtures";
9
+
10
+ describe("compile", () => {
11
+ it("returns empty string for no messages", async () => {
12
+ expect(await compile({ messages: [] })).toBe("");
13
+ });
14
+
15
+ it("produces hybrid output with header + brief transcript", async () => {
16
+ const r = await compile({
17
+ messages: [
18
+ userMsg("Fix login bug"),
19
+ assistantWithToolCall("Read", { path: "auth.ts" }),
20
+ assistantText("Found the issue.\n1. Fix validation"),
21
+ ],
22
+ });
23
+ expect(r).toContain("[Session Goal]");
24
+ expect(r).toContain("Fix login bug");
25
+ expect(r).toContain("---");
26
+ expect(r).toContain("[user]\nFix login bug");
27
+ expect(r).toContain('* Read "auth.ts"');
28
+ expect(r).toContain("Found the issue.");
29
+ });
30
+
31
+ it("merges previous summary goals", async () => {
32
+ const r = await compile({
33
+ messages: [userMsg("New task")],
34
+ previousSummary: "[Session Goal]\n- Original goal\n\n---\n\n[user]\nOriginal goal",
35
+ });
36
+ expect(r).toContain("- Original goal");
37
+ expect(r).toContain("- New task");
38
+ });
39
+
40
+ it("appends brief transcript on merge", async () => {
41
+ const previousSummary = [
42
+ "[Session Goal]\n- Original goal",
43
+ "---",
44
+ "[user]\nOriginal goal\n\n[assistant]\n* Read \"old.ts\"",
45
+ ].join("\n\n");
46
+ const r = await compile({
47
+ previousSummary,
48
+ messages: [
49
+ userMsg("Next step"),
50
+ assistantWithToolCall("Read", { path: "new.ts" }),
51
+ ],
52
+ });
53
+ expect(r).toContain('* Read "old.ts"');
54
+ expect(r).toContain('* Read "new.ts"');
55
+ expect(r).toContain("Next step");
56
+ });
57
+
58
+ it("outstanding context is volatile (fresh only)", async () => {
59
+ const previousSummary = "[Outstanding Context]\n- old blocker\n\n---\n\n[user]\nhi";
60
+ const r = await compile({
61
+ previousSummary,
62
+ messages: [userMsg("continue")],
63
+ });
64
+ expect(r).not.toContain("old blocker");
65
+ });
66
+
67
+ it("caps long brief transcript with rolling window", async () => {
68
+ // Build a very long previous transcript
69
+ const longTranscript = Array.from({ length: 200 }, (_, i) =>
70
+ `[user]\nmessage ${i}`
71
+ ).join("\n\n");
72
+ const previousSummary = `[Session Goal]\n- goal\n\n---\n\n${longTranscript}`;
73
+ const r = await compile({
74
+ previousSummary,
75
+ messages: [userMsg("latest")],
76
+ });
77
+ expect(r).toContain("earlier lines omitted");
78
+ expect(r).toContain("latest");
79
+ });
80
+
81
+ // ── References merge ──
82
+
83
+ it("merges References across compactions with dedup", async () => {
84
+ const previousSummary = [
85
+ "[Session Goal]\n- goal",
86
+ "[References]\n- URL: https://example.com\n- GitHub: #42",
87
+ "---",
88
+ "[user]\noriginal",
89
+ ].join("\n\n");
90
+ const r = await compile({
91
+ previousSummary,
92
+ messages: [userMsg("Check https://other.com and #42")],
93
+ });
94
+ // Should contain both URLs
95
+ expect(r).toContain("https://example.com");
96
+ expect(r).toContain("https://other.com");
97
+ // GitHub #42 should be deduped
98
+ const count = (r.match(/#42/g) ?? []).length;
99
+ // At least one in References section
100
+ expect(count).toBeGreaterThanOrEqual(1);
101
+ });
102
+
103
+ it("preserves References from fresh when no previous", async () => {
104
+ const r = await compile({
105
+ messages: [userMsg("See https://docs.example.com")],
106
+ });
107
+ expect(r).toContain("[References]");
108
+ expect(r).toContain("https://docs.example.com");
109
+ });
110
+ });
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { extractReferences } from "../src/extract/references";
3
+ import { extractSignals } from "../src/extract/signals";
4
+ import type { NormalizedBlock } from "../src/types";
5
+
6
+ describe("config integration: references", () => {
7
+ const blocks: NormalizedBlock[] = [
8
+ { kind: "user", text: "Check https://example.com and fix the bug" },
9
+ ];
10
+
11
+ it("enabled=false returns empty extract", () => {
12
+ const r = extractReferences(blocks, { enabled: false });
13
+ expect(r.urls).toEqual([]);
14
+ expect(r.githubRefs).toEqual([]);
15
+ });
16
+
17
+ it("enabled=undefined (default) extracts normally", () => {
18
+ const r = extractReferences(blocks);
19
+ expect(r.urls.length).toBeGreaterThan(0);
20
+ });
21
+
22
+ it("extraUrlPatterns adds custom patterns", () => {
23
+ const customBlocks: NormalizedBlock[] = [
24
+ { kind: "user", text: "See ftp://files.example.com/data for the dataset" },
25
+ ];
26
+ const r = extractReferences(customBlocks, {
27
+ extraUrlPatterns: ["ftp://\\S+"],
28
+ });
29
+ expect(r.urls.some(u => u.startsWith("ftp://"))).toBe(true);
30
+ });
31
+
32
+ it("extraGithubRefPatterns adds custom patterns", () => {
33
+ const customBlocks: NormalizedBlock[] = [
34
+ { kind: "user", text: "See JIRA-1234 for the issue details" },
35
+ ];
36
+ const r = extractReferences(customBlocks, {
37
+ extraGithubRefPatterns: ["[A-Z]+-\\d+"],
38
+ });
39
+ expect(r.githubRefs).toContain("JIRA-1234");
40
+ });
41
+
42
+ it("extraVersionPatterns adds custom patterns", () => {
43
+ const customBlocks: NormalizedBlock[] = [
44
+ { kind: "user", text: "We need version 2.0-alpha.3 of the package" },
45
+ ];
46
+ const r = extractReferences(customBlocks, {
47
+ extraVersionPatterns: ["\\d+\\.\\d+-[a-z]+\\.\\d+"],
48
+ });
49
+ expect(r.versions.some(v => v.includes("alpha"))).toBe(true);
50
+ });
51
+
52
+ it("extraBranchPatterns adds custom patterns", () => {
53
+ const customBlocks: NormalizedBlock[] = [
54
+ { kind: "user", text: "Work on feature/new-dashboard component" },
55
+ ];
56
+ const r = extractReferences(customBlocks, {
57
+ extraBranchPatterns: ["\\bfeature/[\\w-]+"],
58
+ });
59
+ expect(r.branches.some(b => b.includes("feature/"))).toBe(true);
60
+ });
61
+ });
62
+
63
+ describe("config integration: signals", () => {
64
+ it("enabled=false returns empty extract", () => {
65
+ const blocks: NormalizedBlock[] = [
66
+ { kind: "user", text: "must not push to main directly" },
67
+ ];
68
+ const s = extractSignals(blocks, { enabled: false });
69
+ expect(s.constraints).toEqual([]);
70
+ });
71
+
72
+ it("extraConstraintPatterns adds custom constraint keywords", () => {
73
+ const blocks: NormalizedBlock[] = [
74
+ { kind: "user", text: "this is strictly prohibited in our workflow" },
75
+ ];
76
+ // Without extra pattern: no match
77
+ const s1 = extractSignals(blocks);
78
+ expect(s1.constraints.length).toBe(0);
79
+ // With extra pattern: matches
80
+ const s2 = extractSignals(blocks, {
81
+ extraConstraintPatterns: ["\\bstrictly prohibited\\b"],
82
+ });
83
+ expect(s2.constraints.length).toBe(1);
84
+ });
85
+
86
+ it("extraDecisionPatterns adds custom decision keywords", () => {
87
+ const blocks: NormalizedBlock[] = [
88
+ { kind: "user", text: "opted for the microservices approach because scalability" },
89
+ ];
90
+ const s = extractSignals(blocks, {
91
+ extraDecisionPatterns: ["\\bopted for\\b"],
92
+ });
93
+ expect(s.decisions.length).toBe(1);
94
+ expect(s.decisions[0]).toContain("opted for");
95
+ });
96
+
97
+ it("extraStatusKeywords adds custom status markers", () => {
98
+ const blocks: NormalizedBlock[] = [
99
+ { kind: "assistant", text: "REVIEWED: the PR looks good to merge" },
100
+ ];
101
+ const s = extractSignals(blocks, {
102
+ extraStatusKeywords: ["REVIEWED"],
103
+ });
104
+ expect(s.statuses.length).toBe(1);
105
+ expect(s.statuses[0]).toContain("REVIEWED");
106
+ });
107
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { textParts, textOf, clip, firstLine } from "../src/core/content";
3
+
4
+ describe("textParts", () => {
5
+ it("returns [] for undefined content", () => {
6
+ expect(textParts(undefined as any)).toEqual([]);
7
+ });
8
+
9
+ it("returns [] for null content", () => {
10
+ expect(textParts(null as any)).toEqual([]);
11
+ });
12
+
13
+ it("wraps string content", () => {
14
+ expect(textParts("hello")).toEqual(["hello"]);
15
+ });
16
+
17
+ it("extracts text parts from array content", () => {
18
+ const content = [
19
+ { type: "text" as const, text: "first" },
20
+ { type: "toolCall" as const, name: "x", id: "1", arguments: {} },
21
+ { type: "text" as const, text: "second" },
22
+ ];
23
+ expect(textParts(content)).toEqual(["first", "second"]);
24
+ });
25
+ });
26
+
27
+ describe("textOf", () => {
28
+ it("returns empty string for undefined content", () => {
29
+ expect(textOf(undefined as any)).toBe("");
30
+ });
31
+ });
@@ -0,0 +1,368 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { extractReferences, formatReferences } from "../src/extract/references";
3
+ import { extractSignals, formatSignals } from "../src/extract/signals";
4
+ import { compile } from "../src/core/summarize";
5
+ import { buildSections } from "../src/core/build-sections";
6
+ import type { NormalizedBlock } from "../src/types";
7
+
8
+ // ═══════════════════════════════════════════════════════════════
9
+ // Edge cases for references extractor
10
+ // ═══════════════════════════════════════════════════════════════
11
+
12
+ describe("references edge cases", () => {
13
+ it("strips trailing punctuation from URLs", () => {
14
+ const blocks: NormalizedBlock[] = [
15
+ { kind: "user", text: "See https://example.com/docs." },
16
+ ];
17
+ const r = extractReferences(blocks);
18
+ expect(r.urls[0]).toBe("https://example.com/docs");
19
+ });
20
+
21
+ it("strips trailing paren from URL inside parens", () => {
22
+ const blocks: NormalizedBlock[] = [
23
+ { kind: "user", text: "Check the docs (https://example.com/api)" },
24
+ ];
25
+ const r = extractReferences(blocks);
26
+ expect(r.urls[0]).toBe("https://example.com/api");
27
+ });
28
+
29
+ it("handles URL with port number", () => {
30
+ const blocks: NormalizedBlock[] = [
31
+ { kind: "user", text: "Service at http://100.114.135.99:4747" },
32
+ ];
33
+ const r = extractReferences(blocks);
34
+ expect(r.urls[0]).toBe("http://100.114.135.99:4747");
35
+ });
36
+
37
+ it("handles URL with fragment", () => {
38
+ const blocks: NormalizedBlock[] = [
39
+ { kind: "user", text: "See https://docs.example.com/api/v2#section" },
40
+ ];
41
+ const r = extractReferences(blocks);
42
+ expect(r.urls[0]).toBe("https://docs.example.com/api/v2#section");
43
+ });
44
+
45
+ it("handles multiple URLs in one message", () => {
46
+ const blocks: NormalizedBlock[] = [
47
+ { kind: "user", text: "Check https://example.com and https://other.com/docs" },
48
+ ];
49
+ const r = extractReferences(blocks);
50
+ expect(r.urls.length).toBe(2);
51
+ });
52
+
53
+ it("deduplicates same URL across blocks", () => {
54
+ const blocks: NormalizedBlock[] = [
55
+ { kind: "user", text: "See https://example.com" },
56
+ { kind: "assistant", text: "I checked https://example.com" },
57
+ ];
58
+ const r = extractReferences(blocks);
59
+ expect(r.urls.length).toBe(1);
60
+ });
61
+
62
+ it("skips URLs from tool_result blocks", () => {
63
+ const blocks: NormalizedBlock[] = [
64
+ { kind: "tool_result", name: "bash", text: "curl https://api.example.com/health", isError: false },
65
+ ];
66
+ const r = extractReferences(blocks);
67
+ expect(r.urls).toEqual([]);
68
+ });
69
+
70
+ it("skips URLs from tool_call blocks", () => {
71
+ const blocks: NormalizedBlock[] = [
72
+ { kind: "tool_call", name: "bash", args: { command: "curl https://example.com" } },
73
+ ];
74
+ const r = extractReferences(blocks);
75
+ expect(r.urls).toEqual([]);
76
+ });
77
+
78
+ it("extracts both URL and GitHub ref from full GitHub URL", () => {
79
+ const blocks: NormalizedBlock[] = [
80
+ { kind: "user", text: "See https://github.com/buihongduc132/pi-vcc/issues/42" },
81
+ ];
82
+ const r = extractReferences(blocks);
83
+ expect(r.urls.length).toBeGreaterThan(0);
84
+ expect(r.githubRefs.some(g => g.includes("buihongduc132/pi-vcc#42"))).toBe(true);
85
+ });
86
+
87
+ it("rejects owner/repo with common dir owners like src/components", () => {
88
+ const blocks: NormalizedBlock[] = [
89
+ { kind: "user", text: "Import from src/components/Button" },
90
+ ];
91
+ const r = extractReferences(blocks);
92
+ expect(r.githubRefs).toEqual([]);
93
+ });
94
+
95
+ it("rejects IP octets as versions", () => {
96
+ const blocks: NormalizedBlock[] = [
97
+ { kind: "user", text: "Server at 192.168.1.100" },
98
+ ];
99
+ const r = extractReferences(blocks);
100
+ expect(r.versions).toEqual([]);
101
+ });
102
+
103
+ it("extracts version with v prefix", () => {
104
+ const blocks: NormalizedBlock[] = [
105
+ { kind: "user", text: "Upgrade to v2.3.1 of the SDK" },
106
+ ];
107
+ const r = extractReferences(blocks);
108
+ expect(r.versions).toContain("v2.3.1");
109
+ });
110
+
111
+ it("extracts version without v prefix", () => {
112
+ const blocks: NormalizedBlock[] = [
113
+ { kind: "user", text: "Package version is 0.3.12" },
114
+ ];
115
+ const r = extractReferences(blocks);
116
+ expect(r.versions).toContain("0.3.12");
117
+ });
118
+
119
+ it("extracts branch with prefix", () => {
120
+ const blocks: NormalizedBlock[] = [
121
+ { kind: "user", text: "On branch feat/new-auth-module" },
122
+ ];
123
+ const r = extractReferences(blocks);
124
+ expect(r.branches).toContain("feat/new-auth-module");
125
+ });
126
+
127
+ it("extracts commit ref with hex", () => {
128
+ const blocks: NormalizedBlock[] = [
129
+ { kind: "user", text: "Fixed in commit abc1234def" },
130
+ ];
131
+ const r = extractReferences(blocks);
132
+ expect(r.commitRefs.length).toBeGreaterThan(0);
133
+ });
134
+
135
+ it("rejects pure decimal commit refs", () => {
136
+ const blocks: NormalizedBlock[] = [
137
+ { kind: "user", text: "Issue with number 1234567" },
138
+ ];
139
+ const r = extractReferences(blocks);
140
+ expect(r.commitRefs).toEqual([]);
141
+ });
142
+
143
+ it("respects URL cap at 10", () => {
144
+ const blocks: NormalizedBlock[] = Array.from({ length: 15 }, (_, i) => ({
145
+ kind: "user" as const,
146
+ text: `Check https://example${i}.com`,
147
+ }));
148
+ const r = extractReferences(blocks);
149
+ expect(r.urls.length).toBeLessThanOrEqual(10);
150
+ });
151
+
152
+ it("formatReferences returns empty for empty extract", () => {
153
+ expect(formatReferences({ urls: [], githubRefs: [], versions: [], branches: [], commitRefs: [] })).toEqual([]);
154
+ });
155
+
156
+ it("formatReferences joins all categories", () => {
157
+ const r = formatReferences({
158
+ urls: ["https://example.com"],
159
+ githubRefs: ["#42"],
160
+ versions: ["v1.0.0"],
161
+ branches: ["feat/x"],
162
+ commitRefs: ["abc1234"],
163
+ });
164
+ expect(r.length).toBe(5);
165
+ expect(r[0]).toContain("URL:");
166
+ expect(r[1]).toContain("GitHub:");
167
+ expect(r[2]).toContain("Version:");
168
+ expect(r[3]).toContain("Branch:");
169
+ expect(r[4]).toContain("CommitRef:");
170
+ });
171
+ });
172
+
173
+ // ═══════════════════════════════════════════════════════════════
174
+ // Edge cases for signals extractor
175
+ // ═══════════════════════════════════════════════════════════════
176
+
177
+ describe("signals edge cases", () => {
178
+ it("does not match constraint at end of question", () => {
179
+ const blocks: NormalizedBlock[] = [
180
+ { kind: "user", text: "What if we cannot deploy today?" },
181
+ ];
182
+ const s = extractSignals(blocks);
183
+ expect(s.constraints).toEqual([]);
184
+ });
185
+
186
+ it("does not match very short lines", () => {
187
+ const blocks: NormalizedBlock[] = [
188
+ { kind: "user", text: "do not" },
189
+ ];
190
+ const s = extractSignals(blocks);
191
+ expect(s.constraints).toEqual([]);
192
+ });
193
+
194
+ it("does not match signals from tool_result", () => {
195
+ const blocks: NormalizedBlock[] = [
196
+ { kind: "tool_result", name: "bash", text: "DONE: all tests pass", isError: false },
197
+ ];
198
+ const s = extractSignals(blocks);
199
+ expect(s.statuses).toEqual([]);
200
+ });
201
+
202
+ it("does not match signals from tool_call", () => {
203
+ const blocks: NormalizedBlock[] = [
204
+ { kind: "tool_call", name: "bash", args: { command: "echo 'must not do this'" } },
205
+ ];
206
+ const s = extractSignals(blocks);
207
+ expect(s.constraints).toEqual([]);
208
+ });
209
+
210
+ it("does not match decision from assistant", () => {
211
+ const blocks: NormalizedBlock[] = [
212
+ { kind: "assistant", text: "I decided to use the simpler approach" },
213
+ ];
214
+ const s = extractSignals(blocks);
215
+ expect(s.decisions).toEqual([]);
216
+ });
217
+
218
+ it("matches status DONE from assistant", () => {
219
+ const blocks: NormalizedBlock[] = [
220
+ { kind: "assistant", text: "DONE — auth module migrated successfully" },
221
+ ];
222
+ const s = extractSignals(blocks);
223
+ expect(s.statuses.length).toBe(1);
224
+ expect(s.statuses[0]).toContain("DONE");
225
+ });
226
+
227
+ it("deduplicates signals case-insensitively", () => {
228
+ const blocks: NormalizedBlock[] = [
229
+ { kind: "user", text: "Must Not Push To Main Directly" },
230
+ { kind: "user", text: "must not push to main directly" },
231
+ ];
232
+ const s = extractSignals(blocks);
233
+ expect(s.constraints.length).toBe(1);
234
+ });
235
+
236
+ it("clips long constraint lines to 200 chars", () => {
237
+ const long = "must not " + "a".repeat(300);
238
+ const blocks: NormalizedBlock[] = [
239
+ { kind: "user", text: long },
240
+ ];
241
+ const s = extractSignals(blocks);
242
+ expect(s.constraints.length).toBe(1);
243
+ expect(s.constraints[0].length).toBeLessThanOrEqual(200);
244
+ });
245
+
246
+ it("respects constraint cap at 5", () => {
247
+ const blocks: NormalizedBlock[] = Array.from({ length: 8 }, (_, i) => ({
248
+ kind: "user" as const,
249
+ text: `Constraint ${i}: must not do thing number ${i}`,
250
+ }));
251
+ const s = extractSignals(blocks);
252
+ expect(s.constraints.length).toBeLessThanOrEqual(5);
253
+ });
254
+
255
+ it("formatSignals returns empty for empty extract", () => {
256
+ expect(formatSignals({ constraints: [], decisions: [], statuses: [] })).toEqual([]);
257
+ });
258
+
259
+ it("skips very long lines (>500 chars, likely code dumps)", () => {
260
+ const blocks: NormalizedBlock[] = [
261
+ { kind: "user", text: "must not " + "x".repeat(600) },
262
+ ];
263
+ const s = extractSignals(blocks);
264
+ expect(s.constraints).toEqual([]);
265
+ });
266
+
267
+ it("matches 'off limits' variant", () => {
268
+ const blocks: NormalizedBlock[] = [
269
+ { kind: "user", text: "The admin panel is off limits for this sprint" },
270
+ ];
271
+ const s = extractSignals(blocks);
272
+ expect(s.constraints.length).toBe(1);
273
+ });
274
+
275
+ it("matches 'off-limits' hyphenated variant", () => {
276
+ const blocks: NormalizedBlock[] = [
277
+ { kind: "user", text: "The admin panel is off-limits for this sprint" },
278
+ ];
279
+ const s = extractSignals(blocks);
280
+ expect(s.constraints.length).toBe(1);
281
+ });
282
+ });
283
+
284
+ // ═══════════════════════════════════════════════════════════════
285
+ // Full pipeline integration tests
286
+ // ═══════════════════════════════════════════════════════════════
287
+
288
+ describe("full pipeline integration", () => {
289
+ it("buildSections includes references and keySignals", () => {
290
+ const blocks: NormalizedBlock[] = [
291
+ { kind: "user", text: "Fix https://docs.example.com and don't use SQLite" },
292
+ { kind: "assistant", text: "DONE — investigation complete" },
293
+ ];
294
+ const r = buildSections({ blocks });
295
+ expect(r.references.length).toBeGreaterThan(0);
296
+ expect(r.keySignals.length).toBeGreaterThan(0);
297
+ });
298
+
299
+ it("buildSections returns empty new sections when no matches", () => {
300
+ const blocks: NormalizedBlock[] = [
301
+ { kind: "user", text: "Fix the login bug" },
302
+ ];
303
+ const r = buildSections({ blocks });
304
+ // No URLs, no signals — should be empty
305
+ expect(r.references).toEqual([]);
306
+ expect(r.keySignals).toEqual([]);
307
+ });
308
+
309
+ it("compile produces valid output with new sections", async () => {
310
+ const result = await compile({
311
+ messages: [
312
+ { role: "user", content: "Check https://example.com and fix issue #42. Must not break backward compat." },
313
+ { role: "assistant", content: "DONE — analysis complete" },
314
+ ],
315
+ });
316
+ expect(result).toContain("[References]");
317
+ expect(result).toContain("[Key Signals]");
318
+ expect(result).toContain("https://example.com");
319
+ expect(result).toContain("Constraint:");
320
+ });
321
+
322
+ it("compile merges references across compactions", async () => {
323
+ const prev = await compile({
324
+ messages: [
325
+ { role: "user", content: "See https://docs.example.com" },
326
+ ],
327
+ });
328
+ const fresh = await compile({
329
+ messages: [
330
+ { role: "user", content: "Also check https://api.example.com/v2" },
331
+ ],
332
+ previousSummary: prev,
333
+ });
334
+ // Both URLs should be present after merge
335
+ expect(fresh).toContain("https://docs.example.com");
336
+ expect(fresh).toContain("https://api.example.com/v2");
337
+ });
338
+
339
+ it("compile merges key signals across compactions", async () => {
340
+ const prev = await compile({
341
+ messages: [
342
+ { role: "user", content: "Must not push to main directly" },
343
+ ],
344
+ });
345
+ const fresh = await compile({
346
+ messages: [
347
+ { role: "user", content: "Decided to use Redis for caching" },
348
+ ],
349
+ previousSummary: prev,
350
+ });
351
+ expect(fresh).toContain("Constraint:");
352
+ expect(fresh).toContain("Decision:");
353
+ });
354
+
355
+ it("backwards compat: output identical when new sections empty", async () => {
356
+ const result = await compile({
357
+ messages: [
358
+ { role: "user", content: "Fix the login bug" },
359
+ { role: "assistant", content: "I'll fix it." },
360
+ ],
361
+ });
362
+ // Should have standard sections
363
+ expect(result).toContain("[Session Goal]");
364
+ // New sections should NOT appear when empty
365
+ expect(result).not.toContain("[References]");
366
+ expect(result).not.toContain("[Key Signals]");
367
+ });
368
+ });