1use std::collections::BTreeSet;
9use std::fmt;
10
11use serde::ser::SerializeStruct;
12use serde::{Serialize, Serializer};
13
14use crate::check::{Check, CheckCtx};
15use crate::config::{ConfigValidationError, SeveritySetting};
16use crate::finding::Finding;
17
18#[derive(Debug, Clone, Copy)]
24struct BuiltinEvidenceCode {
25 code: &'static str,
26 #[cfg_attr(not(test), allow(dead_code))]
27 meaning: &'static str,
28 emitted_by: &'static [&'static str],
29}
30
31macro_rules! builtin_codes {
32 (
33 $kind:ident, $registry:ident, $definitions:ident, $error:ident, $registry_doc:literal;
34 $($name:ident => $value:literal,
35 meaning = $meaning:literal,
36 emitted_by = [$($emitter:literal),+ $(,)?]),+ $(,)?
37 ) => {
38 impl $kind {
39 $(#[doc = $meaning] pub const $name: Self = Self($value);)+
40 }
41
42 #[doc = $registry_doc]
43 pub const $registry: &[$kind] = &[$($kind::$name),+];
44
45 const $definitions: &[BuiltinEvidenceCode] = &[
46 $(BuiltinEvidenceCode {
47 code: $value,
48 meaning: $meaning,
49 emitted_by: &[$($emitter),+],
50 }),+
51 ];
52
53 impl $kind {
54 fn builtin_definition(self) -> Option<&'static BuiltinEvidenceCode> {
55 $definitions.iter().find(|definition| definition.code == self.0)
56 }
57
58 fn validate_emitter(self, check_id: &'static str) -> Result<(), EvaluationError> {
59 if self
60 .builtin_definition()
61 .is_some_and(|definition| !definition.emitted_by.contains(&check_id))
62 {
63 return Err(EvaluationError::$error {
64 check_id,
65 code: self,
66 });
67 }
68 Ok(())
69 }
70 }
71 };
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
76#[serde(rename_all = "snake_case")]
77pub enum SelectionState {
78 Selected,
80 Unselected,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
86#[serde(rename_all = "snake_case")]
87pub enum ConfigurationState {
88 Enabled,
90 Disabled,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
97#[serde(rename_all = "snake_case")]
98pub enum Applicability {
99 Applicable,
101 NotApplicable,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
107#[serde(rename_all = "snake_case")]
108pub enum EvaluationState {
109 Complete,
111 Partial,
113 NotEvaluated,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
122#[serde(transparent)]
123pub struct EvaluationScopeCode(&'static str);
124
125builtin_codes!(
126 EvaluationScopeCode,
127 BUILTIN_EVALUATION_SCOPE_CODES,
128 BUILTIN_EVALUATION_SCOPE_CODE_DEFINITIONS,
129 BuiltinEvaluationScopeEmitterMismatch,
130 "Complete built-in evaluation-scope vocabulary for consumers that need to inspect or allow-list animsmith's catalog codes. Custom checks may use additional namespaced codes.";
131 FIRST_FRAME_REST_DELTA => "first_frame_rest_delta",
132 meaning = "The named clip's first-frame/rest-pose rotation evidence was evaluated.",
133 emitted_by = ["bind-pose"],
134 LOOP_CLOSURE => "loop_closure",
135 meaning = "One named clip's per-bone model-space pose closure was measured.",
136 emitted_by = ["loop-closure"],
137 DUPLICATE_LOOP_ENDPOINT => "duplicate_loop_endpoint",
138 meaning = "One named clip's authored tracks were analyzed for redundant closing endpoint keys.",
139 emitted_by = ["duplicate-loop-endpoint"],
140 LOOP_SEAM => "loop_seam",
141 meaning = "One named clip's positional loop seam was measured.",
142 emitted_by = ["loop-seam"],
143 LOOP_SEAM_VELOCITY => "loop_seam_velocity",
144 meaning = "One named clip's per-bone model-space seam velocity continuity was measured.",
145 emitted_by = ["loop-seam-vel"],
146 LOOP_SEAM_ROTATION => "loop_seam_rotation",
147 meaning = "One named clip's per-bone model-space angular seam velocity continuity was measured.",
148 emitted_by = ["loop-seam-rot"],
149 FOOT_STANCE => "foot_stance",
150 meaning = "Whole-clip prerequisites for stance analysis were evaluated.",
151 emitted_by = ["foot-slide"],
152 LEFT_FOOT_STANCE => "left_foot_stance",
153 meaning = "The named clip's left foot/toe stance was evaluated.",
154 emitted_by = ["foot-slide"],
155 RIGHT_FOOT_STANCE => "right_foot_stance",
156 meaning = "The named clip's right foot/toe stance was evaluated.",
157 emitted_by = ["foot-slide"],
158 ROOT_MOTION_SPEED => "root_motion_speed",
159 meaning = "One named clip's root-motion speed was measured.",
160 emitted_by = ["root-motion-speed"],
161 MEMBER_EXISTENCE => "member_existence",
162 meaning = "Configured group members were checked for existence.",
163 emitted_by = ["gait-group", "sync-group", "time-complement"],
164 PHASE_MEASUREMENT => "phase_measurement",
165 meaning = "One named clip's gait phase was measured or lacked usable evidence.",
166 emitted_by = ["gait-group", "time-complement"],
167 PHASE_COHERENCE => "phase_coherence",
168 meaning = "One named group's measurable gait phases were compared.",
169 emitted_by = ["gait-group", "time-complement"],
170 SYNC_MEMBER_MEASUREMENT => "sync_member_measurement",
171 meaning = "One named same-time sync-group member's timing evidence was measured.",
172 emitted_by = ["sync-group"],
173 SYNC_COMPATIBILITY => "sync_compatibility",
174 meaning = "One named same-time sync group had compatible member timing evidence compared.",
175 emitted_by = ["sync-group"],
176 TRAVEL_MODE => "travel_mode",
177 meaning = "One named clip's in-place/root-motion declaration was judged.",
178 emitted_by = ["in-place"],
179 FRAME_GRID => "frame_grid",
180 meaning = "The named clip's declared frame grid was evaluated.",
181 emitted_by = ["fps"],
182 REQUIRED_BONE_PRESENCE => "required_bone_presence",
183 meaning = "Configured structural skeleton-bone presence requirements were evaluated.",
184 emitted_by = ["required-bones"],
185 SELECTED_NODE_REST_SCALE => "selected_node_rest_scale",
186 meaning = "One configured source-node selector resolved and its effective rest-world linear scale was evaluated.",
187 emitted_by = ["rest-world-scale"],
188);
189
190impl EvaluationScopeCode {
191 pub const fn custom(code: &'static str) -> Self {
195 Self(code)
196 }
197
198 pub const fn as_str(self) -> &'static str {
200 self.0
201 }
202}
203
204impl fmt::Display for EvaluationScopeCode {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 f.write_str(self.0)
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
212pub struct EvaluationScope {
213 pub code: EvaluationScopeCode,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub subject: Option<String>,
218}
219
220impl EvaluationScope {
221 pub fn new(code: EvaluationScopeCode) -> Self {
223 Self {
224 code,
225 subject: None,
226 }
227 }
228
229 pub fn subject(mut self, subject: impl Into<String>) -> Self {
231 self.subject = Some(subject.into());
232 self
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
242#[serde(transparent)]
243pub struct CoverageGapCode(&'static str);
244
245builtin_codes!(
246 CoverageGapCode,
247 BUILTIN_COVERAGE_GAP_CODES,
248 BUILTIN_COVERAGE_GAP_CODE_DEFINITIONS,
249 BuiltinCoverageGapEmitterMismatch,
250 "Complete built-in coverage-gap vocabulary for consumers that need to inspect or allow-list animsmith's catalog codes. Custom checks may use additional namespaced codes.";
251 ROLES_UNRESOLVED => "roles_unresolved",
252 meaning = "Required semantic rig roles were not resolved.",
253 emitted_by = ["loop-seam", "root-motion-speed", "in-place", "foot-slide", "gait-group", "time-complement"],
254 MEASUREMENT_UNAVAILABLE => "measurement_unavailable",
255 meaning = "A required numeric measurement could not be produced or did not meet its evidence floor.",
256 emitted_by = ["loop-closure", "duplicate-loop-endpoint", "loop-seam", "loop-seam-vel", "loop-seam-rot", "root-motion-speed", "in-place", "foot-slide", "gait-group", "sync-group", "time-complement", "rest-world-scale"],
257 SKELETON_UNAVAILABLE => "skeleton_unavailable",
258 meaning = "Required skeleton presence work could not run because the file has no usable skeleton.",
259 emitted_by = ["required-bones"],
260 NODE_SELECTOR_NO_MATCH => "node_selector_no_match",
261 meaning = "A configured source-node selector matched no named source node.",
262 emitted_by = ["rest-world-scale"],
263 NODE_SELECTOR_AMBIGUOUS => "node_selector_ambiguous",
264 meaning = "A configured source-node selector matched more than one named source node.",
265 emitted_by = ["rest-world-scale"],
266 INSUFFICIENT_MEASURABLE_MEMBERS => "insufficient_measurable_members",
267 meaning = "Fewer than two configured group members produced usable comparison evidence.",
268 emitted_by = ["gait-group", "sync-group", "time-complement"],
269 MEMBERS_NOT_EVALUATED => "members_not_evaluated",
270 meaning = "Some configured group members did not produce usable comparison evidence.",
271 emitted_by = ["gait-group", "sync-group", "time-complement"],
272 INVALID_DECLARED_FPS => "invalid_declared_fps",
273 meaning = "A declared frame rate was zero, negative, or non-finite.",
274 emitted_by = ["fps"],
275 SYNC_FRAME_GRID_UNAVAILABLE => "sync_frame_grid_unavailable",
276 meaning = "A same-time sync-group member lacks usable declared frame-grid evidence.",
277 emitted_by = ["sync-group"],
278 INSUFFICIENT_ROTATION_EVIDENCE => "insufficient_rotation_evidence",
279 meaning = "Too few usable rotation tracks existed for a bind-pose comparison.",
280 emitted_by = ["bind-pose"],
281);
282
283impl CoverageGapCode {
284 pub const fn custom(code: &'static str) -> Self {
289 Self(code)
290 }
291
292 pub const fn as_str(self) -> &'static str {
294 self.0
295 }
296}
297
298impl fmt::Display for CoverageGapCode {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 f.write_str(self.0)
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
306pub struct CoverageGap {
307 pub code: CoverageGapCode,
309 pub message: String,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub scope: Option<EvaluationScope>,
314}
315
316impl CoverageGap {
317 pub fn new(code: CoverageGapCode, message: impl Into<String>) -> Self {
319 Self {
320 code,
321 message: message.into(),
322 scope: None,
323 }
324 }
325
326 pub fn scope(mut self, scope: EvaluationScope) -> Self {
328 self.scope = Some(scope);
329 self
330 }
331}
332
333#[derive(Debug, Clone)]
338pub struct CheckOutput {
339 findings: Vec<Finding>,
340 evaluated_scopes: Vec<EvaluationScope>,
341 gaps: Vec<CoverageGap>,
342}
343
344impl CheckOutput {
345 pub fn from_coverage(
351 findings: Vec<Finding>,
352 evaluated_scopes: Vec<EvaluationScope>,
353 gaps: Vec<CoverageGap>,
354 ) -> Self {
355 Self {
356 findings,
357 evaluated_scopes,
358 gaps,
359 }
360 }
361
362 pub fn findings(&self) -> &[Finding] {
364 &self.findings
365 }
366
367 pub fn evaluated_scopes(&self) -> &[EvaluationScope] {
369 &self.evaluated_scopes
370 }
371
372 pub fn gaps(&self) -> &[CoverageGap] {
374 &self.gaps
375 }
376}
377
378#[derive(Debug, Clone)]
380pub struct CheckEvaluation {
381 check_id: &'static str,
382 selection: SelectionState,
383 configuration: ConfigurationState,
384 applicability: Applicability,
385 output: CheckOutput,
386}
387
388impl CheckEvaluation {
389 pub fn evaluated(check_id: &'static str, output: CheckOutput) -> Result<Self, EvaluationError> {
398 if check_id.is_empty() {
399 return Err(EvaluationError::InvalidCheckId(check_id));
400 }
401 if !output.gaps.is_empty()
402 && output.evaluated_scopes.is_empty()
403 && !output.findings.is_empty()
404 {
405 return Err(EvaluationError::InvalidCheckOutput {
406 check_id,
407 reason: "not-evaluated output cannot carry content findings",
408 });
409 }
410 if let Some(finding) = output
411 .findings
412 .iter()
413 .find(|finding| finding.check_id != check_id)
414 {
415 return Err(EvaluationError::FindingCheckIdMismatch {
416 check_id,
417 finding_check_id: finding.check_id,
418 });
419 }
420 for scope in &output.evaluated_scopes {
421 if scope.code.as_str().is_empty() {
422 return Err(EvaluationError::InvalidCheckOutput {
423 check_id,
424 reason: "evaluated scope code cannot be empty",
425 });
426 }
427 scope.code.validate_emitter(check_id)?;
428 }
429 for gap in &output.gaps {
430 if gap.code.as_str().is_empty() {
431 return Err(EvaluationError::InvalidCheckOutput {
432 check_id,
433 reason: "coverage gap code cannot be empty",
434 });
435 }
436 gap.code.validate_emitter(check_id)?;
437 if let Some(scope) = &gap.scope {
438 if scope.code.as_str().is_empty() {
439 return Err(EvaluationError::InvalidCheckOutput {
440 check_id,
441 reason: "coverage gap scope code cannot be empty",
442 });
443 }
444 scope.code.validate_emitter(check_id)?;
445 }
446 }
447 Ok(Self {
448 check_id,
449 selection: SelectionState::Selected,
450 configuration: ConfigurationState::Enabled,
451 applicability: Applicability::Applicable,
452 output,
453 })
454 }
455
456 pub fn check_id(&self) -> &'static str {
458 self.check_id
459 }
460
461 pub fn selection(&self) -> SelectionState {
463 self.selection
464 }
465
466 pub fn configuration(&self) -> ConfigurationState {
468 self.configuration
469 }
470
471 pub fn applicability(&self) -> Applicability {
473 self.applicability
474 }
475
476 pub fn evaluation(&self) -> EvaluationState {
478 if self.selection == SelectionState::Unselected
479 || self.configuration == ConfigurationState::Disabled
480 || self.applicability == Applicability::NotApplicable
481 {
482 EvaluationState::NotEvaluated
483 } else if self.output.gaps.is_empty() {
484 EvaluationState::Complete
485 } else if self.output.evaluated_scopes.is_empty() {
486 EvaluationState::NotEvaluated
487 } else {
488 EvaluationState::Partial
489 }
490 }
491
492 pub fn findings(&self) -> &[Finding] {
494 self.output.findings()
495 }
496
497 pub fn evaluated_scopes(&self) -> &[EvaluationScope] {
499 self.output.evaluated_scopes()
500 }
501
502 pub fn gaps(&self) -> &[CoverageGap] {
504 self.output.gaps()
505 }
506
507 fn inactive(
508 check_id: &'static str,
509 selection: SelectionState,
510 configuration: ConfigurationState,
511 applicability: Applicability,
512 ) -> Self {
513 debug_assert!(
514 selection == SelectionState::Unselected
515 || configuration == ConfigurationState::Disabled
516 || applicability == Applicability::NotApplicable
517 );
518 Self {
519 check_id,
520 selection,
521 configuration,
522 applicability,
523 output: CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
524 }
525 }
526
527 fn override_severity(&mut self, severity: SeveritySetting) {
528 if let Some(severity) = severity.as_severity() {
529 for finding in &mut self.output.findings {
530 finding.severity = severity;
531 }
532 }
533 }
534}
535
536impl Serialize for CheckEvaluation {
537 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
538 where
539 S: Serializer,
540 {
541 let mut fields = 6;
542 fields += usize::from(!self.output.evaluated_scopes.is_empty());
543 fields += usize::from(!self.output.gaps.is_empty());
544 let mut state = serializer.serialize_struct("CheckEvaluation", fields)?;
545 state.serialize_field("check_id", &self.check_id)?;
546 state.serialize_field("selection", &self.selection)?;
547 state.serialize_field("configuration", &self.configuration)?;
548 state.serialize_field("applicability", &self.applicability)?;
549 state.serialize_field("evaluation", &self.evaluation())?;
550 state.serialize_field("findings", &self.output.findings)?;
551 if !self.output.evaluated_scopes.is_empty() {
552 state.serialize_field("evaluated_scopes", &self.output.evaluated_scopes)?;
553 }
554 if !self.output.gaps.is_empty() {
555 state.serialize_field("gaps", &self.output.gaps)?;
556 }
557 state.end()
558 }
559}
560
561#[derive(Debug, Clone, Copy)]
563pub enum CheckSelection<'a> {
564 All,
566 Only(&'a BTreeSet<String>),
568}
569
570impl CheckSelection<'_> {
571 fn contains(self, id: &str) -> bool {
572 match self {
573 Self::All => true,
574 Self::Only(ids) => ids.contains(id),
575 }
576 }
577}
578
579#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
581#[non_exhaustive]
582pub enum EvaluationError {
583 #[error("invalid configuration: {0}")]
585 InvalidConfiguration(#[from] ConfigValidationError),
586 #[error("check id cannot be empty")]
588 InvalidCheckId(&'static str),
589 #[error("duplicate check id {0:?}")]
591 DuplicateCheckId(&'static str),
592 #[error("unknown selected check id {0:?}")]
594 UnknownSelection(String),
595 #[error("check {check_id:?} emitted invalid output: {reason}")]
597 InvalidCheckOutput {
598 check_id: &'static str,
600 reason: &'static str,
602 },
603 #[error("check {check_id:?} emitted a finding for {finding_check_id:?}")]
605 FindingCheckIdMismatch {
606 check_id: &'static str,
608 finding_check_id: &'static str,
610 },
611 #[error("check {check_id:?} cannot emit built-in evaluation scope {code}")]
613 BuiltinEvaluationScopeEmitterMismatch {
614 check_id: &'static str,
616 code: EvaluationScopeCode,
618 },
619 #[error("check {check_id:?} cannot emit built-in coverage gap {code}")]
621 BuiltinCoverageGapEmitterMismatch {
622 check_id: &'static str,
624 code: CoverageGapCode,
626 },
627}
628
629pub fn evaluate_checks(
646 ctx: &CheckCtx<'_>,
647 checks: &[Box<dyn Check>],
648 selection: CheckSelection<'_>,
649) -> Result<Vec<CheckEvaluation>, EvaluationError> {
650 ctx.config.validate()?;
651
652 let mut catalog_ids = BTreeSet::new();
653 for check in checks {
654 if check.id().is_empty() {
655 return Err(EvaluationError::InvalidCheckId(check.id()));
656 }
657 if !catalog_ids.insert(check.id()) {
658 return Err(EvaluationError::DuplicateCheckId(check.id()));
659 }
660 }
661 if let CheckSelection::Only(selected) = selection
662 && let Some(unknown) = selected
663 .iter()
664 .find(|id| !catalog_ids.contains(id.as_str()))
665 {
666 return Err(EvaluationError::UnknownSelection(unknown.clone()));
667 }
668
669 let mut records = Vec::with_capacity(checks.len());
670 for check in checks {
671 let selection_state = if selection.contains(check.id()) {
672 SelectionState::Selected
673 } else {
674 SelectionState::Unselected
675 };
676 let setting = ctx.config.check_settings(check.id()).severity;
677 let configuration = match setting {
678 Some(SeveritySetting::Off) => ConfigurationState::Disabled,
679 Some(_) => ConfigurationState::Enabled,
680 None if check.enabled_by_default() => ConfigurationState::Enabled,
681 None => ConfigurationState::Disabled,
682 };
683 let applicability = check.applicability(ctx);
684
685 if selection_state == SelectionState::Unselected
686 || configuration == ConfigurationState::Disabled
687 || applicability == Applicability::NotApplicable
688 {
689 records.push(CheckEvaluation::inactive(
690 check.id(),
691 selection_state,
692 configuration,
693 applicability,
694 ));
695 continue;
696 }
697
698 let mut evaluation = CheckEvaluation::evaluated(check.id(), check.evaluate(ctx))?;
699 if let Some(setting) = setting {
700 evaluation.override_severity(setting);
701 }
702 records.push(evaluation);
703 }
704 Ok(records)
705}
706
707#[cfg(test)]
708mod authority_contract {
709 use std::collections::BTreeSet;
710 use std::path::{Path, PathBuf};
711
712 use super::{
713 BUILTIN_COVERAGE_GAP_CODE_DEFINITIONS, BUILTIN_EVALUATION_SCOPE_CODE_DEFINITIONS,
714 BuiltinEvidenceCode, CheckEvaluation, CheckOutput, CoverageGap, CoverageGapCode,
715 EvaluationError, EvaluationScope, EvaluationScopeCode,
716 };
717
718 fn assert_reference_table(docs: &str, heading: &str, entries: &[BuiltinEvidenceCode]) {
719 let section = docs
720 .split_once(heading)
721 .unwrap_or_else(|| panic!("missing reference heading {heading:?}"))
722 .1;
723 let documented = section
724 .lines()
725 .skip_while(|line| line.trim().is_empty())
726 .take_while(|line| !line.trim().is_empty())
727 .filter(|line| line.starts_with("| `"))
728 .collect::<Vec<_>>();
729 let expected = entries
730 .iter()
731 .map(|definition| {
732 let BuiltinEvidenceCode {
733 code,
734 meaning,
735 emitted_by,
736 } = definition;
737 assert!(
738 !meaning.trim().is_empty() && !meaning.contains(['\r', '\n']),
739 "{code} must have a one-line meaning"
740 );
741 let emitters = emitted_by
742 .iter()
743 .map(|check_id| format!("`{check_id}`"))
744 .collect::<Vec<_>>()
745 .join(", ");
746 format!("| `{code}` | {meaning} | {emitters} |")
747 })
748 .collect::<Vec<_>>();
749
750 assert_eq!(documented.len(), expected.len(), "row count for {heading}");
751 let documented = documented.into_iter().collect::<BTreeSet<_>>();
752 assert_eq!(
753 documented.len(),
754 expected.len(),
755 "duplicate rows for {heading}"
756 );
757 let expected = expected.iter().map(String::as_str).collect::<BTreeSet<_>>();
758 assert_eq!(documented, expected, "exact rows for {heading}");
759 }
760
761 #[test]
762 fn output_docs_match_registered_builtin_evidence_codes_exactly() {
763 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
764 let Some(workspace_root) = source_workspace_root(manifest_dir) else {
765 return;
767 };
768 let docs_path = workspace_root.join("docs/output.md");
769 let docs = std::fs::read_to_string(&docs_path)
770 .unwrap_or_else(|error| panic!("cannot read {}: {error}", docs_path.display()));
771 let crlf = docs.lines().collect::<Vec<_>>().join("\r\n");
772 for line_endings in [docs.as_str(), crlf.as_str()] {
773 assert_reference_table(
774 line_endings,
775 "Built-in gap codes are:",
776 BUILTIN_COVERAGE_GAP_CODE_DEFINITIONS,
777 );
778 assert_reference_table(
779 line_endings,
780 "Built-in completed/gap scope codes are:",
781 BUILTIN_EVALUATION_SCOPE_CODE_DEFINITIONS,
782 );
783 }
784 }
785
786 #[test]
787 fn builtin_evidence_authority_has_unique_codes_and_known_emitters() {
788 let catalog_ids = crate::all_checks()
789 .into_iter()
790 .map(|check| check.id())
791 .collect::<BTreeSet<_>>();
792 let authorities = [
793 ("coverage-gap", BUILTIN_COVERAGE_GAP_CODE_DEFINITIONS),
794 (
795 "evaluation-scope",
796 BUILTIN_EVALUATION_SCOPE_CODE_DEFINITIONS,
797 ),
798 ];
799
800 for (kind, definitions) in authorities {
801 assert!(
802 definitions
803 .iter()
804 .all(|definition| !definition.code.is_empty()),
805 "{kind} authority codes must be nonempty"
806 );
807 let defined_codes = definitions
808 .iter()
809 .map(|definition| definition.code)
810 .collect::<BTreeSet<_>>();
811 assert_eq!(
812 defined_codes.len(),
813 definitions.len(),
814 "duplicate {kind} authority code"
815 );
816 for definition in definitions {
817 let emitters = definition
818 .emitted_by
819 .iter()
820 .copied()
821 .collect::<BTreeSet<_>>();
822 assert_eq!(
823 emitters.len(),
824 definition.emitted_by.len(),
825 "{} has duplicate emitters",
826 definition.code
827 );
828 for emitter in emitters {
829 assert!(
830 catalog_ids.contains(emitter),
831 "{} declares unknown emitter {emitter:?}",
832 definition.code
833 );
834 }
835 }
836 }
837 }
838
839 #[test]
840 fn every_builtin_code_enforces_its_emitter_matrix() {
841 let catalog_ids = crate::all_checks()
842 .into_iter()
843 .map(|check| check.id())
844 .collect::<Vec<_>>();
845
846 for definition in BUILTIN_EVALUATION_SCOPE_CODE_DEFINITIONS {
847 for &check_id in &catalog_ids {
848 let code = EvaluationScopeCode::custom(definition.code);
849 let completed = CheckEvaluation::evaluated(
850 check_id,
851 CheckOutput::from_coverage(
852 Vec::new(),
853 vec![EvaluationScope::new(code)],
854 Vec::new(),
855 ),
856 );
857 let gap_scope = CheckEvaluation::evaluated(
858 check_id,
859 CheckOutput::from_coverage(
860 Vec::new(),
861 Vec::new(),
862 vec![
863 CoverageGap::new(CoverageGapCode::custom("test:gap"), "gap")
864 .scope(EvaluationScope::new(code)),
865 ],
866 ),
867 );
868 if definition.emitted_by.contains(&check_id) {
869 assert!(
870 completed.is_ok(),
871 "{} must allow {check_id:?}",
872 definition.code
873 );
874 assert!(
875 gap_scope.is_ok(),
876 "{} must allow {check_id:?}",
877 definition.code
878 );
879 } else {
880 let expected =
881 EvaluationError::BuiltinEvaluationScopeEmitterMismatch { check_id, code };
882 assert_eq!(completed.unwrap_err(), expected);
883 assert_eq!(gap_scope.unwrap_err(), expected);
884 }
885 }
886 }
887
888 for definition in BUILTIN_COVERAGE_GAP_CODE_DEFINITIONS {
889 for &check_id in &catalog_ids {
890 let code = CoverageGapCode::custom(definition.code);
891 let gap = CheckEvaluation::evaluated(
892 check_id,
893 CheckOutput::from_coverage(
894 Vec::new(),
895 Vec::new(),
896 vec![CoverageGap::new(code, "test gap")],
897 ),
898 );
899 if definition.emitted_by.contains(&check_id) {
900 assert!(gap.is_ok(), "{} must allow {check_id:?}", definition.code);
901 } else {
902 assert_eq!(
903 gap.unwrap_err(),
904 EvaluationError::BuiltinCoverageGapEmitterMismatch { check_id, code }
905 );
906 }
907 }
908 }
909 }
910
911 #[test]
912 fn source_workspace_detection_has_a_positive_checkout_control() {
913 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
914 let detected = source_workspace_root(manifest_dir);
915 if manifest_dir.join(".cargo_vcs_info.json").is_file() {
916 assert!(detected.is_none(), "published packages must skip repo docs");
917 return;
918 }
919
920 let expected = manifest_dir.join("../..");
921 if expected.join("docs/output.md").is_file() {
922 assert_eq!(
923 detected.as_deref(),
924 Some(expected.as_path()),
925 "the exact source checkout must enforce its output docs"
926 );
927 }
928 }
929
930 fn source_workspace_root(manifest_dir: &Path) -> Option<PathBuf> {
931 if manifest_dir.join(".cargo_vcs_info.json").is_file() {
932 return None;
933 }
934 let workspace_root = manifest_dir.join("../..");
935 let current_manifest = manifest_dir.join("Cargo.toml").canonicalize().ok()?;
936 let workspace_manifest = workspace_root
937 .join("crates/animsmith-core/Cargo.toml")
938 .canonicalize()
939 .ok()?;
940 (current_manifest == workspace_manifest).then_some(workspace_root)
941 }
942}