data/FolderEvidence.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | #include <QHash> | ||
| 3 | #include <QList> | ||
| 4 | #include <QString> | ||
| 5 | #include <QStringList> | ||
| 6 | |||
| 7 | struct MailHeader; | ||
| 8 | |||
| 9 | // Sprint 105 S105.3.1/S105.3.2 (D-3): where a mail belongs, decided by the | ||
| 10 | // strongest *evidence* — not by one opaque score. In order: | ||
| 11 | // 1. Thread — an earlier mail of the same thread is filed in folder F. | ||
| 12 | // 2. List — mails of this List-Id were filed to F. | ||
| 13 | // 3. Sender — the sender's filings, weighted by recency (half-life), so a | ||
| 14 | // sender who moved to a new folder follows within a few mails. | ||
| 15 | // 4. Alias — mails to this recipient address went to F (only when that | ||
| 16 | // address concentrates on one folder: the user's main | ||
| 17 | // address, spread over every folder, decides nothing). | ||
| 18 | // 5. Subject — a small word model (Naive Bayes), only when 1–4 are silent. | ||
| 19 | // The confidence of a suggestion is the measured precision of its evidence | ||
| 20 | // kind at its strength (Calibration, filled by the replay over the user's | ||
| 21 | // own filing history), never a softmax. Pure and deterministic. | ||
| 22 | class FolderEvidence { | ||
| 23 | public: | ||
| 24 | enum class Kind { None = 0, Thread = 1, List = 2, Sender = 3, Alias = 4, Subject = 5 }; | ||
| 25 | struct Mail { | ||
| 26 | QString messageId; // canonical, without <> | ||
| 27 | QStringList threadIds; // In-Reply-To and References | ||
| 28 | QString listId; | ||
| 29 | QString sender; // normalized address | ||
| 30 | QStringList recipients; // normalized To and Cc addresses | ||
| 31 | QString subject; | ||
| 32 | qint64 date = 0; // seconds since the epoch | ||
| 33 | }; | ||
| 34 | struct Candidate { | ||
| 35 | QString folder; | ||
| 36 | Kind kind = Kind::None; | ||
| 37 | double strength = 0; // 0..1: share of the evidence for this folder | ||
| 38 | int support = 0; // filings behind the evidence | ||
| 39 | int total = 0; | ||
| 40 | }; | ||
| 41 | struct Options { | ||
| 42 | double senderHalfLifeDays = 180; | ||
| 43 | int maxSenderHistory = 64; | ||
| 44 | }; | ||
| 45 | |||
| 46 | 529 | FolderEvidence() = default; | |
| 47 | explicit FolderEvidence(Options options) : m_options(options) {} | ||
| 48 | static Mail fromHeader(const MailHeader &header); | ||
| 49 | |||
| 50 | // A mail filed in `folder` (training, in date order). | ||
| 51 | void learn(const Mail &mail, const QString &folder); | ||
| 52 | // S105.3.3: the user moved a mail somewhere else than suggested — | ||
| 53 | // the latest choice wins for the next mail of that sender/list. | ||
| 54 | void correct(const Mail &mail, const QString &chosenFolder); | ||
| 55 | // A folder was renamed or removed (renameFolderData/resetFolder). | ||
| 56 | void renameFolder(const QString &from, const QString &to); | ||
| 57 | void removeFolder(const QString &folder); | ||
| 58 | |||
| 59 | // Candidates of distinct folders, best first (kind order, then strength). | ||
| 60 | QList<Candidate> rank(const Mail &mail) const; | ||
| 61 | 434 | int filings() const { return m_filings; } | |
| 62 | |||
| 63 | private: | ||
| 64 | struct Placement { | ||
| 65 | qint64 date = 0; | ||
| 66 | QString folder; | ||
| 67 | }; | ||
| 68 | Options m_options; | ||
| 69 | int m_filings = 0; | ||
| 70 | QHash<QString, QString> m_threadFolder; // message-id -> folder | ||
| 71 | QHash<QString, QHash<QString, int>> m_listFolders; // list-id -> folder -> count | ||
| 72 | QHash<QString, QList<Placement>> m_senderHistory; // sender -> newest last | ||
| 73 | QHash<QString, QHash<QString, int>> m_aliasFolders; // recipient -> folder -> count | ||
| 74 | QHash<QString, QHash<QString, int>> m_wordFolders; // word -> folder -> count | ||
| 75 | QHash<QString, int> m_folderDocs, m_folderWords; | ||
| 76 | QHash<QString, QString> m_senderOverride, m_listOverride; // S105.3.3 | ||
| 77 | QList<Candidate> subjectCandidates(const Mail &mail) const; | ||
| 78 | }; | ||
| 79 | |||
| 80 | // S105.3.2: measured precision per evidence kind and strength (10 bins), | ||
| 81 | // with a Laplace prior; filled by the replay, stored with the model. | ||
| 82 | class FolderCalibration { | ||
| 83 | public: | ||
| 84 | void record(FolderEvidence::Kind kind, double strength, bool correct); | ||
| 85 | // Precision estimate of a suggestion of this kind and strength (0..1). | ||
| 86 | double confidence(FolderEvidence::Kind kind, double strength) const; | ||
| 87 | QString serialize() const; | ||
| 88 | static FolderCalibration deserialize(const QString &text); | ||
| 89 | 1 | bool isEmpty() const { return m_bins.isEmpty(); } | |
| 90 | |||
| 91 | private: | ||
| 92 | static int bin(double strength); | ||
| 93 | QHash<int, QPair<int, int>> m_bins; // kind * 16 + bin -> (correct, total) | ||
| 94 | }; | ||
| 95 | |||
| 96 | // S105.0.5: the filing history replayed in date order — every mail is | ||
| 97 | // predicted from the evidence of earlier mails only, then learned. | ||
| 98 | // Aggregates only (no names, no addresses): what the diagnostic report and | ||
| 99 | // the tests print. | ||
| 100 | struct FolderReplayMetrics { | ||
| 101 | int mails = 0; // filed mails replayed | ||
| 102 | int shown = 0; // a suggestion at or above the display threshold | ||
| 103 | int shownCorrect = 0; | ||
| 104 | int top2Correct = 0; // the right folder among the first two candidates | ||
| 105 | int answered = 0; // any candidate at all | ||
| 106 | QList<QPair<int, int>> reliability; // 10 confidence bins: (correct, total) | ||
| 107 |
2/2✓ Branch 2 → 3 taken 68 times.
✓ Branch 2 → 4 taken 204 times.
|
272 | double coverage() const { return mails ? double(shown) / mails : 0; } |
| 108 |
2/2✓ Branch 2 → 3 taken 50 times.
✓ Branch 2 → 4 taken 221 times.
|
271 | double precision() const { return shown ? double(shownCorrect) / shown : 0; } |
| 109 |
2/2✓ Branch 2 → 3 taken 69 times.
✓ Branch 2 → 4 taken 204 times.
|
273 | double top2() const { return mails ? double(top2Correct) / mails : 0; } |
| 110 | QString report() const; | ||
| 111 | // S105.3.2: the lowest confidence bin bound whose suggestions — together | ||
| 112 | // with all more confident ones — were right at least `precision` of the | ||
| 113 | // time in this replay (1.0 = show nothing). The display threshold. | ||
| 114 | double thresholdFor(double precision) const; | ||
| 115 | // Coverage and precision had `threshold` been the display threshold. | ||
| 116 | QPair<double, double> at(double threshold) const; | ||
| 117 | }; | ||
| 118 | |||
| 119 | struct FolderReplayItem { | ||
| 120 | FolderEvidence::Mail mail; | ||
| 121 | QString folder; | ||
| 122 | }; | ||
| 123 | |||
| 124 | // Replays `items` (sorted by date) through a fresh FolderEvidence and fills | ||
| 125 | // `calibration`; the confidence of each prediction is taken from the | ||
| 126 | // calibration *as learned so far* (an honest out-of-sample estimate). | ||
| 127 | FolderReplayMetrics replayFolderEvidence(const QList<FolderReplayItem> &items, double threshold, | ||
| 128 | FolderCalibration *calibration, FolderEvidence *trained = nullptr); | ||
| 129 |