diff options
| -rw-r--r-- | CHANGELOG.md | 34 | ||||
| -rw-r--r-- | CLAUDE.md | 23 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 46 |
3 files changed, 102 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a555fb4..846e5c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,15 @@ point at which they are stable. ### Added +- The rules that tag your mail as it arrives are now visible and editable from + Message > Tagging rules, and each one shows how much mail it matches so you + can judge a rule before the next sync applies it. They live in a shared file, + `~/.config/mailrules/rules.json`, which the notmuch `post-new` hook reads to + do the tagging and which the companion `mailctl` tool can read too. The rules + previously lived inside the hook as shell, where nothing but a text editor + could see them. Each rule keeps a note, so the reasoning behind it (which + senders it deliberately excludes, and why) travels with the rule instead of + being a comment only one program could read. - A tag change now syncs itself out, about two seconds after you stop making changes, instead of waiting for the Sync button or your cron job. The delay is a debounce, so tagging several threads in a row produces one sync rather than @@ -37,6 +46,31 @@ point at which they are stable. message you are reading in Unread gets marked read, the pane keeps showing it and offers "Show it anyway" instead of going blank. +### Upgrading + +The tagging rules moved out of the notmuch `post-new` hook and into +`~/.config/mailrules/rules.json`. The dialog reads and writes that file, but +nothing applies the rules until the new hook is installed, so this needs two +files copied from the companion `mailctl` project into your notmuch hooks +directory: + +```bash +DB="$(notmuch config get database.path)" +cp post-new mailrules.py "$DB/.notmuch/hooks/" +chmod +x "$DB/.notmuch/hooks/post-new" +``` + +Keep a backup of your previous hook until a sync has run with the new one. Your +existing rules do not convert themselves: each `notmuch tag` line becomes one +entry in the JSON file, with the part after `tag:new and` as its query. Leave +`tag:new` out of the stored query, the hook adds it, and do not carry over the +final `notmuch tag -new` line, which the hook now does itself. + +Two behaviours of the new hook are worth knowing. It refuses to remove `unread` +or `inbox`, since neither belongs in an unattended job that runs every ten +minutes, and it will not consume the `tag:new` marker if the rules file fails +to load, so a broken file delays tagging rather than losing it. + ## [0.15.0] - 2026-08-11 Sent mail becomes a place you can go. A Sent button beside Inbox, Unread and @@ -214,6 +214,29 @@ completion descriptions. Query syntax itself is not user-facing text — notmuch like `tag:` and `date:` are wire format and must never be translated, only the prose describing them. Pre-existing code has not been audited against this rule. +**The auto-tagging rules are NOT in this repo, and notmuch's parser rejects +almost nothing.** Rules live in `~/.config/mailrules/rules.json`, applied by a +notmuch `post-new` hook that ships from the companion `mailctl` project; +`TagRules` here reads and writes the same file and `TagRulesDialog` edits it. +Two things bite. A stored query carries NO scope: the hook supplies `tag:new` +and wraps the query in parentheses, because `tag:new and a or b` binds as +`(tag:new and a) or b` and a rule that is a disjunction of senders would escape +its scope and match everything. And **a malformed query is not an error to +notmuch**: `from:((((` parses cleanly and matches nothing, so a test asserting +a failure or a `-1` count fails against correct code. This was recorded in +`test_notmuchworker.cpp` for thread counts and then learned again, twice, while +building the rules. Assert on the positional contract, never on a provoked +failure. + +**Rule counts must count MESSAGES.** `requestCounts` counts threads, which is +right for the placeholder pane because a click there produces thread rows. A +rule tags messages, so a thread count understates every rule matching part of a +large thread; `requestMessageCounts` exists beside it for that reason. The two +are separate signals with separate generation counters, and a count request +must never bump `m_generation`: that is the *query* generation, and bumping it +discards any thread load in flight, blanking the message pane because the user +asked for counts. + **Config format gotcha:** QSettings treats `/` in a section name as a group separator, so account sections are `[account.work]`, not `[account/work]`. `childKeys` returns keys sorted alphabetically, never in file order. **`[general]` keys are read WITHOUT the 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 c0c1c1e..656773b 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 @@ -100,7 +100,7 @@ taking that too literally. | 41 | A message whose HTML body carries a `Content-Id` renders blank | correctness | S | **done** | | 42 | "Syncing..." says nothing about what is being synced | feedback | S | **done** | | 43 | No "Mark all read" for the current view | workflow | S | **done** | -| 44 | No way to manage the filters applied at sync time | workflow | M | open, specified 2026-08-12; see `specs/2026-08-12-tagging-rules-design.md`. Spans this repo and `mailctl` | +| 44 | No way to manage the filters applied at sync time | workflow | M | **done** 2026-08-13; see `specs/2026-08-12-tagging-rules-design.md`. Spans this repo and `mailctl` | | 45 | Two Sync buttons, and only one of them works properly | correctness | S | **done** | | 46 | `uiStateSurvivesARestart` fails under the offscreen platform | testing | XS | **done** | | 47 | The query bar looks unfinished, and cannot be cleared by mouse | presentation | XS | **done** | @@ -3040,6 +3040,50 @@ A filter engine that rewrites the Maildir would not violate that literally, but it would put qtmaildir in the business of moving mail, which is a decision to take deliberately rather than by implementing a dialog. +### Outcome (done 2026-08-13) + +The rules moved from the shell `post-new` hook to +`~/.config/mailrules/rules.json`, read by both qtmaildir and `mailctl`. The +hook and the shared `mailrules.py` live in the mailctl repository; this repo +has `TagRules`, `tests/test_tagrules.cpp`, a management dialog on the Message +menu, and `NotmuchWorker::requestMessageCounts` for the dry run. Seventeen real +rules were converted and each one's shell comment became its `note`. + +**Four things learned that outlive the item.** + +The parenthesisation of a rule's own query is load-bearing. `tag:new and a or b` +binds as `(tag:new and a) or b`, so a rule that is a disjunction of senders +escapes its scope and matches the whole corpus. Several real rules have exactly +that shape. `mailrules.scoped_query` is the only place that string is built. + +The hook must not consume `tag:new` when the rules failed to load. Clearing the +marker while the rules did not run orphans that mail permanently and silently, +and the gap would surface months later as "why did this stop being tagged". + +**notmuch's query parser rejects almost nothing**, which invalidated two +assertions written into the plan from memory. `from:((((` parses cleanly and +matches nothing rather than failing, so a test expecting a non-zero exit or a +`-1` count fails against correct code. `tests/test_notmuchworker.cpp` already +recorded this for thread counts and the lesson had to be learned twice. + +`requestCounts` counts THREADS, which is right for the placeholder pane and +wrong for a rule: a rule tags messages, so a thread count understates any rule +matching part of a large thread. Hence `requestMessageCounts` beside it rather +than a change to it. + +**Verification, since a rules file that tags the wrong mail is expensive.** All +seventeen converted rules were counted against the real index and matched the +shell hook exactly, tags and counts, before anything was installed. The staged +hook was then run against real mail with a tag deliberately removed, and +restored it. The live swap was confirmed by a real sync: `status=OK`, +`applied 17 rule(s)`, marker consumed, and the per-rule counts moved as new mail +arrived. + +**Backfill remains out of scope**, and is the piece that will force a revision +to the "no destructive-action confirmation, undo instead" rule in `CLAUDE.md`. +The user has said that rule is due for revision anyway. See the spec's "Out of +scope" section. + ## 45. Two Sync buttons on the main window **Observed (user, 2026-08-05):** "there's currently 2 Sync buttons on the main |
