Skip to content

Commit 5fb347a

Browse files
fix(spark): honest-demote dnfinition reversibility claim + theatre gate (P0) (#51)
## What Closes the **P0** of the estate Rust/SPARK audit (`PROOF-DEBT-AND-RUST-SPARK-AUDIT-2026-05-18.adoc`): ambientops bannered the dnfinition reversibility subsystem as "formally verified" SPARK while GNATprove was never wired in, the ghost/lemma bodies were stubbed to `return True` / `null;`, **and the subsystem did not even compile** — so the proof claims were pure theatre. ## Honest demotion (no false proof claims remain) | Unit | Change | |---|---| | `safety_boundary.ads/.adb` | banner `(formally verified)` → `(runtime-enforced; NOT formally verified)`, `SPARK_Mode` → `Off`, honest `PROOF STATUS` note, `Is_Valid` de-ghosted (real runtime fn), obligation **O-SAFE-2** | | `reversibility_types.ads` | `SPARK_Mode Off`, **O-REV-1..4**; `Rollback_Result.Snapshot_ID`→`Target_Snapshot` (name-clash) | | `reversibility_types.adb` | **deleted** — spec is types + expression functions only | | `snapshot_manager.ads/.adb` | `SPARK_Mode Off` + HONESTY NOTE; consumer updated for the `Target_Snapshot` rename | | `safety_invariant.ads/.adb` | `SPARK_Mode Off`, broken `Refined_State` removed | `PROOF-NEEDS.md` records **O-REV-1..4 / O-SAFE-1..2** as the authoritative UNPROVEN obligation list (Idris2 model). ## Build verification — now actually performed The first revision could not `gprbuild` (project `with "ncurses"`, no provider). Resolved by adding **`checkonly.gpr`**, a scoped compile-check over `reversibility/safety/backends/platform` that omits the unrelated, unprovided ncurses TUI dependency. Doing so exposed — and this PR fixes — **3 real legality blockers that were the concrete reason the subsystem never built** (hence why the proof claims could be theatre at all): 1. `backend_interface.ads` — record field named `Package` (Ada reserved word). Hard legality error. Renamed → `Pkg` (unreferenced; local). 2. `safety_boundary.ads` — `Is_Valid` was `with Ghost` but called in real runtime checks: ghost-without-proof in executable code, also illegal once `SPARK_Mode` is Off. Aspect removed. 3. `snapshot_manager.adb` — un-propagated fallout of the `Rollback_Result.Snapshot_ID → Target_Snapshot` demotion rename. 5 refs fixed. Result (`gprbuild -gnatc` via `checkonly.gpr`): | Unit | Result | |---|---| | `safety_boundary.adb` | **compiles, 0 errors** | | `safety_invariant.adb` | **compiles, 0 errors** | | `reversibility_types.ads` | **compiles, 0 errors** | | `snapshot_manager.adb` | only 3 residual errors — all pre-existing undefined `Create_{Btrfs,ZFS,LVM}_Snapshot` stubs, unrelated to the SPARK demotion (separate "never implemented" debt, out of P0 scope) | The demotion is now **proven legal Ada, not merely asserted**. ## Regression guard `.github/workflows/spark-proof-gate.yml` — self-contained SPARK Theatre Gate over `total-update/ada`: **T1** (header proof-claim, no prover) hard-fails; **T2** (zero-contract `SPARK_Mode`) warns during the migration tail. Mirrors the estate reusable gate (**hyperpolymath/standards#141**); collapses to a 3-line `uses:` caller once that merges. Verified to PASS on the final tree and to catch the pre-demotion `safety_boundary` banner regression (T1). Refs hyperpolymath/standards#124 Refs hyperpolymath/standards#127 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4b3d859 commit 5fb347a

12 files changed

Lines changed: 275 additions & 125 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# SPDX-License-Identifier: PMPL-1.0
2+
name: SPARK Proof Gate
3+
4+
# ambientops anti-theatre gate (P0 of the estate Rust/SPARK audit;
5+
# hyperpolymath/standards#124 / #135).
6+
#
7+
# Self-contained on purpose: it does not `uses:` the standards reusable
8+
# SPARK Theatre Gate yet, so it works the moment this lands rather than
9+
# waiting on hyperpolymath/standards#141 to merge. Once #141 is on
10+
# standards@main this file SHOULD be reduced to a 3-line caller:
11+
#
12+
# jobs:
13+
# gate:
14+
# uses: hyperpolymath/standards/.github/workflows/spark-theatre-gate.yml@<sha>
15+
# with: { paths: "total-update/ada", enforce_zero_contract: false }
16+
#
17+
# It exists specifically so the safety_boundary / reversibility_types /
18+
# safety_invariant honest-demotion cannot silently regress to a false
19+
# "formally verified" banner. T1 is a hard failure; T2 (zero-contract
20+
# SPARK_Mode) is a warning while the Ada/SPARK->Rust/SPARK migration tail
21+
# clears (tracked in PROOF-NEEDS.md).
22+
23+
on:
24+
push:
25+
branches: [main, master]
26+
paths: ["total-update/ada/**", ".github/workflows/spark-proof-gate.yml"]
27+
pull_request:
28+
paths: ["total-update/ada/**", ".github/workflows/spark-proof-gate.yml"]
29+
30+
concurrency:
31+
group: ${{ github.workflow }}-${{ github.ref }}
32+
cancel-in-progress: true
33+
34+
permissions:
35+
contents: read
36+
37+
jobs:
38+
spark-proof-gate:
39+
name: SPARK Proof Gate (dnfinition)
40+
runs-on: ubuntu-latest
41+
steps:
42+
- name: Checkout
43+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
44+
45+
- name: Scan total-update/ada for SPARK proof theatre
46+
run: |
47+
set -uo pipefail
48+
SCAN_ROOT="total-update/ada"
49+
50+
prover_evidence=0
51+
if grep -rIlE 'gnatprove' .github/workflows 2>/dev/null | grep -q .; then
52+
prover_evidence=1
53+
fi
54+
for bf in Justfile justfile Mustfile Dustfile; do
55+
[ -f "$bf" ] && grep -qI 'gnatprove' "$bf" && prover_evidence=1
56+
done
57+
if find . \( -name 'gnatprove.out' -o -name '*.spark' \
58+
-o -path '*/gnatprove/*.json' \) \
59+
-not -path '*/.git/*' 2>/dev/null | grep -q .; then
60+
prover_evidence=1
61+
fi
62+
echo "prover_evidence=$prover_evidence"
63+
64+
claim_re='formally verified|SPARK Proof Level|\(Gold\)|all preconditions and postconditions are verified|proving the invariant holds'
65+
contract_re='Pre[[:space:]]*=>|Post[[:space:]]*=>|Global[[:space:]]*=>|Depends[[:space:]]*=>|Contract_Cases'
66+
sparkon_re='pragma[[:space:]]+SPARK_Mode[[:space:]]*\([[:space:]]*On[[:space:]]*\)|with[[:space:]]+SPARK_Mode[[:space:]]*=>[[:space:]]*On'
67+
68+
fail=0
69+
while IFS= read -r f; do
70+
[ -z "$f" ] && continue
71+
grep -vE '^[[:space:]]*--' "$f" | grep -qE "$sparkon_re" || continue
72+
header="$(awk '
73+
/^[[:space:]]*--/ || /^[[:space:]]*$/ { print; next }
74+
{ exit }' "$f")"
75+
has_header_claim=0
76+
printf '%s\n' "$header" | grep -qiE "$claim_re" && has_header_claim=1
77+
has_contract=0
78+
grep -qE "$contract_re" "$f" && has_contract=1
79+
80+
[ "$prover_evidence" -eq 1 ] && continue
81+
82+
if [ "$has_header_claim" -eq 1 ]; then
83+
echo "::error file=$f::T1 SPARK theatre regression: header banner claims formal proof but no GNATprove run exists. Demote (SPARK_Mode Off + tracked obligation in PROOF-NEEDS.md) or wire gnatprove into CI."
84+
fail=1
85+
elif [ "$has_contract" -eq 0 ]; then
86+
echo "::warning file=$f::T2 zero-contract SPARK_Mode (standards#135): proves nothing here. Demote to SPARK_Mode Off or add contracts."
87+
fi
88+
done < <(find "$SCAN_ROOT" \( -name '*.ads' -o -name '*.adb' \) \
89+
-not -path '*/.git/*' -not -path '*/obj/*' 2>/dev/null)
90+
91+
if [ "$fail" -ne 0 ]; then
92+
echo "::error::SPARK Proof Gate FAILED — a demoted unit regressed to a hollow 'verified' claim. See PROOF-NEEDS.md (O-REV-*/O-SAFE-*)."
93+
exit 1
94+
fi
95+
echo "✓ SPARK Proof Gate passed — no hollow proof claims in total-update/ada."

PROOF-NEEDS.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,30 @@
1515
- **Privilege escalation prevention**: Prove system operations respect the principle of least privilege (no operation escalates beyond its declared scope)
1616
- **Rollback atomicity**: Prove that failed operations roll back completely (no partial state)
1717

18+
## Tracked unproven obligations — dnfinition reversibility subsystem
19+
20+
The following obligations are referenced by the `PROOF STATUS` / `HONESTY
21+
NOTE` headers in `total-update/ada/dnfinition/src/{reversibility,safety}/*`.
22+
They were previously expressed as SPARK ghost/lemma bodies stubbed to
23+
`return True` / `null;` (proof theatre). SPARK_Mode is now `Off` on those
24+
units and the obligations are recorded here as the authoritative,
25+
**UNPROVEN** list (Idris2 is the intended model — see Recommended prover).
26+
The SPARK Theatre Gate (`.github/workflows/spark-proof-gate.yml`) prevents
27+
any of these units silently regressing to a false "verified" claim.
28+
29+
| ID | Unit | Obligation |
30+
|----|------|------------|
31+
| O-REV-1 | `reversibility_types.ads` | `Snapshot_Exists`: a snapshot referenced by a non-null `Snapshot_ID` is actually present in backend storage. |
32+
| O-REV-2 | `reversibility_types.ads` | `Snapshot_Is_Valid`: a `Valid`-state snapshot is restorable. |
33+
| O-REV-3 | `reversibility_types.ads` | `System_State_Matches_Snapshot`: after a `Completed` rollback, on-disk system state equals the snapshot's captured state (rollback atomicity / no partial state). Subsumes the "Rollback atomicity" item above. |
34+
| O-REV-4 | `reversibility_types.ads` | Monotonic snapshot IDs: issued IDs strictly increase and are never reused after deletion. |
35+
| O-SAFE-1 | `safety_invariant.ads` | The invariant "every modifying operation is preceded by a recovery point" (`Invariant_Holds`) is preserved by all state-update operations. |
36+
| O-SAFE-2 | `safety_boundary.ads` | The token-typed API (`Recovery_Point_Token`) admits no path that performs a modifying `Safe_*` operation without a valid recovery point, i.e. the runtime-enforced invariant is also statically unviolable. |
37+
38+
Status: **none of O-REV-1..4 / O-SAFE-1..2 are checked at compile time or
39+
runtime today.** They are honest debt, not regressions, and are
40+
deliberately *not* claimed as proven anywhere in the source.
41+
1842
## Recommended prover
1943
- **Idris2** — State machines and idempotency properties are natural fits for dependent types
2044

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
obj-check/
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
-- SPDX-License-Identifier: PMPL-1.0-or-later
2+
-- Scoped compile-check project: verifies the reversibility/safety
3+
-- subsystem (the units touched by the SPARK honest-demotion) WITHOUT
4+
-- pulling the unrelated, unprovided `ncurses` TUI dependency. Not for
5+
-- shipping — a verification harness only.
6+
7+
project Checkonly is
8+
9+
for Source_Dirs use
10+
("src/reversibility",
11+
"src/safety",
12+
"src/backends",
13+
"src/platform");
14+
for Object_Dir use "obj-check";
15+
-- No `for Main`: compile-only, no bind/link, no ncurses needed.
16+
17+
package Compiler is
18+
for Switches ("Ada") use
19+
("-gnat2022",
20+
"-gnatwa",
21+
"-gnato13",
22+
"-gnatf",
23+
"-gnata"); -- assertions on: exercises the retained Pre/Post
24+
end Compiler;
25+
26+
end Checkonly;

total-update/ada/dnfinition/src/backends/backend_interface.ads

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,13 @@ package Backend_Interface is
6868

6969
type Transaction_Item is record
7070
Kind : Transaction_Kind;
71-
Package : Package_Info;
71+
Pkg : Package_Info;
72+
-- Renamed from `Package`: that is an Ada reserved word and was a
73+
-- hard legality error (backend_interface.ads:71) that prevented the
74+
-- dnfinition subsystem from compiling at all — the concrete reason
75+
-- the old SPARK "formally verified" banners were pure theatre
76+
-- (GNATprove can prove nothing about code that does not compile).
77+
-- Field is unreferenced anywhere; rename is purely local.
7278
end record;
7379

7480
type Transaction_List is array (Positive range <>) of Transaction_Item;

total-update/ada/dnfinition/src/reversibility/reversibility_types.adb

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

total-update/ada/dnfinition/src/reversibility/reversibility_types.ads

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,18 @@
22
-- SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell <jonathan@hyperpolymath.io>
33

44
-- Reversibility Types - Core types for transaction safety and rollback
5-
-- This module uses SPARK for formal verification of reversibility guarantees
6-
7-
pragma SPARK_Mode (On);
5+
--
6+
-- HONESTY NOTE (2026-05-18 audit): This unit was previously marked
7+
-- SPARK_Mode (On) and carried ghost functions
8+
-- (Snapshot_Exists / System_State_Matches_Snapshot / ...) whose bodies
9+
-- are stubbed to return True. No GNATprove proof has ever been run or
10+
-- passed (the unit does not even pass SPARK Phase-2 legality: see the
11+
-- record-component / type name clash on Rollback_Result.Snapshot_ID).
12+
-- SPARK_Mode is therefore set Off until a genuine proof obligation
13+
-- exists. The real reversibility obligations are tracked as proof debt
14+
-- in PROOF-NEEDS.md (Idris2), NOT claimed as proven here.
15+
16+
pragma SPARK_Mode (Off);
817

918
with Ada.Calendar;
1019
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
@@ -87,33 +96,33 @@ package Reversibility_Types is
8796
Requires_Reboot);
8897

8998
type Rollback_Result is record
90-
Status : Rollback_Status := Not_Started;
91-
Message : Unbounded_String;
92-
Snapshot_ID : Snapshot_ID := Null_Snapshot;
99+
Status : Rollback_Status := Not_Started;
100+
Message : Unbounded_String;
101+
Target_Snapshot : Snapshot_ID := Null_Snapshot;
102+
-- Renamed from Snapshot_ID: a record component named identically to
103+
-- the type Snapshot_ID is a SPARK/Ada name clash and was one of the
104+
-- legality errors that blocked GNATprove Phase 2.
93105
end record;
94106

95107
-- ═══════════════════════════════════════════════════════════════════════
96-
-- SPARK Contracts for Reversibility Guarantees
108+
-- Reversibility Obligations (UNPROVEN — tracked proof debt)
97109
-- ═══════════════════════════════════════════════════════════════════════
98-
99-
-- Ghost functions for formal verification
100-
function Snapshot_Exists (ID : Snapshot_ID) return Boolean
101-
with Ghost;
102-
-- True if snapshot with given ID exists in the system
103-
104-
function Snapshot_Is_Valid (ID : Snapshot_ID) return Boolean
105-
with Ghost,
106-
Post => (if Snapshot_Is_Valid'Result then Snapshot_Exists (ID));
107-
-- True if snapshot exists and is in Valid state
108-
109-
function System_State_Matches_Snapshot (ID : Snapshot_ID) return Boolean
110-
with Ghost,
111-
Pre => Snapshot_Exists (ID);
112-
-- True if current system state matches the snapshot
113-
114-
function Current_System_State_ID return Snapshot_ID
115-
with Ghost;
116-
-- Conceptual ID of current system state (for verification)
110+
--
111+
-- These properties are NOT formally verified. They were previously
112+
-- expressed as SPARK ghost functions with stub bodies (return True),
113+
-- which constitutes proof theatre. They are recorded here as prose
114+
-- obligations and tracked in PROOF-NEEDS.md for an Idris2 model:
115+
--
116+
-- O-REV-1 Snapshot_Exists: a snapshot referenced by a non-null
117+
-- Snapshot_ID is actually present in backend storage.
118+
-- O-REV-2 Snapshot_Is_Valid: a "Valid"-state snapshot is restorable.
119+
-- O-REV-3 System_State_Matches_Snapshot: after a Completed rollback,
120+
-- on-disk system state equals the snapshot's captured state
121+
-- (rollback atomicity / no partial state).
122+
-- O-REV-4 Monotonic snapshot IDs: issued IDs strictly increase and
123+
-- are never reused after deletion.
124+
--
125+
-- None of O-REV-1..4 are checked at compile time or runtime today.
117126

118127
-- ═══════════════════════════════════════════════════════════════════════
119128
-- Configuration

total-update/ada/dnfinition/src/reversibility/snapshot_manager.adb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,21 +183,21 @@ package body Snapshot_Manager is
183183
Result := (
184184
Status => Completed,
185185
Message => To_Unbounded_String ("Native rollback complete"),
186-
Snapshot_ID => ID
186+
Target_Snapshot => ID
187187
);
188188

189189
when Platform_Detection.Btrfs | Platform_Detection.ZFS =>
190190
Result := (
191191
Status => Requires_Reboot,
192192
Message => To_Unbounded_String ("Snapshot restored, reboot required"),
193-
Snapshot_ID => ID
193+
Target_Snapshot => ID
194194
);
195195

196196
when others =>
197197
Result := (
198198
Status => Failed,
199199
Message => To_Unbounded_String ("Manual rollback required"),
200-
Snapshot_ID => ID
200+
Target_Snapshot => ID
201201
);
202202
end case;
203203

@@ -209,7 +209,7 @@ package body Snapshot_Manager is
209209
Result := (
210210
Status => Failed,
211211
Message => To_Unbounded_String ("Snapshot not found"),
212-
Snapshot_ID => Null_Snapshot
212+
Target_Snapshot => Null_Snapshot
213213
);
214214
end Rollback_To_Snapshot;
215215

@@ -221,7 +221,7 @@ package body Snapshot_Manager is
221221
Result := (
222222
Status => Not_Started,
223223
Message => Ada.Strings.Unbounded.To_Unbounded_String ("No snapshots"),
224-
Snapshot_ID => Null_Snapshot
224+
Target_Snapshot => Null_Snapshot
225225
);
226226
else
227227
Rollback_To_Snapshot (Snapshots (Snapshot_Count).ID, Result);

0 commit comments

Comments
 (0)