diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-31 12:49:53 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-31 12:49:53 +0100 |
| commit | c7d09a3956c2b32f9590651c2088ab34334ac538 (patch) | |
| tree | e7fdf1cc55ecfe836981ad770315e9c6d832958d /docs | |
| parent | 2b6ecd2a1f624c12c071320ff1ed96404d75f839 (diff) | |
| download | qtmaildir-c7d09a3956c2b32f9590651c2088ab34334ac538.tar.gz qtmaildir-c7d09a3956c2b32f9590651c2088ab34334ac538.zip | |
docs: record rule apply and archive-on-arrival
The no-confirmation rule gains its second exception, by property: a write
scoped to the whole index confirms with the count, though it can be undone.
AGENTS.md and the spec now describe the backfill as built: the worker
resolves the ids once, writes one tag at a time over that fixed list, and
answers with a single ruleApplied from which the undo macro is built, with
nothing armed on the shared confirmation queue. Apply is refused during a
sync.
Hand round trip, throwaway index of 2 messages (one from the promo
sender), rules.json written by TagRules::save from the C++ side with
allow_archive and a note: post-new exited 0 and logged "applied 1
rule(s)"; after it, tag:promo 1, tag:inbox 1, tag:promo and tag:inbox 0,
tag:unread 2, tag:new 0. The file read back 1 rule with allow_archive
True and note "kept".
Italian translations for the 16 new strings; lrelease reports 725
finished, 0 unfinished.
Items 212 and 213 closed. Item 214 opened: an undo's write is not armed,
so its confirmation is credited to whatever command is waiting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
3 files changed, 128 insertions, 58 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 c83e41c..376272b 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 @@ -10912,3 +10912,48 @@ while moves report through another signal. An error bumps the same ticket, so a late "done" cannot cover the error message. Tested by `aWriteSaysItIsApplyingUntilTheWorkerHasRunIt`, which fails when the reply is dropped. + +## 212. A rule edit reaches only new mail + +**Observed.** From the user, 2026-10-31: after adding an address to the promo +rule, "I have to manually go and apply the tags, because filters run on new +mail only." + +**Cause.** Not a defect. The `post-new` hook scopes every rule to `tag:new`, +and backfill was deliberately deferred by the tagging-rules spec until the +confirmation question was settled. + +**Approach.** See `specs/2026-10-31-rule-apply-and-archive-design.md`, Part A. + +**Constraints.** Revises the no-confirmation rule in `CLAUDE.md` with a second +exception, by property: a write scoped to the whole mailbox confirms with the +count. Undo still applies. + +**Done 2026-10-31, unreleased.** Built as the spec's Part A with three changes +recorded there and in `CLAUDE.md`: the worker's `applyRule` resolves the ids +once and writes one tag at a time, it answers with one `ruleApplied` from which +`onRuleApplied` builds the undo macro (nothing is armed on the shared +confirmation queue in advance), and Apply during a sync is refused rather than +held. The ordering defect found on the way, in the ordinary undo path, is item +214. + +## 213. A rule removing `inbox` is skipped whole by the hook + +**Observed.** Found while designing 212. Every sync logs +`rule 'promo' would remove inbox; skipped`, so new promotional mail is never +tagged `promo` and stays in the inbox. This is how the 10k messages of item +211 accumulated. + +**Cause.** Verified in `assets/hooks/post-new`: `PROTECTED_REMOVALS` is +`{unread, inbox}` and a rule naming either is `continue`d past entirely, not +applied without the removal. The rules dialog says nothing about it. + +**Approach.** See the same spec, Part B. + +**Constraints.** A rule-format change: both readers, both suites, and the hand +round trip under "Changing the rule format". The hook runs every ten minutes +on real mail. + +**Done 2026-10-31, unreleased.** Built as the spec's Part B. The hand round +trip under "Changing the rule format" step 4 was run over a throwaway index; +its counts are in the commit that closed this item. 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 6e36905..200b6d0 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 @@ -149,8 +149,9 @@ taking that too literally. | 209 | The calendar has no Week view | presentation | M | open, from 206's brainstorm. The occurrence list is the seam: a new painter over CalendarItem, no store change | | 210 | A repeating event cannot be edited "this and following" | workflow | S-M | open, from 206's brainstorm. Splits a series into two files, divides EXDATEs and overrides, shifts moved RECURRENCE-IDs, converts COUNT; see the spec's Out of scope | | 211 | Archiving thousands of selected rows freezes the window | defect | S | **done** 2026-10-31, unreleased. Cubic optimistic-update path made linear: a lazily rebuilt id index in `ThreadListModel` and `MainWindow::batchModelWrites()` refreshing the action labels once per loop. A write now reads "applying…" until the worker has run it, then "done". Hand-tested on a 10158-message archive | -| 212 | A rule edit reaches only new mail; applying it to the backlog is a hand-typed `notmuch tag` | workflow | M | open, designed: `specs/2026-10-31-rule-apply-and-archive-design.md` Part A. Apply saves, counts what would change, confirms with the count (a new documented exception to the no-confirmation rule), writes through `applyTags` for exact undo | -| 213 | A rule removing `inbox` is skipped whole by the hook, so promo mail arrives untagged | defect | S-M | open, designed: same spec, Part B. Optional per-rule `allow_archive`, both readers, no version bump; `unread` stays refused; the dialog warns about rules the hook will skip | +| 212 | A rule edit reaches only new mail; applying it to the backlog is a hand-typed `notmuch tag` | workflow | M | **done** 2026-10-31, unreleased. The rules dialog's Apply to existing mail saves, counts, confirms with the count (the second documented exception to the no-confirmation rule) and writes through `NotmuchWorker::applyRule`, one tag at a time over ids resolved once, with one undo macro built from the worker's answer. Refused during a sync. Section in the closed file | +| 213 | A rule removing `inbox` is skipped whole by the hook, so promo mail arrives untagged | defect | S-M | **done** 2026-10-31, unreleased. Optional per-rule `allow_archive` in both readers, no version bump; the hook lets an opted-in rule remove `inbox`, never `unread`; the dialog names every rule the hook will skip. Section in the closed file | +| 214 | Undoing a tag change while another action is still landing can give the undo's messages to that action | defect | S? | open, 2026-10-31, found by review during 212; pre-existing. An undo's write is not armed, so its confirmation is credited to whatever command is waiting. A correlation token on `TagChange` is the robust fix | | 73 | This backlog is past four thousand lines | maintenance | S | **done** 2026-08-13; 5056 lines to 578, closed sections moved to `2026-08-03-post-0.1.0-usability-closed.md` | | 74 | "Searching..." keeps claiming a query is running while rows are already arriving | feedback | XS | **done** 2026-08-15, unreleased. The status-bar half only: the bar now counts threads per batch. The cold-cache delay itself was measured in 2026-08-11 and is not fixable here | | 75 | The tagging rules window forgets its size and its column widths | persistence | S | **done** 2026-08-13, shipped in 0.17.0. The window-kind question is left open, see the closed-items file | @@ -1701,35 +1702,31 @@ server-normalised source must refuse rather than divide a file it does not recognise. See the spec's Out of scope for the full list of what the split has to divide. -## 212. A rule edit reaches only new mail - -**Observed.** From the user, 2026-10-31: after adding an address to the promo -rule, "I have to manually go and apply the tags, because filters run on new -mail only." - -**Cause.** Not a defect. The `post-new` hook scopes every rule to `tag:new`, -and backfill was deliberately deferred by the tagging-rules spec until the -confirmation question was settled. - -**Approach.** See `specs/2026-10-31-rule-apply-and-archive-design.md`, Part A. - -**Constraints.** Revises the no-confirmation rule in `CLAUDE.md` with a second -exception, by property: a write scoped to the whole mailbox confirms with the -count. Undo still applies. - -## 213. A rule removing `inbox` is skipped whole by the hook - -**Observed.** Found while designing 212. Every sync logs -`rule 'promo' would remove inbox; skipped`, so new promotional mail is never -tagged `promo` and stays in the inbox. This is how the 10k messages of item -211 accumulated. - -**Cause.** Verified in `assets/hooks/post-new`: `PROTECTED_REMOVALS` is -`{unread, inbox}` and a rule naming either is `continue`d past entirely, not -applied without the removal. The rules dialog says nothing about it. - -**Approach.** See the same spec, Part B. - -**Constraints.** A rule-format change: both readers, both suites, and the hand -round trip under "Changing the rule format". The hook runs every ten minutes -on real mail. +## 214. An undo's write is not armed, so its confirmation is credited to whatever command is waiting + +**Observed.** Not a user symptom yet: found by review while building item 212, +and pre-existing. The 212 backfill hit the same mechanism from the other side +(a user gesture during a slow backfill let the user's command take the +backfill's ids, so Ctrl+Z on Archive re-inboxed thousands), which is why it was +redesigned to arm nothing. + +**Cause.** Verified in `src/mainwindow.h`: `MessageTagCommand::undo()` and +`ThreadTagCommand::undo()` call `sendMessageTagChange()` / +`sendThreadTagChange()` WITHOUT `awaitTagConfirmation()`, while `redo()` arms. +`MainWindow::onTagsApplied()` credits every confirmation to the head of +`m_awaitingTagConfirmation`. So if a user gesture's command is still waiting +when an undo's confirmation arrives, that command takes the undo's ids, and its +own later undo acts on messages it never moved. That is item 176's data-safety +property failing by ordering rather than by scope. + +**Approach.** Unsized, probably S. The robust fix is a correlation token on +`TagChange`, set by the sender, echoed by the worker in `tagsApplied`, and used +by `onTagsApplied()` to find the command it belongs to, so the queue's order +stops mattering. Arming the undo's write as well would close this case and +leave the queue's FIFO assumption in place for the next one. + +**Constraints.** Item 176's rule: an undo covers what the write changed, never +what it asked for. A test needs a gesture's command waiting while an undo's +confirmation arrives, with the two touching DIFFERENT messages, or the wrong +credit answers identically. `a30b4c2` tried crediting by matching tags and was +reverted (`aa5fc73`); read both before choosing. diff --git a/docs/superpowers/specs/2026-10-31-rule-apply-and-archive-design.md b/docs/superpowers/specs/2026-10-31-rule-apply-and-archive-design.md index 247cfd5..380c2e3 100644 --- a/docs/superpowers/specs/2026-10-31-rule-apply-and-archive-design.md +++ b/docs/superpowers/specs/2026-10-31-rule-apply-and-archive-design.md @@ -73,26 +73,54 @@ Then: > Apply rule "promo" to 1,234 messages? > Adds: promo. Removes: inbox. - > You can undo this from the main window. + > You can undo this until the next search. + + (Changed during implementation from "from the main window": `runQuery()` + clears the undo stack, so the undo lasts until the next search.) Cancel is the default button. The count and the tag lists are the information the user cannot get from the list on screen, which is the whole reason this write gets a confirmation and others do not. -4. **Write.** A new worker slot, `applyTagsToQuery(query, add, remove, - description)`, runs the same change query to collect message ids, then - calls the existing `applyTags()`. Everything downstream is inherited - unchanged: - - exact undo: `applyTags()` reports only the ids whose tags moved (item - 176), and the undo entry is a `MessageTagCommand` armed with - `awaitTagConfirmation()` before the send, so Ctrl+Z reverts exactly - those; - - the pending-changes count and the auto sync, from `onTagsApplied()`; - - the "applying… / done" status from item 211's `announceAction()`; - - the write lock: if a sync holds it, the edit is held and replayed the - same way a list edit is. - - After the confirmation arrives, the current query is refreshed, since the +4. **Write.** A new worker slot, `applyRule(query, add, remove, + description)`, resolves the message ids ONCE with + `SearchTerm::changes(query, add, remove)`, then writes one tag operation + at a time over THAT FIXED id list, and always answers with one + `ruleApplied(description, changes, error)` carrying each operation's + effective ids. It never emits `tagsApplied` or `errorOccurred`. + (Changed during implementation; the planned `applyTagsToQuery` calling + `applyTags()` once is gone, for the three reasons below.) + + **One tag at a time.** `applyTags()` reports which messages moved, not + which tags, so one `+promo -inbox` write would list a message that + already had `promo`, and undo would strip it. Each tag is its own write + and its own undo command, grouped in one undo macro. + + **Ids resolved once.** Re-resolving per operation made a rule whose query + names its own tags apply only half of itself. + + **Nothing armed in advance.** The planned `MessageTagCommand` armed with + `awaitTagConfirmation()` before the send was built and then removed: a + user gesture during a slow backfill cleared the confirmation queue, and + the user's own command took the backfill's ids (Ctrl+Z on Archive + re-inboxed thousands). `MainWindow::onRuleApplied()` instead records each + non-empty change through `recordConfirmedTagChange()`, which is the + bookkeeping of `onTagsApplied()` without crediting a queued command, and + pushes `MessageTagCommand`s built with the known ids in one undo macro. + + What is inherited: + - exact undo: each command holds the ids its operation moved (item 176); + - the pending-changes count, from `recordConfirmedTagChange()`, and every + account marked edited, since a backfill reaches mail no row shows and + its accounts cannot be resolved through the model; + - the "applying… / done" status from item 211's `announceAction()`; a + failure goes to the status bar, and a partial write still gets its undo; + - the write lock: Apply during a sync is REFUSED with a status message, + checked before the count and again after the confirmation box. + Changed during planning: `HeldEdit` replays message or thread ids, and + a backfill has neither until the worker resolves its query. + + After the answer arrives, the current query is refreshed, since the rows a backfill touches are mostly not in the model and cannot be updated optimistically. @@ -101,16 +129,16 @@ Then: anything when cron runs a sync"; Apply has a human present who has just confirmed a named count. -### The undo command needs its ids from the confirmation +### The undo command's ids come from the worker's answer `MessageTagCommand` is constructed with the ids it acts on. For a backfill -those ids are unknown until the worker has resolved the query. The command is -constructed with an empty id list and filled from the confirmation, which -`TagCommand::addChangedMessageIds()` already does for every command (item -176). The plan must verify that redo and undo both read the EFFECTIVE set and -never the constructor's list; if either reads the constructor's list, an -empty one would redo nothing, and the plan fixes that rather than working -around it. +those ids are unknown until the worker has resolved the query, so the +commands are built only when `ruleApplied` arrives, with the effective ids +each operation reported, and `addChangedMessageIds()` fills what undo reads. +(Changed during implementation from "constructed with an empty id list and +filled from the confirmation", which needed the arming removed above.) The +application has no Redo action; since the commands carry real ids, they +would redo correctly if one is added. ### Rule change to CLAUDE.md @@ -184,7 +212,7 @@ Measurable properties only, per `CLAUDE.md`. - **SearchTerm:** the change query for `+a -r` over a query containing ` or ` is the exact expected string, with both sides parenthesised. Add-only and remove-only rules each produce the right half. -- **Worker:** `applyTagsToQuery` against a throwaway index holding messages in +- **Worker:** `applyRule` against a throwaway index holding messages in BOTH states (some already tagged, some not; the item 87 lesson) reports only the ones that changed, and leaves the rest alone. - **MainWindow, worker-backed:** Apply on a rule with N changeable messages |
