fix(hashline): accept legacy hashes for indented anchors
Keep persisted LINE#ID anchors working after strict whitespace hashing by falling back to the legacy hash for validation-only lookups. Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect } from "bun:test"
|
import { describe, it, expect } from "bun:test"
|
||||||
import {
|
import {
|
||||||
computeLineHash,
|
computeLineHash,
|
||||||
|
computeLegacyLineHash,
|
||||||
formatHashLine,
|
formatHashLine,
|
||||||
formatHashLines,
|
formatHashLines,
|
||||||
streamHashLinesFromLines,
|
streamHashLinesFromLines,
|
||||||
@@ -58,6 +59,19 @@ describe("computeLineHash", () => {
|
|||||||
expect(hash1).not.toBe(hash2)
|
expect(hash1).not.toBe(hash2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("preserves legacy hashes for leading indentation variants", () => {
|
||||||
|
//#given
|
||||||
|
const content1 = "function hello() {"
|
||||||
|
const content2 = " function hello() {"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const hash1 = computeLegacyLineHash(1, content1)
|
||||||
|
const hash2 = computeLegacyLineHash(1, content2)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(hash1).toBe(hash2)
|
||||||
|
})
|
||||||
|
|
||||||
it("ignores trailing whitespace differences", () => {
|
it("ignores trailing whitespace differences", () => {
|
||||||
//#given
|
//#given
|
||||||
const content1 = "function hello() {"
|
const content1 = "function hello() {"
|
||||||
|
|||||||
@@ -3,14 +3,22 @@ import { createHashlineChunkFormatter } from "./hashline-chunk-formatter"
|
|||||||
|
|
||||||
const RE_SIGNIFICANT = /[\p{L}\p{N}]/u
|
const RE_SIGNIFICANT = /[\p{L}\p{N}]/u
|
||||||
|
|
||||||
export function computeLineHash(lineNumber: number, content: string): string {
|
function computeNormalizedLineHash(lineNumber: number, normalizedContent: string): string {
|
||||||
const stripped = content.replace(/\r/g, "").trimEnd()
|
const stripped = normalizedContent
|
||||||
const seed = RE_SIGNIFICANT.test(stripped) ? 0 : lineNumber
|
const seed = RE_SIGNIFICANT.test(stripped) ? 0 : lineNumber
|
||||||
const hash = Bun.hash.xxHash32(stripped, seed)
|
const hash = Bun.hash.xxHash32(stripped, seed)
|
||||||
const index = hash % 256
|
const index = hash % 256
|
||||||
return HASHLINE_DICT[index]
|
return HASHLINE_DICT[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function computeLineHash(lineNumber: number, content: string): string {
|
||||||
|
return computeNormalizedLineHash(lineNumber, content.replace(/\r/g, "").trimEnd())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeLegacyLineHash(lineNumber: number, content: string): string {
|
||||||
|
return computeNormalizedLineHash(lineNumber, content.replace(/\r/g, "").trim())
|
||||||
|
}
|
||||||
|
|
||||||
export function formatHashLine(lineNumber: number, content: string): string {
|
export function formatHashLine(lineNumber: number, content: string): string {
|
||||||
const hash = computeLineHash(lineNumber, content)
|
const hash = computeLineHash(lineNumber, content)
|
||||||
return `${lineNumber}#${hash}|${content}`
|
return `${lineNumber}#${hash}|${content}`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from "bun:test"
|
import { describe, it, expect } from "bun:test"
|
||||||
import { computeLineHash } from "./hash-computation"
|
import { computeLineHash, computeLegacyLineHash } from "./hash-computation"
|
||||||
import { parseLineRef, validateLineRef, validateLineRefs } from "./validation"
|
import { parseLineRef, validateLineRef, validateLineRefs } from "./validation"
|
||||||
|
|
||||||
describe("parseLineRef", () => {
|
describe("parseLineRef", () => {
|
||||||
@@ -116,6 +116,15 @@ describe("validateLineRef", () => {
|
|||||||
expect(() => validateLineRef(lines, "1#ZZ")).toThrow(/>>>\s+1#[ZPMQVRWSNKTXJBYH]{2}\|/)
|
expect(() => validateLineRef(lines, "1#ZZ")).toThrow(/>>>\s+1#[ZPMQVRWSNKTXJBYH]{2}\|/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("accepts legacy hashes for indented lines", () => {
|
||||||
|
//#given
|
||||||
|
const lines = [" function hello() {", " return 42", " }"]
|
||||||
|
const legacyHash = computeLegacyLineHash(1, lines[0])
|
||||||
|
|
||||||
|
//#when / #then
|
||||||
|
expect(() => validateLineRef(lines, `1#${legacyHash}`)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
it("shows >>> mismatch context in batched validation", () => {
|
it("shows >>> mismatch context in batched validation", () => {
|
||||||
//#given
|
//#given
|
||||||
const lines = ["one", "two", "three", "four"]
|
const lines = ["one", "two", "three", "four"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computeLineHash } from "./hash-computation"
|
import { computeLegacyLineHash, computeLineHash } from "./hash-computation"
|
||||||
import { HASHLINE_REF_PATTERN } from "./constants"
|
import { HASHLINE_REF_PATTERN } from "./constants"
|
||||||
|
|
||||||
export interface LineRef {
|
export interface LineRef {
|
||||||
@@ -15,6 +15,10 @@ const MISMATCH_CONTEXT = 2
|
|||||||
|
|
||||||
const LINE_REF_EXTRACT_PATTERN = /([0-9]+#[ZPMQVRWSNKTXJBYH]{2})/
|
const LINE_REF_EXTRACT_PATTERN = /([0-9]+#[ZPMQVRWSNKTXJBYH]{2})/
|
||||||
|
|
||||||
|
function isCompatibleLineHash(line: number, content: string, hash: string): boolean {
|
||||||
|
return computeLineHash(line, content) === hash || computeLegacyLineHash(line, content) === hash
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeLineRef(ref: string): string {
|
export function normalizeLineRef(ref: string): string {
|
||||||
const originalTrimmed = ref.trim()
|
const originalTrimmed = ref.trim()
|
||||||
let trimmed = originalTrimmed
|
let trimmed = originalTrimmed
|
||||||
@@ -71,9 +75,7 @@ export function validateLineRef(lines: string[], ref: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = lines[line - 1]
|
const content = lines[line - 1]
|
||||||
const currentHash = computeLineHash(line, content)
|
if (!isCompatibleLineHash(line, content, hash)) {
|
||||||
|
|
||||||
if (currentHash !== hash) {
|
|
||||||
throw new HashlineMismatchError([{ line, expected: hash }], lines)
|
throw new HashlineMismatchError([{ line, expected: hash }], lines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,8 +142,8 @@ function suggestLineForHash(ref: string, lines: string[]): string | null {
|
|||||||
if (!hashMatch) return null
|
if (!hashMatch) return null
|
||||||
const hash = hashMatch[1]
|
const hash = hashMatch[1]
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
if (computeLineHash(i + 1, lines[i]) === hash) {
|
if (isCompatibleLineHash(i + 1, lines[i], hash)) {
|
||||||
return `Did you mean "${i + 1}#${hash}"?`
|
return `Did you mean "${i + 1}#${computeLineHash(i + 1, lines[i])}"?`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -169,8 +171,7 @@ export function validateLineRefs(lines: string[], refs: string[]): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = lines[line - 1]
|
const content = lines[line - 1]
|
||||||
const currentHash = computeLineHash(line, content)
|
if (!isCompatibleLineHash(line, content, hash)) {
|
||||||
if (currentHash !== hash) {
|
|
||||||
mismatches.push({ line, expected: hash })
|
mismatches.push({ line, expected: hash })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user