use serde::{Deserialize, Serialize};
use ts_rs::TS;
pub const NODE_KIND_ENTRY: &str = "entry";
pub const NODE_KIND_SINK: &str = "sink";
pub const NODE_KIND_FRAMEWORK: &str = "framework";
pub const NODE_KIND_OTHER: &str = "other";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChainReasoningNode {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph_kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ref_id: Option<String>,
pub repo: String,
pub path: String,
#[serde(default)]
pub line: Option<u32>,
pub cap: String,
pub rule: String,
pub severity: String,
pub kind: String,
#[serde(default)]
pub routes: Vec<String>,
#[serde(default)]
pub roles: Vec<String>,
#[serde(default)]
pub objects: Vec<String>,
#[serde(default)]
pub evidence_refs: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChainReasoningEdge {
pub from: String,
pub to: String,
pub label: String,
#[serde(default)]
pub cross_repo: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edge_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evidence_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChainReasoningInput {
pub run_id: String,
pub repos: Vec<String>,
pub nodes: Vec<ChainReasoningNode>,
pub edges: Vec<ChainReasoningEdge>,
pub max_chains: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChainCandidate {
pub member_ids: Vec<String>,
pub rationale: String,
#[serde(default)]
pub prerequisites: Vec<String>,
#[serde(default)]
pub evidence: Vec<String>,
#[serde(default)]
pub blast_radius: Vec<String>,
#[serde(default)]
pub confidence: u8,
#[serde(default)]
pub missing_verification_steps: Vec<String>,
#[serde(default)]
pub edge_provenance: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChainReasoningOutput {
pub chains: Vec<ChainCandidate>,
}
pub const CHAIN_REASONING_DEFAULT_MAX: u32 = 10;
pub const CHAIN_REASONING_PROMPT_VERSION: &str = "phase16.chain_reasoning.v1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
pub struct ChainRecord {
pub id: String,
pub run_id: String,
pub cross_repo: bool,
pub member_ids: String,
pub rationale_blob: Option<String>,
pub attack_provenance: Option<String>,
pub prompt_version: Option<String>,
pub status: String,
pub verification_attempt_id: Option<String>,
pub evidence_blob: Option<String>,
pub severity: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn input_roundtrips_through_serde() {
let inp = ChainReasoningInput {
run_id: "run-1".into(),
repos: vec!["repo-A".into(), "repo-B".into()],
nodes: vec![
ChainReasoningNode {
id: "abc123".into(),
graph_kind: None,
label: None,
ref_id: None,
repo: "repo-A".into(),
path: "src/router.py".into(),
line: Some(7),
cap: "SQL_QUERY".into(),
rule: "py.taint.flow".into(),
severity: "High".into(),
kind: NODE_KIND_ENTRY.into(),
routes: Vec::new(),
roles: Vec::new(),
objects: Vec::new(),
evidence_refs: Vec::new(),
},
ChainReasoningNode {
id: "def456".into(),
graph_kind: None,
label: None,
ref_id: None,
repo: "repo-B".into(),
path: "src/handlers.py".into(),
line: Some(19),
cap: "SQL_QUERY".into(),
rule: "py.sql.exec".into(),
severity: "Critical".into(),
kind: NODE_KIND_SINK.into(),
routes: Vec::new(),
roles: Vec::new(),
objects: Vec::new(),
evidence_refs: Vec::new(),
},
],
edges: vec![ChainReasoningEdge {
from: "abc123".into(),
to: "def456".into(),
label: "Reaches".into(),
cross_repo: true,
edge_id: None,
evidence_ref: None,
source: None,
}],
max_chains: CHAIN_REASONING_DEFAULT_MAX,
};
let s = serde_json::to_string(&inp).unwrap();
let back: ChainReasoningInput = serde_json::from_str(&s).unwrap();
assert_eq!(inp, back);
}
#[test]
fn output_roundtrips_through_serde() {
let out = ChainReasoningOutput {
chains: vec![ChainCandidate {
member_ids: vec!["abc123".into(), "def456".into()],
rationale: "controller in A reaches sink in B".into(),
prerequisites: Vec::new(),
evidence: Vec::new(),
blast_radius: Vec::new(),
confidence: 0,
missing_verification_steps: Vec::new(),
edge_provenance: Vec::new(),
}],
};
let s = serde_json::to_string(&out).unwrap();
let back: ChainReasoningOutput = serde_json::from_str(&s).unwrap();
assert_eq!(out, back);
}
}