animsmith_core/check.rs
1//! The check abstraction, its execution context, and the built-in
2//! check sets.
3
4use crate::config::{ClipExpectations, Config};
5use crate::evaluation::{Applicability, CheckOutput};
6use crate::metrics::MetricGrids;
7use crate::model::Document;
8use crate::profile::ResolvedRoles;
9use crate::sample::PoseGrid;
10use std::rc::Rc;
11
12/// Everything a check may consume: the document, the resolved rig
13/// roles, the configuration, and shared metric [`PoseGrid`] samples.
14#[derive(Debug)]
15pub struct CheckCtx<'a> {
16 /// Document being checked.
17 pub doc: &'a Document,
18 /// Resolved rig roles for semantic checks.
19 pub roles: &'a ResolvedRoles,
20 /// Effective configuration for this run.
21 pub config: &'a Config,
22 grids: &'a MetricGrids<'a>,
23 /// Effective per-clip expectations, resolved once and aligned to
24 /// `doc.clips`. Resolving them means overlaying every matching glob
25 /// entry (see [`Config::expectations_for`]); caching here keeps that
26 /// off the per-check hot loop, which otherwise re-resolved the same
27 /// clip once per check that reads expectations.
28 expectations: Vec<ClipExpectations>,
29}
30
31impl<'a> CheckCtx<'a> {
32 /// Build a check context that shares metric pose grids with
33 /// measurement or report generation.
34 ///
35 /// `roles` must already reflect any [`Config::rig`](crate::Config::rig)
36 /// profile and inline overrides; constructing a context does not resolve
37 /// that declarative configuration.
38 pub fn new(grids: &'a MetricGrids<'a>, roles: &'a ResolvedRoles, config: &'a Config) -> Self {
39 let doc = grids.document();
40 let expectations = doc
41 .clips
42 .iter()
43 .map(|c| config.expectations_for(&c.name))
44 .collect();
45 Self {
46 doc,
47 roles,
48 config,
49 grids,
50 expectations,
51 }
52 }
53
54 /// The metric pose grid for clip `clip_index`, computed once and
55 /// shared. `None` for clips too short to carry a cycle.
56 pub fn grid(&self, clip_index: usize) -> Option<Rc<PoseGrid>> {
57 self.grids.grid(clip_index)
58 }
59
60 /// Effective expectations for clip `clip_index` (resolved once in
61 /// [`CheckCtx::new`]). Index into `doc.clips`.
62 ///
63 /// # Panics
64 ///
65 /// Panics if `clip_index` is outside the document's clip range.
66 pub fn expectations(&self, clip_index: usize) -> &ClipExpectations {
67 &self.expectations[clip_index]
68 }
69
70 /// Per-clip expectations in `doc.clips` order, used by cheap
71 /// applicability predicates that scan for declared work.
72 pub fn clip_expectations(&self) -> &[ClipExpectations] {
73 &self.expectations
74 }
75}
76
77/// A lint check that can inspect a document and emit typed evaluation
78/// coverage plus structured content findings.
79///
80/// Custom embedders may implement this trait and pass their checks to
81/// [`crate::evaluate_checks`] alongside, or instead of, [`all_checks`].
82/// Implementors should keep both methods panic-free for loader-valid
83/// documents. Applicability describes whether declared work exists;
84/// unavailable prerequisites or measurements belong in typed coverage gaps
85/// returned from [`Check::evaluate`]. Custom checks should use namespaced
86/// scope and gap codes. Built-in code values are reserved to the checks named
87/// by animsmith's evidence-code authority, and the evaluation boundary rejects
88/// a built-in value emitted by any other check id.
89pub trait Check {
90 /// Stable identifier, e.g. `"loop-seam"`. Used in config, JSON
91 /// output, and `--select`.
92 fn id(&self) -> &'static str;
93
94 /// Whether the check runs when its configuration has no explicit
95 /// `severity` setting.
96 ///
97 /// Most checks are enabled by default. Checks for intentionally opt-in
98 /// policy signals may return `false`; setting their severity to `note`,
99 /// `warn`, or `error` enables them, while `off` keeps them disabled.
100 fn enabled_by_default(&self) -> bool {
101 true
102 }
103
104 /// Whether this document and configuration declare work for the check.
105 ///
106 /// The runner calls this cheap predicate even for disabled or unselected
107 /// checks so applicability remains an independent result dimension. It
108 /// must not perform the check's substantive evaluation.
109 fn applicability(&self, _ctx: &CheckCtx) -> Applicability {
110 Applicability::Applicable
111 }
112
113 /// Evaluate every modelled work unit, returning content findings and
114 /// explicit coverage. Missing prerequisites are gaps, never findings.
115 fn evaluate(&self, ctx: &CheckCtx) -> CheckOutput;
116}
117
118/// The mechanical P0 catalog: no rig roles or clip expectations required.
119/// Individual policy signals may still be disabled by default and enabled by
120/// an explicit severity.
121pub fn mechanical_checks() -> Vec<Box<dyn Check>> {
122 vec![
123 Box::new(crate::checks::nan::Nan),
124 Box::new(crate::checks::time_monotonic::TimeMonotonic),
125 Box::new(crate::checks::quat_norm::QuatNorm),
126 Box::new(crate::checks::quat_flip::QuatFlip),
127 Box::new(crate::checks::duration_sanity::DurationSanity),
128 Box::new(crate::checks::scale_keys::ScaleKeys),
129 Box::new(crate::checks::non_uniform_scale::NonUniformScale),
130 Box::new(crate::checks::constant_nonunit_scale::ConstantNonunitScale),
131 Box::new(crate::checks::constant_track::ConstantTrack),
132 ]
133}
134
135/// The full built-in catalog: mechanical + semantic checks.
136pub fn all_checks() -> Vec<Box<dyn Check>> {
137 let mut checks = mechanical_checks();
138 checks.push(Box::new(crate::checks::required_bones::RequiredBones));
139 checks.push(Box::new(crate::checks::rest_world_scale::RestWorldScale));
140 checks.push(Box::new(crate::checks::missing_bones::MissingBones));
141 checks.push(Box::new(crate::checks::frozen_bone::FrozenBone));
142 checks.push(Box::new(
143 crate::checks::duplicate_loop_endpoint::DuplicateLoopEndpoint,
144 ));
145 checks.push(Box::new(crate::checks::loop_closure::LoopClosure));
146 checks.push(Box::new(crate::checks::loop_seam::LoopSeam));
147 checks.push(Box::new(crate::checks::loop_seam_vel::LoopSeamVelocity));
148 checks.push(Box::new(crate::checks::loop_seam_rot::LoopSeamRotation));
149 checks.push(Box::new(crate::checks::root_motion_speed::RootMotionSpeed));
150 checks.push(Box::new(crate::checks::gait_group::GaitGroup));
151 checks.push(Box::new(crate::checks::sync_group::SyncGroupCheck));
152 checks.push(Box::new(crate::checks::time_complement::TimeComplement));
153 checks.push(Box::new(crate::checks::in_place::InPlace));
154 checks.push(Box::new(crate::checks::fps::Fps));
155 checks.push(Box::new(crate::checks::bind_pose::BindPose));
156 checks.push(Box::new(crate::checks::foot_slide::FootSlide));
157 checks
158}