diff --git a/Justfile b/Justfile index 5adaa59a..8486f842 100644 --- a/Justfile +++ b/Justfile @@ -2,1218 +2,29 @@ set shell := ["bash", "-uc"] set dotenv-load := true -import? "contractile.just" - project := "echidna" # Show all recipes default: @just --list --unsorted -# Initialize from RSR template (placeholder replacement) -init: - #!/usr/bin/env bash - set -euo pipefail - - echo "═══════════════════════════════════════════════════" - echo " RSR Project Bootstrap" - echo "═══════════════════════════════════════════════════" - echo "" - - # --- Load defaults from config (if exists) --- - # Create yours: ~/.config/rsr/defaults - # Format: OWNER=myorg AUTHOR="My Name" AUTHOR_EMAIL=me@example.org ... - DEFAULTS="${XDG_CONFIG_HOME:-$HOME/.config}/rsr/defaults" - if [ -f "$DEFAULTS" ]; then - echo "Loading defaults from $DEFAULTS" - # shellcheck source=/dev/null - source "$DEFAULTS" - echo "" - fi - - # --- Required values (pre-filled from defaults if available) --- - read -rp "Project name (human-readable, e.g. My Project): " PROJECT_NAME - [ -z "$PROJECT_NAME" ] && echo "Error: project name required" && exit 1 - - read -rp "Repository slug (e.g. my-project): " REPO - [ -z "$REPO" ] && echo "Error: repo slug required" && exit 1 - - read -rp "Owner [${OWNER:-}]: " _OWNER - OWNER="${_OWNER:-${OWNER:-}}" - [ -z "$OWNER" ] && echo "Error: owner required" && exit 1 - - read -rp "Author full name [${AUTHOR:-}]: " _AUTHOR - AUTHOR="${_AUTHOR:-${AUTHOR:-}}" - [ -z "$AUTHOR" ] && echo "Error: author name required" && exit 1 - - read -rp "Author email [${AUTHOR_EMAIL:-}]: " _AUTHOR_EMAIL - AUTHOR_EMAIL="${_AUTHOR_EMAIL:-${AUTHOR_EMAIL:-}}" - [ -z "$AUTHOR_EMAIL" ] && echo "Error: email required" && exit 1 - - # --- Optional values (pre-filled from defaults if available) --- - read -rp "Author organization [${AUTHOR_ORG:-none}]: " _AUTHOR_ORG - AUTHOR_ORG="${_AUTHOR_ORG:-${AUTHOR_ORG:-}}" - - read -rp "Previous/alt email [${AUTHOR_EMAIL_ALT:-none}]: " _AUTHOR_EMAIL_ALT - AUTHOR_EMAIL_ALT="${_AUTHOR_EMAIL_ALT:-${AUTHOR_EMAIL_ALT:-}}" - - read -rp "Project description []: " PROJECT_DESCRIPTION - - read -rp "Forge domain [${FORGE:-github.com}]: " _FORGE - FORGE="${_FORGE:-${FORGE:-github.com}}" - - read -rp "Security contact email [${SECURITY_EMAIL:-$AUTHOR_EMAIL}]: " _SECURITY_EMAIL - SECURITY_EMAIL="${_SECURITY_EMAIL:-${SECURITY_EMAIL:-$AUTHOR_EMAIL}}" - - read -rp "Conduct contact email [${CONDUCT_EMAIL:-$AUTHOR_EMAIL}]: " _CONDUCT_EMAIL - CONDUCT_EMAIL="${_CONDUCT_EMAIL:-${CONDUCT_EMAIL:-$AUTHOR_EMAIL}}" - - read -rp "Project type (library|binary|monorepo|service|website) [library]: " PROJECT_TYPE - PROJECT_TYPE="${PROJECT_TYPE:-library}" - - read -rp "Website URL [https://${FORGE}/${OWNER}/${REPO}]: " WEBSITE - WEBSITE="${WEBSITE:-https://${FORGE}/${OWNER}/${REPO}}" - - # --- Container values (optional — only relevant if container/ exists) --- - if [ -d "container" ]; then - echo "" - echo "── Container configuration (optional) ─────────" - read -rp "Service name [${REPO}]: " _SERVICE_NAME - SERVICE_NAME="${_SERVICE_NAME:-${REPO}}" - read -rp "Primary port [8080]: " _PORT - PORT="${_PORT:-8080}" - read -rp "Container registry [ghcr.io/${OWNER}]: " _REGISTRY - REGISTRY="${_REGISTRY:-ghcr.io/${OWNER}}" - else - SERVICE_NAME="${REPO}" - PORT="8080" - REGISTRY="ghcr.io/${OWNER}" - fi - - # --- Derived values --- - PROJECT_UPPER=$(echo "$REPO" | tr '[:lower:]-' '[:upper:]_') - PROJECT_LOWER=$(echo "$REPO" | tr '[:upper:]-' '[:lower:]_') - CURRENT_YEAR=$(date +%Y) - CURRENT_DATE=$(date +%Y-%m-%d) - VERSION="0.1.0" - - # Derive citation name parts (best-effort split on last space) - AUTHOR_LAST="${AUTHOR##* }" - AUTHOR_FIRST="${AUTHOR% *}" - FIRST_INITIAL="${AUTHOR_FIRST:0:1}." - if [ "$AUTHOR_LAST" = "$AUTHOR_FIRST" ]; then - AUTHOR_FIRST="$AUTHOR" - AUTHOR_LAST="" - FIRST_INITIAL="" - fi - - echo "" - echo "── Summary ──────────────────────────────────────" - echo " Project: $PROJECT_NAME" - echo " Repo: $REPO" - echo " Owner: $OWNER" - echo " Author: $AUTHOR <$AUTHOR_EMAIL>" - [ -n "$AUTHOR_ORG" ] && echo " Organization: $AUTHOR_ORG" - echo " Forge: $FORGE" - echo " Year: $CURRENT_YEAR" - echo "────────────────────────────────────────────────" - echo "" - read -rp "Proceed? [Y/n] " CONFIRM - [[ "${CONFIRM:-Y}" =~ ^[Nn] ]] && echo "Aborted." && exit 0 - - echo "" - echo "Replacing placeholders..." - - # Brace tokens as variables (hex avoids just interpolation) - LB=$(printf '\x7b\x7b') - RB=$(printf '\x7d\x7d') - - # Build the sed expression list - # Note: using | as delimiter since URLs contain / - SED_ARGS=( - -e "s|${LB}PROJECT_NAME${RB}|${PROJECT_NAME}|g" - -e "s|${LB}PROJECT_DESCRIPTION${RB}|${PROJECT_DESCRIPTION}|g" - -e "s|${LB}PROJECT${RB}|${PROJECT_UPPER}|g" - -e "s|${LB}project${RB}|${PROJECT_LOWER}|g" - -e "s|${LB}REPO${RB}|${REPO}|g" - -e "s|${LB}OWNER${RB}|${OWNER}|g" - -e "s|${LB}AUTHOR${RB}|${AUTHOR}|g" - -e "s|${LB}AUTHOR_EMAIL${RB}|${AUTHOR_EMAIL}|g" - -e "s|${LB}AUTHOR_ORG${RB}|${AUTHOR_ORG}|g" - -e "s|${LB}AUTHOR_LAST${RB}|${AUTHOR_LAST}|g" - -e "s|${LB}AUTHOR_FIRST${RB}|${AUTHOR_FIRST}|g" - -e "s|${LB}AUTHOR_INITIALS${RB}|${FIRST_INITIAL}|g" - -e "s|${LB}FORGE${RB}|${FORGE}|g" - -e "s|${LB}CURRENT_YEAR${RB}|${CURRENT_YEAR}|g" - -e "s|${LB}CURRENT_DATE${RB}|${CURRENT_DATE}|g" - -e "s|${LB}DATE${RB}|${CURRENT_DATE}|g" - -e "s|${LB}SECURITY_EMAIL${RB}|${SECURITY_EMAIL}|g" - -e "s|${LB}CONDUCT_EMAIL${RB}|${CONDUCT_EMAIL}|g" - -e "s|${LB}LICENSE${RB}|MPL-2.0|g" - -e "s|${LB}CONDUCT_TEAM${RB}|Code of Conduct Committee|g" - -e "s|${LB}RESPONSE_TIME${RB}|48 hours|g" - -e "s|${LB}MAIN_BRANCH${RB}|main|g" - -e "s|${LB}PROJECT_PURPOSE${RB}|${PROJECT_DESCRIPTION}|g" - -e "s|${LB}PROJECT_ROLE${RB}|${PROJECT_TYPE}|g" - -e "s|${LB}PROJECT_TYPE${RB}|${PROJECT_TYPE}|g" - -e "s|${LB}WEBSITE${RB}|${WEBSITE}|g" - -e "s|${LB}SERVICE_NAME${RB}|${SERVICE_NAME}|g" - -e "s|${LB}PORT${RB}|${PORT}|g" - -e "s|${LB}REGISTRY${RB}|${REGISTRY}|g" - -e "s|${LB}IMAGE${RB}|${REGISTRY}/${SERVICE_NAME}|g" - -e "s|${LB}VERSION${RB}|${VERSION}|g" - -e "s|${LB}EMAIL${RB}|${AUTHOR_EMAIL}|g" - ) - [ -n "$AUTHOR_EMAIL_ALT" ] && SED_ARGS+=(-e "s|${LB}AUTHOR_EMAIL_ALT${RB}|${AUTHOR_EMAIL_ALT}|g") - - # Replace in all text files (skip .git, LICENSE text, and binaries) - find . -type f \ - -not -path './.git/*' \ - -not -name 'MPL-2.0.txt' \ - -not -name '*.png' -not -name '*.jpg' -not -name '*.gif' \ - -not -name '*.woff' -not -name '*.woff2' \ - | while read -r file; do - if file --brief "$file" | grep -qi 'text\|ascii\|utf'; then - sed -i "${SED_ARGS[@]}" "$file" - fi - done - - # Also replace [YOUR-REPO-NAME] and [YOUR-NAME/ORG] in AI manifest - sed -i "s|\[YOUR-REPO-NAME\]|${PROJECT_NAME}|g" 0-AI-MANIFEST.a2ml 2>/dev/null || true - sed -i "s|\[YOUR-NAME/ORG\]|${OWNER}|g" 0-AI-MANIFEST.a2ml 2>/dev/null || true - - echo "" - echo "── Validation ───────────────────────────────────" - - # Check for remaining placeholders - PATTERN="${LB}[A-Z_]*${RB}" - REMAINING=$(grep -rl "$PATTERN" . --include='*.md' --include='*.adoc' --include='*.yml' --include='*.yaml' --include='*.a2ml' --include='*.toml' --include='*.scm' --include='*.ncl' --include='*.guix' --include='*.json' --include='*.sh' 2>/dev/null | grep -v '.git/' | grep -v '.machine_readable/ai/PLACEHOLDERS.adoc' || true) - if [ -n "$REMAINING" ]; then - echo "WARNING: Remaining placeholders in:" - echo "$REMAINING" | sed 's/^/ /' - echo "" - echo "Run: grep -rn '$LB' . --include='*.md' to inspect" - else - echo "All placeholders replaced successfully!" - fi - - echo "" - echo "Done! Next steps:" - echo " 1. Review changes: git diff" - echo " 2. Remove template cruft: rm .machine_readable/ai/PLACEHOLDERS.adoc" - echo " 3. Customize README.adoc for your project" - echo " 4. Commit: git add -A && git commit -m 'feat: initialize from RSR template'" - echo " 5. Push: git remote add origin git@github.com:$OWNER/$REPO.git && git push -u origin main" - -# Build debug -build: - cargo build - -# Build release -build-release: - cargo build --release - -# Run unit tests -test: - cargo test --lib - -# Self-test the wave gates (scripts/gates/*.selftest.sh): fixtures, no network -test-gates: - bash scripts/gates/lib.selftest.sh - bash scripts/gates/w0.selftest.sh - -# Run all tests (unit + integration) -test-all: - cargo test - -# Run tests verbose -test-verbose: - cargo test -- --nocapture - -# Run integration tests only -test-integration: - cargo test --test integration_tests - -# Run neural integration tests (requires Julia server) -test-neural: - ./scripts/test-integration.sh - -# Run the S4 loop-closure integration test against a VeriSimDB instance. -# Skips with a notice when VeriSimDB is unreachable (no failure). -# Set VERISIM_URL to override the default of http://localhost:8080. -# -# Local dev: start verisim-api beforehand (see docs/runbooks/s4-loop-closure.md); -# the test will exercise the full record_proof_attempt → query_by_goal_hash loop. -test-s4-loop: - @echo "Running S4 loop-closure test against ${VERISIM_URL:-http://localhost:8080}" - @echo "Test will skip cleanly if the endpoint is unreachable." - cargo test --features verisim --test s4_loop_closure -- --nocapture - -# ── Dogfood proof corpus (proofs/{coq,lean,agda} + src/idris validator) ── -# Each recipe assumes its toolchain is already installed (see `just doctor`); -# CI installs the toolchains first, then calls these so local and CI run the -# exact same commands. - -# Type-check the whole dogfood proof corpus across every assistant. -proofs: proofs-coq proofs-lean proofs-agda proofs-idris proofs-verif-lean proofs-verif-idris - -# Compile the Coq proof corpus (proofs/coq/**/*.v). -proofs-coq: - #!/usr/bin/env bash - set -euo pipefail - cd proofs/coq - for f in $(find . -name '*.v' | sort); do - echo "coqc $f" - coqc -q "$f" - done - echo "Coq corpus: all files type-check" - -# Build the Lean 4 proof corpus via Lake (proofs/lean). -proofs-lean: - cd proofs/lean && lake build - -# Type-check the Agda proof corpus (proofs/agda/*.agda). -proofs-agda: - #!/usr/bin/env bash - set -euo pipefail - # Agda writes UTF-8 to stdout and aborts under a non-UTF-8 locale (C/POSIX). - export LC_ALL="${LC_ALL:-C.UTF-8}" - cd proofs/agda - for f in $(find . -name '*.agda' -not -path './_build/*' -not -path './dist-newstyle/*' | sort); do - echo "agda --safe --without-K --cubical-compatible $f" - agda --safe --without-K --cubical-compatible "$f" - done - echo "Agda corpus: all files type-check" - -# Type-check the Idris2 validator (src/idris) — --typecheck needs no codegen backend. -proofs-idris: - cd src/idris && idris2 --typecheck echidna-validator.ipkg - -# Distinct from proofs-lean: the verification-pipeline *property* proofs -# (ConfidenceLattice, ParetoMaximality, ParetoStrongMaximality, IntegrityVerification, -# PortfolioCompleteness) under verification/proofs/lean4, not the dogfood corpus under -# proofs/lean. No mathlib dependency (lake-manifest packages: []), so the build is light. -# Build the Lean 4 trust-pipeline property proofs via Lake (verification/proofs/lean4). -proofs-verif-lean: - cd verification/proofs/lean4 && lake build - -# DispatchCorrectness imports EchidnaABI.Types, so the echidnaabi package -# (src/abi) is installed first (--install builds it on the way); the ipkg then -# type-checks all 11 modules with a reliable aggregate exit code. A bare per-file -# `idris2 --check` is unsafe here -- it exits 0 even on an unresolved import -- so -# the ipkg build is the gate and any "Error" line is also treated as failure. -# Type-check the Idris2 trust-pipeline property proofs (verification/proofs/idris2). -proofs-verif-idris: - #!/usr/bin/env bash - set -euo pipefail - (cd src/abi && idris2 --install echidnaabi.ipkg >/dev/null) - cd verification/proofs/idris2 - out=$(idris2 --typecheck echidna-verif-proofs.ipkg 2>&1) - printf '%s\n' "$out" - if printf '%s\n' "$out" | grep -qE '^Error'; then - echo "Idris2 verification corpus: TYPE ERRORS DETECTED" >&2 - exit 1 - fi - echo "Idris2 verification corpus: all files type-check" - -# Accept fixtures must type-check, reject fixtures must be refused (tests/abi). -# Type-check the Idris2 ABI accept/reject fixtures (mechanical falsifier). -abi-fixtures: - bash scripts/abi-fixtures.sh - -# Heavy (downloads the ~500MB Isabelle release), so this is gated by the weekly -# verification-proofs-cron workflow, not the per-PR `proofs` rollup. Covers the -# base-HOL theories; algebra/GroupTheory.thy is excluded (see proofs/isabelle/ROOT). -# Verify the Isabelle/HOL self-proof corpus (proofs/isabelle) via `isabelle build`. -proofs-isabelle: - cd proofs/isabelle && isabelle build -d . -v Echidna_Isabelle - -# Mizar is not packaged for apt and ships only from mizar.org (Tier-4 in this -# project); like Isabelle it is gated by the weekly cron, not per-PR. MIZFILES -# must point at the Mizar share dir. Errors are reported in a per-file .err file -# (the verifier's exit code is not reliable), so a non-empty .err fails the recipe. -# Verify the Mizar self-proof corpus (proofs/mizar) with the Mizar verifier. -proofs-mizar: - #!/usr/bin/env bash - # Not -e: makeenv/verifier signal errors via a per-file .err (line col code), - # not a reliable exit code, so inspect that rather than abort on first non-zero. - set -uo pipefail - : "${MIZFILES:?MIZFILES must point at the Mizar share dir}" - cd proofs/mizar - rc=0 - for f in *.miz; do - base="${f%.miz}" - echo "=== $f ===" - makeenv "$f" || echo "(makeenv exit $?)" - verifier "$f" || echo "(verifier exit $?)" - if [ -s "$base.err" ]; then - echo "--- $base.err (line col code) ---" - cat "$base.err" - while read -r line col code; do - src="$(sed -n "${line}p" "$f" 2>/dev/null)" - [ -n "$src" ] && echo " L${line}:${col} (code ${code}): ${src}" - done < "$base.err" - rc=1 - else - echo "($f verifies clean)" - fi - done - [ "$rc" -eq 0 ] || { echo "Mizar corpus: VERIFICATION ERRORS (see .err above)" >&2; exit 1; } - echo "Mizar corpus: all files verify" - -# Format code -fmt: - cargo fmt - -# Check formatting -fmt-check: - cargo fmt -- --check - -# Run clippy lints -lint: - cargo clippy -- -D warnings - -# Check without building -check: - cargo check - -# Clean build artifacts -clean: - cargo clean - -# Run the project -run *ARGS: - cargo run -- {{ARGS}} - -# Run Invariant Path overlay tools for this repository -invariant-path *ARGS: - ./scripts/invariant-path.sh {{ARGS}} - -# Rebuild the canonical seed vocabulary (training_data/vocabulary_CANON.txt). -# Two stages: (1) mine frequency-filtered identifiers from the per-prover -# proof corpora, then (2) union with the hand-curated sets. Consumed by -# src/julia/training/dataloader.jl:build_vocabulary_from_data. -vocab-canon: - julia scripts/vocabulary_mine_corpus.jl - julia scripts/vocabulary_canonicalize.jl - -# ── Corpus / training pipeline ───────────────────────────────── -# -# Five steps in order: provision upstream mirrors, run every extractor, -# merge per-prover proof_states into UNIFIED, align the premise files -# to UNIFIED's fresh ids, then retrain. Each step is idempotent; -# re-running only touches what has changed. See -# scripts/provision_corpora.sh --list for the source catalogue. - -# Clone every upstream prover corpus into external_corpora/. -# Pass specific names to provision a subset: `just provision-corpora metamath mathlib4`. -provision-corpora *NAMES: - @if [ -z "{{NAMES}}" ]; then \ - scripts/provision_corpora.sh --all; \ - else \ - scripts/provision_corpora.sh {{NAMES}}; \ - fi - -# Report which upstream corpora are on disk and their sizes. -corpora-status: - scripts/provision_corpora.sh --status - -# Run every scripts/extract_*.jl against the provisioned corpora. -# Pass names to run a subset: `just extract-corpora metamath mathlib4`. -extract-corpora *NAMES: - scripts/extract_all.sh {{NAMES}} - -# Merge per-prover proof_states_*.jsonl into proof_states_UNIFIED.jsonl -# with fresh sequential ids (dedupes by prover+theorem). -merge-corpora: - julia --project=src/julia scripts/merge_corpus.jl - -# Rebuild premises_COMPLETE.jsonl with proof_ids that match UNIFIED. -# merge-corpora rewrites every proof_state id to a fresh sequential -# counter; the premise files keep the original extractor ids, so this -# step re-joins premises to UNIFIED via (prover, theorem) — the durable -# key merge_corpus.jl already dedupes on. Without this step the -# dataloader's proof_id join matches ~0% of records. -align-premises: - julia --project=src/julia scripts/align_premises.jl - -# Full retrain from provisioned corpora. Honours ECHIDNA_MAX_PROOF_STATES -# (0 = unlimited), ECHIDNA_NUM_EPOCHS, ECHIDNA_NUM_NEGATIVES. -# -# Depends on align-premises: without that step the dataloader joins -# premise.proof_id against proof_state.id and fails at ~0% match because -# merge_corpus.jl rewrites proof_state ids to fresh sequential counters -# while the premise files keep the original per-extractor ids. Running -# align-premises is idempotent and cheap (seconds on a 2M premise set). -retrain: align-premises - julia --project=src/julia src/julia/run_training.jl - -# Retrain without the align-premises prerequisite — use only when you -# know premises_COMPLETE.jsonl is already aligned (e.g. you just ran -# just align-premises manually and want to iterate on model code). -retrain-skip-align: - julia --project=src/julia src/julia/run_training.jl - -# Full training run on the real corpus (auto-selects GPU when available). -# Honours ECHIDNA_MAX_PROOF_STATES, ECHIDNA_NUM_EPOCHS, ECHIDNA_NUM_NEGATIVES. -# Produces models/neural/{premise_selector,tactic_predictor}.bson, vocab.json, -# updates models/model_metadata.txt, and appends to training_data/metrics_baseline.jsonl. -train: - julia --project=src/julia src/julia/run_training.jl - -# CPU smoke pass: 2000 proof states, 2 epochs — fast end-to-end sanity check. -# Safe to run on any dev box without a GPU. Produces the same artefacts as `train`. -train-cpu: - ECHIDNA_MAX_PROOF_STATES=2000 ECHIDNA_NUM_EPOCHS=2 julia --project=src/julia src/julia/run_training_cpu.jl - -# Evaluate trained model on held-out validation split (MRR / top-k). -# Requires `just train-cpu` (or `just train`) to have produced models/neural/. -# Appends one JSONL row to training_data/eval_results.jsonl and exits non-zero -# if MRR < 0.66 (below the cosine baseline). -eval: - julia --project=src/julia src/julia/eval_held_out.jl - -# End-to-end pipeline: provision → extract → merge → align → retrain. -# Use `ECHIDNA_MAX_PROOF_STATES=0 just corpus-refresh` to lift the sample cap. -corpus-refresh: provision-corpora extract-corpora merge-corpora align-premises retrain - -# Run training using the saturation-campaign corpus adapters as the -# data source. Requires `just provision-corpora` to have populated -# data/corpus/ first. Per-prover invocation; default lean. -train-from-corpus prover="lean": - @julia --project=src/julia src/julia/run_training.jl \ - --prover {{prover}} \ - --corpus-json $(ls data/corpus/*.json | tr '\n' ',') \ - --synonyms-dir data/synonyms - -# Run the eight-axis metrics suite against the current corpus and post -# results to VeriSimDB. Falls back to training_data/metrics_.jsonl -# if VERISIM_URL is unreachable. Target values are documented in -# metrics/README.adoc. -metrics: - julia --project=src/julia metrics/run_all.jl - -# Report corpus balance across provers from stats_UNIFIED.json. -corpus-stats: - @julia -e 'using JSON3, Printf; \ - s = JSON3.read(read("training_data/stats_UNIFIED.json", String)); \ - counts = s.per_prover_counts; \ - pairs = sort(collect(counts), by = x -> -x[2]); \ - total = sum(last.(pairs)); \ - println("Total proofs: ", total, " provers with data: ", length(pairs)); \ - println("Rank Prover Count Share"); \ - for (i,(p,c)) in enumerate(pairs); \ - @printf("%3d %-25s %7d %5.2f%%\n", i, p, c, 100c/total); \ - end' - -# Generate docs -doc: - cargo doc --no-deps --open - -# Update dependencies -update: - cargo update - -# Regenerate Cap'n Proto bindings from schemas/echidna.capnp. -# Requires capnp compiler: https://capnproto.org/install.html -# Output: src/rust/ipc/echidna_capnp.rs (checked in; do not edit by hand) -capnp-gen: - @command -v capnp >/dev/null 2>&1 || { echo "capnp compiler not found — install from https://capnproto.org/install.html"; exit 1; } - capnp compile -orust schemas/echidna.capnp -I /usr/include - @echo "Rust bindings generated. Commit src/rust/ipc/echidna_capnp.rs." - -# Audit dependencies -audit: - cargo audit - -# ── UI Management ────────────────────────────────────────── - -# ── UI ── -# The AffineScript UI was removed 2026-08 (AffineScript is a banned language under the -# estate language policy; AffineScript-TEA is the replacement). The TEA sources -# live in src/ui/tea/ and the static shell in src/ui/public/, but the -# AffineScript compile pipeline is NOT wired yet — see issues #117 and #266 -# (missing Http::fetch / Async / Json primitives). - -# Build the UI — blocked on the AffineScript-TEA toolchain -build-ui: - @echo "build-ui is unavailable: the AffineScript UI was removed and the" - @echo "AffineScript-TEA pipeline is not wired yet (issues #117, #266)." - @echo "Sources: src/ui/tea/echidna_gui.affine; shell: src/ui/public/." - @exit 1 - -# Serve the static UI shell (no compile step) on http://localhost:3000. -# Bun is the estate JavaScript runtime; Deno is retired (2026-09-22 ruling). -serve-ui: - cd src/ui/public && bun ../../../scripts/serve-static.js 3000 - -# Launch full ECHIDNA GUI (Backend + UI + Browser) -gui: - #!/usr/bin/env bash - set -euo pipefail - echo "Launching ECHIDNA GUI..." - # Start backend - cargo run -- server --port 8081 --cors > /dev/null 2>&1 & BACKEND_PID=$! - echo "Started backend (PID $BACKEND_PID)" - # Start UI server - (cd src/ui/public && bun ../../../scripts/serve-static.js 3000) > /dev/null 2>&1 & UI_PID=$! - echo "Started UI server (PID $UI_PID)" - # Cleanup on exit - trap "kill $BACKEND_PID $UI_PID 2>/dev/null || true" EXIT - # Wait for servers to start - echo "Waiting for servers to initialize..." - sleep 2 - # Open browser - URL="http://localhost:3000" - echo "Opening $URL..." - xdg-open "$URL" > /dev/null 2>&1 || open "$URL" > /dev/null 2>&1 || echo "Please open $URL in your browser" - echo "ECHIDNA GUI is running. Press Ctrl+C in this terminal to stop." - # Wait for background processes - wait - -# All checks before commit -pre-commit: fmt-check lint test - @echo "All checks passed!" - -# MVP 1.0 smoke checks -mvp: - ./scripts/mvp-smoke.sh - -# MVP 1.0 dependency checklist (best-effort) -mvp-env: - ./scripts/mvp-smoke.sh || true - -# Build minimal container image (Z3, CVC5, Lean, Idris2) -container-build: - podman build -f .containerization/Containerfile -t echidna:latest . - -# Build full container image (all provers + Julia) -container-build-full: - podman build -f .containerization/Containerfile.full -t echidna:full . - -# ── Tier-3 prover containers (Wave-3) ──────────────────────────────────────── -# All nine targets live in ONE consolidated multi-target Containerfile.wave3. -# The Rust core is compiled once (shared `rust-builder` stage) and every prover -# image is a `--target`; podman caches the shared stage across builds on a host. - -# Build all non-proprietary Tier-3 prover containers (skip imandra — licence required) -container-build-tier3: container-build-tamarin container-build-proverif container-build-metamath \ - container-build-twelf container-build-or-tools container-build-scip \ - container-build-hol4 container-build-acl2 - -container-build-tamarin: - podman build -f .containerization/Containerfile.wave3 --target tamarin -t echidna:tamarin . - -container-build-proverif: - podman build -f .containerization/Containerfile.wave3 --target proverif -t echidna:proverif . - -container-build-metamath: - podman build -f .containerization/Containerfile.wave3 --target metamath -t echidna:metamath . - -container-build-twelf: - podman build -f .containerization/Containerfile.wave3 --target twelf -t echidna:twelf . - -container-build-or-tools: - podman build -f .containerization/Containerfile.wave3 --target or-tools -t echidna:or-tools . - -container-build-scip: - podman build -f .containerization/Containerfile.wave3 --target scip -t echidna:scip . - -container-build-hol4: - podman build -f .containerization/Containerfile.wave3 --target hol4 -t echidna:hol4 . - -container-build-acl2: - podman build -f .containerization/Containerfile.wave3 --target acl2 -t echidna:acl2 . - -# Build imandra stub (proprietary; real binary requires a signed licence — see Containerfile.wave3 imandra target) -container-build-imandra: - podman build -f .containerization/Containerfile.wave3 --target imandra -t echidna:imandra . - -# Run echidna container -container-run *ARGS: - podman run --rm -it echidna:latest {{ARGS}} - -# ── Chapel Accelerator ────────────────────────────────────── - -# Compile the Chapel metalayer into a static library. -# --static (not --dynamic) because the apt-shipped Chapel runtime is -# CHPL_LIB_PIC=none only; see .github/workflows/chapel-ci.yml for the -# longer explanation and #133 for the closure trace. -chapel-build: - @echo "Compiling Chapel metalayer (static)..." - cd src/chapel && chpl --library --static -I ../zig_ffi \ - -o libechidna_chapel \ - chapel_ffi_exports.chpl parallel_proof_search.chpl - @echo "Built src/chapel/lib/libechidna_chapel.a" - -# Build the Chapel "hello" smoke target — exercises coforall + atomic -# CAS + reduction. Used in CI as the toolchain-health signal. -chapel-smoke: - cd src/chapel && chpl -o chapel_smoke smoke.chpl && ./chapel_smoke - -# Run the Chapel-gated Rust tests (`--features chapel`). -chapel-test: build-chapel-ffi - cargo test --locked --features chapel --lib -- proof_search verify_proof_parallel - -# Speedup baseline: compares sequentialProofSearch / -# parallelProofSearch / parallelProofSearchSpeculative against the -# fixture corpus in tests/chapel_fixtures/. Emits CSV to stdout. Doc: -# docs/bench/2026-05-30-chapel-mrr-baseline.adoc. -# -# IDRIS2_PREFIX is derived from `which idris2` because Idris2's prelude -# resolution requires the env var to be set when invoking `--check`; -# the per-prover cwd hook (#158) sets the subprocess working dir but -# doesn't help if the parent never had the env var (see -# src/chapel/parallel_proof_search.chpl ProverInfo[4]). The derived -# prefix is `dirname(dirname(realpath(which idris2)))` — works for -# both source builds and asdf-shimmed installs. -bench-chapel-mrr: - #!/usr/bin/env bash - set -euo pipefail - if command -v idris2 >/dev/null 2>&1 && [ -z "${IDRIS2_PREFIX:-}" ]; then - export IDRIS2_PREFIX="$(dirname "$(dirname "$(readlink -f "$(command -v idris2)")")")" - echo "# IDRIS2_PREFIX=$IDRIS2_PREFIX (derived from which idris2)" >&2 - fi - cd src/chapel - chpl -o bench_mrr bench_mrr.chpl - ./bench_mrr --verbose=false --timeout=10 - echo "# wrote src/chapel/bench_mrr_telemetry.csv + src/chapel/bench_mrr_summary.csv" >&2 - -# Same bench, but surface only the corpus-level outcome breakdown: the -# per-prover preempted / timed-out / success counts and the preemption -# rate next to wall-clock, one line per strategy. This is the #162 view — -# it answers "how much of the speculative win is preemption?" without -# opening the 360-row telemetry file. -bench-chapel-mrr-telemetry: - #!/usr/bin/env bash - set -euo pipefail - if command -v idris2 >/dev/null 2>&1 && [ -z "${IDRIS2_PREFIX:-}" ]; then - export IDRIS2_PREFIX="$(dirname "$(dirname "$(readlink -f "$(command -v idris2)")")")" - fi - cd src/chapel - chpl -o bench_mrr bench_mrr.chpl - ./bench_mrr --verbose=false --timeout=10 --telemetry-only=true - echo "corpus-level outcome breakdown (fixture=ALL):" - grep '^ALL,' bench_mrr_summary.csv | while IFS=, read -r _ strategy att ok fail preempt timeout na err notatt rate wall _ _; do - printf ' %-22s attempted=%-3s success=%-3s failure=%-3s preempted=%-3s timed_out=%-3s rate=%s wall=%ss\n' \ - "$strategy" "$att" "$ok" "$fail" "$preempt" "$timeout" "$rate" "$wall" - done - -# Rebuild Chapel 2.8.0 from source with CHPL_LIB_PIC=pic so that -# `chpl --library --dynamic` can produce a shared-library form of the -# metalayer. The apt deb ships only the `lib_pic-none` runtime -# variant. ~25-35 min wall on a 4-core x86_64. ~5 GB disk under -# ~/.cache/echidna/chapel-pic. Procedure + tradeoffs: -# docs/decisions/2026-05-30-chapel-pic-rebuild.adoc. NOT a CI step. -chapel-pic-from-source: - #!/usr/bin/env bash - set -euo pipefail - CACHE_DIR="${ECHIDNA_CHAPEL_PIC_CACHE:-$HOME/.cache/echidna/chapel-pic}" - CHPL_VERSION="${CHPL_VERSION:-2.8.0}" - mkdir -p "$CACHE_DIR" - cd "$CACHE_DIR" - if [ ! -f "chapel-${CHPL_VERSION}.tar.gz" ]; then - echo "Downloading chapel ${CHPL_VERSION}..." - curl -fL "https://github.com/chapel-lang/chapel/releases/download/${CHPL_VERSION}/chapel-${CHPL_VERSION}.tar.gz" \ - -o "chapel-${CHPL_VERSION}.tar.gz" - fi - if [ ! -d "chapel-${CHPL_VERSION}" ]; then - tar xf "chapel-${CHPL_VERSION}.tar.gz" - fi - cd "chapel-${CHPL_VERSION}" - export CHPL_LIB_PIC=pic - export CHPL_LLVM=bundled - export CHPL_HOME="$PWD" - source util/setchplenv.bash - echo "Building chapel runtime with CHPL_LIB_PIC=pic (~30 min)..." - make -j"$(nproc)" - echo "Verifying lib_pic-pic variant landed..." - if find lib -name 'lib_pic-pic' -type d | head -1 | grep -q .; then - echo "OK: PIC runtime built at $CHPL_HOME" - echo "Use: PATH=$CHPL_HOME/bin/linux64-x86_64:\$PATH chpl --library --dynamic ..." - else - echo "ERROR: lib_pic-pic variant not found after build" >&2 - exit 1 - fi - -# Build Zig FFI bridge for Chapel (prerequisite for --features chapel) -build-chapel-ffi: - @echo "Building Zig FFI bridge..." - cd src/zig_ffi && zig build -Doptimize=ReleaseSafe - @echo "Library built at src/zig_ffi/zig-out/lib/" - -# Build the feature against explicit unavailable stubs. -build-chapel: build-chapel-ffi - ECHIDNA_CHAPEL_EXPECT_MODE=stubs cargo build --locked --features chapel - -# Build the real static bridge and let Cargo consume Chapel's runtime closure. -build-chapel-real: chapel-build - cd src/zig_ffi && zig build -Doptimize=ReleaseSafe -Dstubs=false - ECHIDNA_CHAPEL_EXPECT_MODE=real cargo build --locked --features chapel - -chapel-test-real: build-chapel-real - ECHIDNA_CHAPEL_EXPECT_MODE=real cargo test --locked --features chapel --lib -- proof_search verify_proof_parallel - -# Alias retained for muscle-memory; same as chapel-test. -test-chapel: chapel-test - -# Test Zig FFI bridge independently -test-chapel-ffi: - cd src/zig_ffi && zig build test - -# Full local real-library path, with the independent stub bridge unit tests. -chapel-all: chapel-smoke test-chapel-ffi chapel-test-real - @echo "Real Chapel build and selected Rust/FFI tests completed" - -# ── SPARK axiom-policy layer ───────────────────────────────── - -# Step 1: compile Ada/SPARK to static library (requires GNAT + GNATprove) -build-spark-ada: - gprbuild -P src/ada/spark/echidna_spark.gpr -j0 - @echo "SPARK library built at lib/spark/libechidna_spark.a" - -# Step 2: run GNATprove to formally verify the SPARK contracts -prove-spark: - gnatprove -P src/ada/spark/echidna_spark.gpr --level=2 \ - --prover=z3,cvc5,altergo --timeout=30 --report=fail -j0 - @echo "GNATprove: all SPARK contracts discharged" - -# Step 3: compile Zig bridge (links against libechidna_spark.a) -build-spark-zig: build-spark-ada - cd src/zig && zig build -Dspark=true -Doptimize=ReleaseSafe - @echo "SPARK Zig bridge built at src/zig/zig-out/lib/libechidna_spark_zig.a" - -# Step 4: build Rust with SPARK feature enabled -build-spark: build-spark-zig - cargo build --features spark - @echo "Rust+SPARK build complete" - -# Full SPARK pipeline: prove → build → test -spark-all: prove-spark build-spark-zig - cargo test --features spark -- spark - @echo "SPARK layer: proved, built, tested" - -# Cross-check: run both Rust and SPARK enforce_policy, assert agreement -verify-spark-crosscheck: build-spark - cargo test --features spark -- crosscheck - @echo "SPARK/Rust crosscheck passed" - -# ── Creusot formal verification (Stage 8c) ────────────────── - -# Run Creusot formal verification on the trust-pipeline kernel. -# -# Prerequisites: Creusot + Why3 + Z3/CVC5 (see crates/echidna-core-creusot/CREUSOT-SETUP.adoc). -# The nightly pin lives in crates/echidna-core-creusot/rust-toolchain.toml. -# -# This recipe is currently in report-only mode: CI runs it and posts -# results but does not block the merge until Stage 8c-M3. -verify-trust-pipeline: - @echo "Running Creusot formal verification on echidna-core-creusot..." - cargo +nightly creusot \ - -p echidna-core-creusot \ - -- \ - --features creusot \ - --why3 "$(which why3)" - @echo "Creusot obligations discharged. See Why3 output above." - -# Stable-Rust test suite for the Creusot-annotated trust pipeline. -# Runs on any Rust version; does not require Creusot or Why3. -test-trust-pipeline: - cargo test -p echidna-core-creusot - @echo "Trust-pipeline invariant tests passed ($(cargo test -p echidna-core-creusot 2>&1 | grep 'test result' | head -1 | grep -oP '\d+ passed') tests)." - -# ── Other ─────────────────────────────────────────────────── - -# [AUTO-GENERATED] Multi-arch / RISC-V target -build-riscv: - @echo "Building for RISC-V..." - cross build --target riscv64gc-unknown-linux-gnu - -# Run panic-attacker pre-commit scan -assail: - @command -v panic-attack >/dev/null 2>&1 && panic-attack assail . || echo "panic-attack not found — install from https://github.com/hyperpolymath/panic-attacker" - -# ── Onboarding ──────────────────────────────────────────────── - -# Check that all required tools are installed and working -doctor: - #!/usr/bin/env bash - set -euo pipefail - PASS=0; FAIL=0; WARN=0 - check() { - local name="$1" cmd="$2" - if command -v "$cmd" >/dev/null 2>&1; then - ver=$("$cmd" --version 2>/dev/null | head -1 || echo "unknown") - echo " [OK] $name: $ver" - PASS=$((PASS + 1)) - else - echo " [FAIL] $name: '$cmd' not found" - FAIL=$((FAIL + 1)) - fi - } - check_optional() { - local name="$1" cmd="$2" - if command -v "$cmd" >/dev/null 2>&1; then - ver=$("$cmd" --version 2>/dev/null | head -1 || echo "unknown") - echo " [OK] $name: $ver" - PASS=$((PASS + 1)) - else - echo " [WARN] $name: '$cmd' not found (optional)" - WARN=$((WARN + 1)) - fi - } - echo "=== echidna: Doctor ===" - echo "" - echo "Required tools:" - check "Rust/cargo" "cargo" - check "just" "just" - check "pkg-config" "pkg-config" - # Check openssl headers - if pkg-config --exists openssl 2>/dev/null; then - echo " [OK] openssl-devel: $(pkg-config --modversion openssl)" - PASS=$((PASS + 1)) - else - echo " [FAIL] openssl-devel: not found (install openssl-devel or libssl-dev)" - FAIL=$((FAIL + 1)) - fi - echo "" - echo "Optional tools (full stack):" - check_optional "Podman" "podman" - check_optional "Idris2" "idris2" - check_optional "Zig" "zig" - check_optional "Julia" "julia" - check_optional "Bun" "bun" - check_optional "Chapel" "chpl" - check_optional "panic-attack" "panic-attack" - echo "" - echo "Prover backends (spot check):" - check_optional "Z3" "z3" - check_optional "CVC5" "cvc5" - echo "" - echo "Build artefacts:" - if [ -f "target/debug/echidna" ] || [ -f "target/release/echidna" ]; then - echo " [OK] echidna binary built" - else - echo " [INFO] echidna not built — run 'just build'" - fi - echo "" - echo "=== Results: $PASS passed, $FAIL failed, $WARN warnings ===" - if [ "$FAIL" -gt 0 ]; then - echo "Run 'just heal' for install instructions." - exit 1 - fi - -# Show install instructions for missing tools -heal: - #!/usr/bin/env bash - set -euo pipefail - echo "=== echidna: Heal ===" - echo "" - echo "Install missing tools:" - echo "" - if ! command -v cargo >/dev/null 2>&1; then - echo " Rust (REQUIRED):" - echo " asdf install rust nightly" - echo " # Or: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" - echo "" - fi - if ! pkg-config --exists openssl 2>/dev/null; then - echo " OpenSSL development headers (REQUIRED):" - echo " Fedora: sudo dnf install openssl-devel pkg-config" - echo " Ubuntu: sudo apt install libssl-dev pkg-config" - echo "" - fi - if ! command -v podman >/dev/null 2>&1; then - echo " Podman (for containers):" - echo " Fedora: sudo dnf install podman" - echo " Ubuntu: sudo apt install podman" - echo "" - fi - if ! command -v idris2 >/dev/null 2>&1; then - echo " Idris2 (for ABI layer):" - echo " Install via pack: https://github.com/stefan-hoeck/idris2-pack" - echo "" - fi - if ! command -v zig >/dev/null 2>&1; then - echo " Zig (for FFI bridge):" - echo " asdf plugin add zig && asdf install zig 0.13.0" - echo "" - fi - if ! command -v julia >/dev/null 2>&1; then - echo " Julia (for ML layer):" - echo " asdf plugin add julia && asdf install julia latest" - echo "" - fi - if ! command -v z3 >/dev/null 2>&1; then - echo " Z3 (SMT solver):" - echo " Fedora: sudo dnf install z3" - echo " Ubuntu: sudo apt install z3" - echo "" - fi - if ! command -v panic-attack >/dev/null 2>&1; then - echo " panic-attack (pre-commit scans):" - echo " cargo install --git https://github.com/hyperpolymath/panic-attacker" - echo "" - fi - echo "After installing, run 'just doctor' to verify." - -# Guided tour of the repository structure -tour: - #!/usr/bin/env bash - set -euo pipefail - echo "=== ECHIDNA: Guided Tour ===" - echo "" - echo "ECHIDNA is a neurosymbolic theorem proving platform with 48 prover backends." - echo "" - echo "1. RUST CORE: src/rust/" - echo " The main implementation. 48 prover backends, trust pipeline, CLI, REPL." - echo " src/rust/main.rs - CLI entry point" - echo " src/rust/repl.rs - Interactive REPL" - echo " src/rust/server.rs - HTTP API server" - echo " src/rust/provers/ - 30 prover backend implementations" - echo " src/rust/verification/ - Trust pipeline (portfolio, certificates, axioms)" - echo " src/rust/integrity/ - Solver binary integrity (SHAKE3-512, BLAKE3)" - echo " src/rust/executor/ - Sandboxed execution (Podman, bubblewrap)" - echo " src/rust/exchange/ - Cross-prover proof exchange" - echo " src/rust/agent/ - Agentic proof search (actor model)" - echo "" - echo "2. API INTERFACES: src/interfaces/" - echo " graphql/ - async-graphql (port 8082)" - echo " grpc/ - tonic gRPC (port 50051)" - echo " rest/ - axum + OpenAPI (port 8000)" - echo "" - echo "3. ML LAYER: src/julia/" - echo " Julia-based tactic prediction and premise selection." - echo "" - echo "4. ABI/FFI:" - echo " src/abi/ - Idris2 formal type definitions" - echo " ffi/zig/ - Zig FFI bridge (4 shared libraries)" - echo "" - echo "5. UI: src/ui/" - echo " AffineScript-TEA (src/ui/tea/) + static shell (src/ui/public/)." - echo "" - echo "6. BUILD SYSTEM:" - echo " Cargo.toml - Rust workspace" - echo " Justfile - Primary build system" - echo " guix.scm - Guix package definition (PRIMARY; no Guix mirror — estate ruling 2026-05-18)" - echo " .containerization/Containerfile.wave3 - sealed-container escape hatch (Tier-3 provers)" - echo "" - echo "Try: just build (compile Rust core)" - echo " just test (run 232 unit tests)" - echo " just run repl (interactive theorem prover)" - -# Show available recipes with descriptions -help-me: - #!/usr/bin/env bash - echo "=== ECHIDNA: Help ===" - echo "" - echo "Onboarding:" - echo " just doctor - Check required tools are installed" - echo " just heal - Show install instructions for missing tools" - echo " just tour - Guided walkthrough of repo structure" - echo " just help-me - This help message" - echo "" - echo "Build:" - echo " just build - Build debug binary" - echo " just build-release - Build optimised release binary" - echo " just build-riscv - Cross-compile for RISC-V" - echo "" - echo "Testing:" - echo " just test - Run unit tests (232)" - echo " just test-all - Run all tests (389)" - echo " just test-verbose - Run tests with output" - echo " just test-integration - Integration tests only" - echo " just test-neural - Neural integration tests (requires Julia)" - echo "" - echo "Quality:" - echo " just fmt - Format code" - echo " just fmt-check - Check formatting" - echo " just lint - Run clippy lints" - echo " just check - Cargo check (fast)" - echo " just pre-commit - Full pre-commit checks" - echo " just assail - panic-attacker scan" - echo "" - echo "Running:" - echo " just run repl - Interactive REPL" - echo " just run serve - Start API server" - echo " just run prove - Prove a file" - echo "" - echo "Containers:" - echo " just container-build - Minimal image" - echo " just container-build-full - Full image (all provers)" - echo " just container-build-tier3 - All Wave-3 prover images (except imandra)" - echo " just container-build-tamarin - Tamarin security protocol prover" - echo " just container-build-proverif - ProVerif cryptographic protocol verifier" - echo " just container-build-metamath - Metamath (pure-Rust in-process + bundled databases)" - echo " just container-build-twelf - Twelf LF meta-logical framework" - echo " just container-build-or-tools - OR-Tools combinatorial optimisation C++ library" - echo " just container-build-scip - SCIP constraint integer programming solver" - echo " just container-build-hol4 - HOL4 (Poly/ML build, slow)" - echo " just container-build-acl2 - ACL2 (SBCL build, slow)" - echo " just container-build-imandra - Imandra stub (proprietary licence required)" - echo " just container-run - Run container" - echo "" - echo "Chapel accelerator:" - echo " just chapel-all - Build + test Chapel layer" - echo "" - echo "Housekeeping:" - echo " just clean - Remove build artefacts" - echo " just doc - Generate and open docs" - echo " just update - Update dependencies" - echo " just audit - Audit dependencies" - - -# Print the current CRG grade (reads from READINESS.md '**Current Grade:** X' line) -crg-grade: - @grade=$$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ - [ -z "$$grade" ] && grade="X"; \ - echo "$$grade" - -# Generate a shields.io badge markdown for the current CRG grade -# Looks for '**Current Grade:** X' in READINESS.md; falls back to X -crg-badge: - @grade=$$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ - [ -z "$$grade" ] && grade="X"; \ - case "$$grade" in \ - A) color="brightgreen" ;; B) color="green" ;; C) color="yellow" ;; \ - D) color="orange" ;; E) color="red" ;; F) color="critical" ;; \ - *) color="lightgrey" ;; esac; \ - echo "[![CRG $$grade](https://img.shields.io/badge/CRG-$$grade-$$color?style=flat-square)](https://github.com/hyperpolymath/standards/tree/main/component-readiness-grades)" - - -# ──────────────────────────────────────────────────────────────────── -# Corpus + design-search (added 2026-04-28) -# ──────────────────────────────────────────────────────────────────── - -# Ingest an Agda project into the corpus -corpus-ingest-agda root: - cargo run --bin echidna -- corpus ingest --root {{root}} --adapter agda - -# Ingest a Coq project into the corpus -corpus-ingest-coq root: - cargo run --bin echidna -- corpus ingest --root {{root}} --adapter coq - -# Ingest a Lean 4 project into the corpus -corpus-ingest-lean root: - cargo run --bin echidna -- corpus ingest --root {{root}} --adapter lean - -# Ingest an Idris 2 project into the corpus -corpus-ingest-idris root: - cargo run --bin echidna -- corpus ingest --root {{root}} --adapter idris2 - -# Query the corpus by name with full reverse-dep impact set -corpus-impact name index="data/corpus/agda.json": - cargo run --bin echidna -- corpus query "{{name}}" --index {{index}} --reverse-closure - -# Cosine-similarity nearest-neighbour query -corpus-near query index="data/corpus/agda.json" top="10": - cargo run --bin echidna -- corpus near "{{query}}" --index {{index}} --top {{top}} - -# Cross-prover semantic-class lookup -corpus-cross class: - cargo run --bin echidna -- corpus crossquery {{class}} - -# Export a JSON index as 8-modality octad JSONL (VeriSim-compatible) -corpus-octads in="data/corpus/agda.json" out="data/corpus/agda.octads.jsonl": - cargo run --bin echidna -- corpus export-octads --index {{in}} --out {{out}} - -# Run the SA design-search on a named problem -design-search problem="brouwer-leq" iterations="2000" top="10": - cargo run --bin echidna -- design search {{problem}} --iterations {{iterations}} --top {{top}} - -# Run the parallel-swarm design-search -design-swarm problem="brouwer-leq" agents="4" iterations="800": - cargo run --bin echidna -- design swarm {{problem}} --agents {{agents}} --iterations {{iterations}} --broadcast-every 50 - -# ───────────────────────────────────────────────────────────────────── -# Saturation campaign 2026-06-01 — recipes for the new corpus / vocab / -# arbiter / exchange surface. See docs/decisions/2026-06-01-saturation- -# campaign.md and docs/CORPUS-ADAPTERS.adoc. -# ───────────────────────────────────────────────────────────────────── - -# Ingest a project tree via one of the 13 new corpus adapters -# (isabelle / metamath / mizar / hol_light / hol4 / dafny / why3 / -# fstar / acl2_books / tptp / smtlib / proofnet / minif2f). -corpus-ingest-saturation adapter root: - cargo run --bin echidna -- corpus ingest --root {{root}} --adapter {{adapter}} - -# Ingest an Ada/SPARK tree (specs `*.ads`, bodies `*.adb`). Detects -# GNATprove escape hatches (`pragma Assume`, suppressed checks) and the -# sparklib `Lemma_*` convention. -corpus-ingest-spark root: - cargo run --bin echidna -- corpus ingest --root {{root}} --adapter spark_ada - -# Ingest a Rust/Creusot tree (`#[requires]` / `#[ensures]` / `pearlite!`). -# `#[trusted]` is reported as a postulate, not as a proved function. -corpus-ingest-creusot root: - cargo run --bin echidna -- corpus ingest --root {{root}} --adapter creusot - -# Ingest the hyperpolymath estate: every repo's `src/abi` Idris2 tree plus -# the Agda type-theory libraries (echo/epistemic/secret/tropical/ -# choreographic-types), merged into one corpus tagged with repo + layer. -# ROOT is the directory that contains the repositories. -corpus-ingest-estate root="..": - cargo run --bin echidna -- corpus ingest --root {{root}} --adapter hyperpolymath_estate - -# Run the corpus-stats summary across all 20 adapter outputs found under -# data/corpus/*.json (skips any missing without failing). -corpus-stats-all: - @for f in data/corpus/*.json; do \ - [ -f "$f" ] || continue; \ - echo "--- $f"; \ - cargo run --bin echidna --quiet -- corpus stats --index "$f" 2>/dev/null || echo " (skipped)"; \ - done - -# Load every per-prover synonym TOML + the 5 cross-prover dictionaries -# (_msc2020 / _wordnet_math / _conceptnet_seed / _disciplines / -# _verification_systems) and report counts. -synonym-load-test: - cargo test --lib --quiet suggest::synonyms 2>&1 | tail -20 - -# Lexicon v2: apply the Nickel contract (data/synonyms/schema.ncl) to every -# synonym table and to the accept/reject fixtures. Needs nickel >= 1.12. -lexicon-check: - scripts/lexicon-check.sh - cargo test --locked --test lexicon_v2_test - -# Mine lexicon candidates across training_data/ corpora: cognates, false -# friends and synonym candidates as JCS JSON Lines under target/lexicon-scan. -# Flag-only (evidence = none); never edits data/synonyms/. Presets: learning, -# spanning, in-family, full. -lexicon-scan preset="learning": - cargo run --locked --bin echidna -- lexicon scan --preset {{preset}} - cargo test --locked --test lexicon_scan_test - -# Run the saturation-campaign cargo tests only (139 cases + 1 ignored). -test-saturation: - cargo test --lib -- \ - corpus:: \ - verification::bayesian_arbiter \ - verification::dempster_shafer \ - verification::pareto_arbiter \ - exchange::tptp \ - exchange::smtlib \ - exchange::smtcoq \ - exchange::lambdapi \ - suggest::synonyms - -# Smoke-test the arbiter trio with synthetic evidence. -# Prints posterior verdict from each of: Bayesian, Dempster-Shafer, Pareto. -arbiter-smoke: - cargo run --bin echidna --quiet -- arbiter smoke 2>/dev/null || \ - echo "(arbiter-smoke CLI not yet wired — see src/rust/verification/{bayesian,dempster_shafer,pareto}_arbiter.rs for unit-test entry points)" - -# Validate that the E-R schema doc + Cap'n Proto schema are -# byte-identical with the recorded SHA in .machine_readable/er-schema.sha256. -# (CI gate planned — see docs/architecture/VERISIM-ER-SCHEMA.adoc -# "Drift detection".) -er-schema-drift-check: - @echo "Checking E-R schema drift..." - @sha256sum docs/architecture/VERISIM-ER-SCHEMA.adoc \ - crates/echidna-wire/schemas/verisim_er.capnp \ - | sha256sum | head -c 64 - @echo " (current combined hash; compare against .machine_readable/er-schema.sha256 when CI gate lands)" - - -secret-scan-trufflehog: - @command -v trufflehog >/dev/null && trufflehog filesystem . --only-verified || true +# Recipes live in just/*.just, one file per area, imported here in the +# order they used to appear in this file. +import "just/init.just" +import "just/build.just" +import "just/proofs.just" +import "just/dev.just" +import "just/corpus.just" +import "just/ui.just" +import "just/containers.just" +import "just/chapel.just" +import "just/spark.just" +import "just/creusot.just" +import "just/other.just" +import "just/onboarding.just" +import "just/corpus-design.just" +import "just/saturation.just" + +# Contractile recipes list after the project recipes, as they did before +# the split. +import? "contractile.just" diff --git a/just/build.just b/just/build.just new file mode 100644 index 00000000..bf041862 --- /dev/null +++ b/just/build.just @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# Build debug +build: + cargo build + +# Build release +build-release: + cargo build --release + +# Run unit tests +test: + cargo test --lib + +# Self-test the wave gates (scripts/gates/*.selftest.sh): fixtures, no network +test-gates: + bash scripts/gates/lib.selftest.sh + bash scripts/gates/w0.selftest.sh + +# Run all tests (unit + integration) +test-all: + cargo test + +# Run tests verbose +test-verbose: + cargo test -- --nocapture + +# Run integration tests only +test-integration: + cargo test --test integration_tests + +# Run neural integration tests (requires Julia server) +test-neural: + ./scripts/test-integration.sh + +# Run the S4 loop-closure integration test against a VeriSimDB instance. +# Skips with a notice when VeriSimDB is unreachable (no failure). +# Set VERISIM_URL to override the default of http://localhost:8080. +# +# Local dev: start verisim-api beforehand (see docs/runbooks/s4-loop-closure.md); +# the test will exercise the full record_proof_attempt → query_by_goal_hash loop. +test-s4-loop: + @echo "Running S4 loop-closure test against ${VERISIM_URL:-http://localhost:8080}" + @echo "Test will skip cleanly if the endpoint is unreachable." + cargo test --features verisim --test s4_loop_closure -- --nocapture diff --git a/just/chapel.just b/just/chapel.just new file mode 100644 index 00000000..d8fdeee2 --- /dev/null +++ b/just/chapel.just @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ── Chapel Accelerator ────────────────────────────────────── + +# Compile the Chapel metalayer into a static library. +# --static (not --dynamic) because the apt-shipped Chapel runtime is +# CHPL_LIB_PIC=none only; see .github/workflows/chapel-ci.yml for the +# longer explanation and #133 for the closure trace. +chapel-build: + @echo "Compiling Chapel metalayer (static)..." + cd src/chapel && chpl --library --static -I ../zig_ffi \ + -o libechidna_chapel \ + chapel_ffi_exports.chpl parallel_proof_search.chpl + @echo "Built src/chapel/lib/libechidna_chapel.a" + +# Build the Chapel "hello" smoke target — exercises coforall + atomic +# CAS + reduction. Used in CI as the toolchain-health signal. +chapel-smoke: + cd src/chapel && chpl -o chapel_smoke smoke.chpl && ./chapel_smoke + +# Run the Chapel-gated Rust tests (`--features chapel`). +chapel-test: build-chapel-ffi + cargo test --locked --features chapel --lib -- proof_search verify_proof_parallel + +# Speedup baseline: compares sequentialProofSearch / +# parallelProofSearch / parallelProofSearchSpeculative against the +# fixture corpus in tests/chapel_fixtures/. Emits CSV to stdout. Doc: +# docs/bench/2026-05-30-chapel-mrr-baseline.adoc. +# +# IDRIS2_PREFIX is derived from `which idris2` because Idris2's prelude +# resolution requires the env var to be set when invoking `--check`; +# the per-prover cwd hook (#158) sets the subprocess working dir but +# doesn't help if the parent never had the env var (see +# src/chapel/parallel_proof_search.chpl ProverInfo[4]). The derived +# prefix is `dirname(dirname(realpath(which idris2)))` — works for +# both source builds and asdf-shimmed installs. +bench-chapel-mrr: + #!/usr/bin/env bash + set -euo pipefail + if command -v idris2 >/dev/null 2>&1 && [ -z "${IDRIS2_PREFIX:-}" ]; then + export IDRIS2_PREFIX="$(dirname "$(dirname "$(readlink -f "$(command -v idris2)")")")" + echo "# IDRIS2_PREFIX=$IDRIS2_PREFIX (derived from which idris2)" >&2 + fi + cd src/chapel + chpl -o bench_mrr bench_mrr.chpl + ./bench_mrr --verbose=false --timeout=10 + echo "# wrote src/chapel/bench_mrr_telemetry.csv + src/chapel/bench_mrr_summary.csv" >&2 + +# Same bench, but surface only the corpus-level outcome breakdown: the +# per-prover preempted / timed-out / success counts and the preemption +# rate next to wall-clock, one line per strategy. This is the #162 view — +# it answers "how much of the speculative win is preemption?" without +# opening the 360-row telemetry file. +bench-chapel-mrr-telemetry: + #!/usr/bin/env bash + set -euo pipefail + if command -v idris2 >/dev/null 2>&1 && [ -z "${IDRIS2_PREFIX:-}" ]; then + export IDRIS2_PREFIX="$(dirname "$(dirname "$(readlink -f "$(command -v idris2)")")")" + fi + cd src/chapel + chpl -o bench_mrr bench_mrr.chpl + ./bench_mrr --verbose=false --timeout=10 --telemetry-only=true + echo "corpus-level outcome breakdown (fixture=ALL):" + grep '^ALL,' bench_mrr_summary.csv | while IFS=, read -r _ strategy att ok fail preempt timeout na err notatt rate wall _ _; do + printf ' %-22s attempted=%-3s success=%-3s failure=%-3s preempted=%-3s timed_out=%-3s rate=%s wall=%ss\n' \ + "$strategy" "$att" "$ok" "$fail" "$preempt" "$timeout" "$rate" "$wall" + done + +# Rebuild Chapel 2.8.0 from source with CHPL_LIB_PIC=pic so that +# `chpl --library --dynamic` can produce a shared-library form of the +# metalayer. The apt deb ships only the `lib_pic-none` runtime +# variant. ~25-35 min wall on a 4-core x86_64. ~5 GB disk under +# ~/.cache/echidna/chapel-pic. Procedure + tradeoffs: +# docs/decisions/2026-05-30-chapel-pic-rebuild.adoc. NOT a CI step. +chapel-pic-from-source: + #!/usr/bin/env bash + set -euo pipefail + CACHE_DIR="${ECHIDNA_CHAPEL_PIC_CACHE:-$HOME/.cache/echidna/chapel-pic}" + CHPL_VERSION="${CHPL_VERSION:-2.8.0}" + mkdir -p "$CACHE_DIR" + cd "$CACHE_DIR" + if [ ! -f "chapel-${CHPL_VERSION}.tar.gz" ]; then + echo "Downloading chapel ${CHPL_VERSION}..." + curl -fL "https://github.com/chapel-lang/chapel/releases/download/${CHPL_VERSION}/chapel-${CHPL_VERSION}.tar.gz" \ + -o "chapel-${CHPL_VERSION}.tar.gz" + fi + if [ ! -d "chapel-${CHPL_VERSION}" ]; then + tar xf "chapel-${CHPL_VERSION}.tar.gz" + fi + cd "chapel-${CHPL_VERSION}" + export CHPL_LIB_PIC=pic + export CHPL_LLVM=bundled + export CHPL_HOME="$PWD" + source util/setchplenv.bash + echo "Building chapel runtime with CHPL_LIB_PIC=pic (~30 min)..." + make -j"$(nproc)" + echo "Verifying lib_pic-pic variant landed..." + if find lib -name 'lib_pic-pic' -type d | head -1 | grep -q .; then + echo "OK: PIC runtime built at $CHPL_HOME" + echo "Use: PATH=$CHPL_HOME/bin/linux64-x86_64:\$PATH chpl --library --dynamic ..." + else + echo "ERROR: lib_pic-pic variant not found after build" >&2 + exit 1 + fi + +# Build Zig FFI bridge for Chapel (prerequisite for --features chapel) +build-chapel-ffi: + @echo "Building Zig FFI bridge..." + cd src/zig_ffi && zig build -Doptimize=ReleaseSafe + @echo "Library built at src/zig_ffi/zig-out/lib/" + +# Build the feature against explicit unavailable stubs. +build-chapel: build-chapel-ffi + ECHIDNA_CHAPEL_EXPECT_MODE=stubs cargo build --locked --features chapel + +# Build the real static bridge and let Cargo consume Chapel's runtime closure. +build-chapel-real: chapel-build + cd src/zig_ffi && zig build -Doptimize=ReleaseSafe -Dstubs=false + ECHIDNA_CHAPEL_EXPECT_MODE=real cargo build --locked --features chapel + +chapel-test-real: build-chapel-real + ECHIDNA_CHAPEL_EXPECT_MODE=real cargo test --locked --features chapel --lib -- proof_search verify_proof_parallel + +# Alias retained for muscle-memory; same as chapel-test. +test-chapel: chapel-test + +# Test Zig FFI bridge independently +test-chapel-ffi: + cd src/zig_ffi && zig build test + +# Full local real-library path, with the independent stub bridge unit tests. +chapel-all: chapel-smoke test-chapel-ffi chapel-test-real + @echo "Real Chapel build and selected Rust/FFI tests completed" diff --git a/just/containers.just b/just/containers.just new file mode 100644 index 00000000..a193eced --- /dev/null +++ b/just/containers.just @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ── Tier-3 prover containers (Wave-3) ──────────────────────────────────────── +# All nine targets live in ONE consolidated multi-target Containerfile.wave3. +# The Rust core is compiled once (shared `rust-builder` stage) and every prover +# image is a `--target`; podman caches the shared stage across builds on a host. + +# Build all non-proprietary Tier-3 prover containers (skip imandra — licence required) +container-build-tier3: container-build-tamarin container-build-proverif container-build-metamath \ + container-build-twelf container-build-or-tools container-build-scip \ + container-build-hol4 container-build-acl2 + +container-build-tamarin: + podman build -f .containerization/Containerfile.wave3 --target tamarin -t echidna:tamarin . + +container-build-proverif: + podman build -f .containerization/Containerfile.wave3 --target proverif -t echidna:proverif . + +container-build-metamath: + podman build -f .containerization/Containerfile.wave3 --target metamath -t echidna:metamath . + +container-build-twelf: + podman build -f .containerization/Containerfile.wave3 --target twelf -t echidna:twelf . + +container-build-or-tools: + podman build -f .containerization/Containerfile.wave3 --target or-tools -t echidna:or-tools . + +container-build-scip: + podman build -f .containerization/Containerfile.wave3 --target scip -t echidna:scip . + +container-build-hol4: + podman build -f .containerization/Containerfile.wave3 --target hol4 -t echidna:hol4 . + +container-build-acl2: + podman build -f .containerization/Containerfile.wave3 --target acl2 -t echidna:acl2 . + +# Build imandra stub (proprietary; real binary requires a signed licence — see Containerfile.wave3 imandra target) +container-build-imandra: + podman build -f .containerization/Containerfile.wave3 --target imandra -t echidna:imandra . + +# Run echidna container +container-run *ARGS: + podman run --rm -it echidna:latest {{ARGS}} diff --git a/just/corpus-design.just b/just/corpus-design.just new file mode 100644 index 00000000..8f9313d1 --- /dev/null +++ b/just/corpus-design.just @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ──────────────────────────────────────────────────────────────────── +# Corpus + design-search (added 2026-04-28) +# ──────────────────────────────────────────────────────────────────── + +# Ingest an Agda project into the corpus +corpus-ingest-agda root: + cargo run --bin echidna -- corpus ingest --root {{root}} --adapter agda + +# Ingest a Coq project into the corpus +corpus-ingest-coq root: + cargo run --bin echidna -- corpus ingest --root {{root}} --adapter coq + +# Ingest a Lean 4 project into the corpus +corpus-ingest-lean root: + cargo run --bin echidna -- corpus ingest --root {{root}} --adapter lean + +# Ingest an Idris 2 project into the corpus +corpus-ingest-idris root: + cargo run --bin echidna -- corpus ingest --root {{root}} --adapter idris2 + +# Query the corpus by name with full reverse-dep impact set +corpus-impact name index="data/corpus/agda.json": + cargo run --bin echidna -- corpus query "{{name}}" --index {{index}} --reverse-closure + +# Cosine-similarity nearest-neighbour query +corpus-near query index="data/corpus/agda.json" top="10": + cargo run --bin echidna -- corpus near "{{query}}" --index {{index}} --top {{top}} + +# Cross-prover semantic-class lookup +corpus-cross class: + cargo run --bin echidna -- corpus crossquery {{class}} + +# Export a JSON index as 8-modality octad JSONL (VeriSim-compatible) +corpus-octads in="data/corpus/agda.json" out="data/corpus/agda.octads.jsonl": + cargo run --bin echidna -- corpus export-octads --index {{in}} --out {{out}} + +# Run the SA design-search on a named problem +design-search problem="brouwer-leq" iterations="2000" top="10": + cargo run --bin echidna -- design search {{problem}} --iterations {{iterations}} --top {{top}} + +# Run the parallel-swarm design-search +design-swarm problem="brouwer-leq" agents="4" iterations="800": + cargo run --bin echidna -- design swarm {{problem}} --agents {{agents}} --iterations {{iterations}} --broadcast-every 50 diff --git a/just/corpus.just b/just/corpus.just new file mode 100644 index 00000000..778d7e47 --- /dev/null +++ b/just/corpus.just @@ -0,0 +1,131 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ── Corpus / training pipeline ───────────────────────────────── +# +# Five steps in order: provision upstream mirrors, run every extractor, +# merge per-prover proof_states into UNIFIED, align the premise files +# to UNIFIED's fresh ids, then retrain. Each step is idempotent; +# re-running only touches what has changed. See +# scripts/provision_corpora.sh --list for the source catalogue. + +# Clone every upstream prover corpus into external_corpora/. +# Pass specific names to provision a subset: `just provision-corpora metamath mathlib4`. +provision-corpora *NAMES: + @if [ -z "{{NAMES}}" ]; then \ + scripts/provision_corpora.sh --all; \ + else \ + scripts/provision_corpora.sh {{NAMES}}; \ + fi + +# Report which upstream corpora are on disk and their sizes. +corpora-status: + scripts/provision_corpora.sh --status + +# Run every scripts/extract_*.jl against the provisioned corpora. +# Pass names to run a subset: `just extract-corpora metamath mathlib4`. +extract-corpora *NAMES: + scripts/extract_all.sh {{NAMES}} + +# Merge per-prover proof_states_*.jsonl into proof_states_UNIFIED.jsonl +# with fresh sequential ids (dedupes by prover+theorem). +merge-corpora: + julia --project=src/julia scripts/merge_corpus.jl + +# Rebuild premises_COMPLETE.jsonl with proof_ids that match UNIFIED. +# merge-corpora rewrites every proof_state id to a fresh sequential +# counter; the premise files keep the original extractor ids, so this +# step re-joins premises to UNIFIED via (prover, theorem) — the durable +# key merge_corpus.jl already dedupes on. Without this step the +# dataloader's proof_id join matches ~0% of records. +align-premises: + julia --project=src/julia scripts/align_premises.jl + +# Full retrain from provisioned corpora. Honours ECHIDNA_MAX_PROOF_STATES +# (0 = unlimited), ECHIDNA_NUM_EPOCHS, ECHIDNA_NUM_NEGATIVES. +# +# Depends on align-premises: without that step the dataloader joins +# premise.proof_id against proof_state.id and fails at ~0% match because +# merge_corpus.jl rewrites proof_state ids to fresh sequential counters +# while the premise files keep the original per-extractor ids. Running +# align-premises is idempotent and cheap (seconds on a 2M premise set). +retrain: align-premises + julia --project=src/julia src/julia/run_training.jl + +# Retrain without the align-premises prerequisite — use only when you +# know premises_COMPLETE.jsonl is already aligned (e.g. you just ran +# just align-premises manually and want to iterate on model code). +retrain-skip-align: + julia --project=src/julia src/julia/run_training.jl + +# Full training run on the real corpus (auto-selects GPU when available). +# Honours ECHIDNA_MAX_PROOF_STATES, ECHIDNA_NUM_EPOCHS, ECHIDNA_NUM_NEGATIVES. +# Produces models/neural/{premise_selector,tactic_predictor}.bson, vocab.json, +# updates models/model_metadata.txt, and appends to training_data/metrics_baseline.jsonl. +train: + julia --project=src/julia src/julia/run_training.jl + +# CPU smoke pass: 2000 proof states, 2 epochs — fast end-to-end sanity check. +# Safe to run on any dev box without a GPU. Produces the same artefacts as `train`. +train-cpu: + ECHIDNA_MAX_PROOF_STATES=2000 ECHIDNA_NUM_EPOCHS=2 julia --project=src/julia src/julia/run_training_cpu.jl + +# Evaluate trained model on held-out validation split (MRR / top-k). +# Requires `just train-cpu` (or `just train`) to have produced models/neural/. +# Appends one JSONL row to training_data/eval_results.jsonl and exits non-zero +# if MRR < 0.66 (below the cosine baseline). +eval: + julia --project=src/julia src/julia/eval_held_out.jl + +# End-to-end pipeline: provision → extract → merge → align → retrain. +# Use `ECHIDNA_MAX_PROOF_STATES=0 just corpus-refresh` to lift the sample cap. +corpus-refresh: provision-corpora extract-corpora merge-corpora align-premises retrain + +# Run training using the saturation-campaign corpus adapters as the +# data source. Requires `just provision-corpora` to have populated +# data/corpus/ first. Per-prover invocation; default lean. +train-from-corpus prover="lean": + @julia --project=src/julia src/julia/run_training.jl \ + --prover {{prover}} \ + --corpus-json $(ls data/corpus/*.json | tr '\n' ',') \ + --synonyms-dir data/synonyms + +# Run the eight-axis metrics suite against the current corpus and post +# results to VeriSimDB. Falls back to training_data/metrics_.jsonl +# if VERISIM_URL is unreachable. Target values are documented in +# metrics/README.adoc. +metrics: + julia --project=src/julia metrics/run_all.jl + +# Report corpus balance across provers from stats_UNIFIED.json. +corpus-stats: + @julia -e 'using JSON3, Printf; \ + s = JSON3.read(read("training_data/stats_UNIFIED.json", String)); \ + counts = s.per_prover_counts; \ + pairs = sort(collect(counts), by = x -> -x[2]); \ + total = sum(last.(pairs)); \ + println("Total proofs: ", total, " provers with data: ", length(pairs)); \ + println("Rank Prover Count Share"); \ + for (i,(p,c)) in enumerate(pairs); \ + @printf("%3d %-25s %7d %5.2f%%\n", i, p, c, 100c/total); \ + end' + +# Generate docs +doc: + cargo doc --no-deps --open + +# Update dependencies +update: + cargo update + +# Regenerate Cap'n Proto bindings from schemas/echidna.capnp. +# Requires capnp compiler: https://capnproto.org/install.html +# Output: src/rust/ipc/echidna_capnp.rs (checked in; do not edit by hand) +capnp-gen: + @command -v capnp >/dev/null 2>&1 || { echo "capnp compiler not found — install from https://capnproto.org/install.html"; exit 1; } + capnp compile -orust schemas/echidna.capnp -I /usr/include + @echo "Rust bindings generated. Commit src/rust/ipc/echidna_capnp.rs." + +# Audit dependencies +audit: + cargo audit diff --git a/just/creusot.just b/just/creusot.just new file mode 100644 index 00000000..fc7764b5 --- /dev/null +++ b/just/creusot.just @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ── Creusot formal verification (Stage 8c) ────────────────── + +# Run Creusot formal verification on the trust-pipeline kernel. +# +# Prerequisites: Creusot + Why3 + Z3/CVC5 (see crates/echidna-core-creusot/CREUSOT-SETUP.adoc). +# The nightly pin lives in crates/echidna-core-creusot/rust-toolchain.toml. +# +# This recipe is currently in report-only mode: CI runs it and posts +# results but does not block the merge until Stage 8c-M3. +verify-trust-pipeline: + @echo "Running Creusot formal verification on echidna-core-creusot..." + cargo +nightly creusot \ + -p echidna-core-creusot \ + -- \ + --features creusot \ + --why3 "$(which why3)" + @echo "Creusot obligations discharged. See Why3 output above." + +# Stable-Rust test suite for the Creusot-annotated trust pipeline. +# Runs on any Rust version; does not require Creusot or Why3. +test-trust-pipeline: + cargo test -p echidna-core-creusot + @echo "Trust-pipeline invariant tests passed ($(cargo test -p echidna-core-creusot 2>&1 | grep 'test result' | head -1 | grep -oP '\d+ passed') tests)." diff --git a/just/dev.just b/just/dev.just new file mode 100644 index 00000000..1b496f0a --- /dev/null +++ b/just/dev.just @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# Format code +fmt: + cargo fmt + +# Check formatting +fmt-check: + cargo fmt -- --check + +# Run clippy lints +lint: + cargo clippy -- -D warnings + +# Check without building +check: + cargo check + +# Clean build artifacts +clean: + cargo clean + +# Run the project +run *ARGS: + cargo run -- {{ARGS}} + +# Run Invariant Path overlay tools for this repository +invariant-path *ARGS: + ./scripts/invariant-path.sh {{ARGS}} + +# Rebuild the canonical seed vocabulary (training_data/vocabulary_CANON.txt). +# Two stages: (1) mine frequency-filtered identifiers from the per-prover +# proof corpora, then (2) union with the hand-curated sets. Consumed by +# src/julia/training/dataloader.jl:build_vocabulary_from_data. +vocab-canon: + julia scripts/vocabulary_mine_corpus.jl + julia scripts/vocabulary_canonicalize.jl diff --git a/just/init.just b/just/init.just new file mode 100644 index 00000000..5e5e0017 --- /dev/null +++ b/just/init.just @@ -0,0 +1,197 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# Initialize from RSR template (placeholder replacement) +init: + #!/usr/bin/env bash + set -euo pipefail + + echo "═══════════════════════════════════════════════════" + echo " RSR Project Bootstrap" + echo "═══════════════════════════════════════════════════" + echo "" + + # --- Load defaults from config (if exists) --- + # Create yours: ~/.config/rsr/defaults + # Format: OWNER=myorg AUTHOR="My Name" AUTHOR_EMAIL=me@example.org ... + DEFAULTS="${XDG_CONFIG_HOME:-$HOME/.config}/rsr/defaults" + if [ -f "$DEFAULTS" ]; then + echo "Loading defaults from $DEFAULTS" + # shellcheck source=/dev/null + source "$DEFAULTS" + echo "" + fi + + # --- Required values (pre-filled from defaults if available) --- + read -rp "Project name (human-readable, e.g. My Project): " PROJECT_NAME + [ -z "$PROJECT_NAME" ] && echo "Error: project name required" && exit 1 + + read -rp "Repository slug (e.g. my-project): " REPO + [ -z "$REPO" ] && echo "Error: repo slug required" && exit 1 + + read -rp "Owner [${OWNER:-}]: " _OWNER + OWNER="${_OWNER:-${OWNER:-}}" + [ -z "$OWNER" ] && echo "Error: owner required" && exit 1 + + read -rp "Author full name [${AUTHOR:-}]: " _AUTHOR + AUTHOR="${_AUTHOR:-${AUTHOR:-}}" + [ -z "$AUTHOR" ] && echo "Error: author name required" && exit 1 + + read -rp "Author email [${AUTHOR_EMAIL:-}]: " _AUTHOR_EMAIL + AUTHOR_EMAIL="${_AUTHOR_EMAIL:-${AUTHOR_EMAIL:-}}" + [ -z "$AUTHOR_EMAIL" ] && echo "Error: email required" && exit 1 + + # --- Optional values (pre-filled from defaults if available) --- + read -rp "Author organization [${AUTHOR_ORG:-none}]: " _AUTHOR_ORG + AUTHOR_ORG="${_AUTHOR_ORG:-${AUTHOR_ORG:-}}" + + read -rp "Previous/alt email [${AUTHOR_EMAIL_ALT:-none}]: " _AUTHOR_EMAIL_ALT + AUTHOR_EMAIL_ALT="${_AUTHOR_EMAIL_ALT:-${AUTHOR_EMAIL_ALT:-}}" + + read -rp "Project description []: " PROJECT_DESCRIPTION + + read -rp "Forge domain [${FORGE:-github.com}]: " _FORGE + FORGE="${_FORGE:-${FORGE:-github.com}}" + + read -rp "Security contact email [${SECURITY_EMAIL:-$AUTHOR_EMAIL}]: " _SECURITY_EMAIL + SECURITY_EMAIL="${_SECURITY_EMAIL:-${SECURITY_EMAIL:-$AUTHOR_EMAIL}}" + + read -rp "Conduct contact email [${CONDUCT_EMAIL:-$AUTHOR_EMAIL}]: " _CONDUCT_EMAIL + CONDUCT_EMAIL="${_CONDUCT_EMAIL:-${CONDUCT_EMAIL:-$AUTHOR_EMAIL}}" + + read -rp "Project type (library|binary|monorepo|service|website) [library]: " PROJECT_TYPE + PROJECT_TYPE="${PROJECT_TYPE:-library}" + + read -rp "Website URL [https://${FORGE}/${OWNER}/${REPO}]: " WEBSITE + WEBSITE="${WEBSITE:-https://${FORGE}/${OWNER}/${REPO}}" + + # --- Container values (optional — only relevant if container/ exists) --- + if [ -d "container" ]; then + echo "" + echo "── Container configuration (optional) ─────────" + read -rp "Service name [${REPO}]: " _SERVICE_NAME + SERVICE_NAME="${_SERVICE_NAME:-${REPO}}" + read -rp "Primary port [8080]: " _PORT + PORT="${_PORT:-8080}" + read -rp "Container registry [ghcr.io/${OWNER}]: " _REGISTRY + REGISTRY="${_REGISTRY:-ghcr.io/${OWNER}}" + else + SERVICE_NAME="${REPO}" + PORT="8080" + REGISTRY="ghcr.io/${OWNER}" + fi + + # --- Derived values --- + PROJECT_UPPER=$(echo "$REPO" | tr '[:lower:]-' '[:upper:]_') + PROJECT_LOWER=$(echo "$REPO" | tr '[:upper:]-' '[:lower:]_') + CURRENT_YEAR=$(date +%Y) + CURRENT_DATE=$(date +%Y-%m-%d) + VERSION="0.1.0" + + # Derive citation name parts (best-effort split on last space) + AUTHOR_LAST="${AUTHOR##* }" + AUTHOR_FIRST="${AUTHOR% *}" + FIRST_INITIAL="${AUTHOR_FIRST:0:1}." + if [ "$AUTHOR_LAST" = "$AUTHOR_FIRST" ]; then + AUTHOR_FIRST="$AUTHOR" + AUTHOR_LAST="" + FIRST_INITIAL="" + fi + + echo "" + echo "── Summary ──────────────────────────────────────" + echo " Project: $PROJECT_NAME" + echo " Repo: $REPO" + echo " Owner: $OWNER" + echo " Author: $AUTHOR <$AUTHOR_EMAIL>" + [ -n "$AUTHOR_ORG" ] && echo " Organization: $AUTHOR_ORG" + echo " Forge: $FORGE" + echo " Year: $CURRENT_YEAR" + echo "────────────────────────────────────────────────" + echo "" + read -rp "Proceed? [Y/n] " CONFIRM + [[ "${CONFIRM:-Y}" =~ ^[Nn] ]] && echo "Aborted." && exit 0 + + echo "" + echo "Replacing placeholders..." + + # Brace tokens as variables (hex avoids just interpolation) + LB=$(printf '\x7b\x7b') + RB=$(printf '\x7d\x7d') + + # Build the sed expression list + # Note: using | as delimiter since URLs contain / + SED_ARGS=( + -e "s|${LB}PROJECT_NAME${RB}|${PROJECT_NAME}|g" + -e "s|${LB}PROJECT_DESCRIPTION${RB}|${PROJECT_DESCRIPTION}|g" + -e "s|${LB}PROJECT${RB}|${PROJECT_UPPER}|g" + -e "s|${LB}project${RB}|${PROJECT_LOWER}|g" + -e "s|${LB}REPO${RB}|${REPO}|g" + -e "s|${LB}OWNER${RB}|${OWNER}|g" + -e "s|${LB}AUTHOR${RB}|${AUTHOR}|g" + -e "s|${LB}AUTHOR_EMAIL${RB}|${AUTHOR_EMAIL}|g" + -e "s|${LB}AUTHOR_ORG${RB}|${AUTHOR_ORG}|g" + -e "s|${LB}AUTHOR_LAST${RB}|${AUTHOR_LAST}|g" + -e "s|${LB}AUTHOR_FIRST${RB}|${AUTHOR_FIRST}|g" + -e "s|${LB}AUTHOR_INITIALS${RB}|${FIRST_INITIAL}|g" + -e "s|${LB}FORGE${RB}|${FORGE}|g" + -e "s|${LB}CURRENT_YEAR${RB}|${CURRENT_YEAR}|g" + -e "s|${LB}CURRENT_DATE${RB}|${CURRENT_DATE}|g" + -e "s|${LB}DATE${RB}|${CURRENT_DATE}|g" + -e "s|${LB}SECURITY_EMAIL${RB}|${SECURITY_EMAIL}|g" + -e "s|${LB}CONDUCT_EMAIL${RB}|${CONDUCT_EMAIL}|g" + -e "s|${LB}LICENSE${RB}|MPL-2.0|g" + -e "s|${LB}CONDUCT_TEAM${RB}|Code of Conduct Committee|g" + -e "s|${LB}RESPONSE_TIME${RB}|48 hours|g" + -e "s|${LB}MAIN_BRANCH${RB}|main|g" + -e "s|${LB}PROJECT_PURPOSE${RB}|${PROJECT_DESCRIPTION}|g" + -e "s|${LB}PROJECT_ROLE${RB}|${PROJECT_TYPE}|g" + -e "s|${LB}PROJECT_TYPE${RB}|${PROJECT_TYPE}|g" + -e "s|${LB}WEBSITE${RB}|${WEBSITE}|g" + -e "s|${LB}SERVICE_NAME${RB}|${SERVICE_NAME}|g" + -e "s|${LB}PORT${RB}|${PORT}|g" + -e "s|${LB}REGISTRY${RB}|${REGISTRY}|g" + -e "s|${LB}IMAGE${RB}|${REGISTRY}/${SERVICE_NAME}|g" + -e "s|${LB}VERSION${RB}|${VERSION}|g" + -e "s|${LB}EMAIL${RB}|${AUTHOR_EMAIL}|g" + ) + [ -n "$AUTHOR_EMAIL_ALT" ] && SED_ARGS+=(-e "s|${LB}AUTHOR_EMAIL_ALT${RB}|${AUTHOR_EMAIL_ALT}|g") + + # Replace in all text files (skip .git, LICENSE text, and binaries) + find . -type f \ + -not -path './.git/*' \ + -not -name 'MPL-2.0.txt' \ + -not -name '*.png' -not -name '*.jpg' -not -name '*.gif' \ + -not -name '*.woff' -not -name '*.woff2' \ + | while read -r file; do + if file --brief "$file" | grep -qi 'text\|ascii\|utf'; then + sed -i "${SED_ARGS[@]}" "$file" + fi + done + + # Also replace [YOUR-REPO-NAME] and [YOUR-NAME/ORG] in AI manifest + sed -i "s|\[YOUR-REPO-NAME\]|${PROJECT_NAME}|g" 0-AI-MANIFEST.a2ml 2>/dev/null || true + sed -i "s|\[YOUR-NAME/ORG\]|${OWNER}|g" 0-AI-MANIFEST.a2ml 2>/dev/null || true + + echo "" + echo "── Validation ───────────────────────────────────" + + # Check for remaining placeholders + PATTERN="${LB}[A-Z_]*${RB}" + REMAINING=$(grep -rl "$PATTERN" . --include='*.md' --include='*.adoc' --include='*.yml' --include='*.yaml' --include='*.a2ml' --include='*.toml' --include='*.scm' --include='*.ncl' --include='*.guix' --include='*.json' --include='*.sh' 2>/dev/null | grep -v '.git/' | grep -v '.machine_readable/ai/PLACEHOLDERS.adoc' || true) + if [ -n "$REMAINING" ]; then + echo "WARNING: Remaining placeholders in:" + echo "$REMAINING" | sed 's/^/ /' + echo "" + echo "Run: grep -rn '$LB' . --include='*.md' to inspect" + else + echo "All placeholders replaced successfully!" + fi + + echo "" + echo "Done! Next steps:" + echo " 1. Review changes: git diff" + echo " 2. Remove template cruft: rm .machine_readable/ai/PLACEHOLDERS.adoc" + echo " 3. Customize README.adoc for your project" + echo " 4. Commit: git add -A && git commit -m 'feat: initialize from RSR template'" + echo " 5. Push: git remote add origin git@github.com:$OWNER/$REPO.git && git push -u origin main" diff --git a/just/onboarding.just b/just/onboarding.just new file mode 100644 index 00000000..c2af0fbc --- /dev/null +++ b/just/onboarding.just @@ -0,0 +1,249 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ── Onboarding ──────────────────────────────────────────────── + +# Check that all required tools are installed and working +doctor: + #!/usr/bin/env bash + set -euo pipefail + PASS=0; FAIL=0; WARN=0 + check() { + local name="$1" cmd="$2" + if command -v "$cmd" >/dev/null 2>&1; then + ver=$("$cmd" --version 2>/dev/null | head -1 || echo "unknown") + echo " [OK] $name: $ver" + PASS=$((PASS + 1)) + else + echo " [FAIL] $name: '$cmd' not found" + FAIL=$((FAIL + 1)) + fi + } + check_optional() { + local name="$1" cmd="$2" + if command -v "$cmd" >/dev/null 2>&1; then + ver=$("$cmd" --version 2>/dev/null | head -1 || echo "unknown") + echo " [OK] $name: $ver" + PASS=$((PASS + 1)) + else + echo " [WARN] $name: '$cmd' not found (optional)" + WARN=$((WARN + 1)) + fi + } + echo "=== echidna: Doctor ===" + echo "" + echo "Required tools:" + check "Rust/cargo" "cargo" + check "just" "just" + check "pkg-config" "pkg-config" + # Check openssl headers + if pkg-config --exists openssl 2>/dev/null; then + echo " [OK] openssl-devel: $(pkg-config --modversion openssl)" + PASS=$((PASS + 1)) + else + echo " [FAIL] openssl-devel: not found (install openssl-devel or libssl-dev)" + FAIL=$((FAIL + 1)) + fi + echo "" + echo "Optional tools (full stack):" + check_optional "Podman" "podman" + check_optional "Idris2" "idris2" + check_optional "Zig" "zig" + check_optional "Julia" "julia" + check_optional "Bun" "bun" + check_optional "Chapel" "chpl" + check_optional "panic-attack" "panic-attack" + echo "" + echo "Prover backends (spot check):" + check_optional "Z3" "z3" + check_optional "CVC5" "cvc5" + echo "" + echo "Build artefacts:" + if [ -f "target/debug/echidna" ] || [ -f "target/release/echidna" ]; then + echo " [OK] echidna binary built" + else + echo " [INFO] echidna not built — run 'just build'" + fi + echo "" + echo "=== Results: $PASS passed, $FAIL failed, $WARN warnings ===" + if [ "$FAIL" -gt 0 ]; then + echo "Run 'just heal' for install instructions." + exit 1 + fi + +# Show install instructions for missing tools +heal: + #!/usr/bin/env bash + set -euo pipefail + echo "=== echidna: Heal ===" + echo "" + echo "Install missing tools:" + echo "" + if ! command -v cargo >/dev/null 2>&1; then + echo " Rust (REQUIRED):" + echo " asdf install rust nightly" + echo " # Or: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" + echo "" + fi + if ! pkg-config --exists openssl 2>/dev/null; then + echo " OpenSSL development headers (REQUIRED):" + echo " Fedora: sudo dnf install openssl-devel pkg-config" + echo " Ubuntu: sudo apt install libssl-dev pkg-config" + echo "" + fi + if ! command -v podman >/dev/null 2>&1; then + echo " Podman (for containers):" + echo " Fedora: sudo dnf install podman" + echo " Ubuntu: sudo apt install podman" + echo "" + fi + if ! command -v idris2 >/dev/null 2>&1; then + echo " Idris2 (for ABI layer):" + echo " Install via pack: https://github.com/stefan-hoeck/idris2-pack" + echo "" + fi + if ! command -v zig >/dev/null 2>&1; then + echo " Zig (for FFI bridge):" + echo " asdf plugin add zig && asdf install zig 0.13.0" + echo "" + fi + if ! command -v julia >/dev/null 2>&1; then + echo " Julia (for ML layer):" + echo " asdf plugin add julia && asdf install julia latest" + echo "" + fi + if ! command -v z3 >/dev/null 2>&1; then + echo " Z3 (SMT solver):" + echo " Fedora: sudo dnf install z3" + echo " Ubuntu: sudo apt install z3" + echo "" + fi + if ! command -v panic-attack >/dev/null 2>&1; then + echo " panic-attack (pre-commit scans):" + echo " cargo install --git https://github.com/hyperpolymath/panic-attacker" + echo "" + fi + echo "After installing, run 'just doctor' to verify." + +# Guided tour of the repository structure +tour: + #!/usr/bin/env bash + set -euo pipefail + echo "=== ECHIDNA: Guided Tour ===" + echo "" + echo "ECHIDNA is a neurosymbolic theorem proving platform with 48 prover backends." + echo "" + echo "1. RUST CORE: src/rust/" + echo " The main implementation. 48 prover backends, trust pipeline, CLI, REPL." + echo " src/rust/main.rs - CLI entry point" + echo " src/rust/repl.rs - Interactive REPL" + echo " src/rust/server.rs - HTTP API server" + echo " src/rust/provers/ - 30 prover backend implementations" + echo " src/rust/verification/ - Trust pipeline (portfolio, certificates, axioms)" + echo " src/rust/integrity/ - Solver binary integrity (SHAKE3-512, BLAKE3)" + echo " src/rust/executor/ - Sandboxed execution (Podman, bubblewrap)" + echo " src/rust/exchange/ - Cross-prover proof exchange" + echo " src/rust/agent/ - Agentic proof search (actor model)" + echo "" + echo "2. API INTERFACES: src/interfaces/" + echo " graphql/ - async-graphql (port 8082)" + echo " grpc/ - tonic gRPC (port 50051)" + echo " rest/ - axum + OpenAPI (port 8000)" + echo "" + echo "3. ML LAYER: src/julia/" + echo " Julia-based tactic prediction and premise selection." + echo "" + echo "4. ABI/FFI:" + echo " src/abi/ - Idris2 formal type definitions" + echo " ffi/zig/ - Zig FFI bridge (4 shared libraries)" + echo "" + echo "5. UI: src/ui/" + echo " AffineScript-TEA (src/ui/tea/) + static shell (src/ui/public/)." + echo "" + echo "6. BUILD SYSTEM:" + echo " Cargo.toml - Rust workspace" + echo " Justfile - Primary build system" + echo " guix.scm - Guix package definition (PRIMARY; no Guix mirror — estate ruling 2026-05-18)" + echo " .containerization/Containerfile.wave3 - sealed-container escape hatch (Tier-3 provers)" + echo "" + echo "Try: just build (compile Rust core)" + echo " just test (run 232 unit tests)" + echo " just run repl (interactive theorem prover)" + +# Show available recipes with descriptions +help-me: + #!/usr/bin/env bash + echo "=== ECHIDNA: Help ===" + echo "" + echo "Onboarding:" + echo " just doctor - Check required tools are installed" + echo " just heal - Show install instructions for missing tools" + echo " just tour - Guided walkthrough of repo structure" + echo " just help-me - This help message" + echo "" + echo "Build:" + echo " just build - Build debug binary" + echo " just build-release - Build optimised release binary" + echo " just build-riscv - Cross-compile for RISC-V" + echo "" + echo "Testing:" + echo " just test - Run unit tests (232)" + echo " just test-all - Run all tests (389)" + echo " just test-verbose - Run tests with output" + echo " just test-integration - Integration tests only" + echo " just test-neural - Neural integration tests (requires Julia)" + echo "" + echo "Quality:" + echo " just fmt - Format code" + echo " just fmt-check - Check formatting" + echo " just lint - Run clippy lints" + echo " just check - Cargo check (fast)" + echo " just pre-commit - Full pre-commit checks" + echo " just assail - panic-attacker scan" + echo "" + echo "Running:" + echo " just run repl - Interactive REPL" + echo " just run serve - Start API server" + echo " just run prove - Prove a file" + echo "" + echo "Containers:" + echo " just container-build - Minimal image" + echo " just container-build-full - Full image (all provers)" + echo " just container-build-tier3 - All Wave-3 prover images (except imandra)" + echo " just container-build-tamarin - Tamarin security protocol prover" + echo " just container-build-proverif - ProVerif cryptographic protocol verifier" + echo " just container-build-metamath - Metamath (pure-Rust in-process + bundled databases)" + echo " just container-build-twelf - Twelf LF meta-logical framework" + echo " just container-build-or-tools - OR-Tools combinatorial optimisation C++ library" + echo " just container-build-scip - SCIP constraint integer programming solver" + echo " just container-build-hol4 - HOL4 (Poly/ML build, slow)" + echo " just container-build-acl2 - ACL2 (SBCL build, slow)" + echo " just container-build-imandra - Imandra stub (proprietary licence required)" + echo " just container-run - Run container" + echo "" + echo "Chapel accelerator:" + echo " just chapel-all - Build + test Chapel layer" + echo "" + echo "Housekeeping:" + echo " just clean - Remove build artefacts" + echo " just doc - Generate and open docs" + echo " just update - Update dependencies" + echo " just audit - Audit dependencies" + + +# Print the current CRG grade (reads from READINESS.md '**Current Grade:** X' line) +crg-grade: + @grade=$$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ + [ -z "$$grade" ] && grade="X"; \ + echo "$$grade" + +# Generate a shields.io badge markdown for the current CRG grade +# Looks for '**Current Grade:** X' in READINESS.md; falls back to X +crg-badge: + @grade=$$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ + [ -z "$$grade" ] && grade="X"; \ + case "$$grade" in \ + A) color="brightgreen" ;; B) color="green" ;; C) color="yellow" ;; \ + D) color="orange" ;; E) color="red" ;; F) color="critical" ;; \ + *) color="lightgrey" ;; esac; \ + echo "[![CRG $$grade](https://img.shields.io/badge/CRG-$$grade-$$color?style=flat-square)](https://github.com/hyperpolymath/standards/tree/main/component-readiness-grades)" diff --git a/just/other.just b/just/other.just new file mode 100644 index 00000000..3fc6ca3f --- /dev/null +++ b/just/other.just @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ── Other ─────────────────────────────────────────────────── + +# [AUTO-GENERATED] Multi-arch / RISC-V target +build-riscv: + @echo "Building for RISC-V..." + cross build --target riscv64gc-unknown-linux-gnu + +# Run panic-attacker pre-commit scan +assail: + @command -v panic-attack >/dev/null 2>&1 && panic-attack assail . || echo "panic-attack not found — install from https://github.com/hyperpolymath/panic-attacker" diff --git a/just/proofs.just b/just/proofs.just new file mode 100644 index 00000000..5e97cc0d --- /dev/null +++ b/just/proofs.just @@ -0,0 +1,114 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ── Dogfood proof corpus (proofs/{coq,lean,agda} + src/idris validator) ── +# Each recipe assumes its toolchain is already installed (see `just doctor`); +# CI installs the toolchains first, then calls these so local and CI run the +# exact same commands. + +# Type-check the whole dogfood proof corpus across every assistant. +proofs: proofs-coq proofs-lean proofs-agda proofs-idris proofs-verif-lean proofs-verif-idris + +# Compile the Coq proof corpus (proofs/coq/**/*.v). +proofs-coq: + #!/usr/bin/env bash + set -euo pipefail + cd proofs/coq + for f in $(find . -name '*.v' | sort); do + echo "coqc $f" + coqc -q "$f" + done + echo "Coq corpus: all files type-check" + +# Build the Lean 4 proof corpus via Lake (proofs/lean). +proofs-lean: + cd proofs/lean && lake build + +# Type-check the Agda proof corpus (proofs/agda/*.agda). +proofs-agda: + #!/usr/bin/env bash + set -euo pipefail + # Agda writes UTF-8 to stdout and aborts under a non-UTF-8 locale (C/POSIX). + export LC_ALL="${LC_ALL:-C.UTF-8}" + cd proofs/agda + for f in $(find . -name '*.agda' -not -path './_build/*' -not -path './dist-newstyle/*' | sort); do + echo "agda --safe --without-K --cubical-compatible $f" + agda --safe --without-K --cubical-compatible "$f" + done + echo "Agda corpus: all files type-check" + +# Type-check the Idris2 validator (src/idris) — --typecheck needs no codegen backend. +proofs-idris: + cd src/idris && idris2 --typecheck echidna-validator.ipkg + +# Distinct from proofs-lean: the verification-pipeline *property* proofs +# (ConfidenceLattice, ParetoMaximality, ParetoStrongMaximality, IntegrityVerification, +# PortfolioCompleteness) under verification/proofs/lean4, not the dogfood corpus under +# proofs/lean. No mathlib dependency (lake-manifest packages: []), so the build is light. +# Build the Lean 4 trust-pipeline property proofs via Lake (verification/proofs/lean4). +proofs-verif-lean: + cd verification/proofs/lean4 && lake build + +# DispatchCorrectness imports EchidnaABI.Types, so the echidnaabi package +# (src/abi) is installed first (--install builds it on the way); the ipkg then +# type-checks all 11 modules with a reliable aggregate exit code. A bare per-file +# `idris2 --check` is unsafe here -- it exits 0 even on an unresolved import -- so +# the ipkg build is the gate and any "Error" line is also treated as failure. +# Type-check the Idris2 trust-pipeline property proofs (verification/proofs/idris2). +proofs-verif-idris: + #!/usr/bin/env bash + set -euo pipefail + (cd src/abi && idris2 --install echidnaabi.ipkg >/dev/null) + cd verification/proofs/idris2 + out=$(idris2 --typecheck echidna-verif-proofs.ipkg 2>&1) + printf '%s\n' "$out" + if printf '%s\n' "$out" | grep -qE '^Error'; then + echo "Idris2 verification corpus: TYPE ERRORS DETECTED" >&2 + exit 1 + fi + echo "Idris2 verification corpus: all files type-check" + +# Accept fixtures must type-check, reject fixtures must be refused (tests/abi). +# Type-check the Idris2 ABI accept/reject fixtures (mechanical falsifier). +abi-fixtures: + bash scripts/abi-fixtures.sh + +# Heavy (downloads the ~500MB Isabelle release), so this is gated by the weekly +# verification-proofs-cron workflow, not the per-PR `proofs` rollup. Covers the +# base-HOL theories; algebra/GroupTheory.thy is excluded (see proofs/isabelle/ROOT). +# Verify the Isabelle/HOL self-proof corpus (proofs/isabelle) via `isabelle build`. +proofs-isabelle: + cd proofs/isabelle && isabelle build -d . -v Echidna_Isabelle + +# Mizar is not packaged for apt and ships only from mizar.org (Tier-4 in this +# project); like Isabelle it is gated by the weekly cron, not per-PR. MIZFILES +# must point at the Mizar share dir. Errors are reported in a per-file .err file +# (the verifier's exit code is not reliable), so a non-empty .err fails the recipe. +# Verify the Mizar self-proof corpus (proofs/mizar) with the Mizar verifier. +proofs-mizar: + #!/usr/bin/env bash + # Not -e: makeenv/verifier signal errors via a per-file .err (line col code), + # not a reliable exit code, so inspect that rather than abort on first non-zero. + set -uo pipefail + : "${MIZFILES:?MIZFILES must point at the Mizar share dir}" + cd proofs/mizar + rc=0 + for f in *.miz; do + base="${f%.miz}" + echo "=== $f ===" + makeenv "$f" || echo "(makeenv exit $?)" + verifier "$f" || echo "(verifier exit $?)" + if [ -s "$base.err" ]; then + echo "--- $base.err (line col code) ---" + cat "$base.err" + while read -r line col code; do + src="$(sed -n "${line}p" "$f" 2>/dev/null)" + [ -n "$src" ] && echo " L${line}:${col} (code ${code}): ${src}" + done < "$base.err" + rc=1 + else + echo "($f verifies clean)" + fi + done + [ "$rc" -eq 0 ] || { echo "Mizar corpus: VERIFICATION ERRORS (see .err above)" >&2; exit 1; } + echo "Mizar corpus: all files verify" diff --git a/just/saturation.just b/just/saturation.just new file mode 100644 index 00000000..cf96440f --- /dev/null +++ b/just/saturation.just @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ───────────────────────────────────────────────────────────────────── +# Saturation campaign 2026-06-01 — recipes for the new corpus / vocab / +# arbiter / exchange surface. See docs/decisions/2026-06-01-saturation- +# campaign.md and docs/CORPUS-ADAPTERS.adoc. +# ───────────────────────────────────────────────────────────────────── + +# Ingest a project tree via one of the 13 new corpus adapters +# (isabelle / metamath / mizar / hol_light / hol4 / dafny / why3 / +# fstar / acl2_books / tptp / smtlib / proofnet / minif2f). +corpus-ingest-saturation adapter root: + cargo run --bin echidna -- corpus ingest --root {{root}} --adapter {{adapter}} + +# Ingest an Ada/SPARK tree (specs `*.ads`, bodies `*.adb`). Detects +# GNATprove escape hatches (`pragma Assume`, suppressed checks) and the +# sparklib `Lemma_*` convention. +corpus-ingest-spark root: + cargo run --bin echidna -- corpus ingest --root {{root}} --adapter spark_ada + +# Ingest a Rust/Creusot tree (`#[requires]` / `#[ensures]` / `pearlite!`). +# `#[trusted]` is reported as a postulate, not as a proved function. +corpus-ingest-creusot root: + cargo run --bin echidna -- corpus ingest --root {{root}} --adapter creusot + +# Ingest the hyperpolymath estate: every repo's `src/abi` Idris2 tree plus +# the Agda type-theory libraries (echo/epistemic/secret/tropical/ +# choreographic-types), merged into one corpus tagged with repo + layer. +# ROOT is the directory that contains the repositories. +corpus-ingest-estate root="..": + cargo run --bin echidna -- corpus ingest --root {{root}} --adapter hyperpolymath_estate + +# Run the corpus-stats summary across all 20 adapter outputs found under +# data/corpus/*.json (skips any missing without failing). +corpus-stats-all: + @for f in data/corpus/*.json; do \ + [ -f "$f" ] || continue; \ + echo "--- $f"; \ + cargo run --bin echidna --quiet -- corpus stats --index "$f" 2>/dev/null || echo " (skipped)"; \ + done + +# Load every per-prover synonym TOML + the 5 cross-prover dictionaries +# (_msc2020 / _wordnet_math / _conceptnet_seed / _disciplines / +# _verification_systems) and report counts. +synonym-load-test: + cargo test --lib --quiet suggest::synonyms 2>&1 | tail -20 + +# Lexicon v2: apply the Nickel contract (data/synonyms/schema.ncl) to every +# synonym table and to the accept/reject fixtures. Needs nickel >= 1.12. +lexicon-check: + scripts/lexicon-check.sh + cargo test --locked --test lexicon_v2_test + +# Mine lexicon candidates across training_data/ corpora: cognates, false +# friends and synonym candidates as JCS JSON Lines under target/lexicon-scan. +# Flag-only (evidence = none); never edits data/synonyms/. Presets: learning, +# spanning, in-family, full. +lexicon-scan preset="learning": + cargo run --locked --bin echidna -- lexicon scan --preset {{preset}} + cargo test --locked --test lexicon_scan_test + +# Run the saturation-campaign cargo tests only (139 cases + 1 ignored). +test-saturation: + cargo test --lib -- \ + corpus:: \ + verification::bayesian_arbiter \ + verification::dempster_shafer \ + verification::pareto_arbiter \ + exchange::tptp \ + exchange::smtlib \ + exchange::smtcoq \ + exchange::lambdapi \ + suggest::synonyms + +# Smoke-test the arbiter trio with synthetic evidence. +# Prints posterior verdict from each of: Bayesian, Dempster-Shafer, Pareto. +arbiter-smoke: + cargo run --bin echidna --quiet -- arbiter smoke 2>/dev/null || \ + echo "(arbiter-smoke CLI not yet wired — see src/rust/verification/{bayesian,dempster_shafer,pareto}_arbiter.rs for unit-test entry points)" + +# Validate that the E-R schema doc + Cap'n Proto schema are +# byte-identical with the recorded SHA in .machine_readable/er-schema.sha256. +# (CI gate planned — see docs/architecture/VERISIM-ER-SCHEMA.adoc +# "Drift detection".) +er-schema-drift-check: + @echo "Checking E-R schema drift..." + @sha256sum docs/architecture/VERISIM-ER-SCHEMA.adoc \ + crates/echidna-wire/schemas/verisim_er.capnp \ + | sha256sum | head -c 64 + @echo " (current combined hash; compare against .machine_readable/er-schema.sha256 when CI gate lands)" + + +secret-scan-trufflehog: + @command -v trufflehog >/dev/null && trufflehog filesystem . --only-verified || true diff --git a/just/spark.just b/just/spark.just new file mode 100644 index 00000000..e15ca1d7 --- /dev/null +++ b/just/spark.just @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ── SPARK axiom-policy layer ───────────────────────────────── + +# Step 1: compile Ada/SPARK to static library (requires GNAT + GNATprove) +build-spark-ada: + gprbuild -P src/ada/spark/echidna_spark.gpr -j0 + @echo "SPARK library built at lib/spark/libechidna_spark.a" + +# Step 2: run GNATprove to formally verify the SPARK contracts +prove-spark: + gnatprove -P src/ada/spark/echidna_spark.gpr --level=2 \ + --prover=z3,cvc5,altergo --timeout=30 --report=fail -j0 + @echo "GNATprove: all SPARK contracts discharged" + +# Step 3: compile Zig bridge (links against libechidna_spark.a) +build-spark-zig: build-spark-ada + cd src/zig && zig build -Dspark=true -Doptimize=ReleaseSafe + @echo "SPARK Zig bridge built at src/zig/zig-out/lib/libechidna_spark_zig.a" + +# Step 4: build Rust with SPARK feature enabled +build-spark: build-spark-zig + cargo build --features spark + @echo "Rust+SPARK build complete" + +# Full SPARK pipeline: prove → build → test +spark-all: prove-spark build-spark-zig + cargo test --features spark -- spark + @echo "SPARK layer: proved, built, tested" + +# Cross-check: run both Rust and SPARK enforce_policy, assert agreement +verify-spark-crosscheck: build-spark + cargo test --features spark -- crosscheck + @echo "SPARK/Rust crosscheck passed" diff --git a/just/ui.just b/just/ui.just new file mode 100644 index 00000000..fada9a38 --- /dev/null +++ b/just/ui.just @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: MPL-2.0 +# Imported by the root Justfile (hotspot split); recipes moved verbatim. + +# ── UI Management ────────────────────────────────────────── + +# ── UI ── +# The AffineScript UI was removed 2026-08 (AffineScript is a banned language under the +# estate language policy; AffineScript-TEA is the replacement). The TEA sources +# live in src/ui/tea/ and the static shell in src/ui/public/, but the +# AffineScript compile pipeline is NOT wired yet — see issues #117 and #266 +# (missing Http::fetch / Async / Json primitives). + +# Build the UI — blocked on the AffineScript-TEA toolchain +build-ui: + @echo "build-ui is unavailable: the AffineScript UI was removed and the" + @echo "AffineScript-TEA pipeline is not wired yet (issues #117, #266)." + @echo "Sources: src/ui/tea/echidna_gui.affine; shell: src/ui/public/." + @exit 1 + +# Serve the static UI shell (no compile step) on http://localhost:3000. +# Bun is the estate JavaScript runtime; Deno is retired (2026-09-22 ruling). +serve-ui: + cd src/ui/public && bun ../../../scripts/serve-static.js 3000 + +# Launch full ECHIDNA GUI (Backend + UI + Browser) +gui: + #!/usr/bin/env bash + set -euo pipefail + echo "Launching ECHIDNA GUI..." + # Start backend + cargo run -- server --port 8081 --cors > /dev/null 2>&1 & BACKEND_PID=$! + echo "Started backend (PID $BACKEND_PID)" + # Start UI server + (cd src/ui/public && bun ../../../scripts/serve-static.js 3000) > /dev/null 2>&1 & UI_PID=$! + echo "Started UI server (PID $UI_PID)" + # Cleanup on exit + trap "kill $BACKEND_PID $UI_PID 2>/dev/null || true" EXIT + # Wait for servers to start + echo "Waiting for servers to initialize..." + sleep 2 + # Open browser + URL="http://localhost:3000" + echo "Opening $URL..." + xdg-open "$URL" > /dev/null 2>&1 || open "$URL" > /dev/null 2>&1 || echo "Please open $URL in your browser" + echo "ECHIDNA GUI is running. Press Ctrl+C in this terminal to stop." + # Wait for background processes + wait + +# All checks before commit +pre-commit: fmt-check lint test + @echo "All checks passed!" + +# MVP 1.0 smoke checks +mvp: + ./scripts/mvp-smoke.sh + +# MVP 1.0 dependency checklist (best-effort) +mvp-env: + ./scripts/mvp-smoke.sh || true + +# Build minimal container image (Z3, CVC5, Lean, Idris2) +container-build: + podman build -f .containerization/Containerfile -t echidna:latest . + +# Build full container image (all provers + Julia) +container-build-full: + podman build -f .containerization/Containerfile.full -t echidna:full . diff --git a/src/rust/cli/args.rs b/src/rust/cli/args.rs new file mode 100644 index 00000000..384622df --- /dev/null +++ b/src/rust/cli/args.rs @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: args. Moved verbatim from main.rs (hotspot split). + +use crate::output::OutputFormat; +use clap::{Parser, Subcommand}; +use echidna::ProverKind; +use std::path::PathBuf; + +/// Result form of `echidna prove`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)] +pub(crate) enum ProveOutput { + /// Human-readable report (the default). + Human, + /// One `echidna.prove.result/1` JSON object on stdout. + Json, +} + +/// ECHIDNA - Extensible Cognitive Hybrid Intelligence for Deductive Neural Assistance +#[derive(Parser)] +#[command(name = "echidna")] +#[command(version, about, long_about = None)] +#[command(author = "ECHIDNA Project Team")] +pub(crate) struct Cli { + /// Output format (text, json) + #[arg(long, global = true, default_value = "text")] + pub(crate) format: OutputFormat, + + /// Verbose output + #[arg(short, long, global = true)] + pub(crate) verbose: bool, + + /// Disable colors + #[arg(long, global = true)] + pub(crate) no_color: bool, + + #[command(subcommand)] + pub(crate) command: Commands, +} + +#[derive(Subcommand)] +pub(crate) enum Commands { + /// Prove a theorem from file + Prove { + /// Path to proof file + file: PathBuf, + + /// Prover to use (auto-detect if not specified) + #[arg(short, long)] + prover: Option, + + /// Timeout in seconds + #[arg(short, long, default_value = "300")] + timeout: u64, + + /// Enable neural premise selection + #[arg(long, default_value = "true")] + neural: bool, + + /// Prover executable path (override default) + #[arg(long)] + executable: Option, + + /// Library paths + #[arg(long)] + library: Vec, + + /// On failure, run diagnostics and explain why the proof failed + #[arg(long)] + diagnose: bool, + + /// Project root for backends with session-based builds (EI-1). + /// Today: Isabelle uses this to add `-d ` to the build + /// invocation and to import the file's session from the + /// project's existing ROOT, so theory imports resolve. + #[arg(long)] + project_root: Option, + + /// Sandbox mode for prover invocation (safe-learning b). + /// One of: none (default), bwrap, podman. + #[arg(long, default_value = "none")] + sandbox: String, + + /// Result form. `human` (default) prints the usual report; `json` + /// prints exactly one `echidna.prove.result/1` object (JCS-canonical + /// I-JSON) on stdout and sends logs to stderr. + #[arg(long, value_enum, default_value_t = ProveOutput::Human)] + output: ProveOutput, + }, + + /// Verify an existing proof + Verify { + /// Path to proof file + file: PathBuf, + + /// Prover to use (auto-detect if not specified) + #[arg(short, long)] + prover: Option, + + /// Timeout in seconds + #[arg(short, long, default_value = "300")] + timeout: u64, + + /// Prover executable path (override default) + #[arg(long)] + executable: Option, + + /// Library paths + #[arg(long)] + library: Vec, + + /// On failure, run diagnostics and explain why the proof failed + #[arg(long)] + diagnose: bool, + }, + + /// Search theorem libraries + Search { + /// Search pattern (regex supported) + pattern: String, + + /// Prover to search (searches all if not specified) + #[arg(short, long)] + prover: Option, + + /// Maximum results to return + #[arg(short, long, default_value = "20")] + limit: usize, + }, + + /// Start interactive REPL mode + Interactive { + /// Initial proof file to load + file: Option, + + /// Prover to use + #[arg(short, long)] + prover: Option, + + /// Enable neural premise selection + #[arg(long, default_value = "true")] + neural: bool, + }, + + /// Start HTTP API server + Server { + /// Port to bind to + #[arg(short, long, default_value = "8081")] + port: u16, + + /// Host to bind to + #[arg(long, default_value = "127.0.0.1")] + host: String, + + /// Enable CORS + #[arg(long)] + cors: bool, + }, + + /// List available provers + ListProvers { + /// Show detailed information + #[arg(short, long)] + detailed: bool, + }, + + /// Show information about a specific prover + Info { + /// Prover name + prover: ProverKind, + }, + + /// Interactive diagnostics REPL for health monitoring + Diagnostics, + + /// Build / query a project corpus (named decls + dependency DAG) + Corpus { + #[command(subcommand)] + op: CorpusOp, + }, + + /// Mine lexicon candidates across training corpora (flag-only) + Lexicon { + #[command(subcommand)] + op: LexiconOp, + }, + + /// Search proof-design space via simulated annealing + Design { + #[command(subcommand)] + op: DesignOp, + }, + + /// Suggest tactic variants that close a failing lemma + Suggest { + /// Target in `:` form, e.g. `Foo.thy:bar` + #[arg(value_name = "TARGET")] + target: String, + + /// Prover to use (auto-detected from file extension if absent) + #[arg(short, long)] + prover: Option, + + /// Time budget per variant attempt in seconds + #[arg(long, default_value_t = 60)] + budget: u64, + + /// Maximum number of variants to report + #[arg(long, default_value_t = 10)] + top: usize, + + /// Synonym table directory (defaults to `data/synonyms/`) + #[arg(long)] + synonyms_dir: Option, + + /// Don't run the prover; just print candidate variants + #[arg(long)] + dry_run: bool, + + /// Maximum concurrent variant tests + #[arg(long, default_value_t = 4)] + max_parallel: usize, + }, +} + +#[derive(Subcommand)] +pub(crate) enum CorpusOp { + /// Walk a project tree and build a corpus index. + Ingest { + /// Path to the project root. + #[arg(short, long)] + root: PathBuf, + /// Adapter to use: agda, coq, lean, idris2, acl2_books, creusot, + /// dafny, fstar, hol4, hol_light, isabelle, metamath, minif2f, + /// mizar, proofnet, smtlib, spark_ada, tptp, why3, or + /// hyperpolymath_estate (the multi-repo estate profile). + #[arg(short, long, default_value = "agda")] + adapter: String, + /// Where to write the JSON index. Defaults to + /// `data/corpus/.json`. + #[arg(short, long)] + out: Option, + }, + /// Query a previously-ingested corpus index. + Query { + /// Search term (qualified name, short name, or substring). + pattern: String, + /// Path to the corpus JSON. Defaults to + /// `data/corpus/.json` for the cwd's basename. + #[arg(short, long)] + index: Option, + /// Show transitive (forward) dependencies of the matched entry. + #[arg(long)] + deps: bool, + /// Show direct reverse-dependencies — entries that reference + /// the matched name, i.e. things that would (potentially) + /// break if the matched entry changed. + #[arg(long)] + reverse_deps: bool, + /// Show transitive reverse-closure — full impact set. + #[arg(long)] + reverse_closure: bool, + }, + /// Print summary statistics for a corpus index. + Stats { + #[arg(short, long)] + index: Option, + }, + + /// Look up entries across every synonym table by `semantic_class`. + /// Demonstrates cross-prover semantic equivalence: `corpus crossquery + /// well-foundedness` reports `WellFounded` in Agda, `well_founded` in + /// Coq, and `WellFounded` in Lean and Idris 2. + /// + /// The cross-prover dictionaries are searched too, so classes that + /// only exist there (`wrapping-arithmetic`, `frame-rule`, + /// `estate-snif`, or an MSC code such as `68Q60`) resolve. + Crossquery { + /// Semantic class to look up (e.g. `well-foundedness`). + class: String, + /// Synonym directory (defaults to `data/synonyms`). + #[arg(long)] + synonyms_dir: Option, + /// Only search the per-prover tables, skipping the cross-prover + /// dictionaries. + #[arg(long)] + provers_only: bool, + }, + + /// Find the K nearest-neighbour entries to a free-form text + /// query by cosine similarity over the Vector octad embeddings. + /// The query is canonicalised the same way the corpus is, so a + /// statement written in one prover's syntax matches entries written + /// in another's. Useful for "find lemmas structurally near + /// `WellFounded _<_`". + Near { + query: String, + #[arg(short, long)] + index: Option, + #[arg(short, long, default_value_t = 10)] + top: usize, + }, + + /// Group matching declarations by their *structural* identity key. + /// + /// The key is the SHA-256 of the type's canonical shape — variable + /// names, module paths, operator spellings and binder keywords are + /// normalised away — so `+-comm` in Agda, `Nat.add_comm` in Lean and + /// `eq Nat (add x y) (add y x)` in Dedukti land on one key with no + /// synonym lookup. `--skeleton` additionally prints the binder- and + /// sort-free key, which is what provers that elide binders (Isabelle) + /// match on. + Identity { + /// Name pattern to match: a substring of the qualified or short + /// name, case-insensitive. `*` matches every declaration. + pattern: String, + /// Path to the corpus JSON. Defaults to + /// `data/corpus/.json` for the cwd's basename. + #[arg(short, long)] + index: Option, + /// Also print the skeleton key (binders and sorts erased). + #[arg(long)] + skeleton: bool, + /// Only show keys that group declarations from more than one + /// adapter. + #[arg(long)] + cross_only: bool, + }, + + /// Convert a JSON corpus index into 8-modality octad JSONL + /// (one DeclarationOctad per line). Streamable; compatible with + /// VeriSim's `/api/v1/octads` endpoint when posted line-by-line. + ExportOctads { + /// Path to the JSON corpus index. + #[arg(short, long)] + index: PathBuf, + /// Output JSONL path. + #[arg(short, long)] + out: PathBuf, + }, + + /// Load an octad JSONL stream back into a corpus and persist as + /// JSON. Round-trip is best-effort but lossless for the fields + /// the corpus cares about (statements, proofs, deps, hazards). + ImportOctads { + #[arg(short, long)] + octads: PathBuf, + #[arg(short, long)] + out: PathBuf, + }, +} + +#[derive(Subcommand)] +pub(crate) enum LexiconOp { + /// Pair theorem names and canonical statement shapes across training + /// corpora and write review candidates as JCS JSON Lines. Every + /// candidate carries `evidence = none`; `data/synonyms/` is never edited. + Scan { + /// Directory holding `proof_states_.jsonl` files. + #[arg(long, default_value = "training_data")] + training_dir: PathBuf, + /// A corpus pair, `A:B`. Repeatable; combines with `--preset`. + #[arg(long = "pair", value_name = "A:B")] + pairs: Vec, + /// A named pair set: `learning`, `spanning`, `in-family` or `full`. + #[arg(long)] + preset: Option, + /// Output directory (pairs/, collisions/, summary.jsonl, coverage.jsonl). + #[arg(long, default_value = "target/lexicon-scan")] + out: PathBuf, + /// Also emit pairs whose shared name is generic (`lemma3`, `aux`, `ex1`). + #[arg(long)] + keep_generic: bool, + /// Skip a shared name whose statement cross product exceeds this: + /// exact pairs per raw name, folded pairs per whole folded group. + #[arg(long, default_value_t = 64)] + max_pairs_per_name: usize, + /// Skip a shape or skeleton group larger than this on either side. + #[arg(long, default_value_t = 8)] + max_shape_group: usize, + /// Key-collision groups to sample per corpus. + #[arg(long, default_value_t = 40)] + collision_samples: usize, + }, +} + +#[derive(Subcommand)] +pub(crate) enum DesignOp { + /// Run SA over a named design problem. + Search { + /// Problem identifier. Currently: `brouwer-leq` (Phase 1.3 + /// `_≤_` redesign for `Ordinal.Brouwer`). + problem: String, + /// Iterations per restart. + #[arg(long, default_value_t = 1000)] + iterations: usize, + /// Number of independent restarts. + #[arg(long, default_value_t = 4)] + restarts: usize, + /// Initial Metropolis temperature. + #[arg(long, default_value_t = 4.0)] + temp: f64, + /// Multiplicative cooling factor per iteration. + #[arg(long, default_value_t = 0.995)] + cooling: f64, + /// Seed. + #[arg(long, default_value_t = 0xC0FFEE)] + seed: u64, + /// Top-K candidates to print. + #[arg(long, default_value_t = 8)] + top: usize, + }, + + /// Run a parallel swarm of SA agents with cross-pollination. + Swarm { + /// Problem identifier. Currently: `brouwer-leq`. + problem: String, + /// Number of parallel agents. + #[arg(long, default_value_t = 4)] + agents: usize, + #[arg(long, default_value_t = 1000)] + iterations: usize, + #[arg(long, default_value_t = 4.0)] + temp: f64, + #[arg(long, default_value_t = 0.995)] + cooling: f64, + #[arg(long, default_value_t = 0xC0FFEE)] + seed: u64, + /// Broadcast best-so-far every N iterations. + #[arg(long, default_value_t = 100)] + broadcast_every: usize, + /// Adopt a peer's broadcast only if it beats the agent's best + /// by this lex-margin (0 = adopt on any improvement). + #[arg(long, default_value_t = 0)] + adopt_threshold: i64, + }, +} diff --git a/src/rust/cli/corpus.rs b/src/rust/cli/corpus.rs new file mode 100644 index 00000000..eca0240d --- /dev/null +++ b/src/rust/cli/corpus.rs @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: corpus. Moved verbatim from main.rs (hotspot split). + +use super::*; +use crate::output::OutputFormatter; +use anyhow::{Context, Result}; +use echidna::ProverKind; +use std::path::PathBuf; + +/// Corpus command - ingest, query, or summarise a project corpus. +/// +/// First adapter is `agda`, targeted at echo-types' Buchholz / Brouwer +/// proof tree. Build the index once with `corpus ingest --root …`, +/// then query it without re-walking the source. The index is plain +/// JSON at `data/corpus/.json` so other tools (suggest, the +/// upcoming SA design search) can consume it directly. +/// +/// Crossquery prints exact semantic-class and canonical-name matches from +/// per-prover tables and, unless `provers_only` is set, cross-prover dictionaries. +/// Identity queries skip declarations whose canonical keys are incomplete +/// and report the number skipped; only complete keys form identity groups. +/// +/// # Errors +/// +/// Returns an error for an unknown ingest adapter and propagates errors from +/// corpus ingestion, index or dictionary loading, export/import, saving and +/// output formatting. Resolving a default index can also fail if the working +/// directory cannot be obtained. Queries with no matches succeed. +pub(crate) fn corpus_command(op: CorpusOp, formatter: &OutputFormatter) -> Result<()> { + use echidna::corpus::{self, Corpus}; + + let default_index = |root_basename: &str| -> PathBuf { + PathBuf::from("data/corpus").join(format!("{}.json", root_basename)) + }; + + match op { + CorpusOp::Ingest { root, adapter, out } => { + let root = root.canonicalize().unwrap_or(root); + let basename = root + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "project".to_string()); + let out = out.unwrap_or_else(|| default_index(&basename)); + + let corpus = match adapter.as_str() { + "agda" => corpus::agda::ingest(&root) + .with_context(|| format!("agda ingest of {}", root.display()))?, + "coq" => corpus::coq::ingest(&root) + .with_context(|| format!("coq ingest of {}", root.display()))?, + "lean" | "lean4" => corpus::lean::ingest(&root) + .with_context(|| format!("lean ingest of {}", root.display()))?, + "idris2" | "idris" => corpus::idris2::ingest(&root) + .with_context(|| format!("idris2 ingest of {}", root.display()))?, + "acl2_books" => corpus::acl2_books::ingest(&root) + .with_context(|| format!("acl2_books ingest of {}", root.display()))?, + "dafny" => corpus::dafny::ingest(&root) + .with_context(|| format!("dafny ingest of {}", root.display()))?, + "spark_ada" | "spark" | "ada" => corpus::spark_ada::ingest(&root) + .with_context(|| format!("spark_ada ingest of {}", root.display()))?, + "creusot" | "rust_creusot" => corpus::creusot::ingest(&root) + .with_context(|| format!("creusot ingest of {}", root.display()))?, + "hyperpolymath_estate" | "estate" => corpus::profile::ingest(&root) + .with_context(|| format!("estate profile ingest of {}", root.display()))?, + "fstar" => corpus::fstar::ingest(&root) + .with_context(|| format!("fstar ingest of {}", root.display()))?, + "hol4" => corpus::hol4::ingest(&root) + .with_context(|| format!("hol4 ingest of {}", root.display()))?, + "hol_light" => corpus::hol_light::ingest(&root) + .with_context(|| format!("hol_light ingest of {}", root.display()))?, + "isabelle" => corpus::isabelle::ingest(&root) + .with_context(|| format!("isabelle ingest of {}", root.display()))?, + "metamath" => corpus::metamath::ingest(&root) + .with_context(|| format!("metamath ingest of {}", root.display()))?, + "minif2f" => corpus::minif2f::ingest(&root) + .with_context(|| format!("minif2f ingest of {}", root.display()))?, + "mizar" => corpus::mizar::ingest(&root) + .with_context(|| format!("mizar ingest of {}", root.display()))?, + "proofnet" => corpus::proofnet::ingest(&root) + .with_context(|| format!("proofnet ingest of {}", root.display()))?, + "smtlib" => corpus::smtlib::ingest(&root) + .with_context(|| format!("smtlib ingest of {}", root.display()))?, + "tptp" => corpus::tptp::ingest(&root) + .with_context(|| format!("tptp ingest of {}", root.display()))?, + "why3" => corpus::why3::ingest(&root) + .with_context(|| format!("why3 ingest of {}", root.display()))?, + other => { + return Err(anyhow::anyhow!( + "Unknown corpus adapter '{}' (supported: agda, coq, lean, idris2, acl2_books, creusot, dafny, fstar, hol4, hol_light, isabelle, metamath, minif2f, mizar, proofnet, smtlib, spark_ada, tptp, why3, hyperpolymath_estate)", + other + )) + }, + }; + corpus.save_json(&out)?; + let stats = corpus.stats(); + formatter.info(&format!( + "ingested {} module(s), {} entry/-ies ({} fn, {} data, {} record, {} postulate); {} entry/-ies have hazards; index: {}", + stats.modules, + stats.entries, + stats.functions, + stats.data, + stats.records, + stats.postulates, + stats.with_hazards, + out.display() + ))?; + }, + CorpusOp::Query { + pattern, + index, + deps, + reverse_deps, + reverse_closure, + } => { + let path = match index { + Some(p) => p, + None => { + let basename = std::env::current_dir()? + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "project".to_string()); + default_index(&basename) + }, + }; + let corpus = Corpus::load_json(&path) + .with_context(|| format!("load corpus index {}", path.display()))?; + let hits = corpus.find(&pattern); + if hits.is_empty() { + formatter.info(&format!("no matches for '{}'", pattern))?; + return Ok(()); + } + for entry in &hits { + let module = corpus + .modules + .get(entry.module_idx) + .map(|m| m.name.as_str()) + .unwrap_or("?"); + println!( + "{} [{:?}] {}:{}", + entry.qualified, entry.kind, module, entry.line + ); + println!(" : {}", entry.statement); + if entry.axiom_usage.any() { + println!(" ⚠ hazards: {:?}", entry.axiom_usage); + } + if !entry.dependencies.is_empty() { + let mut deps_list = entry.dependencies.clone(); + deps_list.sort(); + println!(" deps ({}): {}", deps_list.len(), deps_list.join(", ")); + } + } + if deps && hits.len() == 1 { + let closure = corpus.closure(&hits[0].qualified); + println!("\ntransitive closure ({} entries):", closure.len()); + let mut names: Vec<&str> = closure.iter().map(|e| e.qualified.as_str()).collect(); + names.sort(); + for n in names { + println!(" {}", n); + } + } + if reverse_deps && hits.len() == 1 { + let rev = corpus.reverse_deps(&hits[0].qualified); + println!( + "\ndirect reverse-deps of {} ({} entries):", + hits[0].qualified, + rev.len() + ); + let mut names: Vec<&str> = rev.iter().map(|e| e.qualified.as_str()).collect(); + names.sort(); + for n in names { + println!(" {}", n); + } + } + if reverse_closure && hits.len() == 1 { + let closure = corpus.reverse_closure(&hits[0].qualified); + println!( + "\nreverse closure (impact set) of {} ({} entries):", + hits[0].qualified, + closure.len() + ); + let mut names: Vec<&str> = closure.iter().map(|e| e.qualified.as_str()).collect(); + names.sort(); + for n in names { + println!(" {}", n); + } + } + }, + CorpusOp::Crossquery { + class, + synonyms_dir, + provers_only, + } => { + use echidna::suggest::synonyms::{ + load_all, load_cross_prover_dicts, SynonymEntry, SynonymTable, + }; + let dir = synonyms_dir.unwrap_or_else(|| PathBuf::from("data/synonyms")); + let tables = load_all(&dir) + .with_context(|| format!("load synonym tables from {}", dir.display()))?; + // Prints the hits in `table` for `class` and returns how many. + // A row whose canonical name *is* `class` also counts: the MSC + // dictionary is keyed by code, so `crossquery 68Q60` should find + // 68Q60 even though its semantic class is `program-specification`. + let print_hits = |table: &SynonymTable, label: std::fmt::Arguments<'_>| -> usize { + let mut hits: Vec<&SynonymEntry> = table.by_semantic_class(&class); + if let Some(entry) = table.entries.iter().find(|e| e.canonical == class) { + if !hits.iter().any(|h| h.canonical == entry.canonical) { + hits.push(entry); + } + } + if hits.is_empty() { + return 0; + } + println!("{label}:"); + for entry in &hits { + let aliases = if entry.aliases.is_empty() { + String::new() + } else { + format!(" (aliases: {})", entry.aliases.join(", ")) + }; + println!(" {}{}", entry.canonical, aliases); + } + hits.len() + }; + let mut total = 0usize; + let mut sources = 0usize; + for prover in [ + ProverKind::Agda, + ProverKind::Coq, + ProverKind::Lean, + ProverKind::Idris2, + ProverKind::Isabelle, + ProverKind::Metamath, + ProverKind::Mizar, + ProverKind::HOL4, + ProverKind::HOLLight, + ProverKind::Dafny, + ProverKind::Why3, + ProverKind::FStar, + ProverKind::ACL2, + ] { + if let Some(table) = tables.get(&prover) { + let hits = print_hits(table, format_args!("{:?}", prover)); + total += hits; + if hits > 0 { + sources += 1; + } + } + } + if !provers_only { + let dicts = load_cross_prover_dicts(&dir).with_context(|| { + format!("load cross-prover dictionaries from {}", dir.display()) + })?; + for (name, table) in dicts.tables() { + let hits = print_hits(table, format_args!("dictionary {}", name)); + total += hits; + if hits > 0 { + sources += 1; + } + } + } + if total == 0 { + formatter.info(&format!("no entries for semantic class '{}'", class))?; + } else { + formatter.info(&format!( + "{} entries across {} table(s) for semantic class '{}'", + total, sources, class + ))?; + } + }, + + CorpusOp::Near { query, index, top } => { + let path = match index { + Some(p) => p, + None => { + let basename = std::env::current_dir()? + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "project".to_string()); + default_index(&basename) + }, + }; + let corpus = Corpus::load_json(&path) + .with_context(|| format!("load corpus index {}", path.display()))?; + let q_vec = corpus.embed_query(&query); + let nn = corpus.nearest_neighbours(&q_vec, top); + if nn.is_empty() { + formatter.info(&format!( + "no entries near '{}' (corpus has {} entries)", + query, + corpus.entries.len() + ))?; + return Ok(()); + } + for (idx, sim) in &nn { + let entry = &corpus.entries[*idx]; + println!("{:.3} {} [{:?}]", sim, entry.qualified, entry.kind); + println!(" : {}", entry.statement); + } + }, + + CorpusOp::Identity { + pattern, + index, + skeleton, + cross_only, + } => { + let path = match index { + Some(p) => p, + None => { + let basename = std::env::current_dir()? + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "project".to_string()); + default_index(&basename) + }, + }; + let corpus = Corpus::load_json(&path) + .with_context(|| format!("load corpus index {}", path.display()))?; + let needle = pattern.to_lowercase(); + let everything = needle == "*"; + + // Group by structural key, keeping the key itself for display. + let mut groups: std::collections::BTreeMap< + String, + ( + echidna::corpus::canonical::StructuralKey, + Vec<(String, String)>, + ), + > = std::collections::BTreeMap::new(); + // A key the canonicaliser did not derive from the whole statement + // is a shared prefix, not an identity (docs/CORPUS.adoc). + let mut skipped_incomplete = 0usize; + for entry in &corpus.entries { + let matches = everything + || entry.qualified.to_lowercase().contains(&needle) + || entry.name.to_lowercase().contains(&needle); + if !matches { + continue; + } + let key = echidna::corpus::canonical::structural_key(&entry.statement); + if !key.complete { + skipped_incomplete += 1; + continue; + } + let slot = groups + .entry(key.key.clone()) + .or_insert_with(|| (key, Vec::new())); + slot.1 + .push((corpus.adapter.clone(), entry.qualified.clone())); + } + + if skipped_incomplete > 0 { + formatter.info(&format!( + "{skipped_incomplete} matching declaration(s) skipped: the canonicaliser \ + did not read their statement completely, so they carry no identity" + ))?; + } + if groups.is_empty() { + // When every match was skipped, the name is present but carries + // no identity; saying "no declarations" would mislead. + let what = if skipped_incomplete > 0 { + "no completely read declarations" + } else { + "no declarations" + }; + formatter.info(&format!("{what} matching '{}'", pattern))?; + return Ok(()); + } + + let mut shown = 0usize; + for (key, (structural, members)) in &groups { + let adapters: std::collections::BTreeSet<&str> = + members.iter().map(|(a, _)| a.as_str()).collect(); + if cross_only && adapters.len() < 2 { + continue; + } + shown += 1; + println!( + "{} [{}] {} declaration(s) across {} adapter(s)", + &key[..16.min(key.len())], + structural.tier.as_str(), + members.len(), + adapters.len() + ); + println!(" type: {}", structural.shape); + if skeleton { + println!(" skeleton: {}", structural.skeleton); + } + for (adapter, qualified) in members { + println!(" {} {}", adapter, qualified); + } + } + formatter.info(&format!( + "{} key(s) over {} declaration(s) in {}", + shown, + groups.values().map(|(_, m)| m.len()).sum::(), + path.display() + ))?; + }, + + CorpusOp::ExportOctads { index, out } => { + let corpus = Corpus::load_json(&index) + .with_context(|| format!("load corpus index {}", index.display()))?; + let timestamp = chrono::Utc::now().to_rfc3339(); + corpus.save_octads_jsonl(&out, ×tamp)?; + formatter.info(&format!( + "exported {} octad(s) → {}", + corpus.entries.len(), + out.display() + ))?; + }, + + CorpusOp::ImportOctads { octads, out } => { + let corpus = Corpus::load_octads_jsonl(&octads) + .with_context(|| format!("load octads {}", octads.display()))?; + corpus.save_json(&out)?; + formatter.info(&format!( + "imported {} octad(s) ({} module(s)) → {}", + corpus.entries.len(), + corpus.modules.len(), + out.display() + ))?; + }, + + CorpusOp::Stats { index } => { + let path = match index { + Some(p) => p, + None => { + let basename = std::env::current_dir()? + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "project".to_string()); + default_index(&basename) + }, + }; + let corpus = Corpus::load_json(&path) + .with_context(|| format!("load corpus index {}", path.display()))?; + let stats = corpus.stats(); + println!( + "modules: {}\nentries: {}\n functions: {}\n data: {}\n records: {}\n postulates: {}\nwith hazards: {}\nwith deps: {}", + stats.modules, + stats.entries, + stats.functions, + stats.data, + stats.records, + stats.postulates, + stats.with_hazards, + stats.with_deps + ); + }, + } + + Ok(()) +} diff --git a/src/rust/cli/design.rs b/src/rust/cli/design.rs new file mode 100644 index 00000000..d14ca761 --- /dev/null +++ b/src/rust/cli/design.rs @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: design. Moved verbatim from main.rs (hotspot split). + +use super::*; +use crate::output::OutputFormatter; +use anyhow::Result; + +/// Design-search command — simulated annealing over candidate +/// axiomatisations, single-chain or parallel swarm. The first +/// concrete problem is `brouwer-leq`, targeting echo-types' Phase 1.3 +/// `_≤_` redesign. +pub(crate) async fn design_command(op: DesignOp, formatter: &OutputFormatter) -> Result<()> { + use echidna::agent::swarm::{run_swarm, SwarmConfig}; + use echidna::learning::design_search::{anneal, brouwer, AnnealConfig, DesignProblem}; + use std::sync::Arc; + + match op { + DesignOp::Search { + problem, + iterations, + restarts, + temp, + cooling, + seed, + top, + } => { + let cfg = AnnealConfig { + max_iterations: iterations, + initial_temp: temp, + cooling, + seed, + restarts, + }; + + match problem.as_str() { + "brouwer-leq" => { + let p = brouwer::BrouwerLeqProblem::default(); + let result = anneal(&p, &cfg); + formatter.info(&format!( + "annealer: {} steps, {} accepted ({:.1}%); best energy {:?}", + result.steps, + result.accepted, + 100.0 * result.accepted as f64 / result.steps.max(1) as f64, + result.best_energy + ))?; + println!( + "\nbest design (energy = {:?}):\n {}\n", + result.best_energy, + p.describe(&result.best) + ); + println!("top {} distinct candidates:", top.min(result.topk.len())); + for (i, (state, e)) in result.topk.iter().take(top).enumerate() { + println!(" {:2}. energy {:?} {}", i + 1, e, p.describe(state)); + } + println!( + "\nlegend: energy = [mono-blockers, K-hazards, ctor-count, style-pref]" + ); + println!( + " style-pref: 0 = recursive, 1 = data (recursive preferred when tied)" + ); + }, + "buchholz-rank" => { + use echidna::learning::buchholz_rank::{ + render_rank_mono_skeleton, BuchholzRankProblem, + }; + let p = BuchholzRankProblem::with_baseline_corpus(); + let result = anneal(&p, &cfg); + formatter.info(&format!( + "annealer: {} steps, {} accepted ({:.1}%); best energy {:?}", + result.steps, + result.accepted, + 100.0 * result.accepted as f64 / result.steps.max(1) as f64, + result.best_energy + ))?; + println!( + "\nbest rank shape (energy = {:?}):\n{}\n", + result.best_energy, + p.describe(&result.best) + ); + println!("top {} distinct candidates:", top.min(result.topk.len())); + for (i, (state, e)) in result.topk.iter().take(top).enumerate() { + println!(" {:2}. energy {:?}", i + 1, e); + for line in p.describe(state).lines() { + println!(" {}", line); + } + } + println!( + "\nlegend: energy = [downstream-blockers, capability-gaps, structural-cost]" + ); + println!( + " downstream-blockers: # of new lemmas to prove (Phase-1.3 / 2.2)" + ); + println!( + " capability-gaps: # of primitives missing from current corpus" + ); + println!("\n--- Agda skeleton for the recommended shape ---"); + println!("{}", render_rank_mono_skeleton(&result.best)); + }, + other => { + return Err(anyhow::anyhow!( + "Unknown design problem '{}' (available: brouwer-leq, buchholz-rank)", + other + )) + }, + } + }, + + DesignOp::Swarm { + problem, + agents, + iterations, + temp, + cooling, + seed, + broadcast_every, + adopt_threshold, + } => { + let anneal_cfg = AnnealConfig { + max_iterations: iterations, + initial_temp: temp, + cooling, + seed, + restarts: 1, // each swarm agent is its own "restart" + }; + let swarm_cfg = SwarmConfig { + agents, + anneal: anneal_cfg, + broadcast_every, + adopt_threshold, + }; + + match problem.as_str() { + "brouwer-leq" => { + let p = Arc::new(brouwer::BrouwerLeqProblem::default()); + let p_describe = brouwer::BrouwerLeqProblem::default(); + let result = run_swarm(p, swarm_cfg).await; + formatter.info(&format!( + "swarm: {} agents, {} adoptions; best energy {:?}", + result.per_agent.len(), + result.adoptions, + result.best_energy + ))?; + println!( + "\nbest design (energy = {:?}):\n {}\n", + result.best_energy, + p_describe.describe(&result.best) + ); + println!("per-agent reports:"); + for r in &result.per_agent { + println!( + " agent {:2} (seed {:#x}): {} steps, {} accepted, {} adoptions; best {:?}", + r.agent_id, + r.seed, + r.steps, + r.accepted, + r.adopted_count, + r.local_best_energy, + ); + } + }, + "buchholz-rank" => { + use echidna::learning::buchholz_rank::{ + render_rank_mono_skeleton, BuchholzRankProblem, + }; + let p = Arc::new(BuchholzRankProblem::with_baseline_corpus()); + let p_describe = BuchholzRankProblem::with_baseline_corpus(); + let result = run_swarm(p, swarm_cfg).await; + formatter.info(&format!( + "swarm: {} agents, {} adoptions; best energy {:?}", + result.per_agent.len(), + result.adoptions, + result.best_energy + ))?; + println!( + "\nbest rank shape (energy = {:?}):\n{}\n", + result.best_energy, + p_describe.describe(&result.best) + ); + println!("per-agent reports:"); + for r in &result.per_agent { + println!( + " agent {:2} (seed {:#x}): {} steps, {} accepted, {} adoptions; best {:?}", + r.agent_id, + r.seed, + r.steps, + r.accepted, + r.adopted_count, + r.local_best_energy, + ); + } + println!("\n--- Agda skeleton for the recommended shape ---"); + println!("{}", render_rank_mono_skeleton(&result.best)); + }, + other => { + return Err(anyhow::anyhow!( + "Unknown design problem '{}' (available: brouwer-leq, buchholz-rank)", + other + )) + }, + } + }, + } + Ok(()) +} diff --git a/src/rust/cli/lexicon.rs b/src/rust/cli/lexicon.rs new file mode 100644 index 00000000..bd348b7d --- /dev/null +++ b/src/rust/cli/lexicon.rs @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: lexicon. Moved verbatim from main.rs (hotspot split). + +use super::*; +use crate::output::OutputFormatter; +use anyhow::{Context, Result}; + +/// Lexicon command - mine cross-corpus candidates for curation. +/// +/// The scan runs on a thread with a large stack: the canonicaliser's parser +/// recurses on nesting depth, and some corpus statements (ACL2 books) nest +/// deeply. Timings go to stderr; the JCS files under `--out` carry none, so +/// two runs over the same data write the same bytes. +/// +/// # Errors +/// +/// An unknown preset, a malformed `--pair`, no pairs at all, a corpus that +/// fails to load, an output directory that cannot be created or read, an +/// old output file that cannot be removed, or a serialisation or output +/// write failure. Also returns an error if the scan thread cannot be +/// spawned or panics. Failed scans may leave partial output. +pub(crate) fn lexicon_command(op: LexiconOp, formatter: &OutputFormatter) -> Result<()> { + use echidna::lexicon::scan::{self, RunOptions, ScanOptions}; + + let LexiconOp::Scan { + training_dir, + pairs, + preset, + out, + keep_generic, + max_pairs_per_name, + max_shape_group, + collision_samples, + } = op; + + let mut all_pairs = Vec::new(); + if let Some(name) = preset.as_deref() { + all_pairs.extend(scan::preset_pairs(name).with_context(|| { + format!("unknown preset `{name}` (learning, spanning, in-family, full)") + })?); + } + for p in &pairs { + let pair = scan::parse_pair(p)?; + // A pair is the same pair in either direction; the first spelling + // requested is the one scanned. + let seen = all_pairs + .iter() + .any(|(a, b)| (a, b) == (&pair.0, &pair.1) || (a, b) == (&pair.1, &pair.0)); + if !seen { + all_pairs.push(pair); + } + } + let opts = RunOptions { + training_dir, + pairs: all_pairs, + out, + scan: ScanOptions { + keep_generic, + max_pairs_per_name, + max_shape_group, + }, + collision_samples, + }; + + let report = std::thread::Builder::new() + .name("lexicon-scan".into()) + .stack_size(512 * 1024 * 1024) + .spawn(move || scan::run(&opts).map(|r| (r, opts.out))) + .context("spawn the scan thread")? + .join() + .map_err(|_| anyhow::anyhow!("the scan thread panicked"))??; + let (report, out) = report; + + if formatter.is_json() { + // One JCS object on stdout; the timings stay on stderr. + let summary = serde_json::json!({ + "coverage": report.coverage, + "pairs": report.pairs, + "out": out.display().to_string(), + "total_ms": u64::try_from(report.total.as_millis()).unwrap_or(u64::MAX), + }); + std::io::Write::write_all(&mut std::io::stdout(), &scan::jcs_line(&summary)?)?; + eprintln!("total: {:.2?}; output in {}", report.total, out.display()); + return Ok(()); + } + + println!( + "{:<14} {:>8} {:>8} {:>6} {:>7} {:>6} {:>6} {:>6} {:>8}", + "corpus", "rows", "names", "hom%", "struct%", "skel%", "text%", "cmpl%", "collide" + ); + for c in &report.coverage { + let pct = |n: usize| 100.0 * n as f64 / c.rows.max(1) as f64; + println!( + "{:<14} {:>8} {:>8} {:>6.1} {:>7.1} {:>6.1} {:>6.1} {:>6.1} {:>8}", + c.corpus, + c.rows, + c.names, + 100.0 * c.homonym_names as f64 / c.names.max(1) as f64, + pct(c.tiers.structural), + pct(c.tiers.skeleton), + pct(c.tiers.text), + pct(c.tiers.complete), + c.collision_keys + ); + } + println!(); + println!( + "{:<26} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>8} {:>7} {:>7} {:>7}", + "pair", + "shared", + "generic", + "capped", + "exact", + "shape", + "skel", + "differs", + "unparsed", + "syn-tx", + "syn-sh", + "syn-sk" + ); + for s in &report.pairs { + let names = [&s.candidates.name_exact, &s.candidates.name_folded]; + let sum = + |f: fn(&scan::CandidateCounts) -> usize| names.iter().map(|c| f(c)).sum::(); + println!( + "{:<26} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>8} {:>7} {:>7} {:>7}", + format!("{}:{}", s.pair[0], s.pair[1]), + s.folded_names_shared, + s.generic_names_skipped, + s.name_pairs_capped, + sum(|c| c.cognate_exact), + sum(|c| c.cognate_shape), + sum(|c| c.cognate_skeleton), + sum(|c| c.differs), + sum(|c| c.unparsed), + s.candidates.text.total(), + s.candidates.shape.total(), + s.candidates.skeleton.total() + ); + } + for (corpus, t) in &report.load_times { + eprintln!("load {corpus}: {:.2?}", t); + } + for (pair, t) in &report.pair_times { + eprintln!("scan {pair}: {:.2?}", t); + } + eprintln!("total: {:.2?}; output in {}", report.total, out.display()); + Ok(()) +} diff --git a/src/rust/cli/mod.rs b/src/rust/cli/mod.rs new file mode 100644 index 00000000..ecb5cc5e --- /dev/null +++ b/src/rust/cli/mod.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! The `echidna` command line: argument definitions and one module per +//! command group. `main.rs` keeps only `main()`, which parses `Cli` and +//! dispatches to these. + +mod args; +mod corpus; +mod design; +mod lexicon; +mod prove; +mod provers; +mod search; +mod session; +mod util; +mod verify; + +pub(crate) use args::*; +pub(crate) use corpus::*; +pub(crate) use design::*; +pub(crate) use lexicon::*; +pub(crate) use prove::*; +pub(crate) use provers::*; +pub(crate) use search::*; +pub(crate) use session::*; +pub(crate) use util::*; +pub(crate) use verify::*; diff --git a/src/rust/cli/prove.rs b/src/rust/cli/prove.rs new file mode 100644 index 00000000..2c609f1e --- /dev/null +++ b/src/rust/cli/prove.rs @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: prove. Moved verbatim from main.rs (hotspot split). + +use super::*; +use crate::output::OutputFormatter; +use anyhow::{Context, Result}; +use echidna::{ + diagnostics::proof_failure::diagnose_from_outcome, provers::ProverBackend, ProverKind, +}; +use std::path::PathBuf; +use tracing::info; + +/// Prove command implementation +#[allow(clippy::too_many_arguments)] +pub(crate) async fn prove_command( + file: PathBuf, + prover_kind: Option, + timeout: u64, + neural: bool, + executable: Option, + library: Vec, + diagnose: bool, + project_root: Option, + sandbox: String, + formatter: &OutputFormatter, +) -> Result<()> { + info!("Starting proof for: {}", file.display()); + + let kind = detect_prover(prover_kind, &file)?; + let config = create_config( + kind, + timeout, + neural, + executable, + library, + project_root, + &sandbox, + )?; + let prover = echidna::provers::ProverFactory::create(kind, config) + .context("Failed to create prover backend")?; + + let pb = create_progress_bar("Parsing proof file..."); + let state = prover + .parse_file(file.clone()) + .await + .context("Failed to parse proof file")?; + + pb.set_message("Verifying proof..."); + let result = prover + .verify_proof(&state) + .await + .context("Failed to verify proof")?; + pb.finish_and_clear(); + + if result { + formatter.success(&format!( + "✓ Proof verified successfully for {}", + file.display() + ))?; + formatter.output_proof_state(&state)?; + } else { + formatter.error(&format!( + "✗ Proof verification failed for {}", + file.display() + ))?; + formatter.output_proof_state(&state)?; + if diagnose { + emit_diagnostic(kind, &*prover, &state, formatter).await; + } + std::process::exit(1); + } + + Ok(()) +} + +/// `echidna prove --output json`: print one `echidna.prove.result/1` object. +/// +/// Every failure, including a missing backend or unreadable file, becomes an +/// `error` result rather than a non-JSON message. Exits 0 when the status is +/// `verified` and 1 otherwise. With `--diagnose`, the diagnostic report for a +/// non-verified run goes to stderr. +#[allow(clippy::too_many_arguments)] +pub(crate) async fn prove_json_command( + file: PathBuf, + prover_kind: Option, + timeout: u64, + neural: bool, + executable: Option, + library: Vec, + diagnose: bool, + project_root: Option, + sandbox: String, +) { + use echidna::prove_contract::{self, ProveStatus}; + + let goal = file.display().to_string(); + let start = std::time::Instant::now(); + let since = + |start: std::time::Instant| u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); + + let kind = match detect_prover(prover_kind, &file) { + Ok(k) => k, + Err(e) => { + let r = prove_contract::result_from_error(prover_kind, &goal, &e, since(start)); + emit_prove_result(&r); + }, + }; + let backend = match create_config( + kind, + timeout, + neural, + executable, + library, + project_root, + &sandbox, + ) + .and_then(|config| { + echidna::provers::ProverFactory::create(kind, config) + .context("Failed to create prover backend") + }) { + Ok(b) => b, + Err(e) => { + let r = prove_contract::result_from_error(Some(kind), &goal, &e, since(start)); + emit_prove_result(&r); + }, + }; + + let (result, outcome) = prove_contract::run_detailed(kind, &*backend, &file).await; + if let (true, Some(outcome)) = (diagnose && result.status != ProveStatus::Verified, outcome) { + eprintln!("--- Proof Failure Diagnostic ---"); + eprintln!("{}", diagnose_from_outcome(kind, &outcome).display()); + } + emit_prove_result(&result); +} + +/// Print `result` as its one JCS line on stdout and exit (0 iff verified). +/// +/// If the value cannot be serialised (it never should), a minimal `error` +/// object is printed instead, so stdout always carries one contract object. +pub(crate) fn emit_prove_result(result: &echidna::prove_contract::ProveResult) -> ! { + use echidna::prove_contract::{self, ProveStatus}; + let line = result.to_jcs().unwrap_or_else(|e| { + let e = anyhow::anyhow!("could not serialise prove result: {e}"); + prove_contract::result_from_error(None, &result.goal, &e, 0) + .to_jcs() + .expect("an error result with no numbers out of range serialises") + }); + println!("{line}"); + std::process::exit(if result.status == ProveStatus::Verified { + 0 + } else { + 1 + }); +} + +/// Run `check()` on the prover and display a structured diagnostic report. +/// +/// Called after a failed `verify_proof()` when `--diagnose` is active. +/// Uses the richer `ProverOutcome` returned by `check()` to produce an +/// actionable report via `diagnose_from_outcome`. +pub(crate) async fn emit_diagnostic( + kind: ProverKind, + prover: &dyn ProverBackend, + state: &echidna::core::ProofState, + formatter: &OutputFormatter, +) { + let outcome = match prover.check(state).await { + Ok(o) => o, + Err(e) => { + let _ = formatter.error(&format!("[diagnose] could not run check(): {}", e)); + return; + }, + }; + let report = diagnose_from_outcome(kind, &outcome); + let _ = formatter.info("\n--- Proof Failure Diagnostic ---"); + let _ = formatter.info(&report.display()); +} diff --git a/src/rust/cli/provers.rs b/src/rust/cli/provers.rs new file mode 100644 index 00000000..c0ee937b --- /dev/null +++ b/src/rust/cli/provers.rs @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: provers. Moved verbatim from main.rs (hotspot split). + +use crate::output::OutputFormatter; +use anyhow::Result; +use echidna::{ProverConfig, ProverKind}; + +/// List provers command +pub(crate) fn list_provers_command(detailed: bool, formatter: &OutputFormatter) -> Result<()> { + let provers = ProverKind::all(); + + formatter.header("Available Provers")?; + formatter.info(&format!("Total: {} provers\n", provers.len()))?; + + for prover in provers { + let tier = prover.tier(); + let complexity = prover.complexity(); + let time = prover.implementation_time(); + + if detailed { + formatter.section(&format!("{:?}", prover))?; + formatter.info(&format!(" Tier: {}", tier))?; + formatter.info(&format!(" Complexity: {}/5", complexity))?; + formatter.info(&format!(" Implementation time: {} weeks", time))?; + formatter.info("")?; + } else { + formatter.result(&format!( + "{:12} - Tier {} | Complexity {}/5 | {} weeks", + format!("{:?}", prover), + tier, + complexity, + time + ))?; + } + } + + if !detailed { + formatter.info("\nUse --detailed flag for more information")?; + } + + Ok(()) +} + +/// Info command - show detailed information about a prover +pub(crate) fn info_command(prover: ProverKind, formatter: &OutputFormatter) -> Result<()> { + formatter.header(&format!("{:?} Prover Information", prover))?; + formatter.info("")?; + + formatter.section("Classification")?; + formatter.info(&format!(" Tier: {}", prover.tier()))?; + formatter.info(&format!(" Complexity: {}/5", prover.complexity()))?; + formatter.info(&format!( + " Implementation time: {} weeks", + prover.implementation_time() + ))?; + formatter.info("")?; + + formatter.section("Description")?; + let description = match prover { + ProverKind::Agda => { + "Dependently typed functional programming language and proof assistant.\n \ + Tier 1 prover. Strong type system with universe polymorphism." + }, + ProverKind::Coq => { + "Interactive theorem prover using the Calculus of Inductive Constructions.\n \ + Widely used in formal verification and mathematics. Powerful tactic language." + }, + ProverKind::Lean => { + "Modern theorem prover and programming language with dependent types.\n \ + Strong community, extensive mathlib. Excellent for formalized mathematics." + }, + ProverKind::Isabelle => { + "Generic proof assistant supporting multiple logics (mainly HOL).\n \ + Powerful automation through Sledgehammer. Large Archive of Formal Proofs." + }, + ProverKind::Z3 => { + "High-performance SMT (Satisfiability Modulo Theories) solver.\n \ + Excellent for automated reasoning in program verification." + }, + ProverKind::CVC5 => { + "Automatic SMT solver for first-order logic with theories.\n \ + Strong support for quantifiers, datatypes, and strings." + }, + ProverKind::Metamath => { + "Ultra-minimal proof verification system with plain-text proofs.\n \ + EASIEST to integrate (2/5 complexity). Large database of formalized mathematics." + }, + ProverKind::HOLLight => { + "Simple, elegant HOL (Higher-Order Logic) proof assistant in OCaml.\n \ + Part of the 'Big Six' theorem provers. Strong mathematical foundations." + }, + ProverKind::Mizar => { + "Proof assistant with natural-language-like proof style.\n \ + Large Mathematical Library. Readable proof documents." + }, + ProverKind::PVS => { + "Specification and verification system for safety-critical systems.\n \ + Strong support for dependent types and predicate subtyping." + }, + ProverKind::ACL2 => { + "Computational Logic for Applicative Common Lisp.\n \ + Industrial-strength theorem prover for software/hardware verification." + }, + ProverKind::HOL4 => { + "Interactive theorem prover in the HOL family.\n \ + Used extensively in hardware verification. ML-based tactic language." + }, + ProverKind::Idris2 => { + "Dependently typed functional language with quantitative types.\n \ + First-class type-level computation, elaborator reflection, linear types." + }, + ProverKind::Vampire => { + "First-order automated theorem prover. Multiple CASC winner.\n \ + Superposition calculus with excellent performance on CASC benchmarks." + }, + ProverKind::EProver => { + "Highly optimized first-order theorem prover for clausal logic.\n \ + CASC winner. Auto mode with sophisticated strategy selection." + }, + ProverKind::SPASS => { + "First-order theorem prover with sorted logic support.\n \ + DFG format input. Superposition calculus with sort handling." + }, + ProverKind::AltErgo => { + "SMT solver with polymorphic first-order logic.\n \ + Designed for program verification (Why3, Frama-C integration)." + }, + ProverKind::FStar => { + "F* dependent types with effects. Project Everest/HACL* verified crypto." + }, + ProverKind::Dafny => "Auto-active verifier. Pre/postconditions verified via Boogie and Z3.", + ProverKind::Why3 => { + "Multi-prover orchestration. Dispatches to Z3, CVC5, Alt-Ergo in parallel." + }, + ProverKind::TLAPS => "TLA+ Proof System. Verifies distributed system properties.", + ProverKind::Twelf => "Logical framework (LF). Metatheory verification for type systems.", + ProverKind::Nuprl => "Constructive type theory. Large library of formalized mathematics.", + ProverKind::Minlog => "Minimal logic with certified program extraction from proofs.", + ProverKind::Imandra => "ML-based reasoning for industrial verification.", + ProverKind::GLPK => "GNU Linear Programming Kit. LP/MIP constraint solving.", + ProverKind::SCIP => "Mixed-integer nonlinear programming solver.", + ProverKind::MiniZinc => "Constraint modelling language with multiple backend solvers.", + ProverKind::Chuffed => "Lazy clause generation CP solver with SAT-style learning.", + ProverKind::ORTools => "Google OR-Tools. CP-SAT, routing, linear/integer programming.", + ProverKind::TypedWasm => { + "TypedWasm oracle. 10-level type safety validation for .twasm programs." + }, + ProverKind::SPIN => "SPIN model checker for Promela concurrent system specifications.", + ProverKind::CBMC => "C Bounded Model Checker. Verifies C programs via bounded unwinding.", + ProverKind::CaDiCaL => { + "State-of-the-art CDCL SAT solver. DIMACS CNF input.\n \ + Multiple SAT Competition winner. Default SAT backend for ECHIDNA." + }, + ProverKind::Kissat => { + "Fast, highly-optimised CDCL SAT solver. DIMACS CNF input.\n \ + SAT Competition winner. Designed for raw solving speed." + }, + ProverKind::MiniSat => { + "Classic DPLL/CDCL SAT solver. DIMACS CNF input. Reference implementation." + }, + ProverKind::NuSMV => { + "NuSMV/nuXmv symbolic model checker for CTL/LTL verification of finite state systems." + }, + ProverKind::TLC => { + "TLC model checker for exhaustive state exploration of TLA+ specifications." + }, + ProverKind::Alloy => "Alloy relational model finder using SAT-based bounded analysis.", + ProverKind::Prism => "PRISM probabilistic model checker for DTMCs, CTMCs, MDPs, and PTAs.", + ProverKind::UPPAAL => { + "UPPAAL timed automata model checker for real-time system verification." + }, + ProverKind::FramaC => "Frama-C WP deductive verifier for ACSL-annotated C programs.", + ProverKind::SeaHorn => "SeaHorn verification framework for LLVM-based safety checking.", + ProverKind::Viper => "Viper permission-based program verifier with separation logic.", + ProverKind::Tamarin => { + "Tamarin security protocol prover using multiset rewriting in the Dolev-Yao model." + }, + ProverKind::ProVerif => { + "ProVerif automatic cryptographic protocol verifier in the Dolev-Yao model." + }, + ProverKind::KeY => { + "KeY deductive Java verifier using JavaDL (dynamic logic) with JML contracts." + }, + ProverKind::DReal => { + "dReal delta-complete SMT solver for nonlinear real arithmetic over the reals." + }, + ProverKind::ABC => { + "ABC sequential logic synthesis and verification system for hardware circuits." + }, + // HP ecosystem type-disciplines (TypeDiscipline transition, 2026-04-17). + // A single-line fallback for the forty discipline views; per-discipline + // descriptions are a phase-2 followup tracked in AI-WORK-todo.md. + k if k.is_hp_ecosystem() => { + "HP ecosystem type-discipline view. Dispatches through typell / \n \ + katagoria / tropical-resource-typing. See disciplines/ for the \n \ + 40-variant taxonomy (linear, affine, phantom, modal, etc.)." + }, + _ => "No description available for this prover.", + }; + formatter.info(&format!(" {}", description))?; + formatter.info("")?; + + formatter.section("File Extensions")?; + let extension = match prover { + ProverKind::Agda => ".agda", + ProverKind::Coq => ".v", + ProverKind::Lean => ".lean", + ProverKind::Isabelle => ".thy", + ProverKind::Z3 | ProverKind::CVC5 => ".smt2", + ProverKind::Metamath => ".mm", + ProverKind::HOLLight => ".ml", + ProverKind::Mizar => ".miz", + ProverKind::PVS => ".pvs", + ProverKind::ACL2 => ".lisp", + ProverKind::HOL4 => ".sml", + ProverKind::Idris2 => ".idr", + ProverKind::Vampire => ".p / .tptp", + ProverKind::EProver => ".p / .tptp", + ProverKind::SPASS => ".dfg", + ProverKind::AltErgo => ".ae / .why", + ProverKind::FStar => ".fst / .fsti", + ProverKind::Dafny => ".dfy", + ProverKind::Why3 => ".why / .mlw", + ProverKind::TLAPS => ".tla", + ProverKind::Twelf => ".elf", + ProverKind::Nuprl => ".nuprl", + ProverKind::Minlog => ".minlog", + ProverKind::Imandra => ".iml", + ProverKind::GLPK => ".lp / .mps", + ProverKind::SCIP => ".pip / .zpl", + ProverKind::MiniZinc => ".mzn / .dzn", + ProverKind::Chuffed => ".fzn", + ProverKind::ORTools => ".or / .proto", + ProverKind::TypedWasm => ".twasm", + ProverKind::SPIN => ".pml", + ProverKind::CBMC => ".c", + ProverKind::CaDiCaL => ".cnf", + ProverKind::Kissat => ".cnf", + ProverKind::MiniSat => ".cnf", + ProverKind::NuSMV => ".smv", + ProverKind::TLC => ".tla", + ProverKind::Alloy => ".als", + ProverKind::Prism => ".pm / .prism", + ProverKind::UPPAAL => ".xml", + ProverKind::FramaC => ".c (ACSL-annotated)", + ProverKind::SeaHorn => ".bc / .ll (LLVM IR)", + ProverKind::Viper => ".vpr", + ProverKind::Tamarin => ".spthy", + ProverKind::ProVerif => ".pv", + ProverKind::KeY => ".java (JML-annotated)", + ProverKind::DReal => ".smt2 (QF_NRA/NRA)", + ProverKind::ABC => ".blif / .aig", + // HP ecosystem: discipline-tagged source lives in typell; no fixed + // extension. See HPEcosystemBackend for the actual dispatch. + k if k.is_hp_ecosystem() => "typell discipline source (.tll / #discipline header)", + _ => "unknown", + }; + formatter.info(&format!(" {}", extension))?; + formatter.info("")?; + + formatter.section("Features")?; + formatter.info(" ✓ Neural premise selection")?; + formatter.info(" ✓ Aspect tagging")?; + formatter.info(" ✓ OpenCyc integration")?; + formatter.info(" ✓ Tactic suggestion")?; + formatter.info("")?; + + // Try to get version if prover is installed + let config = ProverConfig::default(); + if let Ok(prover_backend) = echidna::provers::ProverFactory::create(prover, config) { + formatter.section("Installation")?; + + // Spawn async runtime to check version + let version_check = tokio::task::spawn(async move { prover_backend.version().await }); + + if let Ok(Ok(version)) = futures::executor::block_on(version_check) { + formatter.success(&format!(" ✓ Installed: {}", version))?; + } else { + formatter.warning(" ✗ Not detected (may not be installed or not in PATH)")?; + } + } + + Ok(()) +} diff --git a/src/rust/cli/search.rs b/src/rust/cli/search.rs new file mode 100644 index 00000000..1e614d8b --- /dev/null +++ b/src/rust/cli/search.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: search. Moved verbatim from main.rs (hotspot split). + +use crate::output::OutputFormatter; +use anyhow::Result; +use echidna::{ProverConfig, ProverKind}; +use tracing::{info, warn}; + +/// Search command implementation +pub(crate) async fn search_command( + pattern: String, + prover_kind: Option, + limit: usize, + formatter: &OutputFormatter, +) -> Result<()> { + info!("Searching for: {}", pattern); + + let provers: Vec = if let Some(kind) = prover_kind { + vec![kind] + } else { + // Search all provers + vec![ + ProverKind::Agda, + ProverKind::Coq, + ProverKind::Lean, + ProverKind::Isabelle, + ProverKind::Z3, + ProverKind::CVC5, + ProverKind::Metamath, + ProverKind::HOLLight, + ProverKind::Mizar, + ProverKind::PVS, + ProverKind::ACL2, + ProverKind::HOL4, + ProverKind::Idris2, + ] + }; + + formatter.info(&format!( + "Searching {} prover(s) for pattern: {}", + provers.len(), + pattern + ))?; + + let mut total_results = 0; + + for kind in provers { + let config = ProverConfig::default(); + + if let Ok(prover) = echidna::provers::ProverFactory::create(kind, config) { + formatter.section(&format!("\n{:?} Results:", kind))?; + + match prover.search_theorems(&pattern).await { + Ok(results) => { + total_results += results.len(); + + for (i, result) in results.iter().take(limit).enumerate() { + formatter.result(&format!(" {}. {}", i + 1, result))?; + } + + if results.len() > limit { + formatter + .info(&format!(" ... and {} more results", results.len() - limit))?; + } + }, + Err(e) => { + warn!("Failed to search {}: {}", kind, e); + }, + } + } + } + + // After per-backend search, add a cross-prover layer: query VeriSimDB + // for matches across every prover that has ever recorded an attempt. + // This compensates for backends without a native search command (the + // 70+ that legitimately return Vec::new() because their underlying + // prover doesn't ship a `Search`-equivalent). No-op without the + // `verisim` feature; logs and continues on Verisim outage. + let verisim_url = + std::env::var("VERISIM_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); + match echidna::vcl_ut::cross_prover_search_names(&verisim_url, &pattern, limit).await { + Ok(cross) if !cross.is_empty() => { + formatter.section("\nCross-prover (VeriSimDB) Results:")?; + total_results += cross.len(); + for (i, result) in cross.iter().take(limit).enumerate() { + formatter.result(&format!(" {}. {}", i + 1, result))?; + } + if cross.len() > limit { + formatter.info(&format!(" ... and {} more results", cross.len() - limit))?; + } + }, + Ok(_) => {}, + Err(e) => { + warn!("Cross-prover search failed: {}", e); + }, + } + + formatter.info(&format!("\nTotal results found: {}", total_results))?; + + Ok(()) +} diff --git a/src/rust/cli/session.rs b/src/rust/cli/session.rs new file mode 100644 index 00000000..9a655abf --- /dev/null +++ b/src/rust/cli/session.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: session. Moved verbatim from main.rs (hotspot split). + +use crate::repl::DiagnosticsREPL; +use crate::{repl, server}; +use anyhow::Result; +use echidna::dispatch::ProverDispatcher; +use echidna::ProverKind; +use std::path::PathBuf; +use std::sync::Arc; +use tracing::info; + +/// Interactive REPL command +pub(crate) async fn interactive_command( + file: Option, + prover_kind: Option, + neural: bool, +) -> Result<()> { + info!("Starting interactive REPL mode"); + + repl::start_repl(file, prover_kind, neural).await +} + +/// HTTP Server command +pub(crate) async fn server_command(port: u16, host: String, cors: bool) -> Result<()> { + info!("Starting HTTP server on {}:{}", host, port); + + server::start_server(port, host, cors).await +} + +/// Diagnostics command - interactive health monitoring REPL +pub(crate) async fn diagnostics_command() -> Result<()> { + info!("Starting diagnostics REPL for health monitoring"); + let dispatcher = Arc::new(ProverDispatcher::new()); + let health = dispatcher.health_status()?; + let mut repl = DiagnosticsREPL::new(); + repl.run(&health).await; + Ok(()) +} diff --git a/src/rust/cli/util.rs b/src/rust/cli/util.rs new file mode 100644 index 00000000..8a4a9954 --- /dev/null +++ b/src/rust/cli/util.rs @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: util. Moved verbatim from main.rs (hotspot split). + +use anyhow::Result; +use echidna::{ProverConfig, ProverKind}; +use indicatif::{ProgressBar, ProgressStyle}; +use std::path::PathBuf; + +/// Initialize tracing/logging; logs go to stderr when `to_stderr` is set. +pub(crate) fn init_tracing(verbose: bool, to_stderr: bool) { + use tracing_subscriber::filter::EnvFilter; + use tracing_subscriber::{fmt, prelude::*}; + + let filter = if verbose { + EnvFilter::new("echidna=debug,info") + } else { + EnvFilter::new("echidna=info,warn") + }; + + let registry = tracing_subscriber::registry().with(filter); + if to_stderr { + registry + .with(fmt::layer().with_writer(std::io::stderr)) + .init(); + } else { + registry.with(fmt::layer()).init(); + } +} + +/// Detect prover from file extension or use specified prover +pub(crate) fn detect_prover( + prover_kind: Option, + file: &std::path::Path, +) -> Result { + if let Some(kind) = prover_kind { + return Ok(kind); + } + + echidna::provers::ProverFactory::detect_from_file(file).ok_or_else(|| { + anyhow::anyhow!( + "Could not detect prover from file extension: {}. Please specify with --prover", + file.display() + ) + }) +} + +/// Create prover configuration +pub(crate) fn create_config( + kind: ProverKind, + timeout: u64, + neural: bool, + executable: Option, + library: Vec, + project_root: Option, + sandbox: &str, +) -> Result { + let sandbox_mode: echidna::provers::SandboxMode = + sandbox.parse().map_err(|e: String| anyhow::anyhow!(e))?; + + let config = ProverConfig { + timeout, + neural_enabled: neural, + executable: executable.unwrap_or_else(|| get_default_executable(kind)), + library_paths: if library.is_empty() { + ProverConfig::default().library_paths + } else { + library + }, + project_root, + sandbox: sandbox_mode, + ..ProverConfig::default() + }; + + Ok(config) +} + +/// Get default executable path for a prover +pub(crate) fn get_default_executable(kind: ProverKind) -> PathBuf { + match kind { + ProverKind::Agda => PathBuf::from("agda"), + ProverKind::Coq => PathBuf::from("coqtop"), + ProverKind::Lean => PathBuf::from("lean"), + ProverKind::Isabelle => PathBuf::from("isabelle"), + ProverKind::Z3 => PathBuf::from("z3"), + ProverKind::CVC5 => PathBuf::from("cvc5"), + ProverKind::Metamath => PathBuf::from("metamath"), + ProverKind::HOLLight => PathBuf::from("ocaml"), + ProverKind::Mizar => PathBuf::from("verifier"), + ProverKind::PVS => PathBuf::from("pvs"), + ProverKind::ACL2 => PathBuf::from("acl2"), + ProverKind::HOL4 => PathBuf::from("hol"), + ProverKind::Idris2 => PathBuf::from("idris2"), + ProverKind::Vampire => PathBuf::from("vampire"), + ProverKind::EProver => PathBuf::from("eprover"), + ProverKind::SPASS => PathBuf::from("SPASS"), + ProverKind::AltErgo => PathBuf::from("alt-ergo"), + ProverKind::FStar => PathBuf::from("fstar.exe"), + ProverKind::Dafny => PathBuf::from("dafny"), + ProverKind::Why3 => PathBuf::from("why3"), + ProverKind::TLAPS => PathBuf::from("tlapm"), + ProverKind::Twelf => PathBuf::from("twelf-server"), + ProverKind::Nuprl => PathBuf::from("nuprl"), + ProverKind::Minlog => PathBuf::from("minlog"), + ProverKind::Imandra => PathBuf::from("imandra"), + ProverKind::GLPK => PathBuf::from("glpsol"), + ProverKind::SCIP => PathBuf::from("scip"), + ProverKind::MiniZinc => PathBuf::from("minizinc"), + ProverKind::Chuffed => PathBuf::from("fzn-chuffed"), + ProverKind::ORTools => PathBuf::from("ortools_solve"), + ProverKind::TypedWasm => PathBuf::from("idris2"), + ProverKind::SPIN => PathBuf::from("spin"), + ProverKind::CBMC => PathBuf::from("cbmc"), + ProverKind::CaDiCaL => PathBuf::from("cadical"), + ProverKind::Kissat => PathBuf::from("kissat"), + ProverKind::MiniSat => PathBuf::from("minisat"), + ProverKind::NuSMV => PathBuf::from("nuXmv"), + ProverKind::TLC => PathBuf::from("tlc2.TLC"), + ProverKind::Alloy => PathBuf::from("alloy"), + ProverKind::Prism => PathBuf::from("prism"), + ProverKind::UPPAAL => PathBuf::from("verifyta"), + ProverKind::FramaC => PathBuf::from("frama-c"), + ProverKind::SeaHorn => PathBuf::from("seahorn"), + ProverKind::Viper => PathBuf::from("viper"), + ProverKind::Tamarin => PathBuf::from("tamarin-prover"), + ProverKind::ProVerif => PathBuf::from("proverif"), + ProverKind::KeY => PathBuf::from("key"), + ProverKind::DReal => PathBuf::from("dreal"), + ProverKind::ABC => PathBuf::from("abc"), + // HP ecosystem: route through the TypeLL kernel CLI, with two named + // exceptions (katagoria, tropical-type-check). Mirrors provers/mod.rs + // default_executable() — source of truth is that function. + kind if kind.is_hp_ecosystem() => PathBuf::from(kind.default_executable()), + _ => PathBuf::from(kind.default_executable()), + } +} + +/// Create a progress bar with standard styling +pub(crate) fn create_progress_bar(message: &str) -> ProgressBar { + let pb = ProgressBar::new_spinner(); + pb.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .expect("Failed to set progress bar template"), + ); + pb.set_message(message.to_string()); + pb.enable_steady_tick(std::time::Duration::from_millis(100)); + pb +} diff --git a/src/rust/cli/verify.rs b/src/rust/cli/verify.rs new file mode 100644 index 00000000..5712bf93 --- /dev/null +++ b/src/rust/cli/verify.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! `echidna` CLI: verify. Moved verbatim from main.rs (hotspot split). + +use super::*; +use crate::output::OutputFormatter; +use anyhow::{Context, Result}; +use echidna::ProverKind; +use std::path::PathBuf; +use tracing::info; + +/// Verify command implementation +pub(crate) async fn verify_command( + file: PathBuf, + prover_kind: Option, + timeout: u64, + executable: Option, + library: Vec, + diagnose: bool, + formatter: &OutputFormatter, +) -> Result<()> { + info!("Verifying proof: {}", file.display()); + + let kind = detect_prover(prover_kind, &file)?; + let config = create_config(kind, timeout, true, executable, library, None, "none")?; + let prover = echidna::provers::ProverFactory::create(kind, config) + .context("Failed to create prover backend")?; + + let pb = create_progress_bar("Parsing proof file..."); + let state = prover + .parse_file(file.clone()) + .await + .context("Failed to parse proof file")?; + + pb.set_message("Verifying proof..."); + let result = prover + .verify_proof(&state) + .await + .context("Failed to verify proof")?; + pb.finish_and_clear(); + + if result { + formatter.success(&format!("✓ Proof is valid: {}", file.display()))?; + formatter.info(&format!("Goals: {}", state.goals.len()))?; + formatter.info(&format!("Tactics: {}", state.proof_script.len()))?; + formatter.info(&format!( + "Theorems in context: {}", + state.context.theorems.len() + ))?; + } else { + formatter.error(&format!("✗ Proof is invalid: {}", file.display()))?; + if diagnose { + emit_diagnostic(kind, &*prover, &state, formatter).await; + } + std::process::exit(1); + } + + Ok(()) +} diff --git a/src/rust/main.rs b/src/rust/main.rs index bd1b23aa..7508555c 100644 --- a/src/rust/main.rs +++ b/src/rust/main.rs @@ -5,453 +5,18 @@ //! //! Provides command-line interface, REPL, and HTTP server for theorem proving -use anyhow::{Context, Result}; -use clap::{Parser, Subcommand}; -use echidna::dispatch::ProverDispatcher; -use echidna::{ - diagnostics::proof_failure::diagnose_from_outcome, provers::ProverBackend, ProverConfig, - ProverKind, -}; -use indicatif::{ProgressBar, ProgressStyle}; +use anyhow::Result; +use clap::Parser; use std::path::PathBuf; -use std::sync::Arc; -use tracing::{info, warn}; +mod cli; mod output; mod repl; mod server; -use output::{OutputFormat, OutputFormatter}; -use repl::DiagnosticsREPL; +use cli::*; -/// Result form of `echidna prove`. -#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)] -enum ProveOutput { - /// Human-readable report (the default). - Human, - /// One `echidna.prove.result/1` JSON object on stdout. - Json, -} - -/// ECHIDNA - Extensible Cognitive Hybrid Intelligence for Deductive Neural Assistance -#[derive(Parser)] -#[command(name = "echidna")] -#[command(version, about, long_about = None)] -#[command(author = "ECHIDNA Project Team")] -struct Cli { - /// Output format (text, json) - #[arg(long, global = true, default_value = "text")] - format: OutputFormat, - - /// Verbose output - #[arg(short, long, global = true)] - verbose: bool, - - /// Disable colors - #[arg(long, global = true)] - no_color: bool, - - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Prove a theorem from file - Prove { - /// Path to proof file - file: PathBuf, - - /// Prover to use (auto-detect if not specified) - #[arg(short, long)] - prover: Option, - - /// Timeout in seconds - #[arg(short, long, default_value = "300")] - timeout: u64, - - /// Enable neural premise selection - #[arg(long, default_value = "true")] - neural: bool, - - /// Prover executable path (override default) - #[arg(long)] - executable: Option, - - /// Library paths - #[arg(long)] - library: Vec, - - /// On failure, run diagnostics and explain why the proof failed - #[arg(long)] - diagnose: bool, - - /// Project root for backends with session-based builds (EI-1). - /// Today: Isabelle uses this to add `-d ` to the build - /// invocation and to import the file's session from the - /// project's existing ROOT, so theory imports resolve. - #[arg(long)] - project_root: Option, - - /// Sandbox mode for prover invocation (safe-learning b). - /// One of: none (default), bwrap, podman. - #[arg(long, default_value = "none")] - sandbox: String, - - /// Result form. `human` (default) prints the usual report; `json` - /// prints exactly one `echidna.prove.result/1` object (JCS-canonical - /// I-JSON) on stdout and sends logs to stderr. - #[arg(long, value_enum, default_value_t = ProveOutput::Human)] - output: ProveOutput, - }, - - /// Verify an existing proof - Verify { - /// Path to proof file - file: PathBuf, - - /// Prover to use (auto-detect if not specified) - #[arg(short, long)] - prover: Option, - - /// Timeout in seconds - #[arg(short, long, default_value = "300")] - timeout: u64, - - /// Prover executable path (override default) - #[arg(long)] - executable: Option, - - /// Library paths - #[arg(long)] - library: Vec, - - /// On failure, run diagnostics and explain why the proof failed - #[arg(long)] - diagnose: bool, - }, - - /// Search theorem libraries - Search { - /// Search pattern (regex supported) - pattern: String, - - /// Prover to search (searches all if not specified) - #[arg(short, long)] - prover: Option, - - /// Maximum results to return - #[arg(short, long, default_value = "20")] - limit: usize, - }, - - /// Start interactive REPL mode - Interactive { - /// Initial proof file to load - file: Option, - - /// Prover to use - #[arg(short, long)] - prover: Option, - - /// Enable neural premise selection - #[arg(long, default_value = "true")] - neural: bool, - }, - - /// Start HTTP API server - Server { - /// Port to bind to - #[arg(short, long, default_value = "8081")] - port: u16, - - /// Host to bind to - #[arg(long, default_value = "127.0.0.1")] - host: String, - - /// Enable CORS - #[arg(long)] - cors: bool, - }, - - /// List available provers - ListProvers { - /// Show detailed information - #[arg(short, long)] - detailed: bool, - }, - - /// Show information about a specific prover - Info { - /// Prover name - prover: ProverKind, - }, - - /// Interactive diagnostics REPL for health monitoring - Diagnostics, - - /// Build / query a project corpus (named decls + dependency DAG) - Corpus { - #[command(subcommand)] - op: CorpusOp, - }, - - /// Mine lexicon candidates across training corpora (flag-only) - Lexicon { - #[command(subcommand)] - op: LexiconOp, - }, - - /// Search proof-design space via simulated annealing - Design { - #[command(subcommand)] - op: DesignOp, - }, - - /// Suggest tactic variants that close a failing lemma - Suggest { - /// Target in `:` form, e.g. `Foo.thy:bar` - #[arg(value_name = "TARGET")] - target: String, - - /// Prover to use (auto-detected from file extension if absent) - #[arg(short, long)] - prover: Option, - - /// Time budget per variant attempt in seconds - #[arg(long, default_value_t = 60)] - budget: u64, - - /// Maximum number of variants to report - #[arg(long, default_value_t = 10)] - top: usize, - - /// Synonym table directory (defaults to `data/synonyms/`) - #[arg(long)] - synonyms_dir: Option, - - /// Don't run the prover; just print candidate variants - #[arg(long)] - dry_run: bool, - - /// Maximum concurrent variant tests - #[arg(long, default_value_t = 4)] - max_parallel: usize, - }, -} - -#[derive(Subcommand)] -enum CorpusOp { - /// Walk a project tree and build a corpus index. - Ingest { - /// Path to the project root. - #[arg(short, long)] - root: PathBuf, - /// Adapter to use: agda, coq, lean, idris2, acl2_books, creusot, - /// dafny, fstar, hol4, hol_light, isabelle, metamath, minif2f, - /// mizar, proofnet, smtlib, spark_ada, tptp, why3, or - /// hyperpolymath_estate (the multi-repo estate profile). - #[arg(short, long, default_value = "agda")] - adapter: String, - /// Where to write the JSON index. Defaults to - /// `data/corpus/.json`. - #[arg(short, long)] - out: Option, - }, - /// Query a previously-ingested corpus index. - Query { - /// Search term (qualified name, short name, or substring). - pattern: String, - /// Path to the corpus JSON. Defaults to - /// `data/corpus/.json` for the cwd's basename. - #[arg(short, long)] - index: Option, - /// Show transitive (forward) dependencies of the matched entry. - #[arg(long)] - deps: bool, - /// Show direct reverse-dependencies — entries that reference - /// the matched name, i.e. things that would (potentially) - /// break if the matched entry changed. - #[arg(long)] - reverse_deps: bool, - /// Show transitive reverse-closure — full impact set. - #[arg(long)] - reverse_closure: bool, - }, - /// Print summary statistics for a corpus index. - Stats { - #[arg(short, long)] - index: Option, - }, - - /// Look up entries across every synonym table by `semantic_class`. - /// Demonstrates cross-prover semantic equivalence: `corpus crossquery - /// well-foundedness` reports `WellFounded` in Agda, `well_founded` in - /// Coq, and `WellFounded` in Lean and Idris 2. - /// - /// The cross-prover dictionaries are searched too, so classes that - /// only exist there (`wrapping-arithmetic`, `frame-rule`, - /// `estate-snif`, or an MSC code such as `68Q60`) resolve. - Crossquery { - /// Semantic class to look up (e.g. `well-foundedness`). - class: String, - /// Synonym directory (defaults to `data/synonyms`). - #[arg(long)] - synonyms_dir: Option, - /// Only search the per-prover tables, skipping the cross-prover - /// dictionaries. - #[arg(long)] - provers_only: bool, - }, - - /// Find the K nearest-neighbour entries to a free-form text - /// query by cosine similarity over the Vector octad embeddings. - /// The query is canonicalised the same way the corpus is, so a - /// statement written in one prover's syntax matches entries written - /// in another's. Useful for "find lemmas structurally near - /// `WellFounded _<_`". - Near { - query: String, - #[arg(short, long)] - index: Option, - #[arg(short, long, default_value_t = 10)] - top: usize, - }, - - /// Group matching declarations by their *structural* identity key. - /// - /// The key is the SHA-256 of the type's canonical shape — variable - /// names, module paths, operator spellings and binder keywords are - /// normalised away — so `+-comm` in Agda, `Nat.add_comm` in Lean and - /// `eq Nat (add x y) (add y x)` in Dedukti land on one key with no - /// synonym lookup. `--skeleton` additionally prints the binder- and - /// sort-free key, which is what provers that elide binders (Isabelle) - /// match on. - Identity { - /// Name pattern to match: a substring of the qualified or short - /// name, case-insensitive. `*` matches every declaration. - pattern: String, - /// Path to the corpus JSON. Defaults to - /// `data/corpus/.json` for the cwd's basename. - #[arg(short, long)] - index: Option, - /// Also print the skeleton key (binders and sorts erased). - #[arg(long)] - skeleton: bool, - /// Only show keys that group declarations from more than one - /// adapter. - #[arg(long)] - cross_only: bool, - }, - - /// Convert a JSON corpus index into 8-modality octad JSONL - /// (one DeclarationOctad per line). Streamable; compatible with - /// VeriSim's `/api/v1/octads` endpoint when posted line-by-line. - ExportOctads { - /// Path to the JSON corpus index. - #[arg(short, long)] - index: PathBuf, - /// Output JSONL path. - #[arg(short, long)] - out: PathBuf, - }, - - /// Load an octad JSONL stream back into a corpus and persist as - /// JSON. Round-trip is best-effort but lossless for the fields - /// the corpus cares about (statements, proofs, deps, hazards). - ImportOctads { - #[arg(short, long)] - octads: PathBuf, - #[arg(short, long)] - out: PathBuf, - }, -} - -#[derive(Subcommand)] -enum LexiconOp { - /// Pair theorem names and canonical statement shapes across training - /// corpora and write review candidates as JCS JSON Lines. Every - /// candidate carries `evidence = none`; `data/synonyms/` is never edited. - Scan { - /// Directory holding `proof_states_.jsonl` files. - #[arg(long, default_value = "training_data")] - training_dir: PathBuf, - /// A corpus pair, `A:B`. Repeatable; combines with `--preset`. - #[arg(long = "pair", value_name = "A:B")] - pairs: Vec, - /// A named pair set: `learning`, `spanning`, `in-family` or `full`. - #[arg(long)] - preset: Option, - /// Output directory (pairs/, collisions/, summary.jsonl, coverage.jsonl). - #[arg(long, default_value = "target/lexicon-scan")] - out: PathBuf, - /// Also emit pairs whose shared name is generic (`lemma3`, `aux`, `ex1`). - #[arg(long)] - keep_generic: bool, - /// Skip a shared name whose statement cross product exceeds this: - /// exact pairs per raw name, folded pairs per whole folded group. - #[arg(long, default_value_t = 64)] - max_pairs_per_name: usize, - /// Skip a shape or skeleton group larger than this on either side. - #[arg(long, default_value_t = 8)] - max_shape_group: usize, - /// Key-collision groups to sample per corpus. - #[arg(long, default_value_t = 40)] - collision_samples: usize, - }, -} - -#[derive(Subcommand)] -enum DesignOp { - /// Run SA over a named design problem. - Search { - /// Problem identifier. Currently: `brouwer-leq` (Phase 1.3 - /// `_≤_` redesign for `Ordinal.Brouwer`). - problem: String, - /// Iterations per restart. - #[arg(long, default_value_t = 1000)] - iterations: usize, - /// Number of independent restarts. - #[arg(long, default_value_t = 4)] - restarts: usize, - /// Initial Metropolis temperature. - #[arg(long, default_value_t = 4.0)] - temp: f64, - /// Multiplicative cooling factor per iteration. - #[arg(long, default_value_t = 0.995)] - cooling: f64, - /// Seed. - #[arg(long, default_value_t = 0xC0FFEE)] - seed: u64, - /// Top-K candidates to print. - #[arg(long, default_value_t = 8)] - top: usize, - }, - - /// Run a parallel swarm of SA agents with cross-pollination. - Swarm { - /// Problem identifier. Currently: `brouwer-leq`. - problem: String, - /// Number of parallel agents. - #[arg(long, default_value_t = 4)] - agents: usize, - #[arg(long, default_value_t = 1000)] - iterations: usize, - #[arg(long, default_value_t = 4.0)] - temp: f64, - #[arg(long, default_value_t = 0.995)] - cooling: f64, - #[arg(long, default_value_t = 0xC0FFEE)] - seed: u64, - /// Broadcast best-so-far every N iterations. - #[arg(long, default_value_t = 100)] - broadcast_every: usize, - /// Adopt a peer's broadcast only if it beats the agent's best - /// by this lex-margin (0 = adopt on any improvement). - #[arg(long, default_value_t = 0)] - adopt_threshold: i64, - }, -} +use output::OutputFormatter; /// Run the selected CLI command after configuring tracing, colours and output. #[tokio::main] @@ -616,1544 +181,4 @@ async fn main() -> Result<()> { Ok(()) } -/// Initialize tracing/logging; logs go to stderr when `to_stderr` is set. -fn init_tracing(verbose: bool, to_stderr: bool) { - use tracing_subscriber::filter::EnvFilter; - use tracing_subscriber::{fmt, prelude::*}; - - let filter = if verbose { - EnvFilter::new("echidna=debug,info") - } else { - EnvFilter::new("echidna=info,warn") - }; - - let registry = tracing_subscriber::registry().with(filter); - if to_stderr { - registry - .with(fmt::layer().with_writer(std::io::stderr)) - .init(); - } else { - registry.with(fmt::layer()).init(); - } -} - -/// Prove command implementation -#[allow(clippy::too_many_arguments)] -async fn prove_command( - file: PathBuf, - prover_kind: Option, - timeout: u64, - neural: bool, - executable: Option, - library: Vec, - diagnose: bool, - project_root: Option, - sandbox: String, - formatter: &OutputFormatter, -) -> Result<()> { - info!("Starting proof for: {}", file.display()); - - let kind = detect_prover(prover_kind, &file)?; - let config = create_config( - kind, - timeout, - neural, - executable, - library, - project_root, - &sandbox, - )?; - let prover = echidna::provers::ProverFactory::create(kind, config) - .context("Failed to create prover backend")?; - - let pb = create_progress_bar("Parsing proof file..."); - let state = prover - .parse_file(file.clone()) - .await - .context("Failed to parse proof file")?; - - pb.set_message("Verifying proof..."); - let result = prover - .verify_proof(&state) - .await - .context("Failed to verify proof")?; - pb.finish_and_clear(); - - if result { - formatter.success(&format!( - "✓ Proof verified successfully for {}", - file.display() - ))?; - formatter.output_proof_state(&state)?; - } else { - formatter.error(&format!( - "✗ Proof verification failed for {}", - file.display() - ))?; - formatter.output_proof_state(&state)?; - if diagnose { - emit_diagnostic(kind, &*prover, &state, formatter).await; - } - std::process::exit(1); - } - - Ok(()) -} - -/// `echidna prove --output json`: print one `echidna.prove.result/1` object. -/// -/// Every failure, including a missing backend or unreadable file, becomes an -/// `error` result rather than a non-JSON message. Exits 0 when the status is -/// `verified` and 1 otherwise. With `--diagnose`, the diagnostic report for a -/// non-verified run goes to stderr. -#[allow(clippy::too_many_arguments)] -async fn prove_json_command( - file: PathBuf, - prover_kind: Option, - timeout: u64, - neural: bool, - executable: Option, - library: Vec, - diagnose: bool, - project_root: Option, - sandbox: String, -) { - use echidna::prove_contract::{self, ProveStatus}; - - let goal = file.display().to_string(); - let start = std::time::Instant::now(); - let since = - |start: std::time::Instant| u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); - - let kind = match detect_prover(prover_kind, &file) { - Ok(k) => k, - Err(e) => { - let r = prove_contract::result_from_error(prover_kind, &goal, &e, since(start)); - emit_prove_result(&r); - }, - }; - let backend = match create_config( - kind, - timeout, - neural, - executable, - library, - project_root, - &sandbox, - ) - .and_then(|config| { - echidna::provers::ProverFactory::create(kind, config) - .context("Failed to create prover backend") - }) { - Ok(b) => b, - Err(e) => { - let r = prove_contract::result_from_error(Some(kind), &goal, &e, since(start)); - emit_prove_result(&r); - }, - }; - - let (result, outcome) = prove_contract::run_detailed(kind, &*backend, &file).await; - if let (true, Some(outcome)) = (diagnose && result.status != ProveStatus::Verified, outcome) { - eprintln!("--- Proof Failure Diagnostic ---"); - eprintln!("{}", diagnose_from_outcome(kind, &outcome).display()); - } - emit_prove_result(&result); -} - -/// Print `result` as its one JCS line on stdout and exit (0 iff verified). -/// -/// If the value cannot be serialised (it never should), a minimal `error` -/// object is printed instead, so stdout always carries one contract object. -fn emit_prove_result(result: &echidna::prove_contract::ProveResult) -> ! { - use echidna::prove_contract::{self, ProveStatus}; - let line = result.to_jcs().unwrap_or_else(|e| { - let e = anyhow::anyhow!("could not serialise prove result: {e}"); - prove_contract::result_from_error(None, &result.goal, &e, 0) - .to_jcs() - .expect("an error result with no numbers out of range serialises") - }); - println!("{line}"); - std::process::exit(if result.status == ProveStatus::Verified { - 0 - } else { - 1 - }); -} - -/// Run `check()` on the prover and display a structured diagnostic report. -/// -/// Called after a failed `verify_proof()` when `--diagnose` is active. -/// Uses the richer `ProverOutcome` returned by `check()` to produce an -/// actionable report via `diagnose_from_outcome`. -async fn emit_diagnostic( - kind: ProverKind, - prover: &dyn ProverBackend, - state: &echidna::core::ProofState, - formatter: &OutputFormatter, -) { - let outcome = match prover.check(state).await { - Ok(o) => o, - Err(e) => { - let _ = formatter.error(&format!("[diagnose] could not run check(): {}", e)); - return; - }, - }; - let report = diagnose_from_outcome(kind, &outcome); - let _ = formatter.info("\n--- Proof Failure Diagnostic ---"); - let _ = formatter.info(&report.display()); -} - -/// Verify command implementation -async fn verify_command( - file: PathBuf, - prover_kind: Option, - timeout: u64, - executable: Option, - library: Vec, - diagnose: bool, - formatter: &OutputFormatter, -) -> Result<()> { - info!("Verifying proof: {}", file.display()); - - let kind = detect_prover(prover_kind, &file)?; - let config = create_config(kind, timeout, true, executable, library, None, "none")?; - let prover = echidna::provers::ProverFactory::create(kind, config) - .context("Failed to create prover backend")?; - - let pb = create_progress_bar("Parsing proof file..."); - let state = prover - .parse_file(file.clone()) - .await - .context("Failed to parse proof file")?; - - pb.set_message("Verifying proof..."); - let result = prover - .verify_proof(&state) - .await - .context("Failed to verify proof")?; - pb.finish_and_clear(); - - if result { - formatter.success(&format!("✓ Proof is valid: {}", file.display()))?; - formatter.info(&format!("Goals: {}", state.goals.len()))?; - formatter.info(&format!("Tactics: {}", state.proof_script.len()))?; - formatter.info(&format!( - "Theorems in context: {}", - state.context.theorems.len() - ))?; - } else { - formatter.error(&format!("✗ Proof is invalid: {}", file.display()))?; - if diagnose { - emit_diagnostic(kind, &*prover, &state, formatter).await; - } - std::process::exit(1); - } - - Ok(()) -} - -/// Search command implementation -async fn search_command( - pattern: String, - prover_kind: Option, - limit: usize, - formatter: &OutputFormatter, -) -> Result<()> { - info!("Searching for: {}", pattern); - - let provers: Vec = if let Some(kind) = prover_kind { - vec![kind] - } else { - // Search all provers - vec![ - ProverKind::Agda, - ProverKind::Coq, - ProverKind::Lean, - ProverKind::Isabelle, - ProverKind::Z3, - ProverKind::CVC5, - ProverKind::Metamath, - ProverKind::HOLLight, - ProverKind::Mizar, - ProverKind::PVS, - ProverKind::ACL2, - ProverKind::HOL4, - ProverKind::Idris2, - ] - }; - - formatter.info(&format!( - "Searching {} prover(s) for pattern: {}", - provers.len(), - pattern - ))?; - - let mut total_results = 0; - - for kind in provers { - let config = ProverConfig::default(); - - if let Ok(prover) = echidna::provers::ProverFactory::create(kind, config) { - formatter.section(&format!("\n{:?} Results:", kind))?; - - match prover.search_theorems(&pattern).await { - Ok(results) => { - total_results += results.len(); - - for (i, result) in results.iter().take(limit).enumerate() { - formatter.result(&format!(" {}. {}", i + 1, result))?; - } - - if results.len() > limit { - formatter - .info(&format!(" ... and {} more results", results.len() - limit))?; - } - }, - Err(e) => { - warn!("Failed to search {}: {}", kind, e); - }, - } - } - } - - // After per-backend search, add a cross-prover layer: query VeriSimDB - // for matches across every prover that has ever recorded an attempt. - // This compensates for backends without a native search command (the - // 70+ that legitimately return Vec::new() because their underlying - // prover doesn't ship a `Search`-equivalent). No-op without the - // `verisim` feature; logs and continues on Verisim outage. - let verisim_url = - std::env::var("VERISIM_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); - match echidna::vcl_ut::cross_prover_search_names(&verisim_url, &pattern, limit).await { - Ok(cross) if !cross.is_empty() => { - formatter.section("\nCross-prover (VeriSimDB) Results:")?; - total_results += cross.len(); - for (i, result) in cross.iter().take(limit).enumerate() { - formatter.result(&format!(" {}. {}", i + 1, result))?; - } - if cross.len() > limit { - formatter.info(&format!(" ... and {} more results", cross.len() - limit))?; - } - }, - Ok(_) => {}, - Err(e) => { - warn!("Cross-prover search failed: {}", e); - }, - } - - formatter.info(&format!("\nTotal results found: {}", total_results))?; - - Ok(()) -} - -/// Interactive REPL command -async fn interactive_command( - file: Option, - prover_kind: Option, - neural: bool, -) -> Result<()> { - info!("Starting interactive REPL mode"); - - repl::start_repl(file, prover_kind, neural).await -} - -/// HTTP Server command -async fn server_command(port: u16, host: String, cors: bool) -> Result<()> { - info!("Starting HTTP server on {}:{}", host, port); - - server::start_server(port, host, cors).await -} - -/// List provers command -fn list_provers_command(detailed: bool, formatter: &OutputFormatter) -> Result<()> { - let provers = ProverKind::all(); - - formatter.header("Available Provers")?; - formatter.info(&format!("Total: {} provers\n", provers.len()))?; - - for prover in provers { - let tier = prover.tier(); - let complexity = prover.complexity(); - let time = prover.implementation_time(); - - if detailed { - formatter.section(&format!("{:?}", prover))?; - formatter.info(&format!(" Tier: {}", tier))?; - formatter.info(&format!(" Complexity: {}/5", complexity))?; - formatter.info(&format!(" Implementation time: {} weeks", time))?; - formatter.info("")?; - } else { - formatter.result(&format!( - "{:12} - Tier {} | Complexity {}/5 | {} weeks", - format!("{:?}", prover), - tier, - complexity, - time - ))?; - } - } - - if !detailed { - formatter.info("\nUse --detailed flag for more information")?; - } - - Ok(()) -} - -/// Info command - show detailed information about a prover -fn info_command(prover: ProverKind, formatter: &OutputFormatter) -> Result<()> { - formatter.header(&format!("{:?} Prover Information", prover))?; - formatter.info("")?; - - formatter.section("Classification")?; - formatter.info(&format!(" Tier: {}", prover.tier()))?; - formatter.info(&format!(" Complexity: {}/5", prover.complexity()))?; - formatter.info(&format!( - " Implementation time: {} weeks", - prover.implementation_time() - ))?; - formatter.info("")?; - - formatter.section("Description")?; - let description = match prover { - ProverKind::Agda => { - "Dependently typed functional programming language and proof assistant.\n \ - Tier 1 prover. Strong type system with universe polymorphism." - }, - ProverKind::Coq => { - "Interactive theorem prover using the Calculus of Inductive Constructions.\n \ - Widely used in formal verification and mathematics. Powerful tactic language." - }, - ProverKind::Lean => { - "Modern theorem prover and programming language with dependent types.\n \ - Strong community, extensive mathlib. Excellent for formalized mathematics." - }, - ProverKind::Isabelle => { - "Generic proof assistant supporting multiple logics (mainly HOL).\n \ - Powerful automation through Sledgehammer. Large Archive of Formal Proofs." - }, - ProverKind::Z3 => { - "High-performance SMT (Satisfiability Modulo Theories) solver.\n \ - Excellent for automated reasoning in program verification." - }, - ProverKind::CVC5 => { - "Automatic SMT solver for first-order logic with theories.\n \ - Strong support for quantifiers, datatypes, and strings." - }, - ProverKind::Metamath => { - "Ultra-minimal proof verification system with plain-text proofs.\n \ - EASIEST to integrate (2/5 complexity). Large database of formalized mathematics." - }, - ProverKind::HOLLight => { - "Simple, elegant HOL (Higher-Order Logic) proof assistant in OCaml.\n \ - Part of the 'Big Six' theorem provers. Strong mathematical foundations." - }, - ProverKind::Mizar => { - "Proof assistant with natural-language-like proof style.\n \ - Large Mathematical Library. Readable proof documents." - }, - ProverKind::PVS => { - "Specification and verification system for safety-critical systems.\n \ - Strong support for dependent types and predicate subtyping." - }, - ProverKind::ACL2 => { - "Computational Logic for Applicative Common Lisp.\n \ - Industrial-strength theorem prover for software/hardware verification." - }, - ProverKind::HOL4 => { - "Interactive theorem prover in the HOL family.\n \ - Used extensively in hardware verification. ML-based tactic language." - }, - ProverKind::Idris2 => { - "Dependently typed functional language with quantitative types.\n \ - First-class type-level computation, elaborator reflection, linear types." - }, - ProverKind::Vampire => { - "First-order automated theorem prover. Multiple CASC winner.\n \ - Superposition calculus with excellent performance on CASC benchmarks." - }, - ProverKind::EProver => { - "Highly optimized first-order theorem prover for clausal logic.\n \ - CASC winner. Auto mode with sophisticated strategy selection." - }, - ProverKind::SPASS => { - "First-order theorem prover with sorted logic support.\n \ - DFG format input. Superposition calculus with sort handling." - }, - ProverKind::AltErgo => { - "SMT solver with polymorphic first-order logic.\n \ - Designed for program verification (Why3, Frama-C integration)." - }, - ProverKind::FStar => { - "F* dependent types with effects. Project Everest/HACL* verified crypto." - }, - ProverKind::Dafny => "Auto-active verifier. Pre/postconditions verified via Boogie and Z3.", - ProverKind::Why3 => { - "Multi-prover orchestration. Dispatches to Z3, CVC5, Alt-Ergo in parallel." - }, - ProverKind::TLAPS => "TLA+ Proof System. Verifies distributed system properties.", - ProverKind::Twelf => "Logical framework (LF). Metatheory verification for type systems.", - ProverKind::Nuprl => "Constructive type theory. Large library of formalized mathematics.", - ProverKind::Minlog => "Minimal logic with certified program extraction from proofs.", - ProverKind::Imandra => "ML-based reasoning for industrial verification.", - ProverKind::GLPK => "GNU Linear Programming Kit. LP/MIP constraint solving.", - ProverKind::SCIP => "Mixed-integer nonlinear programming solver.", - ProverKind::MiniZinc => "Constraint modelling language with multiple backend solvers.", - ProverKind::Chuffed => "Lazy clause generation CP solver with SAT-style learning.", - ProverKind::ORTools => "Google OR-Tools. CP-SAT, routing, linear/integer programming.", - ProverKind::TypedWasm => { - "TypedWasm oracle. 10-level type safety validation for .twasm programs." - }, - ProverKind::SPIN => "SPIN model checker for Promela concurrent system specifications.", - ProverKind::CBMC => "C Bounded Model Checker. Verifies C programs via bounded unwinding.", - ProverKind::CaDiCaL => { - "State-of-the-art CDCL SAT solver. DIMACS CNF input.\n \ - Multiple SAT Competition winner. Default SAT backend for ECHIDNA." - }, - ProverKind::Kissat => { - "Fast, highly-optimised CDCL SAT solver. DIMACS CNF input.\n \ - SAT Competition winner. Designed for raw solving speed." - }, - ProverKind::MiniSat => { - "Classic DPLL/CDCL SAT solver. DIMACS CNF input. Reference implementation." - }, - ProverKind::NuSMV => { - "NuSMV/nuXmv symbolic model checker for CTL/LTL verification of finite state systems." - }, - ProverKind::TLC => { - "TLC model checker for exhaustive state exploration of TLA+ specifications." - }, - ProverKind::Alloy => "Alloy relational model finder using SAT-based bounded analysis.", - ProverKind::Prism => "PRISM probabilistic model checker for DTMCs, CTMCs, MDPs, and PTAs.", - ProverKind::UPPAAL => { - "UPPAAL timed automata model checker for real-time system verification." - }, - ProverKind::FramaC => "Frama-C WP deductive verifier for ACSL-annotated C programs.", - ProverKind::SeaHorn => "SeaHorn verification framework for LLVM-based safety checking.", - ProverKind::Viper => "Viper permission-based program verifier with separation logic.", - ProverKind::Tamarin => { - "Tamarin security protocol prover using multiset rewriting in the Dolev-Yao model." - }, - ProverKind::ProVerif => { - "ProVerif automatic cryptographic protocol verifier in the Dolev-Yao model." - }, - ProverKind::KeY => { - "KeY deductive Java verifier using JavaDL (dynamic logic) with JML contracts." - }, - ProverKind::DReal => { - "dReal delta-complete SMT solver for nonlinear real arithmetic over the reals." - }, - ProverKind::ABC => { - "ABC sequential logic synthesis and verification system for hardware circuits." - }, - // HP ecosystem type-disciplines (TypeDiscipline transition, 2026-04-17). - // A single-line fallback for the forty discipline views; per-discipline - // descriptions are a phase-2 followup tracked in AI-WORK-todo.md. - k if k.is_hp_ecosystem() => { - "HP ecosystem type-discipline view. Dispatches through typell / \n \ - katagoria / tropical-resource-typing. See disciplines/ for the \n \ - 40-variant taxonomy (linear, affine, phantom, modal, etc.)." - }, - _ => "No description available for this prover.", - }; - formatter.info(&format!(" {}", description))?; - formatter.info("")?; - - formatter.section("File Extensions")?; - let extension = match prover { - ProverKind::Agda => ".agda", - ProverKind::Coq => ".v", - ProverKind::Lean => ".lean", - ProverKind::Isabelle => ".thy", - ProverKind::Z3 | ProverKind::CVC5 => ".smt2", - ProverKind::Metamath => ".mm", - ProverKind::HOLLight => ".ml", - ProverKind::Mizar => ".miz", - ProverKind::PVS => ".pvs", - ProverKind::ACL2 => ".lisp", - ProverKind::HOL4 => ".sml", - ProverKind::Idris2 => ".idr", - ProverKind::Vampire => ".p / .tptp", - ProverKind::EProver => ".p / .tptp", - ProverKind::SPASS => ".dfg", - ProverKind::AltErgo => ".ae / .why", - ProverKind::FStar => ".fst / .fsti", - ProverKind::Dafny => ".dfy", - ProverKind::Why3 => ".why / .mlw", - ProverKind::TLAPS => ".tla", - ProverKind::Twelf => ".elf", - ProverKind::Nuprl => ".nuprl", - ProverKind::Minlog => ".minlog", - ProverKind::Imandra => ".iml", - ProverKind::GLPK => ".lp / .mps", - ProverKind::SCIP => ".pip / .zpl", - ProverKind::MiniZinc => ".mzn / .dzn", - ProverKind::Chuffed => ".fzn", - ProverKind::ORTools => ".or / .proto", - ProverKind::TypedWasm => ".twasm", - ProverKind::SPIN => ".pml", - ProverKind::CBMC => ".c", - ProverKind::CaDiCaL => ".cnf", - ProverKind::Kissat => ".cnf", - ProverKind::MiniSat => ".cnf", - ProverKind::NuSMV => ".smv", - ProverKind::TLC => ".tla", - ProverKind::Alloy => ".als", - ProverKind::Prism => ".pm / .prism", - ProverKind::UPPAAL => ".xml", - ProverKind::FramaC => ".c (ACSL-annotated)", - ProverKind::SeaHorn => ".bc / .ll (LLVM IR)", - ProverKind::Viper => ".vpr", - ProverKind::Tamarin => ".spthy", - ProverKind::ProVerif => ".pv", - ProverKind::KeY => ".java (JML-annotated)", - ProverKind::DReal => ".smt2 (QF_NRA/NRA)", - ProverKind::ABC => ".blif / .aig", - // HP ecosystem: discipline-tagged source lives in typell; no fixed - // extension. See HPEcosystemBackend for the actual dispatch. - k if k.is_hp_ecosystem() => "typell discipline source (.tll / #discipline header)", - _ => "unknown", - }; - formatter.info(&format!(" {}", extension))?; - formatter.info("")?; - - formatter.section("Features")?; - formatter.info(" ✓ Neural premise selection")?; - formatter.info(" ✓ Aspect tagging")?; - formatter.info(" ✓ OpenCyc integration")?; - formatter.info(" ✓ Tactic suggestion")?; - formatter.info("")?; - - // Try to get version if prover is installed - let config = ProverConfig::default(); - if let Ok(prover_backend) = echidna::provers::ProverFactory::create(prover, config) { - formatter.section("Installation")?; - - // Spawn async runtime to check version - let version_check = tokio::task::spawn(async move { prover_backend.version().await }); - - if let Ok(Ok(version)) = futures::executor::block_on(version_check) { - formatter.success(&format!(" ✓ Installed: {}", version))?; - } else { - formatter.warning(" ✗ Not detected (may not be installed or not in PATH)")?; - } - } - - Ok(()) -} - -/// Detect prover from file extension or use specified prover -fn detect_prover(prover_kind: Option, file: &std::path::Path) -> Result { - if let Some(kind) = prover_kind { - return Ok(kind); - } - - echidna::provers::ProverFactory::detect_from_file(file).ok_or_else(|| { - anyhow::anyhow!( - "Could not detect prover from file extension: {}. Please specify with --prover", - file.display() - ) - }) -} - -/// Create prover configuration -fn create_config( - kind: ProverKind, - timeout: u64, - neural: bool, - executable: Option, - library: Vec, - project_root: Option, - sandbox: &str, -) -> Result { - let sandbox_mode: echidna::provers::SandboxMode = - sandbox.parse().map_err(|e: String| anyhow::anyhow!(e))?; - - let config = ProverConfig { - timeout, - neural_enabled: neural, - executable: executable.unwrap_or_else(|| get_default_executable(kind)), - library_paths: if library.is_empty() { - ProverConfig::default().library_paths - } else { - library - }, - project_root, - sandbox: sandbox_mode, - ..ProverConfig::default() - }; - - Ok(config) -} - -/// Get default executable path for a prover -fn get_default_executable(kind: ProverKind) -> PathBuf { - match kind { - ProverKind::Agda => PathBuf::from("agda"), - ProverKind::Coq => PathBuf::from("coqtop"), - ProverKind::Lean => PathBuf::from("lean"), - ProverKind::Isabelle => PathBuf::from("isabelle"), - ProverKind::Z3 => PathBuf::from("z3"), - ProverKind::CVC5 => PathBuf::from("cvc5"), - ProverKind::Metamath => PathBuf::from("metamath"), - ProverKind::HOLLight => PathBuf::from("ocaml"), - ProverKind::Mizar => PathBuf::from("verifier"), - ProverKind::PVS => PathBuf::from("pvs"), - ProverKind::ACL2 => PathBuf::from("acl2"), - ProverKind::HOL4 => PathBuf::from("hol"), - ProverKind::Idris2 => PathBuf::from("idris2"), - ProverKind::Vampire => PathBuf::from("vampire"), - ProverKind::EProver => PathBuf::from("eprover"), - ProverKind::SPASS => PathBuf::from("SPASS"), - ProverKind::AltErgo => PathBuf::from("alt-ergo"), - ProverKind::FStar => PathBuf::from("fstar.exe"), - ProverKind::Dafny => PathBuf::from("dafny"), - ProverKind::Why3 => PathBuf::from("why3"), - ProverKind::TLAPS => PathBuf::from("tlapm"), - ProverKind::Twelf => PathBuf::from("twelf-server"), - ProverKind::Nuprl => PathBuf::from("nuprl"), - ProverKind::Minlog => PathBuf::from("minlog"), - ProverKind::Imandra => PathBuf::from("imandra"), - ProverKind::GLPK => PathBuf::from("glpsol"), - ProverKind::SCIP => PathBuf::from("scip"), - ProverKind::MiniZinc => PathBuf::from("minizinc"), - ProverKind::Chuffed => PathBuf::from("fzn-chuffed"), - ProverKind::ORTools => PathBuf::from("ortools_solve"), - ProverKind::TypedWasm => PathBuf::from("idris2"), - ProverKind::SPIN => PathBuf::from("spin"), - ProverKind::CBMC => PathBuf::from("cbmc"), - ProverKind::CaDiCaL => PathBuf::from("cadical"), - ProverKind::Kissat => PathBuf::from("kissat"), - ProverKind::MiniSat => PathBuf::from("minisat"), - ProverKind::NuSMV => PathBuf::from("nuXmv"), - ProverKind::TLC => PathBuf::from("tlc2.TLC"), - ProverKind::Alloy => PathBuf::from("alloy"), - ProverKind::Prism => PathBuf::from("prism"), - ProverKind::UPPAAL => PathBuf::from("verifyta"), - ProverKind::FramaC => PathBuf::from("frama-c"), - ProverKind::SeaHorn => PathBuf::from("seahorn"), - ProverKind::Viper => PathBuf::from("viper"), - ProverKind::Tamarin => PathBuf::from("tamarin-prover"), - ProverKind::ProVerif => PathBuf::from("proverif"), - ProverKind::KeY => PathBuf::from("key"), - ProverKind::DReal => PathBuf::from("dreal"), - ProverKind::ABC => PathBuf::from("abc"), - // HP ecosystem: route through the TypeLL kernel CLI, with two named - // exceptions (katagoria, tropical-type-check). Mirrors provers/mod.rs - // default_executable() — source of truth is that function. - kind if kind.is_hp_ecosystem() => PathBuf::from(kind.default_executable()), - _ => PathBuf::from(kind.default_executable()), - } -} - -/// Create a progress bar with standard styling -fn create_progress_bar(message: &str) -> ProgressBar { - let pb = ProgressBar::new_spinner(); - pb.set_style( - ProgressStyle::default_spinner() - .template("{spinner:.green} {msg}") - .expect("Failed to set progress bar template"), - ); - pb.set_message(message.to_string()); - pb.enable_steady_tick(std::time::Duration::from_millis(100)); - pb -} - -/// Diagnostics command - interactive health monitoring REPL -async fn diagnostics_command() -> Result<()> { - info!("Starting diagnostics REPL for health monitoring"); - let dispatcher = Arc::new(ProverDispatcher::new()); - let health = dispatcher.health_status()?; - let mut repl = DiagnosticsREPL::new(); - repl.run(&health).await; - Ok(()) -} - -/// Lexicon command - mine cross-corpus candidates for curation. -/// -/// The scan runs on a thread with a large stack: the canonicaliser's parser -/// recurses on nesting depth, and some corpus statements (ACL2 books) nest -/// deeply. Timings go to stderr; the JCS files under `--out` carry none, so -/// two runs over the same data write the same bytes. -/// -/// # Errors -/// -/// An unknown preset, a malformed `--pair`, no pairs at all, a corpus that -/// fails to load, an output directory that cannot be created or read, an -/// old output file that cannot be removed, or a serialisation or output -/// write failure. Also returns an error if the scan thread cannot be -/// spawned or panics. Failed scans may leave partial output. -fn lexicon_command(op: LexiconOp, formatter: &OutputFormatter) -> Result<()> { - use echidna::lexicon::scan::{self, RunOptions, ScanOptions}; - - let LexiconOp::Scan { - training_dir, - pairs, - preset, - out, - keep_generic, - max_pairs_per_name, - max_shape_group, - collision_samples, - } = op; - - let mut all_pairs = Vec::new(); - if let Some(name) = preset.as_deref() { - all_pairs.extend(scan::preset_pairs(name).with_context(|| { - format!("unknown preset `{name}` (learning, spanning, in-family, full)") - })?); - } - for p in &pairs { - let pair = scan::parse_pair(p)?; - // A pair is the same pair in either direction; the first spelling - // requested is the one scanned. - let seen = all_pairs - .iter() - .any(|(a, b)| (a, b) == (&pair.0, &pair.1) || (a, b) == (&pair.1, &pair.0)); - if !seen { - all_pairs.push(pair); - } - } - let opts = RunOptions { - training_dir, - pairs: all_pairs, - out, - scan: ScanOptions { - keep_generic, - max_pairs_per_name, - max_shape_group, - }, - collision_samples, - }; - - let report = std::thread::Builder::new() - .name("lexicon-scan".into()) - .stack_size(512 * 1024 * 1024) - .spawn(move || scan::run(&opts).map(|r| (r, opts.out))) - .context("spawn the scan thread")? - .join() - .map_err(|_| anyhow::anyhow!("the scan thread panicked"))??; - let (report, out) = report; - - if formatter.is_json() { - // One JCS object on stdout; the timings stay on stderr. - let summary = serde_json::json!({ - "coverage": report.coverage, - "pairs": report.pairs, - "out": out.display().to_string(), - "total_ms": u64::try_from(report.total.as_millis()).unwrap_or(u64::MAX), - }); - std::io::Write::write_all(&mut std::io::stdout(), &scan::jcs_line(&summary)?)?; - eprintln!("total: {:.2?}; output in {}", report.total, out.display()); - return Ok(()); - } - - println!( - "{:<14} {:>8} {:>8} {:>6} {:>7} {:>6} {:>6} {:>6} {:>8}", - "corpus", "rows", "names", "hom%", "struct%", "skel%", "text%", "cmpl%", "collide" - ); - for c in &report.coverage { - let pct = |n: usize| 100.0 * n as f64 / c.rows.max(1) as f64; - println!( - "{:<14} {:>8} {:>8} {:>6.1} {:>7.1} {:>6.1} {:>6.1} {:>6.1} {:>8}", - c.corpus, - c.rows, - c.names, - 100.0 * c.homonym_names as f64 / c.names.max(1) as f64, - pct(c.tiers.structural), - pct(c.tiers.skeleton), - pct(c.tiers.text), - pct(c.tiers.complete), - c.collision_keys - ); - } - println!(); - println!( - "{:<26} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>8} {:>7} {:>7} {:>7}", - "pair", - "shared", - "generic", - "capped", - "exact", - "shape", - "skel", - "differs", - "unparsed", - "syn-tx", - "syn-sh", - "syn-sk" - ); - for s in &report.pairs { - let names = [&s.candidates.name_exact, &s.candidates.name_folded]; - let sum = - |f: fn(&scan::CandidateCounts) -> usize| names.iter().map(|c| f(c)).sum::(); - println!( - "{:<26} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>8} {:>7} {:>7} {:>7}", - format!("{}:{}", s.pair[0], s.pair[1]), - s.folded_names_shared, - s.generic_names_skipped, - s.name_pairs_capped, - sum(|c| c.cognate_exact), - sum(|c| c.cognate_shape), - sum(|c| c.cognate_skeleton), - sum(|c| c.differs), - sum(|c| c.unparsed), - s.candidates.text.total(), - s.candidates.shape.total(), - s.candidates.skeleton.total() - ); - } - for (corpus, t) in &report.load_times { - eprintln!("load {corpus}: {:.2?}", t); - } - for (pair, t) in &report.pair_times { - eprintln!("scan {pair}: {:.2?}", t); - } - eprintln!("total: {:.2?}; output in {}", report.total, out.display()); - Ok(()) -} - -/// Corpus command - ingest, query, or summarise a project corpus. -/// -/// First adapter is `agda`, targeted at echo-types' Buchholz / Brouwer -/// proof tree. Build the index once with `corpus ingest --root …`, -/// then query it without re-walking the source. The index is plain -/// JSON at `data/corpus/.json` so other tools (suggest, the -/// upcoming SA design search) can consume it directly. -/// -/// Crossquery prints exact semantic-class and canonical-name matches from -/// per-prover tables and, unless `provers_only` is set, cross-prover dictionaries. -/// Identity queries skip declarations whose canonical keys are incomplete -/// and report the number skipped; only complete keys form identity groups. -/// -/// # Errors -/// -/// Returns an error for an unknown ingest adapter and propagates errors from -/// corpus ingestion, index or dictionary loading, export/import, saving and -/// output formatting. Resolving a default index can also fail if the working -/// directory cannot be obtained. Queries with no matches succeed. -fn corpus_command(op: CorpusOp, formatter: &OutputFormatter) -> Result<()> { - use echidna::corpus::{self, Corpus}; - - let default_index = |root_basename: &str| -> PathBuf { - PathBuf::from("data/corpus").join(format!("{}.json", root_basename)) - }; - - match op { - CorpusOp::Ingest { root, adapter, out } => { - let root = root.canonicalize().unwrap_or(root); - let basename = root - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| "project".to_string()); - let out = out.unwrap_or_else(|| default_index(&basename)); - - let corpus = match adapter.as_str() { - "agda" => corpus::agda::ingest(&root) - .with_context(|| format!("agda ingest of {}", root.display()))?, - "coq" => corpus::coq::ingest(&root) - .with_context(|| format!("coq ingest of {}", root.display()))?, - "lean" | "lean4" => corpus::lean::ingest(&root) - .with_context(|| format!("lean ingest of {}", root.display()))?, - "idris2" | "idris" => corpus::idris2::ingest(&root) - .with_context(|| format!("idris2 ingest of {}", root.display()))?, - "acl2_books" => corpus::acl2_books::ingest(&root) - .with_context(|| format!("acl2_books ingest of {}", root.display()))?, - "dafny" => corpus::dafny::ingest(&root) - .with_context(|| format!("dafny ingest of {}", root.display()))?, - "spark_ada" | "spark" | "ada" => corpus::spark_ada::ingest(&root) - .with_context(|| format!("spark_ada ingest of {}", root.display()))?, - "creusot" | "rust_creusot" => corpus::creusot::ingest(&root) - .with_context(|| format!("creusot ingest of {}", root.display()))?, - "hyperpolymath_estate" | "estate" => corpus::profile::ingest(&root) - .with_context(|| format!("estate profile ingest of {}", root.display()))?, - "fstar" => corpus::fstar::ingest(&root) - .with_context(|| format!("fstar ingest of {}", root.display()))?, - "hol4" => corpus::hol4::ingest(&root) - .with_context(|| format!("hol4 ingest of {}", root.display()))?, - "hol_light" => corpus::hol_light::ingest(&root) - .with_context(|| format!("hol_light ingest of {}", root.display()))?, - "isabelle" => corpus::isabelle::ingest(&root) - .with_context(|| format!("isabelle ingest of {}", root.display()))?, - "metamath" => corpus::metamath::ingest(&root) - .with_context(|| format!("metamath ingest of {}", root.display()))?, - "minif2f" => corpus::minif2f::ingest(&root) - .with_context(|| format!("minif2f ingest of {}", root.display()))?, - "mizar" => corpus::mizar::ingest(&root) - .with_context(|| format!("mizar ingest of {}", root.display()))?, - "proofnet" => corpus::proofnet::ingest(&root) - .with_context(|| format!("proofnet ingest of {}", root.display()))?, - "smtlib" => corpus::smtlib::ingest(&root) - .with_context(|| format!("smtlib ingest of {}", root.display()))?, - "tptp" => corpus::tptp::ingest(&root) - .with_context(|| format!("tptp ingest of {}", root.display()))?, - "why3" => corpus::why3::ingest(&root) - .with_context(|| format!("why3 ingest of {}", root.display()))?, - other => { - return Err(anyhow::anyhow!( - "Unknown corpus adapter '{}' (supported: agda, coq, lean, idris2, acl2_books, creusot, dafny, fstar, hol4, hol_light, isabelle, metamath, minif2f, mizar, proofnet, smtlib, spark_ada, tptp, why3, hyperpolymath_estate)", - other - )) - }, - }; - corpus.save_json(&out)?; - let stats = corpus.stats(); - formatter.info(&format!( - "ingested {} module(s), {} entry/-ies ({} fn, {} data, {} record, {} postulate); {} entry/-ies have hazards; index: {}", - stats.modules, - stats.entries, - stats.functions, - stats.data, - stats.records, - stats.postulates, - stats.with_hazards, - out.display() - ))?; - }, - CorpusOp::Query { - pattern, - index, - deps, - reverse_deps, - reverse_closure, - } => { - let path = match index { - Some(p) => p, - None => { - let basename = std::env::current_dir()? - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| "project".to_string()); - default_index(&basename) - }, - }; - let corpus = Corpus::load_json(&path) - .with_context(|| format!("load corpus index {}", path.display()))?; - let hits = corpus.find(&pattern); - if hits.is_empty() { - formatter.info(&format!("no matches for '{}'", pattern))?; - return Ok(()); - } - for entry in &hits { - let module = corpus - .modules - .get(entry.module_idx) - .map(|m| m.name.as_str()) - .unwrap_or("?"); - println!( - "{} [{:?}] {}:{}", - entry.qualified, entry.kind, module, entry.line - ); - println!(" : {}", entry.statement); - if entry.axiom_usage.any() { - println!(" ⚠ hazards: {:?}", entry.axiom_usage); - } - if !entry.dependencies.is_empty() { - let mut deps_list = entry.dependencies.clone(); - deps_list.sort(); - println!(" deps ({}): {}", deps_list.len(), deps_list.join(", ")); - } - } - if deps && hits.len() == 1 { - let closure = corpus.closure(&hits[0].qualified); - println!("\ntransitive closure ({} entries):", closure.len()); - let mut names: Vec<&str> = closure.iter().map(|e| e.qualified.as_str()).collect(); - names.sort(); - for n in names { - println!(" {}", n); - } - } - if reverse_deps && hits.len() == 1 { - let rev = corpus.reverse_deps(&hits[0].qualified); - println!( - "\ndirect reverse-deps of {} ({} entries):", - hits[0].qualified, - rev.len() - ); - let mut names: Vec<&str> = rev.iter().map(|e| e.qualified.as_str()).collect(); - names.sort(); - for n in names { - println!(" {}", n); - } - } - if reverse_closure && hits.len() == 1 { - let closure = corpus.reverse_closure(&hits[0].qualified); - println!( - "\nreverse closure (impact set) of {} ({} entries):", - hits[0].qualified, - closure.len() - ); - let mut names: Vec<&str> = closure.iter().map(|e| e.qualified.as_str()).collect(); - names.sort(); - for n in names { - println!(" {}", n); - } - } - }, - CorpusOp::Crossquery { - class, - synonyms_dir, - provers_only, - } => { - use echidna::suggest::synonyms::{ - load_all, load_cross_prover_dicts, SynonymEntry, SynonymTable, - }; - let dir = synonyms_dir.unwrap_or_else(|| PathBuf::from("data/synonyms")); - let tables = load_all(&dir) - .with_context(|| format!("load synonym tables from {}", dir.display()))?; - // Prints the hits in `table` for `class` and returns how many. - // A row whose canonical name *is* `class` also counts: the MSC - // dictionary is keyed by code, so `crossquery 68Q60` should find - // 68Q60 even though its semantic class is `program-specification`. - let print_hits = |table: &SynonymTable, label: std::fmt::Arguments<'_>| -> usize { - let mut hits: Vec<&SynonymEntry> = table.by_semantic_class(&class); - if let Some(entry) = table.entries.iter().find(|e| e.canonical == class) { - if !hits.iter().any(|h| h.canonical == entry.canonical) { - hits.push(entry); - } - } - if hits.is_empty() { - return 0; - } - println!("{label}:"); - for entry in &hits { - let aliases = if entry.aliases.is_empty() { - String::new() - } else { - format!(" (aliases: {})", entry.aliases.join(", ")) - }; - println!(" {}{}", entry.canonical, aliases); - } - hits.len() - }; - let mut total = 0usize; - let mut sources = 0usize; - for prover in [ - ProverKind::Agda, - ProverKind::Coq, - ProverKind::Lean, - ProverKind::Idris2, - ProverKind::Isabelle, - ProverKind::Metamath, - ProverKind::Mizar, - ProverKind::HOL4, - ProverKind::HOLLight, - ProverKind::Dafny, - ProverKind::Why3, - ProverKind::FStar, - ProverKind::ACL2, - ] { - if let Some(table) = tables.get(&prover) { - let hits = print_hits(table, format_args!("{:?}", prover)); - total += hits; - if hits > 0 { - sources += 1; - } - } - } - if !provers_only { - let dicts = load_cross_prover_dicts(&dir).with_context(|| { - format!("load cross-prover dictionaries from {}", dir.display()) - })?; - for (name, table) in dicts.tables() { - let hits = print_hits(table, format_args!("dictionary {}", name)); - total += hits; - if hits > 0 { - sources += 1; - } - } - } - if total == 0 { - formatter.info(&format!("no entries for semantic class '{}'", class))?; - } else { - formatter.info(&format!( - "{} entries across {} table(s) for semantic class '{}'", - total, sources, class - ))?; - } - }, - - CorpusOp::Near { query, index, top } => { - let path = match index { - Some(p) => p, - None => { - let basename = std::env::current_dir()? - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| "project".to_string()); - default_index(&basename) - }, - }; - let corpus = Corpus::load_json(&path) - .with_context(|| format!("load corpus index {}", path.display()))?; - let q_vec = corpus.embed_query(&query); - let nn = corpus.nearest_neighbours(&q_vec, top); - if nn.is_empty() { - formatter.info(&format!( - "no entries near '{}' (corpus has {} entries)", - query, - corpus.entries.len() - ))?; - return Ok(()); - } - for (idx, sim) in &nn { - let entry = &corpus.entries[*idx]; - println!("{:.3} {} [{:?}]", sim, entry.qualified, entry.kind); - println!(" : {}", entry.statement); - } - }, - - CorpusOp::Identity { - pattern, - index, - skeleton, - cross_only, - } => { - let path = match index { - Some(p) => p, - None => { - let basename = std::env::current_dir()? - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| "project".to_string()); - default_index(&basename) - }, - }; - let corpus = Corpus::load_json(&path) - .with_context(|| format!("load corpus index {}", path.display()))?; - let needle = pattern.to_lowercase(); - let everything = needle == "*"; - - // Group by structural key, keeping the key itself for display. - let mut groups: std::collections::BTreeMap< - String, - ( - echidna::corpus::canonical::StructuralKey, - Vec<(String, String)>, - ), - > = std::collections::BTreeMap::new(); - // A key the canonicaliser did not derive from the whole statement - // is a shared prefix, not an identity (docs/CORPUS.adoc). - let mut skipped_incomplete = 0usize; - for entry in &corpus.entries { - let matches = everything - || entry.qualified.to_lowercase().contains(&needle) - || entry.name.to_lowercase().contains(&needle); - if !matches { - continue; - } - let key = echidna::corpus::canonical::structural_key(&entry.statement); - if !key.complete { - skipped_incomplete += 1; - continue; - } - let slot = groups - .entry(key.key.clone()) - .or_insert_with(|| (key, Vec::new())); - slot.1 - .push((corpus.adapter.clone(), entry.qualified.clone())); - } - - if skipped_incomplete > 0 { - formatter.info(&format!( - "{skipped_incomplete} matching declaration(s) skipped: the canonicaliser \ - did not read their statement completely, so they carry no identity" - ))?; - } - if groups.is_empty() { - // When every match was skipped, the name is present but carries - // no identity; saying "no declarations" would mislead. - let what = if skipped_incomplete > 0 { - "no completely read declarations" - } else { - "no declarations" - }; - formatter.info(&format!("{what} matching '{}'", pattern))?; - return Ok(()); - } - - let mut shown = 0usize; - for (key, (structural, members)) in &groups { - let adapters: std::collections::BTreeSet<&str> = - members.iter().map(|(a, _)| a.as_str()).collect(); - if cross_only && adapters.len() < 2 { - continue; - } - shown += 1; - println!( - "{} [{}] {} declaration(s) across {} adapter(s)", - &key[..16.min(key.len())], - structural.tier.as_str(), - members.len(), - adapters.len() - ); - println!(" type: {}", structural.shape); - if skeleton { - println!(" skeleton: {}", structural.skeleton); - } - for (adapter, qualified) in members { - println!(" {} {}", adapter, qualified); - } - } - formatter.info(&format!( - "{} key(s) over {} declaration(s) in {}", - shown, - groups.values().map(|(_, m)| m.len()).sum::(), - path.display() - ))?; - }, - - CorpusOp::ExportOctads { index, out } => { - let corpus = Corpus::load_json(&index) - .with_context(|| format!("load corpus index {}", index.display()))?; - let timestamp = chrono::Utc::now().to_rfc3339(); - corpus.save_octads_jsonl(&out, ×tamp)?; - formatter.info(&format!( - "exported {} octad(s) → {}", - corpus.entries.len(), - out.display() - ))?; - }, - - CorpusOp::ImportOctads { octads, out } => { - let corpus = Corpus::load_octads_jsonl(&octads) - .with_context(|| format!("load octads {}", octads.display()))?; - corpus.save_json(&out)?; - formatter.info(&format!( - "imported {} octad(s) ({} module(s)) → {}", - corpus.entries.len(), - corpus.modules.len(), - out.display() - ))?; - }, - - CorpusOp::Stats { index } => { - let path = match index { - Some(p) => p, - None => { - let basename = std::env::current_dir()? - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| "project".to_string()); - default_index(&basename) - }, - }; - let corpus = Corpus::load_json(&path) - .with_context(|| format!("load corpus index {}", path.display()))?; - let stats = corpus.stats(); - println!( - "modules: {}\nentries: {}\n functions: {}\n data: {}\n records: {}\n postulates: {}\nwith hazards: {}\nwith deps: {}", - stats.modules, - stats.entries, - stats.functions, - stats.data, - stats.records, - stats.postulates, - stats.with_hazards, - stats.with_deps - ); - }, - } - - Ok(()) -} - -/// Design-search command — simulated annealing over candidate -/// axiomatisations, single-chain or parallel swarm. The first -/// concrete problem is `brouwer-leq`, targeting echo-types' Phase 1.3 -/// `_≤_` redesign. -async fn design_command(op: DesignOp, formatter: &OutputFormatter) -> Result<()> { - use echidna::agent::swarm::{run_swarm, SwarmConfig}; - use echidna::learning::design_search::{anneal, brouwer, AnnealConfig, DesignProblem}; - use std::sync::Arc; - - match op { - DesignOp::Search { - problem, - iterations, - restarts, - temp, - cooling, - seed, - top, - } => { - let cfg = AnnealConfig { - max_iterations: iterations, - initial_temp: temp, - cooling, - seed, - restarts, - }; - - match problem.as_str() { - "brouwer-leq" => { - let p = brouwer::BrouwerLeqProblem::default(); - let result = anneal(&p, &cfg); - formatter.info(&format!( - "annealer: {} steps, {} accepted ({:.1}%); best energy {:?}", - result.steps, - result.accepted, - 100.0 * result.accepted as f64 / result.steps.max(1) as f64, - result.best_energy - ))?; - println!( - "\nbest design (energy = {:?}):\n {}\n", - result.best_energy, - p.describe(&result.best) - ); - println!("top {} distinct candidates:", top.min(result.topk.len())); - for (i, (state, e)) in result.topk.iter().take(top).enumerate() { - println!(" {:2}. energy {:?} {}", i + 1, e, p.describe(state)); - } - println!( - "\nlegend: energy = [mono-blockers, K-hazards, ctor-count, style-pref]" - ); - println!( - " style-pref: 0 = recursive, 1 = data (recursive preferred when tied)" - ); - }, - "buchholz-rank" => { - use echidna::learning::buchholz_rank::{ - render_rank_mono_skeleton, BuchholzRankProblem, - }; - let p = BuchholzRankProblem::with_baseline_corpus(); - let result = anneal(&p, &cfg); - formatter.info(&format!( - "annealer: {} steps, {} accepted ({:.1}%); best energy {:?}", - result.steps, - result.accepted, - 100.0 * result.accepted as f64 / result.steps.max(1) as f64, - result.best_energy - ))?; - println!( - "\nbest rank shape (energy = {:?}):\n{}\n", - result.best_energy, - p.describe(&result.best) - ); - println!("top {} distinct candidates:", top.min(result.topk.len())); - for (i, (state, e)) in result.topk.iter().take(top).enumerate() { - println!(" {:2}. energy {:?}", i + 1, e); - for line in p.describe(state).lines() { - println!(" {}", line); - } - } - println!( - "\nlegend: energy = [downstream-blockers, capability-gaps, structural-cost]" - ); - println!( - " downstream-blockers: # of new lemmas to prove (Phase-1.3 / 2.2)" - ); - println!( - " capability-gaps: # of primitives missing from current corpus" - ); - println!("\n--- Agda skeleton for the recommended shape ---"); - println!("{}", render_rank_mono_skeleton(&result.best)); - }, - other => { - return Err(anyhow::anyhow!( - "Unknown design problem '{}' (available: brouwer-leq, buchholz-rank)", - other - )) - }, - } - }, - - DesignOp::Swarm { - problem, - agents, - iterations, - temp, - cooling, - seed, - broadcast_every, - adopt_threshold, - } => { - let anneal_cfg = AnnealConfig { - max_iterations: iterations, - initial_temp: temp, - cooling, - seed, - restarts: 1, // each swarm agent is its own "restart" - }; - let swarm_cfg = SwarmConfig { - agents, - anneal: anneal_cfg, - broadcast_every, - adopt_threshold, - }; - - match problem.as_str() { - "brouwer-leq" => { - let p = Arc::new(brouwer::BrouwerLeqProblem::default()); - let p_describe = brouwer::BrouwerLeqProblem::default(); - let result = run_swarm(p, swarm_cfg).await; - formatter.info(&format!( - "swarm: {} agents, {} adoptions; best energy {:?}", - result.per_agent.len(), - result.adoptions, - result.best_energy - ))?; - println!( - "\nbest design (energy = {:?}):\n {}\n", - result.best_energy, - p_describe.describe(&result.best) - ); - println!("per-agent reports:"); - for r in &result.per_agent { - println!( - " agent {:2} (seed {:#x}): {} steps, {} accepted, {} adoptions; best {:?}", - r.agent_id, - r.seed, - r.steps, - r.accepted, - r.adopted_count, - r.local_best_energy, - ); - } - }, - "buchholz-rank" => { - use echidna::learning::buchholz_rank::{ - render_rank_mono_skeleton, BuchholzRankProblem, - }; - let p = Arc::new(BuchholzRankProblem::with_baseline_corpus()); - let p_describe = BuchholzRankProblem::with_baseline_corpus(); - let result = run_swarm(p, swarm_cfg).await; - formatter.info(&format!( - "swarm: {} agents, {} adoptions; best energy {:?}", - result.per_agent.len(), - result.adoptions, - result.best_energy - ))?; - println!( - "\nbest rank shape (energy = {:?}):\n{}\n", - result.best_energy, - p_describe.describe(&result.best) - ); - println!("per-agent reports:"); - for r in &result.per_agent { - println!( - " agent {:2} (seed {:#x}): {} steps, {} accepted, {} adoptions; best {:?}", - r.agent_id, - r.seed, - r.steps, - r.accepted, - r.adopted_count, - r.local_best_energy, - ); - } - println!("\n--- Agda skeleton for the recommended shape ---"); - println!("{}", render_rank_mono_skeleton(&result.best)); - }, - other => { - return Err(anyhow::anyhow!( - "Unknown design problem '{}' (available: brouwer-leq, buchholz-rank)", - other - )) - }, - } - }, - } - Ok(()) -} - // We need to add futures crate for the executor diff --git a/src/rust/server.rs b/src/rust/server.rs deleted file mode 100644 index df6b4b0d..00000000 --- a/src/rust/server.rs +++ /dev/null @@ -1,1637 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -// SPDX-License-Identifier: MPL-2.0 - -//! HTTP API server for ECHIDNA -//! -//! Provides REST API and WebSocket endpoints for theorem proving - -use anyhow::Result; -use axum::{ - extract::{Path, Query, State}, - http::StatusCode, - response::{IntoResponse, Response}, - routing::{get, post}, - Json, Router, -}; -use colored::Colorize; -use echidna::agent::meta_controller::{MetaController, Plan}; -use echidna::agent::AgenticGoal; -use echidna::core::{Goal, ProofState, Tactic, TacticResult, Term}; -use echidna::diagnostics::HealthStatus; -use echidna::dispatch::ProverDispatcher; -use echidna::provers::ProverOutcome; -use echidna::verification::StatisticsTracker; -use echidna::{ProverBackend, ProverConfig, ProverKind}; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use std::collections::HashMap; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::{Mutex, RwLock}; -use tower_http::cors::CorsLayer; -use tracing::{debug, info, instrument}; - -/// Application state shared across handlers -#[derive(Clone)] -struct AppState { - /// Active proof sessions (session_id -> ProofSession) - sessions: Arc>>>>, - /// HTTP client for Julia ML API - ml_client: Client, - /// Julia ML API base URL - ml_api_url: String, - /// MetaController — shared (prover × coprocessor) planner with persistent - /// StatisticsTracker. Survives request boundaries so the Bayesian routing - /// state accumulates across the server's lifetime. All handlers that - /// perform goal-aware dispatch share this single instance. - meta_controller: Arc, - /// Shared proof-outcome statistics used by MetaController for Bayesian - /// routing and exported to Julia for GNN online learning. - stats: Arc>, - /// Prover dispatcher for trust-hardening pipeline and health monitoring - dispatcher: Arc, -} - -/// A proof session -struct ProofSession { - prover: Box, - state: Option, - history: Vec, -} - -const DEFAULT_ML_API_URL: &str = "http://127.0.0.1:8090"; - -/// Start the HTTP server -pub async fn start_server(port: u16, host: String, enable_cors: bool) -> Result<()> { - // Create HTTP client for Julia ML API - let ml_client = Client::builder().timeout(Duration::from_secs(10)).build()?; - - let ml_api_url = - std::env::var("ECHIDNA_ML_API_URL").unwrap_or_else(|_| DEFAULT_ML_API_URL.to_string()); - - // Security: Validate that the ML API URL is either local or explicitly allowed - if !ml_api_url.starts_with("http://127.0.0.1") && !ml_api_url.starts_with("http://localhost") { - info!("⚠ Untrusted ML API URL detected: {}. Baseline security policy requires local or trusted endpoint.", ml_api_url); - } - - // Test Julia ML connection - match ml_client.get(format!("{}/health", ml_api_url)).send().await { - Ok(resp) if resp.status().is_success() => { - info!("✓ Connected to Julia ML API at {}", ml_api_url); - }, - _ => { - info!( - "⚠ Julia ML API not available at {} (will use fallback)", - ml_api_url - ); - }, - } - - // Shared stats tracker — MetaController uses it for Bayesian routing; - // the background GNN sync task pushes snapshots to Julia for online learning. - let stats = Arc::new(RwLock::new(StatisticsTracker::new())); - let meta_controller = Arc::new(MetaController::new().with_stats(stats.clone())); - - // Create prover dispatcher with default configuration. - // When compiled with --features verisim, attach the VeriSimDB writer so - // every HTTP proof attempt (including parse failures) feeds the S4 learning - // loop. The writer runs fire-and-forget; a VeriSimDB outage never blocks - // or alters a dispatch result. - let dispatcher = { - let d = ProverDispatcher::new(); - #[cfg(feature = "verisim")] - let d = { - let url = std::env::var("VERISIM_URL") - .unwrap_or_else(|_| "http://localhost:8080".to_string()); - info!("✓ VeriSim writer attached at {}", url); - d.with_verisim_writer(&url) - }; - Arc::new(d) - }; - info!("✓ Initialized prover dispatcher for trust-hardening pipeline"); - - let state = AppState { - sessions: Arc::new(RwLock::new(HashMap::new())), - ml_client, - ml_api_url, - meta_controller, - stats, - dispatcher, - }; - - // GNN training sync — background task that pushes accumulated proof-outcome - // stats to Julia's /training/update endpoint every 60 seconds. - // Only fires when >= 10 new outcomes have accumulated since the last push, - // so idle servers incur no traffic. Errors are logged at debug level and - // do not affect the main server loop (fire-and-forget). - { - let sync_stats = state.stats.clone(); - let sync_client = state.ml_client.clone(); - let sync_url = state.ml_api_url.clone(); - tokio::spawn(async move { - let mut last_push_count: u64 = 0; - let mut interval = tokio::time::interval(Duration::from_secs(60)); - interval.tick().await; // consume the immediate first tick; wait 60s before first push - loop { - interval.tick().await; - let (total, records) = { - let guard = sync_stats.read().await; - (guard.total_attempts(), guard.export_records()) - }; - if total < last_push_count + 10 || records.is_empty() { - continue; - } - last_push_count = total; - let url = format!("{}/training/update", sync_url); - let payload = json!({ "records": records }); - match sync_client.post(&url).json(&payload).send().await { - Ok(resp) if resp.status().is_success() => { - info!( - "GNN training sync: {} records pushed to Julia", - records.len() - ); - }, - Ok(resp) => { - debug!("GNN training sync: Julia returned {}", resp.status()); - }, - Err(e) => { - debug!("GNN training sync: Julia unavailable ({})", e); - }, - } - } - }); - } - - // Build router - let mut app = Router::new() - // Groove discovery endpoint — returns capability manifest for service mesh. - // See Groove.idr echidnaManifest for the canonical ABI definition. - .route("/.well-known/groove", get(groove_manifest)) - // REST API endpoints - .route("/api/health", get(health_check)) - .route("/api/diagnostics/health", get(diagnostics_health_handler)) - .route("/api/provers", get(list_provers)) - .route("/api/prove", post(prove_handler)) - .route("/api/verify", post(verify_handler)) - .route("/api/verify_parallel", post(verify_parallel_handler)) - .route("/api/verify_raw", post(verify_raw_handler)) - .route("/api/suggest", post(suggest_handler)) - .route("/api/search", get(search_handler)) - .route("/api/session/create", post(create_session)) - .route("/api/session/{id}/state", get(get_session_state)) - .route("/api/session/{id}/apply", post(apply_tactic_handler)) - .route("/api/session/{id}/tree", get(get_proof_tree)) - // Additional UI-specific endpoints - .route("/api/agent/plan", post(agent_plan_handler)) - .route("/api/aspect-tags", get(get_aspect_tags)) - .route("/api/tactics/suggest", post(suggest_tactics_ui)) - .route("/api/theorems/search", get(search_theorems_ui)) - // WebSocket endpoint (disabled - requires axum ws feature) - // .route("/ws/interactive", get(websocket_handler)) - .with_state(state); - - // Add CORS if enabled - if enable_cors { - app = app.layer(CorsLayer::permissive()); - } - - // Bind server - let addr: SocketAddr = format!("{}:{}", host, port).parse()?; - - println!( - "{}", - "╔═══════════════════════════════════════════════════════════╗".cyan() - ); - println!( - "{}", - "║ ECHIDNA HTTP API Server ║" - .cyan() - .bold() - ); - println!( - "{}", - "╚═══════════════════════════════════════════════════════════╝".cyan() - ); - println!(); - println!( - "Server listening on: {}", - format!("https://{}", addr).green().bold() - ); - println!(); - println!("{}", "Endpoints:".yellow().bold()); - println!(" GET /.well-known/groove - Groove discovery manifest"); - println!(" GET /api/health - Health check"); - println!(" GET /api/provers - List available provers"); - println!(" POST /api/prove - Prove a theorem"); - println!(" POST /api/verify - Verify a proof"); - println!(" POST /api/suggest - Get tactic suggestions"); - println!(" GET /api/search?q= - Search theorems"); - println!(" POST /api/session/create - Create proof session"); - println!(" GET /api/session/{{id}}/state - Get session state"); - println!(" POST /api/session/{{id}}/apply - Apply tactic to session"); - println!(" WS /ws/interactive - WebSocket live proof session"); - println!(); - println!("Press Ctrl+C to stop the server"); - println!(); - - // Start server - let listener = tokio::net::TcpListener::bind(addr).await?; - axum::serve(listener, app).await?; - - Ok(()) -} - -// ========== Agent Planning ========== - -/// Request to `POST /api/agent/plan`. -/// -/// Asks the MetaController to plan a proof attempt: which prover to use, -/// which coprocessor preconditions to run first, and the Bayesian-estimated -/// timeout. The response is a `Plan` that callers can inspect before -/// deciding whether to submit a full `/api/verify` or `/api/prove` request. -/// -/// `aspects` drive the coprocessor routing (e.g. `"arithmetic.factorisation"` -/// triggers a Math precondition; `"algebra.groebner.basis"` triggers Singular). -/// `preferred_prover` and `candidates` are both optional — omitting both lets -/// the Pareto ranker pick freely from Z3 as the default. -#[derive(Debug, Deserialize)] -struct AgentPlanRequest { - /// Goal aspect tags (e.g. `["arithmetic.factorisation", "crypto.hash.sha256"]`). - #[serde(default)] - aspects: Vec, - - /// Client's preferred prover, if any. Overridden by Pareto ranking when - /// `candidates` is non-empty. - preferred_prover: Option, - - /// Candidate provers to rank. When empty, `preferred_prover` (or Z3) - /// is selected directly without Pareto scoring. - #[serde(default)] - candidates: Vec, - - /// Default timeout hint (ms) used when no historical data is available. - #[serde(default = "default_timeout_ms")] - default_timeout_ms: u64, -} - -fn default_timeout_ms() -> u64 { - 30_000 -} - -/// `POST /api/agent/plan` — return a MetaController plan for the given goal aspects. -/// -/// Does not execute any provers or coprocessors; it only plans. Callers use -/// the returned `coprocessor_preconditions` to understand what computational -/// preparation the system recommends before submitting the proof content. -async fn agent_plan_handler( - State(state): State, - Json(req): Json, -) -> impl IntoResponse { - // Build a minimal AgenticGoal from the request metadata. The goal target - // is a placeholder (`Term::Const("?")`) because planning doesn't inspect - // the proof content — only the aspects and prover preferences matter here. - use echidna::agent::Priority; - let goal = AgenticGoal { - goal: Goal { - id: echidna::ids::new_record_id().to_string(), - target: Term::Const("?".to_string()), - hypotheses: vec![], - }, - priority: Priority::Medium, - attempts: 0, - max_attempts: 3, - preferred_prover: req.preferred_prover, - aspects: req.aspects, - parent: None, - }; - - let plan: Plan = if req.candidates.is_empty() { - state.meta_controller.plan(&goal).await - } else { - use echidna::verification::confidence::TrustLevel; - state - .meta_controller - .plan_with_pareto( - &goal, - &req.candidates, - req.default_timeout_ms, - TrustLevel::Level2, - ) - .await - }; - - Json(json!({ - "prover": format!("{:?}", plan.prover), - "estimated_timeout_ms": plan.estimated_timeout_ms, - "rationale": plan.rationale, - "coprocessor_preconditions": plan.coprocessor_preconditions - .iter() - .map(|p| json!({ - "kind": format!("{:?}", p.kind), - "aspect": p.aspect, - })) - .collect::>(), - })) -} - -// ========== Groove Discovery ========== - -/// Groove capability manifest for ECHIDNA. -/// -/// Returns the service's capabilities in the standard Groove format so that -/// Gossamer, PanLL, and other groove-aware systems can discover ECHIDNA by -/// probing GET /.well-known/groove. See Groove.idr echidnaManifest for the -/// canonical ABI definition. -/// -/// Capabilities offered: -/// - theorem-proving: Multi-backend theorem proving (48 provers) -/// -/// Capabilities consumed: -/// - octad-storage (VeriSimDB): Persist proof artifacts as octad entities -/// - scanning (Hypatia): Trigger re-verification after codebase changes -async fn groove_manifest() -> Json { - let core_count = echidna::ProverKind::all_core().len(); - let advertised_count = echidna::ProverKind::all().len(); - Json(json!({ - "groove_version": "1", - "service_id": "echidna", - "service_version": env!("CARGO_PKG_VERSION"), - "capabilities": { - "theorem_proving": { - "type": "theorem-proving", - "description": format!( - "Neurosymbolic theorem proving — {} core / {} advertised prover backends", - core_count, advertised_count - ), - "protocol": "http", - "endpoint": "/api/prove", - "requires_auth": false, - "panel_compatible": true - } - }, - "consumes": ["octad-storage", "scanning"], - "endpoints": { - "provers": "/api/provers", - "prove": "/api/prove", - "verify": "/api/verify", - "health": "/api/health" - }, - "health": "/api/health", - "applicability": ["individual", "team"] - })) -} - -// ========== REST API Handlers ========== - -/// Health check endpoint -async fn health_check() -> Json { - Json(HealthResponse { - status: "ok".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - }) -} - -/// Detailed diagnostics health endpoint -/// Returns full HealthStatus snapshot from dispatcher with prover metrics and degradation mode -async fn diagnostics_health_handler( - State(app_state): State, -) -> Result, AppError> { - let health = app_state - .dispatcher - .health_status() - .map_err(|error| AppError::InternalError(error.to_string()))?; - Ok(Json(health)) -} - -/// List available provers -async fn list_provers() -> Json { - let provers = ProverKind::all_core() - .into_iter() - .map(|prover| ProverInfo { - name: format!("{:?}", prover), - tier: prover.tier(), - complexity: prover.complexity(), - }) - .collect(); - - Json(ProversResponse { provers }) -} - -/// Prove theorem endpoint -async fn prove_handler( - State(app_state): State, - Json(req): Json, -) -> Result, AppError> { - info!("Prove request for prover: {:?}", req.prover); - - // Route through the full dispatch pipeline when the verisim feature is - // active. This fires the VeriSimDB writer for every HTTP proof (parse - // failures and final results), closing the S4 learning loop. The writer - // runs fire-and-forget — a VeriSimDB outage never blocks or alters a - // result. Response shape (success/goals/message) is preserved. - #[cfg(feature = "verisim")] - { - let result = app_state - .dispatcher - .verify_proof(req.prover, &req.content) - .await - .map_err(|e| AppError::VerificationError(e.to_string()))?; - return Ok(Json(ProveResponse { - success: result.verified, - goals: result.goals_remaining, - message: result.message, - })); - } - - // Non-verisim path: direct factory + prover call (existing behaviour). - #[cfg(not(feature = "verisim"))] - { - // Create prover - let config = ProverConfig { - timeout: req.timeout.unwrap_or(300), - neural_enabled: req.neural.unwrap_or(true), - ..ProverConfig::default() - }; - - let prover = echidna::provers::ProverFactory::create(req.prover, config) - .map_err(|e| AppError::InternalError(e.to_string()))?; - - // Parse proof - let state = prover - .parse_string(&req.content) - .await - .map_err(|e| AppError::ParseError(e.to_string()))?; - - // Fail-fast on empty parse results (fixes false-positive: unrecognised - // content produced an empty ProofState that re-exported to an empty file - // which the backend prover then happily accepted). - if is_empty_state(&state) && !req.content.trim().is_empty() { - let outcome = ProverOutcome::InvalidInput { - reason: "Parse produced no goals, theorems, definitions, or axioms — \ - content not recognised by the selected prover backend" - .to_string(), - location: None, - }; - app_state - .dispatcher - .record_prover_result(req.prover, &outcome, 0) - .map_err(|error| AppError::InternalError(error.to_string()))?; - return Ok(Json(ProveResponse { - success: false, - goals: 0, - message: "Parse produced no goals, theorems, definitions, or axioms — \ - content not recognised by the selected prover backend" - .to_string(), - })); - } - - // Verify proof and record health metrics - let verify_start = Instant::now(); - let valid = prover - .verify_proof(&state) - .await - .map_err(|e| AppError::VerificationError(e.to_string()))?; - let verify_elapsed_ms = verify_start.elapsed().as_millis() as u64; - - let outcome = if valid { - ProverOutcome::Proved { - elapsed_ms: verify_elapsed_ms, - } - } else { - ProverOutcome::NoProofFound { - elapsed_ms: verify_elapsed_ms, - reason: Some("Proof verification failed".to_string()), - } - }; - app_state - .dispatcher - .record_prover_result(req.prover, &outcome, verify_elapsed_ms) - .map_err(|error| AppError::InternalError(error.to_string()))?; - - Ok(Json(ProveResponse { - success: valid, - goals: state.goals.len(), - message: if valid { - "Proof verified successfully".to_string() - } else { - "Proof verification failed".to_string() - }, - })) - } -} - -/// Verify proof endpoint -async fn verify_handler(Json(req): Json) -> Result, AppError> { - info!("Verify request for prover: {:?}", req.prover); - - // SMT query mode: when users send raw SMT-LIB with (check-sat) to Z3/CVC5, - // run passthrough evaluation instead of proof-obligation negation semantics. - if should_passthrough_smt_eval(&req) { - let Json(raw) = verify_raw_handler(Json(req.clone())).await?; - let smt_status = extract_smt_status(&raw.stdout); - - // A completed SMT query is not necessarily a discharged obligation. - // Only unsat closes a query interpreted as the negated obligation. - let verified = raw.valid && discharged_smt_queries(&req.content, &raw.stdout); - let goals_remaining = if verified { 0 } else { 1 }; - let outcome_str = if verified { "PROVED" } else { "NO_PROOF_FOUND" }; - return Ok(Json(VerifyResponse { - valid: verified, - outcome: outcome_str.to_string(), - goals_remaining, - tactics_used: 0, - mode: Some("smt-query".to_string()), - smt_status, - })); - } - - // Create prover - let config = ProverConfig::default(); - let prover = echidna::provers::ProverFactory::create(req.prover, config) - .map_err(|e| AppError::InternalError(e.to_string()))?; - - // Parse proof — parse errors surface as INVALID_INPUT in the outcome. - // We intentionally don't forward the raw error message to the response - // body (it may leak internal paths or prover internals). - let state = match prover.parse_string(&req.content).await { - Ok(s) => s, - Err(_) => { - return Ok(Json(VerifyResponse { - valid: false, - outcome: "INVALID_INPUT".to_string(), - goals_remaining: 0, - tactics_used: 0, - mode: None, - smt_status: None, - })); - }, - }; - - // Fail-fast on empty parse results (see prove_handler comment). - if is_empty_state(&state) && !req.content.trim().is_empty() { - return Ok(Json(VerifyResponse { - valid: false, - outcome: "INVALID_INPUT".to_string(), - goals_remaining: 0, - tactics_used: 0, - mode: None, - smt_status: None, - })); - } - - // NOTE: a richer `.check()` call returning a `ProverOutcome` taxonomy - // lived here until 2026-04-17 but the typed-outcome module was never - // committed (see z3.rs note). Reverted to `verify_proof()` so the HTTP - // API is no worse than it was before the dropped refactor; the outcome - // string is a two-valued approximation of the planned taxonomy. - // Tracked in AI-WORK-todo.md phase-2 followups. - let valid = prover - .verify_proof(&state) - .await - .map_err(|e| AppError::VerificationError(e.to_string()))?; - - Ok(Json(VerifyResponse { - valid, - outcome: if valid { - "PROVED".to_string() - } else { - "NO_PROOF_FOUND".to_string() - }, - goals_remaining: if valid { 0 } else { state.goals.len() }, - tactics_used: state.proof_script.len(), - mode: None, - smt_status: None, - })) -} - -/// `/api/verify_parallel` — L2 Chapel parallel dispatch. -/// -/// Fans out to multiple prover backends simultaneously (when the `chapel` -/// Cargo feature is compiled in and the Chapel runtime is available). -/// Returns the first successful result through the standard trust pipeline. -/// Falls back to sequential `select_prover` heuristic when Chapel is -/// unavailable. Callers that want a specific prover should use `/api/verify`. -async fn verify_parallel_handler( - Json(req): Json, -) -> Result, AppError> { - let content = req - .get("content") - .and_then(|v| v.as_str()) - .ok_or_else(|| AppError::BadRequest("Missing 'content' field".to_string()))?; - - info!("verify_parallel request ({} bytes)", content.len()); - - let dispatcher = ProverDispatcher::new(); - let result = dispatcher - .verify_proof_parallel(content) - .await - .map_err(|e| AppError::VerificationError(e.to_string()))?; - - Ok(Json(serde_json::json!({ - "valid": result.verified, - "outcome": result.outcome.status_str(), - "provers_used": result.provers_used, - "trust_level": result.trust_level as u8, - "proof_time_ms": result.proof_time_ms, - "goals_remaining": result.goals_remaining, - "cross_checked": result.cross_checked, - "message": result.message, - }))) -} - -/// `/api/verify_raw` — writes the original `content` to a temp file with -/// the right extension for the prover, then invokes the backend binary -/// directly. Bypasses `parse_string`/`export` round-trip entirely — the -/// content you send is the content the prover sees. -/// -/// This is the real fix for the parse+export round-trip bug: no -/// information is lost between the wire and the prover binary. -/// Currently supports: z3, cvc5, coq, lean, agda, idris2, vampire, -/// eprover. Other provers fall through to the legacy verify_handler path. -#[instrument(skip(req))] -async fn verify_raw_handler( - Json(req): Json, -) -> Result, AppError> { - use tokio::process::Command; - - info!("Verify_raw request for prover: {:?}", req.prover); - - if req.content.trim().is_empty() { - return Ok(Json(VerifyRawResponse { - valid: false, - exit_code: -1, - stdout: String::new(), - stderr: String::new(), - message: "empty content".to_string(), - })); - } - - type Interpret = fn(i32, &str, &str) -> (bool, String); - let (ext, program, args, interpret): (&str, &str, Vec, Interpret) = match req.prover { - echidna::provers::ProverKind::Z3 => ( - "smt2", - "z3", - vec!["-smt2".to_string()], - |rc, stdout, _stderr| { - let produced = stdout.contains("sat") - || stdout.contains("unsat") - || stdout.contains("unknown"); - (rc == 0 && produced, format!("exit={} smt-output", rc)) - }, - ), - echidna::provers::ProverKind::CVC5 => ("smt2", "cvc5", vec![], |rc, stdout, _stderr| { - let produced = - stdout.contains("sat") || stdout.contains("unsat") || stdout.contains("unknown"); - (rc == 0 && produced, format!("exit={} smt-output", rc)) - }), - echidna::provers::ProverKind::Coq => ( - "v", - "coqc", - vec!["-q".to_string()], - |rc, _stdout, _stderr| (rc == 0, format!("exit={}", rc)), - ), - echidna::provers::ProverKind::Lean => ("lean", "lean", vec![], |rc, _stdout, _stderr| { - (rc == 0, format!("exit={}", rc)) - }), - echidna::provers::ProverKind::Agda => ("agda", "agda", vec![], |rc, _stdout, _stderr| { - (rc == 0, format!("exit={}", rc)) - }), - echidna::provers::ProverKind::Idris2 => ( - "idr", - "idris2", - vec!["--check".to_string()], - |rc, _stdout, _stderr| (rc == 0, format!("exit={}", rc)), - ), - echidna::provers::ProverKind::Vampire => ( - "p", - "vampire", - vec!["--mode".to_string(), "casc".to_string()], - |rc, stdout, _stderr| { - let theorem = stdout.contains("SZS status Theorem") - || stdout.contains("SZS status Unsatisfiable"); - (rc == 0 || theorem, "szs-status".to_string()) - }, - ), - echidna::provers::ProverKind::EProver => ( - "p", - "eprover", - vec!["--auto-schedule".to_string()], - |rc, stdout, _stderr| { - let theorem = stdout.contains("SZS status Theorem") - || stdout.contains("SZS status Unsatisfiable") - || stdout.contains("SZS status ContradictoryAxioms"); - (rc == 0 && theorem, "szs-status".to_string()) - }, - ), - _ => { - return Ok(Json(VerifyRawResponse { - valid: false, - exit_code: -1, - stdout: String::new(), - stderr: String::new(), - message: format!( - "verify_raw not yet implemented for {:?} — use /api/verify", - req.prover - ), - })); - }, - }; - - // Write content to a unique temp file with the right extension. - let tmpdir = std::env::temp_dir().join(format!("echidna_verify_raw_{}", std::process::id())); - if let Err(e) = tokio::fs::create_dir_all(&tmpdir).await { - return Ok(Json(VerifyRawResponse { - valid: false, - exit_code: -1, - stdout: String::new(), - stderr: String::new(), - message: format!("tmpdir create failed: {}", e), - })); - } - let filename = format!("Input.{}", ext); - let path = tmpdir.join(&filename); - if let Err(e) = tokio::fs::write(&path, req.content.as_bytes()).await { - return Ok(Json(VerifyRawResponse { - valid: false, - exit_code: -1, - stdout: String::new(), - stderr: String::new(), - message: format!("write failed: {}", e), - })); - } - - // Run the prover. cd to tmpdir for provers that care about module - // name = filename (agda, coq, idris2, lean). - let mut cmd = Command::new(program); - cmd.current_dir(&tmpdir); - for arg in &args { - cmd.arg(arg); - } - cmd.arg(&filename); - - let output_result = cmd.output().await; - // Best-effort cleanup; ignore errors. - let _ = tokio::fs::remove_dir_all(&tmpdir).await; - - match output_result { - Ok(output) => { - let exit_code = output.status.code().unwrap_or(-1); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let (valid, message) = interpret(exit_code, &stdout, &stderr); - Ok(Json(VerifyRawResponse { - valid, - exit_code, - stdout: stdout.chars().take(1024).collect(), - stderr: stderr.chars().take(1024).collect(), - message, - })) - }, - Err(e) => Ok(Json(VerifyRawResponse { - valid: false, - exit_code: -1, - stdout: String::new(), - stderr: String::new(), - message: format!("spawn failed: {} (is the '{}' binary on PATH?)", e, program), - })), - } -} - -/// Return true if the parsed ProofState contains no meaningful structure. -/// Used to detect the parse+export round-trip bug: a prover backend's -/// `parse_string` returns an empty state on unrecognised input, then -/// `verify_proof` regenerates an empty file which the real backend binary -/// then accepts vacuously (false positive). -fn is_empty_state(state: &echidna::core::ProofState) -> bool { - state.goals.is_empty() - && state.context.theorems.is_empty() - && state.context.axioms.is_empty() - && state.context.definitions.is_empty() - && state.context.variables.is_empty() -} - -fn should_passthrough_smt_eval(req: &VerifyRequest) -> bool { - matches!(req.prover, ProverKind::Z3 | ProverKind::CVC5) - && req.content.to_ascii_lowercase().contains("(check-sat") -} - -fn extract_smt_status(text: &str) -> Option { - // Result tokens are case-sensitive, standalone SMT-LIB responses. - // A satisfiable or unknown query prevents an aggregate proof claim. - let statuses: Vec<_> = text - .lines() - .map(str::trim) - .filter(|line| matches!(*line, "sat" | "unsat" | "unknown")) - .collect(); - ["sat", "unknown", "unsat"] - .into_iter() - .find(|status| statuses.contains(status)) - .map(str::to_owned) -} - -fn discharged_smt_queries(source: &str, stdout: &str) -> bool { - let Some(commands) = smt_commands(source) else { - return false; - }; - // Z3 emits echo strings without quotes. Never authenticate an echoed - // "unsat" as a solver answer, including echo+exit before a query. - if commands - .iter() - .any(|command| matches!(*command, "echo" | "exit")) - { - return false; - } - let queries = commands - .iter() - .filter(|command| matches!(**command, "check-sat" | "check-sat-assuming")) - .count(); - let statuses: Vec<_> = stdout - .lines() - .map(str::trim) - .filter(|line| matches!(*line, "sat" | "unsat" | "unknown")) - .collect(); - queries > 0 && statuses.len() == queries && statuses.iter().all(|status| *status == "unsat") -} - -/// Read command names only; the real solver still validates SMT-LIB terms. -/// Strings, quoted symbols and line comments cannot introduce commands. -fn smt_commands(source: &str) -> Option> { - let bytes = source.as_bytes(); - let mut commands = Vec::new(); - let (mut index, mut depth) = (0, 0usize); - while index < bytes.len() { - match bytes[index] { - b';' => { - while index < bytes.len() && bytes[index] != b'\n' { - index += 1; - } - }, - b'"' | b'|' => { - if depth == 0 { - return None; - } - let delimiter = bytes[index]; - index += 1; - loop { - if index == bytes.len() { - return None; - } - if bytes[index] == delimiter { - index += 1; - if delimiter == b'"' && bytes.get(index) == Some(&b'"') { - index += 1; - } else { - break; - } - } else { - index += 1; - } - } - }, - b'(' => { - if depth == 0 { - index += 1; - // Whitespace and comments are legal before the command. - loop { - while bytes.get(index).is_some_and(u8::is_ascii_whitespace) { - index += 1; - } - if bytes.get(index) != Some(&b';') { - break; - } - while index < bytes.len() && bytes[index] != b'\n' { - index += 1; - } - } - let start = index; - while bytes - .get(index) - .is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'-') - { - index += 1; - } - if index == start - || !bytes - .get(index) - .is_some_and(|b| b.is_ascii_whitespace() || matches!(b, b')' | b';')) - { - return None; - } - commands.push(&source[start..index]); - } else { - index += 1; - } - depth += 1; - }, - b')' => { - depth = depth.checked_sub(1)?; - index += 1; - }, - byte if byte.is_ascii_whitespace() => { - index += 1; - }, - _ => { - if depth == 0 { - return None; - } - index += 1; - }, - } - } - (depth == 0).then_some(commands) -} - -/// Get tactic suggestions -async fn suggest_handler( - State(state): State, - Json(req): Json, -) -> Result, AppError> { - info!("Suggest request for prover: {:?}", req.prover); - - // Try Julia ML API first - let julia_req = json!({ - "goal": req.content, - "prover": format!("{:?}", req.prover), - "top_k": req.limit.unwrap_or(5) - }); - - match state - .ml_client - .post(format!("{}/suggest", state.ml_api_url)) - .json(&julia_req) - .send() - .await - { - Ok(resp) if resp.status().is_success() => { - if let Ok(ml_resp) = resp.json::().await { - if let Some(suggestions_arr) = ml_resp["suggestions"].as_array() { - let suggestions: Vec = suggestions_arr - .iter() - .filter_map(|s| s["tactic"].as_str().map(|t| t.to_string())) - .collect(); - - if !suggestions.is_empty() { - info!("✓ Got {} ML suggestions", suggestions.len()); - return Ok(Json(SuggestResponse { suggestions })); - } - } - } - }, - _ => { - info!("ML API unavailable, using prover fallback"); - }, - } - - // Fallback: Use prover's built-in suggestions - let config = ProverConfig::default(); - let prover = echidna::provers::ProverFactory::create(req.prover, config) - .map_err(|e| AppError::InternalError(e.to_string()))?; - - let state = prover - .parse_string(&req.content) - .await - .map_err(|e| AppError::ParseError(e.to_string()))?; - - let tactics = prover - .suggest_tactics(&state, req.limit.unwrap_or(5)) - .await - .map_err(|e| AppError::InternalError(e.to_string()))?; - - let suggestions = tactics.iter().map(|t| format!("{:?}", t)).collect(); - - Ok(Json(SuggestResponse { suggestions })) -} - -/// Search theorems -async fn search_handler( - Query(params): Query>, -) -> Result, AppError> { - let pattern = params - .get("q") - .ok_or_else(|| AppError::BadRequest("Missing query parameter 'q'".to_string()))?; - - let prover_name = params.get("prover"); - - info!( - "Search request: pattern={}, prover={:?}", - pattern, prover_name - ); - - let mut all_results = Vec::new(); - - // Determine which provers to search - let provers: Vec = if let Some(name) = prover_name { - vec![parse_prover_kind(name)?] - } else { - vec![ - ProverKind::Agda, - ProverKind::Coq, - ProverKind::Lean, - ProverKind::Isabelle, - ] - }; - - // Search each prover - for prover_kind in provers { - let config = ProverConfig::default(); - - if let Ok(prover) = echidna::provers::ProverFactory::create(prover_kind, config) { - if let Ok(results) = prover.search_theorems(pattern).await { - all_results.extend(results); - } - } - } - - // Cross-prover layer (see main.rs::search_command for rationale). - // Folds VeriSimDB matches into the same response so REST clients - // see results from backends without native search commands too. - let verisim_url = - std::env::var("VERISIM_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); - if let Ok(cross) = echidna::vcl_ut::cross_prover_search_names(&verisim_url, pattern, 50).await { - all_results.extend(cross); - } - - let count = all_results.len(); - Ok(Json(SearchResponse { - results: all_results, - count, - })) -} - -/// Create a new proof session -async fn create_session( - State(state): State, - Json(req): Json, -) -> Result, AppError> { - info!("Creating session for prover: {:?}", req.prover); - - // Create prover - let config = ProverConfig::default(); - let prover = echidna::provers::ProverFactory::create(req.prover, config) - .map_err(|e| AppError::InternalError(e.to_string()))?; - - // Create session - let session_id = echidna::ids::new_record_id().to_string(); - let session = ProofSession { - prover, - state: None, - history: Vec::new(), - }; - - // Store session - state - .sessions - .write() - .await - .insert(session_id.clone(), Arc::new(Mutex::new(session))); - - Ok(Json(CreateSessionResponse { session_id })) -} - -/// Get session state -async fn get_session_state( - State(state): State, - Path(session_id): Path, -) -> Result, AppError> { - let sessions = state.sessions.read().await; - let session = sessions - .get(&session_id) - .ok_or_else(|| AppError::NotFound(format!("Session not found: {}", session_id)))?; - - let session = session.lock().await; - - Ok(Json(SessionStateResponse { - goals: session - .state - .as_ref() - .map(|s| s.goals.len()) - .unwrap_or_else(|| 0), - complete: session - .state - .as_ref() - .map(|s| s.is_complete()) - .unwrap_or(false), - tactics_applied: session.history.len(), - })) -} - -/// Apply tactic to session -async fn apply_tactic_handler( - State(state): State, - Path(session_id): Path, - Json(req): Json, -) -> Result, AppError> { - let sessions = state.sessions.read().await; - let session_arc = sessions - .get(&session_id) - .ok_or_else(|| AppError::NotFound(format!("Session not found: {}", session_id)))? - .clone(); - - drop(sessions); - - let mut session = session_arc.lock().await; - - // Ensure we have a state - if session.state.is_none() { - return Err(AppError::BadRequest( - "No proof loaded in session".to_string(), - )); - } - - // Parse tactic (simplified) - let tactic = parse_tactic_from_json(&req.tactic)?; - - // Apply tactic - let proof_state = session.state.as_ref().unwrap(); - let result = session - .prover - .apply_tactic(proof_state, &tactic) - .await - .map_err(|e| AppError::InternalError(e.to_string()))?; - - match result { - TacticResult::Success(new_state) => { - let complete = new_state.is_complete(); - session.state = Some(new_state); - session.history.push(req.tactic.clone()); - - Ok(Json(ApplyTacticResponse { - success: true, - complete, - goals_remaining: session.state.as_ref().unwrap().goals.len(), - message: "Tactic applied successfully".to_string(), - })) - }, - - TacticResult::Error(msg) => Ok(Json(ApplyTacticResponse { - success: false, - complete: false, - goals_remaining: session.state.as_ref().unwrap().goals.len(), - message: msg, - })), - - TacticResult::QED => { - session.state.as_mut().unwrap().goals.clear(); - - Ok(Json(ApplyTacticResponse { - success: true, - complete: true, - goals_remaining: 0, - message: "Proof complete!".to_string(), - })) - }, - } -} - -/// Get aspect tags for filtering. -/// -/// The tag vocabulary is intentionally hard-coded here — it's the UI's -/// facet list, not live-verified data, and needs to stay stable across -/// releases so saved filters keep working. -async fn get_aspect_tags() -> Json { - let tags = vec![ - AspectTag { - name: "algebraic".to_string(), - category: "domain".to_string(), - active: false, - }, - AspectTag { - name: "geometric".to_string(), - category: "domain".to_string(), - active: false, - }, - AspectTag { - name: "logical".to_string(), - category: "domain".to_string(), - active: false, - }, - AspectTag { - name: "inductive".to_string(), - category: "technique".to_string(), - active: false, - }, - AspectTag { - name: "deductive".to_string(), - category: "technique".to_string(), - active: false, - }, - AspectTag { - name: "automated".to_string(), - category: "technique".to_string(), - active: false, - }, - ]; - - Json(AspectTagsResponse { tags }) -} - -/// Get proof tree for a session -async fn get_proof_tree( - State(state): State, - Path(session_id): Path, -) -> Result, AppError> { - let sessions = state.sessions.read().await; - let session = sessions - .get(&session_id) - .ok_or_else(|| AppError::NotFound(format!("Session not found: {}", session_id)))?; - - let session = session.lock().await; - - // Build proof tree from session state - let tree = if let Some(proof_state) = &session.state { - serde_json::json!({ - "id": "root", - "type": "goal", - "label": proof_state.goals.first() - .map(|g| format!("{:?}", g.target)) - .unwrap_or_else(|| "No goals".to_string()), - "children": [] - }) - } else { - serde_json::json!({ - "id": "root", - "type": "goal", - "label": "No proof state", - "children": [] - }) - }; - - Ok(Json(ProofTreeResponse { tree })) -} - -/// UI-specific tactic suggestion endpoint -async fn suggest_tactics_ui( - Json(req): Json, -) -> Result, AppError> { - let prover = req.prover.as_deref().unwrap_or("Coq"); - let top_k = req.top_k.unwrap_or(5); - - info!( - "UI tactic suggestion request - prover: {}, goal: {}", - prover, req.goal - ); - - // Call Julia ML API for real AI predictions - let client = reqwest::Client::new(); - let julia_url = "http://127.0.0.1:8090/suggest"; - - let julia_request = serde_json::json!({ - "goal": req.goal, - "prover": prover, - "top_k": top_k - }); - - match client.post(julia_url).json(&julia_request).send().await { - Ok(response) => { - if response.status().is_success() { - match response.json::().await { - Ok(json) => { - let empty_vec = vec![]; - let suggestions_json = json["suggestions"].as_array().unwrap_or(&empty_vec); - let suggestions: Vec = suggestions_json - .iter() - .map(|s| { - let confidence = s["confidence"].as_f64().unwrap_or(0.0); - let tactic = s["tactic"].as_str().unwrap_or("auto").to_string(); - let premise = s - .get("premises") - .and_then(|p| p.as_array()) - .and_then(|arr| arr.first()) - .and_then(|v| v.as_str()) - .map(String::from); - - // Infer aspect tags from confidence and tactic - let aspect_tags = if confidence > 0.8 { - vec!["automated".to_string()] - } else if tactic.contains("intro") { - vec!["deductive".to_string()] - } else { - vec!["algebraic".to_string(), "deductive".to_string()] - }; - - TacticSuggestion { - tactic, - confidence, - premise, - aspect_tags, - } - }) - .collect(); - - Ok(Json(SuggestTacticsUIResponse { suggestions })) - }, - Err(e) => { - info!("Failed to parse Julia response: {}", e); - // Fall back to mock data - Ok(Json(SuggestTacticsUIResponse { - suggestions: vec![TacticSuggestion { - tactic: "auto".to_string(), - confidence: 0.5, - premise: None, - aspect_tags: vec!["automated".to_string()], - }], - })) - }, - } - } else { - info!("Julia API returned error status: {}", response.status()); - // Fall back to mock data - Ok(Json(SuggestTacticsUIResponse { - suggestions: vec![TacticSuggestion { - tactic: "auto".to_string(), - confidence: 0.5, - premise: None, - aspect_tags: vec!["automated".to_string()], - }], - })) - } - }, - Err(e) => { - info!("Failed to connect to Julia ML API: {}", e); - // Fall back to mock data if Julia service is down - Ok(Json(SuggestTacticsUIResponse { - suggestions: vec![TacticSuggestion { - tactic: "auto".to_string(), - confidence: 0.5, - premise: None, - aspect_tags: vec!["automated".to_string()], - }], - })) - }, - } -} - -/// UI-specific theorem search endpoint -async fn search_theorems_ui( - Query(params): Query>, -) -> Result, AppError> { - let query = params - .get("query") - .or_else(|| params.get("q")) - .ok_or_else(|| AppError::BadRequest("Missing query parameter".to_string()))?; - - let _tags = params - .get("tags") - .map(|t| t.split(',').map(String::from).collect::>()) - .unwrap_or_default(); - - info!("UI theorem search: query={}", query); - - // Full-corpus search lives behind the REST interface - // (`/api/search/theorems`) which indexes training_data/*.jsonl. - // This UI endpoint is the keep-alive shim for the old ReScript - // autocomplete — it returns canned exemplars so the UI has a - // stable shape, and the real query goes through the search - // workspace member. - let results = vec![ - "Theorem: associativity_add (a + b) + c = a + (b + c)".to_string(), - "Theorem: commutativity_mul a * b = b * a".to_string(), - "Lemma: distributivity a * (b + c) = a * b + a * c".to_string(), - ]; - - Ok(Json(SearchTheoremsUIResponse { results })) -} - -// ========== Helper Functions ========== - -/// Parse prover kind from string -fn parse_prover_kind(s: &str) -> Result { - match s.to_lowercase().as_str() { - "agda" => Ok(ProverKind::Agda), - "coq" => Ok(ProverKind::Coq), - "lean" => Ok(ProverKind::Lean), - "isabelle" => Ok(ProverKind::Isabelle), - "z3" => Ok(ProverKind::Z3), - "cvc5" => Ok(ProverKind::CVC5), - "metamath" => Ok(ProverKind::Metamath), - "hollight" => Ok(ProverKind::HOLLight), - "mizar" => Ok(ProverKind::Mizar), - "pvs" => Ok(ProverKind::PVS), - "acl2" => Ok(ProverKind::ACL2), - "hol4" => Ok(ProverKind::HOL4), - _ => Err(AppError::BadRequest(format!("Unknown prover: {}", s))), - } -} - -/// Parse tactic from JSON string (simplified) -fn parse_tactic_from_json(s: &str) -> Result { - // Simplified parsing - in production, use proper JSON parsing - let parts: Vec<&str> = s.split_whitespace().collect(); - - if parts.is_empty() { - return Err(AppError::BadRequest("Empty tactic".to_string())); - } - - match parts[0].to_lowercase().as_str() { - "apply" => Ok(Tactic::Apply(parts[1..].join(" "))), - "intro" => Ok(Tactic::Intro(parts.get(1).map(|s| s.to_string()))), - "rewrite" => Ok(Tactic::Rewrite(parts[1..].join(" "))), - "simplify" => Ok(Tactic::Simplify), - "reflexivity" => Ok(Tactic::Reflexivity), - "assumption" => Ok(Tactic::Assumption), - _ => Ok(Tactic::Custom { - prover: "unknown".to_string(), - command: parts[0].to_string(), - args: parts[1..].iter().map(|s| s.to_string()).collect(), - }), - } -} - -// ========== Request/Response Types ========== - -#[derive(Deserialize)] -struct ProveRequest { - prover: ProverKind, - content: String, - timeout: Option, - neural: Option, -} - -#[derive(Serialize)] -struct ProveResponse { - success: bool, - goals: usize, - message: String, -} - -#[derive(Clone, Deserialize)] -struct VerifyRequest { - prover: ProverKind, - content: String, -} - -#[derive(Serialize)] -struct VerifyResponse { - valid: bool, - /// Typed outcome from the prover — one of PROVED, NO_PROOF_FOUND, - /// INVALID_INPUT, UNSUPPORTED_FEATURE, TIMEOUT, INCONSISTENT_PREMISES, - /// PROVER_ERROR, SYSTEM_ERROR. - /// - /// This is the primary result field; `valid` is a backward-compatible bool - /// summary derived from it (true iff outcome == PROVED). - outcome: String, - goals_remaining: usize, - tactics_used: usize, - #[serde(skip_serializing_if = "Option::is_none")] - mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - smt_status: Option, -} - -/// Raw-content verification response. Skips `parse_string`/`export` round -/// trip and invokes the prover binary directly on the supplied content. -#[derive(Serialize)] -struct VerifyRawResponse { - valid: bool, - exit_code: i32, - stdout: String, - stderr: String, - message: String, -} - -#[derive(Deserialize)] -struct SuggestRequest { - prover: ProverKind, - content: String, - limit: Option, -} - -#[derive(Serialize)] -struct SuggestResponse { - suggestions: Vec, -} - -#[derive(Serialize)] -struct SearchResponse { - results: Vec, - count: usize, -} - -#[derive(Deserialize)] -struct CreateSessionRequest { - prover: ProverKind, -} - -#[derive(Serialize)] -struct CreateSessionResponse { - session_id: String, -} - -#[derive(Serialize)] -struct SessionStateResponse { - goals: usize, - complete: bool, - tactics_applied: usize, -} - -#[derive(Deserialize)] -struct ApplyTacticRequest { - tactic: String, -} - -#[derive(Serialize)] -struct ApplyTacticResponse { - success: bool, - complete: bool, - goals_remaining: usize, - message: String, -} - -#[derive(Serialize)] -struct HealthResponse { - status: String, - version: String, -} - -#[derive(Serialize)] -struct ProversResponse { - provers: Vec, -} - -#[derive(Serialize)] -struct ProverInfo { - name: String, - tier: u8, - complexity: u8, -} - -#[derive(Serialize)] -struct AspectTag { - name: String, - category: String, - active: bool, -} - -#[derive(Serialize)] -struct AspectTagsResponse { - tags: Vec, -} - -#[derive(Serialize)] -struct ProofTreeResponse { - tree: serde_json::Value, -} - -#[derive(Deserialize)] -struct SuggestTacticsUIRequest { - goal: String, // Changed from goal_id to accept goal text directly - prover: Option, - #[allow(dead_code)] // Reserved for future aspect-tag filtering - active_tags: Option>, - top_k: Option, -} - -#[derive(Serialize)] -struct SuggestTacticsUIResponse { - suggestions: Vec, -} - -#[derive(Serialize)] -struct TacticSuggestion { - tactic: String, - confidence: f64, - premise: Option, - aspect_tags: Vec, -} - -#[derive(Serialize)] -struct SearchTheoremsUIResponse { - results: Vec, -} - -// ========== Error Handling ========== - -#[derive(Debug)] -enum AppError { - BadRequest(String), - NotFound(String), - ParseError(String), - VerificationError(String), - InternalError(String), -} - -impl IntoResponse for AppError { - fn into_response(self) -> Response { - let (status, message) = match self { - AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), - AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), - AppError::ParseError(msg) => (StatusCode::BAD_REQUEST, format!("Parse error: {}", msg)), - AppError::VerificationError(msg) => ( - StatusCode::UNPROCESSABLE_ENTITY, - format!("Verification error: {}", msg), - ), - AppError::InternalError(msg) => ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Internal error: {}", msg), - ), - }; - - let body = Json(serde_json::json!({ - "error": message, - })); - - (status, body).into_response() - } -} diff --git a/src/rust/server/agent.rs b/src/rust/server/agent.rs new file mode 100644 index 00000000..5143344b --- /dev/null +++ b/src/rust/server/agent.rs @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell +// SPDX-License-Identifier: MPL-2.0 + +//! `POST /api/agent/plan`: MetaController planning without execution. + +use super::AppState; +use axum::{extract::State, response::IntoResponse, Json}; +use echidna::agent::meta_controller::Plan; +use echidna::agent::AgenticGoal; +use echidna::core::{Goal, Term}; +use echidna::ProverKind; +use serde::Deserialize; +use serde_json::json; + +// ========== Agent Planning ========== + +/// Request to `POST /api/agent/plan`. +/// +/// Asks the MetaController to plan a proof attempt: which prover to use, +/// which coprocessor preconditions to run first, and the Bayesian-estimated +/// timeout. The response is a `Plan` that callers can inspect before +/// deciding whether to submit a full `/api/verify` or `/api/prove` request. +/// +/// `aspects` drive the coprocessor routing (e.g. `"arithmetic.factorisation"` +/// triggers a Math precondition; `"algebra.groebner.basis"` triggers Singular). +/// `preferred_prover` and `candidates` are both optional — omitting both lets +/// the Pareto ranker pick freely from Z3 as the default. +#[derive(Debug, Deserialize)] +pub(super) struct AgentPlanRequest { + /// Goal aspect tags (e.g. `["arithmetic.factorisation", "crypto.hash.sha256"]`). + #[serde(default)] + aspects: Vec, + + /// Client's preferred prover, if any. Overridden by Pareto ranking when + /// `candidates` is non-empty. + preferred_prover: Option, + + /// Candidate provers to rank. When empty, `preferred_prover` (or Z3) + /// is selected directly without Pareto scoring. + #[serde(default)] + candidates: Vec, + + /// Default timeout hint (ms) used when no historical data is available. + #[serde(default = "default_timeout_ms")] + default_timeout_ms: u64, +} + +fn default_timeout_ms() -> u64 { + 30_000 +} + +/// `POST /api/agent/plan` — return a MetaController plan for the given goal aspects. +/// +/// Does not execute any provers or coprocessors; it only plans. Callers use +/// the returned `coprocessor_preconditions` to understand what computational +/// preparation the system recommends before submitting the proof content. +pub(super) async fn agent_plan_handler( + State(state): State, + Json(req): Json, +) -> impl IntoResponse { + // Build a minimal AgenticGoal from the request metadata. The goal target + // is a placeholder (`Term::Const("?")`) because planning doesn't inspect + // the proof content — only the aspects and prover preferences matter here. + use echidna::agent::Priority; + let goal = AgenticGoal { + goal: Goal { + id: echidna::ids::new_record_id().to_string(), + target: Term::Const("?".to_string()), + hypotheses: vec![], + }, + priority: Priority::Medium, + attempts: 0, + max_attempts: 3, + preferred_prover: req.preferred_prover, + aspects: req.aspects, + parent: None, + }; + + let plan: Plan = if req.candidates.is_empty() { + state.meta_controller.plan(&goal).await + } else { + use echidna::verification::confidence::TrustLevel; + state + .meta_controller + .plan_with_pareto( + &goal, + &req.candidates, + req.default_timeout_ms, + TrustLevel::Level2, + ) + .await + }; + + Json(json!({ + "prover": format!("{:?}", plan.prover), + "estimated_timeout_ms": plan.estimated_timeout_ms, + "rationale": plan.rationale, + "coprocessor_preconditions": plan.coprocessor_preconditions + .iter() + .map(|p| json!({ + "kind": format!("{:?}", p.kind), + "aspect": p.aspect, + })) + .collect::>(), + })) +} diff --git a/src/rust/server/mod.rs b/src/rust/server/mod.rs new file mode 100644 index 00000000..aea3be82 --- /dev/null +++ b/src/rust/server/mod.rs @@ -0,0 +1,348 @@ +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell +// SPDX-License-Identifier: MPL-2.0 + +//! HTTP API server for ECHIDNA +//! +//! Provides REST API and WebSocket endpoints for theorem proving + +use anyhow::Result; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, post}, + Json, Router, +}; +use colored::Colorize; +use echidna::agent::meta_controller::MetaController; +use echidna::core::{ProofState, Tactic}; +use echidna::dispatch::ProverDispatcher; +use echidna::verification::StatisticsTracker; +use echidna::{ProverBackend, ProverKind}; +use reqwest::Client; +use serde_json::json; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{Mutex, RwLock}; +use tower_http::cors::CorsLayer; +use tracing::{debug, info}; + +mod agent; +mod prove; +mod search; +mod session; +mod status; +mod ui; +mod verify; + +use agent::agent_plan_handler; +use prove::prove_handler; +use search::{search_handler, suggest_handler}; +use session::{apply_tactic_handler, create_session, get_proof_tree, get_session_state}; +use status::{diagnostics_health_handler, groove_manifest, health_check, list_provers}; +use ui::{get_aspect_tags, search_theorems_ui, suggest_tactics_ui}; +use verify::{verify_handler, verify_parallel_handler, verify_raw_handler}; + +/// Application state shared across handlers +#[derive(Clone)] +struct AppState { + /// Active proof sessions (session_id -> ProofSession) + sessions: Arc>>>>, + /// HTTP client for Julia ML API + ml_client: Client, + /// Julia ML API base URL + ml_api_url: String, + /// MetaController — shared (prover × coprocessor) planner with persistent + /// StatisticsTracker. Survives request boundaries so the Bayesian routing + /// state accumulates across the server's lifetime. All handlers that + /// perform goal-aware dispatch share this single instance. + meta_controller: Arc, + /// Shared proof-outcome statistics used by MetaController for Bayesian + /// routing and exported to Julia for GNN online learning. + stats: Arc>, + /// Prover dispatcher for trust-hardening pipeline and health monitoring + dispatcher: Arc, +} + +/// A proof session +struct ProofSession { + prover: Box, + state: Option, + history: Vec, +} + +const DEFAULT_ML_API_URL: &str = "http://127.0.0.1:8090"; + +/// Start the HTTP server +pub async fn start_server(port: u16, host: String, enable_cors: bool) -> Result<()> { + // Create HTTP client for Julia ML API + let ml_client = Client::builder().timeout(Duration::from_secs(10)).build()?; + + let ml_api_url = + std::env::var("ECHIDNA_ML_API_URL").unwrap_or_else(|_| DEFAULT_ML_API_URL.to_string()); + + // Security: Validate that the ML API URL is either local or explicitly allowed + if !ml_api_url.starts_with("http://127.0.0.1") && !ml_api_url.starts_with("http://localhost") { + info!("⚠ Untrusted ML API URL detected: {}. Baseline security policy requires local or trusted endpoint.", ml_api_url); + } + + // Test Julia ML connection + match ml_client.get(format!("{}/health", ml_api_url)).send().await { + Ok(resp) if resp.status().is_success() => { + info!("✓ Connected to Julia ML API at {}", ml_api_url); + }, + _ => { + info!( + "⚠ Julia ML API not available at {} (will use fallback)", + ml_api_url + ); + }, + } + + // Shared stats tracker — MetaController uses it for Bayesian routing; + // the background GNN sync task pushes snapshots to Julia for online learning. + let stats = Arc::new(RwLock::new(StatisticsTracker::new())); + let meta_controller = Arc::new(MetaController::new().with_stats(stats.clone())); + + // Create prover dispatcher with default configuration. + // When compiled with --features verisim, attach the VeriSimDB writer so + // every HTTP proof attempt (including parse failures) feeds the S4 learning + // loop. The writer runs fire-and-forget; a VeriSimDB outage never blocks + // or alters a dispatch result. + let dispatcher = { + let d = ProverDispatcher::new(); + #[cfg(feature = "verisim")] + let d = { + let url = std::env::var("VERISIM_URL") + .unwrap_or_else(|_| "http://localhost:8080".to_string()); + info!("✓ VeriSim writer attached at {}", url); + d.with_verisim_writer(&url) + }; + Arc::new(d) + }; + info!("✓ Initialized prover dispatcher for trust-hardening pipeline"); + + let state = AppState { + sessions: Arc::new(RwLock::new(HashMap::new())), + ml_client, + ml_api_url, + meta_controller, + stats, + dispatcher, + }; + + // GNN training sync — background task that pushes accumulated proof-outcome + // stats to Julia's /training/update endpoint every 60 seconds. + // Only fires when >= 10 new outcomes have accumulated since the last push, + // so idle servers incur no traffic. Errors are logged at debug level and + // do not affect the main server loop (fire-and-forget). + { + let sync_stats = state.stats.clone(); + let sync_client = state.ml_client.clone(); + let sync_url = state.ml_api_url.clone(); + tokio::spawn(async move { + let mut last_push_count: u64 = 0; + let mut interval = tokio::time::interval(Duration::from_secs(60)); + interval.tick().await; // consume the immediate first tick; wait 60s before first push + loop { + interval.tick().await; + let (total, records) = { + let guard = sync_stats.read().await; + (guard.total_attempts(), guard.export_records()) + }; + if total < last_push_count + 10 || records.is_empty() { + continue; + } + last_push_count = total; + let url = format!("{}/training/update", sync_url); + let payload = json!({ "records": records }); + match sync_client.post(&url).json(&payload).send().await { + Ok(resp) if resp.status().is_success() => { + info!( + "GNN training sync: {} records pushed to Julia", + records.len() + ); + }, + Ok(resp) => { + debug!("GNN training sync: Julia returned {}", resp.status()); + }, + Err(e) => { + debug!("GNN training sync: Julia unavailable ({})", e); + }, + } + } + }); + } + + // Build router + let mut app = Router::new() + // Groove discovery endpoint — returns capability manifest for service mesh. + // See Groove.idr echidnaManifest for the canonical ABI definition. + .route("/.well-known/groove", get(groove_manifest)) + // REST API endpoints + .route("/api/health", get(health_check)) + .route("/api/diagnostics/health", get(diagnostics_health_handler)) + .route("/api/provers", get(list_provers)) + .route("/api/prove", post(prove_handler)) + .route("/api/verify", post(verify_handler)) + .route("/api/verify_parallel", post(verify_parallel_handler)) + .route("/api/verify_raw", post(verify_raw_handler)) + .route("/api/suggest", post(suggest_handler)) + .route("/api/search", get(search_handler)) + .route("/api/session/create", post(create_session)) + .route("/api/session/{id}/state", get(get_session_state)) + .route("/api/session/{id}/apply", post(apply_tactic_handler)) + .route("/api/session/{id}/tree", get(get_proof_tree)) + // Additional UI-specific endpoints + .route("/api/agent/plan", post(agent_plan_handler)) + .route("/api/aspect-tags", get(get_aspect_tags)) + .route("/api/tactics/suggest", post(suggest_tactics_ui)) + .route("/api/theorems/search", get(search_theorems_ui)) + // WebSocket endpoint (disabled - requires axum ws feature) + // .route("/ws/interactive", get(websocket_handler)) + .with_state(state); + + // Add CORS if enabled + if enable_cors { + app = app.layer(CorsLayer::permissive()); + } + + // Bind server + let addr: SocketAddr = format!("{}:{}", host, port).parse()?; + + println!( + "{}", + "╔═══════════════════════════════════════════════════════════╗".cyan() + ); + println!( + "{}", + "║ ECHIDNA HTTP API Server ║" + .cyan() + .bold() + ); + println!( + "{}", + "╚═══════════════════════════════════════════════════════════╝".cyan() + ); + println!(); + println!( + "Server listening on: {}", + format!("https://{}", addr).green().bold() + ); + println!(); + println!("{}", "Endpoints:".yellow().bold()); + println!(" GET /.well-known/groove - Groove discovery manifest"); + println!(" GET /api/health - Health check"); + println!(" GET /api/provers - List available provers"); + println!(" POST /api/prove - Prove a theorem"); + println!(" POST /api/verify - Verify a proof"); + println!(" POST /api/suggest - Get tactic suggestions"); + println!(" GET /api/search?q= - Search theorems"); + println!(" POST /api/session/create - Create proof session"); + println!(" GET /api/session/{{id}}/state - Get session state"); + println!(" POST /api/session/{{id}}/apply - Apply tactic to session"); + println!(" WS /ws/interactive - WebSocket live proof session"); + println!(); + println!("Press Ctrl+C to stop the server"); + println!(); + + // Start server + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +// ========== Helper Functions ========== + +/// Return true if the parsed ProofState contains no meaningful structure. +/// Used to detect the parse+export round-trip bug: a prover backend's +/// `parse_string` returns an empty state on unrecognised input, then +/// `verify_proof` regenerates an empty file which the real backend binary +/// then accepts vacuously (false positive). +fn is_empty_state(state: &echidna::core::ProofState) -> bool { + state.goals.is_empty() + && state.context.theorems.is_empty() + && state.context.axioms.is_empty() + && state.context.definitions.is_empty() + && state.context.variables.is_empty() +} + +/// Parse prover kind from string +fn parse_prover_kind(s: &str) -> Result { + match s.to_lowercase().as_str() { + "agda" => Ok(ProverKind::Agda), + "coq" => Ok(ProverKind::Coq), + "lean" => Ok(ProverKind::Lean), + "isabelle" => Ok(ProverKind::Isabelle), + "z3" => Ok(ProverKind::Z3), + "cvc5" => Ok(ProverKind::CVC5), + "metamath" => Ok(ProverKind::Metamath), + "hollight" => Ok(ProverKind::HOLLight), + "mizar" => Ok(ProverKind::Mizar), + "pvs" => Ok(ProverKind::PVS), + "acl2" => Ok(ProverKind::ACL2), + "hol4" => Ok(ProverKind::HOL4), + _ => Err(AppError::BadRequest(format!("Unknown prover: {}", s))), + } +} + +/// Parse tactic from JSON string (simplified) +fn parse_tactic_from_json(s: &str) -> Result { + // Simplified parsing - in production, use proper JSON parsing + let parts: Vec<&str> = s.split_whitespace().collect(); + + if parts.is_empty() { + return Err(AppError::BadRequest("Empty tactic".to_string())); + } + + match parts[0].to_lowercase().as_str() { + "apply" => Ok(Tactic::Apply(parts[1..].join(" "))), + "intro" => Ok(Tactic::Intro(parts.get(1).map(|s| s.to_string()))), + "rewrite" => Ok(Tactic::Rewrite(parts[1..].join(" "))), + "simplify" => Ok(Tactic::Simplify), + "reflexivity" => Ok(Tactic::Reflexivity), + "assumption" => Ok(Tactic::Assumption), + _ => Ok(Tactic::Custom { + prover: "unknown".to_string(), + command: parts[0].to_string(), + args: parts[1..].iter().map(|s| s.to_string()).collect(), + }), + } +} + +// ========== Error Handling ========== + +#[derive(Debug)] +enum AppError { + BadRequest(String), + NotFound(String), + ParseError(String), + VerificationError(String), + InternalError(String), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match self { + AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), + AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), + AppError::ParseError(msg) => (StatusCode::BAD_REQUEST, format!("Parse error: {}", msg)), + AppError::VerificationError(msg) => ( + StatusCode::UNPROCESSABLE_ENTITY, + format!("Verification error: {}", msg), + ), + AppError::InternalError(msg) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Internal error: {}", msg), + ), + }; + + let body = Json(serde_json::json!({ + "error": message, + })); + + (status, body).into_response() + } +} diff --git a/src/rust/server/prove.rs b/src/rust/server/prove.rs new file mode 100644 index 00000000..5697df4a --- /dev/null +++ b/src/rust/server/prove.rs @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell +// SPDX-License-Identifier: MPL-2.0 + +//! `POST /api/prove`. + +use super::{AppError, AppState}; +use anyhow::Result; +use axum::{extract::State, Json}; +use echidna::ProverKind; +use serde::{Deserialize, Serialize}; +use tracing::info; + +// Used only by the direct factory path, which the `verisim` feature replaces +// with the dispatcher. +#[cfg(not(feature = "verisim"))] +use super::is_empty_state; +#[cfg(not(feature = "verisim"))] +use echidna::provers::ProverOutcome; +#[cfg(not(feature = "verisim"))] +use echidna::ProverConfig; +#[cfg(not(feature = "verisim"))] +use std::time::Instant; + +/// Prove theorem endpoint +pub(super) async fn prove_handler( + State(app_state): State, + Json(req): Json, +) -> Result, AppError> { + info!("Prove request for prover: {:?}", req.prover); + + // Route through the full dispatch pipeline when the verisim feature is + // active. This fires the VeriSimDB writer for every HTTP proof (parse + // failures and final results), closing the S4 learning loop. The writer + // runs fire-and-forget — a VeriSimDB outage never blocks or alters a + // result. Response shape (success/goals/message) is preserved. + #[cfg(feature = "verisim")] + { + let result = app_state + .dispatcher + .verify_proof(req.prover, &req.content) + .await + .map_err(|e| AppError::VerificationError(e.to_string()))?; + return Ok(Json(ProveResponse { + success: result.verified, + goals: result.goals_remaining, + message: result.message, + })); + } + + // Non-verisim path: direct factory + prover call (existing behaviour). + #[cfg(not(feature = "verisim"))] + { + // Create prover + let config = ProverConfig { + timeout: req.timeout.unwrap_or(300), + neural_enabled: req.neural.unwrap_or(true), + ..ProverConfig::default() + }; + + let prover = echidna::provers::ProverFactory::create(req.prover, config) + .map_err(|e| AppError::InternalError(e.to_string()))?; + + // Parse proof + let state = prover + .parse_string(&req.content) + .await + .map_err(|e| AppError::ParseError(e.to_string()))?; + + // Fail-fast on empty parse results (fixes false-positive: unrecognised + // content produced an empty ProofState that re-exported to an empty file + // which the backend prover then happily accepted). + if is_empty_state(&state) && !req.content.trim().is_empty() { + let outcome = ProverOutcome::InvalidInput { + reason: "Parse produced no goals, theorems, definitions, or axioms — \ + content not recognised by the selected prover backend" + .to_string(), + location: None, + }; + app_state + .dispatcher + .record_prover_result(req.prover, &outcome, 0) + .map_err(|error| AppError::InternalError(error.to_string()))?; + return Ok(Json(ProveResponse { + success: false, + goals: 0, + message: "Parse produced no goals, theorems, definitions, or axioms — \ + content not recognised by the selected prover backend" + .to_string(), + })); + } + + // Verify proof and record health metrics + let verify_start = Instant::now(); + let valid = prover + .verify_proof(&state) + .await + .map_err(|e| AppError::VerificationError(e.to_string()))?; + let verify_elapsed_ms = verify_start.elapsed().as_millis() as u64; + + let outcome = if valid { + ProverOutcome::Proved { + elapsed_ms: verify_elapsed_ms, + } + } else { + ProverOutcome::NoProofFound { + elapsed_ms: verify_elapsed_ms, + reason: Some("Proof verification failed".to_string()), + } + }; + app_state + .dispatcher + .record_prover_result(req.prover, &outcome, verify_elapsed_ms) + .map_err(|error| AppError::InternalError(error.to_string()))?; + + Ok(Json(ProveResponse { + success: valid, + goals: state.goals.len(), + message: if valid { + "Proof verified successfully".to_string() + } else { + "Proof verification failed".to_string() + }, + })) + } +} + +// ========== Request/Response Types ========== + +#[derive(Deserialize)] +pub(super) struct ProveRequest { + prover: ProverKind, + content: String, + timeout: Option, + neural: Option, +} + +#[derive(Serialize)] +pub(super) struct ProveResponse { + success: bool, + goals: usize, + message: String, +} diff --git a/src/rust/server/search.rs b/src/rust/server/search.rs new file mode 100644 index 00000000..0b39a5be --- /dev/null +++ b/src/rust/server/search.rs @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell +// SPDX-License-Identifier: MPL-2.0 + +//! Tactic suggestion and theorem search: `/api/suggest`, `/api/search`. + +use super::{parse_prover_kind, AppError, AppState}; +use anyhow::Result; +use axum::{ + extract::{Query, State}, + Json, +}; +use echidna::{ProverConfig, ProverKind}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::collections::HashMap; +use tracing::info; + +/// Get tactic suggestions +pub(super) async fn suggest_handler( + State(state): State, + Json(req): Json, +) -> Result, AppError> { + info!("Suggest request for prover: {:?}", req.prover); + + // Try Julia ML API first + let julia_req = json!({ + "goal": req.content, + "prover": format!("{:?}", req.prover), + "top_k": req.limit.unwrap_or(5) + }); + + match state + .ml_client + .post(format!("{}/suggest", state.ml_api_url)) + .json(&julia_req) + .send() + .await + { + Ok(resp) if resp.status().is_success() => { + if let Ok(ml_resp) = resp.json::().await { + if let Some(suggestions_arr) = ml_resp["suggestions"].as_array() { + let suggestions: Vec = suggestions_arr + .iter() + .filter_map(|s| s["tactic"].as_str().map(|t| t.to_string())) + .collect(); + + if !suggestions.is_empty() { + info!("✓ Got {} ML suggestions", suggestions.len()); + return Ok(Json(SuggestResponse { suggestions })); + } + } + } + }, + _ => { + info!("ML API unavailable, using prover fallback"); + }, + } + + // Fallback: Use prover's built-in suggestions + let config = ProverConfig::default(); + let prover = echidna::provers::ProverFactory::create(req.prover, config) + .map_err(|e| AppError::InternalError(e.to_string()))?; + + let state = prover + .parse_string(&req.content) + .await + .map_err(|e| AppError::ParseError(e.to_string()))?; + + let tactics = prover + .suggest_tactics(&state, req.limit.unwrap_or(5)) + .await + .map_err(|e| AppError::InternalError(e.to_string()))?; + + let suggestions = tactics.iter().map(|t| format!("{:?}", t)).collect(); + + Ok(Json(SuggestResponse { suggestions })) +} + +/// Search theorems +pub(super) async fn search_handler( + Query(params): Query>, +) -> Result, AppError> { + let pattern = params + .get("q") + .ok_or_else(|| AppError::BadRequest("Missing query parameter 'q'".to_string()))?; + + let prover_name = params.get("prover"); + + info!( + "Search request: pattern={}, prover={:?}", + pattern, prover_name + ); + + let mut all_results = Vec::new(); + + // Determine which provers to search + let provers: Vec = if let Some(name) = prover_name { + vec![parse_prover_kind(name)?] + } else { + vec![ + ProverKind::Agda, + ProverKind::Coq, + ProverKind::Lean, + ProverKind::Isabelle, + ] + }; + + // Search each prover + for prover_kind in provers { + let config = ProverConfig::default(); + + if let Ok(prover) = echidna::provers::ProverFactory::create(prover_kind, config) { + if let Ok(results) = prover.search_theorems(pattern).await { + all_results.extend(results); + } + } + } + + // Cross-prover layer (see main.rs::search_command for rationale). + // Folds VeriSimDB matches into the same response so REST clients + // see results from backends without native search commands too. + let verisim_url = + std::env::var("VERISIM_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); + if let Ok(cross) = echidna::vcl_ut::cross_prover_search_names(&verisim_url, pattern, 50).await { + all_results.extend(cross); + } + + let count = all_results.len(); + Ok(Json(SearchResponse { + results: all_results, + count, + })) +} + +// ========== Request/Response Types ========== + +#[derive(Deserialize)] +pub(super) struct SuggestRequest { + prover: ProverKind, + content: String, + limit: Option, +} + +#[derive(Serialize)] +pub(super) struct SuggestResponse { + suggestions: Vec, +} + +#[derive(Serialize)] +pub(super) struct SearchResponse { + results: Vec, + count: usize, +} diff --git a/src/rust/server/session.rs b/src/rust/server/session.rs new file mode 100644 index 00000000..9891e3b6 --- /dev/null +++ b/src/rust/server/session.rs @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell +// SPDX-License-Identifier: MPL-2.0 + +//! Proof-session routes: `/api/session/*`. + +use super::{parse_tactic_from_json, AppError, AppState, ProofSession}; +use anyhow::Result; +use axum::{ + extract::{Path, State}, + Json, +}; +use echidna::core::TacticResult; +use echidna::{ProverConfig, ProverKind}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::info; + +/// Create a new proof session +pub(super) async fn create_session( + State(state): State, + Json(req): Json, +) -> Result, AppError> { + info!("Creating session for prover: {:?}", req.prover); + + // Create prover + let config = ProverConfig::default(); + let prover = echidna::provers::ProverFactory::create(req.prover, config) + .map_err(|e| AppError::InternalError(e.to_string()))?; + + // Create session + let session_id = echidna::ids::new_record_id().to_string(); + let session = ProofSession { + prover, + state: None, + history: Vec::new(), + }; + + // Store session + state + .sessions + .write() + .await + .insert(session_id.clone(), Arc::new(Mutex::new(session))); + + Ok(Json(CreateSessionResponse { session_id })) +} + +/// Get session state +pub(super) async fn get_session_state( + State(state): State, + Path(session_id): Path, +) -> Result, AppError> { + let sessions = state.sessions.read().await; + let session = sessions + .get(&session_id) + .ok_or_else(|| AppError::NotFound(format!("Session not found: {}", session_id)))?; + + let session = session.lock().await; + + Ok(Json(SessionStateResponse { + goals: session + .state + .as_ref() + .map(|s| s.goals.len()) + .unwrap_or_else(|| 0), + complete: session + .state + .as_ref() + .map(|s| s.is_complete()) + .unwrap_or(false), + tactics_applied: session.history.len(), + })) +} + +/// Apply tactic to session +pub(super) async fn apply_tactic_handler( + State(state): State, + Path(session_id): Path, + Json(req): Json, +) -> Result, AppError> { + let sessions = state.sessions.read().await; + let session_arc = sessions + .get(&session_id) + .ok_or_else(|| AppError::NotFound(format!("Session not found: {}", session_id)))? + .clone(); + + drop(sessions); + + let mut session = session_arc.lock().await; + + // Ensure we have a state + if session.state.is_none() { + return Err(AppError::BadRequest( + "No proof loaded in session".to_string(), + )); + } + + // Parse tactic (simplified) + let tactic = parse_tactic_from_json(&req.tactic)?; + + // Apply tactic + let proof_state = session.state.as_ref().unwrap(); + let result = session + .prover + .apply_tactic(proof_state, &tactic) + .await + .map_err(|e| AppError::InternalError(e.to_string()))?; + + match result { + TacticResult::Success(new_state) => { + let complete = new_state.is_complete(); + session.state = Some(new_state); + session.history.push(req.tactic.clone()); + + Ok(Json(ApplyTacticResponse { + success: true, + complete, + goals_remaining: session.state.as_ref().unwrap().goals.len(), + message: "Tactic applied successfully".to_string(), + })) + }, + + TacticResult::Error(msg) => Ok(Json(ApplyTacticResponse { + success: false, + complete: false, + goals_remaining: session.state.as_ref().unwrap().goals.len(), + message: msg, + })), + + TacticResult::QED => { + session.state.as_mut().unwrap().goals.clear(); + + Ok(Json(ApplyTacticResponse { + success: true, + complete: true, + goals_remaining: 0, + message: "Proof complete!".to_string(), + })) + }, + } +} + +/// Get proof tree for a session +pub(super) async fn get_proof_tree( + State(state): State, + Path(session_id): Path, +) -> Result, AppError> { + let sessions = state.sessions.read().await; + let session = sessions + .get(&session_id) + .ok_or_else(|| AppError::NotFound(format!("Session not found: {}", session_id)))?; + + let session = session.lock().await; + + // Build proof tree from session state + let tree = if let Some(proof_state) = &session.state { + serde_json::json!({ + "id": "root", + "type": "goal", + "label": proof_state.goals.first() + .map(|g| format!("{:?}", g.target)) + .unwrap_or_else(|| "No goals".to_string()), + "children": [] + }) + } else { + serde_json::json!({ + "id": "root", + "type": "goal", + "label": "No proof state", + "children": [] + }) + }; + + Ok(Json(ProofTreeResponse { tree })) +} + +// ========== Request/Response Types ========== + +#[derive(Deserialize)] +pub(super) struct CreateSessionRequest { + prover: ProverKind, +} + +#[derive(Serialize)] +pub(super) struct CreateSessionResponse { + session_id: String, +} + +#[derive(Serialize)] +pub(super) struct SessionStateResponse { + goals: usize, + complete: bool, + tactics_applied: usize, +} + +#[derive(Deserialize)] +pub(super) struct ApplyTacticRequest { + tactic: String, +} + +#[derive(Serialize)] +pub(super) struct ApplyTacticResponse { + success: bool, + complete: bool, + goals_remaining: usize, + message: String, +} + +#[derive(Serialize)] +pub(super) struct ProofTreeResponse { + tree: serde_json::Value, +} diff --git a/src/rust/server/status.rs b/src/rust/server/status.rs new file mode 100644 index 00000000..ca11bb09 --- /dev/null +++ b/src/rust/server/status.rs @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell +// SPDX-License-Identifier: MPL-2.0 + +//! Discovery and status routes: Groove manifest, health, diagnostics, prover list. + +use super::{AppError, AppState}; +use axum::{extract::State, Json}; +use echidna::diagnostics::HealthStatus; +use echidna::ProverKind; +use serde::Serialize; +use serde_json::json; + +// ========== Groove Discovery ========== + +/// Groove capability manifest for ECHIDNA. +/// +/// Returns the service's capabilities in the standard Groove format so that +/// Gossamer, PanLL, and other groove-aware systems can discover ECHIDNA by +/// probing GET /.well-known/groove. See Groove.idr echidnaManifest for the +/// canonical ABI definition. +/// +/// Capabilities offered: +/// - theorem-proving: Multi-backend theorem proving (48 provers) +/// +/// Capabilities consumed: +/// - octad-storage (VeriSimDB): Persist proof artifacts as octad entities +/// - scanning (Hypatia): Trigger re-verification after codebase changes +pub(super) async fn groove_manifest() -> Json { + let core_count = echidna::ProverKind::all_core().len(); + let advertised_count = echidna::ProverKind::all().len(); + Json(json!({ + "groove_version": "1", + "service_id": "echidna", + "service_version": env!("CARGO_PKG_VERSION"), + "capabilities": { + "theorem_proving": { + "type": "theorem-proving", + "description": format!( + "Neurosymbolic theorem proving — {} core / {} advertised prover backends", + core_count, advertised_count + ), + "protocol": "http", + "endpoint": "/api/prove", + "requires_auth": false, + "panel_compatible": true + } + }, + "consumes": ["octad-storage", "scanning"], + "endpoints": { + "provers": "/api/provers", + "prove": "/api/prove", + "verify": "/api/verify", + "health": "/api/health" + }, + "health": "/api/health", + "applicability": ["individual", "team"] + })) +} + +// ========== REST API Handlers ========== + +/// Health check endpoint +pub(super) async fn health_check() -> Json { + Json(HealthResponse { + status: "ok".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }) +} + +/// Detailed diagnostics health endpoint +/// Returns full HealthStatus snapshot from dispatcher with prover metrics and degradation mode +pub(super) async fn diagnostics_health_handler( + State(app_state): State, +) -> Result, AppError> { + let health = app_state + .dispatcher + .health_status() + .map_err(|error| AppError::InternalError(error.to_string()))?; + Ok(Json(health)) +} + +/// List available provers +pub(super) async fn list_provers() -> Json { + let provers = ProverKind::all_core() + .into_iter() + .map(|prover| ProverInfo { + name: format!("{:?}", prover), + tier: prover.tier(), + complexity: prover.complexity(), + }) + .collect(); + + Json(ProversResponse { provers }) +} + +// ========== Request/Response Types ========== + +#[derive(Serialize)] +pub(super) struct HealthResponse { + status: String, + version: String, +} + +#[derive(Serialize)] +pub(super) struct ProversResponse { + provers: Vec, +} + +#[derive(Serialize)] +struct ProverInfo { + name: String, + tier: u8, + complexity: u8, +} diff --git a/src/rust/server/ui.rs b/src/rust/server/ui.rs new file mode 100644 index 00000000..74d13c88 --- /dev/null +++ b/src/rust/server/ui.rs @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell +// SPDX-License-Identifier: MPL-2.0 + +//! UI-specific routes: aspect tags, UI tactic suggestions, UI theorem search. + +use super::AppError; +use anyhow::Result; +use axum::{extract::Query, Json}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tracing::info; + +/// Get aspect tags for filtering. +/// +/// The tag vocabulary is intentionally hard-coded here — it's the UI's +/// facet list, not live-verified data, and needs to stay stable across +/// releases so saved filters keep working. +pub(super) async fn get_aspect_tags() -> Json { + let tags = vec![ + AspectTag { + name: "algebraic".to_string(), + category: "domain".to_string(), + active: false, + }, + AspectTag { + name: "geometric".to_string(), + category: "domain".to_string(), + active: false, + }, + AspectTag { + name: "logical".to_string(), + category: "domain".to_string(), + active: false, + }, + AspectTag { + name: "inductive".to_string(), + category: "technique".to_string(), + active: false, + }, + AspectTag { + name: "deductive".to_string(), + category: "technique".to_string(), + active: false, + }, + AspectTag { + name: "automated".to_string(), + category: "technique".to_string(), + active: false, + }, + ]; + + Json(AspectTagsResponse { tags }) +} + +/// UI-specific tactic suggestion endpoint +pub(super) async fn suggest_tactics_ui( + Json(req): Json, +) -> Result, AppError> { + let prover = req.prover.as_deref().unwrap_or("Coq"); + let top_k = req.top_k.unwrap_or(5); + + info!( + "UI tactic suggestion request - prover: {}, goal: {}", + prover, req.goal + ); + + // Call Julia ML API for real AI predictions + let client = reqwest::Client::new(); + let julia_url = "http://127.0.0.1:8090/suggest"; + + let julia_request = serde_json::json!({ + "goal": req.goal, + "prover": prover, + "top_k": top_k + }); + + match client.post(julia_url).json(&julia_request).send().await { + Ok(response) => { + if response.status().is_success() { + match response.json::().await { + Ok(json) => { + let empty_vec = vec![]; + let suggestions_json = json["suggestions"].as_array().unwrap_or(&empty_vec); + let suggestions: Vec = suggestions_json + .iter() + .map(|s| { + let confidence = s["confidence"].as_f64().unwrap_or(0.0); + let tactic = s["tactic"].as_str().unwrap_or("auto").to_string(); + let premise = s + .get("premises") + .and_then(|p| p.as_array()) + .and_then(|arr| arr.first()) + .and_then(|v| v.as_str()) + .map(String::from); + + // Infer aspect tags from confidence and tactic + let aspect_tags = if confidence > 0.8 { + vec!["automated".to_string()] + } else if tactic.contains("intro") { + vec!["deductive".to_string()] + } else { + vec!["algebraic".to_string(), "deductive".to_string()] + }; + + TacticSuggestion { + tactic, + confidence, + premise, + aspect_tags, + } + }) + .collect(); + + Ok(Json(SuggestTacticsUIResponse { suggestions })) + }, + Err(e) => { + info!("Failed to parse Julia response: {}", e); + // Fall back to mock data + Ok(Json(SuggestTacticsUIResponse { + suggestions: vec![TacticSuggestion { + tactic: "auto".to_string(), + confidence: 0.5, + premise: None, + aspect_tags: vec!["automated".to_string()], + }], + })) + }, + } + } else { + info!("Julia API returned error status: {}", response.status()); + // Fall back to mock data + Ok(Json(SuggestTacticsUIResponse { + suggestions: vec![TacticSuggestion { + tactic: "auto".to_string(), + confidence: 0.5, + premise: None, + aspect_tags: vec!["automated".to_string()], + }], + })) + } + }, + Err(e) => { + info!("Failed to connect to Julia ML API: {}", e); + // Fall back to mock data if Julia service is down + Ok(Json(SuggestTacticsUIResponse { + suggestions: vec![TacticSuggestion { + tactic: "auto".to_string(), + confidence: 0.5, + premise: None, + aspect_tags: vec!["automated".to_string()], + }], + })) + }, + } +} + +/// UI-specific theorem search endpoint +pub(super) async fn search_theorems_ui( + Query(params): Query>, +) -> Result, AppError> { + let query = params + .get("query") + .or_else(|| params.get("q")) + .ok_or_else(|| AppError::BadRequest("Missing query parameter".to_string()))?; + + let _tags = params + .get("tags") + .map(|t| t.split(',').map(String::from).collect::>()) + .unwrap_or_default(); + + info!("UI theorem search: query={}", query); + + // Full-corpus search lives behind the REST interface + // (`/api/search/theorems`) which indexes training_data/*.jsonl. + // This UI endpoint is the keep-alive shim for the old ReScript + // autocomplete — it returns canned exemplars so the UI has a + // stable shape, and the real query goes through the search + // workspace member. + let results = vec![ + "Theorem: associativity_add (a + b) + c = a + (b + c)".to_string(), + "Theorem: commutativity_mul a * b = b * a".to_string(), + "Lemma: distributivity a * (b + c) = a * b + a * c".to_string(), + ]; + + Ok(Json(SearchTheoremsUIResponse { results })) +} + +// ========== Request/Response Types ========== + +#[derive(Serialize)] +struct AspectTag { + name: String, + category: String, + active: bool, +} + +#[derive(Serialize)] +pub(super) struct AspectTagsResponse { + tags: Vec, +} + +#[derive(Deserialize)] +pub(super) struct SuggestTacticsUIRequest { + goal: String, // Changed from goal_id to accept goal text directly + prover: Option, + #[allow(dead_code)] // Reserved for future aspect-tag filtering + active_tags: Option>, + top_k: Option, +} + +#[derive(Serialize)] +pub(super) struct SuggestTacticsUIResponse { + suggestions: Vec, +} + +#[derive(Serialize)] +struct TacticSuggestion { + tactic: String, + confidence: f64, + premise: Option, + aspect_tags: Vec, +} + +#[derive(Serialize)] +pub(super) struct SearchTheoremsUIResponse { + results: Vec, +} diff --git a/src/rust/server/verify.rs b/src/rust/server/verify.rs new file mode 100644 index 00000000..89322375 --- /dev/null +++ b/src/rust/server/verify.rs @@ -0,0 +1,459 @@ +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell +// SPDX-License-Identifier: MPL-2.0 + +//! Verification routes: `/api/verify`, `/api/verify_parallel`, `/api/verify_raw` and the SMT passthrough helpers. + +use super::{is_empty_state, AppError}; +use anyhow::Result; +use axum::Json; +use echidna::dispatch::ProverDispatcher; +use echidna::{ProverConfig, ProverKind}; +use serde::{Deserialize, Serialize}; +use tracing::{info, instrument}; + +/// Verify proof endpoint +pub(super) async fn verify_handler( + Json(req): Json, +) -> Result, AppError> { + info!("Verify request for prover: {:?}", req.prover); + + // SMT query mode: when users send raw SMT-LIB with (check-sat) to Z3/CVC5, + // run passthrough evaluation instead of proof-obligation negation semantics. + if should_passthrough_smt_eval(&req) { + let Json(raw) = verify_raw_handler(Json(req.clone())).await?; + let smt_status = extract_smt_status(&raw.stdout); + + // A completed SMT query is not necessarily a discharged obligation. + // Only unsat closes a query interpreted as the negated obligation. + let verified = raw.valid && discharged_smt_queries(&req.content, &raw.stdout); + let goals_remaining = if verified { 0 } else { 1 }; + let outcome_str = if verified { "PROVED" } else { "NO_PROOF_FOUND" }; + return Ok(Json(VerifyResponse { + valid: verified, + outcome: outcome_str.to_string(), + goals_remaining, + tactics_used: 0, + mode: Some("smt-query".to_string()), + smt_status, + })); + } + + // Create prover + let config = ProverConfig::default(); + let prover = echidna::provers::ProverFactory::create(req.prover, config) + .map_err(|e| AppError::InternalError(e.to_string()))?; + + // Parse proof — parse errors surface as INVALID_INPUT in the outcome. + // We intentionally don't forward the raw error message to the response + // body (it may leak internal paths or prover internals). + let state = match prover.parse_string(&req.content).await { + Ok(s) => s, + Err(_) => { + return Ok(Json(VerifyResponse { + valid: false, + outcome: "INVALID_INPUT".to_string(), + goals_remaining: 0, + tactics_used: 0, + mode: None, + smt_status: None, + })); + }, + }; + + // Fail-fast on empty parse results (see prove_handler comment). + if is_empty_state(&state) && !req.content.trim().is_empty() { + return Ok(Json(VerifyResponse { + valid: false, + outcome: "INVALID_INPUT".to_string(), + goals_remaining: 0, + tactics_used: 0, + mode: None, + smt_status: None, + })); + } + + // NOTE: a richer `.check()` call returning a `ProverOutcome` taxonomy + // lived here until 2026-04-17 but the typed-outcome module was never + // committed (see z3.rs note). Reverted to `verify_proof()` so the HTTP + // API is no worse than it was before the dropped refactor; the outcome + // string is a two-valued approximation of the planned taxonomy. + // Tracked in AI-WORK-todo.md phase-2 followups. + let valid = prover + .verify_proof(&state) + .await + .map_err(|e| AppError::VerificationError(e.to_string()))?; + + Ok(Json(VerifyResponse { + valid, + outcome: if valid { + "PROVED".to_string() + } else { + "NO_PROOF_FOUND".to_string() + }, + goals_remaining: if valid { 0 } else { state.goals.len() }, + tactics_used: state.proof_script.len(), + mode: None, + smt_status: None, + })) +} + +/// `/api/verify_parallel` — L2 Chapel parallel dispatch. +/// +/// Fans out to multiple prover backends simultaneously (when the `chapel` +/// Cargo feature is compiled in and the Chapel runtime is available). +/// Returns the first successful result through the standard trust pipeline. +/// Falls back to sequential `select_prover` heuristic when Chapel is +/// unavailable. Callers that want a specific prover should use `/api/verify`. +pub(super) async fn verify_parallel_handler( + Json(req): Json, +) -> Result, AppError> { + let content = req + .get("content") + .and_then(|v| v.as_str()) + .ok_or_else(|| AppError::BadRequest("Missing 'content' field".to_string()))?; + + info!("verify_parallel request ({} bytes)", content.len()); + + let dispatcher = ProverDispatcher::new(); + let result = dispatcher + .verify_proof_parallel(content) + .await + .map_err(|e| AppError::VerificationError(e.to_string()))?; + + Ok(Json(serde_json::json!({ + "valid": result.verified, + "outcome": result.outcome.status_str(), + "provers_used": result.provers_used, + "trust_level": result.trust_level as u8, + "proof_time_ms": result.proof_time_ms, + "goals_remaining": result.goals_remaining, + "cross_checked": result.cross_checked, + "message": result.message, + }))) +} + +/// `/api/verify_raw` — writes the original `content` to a temp file with +/// the right extension for the prover, then invokes the backend binary +/// directly. Bypasses `parse_string`/`export` round-trip entirely — the +/// content you send is the content the prover sees. +/// +/// This is the real fix for the parse+export round-trip bug: no +/// information is lost between the wire and the prover binary. +/// Currently supports: z3, cvc5, coq, lean, agda, idris2, vampire, +/// eprover. Other provers fall through to the legacy verify_handler path. +#[instrument(skip(req))] +pub(super) async fn verify_raw_handler( + Json(req): Json, +) -> Result, AppError> { + use tokio::process::Command; + + info!("Verify_raw request for prover: {:?}", req.prover); + + if req.content.trim().is_empty() { + return Ok(Json(VerifyRawResponse { + valid: false, + exit_code: -1, + stdout: String::new(), + stderr: String::new(), + message: "empty content".to_string(), + })); + } + + type Interpret = fn(i32, &str, &str) -> (bool, String); + let (ext, program, args, interpret): (&str, &str, Vec, Interpret) = match req.prover { + echidna::provers::ProverKind::Z3 => ( + "smt2", + "z3", + vec!["-smt2".to_string()], + |rc, stdout, _stderr| { + let produced = stdout.contains("sat") + || stdout.contains("unsat") + || stdout.contains("unknown"); + (rc == 0 && produced, format!("exit={} smt-output", rc)) + }, + ), + echidna::provers::ProverKind::CVC5 => ("smt2", "cvc5", vec![], |rc, stdout, _stderr| { + let produced = + stdout.contains("sat") || stdout.contains("unsat") || stdout.contains("unknown"); + (rc == 0 && produced, format!("exit={} smt-output", rc)) + }), + echidna::provers::ProverKind::Coq => ( + "v", + "coqc", + vec!["-q".to_string()], + |rc, _stdout, _stderr| (rc == 0, format!("exit={}", rc)), + ), + echidna::provers::ProverKind::Lean => ("lean", "lean", vec![], |rc, _stdout, _stderr| { + (rc == 0, format!("exit={}", rc)) + }), + echidna::provers::ProverKind::Agda => ("agda", "agda", vec![], |rc, _stdout, _stderr| { + (rc == 0, format!("exit={}", rc)) + }), + echidna::provers::ProverKind::Idris2 => ( + "idr", + "idris2", + vec!["--check".to_string()], + |rc, _stdout, _stderr| (rc == 0, format!("exit={}", rc)), + ), + echidna::provers::ProverKind::Vampire => ( + "p", + "vampire", + vec!["--mode".to_string(), "casc".to_string()], + |rc, stdout, _stderr| { + let theorem = stdout.contains("SZS status Theorem") + || stdout.contains("SZS status Unsatisfiable"); + (rc == 0 || theorem, "szs-status".to_string()) + }, + ), + echidna::provers::ProverKind::EProver => ( + "p", + "eprover", + vec!["--auto-schedule".to_string()], + |rc, stdout, _stderr| { + let theorem = stdout.contains("SZS status Theorem") + || stdout.contains("SZS status Unsatisfiable") + || stdout.contains("SZS status ContradictoryAxioms"); + (rc == 0 && theorem, "szs-status".to_string()) + }, + ), + _ => { + return Ok(Json(VerifyRawResponse { + valid: false, + exit_code: -1, + stdout: String::new(), + stderr: String::new(), + message: format!( + "verify_raw not yet implemented for {:?} — use /api/verify", + req.prover + ), + })); + }, + }; + + // Write content to a unique temp file with the right extension. + let tmpdir = std::env::temp_dir().join(format!("echidna_verify_raw_{}", std::process::id())); + if let Err(e) = tokio::fs::create_dir_all(&tmpdir).await { + return Ok(Json(VerifyRawResponse { + valid: false, + exit_code: -1, + stdout: String::new(), + stderr: String::new(), + message: format!("tmpdir create failed: {}", e), + })); + } + let filename = format!("Input.{}", ext); + let path = tmpdir.join(&filename); + if let Err(e) = tokio::fs::write(&path, req.content.as_bytes()).await { + return Ok(Json(VerifyRawResponse { + valid: false, + exit_code: -1, + stdout: String::new(), + stderr: String::new(), + message: format!("write failed: {}", e), + })); + } + + // Run the prover. cd to tmpdir for provers that care about module + // name = filename (agda, coq, idris2, lean). + let mut cmd = Command::new(program); + cmd.current_dir(&tmpdir); + for arg in &args { + cmd.arg(arg); + } + cmd.arg(&filename); + + let output_result = cmd.output().await; + // Best-effort cleanup; ignore errors. + let _ = tokio::fs::remove_dir_all(&tmpdir).await; + + match output_result { + Ok(output) => { + let exit_code = output.status.code().unwrap_or(-1); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let (valid, message) = interpret(exit_code, &stdout, &stderr); + Ok(Json(VerifyRawResponse { + valid, + exit_code, + stdout: stdout.chars().take(1024).collect(), + stderr: stderr.chars().take(1024).collect(), + message, + })) + }, + Err(e) => Ok(Json(VerifyRawResponse { + valid: false, + exit_code: -1, + stdout: String::new(), + stderr: String::new(), + message: format!("spawn failed: {} (is the '{}' binary on PATH?)", e, program), + })), + } +} + +fn should_passthrough_smt_eval(req: &VerifyRequest) -> bool { + matches!(req.prover, ProverKind::Z3 | ProverKind::CVC5) + && req.content.to_ascii_lowercase().contains("(check-sat") +} + +fn extract_smt_status(text: &str) -> Option { + // Result tokens are case-sensitive, standalone SMT-LIB responses. + // A satisfiable or unknown query prevents an aggregate proof claim. + let statuses: Vec<_> = text + .lines() + .map(str::trim) + .filter(|line| matches!(*line, "sat" | "unsat" | "unknown")) + .collect(); + ["sat", "unknown", "unsat"] + .into_iter() + .find(|status| statuses.contains(status)) + .map(str::to_owned) +} + +fn discharged_smt_queries(source: &str, stdout: &str) -> bool { + let Some(commands) = smt_commands(source) else { + return false; + }; + // Z3 emits echo strings without quotes. Never authenticate an echoed + // "unsat" as a solver answer, including echo+exit before a query. + if commands + .iter() + .any(|command| matches!(*command, "echo" | "exit")) + { + return false; + } + let queries = commands + .iter() + .filter(|command| matches!(**command, "check-sat" | "check-sat-assuming")) + .count(); + let statuses: Vec<_> = stdout + .lines() + .map(str::trim) + .filter(|line| matches!(*line, "sat" | "unsat" | "unknown")) + .collect(); + queries > 0 && statuses.len() == queries && statuses.iter().all(|status| *status == "unsat") +} + +/// Read command names only; the real solver still validates SMT-LIB terms. +/// Strings, quoted symbols and line comments cannot introduce commands. +fn smt_commands(source: &str) -> Option> { + let bytes = source.as_bytes(); + let mut commands = Vec::new(); + let (mut index, mut depth) = (0, 0usize); + while index < bytes.len() { + match bytes[index] { + b';' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + }, + b'"' | b'|' => { + if depth == 0 { + return None; + } + let delimiter = bytes[index]; + index += 1; + loop { + if index == bytes.len() { + return None; + } + if bytes[index] == delimiter { + index += 1; + if delimiter == b'"' && bytes.get(index) == Some(&b'"') { + index += 1; + } else { + break; + } + } else { + index += 1; + } + } + }, + b'(' => { + if depth == 0 { + index += 1; + // Whitespace and comments are legal before the command. + loop { + while bytes.get(index).is_some_and(u8::is_ascii_whitespace) { + index += 1; + } + if bytes.get(index) != Some(&b';') { + break; + } + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + let start = index; + while bytes + .get(index) + .is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'-') + { + index += 1; + } + if index == start + || !bytes + .get(index) + .is_some_and(|b| b.is_ascii_whitespace() || matches!(b, b')' | b';')) + { + return None; + } + commands.push(&source[start..index]); + } else { + index += 1; + } + depth += 1; + }, + b')' => { + depth = depth.checked_sub(1)?; + index += 1; + }, + byte if byte.is_ascii_whitespace() => { + index += 1; + }, + _ => { + if depth == 0 { + return None; + } + index += 1; + }, + } + } + (depth == 0).then_some(commands) +} + +// ========== Request/Response Types ========== + +#[derive(Clone, Deserialize)] +pub(super) struct VerifyRequest { + prover: ProverKind, + content: String, +} + +#[derive(Serialize)] +pub(super) struct VerifyResponse { + valid: bool, + /// Typed outcome from the prover — one of PROVED, NO_PROOF_FOUND, + /// INVALID_INPUT, UNSUPPORTED_FEATURE, TIMEOUT, INCONSISTENT_PREMISES, + /// PROVER_ERROR, SYSTEM_ERROR. + /// + /// This is the primary result field; `valid` is a backward-compatible bool + /// summary derived from it (true iff outcome == PROVED). + outcome: String, + goals_remaining: usize, + tactics_used: usize, + #[serde(skip_serializing_if = "Option::is_none")] + mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + smt_status: Option, +} + +/// Raw-content verification response. Skips `parse_string`/`export` round +/// trip and invokes the prover binary directly on the supplied content. +#[derive(Serialize)] +pub(super) struct VerifyRawResponse { + valid: bool, + exit_code: i32, + stdout: String, + stderr: String, + message: String, +} diff --git a/tests/golden/hotspot/help/echidna.corpus.crossquery.txt b/tests/golden/hotspot/help/echidna.corpus.crossquery.txt new file mode 100644 index 00000000..46f8afff --- /dev/null +++ b/tests/golden/hotspot/help/echidna.corpus.crossquery.txt @@ -0,0 +1,30 @@ +Look up entries across every synonym table by `semantic_class`. Demonstrates cross-prover semantic equivalence: `corpus crossquery well-foundedness` reports `WellFounded` in Agda, `well_founded` in Coq, and `WellFounded` in Lean and Idris 2. + +The cross-prover dictionaries are searched too, so classes that only exist there (`wrapping-arithmetic`, `frame-rule`, `estate-snif`, or an MSC code such as `68Q60`) resolve. + +Usage: echidna corpus crossquery [OPTIONS] + +Arguments: + + Semantic class to look up (e.g. `well-foundedness`) + +Options: + --format + Output format (text, json) + + [default: text] + + --synonyms-dir + Synonym directory (defaults to `data/synonyms`) + + --provers-only + Only search the per-prover tables, skipping the cross-prover dictionaries + + -v, --verbose + Verbose output + + --no-color + Disable colors + + -h, --help + Print help (see a summary with '-h') diff --git a/tests/golden/hotspot/help/echidna.corpus.export-octads.txt b/tests/golden/hotspot/help/echidna.corpus.export-octads.txt new file mode 100644 index 00000000..0196962e --- /dev/null +++ b/tests/golden/hotspot/help/echidna.corpus.export-octads.txt @@ -0,0 +1,11 @@ +Convert a JSON corpus index into 8-modality octad JSONL (one DeclarationOctad per line). Streamable; compatible with VeriSim's `/api/v1/octads` endpoint when posted line-by-line + +Usage: echidna corpus export-octads [OPTIONS] --index --out + +Options: + --format Output format (text, json) [default: text] + -i, --index Path to the JSON corpus index + -o, --out Output JSONL path + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.corpus.identity.txt b/tests/golden/hotspot/help/echidna.corpus.identity.txt new file mode 100644 index 00000000..1761eac2 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.corpus.identity.txt @@ -0,0 +1,33 @@ +Group matching declarations by their *structural* identity key. + +The key is the SHA-256 of the type's canonical shape — variable names, module paths, operator spellings and binder keywords are normalised away — so `+-comm` in Agda, `Nat.add_comm` in Lean and `eq Nat (add x y) (add y x)` in Dedukti land on one key with no synonym lookup. `--skeleton` additionally prints the binder- and sort-free key, which is what provers that elide binders (Isabelle) match on. + +Usage: echidna corpus identity [OPTIONS] + +Arguments: + + Name pattern to match: a substring of the qualified or short name, case-insensitive. `*` matches every declaration + +Options: + --format + Output format (text, json) + + [default: text] + + -i, --index + Path to the corpus JSON. Defaults to `data/corpus/.json` for the cwd's basename + + --skeleton + Also print the skeleton key (binders and sorts erased) + + -v, --verbose + Verbose output + + --cross-only + Only show keys that group declarations from more than one adapter + + --no-color + Disable colors + + -h, --help + Print help (see a summary with '-h') diff --git a/tests/golden/hotspot/help/echidna.corpus.import-octads.txt b/tests/golden/hotspot/help/echidna.corpus.import-octads.txt new file mode 100644 index 00000000..12c54dfc --- /dev/null +++ b/tests/golden/hotspot/help/echidna.corpus.import-octads.txt @@ -0,0 +1,5 @@ +[exit 101] + +thread 'main' panicked: +Command import-octads: Short option names must be unique for each argument, but '-o' is in use by both 'octads' and 'out' +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/tests/golden/hotspot/help/echidna.corpus.ingest.txt b/tests/golden/hotspot/help/echidna.corpus.ingest.txt new file mode 100644 index 00000000..fea9180f --- /dev/null +++ b/tests/golden/hotspot/help/echidna.corpus.ingest.txt @@ -0,0 +1,12 @@ +Walk a project tree and build a corpus index + +Usage: echidna corpus ingest [OPTIONS] --root + +Options: + --format Output format (text, json) [default: text] + -r, --root Path to the project root + -a, --adapter Adapter to use: agda, coq, lean, idris2, acl2_books, creusot, dafny, fstar, hol4, hol_light, isabelle, metamath, minif2f, mizar, proofnet, smtlib, spark_ada, tptp, why3, or hyperpolymath_estate (the multi-repo estate profile) [default: agda] + -v, --verbose Verbose output + --no-color Disable colors + -o, --out Where to write the JSON index. Defaults to `data/corpus/.json` + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.corpus.near.txt b/tests/golden/hotspot/help/echidna.corpus.near.txt new file mode 100644 index 00000000..2cf6a38f --- /dev/null +++ b/tests/golden/hotspot/help/echidna.corpus.near.txt @@ -0,0 +1,14 @@ +Find the K nearest-neighbour entries to a free-form text query by cosine similarity over the Vector octad embeddings. The query is canonicalised the same way the corpus is, so a statement written in one prover's syntax matches entries written in another's. Useful for "find lemmas structurally near `WellFounded _<_`" + +Usage: echidna corpus near [OPTIONS] + +Arguments: + + +Options: + --format Output format (text, json) [default: text] + -i, --index + -t, --top [default: 10] + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.corpus.query.txt b/tests/golden/hotspot/help/echidna.corpus.query.txt new file mode 100644 index 00000000..8b811bcb --- /dev/null +++ b/tests/golden/hotspot/help/echidna.corpus.query.txt @@ -0,0 +1,16 @@ +Query a previously-ingested corpus index + +Usage: echidna corpus query [OPTIONS] + +Arguments: + Search term (qualified name, short name, or substring) + +Options: + --format Output format (text, json) [default: text] + -i, --index Path to the corpus JSON. Defaults to `data/corpus/.json` for the cwd's basename + --deps Show transitive (forward) dependencies of the matched entry + -v, --verbose Verbose output + --no-color Disable colors + --reverse-deps Show direct reverse-dependencies — entries that reference the matched name, i.e. things that would (potentially) break if the matched entry changed + --reverse-closure Show transitive reverse-closure — full impact set + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.corpus.stats.txt b/tests/golden/hotspot/help/echidna.corpus.stats.txt new file mode 100644 index 00000000..c0181a18 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.corpus.stats.txt @@ -0,0 +1,10 @@ +Print summary statistics for a corpus index + +Usage: echidna corpus stats [OPTIONS] + +Options: + --format Output format (text, json) [default: text] + -i, --index + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.corpus.txt b/tests/golden/hotspot/help/echidna.corpus.txt new file mode 100644 index 00000000..0f3d088c --- /dev/null +++ b/tests/golden/hotspot/help/echidna.corpus.txt @@ -0,0 +1,20 @@ +Build / query a project corpus (named decls + dependency DAG) + +Usage: echidna corpus [OPTIONS] + +Commands: + ingest Walk a project tree and build a corpus index + query Query a previously-ingested corpus index + stats Print summary statistics for a corpus index + crossquery Look up entries across every synonym table by `semantic_class`. Demonstrates cross-prover semantic equivalence: `corpus crossquery well-foundedness` reports `WellFounded` in Agda, `well_founded` in Coq, and `WellFounded` in Lean and Idris 2 + near Find the K nearest-neighbour entries to a free-form text query by cosine similarity over the Vector octad embeddings. The query is canonicalised the same way the corpus is, so a statement written in one prover's syntax matches entries written in another's. Useful for "find lemmas structurally near `WellFounded _<_`" + identity Group matching declarations by their *structural* identity key + export-octads Convert a JSON corpus index into 8-modality octad JSONL (one DeclarationOctad per line). Streamable; compatible with VeriSim's `/api/v1/octads` endpoint when posted line-by-line + import-octads Load an octad JSONL stream back into a corpus and persist as JSON. Round-trip is best-effort but lossless for the fields the corpus cares about (statements, proofs, deps, hazards) + help Print this message or the help of the given subcommand(s) + +Options: + --format Output format (text, json) [default: text] + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.design.search.txt b/tests/golden/hotspot/help/echidna.design.search.txt new file mode 100644 index 00000000..ee7c70a5 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.design.search.txt @@ -0,0 +1,18 @@ +Run SA over a named design problem + +Usage: echidna design search [OPTIONS] + +Arguments: + Problem identifier. Currently: `brouwer-leq` (Phase 1.3 `_≤_` redesign for `Ordinal.Brouwer`) + +Options: + --format Output format (text, json) [default: text] + --iterations Iterations per restart [default: 1000] + --restarts Number of independent restarts [default: 4] + -v, --verbose Verbose output + --no-color Disable colors + --temp Initial Metropolis temperature [default: 4] + --cooling Multiplicative cooling factor per iteration [default: 0.995] + --seed Seed [default: 12648430] + --top Top-K candidates to print [default: 8] + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.design.swarm.txt b/tests/golden/hotspot/help/echidna.design.swarm.txt new file mode 100644 index 00000000..6b88301a --- /dev/null +++ b/tests/golden/hotspot/help/echidna.design.swarm.txt @@ -0,0 +1,30 @@ +Run a parallel swarm of SA agents with cross-pollination + +Usage: echidna design swarm [OPTIONS] + +Arguments: + Problem identifier. Currently: `brouwer-leq` + +Options: + --agents + Number of parallel agents [default: 4] + --format + Output format (text, json) [default: text] + --iterations + [default: 1000] + -v, --verbose + Verbose output + --no-color + Disable colors + --temp + [default: 4] + --cooling + [default: 0.995] + --seed + [default: 12648430] + --broadcast-every + Broadcast best-so-far every N iterations [default: 100] + --adopt-threshold + Adopt a peer's broadcast only if it beats the agent's best by this lex-margin (0 = adopt on any improvement) [default: 0] + -h, --help + Print help diff --git a/tests/golden/hotspot/help/echidna.design.txt b/tests/golden/hotspot/help/echidna.design.txt new file mode 100644 index 00000000..9be56bdf --- /dev/null +++ b/tests/golden/hotspot/help/echidna.design.txt @@ -0,0 +1,14 @@ +Search proof-design space via simulated annealing + +Usage: echidna design [OPTIONS] + +Commands: + search Run SA over a named design problem + swarm Run a parallel swarm of SA agents with cross-pollination + help Print this message or the help of the given subcommand(s) + +Options: + --format Output format (text, json) [default: text] + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.diagnostics.txt b/tests/golden/hotspot/help/echidna.diagnostics.txt new file mode 100644 index 00000000..e74c7f87 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.diagnostics.txt @@ -0,0 +1,9 @@ +Interactive diagnostics REPL for health monitoring + +Usage: echidna diagnostics [OPTIONS] + +Options: + --format Output format (text, json) [default: text] + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.info.txt b/tests/golden/hotspot/help/echidna.info.txt new file mode 100644 index 00000000..cf69a01f --- /dev/null +++ b/tests/golden/hotspot/help/echidna.info.txt @@ -0,0 +1,12 @@ +Show information about a specific prover + +Usage: echidna info [OPTIONS] + +Arguments: + Prover name + +Options: + --format Output format (text, json) [default: text] + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.interactive.txt b/tests/golden/hotspot/help/echidna.interactive.txt new file mode 100644 index 00000000..37b75548 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.interactive.txt @@ -0,0 +1,14 @@ +Start interactive REPL mode + +Usage: echidna interactive [OPTIONS] [FILE] + +Arguments: + [FILE] Initial proof file to load + +Options: + --format Output format (text, json) [default: text] + -p, --prover Prover to use + --neural Enable neural premise selection + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.lexicon.scan.txt b/tests/golden/hotspot/help/echidna.lexicon.scan.txt new file mode 100644 index 00000000..0357005c --- /dev/null +++ b/tests/golden/hotspot/help/echidna.lexicon.scan.txt @@ -0,0 +1,29 @@ +Pair theorem names and canonical statement shapes across training corpora and write review candidates as JCS JSON Lines. Every candidate carries `evidence = none`; `data/synonyms/` is never edited + +Usage: echidna lexicon scan [OPTIONS] + +Options: + --format + Output format (text, json) [default: text] + --training-dir + Directory holding `proof_states_.jsonl` files [default: training_data] + --pair + A corpus pair, `A:B`. Repeatable; combines with `--preset` + -v, --verbose + Verbose output + --no-color + Disable colors + --preset + A named pair set: `learning`, `spanning`, `in-family` or `full` + --out + Output directory (pairs/, collisions/, summary.jsonl, coverage.jsonl) [default: target/lexicon-scan] + --keep-generic + Also emit pairs whose shared name is generic (`lemma3`, `aux`, `ex1`) + --max-pairs-per-name + Skip a shared name whose statement cross product exceeds this: exact pairs per raw name, folded pairs per whole folded group [default: 64] + --max-shape-group + Skip a shape or skeleton group larger than this on either side [default: 8] + --collision-samples + Key-collision groups to sample per corpus [default: 40] + -h, --help + Print help diff --git a/tests/golden/hotspot/help/echidna.lexicon.txt b/tests/golden/hotspot/help/echidna.lexicon.txt new file mode 100644 index 00000000..19e28861 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.lexicon.txt @@ -0,0 +1,13 @@ +Mine lexicon candidates across training corpora (flag-only) + +Usage: echidna lexicon [OPTIONS] + +Commands: + scan Pair theorem names and canonical statement shapes across training corpora and write review candidates as JCS JSON Lines. Every candidate carries `evidence = none`; `data/synonyms/` is never edited + help Print this message or the help of the given subcommand(s) + +Options: + --format Output format (text, json) [default: text] + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.list-provers.txt b/tests/golden/hotspot/help/echidna.list-provers.txt new file mode 100644 index 00000000..0f1e2ef9 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.list-provers.txt @@ -0,0 +1,10 @@ +List available provers + +Usage: echidna list-provers [OPTIONS] + +Options: + -d, --detailed Show detailed information + --format Output format (text, json) [default: text] + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.prove.txt b/tests/golden/hotspot/help/echidna.prove.txt new file mode 100644 index 00000000..14f05097 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.prove.txt @@ -0,0 +1,59 @@ +Prove a theorem from file + +Usage: echidna prove [OPTIONS] + +Arguments: + + Path to proof file + +Options: + --format + Output format (text, json) + + [default: text] + + -p, --prover + Prover to use (auto-detect if not specified) + + -t, --timeout + Timeout in seconds + + [default: 300] + + -v, --verbose + Verbose output + + --neural + Enable neural premise selection + + --no-color + Disable colors + + --executable + Prover executable path (override default) + + --library + Library paths + + --diagnose + On failure, run diagnostics and explain why the proof failed + + --project-root + Project root for backends with session-based builds (EI-1). Today: Isabelle uses this to add `-d ` to the build invocation and to import the file's session from the project's existing ROOT, so theory imports resolve + + --sandbox + Sandbox mode for prover invocation (safe-learning b). One of: none (default), bwrap, podman + + [default: none] + + --output + Result form. `human` (default) prints the usual report; `json` prints exactly one `echidna.prove.result/1` object (JCS-canonical I-JSON) on stdout and sends logs to stderr + + Possible values: + - human: Human-readable report (the default) + - json: One `echidna.prove.result/1` JSON object on stdout + + [default: human] + + -h, --help + Print help (see a summary with '-h') diff --git a/tests/golden/hotspot/help/echidna.search.txt b/tests/golden/hotspot/help/echidna.search.txt new file mode 100644 index 00000000..c507806e --- /dev/null +++ b/tests/golden/hotspot/help/echidna.search.txt @@ -0,0 +1,14 @@ +Search theorem libraries + +Usage: echidna search [OPTIONS] + +Arguments: + Search pattern (regex supported) + +Options: + --format Output format (text, json) [default: text] + -p, --prover Prover to search (searches all if not specified) + -l, --limit Maximum results to return [default: 20] + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.server.txt b/tests/golden/hotspot/help/echidna.server.txt new file mode 100644 index 00000000..76bb6810 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.server.txt @@ -0,0 +1,12 @@ +Start HTTP API server + +Usage: echidna server [OPTIONS] + +Options: + --format Output format (text, json) [default: text] + -p, --port Port to bind to [default: 8081] + --host Host to bind to [default: 127.0.0.1] + -v, --verbose Verbose output + --cors Enable CORS + --no-color Disable colors + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.suggest.txt b/tests/golden/hotspot/help/echidna.suggest.txt new file mode 100644 index 00000000..ef2bc7ab --- /dev/null +++ b/tests/golden/hotspot/help/echidna.suggest.txt @@ -0,0 +1,18 @@ +Suggest tactic variants that close a failing lemma + +Usage: echidna suggest [OPTIONS] + +Arguments: + Target in `:` form, e.g. `Foo.thy:bar` + +Options: + --format Output format (text, json) [default: text] + -p, --prover Prover to use (auto-detected from file extension if absent) + --budget Time budget per variant attempt in seconds [default: 60] + -v, --verbose Verbose output + --no-color Disable colors + --top Maximum number of variants to report [default: 10] + --synonyms-dir Synonym table directory (defaults to `data/synonyms/`) + --dry-run Don't run the prover; just print candidate variants + --max-parallel Maximum concurrent variant tests [default: 4] + -h, --help Print help diff --git a/tests/golden/hotspot/help/echidna.txt b/tests/golden/hotspot/help/echidna.txt new file mode 100644 index 00000000..caca4e18 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.txt @@ -0,0 +1,25 @@ +Neurosymbolic theorem proving platform with multi-prover support + +Usage: echidna [OPTIONS] + +Commands: + prove Prove a theorem from file + verify Verify an existing proof + search Search theorem libraries + interactive Start interactive REPL mode + server Start HTTP API server + list-provers List available provers + info Show information about a specific prover + diagnostics Interactive diagnostics REPL for health monitoring + corpus Build / query a project corpus (named decls + dependency DAG) + lexicon Mine lexicon candidates across training corpora (flag-only) + design Search proof-design space via simulated annealing + suggest Suggest tactic variants that close a failing lemma + help Print this message or the help of the given subcommand(s) + +Options: + --format Output format (text, json) [default: text] + -v, --verbose Verbose output + --no-color Disable colors + -h, --help Print help + -V, --version Print version diff --git a/tests/golden/hotspot/help/echidna.verify.txt b/tests/golden/hotspot/help/echidna.verify.txt new file mode 100644 index 00000000..ed440697 --- /dev/null +++ b/tests/golden/hotspot/help/echidna.verify.txt @@ -0,0 +1,17 @@ +Verify an existing proof + +Usage: echidna verify [OPTIONS] + +Arguments: + Path to proof file + +Options: + --format Output format (text, json) [default: text] + -p, --prover Prover to use (auto-detect if not specified) + -t, --timeout Timeout in seconds [default: 300] + -v, --verbose Verbose output + --executable Prover executable path (override default) + --no-color Disable colors + --library Library paths + --diagnose On failure, run diagnostics and explain why the proof failed + -h, --help Print help diff --git a/tests/golden/hotspot/just-list.txt b/tests/golden/hotspot/just-list.txt new file mode 100644 index 00000000..8e39902a --- /dev/null +++ b/tests/golden/hotspot/just-list.txt @@ -0,0 +1,166 @@ +Available recipes: + abi-fixtures # Type-check the Idris2 ABI accept/reject fixtures (mechanical falsifier). + adjust-cargo-outdated + adjust-chapel-version + adjust-check + adjust-julia-channel + adjust-julia-outdated + adjust-rust-toolchain + adjust-shas-stale + align-premises # dataloader's proof_id join matches ~0% of records. + arbiter-smoke # Prints posterior verdict from each of: Bayesian, Dempster-Shafer, Pareto. + assail # Run panic-attacker pre-commit scan + audit # Audit dependencies + bench-chapel-mrr # both source builds and asdf-shimmed installs. + bench-chapel-mrr-telemetry # opening the 360-row telemetry file. + build # Build debug + build-chapel # Build the feature against explicit unavailable stubs. + build-chapel-ffi # Build Zig FFI bridge for Chapel (prerequisite for --features chapel) + build-chapel-real # Build the real static bridge and let Cargo consume Chapel's runtime closure. + build-release # Build release + build-riscv # [AUTO-GENERATED] Multi-arch / RISC-V target + build-spark # Step 4: build Rust with SPARK feature enabled + build-spark-ada # Step 1: compile Ada/SPARK to static library (requires GNAT + GNATprove) + build-spark-zig # Step 3: compile Zig bridge (links against libechidna_spark.a) + build-ui # Build the UI — blocked on the AffineScript-TEA toolchain + bust-cargo-build + bust-cargo-cache + bust-cargo-test + bust-chapel-artefacts + bust-check + bust-idris2-abi + bust-prover-binaries + bust-target-dir + bust-zig-cache + capnp-gen # Output: src/rust/ipc/echidna_capnp.rs (checked in; do not edit by hand) + chapel-all # Full local real-library path, with the independent stub bridge unit tests. + chapel-build # longer explanation and #133 for the closure trace. + chapel-pic-from-source # docs/decisions/2026-05-30-chapel-pic-rebuild.adoc. NOT a CI step. + chapel-smoke # CAS + reduction. Used in CI as the toolchain-health signal. + chapel-test # Run the Chapel-gated Rust tests (`--features chapel`). + chapel-test-real + check # Check without building + clean # Clean build artifacts + container-build # Build minimal container image (Z3, CVC5, Lean, Idris2) + container-build-acl2 + container-build-full # Build full container image (all provers + Julia) + container-build-hol4 + container-build-imandra # Build imandra stub (proprietary; real binary requires a signed licence — see Containerfile.wave3 imandra target) + container-build-metamath + container-build-or-tools + container-build-proverif + container-build-scip + container-build-tamarin + container-build-tier3 # Build all non-proprietary Tier-3 prover containers (skip imandra — licence required) + container-build-twelf + container-run *ARGS # Run echidna container + contractile-all # Run every contractile family check (must + trust + adjust-check + bust-check + dust) + contractile-tour # Show the error-code inventory across the family + corpora-status # Report which upstream corpora are on disk and their sizes. + corpus-cross class # Cross-prover semantic-class lookup + corpus-impact name index="data/corpus/agda.json" # Query the corpus by name with full reverse-dep impact set + corpus-ingest-agda root # Ingest an Agda project into the corpus + corpus-ingest-coq root # Ingest a Coq project into the corpus + corpus-ingest-creusot root # `#[trusted]` is reported as a postulate, not as a proved function. + corpus-ingest-estate root=".." # ROOT is the directory that contains the repositories. + corpus-ingest-idris root # Ingest an Idris 2 project into the corpus + corpus-ingest-lean root # Ingest a Lean 4 project into the corpus + corpus-ingest-saturation adapter root # fstar / acl2_books / tptp / smtlib / proofnet / minif2f). + corpus-ingest-spark root # sparklib `Lemma_*` convention. + # Cosine-similarity nearest-neighbour query + corpus-near query index="data/corpus/agda.json" top="10" + # Export a JSON index as 8-modality octad JSONL (VeriSim-compatible) + corpus-octads in="data/corpus/agda.json" out="data/corpus/agda.octads.jsonl" + corpus-refresh # Use `ECHIDNA_MAX_PROOF_STATES=0 just corpus-refresh` to lift the sample cap. + corpus-stats # Report corpus balance across provers from stats_UNIFIED.json. + corpus-stats-all # data/corpus/*.json (skips any missing without failing). + crg-badge # Looks for '**Current Grade:** X' in READINESS.md; falls back to X + crg-grade # Print the current CRG grade (reads from READINESS.md '**Current Grade:** X' line) + default # Show all recipes + # Run the SA design-search on a named problem + design-search problem="brouwer-leq" iterations="2000" top="10" + # Run the parallel-swarm design-search + design-swarm problem="brouwer-leq" agents="4" iterations="800" + doc # Generate docs + doctor # Check that all required tools are installed and working + dust-archive-old-models + dust-check + dust-dangling-worktrees + dust-orphan-pids + dust-prune-worktrees + dust-source-rollback + dust-source-todos + dust-stale-branches + er-schema-drift-check # "Drift detection".) + eval # if MRR < 0.66 (below the cosine baseline). + extract-corpora *NAMES # Pass names to run a subset: `just extract-corpora metamath mathlib4`. + fmt # Format code + fmt-check # Check formatting + gui # Launch full ECHIDNA GUI (Backend + UI + Browser) + heal # Show install instructions for missing tools + heal-provers + help-me # Show available recipes with descriptions + init # Initialize from RSR template (placeholder replacement) + intend-list + invariant-path *ARGS # Run Invariant Path overlay tools for this repository + lexicon-check # synonym table and to the accept/reject fixtures. Needs nickel >= 1.12. + lexicon-scan preset="learning" # spanning, in-family, full. + lint # Run clippy lints + merge-corpora # with fresh sequential ids (dedupes by prover+theorem). + metrics # metrics/README.adoc. + must-check + must-license-present + must-no-banned-files + must-readme-present + must-spdx-headers + mvp # MVP 1.0 smoke checks + mvp-env # MVP 1.0 dependency checklist (best-effort) + pre-commit # All checks before commit + proofs # Type-check the whole dogfood proof corpus across every assistant. + proofs-agda # Type-check the Agda proof corpus (proofs/agda/*.agda). + proofs-coq # Compile the Coq proof corpus (proofs/coq/**/*.v). + proofs-idris # Type-check the Idris2 validator (src/idris) — --typecheck needs no codegen backend. + proofs-isabelle # Verify the Isabelle/HOL self-proof corpus (proofs/isabelle) via `isabelle build`. + proofs-lean # Build the Lean 4 proof corpus via Lake (proofs/lean). + proofs-mizar # Verify the Mizar self-proof corpus (proofs/mizar) with the Mizar verifier. + proofs-verif-idris # Type-check the Idris2 trust-pipeline property proofs (verification/proofs/idris2). + proofs-verif-lean # Build the Lean 4 trust-pipeline property proofs via Lake (verification/proofs/lean4). + prove-spark # Step 2: run GNATprove to formally verify the SPARK contracts + provision-corpora *NAMES # Pass specific names to provision a subset: `just provision-corpora metamath mathlib4`. + retrain # align-premises is idempotent and cheap (seconds on a 2M premise set). + retrain-skip-align # just align-premises manually and want to iterate on model code). + run *ARGS # Run the project + secret-scan-trufflehog + serve-ui # Bun is the estate JavaScript runtime; Deno is retired (2026-09-22 ruling). + spark-all # Full SPARK pipeline: prove → build → test + synonym-load-test # _verification_systems) and report counts. + test # Run unit tests + test-all # Run all tests (unit + integration) + test-chapel # Alias retained for muscle-memory; same as chapel-test. + test-chapel-ffi # Test Zig FFI bridge independently + test-gates # Self-test the wave gates (scripts/gates/*.selftest.sh): fixtures, no network + test-integration # Run integration tests only + test-neural # Run neural integration tests (requires Julia server) + test-s4-loop # the test will exercise the full record_proof_attempt → query_by_goal_hash loop. + test-saturation # Run the saturation-campaign cargo tests only (139 cases + 1 ignored). + test-trust-pipeline # Runs on any Rust version; does not require Creusot or Why3. + test-verbose # Run tests verbose + tour # Guided tour of the repository structure + train # updates models/model_metadata.txt, and appends to training_data/metrics_baseline.jsonl. + train-cpu # Safe to run on any dev box without a GPU. Produces the same artefacts as `train`. + train-from-corpus prover="lean" # data/corpus/ first. Per-prover invocation; default lean. + trust-axiom-tracker + trust-cargo-lock-tracked + trust-gpg-signed + trust-license-content + trust-no-banned-token-in-abi + trust-no-fake-shas + trust-no-secrets + trust-sandbox-enforced + trust-shas-pinned + trust-totality-enforced + trust-verify + update # Update dependencies + verify-spark-crosscheck # Cross-check: run both Rust and SPARK enforce_policy, assert agreement + verify-trust-pipeline # results but does not block the merge until Stage 8c-M3. + vocab-canon # src/julia/training/dataloader.jl:build_vocabulary_from_data. diff --git a/tests/golden/hotspot/router-composition.txt b/tests/golden/hotspot/router-composition.txt new file mode 100644 index 00000000..a5d8220f --- /dev/null +++ b/tests/golden/hotspot/router-composition.txt @@ -0,0 +1,4 @@ +groove: Router::new() +server: .with_state(state); +server: app = app.layer(CorsLayer::permissive()); +server: let mut app = Router::new() diff --git a/tests/golden/hotspot/routes.txt b/tests/golden/hotspot/routes.txt new file mode 100644 index 00000000..e6445d0c --- /dev/null +++ b/tests/golden/hotspot/routes.txt @@ -0,0 +1,20 @@ +groove: .route("/.well-known/groove", get(groove_manifest)) +groove: .route("/health", get(health_check)) +server: .route("/.well-known/groove", get(groove_manifest)) +server: .route("/api/agent/plan", post(agent_plan_handler)) +server: .route("/api/aspect-tags", get(get_aspect_tags)) +server: .route("/api/diagnostics/health", get(diagnostics_health_handler)) +server: .route("/api/health", get(health_check)) +server: .route("/api/prove", post(prove_handler)) +server: .route("/api/provers", get(list_provers)) +server: .route("/api/search", get(search_handler)) +server: .route("/api/session/create", post(create_session)) +server: .route("/api/session/{id}/apply", post(apply_tactic_handler)) +server: .route("/api/session/{id}/state", get(get_session_state)) +server: .route("/api/session/{id}/tree", get(get_proof_tree)) +server: .route("/api/suggest", post(suggest_handler)) +server: .route("/api/tactics/suggest", post(suggest_tactics_ui)) +server: .route("/api/theorems/search", get(search_theorems_ui)) +server: .route("/api/verify", post(verify_handler)) +server: .route("/api/verify_parallel", post(verify_parallel_handler)) +server: .route("/api/verify_raw", post(verify_raw_handler))