use crate::root_envelopes::{RootEnvelopeMode, attach_telemetry_meta, serialize_named_json_output};
use fallow_types::envelope::{ElapsedMs, Meta, ToolVersion};
use serde::Serialize;
use serde_json::Value;
pub const REVIEW_BRIEF_SCHEMA_VERSION: u32 = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ReviewBriefSchemaVersion(pub u32);
impl Default for ReviewBriefSchemaVersion {
fn default() -> Self {
Self(REVIEW_BRIEF_SCHEMA_VERSION)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RiskClass {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ReviewEffort {
Glance,
Review,
DeepDive,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DiffTriage {
pub files: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hunks: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub net_lines: Option<i64>,
pub risk_class: RiskClass,
pub review_effort: ReviewEffort,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct GraphFacts {
pub exports_added: usize,
pub api_width_delta: i64,
pub reachable_from: Vec<String>,
pub boundaries_touched: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ImpactClosureFacts {
pub affected_not_shown: Vec<String>,
pub coordination_gap: Vec<CoordinationGapFact>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CoordinationGapFact {
pub changed_file: String,
pub consumer_file: String,
pub consumed_symbols: Vec<String>,
pub note: String,
}
#[derive(Debug, Clone, Default, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PartitionFacts {
pub units: Vec<ReviewUnitFact>,
pub order: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ReviewUnitFact {
pub module_dir: String,
pub files: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ReviewDeltas {
pub boundary_introduced: Vec<String>,
pub cycle_introduced: Vec<String>,
pub public_api_added: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schema",
schemars(title = "fallow audit --brief --format json")
)]
pub struct ReviewBriefOutput<Focus, Weakening, Routing, Decisions> {
pub schema_version: ReviewBriefSchemaVersion,
pub version: String,
pub command: String,
pub triage: DiffTriage,
pub graph_facts: GraphFacts,
pub partition: PartitionFacts,
pub impact_closure: ImpactClosureFacts,
pub focus: Focus,
pub deltas: ReviewDeltas,
pub weakening: Vec<Weakening>,
pub routing: Routing,
pub decisions: Decisions,
}
pub type StandardReviewBriefOutput = ReviewBriefOutput<
crate::audit_focus::FocusMap,
crate::audit_weakening::WeakeningSignal,
crate::audit_routing::RoutingFacts,
crate::audit_decision_surface::DecisionSurface,
>;
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ReviewBriefHeader<Verdict, Summary, Attribution> {
pub version: ToolVersion,
pub verdict: Verdict,
pub changed_files_count: u32,
pub base_ref: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub head_sha: Option<String>,
pub elapsed_ms: ElapsedMs,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_snapshot_skipped: Option<bool>,
pub summary: Summary,
pub attribution: Attribution,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schema",
schemars(title = "fallow audit --brief --format json")
)]
pub struct ReviewBriefWireOutput<
Focus,
Weakening,
Routing,
Decisions,
Verdict,
Summary,
Attribution,
DeadCode,
Duplication,
Complexity,
> {
pub schema_version: ReviewBriefSchemaVersion,
pub version: ToolVersion,
pub command: String,
pub verdict: Verdict,
pub changed_files_count: u32,
pub base_ref: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub head_sha: Option<String>,
pub elapsed_ms: ElapsedMs,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_snapshot_skipped: Option<bool>,
pub summary: Summary,
pub attribution: Attribution,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub decisions: Decisions,
pub triage: DiffTriage,
pub graph_facts: GraphFacts,
pub partition: PartitionFacts,
pub impact_closure: ImpactClosureFacts,
pub focus: Focus,
pub deltas: ReviewDeltas,
pub weakening: Vec<Weakening>,
pub routing: Routing,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dead_code: Option<DeadCode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duplication: Option<Duplication>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub complexity: Option<Complexity>,
}
#[derive(Debug, Clone, Default)]
pub struct ReviewBriefSubtractSections<DeadCode = Value, Duplication = Value, Complexity = Value> {
pub dead_code: Option<DeadCode>,
pub duplication: Option<Duplication>,
pub complexity: Option<Complexity>,
}
pub fn build_review_brief_json_output<
Focus,
Weakening,
Routing,
Decisions,
Verdict,
Summary,
Attribution,
DeadCode,
Duplication,
Complexity,
>(
brief: ReviewBriefOutput<Focus, Weakening, Routing, Decisions>,
header: ReviewBriefHeader<Verdict, Summary, Attribution>,
subtract: ReviewBriefSubtractSections<DeadCode, Duplication, Complexity>,
) -> Result<Value, serde_json::Error>
where
Focus: Serialize,
Weakening: Serialize,
Routing: Serialize,
Decisions: Serialize,
Verdict: Serialize,
Summary: Serialize,
Attribution: Serialize,
DeadCode: Serialize,
Duplication: Serialize,
Complexity: Serialize,
{
serde_json::to_value(ReviewBriefWireOutput {
schema_version: brief.schema_version,
version: header.version,
command: brief.command,
verdict: header.verdict,
changed_files_count: header.changed_files_count,
base_ref: header.base_ref,
base_description: header.base_description,
head_sha: header.head_sha,
elapsed_ms: header.elapsed_ms,
base_snapshot_skipped: header.base_snapshot_skipped,
summary: header.summary,
attribution: header.attribution,
meta: None,
decisions: brief.decisions,
triage: brief.triage,
graph_facts: brief.graph_facts,
partition: brief.partition,
impact_closure: brief.impact_closure,
focus: brief.focus,
deltas: brief.deltas,
weakening: brief.weakening,
routing: brief.routing,
dead_code: subtract.dead_code,
duplication: subtract.duplication,
complexity: subtract.complexity,
})
}
fn serialize_agent_contract_json_output<T: Serialize>(
output: T,
kind: &'static str,
mode: RootEnvelopeMode,
analysis_run_id: Option<&str>,
) -> Result<Value, serde_json::Error> {
let mut value = serialize_named_json_output(output, kind, mode)?;
attach_telemetry_meta(&mut value, analysis_run_id);
Ok(value)
}
pub fn serialize_review_brief_json_output<T: Serialize>(
output: T,
mode: RootEnvelopeMode,
analysis_run_id: Option<&str>,
) -> Result<Value, serde_json::Error> {
serialize_agent_contract_json_output(output, "audit-brief", mode, analysis_run_id)
}
pub fn serialize_decision_surface_json_output<T: Serialize>(
output: T,
mode: RootEnvelopeMode,
analysis_run_id: Option<&str>,
) -> Result<Value, serde_json::Error> {
serialize_agent_contract_json_output(output, "decision-surface", mode, analysis_run_id)
}
pub fn serialize_walkthrough_guide_json_output<T: Serialize>(
output: T,
mode: RootEnvelopeMode,
analysis_run_id: Option<&str>,
) -> Result<Value, serde_json::Error> {
serialize_agent_contract_json_output(output, "review-walkthrough-guide", mode, analysis_run_id)
}
pub fn serialize_walkthrough_validation_json_output<T: Serialize>(
output: T,
mode: RootEnvelopeMode,
analysis_run_id: Option<&str>,
) -> Result<Value, serde_json::Error> {
serialize_agent_contract_json_output(
output,
"review-walkthrough-validation",
mode,
analysis_run_id,
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn review_brief_json_output_assembles_typed_wire_contract() {
let brief = ReviewBriefOutput {
schema_version: ReviewBriefSchemaVersion::default(),
version: "1.2.3".to_string(),
command: "audit-brief".to_string(),
triage: DiffTriage {
files: 1,
hunks: None,
net_lines: None,
risk_class: RiskClass::Low,
review_effort: ReviewEffort::Glance,
},
graph_facts: GraphFacts {
exports_added: 0,
api_width_delta: 0,
reachable_from: Vec::new(),
boundaries_touched: Vec::new(),
},
partition: PartitionFacts::default(),
impact_closure: ImpactClosureFacts::default(),
focus: json!({"units": []}),
deltas: ReviewDeltas::default(),
weakening: Vec::<Value>::new(),
routing: json!({"units": []}),
decisions: json!({"decisions": []}),
};
let header = ReviewBriefHeader {
version: ToolVersion("1.2.3".to_string()),
verdict: json!("fail"),
changed_files_count: 1,
base_ref: "main".to_string(),
base_description: Some("merge base".to_string()),
head_sha: Some("abc123".to_string()),
elapsed_ms: ElapsedMs(12),
base_snapshot_skipped: Some(false),
summary: json!({"dead_code_issues": 0}),
attribution: json!({"gate": "new_only"}),
};
let value = build_review_brief_json_output(
brief,
header,
ReviewBriefSubtractSections::<Value, Value, Value> {
dead_code: Some(json!({"issues": []})),
duplication: None,
complexity: None,
},
)
.expect("brief output should serialize");
assert_eq!(value["schema_version"], REVIEW_BRIEF_SCHEMA_VERSION);
assert_eq!(value["command"], "audit-brief");
assert_eq!(value["verdict"], "fail");
assert_eq!(value["base_ref"], "main");
assert_eq!(value["summary"]["dead_code_issues"], 0);
assert_eq!(value["attribution"]["gate"], "new_only");
assert_eq!(value["dead_code"]["issues"], json!([]));
}
#[test]
fn review_brief_serializer_owns_root_contract() {
let value = serialize_review_brief_json_output(
json!({"command": "audit-brief"}),
RootEnvelopeMode::Tagged,
Some("run-brief"),
)
.expect("brief output should serialize");
assert_eq!(value["kind"], "audit-brief");
assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-brief");
}
#[test]
fn decision_surface_serializer_owns_root_contract() {
let value = serialize_decision_surface_json_output(
json!({"decisions": []}),
RootEnvelopeMode::Tagged,
Some("run-decision"),
)
.expect("decision surface should serialize");
assert_eq!(value["kind"], "decision-surface");
assert_eq!(
value["_meta"]["telemetry"]["analysis_run_id"],
"run-decision"
);
}
}