refactor: share harness truncation result helpers

This commit is contained in:
Vincent Koc
2026-05-30 04:35:41 +02:00
parent 9f30af5a96
commit e9dee8dfe1

View File

@@ -44,6 +44,14 @@ export interface TruncationOptions {
maxBytes?: number;
}
interface ResolvedTruncationInput {
lines: string[];
totalLines: number;
totalBytes: number;
maxLines: number;
maxBytes: number;
}
interface RuntimeBuffer {
byteLength(content: string, encoding: "utf8"): number;
}
@@ -137,6 +145,50 @@ export function formatSize(bytes: number): string {
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
function resolveTruncationInput(
content: string,
options: TruncationOptions,
): ResolvedTruncationInput {
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
const totalBytes = utf8ByteLength(content);
const lines = splitLinesForCounting(content);
return {
lines,
totalLines: lines.length,
totalBytes,
maxLines,
maxBytes,
};
}
function buildTruncationResult(
input: ResolvedTruncationInput,
params: {
content: string;
truncated: boolean;
truncatedBy: TruncationResult["truncatedBy"];
outputLines: number;
outputBytes?: number;
lastLinePartial?: boolean;
firstLineExceedsLimit?: boolean;
},
): TruncationResult {
return {
content: params.content,
truncated: params.truncated,
truncatedBy: params.truncatedBy,
totalLines: input.totalLines,
totalBytes: input.totalBytes,
outputLines: params.outputLines,
outputBytes: params.outputBytes ?? utf8ByteLength(params.content),
lastLinePartial: params.lastLinePartial ?? false,
firstLineExceedsLimit: params.firstLineExceedsLimit ?? false,
maxLines: input.maxLines,
maxBytes: input.maxBytes,
};
}
/**
* Truncate content from the head (keep first N lines/bytes).
* Suitable for file reads where you want to see the beginning.
@@ -145,58 +197,39 @@ export function formatSize(bytes: number): string {
* returns empty content with firstLineExceedsLimit=true.
*/
export function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
const input = resolveTruncationInput(content, options);
const totalBytes = utf8ByteLength(content);
const lines = splitLinesForCounting(content);
const totalLines = lines.length;
// Check if no truncation needed
if (totalLines <= maxLines && totalBytes <= maxBytes) {
return {
if (input.totalLines <= input.maxLines && input.totalBytes <= input.maxBytes) {
return buildTruncationResult(input, {
content,
truncated: false,
truncatedBy: null,
totalLines,
totalBytes,
outputLines: totalLines,
outputBytes: totalBytes,
lastLinePartial: false,
firstLineExceedsLimit: false,
maxLines,
maxBytes,
};
outputLines: input.totalLines,
outputBytes: input.totalBytes,
});
}
// Check if first line alone exceeds byte limit
const firstLineBytes = utf8ByteLength(lines[0]);
if (firstLineBytes > maxBytes) {
return {
const firstLineBytes = utf8ByteLength(input.lines[0]);
if (firstLineBytes > input.maxBytes) {
return buildTruncationResult(input, {
content: "",
truncated: true,
truncatedBy: "bytes",
totalLines,
totalBytes,
outputLines: 0,
outputBytes: 0,
lastLinePartial: false,
firstLineExceedsLimit: true,
maxLines,
maxBytes,
};
});
}
// Collect complete lines that fit
const outputLinesArr: string[] = [];
let outputBytesCount = 0;
let truncatedBy: "lines" | "bytes" = totalLines > maxLines ? "lines" : "bytes";
let truncatedBy: "lines" | "bytes" = input.totalLines > input.maxLines ? "lines" : "bytes";
for (let i = 0; i < lines.length && i < maxLines; i++) {
const line = lines[i];
for (let i = 0; i < input.lines.length && i < input.maxLines; i++) {
const line = input.lines[i];
const lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline
if (outputBytesCount + lineBytes > maxBytes) {
if (outputBytesCount + lineBytes > input.maxBytes) {
truncatedBy = "bytes";
break;
}
@@ -205,27 +238,22 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
outputBytesCount += lineBytes;
}
// If we exited due to line limit
if (totalLines > maxLines && outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
if (
input.totalLines > input.maxLines &&
outputLinesArr.length >= input.maxLines &&
outputBytesCount <= input.maxBytes
) {
truncatedBy = "lines";
}
const outputContent = outputLinesArr.join("\n");
const finalOutputBytes = utf8ByteLength(outputContent);
return {
return buildTruncationResult(input, {
content: outputContent,
truncated: true,
truncatedBy,
totalLines,
totalBytes,
outputLines: outputLinesArr.length,
outputBytes: finalOutputBytes,
lastLinePartial: false,
firstLineExceedsLimit: false,
maxLines,
maxBytes,
};
});
}
/**
@@ -235,46 +263,33 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
* May return partial first line if the last line of original content exceeds byte limit.
*/
export function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
const input = resolveTruncationInput(content, options);
const totalBytes = utf8ByteLength(content);
const lines = splitLinesForCounting(content);
const totalLines = lines.length;
// Check if no truncation needed
if (totalLines <= maxLines && totalBytes <= maxBytes) {
return {
if (input.totalLines <= input.maxLines && input.totalBytes <= input.maxBytes) {
return buildTruncationResult(input, {
content,
truncated: false,
truncatedBy: null,
totalLines,
totalBytes,
outputLines: totalLines,
outputBytes: totalBytes,
lastLinePartial: false,
firstLineExceedsLimit: false,
maxLines,
maxBytes,
};
outputLines: input.totalLines,
outputBytes: input.totalBytes,
});
}
// Work backwards from the end
const outputLinesArr: string[] = [];
let outputBytesCount = 0;
let truncatedBy: "lines" | "bytes" = totalLines > maxLines ? "lines" : "bytes";
let truncatedBy: "lines" | "bytes" = input.totalLines > input.maxLines ? "lines" : "bytes";
let lastLinePartial = false;
for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
const line = lines[i];
for (let i = input.lines.length - 1; i >= 0 && outputLinesArr.length < input.maxLines; i--) {
const line = input.lines[i];
const lineBytes = utf8ByteLength(line) + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
if (outputBytesCount + lineBytes > maxBytes) {
if (outputBytesCount + lineBytes > input.maxBytes) {
truncatedBy = "bytes";
// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
// take the end of the line (partial)
if (outputLinesArr.length === 0) {
const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
const truncatedLine = truncateStringToBytesFromEnd(line, input.maxBytes);
outputLinesArr.unshift(truncatedLine);
outputBytesCount = utf8ByteLength(truncatedLine);
lastLinePartial = true;
@@ -286,27 +301,23 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
outputBytesCount += lineBytes;
}
// If we exited due to line limit
if (totalLines > maxLines && outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
if (
input.totalLines > input.maxLines &&
outputLinesArr.length >= input.maxLines &&
outputBytesCount <= input.maxBytes
) {
truncatedBy = "lines";
}
const outputContent = outputLinesArr.join("\n");
const finalOutputBytes = utf8ByteLength(outputContent);
return {
return buildTruncationResult(input, {
content: outputContent,
truncated: true,
truncatedBy,
totalLines,
totalBytes,
outputLines: outputLinesArr.length,
outputBytes: finalOutputBytes,
lastLinePartial,
firstLineExceedsLimit: false,
maxLines,
maxBytes,
};
});
}
/**