summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--llm-tools-hl.el184
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.
116Skips 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.
204Returns a list of error strings describing each structural violation found.
205An 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.
211Returns 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.
167Returns a string message that states which files need to be re-read.
168Empty 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)