restverify 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. restverify-0.1.0/.gitignore +9 -0
  2. restverify-0.1.0/CHANGELOG.md +49 -0
  3. restverify-0.1.0/PKG-INFO +83 -0
  4. restverify-0.1.0/README.md +71 -0
  5. restverify-0.1.0/docs/PHASE2-PLAN.md +324 -0
  6. restverify-0.1.0/docs/RELEASE.md +75 -0
  7. restverify-0.1.0/docs/SECURITY.md +221 -0
  8. restverify-0.1.0/pyproject.toml +25 -0
  9. restverify-0.1.0/rv_testrun.txt +0 -0
  10. restverify-0.1.0/scripts/security_sweep.py +380 -0
  11. restverify-0.1.0/src/restverify/__init__.py +25 -0
  12. restverify-0.1.0/src/restverify/__main__.py +11 -0
  13. restverify-0.1.0/src/restverify/cli.py +1039 -0
  14. restverify-0.1.0/src/restverify/compare.py +221 -0
  15. restverify-0.1.0/src/restverify/config.py +176 -0
  16. restverify-0.1.0/src/restverify/errors.py +193 -0
  17. restverify-0.1.0/src/restverify/excludes.py +165 -0
  18. restverify-0.1.0/src/restverify/history.py +292 -0
  19. restverify-0.1.0/src/restverify/manifest.py +226 -0
  20. restverify-0.1.0/src/restverify/restic.py +180 -0
  21. restverify-0.1.0/src/restverify/sampling.py +193 -0
  22. restverify-0.1.0/src/restverify/tempstore.py +128 -0
  23. restverify-0.1.0/tests/conftest.py +293 -0
  24. restverify-0.1.0/tests/test_cli_contract.py +92 -0
  25. restverify-0.1.0/tests/test_i1_run.py +228 -0
  26. restverify-0.1.0/tests/test_i1_safety.py +112 -0
  27. restverify-0.1.0/tests/test_i2_compare.py +285 -0
  28. restverify-0.1.0/tests/test_i2_manifest.py +255 -0
  29. restverify-0.1.0/tests/test_i2_sampling.py +175 -0
  30. restverify-0.1.0/tests/test_i3a_json.py +202 -0
  31. restverify-0.1.0/tests/test_i3b_taxonomy.py +148 -0
  32. restverify-0.1.0/tests/test_i3c_contract.py +102 -0
  33. restverify-0.1.0/tests/test_i4a_history.py +228 -0
  34. restverify-0.1.0/tests/test_i4b_report.py +232 -0
  35. restverify-0.1.0/tests/test_i4c_retention.py +151 -0
  36. restverify-0.1.0/tests/test_i5a_cron.py +206 -0
  37. restverify-0.1.0/tests/test_i5b_demo.py +94 -0
  38. restverify-0.1.0/tests/test_i5c_packaging.py +53 -0
  39. restverify-0.1.0/tests/test_i6_negatives.py +322 -0
  40. restverify-0.1.0/tests/test_i6_sweep.py +204 -0
  41. restverify-0.1.0/tests/test_i7b_reconciliation.py +204 -0
  42. restverify-0.1.0/tests/test_i7c_closeout.py +68 -0
  43. restverify-0.1.0/testsuite_final.txt +5 -0
  44. restverify-0.1.0/testsuite_full.txt +1802 -0
  45. restverify-0.1.0/testsuite_run2.txt +35 -0
  46. restverify-0.1.0/testsuite_run3.txt +11 -0
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .coverage
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] — first release — 2026-09-28
4
+
5
+ ### Shipped (I1→I7)
6
+ - **Config + contract (I1):** TOML config (`RESTVERIFY_CONFIG` override; U1: a
7
+ missing config is not an error); a plain `password` key is refused with a
8
+ teaching error, never silently ignored (R2/R24). `restic.py` is the only
9
+ module permitted to invoke restic, and only read-only verbs — mutating verbs
10
+ raise at the call site (N1/N2/N6).
11
+ - **Restore + cleanup (I1):** private (0700) temp dirs carrying ownership
12
+ markers, removed on exit; leftovers from a killed process are swept by the
13
+ next run; foreign directories are never touched.
14
+ - **Manifest + comparison (I2):** restored-tree manifest, deterministic sample
15
+ sha256, excludes-aware source comparison, exit code 2 on drift.
16
+ - **JSON + taxonomy (I3):** `"schema": 1` envelope on every path; stdout stays
17
+ pure; exhaustive exit-code taxonomy test.
18
+ - **History + report (I4):** SQLite store at the XDG state dir (failures
19
+ included), `report` trend, 1000-row retention per repository.
20
+ - **Cron (I5):** `cron` / `--systemd` print a line or a unit pair and never
21
+ install anything.
22
+ - **Security posture (I6):** the negative requirements as executable
23
+ assertions, the dependency claim as a test, `docs/SECURITY.md` with a
24
+ staleness test.
25
+ - **Real-repo validation (I7):** fake-restic fidelity (real exit behaviour),
26
+ missing-repo teaching, restic-glob exclude semantics — **B1 closed**, **U7
27
+ measured**, and the report can no longer rot.
28
+ - **Native Windows support:** the full suite and the run pipeline work on
29
+ Windows (real `restic.exe` fixture, symlink-privilege probe, suffix-matched
30
+ restore root, UTF-8 output pin, and the previously missing
31
+ `python -m restverify` entry point).
32
+
33
+ ### What changed for users
34
+ - First public version. Install with `pipx install restverify` (B2 ruling);
35
+ requires Python 3.11+ and a `restic` binary on PATH.
36
+ - The exit codes are the integration surface: `0` verified, `1` run could not
37
+ complete, `2` diff mismatch, `64` usage/config — cron/CI act on them; `64`
38
+ never borrows a verification code.
39
+ - Verification only: bring your own orchestration; restores always go to
40
+ private temp dirs and your password is never stored.
41
+
42
+ ### Still open (honest)
43
+ - `init --json` is deferred.
44
+ - C1 (PyYAML ruling) remains open.
45
+ - Verify with a repo you can afford to rehearse on.
46
+
47
+ ## [0.1.0.dev0] — Phase 2 scaffold
48
+ - Package layout, CLI surface with full help/examples, exit-code constants,
49
+ 12 CLI contract tests.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: restverify
3
+ Version: 0.1.0
4
+ Summary: Rehearse and verify restic restores locally, on a schedule, with a diff-proof report.
5
+ Requires-Python: >=3.11
6
+ Provides-Extra: dev
7
+ Requires-Dist: build==1.6.1; extra == 'dev'
8
+ Requires-Dist: pytest==8.3.4; extra == 'dev'
9
+ Provides-Extra: keyring
10
+ Requires-Dist: keyring==25.5.0; extra == 'keyring'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # restverify
14
+
15
+ Rehearse and verify restic restores locally, on a schedule, with a diff-proof report.
16
+
17
+ > **Status: v0.1.0 — first release.** `run`, `report` and `cron` are implemented
18
+ > and verified against real repos; `init --json` is still deferred. Verify with a
19
+ > repo you can afford to rehearse on.
20
+
21
+ ## Why
22
+
23
+ `restic check` validates the repository's internal structure. It does not prove the
24
+ repository can be restored. The restic project tracks this as open issue #2332
25
+ ("Backups are only real if they're able to be restored"). Today everyone hand-rolls a
26
+ `restore && diff` script that decays silently.
27
+
28
+ restverify is the verification layer: it restores to a temp directory, compares
29
+ against the source, records the run, and exits with a code your cron/CI can act on.
30
+ It does **not** orchestrate backups.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pipx install restverify
36
+ ```
37
+
38
+ Requires Python 3.11+ and a `restic` binary on PATH.
39
+
40
+ ## First run
41
+
42
+ ```bash
43
+ restverify run -r /srv/backup
44
+ ```
45
+
46
+ The run line keeps the Phase-2 field order — `✓ restored snapshot <id>: N files / X
47
+ in Ys; N diffs` — rather than the PDF's example order; three increments of
48
+ regression tests pin this order and the rewrite was cosmetic (I5 ruling R1).
49
+
50
+ ## Scheduled verification
51
+
52
+ ```bash
53
+ restverify cron -r /srv/backup # one ready-to-paste crontab line
54
+ restverify cron -r /srv/backup --systemd # a .service + .timer pair instead
55
+ ```
56
+
57
+ `cron` **prints and never installs**: no crontab is modified, `systemctl` is never
58
+ called, and nothing is written into any unit directory. It prints one example
59
+ cadence and leaves the schedule to you. A scheduler runs with a minimal
60
+ environment, so set `RESTIC_PASSWORD_COMMAND` (or `RESTIC_PASSWORD_FILE`) there —
61
+ restverify never stores your password.
62
+
63
+ ## History
64
+
65
+ Every verification writes one row to a local SQLite store — failures included,
66
+ because a trend that hides failures is worse than no trend:
67
+
68
+ - store: `~/.local/state/restverify/history.db` (XDG state dir; override with
69
+ `RESTVERIFY_STATE=/some/dir`)
70
+ - retention: the newest 1000 rows per repository, pruned on write; the first
71
+ prune announces itself on stderr
72
+ - read it with `restverify report` (human) or `restverify report --json`
73
+ (a `"schema": 1` envelope)
74
+ - `--dry-run` writes nothing; disabling the store is not supported yet
75
+
76
+ ## Honest limits (required by the contract)
77
+
78
+ - **Biggest risk:** operators who already run a full orchestrator (resticprofile,
79
+ restickler) will not switch. Mitigation: restverify is verification-only — bring your
80
+ own orchestration — and it offers an exit-code contract nothing else provides.
81
+ - Verification is as good as the comparison you ask for: without a source path it only
82
+ proves the restore completed, not that the data matches.
83
+ - Not a backup tool. No cloud targets, no prune/forget, no dashboards, no bouncer.
@@ -0,0 +1,71 @@
1
+ # restverify
2
+
3
+ Rehearse and verify restic restores locally, on a schedule, with a diff-proof report.
4
+
5
+ > **Status: v0.1.0 — first release.** `run`, `report` and `cron` are implemented
6
+ > and verified against real repos; `init --json` is still deferred. Verify with a
7
+ > repo you can afford to rehearse on.
8
+
9
+ ## Why
10
+
11
+ `restic check` validates the repository's internal structure. It does not prove the
12
+ repository can be restored. The restic project tracks this as open issue #2332
13
+ ("Backups are only real if they're able to be restored"). Today everyone hand-rolls a
14
+ `restore && diff` script that decays silently.
15
+
16
+ restverify is the verification layer: it restores to a temp directory, compares
17
+ against the source, records the run, and exits with a code your cron/CI can act on.
18
+ It does **not** orchestrate backups.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pipx install restverify
24
+ ```
25
+
26
+ Requires Python 3.11+ and a `restic` binary on PATH.
27
+
28
+ ## First run
29
+
30
+ ```bash
31
+ restverify run -r /srv/backup
32
+ ```
33
+
34
+ The run line keeps the Phase-2 field order — `✓ restored snapshot <id>: N files / X
35
+ in Ys; N diffs` — rather than the PDF's example order; three increments of
36
+ regression tests pin this order and the rewrite was cosmetic (I5 ruling R1).
37
+
38
+ ## Scheduled verification
39
+
40
+ ```bash
41
+ restverify cron -r /srv/backup # one ready-to-paste crontab line
42
+ restverify cron -r /srv/backup --systemd # a .service + .timer pair instead
43
+ ```
44
+
45
+ `cron` **prints and never installs**: no crontab is modified, `systemctl` is never
46
+ called, and nothing is written into any unit directory. It prints one example
47
+ cadence and leaves the schedule to you. A scheduler runs with a minimal
48
+ environment, so set `RESTIC_PASSWORD_COMMAND` (or `RESTIC_PASSWORD_FILE`) there —
49
+ restverify never stores your password.
50
+
51
+ ## History
52
+
53
+ Every verification writes one row to a local SQLite store — failures included,
54
+ because a trend that hides failures is worse than no trend:
55
+
56
+ - store: `~/.local/state/restverify/history.db` (XDG state dir; override with
57
+ `RESTVERIFY_STATE=/some/dir`)
58
+ - retention: the newest 1000 rows per repository, pruned on write; the first
59
+ prune announces itself on stderr
60
+ - read it with `restverify report` (human) or `restverify report --json`
61
+ (a `"schema": 1` envelope)
62
+ - `--dry-run` writes nothing; disabling the store is not supported yet
63
+
64
+ ## Honest limits (required by the contract)
65
+
66
+ - **Biggest risk:** operators who already run a full orchestrator (resticprofile,
67
+ restickler) will not switch. Mitigation: restverify is verification-only — bring your
68
+ own orchestration — and it offers an exit-code contract nothing else provides.
69
+ - Verification is as good as the comparison you ask for: without a source path it only
70
+ proves the restore completed, not that the data matches.
71
+ - Not a backup tool. No cloud targets, no prune/forget, no dashboards, no bouncer.
@@ -0,0 +1,324 @@
1
+ # PHASE 2.0 — PLANNING & VERIFICATION (blocking)
2
+
3
+ Contract: **"PRODUCT GAP RESEARCH — 2026-09-14 (final)"** = `GAP-RESEARCH-2026-09.pdf`
4
+ (`/home/deploy/ezmcyber/.firecrawl/GAP-RESEARCH-2026-09.pdf`, 4 pages, text-extracted
5
+ and read in full). Where the PDF, the master prompt, and this repo differ, this
6
+ document says so explicitly (see **Conflicts & deviations**). Nothing here is improvised.
7
+
8
+ Build order is mandated and not parallelised: **restverify → haccheck**.
9
+ Scaffolding + test infrastructure only may precede the GO gate below.
10
+
11
+ ---
12
+
13
+ ## 2.0.1 Spec extraction — numbered, testable requirements
14
+
15
+ ### PRODUCT 1 — restverify (PDF lines 53–90)
16
+
17
+ | # | Requirement (PDF) | Testable assertion |
18
+ |---|---|---|
19
+ | R1 | `init` writes a multi-repo TOML config | config parses with `tomllib`; 2 repos round-trip |
20
+ | R2 | Passwords via keychain / `RESTIC_PASSWORD_COMMAND` **only** | no code path writes a password; absence test over the package |
21
+ | R3 | Config records source paths per repo | source persisted and overridable via `--source` |
22
+ | R4 | Config records excludes | excludes applied to both restore and compare |
23
+ | R5 | Snapshot selector | `latest` default; explicit id/tag selectable |
24
+ | R6 | `run` lists snapshots then picks the newest | `restic snapshots --json` is called once, newest chosen |
25
+ | R7 | Restore to a temp directory | restore target is under the system temp dir |
26
+ | R8 | Produce a manifest + sample sha256 | manifest counts/sizes; N files hashed deterministically |
27
+ | R9 | Optional excludes-aware source compare | `--no-source` skips it; excludes honored when enabled |
28
+ | R10 | `report` reads SQLite run history, PASS/FAIL | rows have verdict, snapshot id, duration, reason |
29
+ | R11 | Exit codes `0=pass 1=restore-fail 2=diff-mismatch` | G2: each code asserted by a test |
30
+ | R12 | `--json` | valid JSON on stdout, complete fields, non-zero exit still emits JSON |
31
+ | R13 | `--dry-run` | restores nothing; prints what would happen; PASS line still printed |
32
+ | R14 | `--no-source` | verification without a source comparison succeeds |
33
+ | R15 | `cron` prints a crontab line | output is a pasteable line; nothing is installed |
34
+ | R16 | Config at `~/.config/restverify` | default path asserted; `--config` overrides |
35
+ | R17 | Temp restore cleaned up on exit | cleanup verified incl. `kill -9` (G3) |
36
+ | R18 | No daemon | absence test: no long-running process/listener created |
37
+ | R19 | Linux/macOS/WSL | path handling avoids platform assumptions |
38
+ | R20 | pipx/pip + Homebrew | entry point installs; Homebrew formula stub shipped (unverifiable on Linux — see C5) |
39
+ | R21 | Install → first verified run < 5 min | V3 timed proof |
40
+ | R22 | 10-second demo shape `✓ restored N files / X GB in Ys; N diffs; snapshot <id>` | output asserted against this shape (U5) |
41
+ | R23 | Read-only on repositories | no write/delete call against a repo |
42
+ | R24 | Passwords never stored by the tool | absence test + no secret in logs/history |
43
+ | R25 | Zero telemetry | V2: no network imports in shipped paths |
44
+ | R26 | Never writes to sources; `--strict` is the explicit compare | source paths opened read-only; `--strict` only tightens comparison |
45
+ | R27 | `restic` binary is a documented prerequisite | missing restic → teaching error (U2), never a traceback |
46
+ | R28 | Pinned, signed releases | release process doc; pinned dev deps |
47
+ | R29 | Human-readable byte totals (`cli.human_bytes()`, binary units, one decimal) | formatting asserted by a parametrised unit test; consumed by the run/demo line (R22) and the I4 `report` |
48
+
49
+ ### Negative requirements — PRODUCT 1 (PDF line 63)
50
+
51
+ | # | Must NOT exist | Absence test |
52
+ |---|---|---|
53
+ | N1 | backup creation | no `restic backup` invocation anywhere in shipped paths |
54
+ | N2 | prune/forget | no `restic prune` / `restic forget` invocation |
55
+ | N3 | cloud targets | no provider SDK/credential handling; repo is an opaque restic location |
56
+ | N4 | dashboards/web UI | no web framework import; no listener |
57
+ | N5 | notification integrations | no SMTP/webhook/chat SDK; only exit codes + an optional command hook that the user supplies |
58
+ | N6 | borg/tar backends | no borg/tar code path |
59
+ | N7 | Windows UI | no Windows-specific code; no GUI toolkit |
60
+
61
+ ### PRODUCT 2 — haccheck (PDF lines 143–168)
62
+
63
+ | # | Requirement (PDF) | Testable assertion |
64
+ |---|---|---|
65
+ | R30 | `haccheck <config-dir>` needs nothing else | positional arg only; works on a fixture dir |
66
+ | R31 | Semantic YAML parse: dup keys, multi-doc, quoting/bool pitfalls | each pitfall has a fixture and a distinct message |
67
+ | R32 | HA-aware unknown-key rule for a ~40-integration starter catalog | catalog ships ≥40 integrations; unknown key in a known integration is reported with file:line |
68
+ | R33 | Integration-name case rule | `Sensor` vs `sensor` produces a finding naming the correct form |
69
+ | R34 | Duplicate `entity_id` rule | cross-file duplicates reported once each |
70
+ | R35 | Automation trigger/action shape rule | malformed trigger/action reported with file:line |
71
+ | R36 | Broken `!include` / `!secret` | missing target reported; existing target not |
72
+ | R37 | Dangling `packages:` entries | dangling reference reported |
73
+ | R38 | Jinja compile, strict-undefined, stubbed context — **the real error, never the masked one** | the `#169958` fixture must show the underlying `TemplateAssertionError` text, not the generic schema message |
74
+ | R39 | Severity + plain-language hint per finding | every finding carries tier + one-line hint |
75
+ | R40 | Exit `0` / `1` | errors → 1; warnings/info only → 0 (with `--strict` escalating) |
76
+ | R41 | `--json` | complete machine-readable findings incl. suppressed counts |
77
+ | R42 | Bundled catalog, no runtime fetch | no network import; catalog read from package data |
78
+
79
+ ### Negative requirements — PRODUCT 2 (PDF line 149)
80
+
81
+ | # | Must NOT exist | Absence test |
82
+ |---|---|---|
83
+ | N8 | full HA schema | no bundled complete schema; catalog is explicitly partial and self-describing |
84
+ | N9 | live-instance communication | no HTTP client to HA; no token/URL handling |
85
+ | N10 | auto-fixing | no write path to the config dir in v1 (read-only; asserted by making the fixture dir read-only) |
86
+ | N11 | YAML rewriting | no YAML dump/write in shipped paths |
87
+ | N12 | Fleet/Edge | absent |
88
+ | N13 | no false-error on unknown integrations | unknown integration → informational tier only (asserted) |
89
+
90
+ ---
91
+
92
+ ## 2.0.2 Environment audit (real output, this box)
93
+
94
+ | Probe | Result | Consequence |
95
+ |---|---|---|
96
+ | `python3 --version` | 3.12.3 | OK |
97
+ | python3.11 / 3.12 | 3.11.15 present, 3.12.3 present | OK |
98
+ | python3.10 | **absent** | `tomllib` needs 3.11 → see C1 |
99
+ | `uv --version` | 0.12.0 | usable for venvs/tools |
100
+ | `pipx` | **absent** | V3 wording says "pipx install"; `uv tool install` is the equivalent — see B2 |
101
+ | `restic` | **absent** | BLOCKS I7 real-repo validation — see B1 |
102
+ | `asciinema` | **absent** | V4 demo artifact needs an alternative — see B3 |
103
+ | `brew` | absent (Linux) | Homebrew formula can be authored, not verified here — see C5 |
104
+ | `pdftotext` | present | PDF read in full (232 lines extracted) |
105
+
106
+ **I7 ran elsewhere.** The table above audits the *primary* build box, and on that
107
+ box `restic`, `pipx` and `asciinema` are all still absent, so the rows stand. The
108
+ operator ruled I7 onto the second host `vmi3416386` (the cowrie box), where
109
+ `restic 0.16.4` (package `0.16.4-2ubuntu0.24.04.3`), `pipx 1.4.3` and
110
+ `asciinema 2.4.0` were installed with authorisation, and the checkout stayed here
111
+ as the single source of truth (the wheel was built and committed here, then
112
+ installed on that host). Evidence: `restverify-I7-briefing.md` §1 and §6.
113
+
114
+ ### Git state (2.0.2 requires a clean tree + recorded base commit)
115
+
116
+ The two products are **standalone repos** (Gate F: one repo, one job). They are
117
+ created at `/home/deploy/oss/<product>`, so the WraithWall monorepo's dirty tree
118
+ (122 uncommitted entries at audit time, from unrelated in-flight work) does not
119
+ contaminate them.
120
+
121
+ - Parent monorepo `/home/deploy/ezmcyber`: branch `phase1-sandbox-contract`,
122
+ HEAD at audit **`07e8e93`** (note: HEAD moved during this session — it was
123
+ `b344f50` at the start; the intervening P4–P7 commits are breach-monitor work
124
+ and unrelated to these products).
125
+ - Product repo `/home/deploy/oss/restverify`: new repo, branch
126
+ **`product/restverify/phase2`**, first commit = this scaffold.
127
+
128
+ ---
129
+
130
+ ## 2.0.3 Dependency lock
131
+
132
+ | Product | Locked set (master prompt 2.0.3) | What we ship | Justification |
133
+ |---|---|---|---|
134
+ | restverify | stdlib + PyYAML (+optional keyring) | **stdlib only**; `keyring==25.5.0` optional extra | Zero runtime deps is strictly *inside* the lock (nothing outside it is introduced). The PDF's "PyYAML/JSON" was a suggestion of capability, and this design does not need YAML at all (see C1). |
135
+ | haccheck | PyYAML + Jinja2, exact pins | `PyYAML==6.0.2`, `Jinja2==3.1.4` (to be pinned at I1) | Exactly the locked set; nothing else. |
136
+ | dev (both) | — | `pytest==8.3.4` | Not shipped to users. |
137
+
138
+ Rationale for the optional keyring: PDF R2 says passwords come "via keychain /
139
+ `RESTIC_PASSWORD_COMMAND` only". `RESTIC_PASSWORD_COMMAND` needs no dependency;
140
+ keyring is an *optional extra* only (`restverify[keyring]`) and is never required.
141
+
142
+ ---
143
+
144
+ ## 2.0.4 Increment plan (commit-sized, each with a gate)
145
+
146
+ Every increment = one commit + one green test run + the standing gates
147
+ G1 (tests incl. ≥1 adversarial), G2 (exit codes), G3 (security: no network, no
148
+ write to sources, no secrets, temp cleanup under `kill -9`), G4 (U2/U3/U5 text),
149
+ G5 (diff ↔ requirement numbers), G6 (honest status).
150
+
151
+ ### PRODUCT 1 — restverify (PDF D1–D7 → I1–I7)
152
+
153
+ | I | Scope | Extra gate beyond the standing five |
154
+ |---|---|---|
155
+ | I1 | config/contract + restore-to-temp + cleanup-on-exit (R1,R2,R3,R5,R7,R16,R17,R23,R24,R27,N1,N2,N6) | adversarial: missing `restic`, empty repo, permission-denied temp dir; `kill -9` cleanup proof |
156
+ | I2 | manifest + sample sha256 + excludes-aware source compare (R4,R8,R9,R26,R29) | N-diffs == 0 on an identical tree; a single-byte change is detected |
157
+ | I3 | error handling + exit codes + `--json` `--dry-run` `--no-source` (R11,R12,R13,R14) | **G2 fully**: all three codes asserted; JSON valid on both success and failure |
158
+ | I4 | SQLite run history + `report` (R10) | trend renders; last failure reason in plain language; history survives restart |
159
+ | I5 | CLI surface + `cron` printer (R15,R22) | demo line matches the PDF shape within tolerance; `cron` installs nothing |
160
+ | I6 | test hardening + security pass (R25,N3,N4,N5,N7) | V2 sweep run and pasted into the report |
161
+ | I7 | docs, packaging, Homebrew stub, **≥2 real restic repos**, release prep (R20,R21,R28) | V1–V6 executed; V3 timed proof recorded |
162
+
163
+ Usability focus carried through: `init` walks the user interactively with sane
164
+ defaults; the first `run` prints the PASS line even under `--dry-run`; `report`
165
+ shows the trend with the last failure explained in plain language.
166
+
167
+ ### Test infrastructure (recorded 2026-09-19, closing the I2 G5 PARTIAL)
168
+
169
+ Test infrastructure is code under `tests/` that never ships. It carries **no
170
+ requirement id by design**; the rule that keeps G5 meaningful is the opposite one
171
+ for shipped code:
172
+
173
+ > Any new *shipped* helper that no requirement id covers must either gain a
174
+ > requirement row in the same commit that introduces it, or not ship.
175
+ > Test-only helpers are exempt, but every non-obvious one must be named in this
176
+ > section and explained in the docstring of the module that holds it.
177
+
178
+ | test infrastructure | why it exists | where it is documented |
179
+ |---|---|---|
180
+ | `tests/conftest.py` fake-restic harness (`FAKE_RESTIC`, `FAKE_FILES`/`FAKE_DIRS`/`FAKE_LINKS`, `plant_fake_tree`, `source_tree`, `tmp_base`, `config_path`) | the entire suite is deterministic and offline because a fake `restic` is put on `PATH` (B1: no real restic on the build box); `plant_fake_tree`/`source_tree` let a test plant a byte-identical source tree on purpose | module docstring + the I2/I1 briefings |
181
+ | `tests/test_i2_compare.py::fingerprint()` | proves the source tree is never written to (size + mtime_ns + sha256 before/after) — evidence for R26/R23 | that module's docstring |
182
+ | `tests/test_i2_manifest.py::test_human_bytes_units` | the R29 formatter's contract test (6 parametrised cases) | test docstring names R29 |
183
+ | `tests/conftest.py` `isolated_state` / `_state_base` | every test gets its own durable-state dir (autouse, `RESTVERIFY_STATE`), so the suite can never write the real `~/.local/state/restverify`; the per-session base prefers tmpfs because the store now does a real fsync per write | that fixture's docstring + the I4 briefing |
184
+ | `tests/test_i6_negatives.py` source index (`SHIPPED`, `_trees`, `_imports`, `_dotted_name`, `_env_read_names`, `_literal_strings`, `_spawn_calls`, `_listener_calls`, `_platform_branches`) | turns N3/N4/N5/N7 from prose into assertions: it parses every shipped module and answers "which module imports/spawns/listens/reads X". Deliberately **not** shared with `scripts/security_sweep.py` (I6b) — one broken walker would otherwise silently disable every absence test at once. Non-obvious parts: dynamic `importlib.import_module("x")` arguments count as imports, and only *named* env reads (`os.environ.get("AWS_…")`) count, not `dict(os.environ)` | module docstring + the I6 briefing |
185
+
186
+ Worked example of the shipped-code rule: `cli.human_bytes()` arrived as an
187
+ unplanned extra during I2 and was **numbered R29** (added to the requirement
188
+ table and to the I2 row) when the G5 PARTIAL was closed — it is user-visible
189
+ output formatting, so it could not stay uncatalogued.
190
+
191
+ ### PRODUCT 2 — haccheck (PDF D1–D7 → I1–I7) — starts only after Product 1 ships
192
+
193
+ | I | Scope | Extra gate |
194
+ |---|---|---|
195
+ | I1 | config-dir walker + robust YAML loader (R30,R31) | malformed/dup-key/multi-doc fixtures; unicode paths |
196
+ | I2 | versioned catalog + unknown-key/case rules (R32,R33,R42) | ≥40 integrations; unknown integration is **info**, never error (N13) |
197
+ | I3 | Jinja compile, strict-undefined, stubbed context (R38) | **MANDATORY #169958 fixture.** If the underlying error cannot be surfaced, STOP and report — no degraded ship |
198
+ | I4 | `!include`/`!secret`/`packages:` + duplicate `entity_id` (R34,R35,R36,R37) | dangling vs resolvable cases distinguished |
199
+ | I5 | severity tiers + suppression + hints + exit 0/1 (R39,R40,R41) | suppression never hides silently: suppressed counts always printed |
200
+ | I6 | tests incl. real public HA config corpora + security pass (N8–N12) | read-only proof on a read-only fixture dir |
201
+ | I7 | docs, packaging, pre-commit stub, release prep | V1–V6 |
202
+
203
+ ---
204
+
205
+ ## 2.0.5 haccheck risk paragraph (load-bearing)
206
+
207
+ **Severity tiers.** Three tiers, and the tier assignment is the contract that keeps
208
+ false positives from killing adoption: `error` (exit 1) is reserved for conditions
209
+ Home Assistant itself will reject or that cannot work — invalid YAML, duplicate
210
+ keys, unresolvable `!include`/`!secret`, a Jinja compile failure (the #169958
211
+ class), duplicate `entity_id`, and an unknown key **inside a known integration**.
212
+ `warning` (exit 0; exit 1 only under `--strict`) covers suspicious-but-possible
213
+ shapes such as a deprecated key or an unusual automation trigger shape. `info`
214
+ (always exit 0) covers catalog-coverage gaps: an integration we do not know is
215
+ **never** an error (PDF mandate N13), it is reported as "not in catalog v0.1" with
216
+ the catalog version, so the tool stays honest about its own partiality instead of
217
+ inventing errors.
218
+
219
+ **Versioned catalog layout.** `src/haccheck/catalog/<version>/` containing
220
+ `integrations` (names + known keys), `rules` (rule id, tier, message template,
221
+ hint), and `meta` (`ha_min_version`, `generated_from`, per-rule
222
+ `false_positive_mode`). `--catalog` selects a version; an unknown version errors
223
+ with the list of shipped ones. New HA releases get a **new catalog version**
224
+ instead of silently editing the old one, so a user can pin behaviour and a
225
+ regression can be bisected to a catalog change. Drift mitigation follows the PDF's
226
+ "check what we know" philosophy: unknown keys only ever reach `warning` when the
227
+ catalog's `ha_min_version` predates the `--ha-version` the user declares, and
228
+ `--explain <rule-id>` prints that rule's documented false-positive mode so a user
229
+ can judge a hit rather than trust it.
230
+
231
+ **Suppression mechanism.** Two one-line forms, both requiring no config file:
232
+ `.haccheckignore` with `rule-id path-or-glob [reason]` per line, and an inline
233
+ `# haccheck: ignore=<rule-id>` on the offending line. `haccheck --suppress-help`
234
+ (also shown in the summary of any run that had suppressions) prints a
235
+ ready-to-paste example. Suppressed findings are **counted and printed** in both
236
+ text and `--json` output — suppression is visible, never silent, mirroring the
237
+ `fp_guard` posture already used in this codebase (a suppressed finding is demoted,
238
+ never deleted).
239
+
240
+ ---
241
+
242
+ ## Conflicts & deviations (required: "PDF wins over this prompt; inform the operator")
243
+
244
+ ### C1 — Config format vs the dependency lock vs the Python floor (BLOCKING, needs your call)
245
+ The PDF says two things that cannot both hold literally:
246
+ - MVP: "`restverify init` (multi-repo **TOML** config)" and "config at `~/.config/restverify`".
247
+ - Architecture: "Python 3.10+ CLI (stdlib + **PyYAML**/JSON + sqlite3)".
248
+
249
+ The master prompt then locks "stdlib + PyYAML (+optional keyring)". A TOML config
250
+ written by the tool needs a TOML *writer*; `tomllib` (stdlib, read-only) only
251
+ exists on **3.11+**, while the PDF's floor is 3.10.
252
+
253
+ Options:
254
+ 1. **TOML config, Python 3.11+** — `tomllib` reads; we write a ~20-line serializer for
255
+ our simple schema. No new dependency, PDF's TOML is honoured, floor raised to 3.11.
256
+ *(Recommended.)*
257
+ 2. TOML config, keep 3.10 — requires a hand-rolled minimal TOML reader (more code,
258
+ more adversarial surface, departs further from the lock).
259
+ 3. YAML config via PyYAML — honours the lock and 3.10, but contradicts the PDF's
260
+ explicit "TOML" and the prompt's U1 spirit (a config the user must not need to touch).
261
+
262
+ **Recommendation: option 1.** Scaffold already reflects it (`requires-python = ">=3.11"`,
263
+ zero runtime deps). I will not proceed on this without your GO.
264
+
265
+ ### C2 — haccheck catalog format: the PDF contradicts itself
266
+ Architecture line says "TOML schema catalog"; the security line says "**bundled JSON**
267
+ catalog". Same document, two formats.
268
+ **Recommendation:** TOML (`tomllib`, read-only, versioned dir under `catalog/`) because
269
+ it is human-diffable in PRs and matches the rest of the tool's config surface; JSON
270
+ only if you prefer the security line to win. Recorded here so the choice is yours, not mine.
271
+
272
+ ### C3 — Dependency lock vs "as few as possible"
273
+ The PDF permits PyYAML; this design ships **zero** runtime dependencies. That is stricter
274
+ than the lock, not looser, so nothing outside the permitted set is introduced. Flagged
275
+ because it is a deliberate deviation from the PDF's stated stack, in the safe direction.
276
+
277
+ ### C4 — No license specified anywhere in the PDF
278
+ Both products are to be open-source (prompt: "free, open-source CLI tools") and V5
279
+ requires an honest README, but the PDF names no license. **Your call: MIT or Apache-2.0.**
280
+ restverify shells out to the `restic` binary (BSD-2-Clause) and links no restic code, so
281
+ there is no copyleft entanglement either way. This is a release-blocking (V-gate) item,
282
+ not a coding blocker.
283
+
284
+ ### C5 — Homebrew stub cannot be verified on this box
285
+ `brew` is absent (Linux). I7 will author the formula and mark it **UNVERIFIED**;
286
+ claiming otherwise would breach G6/V-gates.
287
+
288
+ ---
289
+
290
+ ## Blockers (must be resolved before the increments that need them)
291
+
292
+ | # | Blocker | Needed by | Proposed resolution |
293
+ |---|---|---|---|
294
+ | B1 | `restic` not installed on this box | **I7** (PDF: "validation against ≥2 real restic repos, local backend acceptable") | install restic (apt/release binary) and build two throwaway local repos; I1–I6 can proceed without it using a fake-restic harness for unit tests |
295
+ | B2 | `pipx` absent (V3 says "pipx install from local artifact") | **V3 / I7** | install pipx, or record `uv tool install` as the equivalent and state the substitution explicitly in the V3 proof |
296
+ | B3 | `asciinema` absent (V4 demo artifact) | **V4 / I7** | install asciinema, or ship a scripted `script(1)` typescript + the exact command transcript |
297
+ | B4 | Biggest product risk (PDF): operators running a full orchestrator will not switch | design, not tooling | mitigation is the PDF's own: verification-only positioning + the exit-code contract; stated verbatim in the README's honest-limits section |
298
+
299
+ ---
300
+
301
+ ## HALT-gate check before GO
302
+
303
+ - PDF vs repo divergence found? **Yes → reported** (C1, C2, C3, C5 above, none improvised).
304
+ - Dependency outside the locked set forced? **No** (C3 makes the set smaller).
305
+ - A PDF claim failing under test? **Not yet tested** — the first such claim to test is the
306
+ #169958 masking behaviour at haccheck I3; per the halt rule, if it no longer reproduces
307
+ I will report the delta and **not** improvise.
308
+
309
+ ---
310
+
311
+ ## GO GATE (2.0.4 requires STOP here)
312
+
313
+ **Status: scaffold committed; no product behaviour implemented. Awaiting operator GO.**
314
+
315
+ On `GO`, I proceed in order: **restverify I1 → I2 → … → I7**, then **haccheck I1 → … → I7**,
316
+ reporting the full R1–R9 completion report per product. No parallelisation. No Phase 3.
317
+
318
+ Outstanding decisions requested:
319
+ 1. **C1** — TOML + Python 3.11+ (recommended) / TOML + 3.10 custom reader / YAML.
320
+ 2. **C2** — haccheck catalog: TOML (recommended) or JSON.
321
+ 3. **C4** — license: MIT or Apache-2.0.
322
+ 4. **B2/B3** — may I install `pipx` and `asciinema` on this box, or should I record the
323
+ `uv tool install` / scripted-typescript substitutions for V3/V4?
324
+ 5. **B1** — may I install `restic` + create two throwaway local repos for I7 validation?
@@ -0,0 +1,75 @@
1
+ # Release checklist
2
+
3
+ Feeds **R28** ("Pinned, signed releases"). Seven steps, in order. Nothing here is
4
+ automated yet; each step is a command a human runs and reads the output of.
5
+
6
+ ## 1. Version bump
7
+
8
+ `pyproject.toml` → `[project] version`. Update it in one commit with the changelog
9
+ entry so the tag and the artefact agree. The dev suffix (`0.1.0.dev0`) is dropped
10
+ for the first real release.
11
+
12
+ ## 2. Changelog
13
+
14
+ Add the entry to the release notes with three headings, always in this shape:
15
+
16
+ - what shipped (requirement ids),
17
+ - what changed in behaviour a user will notice,
18
+ - what is still open (B1/C1/U7 style honest limits).
19
+
20
+ ## 3. Build
21
+
22
+ ```bash
23
+ cd <checkout>
24
+ python -m build # requires the dev extra: pip install -e '.[dev]'
25
+ ```
26
+
27
+ Expect `dist/restverify-<version>-py3-none-any.whl` and the sdist. A wheel that
28
+ contains `tests/`, `__pycache__/`, or a `history.db` is a failed build — inspect it:
29
+
30
+ ```bash
31
+ python -m zipfile -l dist/restverify-<version>-py3-none-any.whl
32
+ ```
33
+
34
+ ## 4. Install
35
+
36
+ ```bash
37
+ pipx install ./dist/restverify-<version>-py3-none-any.whl
38
+ pipx list
39
+ ```
40
+
41
+ `pipx` is the supported install path (B2 ruling). One venv per app, no
42
+ site-packages pollution, and the console script lands on PATH.
43
+
44
+ ## 5. Smoke test
45
+
46
+ ```bash
47
+ restverify --version
48
+ restverify --help
49
+ restverify cron -r /srv/backup # prints a line; must not install anything
50
+ restverify run -r <repo> --dry-run # must print the PASS line and write nothing
51
+ ```
52
+
53
+ If any of those fails, stop: the release is not shippable. Do not "fix forward"
54
+ on a tagged artefact - bump the version and rebuild.
55
+
56
+ ## 6. Tag
57
+
58
+ ```bash
59
+ git tag -s v<version> -m "restverify <version>"
60
+ git push --follow-tags
61
+ ```
62
+
63
+ Signed tags only (`-s`). An unsigned tag fails R28's intent.
64
+
65
+ ## 7. Publish
66
+
67
+ Publish the wheel and sdist to the index (or attach both to the signed tag if
68
+ publishing is deliberately deferred). Record the published hash in the release
69
+ notes so a downloader can verify the artefact they got is the one that was built.
70
+
71
+ ## After the release
72
+
73
+ Re-open the loop on the honest limits: B1 (no real restic exercised), C1 (PyYAML
74
+ ruling), U7 (install-to-value not measured). A release that hides those is worse
75
+ than a release note that names them.