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#[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 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 pub fn try_from_linked(
41 linked: &'static [LinkedMachineGraph],
42 ) -> Result<Self, CodebaseDocError> {
43 Self::try_from_linked_with_inventories(linked, &[], &[], &[], &[])
44 }
45
46 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 pub fn machines(&self) -> &[CodebaseMachine] {
108 &self.machines
109 }
110
111 pub fn links(&self) -> &[CodebaseLink] {
113 &self.links
114 }
115
116 pub fn relations(&self) -> &[CodebaseRelation] {
118 &self.relations
119 }
120
121 pub fn machine(&self, index: usize) -> Option<&CodebaseMachine> {
123 self.machines.get(index)
124 }
125
126 pub fn relation(&self, index: usize) -> Option<&CodebaseRelation> {
128 self.relations.get(index)
129 }
130
131 pub fn machine_relation_groups(&self) -> &[CodebaseMachineRelationGroup] {
134 &self.relation_groups
135 }
136
137 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 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 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 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 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 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 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 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 pub const fn display_label(self) -> &'static str {
291 match self {
292 Self::Protocol => "protocol",
293 Self::Composition => "composition",
294 }
295 }
296
297 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#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
314pub struct CodebaseMachine {
315 pub index: usize,
317 pub module_path: &'static str,
319 pub rust_type_path: &'static str,
321 pub role: CodebaseMachineRole,
324 pub label: Option<&'static str>,
326 pub description: Option<&'static str>,
328 pub docs: Option<&'static str>,
330 pub states: Vec<CodebaseState>,
332 pub transitions: Vec<CodebaseTransition>,
334 pub validator_entries: Vec<CodebaseValidatorEntry>,
336}
337
338impl CodebaseMachine {
339 pub fn state(&self, index: usize) -> Option<&CodebaseState> {
341 self.states.get(index)
342 }
343
344 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 pub fn validator_entry(&self, index: usize) -> Option<&CodebaseValidatorEntry> {
354 self.validator_entries.get(index)
355 }
356
357 pub fn transition(&self, index: usize) -> Option<&CodebaseTransition> {
359 self.transitions.get(index)
360 }
361
362 pub fn node_id(&self, state_index: usize) -> String {
364 format!("m{}_s{}", self.index, state_index)
365 }
366
367 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
399pub struct CodebaseState {
400 pub index: usize,
402 pub rust_name: &'static str,
404 pub label: Option<&'static str>,
406 pub description: Option<&'static str>,
408 pub docs: Option<&'static str>,
410 pub has_data: bool,
412 pub direct_construction_available: bool,
414 pub is_graph_root: bool,
416}
417
418impl CodebaseState {
419 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#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
431pub struct CodebaseValidatorEntry {
432 pub index: usize,
434 pub source_module_path: &'static str,
436 pub source_type_display: &'static str,
438 #[doc(hidden)]
440 #[serde(skip_serializing)]
441 pub resolved_source_type_name: &'static str,
442 pub docs: Option<&'static str>,
444 pub target_states: Vec<usize>,
446}
447
448impl CodebaseValidatorEntry {
449 pub fn display_label(&self) -> Cow<'static, str> {
451 Cow::Owned(format!("{}::into_machine()", self.source_type_display))
452 }
453}
454
455#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
457pub struct CodebaseTransition {
458 pub index: usize,
460 pub method_name: &'static str,
462 pub label: Option<&'static str>,
464 pub description: Option<&'static str>,
466 pub docs: Option<&'static str>,
468 pub from: usize,
470 pub to: Vec<usize>,
472}
473
474impl CodebaseTransition {
475 pub fn display_label(&self) -> &'static str {
477 self.label.unwrap_or(self.method_name)
478 }
479}
480
481#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
483pub enum CodebaseRelationKind {
484 StatePayload,
485 MachineField,
486 TransitionParam,
487}
488
489impl CodebaseRelationKind {
490 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#[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 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#[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 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 pub const fn is_composition_owned(self) -> bool {
552 matches!(
553 self,
554 Self::CompositionDirectChild | Self::CompositionDetachedHandoff
555 )
556 }
557}
558
559#[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 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 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 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#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
610pub struct CodebaseRelation {
611 pub index: usize,
613 pub kind: CodebaseRelationKind,
615 pub basis: CodebaseRelationBasis,
617 pub semantic: CodebaseRelationSemantic,
619 pub source: CodebaseRelationSource,
621 pub target_machine: usize,
623 pub target_state: usize,
625 pub declared_reference_type: Option<&'static str>,
628 pub attested_via: Option<CodebaseAttestedRoute>,
631}
632
633impl CodebaseRelation {
634 pub const fn source_machine(&self) -> usize {
636 self.source.machine()
637 }
638
639 pub const fn source_state(&self) -> Option<usize> {
641 self.source.state()
642 }
643
644 pub const fn source_transition(&self) -> Option<usize> {
647 self.source.transition()
648 }
649
650 pub const fn target_transition(&self) -> Option<usize> {
656 None
657 }
658
659 pub const fn is_composition_owned(&self) -> bool {
662 self.semantic.is_composition_owned()
663 }
664}
665
666#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
668pub struct CodebaseAttestedProducer {
669 pub machine: usize,
671 pub state: usize,
673 pub transition: usize,
675}
676
677#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
679pub struct CodebaseAttestedRoute {
680 pub via_module_path: &'static str,
682 pub route_name: &'static str,
684 pub producers: Vec<CodebaseAttestedProducer>,
687}
688
689#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
691pub struct CodebaseMachineRelationGroup {
692 pub index: usize,
694 pub from_machine: usize,
696 pub to_machine: usize,
698 pub semantic: CodebaseMachineRelationGroupSemantic,
700 pub relation_indices: Vec<usize>,
702 pub counts: Vec<CodebaseRelationCount>,
704}
705
706#[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 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 pub const fn is_composition_owned(self) -> bool {
748 !matches!(self, Self::Exact)
749 }
750}
751
752impl CodebaseMachineRelationGroup {
753 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 pub const fn is_composition_owned(&self) -> bool {
767 self.semantic.is_composition_owned()
768 }
769}
770
771#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
773pub struct CodebaseRelationCount {
774 pub kind: CodebaseRelationKind,
776 pub basis: CodebaseRelationBasis,
778 pub count: usize,
780}
781
782impl CodebaseRelationCount {
783 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#[derive(Clone, Copy, Debug)]
800pub struct CodebaseAttestedProducerDetail<'a> {
801 pub producer: &'a CodebaseAttestedProducer,
803 pub machine: &'a CodebaseMachine,
805 pub state: &'a CodebaseState,
807 pub transition: &'a CodebaseTransition,
809}
810
811#[derive(Debug)]
812pub struct CodebaseRelationDetail<'a> {
813 pub relation: &'a CodebaseRelation,
815 pub source_machine: &'a CodebaseMachine,
817 pub source_state: Option<&'a CodebaseState>,
819 pub source_transition: Option<&'a CodebaseTransition>,
822 pub target_machine: &'a CodebaseMachine,
824 pub target_state: &'a CodebaseState,
826 pub attested_via_machine: Option<&'a CodebaseMachine>,
829 pub attested_via_state: Option<&'a CodebaseState>,
832 pub attested_via_transition: Option<&'a CodebaseTransition>,
835 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
990pub struct CodebaseLink {
991 pub index: usize,
993 pub from_machine: usize,
995 pub from_state: usize,
997 pub field_name: Option<&'static str>,
999 pub to_machine: usize,
1001 pub to_state: usize,
1003}
1004
1005impl CodebaseLink {
1006 pub fn display_label(&self) -> &'static str {
1008 self.field_name.unwrap_or("state_data")
1009 }
1010}
1011
1012#[derive(Clone, Debug, Eq, PartialEq)]
1015pub enum CodebaseDocError {
1016 DuplicateMachine { machine: &'static str },
1018 EmptyStateList { machine: &'static str },
1020 DuplicateStateName {
1022 machine: &'static str,
1023 state: &'static str,
1024 },
1025 DuplicateTransitionSite {
1027 machine: &'static str,
1028 state: &'static str,
1029 transition: &'static str,
1030 },
1031 MissingSourceState {
1033 machine: &'static str,
1034 transition: &'static str,
1035 },
1036 MissingTargetState {
1038 machine: &'static str,
1039 transition: &'static str,
1040 },
1041 EmptyTargetSet {
1043 machine: &'static str,
1044 transition: &'static str,
1045 },
1046 DuplicateTargetState {
1048 machine: &'static str,
1049 transition: &'static str,
1050 state: &'static str,
1051 },
1052 MissingValidatorMachine {
1055 machine: &'static str,
1056 source_module_path: &'static str,
1057 source_type_display: &'static str,
1058 },
1059 MissingValidatorTargetState {
1062 machine: &'static str,
1063 source_module_path: &'static str,
1064 source_type_display: &'static str,
1065 state: &'static str,
1066 },
1067 EmptyValidatorTargetSet {
1069 machine: &'static str,
1070 source_module_path: &'static str,
1071 source_type_display: &'static str,
1072 },
1073 DuplicateValidatorTargetState {
1075 machine: &'static str,
1076 source_module_path: &'static str,
1077 source_type_display: &'static str,
1078 state: &'static str,
1079 },
1080 DuplicateValidatorEntry {
1083 machine: &'static str,
1084 source_module_path: &'static str,
1085 source_type_display: &'static str,
1086 },
1087 DuplicateReferenceTypeDeclaration {
1090 rust_type_path: &'static str,
1091 resolved_type_name: &'static str,
1092 },
1093 MissingReferenceTypeTargetMachine {
1096 rust_type_path: &'static str,
1097 target_machine_path: String,
1098 target_state: &'static str,
1099 },
1100 MissingReferenceTypeTargetState {
1103 rust_type_path: &'static str,
1104 target_machine_path: String,
1105 target_state: &'static str,
1106 },
1107 AmbiguousReferenceTypeTarget {
1110 rust_type_path: &'static str,
1111 target_machine_path: String,
1112 target_state: &'static str,
1113 },
1114 MissingRelationMachine {
1116 machine_path: String,
1117 relation: String,
1118 },
1119 AmbiguousRelationMachine {
1121 machine_path: String,
1122 relation: String,
1123 },
1124 MissingRelationSourceState {
1127 machine: &'static str,
1128 state: &'static str,
1129 relation: String,
1130 },
1131 MissingRelationTransition {
1134 machine: &'static str,
1135 state: &'static str,
1136 transition: &'static str,
1137 },
1138 AmbiguousRelationTarget {
1140 relation: String,
1141 target_machine_path: String,
1142 target_state: &'static str,
1143 },
1144 DuplicateViaRoute {
1147 via_module_path: &'static str,
1148 route_name: &'static str,
1149 },
1150 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 MissingRelationViaRoute {
1161 relation: String,
1162 via_module_path: &'static str,
1163 route_name: &'static str,
1164 },
1165 MissingRelationViaSourceState {
1168 machine: &'static str,
1169 state: &'static str,
1170 relation: String,
1171 },
1172 MissingRelationViaTransition {
1175 machine: &'static str,
1176 state: &'static str,
1177 transition: &'static str,
1178 relation: String,
1179 },
1180 MissingRelationViaTargetState {
1183 machine: &'static str,
1184 state: &'static str,
1185 relation: String,
1186 },
1187 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 MissingStaticLinkSourceState {
1199 machine: &'static str,
1200 state: &'static str,
1201 },
1202 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}