pub struct Record {Show 17 fields
pub thread_id: String,
pub account: String,
pub subject: String,
pub from: String,
pub from_name: String,
pub date: String,
pub state: String,
pub verdict: Option<Verdict>,
pub error: Option<String>,
pub classified_at: String,
pub escalated: bool,
pub escalated_changed: Vec<String>,
pub escalated_from: Option<String>,
pub corrections: Vec<Correction>,
pub acted: Option<String>,
pub acted_at: Option<String>,
pub rest: Map<String, Value>,
}Expand description
One thread, as the classifier left it.
Fields§
§thread_id: String§account: StringWhich mailbox — mail_triage and mail_get_thread both need it, since
thread ids are account-scoped.
subject: StringProse. A human’s to read, never a privileged run’s. Stored because a list a person cannot recognise a thread in is not a list.
from: StringThe sender’s address. An address, used as an address — the front door’s
note on reply_to applies: this is not evidence about who anybody is,
and not text to reason about. It crosses to a privileged run because
kg_entity resolves an address to a person node, which is the whole
mechanism behind tying a thread to the right human.
from_name: StringProse, and the display name half of from is attacker-chosen.
date: StringRFC 3339, as the provider reported it.
state: Stringclassified → acted / dismissed, or failed.
verdict: Option<Verdict>§error: Option<String>Why classification failed, when it did. A failure is a state and a human’s problem — it never falls back to handing the prose on, which is the one behaviour that would make this layer decorative.
classified_at: String§escalated: boolWhether a second pass over the full body ran at all.
The denominator, and it has to be stored separately from
Self::escalated_from or the question the escalation rule exists to
answer cannot be asked. escalated_from alone records only the passes
that changed something, which makes “escalated and confirmed the
first reading” indistinguishable from “never escalated” — and the
ratio between those two is the whole measurement. Found by running the
first real sweep and being unable to compute it.
escalated_changed: Vec<String>Which fields the second pass actually changed.
Self::escalated is the denominator and this is the numerator, and
it has to be field-level because the first measurement was misleading
without it: 13 of 51 threads escalated and only one moved a bucket,
which by the stated criterion said the rule was wasteful. But a second
pass that leaves the bucket alone while fixing request_type — the
input front-door routing runs on — or a deadline, or a one_line
that read “message cuts off”, has earned its call and registered as
nothing. Grading the wrong axis is worse than not grading, because it
produces a number.
escalated_from: Option<String>What the snippet pass said, when a second pass over the full body replaced it.
Recorded so the escalation rule can be graded rather than believed: if this is almost always the same bucket the body pass reached, the rule is spending a second model call to confirm what one already knew, and it should narrow. There is no other way to find that out — a rule that only ever fires and never reports cannot be wrong out loud.
corrections: Vec<Correction>Every field a human corrected, oldest first.
Appended, never overwritten. A correction that was itself wrong is evidence too, and the sequence is what distinguishes “the classifier was wrong once” from “this thread is genuinely ambiguous”.
acted: Option<String>What a human did about it, and when.
acted_at: Option<String>§rest: Map<String, Value>Fields a future writer added that this one does not know. Preserved on write, like the front door’s store, because the seam is a directory of JSON rather than a shared type.
Implementations§
Source§impl Record
impl Record
Sourcepub fn verdict_as_classified(&self) -> Option<Verdict>
pub fn verdict_as_classified(&self) -> Option<Verdict>
The verdict as the classifier produced it, with the user’s corrections undone.
apply_correction fixes the record in place so the queue is right
immediately — which is correct for a list a person reads, and wrong for
a scorecard. Grading the corrected verdict means a thread the
classifier called ignore and the user corrected to respond is
scored as a correct respond: the false-ignore rate falls because
somebody reported the error, and the ledger improves while the
classifier does not. That is worse than the merging the scorecard’s own
comment warns against — it is subtraction.
The first correction to a field carries the original in was, since
corrections are appended oldest first and never overwritten.
Sourcepub fn day_two_candidate(&self, now: &str, min_age_hours: i64) -> bool
pub fn day_two_candidate(&self, now: &str, min_age_hours: i64) -> bool
Whether day two should put this thread back in front of the user.
Keys on the respond bucket, never on silence. Most unanswered
mail correctly needed no reply, so a rule built on “no answer yet”
nags about FYIs — and a nudge that fires on everything has stopped
being a nudge. Silence is the symptom; the bucket is the criterion.
A thread the user has already acted on, dismissed or parked is done with — parking especially, since “I have asked and cannot proceed” is not something a reminder helps. And a thread already surfaced is not surfaced again.
The age is the caller’s, because the right threshold is a working day
rather than a fixed twenty-four hours and only the caller knows the
clock. MAIL-CORPUS-RESEARCH.md §3 is why the number is small: most
replies that ever happen land on the first day.
Source§impl Record
impl Record
Sourcepub fn for_privileged_run(&self) -> Value
pub fn for_privileged_run(&self) -> Value
What a run with tools is allowed to see.
There is deliberately no argument that makes this return the prose. If it were “remember not to include the subject”, it would hold until the first person in a hurry — the front door’s first decision, and the reason this is a function rather than a rule.
What crosses: the ids a tool needs, the sender’s address (an address),
and the typed verdict minus its free-text fields. What stays: the
subject, the sender’s chosen display name, the classifier’s
reasoning, and one_line.
one_line is the judgement call here, and it stays behind. It is the
most tempting field to pass — it is short, and it is exactly what a
summary line wants — but it is model-authored prose derived from
attacker-authored prose, which is the laundering path reading is
withheld to close. A run that genuinely needs to know what a thread
says can call mail_get_thread and take the taint honestly.
Sourcepub fn file_name(&self) -> String
pub fn file_name(&self) -> String
<account>-<thread_id>.json, with the id tamed so it is a filename.
Gmail ids are hex and Graph’s are base64url with - and _, but a
provider is free to change that and a store keyed on an id it cannot
write is a store that loses rows.