Skip to main content

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}