From 0e022d63df12bdf76c84348583a540952bd8cf46 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:31:56 +0000 Subject: [PATCH 1/5] test(split): capture hotspot goldens on the pre-split tree scripts/gates/hotspot-goldens.sh capture was run at 7236e9d, the integ lane head, before any file moves. It records: - 23 help texts, every subcommand to depth 3; - the .route table (20 lines); - the router composition (4 lines); - just --list (166 lines). W0 exit check (5) diffs against these, so the split must leave them unchanged. Never re-capture to silence a diff. `echidna corpus import-octads --help` exits 101 (a panic). The golden records that as behaviour. It is a separate defect, not fixed here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WS4mfQeC3N1DWjcCQJaQqw --- .../help/echidna.corpus.crossquery.txt | 30 ++++ .../help/echidna.corpus.export-octads.txt | 11 ++ .../hotspot/help/echidna.corpus.identity.txt | 33 ++++ .../help/echidna.corpus.import-octads.txt | 5 + .../hotspot/help/echidna.corpus.ingest.txt | 12 ++ .../hotspot/help/echidna.corpus.near.txt | 14 ++ .../hotspot/help/echidna.corpus.query.txt | 16 ++ .../hotspot/help/echidna.corpus.stats.txt | 10 ++ tests/golden/hotspot/help/echidna.corpus.txt | 20 +++ .../hotspot/help/echidna.design.search.txt | 18 ++ .../hotspot/help/echidna.design.swarm.txt | 30 ++++ tests/golden/hotspot/help/echidna.design.txt | 14 ++ .../hotspot/help/echidna.diagnostics.txt | 9 + tests/golden/hotspot/help/echidna.info.txt | 12 ++ .../hotspot/help/echidna.interactive.txt | 14 ++ .../hotspot/help/echidna.lexicon.scan.txt | 29 +++ tests/golden/hotspot/help/echidna.lexicon.txt | 13 ++ .../hotspot/help/echidna.list-provers.txt | 10 ++ tests/golden/hotspot/help/echidna.prove.txt | 59 +++++++ tests/golden/hotspot/help/echidna.search.txt | 14 ++ tests/golden/hotspot/help/echidna.server.txt | 12 ++ tests/golden/hotspot/help/echidna.suggest.txt | 18 ++ tests/golden/hotspot/help/echidna.txt | 25 +++ tests/golden/hotspot/help/echidna.verify.txt | 17 ++ tests/golden/hotspot/just-list.txt | 166 ++++++++++++++++++ tests/golden/hotspot/router-composition.txt | 4 + tests/golden/hotspot/routes.txt | 20 +++ 27 files changed, 635 insertions(+) create mode 100644 tests/golden/hotspot/help/echidna.corpus.crossquery.txt create mode 100644 tests/golden/hotspot/help/echidna.corpus.export-octads.txt create mode 100644 tests/golden/hotspot/help/echidna.corpus.identity.txt create mode 100644 tests/golden/hotspot/help/echidna.corpus.import-octads.txt create mode 100644 tests/golden/hotspot/help/echidna.corpus.ingest.txt create mode 100644 tests/golden/hotspot/help/echidna.corpus.near.txt create mode 100644 tests/golden/hotspot/help/echidna.corpus.query.txt create mode 100644 tests/golden/hotspot/help/echidna.corpus.stats.txt create mode 100644 tests/golden/hotspot/help/echidna.corpus.txt create mode 100644 tests/golden/hotspot/help/echidna.design.search.txt create mode 100644 tests/golden/hotspot/help/echidna.design.swarm.txt create mode 100644 tests/golden/hotspot/help/echidna.design.txt create mode 100644 tests/golden/hotspot/help/echidna.diagnostics.txt create mode 100644 tests/golden/hotspot/help/echidna.info.txt create mode 100644 tests/golden/hotspot/help/echidna.interactive.txt create mode 100644 tests/golden/hotspot/help/echidna.lexicon.scan.txt create mode 100644 tests/golden/hotspot/help/echidna.lexicon.txt create mode 100644 tests/golden/hotspot/help/echidna.list-provers.txt create mode 100644 tests/golden/hotspot/help/echidna.prove.txt create mode 100644 tests/golden/hotspot/help/echidna.search.txt create mode 100644 tests/golden/hotspot/help/echidna.server.txt create mode 100644 tests/golden/hotspot/help/echidna.suggest.txt create mode 100644 tests/golden/hotspot/help/echidna.txt create mode 100644 tests/golden/hotspot/help/echidna.verify.txt create mode 100644 tests/golden/hotspot/just-list.txt create mode 100644 tests/golden/hotspot/router-composition.txt create mode 100644 tests/golden/hotspot/routes.txt 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..e776a1e2 --- /dev/null +++ b/tests/golden/hotspot/router-composition.txt @@ -0,0 +1,4 @@ +.with_state(state); +Router::new() +app = app.layer(CorsLayer::permissive()); +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..894b53ff --- /dev/null +++ b/tests/golden/hotspot/routes.txt @@ -0,0 +1,20 @@ +.route("/.well-known/groove", get(groove_manifest)) +.route("/.well-known/groove", get(groove_manifest)) +.route("/api/agent/plan", post(agent_plan_handler)) +.route("/api/aspect-tags", get(get_aspect_tags)) +.route("/api/diagnostics/health", get(diagnostics_health_handler)) +.route("/api/health", get(health_check)) +.route("/api/prove", post(prove_handler)) +.route("/api/provers", get(list_provers)) +.route("/api/search", get(search_handler)) +.route("/api/session/create", post(create_session)) +.route("/api/session/{id}/apply", post(apply_tactic_handler)) +.route("/api/session/{id}/state", get(get_session_state)) +.route("/api/session/{id}/tree", get(get_proof_tree)) +.route("/api/suggest", post(suggest_handler)) +.route("/api/tactics/suggest", post(suggest_tactics_ui)) +.route("/api/theorems/search", get(search_theorems_ui)) +.route("/api/verify", post(verify_handler)) +.route("/api/verify_parallel", post(verify_parallel_handler)) +.route("/api/verify_raw", post(verify_raw_handler)) +.route("/health", get(health_check)) From 74b87310dd80000199757c8c902ccacaeb2fc849 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:34:09 +0000 Subject: [PATCH 2/5] refactor(server): split server.rs into server/{mod,agent,prove,search,session,status,ui,verify}.rs This is a pure move (ultraplan NEW:hotspot-split). Handlers move into modules by concern, mod.rs keeps AppState, the router and the shared helpers, and each module imports what it uses explicitly from super::. Visibility is pub(super); nothing new is exported. The move is the one prepared in bf79c02. Three changes that landed on main afterwards are carried over: diagnostics_health_handler and the two prove_handler record_prover_result calls now propagate dispatcher errors as AppError::InternalError. Checked: - cargo check --bin echidna is clean, with and without --features verisim. - Comparing line multisets of the original server.rs and the new modules, ignoring use lines and visibility, leaves only import regrouping, a wrapped verify_handler signature, and cfg guards on imports that only the non-verisim path uses. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WS4mfQeC3N1DWjcCQJaQqw --- src/rust/server.rs | 1637 ------------------------------------ src/rust/server/agent.rs | 106 +++ src/rust/server/mod.rs | 348 ++++++++ src/rust/server/prove.rs | 142 ++++ src/rust/server/search.rs | 153 ++++ src/rust/server/session.rs | 213 +++++ src/rust/server/status.rs | 114 +++ src/rust/server/ui.rs | 227 +++++ src/rust/server/verify.rs | 459 ++++++++++ 9 files changed, 1762 insertions(+), 1637 deletions(-) delete mode 100644 src/rust/server.rs create mode 100644 src/rust/server/agent.rs create mode 100644 src/rust/server/mod.rs create mode 100644 src/rust/server/prove.rs create mode 100644 src/rust/server/search.rs create mode 100644 src/rust/server/session.rs create mode 100644 src/rust/server/status.rs create mode 100644 src/rust/server/ui.rs create mode 100644 src/rust/server/verify.rs 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, +} From 1d4ea7865a2547c9c1cf95fabcebdf073f57b04d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:37:20 +0000 Subject: [PATCH 3/5] refactor(cli): split main.rs into cli/{args,util,prove,verify,search,session,provers,lexicon,corpus,design}.rs This is a pure move (ultraplan NEW:hotspot-split). main.rs keeps only main(): it parses Cli and dispatches. - cli/args.rs holds the clap definitions (Cli, Commands, CorpusOp, LexiconOp, DesignOp, ProveOutput). - cli/util.rs holds the shared helpers. - Each other module holds one command group, unchanged. Moved items become pub(crate) so main() can reach them, and Cli's fields become pub(crate) for the same reason. Imports were narrowed per module with cargo fix, plus six removed by hand, so nothing is allow-listed. Checked: - cargo check --bin echidna --all-targets has no warnings. - With --features verisim, only the pre-existing ProveRequest never-read warning remains (also on main). - Comparing line multisets of the original main.rs and the new files, ignoring use, mod and visibility, leaves only one signature that rustfmt wrapped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WS4mfQeC3N1DWjcCQJaQqw --- src/rust/cli/args.rs | 438 +++++++++ src/rust/cli/corpus.rs | 455 +++++++++ src/rust/cli/design.rs | 206 ++++ src/rust/cli/lexicon.rs | 152 +++ src/rust/cli/mod.rs | 28 + src/rust/cli/prove.rs | 179 ++++ src/rust/cli/provers.rs | 286 ++++++ src/rust/cli/search.rs | 103 ++ src/rust/cli/session.rs | 41 + src/rust/cli/util.rs | 150 +++ src/rust/cli/verify.rs | 60 ++ src/rust/main.rs | 1985 +-------------------------------------- 12 files changed, 2103 insertions(+), 1980 deletions(-) create mode 100644 src/rust/cli/args.rs create mode 100644 src/rust/cli/corpus.rs create mode 100644 src/rust/cli/design.rs create mode 100644 src/rust/cli/lexicon.rs create mode 100644 src/rust/cli/mod.rs create mode 100644 src/rust/cli/prove.rs create mode 100644 src/rust/cli/provers.rs create mode 100644 src/rust/cli/search.rs create mode 100644 src/rust/cli/session.rs create mode 100644 src/rust/cli/util.rs create mode 100644 src/rust/cli/verify.rs 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 From d9f47203b5c670f4a70127fef403da19a1d4270a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:38:45 +0000 Subject: [PATCH 4/5] refactor(just): split the Justfile into just/*.just, one file per area This is a pure move (ultraplan NEW:hotspot-split). The root Justfile keeps the settings, the project variable and the default recipe, and imports just/{init,build,proofs,dev,corpus,ui,containers,chapel,spark,creusot, other,onboarding,corpus-design,saturation}.just in the order the sections used to appear. The optional contractile.just import moves to the end. Its recipes then list after the project recipes, as before. Without that, `just` (which runs --list --unsorted) would show them first. Checked: - `just --list` and `just --list --unsorted` are byte-identical to their output before the split. - `just --evaluate project` and a dry run of a moved recipe resolve. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WS4mfQeC3N1DWjcCQJaQqw --- Justfile | 1229 +-------------------------------------- just/build.just | 46 ++ just/chapel.just | 134 +++++ just/containers.just | 44 ++ just/corpus-design.just | 46 ++ just/corpus.just | 131 +++++ just/creusot.just | 26 + just/dev.just | 38 ++ just/init.just | 197 +++++++ just/onboarding.just | 249 ++++++++ just/other.just | 13 + just/proofs.just | 114 ++++ just/saturation.just | 95 +++ just/spark.just | 35 ++ just/ui.just | 67 +++ 15 files changed, 1255 insertions(+), 1209 deletions(-) create mode 100644 just/build.just create mode 100644 just/chapel.just create mode 100644 just/containers.just create mode 100644 just/corpus-design.just create mode 100644 just/corpus.just create mode 100644 just/creusot.just create mode 100644 just/dev.just create mode 100644 just/init.just create mode 100644 just/onboarding.just create mode 100644 just/other.just create mode 100644 just/proofs.just create mode 100644 just/saturation.just create mode 100644 just/spark.just create mode 100644 just/ui.just 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 . From d06d8586066ca8caf107821d73958b0b931cc0e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 16:22:35 +0000 Subject: [PATCH 5/5] test(split): owner-labelled route goldens, recaptured from the pre-split tree hotspot-goldens.sh now prefixes each .route/.merge/.nest line with its owning module (first component under src/rust/). These goldens were captured on the pre-split tree (535274c) with that script and match the post-split tree exactly: 18 server routes and 2 groove routes, so a route moved to a different router now shows as a diff. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WS4mfQeC3N1DWjcCQJaQqw --- tests/golden/hotspot/router-composition.txt | 8 ++--- tests/golden/hotspot/routes.txt | 40 ++++++++++----------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/golden/hotspot/router-composition.txt b/tests/golden/hotspot/router-composition.txt index e776a1e2..a5d8220f 100644 --- a/tests/golden/hotspot/router-composition.txt +++ b/tests/golden/hotspot/router-composition.txt @@ -1,4 +1,4 @@ -.with_state(state); -Router::new() -app = app.layer(CorsLayer::permissive()); -let mut app = Router::new() +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 index 894b53ff..e6445d0c 100644 --- a/tests/golden/hotspot/routes.txt +++ b/tests/golden/hotspot/routes.txt @@ -1,20 +1,20 @@ -.route("/.well-known/groove", get(groove_manifest)) -.route("/.well-known/groove", get(groove_manifest)) -.route("/api/agent/plan", post(agent_plan_handler)) -.route("/api/aspect-tags", get(get_aspect_tags)) -.route("/api/diagnostics/health", get(diagnostics_health_handler)) -.route("/api/health", get(health_check)) -.route("/api/prove", post(prove_handler)) -.route("/api/provers", get(list_provers)) -.route("/api/search", get(search_handler)) -.route("/api/session/create", post(create_session)) -.route("/api/session/{id}/apply", post(apply_tactic_handler)) -.route("/api/session/{id}/state", get(get_session_state)) -.route("/api/session/{id}/tree", get(get_proof_tree)) -.route("/api/suggest", post(suggest_handler)) -.route("/api/tactics/suggest", post(suggest_tactics_ui)) -.route("/api/theorems/search", get(search_theorems_ui)) -.route("/api/verify", post(verify_handler)) -.route("/api/verify_parallel", post(verify_parallel_handler)) -.route("/api/verify_raw", post(verify_raw_handler)) -.route("/health", get(health_check)) +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))