diff --git a/src/pacta/estateview.py b/src/pacta/estateview.py
index 1967d79..dbeb664 100644
--- a/src/pacta/estateview.py
+++ b/src/pacta/estateview.py
@@ -173,7 +173,7 @@ ESTATE_HTML = r'''
LTL estate map — repos, services, loops
pasta-pallas-verified
field layer proven · curve layer pending
not attested
fips205-slhdsa-verified
SLH-DSA-SHA2-128s verify path · skeleton — 0 certificates
-
campaign in progressnot attested
+ 11 certs · reviewer attest-with-conditionsnot attested
ltl-accumulator-verified
61 certs · proofs about the log's own accumulator model
attested · entry 13frozen 172a1d0loop 2
@@ -249,7 +249,7 @@ const RUNTIME = {
dalek:"static repo — proofs replay on demand", anza:"static repo — proofs replay on demand",
risc0:"static repo — proofs replay on demand", bet:"static repo — proofs replay on demand",
pasta:"static repo — open work, run manually", corpus:"frozen repo — replay on demand",
- fips:"static repo — no process; campaign sessions are episodic operator-machine runs under lean-guard; check.sh exits non-green by design",
+ fips:"static repo — no process; campaign sessions are episodic operator-machine runs under lean-guard; check.sh GREEN with an 18-attack self-test",
provider:"SPLIT: the write side (check/append/publish) runs ON DEMAND on the operator machine, only during a ceremony; the read-only web face runs ALWAYS ON in the droplet container",
signer:"on demand — invoked only while signing during a ceremony; key offline otherwise",
conslib:"library — runs inside whichever consumer invokes it",
@@ -276,10 +276,10 @@ const DOSSIER = {
srcBet:{lane:"Upstream inputs",mut:"frozen",facts:["Pinned clone of the Betrusted dalek fork.","xous-core and litex-boards sit alongside as platform context.","Input to extraction; never modified."]},
srcPasta:{lane:"Upstream inputs",mut:"frozen",facts:["Pinned clone of the Pasta curves crate.","Feeds pasta-pallas-verified; never modified."]},
srcFips205:{lane:"Upstream inputs",mut:"frozen",facts:["Verbatim snapshot of integritychain/fips205 — pure-Rust FIPS 205 / SLH-DSA (zero unsafe, no_std, const-generic).","Pinned at upstream 30bac08 (2025-09-01); snapshot head 5dca0db — the single deviation from verbatim is stripping upstream CI workflows, documented in that commit.","Aeneas-compat patches land HERE as transparent, individually-justified commits; nothing is proposed upstream (no affiliation)."]},
- dalek:{lane:"Verified subjects",mut:"frozen",facts:["16 reviewed certificates: field, group law, scalars, signature apex (T1–T4).","Attested in all three log generations; current leaf 8.","LOOP 1 anchor: the dogfood signer binary is built from this source — the log's heads are signed by code whose proofs are inside the log.","Attestation pins a commit; the branch only moves for docs."]},
- anza:{lane:"Verified subjects",mut:"frozen",facts:["16 reviewed certificates; current leaf 9.","Same proof pyramid as dalek, rebuilt for the fork's code structure."]},
- risc0:{lane:"Verified subjects",mut:"frozen",facts:["16 reviewed certificates; current leaf 10.","Differs from Betrusted's corpus by 27 changed proof lines (the paper's portability datum)."]},
- bet:{lane:"Verified subjects",mut:"frozen",facts:["16 reviewed certificates; current leaf 11."]},
+ dalek:{lane:"Verified subjects",mut:"frozen",facts:["31 bound certificates (field, group law, scalars, signature apex T1–T4) and 3022 inventoried constants; 16 of the certificates are the curated attested subset in the log.","Attested in all three log generations; current leaf 8.","LOOP 1 anchor: the dogfood signer binary is built from this source — the log's heads are signed by code whose proofs are inside the log.","Attestation pins a commit; the branch only moves for docs."]},
+ anza:{lane:"Verified subjects",mut:"frozen",facts:["31 bound certificates, 3022 inventoried constants; attested subset is 16; current leaf 9.","Same proof pyramid as dalek, rebuilt for the fork's code structure."]},
+ risc0:{lane:"Verified subjects",mut:"frozen",facts:["31 bound certificates, 3022 inventoried constants; attested subset is 16; current leaf 10.","Differs from Betrusted's corpus by 27 changed proof lines (the paper's portability datum)."]},
+ bet:{lane:"Verified subjects",mut:"frozen",facts:["31 bound certificates, 3022 inventoried constants; attested subset is 16; current leaf 11."]},
pasta:{lane:"Verified subjects",mut:"free",facts:["Field layer proven from own extraction; curve layer (group law + scalar mul) remains open work.","NOT attested — the log carries only the four Ed25519 forks + the corpus."]},
fips:{lane:"Verified subjects",mut:"free",facts:["CAMPAIGN IN PROGRESS — ZERO certificates: verification/check.sh exits non-green and says so; that script is the only source of the word «proven» for this repo.","Scope: the FIPS 205 verify path only (slh_verify → fors / hypertree → xmss → wots → chain), parameter set SLH-DSA-SHA2-128s; keygen and signing are trusted base, exactly as ed25519 signing was.","The six SHA-2 hash oracles are opaque external models (TRUSTED-BASE.md), kept outside every future certificate's dependency cone.","Gate-0 (2026-07-22): Charon clean; Aeneas translated the whole cone with exactly one obstruction class (the Hashers fn-pointer struct) — campaign phase 1 is the compat patch in fips205-source.","NOT attested — the log carries nothing from this campaign yet."]},
corpus:{lane:"Verified subjects",mut:"frozen",facts:["61 certificates over one boundary axiom (LTLAcc.sha256); 222-constant environment inventory; 15-gap honest ledger.","Mechanizes the archived report's §6: extractors, consistency binding, per-step pin safety.","LOOP 2 anchor: attested INTO the log as entry 13 — the log carries kernel-checked proofs about its own accumulator model.","Frozen at 172a1d0 (the attested commit); doc-only commits may move the branch.","Docs carry numbering notes: paper references are v0.2 numbering."]},
@@ -418,3 +418,135 @@ window.addEventListener("resize",()=>requestAnimationFrame(draw));
requestAnimationFrame(draw);setTimeout(draw,150);
'''
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# MEASURED PROGRESS PANEL
+#
+# Everything above this line is hand-written prose. That is why, between
+# 2026-07-22 and 2026-07-30, this page told the operator that the SLH-DSA
+# campaign was "in progress" with a button "non-green by design" while it had
+# eleven proven certificates and a green button, and that the ed25519 forks had
+# "16 reviewed certificates" while they had 31 bound and 3022 inventoried. A
+# page that asserts cannot notice it has gone out of date; only a page that
+# measures can.
+#
+# So this panel renders ONLY what formal-verification-control's
+# tools/estate-progress.py derived from the repositories, and it states three
+# things a reader would otherwise have to assume: when it was measured, whether
+# the repositories have moved since, and which parts of this page are measured
+# at all. If there is no snapshot it renders that fact loudly rather than
+# quietly rendering nothing.
+# ─────────────────────────────────────────────────────────────────────────────
+
+import json as _json
+import os as _os
+import subprocess as _sp
+
+PROGRESS_JSON = _os.environ.get(
+ "PACTA_PROGRESS_JSON",
+ "/home/oho/GitClone/FormalVerification/formal-verification-control/ESTATE-PROGRESS.json")
+ESTATE_ROOT = _os.environ.get(
+ "ESTATE_ROOT", "/home/oho/GitClone/Claude/FormalVerification")
+
+
+def _live_head(repo: str):
+ try:
+ r = _sp.run(["git", "-C", _os.path.join(ESTATE_ROOT, repo),
+ "rev-parse", "--short", "HEAD"],
+ capture_output=True, text=True, timeout=5)
+ return r.stdout.strip() or None
+ except Exception:
+ return None
+
+
+def _panel(cls: str, title: str, body: str) -> str:
+ return (f'')
+
+
+def progress_panel() -> str:
+ """The measured half of this page. Never falls back to prose."""
+ style = """
+"""
+
+ if not _os.path.exists(PROGRESS_JSON):
+ return style + _panel("bad", "Progress: NOT MEASURED", f"""
+ No snapshot at {PROGRESS_JSON}, so this page is showing
+ you nothing rather than something stale. That is deliberate:
+ the previous version of this page displayed hand-typed claims that were
+ eight days out of date, and looked exactly as confident as this one.
+ To populate it:
+ formal-verification-control/tools/estate-progress.py --json
""")
+
+ try:
+ d = _json.load(open(PROGRESS_JSON))
+ except Exception as e:
+ return style + _panel("bad", "Progress: SNAPSHOT UNREADABLE", f"{e}
")
+
+ moved = []
+ for repo, recorded in (d.get("repo_heads") or {}).items():
+ live = _live_head(repo)
+ if live and recorded and live != recorded:
+ moved.append((repo, recorded, live))
+
+ rows = []
+ for c in d["campaigns"]:
+ ax = c["axes"]
+ for axis, label in (("proof", "act one · proof"), ("attestation", "act two · attestation")):
+ if axis not in ax:
+ continue
+ a = ax[axis]
+ unm = (f' '
+ f'+{a["unmeasurable"]} unmeasurable' if a["unmeasurable"] else "")
+ rows.append(
+ f'| {c["title"]} | {label} | '
+ f'{a["earned"]} / {a["identified"]} | '
+ f'
| '
+ f'{a["pct"]}%{unm} | '
+ f'{c["reproduction_note"]} |
')
+
+ t = d["totals"]
+ head = (f'act one — proof {t["proof"]["pct"]}% · '
+ f'act two — attestation {t["attestation"]["pct"]}% '
+ f'(two numbers, never one: a single figure is what let the '
+ f'old metric report 100% for work nobody had attacked)
')
+
+ table = ('| campaign | axis | band-points | '
+ ' | verified | reproduction |
'
+ + "".join(rows) + "
")
+
+ contra = ""
+ if d.get("contradictions"):
+ items = "".join(f"{c['id']}: {c['detail']}"
+ for c in d["contradictions"])
+ contra = (f'The ledger contradicts the repositories. '
+ f'These numbers are not trustworthy until this list is empty:
')
+
+ note = (f'Measured {d["generated_at"]} by {d["generator"]}, '
+ f'from the repositories as they were at that moment. Nothing here is cached or '
+ f'carried forward. Everything on this page ABOVE this panel is '
+ f'hand-written prose and can be out of date; only this panel is derived.
')
+
+ if moved:
+ rowsm = "".join(f"{r}: measured at {a}, "
+ f"now {b}" for r, a, b in moved)
+ return style + _panel(
+ "warn", "Progress: MEASURED, BUT THE REPOSITORIES HAVE MOVED SINCE",
+ f'{len(moved)} repository(ies) changed after this snapshot, so the figures '
+ f'below describe an earlier state:
{head}{table}{contra}{note}'
+ f'Re-run tools/estate-progress.py --json to refresh.
')
+
+ return style + _panel("", "Progress — measured, not asserted",
+ head + table + contra + note)
diff --git a/src/pacta/walletui.py b/src/pacta/walletui.py
index 8a60c3a..f126d8d 100644
--- a/src/pacta/walletui.py
+++ b/src/pacta/walletui.py
@@ -1070,8 +1070,11 @@ def make_handler(wallet_dir: Path):
elif route == "/manual":
page("lab manual", "/manual", render_manual())
elif route == "/estate":
- from .estateview import ESTATE_HTML
- self._send(ESTATE_HTML + _ESTATE_BACK_CHIP)
+ from .estateview import ESTATE_HTML, progress_panel
+ # The map is hand-written prose; the panel is derived from the
+ # repositories. Serving them together, in that order, is what
+ # stops a reader mistaking the first for the second.
+ self._send(ESTATE_HTML + progress_panel() + _ESTATE_BACK_CHIP)
elif route.startswith("/station/"):
station_id = route.removeprefix("/station/")
station = STATION_BY_ID.get(station_id)