From b8a6f10f705dbac5ce769f7730e2e22a95be8d15 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 20 Feb 2026 11:07:42 +0900 Subject: [PATCH] refactor(hashline-edit): redesign hashline format with CID-based hashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking Changes: - Change hashline format from 'lineNum:hex|content' to 'lineNum#CID:content' - Replace hex-based hashing (00-ff) with CID-based hashing (ZPMQVRWSNKTXJBYH nibbles) - Simplify constants: HASH_DICT → NIBBLE_STR + HASHLINE_DICT - Update patterns: HASHLINE_PATTERN → HASHLINE_REF_PATTERN + HASHLINE_OUTPUT_PATTERN Benefits: - More compact and memorable CID identifiers - Better alignment with LSP line reference format (lineNum#ID) - Improved error messages and diff metadata clarity - Remove unused toHashlineContent from diff-enhancer hook Updates: - Refactor hash-computation for CID generation - Update all diff-utils to use new format - Update hook to use raw content instead of hashline format - Update tests to match new expectations 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/hooks/hashline-edit-diff-enhancer/hook.ts | 9 +- .../hashline-edit-diff-enhancer/index.test.ts | 23 ++-- src/tools/hashline-edit/constants.ts | 38 ++----- src/tools/hashline-edit/diff-utils.ts | 2 +- .../hashline-edit/hash-computation.test.ts | 102 +++--------------- src/tools/hashline-edit/hash-computation.ts | 10 +- src/tools/hashline-edit/index.ts | 2 +- src/tools/hashline-edit/types.ts | 8 +- 8 files changed, 45 insertions(+), 149 deletions(-) diff --git a/src/hooks/hashline-edit-diff-enhancer/hook.ts b/src/hooks/hashline-edit-diff-enhancer/hook.ts index 8ef7c56a..300a6988 100644 --- a/src/hooks/hashline-edit-diff-enhancer/hook.ts +++ b/src/hooks/hashline-edit-diff-enhancer/hook.ts @@ -1,5 +1,5 @@ import { log } from "../../shared" -import { toHashlineContent, generateUnifiedDiff, countLineDiffs } from "../../tools/hashline-edit/diff-utils" +import { generateUnifiedDiff, countLineDiffs } from "../../tools/hashline-edit/diff-utils" interface HashlineEditDiffEnhancerConfig { hashline_edit?: { enabled: boolean } @@ -86,16 +86,13 @@ export function createHashlineEditDiffEnhancerHook(config: HashlineEditDiffEnhan } const { additions, deletions } = countLineDiffs(oldContent, newContent) - const oldHashlined = toHashlineContent(oldContent) - const newHashlined = toHashlineContent(newContent) - const unifiedDiff = generateUnifiedDiff(oldContent, newContent, filePath) output.metadata.filediff = { file: filePath, path: filePath, - before: oldHashlined, - after: newHashlined, + before: oldContent, + after: newContent, additions, deletions, } diff --git a/src/hooks/hashline-edit-diff-enhancer/index.test.ts b/src/hooks/hashline-edit-diff-enhancer/index.test.ts index 435ad9c3..ee39442e 100644 --- a/src/hooks/hashline-edit-diff-enhancer/index.test.ts +++ b/src/hooks/hashline-edit-diff-enhancer/index.test.ts @@ -88,8 +88,8 @@ describe("hashline-edit-diff-enhancer", () => { expect(filediff).toBeDefined() expect(filediff.file).toBe(tmpFile) expect(filediff.path).toBe(tmpFile) - expect(filediff.before).toMatch(/^\d+:[a-f0-9]{2}\|/) - expect(filediff.after).toMatch(/^\d+:[a-f0-9]{2}\|/) + expect(filediff.before).toBe(oldContent) + expect(filediff.after).toBe(newContent) expect(filediff.additions).toBeGreaterThan(0) expect(filediff.deletions).toBeGreaterThan(0) @@ -163,7 +163,7 @@ describe("hashline-edit-diff-enhancer", () => { const filediff = afterOutput.metadata.filediff as FileDiffMetadata expect(filediff).toBeDefined() expect(filediff.before).toBe("") - expect(filediff.after).toMatch(/^1:[a-f0-9]{2}\|new content/) + expect(filediff.after).toBe("new content\n") expect(filediff.additions).toBeGreaterThan(0) expect(filediff.deletions).toBe(0) @@ -246,8 +246,8 @@ describe("hashline-edit-diff-enhancer", () => { }) }) - describe("hashline format in filediff", () => { - test("filediff.before and filediff.after are in hashline format", async () => { + describe("raw content in filediff", () => { + test("filediff.before and filediff.after are raw file content", async () => { //#given - a temp file const tmpDir = (await import("os")).tmpdir() const tmpFile = `${tmpDir}/hashline-diff-format-${Date.now()}.ts` @@ -264,17 +264,10 @@ describe("hashline-edit-diff-enhancer", () => { const afterOutput = makeAfterOutput() await hook["tool.execute.after"](input, afterOutput) - //#then - before and after should be in LINE:HASH|content format + //#then - before and after should be raw file content const filediff = afterOutput.metadata.filediff as { before: string; after: string } - const beforeLines = filediff.before.split("\n").filter(Boolean) - const afterLines = filediff.after.split("\n").filter(Boolean) - - for (const line of beforeLines) { - expect(line).toMatch(/^\d+:[a-f0-9]{2}\|/) - } - for (const line of afterLines) { - expect(line).toMatch(/^\d+:[a-f0-9]{2}\|/) - } + expect(filediff.before).toBe(oldContent) + expect(filediff.after).toBe(newContent) await (await import("fs/promises")).unlink(tmpFile).catch(() => {}) }) diff --git a/src/tools/hashline-edit/constants.ts b/src/tools/hashline-edit/constants.ts index 17638a49..b62e345e 100644 --- a/src/tools/hashline-edit/constants.ts +++ b/src/tools/hashline-edit/constants.ts @@ -1,30 +1,10 @@ -export const HASH_DICT = [ - "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", - "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", - "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", - "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", - "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", "30", "31", - "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", - "3c", "3d", "3e", "3f", "40", "41", "42", "43", "44", "45", - "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", - "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", - "5a", "5b", "5c", "5d", "5e", "5f", "60", "61", "62", "63", - "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", - "6e", "6f", "70", "71", "72", "73", "74", "75", "76", "77", - "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", "80", "81", - "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", - "8c", "8d", "8e", "8f", "90", "91", "92", "93", "94", "95", - "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", - "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", - "aa", "ab", "ac", "ad", "ae", "af", "b0", "b1", "b2", "b3", - "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", - "be", "bf", "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", - "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", "d0", "d1", - "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", - "dc", "dd", "de", "df", "e0", "e1", "e2", "e3", "e4", "e5", - "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", - "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", - "fa", "fb", "fc", "fd", "fe", "ff", -] as const +export const NIBBLE_STR = "ZPMQVRWSNKTXJBYH" -export const HASHLINE_PATTERN = /^(\d+):([0-9a-f]{2})\|(.*)$/ +export const HASHLINE_DICT = Array.from({ length: 256 }, (_, i) => { + const high = i >>> 4 + const low = i & 0x0f + return `${NIBBLE_STR[high]}${NIBBLE_STR[low]}` +}) + +export const HASHLINE_REF_PATTERN = /^([0-9]+)#([ZPMQVRWSNKTXJBYH]{2})$/ +export const HASHLINE_OUTPUT_PATTERN = /^([0-9]+)#([ZPMQVRWSNKTXJBYH]{2}):(.*)$/ diff --git a/src/tools/hashline-edit/diff-utils.ts b/src/tools/hashline-edit/diff-utils.ts index 163d841f..f2f74732 100644 --- a/src/tools/hashline-edit/diff-utils.ts +++ b/src/tools/hashline-edit/diff-utils.ts @@ -9,7 +9,7 @@ export function toHashlineContent(content: string): string { const hashlined = contentLines.map((line, i) => { const lineNum = i + 1 const hash = computeLineHash(lineNum, line) - return `${lineNum}:${hash}|${line}` + return `${lineNum}#${hash}:${line}` }) return hasTrailingNewline ? hashlined.join("\n") + "\n" : hashlined.join("\n") } diff --git a/src/tools/hashline-edit/hash-computation.test.ts b/src/tools/hashline-edit/hash-computation.test.ts index 180c92ae..e34712a8 100644 --- a/src/tools/hashline-edit/hash-computation.test.ts +++ b/src/tools/hashline-edit/hash-computation.test.ts @@ -2,103 +2,51 @@ import { describe, it, expect } from "bun:test" import { computeLineHash, formatHashLine, formatHashLines } from "./hash-computation" describe("computeLineHash", () => { - it("returns consistent 2-char hex for same input", () => { + it("returns deterministic 2-char CID hash", () => { //#given - const lineNumber = 1 const content = "function hello() {" //#when - const hash1 = computeLineHash(lineNumber, content) - const hash2 = computeLineHash(lineNumber, content) + const hash1 = computeLineHash(1, content) + const hash2 = computeLineHash(999, content) //#then expect(hash1).toBe(hash2) - expect(hash1).toMatch(/^[0-9a-f]{2}$/) + expect(hash1).toMatch(/^[ZPMQVRWSNKTXJBYH]{2}$/) }) - it("strips whitespace before hashing", () => { + it("ignores whitespace differences", () => { //#given - const lineNumber = 1 const content1 = "function hello() {" const content2 = " function hello() { " //#when - const hash1 = computeLineHash(lineNumber, content1) - const hash2 = computeLineHash(lineNumber, content2) + const hash1 = computeLineHash(1, content1) + const hash2 = computeLineHash(1, content2) //#then expect(hash1).toBe(hash2) }) - - it("uses line number in hash input", () => { - //#given - const content = "const stable = true" - - //#when - const hash1 = computeLineHash(1, content) - const hash2 = computeLineHash(2, content) - - //#then - expect(hash1).not.toBe(hash2) - }) - - it("handles empty lines", () => { - //#given - const lineNumber = 1 - const content = "" - - //#when - const hash = computeLineHash(lineNumber, content) - - //#then - expect(hash).toMatch(/^[0-9a-f]{2}$/) - }) - - it("returns different hashes for different content", () => { - //#given - const lineNumber = 1 - const content1 = "function hello() {" - const content2 = "function world() {" - - //#when - const hash1 = computeLineHash(lineNumber, content1) - const hash2 = computeLineHash(lineNumber, content2) - - //#then - expect(hash1).not.toBe(hash2) - }) }) describe("formatHashLine", () => { - it("formats line with hash prefix", () => { + it("formats single line as LINE#ID:content", () => { //#given const lineNumber = 42 - const content = "function hello() {" - - //#when - const result = formatHashLine(lineNumber, content) - - //#then - expect(result).toMatch(/^42:[0-9a-f]{2}\|function hello\(\) \{$/) - }) - - it("preserves content after hash prefix", () => { - //#given - const lineNumber = 1 const content = "const x = 42" //#when const result = formatHashLine(lineNumber, content) //#then - expect(result).toContain("|const x = 42") + expect(result).toMatch(/^42#[ZPMQVRWSNKTXJBYH]{2}:const x = 42$/) }) }) describe("formatHashLines", () => { - it("formats all lines with hash prefixes", () => { + it("formats all lines as LINE#ID:content", () => { //#given - const content = "function hello() {\n return 42\n}" + const content = "a\nb\nc" //#when const result = formatHashLines(content) @@ -106,30 +54,8 @@ describe("formatHashLines", () => { //#then const lines = result.split("\n") expect(lines).toHaveLength(3) - expect(lines[0]).toMatch(/^1:[0-9a-f]{2}\|/) - expect(lines[1]).toMatch(/^2:[0-9a-f]{2}\|/) - expect(lines[2]).toMatch(/^3:[0-9a-f]{2}\|/) - }) - - it("handles empty file", () => { - //#given - const content = "" - - //#when - const result = formatHashLines(content) - - //#then - expect(result).toBe("") - }) - - it("handles single line", () => { - //#given - const content = "const x = 42" - - //#when - const result = formatHashLines(content) - - //#then - expect(result).toMatch(/^1:[0-9a-f]{2}\|const x = 42$/) + expect(lines[0]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}:a$/) + expect(lines[1]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}:b$/) + expect(lines[2]).toMatch(/^3#[ZPMQVRWSNKTXJBYH]{2}:c$/) }) }) diff --git a/src/tools/hashline-edit/hash-computation.ts b/src/tools/hashline-edit/hash-computation.ts index 8144b76c..f76041f7 100644 --- a/src/tools/hashline-edit/hash-computation.ts +++ b/src/tools/hashline-edit/hash-computation.ts @@ -1,16 +1,16 @@ -import { HASH_DICT } from "./constants" +import { HASHLINE_DICT } from "./constants" export function computeLineHash(lineNumber: number, content: string): string { + void lineNumber const stripped = content.replace(/\s+/g, "") - const hashInput = `${lineNumber}:${stripped}` - const hash = Bun.hash.xxHash32(hashInput) + const hash = Bun.hash.xxHash32(stripped) const index = hash % 256 - return HASH_DICT[index] + return HASHLINE_DICT[index] } export function formatHashLine(lineNumber: number, content: string): string { const hash = computeLineHash(lineNumber, content) - return `${lineNumber}:${hash}|${content}` + return `${lineNumber}#${hash}:${content}` } export function formatHashLines(content: string): string { diff --git a/src/tools/hashline-edit/index.ts b/src/tools/hashline-edit/index.ts index 3aaf44e0..2f22a4fd 100644 --- a/src/tools/hashline-edit/index.ts +++ b/src/tools/hashline-edit/index.ts @@ -2,7 +2,7 @@ export { computeLineHash, formatHashLine, formatHashLines } from "./hash-computa export { parseLineRef, validateLineRef } from "./validation" export type { LineRef } from "./validation" export type { SetLine, ReplaceLines, InsertAfter, Replace, HashlineEdit } from "./types" -export { HASH_DICT, HASHLINE_PATTERN } from "./constants" +export { NIBBLE_STR, HASHLINE_DICT, HASHLINE_REF_PATTERN, HASHLINE_OUTPUT_PATTERN } from "./constants" export { applyHashlineEdits, applyInsertAfter, diff --git a/src/tools/hashline-edit/types.ts b/src/tools/hashline-edit/types.ts index 3de342b1..38b57f27 100644 --- a/src/tools/hashline-edit/types.ts +++ b/src/tools/hashline-edit/types.ts @@ -1,26 +1,26 @@ export interface SetLine { type: "set_line" line: string - text: string + text: string | string[] } export interface ReplaceLines { type: "replace_lines" start_line: string end_line: string - text: string + text: string | string[] } export interface InsertAfter { type: "insert_after" line: string - text: string + text: string | string[] } export interface Replace { type: "replace" old_text: string - new_text: string + new_text: string | string[] } export type HashlineEdit = SetLine | ReplaceLines | InsertAfter | Replace