diff --git a/crates/labcolors-core/src/joint.rs b/crates/labcolors-core/src/joint.rs new file mode 100644 index 00000000..324583dd --- /dev/null +++ b/crates/labcolors-core/src/joint.rs @@ -0,0 +1,705 @@ +//! Приватный V2a-срез совместного point-selection. +//! +//! Один code-owned program связывает две Paint-переменные через реальный +//! `lower occurrence -> visible surface -> upper occurrence`. Candidate domain, +//! полный hard-report, declared policy и fresh recheck являются разными типами. +//! Модуль не знает клиентских recipes, role taxonomy или legacy solver state и +//! не минтит terminal output certificate. + +use crate::Srgb8; +use crate::appearance::{ + EncodedPointPaintV1, PaintId, PointOpacityOverSurfaceV1, ResolvedOccurrence, SurfaceInputPortId, +}; +use crate::constraints::{ + ExactPassEvidenceV1, ExactSrgb8IdentityV1, ExactViolationEvidenceV1, HardDecision, + assess_visible_point_hard, +}; +use crate::observation::{RevisionBoundObservationV1, ScenarioId}; + +/// Canonical identity одного joint candidate. Число не является declaration +/// order, расстоянием или скрытым приоритетом. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct CandidateOrdinalV1(u32); + +impl CandidateOrdinalV1 { + pub(crate) const fn new(raw: u32) -> Self { + Self(raw) + } +} + +/// Две solver-owned Paint-переменные одного code-owned joint program. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct JointCandidateTupleV1 { + ordinal: CandidateOrdinalV1, + lower: EncodedPointPaintV1, + upper: EncodedPointPaintV1, +} + +impl JointCandidateTupleV1 { + pub(crate) const fn new( + ordinal: CandidateOrdinalV1, + lower: EncodedPointPaintV1, + upper: EncodedPointPaintV1, + ) -> Self { + Self { + ordinal, + lower, + upper, + } + } +} + +/// Order-free candidate domain. Policy не участвует в его construction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct JointCandidateSetV1 { + candidates: Box<[JointCandidateTupleV1]>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CandidateSetErrorV1 { + Empty, + DuplicateOrdinal(CandidateOrdinalV1), + DuplicatePhysicalTuple { + first: CandidateOrdinalV1, + second: CandidateOrdinalV1, + }, +} + +impl JointCandidateSetV1 { + pub(crate) fn new( + mut candidates: Vec, + ) -> Result { + if candidates.is_empty() { + return Err(CandidateSetErrorV1::Empty); + } + candidates.sort_unstable_by_key(|candidate| candidate.ordinal); + for pair in candidates.windows(2) { + if pair[0].ordinal == pair[1].ordinal { + return Err(CandidateSetErrorV1::DuplicateOrdinal(pair[0].ordinal)); + } + } + for (index, first) in candidates.iter().enumerate() { + if let Some(second) = candidates[index + 1..] + .iter() + .find(|second| first.lower == second.lower && first.upper == second.upper) + { + return Err(CandidateSetErrorV1::DuplicatePhysicalTuple { + first: first.ordinal, + second: second.ordinal, + }); + } + } + Ok(Self { + candidates: candidates.into_boxed_slice(), + }) + } + + pub(crate) fn candidates(&self) -> &[JointCandidateTupleV1] { + &self.candidates + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct JointConstraintIdV1(u32); + +impl JointConstraintIdV1 { + pub(crate) const fn new(raw: u32) -> Self { + Self(raw) + } +} + +/// Physical occurrence, к которому относится exact hard predicate. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum JointVisibleTargetV1 { + Lower, + Upper, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct JointHardConstraintV1 { + id: JointConstraintIdV1, + target: JointVisibleTargetV1, + invocation: Srgb8, +} + +impl JointHardConstraintV1 { + pub(crate) const fn exact( + id: JointConstraintIdV1, + target: JointVisibleTargetV1, + invocation: Srgb8, + ) -> Self { + Self { + id, + target, + invocation, + } + } +} + +/// Identity первой private joint topology. Она не является public Program ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum JointPointProgramIdentityV1 { + TwoPaintDerivedSurfaceExactPointV1, +} + +/// Две связанные occurrences над одним observed root backdrop. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct JointPointProgramV1 { + root_surface: SurfaceInputPortId, + lower_paint: PaintId, + upper_paint: PaintId, + constraints: Box<[JointHardConstraintV1]>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum JointProgramErrorV1 { + SamePaintIdentity(PaintId), + EmptyHardConstraintSet, + DuplicateConstraint(JointConstraintIdV1), +} + +impl JointPointProgramV1 { + pub(crate) fn new( + root_surface: SurfaceInputPortId, + lower_paint: PaintId, + upper_paint: PaintId, + mut constraints: Vec, + ) -> Result { + if lower_paint == upper_paint { + return Err(JointProgramErrorV1::SamePaintIdentity(lower_paint)); + } + if constraints.is_empty() { + return Err(JointProgramErrorV1::EmptyHardConstraintSet); + } + constraints.sort_unstable_by_key(|constraint| constraint.id); + for pair in constraints.windows(2) { + if pair[0].id == pair[1].id { + return Err(JointProgramErrorV1::DuplicateConstraint(pair[0].id)); + } + } + Ok(Self { + root_surface, + lower_paint, + upper_paint, + constraints: constraints.into_boxed_slice(), + }) + } + + const fn identity(&self) -> JointPointProgramIdentityV1 { + JointPointProgramIdentityV1::TwoPaintDerivedSurfaceExactPointV1 + } + + pub(crate) fn evaluate( + &self, + candidates: JointCandidateSetV1, + observation: RevisionBoundObservationV1, + ) -> Result { + self.validate_candidates(&candidates)?; + let root_index = observation + .schema() + .binary_search(&self.root_surface) + .map_err(|_| JointReportErrorV1::MissingRootSurface(self.root_surface))?; + let (execution_count, cell_count) = checked_joint_cardinality( + candidates.candidates.len(), + observation.set().cases().len(), + self.constraints.len(), + )?; + let matrices = self.execute( + candidates.candidates(), + &observation, + root_index, + execution_count, + cell_count, + )?; + Ok(FullHardReportV1 { + program_identity: self.identity(), + program: self.clone(), + candidates, + observation, + executions: matrices.executions, + cells: matrices.cells, + }) + } + + fn validate_candidates( + &self, + candidates: &JointCandidateSetV1, + ) -> Result<(), JointReportErrorV1> { + for candidate in candidates.candidates() { + if candidate.lower.id() != self.lower_paint { + return Err(JointReportErrorV1::CandidatePaintMismatch { + ordinal: candidate.ordinal, + stage: JointVisibleTargetV1::Lower, + expected: self.lower_paint, + actual: candidate.lower.id(), + }); + } + if candidate.upper.id() != self.upper_paint { + return Err(JointReportErrorV1::CandidatePaintMismatch { + ordinal: candidate.ordinal, + stage: JointVisibleTargetV1::Upper, + expected: self.upper_paint, + actual: candidate.upper.id(), + }); + } + } + Ok(()) + } + + fn execute( + &self, + candidates: &[JointCandidateTupleV1], + observation: &RevisionBoundObservationV1, + root_index: usize, + execution_count: usize, + cell_count: usize, + ) -> Result { + let mut executions = Vec::new(); + executions + .try_reserve_exact(execution_count) + .map_err(|_| JointReportErrorV1::ResourceExhausted)?; + let mut cells = Vec::new(); + cells + .try_reserve_exact(cell_count) + .map_err(|_| JointReportErrorV1::ResourceExhausted)?; + + for candidate in candidates { + for (case_index, case) in observation.set().cases().iter().enumerate() { + let root = case.bindings()[root_index]; + let lower = PointOpacityOverSurfaceV1::evaluate_admitted( + candidate.lower.source().bytes(), + candidate.lower.opacity(), + root.bytes(), + ); + let upper = PointOpacityOverSurfaceV1::evaluate_admitted( + candidate.upper.source().bytes(), + candidate.upper.opacity(), + lower.visible(), + ); + debug_assert_eq!(upper.certificate().backdrop_rgb(), lower.visible()); + + executions.push(JointExecutionRecordV1 { + ordinal: candidate.ordinal, + case_index, + lower_paint: candidate.lower, + upper_paint: candidate.upper, + lower, + upper, + }); + + for constraint in self.constraints.iter().copied() { + let occurrence = match constraint.target { + JointVisibleTargetV1::Lower => &lower, + JointVisibleTargetV1::Upper => &upper, + }; + let decision = match assess_visible_point_hard( + occurrence, + &ExactSrgb8IdentityV1, + constraint.invocation, + ) { + Ok(HardDecision::Pass(evidence)) => { + JointConstraintDecisionV1::Pass(evidence) + } + Ok(HardDecision::Violation(evidence)) => { + JointConstraintDecisionV1::Violation(evidence) + } + Err(error) => match error {}, + }; + cells.push(JointConstraintCellV1 { + ordinal: candidate.ordinal, + constraint: constraint.id, + target: constraint.target, + case_index, + decision, + }); + } + } + } + + debug_assert_eq!(executions.len(), execution_count); + debug_assert_eq!(cells.len(), cell_count); + Ok(JointEvaluationMatricesV1 { + executions: executions.into_boxed_slice(), + cells: cells.into_boxed_slice(), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum JointReportErrorV1 { + MissingRootSurface(SurfaceInputPortId), + CandidatePaintMismatch { + ordinal: CandidateOrdinalV1, + stage: JointVisibleTargetV1, + expected: PaintId, + actual: PaintId, + }, + ResourceExhausted, +} + +pub(crate) fn checked_joint_cardinality( + candidates: usize, + cases: usize, + constraints: usize, +) -> Result<(usize, usize), JointReportErrorV1> { + let executions = candidates + .checked_mul(cases) + .ok_or(JointReportErrorV1::ResourceExhausted)?; + let cells = executions + .checked_mul(constraints) + .ok_or(JointReportErrorV1::ResourceExhausted)?; + Ok((executions, cells)) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct JointEvaluationMatricesV1 { + executions: Box<[JointExecutionRecordV1]>, + cells: Box<[JointConstraintCellV1]>, +} + +/// Один execution record существует независимо от наличия constraint на lower. +/// Поэтому связь derived surface доказана даже при единственном upper predicate. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct JointExecutionRecordV1 { + ordinal: CandidateOrdinalV1, + case_index: usize, + lower_paint: EncodedPointPaintV1, + upper_paint: EncodedPointPaintV1, + lower: ResolvedOccurrence, + upper: ResolvedOccurrence, +} + +impl JointExecutionRecordV1 { + pub(crate) const fn ordinal(&self) -> CandidateOrdinalV1 { + self.ordinal + } + + pub(crate) const fn case_index(&self) -> usize { + self.case_index + } + + pub(crate) const fn lower_paint(&self) -> EncodedPointPaintV1 { + self.lower_paint + } + + pub(crate) const fn upper_paint(&self) -> EncodedPointPaintV1 { + self.upper_paint + } + + pub(crate) fn lower_visible(&self) -> Srgb8 { + Srgb8::new(self.lower.visible()) + } + + pub(crate) fn upper_visible(&self) -> Srgb8 { + Srgb8::new(self.upper.visible()) + } + + pub(crate) fn derived_surface_is_exact(&self) -> bool { + self.upper.certificate().backdrop_rgb() == self.lower.visible() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum JointConstraintDecisionV1 { + Pass(ExactPassEvidenceV1), + Violation(ExactViolationEvidenceV1), +} + +impl JointConstraintDecisionV1 { + pub(crate) const fn is_pass(&self) -> bool { + matches!(self, Self::Pass(_)) + } + + pub(crate) fn actual(&self) -> Srgb8 { + match self { + Self::Pass(evidence) => evidence.actual(), + Self::Violation(evidence) => evidence.actual(), + } + } + + pub(crate) fn target(&self) -> Srgb8 { + match self { + Self::Pass(evidence) => evidence.target(), + Self::Violation(evidence) => evidence.target(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct JointConstraintCellV1 { + ordinal: CandidateOrdinalV1, + constraint: JointConstraintIdV1, + target: JointVisibleTargetV1, + case_index: usize, + decision: JointConstraintDecisionV1, +} + +impl JointConstraintCellV1 { + pub(crate) const fn ordinal(&self) -> CandidateOrdinalV1 { + self.ordinal + } + + pub(crate) const fn constraint(&self) -> JointConstraintIdV1 { + self.constraint + } + + pub(crate) const fn target_kind(&self) -> JointVisibleTargetV1 { + self.target + } + + pub(crate) const fn case_index(&self) -> usize { + self.case_index + } + + pub(crate) const fn decision(&self) -> &JointConstraintDecisionV1 { + &self.decision + } +} + +/// Полная матрица candidate x constraint x unique physical case плюс отдельная +/// joint execution matrix candidate x case. Report не знает selection policy. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct FullHardReportV1 { + program_identity: JointPointProgramIdentityV1, + program: JointPointProgramV1, + candidates: JointCandidateSetV1, + observation: RevisionBoundObservationV1, + executions: Box<[JointExecutionRecordV1]>, + cells: Box<[JointConstraintCellV1]>, +} + +impl FullHardReportV1 { + pub(crate) const fn program_identity(&self) -> JointPointProgramIdentityV1 { + self.program_identity + } + + pub(crate) fn candidate_set(&self) -> &JointCandidateSetV1 { + &self.candidates + } + + pub(crate) fn executions(&self) -> &[JointExecutionRecordV1] { + &self.executions + } + + pub(crate) fn cells(&self) -> &[JointConstraintCellV1] { + &self.cells + } + + pub(crate) const fn observation(&self) -> &RevisionBoundObservationV1 { + &self.observation + } + + pub(crate) fn provenance(&self, case_index: usize) -> Option<&[ScenarioId]> { + self.observation + .set() + .cases() + .get(case_index) + .map(|case| case.provenance()) + } + + pub(crate) fn classify(self) -> HardFeasibilityV1 { + let mut feasible = Vec::new(); + for candidate in self.candidates.candidates() { + if self + .cells + .iter() + .filter(|cell| cell.ordinal == candidate.ordinal) + .all(|cell| cell.decision.is_pass()) + { + feasible.push(candidate.ordinal); + } + } + if feasible.is_empty() { + HardFeasibilityV1::Infeasible(self) + } else { + HardFeasibilityV1::NonEmpty(NonEmptyFeasibleJointTuplesV1 { + report: self, + feasible: feasible.into_boxed_slice(), + }) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum HardFeasibilityV1 { + Infeasible(FullHardReportV1), + NonEmpty(NonEmptyFeasibleJointTuplesV1), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NonEmptyFeasibleJointTuplesV1 { + report: FullHardReportV1, + feasible: Box<[CandidateOrdinalV1]>, +} + +impl NonEmptyFeasibleJointTuplesV1 { + pub(crate) fn feasible(&self) -> &[CandidateOrdinalV1] { + &self.feasible + } + + pub(crate) fn candidate_set(&self) -> &JointCandidateSetV1 { + self.report.candidate_set() + } + + pub(crate) fn select(self, policy: DeclaredTotalOrderV1) -> SelectedJointTupleV1 { + let ordinal = policy + .order + .iter() + .copied() + .find(|ordinal| self.feasible.binary_search(ordinal).is_ok()) + .unwrap_or_else(|| unreachable!("validated total order covers nonempty feasible set")); + let candidate = *self + .report + .candidates + .candidates() + .iter() + .find(|candidate| candidate.ordinal == ordinal) + .unwrap_or_else(|| unreachable!("validated ordinal belongs to candidate set")); + SelectedJointTupleV1 { + report: self.report, + policy, + candidate, + } + } +} + +/// Полный client-declared tie-break. Он не участвует в measurement/report. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct DeclaredTotalOrderV1 { + order: Box<[CandidateOrdinalV1]>, +} + +impl DeclaredTotalOrderV1 { + pub(crate) fn new( + candidates: &JointCandidateSetV1, + order: Vec, + ) -> Result { + if order.len() != candidates.candidates.len() { + return Err(SelectionPolicyErrorV1::NotATotalOrder); + } + let mut canonical = order.clone(); + canonical.sort_unstable(); + for pair in canonical.windows(2) { + if pair[0] == pair[1] { + return Err(SelectionPolicyErrorV1::DuplicateOrdinal(pair[0])); + } + } + if canonical.iter().copied().ne(candidates + .candidates + .iter() + .map(|candidate| candidate.ordinal)) + { + return Err(SelectionPolicyErrorV1::NotATotalOrder); + } + Ok(Self { + order: order.into_boxed_slice(), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SelectionPolicyErrorV1 { + DuplicateOrdinal(CandidateOrdinalV1), + NotATotalOrder, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SelectedJointTupleV1 { + report: FullHardReportV1, + policy: DeclaredTotalOrderV1, + candidate: JointCandidateTupleV1, +} + +impl SelectedJointTupleV1 { + pub(crate) const fn ordinal(&self) -> CandidateOrdinalV1 { + self.candidate.ordinal + } + + pub(crate) fn recheck( + self, + ) -> Result { + let root_index = self + .report + .observation + .schema() + .binary_search(&self.report.program.root_surface) + .map_err(|_| SelectedRecheckErrorV1::InvariantDrift)?; + let cases = self.report.observation.set().cases().len(); + let (execution_count, cell_count) = + checked_joint_cardinality(1, cases, self.report.program.constraints.len()) + .map_err(|_| SelectedRecheckErrorV1::ResourceExhausted)?; + let matrices = self + .report + .program + .execute( + core::slice::from_ref(&self.candidate), + &self.report.observation, + root_index, + execution_count, + cell_count, + ) + .map_err(|error| match error { + JointReportErrorV1::ResourceExhausted => SelectedRecheckErrorV1::ResourceExhausted, + JointReportErrorV1::MissingRootSurface(_) + | JointReportErrorV1::CandidatePaintMismatch { .. } => { + SelectedRecheckErrorV1::InvariantDrift + } + })?; + if let Some(violation) = matrices + .cells + .iter() + .copied() + .find(|cell| !cell.decision.is_pass()) + { + return Err(SelectedRecheckErrorV1::Violation(violation)); + } + Ok(RevisionBoundVerifiedSelectionV1 { + selected: self, + recheck: FreshJointRecheckV1 { + executions: matrices.executions, + cells: matrices.cells, + }, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SelectedRecheckErrorV1 { + ResourceExhausted, + InvariantDrift, + Violation(JointConstraintCellV1), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct FreshJointRecheckV1 { + executions: Box<[JointExecutionRecordV1]>, + cells: Box<[JointConstraintCellV1]>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RevisionBoundVerifiedSelectionV1 { + selected: SelectedJointTupleV1, + recheck: FreshJointRecheckV1, +} + +impl RevisionBoundVerifiedSelectionV1 { + pub(crate) const fn ordinal(&self) -> CandidateOrdinalV1 { + self.selected.candidate.ordinal + } + + pub(crate) const fn report(&self) -> &FullHardReportV1 { + &self.selected.report + } + + pub(crate) fn policy(&self) -> &[CandidateOrdinalV1] { + &self.selected.policy.order + } + + pub(crate) fn fresh_executions(&self) -> &[JointExecutionRecordV1] { + &self.recheck.executions + } + + pub(crate) fn fresh_cells(&self) -> &[JointConstraintCellV1] { + &self.recheck.cells + } +} diff --git a/crates/labcolors-core/src/joint_tests.rs b/crates/labcolors-core/src/joint_tests.rs new file mode 100644 index 00000000..2da15731 --- /dev/null +++ b/crates/labcolors-core/src/joint_tests.rs @@ -0,0 +1,409 @@ +use crate::Srgb8; +use crate::appearance::{EncodedPointPaintV1, PaintId, SurfaceInputPortId}; +use crate::composition::AdmittedOpacityV1; +use crate::joint::{ + CandidateOrdinalV1, CandidateSetErrorV1, DeclaredTotalOrderV1, HardFeasibilityV1, + JointCandidateSetV1, JointCandidateTupleV1, JointConstraintDecisionV1, JointConstraintIdV1, + JointHardConstraintV1, JointPointProgramIdentityV1, JointPointProgramV1, JointProgramErrorV1, + JointReportErrorV1, JointVisibleTargetV1, SelectionPolicyErrorV1, checked_joint_cardinality, +}; +use crate::observation::{ + ObservationPayloadInput, ObservationSnapshot, ObservationState, ObservationStreamId, + ObservationUpdateInput, ObservedScenarioSetInput, Revision, RevisionBoundObservationV1, + ScenarioId, ScenarioInput, SurfaceInputBinding, +}; + +const ROOT: SurfaceInputPortId = SurfaceInputPortId::new(7); +const LOWER: PaintId = PaintId::new(11); +const UPPER: PaintId = PaintId::new(12); +const STREAM: ObservationStreamId = ObservationStreamId::new(3); + +fn paint(id: PaintId, bytes: [u8; 3], opacity: f64) -> EncodedPointPaintV1 { + EncodedPointPaintV1::from_admitted( + id, + Srgb8::new(bytes), + AdmittedOpacityV1::new(opacity).unwrap(), + ) +} + +fn candidate(ordinal: u32, lower: ([u8; 3], f64), upper: ([u8; 3], f64)) -> JointCandidateTupleV1 { + JointCandidateTupleV1::new( + CandidateOrdinalV1::new(ordinal), + paint(LOWER, lower.0, lower.1), + paint(UPPER, upper.0, upper.1), + ) +} + +fn candidates(values: Vec) -> JointCandidateSetV1 { + JointCandidateSetV1::new(values).unwrap() +} + +fn program(constraints: Vec) -> JointPointProgramV1 { + JointPointProgramV1::new(ROOT, LOWER, UPPER, constraints).unwrap() +} + +fn exact_upper(id: u32, target: [u8; 3]) -> JointHardConstraintV1 { + JointHardConstraintV1::exact( + JointConstraintIdV1::new(id), + JointVisibleTargetV1::Upper, + Srgb8::new(target), + ) +} + +fn exact_lower(id: u32, target: [u8; 3]) -> JointHardConstraintV1 { + JointHardConstraintV1::exact( + JointConstraintIdV1::new(id), + JointVisibleTargetV1::Lower, + Srgb8::new(target), + ) +} + +fn observation(revision: u64, cases: Vec<(u32, [u8; 3])>) -> RevisionBoundObservationV1 { + let mut state = ObservationState::new(STREAM, vec![ROOT]).unwrap(); + state + .apply(ObservationUpdateInput { + stream: STREAM, + revision: Revision::new(revision), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: cases + .into_iter() + .map(|(id, value)| ScenarioInput { + id: ScenarioId::new(id), + bindings: vec![SurfaceInputBinding { + port: ROOT, + value: Srgb8::new(value), + }], + }) + .collect(), + }), + }) + .unwrap(); + match state.snapshot() { + ObservationSnapshot::Ready { observation } => observation, + snapshot => panic!("expected Ready, got {snapshot:?}"), + } +} + +#[test] +fn linked_candidate_is_selected_only_after_upper_sees_lower_visible_surface() { + let observed = observation(1, vec![(1, [0; 3])]); + let domain = candidates(vec![ + candidate(0, ([0; 3], 1.0), ([255; 3], 0.5)), + candidate(1, ([128; 3], 1.0), ([255; 3], 0.5)), + ]); + let report = program(vec![exact_upper(1, [192; 3])]) + .evaluate(domain, observed) + .unwrap(); + + assert_eq!( + report.program_identity(), + JointPointProgramIdentityV1::TwoPaintDerivedSurfaceExactPointV1 + ); + assert_eq!(report.executions().len(), 2); + assert!( + report + .executions() + .iter() + .all(|execution| execution.derived_surface_is_exact()) + ); + assert_eq!(report.executions()[0].ordinal(), CandidateOrdinalV1::new(0)); + assert_eq!(report.executions()[0].case_index(), 0); + assert_eq!(report.executions()[0].lower_paint().id(), LOWER); + assert_eq!(report.executions()[0].upper_paint().id(), UPPER); + assert_eq!(report.executions()[0].lower_visible(), Srgb8::new([0; 3])); + assert_eq!(report.executions()[0].upper_visible(), Srgb8::new([128; 3])); + assert_eq!(report.executions()[1].upper_visible(), Srgb8::new([192; 3])); + assert_eq!(report.cells()[0].ordinal(), CandidateOrdinalV1::new(0)); + assert_eq!(report.cells()[0].constraint(), JointConstraintIdV1::new(1)); + assert_eq!(report.cells()[0].target_kind(), JointVisibleTargetV1::Upper); + assert_eq!(report.cells()[0].case_index(), 0); + assert_eq!(report.cells()[0].decision().target(), Srgb8::new([192; 3])); + assert!(matches!( + report.cells()[0].decision(), + JointConstraintDecisionV1::Violation(_) + )); + assert!(matches!( + report.cells()[1].decision(), + JointConstraintDecisionV1::Pass(_) + )); + + let HardFeasibilityV1::NonEmpty(feasible) = report.classify() else { + panic!("second joint tuple must be feasible"); + }; + assert_eq!(feasible.feasible(), &[CandidateOrdinalV1::new(1)]); + let policy = DeclaredTotalOrderV1::new( + feasible.candidate_set(), + vec![CandidateOrdinalV1::new(0), CandidateOrdinalV1::new(1)], + ) + .unwrap(); + let selected = feasible.select(policy); + assert_eq!(selected.ordinal(), CandidateOrdinalV1::new(1)); + let verified = selected.recheck().unwrap(); + assert_eq!(verified.ordinal(), CandidateOrdinalV1::new(1)); + assert_eq!(verified.fresh_executions().len(), 1); + assert_eq!(verified.fresh_cells().len(), 1); +} + +#[test] +fn every_unique_physical_case_must_pass_without_worst_or_average_reduction() { + let observed = observation(2, vec![(1, [0; 3]), (2, [255; 3])]); + let domain = candidates(vec![candidate(0, ([0; 3], 0.5), ([255; 3], 0.5))]); + let report = program(vec![exact_upper(1, [128; 3])]) + .evaluate(domain, observed) + .unwrap(); + + assert_eq!(report.executions().len(), 2); + assert_eq!(report.cells().len(), 2); + assert_eq!(report.cells()[0].decision().actual(), Srgb8::new([128; 3])); + assert_eq!(report.cells()[1].decision().actual(), Srgb8::new([192; 3])); + let HardFeasibilityV1::Infeasible(report) = report.classify() else { + panic!("one violated case must exclude the whole tuple"); + }; + assert_eq!( + report + .cells() + .iter() + .filter(|cell| !cell.decision().is_pass()) + .count(), + 1 + ); +} + +#[test] +fn full_report_does_not_short_circuit_after_first_violation() { + let observed = observation(3, vec![(1, [0; 3])]); + let domain = candidates(vec![candidate(0, ([0; 3], 1.0), ([255; 3], 0.5))]); + crate::composition::reset_source_over_evaluation_count(); + let report = program(vec![ + exact_upper(1, [0; 3]), + exact_upper(2, [128; 3]), + exact_lower(3, [0; 3]), + ]) + .evaluate(domain, observed) + .unwrap(); + + assert_eq!(crate::composition::source_over_evaluation_count(), 2); + assert_eq!(report.executions().len(), 1); + assert_eq!(report.cells().len(), 3); + assert_eq!( + report + .cells() + .iter() + .filter(|cell| cell.decision().is_pass()) + .count(), + 2 + ); + assert_eq!( + report + .cells() + .iter() + .filter(|cell| !cell.decision().is_pass()) + .count(), + 1 + ); +} + +#[test] +fn candidate_and_constraint_declaration_permutations_are_canonical() { + let observed = observation(4, vec![(1, [0; 3])]); + let first = program(vec![exact_upper(9, [255; 3]), exact_lower(4, [0; 3])]) + .evaluate( + candidates(vec![ + candidate(8, ([255; 3], 0.0), ([255; 3], 1.0)), + candidate(2, ([0; 3], 1.0), ([255; 3], 1.0)), + ]), + observed.clone(), + ) + .unwrap(); + let second = program(vec![exact_lower(4, [0; 3]), exact_upper(9, [255; 3])]) + .evaluate( + candidates(vec![ + candidate(2, ([0; 3], 1.0), ([255; 3], 1.0)), + candidate(8, ([255; 3], 0.0), ([255; 3], 1.0)), + ]), + observed, + ) + .unwrap(); + + assert_eq!(first, second); +} + +#[test] +fn scenario_declaration_permutation_is_canonical() { + let first = program(vec![exact_upper(1, [255; 3])]) + .evaluate( + candidates(vec![candidate(0, ([17; 3], 0.5), ([255; 3], 1.0))]), + observation(5, vec![(2, [255; 3]), (1, [0; 3])]), + ) + .unwrap(); + let second = program(vec![exact_upper(1, [255; 3])]) + .evaluate( + candidates(vec![candidate(0, ([17; 3], 0.5), ([255; 3], 1.0))]), + observation(5, vec![(1, [0; 3]), (2, [255; 3])]), + ) + .unwrap(); + + assert_eq!(first, second); +} + +#[test] +fn duplicate_provenance_does_not_repeat_physical_execution() { + let observed = observation(6, vec![(9, [1, 2, 3]), (3, [1, 2, 3])]); + let report = program(vec![exact_upper(1, [255; 3])]) + .evaluate( + candidates(vec![candidate(0, ([0; 3], 1.0), ([255; 3], 1.0))]), + observed, + ) + .unwrap(); + + assert_eq!(report.executions().len(), 1); + assert_eq!(report.cells().len(), 1); + assert_eq!( + report.provenance(0).unwrap(), + &[ScenarioId::new(3), ScenarioId::new(9)] + ); +} + +#[test] +fn declared_policy_is_separate_from_report_and_is_the_only_tie_break() { + let observed = observation(7, vec![(1, [0; 3])]); + let make_report = || { + program(vec![exact_upper(1, [42; 3])]) + .evaluate( + candidates(vec![ + candidate(7, ([1; 3], 1.0), ([42; 3], 1.0)), + candidate(4, ([250; 3], 1.0), ([42; 3], 1.0)), + ]), + observed.clone(), + ) + .unwrap() + }; + + let HardFeasibilityV1::NonEmpty(first) = make_report().classify() else { + panic!("both tuples must pass"); + }; + let first_policy = DeclaredTotalOrderV1::new( + first.candidate_set(), + vec![CandidateOrdinalV1::new(7), CandidateOrdinalV1::new(4)], + ) + .unwrap(); + let HardFeasibilityV1::NonEmpty(second) = make_report().classify() else { + panic!("both tuples must pass"); + }; + let second_policy = DeclaredTotalOrderV1::new( + second.candidate_set(), + vec![CandidateOrdinalV1::new(4), CandidateOrdinalV1::new(7)], + ) + .unwrap(); + assert_eq!( + first.select(first_policy).ordinal(), + CandidateOrdinalV1::new(7) + ); + assert_eq!( + second.select(second_policy).ordinal(), + CandidateOrdinalV1::new(4) + ); +} + +#[test] +fn fresh_recheck_executes_the_selected_joint_program_again_on_the_same_revision() { + let observed = observation(8, vec![(1, [0; 3]), (2, [255; 3])]); + let report = program(vec![exact_upper(1, [17; 3])]) + .evaluate( + candidates(vec![candidate(0, ([9; 3], 1.0), ([17; 3], 1.0))]), + observed, + ) + .unwrap(); + crate::composition::reset_source_over_evaluation_count(); + let HardFeasibilityV1::NonEmpty(feasible) = report.classify() else { + panic!("opaque upper must pass on both roots"); + }; + let policy = + DeclaredTotalOrderV1::new(feasible.candidate_set(), vec![CandidateOrdinalV1::new(0)]) + .unwrap(); + let selected = feasible.select(policy); + let verified = selected.recheck().unwrap(); + + assert_eq!(crate::composition::source_over_evaluation_count(), 4); + assert_eq!(verified.report().observation().revision(), Revision::new(8)); + assert_eq!(verified.fresh_executions().len(), 2); + assert_eq!(verified.fresh_cells().len(), 2); + assert_eq!(verified.policy(), &[CandidateOrdinalV1::new(0)]); +} + +#[test] +fn invalid_domains_and_policies_fail_before_compositing() { + assert_eq!( + JointCandidateSetV1::new(vec![]), + Err(CandidateSetErrorV1::Empty) + ); + assert_eq!( + JointCandidateSetV1::new(vec![ + candidate(1, ([0; 3], 1.0), ([0; 3], 1.0)), + candidate(1, ([1; 3], 1.0), ([1; 3], 1.0)), + ]), + Err(CandidateSetErrorV1::DuplicateOrdinal( + CandidateOrdinalV1::new(1) + )) + ); + assert_eq!( + JointCandidateSetV1::new(vec![ + candidate(1, ([0; 3], 1.0), ([0; 3], 1.0)), + candidate(2, ([0; 3], 1.0), ([0; 3], 1.0)), + ]), + Err(CandidateSetErrorV1::DuplicatePhysicalTuple { + first: CandidateOrdinalV1::new(1), + second: CandidateOrdinalV1::new(2), + }) + ); + assert_eq!( + JointPointProgramV1::new(ROOT, LOWER, LOWER, vec![exact_upper(1, [0; 3])]), + Err(JointProgramErrorV1::SamePaintIdentity(LOWER)) + ); + assert_eq!( + JointPointProgramV1::new(ROOT, LOWER, UPPER, vec![]), + Err(JointProgramErrorV1::EmptyHardConstraintSet) + ); + + let observed = observation(9, vec![(1, [0; 3])]); + let wrong = JointCandidateSetV1::new(vec![JointCandidateTupleV1::new( + CandidateOrdinalV1::new(0), + paint(PaintId::new(999), [0; 3], 1.0), + paint(UPPER, [0; 3], 1.0), + )]) + .unwrap(); + crate::composition::reset_source_over_evaluation_count(); + assert!(matches!( + program(vec![exact_upper(1, [0; 3])]).evaluate(wrong, observed), + Err(JointReportErrorV1::CandidatePaintMismatch { + stage: JointVisibleTargetV1::Lower, + .. + }) + )); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + let domain = candidates(vec![candidate(0, ([0; 3], 1.0), ([0; 3], 1.0))]); + assert_eq!( + DeclaredTotalOrderV1::new(&domain, vec![]), + Err(SelectionPolicyErrorV1::NotATotalOrder) + ); + assert_eq!( + DeclaredTotalOrderV1::new( + &domain, + vec![CandidateOrdinalV1::new(0), CandidateOrdinalV1::new(0)], + ), + Err(SelectionPolicyErrorV1::NotATotalOrder) + ); +} + +#[test] +fn cardinality_overflow_is_rejected_by_preflight() { + assert_eq!( + checked_joint_cardinality(usize::MAX, 2, 1), + Err(JointReportErrorV1::ResourceExhausted) + ); + assert_eq!( + checked_joint_cardinality(usize::MAX / 2 + 1, 2, 2), + Err(JointReportErrorV1::ResourceExhausted) + ); +} diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index b152d539..44073ffb 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -73,6 +73,18 @@ pub(crate) mod recheck; #[cfg(test)] mod recheck_tests; +#[cfg_attr( + not(test), + expect( + dead_code, + reason = "private V2a joint selection is production-compiled before Pair lowering or a public Program exists" + ) +)] +pub(crate) mod joint; + +#[cfg(test)] +mod joint_tests; + #[cfg(test)] mod constraint_tests;