pub struct TransitionDecision {
pub timestamp: String,
pub actor: String,
pub from: String,
pub to: String,
pub from_category: String,
pub to_category: String,
pub override_reason: Option<String>,
}Expand description
The auditable record of one accepted status transition (data-model.md
§5, WP03 T012).
§Why this is not a HistoryEventKind alone
A HistoryEvent has exactly four fields — timestamp, actor, kind, detail —
and the on-disk line format is fixed at four by the U+2014 split in
HistoryEvent::parse. data-model.md §5 wants seven. The three surplus
fields therefore live in detail, and this struct is what converts between
the two: Self::to_event and Self::from_event are the only places that
encoding is written or read.
§The detail grammar
detail := head [ "; override: " reason ]
head := from " -> " to " (" from_category " -> " to_category ")"e.g. todo -> in progress (unstarted -> active), or
done -> in progress (completed -> active); override: mis-filed.
It is a grammar, not prose, and that is a load-bearing choice rather than a
tidiness one. WP06’s lint_direct_edits has to compare a file’s current
status against the column its last accepted transition landed in; recovering
that column out of free-form English is guesswork, and a heuristic that
misreads it produces a lint result nobody can trust. A fixed grammar makes the
comparison exact, and makes “this detail is not a transition record”
detectable rather than a guess.
The parse is deliberately anchored rather than free-floating: the override is
split off at the first ") ; override: ", and the categories are taken from
between the last " (" and the closing ")", so a reason may contain the
bare "-> " and the bare "; override: " without disturbing the fields. That
anchoring is the whole reason the marker includes the paren — see
[OVERRIDE_MARKER].
§Known limit: a column name containing " -> "
The two column names are separated by the first " -> ", and nothing escapes
it, so a column literally named a -> b would render as
a -> b -> in progress (unstarted -> active) and parse back with from == "a".
The categories still parse correctly, and the categories are what an audit
reader wants, but a WP06 that recovers to from a file would recover b.
This is inherent to a delimiter-based grammar, not a bug in this
implementation: any unescaped separator has a name it cannot represent, and
escaping would put backslashes into a line a human reads. A board column named
with an embedded arrow is not a realistic configuration, so the limit is
documented and pinned by a test rather than designed around. WP06 reads to
from these records, so it inherits the assumption and should not be surprised
by it.
Fields§
§timestamp: StringWhen the transition was accepted, RFC 3339 — the same shape and clock as every other History entry.
actor: StringWho performed it.
A plain String, carried through to HistoryEvent::actor untouched, so
identity handling is the History log’s existing one: a bare handle or a full
Name <email>, with a legacy @alice tolerated on read. This struct
re-implements none of that and cannot drift from it.
from: StringThe column the issue was in, as stored.
to: StringThe column it was moved to, as stored.
from_category: StringThe category from resolved to, recorded for auditability.
Derived, never stored on the issue and never re-derived on read — that
is the point. If a board’s category_map is later edited, the historical
record still says what the category was when the move was accepted, which
is the only way the audit trail stays true.
to_category: StringThe category to resolved to, recorded for the same reason.
override_reason: Option<String>Why a policy rule was overridden, when one was.
Required to be Some for an override to exist at all: the service refuses a
bare “I did it anyway” (see TransitionOverride), so this is never Some
with an empty or blank reason.
Implementations§
Source§impl TransitionDecision
impl TransitionDecision
Sourcepub fn new(
timestamp: impl Into<String>,
actor: impl Into<String>,
from: impl Into<String>,
to: impl Into<String>,
from_category: impl Into<String>,
to_category: impl Into<String>,
override_reason: Option<String>,
) -> Self
pub fn new( timestamp: impl Into<String>, actor: impl Into<String>, from: impl Into<String>, to: impl Into<String>, from_category: impl Into<String>, to_category: impl Into<String>, override_reason: Option<String>, ) -> Self
Build a decision from an accepted transition.
Sourcepub fn from_event(event: &HistoryEvent) -> Result<Self>
pub fn from_event(event: &HistoryEvent) -> Result<Self>
Recover a decision from a History entry.
Err when event is not a transition_accepted entry or its detail does
not match the grammar — never a partially-filled struct. A caller can
therefore treat a successful parse as “this file records a transition to
to”, which is exactly the question WP06 asks.
Sourcepub fn to_event(&self) -> HistoryEvent
pub fn to_event(&self) -> HistoryEvent
The History entry to append for this decision.
Sourcepub fn is_no_op(&self) -> bool
pub fn is_no_op(&self) -> bool
Whether this decision is a same-column no-op record.
The seam never appends one — idempotency is decided by
TransitionOutcome::Accepted { changed: false } — but a hand-edited file
can contain one, and WP06’s lint_direct_edits needs to tell that apart
from a real move rather than reading it as “status became todo”.
Trait Implementations§
Source§impl Clone for TransitionDecision
impl Clone for TransitionDecision
Source§impl Debug for TransitionDecision
impl Debug for TransitionDecision
Source§impl<'de> Deserialize<'de> for TransitionDecision
impl<'de> Deserialize<'de> for TransitionDecision
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for TransitionDecision
Source§impl PartialEq for TransitionDecision
impl PartialEq for TransitionDecision
Source§impl Serialize for TransitionDecision
impl Serialize for TransitionDecision
impl StructuralPartialEq for TransitionDecision
Auto Trait Implementations§
impl Freeze for TransitionDecision
impl RefUnwindSafe for TransitionDecision
impl Send for TransitionDecision
impl Sync for TransitionDecision
impl Unpin for TransitionDecision
impl UnsafeUnpin for TransitionDecision
impl UnwindSafe for TransitionDecision
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more