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