aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/plans/2026-08-26-card-avatars.md80
-rw-r--r--docs/superpowers/specs/2026-08-26-card-avatars-design.md10
2 files changed, 87 insertions, 3 deletions
diff --git a/docs/superpowers/plans/2026-08-26-card-avatars.md b/docs/superpowers/plans/2026-08-26-card-avatars.md
index 0fba38f..03ea78a 100644
--- a/docs/superpowers/plans/2026-08-26-card-avatars.md
+++ b/docs/superpowers/plans/2026-08-26-card-avatars.md
@@ -988,7 +988,41 @@ void TestBusinessSenders::onlyBulkLookingLocalPartsAreProposed()
}
```
-Declare all three in `private slots:` and add `#include <QFileInfo>` to the test's includes.
+```cpp
+void TestBusinessSenders::theFirstRunScansEverything()
+{
+ QTemporaryDir dir;
+ const QString missing = dir.filePath(QStringLiteral("business-senders"));
+
+ // No file at all: a week of mail would propose almost nothing and the
+ // list would take months to become useful, so the first run pays for a
+ // full scan once.
+ QCOMPARE(BusinessSenders::scanQuery(missing), QStringLiteral("*"));
+
+ // A file holding ONLY rejected candidates is still a first run: nothing
+ // has been accepted yet. Rescanning re-proposes none of them, since
+ // appendCandidates skips anything already mentioned.
+ QFile rejected(missing);
+ QVERIFY(rejected.open(QIODevice::WriteOnly | QIODevice::Text));
+ rejected.write("# noreply@cofidis.it (47 messages)\n");
+ rejected.close();
+ QCOMPARE(BusinessSenders::scanQuery(missing), QStringLiteral("*"));
+}
+
+void TestBusinessSenders::alaterRunScansOnlyRecentMail()
+{
+ QTemporaryDir dir;
+ const QString path = dir.filePath(QStringLiteral("business-senders"));
+ QFile file(path);
+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
+ file.write("billing@example.org\n");
+ file.close();
+
+ QCOMPARE(BusinessSenders::scanQuery(path), QStringLiteral("date:1week.."));
+}
+```
+
+Declare all five in `private slots:` and add `#include <QFileInfo>` to the test's includes.
- [ ] **Step 2: Run test to verify it fails**
@@ -1019,6 +1053,18 @@ bool looksLikeBulk(const QString &address);
/// rejected is never re-proposed, and one they deleted only returns if that
/// sender writes again.
void appendCandidates(const QString &path, const QHash<QString, int> &counts);
+
+/// The query the candidate scan should run.
+///
+/// A week of mail once the file exists, so the step stays incremental and
+/// cheap. EVERYTHING when the file is missing or holds no entries, because
+/// that is the first run: a week's mail proposes almost nothing, and the file
+/// would then take months to become useful. The whole-database scan is
+/// affordable precisely because it happens once, measured at 76 ms over 5105
+/// messages.
+///
+/// Returns notmuch query syntax, which is wire format and is never translated.
+QString scanQuery(const QString &path);
```
Add `#include <QHash>` to the header.
@@ -1097,15 +1143,35 @@ void appendCandidates(const QString &path, const QHash<QString, int> &counts)
}
```
+```cpp
+QString scanQuery(const QString &path)
+{
+ // "*" is notmuch's match-everything. An EMPTY string would also match
+ // everything, which is why Config::matchNothingQuery() exists elsewhere in
+ // this codebase; being explicit here means a reader never has to wonder
+ // which of the two an empty return meant.
+ const List existing = load(path);
+ if (existing.addresses.isEmpty() && existing.domains.isEmpty())
+ return QStringLiteral("*");
+ return QStringLiteral("date:1week..");
+}
+```
+
Add `#include <QFileInfo>` to `src/businesssenders.cpp`.
+Note what the emptiness test is deliberately NOT: it asks whether the file holds
+any usable ENTRY, not whether the file exists or has bytes. A file holding only
+rejected candidates, every line commented out, is still a first run as far as
+this is concerned, and rescanning it costs 76 ms and re-proposes nothing, since
+`appendCandidates` skips everything already mentioned.
+
- [ ] **Step 5: Run test to verify it passes**
```bash
cmake --build build && QT_QPA_PLATFORM=offscreen ctest --test-dir build -R businesssenders --output-on-failure
```
-Expected: PASS, 9 tests.
+Expected: PASS, 11 tests.
- [ ] **Step 6: Commit**
@@ -1785,7 +1851,11 @@ In `MainWindow`, where a sync completes (search for where the unsynced count is
});
```
-Request the counts scoped to recently indexed mail rather than the whole database, so the step stays incremental: `countSenders(QStringLiteral("date:1week.."))`.
+Scope the request with `BusinessSenders::scanQuery()`, added below: a week of mail once the file exists, and everything on the first run.
+
+```cpp
+ countSenders(BusinessSenders::scanQuery(BusinessSenders::defaultPath()));
+```
- [ ] **Step 5: Run test to verify it passes**
@@ -1853,6 +1923,10 @@ After each sync the application appends addresses that look like bulk mail,
it. Anything already in the file, commented or not, is never proposed again:
commenting a line out is therefore the permanent way to reject it, while
deleting it lets that sender be proposed again if they write to you.
+
+The first scan, when the file does not exist or holds no active entry, covers
+the whole database so the list is useful straight away. Afterwards it covers
+the last week's mail.
```
- [ ] **Step 4: Update the changelog**
diff --git a/docs/superpowers/specs/2026-08-26-card-avatars-design.md b/docs/superpowers/specs/2026-08-26-card-avatars-design.md
index 14829c0..e396a34 100644
--- a/docs/superpowers/specs/2026-08-26-card-avatars-design.md
+++ b/docs/superpowers/specs/2026-08-26-card-avatars-design.md
@@ -217,6 +217,16 @@ of the newly arrived mail and appends CANDIDATES, commented out:
# noreply@cofidis.it (47 messages)
```
+**How much mail the scan covers** depends on whether the list has ever been
+used. With no file, or a file holding no active entry, it scans the WHOLE
+database; afterwards it scans the last week. The first run is exactly when a
+full scan earns its cost: a week of mail proposes almost nothing, so a
+week-only rule would leave the list taking months to become useful. It is
+affordable because it happens once, measured at 76 ms over 5105 messages.
+
+A file holding only rejected candidates still counts as unused. Rescanning it
+re-proposes none of them, since anything already mentioned is skipped.
+
A candidate is an address whose local part is in a small built-in word list
(`noreply`, `no-reply`, `donotreply`, `info`, `support`, `billing`,
`newsletter`, `notifications`, `mailer-daemon`), or one that recurs with no