diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 63 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-08-14-query-to-rule-design.md | 125 |
2 files changed, 150 insertions, 38 deletions
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 e60b88c..316b5dc 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 @@ -145,7 +145,7 @@ taking that too literally. | 78 | No way to build a rule from something visible in a message | workflow | M | open; wants 76 first, so the created rule lands in a form that can hold it | | 80 | A rule with many conditions squeezes the rule list to one visible row | defect | XS | **done** 2026-08-13, shipped in 0.17.0. Follows item 76 | | 79 | Opening the rules dialog and saving destroys the first rule | defect | XS | **fixed on `rule-builder`** 2026-08-13, unreleased. Shipped in 0.16.0; damaged one real rule, repaired by hand | -| 81 | No way to turn a saved query into a tagging rule | workflow | S | open; 23 has shipped, so the dialog it depends on exists. Writes to the shared rules file, so it spans this repo and `mailctl` | +| 81 | No way to turn a saved query into a tagging rule | workflow | S | open, specified 2026-08-14; see `specs/2026-08-14-query-to-rule-design.md`. A context-menu action seeding the rules dialog, single repo | | 82 | A saved query cannot be edited, unpinned or deleted from the UI | defect | S | **done** 2026-08-13, shipped in 0.18.0. Right-click offers Edit, Pin/Unpin and Delete | | 83 | A rule named with spaces is written to the file and dropped by every reader | defect | S | **done** 2026-08-14, unreleased. The name is sanitised into an id, save validates, a bad id loads for repair | @@ -504,43 +504,30 @@ in CLAUDE.md. ## 81. No way to turn a saved query into a tagging rule -**Observed (user, 2026-08-13):** raised while describing item 23. The save-query -dialog could optionally keep the query as a tagging rule as well as a saved -query, "going the opposite route of creating a filter in the dialog and running -a preview first". Item 77 shipped that opposite route: a rule in the dialog can -be previewed in the thread list. This is the same journey backwards, from a -query the user has already run and can see the results of, to a rule that tags -it from then on. - -**Why it is its own item rather than part of 23.** A saved query is a view and -costs nothing if it is wrong. A rule is applied to real mail by a `post-new` -hook every ten minutes, and lives in `~/.config/mailrules/rules.json`, which has -two independent implementations that agree by test rather than by shared code. -Item 23 is otherwise a presentation change; folding this in would make it carry -a two-repo commitment and a live blast radius. Kept separate so 23 can ship -without it. - -**Approach.** A checkbox or a second button in the save dialog that hands the -query to `TagRules` alongside the `queries.json` write, with the tags to apply -asked for at that point: a rule needs an action, and a saved query has none. -Preserve unknown fields, per "Changing the shared rule format" in CLAUDE.md. -Note that two separate files are then written from one dialog, `queries.json` -here and `rules.json` shared with mailctl, so decide what happens when the -second write fails after the first succeeded. - -**Constraints.** A stored query carries no scope: the hook supplies `tag:new` -and parenthesises the query, because `tag:new and a or b` binds as -`(tag:new and a) or b`. A saved query written for a view is frequently a -disjunction, so this is the common case here rather than an edge one, and a -rule made this way is scoped differently from the query the user just ran. Say -so in the dialog. The hook also refuses to remove `unread` or `inbox`, so a -dialog that offers to write such a rule produces one the hook will not honour; -that is the correct direction, but it has to be said in the UI rather than -failing silently. - -**Depends on item 23**, which builds the dialog this hangs off. - -**Size: S** on top of 23, and not meaningful before it. +**Observed (user, 2026-08-13):** raised while describing item 23. A query the +user has already run and can see the results of should be turnable into a rule +that tags it from then on. Item 77 shipped the opposite route, previewing a +rule's mail in the thread list; this is that journey backwards. + +**Specified 2026-08-14. Read `specs/2026-08-14-query-to-rule-design.md` +instead of planning from here.** + +The three things that decide whether it can be picked up: + +- **A context-menu action, not a checkbox in the Save query dialog.** The + original approach below was rejected: it would make one dialog write both + `queries.json` and the shared `rules.json`, and `SaveQueryDialog` is + deliberately pure UI that writes nothing. +- **It is a single-repo change after all.** The rule it creates is an ordinary + one, so nothing about the shared format changes and `mailrules.py` is + untouched. The earlier note that this item spans two repos was about the file + it lands in, not about the work. +- **Stored queries only.** A generated entry's query is composed from the + accounts at runtime, so a rule made from one would freeze a snapshot that goes + stale when an account is added. + +**Item 78 folds into the same path**, as a second caller with a different seed +rather than a second mechanism. ## Deferred, unsized, or split out diff --git a/docs/superpowers/specs/2026-08-14-query-to-rule-design.md b/docs/superpowers/specs/2026-08-14-query-to-rule-design.md new file mode 100644 index 0000000..8c9c66b --- /dev/null +++ b/docs/superpowers/specs/2026-08-14-query-to-rule-design.md @@ -0,0 +1,125 @@ +# Turning a saved query into a tagging rule + +Resolves backlog item **81**. Depends on item 23 (the saved-query file and its +context menu) and item 76 (the rule builder), both shipped. + +## What this is + +A saved query is a view: it costs nothing if it is wrong, and the user can see +exactly what it collects. A tagging rule is the same query given an action and +handed to a `post-new` hook that runs against real mail every ten minutes. This +item is the journey from the first to the second, for a query the user has +already run and whose results are on screen. + +Item 77 shipped the opposite direction: a rule in the rules dialog can be +previewed in the thread list. This is that road backwards. + +## The shape + +**Entry point.** The saved-query context menu gains **Create tagging rule...**, +after Delete and below a separator. The menu is built once, in +`MainWindow::addSavedQueryActions()` (`src/mainwindow.cpp:1743`), and is used +for both the pinned buttons and the More queries entries, so one action added +there appears in both places. + +**Stored queries only.** A generated entry (Sent, and the account-scoped ones) +does not get the item. Its query is composed at runtime from the configured +accounts, so a rule made from it would freeze a snapshot that goes stale the day +an account is added, in a file the hook reads unattended and nobody re-reads. +`SavedQuery::isGenerated()` (`src/config.h:144`) is the test, and item 82 +already uses it to make the Edit dialog show a generated query read-only. + +**What the action does.** Opens `TagRulesDialog` seeded with one new rule, +appended to the working list and selected: + +| field | value | +|---|---| +| `query` | the saved query's stored query, verbatim | +| `id` | `TagRules::uniqueId(saved.name, <ids already in the list>)` | +| `add` / `remove` | empty, and the Add tags field takes focus | +| `enabled` | `true` | +| `stage` | 50, the default | + +Nothing reaches disk until Save, exactly as the existing **Add rule** button +behaves. Cancel discards the seeded rule along with any other pending edit. + +## Decisions, and why + +**Not a checkbox in the Save query dialog.** The backlog proposed one. It was +rejected in favour of a separate action for three reasons: it would give the +save path a second, more dangerous job; it would make one dialog write two +files, `queries.json` here and `rules.json` shared with mailctl, raising a +partial-failure question with no good answer; and `SaveQueryDialog` is +deliberately pure UI that writes nothing and returns a value +(`src/savequerydialog.h:31-35`), which is worth keeping. The rules dialog is +also where the match count and the preview live, and those are what make a rule +safe to create. + +**The seeded rule is enabled.** It matches what Add rule already does, and an +inconsistent default between two ways of making the same thing is worse than +either default. A rule created disabled and then forgotten is its own silent +failure, and the match count in the list is the check that the user has anyway. + +**The tags are left empty on purpose.** A rule that adds and removes nothing +fails `TagRules::validate()`, so Save is refused with the red banner naming the +rule. The one field the user must supply is the one the dialog opens on, and +forgetting it is caught rather than written. This is the validation added for +item 83 doing the job it was built for. + +**The scope difference is not restated in the dialog.** When the hook applies a +rule it supplies `tag:new` and parenthesises the query, so a rule only ever +matches newly arrived mail, never the messages the user was just looking at. +That is a real and counterintuitive difference, and it is already stated in the +dialog's intro text: "Rules tag mail as it arrives ... it does not retag mail +you already have." A second notice beside the red warning banner would dilute +the banner, which item 83 has just finished making load-bearing. If the intro +turns out not to carry it, fix the intro rather than adding a second line. + +## Structure + +Three small changes, no new file. + +- **`TagRulesDialog`** gains a constructor taking a seed `TagRule`. It appends + the seed to `m_working` after the normal load, selects it, and focuses the Add + tags field. The existing constructor delegates to it with no seed. +- **`MainWindow::showTagRulesDialog()`** gains an optional seed parameter, + passed through. Every existing caller is unchanged. +- **`MainWindow::addSavedQueryActions()`** gains the action, guarded on + `!saved.isGenerated()`. + +**Item 78 folds into this.** "Create rule from sender" is the same seeded-dialog +path with a different seed, so it becomes a second caller rather than a second +mechanism. This is the reason the seed is a whole `TagRule` rather than a query +string: a sender rule will want to set tags too. + +## Testing + +In `test_tagrules`: + +- A seeded dialog carries the query and a sanitised id, and the rule is in the + working list and **not** on disk. +- Save writes it. Cancel does not, asserted by rereading the file. +- A seed whose name collides with an existing rule id gets a suffix rather than + replacing that rule. This is the case `uniqueId` exists for, and it is + reachable here in a way it is not from the rules dialog alone, since the name + comes from a different file. +- A seeded rule saved with no tags is refused, and the banner names it. + +In `test_mainwindow`: + +- The menu carries `Create tagging rule...` for a stored query and does not for + a generated one. The generated half needs a guard proving the menu was built + at all: item 82 records that a test asserting only the ABSENCE of a widget + passes against no implementation whatever. + +## Out of scope + +**Backfill.** Applying the new rule to the mail the user is looking at is the +obvious next question and is deliberately unbuilt, here as everywhere else. See +`2026-08-12-tagging-rules-design.md` for what it needs first, including the +revision it forces to the no-confirmation rule in CLAUDE.md. + +**Editing the saved query from the rules dialog.** The two objects are +independent once the rule exists. A rule that remembers where it came from would +raise the question of what happens when the query is later edited, and neither +answer is obviously right. |
