Skip to content

Commit 290eb14

Browse files
authored
fix: check --json missing-file envelope and MPI hygiene (#1449)
## Summary Multi-perspective improvement cycle (2026-08-02) on a clean main. - **Agent JSON purity:** `assura check --json` on a missing path no longer emits a bare diagnostic array. It uses the same report object as a normal check (`diagnostics`, `file_info`, `layer`, `verification`) with code `A01000`. - **SMT:** Evolution unmodelable path uses `VerificationResult::unknown_not_encoded` instead of an open-coded limitation string. - **Docs:** MASTER-PLAN recount (201,160 LOC / 5,781 tests / 31 demos / 6 MCP tools); error-codes index adds `A01000` and clarifies `A0510x`; DESIGN closed-loop doc records `ir_verify` MCP + `assura ir --verify`. - **Deps:** clap 4.6.5, rmcp 3.1.0. - **Tests:** Integration coverage for the JSON envelope; stronger if-encode unit asserts. ## Test plan - [x] `cargo test -p assura --test cli_integration check_missing_file_json_envelope --locked` - [x] `cargo test -p assura-smt --lib evolution_ --locked` - [x] `cargo test -p assura --lib if_encodes --locked` (subset) - [x] `bash scripts/guards.sh` - [x] Live: `assura --json check /nonexistent` returns object envelope - [ ] CI green on this PR ## Follow-ups (not in this PR) - #1448 rowan 0.16 → 0.17 major evaluation - #1410 / #1411 human launch outreach - #436 cvc5-rs upstream --------- Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent 8c1f69d commit 290eb14

9 files changed

Lines changed: 85 additions & 46 deletions

File tree

Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MASTER-PLAN.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Assura Development Status
22

3-
> 164,785 lines of Rust, 4,925 tests, **21 workspace members** (re-counted 2026-07-04 via `scripts/count-crates.sh`).
3+
> 201,160 lines of Rust, 5,781 tests, **21 workspace members** (re-counted 2026-08-02 via `scripts/count-crates.sh`).
44
55
## What Works Today
66

77
| Capability | Status |
88
|------------|--------|
9-
| Parse `.assura` contracts | 24 demos, 157 test fixtures |
9+
| Parse `.assura` contracts | 31 demos, 187 test fixtures |
1010
| Name resolution with stdlib prelude types | Done |
1111
| Type checking with 60+ checkers across all 50 spec features | Done |
1212
| Z3 verification of requires/ensures/invariant clauses | Done |
@@ -15,7 +15,7 @@
1515
| WASM codegen (`--target wasm`) | Done |
1616
| IR prompt generation for AI coding agents | Done |
1717
| IR parsing, structural validation, and SMT verification | Done |
18-
| MCP server (5 tools) | Done |
18+
| MCP server (6 tools: check, infer, explain, type_map, ir_prompt, ir_verify) | Done |
1919
| gRPC server with streaming verification (5 RPCs) | Done |
2020
| LSP server (hover, completion, go-to-def, symbols) | Done |
2121
| VS Code extension (TextMate grammar + LSP client) | Done |
@@ -36,28 +36,28 @@ Workspace members only (`Cargo.toml` `members = ["crates/*"]` with
3636

3737
| Crate (package) | LOC | Tests | Role |
3838
|-----------------|-----|-------|------|
39-
| assura-parser | 9,597 | 188 | Lexer (logos) + recursive-descent parser (rowan CST), Pratt expressions |
40-
| assura-ast | 2,942 | 39 | Canonical AST, DeclVisitor, ExprVisitor, ExprFolder |
41-
| assura-resolve | 5,767 | 184 | Scope analysis, imports, stdlib prelude injection |
42-
| assura-types | 43,305 | 1,704 | 60+ checkers in CHECKER_PIPELINE, all 50 spec features |
43-
| assura-smt | 53,152 | 1,245 | Z3 + CVC5, Layer 2 verifier, prophecy/liveness/weak-memory, IR exec |
44-
| assura-codegen | 15,867 | 658 | Multi-file Rust projects, proptest gen, WASM, IR body substitution |
45-
| assura-pipeline | 2,103 | 66 | Canonical compile/compile_full/verify_typed/run_at |
39+
| assura-parser | 10,402 | 209 | Lexer (logos) + recursive-descent parser (rowan CST), Pratt expressions |
40+
| assura-ast | 3,162 | 46 | Canonical AST, DeclVisitor, ExprVisitor, ExprFolder |
41+
| assura-resolve | 6,095 | 189 | Scope analysis, imports, stdlib prelude injection |
42+
| assura-types | 44,727 | 1,750 | 60+ checkers in CHECKER_PIPELINE, all 50 spec features |
43+
| assura-smt | 59,594 | 1,348 | Z3 + CVC5, Layer 2 verifier, prophecy/liveness/weak-memory, IR exec |
44+
| assura-codegen | 16,706 | 670 | Multi-file Rust projects, proptest gen, WASM, IR body substitution |
45+
| assura-pipeline | 2,379 | 71 | Canonical compile/compile_full/verify_typed/run_at |
4646
| assura-config | 1,299 | 53 | assura.toml, VerifyOptions, CompilerConfig |
47-
| assura-diagnostics | 4,163 | 73 | Error codes, ariadne + JSON rendering |
48-
| assura (dir: assura-cli) | 12,910 | 283 | CLI binary: check, build, init, fmt, infer, … |
47+
| assura-diagnostics | 4,224 | 75 | Error codes, ariadne + JSON rendering |
48+
| assura (dir: assura-cli) | 38,774 | 937 | CLI binary: check, build, init, fmt, infer, check-rust, … |
4949
| assura-lsp | 1,965 | 55 | Language server (tower-lsp) |
5050
| assura-server | 809 | 27 | gRPC + HTTP/JSON API |
51-
| assura-mcp | 841 | 28 | MCP server for AI agent integration |
52-
| assura-fmt | 648 | 52 | Formatter |
53-
| assura-macros | 1,973 | 58 | Proc macros (`#[contract]`, `#[trust]`) |
51+
| assura-mcp | 865 | 28 | MCP server for AI agent integration |
52+
| assura-fmt | 741 | 53 | Formatter |
53+
| assura-macros | 1,974 | 58 | Proc macros (`#[contract]`, `#[trust]`) |
5454
| assura-stdlib | 409 | 18 | Stdlib modules (math, string, collections, …) |
5555
| assura-rust-analyzer | 2,514 | 92 | Syn-based Rust source parser for contract inference |
5656
| assura-test-support | 376 | 10 | Shared test helpers |
5757
| assura-bench | 421 | 0 | Criterion benchmarks |
5858
| assura-runtime | 262 | 10 | Runtime support for contracts |
5959
| assura-llm | 3,462 | 82 | LLM provider abstraction for auto-implement / suggest |
60-
| **Total** | **164,785** | **4,925** | |
60+
| **Total** | **201,160** | **5,781** | |
6161

6262
`crates/assura-driver` is **excluded** from the workspace (exploratory rustc
6363
driver). Refresh counts with `bash scripts/count-crates.sh`.

crates/assura-cli/src/check/run.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,20 @@ pub(crate) fn run_check(opts: CheckOptions<'_>) {
103103

104104
let (source, display_name) = read_source_arg(filename).unwrap_or_else(|e| {
105105
if output_mode == OutputMode::Json {
106+
// Match the normal check --json envelope so agents can always
107+
// parse `{diagnostics, file_info, ...}` (not a bare diag array).
106108
let diag = assura_diagnostics::Diagnostic::error("A01000", format!("{e}"), 0..0)
107109
.with_file(filename);
108-
println!("{}", serde_json::to_string_pretty(&[diag]).unwrap());
110+
let report = serde_json::json!({
111+
"diagnostics": [diag],
112+
"file_info": {
113+
"file": filename,
114+
"success": false,
115+
},
116+
"layer": compiler_config.verify.layer,
117+
"verification": [],
118+
});
119+
println!("{}", serde_json::to_string_pretty(&report).unwrap());
109120
} else {
110121
eprintln!("Error: {filename}: {e}");
111122
}

crates/assura-cli/src/check/rust_body_ir/tests.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,14 +1932,16 @@ fn simple_nested_if_in_then() {
19321932

19331933
#[test]
19341934
fn unary_neg_if_encodes() {
1935-
let ir = try_ir_from_rust_body("U", &px(), Some("i64"), "-(if x > 0 { x } else { 1 })");
1936-
assert!(ir.is_some(), "unary if");
1935+
let ir = try_ir_from_rust_body("U", &px(), Some("i64"), "-(if x > 0 { x } else { 1 })")
1936+
.expect("unary if should encode");
1937+
assert!(ir.contains("then #") || ir.contains("neg"), "{ir}");
19371938
}
19381939

19391940
#[test]
19401941
fn method_on_if_encodes() {
1941-
let ir = try_ir_from_rust_body("M", &px(), Some("i64"), "(if x > 0 { x } else { 1 }).abs()");
1942-
assert!(ir.is_some(), "method on if");
1942+
let ir = try_ir_from_rust_body("M", &px(), Some("i64"), "(if x > 0 { x } else { 1 }).abs()")
1943+
.expect("method on if should encode");
1944+
assert!(ir.contains("then #") || ir.contains("abs"), "{ir}");
19431945
}
19441946

19451947
#[test]
@@ -1963,8 +1965,9 @@ fn cast_of_if_encodes() {
19631965
&px(),
19641966
Some("i64"),
19651967
"(if x > 0 { x } else { 0 }) as i64",
1966-
);
1967-
assert!(ir.is_some(), "cast of if");
1968+
)
1969+
.expect("cast of if should encode");
1970+
assert!(ir.contains("then #"), "{ir}");
19681971
}
19691972

19701973
#[test]
@@ -1984,8 +1987,9 @@ fn if_as_method_arg_encodes() {
19841987
&pxy,
19851988
Some("i64"),
19861989
"x.saturating_add(if y > 0 { 1 } else { 0 })",
1987-
);
1988-
assert!(ir.is_some(), "if as method arg: {ir:?}");
1990+
)
1991+
.expect("if as method arg should encode");
1992+
assert!(ir.contains("then #") || ir.contains("add"), "{ir}");
19891993
}
19901994

19911995
#[test]

crates/assura-cli/tests/cli_integration.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2844,6 +2844,33 @@ fn check_watch_missing_path_json() {
28442844
assert_eq!(v["watch"], true);
28452845
}
28462846

2847+
/// `check --json` on a missing file must use the same report envelope as a
2848+
/// normal check (object with `diagnostics` / `file_info`), not a bare array.
2849+
#[test]
2850+
fn check_missing_file_json_envelope() {
2851+
let out = Command::new(assura_bin())
2852+
.args(["check", "/no/such/check/path.assura", "--json"])
2853+
.output()
2854+
.expect("failed to run assura check --json missing");
2855+
assert_eq!(
2856+
out.status.code(),
2857+
Some(2),
2858+
"missing path should exit 2: stdout={} stderr={}",
2859+
String::from_utf8_lossy(&out.stdout),
2860+
String::from_utf8_lossy(&out.stderr)
2861+
);
2862+
let stdout = String::from_utf8_lossy(&out.stdout);
2863+
let v: serde_json::Value =
2864+
serde_json::from_str(&stdout).expect("missing path --json must be a JSON object");
2865+
assert!(v.is_object(), "expected object envelope, got: {stdout}");
2866+
assert_eq!(v["file_info"]["success"], false);
2867+
assert_eq!(v["file_info"]["file"], "/no/such/check/path.assura");
2868+
let diags = v["diagnostics"].as_array().expect("diagnostics array");
2869+
assert_eq!(diags.len(), 1);
2870+
assert_eq!(diags[0]["code"], "A01000");
2871+
assert!(v["verification"].is_array());
2872+
}
2873+
28472874
#[test]
28482875
fn check_showcase_only_filters_by_header() {
28492876
let tmp = unique_temp("assura_showcase_only");

crates/assura-llm/src/provider.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ impl LlmProvider for MockProvider {
218218
Ok(serde_json::to_string(&serde_json::json!({
219219
"suggestions": []
220220
}))
221-
.unwrap())
221+
.expect("static JSON object always serializes"))
222222
}
223223

224224
fn model_id(&self) -> &str {

crates/assura-smt/src/entry/evolution.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,7 @@ fn check_implication(
148148
let all_exprs: Vec<&&SpExpr> = antecedents.iter().chain(consequents.iter()).collect();
149149
for expr in &all_exprs {
150150
if expr_has_unmodelable_features(expr) {
151-
return VerificationResult::Unknown {
152-
clause_desc: desc.to_string(),
153-
reason: "clause uses features not yet encoded in SMT".into(),
154-
};
151+
return VerificationResult::unknown_not_encoded(desc, "clause uses features");
155152
}
156153
}
157154

docs/DESIGN-AI-VERIFICATION-LOOP.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -251,15 +251,14 @@ The pieces exist but are not connected:
251251
| JSON output | `VerificationSummary` | Works (JSON-serializable) |
252252
| IR prompt generation | `render_ir_prompt` / `ir_prompt_contexts_for_typed` | Works |
253253
| Heuristic IR generation | `ir_generate::generate_ir_sidecar_text` | Works (pattern detection) |
254-
| MCP server | `assura-mcp` (5 tools) | Works, but no IR verify tool |
254+
| MCP server | `assura-mcp` (6 tools) | Works: check, infer, explain, type_map, ir_prompt, **ir_verify** |
255+
| CLI IR + SMT | `assura ir --verify` / `--verify-only` | Works (requires `--contract`) |
255256

256-
**Missing connections:**
257+
**Remaining gaps (not blocking the closed loop):**
257258

258-
1. No MCP tool to submit IR text and get verification results
259-
2. `assura ir` command does not run SMT verification
260-
3. No counterexample-to-IR-slot mapping
261-
4. No structured repair hints
262-
5. No multi-turn protocol with progress tracking
259+
1. No counterexample-to-IR-slot mapping
260+
2. No structured repair hints beyond Unknown/CE reasons
261+
3. No multi-turn protocol with progress tracking
263262

264263
---
265264

docs/error-codes.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
| A12xxx | types | assura-types | checks/concurrency.rs, checkers/security/ |
2727
| A13xxx | types | assura-types | checks/numeric.rs, domain/numeric.rs |
2828
| A31xxx | types | assura-types | checks/core.rs (liveness prove/fairness) |
29-
| A05 (impl) | smt+cli | assura-smt / assura-cli | `A05100` SMT inconclusive / limitation |
29+
| A05 (impl) | smt+cli | assura-smt / assura-cli | `A05100` CE, `A05101` timeout, `A05102` known limitation, `A05103` inconclusive |
3030

3131
## Codes from SPEC §7.2 (plus a few high-traffic impl codes)
3232

@@ -104,8 +104,8 @@
104104
| A22003 | ? | ? | Unbounded allocation detected | No allocation bound proved | Complexity Bounds (A22xxx) | rg code in crates |
105105
| A05100 | smt+cli | assura-smt / assura-cli | SMT counterexample found (verification failed) | Fix the contract (real violation) | (impl) | check/report.rs |
106106
| A05101 | cli | assura-cli | SMT solver timed out | Increase `--timeout` | (impl) | check/report.rs |
107-
| A05102 | cli | assura-cli | Known compiler limitation (warning, exit 0) | No action needed | (impl) | check/report.rs |
108-
| A05103 | cli | assura-cli | Solver inconclusive (error, exit 1) | Simplify the contract | (impl) | check/report.rs |
107+
| A05102 | cli | assura-cli | Known compiler limitation (warning, exit 0; error under `--strict`) | Simplify ensures, add IR, or ignore until encoding lands | (impl) | check/report.rs |
108+
| A05103 | cli | assura-cli | Solver inconclusive (error, exit 1) | Simplify the contract or raise `--timeout` | (impl) | check/report.rs |
109109
| A10002 | types | assura-types | Match on unknown scrutinee without wildcard | (implementation; see CLI/SMT Unknown policy) | (impl) | checks/meta.rs (match exhaustiveness) |
110110

111111
## High-traffic implementation codes (not always in SPEC §7.2 table above)
@@ -115,6 +115,7 @@ this table over guessing the phase.
115115

116116
| Code | Phase | Primary crate | Typical meaning | Start in tree |
117117
|------|-------|---------------|-----------------|---------------|
118+
| A01000 | cli/pipeline | assura-cli / assura-pipeline | Source file read/IO failure | check/run.rs, pipeline |
118119
| A02006 | resolve | assura-resolve | Duplicate import | imports.rs |
119120
| A02007 | resolve | assura-resolve | Unused import | unused.rs |
120121
| A02008 | resolve | assura-resolve | Invalid import path segment | imports.rs |

0 commit comments

Comments
 (0)