Skip to content

Commit 75eac52

Browse files
ArghDA M1: Backend abstraction — make the engine prover-parametric (#32)
## What & why The keystone of the "Flying Logic for provers/solvers" epic. Every Agda-baked seam now lives behind an object-safe `Backend` trait, so the four-state workspace, DAG builder, content-hash invalidation and the cycle-safe reachability walk become backend-neutral. Agda is the first (v0.1 reference) impl; adding Idris2 / Lean4 / Z3 / CVC5 / Coq / Isabelle / Mizar is now *an impl*, not a rewrite. Follow-on from M0 (toolchain provisioning, merged as #31). #31 being merged, this is a fresh PR on the same working branch. ## This is a pure refactor — the test suite is the oracle Behaviour is unchanged, ground-truthed by running the tools (not inferred): - **`cargo test`** — the 64 pre-existing tests (50 lib + 14 integration) all stay green; +2 new Agda-backend tests = **66 passing, 0 failed**. - **`cargo clippy --all-targets --all-features -- -D warnings`** — clean. - **`cargo fmt --check`** — clean. - **`scripts/check-spdx.sh .`** — `SPDX licence invariant: OK`. - **Dogfood** — `arghda dag tests/fixtures/wellformed` emits byte-compatible JSON (`version: "0.1"`, same nodes/edges); `arghda check …/All.agda` ran a **real** Agda typecheck → `proven-eligible` (agda is present from #31's provisioning). ## Changes **New `src/prover/`:** - `Backend` trait — `name` / `kind` / `extensions` / `safe_mode` / `check_file` + the import-graph trio (`module_name_of` / `module_to_path` / `direct_imports`) + `discover_roots` + `lint_rules`. - `BackendKind { Assistant, Solver }` — the two interaction models (typecheck-a-file vs SMT query). - `Verdict { Proven, Refuted, Unknown, Admitted, Postulated, Error, Unavailable }` — the common verdict both models map into. **Only `Proven` is green; `Admitted`/`Postulated` are amber.** - `Outcome` — superset of the old `AgdaOutcome` (raw process facts + `kind` + `verdict`). **Refactor:** - `src/agda.rs` → `src/prover/agda.rs`. `Agda` is the first impl: owns the `agda` shell-out, delegates parsing to `graph::` free fns and the lint pack to `lint::`. Verdict is **exit-code-only** (0 → `Proven`, ran-nonzero → `Error`, absent → `Unavailable`) — arghda never claims a result Agda did not return; `Admitted`/`Postulated` stay lint-derived, never manufactured from a green exit. - `graph::build` and `dag::build` take `&dyn Backend` and use the backend's extension filter + import parsing. **The cycle-safe reachability walk and its `transitive_imports_terminates_on_a_cycle` regression are untouched** — cycle safety is inherited free by every backend. - `main.rs` `scan`/`check`/`dag`/`resolve_roots_and_rules` route through the backend; the `check` JSON key `agda` → `backend` (now carries `kind` + `verdict`). - `STATE.a2ml` records M1 = 100%; `.gitignore` ignores `*.agdai`. ## Honesty note (owner directive + AGENTIC.a2ml) The `Backend` module is the boundary where "never report a verdict the tool did not emit" is enforced: `check_file` derives its verdict from the actual exit code and degrades to `Unavailable` when the binary is absent. ## Next (trait now stable → parallelisable) M3 reasoning-graph (`src/reason`), M4 Idris2, M5 SMT (Z3/CVC5), M2 Cubical-Agda. --- _Generated by [Claude Code](https://claude.ai/code/session_012MpYSh6Wy8YMBH2E3qVyT7)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent f3ff6f9 commit 75eac52

10 files changed

Lines changed: 428 additions & 133 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ dist-newstyle/
1818
.direnv/
1919
result
2020
result-*
21+
22+
# Agda compiled interface files (generated by typecheck runs)
23+
*.agdai

.machine_readable/6a2/STATE.a2ml

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ milestones = [
4141
[route-to-flying-logic]
4242
milestones = [
4343
{ name = "M0: provision-provers.sh (pinned, honest --version verification) + `just provision/doctor` recipes", completion = 80 },
44-
{ name = "M1: Backend trait (Assistant|Solver kinds; Outcome/Verdict); agda.rs -> prover/agda.rs; graph/dag take &dyn Backend (pure refactor, 50 tests stay green)", completion = 0 },
44+
{ name = "M1: Backend trait (Assistant|Solver kinds; Outcome/Verdict); agda.rs -> prover/agda.rs; graph/dag take &dyn Backend (pure refactor, 50 tests stay green)", completion = 100 },
4545
{ name = "M2: Cubical-Agda variant (Agda::cubical(), --cubical --safe island)", completion = 0 },
4646
{ name = "M3: Flying-Logic reasoning graph (src/reason: And|Or juncts, demote-only cycle-safe Verdict propagation, additive studio JSON)", completion = 0 },
4747
{ name = "M4: Idris2 adapter (core/ABI language: .ipkg/--check/totality; escape-hatches)", completion = 0 },
@@ -59,6 +59,21 @@ milestones = [
5959
# are scripted (behind --heavy/--mizar) but not installed this session.
6060
# Remaining M0: `arghda doctor` Rust subcommand (interim: `just doctor` shells
6161
# the script's --verify-only table).
62+
#
63+
# M1 landed 2026-07-01 as the promised PURE REFACTOR — behaviour unchanged,
64+
# oracle held: the 64 pre-existing tests (50 lib + 14 integration) all stay
65+
# green, +2 new Agda-backend tests = 66 passing; clippy -D warnings + fmt +
66+
# SPDX all clean; `dag` JSON output byte-compatible. New src/prover/ module:
67+
# Backend trait (name/kind/extensions/safe_mode/check_file/module_name_of/
68+
# module_to_path/direct_imports/discover_roots/lint_rules), BackendKind
69+
# {Assistant|Solver}, Verdict {Proven|Refuted|Unknown|Admitted|Postulated|
70+
# Error|Unavailable}, Outcome (superset of the old AgdaOutcome + kind +
71+
# verdict). src/agda.rs -> src/prover/agda.rs (Agda = first impl; owns the
72+
# shell-out, delegates parsing to graph:: free fns + lint pack to lint::).
73+
# graph::build + dag::build + main's scan/check/dag/resolve_roots_and_rules
74+
# all consume &dyn Backend; the cycle-safe reachability walk is untouched and
75+
# inherited free. Agda verdict is exit-code-only (0 -> Proven, ran-nonzero ->
76+
# Error, absent -> Unavailable); Admitted/Postulated stay lint-derived amber.
6277

6378
[blockers-and-issues]
6479
# No active blockers. v0.1 lint set + DAG schema (arghda-spec.adoc) are
@@ -72,21 +87,24 @@ milestones = [
7287

7388
[critical-next-actions]
7489
actions = [
75-
"Flying-Logic epic M1: introduce the Backend trait (Assistant|Solver) and move agda.rs to prover/agda.rs as a PURE REFACTOR — the 50 existing tests are the oracle, they must stay green with no behaviour change. This is the keystone everything else rides on.",
76-
"Flying-Logic epic M3: src/reason reasoning graph (And|Or juncts + demote-only cycle-safe Verdict propagation) — independent of new backends, lands the headline capability early.",
90+
"M1 keystone DONE (Backend trait, pure refactor, 66 tests green). Next, in parallel now that the trait is stable:",
91+
"Flying-Logic epic M3: src/reason reasoning graph (And|Or juncts + demote-only cycle-safe Verdict propagation over the common Verdict) — independent of new backends, lands the headline capability early.",
92+
"Flying-Logic epic M4: Idris2 adapter (core/ABI language: .ipkg/--check/totality; believe_me/assert_total/%partial escape-hatches) — Idris2 v0.7.0 is provisioned.",
93+
"Flying-Logic epic M5: SMT solver backends (Z3, CVC5) — the Solver kind; SMT-LIB2 -> sat(Refuted)/unsat(Proven)/unknown(Unknown). Both binaries are provisioned.",
94+
"Flying-Logic epic M2: Cubical-Agda variant (Agda::cubical(), --cubical --safe island; cross-flag import edges forbidden).",
7795
"Finish M0: add the `arghda doctor` Rust subcommand and repoint `just doctor` at it (interim shells provision-provers.sh --verify-only).",
78-
"Transitive-import closure hashing landed; follow-up: expose it as a --include-root CLI flag on promote/stale.",
7996
"Serve /.well-known/groove for PanLL discovery (Groove protocol) — folds into M11.",
8097
]
8198

8299
[maintenance-status]
83-
last-run-utc = "2026-06-20"
100+
last-run-utc = "2026-07-01"
84101
last-result = "pass" # unknown | pass | warn | fail
85102
open-warnings = 0
86103
open-failures = 0
87-
# `just check` (fmt-check + clippy -D warnings + build + test) green; ~50 lib
88-
# tests + integration suites pass. Dogfooded against echo-types + a live
89-
# agda-unused build.
104+
# `just check` (fmt-check + clippy -D warnings + build + test + SPDX) green
105+
# post-M1: 52 lib + 14 integration = 66 tests pass. `dag`/`check` dogfooded
106+
# via real CLI runs (dag JSON byte-compatible; check ran a real agda typecheck
107+
# -> proven-eligible). Dogfooded historically against echo-types + agda-unused.
90108

91109
[ecosystem]
92110
part-of = ["arghda"]

src/agda.rs

Lines changed: 0 additions & 84 deletions
This file was deleted.

src/dag.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use crate::diagnostic::Severity;
1414
use crate::graph::{self, Edge};
1515
use crate::lint::{run_lints, unpinned_headline, LintContext, LintRule};
16+
use crate::prover::Backend;
1617
use crate::timestamp::now_rfc3339;
1718
use anyhow::{Context, Result};
1819
use regex::Regex;
@@ -61,18 +62,20 @@ pub struct DagDocument {
6162
}
6263

6364
/// Build the DAG document for the source tree at `include_root`, using
64-
/// `entry_modules` (the union of CI roots) for the orphan-reachability rule,
65-
/// `rules` as the lint pack, and `headline_pattern` (the same regex the
66-
/// `unpinned-headline` rule uses) to populate each node's `headlines` array.
65+
/// `backend` for the per-language import graph, `entry_modules` (the union
66+
/// of CI roots) for the orphan-reachability rule, `rules` as the lint pack,
67+
/// and `headline_pattern` (the same regex the `unpinned-headline` rule
68+
/// uses) to populate each node's `headlines` array.
6769
pub fn build(
6870
include_root: &Path,
6971
entry_modules: &[PathBuf],
7072
rules: &[Box<dyn LintRule>],
7173
headline_pattern: &str,
74+
backend: &dyn Backend,
7275
) -> Result<DagDocument> {
7376
let headline_matcher = Regex::new(headline_pattern)
7477
.with_context(|| format!("compiling headline pattern `{headline_pattern}`"))?;
75-
let graph = graph::build(include_root)?;
78+
let graph = graph::build(include_root, backend)?;
7679
let ctx = LintContext {
7780
include_root,
7881
entry_modules,

src/graph.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
//! include root: the graph is the project's internal dependency DAG, so
1515
//! stdlib / external imports are intentionally omitted.
1616
17+
use crate::prover::Backend;
1718
use anyhow::{Context, Result};
1819
use serde::Serialize;
1920
use std::collections::{BTreeMap, HashSet};
@@ -177,21 +178,29 @@ pub struct ImportGraph {
177178
pub edges: Vec<Edge>,
178179
}
179180

180-
/// Walk every `.agda` file under `include_root` and build the internal
181-
/// import graph. Output is deterministic (nodes and edges are sorted),
182-
/// which keeps the emitted DAG stable for diffing and tests.
183-
pub fn build(include_root: &Path) -> Result<ImportGraph> {
181+
/// Walk every source file the `backend` claims (by extension) under
182+
/// `include_root` and build the internal import graph, using the backend's
183+
/// per-language module-name / import parsing. Output is deterministic
184+
/// (nodes and edges are sorted), which keeps the emitted DAG stable for
185+
/// diffing and tests. A solver backend with no import notion yields an
186+
/// edge-free set of isolated nodes, which is valid.
187+
pub fn build(include_root: &Path, backend: &dyn Backend) -> Result<ImportGraph> {
188+
let exts = backend.extensions();
184189
// module id -> relative file path, for every in-tree module.
185190
let mut by_id: BTreeMap<String, PathBuf> = BTreeMap::new();
186191
for entry in WalkDir::new(include_root)
187192
.into_iter()
188193
.filter_map(|e| e.ok())
189194
{
190195
let path = entry.path();
191-
if path.extension().and_then(|s| s.to_str()) != Some("agda") {
196+
let is_source = path
197+
.extension()
198+
.and_then(|s| s.to_str())
199+
.is_some_and(|e| exts.contains(&e));
200+
if !is_source {
192201
continue;
193202
}
194-
if let Some(id) = module_name_of(path, include_root) {
203+
if let Some(id) = backend.module_name_of(path, include_root) {
195204
let rel = path
196205
.strip_prefix(include_root)
197206
.unwrap_or(path)
@@ -210,8 +219,8 @@ pub fn build(include_root: &Path) -> Result<ImportGraph> {
210219

211220
let mut edges = Vec::new();
212221
for id in by_id.keys() {
213-
let path = module_to_path(id, include_root);
214-
for imp in direct_imports(&path)? {
222+
let path = backend.module_to_path(id, include_root);
223+
for imp in backend.direct_imports(&path)? {
215224
// Keep only edges to modules that exist in-tree.
216225
if by_id.contains_key(&imp) {
217226
edges.push(Edge {

src/lib.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
// SPDX-License-Identifier: MPL-2.0
22
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
33

4-
//! arghda-core: proof-workspace manager for Agda.
4+
//! arghda-core: proof-workspace manager for provers and solvers.
55
//!
6-
//! Public surface is intentionally small: `Workspace`, the lint traits,
7-
//! and the diagnostic types. The CLI in `main.rs` is a thin consumer.
6+
//! Agda is the first (v0.1 reference) backend; every prover-specific seam
7+
//! lives behind the [`prover::Backend`] trait, so the four-state workspace,
8+
//! DAG builder and content-hash invalidation are backend-neutral. Public
9+
//! surface is intentionally small: `Workspace`, the `Backend` trait, the
10+
//! lint traits, and the diagnostic types. The CLI in `main.rs` is a thin
11+
//! consumer.
812
9-
pub mod agda;
1013
pub mod config;
1114
pub mod dag;
1215
pub mod diagnostic;
@@ -15,15 +18,16 @@ pub mod graph;
1518
pub mod hash;
1619
pub mod lint;
1720
pub mod proven;
21+
pub mod prover;
1822
pub mod timestamp;
1923
pub mod unused;
2024
pub mod watcher;
2125
pub mod workspace;
2226

23-
pub use agda::{check_file, AgdaOutcome};
2427
pub use dag::{build as build_dag, DagDocument};
2528
pub use diagnostic::{Diagnostic, LintReport, Severity};
2629
pub use event::{Event, EventKind};
2730
pub use graph::{build as build_graph, ImportGraph};
2831
pub use lint::{default_rules, rules_with_config, run_lints, LintRule, RuleConfig};
32+
pub use prover::{default_backend, Agda, Backend, BackendKind, Outcome, Verdict};
2933
pub use workspace::{StaleEntry, State, Workspace};

0 commit comments

Comments
 (0)