fix(hashline-edit): address Cubic review issues - boundary echo, chunking dedup, empty stream alignment

- Fix single-line anchor-echo stripping to trigger empty-insert validation

- Fix trailing boundary-echo stripping for boundary-only payloads

- Extract shared chunking logic to hashline-chunk-formatter

- Align empty stream/iterable handling with formatHashLines

- Add regression tests for all fixes
This commit is contained in:
YeonGyu-Kim
2026-02-22 03:54:31 +09:00
parent 22b4f465ab
commit 7e68690c70
5 changed files with 112 additions and 84 deletions
+10 -81
View File
@@ -1,4 +1,5 @@
import { HASHLINE_DICT } from "./constants"
import { createHashlineChunkFormatter } from "./hashline-chunk-formatter"
export function computeLineHash(lineNumber: number, content: string): string {
const stripped = content.replace(/\s+/g, "")
@@ -61,42 +62,12 @@ export async function* streamHashLinesFromUtf8(
let pending = ""
let sawAnyText = false
let endedWithNewline = false
let outputLines: string[] = []
let outputBytes = 0
const flush = (): string | undefined => {
if (outputLines.length === 0) return undefined
const chunk = outputLines.join("\n")
outputLines = []
outputBytes = 0
return chunk
}
const chunkFormatter = createHashlineChunkFormatter({ maxChunkLines, maxChunkBytes })
const pushLine = (line: string): string[] => {
const formatted = `${lineNumber}#${computeLineHash(lineNumber, line)}:${line}`
const formatted = formatHashLine(lineNumber, line)
lineNumber += 1
const chunksToYield: string[] = []
const separatorBytes = outputLines.length === 0 ? 0 : 1
const lineBytes = Buffer.byteLength(formatted, "utf-8")
if (
outputLines.length > 0 &&
(outputLines.length >= maxChunkLines || outputBytes + separatorBytes + lineBytes > maxChunkBytes)
) {
const flushed = flush()
if (flushed) chunksToYield.push(flushed)
}
outputLines.push(formatted)
outputBytes += (outputLines.length === 1 ? 0 : 1) + lineBytes
if (outputLines.length >= maxChunkLines || outputBytes >= maxChunkBytes) {
const flushed = flush()
if (flushed) chunksToYield.push(flushed)
}
return chunksToYield
return chunkFormatter.push(formatted)
}
const consumeText = (text: string): string[] => {
@@ -128,17 +99,13 @@ export async function* streamHashLinesFromUtf8(
yield out
}
if (!sawAnyText) {
for (const out of pushLine("")) {
yield out
}
} else if (pending.length > 0 || endedWithNewline) {
if (sawAnyText && (pending.length > 0 || endedWithNewline)) {
for (const out of pushLine(pending)) {
yield out
}
}
const finalChunk = flush()
const finalChunk = chunkFormatter.flush()
if (finalChunk) yield finalChunk
}
@@ -151,44 +118,12 @@ export async function* streamHashLinesFromLines(
const maxChunkBytes = options.maxChunkBytes ?? 64 * 1024
let lineNumber = startLine
let outputLines: string[] = []
let outputBytes = 0
let sawAnyLine = false
const flush = (): string | undefined => {
if (outputLines.length === 0) return undefined
const chunk = outputLines.join("\n")
outputLines = []
outputBytes = 0
return chunk
}
const chunkFormatter = createHashlineChunkFormatter({ maxChunkLines, maxChunkBytes })
const pushLine = (line: string): string[] => {
sawAnyLine = true
const formatted = `${lineNumber}#${computeLineHash(lineNumber, line)}:${line}`
const formatted = formatHashLine(lineNumber, line)
lineNumber += 1
const chunksToYield: string[] = []
const separatorBytes = outputLines.length === 0 ? 0 : 1
const lineBytes = Buffer.byteLength(formatted, "utf-8")
if (
outputLines.length > 0 &&
(outputLines.length >= maxChunkLines || outputBytes + separatorBytes + lineBytes > maxChunkBytes)
) {
const flushed = flush()
if (flushed) chunksToYield.push(flushed)
}
outputLines.push(formatted)
outputBytes += (outputLines.length === 1 ? 0 : 1) + lineBytes
if (outputLines.length >= maxChunkLines || outputBytes >= maxChunkBytes) {
const flushed = flush()
if (flushed) chunksToYield.push(flushed)
}
return chunksToYield
return chunkFormatter.push(formatted)
}
const asyncIterator = (lines as AsyncIterable<string>)[Symbol.asyncIterator]
@@ -202,12 +137,6 @@ export async function* streamHashLinesFromLines(
}
}
if (!sawAnyLine) {
for (const out of pushLine("")) {
yield out
}
}
const finalChunk = flush()
const finalChunk = chunkFormatter.flush()
if (finalChunk) yield finalChunk
}