1use crate::pack::resolution::{resolve_pin, TouchInput};
11use crate::pack::standards::RuleStage;
12use crate::types::{Finding, PinnedGate, PinnedRule, RuleCitation, StandardsPin};
13
14pub struct PreparedGate {
19 report: crate::gate::GateReport,
20}
21
22impl PreparedGate {
23 pub fn new(report: crate::gate::GateReport) -> Self {
24 Self { report }
25 }
26}
27
28impl crate::gate::Gate for PreparedGate {
29 fn name(&self) -> &str {
30 &self.report.name
31 }
32
33 fn kind(&self) -> crate::gate::GateKind {
34 self.report.kind
35 }
36
37 fn evaluate(&self) -> crate::gate::GateOutcome {
38 self.report.outcome.clone()
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum RuleMode {
44 Absent,
46 Advisory,
48 Authoritative,
51}
52
53pub fn rule_mode(rule: &PinnedRule) -> RuleMode {
57 match (rule.effective_status.as_str(), rule.level.as_str()) {
58 ("draft" | "retired", _) => RuleMode::Absent,
59 ("approved", "must" | "should") | ("enforced", "should") => RuleMode::Advisory,
60 ("enforced", "must") => RuleMode::Authoritative,
61 _ => RuleMode::Authoritative,
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum CheckerBinding<'a> {
67 Gate(&'a PinnedGate),
68 AgentJudgement,
69 ManualAttestation,
70 Unavailable(String),
71}
72
73pub fn checker_binding<'a>(
78 pin: &'a StandardsPin,
79 rule: &PinnedRule,
80 actual_paths: &[String],
81) -> CheckerBinding<'a> {
82 let evaluation_paths = crate::pack::resolution::evaluation_paths(pin, actual_paths);
83 match rule.checker.as_deref() {
84 Some("agent-judgement") => CheckerBinding::AgentJudgement,
85 Some("manual-attestation") => CheckerBinding::ManualAttestation,
86 Some(checker) => match checker.strip_prefix("gate:") {
87 Some(id) if !id.is_empty() => match pin.gates.iter().find(|gate| gate.id == id) {
88 Some(gate)
89 if crate::merge_gate::when_paths_match(&gate.when_paths, &evaluation_paths) =>
90 {
91 CheckerBinding::Gate(gate)
92 }
93 Some(_) => CheckerBinding::Unavailable(format!(
94 "checker `gate:{id}` is stage/path-incompatible with the actual diff"
95 )),
96 None => CheckerBinding::Unavailable(format!(
97 "checker `gate:{id}` has no approval-pinned gate declaration"
98 )),
99 },
100 _ => CheckerBinding::Unavailable(format!(
101 "unknown checker `{checker}`; supported forms are gate:<id>, \
102 agent-judgement, and manual-attestation"
103 )),
104 },
105 None => CheckerBinding::Unavailable("rule has no approval-pinned checker".to_string()),
106 }
107}
108
109pub fn applicable_rules(
112 pin: &StandardsPin,
113 stages: &[RuleStage],
114 actual_paths: &[String],
115) -> Vec<PinnedRule> {
116 let mut rules = std::collections::BTreeMap::new();
117 for stage in stages {
118 for rule in resolve_pin(pin, *stage, &TouchInput::Actual(actual_paths)) {
119 if rule_mode(&rule) != RuleMode::Absent {
120 rules.entry(rule.id.clone()).or_insert(rule);
121 }
122 }
123 }
124 rules.into_values().collect()
125}
126
127pub fn citation(pin: &StandardsPin, rule: &PinnedRule) -> RuleCitation {
128 RuleCitation {
129 id: rule.id.clone(),
130 revision: rule.revision,
131 source: format!("{} {}", pin.pack_name, pin.standards_root),
132 digest: pin.digest.clone(),
133 lifecycle: rule.effective_status.clone(),
134 level: rule.level.clone(),
135 checker: rule.checker.clone(),
136 }
137}
138
139pub fn failure_finding(pin: &StandardsPin, rule: &PinnedRule, evidence: &str) -> Finding {
144 Finding {
145 subject: format!("flight-rule:{}", rule.id),
146 severity: if rule_mode(rule) == RuleMode::Authoritative {
147 "critical".to_string()
148 } else {
149 "major".to_string()
150 },
151 evidence: evidence.to_string(),
152 suggested_fix: format!(
153 "bring the change into compliance with {} r{} or request an authorized exact waiver when the rule permits one",
154 rule.id, rule.revision
155 ),
156 class: if rule_mode(rule) == RuleMode::Authoritative {
157 "standards-authoritative".to_string()
158 } else {
159 "standards-advisory".to_string()
160 },
161 rule: Some(citation(pin, rule)),
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::types::{PinnedGate, StandardsPinSource};
169
170 fn rule(status: &str, level: &str, checker: Option<&str>) -> PinnedRule {
171 PinnedRule {
172 id: "ZZ-RULE-001".to_string(),
173 revision: 1,
174 rfc: "RFC-001".to_string(),
175 level: level.to_string(),
176 effective_status: status.to_string(),
177 statement: "Do the safe thing.".to_string(),
178 domains: Vec::new(),
179 stages: vec!["validation".to_string(), "merge".to_string()],
180 when_paths: Vec::new(),
181 task_classes: Vec::new(),
182 checker: checker.map(str::to_string),
183 waivable: true,
184 }
185 }
186
187 fn pin(rule: PinnedRule) -> StandardsPin {
188 StandardsPin {
189 pack_name: "zz-pack".to_string(),
190 pack_dir: "vendor/pack".to_string(),
191 standards_root: "standards".to_string(),
192 digest: "ab".repeat(32),
193 source: StandardsPinSource::RepoTracked,
194 task_class: None,
195 touch_set: vec!["src/**".to_string()],
196 context_paths: Vec::new(),
197 gates: vec![PinnedGate {
198 id: "zz-gate".to_string(),
199 command: "true".to_string(),
200 when_paths: Vec::new(),
201 }],
202 rules: vec![rule],
203 }
204 }
205
206 #[test]
207 fn flight_rules_enforcement_lifecycle_level_matrix_is_exact() {
208 for (status, level, expected) in [
209 ("draft", "must", RuleMode::Absent),
210 ("retired", "must", RuleMode::Absent),
211 ("approved", "must", RuleMode::Advisory),
212 ("approved", "should", RuleMode::Advisory),
213 ("enforced", "should", RuleMode::Advisory),
214 ("enforced", "must", RuleMode::Authoritative),
215 ] {
216 assert_eq!(
217 rule_mode(&rule(status, level, Some("gate:zz-gate"))),
218 expected
219 );
220 }
221 assert_eq!(
222 rule_mode(&rule("unknown", "unknown", None)),
223 RuleMode::Authoritative,
224 "corrupt pins fail closed"
225 );
226 }
227
228 #[test]
229 fn flight_rules_enforcement_checker_uses_only_pinned_gate_binding() {
230 let enforced_rule = rule("enforced", "must", Some("gate:zz-gate"));
231 let pin = pin(enforced_rule.clone());
232 let CheckerBinding::Gate(gate) =
233 checker_binding(&pin, &enforced_rule, &["src/x.rs".into()])
234 else {
235 panic!("expected pinned gate")
236 };
237 assert_eq!(gate.command, "true");
238
239 let missing = rule("enforced", "must", Some("gate:not-there"));
240 let CheckerBinding::Unavailable(reason) = checker_binding(&pin, &missing, &[]) else {
241 panic!("missing binding must fail closed")
242 };
243 assert!(reason.contains("no approval-pinned"), "{reason}");
244 }
245
246 #[test]
247 fn flight_rules_enforcement_stage_selection_is_stable_and_nonduplicating() {
248 let rule = rule("enforced", "must", Some("gate:zz-gate"));
249 let pin = pin(rule);
250 let selected = applicable_rules(
251 &pin,
252 &[RuleStage::Validation, RuleStage::Merge],
253 &["src/x.rs".to_string()],
254 );
255 assert_eq!(selected.len(), 1);
256 assert_eq!(selected[0].id, "ZZ-RULE-001");
257 }
258}