Skip to main content

hikmah_kernel/
council.rs

1//! Deterministic challenge lanes.
2//!
3//! Each lane reads one count supplied by the caller (or by a decision engine through the typed
4//! port) and reports a severity in `[0, 1]`. A lane at or above `BLOCK_AT` blocks. The risk and
5//! human-impact lanes are vetoes: one irreversible action or one unresolved human-impact
6//! question blocks on its own, so those concerns are never averaged away. The other lanes
7//! scale with their count up to a per-lane limit.
8use 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}