Skip to main content

aidens_governance_kit/
lib.rs

1//! Thin governance facade over canonical verification and policy crates.
2
3pub mod verification;
4
5pub use verification::{
6    evaluate_promotion, ClaimEvidenceBundleV1, GovernanceAction, GovernanceDecisionV1, RiskClass,
7    VerificationDisposition, VerificationPlanV1,
8};
9
10pub mod canonical_stack {
11    pub use assurance_runtime;
12    pub use authority_delegation::{
13        ActingOnBehalfReceiptV1, AuthorityChainV1, AuthorityLeaseV1, DelegationBundleV1,
14    };
15    pub use stack_ids::{AttemptId, ClaimVersionId, ScopeKey, TraceCtx};
16    pub use verification_adjudication;
17    pub use verification_adjudication::{
18        AdjudicationResult, PromotionDecision, VerificationDisposition,
19    };
20    pub use verification_calibration::CalibrationSnapshot;
21    pub use verification_control::{
22        CaseRegion, CheckMethod, CheckPlan, ControlReceipt, PromotionClass, ReversibilityClass,
23        TerminalDisposition, VerificationAttempt, VerificationAttemptState, VerificationCase,
24        VerificationCaseClass,
25    };
26    pub use verification_policy::{
27        ApprovalRecord, ApprovalRequirement, ApprovalScope, AutonomyCeiling,
28        DelegationPolicyProfileV1, EffectPolicyProfileV1, ExecutionPermit, MethodPolicy,
29        PermitIssuanceError, PolicyDecision, PolicySnapshot, ReleasePolicyProfileV1,
30    };
31}
32
33pub use canonical_stack::{CheckPlan, VerificationCase};
34
35#[derive(Debug, Clone, Copy, Default)]
36pub struct CanonicalGovernanceAdapter;
37
38impl CanonicalGovernanceAdapter {
39    #[allow(clippy::too_many_arguments)]
40    pub fn verification_case(
41        &self,
42        class: canonical_stack::VerificationCaseClass,
43        namespace: impl Into<String>,
44        target_key: impl Into<String>,
45        trace_ctx: canonical_stack::TraceCtx,
46        attempt_id: canonical_stack::AttemptId,
47        opened_at: impl Into<String>,
48        degraded: bool,
49        advisory_only: bool,
50    ) -> canonical_stack::VerificationCase {
51        let namespace = namespace.into();
52        canonical_stack::VerificationCase::new(
53            class,
54            canonical_stack::CaseRegion {
55                scope_key: Some(canonical_stack::ScopeKey::namespace_only(namespace.clone())),
56                namespace,
57                target_key: target_key.into(),
58                region_id: None,
59                region_digest_id: None,
60                claim_version_id: None,
61                as_of_recorded_at: None,
62            },
63            trace_ctx,
64            attempt_id,
65            opened_at,
66            degraded,
67            advisory_only,
68        )
69    }
70
71    pub fn claim_version_case(
72        &self,
73        namespace: impl Into<String>,
74        claim_version_id: canonical_stack::ClaimVersionId,
75        trace_ctx: canonical_stack::TraceCtx,
76        attempt_id: canonical_stack::AttemptId,
77        opened_at: impl Into<String>,
78    ) -> canonical_stack::VerificationCase {
79        let namespace = namespace.into();
80        canonical_stack::VerificationCase::new(
81            canonical_stack::VerificationCaseClass::UnverifiedClaimVersion,
82            canonical_stack::CaseRegion {
83                scope_key: Some(canonical_stack::ScopeKey::namespace_only(namespace.clone())),
84                namespace,
85                target_key: claim_version_id.as_str().to_owned(),
86                region_id: None,
87                region_digest_id: None,
88                claim_version_id: Some(claim_version_id),
89                as_of_recorded_at: None,
90            },
91            trace_ctx,
92            attempt_id,
93            opened_at,
94            false,
95            false,
96        )
97    }
98
99    #[allow(clippy::too_many_arguments)]
100    pub fn check_plan(
101        &self,
102        case: &canonical_stack::VerificationCase,
103        method: canonical_stack::CheckMethod,
104        check_names: Vec<String>,
105        promotion_class: canonical_stack::PromotionClass,
106        reversibility_class: canonical_stack::ReversibilityClass,
107        promotable_if_completed: bool,
108        advisory_only: bool,
109        degraded: bool,
110        rationale: impl Into<String>,
111        input: serde_json::Value,
112    ) -> canonical_stack::CheckPlan {
113        canonical_stack::CheckPlan::new(
114            case.case_id.clone(),
115            method,
116            check_names,
117            promotion_class,
118            reversibility_class,
119            promotable_if_completed,
120            advisory_only,
121            degraded,
122            rationale,
123            input,
124        )
125    }
126
127    #[allow(clippy::too_many_arguments)]
128    pub fn completed_attempt(
129        &self,
130        case: &canonical_stack::VerificationCase,
131        plan: &canonical_stack::CheckPlan,
132        state: canonical_stack::VerificationAttemptState,
133        started_at: impl Into<String>,
134        finished_at: impl Into<String>,
135        outcome_signature: Option<String>,
136    ) -> canonical_stack::VerificationAttempt {
137        canonical_stack::VerificationAttempt::completed(
138            case.case_id.clone(),
139            plan.plan_id.clone(),
140            case.attempt_id.clone(),
141            None,
142            state,
143            plan.advisory_only,
144            plan.degraded,
145            started_at,
146            finished_at,
147            outcome_signature,
148        )
149    }
150
151    pub fn control_receipt_for_attempt(
152        &self,
153        case: &canonical_stack::VerificationCase,
154        plan: &canonical_stack::CheckPlan,
155        attempt: &canonical_stack::VerificationAttempt,
156        promotable: bool,
157        details: serde_json::Value,
158    ) -> canonical_stack::ControlReceipt {
159        canonical_stack::ControlReceipt::new_case_execution(
160            case, plan, attempt, promotable, details,
161        )
162    }
163
164    #[allow(clippy::too_many_arguments)]
165    pub fn approval_record(
166        &self,
167        policy_version: impl Into<String>,
168        case: Option<&canonical_stack::VerificationCase>,
169        plan: Option<&canonical_stack::CheckPlan>,
170        scope: canonical_stack::ApprovalScope,
171        approver: impl Into<String>,
172        approved_at: impl Into<String>,
173        reviewed_artifact_refs: Vec<String>,
174        rationale: impl Into<String>,
175    ) -> canonical_stack::ApprovalRecord {
176        canonical_stack::ApprovalRecord::new(
177            policy_version,
178            case.map(|case| case.case_id.clone()),
179            plan.map(|plan| plan.plan_id.clone()),
180            scope,
181            approver,
182            approved_at,
183            reviewed_artifact_refs,
184            rationale,
185        )
186    }
187
188    pub fn evaluate_policy(
189        &self,
190        snapshot: &canonical_stack::PolicySnapshot,
191        case: &canonical_stack::VerificationCase,
192        plan: &canonical_stack::CheckPlan,
193        approvals: &[canonical_stack::ApprovalRecord],
194        degraded: bool,
195        budget_exhausted: bool,
196    ) -> canonical_stack::PolicyDecision {
197        verification_policy::evaluate_policy(
198            snapshot,
199            case,
200            plan,
201            approvals,
202            degraded,
203            budget_exhausted,
204        )
205    }
206
207    #[allow(clippy::too_many_arguments)]
208    pub fn calibration_snapshot(
209        &self,
210        case: &canonical_stack::VerificationCase,
211        recorded_at: impl Into<String>,
212        comparability_available: bool,
213        oracle_calibrated: bool,
214        abstention_threshold_micros: u64,
215        observed_risk_micros: u64,
216        drift_markers: Vec<String>,
217    ) -> canonical_stack::CalibrationSnapshot {
218        canonical_stack::CalibrationSnapshot::evaluate(
219            case.case_id.clone(),
220            recorded_at,
221            comparability_available,
222            oracle_calibrated,
223            abstention_threshold_micros,
224            observed_risk_micros,
225            drift_markers,
226        )
227    }
228
229    #[allow(clippy::too_many_arguments)]
230    pub fn adjudicate_case(
231        &self,
232        case: &canonical_stack::VerificationCase,
233        plan: &canonical_stack::CheckPlan,
234        attempt: &canonical_stack::VerificationAttempt,
235        control_receipt: &canonical_stack::ControlReceipt,
236        policy_decision: &canonical_stack::PolicyDecision,
237        calibration_snapshot: &canonical_stack::CalibrationSnapshot,
238        refuted: bool,
239        budget_exhausted: bool,
240        currently_promoted: bool,
241    ) -> canonical_stack::AdjudicationResult {
242        verification_adjudication::adjudicate_case(
243            case,
244            plan,
245            attempt,
246            control_receipt,
247            policy_decision,
248            calibration_snapshot,
249            refuted,
250            budget_exhausted,
251            currently_promoted,
252        )
253    }
254
255    pub fn release_readiness_decision(
256        &self,
257        deployment_profile: &assurance_runtime::DeploymentProfileV1,
258        blocking_gaps: Vec<String>,
259        required_monitors: Vec<String>,
260        advisory_only: bool,
261    ) -> assurance_runtime::ReleaseReadinessDecisionV1 {
262        let decision_state = if !blocking_gaps.is_empty() {
263            assurance_runtime::ReleaseDecisionStateV1::Blocked
264        } else if !required_monitors.is_empty() {
265            assurance_runtime::ReleaseDecisionStateV1::ApprovedWithMonitoring
266        } else {
267            assurance_runtime::ReleaseDecisionStateV1::Approved
268        };
269
270        let decision_id = stack_ids::ArtifactId::generate().as_str().to_string();
271        let deployment_profile_id = deployment_profile.deployment_profile_id.clone();
272        let generated_at = chrono::Utc::now().to_rfc3339();
273
274        match assurance_runtime::ReleaseReadinessDecisionV1::new(
275            decision_id.clone(),
276            deployment_profile_id.clone(),
277            decision_state,
278            blocking_gaps,
279            required_monitors,
280            advisory_only,
281            generated_at.clone(),
282        ) {
283            Ok(decision) => decision,
284            Err(_) => assurance_runtime::ReleaseReadinessDecisionV1 {
285                schema_version: "ReleaseReadinessDecisionV1".to_string(),
286                release_readiness_decision_id: decision_id,
287                deployment_profile_id,
288                decision_state: assurance_runtime::ReleaseDecisionStateV1::Blocked,
289                blocking_gaps: vec!["construct-fallback".to_string()],
290                required_monitors: Vec::new(),
291                advisory_only,
292                generated_at,
293            },
294        }
295    }
296
297    pub fn create_verification_plan(
298        &self,
299        claim_id: &str,
300        risk_class: crate::RiskClass,
301    ) -> crate::VerificationPlanV1 {
302        crate::VerificationPlanV1::new(claim_id, risk_class)
303    }
304
305    pub fn create_claim_evidence_bundle(
306        &self,
307        claim_id: &str,
308        treatment: &str,
309        outcome: &str,
310    ) -> crate::ClaimEvidenceBundleV1 {
311        crate::ClaimEvidenceBundleV1::new(claim_id, treatment, outcome)
312    }
313
314    pub fn authority_chain(
315        &self,
316        lease: &canonical_stack::AuthorityLeaseV1,
317        acting_on_behalf: &canonical_stack::ActingOnBehalfReceiptV1,
318    ) -> canonical_stack::DelegationBundleV1 {
319        let delegation_bundle_id = stack_ids::ArtifactId::generate().as_str().to_string();
320
321        let mut delegated_rights = vec![acting_on_behalf.delegated_action.clone()];
322        if delegated_rights.iter().all(|value| value.trim().is_empty()) {
323            delegated_rights = vec!["delegated-action:fallback".to_string()];
324        }
325
326        let mut replay_requirements = vec![acting_on_behalf.effect_execution_receipt_id.clone()];
327        if replay_requirements
328            .iter()
329            .all(|value| value.trim().is_empty())
330        {
331            replay_requirements = vec!["replay-requirement:fallback".to_string()];
332        }
333
334        let authority_lease_id = if lease.authority_lease_id.is_empty() {
335            "authority-lease:unknown".to_string()
336        } else {
337            lease.authority_lease_id.clone()
338        };
339
340        match canonical_stack::DelegationBundleV1::new(
341            delegation_bundle_id.clone(),
342            authority_lease_id,
343            delegated_rights,
344            lease.hard_ceilings.clone(),
345            replay_requirements,
346            acting_on_behalf.within_lease,
347        ) {
348            Ok(bundle) => bundle,
349            Err(_) => canonical_stack::DelegationBundleV1 {
350                schema_version: "DelegationBundleV1".to_string(),
351                delegation_bundle_id,
352                authority_lease_id: lease.authority_lease_id.clone(),
353                delegated_rights: vec!["delegated-action:fallback".to_string()],
354                reduced_ceilings: lease.hard_ceilings.clone(),
355                replay_requirements: vec!["replay-requirement:fallback".to_string()],
356                further_delegation_permitted: false,
357            },
358        }
359    }
360}
361
362#[derive(Debug, Clone)]
363pub struct GovernanceContext {
364    adapter: CanonicalGovernanceAdapter,
365    policy_snapshot: canonical_stack::PolicySnapshot,
366}
367
368impl GovernanceContext {
369    pub fn new(policy: canonical_stack::PolicySnapshot) -> Self {
370        Self {
371            adapter: CanonicalGovernanceAdapter::default(),
372            policy_snapshot: policy,
373        }
374    }
375
376    pub fn open_case(
377        &self,
378        class: canonical_stack::VerificationCaseClass,
379        namespace: impl Into<String>,
380        target_key: impl Into<String>,
381        trace_ctx: canonical_stack::TraceCtx,
382        attempt_id: canonical_stack::AttemptId,
383    ) -> canonical_stack::VerificationCase {
384        self.adapter.verification_case(
385            class,
386            namespace,
387            target_key,
388            trace_ctx,
389            attempt_id,
390            chrono::Utc::now().to_rfc3339(),
391            false,
392            false,
393        )
394    }
395
396    pub fn check_permit(
397        &self,
398        case: &canonical_stack::VerificationCase,
399        plan: &canonical_stack::CheckPlan,
400    ) -> Result<canonical_stack::ExecutionPermit, canonical_stack::PermitIssuanceError> {
401        let decision =
402            self.adapter
403                .evaluate_policy(&self.policy_snapshot, case, plan, &[], false, false);
404        decision.issue_execution_permit(case, plan, &[])
405    }
406
407    pub fn adjudicate(
408        &self,
409        case: &canonical_stack::VerificationCase,
410        plan: &canonical_stack::CheckPlan,
411        attempt: &canonical_stack::VerificationAttempt,
412        control_receipt: &canonical_stack::ControlReceipt,
413        policy_decision: &canonical_stack::PolicyDecision,
414        calibration: &canonical_stack::CalibrationSnapshot,
415        refuted: bool,
416        budget_exhausted: bool,
417        currently_promoted: bool,
418    ) -> canonical_stack::AdjudicationResult {
419        self.adapter.adjudicate_case(
420            case,
421            plan,
422            attempt,
423            control_receipt,
424            policy_decision,
425            calibration,
426            refuted,
427            budget_exhausted,
428            currently_promoted,
429        )
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn creates_canonical_control_receipt() {
439        let adapter = CanonicalGovernanceAdapter;
440        let case = adapter.claim_version_case(
441            "aidens",
442            canonical_stack::ClaimVersionId::new("claim-version:governance"),
443            canonical_stack::TraceCtx::from_trace_id("trace:governance"),
444            canonical_stack::AttemptId::new("attempt:governance"),
445            "2026-04-28T00:00:00Z",
446        );
447        let plan = adapter.check_plan(
448            &case,
449            canonical_stack::CheckMethod::CausalRefuter,
450            vec!["causal-refuter".into()],
451            canonical_stack::PromotionClass::P1,
452            canonical_stack::ReversibilityClass::RequiresSupersession,
453            true,
454            false,
455            false,
456            "canonical verification-control check plan",
457            serde_json::json!({"source": "aidens-governance-kit"}),
458        );
459        let attempt = adapter.completed_attempt(
460            &case,
461            &plan,
462            canonical_stack::VerificationAttemptState::Succeeded,
463            "2026-04-28T00:00:00Z",
464            "2026-04-28T00:00:01Z",
465            Some("ok".into()),
466        );
467        let receipt = adapter.control_receipt_for_attempt(
468            &case,
469            &plan,
470            &attempt,
471            true,
472            serde_json::json!({"promotable": true}),
473        );
474
475        assert_eq!(
476            receipt.schema_version,
477            verification_control::CONTROL_RECEIPT_V1_SCHEMA
478        );
479        assert_eq!(receipt.case_id, Some(case.case_id));
480        assert_eq!(receipt.plan_id, Some(plan.plan_id));
481    }
482
483    #[test]
484    fn governance_context_open_case_and_issue_permit() {
485        let policy = canonical_stack::PolicySnapshot::permissive(
486            "policy:p30-permissive",
487            "2026-01-01T00:00:00Z",
488        );
489        let context = GovernanceContext::new(policy);
490
491        let case = context.open_case(
492            canonical_stack::VerificationCaseClass::UnverifiedClaimVersion,
493            "aidens",
494            "claim-version:governance",
495            canonical_stack::TraceCtx::from_trace_id("trace:governance"),
496            canonical_stack::AttemptId::new("attempt:governance"),
497        );
498        let plan = CanonicalGovernanceAdapter.check_plan(
499            &case,
500            canonical_stack::CheckMethod::CausalRefuter,
501            vec!["causal-refuter".into()],
502            canonical_stack::PromotionClass::P1,
503            canonical_stack::ReversibilityClass::RequiresSupersession,
504            true,
505            false,
506            false,
507            "permissive governance check plan",
508            serde_json::json!({"source": "aidens-governance-kit"}),
509        );
510
511        let permit = context
512            .check_permit(&case, &plan)
513            .expect("permissive policy should issue permit");
514
515        assert_eq!(permit.case_id(), &case.case_id);
516        assert_eq!(permit.plan_id(), &plan.plan_id);
517    }
518
519    #[test]
520    fn risk_bearing_claim_cannot_promote_without_verification_plan() {
521        let adapter = CanonicalGovernanceAdapter;
522        let plan = adapter.create_verification_plan("claim-01", RiskClass::High);
523        // A freshly-created plan is Pending — it cannot be promoted yet
524        assert!(
525            !plan.can_promote(),
526            "high-risk claim must not promote with Pending plan"
527        );
528        assert!(
529            !plan.is_blocked(),
530            "plan should not be blocked, just pending"
531        );
532    }
533
534    #[test]
535    fn create_verification_plan_high_risk_requires_falsification() {
536        let adapter = CanonicalGovernanceAdapter;
537        let plan = adapter.create_verification_plan("claim-high", RiskClass::High);
538        assert!(!plan.required_falsification_checks.is_empty());
539        assert!(!plan.required_replay_checks.is_empty());
540    }
541
542    #[test]
543    fn create_claim_evidence_bundle_marks_support() {
544        let adapter = CanonicalGovernanceAdapter;
545        let bundle = adapter.create_claim_evidence_bundle("claim-01", "experiment", "pass");
546        assert!(bundle.supports);
547        assert_eq!(bundle.claim_id, "claim-01");
548    }
549
550    #[test]
551    fn create_claim_evidence_bundle_marks_refutation() {
552        let adapter = CanonicalGovernanceAdapter;
553        let bundle = adapter.create_claim_evidence_bundle("claim-01", "experiment", "fail");
554        assert!(!bundle.supports);
555    }
556}