open-standards/wai/wai-pack/src/classify.rs
Transaction Science 6012a7c3cf wai: a package is bound to the policy its receipt signs; malformed packages are refused, never panicked on; floor key frames and members checked against the package; receipt JSON read strictly; MoQ dependencies never dangle
Review round 2, items 1-10:
- 1: §11.3 rewritten in order. The verifier (Rust and Python) requires the receipt's
  components to be the policy's by id, order and media (policy-component-mismatch,
  so a zero-component receipt is refused); runs pack-id and §3.6's capability rules
  under the effective policy; holds each index's timebase, media, string table,
  class records, every entry's role/capability/layer, and every piece's rules,
  moments and class plan to the policy (index-policy-mismatch). Seal and verify
  refuse a receipt with no component (receipt-index-count).
- 2: verify reads objects by get(), skips the floor and catalogs once anything has
  failed; the catalog renderers return Result (a start past 2^53 - 1 is
  catalog-mismatch); the fragment time uses checked_mul; Python maps encoding,
  overflow and value errors to refusals. New index rule window-start: an index with
  no previous window starts at piece 0, entry 0 and time 0 (§4.5), in both.
- 3: each floor piece opens on a shown key frame, and holds no other unless
  extra_keyframes; every sequence header is the first's bytes (both, §11.3).
- 4: both readers refuse a member named twice before reading the version; the
  package's receipt file must be its canonical bytes; Python refuses -0.
- 5: a refinement track depends on the tracks carrying what its entries refine
  (MSF's depends is all-of); never a track the catalog lacks. catalogs/ corpus.
- 6: Rust binds a supplied floor's renderer, settings, framing header and target
  (floor-object-mismatch); SPEC §7.1 says a profile may fix those members.
- 7: verify reads the directory strictly (dot-names, *.sha256 included).
- 8: Python's manifest parse is the typed one; the object differential is 0.
- 9: Python refuses two components of one id (receipt-component).
- 10: v2 writes every member, energy_provenance null when unmetered, evidence
  exactly as its class carries; v1 keeps reading an absent class.
Regression tests: tests/review.rs (the review's probes, re-sealed with the key, and a
seeded run of random alterations that never panics); mutation corpus receipt cases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 07:42:47 -04:00

86 lines
3.3 KiB
Rust

//! Piece classification ([packaging] §4.6), from the statistics the segmenter
//! gathers. Precedence: static, then synthetic, then high motion, then natural.
//!
//! - `mad_milli = abs_diff · 1000 / compared`, 0 for a one-frame piece: the mean
//! absolute luma change between consecutive frames, in thousandths.
//! - `changed_permille = changed · 1000 / compared`, 0 for a one-frame piece.
//! - static: `mad_milli ≤ static_mad_milli` and `changed_permille ≤ static_permille`;
//! - synthetic: the first frame's histogram has at most `synthetic_bins` non-empty bins;
//! - high motion: `mad_milli ≥ motion_high_milli`.
//!
//! [packaging]: ../../../extensions/packaging.md
use crate::policy::ClassifyParams;
use crate::scenecut::PieceStats;
/// A piece's kind. The discriminant is the stage index's `kind` code.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PieceKind {
Natural = 0,
Static = 1,
Synthetic = 2,
MotionHigh = 3,
}
impl PieceKind {
/// The kind a stage index's `kind` code names, of the four a video piece has.
pub fn from_code(c: u8) -> Option<PieceKind> {
[PieceKind::Natural, PieceKind::Static, PieceKind::Synthetic, PieceKind::MotionHigh].get(c as usize).copied()
}
/// The policy's spelling (`rule.when.piece_kind`).
pub fn label(self) -> &'static str {
match self {
PieceKind::Natural => "natural",
PieceKind::Static => "static",
PieceKind::Synthetic => "synthetic",
PieceKind::MotionHigh => "motion_high",
}
}
}
/// `mad_milli` of a piece.
pub fn mad_milli(s: &PieceStats) -> u64 {
(s.abs_diff * 1000).checked_div(s.compared).unwrap_or(0)
}
/// `changed_permille` of a piece.
pub fn changed_permille(s: &PieceStats) -> u64 {
(s.changed * 1000).checked_div(s.compared).unwrap_or(0)
}
/// The kind of a piece.
pub fn classify(s: &PieceStats, p: &ClassifyParams) -> PieceKind {
let mad = mad_milli(s);
if mad <= p.static_mad_milli && changed_permille(s) <= p.static_permille {
PieceKind::Static
} else if s.occupancy as u64 <= p.synthetic_bins {
PieceKind::Synthetic
} else if mad >= p.motion_high_milli {
PieceKind::MotionHigh
} else {
PieceKind::Natural
}
}
#[cfg(test)]
mod tests {
use super::*;
const P: ClassifyParams = ClassifyParams { static_permille: 5, static_mad_milli: 300, synthetic_bins: 12, motion_high_milli: 9000 };
#[test]
fn precedence_and_thresholds() {
let st = |abs_diff, changed, occupancy| PieceStats { abs_diff, changed, compared: 1000, occupancy };
assert_eq!(classify(&st(300, 5, 40), &P), PieceKind::Static);
// Static and synthetic both hold: static comes first.
assert_eq!(classify(&st(0, 0, 3), &P), PieceKind::Static);
assert_eq!(classify(&st(301, 5, 40), &P), PieceKind::Natural);
assert_eq!(classify(&st(300, 6, 12), &P), PieceKind::Synthetic);
assert_eq!(classify(&st(9000, 900, 12), &P), PieceKind::Synthetic);
assert_eq!(classify(&st(9000, 900, 13), &P), PieceKind::MotionHigh);
assert_eq!(classify(&st(8999, 900, 13), &P), PieceKind::Natural);
// A one-frame piece compares nothing: static.
assert_eq!(classify(&PieceStats { occupancy: 40, ..PieceStats::default() }, &P), PieceKind::Static);
}
}