Skip to content

Commit d72883f

Browse files
hyperpolymathclaude
andcommitted
fix(batch_driver): M1+M2+M6 — bypass echidna, direct prover invocation
M1 (corrective): iso_now() now uses %3N for fractional-second precision. Previous .000 hardcode caused evidence ordering to be non-deterministic when multiple attempts landed in the same second. M2 (corrective): rewrite to shell out to prover binaries directly. ROOT CAUSE: echidna's /api/verify parses input into an abstract ProofState via parse_string(), then re-exports through state.export() before running the backend. The round trip loses the original proof body — Idris2 writes ?main_todo holes, Coq/Lean get near-empty files. Result: valid=true on garbage input to Coq, valid=false on any real Idris2 file. Direct invocation: z3, coqc -q, lean. Each runner returns (outcome, duration_ms) from actual exit status + wall-clock timing. Timeout via `timeout ${TIMEOUT_SEC}s` with outcome=timeout on exit 124. strategy_tag bumped to "batch-direct" so the new ground-truth rows are distinguishable from earlier polluted "batch-manual" signal. 29 polluted rows purged from proof_attempts via ALTER DELETE. M6 (perfective): RATE_MS env var sleeps between provers (default 100ms), TIMEOUT_SEC env var (default 30) caps per-prover wall-clock. Accept both HTTP 200 and 201 from verisim-api (was rejecting 201-Created as failure). Idris2 + Agda targets dropped until their direct invocation is added — both need more elaborate setup (package paths, std lib imports). Echidna's /api/verify bug tracked separately as echidna-side issue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 24a8105 commit d72883f

1 file changed

Lines changed: 123 additions & 92 deletions

File tree

bots/echidnabot/scripts/batch_driver.sh

Lines changed: 123 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,154 @@
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

2222
set -euo pipefail
2323

24-
ECHIDNA_URL="${ECHIDNA_URL:-http://127.0.0.1:8090}"
2524
VERISIM_URL="${VERISIM_URL:-http://127.0.0.1:8080}"
2625
NOTIFY="${NOTIFY:-1}"
2726
DRY_RUN="${DRY_RUN:-0}"
2827
MAX_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

3132
REPOS_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.
3738
TARGETS=(
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

6451
obligation_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-
7355
notify() {
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

81135
verify_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"
172205
echo
173206

174207
for 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)"
@@ -195,7 +226,7 @@ done
195226

196227
# Summary
197228
if [ "$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

Comments
 (0)