summaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-19 10:09:09 +0200
committerDanilo M. <danix@danix.xyz>2026-08-19 10:09:09 +0200
commit6de81918dc7a9342fb532d6a5542c868ff65826c (patch)
tree893b93d299d640d11ca19cea6553971bea94b578 /docs/superpowers
parente91a7b519f8172b0d140dfed5fba76ba96728b0a (diff)
downloadqtmaildir-6de81918dc7a9342fb532d6a5542c868ff65826c.tar.gz
qtmaildir-6de81918dc7a9342fb532d6a5542c868ff65826c.zip
docs: close item 103 and tick the delete-to-trash plan
Every step of the plan is done. Item 103's section moves to the closed file on the same commit, per this repo's own rule, with its outcome recorded: what was built, the ten defects hand testing found that the suite did not, and the two process gaps closed alongside them. The fact worth carrying forward is the one that damaged real mail. Under mbsync's Create Both, a wrongly named origin folder propagates to the mail server, so any code composing a folder name reaches the server whether it means to or not. Item 118, emptying the trash, remains deferred at the user's request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md87
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md53
-rw-r--r--docs/superpowers/plans/2026-08-17-delete-to-trash.md94
3 files changed, 135 insertions, 99 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
index f46fec5..ef51b5c 100644
--- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
+++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
@@ -6612,3 +6612,90 @@ Three lessons, in the order they were paid for:
`image/png` that is demonstrably on the clipboard: a Wayland clipboard-manager
interaction, or GIMP's own paste path. Neither is this repository's, and neither
needs an item here until it is shown to be.
+
+## 103. What Delete does to mail on the server is undocumented and unverified
+
+**Observed (user, from the notes):** "verify how 'delete' works", with two
+sub-questions of their own: "trash bin (?)" and "delete from server (?)".
+
+**This is a question first.** The user is not reporting a defect; they are saying
+they do not know what the button does to their mail, which for a destructive
+action is its own problem regardless of the answer.
+
+**What the code does (verified).** `src/mainwindow.cpp:825` adds and removes the
+`deleted` tag, and nothing else. It is a toggle, it goes through the undo stack,
+and it writes a notmuch tag.
+
+**What that means downstream is what needs verifying, and it is NOT in this
+repo.** `maildir.synchronize_flags` is true, so notmuch maps certain tags to
+Maildir filename flags, and mbsync carries filename flags to the server. Whether
+`deleted` is one of those, whether the user's `~/.mbsyncrc` has `Expunge Both`
+(it does, on every channel), and what each provider does with a message flagged
+deleted, together decide whether this button is reversible. The undo stack makes
+the TAG reversible; it says nothing about what a sync did with it in between.
+
+**Measured 2026-08-17, and the answer is that Delete does not delete.** notmuch's
+tag-to-flag table has no row for `deleted` and no `T` flag, confirmed by a probe
+on a throwaway database: `+deleted` left the filename untouched while the
+control `+flagged` immediately produced `:2,F`. mbsync carries filename flags, so
+`Expunge Both` never sees anything to expunge, and `assets/mailsync.sh` contains
+no delete path. Both sub-questions answer no: no trash bin, no deletion from the
+server, and the mail stays in the Maildir and the index forever.
+
+**Specified in
+`specs/2026-08-17-delete-to-trash-design.md`. Read that before writing code.**
+Delete becomes a real move into the account's trash folder, with a `Trash`
+filter beside the other built-ins and a Restore action. Three constraints decide
+whether the spec is worth opening:
+
+- It needs a **mandatory per-account `trash` key**, so an existing config warns
+ until five keys are added. User-visible: minor bump and an `### Upgrading`
+ note.
+- The worker gains its **first non-tag mutation**, a rename plus a reindex,
+ deliberately shaped as `moveMessages(ids, destFolder)` so Send in v2 reuses it
+ for Drafts and Sent.
+- **848 messages carry the old tag** while sitting in an inbox, and would be
+ invisibly half-deleted after the change. A repeatable menu entry queries them
+ into the list for review; it is not a startup migration.
+
+The no-confirmation rule survives, with its justification amended: the mail
+lands in a browsable folder, but reversibility is now bounded by the provider
+where the trash is purged on a timer.
+
+**Size: S** for the investigation, which is done. The build is **M**.
+
+**Built 2026-08-19, over eight commits on `delete-to-trash`.** Delete moves the
+file into the account's `trash` folder and records where it came from in a
+`deleted-from:<folder>` tag, because the file has moved and nothing else still
+knows. A `Trash` filter sits beside the other four, `Restore from trash`
+(`Ctrl+R`) is the inverse, and `Find stranded deleted mail` (`Ctrl+Alt+T`, a
+menu entry only at the user's request) lists the 848 messages the old behaviour
+left tagged but unmoved. It reports and moves nothing: acting on its own would
+be a bulk delete with no selection behind it.
+
+**Ten defects were found by hand testing, not by the suite**, each reproduced
+with a probe before being fixed. One had already damaged real mail: a folder
+name containing a space was truncated by the origin tag, creating a bogus folder
+beside the real one and stranding four messages of a thread. mbsync had adopted
+it and written state files for it. That is the fact worth carrying out of this
+item: **under `Create Both` a wrongly named origin folder propagates to the mail
+server**, so any code composing a folder name is reaching the server whether it
+means to or not. Tags are joined by a TAB across the thread boundary for the
+same reason, since a notmuch tag may contain a space.
+
+**Two process gaps closed alongside it.** Adding an action is FIVE places and
+this repository's CLAUDE.md said four; the missing one is a menu, and nothing
+enforced it, so `restore` shipped reachable by a chord and by nothing a user
+could see. `everyActionIsReachableFromAMenu()` now asserts it and found three
+more of the same (`open_thread`, `clear_pane`, `clear_selection`). And a guard
+in `deletingTwiceLeavesNoOriginTagBehind()` queried through the query bar in the
+gap between the file rename and the tag writes, where a run returns zero rows
+forever because `QTRY_VERIFY` re-reads `rowCount()` and never re-runs the query:
+3 failures in 12 runs, each burning a full 15s timeout, 0 in 8 after asking the
+database directly.
+
+**Emptying the trash is item 118 and remains deferred**, at the user's request.
+
+**Verified:** clean build with no warnings from any changed file, 24 of 24
+suites over three consecutive runs, 211 tests in `test_mainwindow`. Every one of
+the spec's six testing bullets has a test, each mutation-checked.
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
index 6a5e64d..1f71006 100644
--- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
+++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
@@ -167,7 +167,7 @@ taking that too literally.
| 100 | The message pane offers Back, Forward, Reload and Save page, none of which mean anything | defect | XS | **done** 2026-08-17, unreleased. `MessageView::removeBrowserActions()` filters the standard menu by `pageAction()` POINTER, never by text; `ViewSource` went with them, and stranded separators are swept |
| 101 | Sync is account-aware for edits but not for the account the user is looking at | workflow | S | open; item 49 built the edit half deliberately. Needs a decision, see the entry |
| 102 | The rules table shows no note, so the field explaining a rule is invisible until it is opened | workflow | XS | **done** 2026-08-17, unreleased. A Note column before `ColumnCount`, so the appended Matches column stays last. Found a second defect on the way: `restoreState` REFUSES a header state with a different column count, and the sized flags were being set regardless |
-| 103 | What Delete does to mail on the server is undocumented and unverified | clarification | S | open; a question first, possibly no code at all |
+| 103 | What Delete does to mail on the server is undocumented and unverified | clarification | S+M | done; Delete moves to the account trash, with Restore and a stranded-mail cleanup. Section in the closed file |
| 104 | Mail visible in Thunderbird never reaches qtmaildir | defect | ? | open, reported 2026-08-16, cause NOT established. Most likely outside this repo; see the entry before writing code |
| 109 | A root card's own message is invisible to a message-scoped write | defect | S | **done** 2026-08-16, unreleased. Found by hand-testing 108. `applyMessageTagChange` and `messageById` searched only the loaded replies, and a root's message is never among them, so the ORDINARY gesture repainted nothing and wiped the pane's chip row |
| 110 | A card and the message pane show tags belonging to a message's siblings | defect | S | **done** 2026-08-16, unreleased. Found by hand-testing 109 against a real 4-message thread. `ThreadSummary::tags` is notmuch's UNION; a card standing for one message drew it. Also the reason a root card could not repaint at all |
@@ -485,57 +485,6 @@ reaches it (item 42), so most of this exists.
**Size: S** for the on-demand button, XS for the visibility half. Ask which.
-## 103. What Delete does to mail on the server is undocumented and unverified
-
-**Observed (user, from the notes):** "verify how 'delete' works", with two
-sub-questions of their own: "trash bin (?)" and "delete from server (?)".
-
-**This is a question first.** The user is not reporting a defect; they are saying
-they do not know what the button does to their mail, which for a destructive
-action is its own problem regardless of the answer.
-
-**What the code does (verified).** `src/mainwindow.cpp:825` adds and removes the
-`deleted` tag, and nothing else. It is a toggle, it goes through the undo stack,
-and it writes a notmuch tag.
-
-**What that means downstream is what needs verifying, and it is NOT in this
-repo.** `maildir.synchronize_flags` is true, so notmuch maps certain tags to
-Maildir filename flags, and mbsync carries filename flags to the server. Whether
-`deleted` is one of those, whether the user's `~/.mbsyncrc` has `Expunge Both`
-(it does, on every channel), and what each provider does with a message flagged
-deleted, together decide whether this button is reversible. The undo stack makes
-the TAG reversible; it says nothing about what a sync did with it in between.
-
-**Measured 2026-08-17, and the answer is that Delete does not delete.** notmuch's
-tag-to-flag table has no row for `deleted` and no `T` flag, confirmed by a probe
-on a throwaway database: `+deleted` left the filename untouched while the
-control `+flagged` immediately produced `:2,F`. mbsync carries filename flags, so
-`Expunge Both` never sees anything to expunge, and `assets/mailsync.sh` contains
-no delete path. Both sub-questions answer no: no trash bin, no deletion from the
-server, and the mail stays in the Maildir and the index forever.
-
-**Specified in
-`specs/2026-08-17-delete-to-trash-design.md`. Read that before writing code.**
-Delete becomes a real move into the account's trash folder, with a `Trash`
-filter beside the other built-ins and a Restore action. Three constraints decide
-whether the spec is worth opening:
-
-- It needs a **mandatory per-account `trash` key**, so an existing config warns
- until five keys are added. User-visible: minor bump and an `### Upgrading`
- note.
-- The worker gains its **first non-tag mutation**, a rename plus a reindex,
- deliberately shaped as `moveMessages(ids, destFolder)` so Send in v2 reuses it
- for Drafts and Sent.
-- **848 messages carry the old tag** while sitting in an inbox, and would be
- invisibly half-deleted after the change. A repeatable menu entry queries them
- into the list for review; it is not a startup migration.
-
-The no-confirmation rule survives, with its justification amended: the mail
-lands in a browsable folder, but reversibility is now bounded by the provider
-where the trash is purged on a timer.
-
-**Size: S** for the investigation, which is done. The build is **M**.
-
## 104. Mail visible in Thunderbird never reaches qtmaildir
**Observed (user, from the notes):** "sync doesn't work compared to thunderbird.
diff --git a/docs/superpowers/plans/2026-08-17-delete-to-trash.md b/docs/superpowers/plans/2026-08-17-delete-to-trash.md
index 4e52789..70742b5 100644
--- a/docs/superpowers/plans/2026-08-17-delete-to-trash.md
+++ b/docs/superpowers/plans/2026-08-17-delete-to-trash.md
@@ -52,7 +52,7 @@ No new files. Every change lands in a file that already owns that responsibility
- Modify: `src/config.cpp` (`Account::trashQuery()` beside `sentQuery()` at line 130, and the account parser)
- Test: `tests/test_config.cpp`
-- [ ] **Step 1: Write the failing test**
+- [x] **Step 1: Write the failing test**
Add to `tests/test_config.cpp`, and declare both slots in the `private slots:` block:
@@ -94,7 +94,7 @@ void TestConfig::aBracketedTrashFolderIsQuoted()
}
```
-- [ ] **Step 2: Run test to verify it fails**
+- [x] **Step 2: Run test to verify it fails**
```bash
cmake --build build 2>&1 | tail -5
@@ -102,7 +102,7 @@ cmake --build build 2>&1 | tail -5
Expected: FAIL to compile, `'trash' is not a member of 'Account'`.
-- [ ] **Step 3: Write minimal implementation**
+- [x] **Step 3: Write minimal implementation**
In `src/config.h`, add to `Account` immediately after the `sent` member:
@@ -143,7 +143,7 @@ Then find where the account parser reads `sent` (search for `QStringLiteral("sen
account.trash = settings.value(QStringLiteral("trash")).toString();
```
-- [ ] **Step 4: Run test to verify it passes**
+- [x] **Step 4: Run test to verify it passes**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_config
@@ -151,7 +151,7 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_config
Expected: PASS, all tests.
-- [ ] **Step 5: Commit**
+- [x] **Step 5: Commit**
```bash
git add src/config.h src/config.cpp tests/test_config.cpp
@@ -170,7 +170,7 @@ First read how warnings are currently raised: search `src/config.cpp` for
`m_warnings` and copy the surrounding form exactly. Do not invent a new
mechanism.
-- [ ] **Step 1: Write the failing test**
+- [x] **Step 1: Write the failing test**
```cpp
void TestConfig::anAccountWithoutATrashFolderWarns()
@@ -204,7 +204,7 @@ void TestConfig::anAccountWithoutATrashFolderWarns()
}
```
-- [ ] **Step 2: Run test to verify it fails**
+- [x] **Step 2: Run test to verify it fails**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_config -functions | grep -i trash
@@ -213,7 +213,7 @@ QT_QPA_PLATFORM=offscreen ./build/tests/test_config anAccountWithoutATrashFolder
Expected: FAIL, `warnings` is empty.
-- [ ] **Step 3: Write minimal implementation**
+- [x] **Step 3: Write minimal implementation**
In the account-loading loop in `src/config.cpp`, after the account is parsed and
before it is appended:
@@ -232,7 +232,7 @@ before it is appended:
}
```
-- [ ] **Step 4: Run test to verify it passes**
+- [x] **Step 4: Run test to verify it passes**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_config
@@ -240,7 +240,7 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_config
Expected: PASS.
-- [ ] **Step 5: Commit**
+- [x] **Step 5: Commit**
```bash
git add src/config.cpp tests/test_config.cpp
@@ -256,7 +256,7 @@ git commit -m "feat(config): warn when an account configures no trash folder"
- Modify: `src/config.cpp` (`kQueryGenerators` line 60, `builtinFilter()` line 757, `resolvedQuery()` line 790, `allTrashQuery()` beside line 140)
- Test: `tests/test_config.cpp`
-- [ ] **Step 1: Write the failing test**
+- [x] **Step 1: Write the failing test**
```cpp
void TestConfig::theTrashFilterComposesPerAccount()
@@ -307,7 +307,7 @@ void TestConfig::theTrashFilterMatchesNothingWithoutAFolder()
}
```
-- [ ] **Step 2: Run test to verify it fails**
+- [x] **Step 2: Run test to verify it fails**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_config theTrashFilterComposesPerAccount
@@ -315,7 +315,7 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_config theTr
Expected: FAIL, `trash.isGenerated()` is false because the generator is unknown.
-- [ ] **Step 3: Write minimal implementation**
+- [x] **Step 3: Write minimal implementation**
In `src/config.cpp`, add to `kQueryGenerators` at line 60, last so it sits
rightmost on the query row:
@@ -377,7 +377,7 @@ And in the per-account branch, beside the `sent` case:
}
```
-- [ ] **Step 4: Run test to verify it passes**
+- [x] **Step 4: Run test to verify it passes**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_config
@@ -385,13 +385,13 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_config
Expected: PASS.
-- [ ] **Step 5: Verify the mutation fails**
+- [x] **Step 5: Verify the mutation fails**
Temporarily change the per-account branch to `return allTrashQuery();` and
rebuild. `theTrashFilterComposesPerAccount` must FAIL on the `QCOMPARE`. Revert
the mutation. This proves the test asserts on the string rather than on rows.
-- [ ] **Step 6: Commit**
+- [x] **Step 6: Commit**
```bash
git add src/config.h src/config.cpp tests/test_config.cpp
@@ -412,7 +412,7 @@ This is the first mutation in the project that is not a notmuch tag. Read
uses is required, not stylistic, because notmuch permits one open handle per
process.
-- [ ] **Step 1: Write the failing test**
+- [x] **Step 1: Write the failing test**
Add to `tests/test_notmuchworker.cpp`, declaring each slot in `private slots:`:
@@ -536,7 +536,7 @@ void TestNotmuchWorker::moveMessagesReportsOnlyWhatMoved()
}
```
-- [ ] **Step 2: Run test to verify it fails**
+- [x] **Step 2: Run test to verify it fails**
```bash
cmake --build build 2>&1 | tail -5
@@ -544,7 +544,7 @@ cmake --build build 2>&1 | tail -5
Expected: FAIL to compile, `'moveMessages' is not a member of 'NotmuchWorker'`.
-- [ ] **Step 3: Write minimal implementation**
+- [x] **Step 3: Write minimal implementation**
In `src/notmuchworker.h`, beside `applyTags()`:
@@ -675,7 +675,7 @@ void NotmuchWorker::moveMessages(const QStringList &messageIds,
}
```
-- [ ] **Step 4: Run test to verify it passes**
+- [x] **Step 4: Run test to verify it passes**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_notmuchworker
@@ -683,7 +683,7 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_notmuchworke
Expected: PASS, all tests.
-- [ ] **Step 5: Verify the ordering mutation fails**
+- [x] **Step 5: Verify the ordering mutation fails**
Move the `notmuch_database_remove_message` call to immediately BEFORE the
`notmuch_database_index_file` call and rebuild.
@@ -691,7 +691,7 @@ Move the `notmuch_database_remove_message` call to immediately BEFORE the
the mutation. This is the single most important check in the task: the wrong
order silently destroys user tags and every other test still passes.
-- [ ] **Step 6: Commit**
+- [x] **Step 6: Commit**
```bash
git add src/notmuchworker.h src/notmuchworker.cpp tests/test_notmuchworker.cpp
@@ -716,7 +716,7 @@ selection, resolved through `everySelectedRowHasTag()`, and `CLAUDE.md` records
two separate bugs that lived in those three lines. Preserve the toggle: Delete
twice still means "put it back".
-- [ ] **Step 1: Write the failing test**
+- [x] **Step 1: Write the failing test**
```cpp
void TestMainWindow::deleteMovesTheMessageToTrash()
@@ -793,7 +793,7 @@ whichever helpers are missing, following the form of the ones already there.
observable state with `QTRY_VERIFY_WITH_TIMEOUT` and never on worker signals or
a fixed `qWait(n)`.
-- [ ] **Step 2: Run test to verify it fails**
+- [x] **Step 2: Run test to verify it fails**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow deleteMovesTheMessageToTrash
@@ -801,7 +801,7 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow d
Expected: FAIL, the message is still in Inbox because Delete only tags.
-- [ ] **Step 3: Write minimal implementation**
+- [x] **Step 3: Write minimal implementation**
Add a `MoveCommand` in `src/mainwindow.h`, beside `MessageTagCommand` around
line 1176, following its shape exactly:
@@ -888,7 +888,7 @@ the current path, and pushes one `MoveCommand` carrying
`deleted` and `deleted-from:<origin>`. An account with no `trash` key contributes
nothing and reports through `statusMessage`, since Task 2 already warned at load.
-- [ ] **Step 4: Run test to verify it passes**
+- [x] **Step 4: Run test to verify it passes**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow
@@ -896,7 +896,7 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow
Expected: PASS, all tests.
-- [ ] **Step 5: Commit**
+- [x] **Step 5: Commit**
```bash
git add src/mainwindow.h src/mainwindow.cpp tests/test_mainwindow.cpp
@@ -918,7 +918,7 @@ are enforced by tests that fail confusingly: `KeyMap::knownActions()` (a
`defaultBindings()` (every action must be keyboard-reachable), and the icon
table (every action must carry one).
-- [ ] **Step 1: Write the failing test**
+- [x] **Step 1: Write the failing test**
```cpp
void TestMainWindow::restoreIsOnlyEnabledInTheTrashView()
@@ -985,7 +985,7 @@ void TestMainWindow::restoreFallsBackToInboxWithoutAnOriginTag()
}
```
-- [ ] **Step 2: Run test to verify it fails**
+- [x] **Step 2: Run test to verify it fails**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow restoreIsOnlyEnabledInTheTrashView
@@ -993,7 +993,7 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow r
Expected: FAIL, there is no `restore` action.
-- [ ] **Step 3: Write minimal implementation**
+- [x] **Step 3: Write minimal implementation**
Add `"restore"` to `KeyMap::knownActions()` and give it a binding in
`defaultBindings()`. Check what is free first:
@@ -1030,7 +1030,7 @@ generator's, for the current account selection.
`restoreSelected()` reads each row's `deleted-from:` tag for its destination and
falls back to `Inbox`, reporting which through `statusMessage`.
-- [ ] **Step 4: Run test to verify it passes**
+- [x] **Step 4: Run test to verify it passes**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow
@@ -1040,7 +1040,7 @@ ctest --test-dir build --output-on-failure
Expected: PASS, 24 of 24. The keymap and icon-table tests fail loudly if a
place was missed.
-- [ ] **Step 5: Commit**
+- [x] **Step 5: Commit**
```bash
git add src/mainwindow.h src/mainwindow.cpp src/keymap.cpp tests/test_mainwindow.cpp
@@ -1059,7 +1059,7 @@ git commit -m "feat: restore mail from the trash view"
The user's constraint, verbatim: "the cleanup should be a menu entry only, not
to be confused with the filter Trash". Do not add a sixth button.
-- [ ] **Step 1: Write the failing test**
+- [x] **Step 1: Write the failing test**
```cpp
void TestMainWindow::theCleanupQueryFindsStrandedMail()
@@ -1109,7 +1109,7 @@ void TestMainWindow::theCleanupQueryExcludesMailAlreadyInTrash()
}
```
-- [ ] **Step 2: Run test to verify it fails**
+- [x] **Step 2: Run test to verify it fails**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow theCleanupQueryFindsStrandedMail
@@ -1117,7 +1117,7 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow t
Expected: FAIL, there is no `cleanup_stranded` action.
-- [ ] **Step 3: Write minimal implementation**
+- [x] **Step 3: Write minimal implementation**
Register the action in `knownActions()`, `defaultBindings()` and the icon table
as in Task 6, then:
@@ -1141,7 +1141,7 @@ as in Task 6, then:
Add it to a menu, not to the query row. Find where the other menu entries are
built and follow that form.
-- [ ] **Step 4: Run test to verify it passes**
+- [x] **Step 4: Run test to verify it passes**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow
@@ -1149,7 +1149,7 @@ cmake --build build && QT_QPA_PLATFORM=offscreen ./build/tests/test_mainwindow
Expected: PASS.
-- [ ] **Step 5: Commit**
+- [x] **Step 5: Commit**
```bash
git add src/mainwindow.h src/mainwindow.cpp src/keymap.cpp tests/test_mainwindow.cpp
@@ -1168,7 +1168,7 @@ Every user-facing string added above needs a translation, and `lrelease`
silently DROPS an unfinished string and ships it as English inside an otherwise
Italian UI. Item 108 shipped fifteen strings that way.
-- [ ] **Step 1: Refresh the translation source**
+- [x] **Step 1: Refresh the translation source**
```bash
lupdate-qt6 src/ -ts translations/qtmaildir_it_IT.ts -no-obsolete -locations none
@@ -1178,7 +1178,7 @@ Expected: a clean run reporting zero context warnings. A "tr() cannot be called
without context" warning means a literal needs `QT_TRANSLATE_NOOP("TheClass",
"Text")` rather than `tr()`.
-- [ ] **Step 2: Translate every new string**
+- [x] **Step 2: Translate every new string**
Open `translations/qtmaildir_it_IT.ts` and fill in each `<translation
type="unfinished">`. The new strings are the Trash filter name, the Restore and
@@ -1188,7 +1188,7 @@ messages.
Do NOT translate notmuch query syntax. `tag:deleted`, `path:` and
`deleted-from:` are wire format.
-- [ ] **Step 3: Verify nothing is unfinished**
+- [x] **Step 3: Verify nothing is unfinished**
```bash
lrelease-qt6 translations/qtmaildir_it_IT.ts
@@ -1197,7 +1197,7 @@ lrelease-qt6 translations/qtmaildir_it_IT.ts
Expected: "Generated N translation(s) (N finished, 0 unfinished)". A nonzero
unfinished count means a string will ship as English.
-- [ ] **Step 4: Run the translations test**
+- [x] **Step 4: Run the translations test**
```bash
ctest --test-dir build -R translations --output-on-failure
@@ -1205,7 +1205,7 @@ ctest --test-dir build -R translations --output-on-failure
Expected: PASS.
-- [ ] **Step 5: Write the changelog entry**
+- [x] **Step 5: Write the changelog entry**
Under `## [Unreleased]` in `CHANGELOG.md`, with an `### Upgrading` section,
since a working config now warns until five keys are added:
@@ -1243,7 +1243,7 @@ Note that Delete's reversibility depends on your provider: a trash folder that
the provider purges on a timer will eventually remove the mail for good.
```
-- [ ] **Step 6: Commit**
+- [x] **Step 6: Commit**
```bash
git add translations/qtmaildir_it_IT.ts CHANGELOG.md
@@ -1254,7 +1254,7 @@ git commit -m "i18n: translate the trash strings, and document the trash key"
## Task 9: Full verification
-- [ ] **Step 1: Clean build**
+- [x] **Step 1: Clean build**
```bash
rm -rf build
@@ -1264,7 +1264,7 @@ cmake --build build
Expected: no warnings from the changed files.
-- [ ] **Step 2: Full suite**
+- [x] **Step 2: Full suite**
```bash
ctest --test-dir build --output-on-failure
@@ -1272,13 +1272,13 @@ ctest --test-dir build --output-on-failure
Expected: 24 of 24 passing.
-- [ ] **Step 3: Confirm the spec's claims hold**
+- [x] **Step 3: Confirm the spec's claims hold**
Re-read `docs/superpowers/specs/2026-08-17-delete-to-trash-design.md` and check
each testing bullet has a test. The spec lists six; all six are covered by
Tasks 1, 3, 4, 5, 6 and 7.
-- [ ] **Step 4: Hand the build to the user**
+- [x] **Step 4: Hand the build to the user**
Do NOT run `./build/src/qtmaildir`. Report what to look at: