use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum D3Category {
Confirmed,
Surprise,
Failure,
Contradiction,
Partial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DevSignature {
DevTauriAssetMissing,
DevWebviewConnRefused,
DevPortBindFail,
DevPanicStartup,
DevUiErrorBoundary,
DevCompileProcMacroFlake,
}
impl DevSignature {
pub fn as_str(&self) -> &'static str {
match self {
DevSignature::DevTauriAssetMissing => "DEV-TAURI-ASSET-MISSING",
DevSignature::DevWebviewConnRefused => "DEV-WEBVIEW-CONN-REFUSED",
DevSignature::DevPortBindFail => "DEV-PORT-BIND-FAIL",
DevSignature::DevPanicStartup => "DEV-PANIC-STARTUP",
DevSignature::DevUiErrorBoundary => "DEV-UI-ERROR-BOUNDARY",
DevSignature::DevCompileProcMacroFlake => "DEV-COMPILE-PROC-MACRO-FLAKE",
}
}
pub fn default_category(&self) -> D3Category {
match self {
DevSignature::DevTauriAssetMissing
| DevSignature::DevWebviewConnRefused
| DevSignature::DevUiErrorBoundary => D3Category::Contradiction,
DevSignature::DevPortBindFail
| DevSignature::DevPanicStartup
| DevSignature::DevCompileProcMacroFlake => D3Category::Failure,
}
}
pub fn remediation_ref(&self) -> Option<&'static str> {
match self {
DevSignature::DevTauriAssetMissing => {
Some("reference_runner_asset_not_found_legacy_exe_fallback")
}
DevSignature::DevWebviewConnRefused => Some("feedback_runner_manual_build"),
DevSignature::DevPortBindFail
| DevSignature::DevPanicStartup
| DevSignature::DevUiErrorBoundary
| DevSignature::DevCompileProcMacroFlake => None,
}
}
pub fn all() -> &'static [DevSignature] {
&[
DevSignature::DevTauriAssetMissing,
DevSignature::DevWebviewConnRefused,
DevSignature::DevPortBindFail,
DevSignature::DevPanicStartup,
DevSignature::DevUiErrorBoundary,
DevSignature::DevCompileProcMacroFlake,
]
}
}
impl fmt::Display for DevSignature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseDevSignatureError(pub String);
impl fmt::Display for ParseDevSignatureError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unrecognized dev-event signature: {}", self.0)
}
}
impl std::error::Error for ParseDevSignatureError {}
impl FromStr for DevSignature {
type Err = ParseDevSignatureError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"DEV-TAURI-ASSET-MISSING" => Ok(DevSignature::DevTauriAssetMissing),
"DEV-WEBVIEW-CONN-REFUSED" => Ok(DevSignature::DevWebviewConnRefused),
"DEV-PORT-BIND-FAIL" => Ok(DevSignature::DevPortBindFail),
"DEV-PANIC-STARTUP" => Ok(DevSignature::DevPanicStartup),
"DEV-UI-ERROR-BOUNDARY" => Ok(DevSignature::DevUiErrorBoundary),
"DEV-COMPILE-PROC-MACRO-FLAKE" => Ok(DevSignature::DevCompileProcMacroFlake),
other => Err(ParseDevSignatureError(other.to_string())),
}
}
}
impl<'a> TryFrom<&'a str> for DevSignature {
type Error = ParseDevSignatureError;
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
s.parse()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn as_str_from_str_round_trip_for_every_variant() {
for &sig in DevSignature::all() {
let s = sig.as_str();
let parsed = DevSignature::from_str(s).expect("round-trip");
assert_eq!(parsed, sig, "round-trip mismatch for {s}");
assert_eq!(DevSignature::try_from(s).unwrap(), sig);
assert_eq!(format!("{sig}"), s);
}
}
#[test]
fn from_str_rejects_unknown() {
let err = DevSignature::from_str("DEV-NOPE").unwrap_err();
assert_eq!(err, ParseDevSignatureError("DEV-NOPE".to_string()));
}
#[test]
fn serde_round_trip_for_every_variant() {
for &sig in DevSignature::all() {
let json = serde_json::to_string(&sig).expect("serialize");
let back: DevSignature = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, sig);
}
}
#[test]
fn default_category_mapping() {
assert_eq!(
DevSignature::DevTauriAssetMissing.default_category(),
D3Category::Contradiction
);
assert_eq!(
DevSignature::DevWebviewConnRefused.default_category(),
D3Category::Contradiction
);
assert_eq!(
DevSignature::DevUiErrorBoundary.default_category(),
D3Category::Contradiction
);
assert_eq!(
DevSignature::DevPortBindFail.default_category(),
D3Category::Failure
);
assert_eq!(
DevSignature::DevPanicStartup.default_category(),
D3Category::Failure
);
assert_eq!(
DevSignature::DevCompileProcMacroFlake.default_category(),
D3Category::Failure
);
}
#[test]
fn remediation_ref_mapping() {
assert_eq!(
DevSignature::DevTauriAssetMissing.remediation_ref(),
Some("reference_runner_asset_not_found_legacy_exe_fallback")
);
assert_eq!(
DevSignature::DevWebviewConnRefused.remediation_ref(),
Some("feedback_runner_manual_build")
);
assert_eq!(DevSignature::DevPortBindFail.remediation_ref(), None);
assert_eq!(DevSignature::DevPanicStartup.remediation_ref(), None);
assert_eq!(DevSignature::DevUiErrorBoundary.remediation_ref(), None);
assert_eq!(
DevSignature::DevCompileProcMacroFlake.remediation_ref(),
None
);
}
#[test]
fn d3_category_serializes_to_five_outcome_category_strings() {
let cases = [
(D3Category::Confirmed, "\"confirmed\""),
(D3Category::Surprise, "\"surprise\""),
(D3Category::Failure, "\"failure\""),
(D3Category::Contradiction, "\"contradiction\""),
(D3Category::Partial, "\"partial\""),
];
for (cat, wire) in cases {
assert_eq!(serde_json::to_string(&cat).unwrap(), wire, "{cat:?}");
let back: D3Category = serde_json::from_str(wire).unwrap();
assert_eq!(back, cat);
}
}
#[test]
fn matches_supervisor_phase1_literals() {
const SUPERVISOR_PHASE1: &[&str] = &[
"DEV-TAURI-ASSET-MISSING",
"DEV-WEBVIEW-CONN-REFUSED",
"DEV-PORT-BIND-FAIL",
"DEV-PANIC-STARTUP",
"DEV-UI-ERROR-BOUNDARY",
];
for &lit in SUPERVISOR_PHASE1 {
let sig = DevSignature::from_str(lit)
.unwrap_or_else(|_| panic!("shared registry missing supervisor literal {lit}"));
assert_eq!(sig.as_str(), lit, "byte-for-byte mismatch for {lit}");
}
assert!(DevSignature::from_str("DEV-COMPILE-PROC-MACRO-FLAKE").is_ok());
}
}