Skip to content

Commit 08771e6

Browse files
feat(api/graphql): add verifyProof + suggestTactics(prover/context/goalState) + proverStatus (closes #180) (#188)
## Summary Resolves the echidna ↔ echidnabot GraphQL contract mismatch surfaced in #180 by adding the three operations the echidnabot client expects, plus surfacing the typed REST `/api/verify` taxonomy (`outcome`, optional `mode` / `smtStatus`) through GraphQL so future clients no longer need to round-trip via REST to disambiguate Timeout vs NoProofFound. ## New GraphQL operations | Operation | Kind | Signature | Returns | |---|---|---|---| | `verifyProof` | mutation | `(prover: String!, content: String!)` | `VerifyProofResult!` | | `suggestTactics` | mutation | `(prover: String!, context: String!, goalState: String!)` | `[SuggestedTactic!]!` | | `proverStatus` | query | `(prover: String!)` | `ProverStatusInfo!` | ## New GraphQL types - `VerifyOutcome` (enum: `PROVED` / `NO_PROOF_FOUND` / `INVALID_INPUT` / `UNSUPPORTED_FEATURE` / `TIMEOUT` / `INCONSISTENT_PREMISES` / `PROVER_ERROR` / `SYSTEM_ERROR`) — mirrors the REST `/api/verify` `outcome` field - `VerifyProofResult` — `status` + `message` + `proverOutput` + `durationMs` + `artifacts` (echidnabot client's existing `VerifyProofData` shape), **plus** `outcome` + `mode` + `smtStatus` (the typed REST taxonomy newly threaded through) - `SuggestedTactic` — `tactic` + `confidence` + `explanation` (matches echidnabot's `TacticSuggestionData`) - `ProverStatusInfo` — `available` + `message` ## Breaking change The existing **query** `suggestTactics(proofId, limit)` has been renamed to `suggestTacticsByProofId(proofId, limit)` to free the `suggestTactics` name for the new **mutation** the echidnabot client expects. - Argument shape and return type (`[Tactic!]!`) are unchanged. - Any consumer issuing `query { suggestTactics(proofId: ..., limit: ...) }` must update the operation name to `suggestTacticsByProofId`. The new `suggestTactics(prover, context, goalState)` on `MutationRoot` is the only `suggestTactics` field after this PR. - `interfaces/README.md` updated to reflect the new GraphQL surface. ## DRY - `verifyProof` delegates to the same `ProverFactory::create` + `parse_string` + `verify_proof` chain that backs REST `/api/verify`, so behaviour matches by construction. - `suggestTactics` (for-goal) tries the Julia ML coprocessor first and falls back to backend-native suggestions — same pattern as the existing session-based `suggest_tactics` path, just without requiring a `proof_id`. ## Tests `+8` in `src/interfaces/graphql/schema.rs::tests`: 1. `verify_outcome_from_rest_str_covers_taxonomy` — every REST taxon round-trips to a distinct enum variant 2. `verify_outcome_loose_status_matches_client` — `loose_status()` aligns with echidnabot's `parse_proof_status` 3. `verify_proof_mutation_unknown_prover_returns_system_error` — end-to-end execution; structured `SYSTEM_ERROR` outcome 4. `prover_status_unknown_prover_reports_unavailable` — query path; `available: false` + populated message 5. `suggest_tactics_mutation_unknown_prover_errors` — top-level GraphQL error (non-null list has no in-band error slot) 6. `suggest_tactics_is_a_mutation` — guard against accidental kind drift; `suggestTactics` on QueryRoot must not resolve 7. `suggest_tactics_by_proof_id_is_a_query` — renamed legacy query still registered on QueryRoot 8. `introspection_exposes_all_three_new_operations` — the contract assertion #180 asked for Baseline lib tests unchanged: **1070 passed** (`cargo test --lib -p echidna`). ## Don't-touched - GraphQL playground / introspection guards - Existing mutations `submitProof` / `applyTactic` / `cancelProof` - Existing queries `provers` / `proofState` / `listProofs` / `health` - REST handlers (already had the typed taxonomy; this PR is GraphQL-only) - echidnabot client (separate follow-up to consume the new typed `outcome`) ## Test plan - [x] `cargo build -p echidna-graphql` — clean - [x] `cargo test -p echidna-graphql` — 8/8 new tests pass - [x] `cargo test --lib -p echidna` — 1070 baseline tests still pass - [ ] Manual: spin up server, hit `verifyProof` + `suggestTactics` + `proverStatus` from echidnabot client in `EchidnaApiMode::Graphql` (deferred — needs a follow-up in echidnabot for the typed `outcome` consumption) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e045c20 commit 08771e6

3 files changed

Lines changed: 688 additions & 6 deletions

File tree

src/interfaces/README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ This directory contains all interface implementations for ECHIDNA theorem provin
1010
- **Playground:** http://localhost:8081/
1111
- **Features:**
1212
- Type-safe schema with all 17 provers
13-
- Queries: provers, proof_state, list_proofs, suggest_tactics
14-
- Mutations: submit_proof, apply_tactic, cancel_proof
15-
- Subscriptions: proof_updates (planned)
13+
- Queries: `provers`, `proofState`, `listProofs`,
14+
`suggestTacticsByProofId` (renamed from `suggestTactics` 2026-06-01,
15+
issue #180), `proverStatus`, `health`
16+
- Mutations: `submitProof`, `applyTactic`, `cancelProof`,
17+
`verifyProof` (synchronous one-shot, returns typed `VerifyOutcome` +
18+
optional `mode` / `smtStatus`), `suggestTactics(prover, context,
19+
goalState)` (ad-hoc, ML-backed)
20+
- Subscriptions: `proofUpdates` (planned)
1621

1722
### gRPC (`grpc/`)
1823
- **Framework:** Rust + tonic + Protocol Buffers

src/interfaces/graphql/resolvers.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use echidna::core::{
66
};
77
use echidna::provers::{ProverBackend, ProverConfig, ProverFactory, ProverKind as CoreProverKind};
88
use std::collections::HashMap;
9+
use std::str::FromStr;
910
use std::sync::Arc;
1011
use tokio::sync::{Mutex, RwLock};
1112

@@ -594,6 +595,90 @@ impl EchidnaContext {
594595
Ok(result)
595596
}
596597

598+
/// Suggest tactics for an ad-hoc `(prover, context, goal_state)` triple
599+
/// without requiring an existing proof session.
600+
///
601+
/// Added 2026-06-01 for the `suggestTactics(prover, context,
602+
/// goalState)` mutation (issue #180). The signature mirrors the
603+
/// echidnabot client's expectation. Tries the Julia ML coprocessor
604+
/// first; falls back to constructing a one-shot prover backend and
605+
/// calling its `suggest_tactics` against a fresh parse of the goal.
606+
pub async fn suggest_tactics_for_goal(
607+
&self,
608+
prover: &str,
609+
context: &str,
610+
goal_state: &str,
611+
limit: usize,
612+
) -> anyhow::Result<Vec<crate::schema::SuggestedTactic>> {
613+
// Parse the free-form prover string (same alias set as the REST handler).
614+
let core_kind = CoreProverKind::from_str(prover)
615+
.map_err(|e| anyhow::anyhow!("Unknown prover '{}': {}", prover, e))?;
616+
617+
// Try Julia ML first — the new shape ships `prover`/`context`/`goal`
618+
// directly so the bridge can use whichever it prefers.
619+
let ml_req = serde_json::json!({
620+
"goal": goal_state,
621+
"context": context,
622+
"prover": format!("{:?}", core_kind),
623+
"top_k": limit,
624+
});
625+
626+
if let Ok(resp) = self
627+
.ml_client
628+
.post(format!("{}/suggest", self.ml_api_url))
629+
.json(&ml_req)
630+
.send()
631+
.await
632+
{
633+
if resp.status().is_success() {
634+
if let Ok(ml_resp) = resp.json::<serde_json::Value>().await {
635+
if let Some(arr) = ml_resp["suggestions"].as_array() {
636+
let suggestions: Vec<crate::schema::SuggestedTactic> = arr
637+
.iter()
638+
.filter_map(|s| {
639+
let name = s["tactic"].as_str()?;
640+
let confidence =
641+
s["confidence"].as_f64().unwrap_or(0.5);
642+
let explanation =
643+
s["explanation"].as_str().map(|t| t.to_string());
644+
Some(crate::schema::SuggestedTactic {
645+
tactic: name.to_string(),
646+
confidence,
647+
explanation,
648+
})
649+
})
650+
.collect();
651+
652+
if !suggestions.is_empty() {
653+
return Ok(suggestions);
654+
}
655+
}
656+
}
657+
}
658+
}
659+
660+
// Fallback: spin up a one-shot prover backend and ask it.
661+
let config = ProverConfig::default();
662+
let backend = ProverFactory::create(core_kind, config)?;
663+
// Prefer goal_state if non-empty (matches the REST `/api/suggest`
664+
// fallback that the echidnabot client uses).
665+
let content = if !goal_state.trim().is_empty() {
666+
goal_state
667+
} else {
668+
context
669+
};
670+
let state = backend.parse_string(content).await?;
671+
let tactics = backend.suggest_tactics(&state, limit).await?;
672+
Ok(tactics
673+
.into_iter()
674+
.map(|t| crate::schema::SuggestedTactic {
675+
tactic: format!("{:?}", t),
676+
confidence: 0.5,
677+
explanation: Some("Backend heuristic suggestion".to_string()),
678+
})
679+
.collect())
680+
}
681+
597682
pub async fn suggest_tactics(
598683
&self,
599684
proof_id: &str,

0 commit comments

Comments
 (0)