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::finding::{Finding, Severity};
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 — for the readiness
71 /// predicates that scan every clip for pending work.
72 pub fn clip_expectations(&self) -> &[ClipExpectations] {
73 &self.expectations
74 }
75}
76
77/// Whether a check can run against a document — decided by the runner
78/// *before* `run`, so requirement diagnostics are emitted in one place
79/// and never subject to per-check severity overrides.
80pub enum Readiness {
81 /// Requirements met; run the check.
82 Ready,
83 /// The check has pending work (relevant expectations are declared)
84 /// but a prerequisite — a rig role — is unresolved. The runner
85 /// emits one standardized skip-note carrying `reason` at `Note`
86 /// severity, exempt from overrides. `reason` states what is needed.
87 Skipped(String),
88 /// No pending work for this document/config; stay silent.
89 Idle,
90}
91
92/// A lint check that can inspect a document and emit structured
93/// [`Finding`] values.
94///
95/// Custom embedders may implement this trait and pass their checks to
96/// [`run_checks`] alongside, or instead of, [`all_checks`]. Implementors
97/// should keep `run` panic-free for loader-valid documents; use
98/// [`Check::readiness`] to describe missing rig roles or configuration
99/// prerequisites.
100pub trait Check {
101 /// Stable identifier, e.g. `"loop-seam"`. Used in config, JSON
102 /// output, and `--select`.
103 fn id(&self) -> &'static str;
104
105 /// Whether the check's prerequisites are met. The default is
106 /// [`Readiness::Ready`] — mechanical checks need no rig or config.
107 /// Role-dependent checks override this to declare their needs so
108 /// the runner, not the check, owns skip-note emission.
109 fn readiness(&self, _ctx: &CheckCtx) -> Readiness {
110 Readiness::Ready
111 }
112
113 /// Execute the check and append any findings to `out`.
114 fn run(&self, ctx: &CheckCtx, out: &mut Vec<Finding>);
115}
116
117/// The mechanical P0 checks: no rig profile, no config required.
118pub fn mechanical_checks() -> Vec<Box<dyn Check>> {
119 vec![
120 Box::new(crate::checks::nan::Nan),
121 Box::new(crate::checks::time_monotonic::TimeMonotonic),
122 Box::new(crate::checks::quat_norm::QuatNorm),
123 Box::new(crate::checks::quat_flip::QuatFlip),
124 Box::new(crate::checks::duration_sanity::DurationSanity),
125 Box::new(crate::checks::scale_keys::ScaleKeys),
126 Box::new(crate::checks::constant_track::ConstantTrack),
127 ]
128}
129
130/// The full built-in catalog: mechanical + semantic checks.
131pub fn all_checks() -> Vec<Box<dyn Check>> {
132 let mut checks = mechanical_checks();
133 checks.push(Box::new(crate::checks::missing_bones::MissingBones));
134 checks.push(Box::new(crate::checks::frozen_bone::FrozenBone));
135 checks.push(Box::new(crate::checks::loop_seam::LoopSeam));
136 checks.push(Box::new(crate::checks::root_motion_speed::RootMotionSpeed));
137 checks.push(Box::new(crate::checks::gait_group::GaitGroup));
138 checks.push(Box::new(crate::checks::in_place::InPlace));
139 checks.push(Box::new(crate::checks::fps::Fps));
140 checks.push(Box::new(crate::checks::bind_pose::BindPose));
141 checks.push(Box::new(crate::checks::foot_slide::FootSlide));
142 checks
143}
144
145/// Run `checks`, honouring per-check severity settings:
146///
147/// - `severity = "off"` removes the check from the run set — it never
148/// executes (no wasted sampling, no discarded findings).
149/// - Any other override replaces the severity of the check's
150/// *violations*. Diagnostics — the requirement skip-notes emitted by
151/// the runner (via [`Check::readiness`]) and any a check marks with
152/// [`Finding::as_diagnostic`] — are exempt, so declaring `severity =
153/// "error"` never turns a "roles unresolved" note into a false
154/// failure.
155pub fn run_checks(ctx: &CheckCtx, checks: &[Box<dyn Check>]) -> Vec<Finding> {
156 use crate::config::SeveritySetting;
157
158 let mut out = Vec::new();
159 for check in checks {
160 let setting = ctx.config.check_settings(check.id()).severity;
161 if setting == Some(SeveritySetting::Off) {
162 continue; // off removes the check from the run set
163 }
164 match check.readiness(ctx) {
165 Readiness::Idle => {}
166 Readiness::Skipped(reason) => {
167 out.push(
168 Finding::new(check.id(), Severity::Note, format!("skipped: {reason}"))
169 .as_diagnostic(),
170 );
171 }
172 Readiness::Ready => {
173 let mut findings = Vec::new();
174 check.run(ctx, &mut findings);
175 if let Some(severity) = setting.and_then(SeveritySetting::as_severity) {
176 for f in &mut findings {
177 if !f.diagnostic {
178 f.severity = severity;
179 }
180 }
181 }
182 out.append(&mut findings);
183 }
184 }
185 }
186 out
187}