mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-18 20:15:14 +02:00
152 lines
5.7 KiB
Rust
152 lines
5.7 KiB
Rust
//! Phase 24 — feasibility scoring for chain edges.
|
|
//!
|
|
//! Each edge produced by [`crate::chain::edges::findings_to_edges`]
|
|
//! carries a feasibility weight in `[0.0, 1.0]`. The weight enters
|
|
//! Phase 25's path score as the multiplicative factor in
|
|
//! `score(path) = sum(impact) * product(feasibility)`, so a single
|
|
//! low-feasibility hop dampens the entire chain.
|
|
//!
|
|
//! # Buckets
|
|
//!
|
|
//! | Bucket | Weight | Trigger |
|
|
//! |-------------------------|--------|-------------------------------------------------------------|
|
|
//! | [`Confirmed`] | `1.0` | dynamic [`VerifyStatus::Confirmed`] |
|
|
//! | [`InconclusiveHighConf`]| `0.5` | dynamic [`VerifyStatus::Inconclusive`] + static `High` |
|
|
//! | [`Unverified`] | `0.1` | everything else (no verdict, `NotConfirmed`, `Unsupported`, |
|
|
//! | | | or `Inconclusive` without a high static confidence) |
|
|
//!
|
|
//! [`Confirmed`]: Feasibility::Confirmed
|
|
//! [`InconclusiveHighConf`]: Feasibility::InconclusiveHighConf
|
|
//! [`Unverified`]: Feasibility::Unverified
|
|
//! [`VerifyStatus::Confirmed`]: crate::evidence::VerifyStatus::Confirmed
|
|
//! [`VerifyStatus::Inconclusive`]: crate::evidence::VerifyStatus::Inconclusive
|
|
|
|
use crate::commands::scan::Diag;
|
|
use crate::evidence::{Confidence, VerifyResult, VerifyStatus};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Discrete feasibility bucket for a chain edge.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Feasibility {
|
|
/// Dynamic verification fired the sink probe.
|
|
Confirmed,
|
|
/// Dynamic verification was Inconclusive but the static engine's
|
|
/// confidence in the finding is `High`. Used for findings that
|
|
/// the verifier could not exercise (build failure, sandbox refuse)
|
|
/// but where the static evidence is strong.
|
|
InconclusiveHighConf,
|
|
/// Everything else — no dynamic verification, dynamic verdict was
|
|
/// `NotConfirmed`/`Unsupported`, or dynamic was `Inconclusive` but
|
|
/// static confidence is not `High`.
|
|
Unverified,
|
|
}
|
|
|
|
impl Feasibility {
|
|
/// Multiplicative weight contributed to Phase 25's path score.
|
|
pub const fn score(self) -> f32 {
|
|
match self {
|
|
Feasibility::Confirmed => 1.0,
|
|
Feasibility::InconclusiveHighConf => 0.5,
|
|
Feasibility::Unverified => 0.1,
|
|
}
|
|
}
|
|
|
|
/// Translate a dynamic [`VerifyResult`] into a feasibility weight.
|
|
///
|
|
/// This is the literal signature the design doc specifies. It
|
|
/// cannot distinguish `Inconclusive` with high static confidence
|
|
/// from `Inconclusive` with low static confidence (the static
|
|
/// confidence is carried on the [`Diag`], not on the
|
|
/// [`VerifyResult`]); use [`Feasibility::for_finding`] when both
|
|
/// halves of the input are available.
|
|
pub fn from_verdict(verdict: Option<&VerifyResult>) -> f32 {
|
|
Self::bucket_from_verdict(verdict, None).score()
|
|
}
|
|
|
|
/// Same as [`from_verdict`](Self::from_verdict) but consults the
|
|
/// static `Diag.confidence` so the `Inconclusive_HighConf` bucket
|
|
/// in the doc's table can fire. Phase 25's scoring pass uses this
|
|
/// flavour.
|
|
pub fn for_finding(diag: &Diag) -> Feasibility {
|
|
let verdict = diag.evidence.as_ref().and_then(|e| e.dynamic_verdict.as_ref());
|
|
Self::bucket_from_verdict(verdict, diag.confidence)
|
|
}
|
|
|
|
/// Discrete-bucket flavour of [`from_verdict`](Self::from_verdict).
|
|
/// Exposed for callers that want the bucket (e.g. for telemetry or
|
|
/// UI badges) before reducing to an `f32`.
|
|
pub fn bucket_from_verdict(
|
|
verdict: Option<&VerifyResult>,
|
|
static_confidence: Option<Confidence>,
|
|
) -> Feasibility {
|
|
match verdict.map(|v| v.status) {
|
|
Some(VerifyStatus::Confirmed) => Feasibility::Confirmed,
|
|
Some(VerifyStatus::Inconclusive)
|
|
if static_confidence == Some(Confidence::High) =>
|
|
{
|
|
Feasibility::InconclusiveHighConf
|
|
}
|
|
_ => Feasibility::Unverified,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::evidence::VerifyResult;
|
|
|
|
fn verdict(status: VerifyStatus) -> VerifyResult {
|
|
VerifyResult {
|
|
finding_id: "f".into(),
|
|
status,
|
|
triggered_payload: None,
|
|
reason: None,
|
|
inconclusive_reason: None,
|
|
detail: None,
|
|
attempts: vec![],
|
|
toolchain_match: None,
|
|
differential: None,
|
|
replay_stable: None,
|
|
wrong: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn confirmed_returns_one() {
|
|
let v = verdict(VerifyStatus::Confirmed);
|
|
assert_eq!(Feasibility::from_verdict(Some(&v)), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn inconclusive_without_confidence_returns_unverified() {
|
|
let v = verdict(VerifyStatus::Inconclusive);
|
|
assert_eq!(Feasibility::from_verdict(Some(&v)), 0.1);
|
|
}
|
|
|
|
#[test]
|
|
fn inconclusive_with_high_confidence_returns_half() {
|
|
let v = verdict(VerifyStatus::Inconclusive);
|
|
let b = Feasibility::bucket_from_verdict(Some(&v), Some(Confidence::High));
|
|
assert_eq!(b, Feasibility::InconclusiveHighConf);
|
|
assert_eq!(b.score(), 0.5);
|
|
}
|
|
|
|
#[test]
|
|
fn not_confirmed_returns_unverified() {
|
|
let v = verdict(VerifyStatus::NotConfirmed);
|
|
assert_eq!(Feasibility::from_verdict(Some(&v)), 0.1);
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_returns_unverified() {
|
|
let v = verdict(VerifyStatus::Unsupported);
|
|
assert_eq!(Feasibility::from_verdict(Some(&v)), 0.1);
|
|
}
|
|
|
|
#[test]
|
|
fn no_verdict_returns_unverified() {
|
|
assert_eq!(Feasibility::from_verdict(None), 0.1);
|
|
}
|
|
}
|