aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
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: