11#! /usr/bin/env bash
22# SPDX-License-Identifier: PMPL-1.0-or-later
3- # batch_driver.sh — walk local proof trees, verify via echidna, record in verisim-api.
3+ # batch_driver.sh — walk local proof trees, verify via prover binaries directly,
4+ # record outcomes in verisim-api for the proof_strategy_selection MV.
45#
5- # For each configured (repo, glob, prover, obligation_class) target:
6- # 1. Read proof file content
7- # 2. POST to echidna /api/verify with (prover, content) → valid/invalid + duration
8- # 3. POST to verisim-api /api/v1/proof_attempts with outcome + duration
9- # 4. Emit desktop notification via notify-send
10- #
11- # Bypasses echidnabot's clone-based pipeline to bootstrap training data for
12- # the proof_strategy_selection MV directly from local working trees.
6+ # IMPORTANT (2026-04-05): this script bypasses echidna's /api/verify endpoint.
7+ # That endpoint parses content into an abstract ProofState and re-exports it
8+ # before running the backend, which loses the original proof body and
9+ # silently passes garbage input as `valid=true` for Coq/Lean/Agda. Our
10+ # training data needs ground truth, so we shell out to the prover binaries
11+ # directly and interpret exit status.
1312#
1413# Environment:
15- # ECHIDNA_URL (default http://127.0.0.1:8090)
1614# VERISIM_URL (default http://127.0.0.1:8080)
1715# NOTIFY (1=notify-send on each result, 0=silent; default 1)
1816# DRY_RUN (1=print only, no HTTP; default 0)
19- # MAX_FILES (cap per target, default 10 to avoid flooding)
20- # STRATEGY_TAG (column stored in proof_attempts, default "batch-manual")
17+ # MAX_FILES (cap per target, default 10)
18+ # STRATEGY_TAG (column stored in proof_attempts, default "batch-direct")
19+ # RATE_MS (sleep ms between provers, default 100)
20+ # TIMEOUT_SEC (per-prover timeout, default 30)
2121
2222set -euo pipefail
2323
24- ECHIDNA_URL=" ${ECHIDNA_URL:- http:// 127.0.0.1: 8090} "
2524VERISIM_URL=" ${VERISIM_URL:- http:// 127.0.0.1: 8080} "
2625NOTIFY=" ${NOTIFY:- 1} "
2726DRY_RUN=" ${DRY_RUN:- 0} "
2827MAX_FILES=" ${MAX_FILES:- 10} "
29- STRATEGY_TAG=" ${STRATEGY_TAG:- batch-manual} "
28+ STRATEGY_TAG=" ${STRATEGY_TAG:- batch-direct} "
29+ RATE_MS=" ${RATE_MS:- 100} "
30+ TIMEOUT_SEC=" ${TIMEOUT_SEC:- 30} "
3031
3132REPOS_ROOT=" /var/mnt/eclipse/repos"
3233
3334# Target definitions: repo | obligation_class | prover | glob-root | extension
34- # obligation_class values match ClickHouse Enum8 in proof_attempts.sql:
35- # safety, linearity, termination, equiv, correctness, confluence, totality,
36- # invariant, refinement, model- check, other
35+ # Provers invoked directly (not via echidna). Only provers with reliable
36+ # exit-status semantics are included; Agda's invocation is more involved and
37+ # Idris2's -- check needs a source directory so it's tracked per-target.
3738TARGETS=(
38- " echidna|safety|Z3|examples|smt2"
39- " echidna|safety|Z3|proofs/z3|smt2"
40- " echidna|equiv|Coq|proofs/coq|v"
41- " echidna|equiv|Lean|proofs/lean|lean"
42- " echidna|equiv|Agda|proofs/agda|agda"
43- " echidna|totality|Idris2|verification/proofs/idris2|idr"
44- " ephapax|equiv|Coq|formal|v"
45- " ephapax|linearity|Idris2|src/formal/Ephapax/Formal|idr"
46- " verisimdb|totality|Idris2|verification/proofs/idris2|idr"
39+ " echidna|safety|z3|examples|smt2"
40+ " echidna|safety|z3|proofs/z3|smt2"
41+ " echidna|equiv|coq|proofs/coq|v"
42+ " echidna|equiv|lean|proofs/lean|lean"
43+ " ephapax|equiv|coq|formal|v"
4744)
4845
4946# --- helpers ----------------------------------------------------------------
5047
51- prover_lower () {
52- # echidna REST names (Z3/Coq/Lean/Idris2/Agda) → verisim-api enum (lowercase)
53- case " $1 " in
54- Z3) echo " z3" ;;
55- Coq) echo " coq" ;;
56- Lean) echo " lean" ;;
57- Idris2) echo " idris2" ;;
58- Agda) echo " agda" ;;
59- CVC5|Cvc5) echo " cvc5" ;;
60- * ) echo " other" ;;
61- esac
62- }
48+ iso_now () { date -u +' %Y-%m-%dT%H:%M:%S.%3N' ; } # ClickHouse DateTime64(3), no Z
49+ uuidv4 () { cat /proc/sys/kernel/random/uuid; }
6350
6451obligation_id_for () {
65- # Stable SHA-256 prefix of repo|file|claim → hex string. ClickHouse column
66- # is String, not UUID, so any stable identifier works.
6752 printf " %s|%s|%s" " $1 " " $2 " " $3 " | sha256sum | cut -c1-40
6853}
6954
70- iso_now () { date -u +' %Y-%m-%dT%H:%M:%S.000' ; } # ClickHouse DateTime64(3), no Z
71- uuidv4 () { cat /proc/sys/kernel/random/uuid; }
72-
7355notify () {
74- # $1 = urgency (normal|critical), $2 = title, $3 = body
7556 [ " $NOTIFY " = " 1" ] || return 0
7657 notify-send --urgency=" $1 " --app-name=" echidna-batch" " $2 " " $3 " 2> /dev/null || true
7758}
7859
60+ # Sleep in integer milliseconds using usleep fallback
61+ rate_sleep () {
62+ [ " $RATE_MS " = " 0" ] && return 0
63+ # Bash can't sleep ms natively; use perl or python3 fallback
64+ perl -e " select(undef,undef,undef,$RATE_MS /1000)" 2> /dev/null || sleep 1
65+ }
66+
67+ # --- per-prover runners -----------------------------------------------------
68+ #
69+ # Each runner echoes two lines to stdout:
70+ # OUTCOME (success|failure|timeout|unknown)
71+ # DURATION_MS
72+
73+ run_z3 () {
74+ local file=" $1 "
75+ local t0 t1 output rc
76+ t0=$( date +%s%3N)
77+ output=$( timeout " ${TIMEOUT_SEC} s" z3 -smt2 " $file " 2>&1 )
78+ rc=$?
79+ t1=$( date +%s%3N)
80+ echo $(( t1 - t0 ))
81+ if [ $rc -eq 124 ]; then
82+ echo timeout; return
83+ fi
84+ # Z3's check-sat result is on stdout. For proof obligations, we want
85+ # unsat (meaning the assertion is valid). Successful termination on a
86+ # well-formed SMT-LIB file counts as success even if sat — the prover
87+ # ran and produced a decision. We distinguish only "prover ran" from
88+ # "prover errored or timed out".
89+ if echo " $output " | grep -qE " ^(sat|unsat|unknown)$" ; then
90+ echo success
91+ elif echo " $output " | grep -qiE " error|parse" ; then
92+ echo failure
93+ else
94+ echo unknown
95+ fi
96+ }
97+
98+ run_coq () {
99+ local file=" $1 "
100+ local t0 t1 rc
101+ t0=$( date +%s%3N)
102+ ( cd " $( dirname " $file " ) " && timeout " ${TIMEOUT_SEC} s" coqc -q " $( basename " $file " ) " > /dev/null 2>&1 )
103+ rc=$?
104+ t1=$( date +%s%3N)
105+ echo $(( t1 - t0 ))
106+ [ $rc -eq 124 ] && { echo timeout; return ; }
107+ [ $rc -eq 0 ] && echo success || echo failure
108+ }
109+
110+ run_lean () {
111+ local file=" $1 "
112+ local t0 t1 rc
113+ t0=$( date +%s%3N)
114+ timeout " ${TIMEOUT_SEC} s" lean " $file " > /dev/null 2>&1
115+ rc=$?
116+ t1=$( date +%s%3N)
117+ echo $(( t1 - t0 ))
118+ [ $rc -eq 124 ] && { echo timeout; return ; }
119+ [ $rc -eq 0 ] && echo success || echo failure
120+ }
121+
122+ # Dispatch to the right runner by prover name.
123+ run_prover () {
124+ local prover=" $1 " file=" $2 "
125+ case " $prover " in
126+ z3) run_z3 " $file " ;;
127+ coq) run_coq " $file " ;;
128+ lean) run_lean " $file " ;;
129+ * ) echo 0; echo unknown ;;
130+ esac
131+ }
132+
79133# --- per-file verification --------------------------------------------------
80134
81135verify_one () {
82136 local repo=" $1 " obligation_class=" $2 " prover=" $3 " file=" $4 "
83137 local rel_file=" ${file# " $REPOS_ROOT /$repo /" } "
84138 local claim
85139 claim=$( head -c 200 " $file " | tr ' \n' ' ' | tr -s ' ' )
86- local obl_id
140+ local obl_id attempt_id started completed
87141 obl_id=$( obligation_id_for " $repo " " $rel_file " " $claim " )
88- local attempt_id
89142 attempt_id=$( uuidv4)
90- local started
91143 started=$( iso_now)
92144
93- # POST /api/verify to echidna
94- local verify_body
95- verify_body=$( jq -Rs --arg p " $prover " ' {prover:$p, content:.}' < " $file " )
96-
97- local t0_ms t1_ms duration_ms
98- t0_ms=$( date +%s%3N)
99- local verify_resp
100- if [ " $DRY_RUN " = " 1" ]; then
101- verify_resp=' {"valid":true,"goals_remaining":0}'
102- else
103- verify_resp=$( curl -sS -X POST " $ECHIDNA_URL /api/verify" \
104- -H ' Content-Type: application/json' -d " $verify_body " \
105- --max-time 60 || echo ' {"valid":false,"error":"http_failure"}' )
106- fi
107- t1_ms=$( date +%s%3N)
108- duration_ms=$(( t1 _ms - t0 _ms))
109-
110- local valid outcome
111- valid=$( jq -r ' .valid // false' <<< " $verify_resp" )
112- if [ " $valid " = " true" ]; then
113- outcome=" success"
114- elif jq -e ' has("error")' <<< " $verify_resp" > /dev/null; then
115- outcome=" unknown"
116- else
117- outcome=" failure"
118- fi
119-
120- local completed
145+ local runner_output duration_ms outcome
146+ runner_output=$( run_prover " $prover " " $file " )
147+ duration_ms=$( echo " $runner_output " | sed -n ' 1p' )
148+ outcome=$( echo " $runner_output " | sed -n ' 2p' )
121149 completed=$( iso_now)
122- local prover_lc
123- prover_lc=$( prover_lower " $prover " )
124150
125- # POST /api/v1/proof_attempts to verisim-api
151+ # Build + send insert
126152 local insert_body
127153 insert_body=$( jq -n \
128154 --arg attempt_id " $attempt_id " \
@@ -131,14 +157,14 @@ verify_one() {
131157 --arg file " $rel_file " \
132158 --arg claim " $claim " \
133159 --arg obligation_class " $obligation_class " \
134- --arg prover_used " $prover_lc " \
160+ --arg prover_used " $prover " \
135161 --arg outcome " $outcome " \
136162 --argjson duration_ms " $duration_ms " \
137163 --argjson confidence 0.5 \
138164 --arg strategy_tag " $STRATEGY_TAG " \
139165 --arg started_at " $started " \
140166 --arg completed_at " $completed " \
141- --arg prover_output " $( printf ' %s ' " $verify_resp " | head -c 512 ) " \
167+ --arg prover_output " direct: $prover " \
142168 ' {attempt_id:$attempt_id, obligation_id:$obligation_id, repo:$repo, file:$file,
143169 claim:$claim, obligation_class:$obligation_class, prover_used:$prover_used,
144170 outcome:$outcome, duration_ms:$duration_ms, confidence:$confidence,
@@ -147,43 +173,48 @@ verify_one() {
147173 prover_output:$prover_output, error_message:null}' )
148174
149175 if [ " $DRY_RUN " != " 1" ]; then
150- curl -sS -X POST " $VERISIM_URL /api/v1/proof_attempts" \
176+ local resp
177+ resp=$( curl -sS -X POST " $VERISIM_URL /api/v1/proof_attempts" \
151178 -H ' Content-Type: application/json' -d " $insert_body " \
152- --max-time 10 > /dev/null || echo " insert failed" >&2
179+ --max-time 10 -w ' \n%{http_code}' )
180+ local http_code=" ${resp##* $' \n ' } "
181+ if [ " $http_code " != " 200" ] && [ " $http_code " != " 201" ]; then
182+ echo " insert failed (HTTP $http_code ): ${resp% $' \n ' * } " >&2
183+ fi
153184 fi
154185
155- # Emit notification + log line
156186 local icon urgency
157187 case " $outcome " in
158188 success) icon=" ✓" ; urgency=" normal" ;;
159189 failure) icon=" ✗" ; urgency=" critical" ;;
190+ timeout) icon=" ⏱" ; urgency=" critical" ;;
160191 * ) icon=" ?" ; urgency=" low" ;;
161192 esac
162- printf " %s %-8s %-7s %5dms %s\n" \
163- " $icon " " $outcome " " $prover_lc " " $duration_ms " " $rel_file "
193+ printf " %s %-8s %-7s %6dms %s\n" \
194+ " $icon " " $outcome " " $prover " " $duration_ms " " $rel_file "
164195 notify " $urgency " \
165196 " $icon $repo : $outcome " \
166- " $prover_lc on $rel_file (${duration_ms} ms, class=$obligation_class )"
197+ " $prover on $rel_file (${duration_ms} ms, class=$obligation_class )"
198+
199+ rate_sleep
167200}
168201
169202# --- main loop --------------------------------------------------------------
170203
171- echo " batch_driver: echidna= $ECHIDNA_URL verisim=$VERISIM_URL dry_run=$DRY_RUN "
204+ echo " batch_driver: verisim=$VERISIM_URL dry_run=$DRY_RUN timeout= ${TIMEOUT_SEC} s rate= ${RATE_MS} ms "
172205echo
173206
174207for target in " ${TARGETS[@]} " ; do
175208 IFS=' |' read -r repo obligation_class prover glob_root extension <<< " $target"
176209 dir=" $REPOS_ROOT /$repo /$glob_root "
177210 if [ ! -d " $dir " ]; then
178- echo " [$repo ] SKIP: $dir not present"
179- continue
211+ echo " [$repo ] SKIP: $dir not present" ; continue
180212 fi
181213
182214 # shellcheck disable=SC2207
183215 files=($( find " $dir " -type f -name " *.${extension} " 2> /dev/null | head -n " $MAX_FILES " ) )
184216 if [ " ${# files[@]} " -eq 0 ]; then
185- echo " [$repo ] SKIP: no *.${extension} under $glob_root "
186- continue
217+ echo " [$repo ] SKIP: no *.${extension} under $glob_root " ; continue
187218 fi
188219
189220 echo " [$repo ] target class=$obligation_class prover=$prover glob=$glob_root /*.$extension (${# files[@]} files)"
195226
196227# Summary
197228if [ " $DRY_RUN " != " 1" ]; then
198- echo " === current strategy snapshot ==="
229+ echo " === current strategy snapshot (strategy_tag= $STRATEGY_TAG only) ==="
199230 for cls in safety linearity termination equiv correctness totality; do
200231 row=$( curl -sS " $VERISIM_URL /api/v1/proof_attempts/strategy?class=$cls &limit=3" 2> /dev/null)
201232 top=$( jq -r ' .recommendations[0] // empty | "top=\(.prover) rate=\(.success_rate) n=\(.total_attempts)"' <<< " $row" 2> /dev/null)
0 commit comments