fix: -o validation, multiple-arg warning, missing-file error, precise residual match, add --help/Makefile/tests

ober

4f3e89cc4f8dbc988dd76b3cb9db3ec9329e16fa

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..60420ca
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+GXI ?= gxi
+
+# Parent of the package dir, so :jerboa-gerbil/convert resolves on the loadpath.
+REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
+LOADPATH := $(dir $(REPO_DIR))
+
+.PHONY: test lint
+
+test:
+	GERBIL_LOADPATH='$(LOADPATH)' $(GXI) test/convert-test.ss
+
+lint:
+	GERBIL_LOADPATH='$(LOADPATH)' $(GXI) -e '(import :jerboa-gerbil/convert)' \
+		-e '(displayln "lint OK: convert.ss loads and expands cleanly")'
diff --git a/convert.ss b/convert.ss
index 1b697af..d5830b1 100644
--- a/convert.ss
+++ b/convert.ss
@@ -8,6 +8,7 @@
 ;;; references that need a human (e.g. libraries not yet ported to Gerbil).
 ;;;
 ;;;   gxi convert.ss INPUT.ss [-o OUTPUT.ss]      # default: write to stdout
+;;;   gxi convert.ss --help                       # print usage
 ;;;
 ;;; Library use: (import :jerboa-gerbil/convert) then (convert-string text).
 
@@ -45,8 +46,10 @@
 (def (convert-string text)
   (let loop ((rules +rename-rules+) (s text) (report '()))
     (if (null? rules)
-      ;; residual = leftover :jerboa-* refs, excluding our intended :jerboa-compat target
-      (values s (reverse report) (- (count-sub ":jerboa-" s) (count-sub ":jerboa-compat" s)))
+      ;; residual = leftover :jerboa-* refs, excluding our intended :jerboa-compat/ target.
+      ;; Match the package prefix ":jerboa-compat/" precisely (note the slash) so a
+      ;; hypothetical ":jerboa-compatibility" is still counted as residual.
+      (values s (reverse report) (- (count-sub ":jerboa-" s) (count-sub ":jerboa-compat/" s)))
       (let* ((rule (car rules))
              (name (car rule)) (old (cadr rule)) (new (caddr rule))
              (c (count-sub old s)))
@@ -61,11 +64,17 @@
       (display txt))
     (values report residual)))
 
+(def +usage+
+  "usage: gxi convert.ss INPUT.ss [-o OUTPUT.ss]      # default: write to stdout
+       gxi convert.ss --help")
+
 (def (main . args)
   (let loop ((args args) (out #f) (in #f))
     (cond
       ((null? args)
-       (unless in (error "usage: gxi convert.ss INPUT.ss [-o OUTPUT.ss]"))
+       (unless in (error +usage+))
+       (unless (file-exists? in)
+         (error (format "jerboa-gerbil: input file not found: ~a" in)))
        (let-values (((report residual) (convert-file! in out)))
          (let (ep (current-error-port))
            (fprintf ep "jerboa-gerbil: converted ~a~a\n" in (if out (format " -> ~a" out) ""))
@@ -76,5 +85,14 @@
            (when (> residual 0)
              (fprintf ep "  WARNING: ~a residual :jerboa-* reference(s) need manual review\n"
                       residual)))))
-      ((equal? (car args) "-o") (loop (cddr args) (cadr args) in))
-      (else (loop (cdr args) out (car args))))))
+      ((or (equal? (car args) "--help") (equal? (car args) "-h"))
+       (displayln +usage+))
+      ((equal? (car args) "-o")
+       (when (null? (cdr args))
+         (error (format "-o requires an output filename\n~a" +usage+)))
+       (loop (cddr args) (cadr args) in))
+      (else
+       (when in
+         (error (format "jerboa-gerbil: multiple input files: ~a and ~a\n~a"
+                        in (car args) +usage+)))
+       (loop (cdr args) out (car args))))))
diff --git a/test/convert-test.ss b/test/convert-test.ss
new file mode 100644
index 0000000..fb38e21
--- /dev/null
+++ b/test/convert-test.ss
@@ -0,0 +1,54 @@
+;;; -*- Gerbil -*-
+;;; jerboa-gerbil/test/convert-test — minimal tests for convert-string.
+;;;
+;;; Run via `make test` (or: GERBIL_LOADPATH=<parent-of-package> gxi test/convert-test.ss).
+
+(import :std/srfi/13 (only-in :jerboa-gerbil/convert convert-string))
+
+(def failures (box 0))
+
+(def (check name ok?)
+  (if ok?
+    (displayln "PASS: " name)
+    (begin
+      (displayln "FAIL: " name)
+      (set-box! failures (+ (unbox failures) 1)))))
+
+;; rename rule fires: :jerboa-emacs/chez-powers -> :jerboa-compat/powers
+(let-values (((txt report residual)
+              (convert-string "(import :jerboa-emacs/chez-powers)\n")))
+  (check "chez-powers rewritten to :jerboa-compat/powers"
+         (string-contains txt ":jerboa-compat/powers"))
+  (check "chez-powers rule reports 1 application"
+         (= 1 (cdr (assoc "chez-powers" report))))
+  (check "clean rename leaves no residual"
+         (= 0 residual)))
+
+;; generic emacs rule: :jerboa-emacs/foo -> :gemacs/foo
+(let-values (((txt report residual)
+              (convert-string "(import :jerboa-emacs/foo)\n")))
+  (check "generic :jerboa-emacs/ rewritten to :gemacs/"
+         (string-contains txt ":gemacs/foo")))
+
+;; residual detection: an unported :jerboa-* ref is counted
+(let-values (((txt report residual)
+              (convert-string "(import :jerboa-unknown/thing)\n")))
+  (check "unported :jerboa-* ref counted as residual"
+         (= 1 residual)))
+
+;; precise exclusion: the intended :jerboa-compat/ target is NOT residual
+(let-values (((txt report residual)
+              (convert-string "(import :jerboa-compat/powers)\n")))
+  (check "intended :jerboa-compat/ target not counted as residual"
+         (= 0 residual)))
+
+;; precise exclusion: a hypothetical :jerboa-compatibility IS still residual
+(let-values (((txt report residual)
+              (convert-string "(import :jerboa-compatibility/x)\n")))
+  (check ":jerboa-compatibility counted as residual (precise match)"
+         (= 1 residual)))
+
+(let (n (unbox failures))
+  (if (> n 0)
+    (begin (displayln n " failure(s)") (exit 1))
+    (displayln "all tests passed")))