use serde::{Deserialize, Serialize};
use crate::surface_evidence::{EvidenceCell, SurfaceEvidenceMatrix};
use crate::surface_graph::ApplicationSurfaceKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceColumn {
Intent,
Runtime,
Test,
Proof,
Coverage,
Protection,
Ui,
A11y,
Mutation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceProducer {
ExistingTestAdaptation,
RecordedSession,
StorybookFlow,
SourceMutation,
BrowserExplore,
SpecRecovery,
ProductReview,
AiTestProgram,
}
impl EvidenceProducer {
#[must_use]
pub const fn cost(self) -> u64 {
match self {
Self::ExistingTestAdaptation => 1,
Self::RecordedSession => 2,
Self::StorybookFlow => 3,
Self::SourceMutation => 4,
Self::BrowserExplore | Self::SpecRecovery => 5,
Self::ProductReview => 8,
Self::AiTestProgram => 10,
}
}
#[must_use]
pub const fn available(self) -> bool {
!matches!(self, Self::BrowserExplore)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
#[serde(deny_unknown_fields)]
pub struct ProducerInventory {
pub matching_tests: bool,
pub recorded_sessions: bool,
pub stories: bool,
pub mutation_available: bool,
pub spec_recovery_available: bool,
pub ai_budget: bool,
}
impl Default for ProducerInventory {
fn default() -> Self {
Self {
matching_tests: true,
recorded_sessions: true,
stories: true,
mutation_available: true,
spec_recovery_available: true,
ai_budget: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProducerOffer {
pub producer: EvidenceProducer,
pub cost: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceNeed {
#[default]
MeasuredAbsent,
Unmeasured,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EvidenceGap {
pub surface: String,
pub kind: ApplicationSurfaceKind,
pub column: EvidenceColumn,
#[serde(default)]
pub need: EvidenceNeed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EvidencePlan {
pub surface: String,
pub kind: ApplicationSurfaceKind,
pub column: EvidenceColumn,
#[serde(default)]
pub need: EvidenceNeed,
pub cheapest: Option<EvidenceProducer>,
pub producers: Vec<ProducerOffer>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CheapestEvidencePlan {
pub gaps: Vec<EvidencePlan>,
pub truncated: bool,
}
#[must_use]
pub fn classify_evidence_gaps(matrix: &SurfaceEvidenceMatrix) -> Vec<EvidenceGap> {
let mut gaps = Vec::new();
for row in &matrix.surfaces {
for (column, cell) in row.cells() {
let need = match cell {
EvidenceCell::Absent => EvidenceNeed::MeasuredAbsent,
EvidenceCell::Unmeasured => EvidenceNeed::Unmeasured,
EvidenceCell::Present => continue,
};
gaps.push(EvidenceGap {
surface: row.surface.clone(),
kind: row.kind,
column,
need,
});
}
}
gaps.sort_by(|left, right| {
left.surface
.cmp(&right.surface)
.then(left.need.cmp(&right.need))
.then(left.column.cmp(&right.column))
});
gaps
}
#[must_use]
pub fn plan_cheapest_evidence(matrix: &SurfaceEvidenceMatrix) -> CheapestEvidencePlan {
plan_cheapest_evidence_with(matrix, &ProducerInventory::default())
}
#[must_use]
pub fn plan_cheapest_evidence_with(
matrix: &SurfaceEvidenceMatrix,
inventory: &ProducerInventory,
) -> CheapestEvidencePlan {
let gaps = classify_evidence_gaps(matrix)
.into_iter()
.map(|gap| {
let intent_present = matrix
.surfaces
.iter()
.find(|row| row.surface == gap.surface)
.is_some_and(|row| row.intent == EvidenceCell::Present);
let producers = offers(gap.column, gap.kind, intent_present, inventory);
EvidencePlan {
surface: gap.surface,
kind: gap.kind,
column: gap.column,
need: gap.need,
cheapest: producers.first().map(|offer| offer.producer),
producers,
}
})
.collect();
CheapestEvidencePlan {
gaps,
truncated: matrix.truncated,
}
}
fn offers(
column: EvidenceColumn,
kind: ApplicationSurfaceKind,
intent_present: bool,
inventory: &ProducerInventory,
) -> Vec<ProducerOffer> {
let ui = matches!(
kind,
ApplicationSurfaceKind::Route | ApplicationSurfaceKind::Component
);
let mut producers = Vec::new();
match column {
EvidenceColumn::Intent => {
if inventory.spec_recovery_available {
producers.push(EvidenceProducer::SpecRecovery);
producers.push(EvidenceProducer::ProductReview);
}
}
EvidenceColumn::Mutation => {
if inventory.mutation_available {
producers.push(EvidenceProducer::SourceMutation);
}
}
EvidenceColumn::Runtime => {
if inventory.recorded_sessions {
producers.push(EvidenceProducer::RecordedSession);
}
if ui {
producers.push(EvidenceProducer::BrowserExplore);
}
if inventory.ai_budget {
producers.push(EvidenceProducer::AiTestProgram);
}
}
EvidenceColumn::Test => {
if inventory.matching_tests {
producers.push(EvidenceProducer::ExistingTestAdaptation);
}
if inventory.recorded_sessions {
producers.push(EvidenceProducer::RecordedSession);
}
if ui && inventory.stories {
producers.push(EvidenceProducer::StorybookFlow);
}
if ui {
producers.push(EvidenceProducer::BrowserExplore);
}
if inventory.ai_budget {
producers.push(EvidenceProducer::AiTestProgram);
}
}
EvidenceColumn::Proof => {
if intent_present {
if inventory.matching_tests {
producers.push(EvidenceProducer::ExistingTestAdaptation);
}
if inventory.recorded_sessions {
producers.push(EvidenceProducer::RecordedSession);
}
if ui && inventory.stories {
producers.push(EvidenceProducer::StorybookFlow);
}
if inventory.ai_budget {
producers.push(EvidenceProducer::AiTestProgram);
}
}
}
EvidenceColumn::Coverage | EvidenceColumn::Protection => {
if inventory.matching_tests {
producers.push(EvidenceProducer::ExistingTestAdaptation);
}
if ui && inventory.stories {
producers.push(EvidenceProducer::StorybookFlow);
}
if inventory.ai_budget {
producers.push(EvidenceProducer::AiTestProgram);
}
}
EvidenceColumn::Ui | EvidenceColumn::A11y => {
if inventory.recorded_sessions {
producers.push(EvidenceProducer::RecordedSession);
}
if ui && inventory.stories {
producers.push(EvidenceProducer::StorybookFlow);
}
if ui {
producers.push(EvidenceProducer::BrowserExplore);
}
if inventory.ai_budget {
producers.push(EvidenceProducer::AiTestProgram);
}
}
}
producers.retain(|producer| producer.available());
producers.sort_by_key(|producer| (producer.cost(), *producer));
producers
.into_iter()
.map(|producer| ProducerOffer {
cost: producer.cost(),
producer,
})
.collect()
}