Layer 1: guest 9301 destroyed, vm-9301-disk-0 removed, pct list clean. Layer 2: local-lvm available returned to 258 702 410 KiB — EXACTLY the pre-run baseline (29.27%). Layer 3: no hub record was ever created (the scratch guest ran no controller and was never enrolled, by design); confirmed absent from /configs and /hosts after teardown. papra referral updated: c10-soak was torn down by the other session mid-run and papra disappeared from hub telemetry with grafana, homebox and rallly — Campaign 10's four discriminator apps (§A4). So papra's one deployment was on c10-soak. The fix is STILL not applied: that is absence evidence, demo-hp is fenced, and applying it wrongly is irreversible while leaving it is a one-line push. The single command that settles it is recorded.
3.6 KiB
REPORT — catalog persistence sweep (2026-08-02)
Question: does every app's data actually land in a folder the template preserves? Answer, over all 53 templates: 43 CLEAN · 3 BROKEN · 7 UNDETERMINED.
Full report, per-app evidence, proofs and proposed register entries:
audits/persistence-sweep-2026-08-02/README.md.
What was found
| app | defect | disposition |
|---|---|---|
| gramps-web | Mounted /app/data (a path the app never writes), /app/media, /tmp. Its accounts database AND its family tree landed in the container's writable layer — destroyed by any redeploy, absent from every backup, while gramps_data was tarred nightly as an empty directory. |
FIXED — persists the 8 paths the image's own environment names. Survives-a-redeploy proven: users.sqlite and the family-tree files byte-identical, same inode. |
| wishlist | Mounted wishlist_data:/data, another path the app never writes. prod.db landed in the anonymous volume from the image's VOLUME directive — never in ResolveDockerVolumeNames, so never backed up, and orphaned by a redeploy. |
FIXED — mounts /usr/src/app/data + /usr/src/app/uploads. prod.db byte-identical with the same inode across a redeploy. |
| papra (R-156) | Mounts papra_data:/app/data, which is root-owned, unwritable by uid 999, and does not exist in the papra image at all; the database is written to /app/app-data/db/. |
NOT FIXED — referred to the operator. Fix prepared and proven in the scratch guest (current: db.sqlite differs after a redeploy, so a real account created via the API is lost; fixed: byte-identical, it survives). It was deployed on one box; that box (c10-soak) was torn down by the other session mid-run and papra now appears nowhere in hub telemetry — but that is absence evidence, and demo-hp is fenced. One command settles it: §6.1a. |
7 UNDETERMINED are listed with reasons and never folded into CLEAN (§4.2): 4 apps write nothing until a user completes setup, 3 could not be started.
What was built
scripts/check-volume-persistence.py — the third catalog gate and the only RUNTIME one. This
defect class is invisible to static analysis, measured rather than assumed: a static audit of
all 53 composes reports the catalog clean and reports papra clean. So the gate deploys each
template, exercises it into writing data, and compares where the data landed with what is mounted.
Exit 0 clean / 1 REFUSED / 2 undecided; UNDETERMINED is never a pass.
It refuses to report at all unless it has just re-proven itself in both directions against two
canary templates that differ only in which path the volume mounts at — so every run carries a live
demonstration of R-156 and of its fix. 41 fixture tests driving check() (the function __main__
calls); every rule red-proofed.
Registered in CLAUDE.md and REUSE.md. Enforcement is convention, not CI — the catalog repo
has no CI of any kind. Raising that is proposed as R-160.
Method notes worth carrying forward
- Seven detector defects were found by adjudicating flagged apps rather than trusting the label, and six of them made a correct app look broken. Every one came from a rule identifying data by its name rather than by evidence of what it is. Detail: §3.5.
- The sweep was restarted from the first app twice rather than merge a matrix built by more than one version of the rules.
- No
docker execanywhere — Campaign 7 §1.1's OCI-error-to-stdout trap. - No hub record was created; the hub was read-only throughout. Teardown, all three layers: §9.