Skip to main content

TransitionDecision

Struct TransitionDecision 

Source
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: String

When the transition was accepted, RFC 3339 — the same shape and clock as every other History entry.

§actor: String

Who 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: String

The column the issue was in, as stored.

§to: String

The column it was moved to, as stored.

§from_category: String

The 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: String

The 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

Source

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.

Source

pub fn detail(&self) -> String

Render to the single detail string the History line carries.

Source

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.

Source

pub fn to_event(&self) -> HistoryEvent

The History entry to append for this decision.

Source

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

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for TransitionDecision

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for TransitionDecision

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Eq for TransitionDecision

Source§

impl PartialEq for TransitionDecision

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for TransitionDecision

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for TransitionDecision

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.