fix(hashline-edit): improve error messages for invalid LINE#ID references

- Detect non-numeric prefixes (e.g., "LINE#HK", "POS#VK") and explain
  that the prefix must be an actual line number, not literal text
- Add suggestLineForHash() that reverse-looks up a hash in file lines
  to suggest the correct reference (e.g., Did you mean "1#HK"?)
- Unify error message format from "LINE#ID" to "{line_number}#{hash_id}"
  matching the tool description convention
- Add 3 tests covering non-numeric prefix detection and hash suggestion
This commit is contained in:
minpeter
2026-02-24 15:35:10 +09:00
parent 5fd65f2935
commit 2aeb96c3f6
2 changed files with 69 additions and 5 deletions

View File

@@ -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}`))
})
})

View File

@@ -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)`)