Skip to main content

statum_graph/codebase/
mod.rs

1use std::borrow::Cow;
2use std::collections::{BTreeMap, HashMap, HashSet};
3
4use serde::Serialize;
5use statum::{
6    LinkedMachineGraph, LinkedReferenceTypeDescriptor, LinkedRelationBasis,
7    LinkedRelationDescriptor, LinkedRelationKind, LinkedRelationSource, LinkedRelationTarget,
8    LinkedValidatorEntryDescriptor, LinkedViaRouteDescriptor, StaticMachineLinkDescriptor,
9};
10
11pub mod render;
12
13/// Stable export model for the linked compiled machine inventory.
14#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
15pub struct CodebaseDoc {
16    machines: Vec<CodebaseMachine>,
17    links: Vec<CodebaseLink>,
18    relations: Vec<CodebaseRelation>,
19    #[serde(skip)]
20    relation_groups: Vec<CodebaseMachineRelationGroup>,
21    #[serde(skip)]
22    relation_index: CodebaseRelationIndex,
23}
24
25impl CodebaseDoc {
26    /// Builds a combined codebase document from every linked machine visible to
27    /// the current build.
28    pub fn linked() -> Result<Self, CodebaseDocError> {
29        Self::try_from_linked_with_inventories(
30            statum::linked_machines(),
31            statum::linked_validator_entries(),
32            statum::linked_relations(),
33            statum::linked_via_routes(),
34            statum::linked_reference_types(),
35        )
36    }
37
38    /// Builds a combined codebase document from an explicit linked machine
39    /// inventory.
40    pub fn try_from_linked(
41        linked: &'static [LinkedMachineGraph],
42    ) -> Result<Self, CodebaseDocError> {
43        Self::try_from_linked_with_inventories(linked, &[], &[], &[], &[])
44    }
45
46    /// Builds a combined codebase document from explicit linked machine and
47    /// validator-entry inventories.
48    pub fn try_from_linked_with_validator_entries(
49        linked: &'static [LinkedMachineGraph],
50        validator_entries: &'static [LinkedValidatorEntryDescriptor],
51    ) -> Result<Self, CodebaseDocError> {
52        Self::try_from_linked_with_inventories(linked, validator_entries, &[], &[], &[])
53    }
54
55    fn try_from_linked_with_inventories(
56        linked: &'static [LinkedMachineGraph],
57        validator_entries: &'static [LinkedValidatorEntryDescriptor],
58        relations: &'static [LinkedRelationDescriptor],
59        via_routes: &'static [LinkedViaRouteDescriptor],
60        reference_types: &'static [LinkedReferenceTypeDescriptor],
61    ) -> Result<Self, CodebaseDocError> {
62        let mut linked = linked.to_vec();
63        linked.sort_by(|left, right| {
64            left.machine
65                .rust_type_path
66                .cmp(right.machine.rust_type_path)
67        });
68
69        let mut machines = Vec::with_capacity(linked.len());
70        let mut machine_paths = HashSet::with_capacity(linked.len());
71        let mut static_links = Vec::with_capacity(linked.len());
72
73        for (machine_index, machine) in linked.iter().enumerate() {
74            if !machine_paths.insert(machine.machine.rust_type_path) {
75                return Err(CodebaseDocError::DuplicateMachine {
76                    machine: machine.machine.rust_type_path,
77                });
78            }
79
80            let (built_machine, built_links) = build_machine(machine_index, *machine)?;
81            machines.push(built_machine);
82            static_links.push(built_links);
83        }
84
85        let resolved_validator_entries =
86            resolve_validator_entries(&mut machines, validator_entries)?;
87        let links = resolve_static_links(&machines, &static_links)?;
88        let relations = resolve_relations(&machines, relations, via_routes, reference_types)?;
89        let relation_groups = build_machine_relation_groups(&relations);
90        let relation_index = CodebaseRelationIndex::new(&machines, &relations);
91
92        debug_assert_eq!(
93            resolved_validator_entries,
94            total_validator_entries(&machines)
95        );
96
97        Ok(Self {
98            machines,
99            links,
100            relations,
101            relation_groups,
102            relation_index,
103        })
104    }
105
106    /// Exported machines in stable codebase order.
107    pub fn machines(&self) -> &[CodebaseMachine] {
108        &self.machines
109    }
110
111    /// Resolved static cross-machine links in stable order.
112    pub fn links(&self) -> &[CodebaseLink] {
113        &self.links
114    }
115
116    /// Resolved exact static relations in stable order.
117    pub fn relations(&self) -> &[CodebaseRelation] {
118        &self.relations
119    }
120
121    /// Returns one exported machine by its stable codebase index.
122    pub fn machine(&self, index: usize) -> Option<&CodebaseMachine> {
123        self.machines.get(index)
124    }
125
126    /// Returns one exported relation by its stable codebase index.
127    pub fn relation(&self, index: usize) -> Option<&CodebaseRelation> {
128        self.relations.get(index)
129    }
130
131    /// Groups exact relations by source and target machine for renderer and
132    /// inspector use.
133    pub fn machine_relation_groups(&self) -> &[CodebaseMachineRelationGroup] {
134        &self.relation_groups
135    }
136
137    /// Groups exact relations that are owned by composition machines.
138    pub fn composition_relation_groups(&self) -> Vec<CodebaseMachineRelationGroup> {
139        self.machine_relation_groups()
140            .iter()
141            .filter(|group| group.is_composition_owned() && group.from_machine != group.to_machine)
142            .cloned()
143            .collect()
144    }
145
146    /// Exact relations whose source belongs to `machine_index`.
147    pub fn outbound_relations_for_machine(
148        &self,
149        machine_index: usize,
150    ) -> impl Iterator<Item = &CodebaseRelation> + '_ {
151        self.relation_index
152            .outbound_machine(machine_index)
153            .iter()
154            .filter_map(|index| self.relation(*index))
155    }
156
157    /// Exact relations whose target belongs to `machine_index`.
158    pub fn inbound_relations_for_machine(
159        &self,
160        machine_index: usize,
161    ) -> impl Iterator<Item = &CodebaseRelation> + '_ {
162        self.relation_index
163            .inbound_machine(machine_index)
164            .iter()
165            .filter_map(|index| self.relation(*index))
166    }
167
168    /// Exact relations whose source belongs to one exported state.
169    pub fn outbound_relations_for_state(
170        &self,
171        machine_index: usize,
172        state_index: usize,
173    ) -> impl Iterator<Item = &CodebaseRelation> + '_ {
174        self.relation_index
175            .outbound_state(machine_index, state_index)
176            .iter()
177            .filter_map(|index| self.relation(*index))
178    }
179
180    /// Exact relations whose target belongs to one exported state.
181    pub fn inbound_relations_for_state(
182        &self,
183        machine_index: usize,
184        state_index: usize,
185    ) -> impl Iterator<Item = &CodebaseRelation> + '_ {
186        self.relation_index
187            .inbound_state(machine_index, state_index)
188            .iter()
189            .filter_map(|index| self.relation(*index))
190    }
191
192    /// Exact relations whose source belongs to one exported transition site.
193    pub fn outbound_relations_for_transition(
194        &self,
195        machine_index: usize,
196        transition_index: usize,
197    ) -> impl Iterator<Item = &CodebaseRelation> + '_ {
198        self.relation_index
199            .outbound_transition(machine_index, transition_index)
200            .iter()
201            .filter_map(|index| self.relation(*index))
202    }
203
204    /// Exact relations whose target belongs to one exported transition site.
205    ///
206    /// The current exact relation surface never targets transitions, so this
207    /// iterator is always empty. It exists so the inspector can use the same
208    /// navigation API shape for machines, states, and transitions.
209    pub fn inbound_relations_for_transition(
210        &self,
211        machine_index: usize,
212        transition_index: usize,
213    ) -> impl Iterator<Item = &CodebaseRelation> + '_ {
214        self.relation_index
215            .inbound_transition(machine_index, transition_index)
216            .iter()
217            .filter_map(|index| self.relation(*index))
218    }
219
220    /// Resolves one exact relation into typed source and target references for
221    /// downstream consumers such as the inspector TUI.
222    pub fn relation_detail(&self, index: usize) -> Option<CodebaseRelationDetail<'_>> {
223        let relation = self.relation(index)?;
224        let source_machine = self.machine(relation.source_machine())?;
225        let source_state = relation
226            .source_state()
227            .and_then(|state| source_machine.state(state));
228        let source_transition = relation
229            .source_transition()
230            .and_then(|transition| source_machine.transition(transition));
231        let target_machine = self.machine(relation.target_machine)?;
232        let target_state = target_machine.state(relation.target_state)?;
233        let attested_via_producers = relation
234            .attested_via
235            .as_ref()
236            .map(|route| {
237                route
238                    .producers
239                    .iter()
240                    .filter_map(|producer| {
241                        let machine = self.machine(producer.machine)?;
242                        let state = machine.state(producer.state)?;
243                        let transition = machine.transition(producer.transition)?;
244                        Some(CodebaseAttestedProducerDetail {
245                            producer,
246                            machine,
247                            state,
248                            transition,
249                        })
250                    })
251                    .collect::<Vec<_>>()
252            })
253            .unwrap_or_default();
254        let (attested_via_machine, attested_via_state, attested_via_transition) =
255            if attested_via_producers.len() == 1 {
256                let producer = &attested_via_producers[0];
257                (
258                    Some(producer.machine),
259                    Some(producer.state),
260                    Some(producer.transition),
261                )
262            } else {
263                (None, None, None)
264            };
265
266        Some(CodebaseRelationDetail {
267            relation,
268            source_machine,
269            source_state,
270            source_transition,
271            target_machine,
272            target_state,
273            attested_via_machine,
274            attested_via_state,
275            attested_via_transition,
276            attested_via_producers,
277        })
278    }
279}
280
281#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
282#[serde(rename_all = "snake_case")]
283pub enum CodebaseMachineRole {
284    Protocol,
285    Composition,
286}
287
288impl CodebaseMachineRole {
289    /// Human-facing machine-role label for inspector and renderer detail.
290    pub const fn display_label(self) -> &'static str {
291        match self {
292            Self::Protocol => "protocol",
293            Self::Composition => "composition",
294        }
295    }
296
297    /// Whether this machine participates as a composition machine.
298    pub const fn is_composition(self) -> bool {
299        matches!(self, Self::Composition)
300    }
301}
302
303impl From<statum::MachineRole> for CodebaseMachineRole {
304    fn from(value: statum::MachineRole) -> Self {
305        match value {
306            statum::MachineRole::Protocol => Self::Protocol,
307            statum::MachineRole::Composition => Self::Composition,
308        }
309    }
310}
311
312/// One machine family in the codebase export surface.
313#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
314pub struct CodebaseMachine {
315    /// Stable codebase-local machine index.
316    pub index: usize,
317    /// `module_path!()` for the source module that owns the machine.
318    pub module_path: &'static str,
319    /// Fully qualified Rust type path for the machine family.
320    pub rust_type_path: &'static str,
321    /// Whether this machine is a local protocol machine or a composition
322    /// machine.
323    pub role: CodebaseMachineRole,
324    /// Optional human-facing machine label.
325    pub label: Option<&'static str>,
326    /// Optional human-facing machine description.
327    pub description: Option<&'static str>,
328    /// Optional longer-form source documentation from outer rustdoc comments.
329    pub docs: Option<&'static str>,
330    /// States exported in source order.
331    pub states: Vec<CodebaseState>,
332    /// Transition sites exported in deterministic order.
333    pub transitions: Vec<CodebaseTransition>,
334    /// Declared validator-entry surfaces exported in deterministic order.
335    pub validator_entries: Vec<CodebaseValidatorEntry>,
336}
337
338impl CodebaseMachine {
339    /// Returns one exported state by its stable state index.
340    pub fn state(&self, index: usize) -> Option<&CodebaseState> {
341        self.states.get(index)
342    }
343
344    /// Returns one exported state by its Rust state name.
345    pub fn state_named(&self, rust_name: &str) -> Option<&CodebaseState> {
346        self.states
347            .iter()
348            .find(|state| state.rust_name == rust_name)
349    }
350
351    /// Returns one exported validator-entry surface by its stable machine-local
352    /// index.
353    pub fn validator_entry(&self, index: usize) -> Option<&CodebaseValidatorEntry> {
354        self.validator_entries.get(index)
355    }
356
357    /// Returns one exported transition site by its stable machine-local index.
358    pub fn transition(&self, index: usize) -> Option<&CodebaseTransition> {
359        self.transitions.get(index)
360    }
361
362    /// Stable renderer node id for one state in this machine.
363    pub fn node_id(&self, state_index: usize) -> String {
364        format!("m{}_s{}", self.index, state_index)
365    }
366
367    /// Stable renderer node id for one validator entry in this machine.
368    pub fn validator_node_id(&self, entry_index: usize) -> String {
369        format!("m{}_v{}", self.index, entry_index)
370    }
371
372    fn cluster_id(&self) -> String {
373        format!("m{}", self.index)
374    }
375
376    fn summary_node_id(&self) -> String {
377        format!("m{}_g", self.index)
378    }
379
380    fn display_label(&self) -> Cow<'static, str> {
381        match self.label {
382            Some(label) => Cow::Borrowed(label),
383            None => Cow::Borrowed(self.rust_type_path),
384        }
385    }
386
387    fn transition_site(&self, state: &str, method_name: &str) -> Option<&CodebaseTransition> {
388        self.transitions.iter().find(|transition| {
389            transition.method_name == method_name
390                && self
391                    .state(transition.from)
392                    .is_some_and(|source| source.rust_name == state)
393        })
394    }
395}
396
397/// One state in the codebase export surface.
398#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
399pub struct CodebaseState {
400    /// Stable machine-local state index.
401    pub index: usize,
402    /// Rust variant name emitted by Statum.
403    pub rust_name: &'static str,
404    /// Optional human-facing state label.
405    pub label: Option<&'static str>,
406    /// Optional human-facing state description.
407    pub description: Option<&'static str>,
408    /// Optional longer-form source documentation from outer rustdoc comments.
409    pub docs: Option<&'static str>,
410    /// Whether the state carries `state_data`.
411    pub has_data: bool,
412    /// Whether direct construction is available for this state.
413    pub direct_construction_available: bool,
414    /// Whether the state has no incoming transition in its machine.
415    pub is_graph_root: bool,
416}
417
418impl CodebaseState {
419    /// Human-facing state label used by text renderers.
420    pub fn display_label(&self) -> Cow<'static, str> {
421        match self.label {
422            Some(label) => Cow::Borrowed(label),
423            None if self.has_data => Cow::Owned(format!("{} (data)", self.rust_name)),
424            None => Cow::Borrowed(self.rust_name),
425        }
426    }
427}
428
429/// One declared validator-entry surface in the codebase export surface.
430#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
431pub struct CodebaseValidatorEntry {
432    /// Stable machine-local validator-entry index.
433    pub index: usize,
434    /// `module_path!()` for the module that owns the `#[validators]` impl.
435    pub source_module_path: &'static str,
436    /// Human-facing source syntax for the persisted impl self type as written.
437    pub source_type_display: &'static str,
438    /// Compiler-resolved source type identity for this validator impl.
439    #[doc(hidden)]
440    #[serde(skip_serializing)]
441    pub resolved_source_type_name: &'static str,
442    /// Optional longer-form source documentation from outer rustdoc comments.
443    pub docs: Option<&'static str>,
444    /// Stable target-state indices in machine state order.
445    pub target_states: Vec<usize>,
446}
447
448impl CodebaseValidatorEntry {
449    /// Human-facing node label used by text renderers.
450    pub fn display_label(&self) -> Cow<'static, str> {
451        Cow::Owned(format!("{}::into_machine()", self.source_type_display))
452    }
453}
454
455/// One transition site in the codebase export surface.
456#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
457pub struct CodebaseTransition {
458    /// Stable machine-local transition index.
459    pub index: usize,
460    /// Rust method name emitted by Statum.
461    pub method_name: &'static str,
462    /// Optional human-facing transition label.
463    pub label: Option<&'static str>,
464    /// Optional human-facing transition description.
465    pub description: Option<&'static str>,
466    /// Optional longer-form source documentation from outer rustdoc comments.
467    pub docs: Option<&'static str>,
468    /// Stable source-state index.
469    pub from: usize,
470    /// Stable legal target-state indices for this transition site.
471    pub to: Vec<usize>,
472}
473
474impl CodebaseTransition {
475    /// Human-facing edge label used by text renderers.
476    pub fn display_label(&self) -> &'static str {
477        self.label.unwrap_or(self.method_name)
478    }
479}
480
481/// Exact relation kinds exported by the codebase document.
482#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
483pub enum CodebaseRelationKind {
484    StatePayload,
485    MachineField,
486    TransitionParam,
487}
488
489impl CodebaseRelationKind {
490    /// Human-facing kind label for relation summaries and inspector details.
491    pub const fn display_label(self) -> &'static str {
492        match self {
493            Self::StatePayload => "payload",
494            Self::MachineField => "field",
495            Self::TransitionParam => "param",
496        }
497    }
498}
499
500/// Why one exact relation was inferred.
501#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
502pub enum CodebaseRelationBasis {
503    DirectTypeSyntax,
504    AttestedTypeSyntax,
505    DeclaredReferenceType,
506    ViaDeclaration,
507}
508
509impl CodebaseRelationBasis {
510    /// Human-facing basis label for relation summaries and inspector details.
511    pub const fn display_label(self) -> &'static str {
512        match self {
513            Self::DirectTypeSyntax => "direct type",
514            Self::AttestedTypeSyntax => "attested type",
515            Self::DeclaredReferenceType => "declared ref",
516            Self::ViaDeclaration => "via declaration",
517        }
518    }
519
520    fn summary_suffix(self) -> &'static str {
521        match self {
522            Self::DirectTypeSyntax => "",
523            Self::AttestedTypeSyntax => " [attested]",
524            Self::DeclaredReferenceType => " [ref]",
525            Self::ViaDeclaration => " [via]",
526        }
527    }
528}
529
530/// Higher-level exact semantics for one relation after machine-role
531/// classification.
532#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
533#[serde(rename_all = "snake_case")]
534pub enum CodebaseRelationSemantic {
535    Exact,
536    CompositionDirectChild,
537    CompositionDetachedHandoff,
538}
539
540impl CodebaseRelationSemantic {
541    /// Human-facing semantic label for relation detail and search.
542    pub const fn display_label(self) -> &'static str {
543        match self {
544            Self::Exact => "exact",
545            Self::CompositionDirectChild => "composition direct child",
546            Self::CompositionDetachedHandoff => "composition detached handoff",
547        }
548    }
549
550    /// Whether this exact relation comes from direct child-machine composition.
551    pub const fn is_composition_owned(self) -> bool {
552        matches!(
553            self,
554            Self::CompositionDirectChild | Self::CompositionDetachedHandoff
555        )
556    }
557}
558
559/// One exact relation source in the codebase export surface.
560#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
561pub enum CodebaseRelationSource {
562    StatePayload {
563        machine: usize,
564        state: usize,
565        field_name: Option<&'static str>,
566    },
567    MachineField {
568        machine: usize,
569        field_name: Option<&'static str>,
570        field_index: usize,
571    },
572    TransitionParam {
573        machine: usize,
574        transition: usize,
575        param_index: usize,
576        param_name: Option<&'static str>,
577    },
578}
579
580impl CodebaseRelationSource {
581    /// Stable source machine index for this exact relation source.
582    pub const fn machine(self) -> usize {
583        match self {
584            Self::StatePayload { machine, .. }
585            | Self::MachineField { machine, .. }
586            | Self::TransitionParam { machine, .. } => machine,
587        }
588    }
589
590    /// Stable source state index when the relation source is state-local.
591    pub const fn state(self) -> Option<usize> {
592        match self {
593            Self::StatePayload { state, .. } => Some(state),
594            Self::MachineField { .. } | Self::TransitionParam { .. } => None,
595        }
596    }
597
598    /// Stable source transition index when the relation source is one
599    /// transition parameter.
600    pub const fn transition(self) -> Option<usize> {
601        match self {
602            Self::TransitionParam { transition, .. } => Some(transition),
603            Self::StatePayload { .. } | Self::MachineField { .. } => None,
604        }
605    }
606}
607
608/// One resolved exact relation in the codebase export surface.
609#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
610pub struct CodebaseRelation {
611    /// Stable codebase-local relation index.
612    pub index: usize,
613    /// Exact relation kind.
614    pub kind: CodebaseRelationKind,
615    /// Why Statum considered this relation exact.
616    pub basis: CodebaseRelationBasis,
617    /// Higher-level exact semantics after machine-role classification.
618    pub semantic: CodebaseRelationSemantic,
619    /// Exact source location for this relation.
620    pub source: CodebaseRelationSource,
621    /// Resolved target machine index.
622    pub target_machine: usize,
623    /// Resolved target state index.
624    pub target_state: usize,
625    /// Declared nominal reference type when this relation came through
626    /// `#[machine_ref(...)]`.
627    pub declared_reference_type: Option<&'static str>,
628    /// Exact attested producer route when this relation came from
629    /// `#[via(...)]` or a canonical `statum::Attested<_, Route>` wrapper.
630    pub attested_via: Option<CodebaseAttestedRoute>,
631}
632
633impl CodebaseRelation {
634    /// Stable source machine index for this exact relation.
635    pub const fn source_machine(&self) -> usize {
636        self.source.machine()
637    }
638
639    /// Stable source state index when this relation is state-local.
640    pub const fn source_state(&self) -> Option<usize> {
641        self.source.state()
642    }
643
644    /// Stable source transition index when this relation comes from one
645    /// transition parameter.
646    pub const fn source_transition(&self) -> Option<usize> {
647        self.source.transition()
648    }
649
650    /// Stable target transition index.
651    ///
652    /// The current exact relation substrate does not target transitions, so
653    /// this always returns `None`. The method exists so downstream navigation
654    /// can keep one consistent source/target API shape.
655    pub const fn target_transition(&self) -> Option<usize> {
656        None
657    }
658
659    /// Whether this exact relation is owned by one composition machine through
660    /// direct child-machine composition.
661    pub const fn is_composition_owned(&self) -> bool {
662        self.semantic.is_composition_owned()
663    }
664}
665
666/// One exact producer transition reachable through an attested route.
667#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
668pub struct CodebaseAttestedProducer {
669    /// Stable producer machine index.
670    pub machine: usize,
671    /// Stable producer source-state index.
672    pub state: usize,
673    /// Stable producer transition index.
674    pub transition: usize,
675}
676
677/// One resolved producer route attached to one exact attested relation.
678#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
679pub struct CodebaseAttestedRoute {
680    /// Machine-module path that owns the attested route namespace.
681    pub via_module_path: &'static str,
682    /// Human-facing route name such as `Capture`.
683    pub route_name: &'static str,
684    /// Exact producer transitions that can attest this route and still satisfy
685    /// the resolved consumer target state.
686    pub producers: Vec<CodebaseAttestedProducer>,
687}
688
689/// One grouped machine-to-machine view derived from exact relations.
690#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
691pub struct CodebaseMachineRelationGroup {
692    /// Stable group index in `(from_machine, to_machine)` order.
693    pub index: usize,
694    /// Source machine index shared by the grouped relations.
695    pub from_machine: usize,
696    /// Target machine index shared by the grouped relations.
697    pub to_machine: usize,
698    /// Higher-level group semantics derived from the grouped exact relations.
699    pub semantic: CodebaseMachineRelationGroupSemantic,
700    /// Stable exact relation indices included in this group.
701    pub relation_indices: Vec<usize>,
702    /// Stable grouped counts by relation kind and basis.
703    pub counts: Vec<CodebaseRelationCount>,
704}
705
706/// Higher-level exact semantics for one grouped machine relation summary.
707#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
708#[serde(rename_all = "snake_case")]
709pub enum CodebaseMachineRelationGroupSemantic {
710    Exact,
711    CompositionDirectChild,
712    Mixed,
713}
714
715impl CodebaseMachineRelationGroupSemantic {
716    const fn from_relation_counts(
717        composition_owned_relations: usize,
718        total_relations: usize,
719    ) -> Self {
720        if composition_owned_relations == 0 {
721            Self::Exact
722        } else if composition_owned_relations == total_relations {
723            Self::CompositionDirectChild
724        } else {
725            Self::Mixed
726        }
727    }
728
729    /// Human-facing semantic label for grouped relation detail.
730    pub const fn display_label(self) -> &'static str {
731        match self {
732            Self::Exact => "exact",
733            Self::CompositionDirectChild => "composition-owned",
734            Self::Mixed => "composition + exact",
735        }
736    }
737
738    const fn summary_prefix(self) -> &'static str {
739        match self {
740            Self::Exact => "exact refs",
741            Self::CompositionDirectChild => "composition refs",
742            Self::Mixed => "composition + exact refs",
743        }
744    }
745
746    /// Whether this group includes any composition-owned exact relations.
747    pub const fn is_composition_owned(self) -> bool {
748        !matches!(self, Self::Exact)
749    }
750}
751
752impl CodebaseMachineRelationGroup {
753    /// Human-facing label used by machine summary edges in text renderers.
754    pub fn display_label(&self) -> String {
755        let counts = self
756            .counts
757            .iter()
758            .map(CodebaseRelationCount::display_label)
759            .collect::<Vec<_>>()
760            .join(", ");
761        format!("{}: {counts}", self.semantic.summary_prefix())
762    }
763
764    /// Whether this grouped summary includes composition-owned exact
765    /// relations.
766    pub const fn is_composition_owned(&self) -> bool {
767        self.semantic.is_composition_owned()
768    }
769}
770
771/// One grouped count inside a machine-to-machine exact relation summary.
772#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
773pub struct CodebaseRelationCount {
774    /// Exact relation kind for this grouped count.
775    pub kind: CodebaseRelationKind,
776    /// Exact relation basis for this grouped count.
777    pub basis: CodebaseRelationBasis,
778    /// Number of exact relations in this `(kind, basis)` class.
779    pub count: usize,
780}
781
782impl CodebaseRelationCount {
783    /// Human-facing grouped-count label used by machine summary edges.
784    pub fn display_label(&self) -> String {
785        let label = format!(
786            "{}{}",
787            self.kind.display_label(),
788            self.basis.summary_suffix()
789        );
790        if self.count == 1 {
791            label
792        } else {
793            format!("{label} x{}", self.count)
794        }
795    }
796}
797
798/// One typed resolved view of an exact relation for downstream consumers.
799#[derive(Clone, Copy, Debug)]
800pub struct CodebaseAttestedProducerDetail<'a> {
801    /// The exact producer record itself.
802    pub producer: &'a CodebaseAttestedProducer,
803    /// The resolved producer machine.
804    pub machine: &'a CodebaseMachine,
805    /// The resolved producer source state.
806    pub state: &'a CodebaseState,
807    /// The resolved producer transition.
808    pub transition: &'a CodebaseTransition,
809}
810
811#[derive(Debug)]
812pub struct CodebaseRelationDetail<'a> {
813    /// The exact relation record itself.
814    pub relation: &'a CodebaseRelation,
815    /// The resolved source machine.
816    pub source_machine: &'a CodebaseMachine,
817    /// The resolved source state when the relation is state-local.
818    pub source_state: Option<&'a CodebaseState>,
819    /// The resolved source transition when the relation comes from one
820    /// transition parameter.
821    pub source_transition: Option<&'a CodebaseTransition>,
822    /// The resolved target machine.
823    pub target_machine: &'a CodebaseMachine,
824    /// The resolved target state.
825    pub target_state: &'a CodebaseState,
826    /// The resolved producer machine when this exact relation came from one
827    /// attested route declaration.
828    pub attested_via_machine: Option<&'a CodebaseMachine>,
829    /// The resolved producer source state when this exact relation came from
830    /// one attested route declaration.
831    pub attested_via_state: Option<&'a CodebaseState>,
832    /// The resolved producer transition when this exact relation came from one
833    /// attested route declaration and exactly one producer matched.
834    pub attested_via_transition: Option<&'a CodebaseTransition>,
835    /// All resolved producer transitions when this exact relation came from an
836    /// attested route declaration.
837    pub attested_via_producers: Vec<CodebaseAttestedProducerDetail<'a>>,
838}
839
840#[derive(Clone, Debug, Default, Eq, PartialEq)]
841struct CodebaseRelationIndex {
842    outbound_machine: Vec<Vec<usize>>,
843    inbound_machine: Vec<Vec<usize>>,
844    outbound_state: Vec<Vec<Vec<usize>>>,
845    inbound_state: Vec<Vec<Vec<usize>>>,
846    outbound_transition: Vec<Vec<Vec<usize>>>,
847    inbound_transition: Vec<Vec<Vec<usize>>>,
848}
849
850impl CodebaseRelationIndex {
851    fn new(machines: &[CodebaseMachine], relations: &[CodebaseRelation]) -> Self {
852        let machine_count = machines.len();
853        let mut index = Self {
854            outbound_machine: vec![Vec::new(); machine_count],
855            inbound_machine: vec![Vec::new(); machine_count],
856            outbound_state: machines
857                .iter()
858                .map(|machine| vec![Vec::new(); machine.states.len()])
859                .collect(),
860            inbound_state: machines
861                .iter()
862                .map(|machine| vec![Vec::new(); machine.states.len()])
863                .collect(),
864            outbound_transition: machines
865                .iter()
866                .map(|machine| vec![Vec::new(); machine.transitions.len()])
867                .collect(),
868            inbound_transition: machines
869                .iter()
870                .map(|machine| vec![Vec::new(); machine.transitions.len()])
871                .collect(),
872        };
873
874        for (position, relation) in relations.iter().enumerate() {
875            debug_assert_eq!(relation.index, position);
876
877            index.outbound_machine[relation.source_machine()].push(position);
878            index.inbound_machine[relation.target_machine].push(position);
879            if let Some(state) = relation.source_state() {
880                index.outbound_state[relation.source_machine()][state].push(position);
881            }
882            index.inbound_state[relation.target_machine][relation.target_state].push(position);
883            if let Some(transition) = relation.source_transition() {
884                index.outbound_transition[relation.source_machine()][transition].push(position);
885            }
886            if let Some(transition) = relation.target_transition() {
887                index.inbound_transition[relation.target_machine][transition].push(position);
888            }
889        }
890
891        index
892    }
893
894    fn outbound_machine(&self, machine_index: usize) -> &[usize] {
895        self.outbound_machine
896            .get(machine_index)
897            .map(Vec::as_slice)
898            .unwrap_or(&[])
899    }
900
901    fn inbound_machine(&self, machine_index: usize) -> &[usize] {
902        self.inbound_machine
903            .get(machine_index)
904            .map(Vec::as_slice)
905            .unwrap_or(&[])
906    }
907
908    fn outbound_state(&self, machine_index: usize, state_index: usize) -> &[usize] {
909        self.outbound_state
910            .get(machine_index)
911            .and_then(|states| states.get(state_index))
912            .map(Vec::as_slice)
913            .unwrap_or(&[])
914    }
915
916    fn inbound_state(&self, machine_index: usize, state_index: usize) -> &[usize] {
917        self.inbound_state
918            .get(machine_index)
919            .and_then(|states| states.get(state_index))
920            .map(Vec::as_slice)
921            .unwrap_or(&[])
922    }
923
924    fn outbound_transition(&self, machine_index: usize, transition_index: usize) -> &[usize] {
925        self.outbound_transition
926            .get(machine_index)
927            .and_then(|transitions| transitions.get(transition_index))
928            .map(Vec::as_slice)
929            .unwrap_or(&[])
930    }
931
932    fn inbound_transition(&self, machine_index: usize, transition_index: usize) -> &[usize] {
933        self.inbound_transition
934            .get(machine_index)
935            .and_then(|transitions| transitions.get(transition_index))
936            .map(Vec::as_slice)
937            .unwrap_or(&[])
938    }
939}
940
941fn build_machine_relation_groups(
942    relations: &[CodebaseRelation],
943) -> Vec<CodebaseMachineRelationGroup> {
944    let mut groups = BTreeMap::<(usize, usize), Vec<usize>>::new();
945    for (position, relation) in relations.iter().enumerate() {
946        debug_assert_eq!(relation.index, position);
947        groups
948            .entry((relation.source_machine(), relation.target_machine))
949            .or_default()
950            .push(position);
951    }
952
953    groups
954        .into_iter()
955        .enumerate()
956        .map(|(index, ((from_machine, to_machine), relation_indices))| {
957            let mut counts =
958                BTreeMap::<(CodebaseRelationKind, CodebaseRelationBasis), usize>::new();
959            let mut composition_owned_relations = 0usize;
960            for relation_index in &relation_indices {
961                let relation = &relations[*relation_index];
962                *counts.entry((relation.kind, relation.basis)).or_default() += 1;
963                if relation.is_composition_owned() {
964                    composition_owned_relations += 1;
965                }
966            }
967
968            CodebaseMachineRelationGroup {
969                index,
970                from_machine,
971                to_machine,
972                semantic: CodebaseMachineRelationGroupSemantic::from_relation_counts(
973                    composition_owned_relations,
974                    relation_indices.len(),
975                ),
976                relation_indices,
977                counts: counts
978                    .into_iter()
979                    .map(|((kind, basis), count)| CodebaseRelationCount { kind, basis, count })
980                    .collect(),
981            }
982        })
983        .collect()
984}
985
986type ResolvedRelationTarget = (usize, usize, Option<&'static str>);
987
988/// One resolved static cross-machine payload link.
989#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
990pub struct CodebaseLink {
991    /// Stable codebase-local link index.
992    pub index: usize,
993    /// Source machine index.
994    pub from_machine: usize,
995    /// Source state index within `from_machine`.
996    pub from_state: usize,
997    /// Named field for named payloads; `None` for tuple payloads.
998    pub field_name: Option<&'static str>,
999    /// Target machine index.
1000    pub to_machine: usize,
1001    /// Target state index within `to_machine`.
1002    pub to_state: usize,
1003}
1004
1005impl CodebaseLink {
1006    /// Human-facing link label used by text renderers.
1007    pub fn display_label(&self) -> &'static str {
1008        self.field_name.unwrap_or("state_data")
1009    }
1010}
1011
1012/// Error returned when a linked machine inventory cannot be exported into a
1013/// stable codebase document.
1014#[derive(Clone, Debug, Eq, PartialEq)]
1015pub enum CodebaseDocError {
1016    /// One linked machine family appears more than once in the inventory.
1017    DuplicateMachine { machine: &'static str },
1018    /// One linked machine exports no states.
1019    EmptyStateList { machine: &'static str },
1020    /// One state name appears more than once in one machine.
1021    DuplicateStateName {
1022        machine: &'static str,
1023        state: &'static str,
1024    },
1025    /// One source state declares the same transition method name more than once.
1026    DuplicateTransitionSite {
1027        machine: &'static str,
1028        state: &'static str,
1029        transition: &'static str,
1030    },
1031    /// One transition source state is not present in the machine state list.
1032    MissingSourceState {
1033        machine: &'static str,
1034        transition: &'static str,
1035    },
1036    /// One transition target state is not present in the machine state list.
1037    MissingTargetState {
1038        machine: &'static str,
1039        transition: &'static str,
1040    },
1041    /// One transition site declares no legal target states.
1042    EmptyTargetSet {
1043        machine: &'static str,
1044        transition: &'static str,
1045    },
1046    /// One transition lists the same target state more than once.
1047    DuplicateTargetState {
1048        machine: &'static str,
1049        transition: &'static str,
1050        state: &'static str,
1051    },
1052    /// One validator-entry surface points at a machine missing from the linked
1053    /// machine inventory.
1054    MissingValidatorMachine {
1055        machine: &'static str,
1056        source_module_path: &'static str,
1057        source_type_display: &'static str,
1058    },
1059    /// One validator-entry surface points at a target state missing from the
1060    /// linked machine state list.
1061    MissingValidatorTargetState {
1062        machine: &'static str,
1063        source_module_path: &'static str,
1064        source_type_display: &'static str,
1065        state: &'static str,
1066    },
1067    /// One validator-entry surface declares no target states.
1068    EmptyValidatorTargetSet {
1069        machine: &'static str,
1070        source_module_path: &'static str,
1071        source_type_display: &'static str,
1072    },
1073    /// One validator-entry surface lists the same target state more than once.
1074    DuplicateValidatorTargetState {
1075        machine: &'static str,
1076        source_module_path: &'static str,
1077        source_type_display: &'static str,
1078        state: &'static str,
1079    },
1080    /// One validator-entry surface appears more than once for the same machine
1081    /// and impl site.
1082    DuplicateValidatorEntry {
1083        machine: &'static str,
1084        source_module_path: &'static str,
1085        source_type_display: &'static str,
1086    },
1087    /// One declared `#[machine_ref(...)]` type appears more than once for the
1088    /// same compiler-resolved nominal type identity.
1089    DuplicateReferenceTypeDeclaration {
1090        rust_type_path: &'static str,
1091        resolved_type_name: &'static str,
1092    },
1093    /// One declared `#[machine_ref(...)]` target machine is missing from the
1094    /// linked machine inventory.
1095    MissingReferenceTypeTargetMachine {
1096        rust_type_path: &'static str,
1097        target_machine_path: String,
1098        target_state: &'static str,
1099    },
1100    /// One declared `#[machine_ref(...)]` target state is missing from the
1101    /// linked machine inventory.
1102    MissingReferenceTypeTargetState {
1103        rust_type_path: &'static str,
1104        target_machine_path: String,
1105        target_state: &'static str,
1106    },
1107    /// One declared `#[machine_ref(...)]` target resolves to multiple linked
1108    /// machines.
1109    AmbiguousReferenceTypeTarget {
1110        rust_type_path: &'static str,
1111        target_machine_path: String,
1112        target_state: &'static str,
1113    },
1114    /// One linked exact relation cannot resolve its source machine.
1115    MissingRelationMachine {
1116        machine_path: String,
1117        relation: String,
1118    },
1119    /// One linked exact relation matches multiple source machines.
1120    AmbiguousRelationMachine {
1121        machine_path: String,
1122        relation: String,
1123    },
1124    /// One linked exact relation points at a source state missing from the
1125    /// resolved source machine.
1126    MissingRelationSourceState {
1127        machine: &'static str,
1128        state: &'static str,
1129        relation: String,
1130    },
1131    /// One linked exact relation points at a transition site missing from the
1132    /// resolved source machine.
1133    MissingRelationTransition {
1134        machine: &'static str,
1135        state: &'static str,
1136        transition: &'static str,
1137    },
1138    /// One linked exact relation matches multiple target machines.
1139    AmbiguousRelationTarget {
1140        relation: String,
1141        target_machine_path: String,
1142        target_state: &'static str,
1143    },
1144    /// One attested producer route appears more than once in the linked
1145    /// inventory.
1146    DuplicateViaRoute {
1147        via_module_path: &'static str,
1148        route_name: &'static str,
1149    },
1150    /// One linked attested producer route reuses the same route identity with a
1151    /// different target state.
1152    ConflictingViaRouteTarget {
1153        via_module_path: &'static str,
1154        route_name: &'static str,
1155        expected_target_state: &'static str,
1156        conflicting_target_state: &'static str,
1157    },
1158    /// One `#[via(...)]` exact relation points at a producer route missing from
1159    /// the linked inventory.
1160    MissingRelationViaRoute {
1161        relation: String,
1162        via_module_path: &'static str,
1163        route_name: &'static str,
1164    },
1165    /// One `#[via(...)]` relation points at a producer source state missing
1166    /// from the resolved machine graph.
1167    MissingRelationViaSourceState {
1168        machine: &'static str,
1169        state: &'static str,
1170        relation: String,
1171    },
1172    /// One `#[via(...)]` relation points at a producer transition missing from
1173    /// the resolved machine graph.
1174    MissingRelationViaTransition {
1175        machine: &'static str,
1176        state: &'static str,
1177        transition: &'static str,
1178        relation: String,
1179    },
1180    /// One attested producer route points at a target state missing from the
1181    /// resolved machine graph.
1182    MissingRelationViaTargetState {
1183        machine: &'static str,
1184        state: &'static str,
1185        relation: String,
1186    },
1187    /// One `#[via(...)]` relation declared an inner target state that does not
1188    /// match the attested producer route it references.
1189    MismatchedRelationViaTarget {
1190        relation: String,
1191        via_module_path: &'static str,
1192        route_name: &'static str,
1193        declared_target_state: &'static str,
1194        producer_target_state: &'static str,
1195    },
1196    /// One static payload link points at a source state missing from the
1197    /// machine state list.
1198    MissingStaticLinkSourceState {
1199        machine: &'static str,
1200        state: &'static str,
1201    },
1202    /// One static payload link matches multiple linked machine families.
1203    AmbiguousStaticLink {
1204        machine: &'static str,
1205        state: &'static str,
1206        field_name: Option<&'static str>,
1207        target_machine_path: String,
1208        target_state: &'static str,
1209    },
1210}
1211
1212impl core::fmt::Display for CodebaseDocError {
1213    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1214        match self {
1215            Self::DuplicateMachine { machine } => write!(
1216                formatter,
1217                "linked codebase export cannot merge duplicate machine path `{machine}`. \
1218This usually means multiple linked crates define machines at the same crate-local module path. \
1219Whole-workspace export treats that path as the machine identity, so the merge would be ambiguous. \
1220Fix: rerun with `--package` to export one crate, or move one machine to a distinct module path."
1221            ),
1222            Self::EmptyStateList { machine } => {
1223                write!(formatter, "linked machine `{machine}` contains no states")
1224            }
1225            Self::DuplicateStateName { machine, state } => write!(
1226                formatter,
1227                "linked machine `{machine}` contains duplicate state `{state}`"
1228            ),
1229            Self::DuplicateTransitionSite {
1230                machine,
1231                state,
1232                transition,
1233            } => write!(
1234                formatter,
1235                "linked machine `{machine}` contains duplicate transition site `{state}::{transition}`"
1236            ),
1237            Self::MissingSourceState { machine, transition } => write!(
1238                formatter,
1239                "linked machine `{machine}` contains transition `{transition}` whose source state is missing from the state list"
1240            ),
1241            Self::MissingTargetState { machine, transition } => write!(
1242                formatter,
1243                "linked machine `{machine}` contains transition `{transition}` whose target state is missing from the state list"
1244            ),
1245            Self::EmptyTargetSet { machine, transition } => write!(
1246                formatter,
1247                "linked machine `{machine}` contains transition `{transition}` with no target states"
1248            ),
1249            Self::DuplicateTargetState {
1250                machine,
1251                transition,
1252                state,
1253            } => write!(
1254                formatter,
1255                "linked machine `{machine}` contains transition `{transition}` with duplicate target state `{state}`"
1256            ),
1257            Self::MissingValidatorMachine {
1258                machine,
1259                source_module_path,
1260                source_type_display,
1261            } => write!(
1262                formatter,
1263                "linked validator entry `{source_type_display}::into_machine()` from module `{source_module_path}` points at missing machine `{machine}`"
1264            ),
1265            Self::MissingValidatorTargetState {
1266                machine,
1267                source_module_path,
1268                source_type_display,
1269                state,
1270            } => write!(
1271                formatter,
1272                "linked validator entry `{source_type_display}::into_machine()` from module `{source_module_path}` points at missing state `{machine}::{state}`"
1273            ),
1274            Self::EmptyValidatorTargetSet {
1275                machine,
1276                source_module_path,
1277                source_type_display,
1278            } => write!(
1279                formatter,
1280                "linked validator entry `{source_type_display}::into_machine()` from module `{source_module_path}` for machine `{machine}` contains no target states"
1281            ),
1282            Self::DuplicateValidatorTargetState {
1283                machine,
1284                source_module_path,
1285                source_type_display,
1286                state,
1287            } => write!(
1288                formatter,
1289                "linked validator entry `{source_type_display}::into_machine()` from module `{source_module_path}` for machine `{machine}` contains duplicate target state `{state}`"
1290            ),
1291            Self::DuplicateValidatorEntry {
1292                machine,
1293                source_module_path,
1294                source_type_display,
1295            } => write!(
1296                formatter,
1297                "linked validator entry `{source_type_display}::into_machine()` from module `{source_module_path}` appears more than once for machine `{machine}`"
1298            ),
1299            Self::DuplicateReferenceTypeDeclaration {
1300                rust_type_path,
1301                resolved_type_name,
1302            } => write!(
1303                formatter,
1304                "linked machine reference type `{rust_type_path}` appears more than once for resolved type identity `{resolved_type_name}`"
1305            ),
1306            Self::MissingReferenceTypeTargetMachine {
1307                rust_type_path,
1308                target_machine_path,
1309                target_state,
1310            } => write!(
1311                formatter,
1312                "linked machine reference type `{rust_type_path}` points at missing target `{target_machine_path}<{target_state}>`"
1313            ),
1314            Self::MissingReferenceTypeTargetState {
1315                rust_type_path,
1316                target_machine_path,
1317                target_state,
1318            } => write!(
1319                formatter,
1320                "linked machine reference type `{rust_type_path}` points at missing target state `{target_machine_path}::{target_state}`"
1321            ),
1322            Self::AmbiguousReferenceTypeTarget {
1323                rust_type_path,
1324                target_machine_path,
1325                target_state,
1326            } => write!(
1327                formatter,
1328                "linked machine reference type `{rust_type_path}` ambiguously matches target `{target_machine_path}<{target_state}>`"
1329            ),
1330            Self::MissingRelationMachine {
1331                machine_path,
1332                relation,
1333            } => write!(
1334                formatter,
1335                "linked exact relation `{relation}` points at missing source machine `{machine_path}`"
1336            ),
1337            Self::AmbiguousRelationMachine {
1338                machine_path,
1339                relation,
1340            } => write!(
1341                formatter,
1342                "linked exact relation `{relation}` ambiguously matches source machine `{machine_path}`"
1343            ),
1344            Self::MissingRelationSourceState {
1345                machine,
1346                state,
1347                relation,
1348            } => write!(
1349                formatter,
1350                "linked exact relation `{relation}` points at missing source state `{machine}::{state}`"
1351            ),
1352            Self::MissingRelationTransition {
1353                machine,
1354                state,
1355                transition,
1356            } => write!(
1357                formatter,
1358                "linked exact relation for transition `{machine}::{state}::{transition}` points at a transition site missing from the exported machine graph"
1359            ),
1360            Self::AmbiguousRelationTarget {
1361                relation,
1362                target_machine_path,
1363                target_state,
1364            } => write!(
1365                formatter,
1366                "linked exact relation `{relation}` ambiguously matches target `{target_machine_path}<{target_state}>`"
1367            ),
1368            Self::DuplicateViaRoute {
1369                via_module_path,
1370                route_name,
1371            } => write!(
1372                formatter,
1373                "linked attested route `{via_module_path}::{route_name}` appears more than once in the producer inventory"
1374            ),
1375            Self::ConflictingViaRouteTarget {
1376                via_module_path,
1377                route_name,
1378                expected_target_state,
1379                conflicting_target_state,
1380            } => write!(
1381                formatter,
1382                "linked attested route `{via_module_path}::{route_name}` conflicts on target state: expected `{expected_target_state}`, found `{conflicting_target_state}`"
1383            ),
1384            Self::MissingRelationViaRoute {
1385                relation,
1386                via_module_path,
1387                route_name,
1388            } => write!(
1389                formatter,
1390                "linked exact relation `{relation}` points at missing attested route `{via_module_path}::{route_name}`"
1391            ),
1392            Self::MissingRelationViaSourceState {
1393                machine,
1394                state,
1395                relation,
1396            } => write!(
1397                formatter,
1398                "linked exact relation `{relation}` points at missing attested source state `{machine}::{state}`"
1399            ),
1400            Self::MissingRelationViaTransition {
1401                machine,
1402                state,
1403                transition,
1404                relation,
1405            } => write!(
1406                formatter,
1407                "linked exact relation `{relation}` points at missing attested producer transition `{machine}::{state}::{transition}`"
1408            ),
1409            Self::MissingRelationViaTargetState {
1410                machine,
1411                state,
1412                relation,
1413            } => write!(
1414                formatter,
1415                "linked exact relation `{relation}` points at missing attested target state `{machine}::{state}`"
1416            ),
1417            Self::MismatchedRelationViaTarget {
1418                relation,
1419                via_module_path,
1420                route_name,
1421                declared_target_state,
1422                producer_target_state,
1423            } => write!(
1424                formatter,
1425                "linked exact relation `{relation}` declares target state `{declared_target_state}`, but attested route `{via_module_path}::{route_name}` produces `{producer_target_state}`"
1426            ),
1427            Self::MissingStaticLinkSourceState { machine, state } => write!(
1428                formatter,
1429                "linked machine `{machine}` contains a static payload link from missing source state `{state}`"
1430            ),
1431            Self::AmbiguousStaticLink {
1432                machine,
1433                state,
1434                field_name,
1435                target_machine_path,
1436                target_state,
1437            } => match field_name {
1438                Some(field_name) => write!(
1439                    formatter,
1440                    "linked machine `{machine}` state `{state}` field `{field_name}` ambiguously matches static target `{target_machine_path}<{target_state}>`"
1441                ),
1442                None => write!(
1443                    formatter,
1444                    "linked machine `{machine}` state `{state}` ambiguously matches static target `{target_machine_path}<{target_state}>`"
1445                ),
1446            },
1447        }
1448    }
1449}
1450
1451impl std::error::Error for CodebaseDocError {}
1452
1453fn build_machine(
1454    machine_index: usize,
1455    linked: LinkedMachineGraph,
1456) -> Result<(CodebaseMachine, Vec<&'static StaticMachineLinkDescriptor>), CodebaseDocError> {
1457    if linked.states.is_empty() {
1458        return Err(CodebaseDocError::EmptyStateList {
1459            machine: linked.machine.rust_type_path,
1460        });
1461    }
1462
1463    let mut states = Vec::with_capacity(linked.states.len());
1464    let mut state_positions = HashMap::with_capacity(linked.states.len());
1465    for (index, state) in linked.states.iter().enumerate() {
1466        if state_positions.insert(state.rust_name, index).is_some() {
1467            return Err(CodebaseDocError::DuplicateStateName {
1468                machine: linked.machine.rust_type_path,
1469                state: state.rust_name,
1470            });
1471        }
1472
1473        states.push(CodebaseState {
1474            index,
1475            rust_name: state.rust_name,
1476            label: state.label,
1477            description: state.description,
1478            docs: state.docs,
1479            has_data: state.has_data,
1480            direct_construction_available: state.direct_construction_available,
1481            is_graph_root: true,
1482        });
1483    }
1484
1485    let mut transitions = linked.transitions.as_slice().to_vec();
1486    transitions.sort_by(|left, right| compare_transitions(&state_positions, left, right));
1487
1488    let mut exported_transitions = Vec::with_capacity(transitions.len());
1489    let mut incoming = HashSet::new();
1490    let mut seen_sites = HashSet::with_capacity(transitions.len());
1491
1492    for (index, transition) in transitions.iter().enumerate() {
1493        let Some(&from) = state_positions.get(transition.from) else {
1494            return Err(CodebaseDocError::MissingSourceState {
1495                machine: linked.machine.rust_type_path,
1496                transition: transition.method_name,
1497            });
1498        };
1499        if !seen_sites.insert((transition.from, transition.method_name)) {
1500            return Err(CodebaseDocError::DuplicateTransitionSite {
1501                machine: linked.machine.rust_type_path,
1502                state: transition.from,
1503                transition: transition.method_name,
1504            });
1505        }
1506        if transition.to.is_empty() {
1507            return Err(CodebaseDocError::EmptyTargetSet {
1508                machine: linked.machine.rust_type_path,
1509                transition: transition.method_name,
1510            });
1511        }
1512
1513        let mut to = Vec::with_capacity(transition.to.len());
1514        let mut seen_targets = HashSet::with_capacity(transition.to.len());
1515        for target in transition.to {
1516            let Some(&target_index) = state_positions.get(target) else {
1517                return Err(CodebaseDocError::MissingTargetState {
1518                    machine: linked.machine.rust_type_path,
1519                    transition: transition.method_name,
1520                });
1521            };
1522            if !seen_targets.insert(*target) {
1523                return Err(CodebaseDocError::DuplicateTargetState {
1524                    machine: linked.machine.rust_type_path,
1525                    transition: transition.method_name,
1526                    state: target,
1527                });
1528            }
1529            incoming.insert(target_index);
1530            to.push(target_index);
1531        }
1532
1533        exported_transitions.push(CodebaseTransition {
1534            index,
1535            method_name: transition.method_name,
1536            label: transition.label,
1537            description: transition.description,
1538            docs: transition.docs,
1539            from,
1540            to,
1541        });
1542    }
1543
1544    for state in &mut states {
1545        state.is_graph_root = !incoming.contains(&state.index);
1546    }
1547
1548    Ok((
1549        CodebaseMachine {
1550            index: machine_index,
1551            module_path: linked.machine.module_path,
1552            rust_type_path: linked.machine.rust_type_path,
1553            role: linked.machine.role.into(),
1554            label: linked.label,
1555            description: linked.description,
1556            docs: linked.docs,
1557            states,
1558            transitions: exported_transitions,
1559            validator_entries: Vec::new(),
1560        },
1561        linked.static_links.iter().collect(),
1562    ))
1563}
1564
1565fn compare_transitions(
1566    state_positions: &HashMap<&'static str, usize>,
1567    left: &statum::LinkedTransitionDescriptor,
1568    right: &statum::LinkedTransitionDescriptor,
1569) -> core::cmp::Ordering {
1570    transition_sort_key(state_positions, left).cmp(&transition_sort_key(state_positions, right))
1571}
1572
1573fn transition_sort_key(
1574    state_positions: &HashMap<&'static str, usize>,
1575    transition: &statum::LinkedTransitionDescriptor,
1576) -> (Option<usize>, &'static str, &'static [&'static str]) {
1577    (
1578        state_positions.get(transition.from).copied(),
1579        transition.method_name,
1580        transition.to,
1581    )
1582}
1583
1584fn resolve_static_links(
1585    machines: &[CodebaseMachine],
1586    static_links: &[Vec<&'static StaticMachineLinkDescriptor>],
1587) -> Result<Vec<CodebaseLink>, CodebaseDocError> {
1588    let mut links = Vec::new();
1589
1590    for (machine_index, machine_links) in static_links.iter().enumerate() {
1591        let machine = &machines[machine_index];
1592        for link in machine_links {
1593            let Some(from_state) = machine
1594                .state_named(link.from_state)
1595                .map(|state| state.index)
1596            else {
1597                return Err(CodebaseDocError::MissingStaticLinkSourceState {
1598                    machine: machine.rust_type_path,
1599                    state: link.from_state,
1600                });
1601            };
1602
1603            let candidates = machines
1604                .iter()
1605                .filter_map(|candidate| {
1606                    if !path_suffix_matches(candidate.rust_type_path, link.to_machine_path) {
1607                        return None;
1608                    }
1609
1610                    candidate
1611                        .state_named(link.to_state)
1612                        .map(|target_state| (candidate.index, target_state.index))
1613                })
1614                .collect::<Vec<_>>();
1615
1616            match candidates.as_slice() {
1617                [] => {}
1618                [(to_machine, to_state)] => links.push(CodebaseLink {
1619                    index: links.len(),
1620                    from_machine: machine_index,
1621                    from_state,
1622                    field_name: link.field_name,
1623                    to_machine: *to_machine,
1624                    to_state: *to_state,
1625                }),
1626                _ => {
1627                    return Err(CodebaseDocError::AmbiguousStaticLink {
1628                        machine: machine.rust_type_path,
1629                        state: link.from_state,
1630                        field_name: link.field_name,
1631                        target_machine_path: link.to_machine_path.join("::"),
1632                        target_state: link.to_state,
1633                    });
1634                }
1635            }
1636        }
1637    }
1638
1639    Ok(links)
1640}
1641
1642#[derive(Clone, Copy)]
1643struct ResolvedReferenceTypeTarget {
1644    rust_type_path: &'static str,
1645    target_machine: usize,
1646    target_state: usize,
1647}
1648
1649#[derive(Clone, Copy)]
1650struct ResolvedViaProducer {
1651    machine: usize,
1652    state: usize,
1653    transition: usize,
1654    target_state_name: &'static str,
1655}
1656
1657#[derive(Clone)]
1658struct ResolvedViaRoute {
1659    via_module_path: &'static str,
1660    route_name: &'static str,
1661    target_machine: usize,
1662    target_state: usize,
1663    target_state_name: &'static str,
1664    producers: Vec<ResolvedViaProducer>,
1665}
1666
1667fn resolve_relations(
1668    machines: &[CodebaseMachine],
1669    relations: &'static [LinkedRelationDescriptor],
1670    via_routes: &'static [LinkedViaRouteDescriptor],
1671    reference_types: &'static [LinkedReferenceTypeDescriptor],
1672) -> Result<Vec<CodebaseRelation>, CodebaseDocError> {
1673    let reference_types = resolve_reference_type_targets(machines, reference_types)?;
1674    let via_routes = resolve_via_routes(machines, via_routes)?;
1675    let mut relations = relations.to_vec();
1676    relations.sort_by(compare_relations);
1677
1678    let exact_machine_positions = machines
1679        .iter()
1680        .map(|machine| (machine.rust_type_path, machine.index))
1681        .collect::<HashMap<_, _>>();
1682    let mut exported = Vec::new();
1683
1684    for relation in relations {
1685        let source_machine = resolve_relation_source_machine(
1686            machines,
1687            &exact_machine_positions,
1688            relation.machine.rust_type_path,
1689            &relation_summary(&relation),
1690        )?;
1691        let machine = &machines[source_machine];
1692        let source = match relation.source {
1693            LinkedRelationSource::StatePayload { state, field_name } => {
1694                let Some(state_index) = machine.state_named(state).map(|state| state.index) else {
1695                    return Err(CodebaseDocError::MissingRelationSourceState {
1696                        machine: machine.rust_type_path,
1697                        state,
1698                        relation: relation_summary(&relation),
1699                    });
1700                };
1701                CodebaseRelationSource::StatePayload {
1702                    machine: machine.index,
1703                    state: state_index,
1704                    field_name,
1705                }
1706            }
1707            LinkedRelationSource::MachineField {
1708                field_name,
1709                field_index,
1710            } => CodebaseRelationSource::MachineField {
1711                machine: machine.index,
1712                field_name,
1713                field_index,
1714            },
1715            LinkedRelationSource::TransitionParam {
1716                state,
1717                transition,
1718                param_index,
1719                param_name,
1720            } => {
1721                let Some(transition_index) = machine
1722                    .transition_site(state, transition)
1723                    .map(|transition| transition.index)
1724                else {
1725                    return Err(CodebaseDocError::MissingRelationTransition {
1726                        machine: machine.rust_type_path,
1727                        state,
1728                        transition,
1729                    });
1730                };
1731                CodebaseRelationSource::TransitionParam {
1732                    machine: machine.index,
1733                    transition: transition_index,
1734                    param_index,
1735                    param_name,
1736                }
1737            }
1738        };
1739
1740        let (resolved_target, attested_via) = match relation.target {
1741            LinkedRelationTarget::DirectMachine {
1742                machine_path,
1743                resolved_machine_type_name,
1744                state,
1745            } => (
1746                resolve_optional_target_machine(
1747                    machines,
1748                    resolved_machine_type_name(),
1749                    machine_path,
1750                    state,
1751                    &relation_summary(&relation),
1752                )?,
1753                None,
1754            ),
1755            LinkedRelationTarget::DeclaredReferenceType { resolved_type_name } => (
1756                reference_types
1757                    .get(resolved_type_name())
1758                    .copied()
1759                    .map(|target| {
1760                        (
1761                            target.target_machine,
1762                            target.target_state,
1763                            Some(target.rust_type_path),
1764                        )
1765                    }),
1766                None,
1767            ),
1768            LinkedRelationTarget::AttestedProducerRoute {
1769                via_module_path,
1770                route_name,
1771                resolved_route_type_name,
1772                route_id: _,
1773            } => {
1774                let relation_label = relation_summary(&relation);
1775                let resolved_route =
1776                    via_routes.get(resolved_route_type_name()).ok_or_else(|| {
1777                        CodebaseDocError::MissingRelationViaRoute {
1778                            relation: relation_label.clone(),
1779                            via_module_path,
1780                            route_name,
1781                        }
1782                    })?;
1783                (
1784                    Some((
1785                        resolved_route.target_machine,
1786                        resolved_route.target_state,
1787                        None,
1788                    )),
1789                    Some(CodebaseAttestedRoute {
1790                        via_module_path: resolved_route.via_module_path,
1791                        route_name: resolved_route.route_name,
1792                        producers: resolved_route
1793                            .producers
1794                            .iter()
1795                            .map(|producer| CodebaseAttestedProducer {
1796                                machine: producer.machine,
1797                                state: producer.state,
1798                                transition: producer.transition,
1799                            })
1800                            .collect(),
1801                    }),
1802                )
1803            }
1804            LinkedRelationTarget::AttestedRoute {
1805                via_module_path,
1806                route_name,
1807                resolved_route_type_name,
1808                route_id: _,
1809                machine_path,
1810                resolved_machine_type_name,
1811                state,
1812            } => {
1813                let relation_label = relation_summary(&relation);
1814                let resolved_target = resolve_optional_target_machine(
1815                    machines,
1816                    resolved_machine_type_name(),
1817                    machine_path,
1818                    state,
1819                    &relation_label,
1820                )?;
1821                let resolved_route =
1822                    via_routes.get(resolved_route_type_name()).ok_or_else(|| {
1823                        CodebaseDocError::MissingRelationViaRoute {
1824                            relation: relation_label.clone(),
1825                            via_module_path,
1826                            route_name,
1827                        }
1828                    })?;
1829                let matched_producers = resolved_route
1830                    .producers
1831                    .iter()
1832                    .filter(|producer| producer.target_state_name == state)
1833                    .copied()
1834                    .collect::<Vec<_>>();
1835                if matched_producers.is_empty() {
1836                    return Err(CodebaseDocError::MismatchedRelationViaTarget {
1837                        relation: relation_label,
1838                        via_module_path,
1839                        route_name,
1840                        declared_target_state: state,
1841                        producer_target_state: resolved_route
1842                            .producers
1843                            .first()
1844                            .map(|producer| producer.target_state_name)
1845                            .unwrap_or("<missing-producer-target>"),
1846                    });
1847                }
1848                (
1849                    resolved_target,
1850                    Some(CodebaseAttestedRoute {
1851                        via_module_path,
1852                        route_name,
1853                        producers: matched_producers
1854                            .into_iter()
1855                            .map(|producer| CodebaseAttestedProducer {
1856                                machine: producer.machine,
1857                                state: producer.state,
1858                                transition: producer.transition,
1859                            })
1860                            .collect(),
1861                    }),
1862                )
1863            }
1864        };
1865        let Some((target_machine, target_state, declared_reference_type)) = resolved_target else {
1866            continue;
1867        };
1868        let basis = map_relation_basis(relation.basis);
1869        let semantic = classify_relation_semantic(
1870            machine.role,
1871            basis,
1872            machine.index,
1873            target_machine,
1874            relation.target,
1875        );
1876
1877        exported.push(CodebaseRelation {
1878            index: exported.len(),
1879            kind: map_relation_kind(relation.kind),
1880            basis,
1881            semantic,
1882            source,
1883            target_machine,
1884            target_state,
1885            declared_reference_type,
1886            attested_via,
1887        });
1888    }
1889
1890    Ok(exported)
1891}
1892
1893fn resolve_via_routes(
1894    machines: &[CodebaseMachine],
1895    via_routes: &'static [LinkedViaRouteDescriptor],
1896) -> Result<HashMap<&'static str, ResolvedViaRoute>, CodebaseDocError> {
1897    let exact_machine_positions = machines
1898        .iter()
1899        .map(|machine| (machine.rust_type_path, machine.index))
1900        .collect::<HashMap<_, _>>();
1901    let mut resolved = HashMap::with_capacity(via_routes.len());
1902
1903    for route in via_routes {
1904        let machine_index = resolve_relation_source_machine(
1905            machines,
1906            &exact_machine_positions,
1907            route.machine.rust_type_path,
1908            route.route_name,
1909        )?;
1910        let machine = &machines[machine_index];
1911        let state = machine
1912            .state_named(route.source_state)
1913            .map(|state| state.index)
1914            .ok_or_else(|| CodebaseDocError::MissingRelationViaSourceState {
1915                machine: machine.rust_type_path,
1916                state: route.source_state,
1917                relation: format!("{}::{}", route.via_module_path, route.route_name),
1918            })?;
1919        let transition = machine
1920            .transition_site(route.source_state, route.transition)
1921            .map(|transition| transition.index)
1922            .ok_or_else(|| CodebaseDocError::MissingRelationViaTransition {
1923                machine: machine.rust_type_path,
1924                state: route.source_state,
1925                transition: route.transition,
1926                relation: format!("{}::{}", route.via_module_path, route.route_name),
1927            })?;
1928        let target_state = machine
1929            .state_named(route.target_state)
1930            .map(|state| state.index)
1931            .ok_or_else(|| CodebaseDocError::MissingRelationViaTargetState {
1932                machine: machine.rust_type_path,
1933                state: route.target_state,
1934                relation: format!("{}::{}", route.via_module_path, route.route_name),
1935            })?;
1936        let producer = ResolvedViaProducer {
1937            machine: machine.index,
1938            state,
1939            transition,
1940            target_state_name: route.target_state,
1941        };
1942        let resolved_route_type_name = (route.resolved_route_type_name)();
1943        match resolved.entry(resolved_route_type_name) {
1944            std::collections::hash_map::Entry::Vacant(entry) => {
1945                entry.insert(ResolvedViaRoute {
1946                    via_module_path: route.via_module_path,
1947                    route_name: route.route_name,
1948                    target_machine: machine.index,
1949                    target_state,
1950                    target_state_name: route.target_state,
1951                    producers: vec![producer],
1952                });
1953            }
1954            std::collections::hash_map::Entry::Occupied(mut entry) => {
1955                let resolved_route = entry.get_mut();
1956                if resolved_route.via_module_path != route.via_module_path
1957                    || resolved_route.route_name != route.route_name
1958                {
1959                    return Err(CodebaseDocError::DuplicateViaRoute {
1960                        via_module_path: route.via_module_path,
1961                        route_name: route.route_name,
1962                    });
1963                }
1964                if resolved_route.target_machine != machine.index {
1965                    return Err(CodebaseDocError::DuplicateViaRoute {
1966                        via_module_path: route.via_module_path,
1967                        route_name: route.route_name,
1968                    });
1969                }
1970                if resolved_route.target_state != target_state {
1971                    return Err(CodebaseDocError::ConflictingViaRouteTarget {
1972                        via_module_path: route.via_module_path,
1973                        route_name: route.route_name,
1974                        expected_target_state: resolved_route.target_state_name,
1975                        conflicting_target_state: route.target_state,
1976                    });
1977                }
1978                if resolved_route.producers.iter().any(|existing| {
1979                    existing.machine == producer.machine
1980                        && existing.state == producer.state
1981                        && existing.transition == producer.transition
1982                        && existing.target_state_name == producer.target_state_name
1983                }) {
1984                    return Err(CodebaseDocError::DuplicateViaRoute {
1985                        via_module_path: route.via_module_path,
1986                        route_name: route.route_name,
1987                    });
1988                }
1989                resolved_route.producers.push(producer);
1990                resolved_route.producers.sort_by(|left, right| {
1991                    left.machine
1992                        .cmp(&right.machine)
1993                        .then(left.state.cmp(&right.state))
1994                        .then(left.transition.cmp(&right.transition))
1995                        .then(left.target_state_name.cmp(right.target_state_name))
1996                });
1997            }
1998        }
1999    }
2000
2001    Ok(resolved)
2002}
2003
2004fn resolve_reference_type_targets(
2005    machines: &[CodebaseMachine],
2006    reference_types: &'static [LinkedReferenceTypeDescriptor],
2007) -> Result<HashMap<&'static str, ResolvedReferenceTypeTarget>, CodebaseDocError> {
2008    let mut reference_types = reference_types.to_vec();
2009    reference_types.sort_by(compare_reference_types);
2010
2011    let mut resolved = HashMap::with_capacity(reference_types.len());
2012    for reference_type in reference_types {
2013        let resolved_type_name = (reference_type.resolved_type_name)();
2014        if resolved.contains_key(resolved_type_name) {
2015            return Err(CodebaseDocError::DuplicateReferenceTypeDeclaration {
2016                rust_type_path: reference_type.rust_type_path,
2017                resolved_type_name,
2018            });
2019        }
2020
2021        let target = resolve_required_target_machine(
2022            machines,
2023            (reference_type.resolved_target_machine_type_name)(),
2024            reference_type.to_machine_path,
2025            reference_type.to_state,
2026            |target_machine_path, target_state| {
2027                CodebaseDocError::MissingReferenceTypeTargetMachine {
2028                    rust_type_path: reference_type.rust_type_path,
2029                    target_machine_path,
2030                    target_state,
2031                }
2032            },
2033            |target_machine_path, target_state| CodebaseDocError::MissingReferenceTypeTargetState {
2034                rust_type_path: reference_type.rust_type_path,
2035                target_machine_path,
2036                target_state,
2037            },
2038            |target_machine_path, target_state| CodebaseDocError::AmbiguousReferenceTypeTarget {
2039                rust_type_path: reference_type.rust_type_path,
2040                target_machine_path,
2041                target_state,
2042            },
2043        )?;
2044
2045        resolved.insert(
2046            resolved_type_name,
2047            ResolvedReferenceTypeTarget {
2048                rust_type_path: reference_type.rust_type_path,
2049                target_machine: target.0,
2050                target_state: target.1,
2051            },
2052        );
2053    }
2054
2055    Ok(resolved)
2056}
2057
2058fn resolve_relation_source_machine(
2059    machines: &[CodebaseMachine],
2060    exact_machine_positions: &HashMap<&'static str, usize>,
2061    machine_path: &'static str,
2062    relation: &str,
2063) -> Result<usize, CodebaseDocError> {
2064    if let Some(&machine_index) = exact_machine_positions.get(machine_path) {
2065        return Ok(machine_index);
2066    }
2067
2068    let candidates = machines
2069        .iter()
2070        .filter(|candidate| path_string_suffix_matches(candidate.rust_type_path, machine_path))
2071        .map(|candidate| candidate.index)
2072        .collect::<Vec<_>>();
2073
2074    match candidates.as_slice() {
2075        [] => Err(CodebaseDocError::MissingRelationMachine {
2076            machine_path: machine_path.to_owned(),
2077            relation: relation.to_owned(),
2078        }),
2079        [machine_index] => Ok(*machine_index),
2080        _ => Err(CodebaseDocError::AmbiguousRelationMachine {
2081            machine_path: machine_path.to_owned(),
2082            relation: relation.to_owned(),
2083        }),
2084    }
2085}
2086
2087fn resolve_optional_target_machine(
2088    machines: &[CodebaseMachine],
2089    resolved_machine_type_name: &str,
2090    machine_path: &'static [&'static str],
2091    state: &'static str,
2092    relation: &str,
2093) -> Result<Option<ResolvedRelationTarget>, CodebaseDocError> {
2094    let candidates = target_candidates(machines, resolved_machine_type_name, machine_path, state);
2095    match candidates.as_slice() {
2096        [] => Ok(None),
2097        [(machine_index, state_index)] => Ok(Some((*machine_index, *state_index, None))),
2098        _ => Err(CodebaseDocError::AmbiguousRelationTarget {
2099            relation: relation.to_owned(),
2100            target_machine_path: machine_path.join("::"),
2101            target_state: state,
2102        }),
2103    }
2104}
2105
2106fn resolve_required_target_machine<FMissingMachine, FMissingState, FAmbiguous>(
2107    machines: &[CodebaseMachine],
2108    resolved_machine_type_name: &str,
2109    machine_path: &'static [&'static str],
2110    state: &'static str,
2111    missing_machine: FMissingMachine,
2112    missing_state: FMissingState,
2113    ambiguous: FAmbiguous,
2114) -> Result<(usize, usize), CodebaseDocError>
2115where
2116    FMissingMachine: FnOnce(String, &'static str) -> CodebaseDocError,
2117    FMissingState: FnOnce(String, &'static str) -> CodebaseDocError,
2118    FAmbiguous: FnOnce(String, &'static str) -> CodebaseDocError,
2119{
2120    let machine_path_string = machine_path.join("::");
2121    let matching_machines = machines
2122        .iter()
2123        .filter(|candidate| {
2124            machine_path_matches(
2125                candidate.rust_type_path,
2126                resolved_machine_type_name,
2127                machine_path,
2128            )
2129        })
2130        .collect::<Vec<_>>();
2131    if matching_machines.is_empty() {
2132        return Err(missing_machine(machine_path_string, state));
2133    }
2134
2135    let candidates = matching_machines
2136        .iter()
2137        .filter_map(|candidate| {
2138            candidate
2139                .state_named(state)
2140                .map(|target_state| (candidate.index, target_state.index))
2141        })
2142        .collect::<Vec<_>>();
2143
2144    match candidates.as_slice() {
2145        [] => Err(missing_state(machine_path.join("::"), state)),
2146        [(machine_index, state_index)] => Ok((*machine_index, *state_index)),
2147        _ => Err(ambiguous(machine_path.join("::"), state)),
2148    }
2149}
2150
2151fn target_candidates(
2152    machines: &[CodebaseMachine],
2153    resolved_machine_type_name: &str,
2154    machine_path: &'static [&'static str],
2155    state: &'static str,
2156) -> Vec<(usize, usize)> {
2157    machines
2158        .iter()
2159        .filter_map(|candidate| {
2160            if !machine_path_matches(
2161                candidate.rust_type_path,
2162                resolved_machine_type_name,
2163                machine_path,
2164            ) {
2165                return None;
2166            }
2167
2168            candidate
2169                .state_named(state)
2170                .map(|target_state| (candidate.index, target_state.index))
2171        })
2172        .collect()
2173}
2174
2175fn machine_path_matches(
2176    candidate: &str,
2177    resolved_machine_type_name: &str,
2178    path: &[&'static str],
2179) -> bool {
2180    machine_family_path_suffix_matches(resolved_machine_type_name, candidate)
2181        || path_suffix_matches(candidate, path)
2182}
2183
2184fn map_relation_kind(kind: LinkedRelationKind) -> CodebaseRelationKind {
2185    match kind {
2186        LinkedRelationKind::StatePayload => CodebaseRelationKind::StatePayload,
2187        LinkedRelationKind::MachineField => CodebaseRelationKind::MachineField,
2188        LinkedRelationKind::TransitionParam => CodebaseRelationKind::TransitionParam,
2189    }
2190}
2191
2192fn map_relation_basis(basis: LinkedRelationBasis) -> CodebaseRelationBasis {
2193    match basis {
2194        LinkedRelationBasis::DirectTypeSyntax => CodebaseRelationBasis::DirectTypeSyntax,
2195        LinkedRelationBasis::AttestedTypeSyntax => CodebaseRelationBasis::AttestedTypeSyntax,
2196        LinkedRelationBasis::DeclaredReferenceType => CodebaseRelationBasis::DeclaredReferenceType,
2197        LinkedRelationBasis::ViaDeclaration => CodebaseRelationBasis::ViaDeclaration,
2198    }
2199}
2200
2201fn classify_relation_semantic(
2202    source_role: CodebaseMachineRole,
2203    basis: CodebaseRelationBasis,
2204    source_machine: usize,
2205    target_machine: usize,
2206    target: LinkedRelationTarget,
2207) -> CodebaseRelationSemantic {
2208    if source_role == CodebaseMachineRole::Composition && source_machine != target_machine {
2209        match (basis, target) {
2210            (
2211                CodebaseRelationBasis::DirectTypeSyntax,
2212                LinkedRelationTarget::DirectMachine { .. },
2213            ) => CodebaseRelationSemantic::CompositionDirectChild,
2214            (
2215                CodebaseRelationBasis::AttestedTypeSyntax | CodebaseRelationBasis::ViaDeclaration,
2216                LinkedRelationTarget::AttestedProducerRoute { .. },
2217            ) => CodebaseRelationSemantic::CompositionDetachedHandoff,
2218            _ => CodebaseRelationSemantic::Exact,
2219        }
2220    } else {
2221        CodebaseRelationSemantic::Exact
2222    }
2223}
2224
2225fn compare_reference_types(
2226    left: &LinkedReferenceTypeDescriptor,
2227    right: &LinkedReferenceTypeDescriptor,
2228) -> core::cmp::Ordering {
2229    (left.resolved_type_name)()
2230        .cmp((right.resolved_type_name)())
2231        .then_with(|| left.rust_type_path.cmp(right.rust_type_path))
2232        .then_with(|| left.to_machine_path.cmp(right.to_machine_path))
2233        .then_with(|| {
2234            (left.resolved_target_machine_type_name)()
2235                .cmp((right.resolved_target_machine_type_name)())
2236        })
2237        .then_with(|| left.to_state.cmp(right.to_state))
2238}
2239
2240fn compare_relations(
2241    left: &LinkedRelationDescriptor,
2242    right: &LinkedRelationDescriptor,
2243) -> core::cmp::Ordering {
2244    left.machine
2245        .rust_type_path
2246        .cmp(right.machine.rust_type_path)
2247        .then_with(|| compare_relation_sources(&left.source, &right.source))
2248        .then_with(|| {
2249            linked_relation_kind_rank(left.kind).cmp(&linked_relation_kind_rank(right.kind))
2250        })
2251        .then_with(|| {
2252            linked_relation_basis_rank(left.basis).cmp(&linked_relation_basis_rank(right.basis))
2253        })
2254        .then_with(|| compare_relation_targets(&left.target, &right.target))
2255}
2256
2257fn compare_relation_sources(
2258    left: &LinkedRelationSource,
2259    right: &LinkedRelationSource,
2260) -> core::cmp::Ordering {
2261    match (left, right) {
2262        (
2263            LinkedRelationSource::StatePayload {
2264                state: left_state,
2265                field_name: left_field,
2266            },
2267            LinkedRelationSource::StatePayload {
2268                state: right_state,
2269                field_name: right_field,
2270            },
2271        ) => left_state
2272            .cmp(right_state)
2273            .then_with(|| left_field.cmp(right_field)),
2274        (
2275            LinkedRelationSource::MachineField {
2276                field_name: left_field,
2277                field_index: left_index,
2278            },
2279            LinkedRelationSource::MachineField {
2280                field_name: right_field,
2281                field_index: right_index,
2282            },
2283        ) => left_index
2284            .cmp(right_index)
2285            .then_with(|| left_field.cmp(right_field)),
2286        (
2287            LinkedRelationSource::TransitionParam {
2288                state: left_state,
2289                transition: left_transition,
2290                param_index: left_index,
2291                param_name: left_name,
2292            },
2293            LinkedRelationSource::TransitionParam {
2294                state: right_state,
2295                transition: right_transition,
2296                param_index: right_index,
2297                param_name: right_name,
2298            },
2299        ) => left_state
2300            .cmp(right_state)
2301            .then_with(|| left_transition.cmp(right_transition))
2302            .then_with(|| left_index.cmp(right_index))
2303            .then_with(|| left_name.cmp(right_name)),
2304        (left, right) => linked_relation_source_rank(left).cmp(&linked_relation_source_rank(right)),
2305    }
2306}
2307
2308fn compare_relation_targets(
2309    left: &LinkedRelationTarget,
2310    right: &LinkedRelationTarget,
2311) -> core::cmp::Ordering {
2312    match (left, right) {
2313        (
2314            LinkedRelationTarget::DirectMachine {
2315                machine_path: left_path,
2316                resolved_machine_type_name: left_type_name,
2317                state: left_state,
2318            },
2319            LinkedRelationTarget::DirectMachine {
2320                machine_path: right_path,
2321                resolved_machine_type_name: right_type_name,
2322                state: right_state,
2323            },
2324        ) => left_path
2325            .cmp(right_path)
2326            .then_with(|| left_type_name().cmp(right_type_name()))
2327            .then_with(|| left_state.cmp(right_state)),
2328        (
2329            LinkedRelationTarget::DeclaredReferenceType {
2330                resolved_type_name: left_name,
2331            },
2332            LinkedRelationTarget::DeclaredReferenceType {
2333                resolved_type_name: right_name,
2334            },
2335        ) => left_name().cmp(right_name()),
2336        (
2337            LinkedRelationTarget::AttestedProducerRoute {
2338                via_module_path: left_module_path,
2339                route_name: left_route_name,
2340                resolved_route_type_name: left_type_name,
2341                route_id: left_route_id,
2342            },
2343            LinkedRelationTarget::AttestedProducerRoute {
2344                via_module_path: right_module_path,
2345                route_name: right_route_name,
2346                resolved_route_type_name: right_type_name,
2347                route_id: right_route_id,
2348            },
2349        ) => left_module_path
2350            .cmp(right_module_path)
2351            .then_with(|| left_route_name.cmp(right_route_name))
2352            .then_with(|| left_type_name().cmp(right_type_name()))
2353            .then_with(|| left_route_id.cmp(right_route_id)),
2354        (
2355            LinkedRelationTarget::AttestedRoute {
2356                via_module_path: left_module_path,
2357                route_name: left_route_name,
2358                resolved_route_type_name: left_type_name,
2359                route_id: left_route_id,
2360                machine_path: left_machine_path,
2361                resolved_machine_type_name: left_machine_type_name,
2362                state: left_state,
2363            },
2364            LinkedRelationTarget::AttestedRoute {
2365                via_module_path: right_module_path,
2366                route_name: right_route_name,
2367                resolved_route_type_name: right_type_name,
2368                route_id: right_route_id,
2369                machine_path: right_machine_path,
2370                resolved_machine_type_name: right_machine_type_name,
2371                state: right_state,
2372            },
2373        ) => left_module_path
2374            .cmp(right_module_path)
2375            .then_with(|| left_route_name.cmp(right_route_name))
2376            .then_with(|| left_type_name().cmp(right_type_name()))
2377            .then_with(|| left_route_id.cmp(right_route_id))
2378            .then_with(|| left_machine_path.cmp(right_machine_path))
2379            .then_with(|| left_machine_type_name().cmp(right_machine_type_name()))
2380            .then_with(|| left_state.cmp(right_state)),
2381        (left, right) => linked_relation_target_rank(left).cmp(&linked_relation_target_rank(right)),
2382    }
2383}
2384
2385fn linked_relation_kind_rank(kind: LinkedRelationKind) -> u8 {
2386    match kind {
2387        LinkedRelationKind::StatePayload => 0,
2388        LinkedRelationKind::MachineField => 1,
2389        LinkedRelationKind::TransitionParam => 2,
2390    }
2391}
2392
2393fn linked_relation_basis_rank(basis: LinkedRelationBasis) -> u8 {
2394    match basis {
2395        LinkedRelationBasis::DirectTypeSyntax => 0,
2396        LinkedRelationBasis::AttestedTypeSyntax => 1,
2397        LinkedRelationBasis::DeclaredReferenceType => 2,
2398        LinkedRelationBasis::ViaDeclaration => 3,
2399    }
2400}
2401
2402fn linked_relation_source_rank(source: &LinkedRelationSource) -> u8 {
2403    match source {
2404        LinkedRelationSource::StatePayload { .. } => 0,
2405        LinkedRelationSource::MachineField { .. } => 1,
2406        LinkedRelationSource::TransitionParam { .. } => 2,
2407    }
2408}
2409
2410fn linked_relation_target_rank(target: &LinkedRelationTarget) -> u8 {
2411    match target {
2412        LinkedRelationTarget::DirectMachine { .. } => 0,
2413        LinkedRelationTarget::DeclaredReferenceType { .. } => 1,
2414        LinkedRelationTarget::AttestedProducerRoute { .. } => 2,
2415        LinkedRelationTarget::AttestedRoute { .. } => 3,
2416    }
2417}
2418
2419fn relation_summary(relation: &LinkedRelationDescriptor) -> String {
2420    let base = match relation.source {
2421        LinkedRelationSource::StatePayload { state, field_name } => match field_name {
2422            Some(field_name) => format!(
2423                "{} state payload {}::{}",
2424                relation.machine.rust_type_path, state, field_name
2425            ),
2426            None => format!(
2427                "{} state payload {}",
2428                relation.machine.rust_type_path, state
2429            ),
2430        },
2431        LinkedRelationSource::MachineField {
2432            field_name,
2433            field_index,
2434        } => match field_name {
2435            Some(field_name) => format!(
2436                "{} machine field {}",
2437                relation.machine.rust_type_path, field_name
2438            ),
2439            None => format!(
2440                "{} machine field #{}",
2441                relation.machine.rust_type_path, field_index
2442            ),
2443        },
2444        LinkedRelationSource::TransitionParam {
2445            state,
2446            transition,
2447            param_index,
2448            param_name,
2449        } => match param_name {
2450            Some(param_name) => format!(
2451                "{} transition param {}::{}({})",
2452                relation.machine.rust_type_path, state, transition, param_name
2453            ),
2454            None => format!(
2455                "{} transition param {}::{}[#{}]",
2456                relation.machine.rust_type_path, state, transition, param_index
2457            ),
2458        },
2459    };
2460
2461    match relation.target {
2462        LinkedRelationTarget::AttestedProducerRoute {
2463            via_module_path,
2464            route_name,
2465            ..
2466        }
2467        | LinkedRelationTarget::AttestedRoute {
2468            via_module_path,
2469            route_name,
2470            ..
2471        } => format!("{base} via {via_module_path}::{route_name}"),
2472        LinkedRelationTarget::DirectMachine { .. }
2473        | LinkedRelationTarget::DeclaredReferenceType { .. } => base,
2474    }
2475}
2476
2477fn resolve_validator_entries(
2478    machines: &mut [CodebaseMachine],
2479    validator_entries: &'static [LinkedValidatorEntryDescriptor],
2480) -> Result<usize, CodebaseDocError> {
2481    let mut validator_entries = validator_entries.to_vec();
2482    validator_entries.sort_by(compare_validator_entries);
2483
2484    let machine_positions = machines
2485        .iter()
2486        .map(|machine| (machine.rust_type_path, machine.index))
2487        .collect::<HashMap<_, _>>();
2488    let mut seen_entries = HashSet::with_capacity(validator_entries.len());
2489
2490    for entry in validator_entries {
2491        let Some(&machine_index) = machine_positions.get(entry.machine.rust_type_path) else {
2492            return Err(CodebaseDocError::MissingValidatorMachine {
2493                machine: entry.machine.rust_type_path,
2494                source_module_path: entry.source_module_path,
2495                source_type_display: entry.source_type_display,
2496            });
2497        };
2498        if !seen_entries.insert((
2499            entry.machine.rust_type_path,
2500            entry.source_module_path,
2501            entry.source_type_display,
2502        )) {
2503            return Err(CodebaseDocError::DuplicateValidatorEntry {
2504                machine: entry.machine.rust_type_path,
2505                source_module_path: entry.source_module_path,
2506                source_type_display: entry.source_type_display,
2507            });
2508        }
2509        if entry.target_states.is_empty() {
2510            return Err(CodebaseDocError::EmptyValidatorTargetSet {
2511                machine: entry.machine.rust_type_path,
2512                source_module_path: entry.source_module_path,
2513                source_type_display: entry.source_type_display,
2514            });
2515        }
2516
2517        let machine = &mut machines[machine_index];
2518        let mut target_states = Vec::with_capacity(entry.target_states.len());
2519        let mut seen_target_states = HashSet::with_capacity(entry.target_states.len());
2520
2521        for target_state in entry.target_states {
2522            let Some(target_index) = machine.state_named(target_state).map(|state| state.index)
2523            else {
2524                return Err(CodebaseDocError::MissingValidatorTargetState {
2525                    machine: machine.rust_type_path,
2526                    source_module_path: entry.source_module_path,
2527                    source_type_display: entry.source_type_display,
2528                    state: target_state,
2529                });
2530            };
2531            if !seen_target_states.insert(*target_state) {
2532                return Err(CodebaseDocError::DuplicateValidatorTargetState {
2533                    machine: machine.rust_type_path,
2534                    source_module_path: entry.source_module_path,
2535                    source_type_display: entry.source_type_display,
2536                    state: target_state,
2537                });
2538            }
2539            target_states.push(target_index);
2540        }
2541
2542        target_states.sort_unstable();
2543
2544        machine.validator_entries.push(CodebaseValidatorEntry {
2545            index: machine.validator_entries.len(),
2546            source_module_path: entry.source_module_path,
2547            source_type_display: entry.source_type_display,
2548            resolved_source_type_name: (entry.resolved_source_type_name)(),
2549            docs: entry.docs,
2550            target_states,
2551        });
2552    }
2553
2554    Ok(total_validator_entries(machines))
2555}
2556
2557fn total_validator_entries(machines: &[CodebaseMachine]) -> usize {
2558    machines
2559        .iter()
2560        .map(|machine| machine.validator_entries.len())
2561        .sum()
2562}
2563
2564fn compare_validator_entries(
2565    left: &LinkedValidatorEntryDescriptor,
2566    right: &LinkedValidatorEntryDescriptor,
2567) -> core::cmp::Ordering {
2568    left.machine
2569        .rust_type_path
2570        .cmp(right.machine.rust_type_path)
2571        .then_with(|| left.source_module_path.cmp(right.source_module_path))
2572        .then_with(|| left.source_type_display.cmp(right.source_type_display))
2573        .then_with(|| left.target_states.cmp(right.target_states))
2574}
2575
2576fn path_suffix_matches(candidate: &str, suffix: &[&'static str]) -> bool {
2577    let candidate = candidate.split("::").collect::<Vec<_>>();
2578    candidate.ends_with(suffix)
2579}
2580
2581fn path_string_suffix_matches(candidate: &str, suffix: &str) -> bool {
2582    let suffix = suffix
2583        .split("::")
2584        .filter(|segment| !segment.is_empty())
2585        .collect::<Vec<_>>();
2586    if suffix.is_empty() {
2587        return false;
2588    }
2589
2590    let candidate = candidate.split("::").collect::<Vec<_>>();
2591    candidate.ends_with(&suffix)
2592}
2593
2594fn machine_family_path_suffix_matches(
2595    resolved_machine_type_name: &str,
2596    machine_path: &str,
2597) -> bool {
2598    let family_path = resolved_machine_type_name
2599        .split_once('<')
2600        .map(|(family_path, _)| family_path)
2601        .unwrap_or(resolved_machine_type_name);
2602    path_string_suffix_matches(family_path, machine_path)
2603}
2604
2605#[cfg(test)]
2606mod tests {
2607    use super::*;
2608
2609    use statum::{
2610        LinkedMachineGraph, LinkedStateDescriptor, LinkedTransitionDescriptor,
2611        LinkedTransitionInventory, MachineDescriptor,
2612    };
2613
2614    static PRODUCER_STATES: [LinkedStateDescriptor; 2] = [
2615        LinkedStateDescriptor {
2616            rust_name: "Authorized",
2617            label: None,
2618            description: None,
2619            docs: None,
2620            has_data: false,
2621            direct_construction_available: false,
2622        },
2623        LinkedStateDescriptor {
2624            rust_name: "Captured",
2625            label: None,
2626            description: None,
2627            docs: None,
2628            has_data: false,
2629            direct_construction_available: false,
2630        },
2631    ];
2632    static PRODUCER_TRANSITIONS: [LinkedTransitionDescriptor; 1] = [LinkedTransitionDescriptor {
2633        method_name: "capture",
2634        label: None,
2635        description: None,
2636        docs: None,
2637        from: "Authorized",
2638        to: &["Captured"],
2639    }];
2640    static PRODUCER_MACHINE: LinkedMachineGraph = LinkedMachineGraph {
2641        machine: MachineDescriptor {
2642            module_path: "crate::payment",
2643            rust_type_path: "crate::payment::Machine",
2644            role: statum::MachineRole::Protocol,
2645        },
2646        label: None,
2647        description: None,
2648        docs: None,
2649        states: &PRODUCER_STATES,
2650        transitions: LinkedTransitionInventory::new(producer_transitions),
2651        static_links: &[],
2652    };
2653
2654    static CONSUMER_STATES: [LinkedStateDescriptor; 2] = [
2655        LinkedStateDescriptor {
2656            rust_name: "Draft",
2657            label: None,
2658            description: None,
2659            docs: None,
2660            has_data: false,
2661            direct_construction_available: false,
2662        },
2663        LinkedStateDescriptor {
2664            rust_name: "Done",
2665            label: None,
2666            description: None,
2667            docs: None,
2668            has_data: false,
2669            direct_construction_available: false,
2670        },
2671    ];
2672    static CONSUMER_TRANSITIONS: [LinkedTransitionDescriptor; 1] = [LinkedTransitionDescriptor {
2673        method_name: "finish",
2674        label: None,
2675        description: None,
2676        docs: None,
2677        from: "Draft",
2678        to: &["Done"],
2679    }];
2680    static CONSUMER_MACHINE: LinkedMachineGraph = LinkedMachineGraph {
2681        machine: MachineDescriptor {
2682            module_path: "crate::audit",
2683            rust_type_path: "crate::audit::Machine",
2684            role: statum::MachineRole::Protocol,
2685        },
2686        label: None,
2687        description: None,
2688        docs: None,
2689        states: &CONSUMER_STATES,
2690        transitions: LinkedTransitionInventory::new(consumer_transitions),
2691        static_links: &[],
2692    };
2693
2694    static LINKED_MACHINES: [LinkedMachineGraph; 2] = [PRODUCER_MACHINE, CONSUMER_MACHINE];
2695    static CONFLICTING_VIA_ROUTES: [LinkedViaRouteDescriptor; 2] = [
2696        LinkedViaRouteDescriptor {
2697            machine: MachineDescriptor {
2698                module_path: "crate::payment",
2699                rust_type_path: "crate::payment::Machine",
2700                role: statum::MachineRole::Protocol,
2701            },
2702            via_module_path: "crate::payment::via",
2703            route_name: "Capture",
2704            resolved_route_type_name: capture_route_type_name,
2705            route_id: 1,
2706            transition: "capture",
2707            source_state: "Authorized",
2708            target_state: "Captured",
2709        },
2710        LinkedViaRouteDescriptor {
2711            machine: MachineDescriptor {
2712                module_path: "crate::payment",
2713                rust_type_path: "crate::payment::Machine",
2714                role: statum::MachineRole::Protocol,
2715            },
2716            via_module_path: "crate::receipts::via",
2717            route_name: "Release",
2718            resolved_route_type_name: capture_route_type_name,
2719            route_id: 2,
2720            transition: "capture",
2721            source_state: "Authorized",
2722            target_state: "Captured",
2723        },
2724    ];
2725    static CONFLICTING_VIA_ROUTE_TARGETS: [LinkedViaRouteDescriptor; 2] = [
2726        LinkedViaRouteDescriptor {
2727            machine: MachineDescriptor {
2728                module_path: "crate::payment",
2729                rust_type_path: "crate::payment::Machine",
2730                role: statum::MachineRole::Protocol,
2731            },
2732            via_module_path: "crate::payment::via",
2733            route_name: "Capture",
2734            resolved_route_type_name: capture_route_type_name,
2735            route_id: 1,
2736            transition: "capture",
2737            source_state: "Authorized",
2738            target_state: "Captured",
2739        },
2740        LinkedViaRouteDescriptor {
2741            machine: MachineDescriptor {
2742                module_path: "crate::payment",
2743                rust_type_path: "crate::payment::Machine",
2744                role: statum::MachineRole::Protocol,
2745            },
2746            via_module_path: "crate::payment::via",
2747            route_name: "Capture",
2748            resolved_route_type_name: capture_route_type_name,
2749            route_id: 1,
2750            transition: "capture",
2751            source_state: "Authorized",
2752            target_state: "Authorized",
2753        },
2754    ];
2755
2756    #[test]
2757    fn conflicting_attested_route_identities_fail_closed() {
2758        let err = CodebaseDoc::try_from_linked_with_inventories(
2759            &LINKED_MACHINES,
2760            &[],
2761            &[],
2762            &CONFLICTING_VIA_ROUTES,
2763            &[],
2764        )
2765        .expect_err("conflicting route identities should fail closed");
2766
2767        assert_eq!(
2768            err,
2769            CodebaseDocError::DuplicateViaRoute {
2770                via_module_path: "crate::receipts::via",
2771                route_name: "Release",
2772            }
2773        );
2774        assert_eq!(
2775            err.to_string(),
2776            "linked attested route `crate::receipts::via::Release` appears more than once in the producer inventory"
2777        );
2778    }
2779
2780    #[test]
2781    fn conflicting_attested_route_targets_fail_closed() {
2782        let err = CodebaseDoc::try_from_linked_with_inventories(
2783            &LINKED_MACHINES,
2784            &[],
2785            &[],
2786            &CONFLICTING_VIA_ROUTE_TARGETS,
2787            &[],
2788        )
2789        .expect_err("conflicting route targets should fail closed");
2790
2791        assert_eq!(
2792            err,
2793            CodebaseDocError::ConflictingViaRouteTarget {
2794                via_module_path: "crate::payment::via",
2795                route_name: "Capture",
2796                expected_target_state: "Captured",
2797                conflicting_target_state: "Authorized",
2798            }
2799        );
2800        assert_eq!(
2801            err.to_string(),
2802            "linked attested route `crate::payment::via::Capture` conflicts on target state: expected `Captured`, found `Authorized`"
2803        );
2804    }
2805
2806    #[test]
2807    fn composition_semantics_require_cross_machine_direct_targets() {
2808        assert_eq!(
2809            classify_relation_semantic(
2810                CodebaseMachineRole::Composition,
2811                CodebaseRelationBasis::DirectTypeSyntax,
2812                1,
2813                2,
2814                LinkedRelationTarget::DirectMachine {
2815                    machine_path: &["crate", "task", "Machine"],
2816                    resolved_machine_type_name: capture_route_type_name,
2817                    state: "Running",
2818                },
2819            ),
2820            CodebaseRelationSemantic::CompositionDirectChild
2821        );
2822        assert_eq!(
2823            classify_relation_semantic(
2824                CodebaseMachineRole::Composition,
2825                CodebaseRelationBasis::DirectTypeSyntax,
2826                1,
2827                1,
2828                LinkedRelationTarget::DirectMachine {
2829                    machine_path: &["crate", "task", "Machine"],
2830                    resolved_machine_type_name: capture_route_type_name,
2831                    state: "Running",
2832                },
2833            ),
2834            CodebaseRelationSemantic::Exact
2835        );
2836        assert_eq!(
2837            classify_relation_semantic(
2838                CodebaseMachineRole::Composition,
2839                CodebaseRelationBasis::DeclaredReferenceType,
2840                1,
2841                2,
2842                LinkedRelationTarget::DeclaredReferenceType {
2843                    resolved_type_name: capture_route_type_name,
2844                },
2845            ),
2846            CodebaseRelationSemantic::Exact
2847        );
2848        assert_eq!(
2849            classify_relation_semantic(
2850                CodebaseMachineRole::Composition,
2851                CodebaseRelationBasis::AttestedTypeSyntax,
2852                1,
2853                2,
2854                LinkedRelationTarget::AttestedProducerRoute {
2855                    via_module_path: "crate::task::via",
2856                    route_name: "Capture",
2857                    resolved_route_type_name: capture_route_type_name,
2858                    route_id: 1,
2859                },
2860            ),
2861            CodebaseRelationSemantic::CompositionDetachedHandoff
2862        );
2863    }
2864
2865    #[test]
2866    fn cached_relation_indices_follow_stable_export_order() {
2867        let machines = vec![
2868            CodebaseMachine {
2869                index: 0,
2870                module_path: "crate::producer",
2871                rust_type_path: "crate::producer::Machine",
2872                role: CodebaseMachineRole::Protocol,
2873                label: None,
2874                description: None,
2875                docs: None,
2876                states: vec![CodebaseState {
2877                    index: 0,
2878                    rust_name: "Idle",
2879                    label: None,
2880                    description: None,
2881                    docs: None,
2882                    has_data: false,
2883                    direct_construction_available: false,
2884                    is_graph_root: true,
2885                }],
2886                transitions: vec![CodebaseTransition {
2887                    index: 0,
2888                    method_name: "start",
2889                    label: None,
2890                    description: None,
2891                    docs: None,
2892                    from: 0,
2893                    to: vec![0],
2894                }],
2895                validator_entries: Vec::new(),
2896            },
2897            CodebaseMachine {
2898                index: 1,
2899                module_path: "crate::consumer",
2900                rust_type_path: "crate::consumer::Machine",
2901                role: CodebaseMachineRole::Composition,
2902                label: None,
2903                description: None,
2904                docs: None,
2905                states: vec![CodebaseState {
2906                    index: 0,
2907                    rust_name: "Done",
2908                    label: None,
2909                    description: None,
2910                    docs: None,
2911                    has_data: false,
2912                    direct_construction_available: false,
2913                    is_graph_root: true,
2914                }],
2915                transitions: Vec::new(),
2916                validator_entries: Vec::new(),
2917            },
2918        ];
2919        let relations = vec![
2920            CodebaseRelation {
2921                index: 0,
2922                kind: CodebaseRelationKind::TransitionParam,
2923                basis: CodebaseRelationBasis::DirectTypeSyntax,
2924                semantic: CodebaseRelationSemantic::CompositionDirectChild,
2925                source: CodebaseRelationSource::TransitionParam {
2926                    machine: 0,
2927                    transition: 0,
2928                    param_index: 0,
2929                    param_name: None,
2930                },
2931                target_machine: 1,
2932                target_state: 0,
2933                declared_reference_type: None,
2934                attested_via: None,
2935            },
2936            CodebaseRelation {
2937                index: 1,
2938                kind: CodebaseRelationKind::StatePayload,
2939                basis: CodebaseRelationBasis::DirectTypeSyntax,
2940                semantic: CodebaseRelationSemantic::Exact,
2941                source: CodebaseRelationSource::StatePayload {
2942                    machine: 0,
2943                    state: 0,
2944                    field_name: Some("child"),
2945                },
2946                target_machine: 1,
2947                target_state: 0,
2948                declared_reference_type: None,
2949                attested_via: None,
2950            },
2951        ];
2952
2953        let index = CodebaseRelationIndex::new(&machines, &relations);
2954        let groups = build_machine_relation_groups(&relations);
2955
2956        assert_eq!(index.outbound_machine(0), &[0, 1]);
2957        assert_eq!(index.inbound_machine(1), &[0, 1]);
2958        assert_eq!(groups[0].relation_indices, vec![0, 1]);
2959    }
2960
2961    fn producer_transitions() -> &'static [LinkedTransitionDescriptor] {
2962        &PRODUCER_TRANSITIONS
2963    }
2964
2965    fn consumer_transitions() -> &'static [LinkedTransitionDescriptor] {
2966        &CONSUMER_TRANSITIONS
2967    }
2968
2969    fn capture_route_type_name() -> &'static str {
2970        "crate::payment::machine::via::Capture"
2971    }
2972}