Guide unbound canvas repair targets

Jaime Fournier <jaimef@linbsd.org>

e71c4a7e04f97dd6b18615ad7fc486d8bfd8af93

diff --git a/src/jcode/core/verified-run.ss b/src/jcode/core/verified-run.ss
index 74b6d74..8c1141b 100644
--- a/src/jcode/core/verified-run.ss
+++ b/src/jcode/core/verified-run.ss
@@ -1783,6 +1783,35 @@
                          (runtime-primitive-specific-guidance
                            detail primitive)))))))))
 
+(def (unbound-symbol-specific-guidance symbol)
+  (cond
+    ((string=? symbol "canvas")
+     (string-append
+       "\nQt callback scope hint: a top-level helper cannot see a `canvas` "
+       "bound inside `with-qt-app` or `let*`. Prefer the smallest repair that "
+       "keeps callbacks and repaint requests in scope: define the key/timer "
+       "handlers inside the same `let*` that binds `canvas`, pass `canvas` as "
+       "an argument to the helper, or initialize a top-level `current-canvas` "
+       "before callbacks can run."))
+    (else "")))
+
+(def (unbound-symbol-source-guidance detail cwd)
+  (let* ((symbol (unbound-variable-name detail))
+         (path (and symbol (failure-source-target-path detail cwd))))
+    (and path
+         (let ((absolute (abs-path cwd path)))
+           (and (file-exists? absolute)
+                (let* ((lines (string-split (read-file-string absolute) #\newline))
+                       (targets
+                         (source-target-lines
+                           lines path (string-downcase symbol) 10)))
+                  (and (pair? targets)
+                       (string-append
+                         "Source targets for unbound `" symbol "`:\n"
+                         (string-join targets "\n")
+                         "\nRepair the binding scope or the referenced name with the smallest local edit, then call verify."
+                         (unbound-symbol-specific-guidance symbol)))))))))
+
 (def (append-guidance-section base title text)
   (if (and (string? text)
            (> (string-length (string-trim text)) 0))
@@ -2020,14 +2049,21 @@
          (source-guidance (runtime-source-guidance base cwd))
          (with-source (append-guidance-section
                         repair-first "Source target guidance" source-guidance))
+         (unbound-source-guidance (unbound-symbol-source-guidance base cwd))
+         (with-unbound-source (append-guidance-section
+                                with-source
+                                "Unbound symbol source guidance"
+                                unbound-source-guidance))
          ;; Keep one bounded automatic advisor. Error-fix and anti-pattern MCP
          ;; tools remain available for targeted model-directed lookup.
          (mcp-advice
            (bounded-failure-guidance (mcp-failure-guidance base cwd)))
          (with-failure (if (usable-mcp-guidance? mcp-advice)
                          (append-guidance-section
-                           with-source "MCP failure advisor" mcp-advice)
-                         with-source)))
+                           with-unbound-source
+                           "MCP failure advisor"
+                           mcp-advice)
+                         with-unbound-source)))
     with-failure))
 
 (def failure-evidence-line-limit 12)
diff --git a/test/run.ss b/test/run.ss
index c83097e..aa91524 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -6506,6 +6506,31 @@
              (str-contains? s "Do not rewrite the whole constant block")))))
   (safe-delete-test-file! target-path))
 
+(let* ([vr-dir "/tmp"]
+       [target "jcode-verified-unbound-canvas-source-target.ss"]
+       [target-path (string-append vr-dir "/" target)]
+       [source "(import (jerboa prelude))\n(import (jerboa-qt qt))\n(def (handle-key!)\n  (qt-paint-widget-update! canvas))\n(with-qt-app app\n  (let* ([canvas (qt-paint-widget-create)])\n    (qt-on-key-press! canvas (lambda () (handle-key!)))))\n"])
+  (safe-delete-test-file! target-path)
+  (write-test-output-file target-path
+    (lambda (o) (display source o))
+    'replace)
+  (let* ([scope (parse-write-scope target)]
+         [result (parameterize ((current-write-scope scope))
+                   (run-verify-command
+                    "sh -c 'echo \"Exception: variable canvas is not bound\"; exit 1'"
+                    vr-dir))])
+    (check! "verified-run: unbound canvas source guidance keeps verify failing"
+            (car result) #f)
+    (check-pred! "verified-run: unbound canvas guidance points at callback scope"
+      (cdr result)
+      (lambda (s)
+        (and (str-contains? s "Source targets for unbound `canvas`")
+             (str-contains? s "qt-paint-widget-update! canvas")
+             (str-contains? s "Qt callback scope hint")
+             (str-contains? s "current-canvas")
+             (str-contains? s "same `let*` that binds `canvas`")))))
+  (safe-delete-test-file! target-path))
+
 (let* ([wf (coding-workflow "true" "/tmp"
              (list (cons 'task-guidance "recipe-context-marker")))])
   (check-pred! "verified-run: caller guidance appears in prompt"