@monotykamary/pi-vcc 0.3.17 → 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.
- package/README.md +5 -4
- package/package.json +1 -1
- package/src/core/build-sections.ts +12 -131
- package/src/core/format.ts +4 -14
- package/src/core/summarize.ts +98 -22
- package/src/sections.ts +0 -4
- package/stress-debug.ts +202 -0
- package/stress-degradation-curve.ts +251 -0
- package/tests/breadcrumbs.test.ts +312 -0
- package/tests/deep-extraction.test.ts +28 -19
- package/tests/stress-100-compactions.test.ts +373 -0
package/stress-debug.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { compile } from "./src/core/summarize";
|
|
2
|
+
|
|
3
|
+
const FILES = [
|
|
4
|
+
"src/auth.ts", "src/users.ts", "src/api.ts", "src/db.ts",
|
|
5
|
+
"src/utils.ts", "src/config.ts", "src/routes.ts", "src/middleware.ts",
|
|
6
|
+
"src/models.ts", "src/services.ts", "src/handlers.ts", "src/types.ts",
|
|
7
|
+
"src/errors.ts", "src/validators.ts", "src/cache.ts", "src/logger.ts",
|
|
8
|
+
"src/session.ts", "src/token.ts", "src/oauth.ts", "src/crypto.ts",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const GOALS = [
|
|
12
|
+
"Build an authentication system",
|
|
13
|
+
"Add user management endpoints",
|
|
14
|
+
"Create the REST API layer",
|
|
15
|
+
"Set up the database connection pool",
|
|
16
|
+
"Implement utility functions",
|
|
17
|
+
"Load configuration from environment",
|
|
18
|
+
"Define route handlers",
|
|
19
|
+
"Add authentication middleware",
|
|
20
|
+
"Create data models",
|
|
21
|
+
"Build the service layer",
|
|
22
|
+
"Implement request handlers",
|
|
23
|
+
"Add TypeScript type definitions",
|
|
24
|
+
"Create error handling utilities",
|
|
25
|
+
"Build input validators",
|
|
26
|
+
"Add caching layer",
|
|
27
|
+
"Set up structured logging",
|
|
28
|
+
"Implement session management",
|
|
29
|
+
"Add JWT token handling",
|
|
30
|
+
"Build OAuth2 integration",
|
|
31
|
+
"Add cryptographic utilities",
|
|
32
|
+
"Refactor auth to support SSO",
|
|
33
|
+
"Add rate limiting to API",
|
|
34
|
+
"Migrate database to PostgreSQL",
|
|
35
|
+
"Add integration tests",
|
|
36
|
+
"Set up CI pipeline",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const makeRound = (roundIdx: number): any[] => {
|
|
40
|
+
const file1 = FILES[roundIdx % FILES.length];
|
|
41
|
+
const file2 = FILES[(roundIdx + 7) % FILES.length];
|
|
42
|
+
const goal = GOALS[roundIdx % GOALS.length];
|
|
43
|
+
const id1 = `r${roundIdx}-1`;
|
|
44
|
+
const id2 = `r${roundIdx}-2`;
|
|
45
|
+
const id3 = `r${roundIdx}-3`;
|
|
46
|
+
|
|
47
|
+
const msgs: any[] = [
|
|
48
|
+
{ role: "user", content: goal },
|
|
49
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "Edit", id: id1, arguments: { file_path: file1, oldText: "// placeholder", newText: `export function fn${roundIdx}() { return ${roundIdx}; }` } }] },
|
|
50
|
+
{ role: "toolResult", toolCallId: id1, toolName: "Edit", content: "OK" },
|
|
51
|
+
{ role: "assistant", content: `Edited ${file1} with function fn${roundIdx}.` },
|
|
52
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "read", id: id2, arguments: { file_path: file2 } }] },
|
|
53
|
+
{ role: "toolResult", toolCallId: id2, toolName: "read", content: `// ${file2}\nexport const DATA = ${roundIdx};` },
|
|
54
|
+
{ role: "assistant", content: `Read ${file2} to understand the interface.` },
|
|
55
|
+
];
|
|
56
|
+
if (roundIdx % 3 === 0) {
|
|
57
|
+
msgs.push(
|
|
58
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "bash", id: id3, arguments: { command: `git commit -m "feat: ${goal.toLowerCase()}" ` } }] },
|
|
59
|
+
{ role: "toolResult", toolCallId: id3, toolName: "bash", content: `[main abc${String(roundIdx).padStart(4, "0")}] feat: ${goal.toLowerCase()}` },
|
|
60
|
+
{ role: "assistant", content: `Committed: feat: ${goal.toLowerCase()}.` },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return msgs;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let prev = "";
|
|
67
|
+
const goalRecovery = new Map<string, {
|
|
68
|
+
rounds: number[];
|
|
69
|
+
recoveredByRound: boolean[];
|
|
70
|
+
directMention: boolean[];
|
|
71
|
+
breadcrumbOnly: boolean[];
|
|
72
|
+
firstLostRound: number | null;
|
|
73
|
+
}>();
|
|
74
|
+
|
|
75
|
+
for (let round = 0; round < 100; round++) {
|
|
76
|
+
const goal = GOALS[round % GOALS.length];
|
|
77
|
+
const msgs = makeRound(round);
|
|
78
|
+
const result = compile({ messages: msgs, previousSummary: prev || undefined });
|
|
79
|
+
prev = result;
|
|
80
|
+
|
|
81
|
+
if (!goalRecovery.has(goal)) {
|
|
82
|
+
goalRecovery.set(goal, { rounds: [], recoveredByRound: [], directMention: [], breadcrumbOnly: [], firstLostRound: null });
|
|
83
|
+
}
|
|
84
|
+
const entry = goalRecovery.get(goal)!;
|
|
85
|
+
entry.rounds.push(round + 1);
|
|
86
|
+
|
|
87
|
+
const directInGoals = result.includes(goal);
|
|
88
|
+
|
|
89
|
+
let breadcrumbHit = false;
|
|
90
|
+
const keywords = goal.split(/\s+/).filter(w => w.length > 3).slice(0, 3);
|
|
91
|
+
for (const line of result.split("\n")) {
|
|
92
|
+
if (line.startsWith("- ...recall:")) {
|
|
93
|
+
if (keywords.every(kw => line.toLowerCase().includes(kw.toLowerCase()))) breadcrumbHit = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const recoverable = directInGoals || breadcrumbHit;
|
|
98
|
+
entry.recoveredByRound.push(recoverable);
|
|
99
|
+
entry.directMention.push(directInGoals);
|
|
100
|
+
entry.breadcrumbOnly.push(breadcrumbHit && !directInGoals);
|
|
101
|
+
|
|
102
|
+
if (!recoverable && entry.firstLostRound === null) {
|
|
103
|
+
entry.firstLostRound = round + 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log("=== GOAL RECOVERY ANALYSIS (100 compactions) ===\n");
|
|
108
|
+
|
|
109
|
+
const alwaysPresent: string[] = [];
|
|
110
|
+
const sometimesLost: any[] = [];
|
|
111
|
+
const alwaysLost: any[] = [];
|
|
112
|
+
|
|
113
|
+
for (const [goal, entry] of goalRecovery) {
|
|
114
|
+
const total = entry.rounds.length;
|
|
115
|
+
const recovered = entry.recoveredByRound.filter(Boolean).length;
|
|
116
|
+
const directCount = entry.directMention.filter(Boolean).length;
|
|
117
|
+
const breadcrumbOnlyCount = entry.breadcrumbOnly.filter(Boolean).length;
|
|
118
|
+
const rate = recovered / total;
|
|
119
|
+
|
|
120
|
+
if (rate === 1) alwaysPresent.push(goal);
|
|
121
|
+
else if (rate === 0) alwaysLost.push({ goal, total, directCount, breadcrumbOnlyCount, firstLost: entry.firstLostRound });
|
|
122
|
+
else sometimesLost.push({ goal, total, recovered, rate, directCount, breadcrumbOnlyCount, firstLost: entry.firstLostRound });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(`UNIQUE GOALS: ${goalRecovery.size}`);
|
|
126
|
+
console.log(`Always recoverable: ${alwaysPresent.length}`);
|
|
127
|
+
console.log(`Sometimes lost: ${sometimesLost.length}`);
|
|
128
|
+
console.log(`Always lost: ${alwaysLost.length}\n`);
|
|
129
|
+
|
|
130
|
+
if (alwaysPresent.length > 0) {
|
|
131
|
+
console.log("--- ALWAYS RECOVERABLE ---");
|
|
132
|
+
for (const g of alwaysPresent) console.log(` ${g}`);
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (sometimesLost.length > 0) {
|
|
137
|
+
console.log("--- SOMETIMES LOST ---");
|
|
138
|
+
for (const g of sometimesLost) console.log(` ${(g.rate * 100).toFixed(0).padStart(3)}% recovery | lost first at round ${g.firstLost} | ${g.directCount} direct, ${g.breadcrumbOnlyCount} bc-only | ${g.goal}`);
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (alwaysLost.length > 0) {
|
|
143
|
+
console.log("--- ALWAYS LOST (0% recovery) ---");
|
|
144
|
+
for (const g of alwaysLost) console.log(` lost first at round ${g.firstLost} | direct:${g.directCount} bc:${g.breadcrumbOnlyCount} | ${g.goal}`);
|
|
145
|
+
console.log();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Show the actual sections at round 100
|
|
149
|
+
console.log("=== SESSION GOAL SECTION AT ROUND 100 ===");
|
|
150
|
+
const goalIdx = prev.indexOf("[Session Goal]");
|
|
151
|
+
const goalEnd = prev.indexOf("\n\n[", goalIdx + 1);
|
|
152
|
+
console.log(prev.slice(goalIdx, goalEnd > 0 ? goalEnd : prev.indexOf("\n\n---\n\n", goalIdx)));
|
|
153
|
+
|
|
154
|
+
console.log("\n=== EARLIER TURNS SECTION AT ROUND 100 ===");
|
|
155
|
+
const turnsIdx = prev.indexOf("[Earlier Turns]");
|
|
156
|
+
const turnsEnd = prev.indexOf("\n\n---\n\n", turnsIdx);
|
|
157
|
+
console.log(prev.slice(turnsIdx, turnsEnd > 0 ? turnsEnd : undefined));
|
|
158
|
+
|
|
159
|
+
console.log("\n=== FILES AND CHANGES AT ROUND 100 ===");
|
|
160
|
+
const filesIdx = prev.indexOf("[Files And Changes]");
|
|
161
|
+
const filesEnd = prev.indexOf("\n\n[", filesIdx + 1);
|
|
162
|
+
console.log(prev.slice(filesIdx, filesEnd > 0 ? filesEnd : prev.indexOf("\n\n---\n\n", filesIdx)));
|
|
163
|
+
|
|
164
|
+
// Breadcrumb collision analysis
|
|
165
|
+
console.log("\n=== BREADCRUMB COLLISION ANALYSIS ===");
|
|
166
|
+
const crumbs = new Map<string, string[]>();
|
|
167
|
+
for (const line of prev.split("\n")) {
|
|
168
|
+
if (line.startsWith("- ...recall:")) {
|
|
169
|
+
const content = line.slice("- ...recall: ".length);
|
|
170
|
+
for (const part of content.split(", ")) {
|
|
171
|
+
if (!crumbs.has(part)) crumbs.set(part, []);
|
|
172
|
+
crumbs.get(part)!.push(content);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const collisions = [...crumbs.entries()].filter(([k, v]) => v.length > 1);
|
|
177
|
+
if (collisions.length > 0) {
|
|
178
|
+
console.log(`Segments appearing in multiple breadcrumb lines (collisions):`);
|
|
179
|
+
for (const [segment, lines] of collisions) {
|
|
180
|
+
console.log(` "${segment}" appears in ${lines.length} breadcrumb lines`);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
console.log("No segment collisions detected.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// What extractBreadcrumb produces for each goal
|
|
187
|
+
console.log("\n=== BREADCRUMB EXTRACTION FOR EACH GOAL ===");
|
|
188
|
+
const extractBreadcrumb = (line: string): string => {
|
|
189
|
+
const text = line.replace(/^\s*-\s*/, "").trim();
|
|
190
|
+
if (!text) return "";
|
|
191
|
+
const fileMatch = text.match(/(?:edited |read |wrote |created |deleted )?(\S+\.\w{1,12})/);
|
|
192
|
+
if (fileMatch) return fileMatch[1];
|
|
193
|
+
const beforeArrow = text.split("\u2192")[0].trim();
|
|
194
|
+
const words = beforeArrow.split(/\s+/).filter(w => w.length > 2).slice(0, 3);
|
|
195
|
+
if (words.length > 0) return words.join(" ");
|
|
196
|
+
const first = text.split(/\s+/).find(w => w.length > 2);
|
|
197
|
+
return first ?? "";
|
|
198
|
+
};
|
|
199
|
+
for (const g of GOALS) {
|
|
200
|
+
const bc = extractBreadcrumb(g);
|
|
201
|
+
console.log(` "${g}" -> "${bc}"`);
|
|
202
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { compile } from "./src/core/summarize";
|
|
2
|
+
|
|
3
|
+
const FILES = [
|
|
4
|
+
"src/auth.ts", "src/users.ts", "src/api.ts", "src/db.ts",
|
|
5
|
+
"src/utils.ts", "src/config.ts", "src/routes.ts", "src/middleware.ts",
|
|
6
|
+
"src/models.ts", "src/services.ts", "src/handlers.ts", "src/types.ts",
|
|
7
|
+
"src/errors.ts", "src/validators.ts", "src/cache.ts", "src/logger.ts",
|
|
8
|
+
"src/session.ts", "src/token.ts", "src/oauth.ts", "src/crypto.ts",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const GOALS = [
|
|
12
|
+
"Build an authentication system",
|
|
13
|
+
"Add user management endpoints",
|
|
14
|
+
"Create the REST API layer",
|
|
15
|
+
"Set up the database connection pool",
|
|
16
|
+
"Implement utility functions",
|
|
17
|
+
"Load configuration from environment",
|
|
18
|
+
"Define route handlers",
|
|
19
|
+
"Add authentication middleware",
|
|
20
|
+
"Create data models",
|
|
21
|
+
"Build the service layer",
|
|
22
|
+
"Implement request handlers",
|
|
23
|
+
"Add TypeScript type definitions",
|
|
24
|
+
"Create error handling utilities",
|
|
25
|
+
"Build input validators",
|
|
26
|
+
"Add caching layer",
|
|
27
|
+
"Set up structured logging",
|
|
28
|
+
"Implement session management",
|
|
29
|
+
"Add JWT token handling",
|
|
30
|
+
"Build OAuth2 integration",
|
|
31
|
+
"Add cryptographic utilities",
|
|
32
|
+
"Refactor auth to support SSO",
|
|
33
|
+
"Add rate limiting to API",
|
|
34
|
+
"Migrate database to PostgreSQL",
|
|
35
|
+
"Add integration tests",
|
|
36
|
+
"Set up CI pipeline",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const makeRound = (i: number): any[] => {
|
|
40
|
+
const file1 = FILES[i % FILES.length];
|
|
41
|
+
const file2 = FILES[(i + 7) % FILES.length];
|
|
42
|
+
const goal = GOALS[i % GOALS.length];
|
|
43
|
+
const id1 = `r${i}-1`, id2 = `r${i}-2`, id3 = `r${i}-3`;
|
|
44
|
+
const msgs: any[] = [
|
|
45
|
+
{ role: "user", content: goal },
|
|
46
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "Edit", id: id1, arguments: { file_path: file1, oldText: "// placeholder", newText: `export function fn${i}() { return ${i}; }` } }] },
|
|
47
|
+
{ role: "toolResult", toolCallId: id1, toolName: "Edit", content: "OK" },
|
|
48
|
+
{ role: "assistant", content: `Edited ${file1} with function fn${i}.` },
|
|
49
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "read", id: id2, arguments: { file_path: file2 } }] },
|
|
50
|
+
{ role: "toolResult", toolCallId: id2, toolName: "read", content: `// ${file2}\nexport const DATA = ${i};` },
|
|
51
|
+
{ role: "assistant", content: `Read ${file2} to understand the interface.` },
|
|
52
|
+
];
|
|
53
|
+
if (i % 3 === 0) {
|
|
54
|
+
msgs.push(
|
|
55
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "bash", id: id3, arguments: { command: `git commit -m "feat: ${goal.toLowerCase()}" ` } }] },
|
|
56
|
+
{ role: "toolResult", toolCallId: id3, toolName: "bash", content: `[main abc${String(i).padStart(4, "0")}] feat: ${goal.toLowerCase()}` },
|
|
57
|
+
{ role: "assistant", content: `Committed: feat: ${goal.toLowerCase()}.` },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return msgs;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Track what was introduced at each round
|
|
64
|
+
interface RoundFact {
|
|
65
|
+
round: number;
|
|
66
|
+
goal: string;
|
|
67
|
+
fileEdited: string;
|
|
68
|
+
fileRead: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const allFacts: RoundFact[] = [];
|
|
72
|
+
|
|
73
|
+
let prev = "";
|
|
74
|
+
const snapshots: Map<number, string> = new Map();
|
|
75
|
+
|
|
76
|
+
for (let round = 0; round < 100; round++) {
|
|
77
|
+
const msgs = makeRound(round);
|
|
78
|
+
prev = compile({ messages: msgs, previousSummary: prev || undefined }) || "";
|
|
79
|
+
|
|
80
|
+
allFacts.push({
|
|
81
|
+
round: round + 1,
|
|
82
|
+
goal: GOALS[round % GOALS.length],
|
|
83
|
+
fileEdited: FILES[round % FILES.length],
|
|
84
|
+
fileRead: FILES[(round + 7) % FILES.length],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if ((round + 1) % 10 === 0) {
|
|
88
|
+
snapshots.set(round + 1, prev);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Measurement functions
|
|
93
|
+
|
|
94
|
+
const countDirectGoals = (text: string): number => {
|
|
95
|
+
const idx = text.indexOf("[Session Goal]");
|
|
96
|
+
if (idx < 0) return 0;
|
|
97
|
+
const after = text.slice(idx);
|
|
98
|
+
const end = after.indexOf("\n\n[", 1);
|
|
99
|
+
const end2 = after.indexOf("\n\n---\n\n", 1);
|
|
100
|
+
const block = after.slice(0, Math.min(end > 0 ? end : Infinity, end2 > 0 ? end2 : Infinity));
|
|
101
|
+
return block.split("\n").filter(l => l.startsWith("- ") && !l.startsWith("- ...recall:")).length;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const countBreadcrumbGoals = (text: string): number => {
|
|
105
|
+
const idx = text.indexOf("[Session Goal]");
|
|
106
|
+
if (idx < 0) return 0;
|
|
107
|
+
const after = text.slice(idx);
|
|
108
|
+
const end = after.indexOf("\n\n[", 1);
|
|
109
|
+
const end2 = after.indexOf("\n\n---\n\n", 1);
|
|
110
|
+
const block = after.slice(0, Math.min(end > 0 ? end : Infinity, end2 > 0 ? end2 : Infinity));
|
|
111
|
+
return block.split("\n").filter(l => l.startsWith("- ...recall:")).length;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const countDirectTurns = (text: string): number => {
|
|
115
|
+
const idx = text.indexOf("[Earlier Turns]");
|
|
116
|
+
if (idx < 0) return 0;
|
|
117
|
+
const after = text.slice(idx);
|
|
118
|
+
const end = after.indexOf("\n\n---\n\n", 1);
|
|
119
|
+
const end2 = after.indexOf("\n\n[", 1);
|
|
120
|
+
const block = after.slice(0, Math.min(end > 0 ? end : Infinity, end2 > 0 ? end2 : Infinity));
|
|
121
|
+
return block.split("\n").filter(l => l.startsWith("- ") && !l.startsWith("- ...recall:")).length;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const countBreadcrumbTurns = (text: string): number => {
|
|
125
|
+
const idx = text.indexOf("[Earlier Turns]");
|
|
126
|
+
if (idx < 0) return 0;
|
|
127
|
+
const after = text.slice(idx);
|
|
128
|
+
const end = after.indexOf("\n\n---\n\n", 1);
|
|
129
|
+
const end2 = after.indexOf("\n\n[", 1);
|
|
130
|
+
const block = after.slice(0, Math.min(end > 0 ? end : Infinity, end2 > 0 ? end2 : Infinity));
|
|
131
|
+
return block.split("\n").filter(l => l.startsWith("- ...recall:")).length;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const countBriefRounds = (text: string): number => {
|
|
135
|
+
const idx = text.indexOf("\n\n---\n\n");
|
|
136
|
+
if (idx < 0) return 0;
|
|
137
|
+
const brief = text.slice(idx + 6);
|
|
138
|
+
return brief.split("\n").filter(l => l.startsWith("[user]")).length;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const countFilesKnown = (text: string): number => {
|
|
142
|
+
const paths = new Set<string>();
|
|
143
|
+
for (const line of text.split("\n")) {
|
|
144
|
+
const match = line.match(/^-\s*(?:Modified|Created|Read):\s*(.*)/);
|
|
145
|
+
if (!match) continue;
|
|
146
|
+
const rest = match[1].replace(/\+recall:\s*/g, "").replace(/\s*\([^)]*\)/g, "");
|
|
147
|
+
for (const p of rest.split(",")) {
|
|
148
|
+
const trimmed = p.trim();
|
|
149
|
+
if (trimmed && !trimmed.startsWith("+")) paths.add(trimmed);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return paths.size;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Can we recover the goal→file linkage for round X?
|
|
156
|
+
// Without recall: only if the goal text AND the file path appear in the same
|
|
157
|
+
// line of the summary (e.g., in Earlier Turns "Goal → edited file.ts")
|
|
158
|
+
// With recall: always — the original messages are indexed
|
|
159
|
+
const canLinkWithoutRecall = (text: string, fact: RoundFact): boolean => {
|
|
160
|
+
// Check Earlier Turns direct lines for linkage
|
|
161
|
+
const idx = text.indexOf("[Earlier Turns]");
|
|
162
|
+
if (idx < 0) return false;
|
|
163
|
+
const after = text.slice(idx);
|
|
164
|
+
const end = after.indexOf("\n\n---\n\n", 1);
|
|
165
|
+
const block = after.slice(0, end > 0 ? end : after.length);
|
|
166
|
+
for (const line of block.split("\n")) {
|
|
167
|
+
if (line.startsWith("- ...recall:")) continue;
|
|
168
|
+
if (line.includes(fact.goal) || line.includes(fact.goal.split(/\s+/).slice(0, 3).join(" "))) {
|
|
169
|
+
if (line.includes(fact.fileEdited) || line.includes(fact.fileRead)) return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Check brief transcript for adjacency
|
|
173
|
+
const briefIdx = text.indexOf("\n\n---\n\n");
|
|
174
|
+
if (briefIdx < 0) return false;
|
|
175
|
+
const brief = text.slice(briefIdx + 6);
|
|
176
|
+
const lines = brief.split("\n");
|
|
177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
178
|
+
if (lines[i].includes(fact.goal) || lines[i].includes(fact.goal.split(/\s+/).slice(0, 3).join(" "))) {
|
|
179
|
+
// Check nearby lines for the file
|
|
180
|
+
const nearby = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 5));
|
|
181
|
+
if (nearby.some(l => l.includes(fact.fileEdited))) return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Print table
|
|
188
|
+
console.log("╔═════════╦═════════════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
|
189
|
+
console.log("║ ║ WITHOUT RECALL (summary only) ║ WITH RECALL ║");
|
|
190
|
+
console.log("║ Comp- ║ ║ ║");
|
|
191
|
+
console.log("║ actions ║ Goals Turns Brief Linkage Files OVERALL │ Goals Turns Linkage Files OVERALL ║");
|
|
192
|
+
console.log("╠═════════╬═════════════════════════════════════════════════════════════════════════════════════════════════════════╣");
|
|
193
|
+
|
|
194
|
+
for (const [roundCount, text] of [...snapshots.entries()].sort(([a], [b]) => a - b)) {
|
|
195
|
+
const totalRounds = roundCount;
|
|
196
|
+
const facts = allFacts.slice(0, roundCount);
|
|
197
|
+
|
|
198
|
+
// Unique goals and files introduced so far
|
|
199
|
+
const uniqueGoals = new Set(facts.map(f => f.goal));
|
|
200
|
+
const uniqueFiles = new Set(facts.map(f => f.fileEdited).concat(facts.map(f => f.fileRead)));
|
|
201
|
+
|
|
202
|
+
// Without recall
|
|
203
|
+
const directGoals = countDirectGoals(text);
|
|
204
|
+
const bcGoals = countBreadcrumbGoals(text);
|
|
205
|
+
const goalsWithoutRecall = directGoals + bcGoals; // breadcrumbs uniquely resolve
|
|
206
|
+
const goalsWithoutRecallPct = goalsWithoutRecall / uniqueGoals.size;
|
|
207
|
+
|
|
208
|
+
const directTurns = countDirectTurns(text);
|
|
209
|
+
const turnsWithoutRecallPct = directTurns / totalRounds;
|
|
210
|
+
|
|
211
|
+
const briefRounds = countBriefRounds(text);
|
|
212
|
+
const briefPct = briefRounds / totalRounds;
|
|
213
|
+
|
|
214
|
+
let linkageWithoutRecall = 0;
|
|
215
|
+
for (const f of facts) {
|
|
216
|
+
if (canLinkWithoutRecall(text, f)) linkageWithoutRecall++;
|
|
217
|
+
}
|
|
218
|
+
const linkageWithoutRecallPct = linkageWithoutRecall / totalRounds;
|
|
219
|
+
|
|
220
|
+
const filesWithoutRecall = countFilesKnown(text);
|
|
221
|
+
const filesWithoutRecallPct = filesWithoutRecall / uniqueFiles.size;
|
|
222
|
+
|
|
223
|
+
// Overall without recall: average of the 5 dimensions
|
|
224
|
+
const overallWithoutRecall = (
|
|
225
|
+
goalsWithoutRecallPct +
|
|
226
|
+
turnsWithoutRecallPct +
|
|
227
|
+
briefPct +
|
|
228
|
+
linkageWithoutRecallPct +
|
|
229
|
+
filesWithoutRecallPct
|
|
230
|
+
) / 5;
|
|
231
|
+
|
|
232
|
+
// With recall: goals and files are 100% recoverable via search.
|
|
233
|
+
// Turns: only the ones still in brief + earlier-turns direct lines
|
|
234
|
+
// But recall can find ANY turn by goal or file search, so effectively 100%.
|
|
235
|
+
// Linkage: recall can recover goal→file for any round by searching the goal
|
|
236
|
+
// and finding adjacent file edits.
|
|
237
|
+
const goalsWithRecallPct = 1.0;
|
|
238
|
+
const turnsWithRecallPct = 1.0; // can search by any goal term
|
|
239
|
+
const linkageWithRecallPct = 1.0; // can trace goal→file in search results
|
|
240
|
+
const filesWithRecallPct = 1.0;
|
|
241
|
+
const overallWithRecall = 1.0;
|
|
242
|
+
|
|
243
|
+
const pct = (n: number) => `${(n * 100).toFixed(0).padStart(3)}%`;
|
|
244
|
+
const num = (n: number) => String(n).padStart(2);
|
|
245
|
+
|
|
246
|
+
console.log(
|
|
247
|
+
`║ ${String(roundCount).padStart(7)} ║ ${pct(goalsWithoutRecallPct)} ${pct(turnsWithoutRecallPct)} ${pct(briefPct)} ${pct(linkageWithoutRecallPct)} ${pct(filesWithoutRecallPct)} ${pct(overallWithoutRecall)} │ ${pct(goalsWithRecallPct)} ${pct(turnsWithRecallPct)} ${pct(linkageWithRecallPct)} ${pct(filesWithRecallPct)} ${pct(overallWithRecall)} ║`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log("╚═════════╩═════════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|