Skip to main content

mempal_runtime/
knowledge_gate.rs

1use anyhow::{Context, Result, bail};
2use serde::Serialize;
3
4use crate::core::{
5    db::Database,
6    types::{Drawer, KnowledgeStatus, KnowledgeTier, MemoryKind},
7};
8
9#[derive(Debug, Clone, Serialize)]
10pub struct GateReport {
11    pub drawer_id: String,
12    pub tier: String,
13    pub status: String,
14    pub target_status: String,
15    pub allowed: bool,
16    pub reasons: Vec<String>,
17    pub requirements: GateRequirements,
18    pub evidence_counts: GateEvidenceCounts,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct GateRequirements {
23    pub min_supporting_refs: usize,
24    pub min_verification_refs: usize,
25    pub min_teaching_refs: usize,
26    pub reviewer_required: bool,
27    pub counterexamples_block: bool,
28}
29
30#[derive(Debug, Clone, Serialize)]
31pub struct GateEvidenceCounts {
32    pub supporting: usize,
33    pub counterexample: usize,
34    pub teaching: usize,
35    pub verification: usize,
36}
37
38#[derive(Debug, Clone, Serialize)]
39pub struct PromotionPolicyEntry {
40    pub tier: String,
41    pub target_status: String,
42    pub requirements: GateRequirements,
43}
44
45pub fn promotion_policy() -> Vec<PromotionPolicyEntry> {
46    [
47        (KnowledgeTier::DaoTian, KnowledgeStatus::Canonical),
48        (KnowledgeTier::DaoRen, KnowledgeStatus::Promoted),
49        (KnowledgeTier::Shu, KnowledgeStatus::Promoted),
50        (KnowledgeTier::Qi, KnowledgeStatus::Promoted),
51    ]
52    .into_iter()
53    .map(|(tier, target_status)| PromotionPolicyEntry {
54        tier: tier_slug(&tier).to_string(),
55        target_status: status_slug(&target_status).to_string(),
56        requirements: gate_requirements_for_policy(&tier, &target_status),
57    })
58    .collect()
59}
60
61pub fn evaluate_gate_by_id(
62    db: &Database,
63    drawer_id: &str,
64    target_status: Option<&str>,
65    reviewer: Option<&str>,
66    allow_counterexamples: bool,
67) -> Result<GateReport> {
68    let drawer = load_gate_knowledge_drawer(db, drawer_id)?;
69    let tier = drawer.tier.as_ref().expect("knowledge drawer has tier");
70    let target_status = match target_status {
71        Some(value) => parse_status(value)?,
72        None => default_target_status(tier),
73    };
74    validate_tier_status(tier, &target_status)?;
75    evaluate_gate(db, &drawer, &target_status, reviewer, allow_counterexamples)
76}
77
78pub fn evaluate_gate_for_drawer(
79    db: &Database,
80    drawer: &Drawer,
81    target_status: &KnowledgeStatus,
82    reviewer: Option<&str>,
83    allow_counterexamples: bool,
84) -> Result<GateReport> {
85    if drawer.memory_kind != MemoryKind::Knowledge {
86        bail!("knowledge gate requires a knowledge drawer");
87    }
88    let tier = drawer
89        .tier
90        .as_ref()
91        .context("knowledge gate requires tier metadata")?;
92    validate_tier_status(tier, target_status)?;
93    evaluate_gate(db, drawer, target_status, reviewer, allow_counterexamples)
94}
95
96fn load_gate_knowledge_drawer(db: &Database, drawer_id: &str) -> Result<Drawer> {
97    let drawer = db
98        .get_drawer(drawer_id)
99        .context("failed to look up drawer")?
100        .with_context(|| format!("drawer not found: {drawer_id}"))?;
101    if drawer.memory_kind != MemoryKind::Knowledge {
102        bail!("knowledge gate requires a knowledge drawer");
103    }
104    if drawer.tier.is_none() || drawer.status.is_none() {
105        bail!("knowledge gate requires tier and status metadata");
106    }
107    Ok(drawer)
108}
109
110fn parse_status(value: &str) -> Result<KnowledgeStatus> {
111    match value.trim() {
112        "candidate" => Ok(KnowledgeStatus::Candidate),
113        "promoted" => Ok(KnowledgeStatus::Promoted),
114        "canonical" => Ok(KnowledgeStatus::Canonical),
115        "demoted" => Ok(KnowledgeStatus::Demoted),
116        "retired" => Ok(KnowledgeStatus::Retired),
117        other => bail!("unsupported knowledge status: {other}"),
118    }
119}
120
121fn default_target_status(tier: &KnowledgeTier) -> KnowledgeStatus {
122    match tier {
123        KnowledgeTier::DaoTian => KnowledgeStatus::Canonical,
124        KnowledgeTier::DaoRen | KnowledgeTier::Shu | KnowledgeTier::Qi => KnowledgeStatus::Promoted,
125    }
126}
127
128fn validate_tier_status(tier: &KnowledgeTier, status: &KnowledgeStatus) -> Result<()> {
129    let allowed = match tier {
130        KnowledgeTier::DaoTian => &[KnowledgeStatus::Canonical, KnowledgeStatus::Demoted][..],
131        KnowledgeTier::DaoRen => &[
132            KnowledgeStatus::Candidate,
133            KnowledgeStatus::Promoted,
134            KnowledgeStatus::Demoted,
135            KnowledgeStatus::Retired,
136        ][..],
137        KnowledgeTier::Shu => &[
138            KnowledgeStatus::Promoted,
139            KnowledgeStatus::Demoted,
140            KnowledgeStatus::Retired,
141        ][..],
142        KnowledgeTier::Qi => &[
143            KnowledgeStatus::Candidate,
144            KnowledgeStatus::Promoted,
145            KnowledgeStatus::Demoted,
146            KnowledgeStatus::Retired,
147        ][..],
148    };
149
150    if allowed.contains(status) {
151        return Ok(());
152    }
153
154    match tier {
155        KnowledgeTier::DaoTian => bail!("dao_tian only allows canonical or demoted"),
156        KnowledgeTier::DaoRen => {
157            bail!("dao_ren only allows candidate, promoted, demoted, or retired")
158        }
159        KnowledgeTier::Shu => bail!("shu only allows promoted, demoted, or retired"),
160        KnowledgeTier::Qi => bail!("qi only allows candidate, promoted, demoted, or retired"),
161    }
162}
163
164fn gate_requirements(tier: &KnowledgeTier, target_status: &KnowledgeStatus) -> GateRequirements {
165    promotion_policy()
166        .into_iter()
167        .find(|entry| {
168            entry.tier == tier_slug(tier) && entry.target_status == status_slug(target_status)
169        })
170        .map(|entry| entry.requirements)
171        .unwrap_or_else(|| GateRequirements {
172            min_supporting_refs: 0,
173            min_verification_refs: 0,
174            min_teaching_refs: 0,
175            reviewer_required: false,
176            counterexamples_block: true,
177        })
178}
179
180fn gate_requirements_for_policy(
181    tier: &KnowledgeTier,
182    target_status: &KnowledgeStatus,
183) -> GateRequirements {
184    match (tier, target_status) {
185        (KnowledgeTier::DaoTian, KnowledgeStatus::Canonical) => GateRequirements {
186            min_supporting_refs: 3,
187            min_verification_refs: 2,
188            min_teaching_refs: 1,
189            reviewer_required: true,
190            counterexamples_block: true,
191        },
192        (KnowledgeTier::DaoRen, KnowledgeStatus::Promoted) => GateRequirements {
193            min_supporting_refs: 2,
194            min_verification_refs: 1,
195            min_teaching_refs: 0,
196            reviewer_required: false,
197            counterexamples_block: true,
198        },
199        (KnowledgeTier::Shu | KnowledgeTier::Qi, KnowledgeStatus::Promoted) => GateRequirements {
200            min_supporting_refs: 1,
201            min_verification_refs: 1,
202            min_teaching_refs: 0,
203            reviewer_required: false,
204            counterexamples_block: true,
205        },
206        _ => unreachable!("promotion_policy only requests defined gate policy entries"),
207    }
208}
209
210fn evaluate_gate(
211    db: &Database,
212    drawer: &Drawer,
213    target_status: &KnowledgeStatus,
214    reviewer: Option<&str>,
215    allow_counterexamples: bool,
216) -> Result<GateReport> {
217    validate_gate_refs(db, &drawer.supporting_refs)?;
218    validate_gate_refs(db, &drawer.counterexample_refs)?;
219    validate_gate_refs(db, &drawer.teaching_refs)?;
220    validate_gate_refs(db, &drawer.verification_refs)?;
221
222    let tier = drawer.tier.as_ref().expect("knowledge drawer has tier");
223    let status = drawer.status.as_ref().expect("knowledge drawer has status");
224    let requirements = gate_requirements(tier, target_status);
225    let evidence_counts = GateEvidenceCounts {
226        supporting: drawer.supporting_refs.len(),
227        counterexample: drawer.counterexample_refs.len(),
228        teaching: drawer.teaching_refs.len(),
229        verification: drawer.verification_refs.len(),
230    };
231    let mut reasons = Vec::new();
232    if evidence_counts.supporting < requirements.min_supporting_refs {
233        reasons.push(format!(
234            "supporting evidence refs below requirement: have {}, need {}",
235            evidence_counts.supporting, requirements.min_supporting_refs
236        ));
237    }
238    if evidence_counts.verification < requirements.min_verification_refs {
239        reasons.push(format!(
240            "verification evidence refs below requirement: have {}, need {}",
241            evidence_counts.verification, requirements.min_verification_refs
242        ));
243    }
244    if evidence_counts.teaching < requirements.min_teaching_refs {
245        reasons.push(format!(
246            "teaching evidence refs below requirement: have {}, need {}",
247            evidence_counts.teaching, requirements.min_teaching_refs
248        ));
249    }
250    if requirements.reviewer_required
251        && reviewer
252            .map(str::trim)
253            .filter(|value| !value.is_empty())
254            .is_none()
255    {
256        reasons.push("reviewer is required for this gate".to_string());
257    }
258    if requirements.counterexamples_block
259        && evidence_counts.counterexample > 0
260        && !allow_counterexamples
261    {
262        reasons.push(format!(
263            "counterexample refs present: {}",
264            evidence_counts.counterexample
265        ));
266    }
267
268    Ok(GateReport {
269        drawer_id: drawer.id.clone(),
270        tier: tier_slug(tier).to_string(),
271        status: status_slug(status).to_string(),
272        target_status: status_slug(target_status).to_string(),
273        allowed: reasons.is_empty(),
274        reasons,
275        requirements,
276        evidence_counts,
277    })
278}
279
280fn validate_gate_refs(db: &Database, refs: &[String]) -> Result<()> {
281    for drawer_id in refs {
282        if !drawer_id.starts_with("drawer_") {
283            bail!("gate refs must contain drawer ids");
284        }
285        let drawer = db
286            .get_drawer(drawer_id)
287            .with_context(|| format!("failed to load ref drawer {drawer_id}"))?
288            .with_context(|| format!("ref drawer not found: {drawer_id}"))?;
289        if drawer.memory_kind != MemoryKind::Evidence {
290            bail!("gate refs must point to evidence drawers");
291        }
292    }
293    Ok(())
294}
295
296fn tier_slug(value: &KnowledgeTier) -> &'static str {
297    match value {
298        KnowledgeTier::Qi => "qi",
299        KnowledgeTier::Shu => "shu",
300        KnowledgeTier::DaoRen => "dao_ren",
301        KnowledgeTier::DaoTian => "dao_tian",
302    }
303}
304
305fn status_slug(value: &KnowledgeStatus) -> &'static str {
306    match value {
307        KnowledgeStatus::Candidate => "candidate",
308        KnowledgeStatus::Promoted => "promoted",
309        KnowledgeStatus::Canonical => "canonical",
310        KnowledgeStatus::Demoted => "demoted",
311        KnowledgeStatus::Retired => "retired",
312    }
313}