diff --git a/src/tools/hashline-edit/validation.test.ts b/src/tools/hashline-edit/validation.test.ts index cf2457ab..4d4c1b00 100644 --- a/src/tools/hashline-edit/validation.test.ts +++ b/src/tools/hashline-edit/validation.test.ts @@ -19,7 +19,23 @@ describe("parseLineRef", () => { const ref = "42:VK" //#when / #then - expect(() => parseLineRef(ref)).toThrow("LINE#ID") + expect(() => parseLineRef(ref)).toThrow("{line_number}#{hash_id}") + }) + + it("gives specific hint when literal text is used instead of line number", () => { + //#given — model sends "LINE#HK" instead of "1#HK" + const ref = "LINE#HK" + + //#when / #then — error should mention that LINE is not a valid number + expect(() => parseLineRef(ref)).toThrow(/not a line number/i) + }) + + it("gives specific hint for other non-numeric prefixes like POS#VK", () => { + //#given + const ref = "POS#VK" + + //#when / #then + expect(() => parseLineRef(ref)).toThrow(/not a line number/i) }) it("accepts refs copied with markers and trailing content", () => { @@ -60,4 +76,13 @@ describe("validateLineRef", () => { expect(() => validateLineRefs(lines, ["2#ZZ"])) .toThrow(/>>>\s+2#[ZPMQVRWSNKTXJBYH]{2}\|two/) }) + + it("suggests correct line number when hash matches a file line", () => { + //#given — model sends LINE#XX where XX is the actual hash for line 1 + const lines = ["function hello() {", " return 42", "}"] + const hash = computeLineHash(1, lines[0]) + + //#when / #then — error should suggest the correct reference + expect(() => validateLineRefs(lines, [`LINE#${hash}`])).toThrow(new RegExp(`1#${hash}`)) + }) }) diff --git a/src/tools/hashline-edit/validation.ts b/src/tools/hashline-edit/validation.ts index 72a33286..89c5d6eb 100644 --- a/src/tools/hashline-edit/validation.ts +++ b/src/tools/hashline-edit/validation.ts @@ -38,13 +38,30 @@ export function parseLineRef(ref: string): LineRef { hash: match[2], } } + const nonNumericMatch = ref.trim().match(/^([A-Za-z_]+)#([ZPMQVRWSNKTXJBYH]{2})$/) + if (nonNumericMatch) { + throw new Error( + `Invalid line reference: "${ref}". "${nonNumericMatch[1]}" is not a line number. ` + + `Use the actual line number from the read output.` + ) + } throw new Error( - `Invalid line reference format: "${ref}". Expected format: "LINE#ID" (e.g., "42#VK")` + `Invalid line reference format: "${ref}". Expected format: "{line_number}#{hash_id}"` ) } export function validateLineRef(lines: string[], ref: string): void { - const { line, hash } = parseLineRef(ref) + let parsed: LineRef + try { + parsed = parseLineRef(ref) + } catch (parseError) { + const hint = suggestLineForHash(ref, lines) + if (hint && parseError instanceof Error) { + throw new Error(`${parseError.message} ${hint}`) + } + throw parseError + } + const { line, hash } = parsed if (line < 1 || line > lines.length) { throw new Error( @@ -92,7 +109,7 @@ export class HashlineMismatchError extends Error { const output: string[] = [] output.push( `${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since last read. ` + - "Use updated LINE#ID references below (>>> marks changed lines)." + "Use updated {line_number}#{hash_id} references below (>>> marks changed lines)." ) output.push("") @@ -117,11 +134,33 @@ export class HashlineMismatchError extends Error { } } +function suggestLineForHash(ref: string, lines: string[]): string | null { + const hashMatch = ref.trim().match(/#([ZPMQVRWSNKTXJBYH]{2})$/) + if (!hashMatch) return null + const hash = hashMatch[1] + for (let i = 0; i < lines.length; i++) { + if (computeLineHash(i + 1, lines[i]) === hash) { + return `Did you mean "${i + 1}#${hash}"?` + } + } + return null +} + export function validateLineRefs(lines: string[], refs: string[]): void { const mismatches: HashMismatch[] = [] for (const ref of refs) { - const { line, hash } = parseLineRef(ref) + let parsed: LineRef + try { + parsed = parseLineRef(ref) + } catch (parseError) { + const hint = suggestLineForHash(ref, lines) + if (hint && parseError instanceof Error) { + throw new Error(`${parseError.message} ${hint}`) + } + throw parseError + } + const { line, hash } = parsed if (line < 1 || line > lines.length) { throw new Error(`Line number ${line} out of bounds (file has ${lines.length} lines)`)