diff options
| author | Vineet Kumar <git@vineetk.net> | 2026-05-25 16:13:05 -0400 |
|---|---|---|
| committer | Vineet Kumar <git@vineetk.net> | 2026-05-25 16:36:13 -0400 |
| commit | bacb87e48d0757deb40df1faa5b189d0904cc872 (patch) | |
| tree | a640a9609a11a749dce458d88e46b48ac7ae40ad | |
| parent | 0000dae69fbbe55988eef2d3b179340fa8c6a7cc (diff) | |
add some hashline patch validation
| -rw-r--r-- | llm-tools-hl.el | 184 |
1 files changed, 126 insertions, 58 deletions
diff --git a/llm-tools-hl.el b/llm-tools-hl.el index 808b837..61c4919 100644 --- a/llm-tools-hl.el +++ b/llm-tools-hl.el | |||
| @@ -99,6 +99,129 @@ Each element is a line number and two-character hash, like `1vx'." | |||
| 99 | (let ((lines (llm-tools--hl-file-lines file))) | 99 | (let ((lines (llm-tools--hl-file-lines file))) |
| 100 | (llm-tools--hl-hash (elt lines (1- n))))) | 100 | (llm-tools--hl-hash (elt lines (1- n))))) |
| 101 | 101 | ||
| 102 | ;;;; Diff Validation | ||
| 103 | ;; TODO this should also validate the operators (e.g. any invalid | ||
| 104 | ;; op-chars, payload given before all other operations, no payload | ||
| 105 | ;; after +/< or payload given to -, or anchors without line | ||
| 106 | ;; number). it should return a string saying it's a malformed patch | ||
| 107 | ;; and what went wrong. | ||
| 108 | (cl-defstruct hl-verify-op | ||
| 109 | (type nil) ; 'insert-after 'insert-before 'delete 'replace | ||
| 110 | (anchor nil) ; string like "4ei" or "EOF" for +/-/< | ||
| 111 | (payload nil) ; list of strings (nil is no payload) | ||
| 112 | (raw nil)) ; raw op line text for error messages | ||
| 113 | |||
| 114 | (defun llm-tools--hl-parse-section-ops (section-body) | ||
| 115 | "Parse SECTION-BODY into a list of `hl-verify-op' structs. | ||
| 116 | Skips comment lines and blank lines." | ||
| 117 | (let (ops op) | ||
| 118 | (dolist (line (string-lines section-body)) | ||
| 119 | (when (string-match-p "\\S-" line) | ||
| 120 | (let ((ch (substring line 0 1))) | ||
| 121 | (pcase ch | ||
| 122 | ("#" nil) | ||
| 123 | ("~" | ||
| 124 | (when op | ||
| 125 | (setf (hl-verify-op-payload op) | ||
| 126 | (append (hl-verify-op-payload op) (list (substring line 1)))))) | ||
| 127 | (_ | ||
| 128 | (when op (push op ops)) | ||
| 129 | (setq op (make-hl-verify-op | ||
| 130 | :type (pcase ch | ||
| 131 | ("+" 'insert-after) | ||
| 132 | ("<" 'insert-before) | ||
| 133 | ("-" 'delete) | ||
| 134 | ("=" 'replace)) | ||
| 135 | :anchor (substring line 2) | ||
| 136 | :raw line)))))) | ||
| 137 | (when op (push op ops)) | ||
| 138 | (nreverse ops)))) | ||
| 139 | |||
| 140 | (defun llm-tools--hl-validate-no-payload-before-op (ops) | ||
| 141 | "Check that no payload lines appear before the first op." | ||
| 142 | (when-let ((first (car-safe ops))) | ||
| 143 | (if (hl-verify-op-payload first) | ||
| 144 | (list "Payload line (~) appears before any operation") | ||
| 145 | '()))) | ||
| 146 | |||
| 147 | (defun llm-tools--hl-validate-insert-has-payload (ops) | ||
| 148 | "Check that all insert ops have at least one payload line." | ||
| 149 | (let (errors) | ||
| 150 | (dolist (op ops) | ||
| 151 | (when (and (memq (hl-verify-op-type op) '(insert-after insert-before)) | ||
| 152 | (not (hl-verify-op-payload op))) | ||
| 153 | (push (format "Insert operation (%s) has no payload lines following it" | ||
| 154 | (hl-verify-op-type op)) | ||
| 155 | errors))) | ||
| 156 | (nreverse errors))) | ||
| 157 | |||
| 158 | (defun llm-tools--hl-validate-delete-no-payload (ops) | ||
| 159 | "Check that delete ops have no payload lines." | ||
| 160 | (let (errors) | ||
| 161 | (dolist (op ops) | ||
| 162 | (when (and (eq (hl-verify-op-type op) 'delete) | ||
| 163 | (hl-verify-op-payload op)) | ||
| 164 | (push "Delete operation (-) must not have payload lines" errors))) | ||
| 165 | errors)) | ||
| 166 | |||
| 167 | (defun llm-tools--hl-validate-anchors (ops) | ||
| 168 | "Check that all anchors are well-formed." | ||
| 169 | (let (errors) | ||
| 170 | (dolist (op ops) | ||
| 171 | (let ((anchor (hl-verify-op-anchor op))) | ||
| 172 | (pcase (hl-verify-op-type op) | ||
| 173 | ((or 'insert-after 'insert-before) | ||
| 174 | (when (and anchor | ||
| 175 | (not (string= anchor "EOF")) | ||
| 176 | (not (string= anchor "BOF"))) | ||
| 177 | (unless (and (>= (length anchor) 3) | ||
| 178 | (string-match-p "\\`[0-9]" anchor)) | ||
| 179 | (push (format "Invalid anchor %S (expected LINEHASH or EOF/BOF)" anchor) | ||
| 180 | errors)))) | ||
| 181 | ((or 'delete 'replace) | ||
| 182 | (let ((parts (split-string anchor "\\.\\."))) | ||
| 183 | (unless (= (length parts) 2) | ||
| 184 | (push (format "Invalid range %S (expected A..B)" anchor) | ||
| 185 | errors)) | ||
| 186 | (dolist (part parts) | ||
| 187 | (unless (and (>= (length part) 3) | ||
| 188 | (string-match-p "\\`[0-9]" part)) | ||
| 189 | (push (format "Invalid anchor %S in range (expected numeric line anchor)" part) | ||
| 190 | errors))))))) | ||
| 191 | errors))) | ||
| 192 | |||
| 193 | (defun llm-tools--hl-verify-section (section) | ||
| 194 | "Validate a single SECTION. Returns a list of error strings." | ||
| 195 | (let ((ops (llm-tools--hl-parse-section-ops (cdr section)))) | ||
| 196 | (append | ||
| 197 | (llm-tools--hl-validate-no-payload-before-op ops) | ||
| 198 | (llm-tools--hl-validate-insert-has-payload ops) | ||
| 199 | (llm-tools--hl-validate-delete-no-payload ops) | ||
| 200 | (llm-tools--hl-validate-anchors ops)))) | ||
| 201 | |||
| 202 | (defun llm-tools--hl-verify-structure (patch) | ||
| 203 | "Validate the structural integrity of PATCH. | ||
| 204 | Returns a list of error strings describing each structural violation found. | ||
| 205 | An empty list means the patch is well-formed." | ||
| 206 | (mapcan #'llm-tools--hl-verify-section | ||
| 207 | (llm-tools--split-patch-sections patch))) | ||
| 208 | |||
| 209 | (defun llm-tools--hl-verify (patch) | ||
| 210 | "Verify PATCH is structurally valid and all anchors match current files. | ||
| 211 | Returns a string with error messages, or an empty string if clean." | ||
| 212 | (let ((struct-errors (llm-tools--hl-verify-structure patch))) | ||
| 213 | (if struct-errors | ||
| 214 | (string-join struct-errors "\n") | ||
| 215 | (string-join | ||
| 216 | (delete-dups | ||
| 217 | (remq nil | ||
| 218 | (mapcar (lambda (elem) | ||
| 219 | (unless (cdr elem) | ||
| 220 | (format "%s had incorrect anchors. Use the 'read_file' tool to get correct anchors." | ||
| 221 | (car elem)))) | ||
| 222 | (llm-tools--hl-verify-anchors patch)))) | ||
| 223 | "\n")))) | ||
| 224 | |||
| 102 | ;;;; Diff Parsing | 225 | ;;;; Diff Parsing |
| 103 | (defun llm-tools--split-patch-sections (patch) | 226 | (defun llm-tools--split-patch-sections (patch) |
| 104 | "Return a list of (FILENAME . REST) cons cells from PATCH. | 227 | "Return a list of (FILENAME . REST) cons cells from PATCH. |
| @@ -157,25 +280,6 @@ line in the on-disk file." | |||
| 157 | (cons file (cl-subsetp anchors hl :test #'equal)))) | 280 | (cons file (cl-subsetp anchors hl :test #'equal)))) |
| 158 | patch-anchors))) | 281 | patch-anchors))) |
| 159 | 282 | ||
| 160 | ;; TODO this should also validate the operators (e.g. any invalid | ||
| 161 | ;; op-chars, payload given before all other operations, no payload | ||
| 162 | ;; after +/< or payload given to -, or anchors without line | ||
| 163 | ;; number). it should return a string saying it's a malformed patch | ||
| 164 | ;; and what went wrong. | ||
| 165 | (defun llm-tools--hl-verify (patch) | ||
| 166 | "Verify if the files in PATCH need to be re-read. | ||
| 167 | Returns a string message that states which files need to be re-read. | ||
| 168 | Empty string means no files need to be re-read." | ||
| 169 | (string-join | ||
| 170 | (delete-dups | ||
| 171 | (remq nil | ||
| 172 | (mapcar (lambda (elem) | ||
| 173 | (unless (cdr elem) | ||
| 174 | (format "%s had incorrect anchors. Use the 'read_file' tool to get correct anchors." | ||
| 175 | (car elem)))) | ||
| 176 | (llm-tools--hl-verify-anchors patch)))) | ||
| 177 | "\n")) | ||
| 178 | |||
| 179 | (cl-defstruct hl-op | 283 | (cl-defstruct hl-op |
| 180 | (type nil) ; 'insert-after 'insert-before 'delete 'replace | 284 | (type nil) ; 'insert-after 'insert-before 'delete 'replace |
| 181 | (anchor nil) ; string like "4ei" or "EOF" for +/-/< | 285 | (anchor nil) ; string like "4ei" or "EOF" for +/-/< |
| @@ -219,9 +323,9 @@ Skips comment lines (starting with `#`) and blank lines." | |||
| 219 | ("#" nil) | 323 | ("#" nil) |
| 220 | (_ | 324 | (_ |
| 221 | (when op (push op ops)) | 325 | (when op (push op ops)) |
| 222 | (setq op (llm-tools--hl-parse-op-line line)))))) | 326 | (setq op (llm-tools--hl-parse-op-line line))))))) |
| 223 | (when op (push op ops)) | 327 | (when op (push op ops)) |
| 224 | (nreverse ops)))) | 328 | (nreverse ops))) |
| 225 | 329 | ||
| 226 | (defun llm-tools--hl-group-by-file (sections) | 330 | (defun llm-tools--hl-group-by-file (sections) |
| 227 | "Group SECTIONS by file path. | 331 | "Group SECTIONS by file path. |
| @@ -335,40 +439,4 @@ if verification failed." | |||
| 335 | (format "Finished applying patch.")) | 439 | (format "Finished applying patch.")) |
| 336 | (format "%s" msg)))) | 440 | (format "%s" msg)))) |
| 337 | 441 | ||
| 338 | (let* ((patch "\ | ||
| 339 | # Replace one line (the payload must re-emit the original indentation) | ||
| 340 | @@ mod.ts | ||
| 341 | = 1vx..1vx | ||
| 342 | ~const TITLE = \"Mrs\"; | ||
| 343 | |||
| 344 | # Replace a full multiline statement (widen to a self-contained boundary) | ||
| 345 | @@ mod.ts | ||
| 346 | = 3gv..6be | ||
| 347 | ~ return [ | ||
| 348 | ~ \"Mrs\", | ||
| 349 | ~ name?.trim() || \"guest\", | ||
| 350 | ~ ].join(\" \"); | ||
| 351 | |||
| 352 | # Insert AFTER/BEFORE a line | ||
| 353 | @@ mod.ts | ||
| 354 | + 4ei | ||
| 355 | ~ \"Dr\", | ||
| 356 | < 5ff | ||
| 357 | ~ \"Dr\", | ||
| 358 | |||
| 359 | # Append to file | ||
| 360 | @@ mod.ts | ||
| 361 | + EOF | ||
| 362 | ~export const done = true; | ||
| 363 | |||
| 364 | # Delete a line | ||
| 365 | @@ mod.ts | ||
| 366 | - 5ff..5ff | ||
| 367 | |||
| 368 | # Blank a line (replace with LF) | ||
| 369 | @@ mod.ts | ||
| 370 | = 5ff..5ff | ||
| 371 | ")) | ||
| 372 | (llm-tools--hl-apply patch)) | ||
| 373 | |||
| 374 | (provide 'llm-tools-hl) | 442 | (provide 'llm-tools-hl) |
