pi-rtk-optimizer 0.7.0 → 0.8.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/CHANGELOG.md +25 -1
- package/README.md +14 -8
- package/package.json +67 -64
- package/src/additional-coverage-test.ts +117 -58
- package/src/command-completions.ts +49 -49
- package/src/command-rewriter-test.ts +187 -118
- package/src/command-rewriter.ts +46 -43
- package/src/config-modal-test.ts +97 -31
- package/src/config-modal.ts +91 -12
- package/src/constants.ts +1 -1
- package/src/index-test.ts +198 -3
- package/src/index.ts +49 -5
- package/src/output-compactor-test.ts +208 -3
- package/src/output-compactor.ts +316 -16
- package/src/rewrite-pipeline-safety.ts +203 -178
- package/src/rtk-command-environment.ts +73 -69
- package/src/rtk-executable-resolver.ts +97 -0
- package/src/rtk-rewrite-provider.ts +126 -90
- package/src/shell-env-prefix.ts +5 -1
- package/src/test-helpers.ts +23 -10
- package/src/tool-execution-sanitizer.ts +80 -69
- package/src/types-shims.d.ts +4 -2
- package/src/types.ts +4 -0
- package/src/windows-command-helpers.ts +92 -16
- package/src/zellij-modal.ts +137 -30
|
@@ -6,7 +6,7 @@ import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
|
|
|
6
6
|
|
|
7
7
|
const TEST_AGENT_DIR = "/tmp/.pi/agent";
|
|
8
8
|
|
|
9
|
-
mock.module("@
|
|
9
|
+
mock.module("@earendil-works/pi-coding-agent", () => ({
|
|
10
10
|
getAgentDir: () => TEST_AGENT_DIR,
|
|
11
11
|
}));
|
|
12
12
|
|
|
@@ -75,6 +75,14 @@ function assertNoOutputEmoji(text: string): void {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function assertNoPartialHashlineAnchors(text: string): void {
|
|
79
|
+
for (const line of text.split(/\r?\n/)) {
|
|
80
|
+
if (/^\s*\d+\s*#[A-Za-z0-9_-]{2,32}:/.test(line)) {
|
|
81
|
+
assert.equal(line.endsWith("..."), false, `Anchor line was partially truncated: ${line}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
runTest("precision read with offset keeps exact output (no source/smart/hard truncation)", () => {
|
|
79
87
|
const config = cloneDefaultConfig();
|
|
80
88
|
setReadCompaction(config, true);
|
|
@@ -168,6 +176,203 @@ runTest("normal read compacts and adds banner when read compaction is enabled",
|
|
|
168
176
|
assert.ok(compacted.includes("source:minimal"));
|
|
169
177
|
});
|
|
170
178
|
|
|
179
|
+
runTest("line-anchor read output compacts without corrupting LINE#HASH anchors", () => {
|
|
180
|
+
const config = cloneDefaultConfig();
|
|
181
|
+
setReadCompaction(config, true);
|
|
182
|
+
config.outputCompaction.sourceCodeFilteringEnabled = true;
|
|
183
|
+
config.outputCompaction.sourceCodeFiltering = "minimal";
|
|
184
|
+
config.outputCompaction.smartTruncate.enabled = true;
|
|
185
|
+
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
186
|
+
config.outputCompaction.truncate.enabled = true;
|
|
187
|
+
config.outputCompaction.truncate.maxChars = 5000;
|
|
188
|
+
|
|
189
|
+
const content = Array.from({ length: 120 }, (_value, index) => {
|
|
190
|
+
const lineNumber = index + 1;
|
|
191
|
+
const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
|
|
192
|
+
return `${String(lineNumber).padStart(3, " ")}#ZP:${sourceLine}`;
|
|
193
|
+
}).join("\n");
|
|
194
|
+
const result = compactToolResult(
|
|
195
|
+
{
|
|
196
|
+
toolName: "read",
|
|
197
|
+
input: { path: "sample.ts" },
|
|
198
|
+
content: [{ type: "text", text: content }],
|
|
199
|
+
},
|
|
200
|
+
config,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
assert.equal(result.changed, true);
|
|
204
|
+
assert.ok(result.techniques.includes("source:minimal"));
|
|
205
|
+
|
|
206
|
+
const compacted = firstTextBlock(result.content);
|
|
207
|
+
assert.ok(compacted.startsWith("[RTK compacted output:"));
|
|
208
|
+
assert.ok(compacted.includes("source:minimal"));
|
|
209
|
+
assert.match(compacted, /\n\s*2#ZP:const value2 = 2;/);
|
|
210
|
+
assert.equal(compacted.includes("#ZP:// comment"), false);
|
|
211
|
+
assertNoPartialHashlineAnchors(compacted);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
runTest("colon-pipe anchor read output compacts without requiring hashline extension", () => {
|
|
215
|
+
const config = cloneDefaultConfig();
|
|
216
|
+
setReadCompaction(config, true);
|
|
217
|
+
config.outputCompaction.sourceCodeFilteringEnabled = true;
|
|
218
|
+
config.outputCompaction.sourceCodeFiltering = "minimal";
|
|
219
|
+
config.outputCompaction.smartTruncate.enabled = true;
|
|
220
|
+
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
221
|
+
config.outputCompaction.truncate.enabled = true;
|
|
222
|
+
config.outputCompaction.truncate.maxChars = 5000;
|
|
223
|
+
|
|
224
|
+
const content = [
|
|
225
|
+
"Read sample.ts: 120 lines",
|
|
226
|
+
"",
|
|
227
|
+
...Array.from({ length: 120 }, (_value, index) => {
|
|
228
|
+
const lineNumber = index + 1;
|
|
229
|
+
const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
|
|
230
|
+
return `${lineNumber}:${(lineNumber % 256).toString(16).padStart(2, "0")}|${sourceLine}`;
|
|
231
|
+
}),
|
|
232
|
+
].join("\n");
|
|
233
|
+
const result = compactToolResult(
|
|
234
|
+
{
|
|
235
|
+
toolName: "read",
|
|
236
|
+
input: { path: "sample.ts" },
|
|
237
|
+
content: [{ type: "text", text: content }],
|
|
238
|
+
},
|
|
239
|
+
config,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
assert.equal(result.changed, true);
|
|
243
|
+
assert.ok(result.techniques.includes("source:minimal"));
|
|
244
|
+
|
|
245
|
+
const compacted = firstTextBlock(result.content);
|
|
246
|
+
assert.ok(compacted.includes("Read sample.ts: 120 lines"));
|
|
247
|
+
assert.match(compacted, /\n2:02\|const value2 = 2;/);
|
|
248
|
+
assert.equal(compacted.includes("|// comment"), false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
runTest("compact LINEHASH pipe anchors from oh-my-pi style reads", () => {
|
|
252
|
+
const config = cloneDefaultConfig();
|
|
253
|
+
setReadCompaction(config, true);
|
|
254
|
+
config.outputCompaction.sourceCodeFilteringEnabled = true;
|
|
255
|
+
config.outputCompaction.sourceCodeFiltering = "minimal";
|
|
256
|
+
config.outputCompaction.smartTruncate.enabled = true;
|
|
257
|
+
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
258
|
+
config.outputCompaction.truncate.enabled = true;
|
|
259
|
+
config.outputCompaction.truncate.maxChars = 5000;
|
|
260
|
+
|
|
261
|
+
const content = Array.from({ length: 120 }, (_value, index) => {
|
|
262
|
+
const lineNumber = index + 1;
|
|
263
|
+
const hash = lineNumber % 2 === 0 ? "sr" : "ab";
|
|
264
|
+
const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
|
|
265
|
+
return `${lineNumber}${hash}|${sourceLine}`;
|
|
266
|
+
}).join("\n");
|
|
267
|
+
const result = compactToolResult(
|
|
268
|
+
{
|
|
269
|
+
toolName: "read",
|
|
270
|
+
input: { path: "sample.ts" },
|
|
271
|
+
content: [{ type: "text", text: content }],
|
|
272
|
+
},
|
|
273
|
+
config,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
assert.equal(result.changed, true);
|
|
277
|
+
assert.ok(result.techniques.includes("source:minimal"));
|
|
278
|
+
|
|
279
|
+
const compacted = firstTextBlock(result.content);
|
|
280
|
+
assert.match(compacted, /\n2sr\|const value2 = 2;/);
|
|
281
|
+
assert.equal(compacted.includes("|// comment"), false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
runTest("compact hashline-tools file wrapper while preserving non-anchor wrapper lines", () => {
|
|
285
|
+
const config = cloneDefaultConfig();
|
|
286
|
+
setReadCompaction(config, true);
|
|
287
|
+
config.outputCompaction.sourceCodeFilteringEnabled = true;
|
|
288
|
+
config.outputCompaction.sourceCodeFiltering = "minimal";
|
|
289
|
+
config.outputCompaction.smartTruncate.enabled = true;
|
|
290
|
+
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
291
|
+
config.outputCompaction.truncate.enabled = true;
|
|
292
|
+
config.outputCompaction.truncate.maxChars = 5000;
|
|
293
|
+
|
|
294
|
+
const content = [
|
|
295
|
+
"<file>",
|
|
296
|
+
...Array.from({ length: 120 }, (_value, index) => {
|
|
297
|
+
const lineNumber = index + 1;
|
|
298
|
+
const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
|
|
299
|
+
return `${lineNumber}#ZM:${sourceLine}`;
|
|
300
|
+
}),
|
|
301
|
+
"",
|
|
302
|
+
"(End of file - 120 total lines)",
|
|
303
|
+
"</file>",
|
|
304
|
+
].join("\n");
|
|
305
|
+
const result = compactToolResult(
|
|
306
|
+
{
|
|
307
|
+
toolName: "read",
|
|
308
|
+
input: { path: "sample.ts" },
|
|
309
|
+
content: [{ type: "text", text: content }],
|
|
310
|
+
},
|
|
311
|
+
config,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
assert.equal(result.changed, true);
|
|
315
|
+
assert.ok(result.techniques.includes("source:minimal"));
|
|
316
|
+
|
|
317
|
+
const compacted = firstTextBlock(result.content);
|
|
318
|
+
assert.ok(compacted.includes("<file>"));
|
|
319
|
+
assert.ok(compacted.includes("(End of file - 120 total lines)"));
|
|
320
|
+
assert.ok(compacted.includes("</file>"));
|
|
321
|
+
assert.match(compacted, /\n2#ZM:const value2 = 2;/);
|
|
322
|
+
assert.equal(compacted.includes("#ZM:// comment"), false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
runTest("anchor-safe read hard truncation preserves whole hashline anchors", () => {
|
|
326
|
+
const config = cloneDefaultConfig();
|
|
327
|
+
setReadCompaction(config, true);
|
|
328
|
+
config.outputCompaction.sourceCodeFilteringEnabled = false;
|
|
329
|
+
config.outputCompaction.smartTruncate.enabled = false;
|
|
330
|
+
config.outputCompaction.truncate.enabled = true;
|
|
331
|
+
config.outputCompaction.truncate.maxChars = 350;
|
|
332
|
+
|
|
333
|
+
const content = Array.from({ length: 120 }, (_value, index) => {
|
|
334
|
+
const lineNumber = index + 1;
|
|
335
|
+
return `${lineNumber}#ZP:const value${lineNumber} = "${"x".repeat(40)}";`;
|
|
336
|
+
}).join("\n");
|
|
337
|
+
const result = compactToolResult(
|
|
338
|
+
{
|
|
339
|
+
toolName: "read",
|
|
340
|
+
input: { path: "sample.ts" },
|
|
341
|
+
content: [{ type: "text", text: content }],
|
|
342
|
+
},
|
|
343
|
+
config,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
assert.equal(result.changed, true);
|
|
347
|
+
assert.ok(result.techniques.includes("truncate"));
|
|
348
|
+
|
|
349
|
+
const compacted = firstTextBlock(result.content);
|
|
350
|
+
assert.ok(compacted.includes("anchor-safe truncate"));
|
|
351
|
+
assertNoPartialHashlineAnchors(compacted);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
runTest("incidental single anchor-like line does not disable normal read compaction", () => {
|
|
355
|
+
const config = cloneDefaultConfig();
|
|
356
|
+
setReadCompaction(config, true);
|
|
357
|
+
config.outputCompaction.sourceCodeFilteringEnabled = true;
|
|
358
|
+
config.outputCompaction.sourceCodeFiltering = "minimal";
|
|
359
|
+
config.outputCompaction.smartTruncate.enabled = true;
|
|
360
|
+
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
361
|
+
|
|
362
|
+
const content = [`1#ZP:not an anchored read`, buildReadContent(120)].join("\n");
|
|
363
|
+
const result = compactToolResult(
|
|
364
|
+
{
|
|
365
|
+
toolName: "read",
|
|
366
|
+
input: { path: "sample.ts" },
|
|
367
|
+
content: [{ type: "text", text: content }],
|
|
368
|
+
},
|
|
369
|
+
config,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
assert.equal(result.changed, true);
|
|
373
|
+
assert.ok(result.techniques.includes("source:minimal") || result.techniques.includes("smart-truncate"));
|
|
374
|
+
});
|
|
375
|
+
|
|
171
376
|
runTest("short read output stays exact below threshold", () => {
|
|
172
377
|
const config = cloneDefaultConfig();
|
|
173
378
|
setReadCompaction(config, true);
|
|
@@ -435,7 +640,7 @@ runTest("rtk grep-style output sanitizes emoji file markers", () => {
|
|
|
435
640
|
runTest("rtk git diff verbose summary sanitizes file markers", () => {
|
|
436
641
|
const compacted = compactBashOutput(
|
|
437
642
|
"rtk git diff -- agent/extensions/pi-mcp-adapter/package.json",
|
|
438
|
-
"agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@
|
|
643
|
+
"agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@earendil-works/pi-coding-agent\": \"^0.58.1\",\n",
|
|
439
644
|
);
|
|
440
645
|
|
|
441
646
|
assert.ok(compacted.includes("--- Changes ---"));
|
|
@@ -446,7 +651,7 @@ runTest("rtk git diff verbose summary sanitizes file markers", () => {
|
|
|
446
651
|
runTest("git diff compaction skips already-compacted RTK-shaped output", () => {
|
|
447
652
|
const compacted = compactBashOutput(
|
|
448
653
|
"git diff -- agent/extensions/pi-mcp-adapter/package.json",
|
|
449
|
-
"agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@
|
|
654
|
+
"agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@earendil-works/pi-coding-agent\": \"^0.58.1\",\n",
|
|
450
655
|
);
|
|
451
656
|
|
|
452
657
|
assert.ok(compacted.includes("--- Changes ---"));
|
package/src/output-compactor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAgentDir } from "@
|
|
1
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import {
|
|
@@ -48,6 +48,24 @@ export interface ToolResultCompactionOutcome {
|
|
|
48
48
|
metadata?: ToolResultCompactionMetadata;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
interface AnchoredReadLine {
|
|
52
|
+
lineNumber: number;
|
|
53
|
+
content: string;
|
|
54
|
+
originalLine: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface AnchorSafeReadLine {
|
|
58
|
+
text: string;
|
|
59
|
+
content: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface AnchorSafeReadParts {
|
|
63
|
+
prefixLines: string[];
|
|
64
|
+
anchoredLines: AnchoredReadLine[];
|
|
65
|
+
suffixLines: string[];
|
|
66
|
+
trailingNewline: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
const LOSSY_TECHNIQUE_PREFIXES = [
|
|
52
70
|
"build",
|
|
53
71
|
"test",
|
|
@@ -61,6 +79,15 @@ const LOSSY_TECHNIQUE_PREFIXES = [
|
|
|
61
79
|
|
|
62
80
|
const READ_EXACT_OUTPUT_LINE_THRESHOLD = 80;
|
|
63
81
|
const READ_COMPACTION_BANNER_PREFIX = "[RTK compacted output:";
|
|
82
|
+
const ANCHORED_READ_LINE_MIN_MATCHES = 2;
|
|
83
|
+
const ANCHORED_READ_LINE_MIN_RATIO = 0.5;
|
|
84
|
+
const ANCHORED_READ_LINE_SAMPLE_LIMIT = 200;
|
|
85
|
+
const ANCHORED_READ_LINE_PATTERNS = [
|
|
86
|
+
/^\s*(?:>>>|>>|[>+\-*]+)?\s*(\d+)\s*#\s*[A-Za-z0-9_-]{2,32}:(.*)$/,
|
|
87
|
+
/^\s*(?:>>>|>>|[>+\-*]+)?\s*(\d+)\s*:\s*[A-Za-z0-9_-]{1,32}\|(.*)$/,
|
|
88
|
+
/^\s*(?:>>>|>>|[>+\-*]+)?\s*(\d+)[a-z]{2}\|(.*)$/,
|
|
89
|
+
] as const;
|
|
90
|
+
const ANCHORED_READ_INFORMATIONAL_LINE_PATTERN = /^\s*(?:$|<\/?file>|\.{3}|\[[^\]]+\]|Read\s+.+:\s+\d+\s+lines\b)/;
|
|
64
91
|
const USER_SKILL_ROOTS = [join(getAgentDir(), "skills"), join(homedir(), ".agents", "skills")];
|
|
65
92
|
|
|
66
93
|
function normalizePathForComparison(path: string): string {
|
|
@@ -135,6 +162,87 @@ function hasExplicitReadRange(input: Record<string, unknown>): boolean {
|
|
|
135
162
|
return input.offset !== undefined || input.limit !== undefined;
|
|
136
163
|
}
|
|
137
164
|
|
|
165
|
+
function splitReadLines(text: string): { lines: string[]; trailingNewline: boolean } {
|
|
166
|
+
if (!text) {
|
|
167
|
+
return { lines: [], trailingNewline: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const trailingNewline = text.endsWith("\n");
|
|
171
|
+
const lines = text.split(/\r?\n/);
|
|
172
|
+
if (trailingNewline) {
|
|
173
|
+
lines.pop();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { lines, trailingNewline };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function joinReadLines(lines: string[], trailingNewline: boolean): string {
|
|
180
|
+
const joined = lines.join("\n");
|
|
181
|
+
return trailingNewline && joined ? `${joined}\n` : joined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseAnchoredReadLine(line: string): AnchoredReadLine | undefined {
|
|
185
|
+
for (const pattern of ANCHORED_READ_LINE_PATTERNS) {
|
|
186
|
+
const match = line.match(pattern);
|
|
187
|
+
if (!match) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const lineNumber = Number.parseInt(match[1] ?? "", 10);
|
|
192
|
+
if (!Number.isSafeInteger(lineNumber) || lineNumber <= 0) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const content = match[2] ?? "";
|
|
197
|
+
return {
|
|
198
|
+
lineNumber,
|
|
199
|
+
content,
|
|
200
|
+
originalLine: line,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function parseAnchoredReadLineNumber(line: string): number | undefined {
|
|
208
|
+
return parseAnchoredReadLine(line)?.lineNumber;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function looksLikeAnchoredLineOutput(text: string, parseLineNumber: (line: string) => number | undefined): boolean {
|
|
212
|
+
let matchCount = 0;
|
|
213
|
+
let relevantLineCount = 0;
|
|
214
|
+
let previousMatchedLineNumber: number | undefined;
|
|
215
|
+
let hasIncreasingAnchors = false;
|
|
216
|
+
|
|
217
|
+
for (const line of splitReadLines(text).lines.slice(0, ANCHORED_READ_LINE_SAMPLE_LIMIT)) {
|
|
218
|
+
if (!ANCHORED_READ_INFORMATIONAL_LINE_PATTERN.test(line)) {
|
|
219
|
+
relevantLineCount += 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const lineNumber = parseLineNumber(line);
|
|
223
|
+
if (lineNumber === undefined) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
matchCount += 1;
|
|
228
|
+
if (previousMatchedLineNumber !== undefined && lineNumber > previousMatchedLineNumber) {
|
|
229
|
+
hasIncreasingAnchors = true;
|
|
230
|
+
}
|
|
231
|
+
previousMatchedLineNumber = lineNumber;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (matchCount < ANCHORED_READ_LINE_MIN_MATCHES || !hasIncreasingAnchors) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const ratioBase = Math.max(relevantLineCount, matchCount);
|
|
239
|
+
return matchCount / ratioBase >= ANCHORED_READ_LINE_MIN_RATIO;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function looksLikeAnchoredReadOutput(text: string): boolean {
|
|
243
|
+
return looksLikeAnchoredLineOutput(text, parseAnchoredReadLineNumber);
|
|
244
|
+
}
|
|
245
|
+
|
|
138
246
|
function shouldPreserveExactReadOutput(
|
|
139
247
|
text: string,
|
|
140
248
|
input: Record<string, unknown>,
|
|
@@ -165,6 +273,179 @@ function shouldApplyReadSourceFiltering(text: string, config: RtkIntegrationConf
|
|
|
165
273
|
);
|
|
166
274
|
}
|
|
167
275
|
|
|
276
|
+
function extractAnchoredReadParts(text: string): AnchorSafeReadParts | undefined {
|
|
277
|
+
if (!looksLikeAnchoredReadOutput(text)) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { lines, trailingNewline } = splitReadLines(text);
|
|
282
|
+
const parsedLines = lines.map((line) => parseAnchoredReadLine(line));
|
|
283
|
+
const firstAnchorIndex = parsedLines.findIndex((line) => line !== undefined);
|
|
284
|
+
if (firstAnchorIndex === -1) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let lastAnchorIndex = firstAnchorIndex;
|
|
289
|
+
for (let index = parsedLines.length - 1; index >= firstAnchorIndex; index -= 1) {
|
|
290
|
+
if (parsedLines[index] !== undefined) {
|
|
291
|
+
lastAnchorIndex = index;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const anchoredLines: AnchoredReadLine[] = [];
|
|
297
|
+
for (let index = firstAnchorIndex; index <= lastAnchorIndex; index += 1) {
|
|
298
|
+
const anchoredLine = parsedLines[index];
|
|
299
|
+
if (!anchoredLine) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
anchoredLines.push(anchoredLine);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
prefixLines: lines.slice(0, firstAnchorIndex),
|
|
307
|
+
anchoredLines,
|
|
308
|
+
suffixLines: lines.slice(lastAnchorIndex + 1),
|
|
309
|
+
trailingNewline,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function toAnchorSafeReadLines(anchoredLines: AnchoredReadLine[]): AnchorSafeReadLine[] {
|
|
314
|
+
return anchoredLines.map((line) => ({
|
|
315
|
+
text: line.originalLine,
|
|
316
|
+
content: line.content,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function renderAnchorSafeReadBody(lines: AnchorSafeReadLine[]): string {
|
|
321
|
+
return lines.map((line) => line.text).join("\n");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function renderAnchorSafeReadText(parts: AnchorSafeReadParts, lines: AnchorSafeReadLine[]): string {
|
|
325
|
+
return joinReadLines(
|
|
326
|
+
[...parts.prefixLines, ...lines.map((line) => line.text), ...parts.suffixLines],
|
|
327
|
+
parts.trailingNewline,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function remapTransformedContentToAnchorSafeLines(
|
|
332
|
+
sourceLines: AnchorSafeReadLine[],
|
|
333
|
+
transformedContent: string,
|
|
334
|
+
): AnchorSafeReadLine[] {
|
|
335
|
+
const transformedLines = splitReadLines(transformedContent).lines;
|
|
336
|
+
const remappedLines: AnchorSafeReadLine[] = [];
|
|
337
|
+
let searchStartIndex = 0;
|
|
338
|
+
|
|
339
|
+
for (const transformedLine of transformedLines) {
|
|
340
|
+
let matchedIndex = -1;
|
|
341
|
+
for (let index = searchStartIndex; index < sourceLines.length; index += 1) {
|
|
342
|
+
if (sourceLines[index]?.content === transformedLine) {
|
|
343
|
+
matchedIndex = index;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (matchedIndex === -1) {
|
|
349
|
+
remappedLines.push({
|
|
350
|
+
text: transformedLine,
|
|
351
|
+
content: transformedLine,
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
remappedLines.push(sourceLines[matchedIndex]!);
|
|
357
|
+
searchStartIndex = matchedIndex + 1;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return remappedLines;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function truncateAnchorSafeReadLines(lines: AnchorSafeReadLine[], maxChars: number): AnchorSafeReadLine[] {
|
|
364
|
+
if (renderAnchorSafeReadBody(lines).length <= maxChars) {
|
|
365
|
+
return lines;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const marker = "[RTK anchor-safe truncate: remaining anchored read lines omitted to preserve complete anchors]";
|
|
369
|
+
const truncatedLines: AnchorSafeReadLine[] = [];
|
|
370
|
+
let charCount = 0;
|
|
371
|
+
|
|
372
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
373
|
+
const line = lines[index]!;
|
|
374
|
+
const separatorLength = truncatedLines.length > 0 ? 1 : 0;
|
|
375
|
+
const nextCharCount = charCount + separatorLength + line.text.length;
|
|
376
|
+
const remainingAfter = lines.length - index - 1;
|
|
377
|
+
const markerLength = remainingAfter > 0 ? (nextCharCount > 0 ? 1 : 0) + marker.length : 0;
|
|
378
|
+
|
|
379
|
+
if (nextCharCount + markerLength > maxChars) {
|
|
380
|
+
const markerLine = { text: marker, content: marker };
|
|
381
|
+
return truncatedLines.length > 0 ? [...truncatedLines, markerLine] : [markerLine];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
truncatedLines.push(line);
|
|
385
|
+
charCount = nextCharCount;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return truncatedLines;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function compactAnchoredReadText(
|
|
392
|
+
text: string,
|
|
393
|
+
filePath: string,
|
|
394
|
+
config: RtkIntegrationConfig,
|
|
395
|
+
): { text: string; techniques: string[] } {
|
|
396
|
+
const parts = extractAnchoredReadParts(text);
|
|
397
|
+
if (!parts) {
|
|
398
|
+
return { text, techniques: [] };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let lines = toAnchorSafeReadLines(parts.anchoredLines);
|
|
402
|
+
const techniques: string[] = [];
|
|
403
|
+
const compaction = config.outputCompaction;
|
|
404
|
+
const language = detectLanguage(filePath);
|
|
405
|
+
|
|
406
|
+
if (
|
|
407
|
+
compaction.sourceCodeFilteringEnabled &&
|
|
408
|
+
compaction.sourceCodeFiltering !== "none" &&
|
|
409
|
+
shouldApplyReadSourceFiltering(text, config)
|
|
410
|
+
) {
|
|
411
|
+
const currentSource = lines.map((line) => line.content).join("\n");
|
|
412
|
+
const filtered = normalizeTechniqueResult(
|
|
413
|
+
filterSourceCode(currentSource, language, compaction.sourceCodeFiltering),
|
|
414
|
+
currentSource,
|
|
415
|
+
);
|
|
416
|
+
const filteredLines = remapTransformedContentToAnchorSafeLines(lines, filtered);
|
|
417
|
+
if (renderAnchorSafeReadBody(filteredLines) !== renderAnchorSafeReadBody(lines)) {
|
|
418
|
+
lines = filteredLines;
|
|
419
|
+
techniques.push(`source:${compaction.sourceCodeFiltering}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (compaction.smartTruncate.enabled && lines.length > compaction.smartTruncate.maxLines) {
|
|
424
|
+
const currentSource = lines.map((line) => line.content).join("\n");
|
|
425
|
+
const compacted = smartTruncate(currentSource, compaction.smartTruncate.maxLines, language);
|
|
426
|
+
const compactedLines = remapTransformedContentToAnchorSafeLines(lines, compacted);
|
|
427
|
+
if (renderAnchorSafeReadBody(compactedLines) !== renderAnchorSafeReadBody(lines)) {
|
|
428
|
+
lines = compactedLines;
|
|
429
|
+
techniques.push("smart-truncate");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (compaction.truncate.enabled && renderAnchorSafeReadText(parts, lines).length > compaction.truncate.maxChars) {
|
|
434
|
+
const nonBodyOverhead = renderAnchorSafeReadText(parts, []).length;
|
|
435
|
+
const bodyMaxChars = Math.max(1, compaction.truncate.maxChars - nonBodyOverhead);
|
|
436
|
+
const truncatedLines = truncateAnchorSafeReadLines(lines, bodyMaxChars);
|
|
437
|
+
if (renderAnchorSafeReadBody(truncatedLines) !== renderAnchorSafeReadBody(lines)) {
|
|
438
|
+
lines = truncatedLines;
|
|
439
|
+
techniques.push("truncate");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
text: renderAnchorSafeReadText(parts, lines),
|
|
445
|
+
techniques,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
168
449
|
function formatReadCompactionBanner(techniques: string[]): string {
|
|
169
450
|
return `${READ_COMPACTION_BANNER_PREFIX} ${techniques.join(", ")}]`;
|
|
170
451
|
}
|
|
@@ -190,6 +471,10 @@ function hasLossyCompaction(techniques: string[]): boolean {
|
|
|
190
471
|
);
|
|
191
472
|
}
|
|
192
473
|
|
|
474
|
+
function normalizeTechniqueResult(result: string | null, currentText: string): string {
|
|
475
|
+
return result === null ? currentText : result;
|
|
476
|
+
}
|
|
477
|
+
|
|
193
478
|
function compactBashText(
|
|
194
479
|
text: string,
|
|
195
480
|
command: string | undefined,
|
|
@@ -207,45 +492,45 @@ function compactBashText(
|
|
|
207
492
|
}
|
|
208
493
|
}
|
|
209
494
|
|
|
210
|
-
const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
|
|
211
|
-
if (withoutRtkHookWarnings !==
|
|
495
|
+
const withoutRtkHookWarnings = normalizeTechniqueResult(stripRtkHookWarnings(nextText, command), nextText);
|
|
496
|
+
if (withoutRtkHookWarnings !== nextText) {
|
|
212
497
|
nextText = withoutRtkHookWarnings;
|
|
213
498
|
techniques.push("rtk-hook-warning");
|
|
214
499
|
}
|
|
215
500
|
|
|
216
|
-
const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
|
|
217
|
-
if (withoutRtkEmoji !==
|
|
501
|
+
const withoutRtkEmoji = normalizeTechniqueResult(sanitizeRtkEmojiOutput(nextText, command), nextText);
|
|
502
|
+
if (withoutRtkEmoji !== nextText) {
|
|
218
503
|
nextText = withoutRtkEmoji;
|
|
219
504
|
techniques.push("rtk-emoji");
|
|
220
505
|
}
|
|
221
506
|
|
|
222
507
|
if (compaction.filterBuildOutput) {
|
|
223
|
-
const compacted = filterBuildOutput(nextText, command);
|
|
224
|
-
if (compacted !==
|
|
508
|
+
const compacted = normalizeTechniqueResult(filterBuildOutput(nextText, command), nextText);
|
|
509
|
+
if (compacted !== nextText) {
|
|
225
510
|
nextText = compacted;
|
|
226
511
|
techniques.push("build");
|
|
227
512
|
}
|
|
228
513
|
}
|
|
229
514
|
|
|
230
515
|
if (compaction.aggregateTestOutput) {
|
|
231
|
-
const compacted = aggregateTestOutput(nextText, command);
|
|
232
|
-
if (compacted !==
|
|
516
|
+
const compacted = normalizeTechniqueResult(aggregateTestOutput(nextText, command), nextText);
|
|
517
|
+
if (compacted !== nextText) {
|
|
233
518
|
nextText = compacted;
|
|
234
519
|
techniques.push("test");
|
|
235
520
|
}
|
|
236
521
|
}
|
|
237
522
|
|
|
238
523
|
if (compaction.compactGitOutput) {
|
|
239
|
-
const compacted = compactGitOutput(nextText, command);
|
|
240
|
-
if (compacted !==
|
|
524
|
+
const compacted = normalizeTechniqueResult(compactGitOutput(nextText, command), nextText);
|
|
525
|
+
if (compacted !== nextText) {
|
|
241
526
|
nextText = compacted;
|
|
242
527
|
techniques.push("git");
|
|
243
528
|
}
|
|
244
529
|
}
|
|
245
530
|
|
|
246
531
|
if (compaction.aggregateLinterOutput) {
|
|
247
|
-
const compacted = aggregateLinterOutput(nextText, command);
|
|
248
|
-
if (compacted !==
|
|
532
|
+
const compacted = normalizeTechniqueResult(aggregateLinterOutput(nextText, command), nextText);
|
|
533
|
+
if (compacted !== nextText) {
|
|
249
534
|
nextText = compacted;
|
|
250
535
|
techniques.push("linter");
|
|
251
536
|
}
|
|
@@ -281,6 +566,18 @@ function compactReadText(
|
|
|
281
566
|
}
|
|
282
567
|
}
|
|
283
568
|
|
|
569
|
+
if (looksLikeAnchoredReadOutput(nextText)) {
|
|
570
|
+
const anchored = compactAnchoredReadText(nextText, filePath, config);
|
|
571
|
+
nextText = anchored.text;
|
|
572
|
+
techniques.push(...anchored.techniques);
|
|
573
|
+
|
|
574
|
+
if (techniques.length > 0 && !nextText.startsWith(READ_COMPACTION_BANNER_PREFIX)) {
|
|
575
|
+
nextText = `${formatReadCompactionBanner(techniques)}\n${nextText}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return { text: nextText, techniques };
|
|
579
|
+
}
|
|
580
|
+
|
|
284
581
|
const language = detectLanguage(filePath);
|
|
285
582
|
// Only apply lossy source filtering when a downstream line/char safeguard would otherwise trigger.
|
|
286
583
|
if (
|
|
@@ -288,7 +585,10 @@ function compactReadText(
|
|
|
288
585
|
compaction.sourceCodeFiltering !== "none" &&
|
|
289
586
|
shouldApplyReadSourceFiltering(text, config)
|
|
290
587
|
) {
|
|
291
|
-
const filtered =
|
|
588
|
+
const filtered = normalizeTechniqueResult(
|
|
589
|
+
filterSourceCode(nextText, language, compaction.sourceCodeFiltering),
|
|
590
|
+
nextText,
|
|
591
|
+
);
|
|
292
592
|
if (filtered !== nextText) {
|
|
293
593
|
nextText = filtered;
|
|
294
594
|
techniques.push(`source:${compaction.sourceCodeFiltering}`);
|
|
@@ -332,8 +632,8 @@ function compactGrepText(text: string, config: RtkIntegrationConfig): { text: st
|
|
|
332
632
|
}
|
|
333
633
|
|
|
334
634
|
if (compaction.groupSearchOutput) {
|
|
335
|
-
const grouped = groupSearchResults(nextText);
|
|
336
|
-
if (grouped !==
|
|
635
|
+
const grouped = normalizeTechniqueResult(groupSearchResults(nextText), nextText);
|
|
636
|
+
if (grouped !== nextText) {
|
|
337
637
|
nextText = grouped;
|
|
338
638
|
techniques.push("search");
|
|
339
639
|
}
|