aboutsummaryrefslogtreecommitdiffstats
path: root/AGENTS.md
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-31 12:49:53 +0100
committerDanilo M. <danix@danix.xyz>2026-10-31 12:49:53 +0100
commitc7d09a3956c2b32f9590651c2088ab34334ac538 (patch)
treee7fdf1cc55ecfe836981ad770315e9c6d832958d /AGENTS.md
parent2b6ecd2a1f624c12c071320ff1ed96404d75f839 (diff)
downloadqtmaildir-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 'AGENTS.md')
-rw-r--r--AGENTS.md51
1 files changed, 45 insertions, 6 deletions
diff --git a/AGENTS.md b/AGENTS.md
index ce0f380..af3195c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -290,7 +290,7 @@ A test for this needs a thread whose messages DISAGREE about the tag, for the
same reason item 87 records: two messages in the same state answer identically
whichever way the code resolves them.
-**There is exactly ONE exception, and its shape is the rule's own logic rather
+**There are two exceptions, and their shape is the rule's own logic rather
than a hole in it.** `empty_trash` (item 118) destroys files and index entries,
so it has no inverse to push, and the protection the rule actually provides —
that a user never loses work to a keystroke — has to come from somewhere else.
@@ -298,7 +298,16 @@ It therefore asks, naming the count and the account, defaulting to Cancel, and
it carries **no default shortcut** for the same reason. `NotmuchWorker::purgeMessages()`
is a separate entry point from `moveMessages()` deliberately: the two look
alike and only one of them can be undone. A second confirmation anywhere is a
-defect unless the action is likewise irreversible. All actions funnel through one `applyTags` path;
+defect unless the action is likewise irreversible, or is the second exception.
+
+The second is **a write scoped to the whole index rather than to rows on
+screen**: applying a tagging rule to existing mail (item 212). It IS undoable,
+through the same exact-undo path as every tag write, and it confirms anyway,
+naming the count, because the user cannot see what it will touch. The two
+exceptions have different reasons and must keep them: `empty_trash` confirms
+because it cannot be undone, Apply because it cannot be seen.
+
+All actions funnel through one `applyTags` path;
multi-row selections go through `applyTagsToThreads`, which resolves every thread in ONE
combined `thread:a or thread:b` query rather than one query per thread.
@@ -1311,7 +1320,8 @@ schema:
**Two hook properties are safety-critical, and being ours now is not a reason to
weaken them.**
-The hook refuses to remove `unread` or `inbox` (`maildir.synchronize_flags` is
+The hook refuses to remove `unread`, and `inbox` unless the rule sets
+`allow_archive`, item 213 (`maildir.synchronize_flags` is
true, so removing `unread` rewrites Maildir filenames and reaches the server),
and it does not consume the `tag:new` marker when the rules fail to load
(clearing it while rules did not run orphans that mail permanently and
@@ -1319,6 +1329,35 @@ invisibly). A dialog here that offers to write such a rule would produce one the
hook then refuses; that is the correct direction, but say so in the UI rather
than letting it fail silently.
-Backfill, applying a rule to existing mail, is deliberately unbuilt. See
-`docs/superpowers/specs/2026-08-12-tagging-rules-design.md` for what it needs
-first, including the revision it forces to the no-confirmation rule above.
+Backfill exists since item 212 as the dialog's **Apply to existing mail**, and
+it runs in qtmaildir, attended and confirmed, never in the hook. Three of its
+properties are load-bearing, and each replaced a version that broke:
+
+- **The worker resolves the ids ONCE and writes one tag at a time over that
+ fixed list.** `MainWindow::confirmRuleApply()` sends
+ `NotmuchWorker::applyRule(query, add, remove, description)`, which resolves
+ `SearchTerm::changes(query, add, remove)` once. One tag per write, because
+ `applyTags()` reports which MESSAGES moved, not which tags: one `+promo
+ -inbox` write lists a message that already had `promo` (it lost `inbox`), and
+ its undo strips `promo` from it. Ids once, because re-resolving per operation
+ made a rule whose query names its own tags apply only half of itself.
+- **The worker always answers with ONE `ruleApplied(description, changes,
+ error)`**, one `TagChange` per operation carrying that operation's effective
+ ids, and never emits `tagsApplied` or `errorOccurred`. `onRuleApplied()`
+ records each non-empty change through `recordConfirmedTagChange()` (the
+ bookkeeping of `onTagsApplied()` without crediting a queued command), pushes
+ `MessageTagCommand`s built with the known ids in one undo macro, marks every
+ account edited so the next sync covers it, and re-queries. A partial write
+ still gets its undo, and the error goes to the status bar.
+- **Nothing is armed in advance.** An earlier version queued its commands on
+ `m_awaitingTagConfirmation` before the write; a user gesture during a slow
+ backfill cleared that queue, and the user's own command then took the
+ backfill's ids, so Ctrl+Z on an Archive re-inboxed thousands of messages.
+ Do not route a backfill through `awaitTagConfirmation()` again.
+
+Apply during a sync is REFUSED, checked before the count and again after the
+confirmation box, whose nested event loop a sync can start inside: `HeldEdit`
+replays ids, and a backfill has none until the worker resolves its query. The
+box says the write can be undone "until the next search", because `runQuery()`
+clears the undo stack. There is no Redo action; the backfill's commands carry
+real ids, so they would redo correctly if one is added.