Files
app-catalog-felhom.eu/REPORT.md
T
admin 6d45b60f94 persistence sweep: renumber proposed findings R-159..R-162 (R-158 collided mid-session)
The register grep that put these at R-158..R-161 was true when run and stale within hours: the
parallel session pushed SPIKE-recovery-unit-space-2026-08-02.md and CAMPAIGN-10-closeout-2026-08-02.md
mid-run, both using R-158 for an unrelated finding, and neither files it — ROADMAP.md and
OPEN-ITEMS.md still stop at R-155.

So two sessions minted the same number for different findings on the same day, which is the exact
failure §8.0 was already documenting about R-154/R-155 and R-156/R-157. Now recorded with itself as
the third instance. R-156, R-157 and R-158 are all live in audit documents and none is filed.
2026-08-02 12:24:48 +02:00

45 lines
3.6 KiB
Markdown

# 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-161.
## 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 exec` anywhere — 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.