|
| 1 | +# Snapshot Verification Docs Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Give contributors and consumers a copy-pasteable way to decode and verify `snapshot.json.gz.age`, closing the gap where the docs describe the encryption pipeline but show no runnable command. |
| 6 | + |
| 7 | +**Architecture:** Two additive documentation edits, no code/schema/behaviour change. CONTRIBUTING gains a verification step that round-trips a real export through the project's own `snapshot.read()` path; README gains a language-agnostic `age` CLI decode snippet next to the existing pipeline description, with a one-line "it's ciphertext, you can't open it directly" caveat. |
| 8 | + |
| 9 | +**Tech Stack:** Markdown only (`README.md`, `CONTRIBUTING.md`). Verification uses `grep`, the `age` CLI, and the existing `mt5-pnl-exporter` package. |
| 10 | + |
| 11 | +**Spec:** [`docs/superpowers/specs/2026-06-10-snapshot-verification-docs-design.md`](../specs/2026-06-10-snapshot-verification-docs-design.md) |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## File Structure |
| 16 | + |
| 17 | +- `CONTRIBUTING.md` — append step 6 to "Smoke-test a real export" (after line 40); update the "Steps 2–5" lead-in (line 42) to "Steps 2–6". |
| 18 | +- `README.md` — insert the consumer decode snippet immediately after the pipeline sentence in "How it works" (after line 177). |
| 19 | + |
| 20 | +No new files. No `CLAUDE.md` edit (this is not a command/architecture/gotcha change). |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +### Task 1: CONTRIBUTING — add the verification step |
| 25 | + |
| 26 | +**Files:** |
| 27 | +- Modify: `CONTRIBUTING.md:40-42` |
| 28 | + |
| 29 | +- [ ] **Step 1: Insert step 6 after the current step 5** |
| 30 | + |
| 31 | +Find this block (lines 40–42): |
| 32 | + |
| 33 | +```markdown |
| 34 | +5. `uv run mt5-pnl-exporter export` — confirm it logs `OK` per account and writes the snapshot. |
| 35 | + |
| 36 | +Steps 2–5 test the code in your working tree. To also test the **packaged artifact** a consumer installs (entry point, the `[mt5]` extra, the bundled schema file), build and install the wheel before publishing: |
| 37 | +``` |
| 38 | + |
| 39 | +Replace it with (adds step 6; changes "Steps 2–5" to "Steps 2–6"): |
| 40 | + |
| 41 | +````markdown |
| 42 | +5. `uv run mt5-pnl-exporter export` — confirm it logs `OK` per account and writes the snapshot. |
| 43 | +6. Verify the snapshot decrypts and validates — this exercises the same `age → gzip → JSON` read path a consumer uses. The on-disk file is ciphertext, so opening it directly won't work; read it back via the API: |
| 44 | + |
| 45 | + ```bash |
| 46 | + uv run python -c "from pathlib import Path; import mt5_pnl_exporter.snapshot as s, mt5_pnl_exporter.secrets as sec; snap = s.read(Path('<snapshot_path>'), sec.get_encryption_passphrase()); print(snap.generated_at, '|', len(snap.closed_deals), 'deals,', len(snap.open_positions), 'open,', len(snap.cash_flows), 'cash flows'); [print(a.login, a.label, a.balance, a.equity) for a in snap.accounts]" |
| 47 | + ``` |
| 48 | + |
| 49 | + Replace `<snapshot_path>` with your configured `snapshot_path`. If it prints without raising, the file is structurally sound — `read()` reverses the pipeline and validates the full pydantic model. |
| 50 | + |
| 51 | +Steps 2–6 test the code in your working tree. To also test the **packaged artifact** a consumer installs (entry point, the `[mt5]` extra, the bundled schema file), build and install the wheel before publishing: |
| 52 | +```` |
| 53 | + |
| 54 | +- [ ] **Step 2: Verify the edit** |
| 55 | + |
| 56 | +Run: `grep -n "Steps 2–6\|Verify the snapshot decrypts\|get_encryption_passphrase" CONTRIBUTING.md` |
| 57 | +Expected: three matches — the updated lead-in, the new step-6 heading, and the one-liner. Confirm no remaining "Steps 2–5" in the file: `grep -n "Steps 2–5" CONTRIBUTING.md` returns nothing. |
| 58 | + |
| 59 | +- [ ] **Step 3: Commit** |
| 60 | + |
| 61 | +```bash |
| 62 | +git add CONTRIBUTING.md |
| 63 | +git commit -m "$(cat <<'EOF' |
| 64 | +docs: add snapshot verification step to smoke test |
| 65 | +
|
| 66 | +Step 6 reads a freshly-exported snapshot back through snapshot.read(), |
| 67 | +confirming the artifact round-trips (not just that export ran) and |
| 68 | +exercising the same age → gzip → JSON path a consumer uses. |
| 69 | +
|
| 70 | +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
| 71 | +EOF |
| 72 | +)" |
| 73 | +``` |
| 74 | + |
| 75 | +--- |
| 76 | + |
| 77 | +### Task 2: README — add the consumer decode snippet |
| 78 | + |
| 79 | +**Files:** |
| 80 | +- Modify: `README.md:177` |
| 81 | + |
| 82 | +- [ ] **Step 1: Insert the decode snippet after the pipeline sentence** |
| 83 | + |
| 84 | +Find this line (line 177, in the "How it works" section): |
| 85 | + |
| 86 | +```markdown |
| 87 | +Gzip + `age` encryption is mandatory, not optional. The on-disk file is always `snapshot.json.gz.age`; readers must reverse the pipeline (`age decrypt → gunzip → json.loads`) to decrypt. Sync services (Dropbox, OneDrive, Syncthing) and backups only ever see ciphertext. |
| 88 | +``` |
| 89 | + |
| 90 | +Insert immediately after it (new blank line, then the snippet): |
| 91 | + |
| 92 | +````markdown |
| 93 | +The on-disk file is ciphertext — you can't open it directly (double-clicking a `.age` file just fails). To read it, reverse the pipeline. With the [age](https://age-encryption.org/) CLI installed (`brew install age` on macOS; see the age site for other platforms): |
| 94 | + |
| 95 | +```bash |
| 96 | +age -d mt5.json.gz.age | gunzip # prompts for the passphrase, prints the JSON |
| 97 | +``` |
| 98 | +```` |
| 99 | + |
| 100 | +- [ ] **Step 2: Verify the edit** |
| 101 | + |
| 102 | +Run: `grep -n "age -d mt5.json.gz.age\|can't open it directly\|brew install age" README.md` |
| 103 | +Expected: three matches, all within the "How it works" section (immediately after the line ending "only ever see ciphertext"). |
| 104 | + |
| 105 | +- [ ] **Step 3: Commit** |
| 106 | + |
| 107 | +```bash |
| 108 | +git add README.md |
| 109 | +git commit -m "$(cat <<'EOF' |
| 110 | +docs: show how to decode the encrypted snapshot |
| 111 | +
|
| 112 | +Add a language-agnostic `age -d | gunzip` snippet next to the pipeline |
| 113 | +description, with a one-line caveat naming the "can't open .age directly" |
| 114 | +dead-end. Install guidance is one example plus the upstream link, not a |
| 115 | +per-OS matrix. |
| 116 | +
|
| 117 | +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
| 118 | +EOF |
| 119 | +)" |
| 120 | +``` |
| 121 | + |
| 122 | +--- |
| 123 | + |
| 124 | +### Task 3: Final verification |
| 125 | + |
| 126 | +**Files:** none (read-only checks). |
| 127 | + |
| 128 | +- [ ] **Step 1: Confirm docs-only — no code/schema touched** |
| 129 | + |
| 130 | +Run: `git diff main --stat` |
| 131 | +Expected: only `CONTRIBUTING.md`, `README.md`, and the two spec/plan files under `docs/superpowers/` appear. No files under `src/`, `tests/`, or `schema/`. |
| 132 | + |
| 133 | +- [ ] **Step 2: Confirm the test suite is unaffected** |
| 134 | + |
| 135 | +Run: `uv run pytest -q` |
| 136 | +Expected: PASS (docs-only change; nothing in the suite depends on these files). |
| 137 | + |
| 138 | +- [ ] **Step 3 (host-dependent — run where possible, note if skipped): live decode checks** |
| 139 | + |
| 140 | +On the Windows host mid-smoke-test, run the CONTRIBUTING step-6 one-liner against the real snapshot (substituting the real `snapshot_path`): |
| 141 | +Expected: prints `generated_at | N deals, N open, N cash flows` then one line per account, without raising. |
| 142 | + |
| 143 | +On a host with the `age` CLI installed (e.g. macOS after `brew install age`), run `age -d mt5.json.gz.age | gunzip`: |
| 144 | +Expected: prompts `Enter passphrase:`, then prints the snapshot JSON to stdout. |
| 145 | + |
| 146 | +If either host isn't available in this session, note it as skipped rather than marking it done. |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +## Self-Review |
| 151 | + |
| 152 | +**Spec coverage:** |
| 153 | +- "Contributor verification step via `snapshot.read()`" → Task 1. ✓ |
| 154 | +- "Consumer language-agnostic decode command" → Task 2. ✓ |
| 155 | +- "One-line ciphertext caveat" → Task 2, step 1. ✓ |
| 156 | +- "`age -d` no `-p` on decrypt" → Task 2 snippet uses `age -d` with no `-p`. ✓ |
| 157 | +- "Install guidance = link + one example, not a matrix" → Task 2 snippet. ✓ |
| 158 | +- "No `CLAUDE.md` change" → File Structure note; Task 3 confirms docs-only. ✓ |
| 159 | +- "Verification: live decode on Windows + age CLI" → Task 3, step 3. ✓ |
| 160 | + |
| 161 | +**Placeholder scan:** `<snapshot_path>` is the only placeholder; it is intentional and the step tells the engineer to substitute the configured `snapshot_path`. No TBD/TODO/"handle edge cases". |
| 162 | + |
| 163 | +**Type consistency:** Function names match the source — `snapshot.read(path, passphrase)` and `secrets.get_encryption_passphrase()` (verified against `src/mt5_pnl_exporter/snapshot.py` and `secrets.py`). `age -d` (no `-p`) is the correct decrypt invocation. |
0 commit comments