refactor(hashline-edit): align tool payload to op/pos/end/lines
Unify hashline_edit input with replace/append/prepend + pos/end/lines semantics so callers use a single stable shape. Add normalization coverage and refresh tool guidance/tests to reduce schema confusion and stale legacy payload usage.
This commit is contained in:
@@ -1,142 +1,114 @@
|
||||
import type { HashlineEdit } from "./types"
|
||||
|
||||
type HashlineToolOp = "replace" | "append" | "prepend"
|
||||
|
||||
export interface RawHashlineEdit {
|
||||
type?:
|
||||
| "set_line"
|
||||
| "replace_lines"
|
||||
| "insert_after"
|
||||
| "insert_before"
|
||||
| "insert_between"
|
||||
| "replace"
|
||||
| "append"
|
||||
| "prepend"
|
||||
line?: string
|
||||
start_line?: string
|
||||
end_line?: string
|
||||
after_line?: string
|
||||
before_line?: string
|
||||
text?: string | string[]
|
||||
old_text?: string
|
||||
new_text?: string | string[]
|
||||
op?: HashlineToolOp
|
||||
pos?: string
|
||||
end?: string
|
||||
lines?: string | string[] | null
|
||||
}
|
||||
|
||||
function firstDefined(...values: Array<string | undefined>): string | undefined {
|
||||
for (const value of values) {
|
||||
if (typeof value === "string" && value.trim() !== "") return value
|
||||
function normalizeAnchor(value: string | undefined): string | undefined {
|
||||
if (typeof value !== "string") return undefined
|
||||
const trimmed = value.trim()
|
||||
return trimmed === "" ? undefined : trimmed
|
||||
}
|
||||
|
||||
function requireLines(edit: RawHashlineEdit, index: number): string | string[] {
|
||||
if (edit.lines === undefined) {
|
||||
throw new Error(`Edit ${index}: lines is required for ${edit.op ?? "unknown"}`)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function requireText(edit: RawHashlineEdit, index: number): string | string[] {
|
||||
const text = edit.text ?? edit.new_text
|
||||
if (text === undefined) {
|
||||
throw new Error(`Edit ${index}: text is required for ${edit.type ?? "unknown"}`)
|
||||
if (edit.lines === null) {
|
||||
return []
|
||||
}
|
||||
return text
|
||||
return edit.lines
|
||||
}
|
||||
|
||||
function requireLine(anchor: string | undefined, index: number, op: string): string {
|
||||
function requireLine(anchor: string | undefined, index: number, op: HashlineToolOp): string {
|
||||
if (!anchor) {
|
||||
throw new Error(`Edit ${index}: ${op} requires at least one anchor line reference`)
|
||||
throw new Error(`Edit ${index}: ${op} requires at least one anchor line reference (pos or end)`)
|
||||
}
|
||||
return anchor
|
||||
}
|
||||
|
||||
export function normalizeHashlineEdits(rawEdits: RawHashlineEdit[]): HashlineEdit[] {
|
||||
const normalized: HashlineEdit[] = []
|
||||
function normalizeReplaceEdit(edit: RawHashlineEdit, index: number): HashlineEdit {
|
||||
const pos = normalizeAnchor(edit.pos)
|
||||
const end = normalizeAnchor(edit.end)
|
||||
const anchor = requireLine(pos ?? end, index, "replace")
|
||||
const text = requireLines(edit, index)
|
||||
|
||||
for (let index = 0; index < rawEdits.length; index += 1) {
|
||||
const edit = rawEdits[index] ?? {}
|
||||
const type = edit.type
|
||||
|
||||
switch (type) {
|
||||
case "set_line": {
|
||||
const anchor = firstDefined(edit.line, edit.start_line, edit.end_line, edit.after_line, edit.before_line)
|
||||
normalized.push({
|
||||
type: "set_line",
|
||||
line: requireLine(anchor, index, "set_line"),
|
||||
text: requireText(edit, index),
|
||||
})
|
||||
break
|
||||
}
|
||||
case "replace_lines": {
|
||||
const startAnchor = firstDefined(edit.start_line, edit.line, edit.after_line)
|
||||
const endAnchor = firstDefined(edit.end_line, edit.line, edit.before_line)
|
||||
|
||||
if (!startAnchor && !endAnchor) {
|
||||
throw new Error(`Edit ${index}: replace_lines requires start_line or end_line`)
|
||||
}
|
||||
|
||||
if (startAnchor && endAnchor) {
|
||||
normalized.push({
|
||||
type: "replace_lines",
|
||||
start_line: startAnchor,
|
||||
end_line: endAnchor,
|
||||
text: requireText(edit, index),
|
||||
})
|
||||
} else {
|
||||
normalized.push({
|
||||
type: "set_line",
|
||||
line: requireLine(startAnchor ?? endAnchor, index, "replace_lines"),
|
||||
text: requireText(edit, index),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "insert_after": {
|
||||
const anchor = firstDefined(edit.line, edit.after_line, edit.end_line, edit.start_line)
|
||||
normalized.push({
|
||||
type: "insert_after",
|
||||
line: requireLine(anchor, index, "insert_after"),
|
||||
text: requireText(edit, index),
|
||||
})
|
||||
break
|
||||
}
|
||||
case "insert_before": {
|
||||
const anchor = firstDefined(edit.line, edit.before_line, edit.start_line, edit.end_line)
|
||||
normalized.push({
|
||||
type: "insert_before",
|
||||
line: requireLine(anchor, index, "insert_before"),
|
||||
text: requireText(edit, index),
|
||||
})
|
||||
break
|
||||
}
|
||||
case "insert_between": {
|
||||
const afterLine = firstDefined(edit.after_line, edit.line, edit.start_line)
|
||||
const beforeLine = firstDefined(edit.before_line, edit.end_line, edit.line)
|
||||
normalized.push({
|
||||
type: "insert_between",
|
||||
after_line: requireLine(afterLine, index, "insert_between.after_line"),
|
||||
before_line: requireLine(beforeLine, index, "insert_between.before_line"),
|
||||
text: requireText(edit, index),
|
||||
})
|
||||
break
|
||||
}
|
||||
case "replace": {
|
||||
const oldText = edit.old_text
|
||||
const newText = edit.new_text ?? edit.text
|
||||
if (!oldText) {
|
||||
throw new Error(`Edit ${index}: replace requires old_text`)
|
||||
}
|
||||
if (newText === undefined) {
|
||||
throw new Error(`Edit ${index}: replace requires new_text or text`)
|
||||
}
|
||||
normalized.push({ type: "replace", old_text: oldText, new_text: newText })
|
||||
break
|
||||
}
|
||||
case "append": {
|
||||
normalized.push({ type: "append", text: requireText(edit, index) })
|
||||
break
|
||||
}
|
||||
case "prepend": {
|
||||
normalized.push({ type: "prepend", text: requireText(edit, index) })
|
||||
break
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Edit ${index}: unsupported type "${String(type)}"`)
|
||||
}
|
||||
if (pos && end) {
|
||||
return {
|
||||
type: "replace_lines",
|
||||
start_line: pos,
|
||||
end_line: end,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
return normalized
|
||||
return {
|
||||
type: "set_line",
|
||||
line: anchor,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAppendEdit(edit: RawHashlineEdit, index: number): HashlineEdit {
|
||||
const pos = normalizeAnchor(edit.pos)
|
||||
const end = normalizeAnchor(edit.end)
|
||||
const anchor = pos ?? end
|
||||
const text = requireLines(edit, index)
|
||||
|
||||
if (!anchor) {
|
||||
return {
|
||||
type: "append",
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "insert_after",
|
||||
line: anchor,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePrependEdit(edit: RawHashlineEdit, index: number): HashlineEdit {
|
||||
const pos = normalizeAnchor(edit.pos)
|
||||
const end = normalizeAnchor(edit.end)
|
||||
const anchor = pos ?? end
|
||||
const text = requireLines(edit, index)
|
||||
|
||||
if (!anchor) {
|
||||
return {
|
||||
type: "prepend",
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "insert_before",
|
||||
line: anchor,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeHashlineEdits(rawEdits: RawHashlineEdit[]): HashlineEdit[] {
|
||||
return rawEdits.map((rawEdit, index) => {
|
||||
const edit = rawEdit ?? {}
|
||||
|
||||
switch (edit.op) {
|
||||
case "replace":
|
||||
return normalizeReplaceEdit(edit, index)
|
||||
case "append":
|
||||
return normalizeAppendEdit(edit, index)
|
||||
case "prepend":
|
||||
return normalizePrependEdit(edit, index)
|
||||
default:
|
||||
throw new Error(
|
||||
`Edit ${index}: unsupported op "${String(edit.op)}". Legacy format was removed; use op/pos/end/lines.`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user