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}