use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Observation {
Kept,
Broken,
Partial,
Skipped,
}
impl Observation {
pub fn value(&self) -> Option<f64> {
match self {
Observation::Kept => Some(1.0),
Observation::Broken => Some(0.0),
Observation::Partial => Some(0.5),
Observation::Skipped => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Confidence {
High,
Medium,
Low,
}
impl Confidence {
pub fn weight(&self) -> f64 {
match self {
Confidence::High => 1.0,
Confidence::Medium => 0.6,
Confidence::Low => 0.3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PromiseType {
Standing,
Structural,
Behavioral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Method {
Grep,
GrepAbsent,
ConsecutiveComments,
OutputLength,
FileCheck,
OutputContains,
OutputStructure,
TokenMetric,
Timing,
TestAssertionPatterns,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethodOutcome {
pub result: Observation,
pub evidence: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationResult {
pub promise_id: String,
pub method: String,
pub confidence: Confidence,
pub result: Observation,
pub evidence: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum Requires {
One(String),
List(Vec<String>),
}
#[derive(Debug, Clone, Deserialize)]
pub struct PromiseSpec {
#[serde(skip)]
pub id: String,
#[serde(rename = "type")]
pub promise_type: PromiseType,
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(rename = "method")]
pub method_raw: String,
#[serde(default)]
pub confidence: Option<Confidence>,
#[serde(default)]
pub requires: Option<Requires>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub pattern: Option<String>,
#[serde(default)]
pub min_lines: Option<i64>,
#[serde(default)]
pub min_chars: Option<i64>,
#[serde(default)]
pub check: Option<String>,
#[serde(default)]
pub max_tokens: Option<i64>,
#[serde(default)]
pub max_ms: Option<i64>,
#[serde(default)]
pub test_file_pattern: Option<String>,
#[serde(default)]
pub forbidden_patterns: Option<Vec<String>>,
#[serde(default)]
pub threshold: Option<i64>,
#[serde(default)]
pub tool_pattern: Option<String>,
#[serde(skip)]
pub method: Option<Method>,
}
fn default_enabled() -> bool {
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReviewAction {
Accept,
Retry,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ReviewParser {
Ok,
UntaggedFallback,
RetryWithoutFeedback,
Failed,
DispatchError,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum Independence {
Independent,
SameHarness,
#[default]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewDecision {
pub action: ReviewAction,
pub feedback: Option<String>,
pub reasoning: Option<String>,
pub parser: ReviewParser,
pub reviewer_skill: String,
#[serde(default)]
pub independence: Independence,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn observation_serializes_lowercase() {
assert_eq!(
serde_json::to_string(&Observation::Kept).unwrap(),
"\"kept\""
);
assert_eq!(
serde_json::to_string(&Observation::Skipped).unwrap(),
"\"skipped\""
);
}
#[test]
fn observation_values_match_ema_contract() {
assert_eq!(Observation::Kept.value(), Some(1.0));
assert_eq!(Observation::Broken.value(), Some(0.0));
assert_eq!(Observation::Partial.value(), Some(0.5));
assert_eq!(Observation::Skipped.value(), None);
}
#[test]
fn confidence_weights_match() {
assert_eq!(Confidence::High.weight(), 1.0);
assert_eq!(Confidence::Medium.weight(), 0.6);
assert_eq!(Confidence::Low.weight(), 0.3);
}
#[test]
fn method_parses_registry_strings() {
let m: Method = serde_json::from_str("\"grep_absent\"").unwrap();
assert_eq!(m, Method::GrepAbsent);
}
#[test]
fn method_outcome_json_keys() {
let o = MethodOutcome {
result: Observation::Broken,
evidence: "x".into(),
};
let v: serde_json::Value = serde_json::to_value(&o).unwrap();
assert!(v.get("result").is_some() && v.get("evidence").is_some());
}
#[test]
fn review_action_serializes_lowercase() {
assert_eq!(
serde_json::to_value(ReviewAction::Accept).unwrap(),
serde_json::json!("accept")
);
assert_eq!(
serde_json::to_value(ReviewAction::Retry).unwrap(),
serde_json::json!("retry")
);
}
const PARSER_WIRE: [(ReviewParser, &str); 5] = [
(ReviewParser::Ok, "ok"),
(ReviewParser::UntaggedFallback, "untagged-fallback"),
(ReviewParser::RetryWithoutFeedback, "retry-without-feedback"),
(ReviewParser::Failed, "failed"),
(ReviewParser::DispatchError, "dispatch-error"),
];
#[test]
fn every_review_parser_value_round_trips_its_wire_form() {
for (value, wire) in PARSER_WIRE {
assert_eq!(
serde_json::to_value(value).unwrap(),
serde_json::json!(wire),
"{value:?} must serialize as {wire:?}"
);
let back: ReviewParser = serde_json::from_str(&format!("\"{wire}\"")).unwrap();
assert_eq!(back, value, "{wire:?} must deserialize back to {value:?}");
}
}
#[test]
fn no_review_parser_value_is_undeclared() {
let err = serde_json::from_str::<ReviewParser>("\"definitely-not-a-variant\"")
.expect_err("an unknown wire string must not deserialize")
.to_string();
let listed = err
.split_once("expected one of ")
.map(|(_, rest)| rest)
.unwrap_or_default();
let from_serde: std::collections::BTreeSet<&str> =
listed.split('`').skip(1).step_by(2).collect();
assert!(
from_serde.contains("ok"),
"serde's unknown-variant error no longer lists variants in the form this test \
parses, so it is not enumerating anything: {err:?}"
);
let declared: std::collections::BTreeSet<&str> =
PARSER_WIRE.iter().map(|(_, wire)| *wire).collect();
assert_eq!(
from_serde, declared,
"every variant of the type must have a PARSER_WIRE line, and every line must name \
a real variant"
);
}
#[test]
fn an_untagged_fallback_is_not_a_failure() {
let d = ReviewDecision {
action: ReviewAction::Retry,
feedback: Some("do it again".into()),
reasoning: None,
parser: ReviewParser::UntaggedFallback,
reviewer_skill: "s".into(),
independence: Independence::Unknown,
};
let v = serde_json::to_value(&d).unwrap();
assert_eq!(v["parser"], serde_json::json!("untagged-fallback"));
assert_eq!(v["action"], serde_json::json!("retry"));
assert_eq!(v["feedback"], serde_json::json!("do it again"));
}
#[test]
fn a_pre_independence_record_deserializes_to_unknown() {
let old = serde_json::json!({
"action": "accept",
"feedback": null,
"reasoning": "looks fine",
"parser": "ok",
"reviewer_skill": "generic"
});
let d: ReviewDecision = serde_json::from_value(old)
.expect("a record with no independence key must still deserialize");
assert_eq!(d.independence, Independence::Unknown);
}
#[test]
fn every_independence_value_round_trips_its_wire_form() {
const WIRE: [(Independence, &str); 3] = [
(Independence::Independent, "independent"),
(Independence::SameHarness, "same-harness"),
(Independence::Unknown, "unknown"),
];
for (value, wire) in WIRE {
assert_eq!(
serde_json::to_value(value).unwrap(),
serde_json::json!(wire),
"{value:?} must serialize as {wire:?}"
);
let back: Independence = serde_json::from_str(&format!("\"{wire}\"")).unwrap();
assert_eq!(back, value, "{wire:?} must deserialize back to {value:?}");
}
}
#[test]
fn independence_default_is_unknown() {
assert_eq!(Independence::default(), Independence::Unknown);
}
#[test]
fn review_parser_serializes_kebab_case() {
assert_eq!(
serde_json::to_value(ReviewParser::Ok).unwrap(),
serde_json::json!("ok")
);
assert_eq!(
serde_json::to_value(ReviewParser::RetryWithoutFeedback).unwrap(),
serde_json::json!("retry-without-feedback")
);
assert_eq!(
serde_json::to_value(ReviewParser::Failed).unwrap(),
serde_json::json!("failed")
);
assert_eq!(
serde_json::to_value(ReviewParser::DispatchError).unwrap(),
serde_json::json!("dispatch-error")
);
}
}