Skip to main content

statum_core/
introspection.rs

1/// Static introspection surface emitted for a generated Statum machine.
2pub trait MachineIntrospection {
3    /// Machine-scoped state identifier emitted by `#[machine]`.
4    type StateId: Copy + Eq + core::hash::Hash + 'static;
5
6    /// Machine-scoped transition-site identifier emitted by `#[machine]`.
7    type TransitionId: Copy + Eq + core::hash::Hash + 'static;
8
9    /// Static graph descriptor for the machine family.
10    const GRAPH: &'static MachineGraph<Self::StateId, Self::TransitionId>;
11}
12
13/// Runtime accessor for transition descriptors that may be supplied by a
14/// distributed registration surface.
15#[derive(Clone, Copy)]
16pub struct TransitionInventory<S: 'static, T: 'static> {
17    get: fn() -> &'static [TransitionDescriptor<S, T>],
18}
19
20impl<S, T> TransitionInventory<S, T> {
21    /// Creates a transition inventory from a `'static` getter.
22    pub const fn new(get: fn() -> &'static [TransitionDescriptor<S, T>]) -> Self {
23        Self { get }
24    }
25
26    /// Returns the transition descriptors as a slice.
27    pub fn as_slice(&self) -> &'static [TransitionDescriptor<S, T>] {
28        (self.get)()
29    }
30}
31
32impl<S, T> core::ops::Deref for TransitionInventory<S, T> {
33    type Target = [TransitionDescriptor<S, T>];
34
35    fn deref(&self) -> &Self::Target {
36        self.as_slice()
37    }
38}
39
40impl<S, T> core::fmt::Debug for TransitionInventory<S, T> {
41    fn fmt(
42        &self,
43        formatter: &mut core::fmt::Formatter<'_>,
44    ) -> core::result::Result<(), core::fmt::Error> {
45        formatter.debug_tuple("TransitionInventory").finish()
46    }
47}
48
49impl<S, T> core::cmp::PartialEq for TransitionInventory<S, T> {
50    fn eq(&self, other: &Self) -> bool {
51        core::ptr::eq(self.as_slice(), other.as_slice())
52    }
53}
54
55impl<S, T> core::cmp::Eq for TransitionInventory<S, T> {}
56
57/// Runtime accessor for transition presentation metadata that may be supplied
58/// by a distributed registration surface.
59#[derive(Clone, Copy)]
60pub struct TransitionPresentationInventory<T: 'static, M: 'static = ()> {
61    get: fn() -> &'static [TransitionPresentation<T, M>],
62}
63
64impl<T, M> TransitionPresentationInventory<T, M> {
65    /// Creates a transition presentation inventory from a `'static` getter.
66    pub const fn new(get: fn() -> &'static [TransitionPresentation<T, M>]) -> Self {
67        Self { get }
68    }
69
70    /// Returns the transition presentation descriptors as a slice.
71    pub fn as_slice(&self) -> &'static [TransitionPresentation<T, M>] {
72        (self.get)()
73    }
74}
75
76impl<T, M> core::ops::Deref for TransitionPresentationInventory<T, M> {
77    type Target = [TransitionPresentation<T, M>];
78
79    fn deref(&self) -> &Self::Target {
80        self.as_slice()
81    }
82}
83
84impl<T, M> core::fmt::Debug for TransitionPresentationInventory<T, M> {
85    fn fmt(
86        &self,
87        formatter: &mut core::fmt::Formatter<'_>,
88    ) -> core::result::Result<(), core::fmt::Error> {
89        formatter
90            .debug_tuple("TransitionPresentationInventory")
91            .finish()
92    }
93}
94
95impl<T, M> core::cmp::PartialEq for TransitionPresentationInventory<T, M> {
96    fn eq(&self, other: &Self) -> bool {
97        core::ptr::eq(self.as_slice(), other.as_slice())
98    }
99}
100
101impl<T, M> core::cmp::Eq for TransitionPresentationInventory<T, M> {}
102
103/// Runtime accessor for erased transition descriptors supplied by a
104/// distributed machine inventory.
105#[derive(Clone, Copy)]
106pub struct LinkedTransitionInventory {
107    get: fn() -> &'static [LinkedTransitionDescriptor],
108}
109
110impl LinkedTransitionInventory {
111    /// Creates a linked transition inventory from a `'static` getter.
112    pub const fn new(get: fn() -> &'static [LinkedTransitionDescriptor]) -> Self {
113        Self { get }
114    }
115
116    /// Returns the linked transition descriptors as a slice.
117    pub fn as_slice(&self) -> &'static [LinkedTransitionDescriptor] {
118        (self.get)()
119    }
120}
121
122impl core::ops::Deref for LinkedTransitionInventory {
123    type Target = [LinkedTransitionDescriptor];
124
125    fn deref(&self) -> &Self::Target {
126        self.as_slice()
127    }
128}
129
130impl core::fmt::Debug for LinkedTransitionInventory {
131    fn fmt(
132        &self,
133        formatter: &mut core::fmt::Formatter<'_>,
134    ) -> core::result::Result<(), core::fmt::Error> {
135        formatter.debug_tuple("LinkedTransitionInventory").finish()
136    }
137}
138
139impl core::cmp::PartialEq for LinkedTransitionInventory {
140    fn eq(&self, other: &Self) -> bool {
141        core::ptr::eq(self.as_slice(), other.as_slice())
142    }
143}
144
145impl core::cmp::Eq for LinkedTransitionInventory {}
146
147/// Identity for one concrete machine state.
148pub trait MachineStateIdentity: MachineIntrospection {
149    /// The state id for this concrete machine instantiation.
150    const STATE_ID: Self::StateId;
151}
152
153/// Optional human-facing metadata layered on top of a machine graph.
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155pub struct MachinePresentation<
156    S: 'static,
157    T: 'static,
158    MachineMeta: 'static = (),
159    StateMeta: 'static = (),
160    TransitionMeta: 'static = (),
161> {
162    /// Optional machine-level presentation metadata.
163    pub machine: Option<MachinePresentationDescriptor<MachineMeta>>,
164    /// Optional state-level presentation metadata keyed by state id.
165    pub states: &'static [StatePresentation<S, StateMeta>],
166    /// Optional transition-level presentation metadata keyed by transition id.
167    pub transitions: TransitionPresentationInventory<T, TransitionMeta>,
168}
169
170impl<S, T, MachineMeta, StateMeta, TransitionMeta>
171    MachinePresentation<S, T, MachineMeta, StateMeta, TransitionMeta>
172where
173    S: Copy + Eq + 'static,
174    T: Copy + Eq + 'static,
175{
176    /// Finds state presentation metadata by id.
177    pub fn state(&self, id: S) -> Option<&StatePresentation<S, StateMeta>> {
178        self.states.iter().find(|state| state.id == id)
179    }
180
181    /// Finds transition presentation metadata by id.
182    pub fn transition(&self, id: T) -> Option<&TransitionPresentation<T, TransitionMeta>> {
183        self.transitions
184            .iter()
185            .find(|transition| transition.id == id)
186    }
187}
188
189/// Optional machine-level presentation metadata.
190#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub struct MachinePresentationDescriptor<M: 'static = ()> {
192    /// Optional short human-facing machine label.
193    pub label: Option<&'static str>,
194    /// Optional longer human-facing machine description.
195    pub description: Option<&'static str>,
196    /// Consumer-owned typed machine metadata.
197    pub metadata: M,
198}
199
200/// Optional state-level presentation metadata.
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub struct StatePresentation<S: 'static, M: 'static = ()> {
203    /// Typed state identifier.
204    pub id: S,
205    /// Optional short human-facing state label.
206    pub label: Option<&'static str>,
207    /// Optional longer human-facing state description.
208    pub description: Option<&'static str>,
209    /// Consumer-owned typed state metadata.
210    pub metadata: M,
211}
212
213/// Optional transition-level presentation metadata.
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub struct TransitionPresentation<T: 'static, M: 'static = ()> {
216    /// Typed transition-site identifier.
217    pub id: T,
218    /// Optional short human-facing transition label.
219    pub label: Option<&'static str>,
220    /// Optional longer human-facing transition description.
221    pub description: Option<&'static str>,
222    /// Consumer-owned typed transition metadata.
223    pub metadata: M,
224}
225
226/// A runtime record of one chosen transition.
227#[derive(Clone, Copy, Debug, Eq, PartialEq)]
228pub struct RecordedTransition<S: 'static, T: 'static> {
229    /// Rust-facing identity of the machine family.
230    pub machine: MachineDescriptor,
231    /// Exact source state where the transition was taken.
232    pub from: S,
233    /// Exact transition site that was chosen.
234    pub transition: T,
235    /// Exact target state that actually happened at runtime.
236    pub chosen: S,
237}
238
239impl<S, T> RecordedTransition<S, T>
240where
241    S: 'static,
242    T: 'static,
243{
244    /// Builds a runtime transition record from typed machine ids.
245    pub const fn new(machine: MachineDescriptor, from: S, transition: T, chosen: S) -> Self {
246        Self {
247            machine,
248            from,
249            transition,
250            chosen,
251        }
252    }
253
254    /// Finds the static transition descriptor for this runtime event.
255    pub fn transition_in<'a>(
256        &self,
257        graph: &'a MachineGraph<S, T>,
258    ) -> Option<&'a TransitionDescriptor<S, T>>
259    where
260        S: Copy + Eq,
261        T: Copy + Eq,
262    {
263        let descriptor = graph.transition(self.transition)?;
264        if descriptor.from == self.from && descriptor.to.contains(&self.chosen) {
265            Some(descriptor)
266        } else {
267            None
268        }
269    }
270
271    /// Finds the static source-state descriptor for this runtime event.
272    pub fn source_state_in<'a>(
273        &self,
274        graph: &'a MachineGraph<S, T>,
275    ) -> Option<&'a StateDescriptor<S>>
276    where
277        S: Copy + Eq,
278        T: Copy + Eq,
279    {
280        self.transition_in(graph)?;
281        graph.state(self.from)
282    }
283
284    /// Finds the static chosen-target descriptor for this runtime event.
285    pub fn chosen_state_in<'a>(
286        &self,
287        graph: &'a MachineGraph<S, T>,
288    ) -> Option<&'a StateDescriptor<S>>
289    where
290        S: Copy + Eq,
291        T: Copy + Eq,
292    {
293        self.transition_in(graph)?;
294        graph.state(self.chosen)
295    }
296}
297
298/// Runtime recording helpers layered on top of static machine introspection.
299pub trait MachineTransitionRecorder: MachineStateIdentity {
300    /// Records a runtime transition if `transition` is valid from `Self::STATE_ID`
301    /// and `chosen` is one of its legal target states.
302    fn try_record_transition(
303        transition: Self::TransitionId,
304        chosen: Self::StateId,
305    ) -> Option<RecordedTransition<Self::StateId, Self::TransitionId>> {
306        let graph = Self::GRAPH;
307        let descriptor = graph.transition(transition)?;
308        if descriptor.from != Self::STATE_ID || !descriptor.to.contains(&chosen) {
309            return None;
310        }
311
312        Some(RecordedTransition::new(
313            graph.machine,
314            Self::STATE_ID,
315            transition,
316            chosen,
317        ))
318    }
319
320    /// Records a runtime transition using a typed target machine state.
321    fn try_record_transition_to<Next>(
322        transition: Self::TransitionId,
323    ) -> Option<RecordedTransition<Self::StateId, Self::TransitionId>>
324    where
325        Next: MachineStateIdentity<StateId = Self::StateId, TransitionId = Self::TransitionId>,
326    {
327        Self::try_record_transition(transition, Next::STATE_ID)
328    }
329}
330
331impl<M> MachineTransitionRecorder for M where M: MachineStateIdentity {}
332
333/// Linked compiled machine families visible to the current build.
334#[doc(hidden)]
335#[linkme::distributed_slice]
336pub static __STATUM_LINKED_MACHINES: [LinkedMachineGraph];
337
338/// Returns every linked compiled machine family visible to the current build.
339pub fn linked_machines() -> &'static [LinkedMachineGraph] {
340    &__STATUM_LINKED_MACHINES
341}
342
343/// Linked compiled validator-entry surfaces visible to the current build.
344#[doc(hidden)]
345#[linkme::distributed_slice]
346pub static __STATUM_LINKED_VALIDATOR_ENTRIES: [LinkedValidatorEntryDescriptor];
347
348/// Returns every linked compiled validator-entry surface visible to the current
349/// build.
350pub fn linked_validator_entries() -> &'static [LinkedValidatorEntryDescriptor] {
351    &__STATUM_LINKED_VALIDATOR_ENTRIES
352}
353
354/// Linked exact relation records visible to the current build.
355#[doc(hidden)]
356#[linkme::distributed_slice]
357pub static __STATUM_LINKED_RELATIONS: [LinkedRelationDescriptor];
358
359/// Returns every linked exact relation record visible to the current build.
360pub fn linked_relations() -> &'static [LinkedRelationDescriptor] {
361    &__STATUM_LINKED_RELATIONS
362}
363
364/// Linked attested transition routes visible to the current build.
365#[doc(hidden)]
366#[linkme::distributed_slice]
367pub static __STATUM_LINKED_VIA_ROUTES: [LinkedViaRouteDescriptor];
368
369/// Returns every linked attested transition route visible to the current
370/// build.
371pub fn linked_via_routes() -> &'static [LinkedViaRouteDescriptor] {
372    &__STATUM_LINKED_VIA_ROUTES
373}
374
375/// Linked reference-type declarations visible to the current build.
376#[doc(hidden)]
377#[linkme::distributed_slice]
378pub static __STATUM_LINKED_REFERENCE_TYPES: [LinkedReferenceTypeDescriptor];
379
380/// Returns every linked reference-type declaration visible to the current
381/// build.
382pub fn linked_reference_types() -> &'static [LinkedReferenceTypeDescriptor] {
383    &__STATUM_LINKED_REFERENCE_TYPES
384}
385
386/// Structural machine graph emitted from macro-generated metadata.
387#[derive(Clone, Copy, Debug, Eq, PartialEq)]
388pub struct MachineGraph<S: 'static, T: 'static> {
389    /// Rust-facing identity of the machine family.
390    pub machine: MachineDescriptor,
391    /// All states known to the machine.
392    pub states: &'static [StateDescriptor<S>],
393    /// All transition sites known to the machine.
394    pub transitions: TransitionInventory<S, T>,
395}
396
397impl<S, T> MachineGraph<S, T>
398where
399    S: Copy + Eq + 'static,
400    T: Copy + Eq + 'static,
401{
402    /// Finds a state descriptor by id.
403    pub fn state(&self, id: S) -> Option<&StateDescriptor<S>> {
404        self.states.iter().find(|state| state.id == id)
405    }
406
407    /// Finds a transition descriptor by id.
408    pub fn transition(&self, id: T) -> Option<&TransitionDescriptor<S, T>> {
409        self.transitions
410            .iter()
411            .find(|transition| transition.id == id)
412    }
413
414    /// Yields all transition sites originating from `state`.
415    pub fn transitions_from(
416        &self,
417        state: S,
418    ) -> impl Iterator<Item = &TransitionDescriptor<S, T>> + '_ {
419        self.transitions
420            .iter()
421            .filter(move |transition| transition.from == state)
422    }
423
424    /// Finds the transition site for `method_name` on `state`.
425    pub fn transition_from_method(
426        &self,
427        state: S,
428        method_name: &str,
429    ) -> Option<&TransitionDescriptor<S, T>> {
430        self.transitions
431            .iter()
432            .find(|transition| transition.from == state && transition.method_name == method_name)
433    }
434
435    /// Yields all transition sites that share the same method name.
436    pub fn transitions_named<'a>(
437        &'a self,
438        method_name: &'a str,
439    ) -> impl Iterator<Item = &'a TransitionDescriptor<S, T>> + 'a {
440        self.transitions
441            .iter()
442            .filter(move |transition| transition.method_name == method_name)
443    }
444
445    /// Returns the exact legal target states for a transition site.
446    pub fn legal_targets(&self, id: T) -> Option<&'static [S]> {
447        self.transition(id).map(|transition| transition.to)
448    }
449}
450
451/// Erased machine graph emitted for codebase-level export over the linked
452/// build.
453#[derive(Clone, Copy, Debug, Eq, PartialEq)]
454pub struct LinkedMachineGraph {
455    /// Rust-facing identity of the machine family.
456    pub machine: MachineDescriptor,
457    /// Optional human-facing machine label.
458    pub label: Option<&'static str>,
459    /// Optional human-facing machine description.
460    pub description: Option<&'static str>,
461    /// Optional longer-form source documentation from outer rustdoc comments.
462    pub docs: Option<&'static str>,
463    /// All states known to the machine.
464    pub states: &'static [LinkedStateDescriptor],
465    /// All transition sites known to the machine.
466    pub transitions: LinkedTransitionInventory,
467    /// Direct machine-like payload references written in state data.
468    pub static_links: &'static [StaticMachineLinkDescriptor],
469}
470
471impl LinkedMachineGraph {
472    /// Finds a state descriptor by Rust state name.
473    pub fn state(&self, rust_name: &str) -> Option<&LinkedStateDescriptor> {
474        self.states
475            .iter()
476            .find(|state| state.rust_name == rust_name)
477    }
478
479    /// Yields all transition sites originating from `state`.
480    pub fn transitions_from(
481        &self,
482        state: &'static str,
483    ) -> impl Iterator<Item = &LinkedTransitionDescriptor> + '_ {
484        self.transitions
485            .iter()
486            .filter(move |transition| transition.from == state)
487    }
488
489    /// Finds the transition site for `method_name` on `state`.
490    pub fn transition_from_method(
491        &self,
492        state: &'static str,
493        method_name: &str,
494    ) -> Option<&LinkedTransitionDescriptor> {
495        self.transitions
496            .iter()
497            .find(|transition| transition.from == state && transition.method_name == method_name)
498    }
499}
500
501/// Whether one machine family is a local protocol machine or a higher-level
502/// composition machine.
503#[derive(Clone, Copy, Debug, Eq, PartialEq)]
504pub enum MachineRole {
505    /// Ordinary local protocol machine.
506    Protocol,
507    /// Higher-level machine that composes other machines or exact handoff
508    /// evidence into one workspace flow.
509    Composition,
510}
511
512/// Rust-facing identity for a machine family.
513#[derive(Clone, Copy, Debug, Eq, PartialEq)]
514pub struct MachineDescriptor {
515    /// `module_path!()` for the source module that owns the machine.
516    pub module_path: &'static str,
517    /// Fully qualified Rust type path for the machine family.
518    pub rust_type_path: &'static str,
519    /// Whether this machine is a protocol machine or a composition machine.
520    pub role: MachineRole,
521}
522
523/// Static descriptor for one generated state id.
524#[derive(Clone, Copy, Debug, Eq, PartialEq)]
525pub struct StateDescriptor<S: 'static> {
526    /// Typed state identifier.
527    pub id: S,
528    /// Rust variant name of the state marker.
529    pub rust_name: &'static str,
530    /// Whether the state carries `state_data`.
531    pub has_data: bool,
532}
533
534/// Erased state descriptor carried by the linked machine inventory.
535#[derive(Clone, Copy, Debug, Eq, PartialEq)]
536pub struct LinkedStateDescriptor {
537    /// Rust variant name of the state marker.
538    pub rust_name: &'static str,
539    /// Optional human-facing state label.
540    pub label: Option<&'static str>,
541    /// Optional human-facing state description.
542    pub description: Option<&'static str>,
543    /// Optional longer-form source documentation from outer rustdoc comments.
544    pub docs: Option<&'static str>,
545    /// Whether the state carries `state_data`.
546    pub has_data: bool,
547    /// Whether the machine exposes direct construction for this state.
548    pub direct_construction_available: bool,
549}
550
551/// Static descriptor for one transition site.
552#[derive(Clone, Copy, Debug, Eq, PartialEq)]
553pub struct TransitionDescriptor<S: 'static, T: 'static> {
554    /// Typed transition-site identifier.
555    pub id: T,
556    /// Rust method name for the transition site.
557    pub method_name: &'static str,
558    /// Exact source state for the transition site.
559    pub from: S,
560    /// Exact legal target states for the transition site.
561    pub to: &'static [S],
562}
563
564/// Erased transition descriptor carried by the linked machine inventory.
565#[derive(Clone, Copy, Debug, Eq, PartialEq)]
566pub struct LinkedTransitionDescriptor {
567    /// Rust method name for the transition site.
568    pub method_name: &'static str,
569    /// Optional human-facing transition label.
570    pub label: Option<&'static str>,
571    /// Optional human-facing transition description.
572    pub description: Option<&'static str>,
573    /// Optional longer-form source documentation from outer rustdoc comments.
574    pub docs: Option<&'static str>,
575    /// Exact source state for the transition site.
576    pub from: &'static str,
577    /// Exact legal target states for the transition site.
578    pub to: &'static [&'static str],
579}
580
581/// One direct machine-like payload reference written in state data.
582#[derive(Clone, Copy, Debug, Eq, PartialEq)]
583pub struct StaticMachineLinkDescriptor {
584    /// Source state that carries the nested machine payload.
585    pub from_state: &'static str,
586    /// Field name for named payloads; `None` for tuple payloads.
587    pub field_name: Option<&'static str>,
588    /// Machine-path suffix segments written in the payload type.
589    pub to_machine_path: &'static [&'static str],
590    /// Target state marker name taken from the payload type's first generic.
591    pub to_state: &'static str,
592}
593
594/// Exact relation kinds exported by the linked build.
595#[derive(Clone, Copy, Debug, Eq, PartialEq)]
596pub enum LinkedRelationKind {
597    /// A state payload type points at another machine state.
598    StatePayload,
599    /// A machine struct field type points at another machine state.
600    MachineField,
601    /// A transition parameter type points at another machine state.
602    TransitionParam,
603}
604
605/// Why one exact relation was inferred.
606#[derive(Clone, Copy, Debug, Eq, PartialEq)]
607pub enum LinkedRelationBasis {
608    /// The target was visible directly in the scanned type syntax.
609    DirectTypeSyntax,
610    /// The target came from a canonical `statum::Attested<_, Route>` wrapper
611    /// visible directly in the scanned type syntax.
612    AttestedTypeSyntax,
613    /// The scanned type resolved through one declared reference type.
614    DeclaredReferenceType,
615    /// The target was declared explicitly through `#[via(...)]`.
616    ViaDeclaration,
617}
618
619/// Exact target written into one linked relation record.
620#[derive(Clone, Copy, Debug)]
621pub enum LinkedRelationTarget {
622    /// A directly visible machine target written in the scanned type syntax.
623    DirectMachine {
624        /// Exact machine path segments resolved from the source type.
625        machine_path: &'static [&'static str],
626        /// Compiler-resolved concrete machine type identity. The codebase
627        /// export strips the state generic and uses the remaining machine
628        /// family path as the exact join key, even when the source syntax
629        /// names a public re-export like `flows::task::Flow`.
630        resolved_machine_type_name: fn() -> &'static str,
631        /// Target state marker name.
632        state: &'static str,
633    },
634    /// A nominal reference type that must resolve through one declaration.
635    DeclaredReferenceType {
636        /// Compiler-resolved type identity getter for the named reference type.
637        resolved_type_name: fn() -> &'static str,
638    },
639    /// One exact producer route carried through a canonical
640    /// `statum::Attested<_, Route>` wrapper or declared through `#[via(...)]`
641    /// without a direct machine target at the consumer site.
642    AttestedProducerRoute {
643        /// Machine module path that owns the attested route namespace.
644        via_module_path: &'static str,
645        /// Human-facing attested route name, such as `Capture`.
646        route_name: &'static str,
647        /// Compiler-resolved route marker type identity. This is the exact join
648        /// key across producer registrations and consumer declarations, even
649        /// when the consumer names a public re-export path.
650        resolved_route_type_name: fn() -> &'static str,
651        /// Stable route id used to join consumer declarations with producer
652        /// attested transition metadata.
653        route_id: u64,
654    },
655    /// An attested route declared through `#[via(...)]`.
656    AttestedRoute {
657        /// Machine module path that owns the attested route namespace.
658        via_module_path: &'static str,
659        /// Human-facing attested route name, such as `Capture`.
660        route_name: &'static str,
661        /// Compiler-resolved route marker type identity. This is the exact
662        /// join key across producer registrations and consumer declarations,
663        /// even when the consumer names a public re-export path.
664        resolved_route_type_name: fn() -> &'static str,
665        /// Stable route id used to join consumer declarations with producer
666        /// attested transition metadata.
667        route_id: u64,
668        /// Exact target machine path segments carried by the attested inner
669        /// machine type.
670        machine_path: &'static [&'static str],
671        /// Compiler-resolved concrete target machine type identity for the
672        /// attested inner machine. The codebase export strips the state
673        /// generic and uses the remaining machine family path as the exact
674        /// join key.
675        resolved_machine_type_name: fn() -> &'static str,
676        /// Target state marker name carried by the attested inner machine type.
677        state: &'static str,
678    },
679}
680
681impl PartialEq for LinkedRelationTarget {
682    fn eq(&self, other: &Self) -> bool {
683        match (self, other) {
684            (
685                Self::DirectMachine {
686                    machine_path: left_machine_path,
687                    resolved_machine_type_name: left_type_name,
688                    state: left_state,
689                },
690                Self::DirectMachine {
691                    machine_path: right_machine_path,
692                    resolved_machine_type_name: right_type_name,
693                    state: right_state,
694                },
695            ) => {
696                left_machine_path == right_machine_path
697                    && left_type_name() == right_type_name()
698                    && left_state == right_state
699            }
700            (
701                Self::DeclaredReferenceType {
702                    resolved_type_name: left_name,
703                },
704                Self::DeclaredReferenceType {
705                    resolved_type_name: right_name,
706                },
707            ) => left_name() == right_name(),
708            (
709                Self::AttestedProducerRoute {
710                    via_module_path: left_module_path,
711                    route_name: left_route_name,
712                    resolved_route_type_name: left_type_name,
713                    route_id: left_route_id,
714                },
715                Self::AttestedProducerRoute {
716                    via_module_path: right_module_path,
717                    route_name: right_route_name,
718                    resolved_route_type_name: right_type_name,
719                    route_id: right_route_id,
720                },
721            ) => {
722                left_module_path == right_module_path
723                    && left_route_name == right_route_name
724                    && left_type_name() == right_type_name()
725                    && left_route_id == right_route_id
726            }
727            (
728                Self::AttestedRoute {
729                    via_module_path: left_module_path,
730                    route_name: left_route_name,
731                    resolved_route_type_name: left_type_name,
732                    route_id: left_route_id,
733                    machine_path: left_machine_path,
734                    resolved_machine_type_name: left_machine_type_name,
735                    state: left_state,
736                },
737                Self::AttestedRoute {
738                    via_module_path: right_module_path,
739                    route_name: right_route_name,
740                    resolved_route_type_name: right_type_name,
741                    route_id: right_route_id,
742                    machine_path: right_machine_path,
743                    resolved_machine_type_name: right_machine_type_name,
744                    state: right_state,
745                },
746            ) => {
747                left_module_path == right_module_path
748                    && left_route_name == right_route_name
749                    && left_type_name() == right_type_name()
750                    && left_route_id == right_route_id
751                    && left_machine_path == right_machine_path
752                    && left_machine_type_name() == right_machine_type_name()
753                    && left_state == right_state
754            }
755            _ => false,
756        }
757    }
758}
759
760impl Eq for LinkedRelationTarget {}
761
762/// One exact relation source exported by the linked build.
763#[derive(Clone, Copy, Debug, Eq, PartialEq)]
764pub enum LinkedRelationSource {
765    /// A state payload type produced the relation.
766    StatePayload {
767        /// Source state that carries the relation.
768        state: &'static str,
769        /// Named field for named payloads; `None` for tuple payloads.
770        field_name: Option<&'static str>,
771    },
772    /// A machine struct field produced the relation.
773    MachineField {
774        /// Named field for named structs; `None` for tuple structs.
775        field_name: Option<&'static str>,
776        /// Stable field position within the machine struct.
777        field_index: usize,
778    },
779    /// One transition parameter produced the relation.
780    TransitionParam {
781        /// Source state where the transition is defined.
782        state: &'static str,
783        /// Transition method name.
784        transition: &'static str,
785        /// Stable non-receiver parameter index.
786        param_index: usize,
787        /// Simple identifier name when available.
788        param_name: Option<&'static str>,
789    },
790}
791
792/// One exact machine relation carried by the linked build inventory.
793#[derive(Clone, Copy, Debug, Eq, PartialEq)]
794pub struct LinkedRelationDescriptor {
795    /// Rust-facing identity of the source machine family.
796    pub machine: MachineDescriptor,
797    /// Exact relation kind.
798    pub kind: LinkedRelationKind,
799    /// Exact relation source.
800    pub source: LinkedRelationSource,
801    /// Why Statum considered this relation exact.
802    pub basis: LinkedRelationBasis,
803    /// Exact relation target.
804    pub target: LinkedRelationTarget,
805}
806
807/// One producer transition that can generate an attested route value.
808#[derive(Clone, Copy, Debug)]
809pub struct LinkedViaRouteDescriptor {
810    /// Rust-facing identity of the producer machine family.
811    pub machine: MachineDescriptor,
812    /// Machine-module path that owns the generated `machine::via` namespace.
813    pub via_module_path: &'static str,
814    /// Human-facing route name, such as `Capture`.
815    pub route_name: &'static str,
816    /// Compiler-resolved route marker type identity used to join producer
817    /// inventories with consumer `#[via(...)]` declarations.
818    pub resolved_route_type_name: fn() -> &'static str,
819    /// Stable route id shared with `LinkedRelationTarget::AttestedRoute`.
820    pub route_id: u64,
821    /// Rust transition method name on the producer machine.
822    pub transition: &'static str,
823    /// Exact producer source state.
824    pub source_state: &'static str,
825    /// Exact producer target state.
826    pub target_state: &'static str,
827}
828
829impl PartialEq for LinkedViaRouteDescriptor {
830    fn eq(&self, other: &Self) -> bool {
831        self.machine == other.machine
832            && self.via_module_path == other.via_module_path
833            && self.route_name == other.route_name
834            && (self.resolved_route_type_name)() == (other.resolved_route_type_name)()
835            && self.route_id == other.route_id
836            && self.transition == other.transition
837            && self.source_state == other.source_state
838            && self.target_state == other.target_state
839    }
840}
841
842impl Eq for LinkedViaRouteDescriptor {}
843
844/// One declared reference type carried by the linked build inventory.
845#[derive(Clone, Copy, Debug)]
846pub struct LinkedReferenceTypeDescriptor {
847    /// Human-facing Rust path of the declared reference type.
848    pub rust_type_path: &'static str,
849    /// Compiler-resolved type identity getter for the declared reference type.
850    pub resolved_type_name: fn() -> &'static str,
851    /// Exact machine path segments resolved from the declaration target.
852    pub to_machine_path: &'static [&'static str],
853    /// Compiler-resolved concrete machine type identity for the declared
854    /// target machine.
855    pub resolved_target_machine_type_name: fn() -> &'static str,
856    /// Target state marker name written in the declaration.
857    pub to_state: &'static str,
858}
859
860impl PartialEq for LinkedReferenceTypeDescriptor {
861    fn eq(&self, other: &Self) -> bool {
862        self.rust_type_path == other.rust_type_path
863            && (self.resolved_type_name)() == (other.resolved_type_name)()
864            && self.to_machine_path == other.to_machine_path
865            && (self.resolved_target_machine_type_name)()
866                == (other.resolved_target_machine_type_name)()
867            && self.to_state == other.to_state
868    }
869}
870
871impl Eq for LinkedReferenceTypeDescriptor {}
872
873
874/// One declared validator-entry surface carried by the linked build inventory.
875#[derive(Clone, Copy, Debug)]
876pub struct LinkedValidatorEntryDescriptor {
877    /// Rust-facing identity of the rebuilt machine family.
878    pub machine: MachineDescriptor,
879    /// `module_path!()` for the module that owns the `#[validators]` impl.
880    pub source_module_path: &'static str,
881    /// Human-facing source syntax for the persisted impl self type as written.
882    pub source_type_display: &'static str,
883    /// Compiler-resolved source type identity for this validator impl.
884    pub resolved_source_type_name: fn() -> &'static str,
885    /// Optional longer-form source documentation from outer rustdoc comments.
886    pub docs: Option<&'static str>,
887    /// State marker names this `#[validators]` impl can rebuild when it matches.
888    pub target_states: &'static [&'static str],
889}
890
891impl PartialEq for LinkedValidatorEntryDescriptor {
892    fn eq(&self, other: &Self) -> bool {
893        self.machine == other.machine
894            && self.source_module_path == other.source_module_path
895            && self.source_type_display == other.source_type_display
896            && (self.resolved_source_type_name)() == (other.resolved_source_type_name)()
897            && self.docs == other.docs
898            && self.target_states == other.target_states
899    }
900}
901
902impl Eq for LinkedValidatorEntryDescriptor {}
903
904#[cfg(test)]
905mod tests {
906    use super::{
907        LinkedMachineGraph, LinkedStateDescriptor, LinkedTransitionDescriptor,
908        LinkedTransitionInventory, LinkedValidatorEntryDescriptor, MachineDescriptor, MachineGraph,
909        MachineIntrospection, MachinePresentation, MachinePresentationDescriptor, MachineRole,
910        MachineStateIdentity, MachineTransitionRecorder, RecordedTransition, StateDescriptor,
911        StatePresentation, StaticMachineLinkDescriptor, TransitionDescriptor, TransitionInventory,
912        TransitionPresentation, TransitionPresentationInventory,
913    };
914    use core::marker::PhantomData;
915
916    #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
917    enum StateId {
918        Draft,
919        Review,
920        Published,
921    }
922
923    #[derive(Clone, Copy)]
924    struct TransitionId(&'static crate::__private::TransitionToken);
925
926    impl TransitionId {
927        const fn from_token(token: &'static crate::__private::TransitionToken) -> Self {
928            Self(token)
929        }
930    }
931
932    impl core::fmt::Debug for TransitionId {
933        fn fmt(
934            &self,
935            formatter: &mut core::fmt::Formatter<'_>,
936        ) -> core::result::Result<(), core::fmt::Error> {
937            formatter.write_str("TransitionId(..)")
938        }
939    }
940
941    impl core::cmp::PartialEq for TransitionId {
942        fn eq(&self, other: &Self) -> bool {
943            core::ptr::eq(self.0, other.0)
944        }
945    }
946
947    impl core::cmp::Eq for TransitionId {}
948
949    impl core::hash::Hash for TransitionId {
950        fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
951            let ptr = core::ptr::from_ref(self.0) as usize;
952            <usize as core::hash::Hash>::hash(&ptr, state);
953        }
954    }
955
956    static REVIEW_TARGETS: [StateId; 1] = [StateId::Review];
957    static PUBLISH_TARGETS: [StateId; 1] = [StateId::Published];
958    static SUBMIT_FROM_DRAFT_TOKEN: crate::__private::TransitionToken =
959        crate::__private::TransitionToken::new();
960    static PUBLISH_FROM_REVIEW_TOKEN: crate::__private::TransitionToken =
961        crate::__private::TransitionToken::new();
962    const SUBMIT_FROM_DRAFT: TransitionId = TransitionId::from_token(&SUBMIT_FROM_DRAFT_TOKEN);
963    const PUBLISH_FROM_REVIEW: TransitionId = TransitionId::from_token(&PUBLISH_FROM_REVIEW_TOKEN);
964    static STATES: [StateDescriptor<StateId>; 3] = [
965        StateDescriptor {
966            id: StateId::Draft,
967            rust_name: "Draft",
968            has_data: false,
969        },
970        StateDescriptor {
971            id: StateId::Review,
972            rust_name: "Review",
973            has_data: true,
974        },
975        StateDescriptor {
976            id: StateId::Published,
977            rust_name: "Published",
978            has_data: false,
979        },
980    ];
981    static TRANSITIONS: [TransitionDescriptor<StateId, TransitionId>; 2] = [
982        TransitionDescriptor {
983            id: SUBMIT_FROM_DRAFT,
984            method_name: "submit",
985            from: StateId::Draft,
986            to: &REVIEW_TARGETS,
987        },
988        TransitionDescriptor {
989            id: PUBLISH_FROM_REVIEW,
990            method_name: "publish",
991            from: StateId::Review,
992            to: &PUBLISH_TARGETS,
993        },
994    ];
995    static TRANSITION_PRESENTATIONS: [TransitionPresentation<TransitionId, TransitionMeta>; 2] = [
996        TransitionPresentation {
997            id: SUBMIT_FROM_DRAFT,
998            label: Some("Submit"),
999            description: Some("Move work into review."),
1000            metadata: TransitionMeta {
1001                phase: Phase::Review,
1002                branch: false,
1003            },
1004        },
1005        TransitionPresentation {
1006            id: PUBLISH_FROM_REVIEW,
1007            label: Some("Publish"),
1008            description: Some("Complete the workflow."),
1009            metadata: TransitionMeta {
1010                phase: Phase::Output,
1011                branch: false,
1012            },
1013        },
1014    ];
1015
1016    struct Workflow<S>(PhantomData<S>);
1017    struct DraftMarker;
1018    struct ReviewMarker;
1019    struct PublishedMarker;
1020
1021    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1022    enum Phase {
1023        Intake,
1024        Review,
1025        Output,
1026    }
1027
1028    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1029    struct MachineMeta {
1030        phase: Phase,
1031    }
1032
1033    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1034    struct StateMeta {
1035        phase: Phase,
1036        term: &'static str,
1037    }
1038
1039    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1040    struct TransitionMeta {
1041        phase: Phase,
1042        branch: bool,
1043    }
1044
1045    static PRESENTATION: MachinePresentation<
1046        StateId,
1047        TransitionId,
1048        MachineMeta,
1049        StateMeta,
1050        TransitionMeta,
1051    > = MachinePresentation {
1052        machine: Some(MachinePresentationDescriptor {
1053            label: Some("Workflow"),
1054            description: Some("Example presentation metadata for introspection."),
1055            metadata: MachineMeta {
1056                phase: Phase::Intake,
1057            },
1058        }),
1059        states: &[
1060            StatePresentation {
1061                id: StateId::Draft,
1062                label: Some("Draft"),
1063                description: Some("Work has not been submitted yet."),
1064                metadata: StateMeta {
1065                    phase: Phase::Intake,
1066                    term: "draft",
1067                },
1068            },
1069            StatePresentation {
1070                id: StateId::Review,
1071                label: Some("Review"),
1072                description: Some("Work is awaiting review."),
1073                metadata: StateMeta {
1074                    phase: Phase::Review,
1075                    term: "review",
1076                },
1077            },
1078            StatePresentation {
1079                id: StateId::Published,
1080                label: Some("Published"),
1081                description: Some("Work is complete."),
1082                metadata: StateMeta {
1083                    phase: Phase::Output,
1084                    term: "published",
1085                },
1086            },
1087        ],
1088        transitions: TransitionPresentationInventory::new(|| &TRANSITION_PRESENTATIONS),
1089    };
1090
1091    impl<S> MachineIntrospection for Workflow<S> {
1092        type StateId = StateId;
1093        type TransitionId = TransitionId;
1094
1095        const GRAPH: &'static MachineGraph<Self::StateId, Self::TransitionId> = &MachineGraph {
1096            machine: MachineDescriptor {
1097                module_path: "workflow",
1098                rust_type_path: "workflow::Machine",
1099                role: MachineRole::Protocol,
1100            },
1101            states: &STATES,
1102            transitions: TransitionInventory::new(|| &TRANSITIONS),
1103        };
1104    }
1105
1106    impl MachineStateIdentity for Workflow<DraftMarker> {
1107        const STATE_ID: Self::StateId = StateId::Draft;
1108    }
1109
1110    impl MachineStateIdentity for Workflow<ReviewMarker> {
1111        const STATE_ID: Self::StateId = StateId::Review;
1112    }
1113
1114    impl MachineStateIdentity for Workflow<PublishedMarker> {
1115        const STATE_ID: Self::StateId = StateId::Published;
1116    }
1117
1118    #[test]
1119    fn query_helpers_find_expected_items() {
1120        let graph = MachineGraph {
1121            machine: MachineDescriptor {
1122                module_path: "workflow",
1123                rust_type_path: "workflow::Machine",
1124                role: MachineRole::Protocol,
1125            },
1126            states: &STATES,
1127            transitions: TransitionInventory::new(|| &TRANSITIONS),
1128        };
1129
1130        assert_eq!(
1131            graph.state(StateId::Review).map(|state| state.rust_name),
1132            Some("Review")
1133        );
1134        assert_eq!(
1135            graph
1136                .transition(PUBLISH_FROM_REVIEW)
1137                .map(|transition| transition.method_name),
1138            Some("publish")
1139        );
1140        assert_eq!(
1141            graph
1142                .transition_from_method(StateId::Draft, "submit")
1143                .map(|transition| transition.id),
1144            Some(SUBMIT_FROM_DRAFT)
1145        );
1146        assert_eq!(
1147            graph.legal_targets(SUBMIT_FROM_DRAFT),
1148            Some(REVIEW_TARGETS.as_slice())
1149        );
1150        assert_eq!(graph.transitions_from(StateId::Draft).count(), 1);
1151        assert_eq!(graph.transitions_named("publish").count(), 1);
1152    }
1153
1154    #[test]
1155    fn runtime_transition_recording_joins_back_to_static_graph() {
1156        let event = Workflow::<DraftMarker>::try_record_transition_to::<Workflow<ReviewMarker>>(
1157            SUBMIT_FROM_DRAFT,
1158        )
1159        .expect("valid runtime transition");
1160
1161        assert_eq!(
1162            event,
1163            RecordedTransition::new(
1164                MachineDescriptor {
1165                    module_path: "workflow",
1166                    rust_type_path: "workflow::Machine",
1167                    role: MachineRole::Protocol,
1168                },
1169                StateId::Draft,
1170                SUBMIT_FROM_DRAFT,
1171                StateId::Review,
1172            )
1173        );
1174        assert_eq!(
1175            Workflow::<DraftMarker>::GRAPH
1176                .transition(event.transition)
1177                .map(|transition| (transition.from, transition.to)),
1178            Some((StateId::Draft, REVIEW_TARGETS.as_slice()))
1179        );
1180        assert_eq!(
1181            event.source_state_in(Workflow::<DraftMarker>::GRAPH),
1182            Some(&StateDescriptor {
1183                id: StateId::Draft,
1184                rust_name: "Draft",
1185                has_data: false,
1186            })
1187        );
1188    }
1189
1190    #[test]
1191    fn runtime_transition_recording_rejects_illegal_target_or_site() {
1192        assert!(Workflow::<DraftMarker>::try_record_transition(
1193            PUBLISH_FROM_REVIEW,
1194            StateId::Published,
1195        )
1196        .is_none());
1197        assert!(
1198            Workflow::<ReviewMarker>::try_record_transition_to::<Workflow<PublishedMarker>>(
1199                SUBMIT_FROM_DRAFT,
1200            )
1201            .is_none()
1202        );
1203    }
1204
1205    #[test]
1206    fn presentation_queries_join_with_runtime_transitions() {
1207        let event = Workflow::<DraftMarker>::try_record_transition_to::<Workflow<ReviewMarker>>(
1208            SUBMIT_FROM_DRAFT,
1209        )
1210        .expect("valid runtime transition");
1211
1212        assert_eq!(
1213            PRESENTATION.machine,
1214            Some(MachinePresentationDescriptor {
1215                label: Some("Workflow"),
1216                description: Some("Example presentation metadata for introspection."),
1217                metadata: MachineMeta {
1218                    phase: Phase::Intake,
1219                },
1220            })
1221        );
1222        assert_eq!(
1223            PRESENTATION.transition(event.transition),
1224            Some(&TransitionPresentation {
1225                id: SUBMIT_FROM_DRAFT,
1226                label: Some("Submit"),
1227                description: Some("Move work into review."),
1228                metadata: TransitionMeta {
1229                    phase: Phase::Review,
1230                    branch: false,
1231                },
1232            })
1233        );
1234        assert_eq!(
1235            PRESENTATION.state(event.chosen),
1236            Some(&StatePresentation {
1237                id: StateId::Review,
1238                label: Some("Review"),
1239                description: Some("Work is awaiting review."),
1240                metadata: StateMeta {
1241                    phase: Phase::Review,
1242                    term: "review",
1243                },
1244            })
1245        );
1246    }
1247
1248    fn linked_transitions() -> &'static [LinkedTransitionDescriptor] {
1249        static TRANSITIONS: [LinkedTransitionDescriptor; 1] = [LinkedTransitionDescriptor {
1250            method_name: "submit",
1251            label: Some("Submit"),
1252            description: None,
1253            docs: Some("Submits the draft for review."),
1254            from: "Draft",
1255            to: &["Review"],
1256        }];
1257        &TRANSITIONS
1258    }
1259
1260    #[test]
1261    fn linked_machine_graph_helpers_work() {
1262        static STATES: [LinkedStateDescriptor; 2] = [
1263            LinkedStateDescriptor {
1264                rust_name: "Draft",
1265                label: None,
1266                description: None,
1267                docs: Some("Initial draft state."),
1268                has_data: false,
1269                direct_construction_available: true,
1270            },
1271            LinkedStateDescriptor {
1272                rust_name: "Review",
1273                label: Some("Review"),
1274                description: None,
1275                docs: Some("Review state with payload."),
1276                has_data: true,
1277                direct_construction_available: true,
1278            },
1279        ];
1280        static LINKS: [StaticMachineLinkDescriptor; 1] = [StaticMachineLinkDescriptor {
1281            from_state: "Review",
1282            field_name: None,
1283            to_machine_path: &["task", "Machine"],
1284            to_state: "Running",
1285        }];
1286
1287        let linked = LinkedMachineGraph {
1288            machine: MachineDescriptor {
1289                module_path: "workflow",
1290                rust_type_path: "workflow::Machine",
1291                role: MachineRole::Protocol,
1292            },
1293            label: Some("Workflow"),
1294            description: None,
1295            docs: Some("Workflow machine docs."),
1296            states: &STATES,
1297            transitions: LinkedTransitionInventory::new(linked_transitions),
1298            static_links: &LINKS,
1299        };
1300
1301        assert_eq!(linked.state("Review"), Some(&STATES[1]));
1302        assert_eq!(linked.docs, Some("Workflow machine docs."));
1303        assert_eq!(
1304            linked.state("Draft").and_then(|state| state.docs),
1305            Some("Initial draft state.")
1306        );
1307        assert_eq!(
1308            linked.transition_from_method("Draft", "submit"),
1309            Some(&linked_transitions()[0])
1310        );
1311        assert_eq!(
1312            linked_transitions()[0].docs,
1313            Some("Submits the draft for review.")
1314        );
1315        assert_eq!(linked.transitions_from("Draft").count(), 1);
1316        assert_eq!(linked.static_links, &LINKS);
1317    }
1318
1319    #[test]
1320    fn linked_validator_entry_descriptor_exposes_declared_surface() {
1321        fn db_row_type_name() -> &'static str {
1322            "workflow::rows::DbRow"
1323        }
1324
1325        static ENTRY: LinkedValidatorEntryDescriptor = LinkedValidatorEntryDescriptor {
1326            machine: MachineDescriptor {
1327                module_path: "workflow",
1328                rust_type_path: "workflow::Machine",
1329                role: MachineRole::Protocol,
1330            },
1331            source_module_path: "workflow::rows",
1332            source_type_display: "DbRow",
1333            resolved_source_type_name: db_row_type_name,
1334            docs: Some("Rebuilds workflow machines from database rows."),
1335            target_states: &["Draft", "Review"],
1336        };
1337
1338        assert_eq!(ENTRY.machine.rust_type_path, "workflow::Machine");
1339        assert_eq!(ENTRY.source_module_path, "workflow::rows");
1340        assert_eq!(ENTRY.source_type_display, "DbRow");
1341        assert_eq!((ENTRY.resolved_source_type_name)(), "workflow::rows::DbRow");
1342        assert_eq!(
1343            ENTRY.docs,
1344            Some("Rebuilds workflow machines from database rows.")
1345        );
1346        assert_eq!(ENTRY.target_states, &["Draft", "Review"]);
1347    }
1348}