refactor(hashline): change content separator from colon to pipe
Change LINE#HASH:content format to LINE#HASH|content across the entire codebase. The pipe separator is more visually distinct and avoids conflicts with TypeScript colons in code content. 15 files updated: implementation, prompts, tests, and READMEs.
This commit is contained in:
@@ -56,7 +56,7 @@ function transformLine(line: string): string {
|
||||
return line
|
||||
}
|
||||
const hash = computeLineHash(parsed.lineNumber, parsed.content)
|
||||
return `${parsed.lineNumber}#${hash}:${parsed.content}`
|
||||
return `${parsed.lineNumber}#${hash}|${parsed.content}`
|
||||
}
|
||||
|
||||
function transformOutput(output: string): string {
|
||||
@@ -137,7 +137,7 @@ function extractFilePath(metadata: unknown): string | undefined {
|
||||
}
|
||||
|
||||
async function appendWriteHashlineOutput(output: { output: string; metadata: unknown }): Promise<void> {
|
||||
if (output.output.includes("Updated file (LINE#ID:content):")) {
|
||||
if (output.output.includes("Updated file (LINE#ID|content):")) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ async function appendWriteHashlineOutput(output: { output: string; metadata: unk
|
||||
|
||||
const content = await file.text()
|
||||
const hashlined = toHashlineContent(content)
|
||||
output.output = `${output.output}\n\nUpdated file (LINE#ID:content):\n${hashlined}`
|
||||
output.output = `${output.output}\n\nUpdated file (LINE#ID|content):\n${hashlined}`
|
||||
}
|
||||
|
||||
export function createHashlineReadEnhancerHook(
|
||||
|
||||
@@ -47,8 +47,8 @@ describe("hashline-read-enhancer", () => {
|
||||
|
||||
//#then
|
||||
const lines = output.output.split("\n")
|
||||
expect(lines[3]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}:const x = 1$/)
|
||||
expect(lines[4]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}:const y = 2$/)
|
||||
expect(lines[3]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}\|const x = 1$/)
|
||||
expect(lines[4]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}\|const y = 2$/)
|
||||
expect(lines[10]).toBe("1: keep this unchanged")
|
||||
})
|
||||
|
||||
@@ -78,8 +78,8 @@ describe("hashline-read-enhancer", () => {
|
||||
expect(lines[0]).toBe("<path>/tmp/demo.ts</path>")
|
||||
expect(lines[1]).toBe("<type>file</type>")
|
||||
expect(lines[2]).toBe("<content>")
|
||||
expect(lines[3]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}:const x = 1$/)
|
||||
expect(lines[4]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}:const y = 2$/)
|
||||
expect(lines[3]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}\|const x = 1$/)
|
||||
expect(lines[4]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}\|const y = 2$/)
|
||||
expect(lines[6]).toBe("(End of file - total 2 lines)")
|
||||
expect(lines[7]).toBe("</content>")
|
||||
})
|
||||
@@ -105,9 +105,9 @@ describe("hashline-read-enhancer", () => {
|
||||
|
||||
//#then
|
||||
const lines = output.output.split("\n")
|
||||
expect(lines[0]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}:# Oh-My-OpenCode Features$/)
|
||||
expect(lines[1]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}:$/)
|
||||
expect(lines[2]).toMatch(/^3#[ZPMQVRWSNKTXJBYH]{2}:Hashline test$/)
|
||||
expect(lines[0]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}\|# Oh-My-OpenCode Features$/)
|
||||
expect(lines[1]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}\|$/)
|
||||
expect(lines[2]).toMatch(/^3#[ZPMQVRWSNKTXJBYH]{2}\|Hashline test$/)
|
||||
expect(lines[4]).toBe("(End of file - total 3 lines)")
|
||||
})
|
||||
|
||||
@@ -133,8 +133,8 @@ describe("hashline-read-enhancer", () => {
|
||||
|
||||
//#then
|
||||
const lines = output.output.split("\n")
|
||||
expect(lines[1]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}:const x = 1$/)
|
||||
expect(lines[2]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}:const y = 2$/)
|
||||
expect(lines[1]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}\|const x = 1$/)
|
||||
expect(lines[2]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}\|const y = 2$/)
|
||||
expect(lines[5]).toBe("</file>")
|
||||
})
|
||||
|
||||
@@ -160,8 +160,8 @@ describe("hashline-read-enhancer", () => {
|
||||
|
||||
//#then
|
||||
const lines = output.output.split("\n")
|
||||
expect(lines[1]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}:const x = 1$/)
|
||||
expect(lines[2]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}:const y = 2$/)
|
||||
expect(lines[1]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}\|const x = 1$/)
|
||||
expect(lines[2]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}\|const y = 2$/)
|
||||
})
|
||||
|
||||
it("appends LINE#ID output for write tool using metadata filepath", async () => {
|
||||
@@ -181,9 +181,9 @@ describe("hashline-read-enhancer", () => {
|
||||
await hook["tool.execute.after"](input, output)
|
||||
|
||||
//#then
|
||||
expect(output.output).toContain("Updated file (LINE#ID:content):")
|
||||
expect(output.output).toMatch(/1#[ZPMQVRWSNKTXJBYH]{2}:const x = 1/)
|
||||
expect(output.output).toMatch(/2#[ZPMQVRWSNKTXJBYH]{2}:const y = 2/)
|
||||
expect(output.output).toContain("Updated file (LINE#ID|content):")
|
||||
expect(output.output).toMatch(/1#[ZPMQVRWSNKTXJBYH]{2}\|const x = 1/)
|
||||
expect(output.output).toMatch(/2#[ZPMQVRWSNKTXJBYH]{2}\|const y = 2/)
|
||||
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ describe("hashline edit operations", () => {
|
||||
const lines = ["line 1", "line 2", "line 3"]
|
||||
|
||||
//#when
|
||||
const result = applySetLine(lines, anchorFor(lines, 2), "1#VK:first\n2#NP:second")
|
||||
const result = applySetLine(lines, anchorFor(lines, 2), "1#VK|first\n2#NP|second")
|
||||
|
||||
//#then
|
||||
expect(result).toEqual(["line 1", "first", "second", "line 3"])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*\d+\s*#\s*[ZPMQVRWSNKTXJBYH]{2}:/
|
||||
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*\d+\s*#\s*[ZPMQVRWSNKTXJBYH]{2}\|/
|
||||
const DIFF_PLUS_RE = /^[+](?![+])/
|
||||
|
||||
function equalsIgnoringWhitespace(a: string, b: string): boolean {
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("computeLineHash", () => {
|
||||
})
|
||||
|
||||
describe("formatHashLine", () => {
|
||||
it("formats single line as LINE#ID:content", () => {
|
||||
it("formats single line as LINE#ID|content", () => {
|
||||
//#given
|
||||
const lineNumber = 42
|
||||
const content = "const x = 42"
|
||||
@@ -69,12 +69,12 @@ describe("formatHashLine", () => {
|
||||
const result = formatHashLine(lineNumber, content)
|
||||
|
||||
//#then
|
||||
expect(result).toMatch(/^42#[ZPMQVRWSNKTXJBYH]{2}:const x = 42$/)
|
||||
expect(result).toMatch(/^42#[ZPMQVRWSNKTXJBYH]{2}\|const x = 42$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatHashLines", () => {
|
||||
it("formats all lines as LINE#ID:content", () => {
|
||||
it("formats all lines as LINE#ID|content", () => {
|
||||
//#given
|
||||
const content = "a\nb\nc"
|
||||
|
||||
@@ -84,9 +84,9 @@ describe("formatHashLines", () => {
|
||||
//#then
|
||||
const lines = result.split("\n")
|
||||
expect(lines).toHaveLength(3)
|
||||
expect(lines[0]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}:a$/)
|
||||
expect(lines[1]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}:b$/)
|
||||
expect(lines[2]).toMatch(/^3#[ZPMQVRWSNKTXJBYH]{2}:c$/)
|
||||
expect(lines[0]).toMatch(/^1#[ZPMQVRWSNKTXJBYH]{2}\|a$/)
|
||||
expect(lines[1]).toMatch(/^2#[ZPMQVRWSNKTXJBYH]{2}\|b$/)
|
||||
expect(lines[2]).toMatch(/^3#[ZPMQVRWSNKTXJBYH]{2}\|c$/)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export function computeLineHash(lineNumber: number, content: string): string {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -14,16 +14,16 @@ export function generateHashlineDiff(oldContent: string, newContent: string, fil
|
||||
const hash = computeLineHash(lineNum, newLine)
|
||||
|
||||
if (i >= oldLines.length) {
|
||||
diff += `+ ${lineNum}#${hash}:${newLine}\n`
|
||||
diff += `+ ${lineNum}#${hash}|${newLine}\n`
|
||||
continue
|
||||
}
|
||||
if (i >= newLines.length) {
|
||||
diff += `- ${lineNum}# :${oldLine}\n`
|
||||
diff += `- ${lineNum}# |${oldLine}\n`
|
||||
continue
|
||||
}
|
||||
if (oldLine !== newLine) {
|
||||
diff += `- ${lineNum}# :${oldLine}\n`
|
||||
diff += `+ ${lineNum}#${hash}:${newLine}\n`
|
||||
diff += `- ${lineNum}# |${oldLine}\n`
|
||||
diff += `+ ${lineNum}#${hash}|${newLine}\n`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ WORKFLOW:
|
||||
2. Pick the smallest operation per logical mutation site.
|
||||
3. Submit one edit call per file with all related operations.
|
||||
4. If same file needs another call, re-read first.
|
||||
5. Use anchors as "LINE#ID" only (never include trailing ":content").
|
||||
5. Use anchors as "LINE#ID" only (never include trailing "|content").
|
||||
|
||||
VALIDATION:
|
||||
Payload shape: { "filePath": string, "edits": [...], "delete"?: boolean, "rename"?: string }
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("parseLineRef", () => {
|
||||
|
||||
it("accepts refs copied with markers and trailing content", () => {
|
||||
//#given
|
||||
const ref = ">>> 42#VK:const value = 1"
|
||||
const ref = ">>> 42#VK|const value = 1"
|
||||
|
||||
//#when
|
||||
const result = parseLineRef(ref)
|
||||
@@ -49,7 +49,7 @@ describe("validateLineRef", () => {
|
||||
const lines = ["function hello() {"]
|
||||
|
||||
//#when / #then
|
||||
expect(() => validateLineRef(lines, "1#ZZ")).toThrow(/>>>\s+1#[ZPMQVRWSNKTXJBYH]{2}:/)
|
||||
expect(() => validateLineRef(lines, "1#ZZ")).toThrow(/>>>\s+1#[ZPMQVRWSNKTXJBYH]{2}\|/)
|
||||
})
|
||||
|
||||
it("shows >>> mismatch context in batched validation", () => {
|
||||
@@ -58,7 +58,7 @@ describe("validateLineRef", () => {
|
||||
|
||||
//#when / #then
|
||||
expect(() => validateLineRefs(lines, ["2#ZZ"]))
|
||||
.toThrow(/>>>\s+2#[ZPMQVRWSNKTXJBYH]{2}:two/)
|
||||
.toThrow(/>>>\s+2#[ZPMQVRWSNKTXJBYH]{2}\|two/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("legacy LINE:HEX backward compatibility", () => {
|
||||
const lines = ["function hello() {"]
|
||||
|
||||
//#when / #then
|
||||
expect(() => validateLineRef(lines, "1:ab")).toThrow(/>>>\s+1#[ZPMQVRWSNKTXJBYH]{2}:/)
|
||||
expect(() => validateLineRef(lines, "1:ab")).toThrow(/>>>\s+1#[ZPMQVRWSNKTXJBYH]{2}\|/)
|
||||
})
|
||||
|
||||
it("extracts legacy ref from content with markers", () => {
|
||||
|
||||
@@ -115,7 +115,7 @@ export class HashlineMismatchError extends Error {
|
||||
|
||||
const content = fileLines[line - 1] ?? ""
|
||||
const hash = computeLineHash(line, content)
|
||||
const prefix = `${line}#${hash}:${content}`
|
||||
const prefix = `${line}#${hash}|${content}`
|
||||
if (mismatchByLine.has(line)) {
|
||||
output.push(`>>> ${prefix}`)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user