1use serde::{Deserialize, Serialize};
9
10pub const BLOCK_AT: f32 = 0.8;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct DeliberationInput {
14 pub unverified_consequential_claims: usize,
15 pub memory_conflicts: usize,
16 pub irreversible_actions: usize,
17 pub unresolved_human_impact_questions: usize,
18 pub missing_acceptance_criteria: usize,
19}
20
21#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
22#[serde(rename_all = "snake_case")]
23pub enum Lane {
24 Evidence,
25 Memory,
26 Risk,
27 HumanImpact,
28 Delivery,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct LaneSignal {
33 pub lane: Lane,
34 pub severity: f32,
35 pub veto: bool,
36 pub message: String,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CouncilResult {
41 pub can_proceed: bool,
42 pub blocking_lanes: Vec<Lane>,
43 pub signals: Vec<LaneSignal>,
44}
45
46pub fn deliberate(input: &DeliberationInput) -> CouncilResult {
47 let signals = vec![
48 LaneSignal {
49 lane: Lane::Evidence,
50 severity: ratio(input.unverified_consequential_claims, 3),
51 veto: false,
52 message: format!(
53 "{} consequential claims still lack verification (blocks at 3)",
54 input.unverified_consequential_claims
55 ),
56 },
57 LaneSignal {
58 lane: Lane::Memory,
59 severity: ratio(input.memory_conflicts, 2),
60 veto: false,
61 message: format!(
62 "{} unresolved memory conflicts (blocks at 2)",
63 input.memory_conflicts
64 ),
65 },
66 LaneSignal {
67 lane: Lane::Risk,
68 severity: veto(input.irreversible_actions),
69 veto: true,
70 message: format!(
71 "{} irreversible actions in scope (any one blocks until a named owner accepts it)",
72 input.irreversible_actions
73 ),
74 },
75 LaneSignal {
76 lane: Lane::HumanImpact,
77 severity: veto(input.unresolved_human_impact_questions),
78 veto: true,
79 message: format!(
80 "{} unresolved human-impact questions (any one blocks)",
81 input.unresolved_human_impact_questions
82 ),
83 },
84 LaneSignal {
85 lane: Lane::Delivery,
86 severity: ratio(input.missing_acceptance_criteria, 3),
87 veto: false,
88 message: format!(
89 "{} acceptance criteria are still missing (blocks at 3)",
90 input.missing_acceptance_criteria
91 ),
92 },
93 ];
94 let blocking_lanes: Vec<Lane> = signals
95 .iter()
96 .filter(|signal| signal.severity >= BLOCK_AT)
97 .map(|signal| signal.lane)
98 .collect();
99 CouncilResult {
100 can_proceed: blocking_lanes.is_empty(),
101 blocking_lanes,
102 signals,
103 }
104}
105
106fn ratio(value: usize, blocking_at: usize) -> f32 {
107 (value as f32 / blocking_at.max(1) as f32).clamp(0.0, 1.0)
108}
109
110fn veto(value: usize) -> f32 {
111 if value > 0 {
112 1.0
113 } else {
114 0.0
115 }
116}