(existingLines: string[], edits: PlanEdit[])
| 53 | * Returns an error string if invalid, or null if all edits are acceptable. |
| 54 | */ |
| 55 | export function validateEdits(existingLines: string[], edits: PlanEdit[]): string | null { |
| 56 | const lineCount = existingLines.length; |
| 57 | |
| 58 | for (const edit of edits) { |
| 59 | if (!Number.isInteger(edit.start) || edit.start < 1) { |
| 60 | return `start must be a positive integer >= 1, got ${edit.start}`; |
| 61 | } |
| 62 | if (edit.start > lineCount + 1) { |
| 63 | return `start (${edit.start}) exceeds file length + 1 (${lineCount + 1})`; |
| 64 | } |
| 65 | if (edit.end != null) { |
| 66 | if (!Number.isInteger(edit.end) || edit.end < edit.start) { |
| 67 | return `end (${edit.end}) must be >= start (${edit.start})`; |
| 68 | } |
| 69 | // On an empty file (lineCount === 0) every edit is a pure insert; |
| 70 | // end is semantically meaningless and applyEdits handles it via splice |
| 71 | // clamping. Rejecting here breaks first-call payloads where the agent |
| 72 | // or framework includes end (see #742). |
| 73 | if (edit.end > lineCount && lineCount > 0) { |
| 74 | return `end (${edit.end}) exceeds file length (${lineCount})`; |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | const sorted = [...edits].sort((a, b) => a.start - b.start); |
| 80 | for (let i = 1; i < sorted.length; i++) { |
| 81 | const prev = sorted[i - 1]; |
| 82 | const curr = sorted[i]; |
| 83 | if (prev.start > lineCount) continue; |
| 84 | const prevEnd = prev.end ?? lineCount; |
| 85 | if (curr.start <= prevEnd) { |
| 86 | return `edits overlap: [${prev.start},${prevEnd}] and [${curr.start},${curr.end ?? "end"}]`; |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | return null; |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Format the plan content with line numbers for the agent's reference. |
no outgoing calls
no test coverage detected