Skip to main content

forge_pilot/
governance_gate.rs

1//! Governance surface checks for the forge-pilot OODA loop.
2//!
3//! This module evaluates governance artifact state during observation
4//! and gates execution during the act phase. It does not own authority —
5//! it reads governance artifacts and reports their status to the loop.
6//!
7//! ## Observation scope
8//!
9//! The following six predicates are checked by `observe_governance()`:
10//!
11//! 1. **Effect preflight status** — reads the latest effect-runtime preflight disposition.
12//! 2. **Assurance readiness** — checks whether an assurance-runtime case is release-ready.
13//! 3. **Authority delegation validity** — verifies the authority-delegation chain is intact.
14//! 4. **Continuity incident state** — detects active continuity-runtime incidents.
15//! 5. **Constitutional amendment state** — detects pending constitutional-memory amendments.
16//! 6. **Mechanism fit disposition** — reads the latest mechanism-runtime fit evaluation.
17//!
18//! ## Not yet observed
19//!
20//! - **Attestation exchange state** — `attestation-exchange` is wired but not yet consumed
21//!   by the observation pipeline. See GOV-002 / SCOPE_NOTES.md.
22//! - **Detailed mechanism state** — only the fit disposition is observed, not internal
23//!   mechanism-runtime evaluation details.
24//! - **Detailed assurance state** — only the ready/not-ready flag is observed, not the
25//!   full assurance case tree.
26//!
27//! ## Why the current scope is sufficient for CLARA V1
28//!
29//! The six observed predicates cover all governance surfaces that can block or degrade
30//! execution in the OODA loop. The unobserved surfaces (attestation, detailed mechanism
31//! and assurance state) are informational and do not gate any execution decision in
32//! the current pipeline. They are planned for V2.
33//!
34//! ## Design constraints
35//!
36//! - **Read-only observation.** `observe_governance()` reads governance artifact state.
37//!   It never writes, creates, or modifies governance artifacts.
38//! - **Fail-open on missing governance state.** When no governance artifacts are
39//!   present, the gate returns an empty observation and the loop proceeds normally.
40//! - **No external dependencies.** Reads only from semantic-memory's SQLite store.
41
42use schemars::JsonSchema;
43use semantic_memory::{MemoryStore, ProjectionClaimVersion, ProjectionQuery};
44use serde::{Deserialize, Serialize};
45use stack_ids::ScopeKey;
46
47/// LIB-001: Governance enforcement mode.
48///
49/// `FailOpen` preserves existing behavior — missing or broken governance
50/// artifacts produce a default observation and execution proceeds.
51/// `Strict` fails closed — observation errors and missing governance
52/// artifacts produce a `GovernanceGateError` and the caller must handle it.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
54#[serde(rename_all = "snake_case")]
55pub enum GovernanceMode {
56    /// Existing default: return empty observation on errors (fail-open).
57    #[default]
58    FailOpen,
59    /// Constitutional mode: return error on missing/broken governance (fail-closed).
60    Strict,
61}
62
63/// LIB-001: Error returned when strict governance mode detects a problem.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
65pub enum GovernanceGateError {
66    /// Governance observation failed and strict mode does not allow fallback.
67    #[error("governance observation failed in strict mode: {reason}")]
68    ObservationFailed { reason: String },
69    /// No governance claims found — strict mode requires governance artifacts.
70    #[error("no governance claims found in strict mode")]
71    NoGovernanceClaims,
72}
73
74/// Observed governance artifact state at a point in time.
75#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
76pub struct GovernanceObservation {
77    /// Effect preflight disposition, if any governance effect artifact was found.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub effect_preflight_status: Option<String>,
80    /// Whether an assurance case is ready for release.
81    #[serde(default)]
82    pub assurance_ready: bool,
83    /// Whether authority delegation chain is valid.
84    #[serde(default)]
85    pub authority_delegation_valid: bool,
86    /// Whether a continuity incident is currently active.
87    #[serde(default)]
88    pub continuity_incident_active: bool,
89    /// Whether a constitutional amendment is pending.
90    #[serde(default)]
91    pub constitutional_amendment_pending: bool,
92    /// Mechanism fit disposition, if evaluated.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub mechanism_fit_disposition: Option<String>,
95    /// Any governance degradations detected.
96    #[serde(default, skip_serializing_if = "Vec::is_empty")]
97    pub governance_degradations: Vec<GovernanceDegradation>,
98}
99
100/// A degradation in a governance surface.
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
102pub struct GovernanceDegradation {
103    pub surface: String,
104    pub reason: String,
105    pub blocks_promotion: bool,
106}
107
108/// Result of the governance gate evaluation.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
110#[serde(rename_all = "snake_case")]
111pub enum GovernanceGateResult {
112    /// Governance state allows execution to proceed.
113    Allow,
114    /// Governance state recommends advisory-only execution (no promotion).
115    AdvisoryOnly { reason: String },
116    /// Governance state blocks execution.
117    Blocked { reason: String },
118}
119
120/// Typed receipt for governance observation within a loop iteration.
121#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
122pub struct GovernanceReceiptV1 {
123    pub schema_version: String,
124    pub gate_result: GovernanceGateResult,
125    pub observation_summary: GovernanceObservationSummary,
126}
127
128/// Summary of governance observation included in the receipt.
129#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
130pub struct GovernanceObservationSummary {
131    pub effect_preflight_present: bool,
132    pub assurance_ready: bool,
133    pub authority_delegation_valid: bool,
134    pub continuity_incident_active: bool,
135    pub constitutional_amendment_pending: bool,
136    pub mechanism_fit_present: bool,
137    pub degradation_count: usize,
138}
139
140pub const GOVERNANCE_RECEIPT_V1_SCHEMA: &str = "governance_receipt_v1";
141
142/// Governance projection family constant. Claims projected with this family are
143/// governance artifacts readable by `observe_governance()`.
144pub const GOVERNANCE_PROJECTION_FAMILY: &str = "governance";
145
146/// Governance-scoped namespace in semantic-memory claim projections.
147pub const GOVERNANCE_SCOPE_NAMESPACE: &str = "governance";
148
149/// Well-known predicate constants for governance claim projections.
150pub mod predicates {
151    /// Predicate for effect preflight disposition claims.
152    pub const EFFECT_PREFLIGHT: &str = "effect_preflight_status";
153    /// Predicate for assurance case readiness claims.
154    pub const ASSURANCE_READY: &str = "assurance_ready";
155    /// Predicate for authority delegation chain validity claims.
156    pub const AUTHORITY_DELEGATION_VALID: &str = "authority_chain_validity";
157    /// Predicate for continuity incident active status claims.
158    pub const CONTINUITY_INCIDENT_ACTIVE: &str = "continuity_incident_active";
159    /// Predicate for constitutional amendment pending status claims.
160    pub const CONSTITUTIONAL_AMENDMENT_PENDING: &str = "constitutional_amendment_pending";
161    /// Predicate for mechanism fit disposition claims.
162    pub const MECHANISM_FIT: &str = "mechanism_fit_disposition";
163}
164
165/// Observes governance artifact state from semantic-memory projections.
166///
167/// Queries the `claim_versions` table in the governance scope namespace for
168/// claim projections that represent governance artifact state. Each governance
169/// surface is identified by its predicate value:
170///
171/// - `effect_preflight_status` — effect-runtime preflight disposition
172/// - `assurance_ready` — assurance-runtime case readiness
173/// - `authority_chain_validity` — authority-delegation chain state
174/// - `continuity_incident_active` — continuity-runtime incident state
175/// - `constitutional_amendment_pending` — constitutional-memory amendment state
176/// - `mechanism_fit_disposition` — mechanism-runtime fit run disposition
177///
178/// Returns a default (empty) observation on any error — fail-open by design.
179/// This function is read-only and never writes governance artifacts.
180///
181/// For strict (fail-closed) behavior, use [`observe_governance_with_mode`]
182/// with [`GovernanceMode::Strict`].
183pub async fn observe_governance(store: &MemoryStore) -> GovernanceObservation {
184    match observe_governance_inner(store).await {
185        Ok(obs) => obs,
186        Err(err) => {
187            tracing::warn!(
188                error = %err,
189                "governance observation failed, returning default (fail-open)"
190            );
191            GovernanceObservation::default()
192        }
193    }
194}
195
196/// LIB-001: Observe governance with explicit mode selection.
197///
198/// In `FailOpen` mode, behaves identically to [`observe_governance`].
199/// In `Strict` mode, returns `Err` when governance observation fails or
200/// when no governance claims are found. This forces callers to explicitly
201/// handle the absence of governance artifacts rather than silently proceeding.
202pub async fn observe_governance_with_mode(
203    store: &MemoryStore,
204    mode: GovernanceMode,
205) -> Result<GovernanceObservation, GovernanceGateError> {
206    match observe_governance_inner(store).await {
207        Ok(obs) => {
208            // LIB-001: In strict mode, require at least one governance claim.
209            if mode == GovernanceMode::Strict && is_empty_observation(&obs) {
210                tracing::warn!(
211                    "strict governance mode: no governance claims found, failing closed"
212                );
213                return Err(GovernanceGateError::NoGovernanceClaims);
214            }
215            Ok(obs)
216        }
217        Err(err) => match mode {
218            GovernanceMode::FailOpen => {
219                tracing::warn!(
220                    error = %err,
221                    "governance observation failed, returning default (fail-open)"
222                );
223                Ok(GovernanceObservation::default())
224            }
225            GovernanceMode::Strict => {
226                tracing::error!(
227                    error = %err,
228                    "governance observation failed in strict mode, failing closed"
229                );
230                Err(GovernanceGateError::ObservationFailed {
231                    reason: err.to_string(),
232                })
233            }
234        },
235    }
236}
237
238/// Returns true if the observation has no governance data populated.
239fn is_empty_observation(obs: &GovernanceObservation) -> bool {
240    obs.effect_preflight_status.is_none()
241        && !obs.assurance_ready
242        && !obs.authority_delegation_valid
243        && !obs.continuity_incident_active
244        && !obs.constitutional_amendment_pending
245        && obs.mechanism_fit_disposition.is_none()
246        && obs.governance_degradations.is_empty()
247}
248
249/// Inner implementation that can propagate errors. The outer function catches
250/// all errors and returns Default (fail-open).
251async fn observe_governance_inner(
252    store: &MemoryStore,
253) -> Result<GovernanceObservation, semantic_memory::MemoryError> {
254    let query = ProjectionQuery {
255        scope: ScopeKey {
256            namespace: GOVERNANCE_SCOPE_NAMESPACE.to_string(),
257            domain: None,
258            workspace_id: None,
259            repo_id: None,
260        },
261        text_query: None,
262        valid_at: None,
263        recorded_at_or_before: None,
264        subject_entity_id: None,
265        canonical_entity_id: None,
266        claim_state: Some("active".to_string()),
267        claim_id: None,
268        claim_version_id: None,
269        limit: 100,
270    };
271
272    let claims = store.query_claim_versions(query).await?;
273    if claims.is_empty() {
274        tracing::debug!("no governance claims found in scope, returning default observation");
275        return Ok(GovernanceObservation::default());
276    }
277
278    // Filter to governance projection family claims.
279    let gov_claims: Vec<&ProjectionClaimVersion> = claims
280        .iter()
281        .filter(|c| c.projection_family == GOVERNANCE_PROJECTION_FAMILY)
282        .collect();
283
284    if gov_claims.is_empty() {
285        tracing::debug!(
286            total_claims = claims.len(),
287            "claims found but none in governance projection family"
288        );
289        return Ok(GovernanceObservation::default());
290    }
291
292    let mut observation = GovernanceObservation::default();
293    let mut degradations = Vec::new();
294
295    for claim in &gov_claims {
296        match claim.predicate.as_str() {
297            predicates::EFFECT_PREFLIGHT => {
298                observation.effect_preflight_status = Some(claim.content.clone());
299            }
300            predicates::ASSURANCE_READY => {
301                observation.assurance_ready = parse_bool_claim(&claim.content);
302            }
303            predicates::AUTHORITY_DELEGATION_VALID => {
304                observation.authority_delegation_valid = parse_bool_claim(&claim.content);
305            }
306            predicates::CONTINUITY_INCIDENT_ACTIVE => {
307                observation.continuity_incident_active = parse_bool_claim(&claim.content);
308            }
309            predicates::CONSTITUTIONAL_AMENDMENT_PENDING => {
310                observation.constitutional_amendment_pending = parse_bool_claim(&claim.content);
311            }
312            predicates::MECHANISM_FIT => {
313                observation.mechanism_fit_disposition = Some(claim.content.clone());
314            }
315            other => {
316                tracing::trace!(
317                    predicate = other,
318                    "unrecognized governance predicate, skipping"
319                );
320            }
321        }
322
323        // If the claim is stale or contradicted, record a degradation.
324        if claim.freshness != "current" || claim.contradiction_status != "none" {
325            degradations.push(GovernanceDegradation {
326                surface: claim.predicate.clone(),
327                reason: format!(
328                    "freshness={}, contradiction={}",
329                    claim.freshness, claim.contradiction_status
330                ),
331                blocks_promotion: claim.freshness == "superseded"
332                    || claim.contradiction_status != "none",
333            });
334        }
335    }
336
337    observation.governance_degradations = degradations;
338
339    tracing::debug!(
340        claim_count = gov_claims.len(),
341        effect_preflight = observation.effect_preflight_status.is_some(),
342        assurance_ready = observation.assurance_ready,
343        authority_valid = observation.authority_delegation_valid,
344        incident_active = observation.continuity_incident_active,
345        amendment_pending = observation.constitutional_amendment_pending,
346        mechanism_fit = observation.mechanism_fit_disposition.is_some(),
347        degradation_count = observation.governance_degradations.len(),
348        "governance observation populated from semantic-memory projections"
349    );
350
351    Ok(observation)
352}
353
354/// Parse a claim content string as a boolean. Supports "true"/"false"
355/// and "1"/"0". Returns false for unrecognized values (fail-safe).
356fn parse_bool_claim(content: &str) -> bool {
357    matches!(content.trim().to_lowercase().as_str(), "true" | "1" | "yes")
358}
359
360/// Evaluates whether governance state permits execution to proceed.
361///
362/// For strict-mode gating that returns an error on governance failures,
363/// use [`gate_execution_with_mode`].
364pub fn gate_execution(observation: &GovernanceObservation) -> GovernanceGateResult {
365    // Active continuity incident blocks execution.
366    if observation.continuity_incident_active {
367        return GovernanceGateResult::Blocked {
368            reason: "continuity incident is active".into(),
369        };
370    }
371    // Invalid authority delegation blocks execution.
372    if observation.effect_preflight_status.is_some() && !observation.authority_delegation_valid {
373        return GovernanceGateResult::Blocked {
374            reason: "authority delegation chain is not valid".into(),
375        };
376    }
377    // Pending constitutional amendment downgrades to advisory-only.
378    if observation.constitutional_amendment_pending {
379        return GovernanceGateResult::AdvisoryOnly {
380            reason: "constitutional amendment is pending".into(),
381        };
382    }
383    // Promotion-blocking degradations downgrade to advisory-only.
384    if observation
385        .governance_degradations
386        .iter()
387        .any(|d| d.blocks_promotion)
388    {
389        return GovernanceGateResult::AdvisoryOnly {
390            reason: "governance degradation blocks promotion".into(),
391        };
392    }
393    GovernanceGateResult::Allow
394}
395
396/// LIB-001: Evaluates governance with explicit mode.
397///
398/// In `Strict` mode, a `Blocked` result is promoted to an error so callers
399/// cannot accidentally ignore it. `Allow` and `AdvisoryOnly` pass through.
400/// In `FailOpen` mode, behaves identically to [`gate_execution`].
401pub fn gate_execution_with_mode(
402    observation: &GovernanceObservation,
403    mode: GovernanceMode,
404) -> Result<GovernanceGateResult, GovernanceGateError> {
405    let result = gate_execution(observation);
406    match (&result, mode) {
407        (GovernanceGateResult::Blocked { reason }, GovernanceMode::Strict) => {
408            Err(GovernanceGateError::ObservationFailed {
409                reason: format!("governance blocked in strict mode: {reason}"),
410            })
411        }
412        _ => Ok(result),
413    }
414}
415
416/// Builds a typed governance receipt for a loop iteration report.
417pub fn build_governance_receipt(
418    observation: &GovernanceObservation,
419    gate_result: &GovernanceGateResult,
420) -> GovernanceReceiptV1 {
421    GovernanceReceiptV1 {
422        schema_version: GOVERNANCE_RECEIPT_V1_SCHEMA.into(),
423        gate_result: gate_result.clone(),
424        observation_summary: GovernanceObservationSummary {
425            effect_preflight_present: observation.effect_preflight_status.is_some(),
426            assurance_ready: observation.assurance_ready,
427            authority_delegation_valid: observation.authority_delegation_valid,
428            continuity_incident_active: observation.continuity_incident_active,
429            constitutional_amendment_pending: observation.constitutional_amendment_pending,
430            mechanism_fit_present: observation.mechanism_fit_disposition.is_some(),
431            degradation_count: observation.governance_degradations.len(),
432        },
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn default_observation_allows_execution() {
442        let obs = GovernanceObservation::default();
443        let result = gate_execution(&obs);
444        assert_eq!(result, GovernanceGateResult::Allow);
445    }
446
447    #[test]
448    fn active_incident_blocks_execution() {
449        let obs = GovernanceObservation {
450            continuity_incident_active: true,
451            ..Default::default()
452        };
453        let result = gate_execution(&obs);
454        assert!(matches!(result, GovernanceGateResult::Blocked { .. }));
455    }
456
457    #[test]
458    fn pending_amendment_downgrades_to_advisory() {
459        let obs = GovernanceObservation {
460            constitutional_amendment_pending: true,
461            ..Default::default()
462        };
463        let result = gate_execution(&obs);
464        assert!(matches!(result, GovernanceGateResult::AdvisoryOnly { .. }));
465    }
466
467    #[test]
468    fn governance_receipt_roundtrip() {
469        let obs = GovernanceObservation::default();
470        let gate = gate_execution(&obs);
471        let receipt = build_governance_receipt(&obs, &gate);
472        assert_eq!(receipt.schema_version, GOVERNANCE_RECEIPT_V1_SCHEMA);
473        let json = serde_json::to_string(&receipt).unwrap();
474        let roundtrip: GovernanceReceiptV1 = serde_json::from_str(&json).unwrap();
475        assert_eq!(roundtrip.schema_version, GOVERNANCE_RECEIPT_V1_SCHEMA);
476    }
477
478    #[test]
479    fn parse_bool_claim_values() {
480        assert!(parse_bool_claim("true"));
481        assert!(parse_bool_claim("True"));
482        assert!(parse_bool_claim("TRUE"));
483        assert!(parse_bool_claim("1"));
484        assert!(parse_bool_claim("yes"));
485        assert!(parse_bool_claim("  true  "));
486        assert!(!parse_bool_claim("false"));
487        assert!(!parse_bool_claim("0"));
488        assert!(!parse_bool_claim("no"));
489        assert!(!parse_bool_claim(""));
490        assert!(!parse_bool_claim("unknown"));
491    }
492
493    #[test]
494    fn degradation_blocks_promotion_downgrades_to_advisory() {
495        let obs = GovernanceObservation {
496            governance_degradations: vec![GovernanceDegradation {
497                surface: "effect_preflight_status".into(),
498                reason: "freshness=superseded, contradiction=none".into(),
499                blocks_promotion: true,
500            }],
501            ..Default::default()
502        };
503        let result = gate_execution(&obs);
504        assert!(matches!(result, GovernanceGateResult::AdvisoryOnly { .. }));
505    }
506
507    #[test]
508    fn non_blocking_degradation_allows_execution() {
509        let obs = GovernanceObservation {
510            governance_degradations: vec![GovernanceDegradation {
511                surface: "mechanism_fit_disposition".into(),
512                reason: "freshness=stale, contradiction=none".into(),
513                blocks_promotion: false,
514            }],
515            ..Default::default()
516        };
517        let result = gate_execution(&obs);
518        assert_eq!(result, GovernanceGateResult::Allow);
519    }
520
521    #[test]
522    fn invalid_authority_without_preflight_allows() {
523        // authority_delegation_valid is false but no effect_preflight_status is present,
524        // so execution is not blocked
525        let obs = GovernanceObservation {
526            authority_delegation_valid: false,
527            ..Default::default()
528        };
529        let result = gate_execution(&obs);
530        assert_eq!(result, GovernanceGateResult::Allow);
531    }
532
533    #[test]
534    fn invalid_authority_with_preflight_blocks() {
535        let obs = GovernanceObservation {
536            effect_preflight_status: Some("commit_eligible".into()),
537            authority_delegation_valid: false,
538            ..Default::default()
539        };
540        let result = gate_execution(&obs);
541        assert!(matches!(result, GovernanceGateResult::Blocked { .. }));
542    }
543
544    /// Verifies that observe_governance returns default when the store has no
545    /// governance claims (empty store, fail-open).
546    #[tokio::test]
547    async fn observe_governance_empty_store_returns_default() {
548        let dir = tempfile::tempdir().expect("create temp dir");
549        let config = semantic_memory::MemoryConfig {
550            base_dir: dir.path().to_path_buf(),
551            ..Default::default()
552        };
553        let store = MemoryStore::open(config).expect("open store");
554        let obs = observe_governance(&store).await;
555        assert_eq!(obs.effect_preflight_status, None);
556        assert!(!obs.assurance_ready);
557        assert!(!obs.authority_delegation_valid);
558        assert!(!obs.continuity_incident_active);
559        assert!(!obs.constitutional_amendment_pending);
560        assert_eq!(obs.mechanism_fit_disposition, None);
561        assert!(obs.governance_degradations.is_empty());
562        // Default observation allows execution.
563        assert_eq!(gate_execution(&obs), GovernanceGateResult::Allow);
564    }
565
566    /// Verifies that observe_governance populates observation fields from
567    /// governance claim projections stored in semantic-memory.
568    #[tokio::test]
569    async fn observe_governance_reads_real_artifacts() {
570        let dir = tempfile::tempdir().expect("create temp dir");
571        let config = semantic_memory::MemoryConfig {
572            base_dir: dir.path().to_path_buf(),
573            ..Default::default()
574        };
575        let store = MemoryStore::open(config).expect("open store");
576
577        // Insert governance claims directly into the claim_versions table.
578        insert_governance_claim(&store, predicates::EFFECT_PREFLIGHT, "commit_eligible").await;
579        insert_governance_claim(&store, predicates::ASSURANCE_READY, "true").await;
580        insert_governance_claim(&store, predicates::AUTHORITY_DELEGATION_VALID, "true").await;
581        insert_governance_claim(&store, predicates::CONTINUITY_INCIDENT_ACTIVE, "false").await;
582        insert_governance_claim(&store, predicates::CONSTITUTIONAL_AMENDMENT_PENDING, "true").await;
583        insert_governance_claim(
584            &store,
585            predicates::MECHANISM_FIT,
586            "eligible_for_local_review",
587        )
588        .await;
589
590        let obs = observe_governance(&store).await;
591
592        // Verify non-default observation was populated.
593        assert_eq!(
594            obs.effect_preflight_status.as_deref(),
595            Some("commit_eligible")
596        );
597        assert!(obs.assurance_ready);
598        assert!(obs.authority_delegation_valid);
599        assert!(!obs.continuity_incident_active);
600        assert!(obs.constitutional_amendment_pending);
601        assert_eq!(
602            obs.mechanism_fit_disposition.as_deref(),
603            Some("eligible_for_local_review")
604        );
605
606        // Constitutional amendment pending => advisory-only.
607        let gate = gate_execution(&obs);
608        assert!(
609            matches!(gate, GovernanceGateResult::AdvisoryOnly { .. }),
610            "expected AdvisoryOnly due to pending amendment, got: {:?}",
611            gate
612        );
613
614        // Receipt should capture the observation accurately.
615        let receipt = build_governance_receipt(&obs, &gate);
616        assert!(receipt.observation_summary.effect_preflight_present);
617        assert!(receipt.observation_summary.assurance_ready);
618        assert!(receipt.observation_summary.authority_delegation_valid);
619        assert!(!receipt.observation_summary.continuity_incident_active);
620        assert!(receipt.observation_summary.constitutional_amendment_pending);
621        assert!(receipt.observation_summary.mechanism_fit_present);
622    }
623
624    /// Verifies that an active incident claim causes observe_governance
625    /// to report it and gate_execution to block.
626    #[tokio::test]
627    async fn observe_governance_active_incident_blocks() {
628        let dir = tempfile::tempdir().expect("create temp dir");
629        let config = semantic_memory::MemoryConfig {
630            base_dir: dir.path().to_path_buf(),
631            ..Default::default()
632        };
633        let store = MemoryStore::open(config).expect("open store");
634
635        insert_governance_claim(&store, predicates::CONTINUITY_INCIDENT_ACTIVE, "true").await;
636
637        let obs = observe_governance(&store).await;
638        assert!(obs.continuity_incident_active);
639
640        let gate = gate_execution(&obs);
641        assert!(
642            matches!(gate, GovernanceGateResult::Blocked { .. }),
643            "expected Blocked due to active incident, got: {:?}",
644            gate
645        );
646    }
647
648    // --- LIB-001: strict mode tests ---
649
650    #[test]
651    fn strict_mode_blocks_on_blocked_gate_result() {
652        // LIB-001: In strict mode, a Blocked gate result becomes an error.
653        let obs = GovernanceObservation {
654            continuity_incident_active: true,
655            ..Default::default()
656        };
657        let result = gate_execution_with_mode(&obs, GovernanceMode::Strict);
658        assert!(result.is_err(), "strict mode should error on blocked gate");
659    }
660
661    #[test]
662    fn strict_mode_allows_on_allow_gate_result() {
663        // LIB-001: In strict mode, Allow passes through.
664        let obs = GovernanceObservation {
665            assurance_ready: true,
666            authority_delegation_valid: true,
667            ..Default::default()
668        };
669        let result = gate_execution_with_mode(&obs, GovernanceMode::Strict);
670        assert!(result.is_ok());
671        assert_eq!(result.unwrap(), GovernanceGateResult::Allow);
672    }
673
674    #[test]
675    fn strict_mode_allows_advisory_only() {
676        // LIB-001: In strict mode, AdvisoryOnly passes through (not an error).
677        let obs = GovernanceObservation {
678            constitutional_amendment_pending: true,
679            ..Default::default()
680        };
681        let result = gate_execution_with_mode(&obs, GovernanceMode::Strict);
682        assert!(result.is_ok());
683        assert!(matches!(
684            result.unwrap(),
685            GovernanceGateResult::AdvisoryOnly { .. }
686        ));
687    }
688
689    #[test]
690    fn fail_open_mode_returns_blocked_without_error() {
691        // LIB-001: In fail-open mode, Blocked is returned as-is (not an error).
692        let obs = GovernanceObservation {
693            continuity_incident_active: true,
694            ..Default::default()
695        };
696        let result = gate_execution_with_mode(&obs, GovernanceMode::FailOpen);
697        assert!(result.is_ok());
698        assert!(matches!(
699            result.unwrap(),
700            GovernanceGateResult::Blocked { .. }
701        ));
702    }
703
704    #[tokio::test]
705    async fn strict_mode_errors_on_empty_store() {
706        // LIB-001: Strict mode rejects empty governance state.
707        let dir = tempfile::tempdir().expect("create temp dir");
708        let config = semantic_memory::MemoryConfig {
709            base_dir: dir.path().to_path_buf(),
710            ..Default::default()
711        };
712        let store = MemoryStore::open(config).expect("open store");
713        let result = observe_governance_with_mode(&store, GovernanceMode::Strict).await;
714        assert!(
715            result.is_err(),
716            "strict mode should error when no governance claims exist"
717        );
718        assert!(matches!(
719            result.unwrap_err(),
720            GovernanceGateError::NoGovernanceClaims
721        ));
722    }
723
724    #[tokio::test]
725    async fn strict_mode_allows_populated_store() {
726        // LIB-001: Strict mode succeeds when governance claims exist.
727        let dir = tempfile::tempdir().expect("create temp dir");
728        let config = semantic_memory::MemoryConfig {
729            base_dir: dir.path().to_path_buf(),
730            ..Default::default()
731        };
732        let store = MemoryStore::open(config).expect("open store");
733        insert_governance_claim(&store, predicates::ASSURANCE_READY, "true").await;
734
735        let result = observe_governance_with_mode(&store, GovernanceMode::Strict).await;
736        assert!(
737            result.is_ok(),
738            "strict mode should succeed with governance claims present"
739        );
740        let obs = result.unwrap();
741        assert!(obs.assurance_ready);
742    }
743
744    #[tokio::test]
745    async fn fail_open_mode_returns_default_on_empty_store() {
746        // LIB-001: Fail-open mode returns default (not error) when store is empty.
747        let dir = tempfile::tempdir().expect("create temp dir");
748        let config = semantic_memory::MemoryConfig {
749            base_dir: dir.path().to_path_buf(),
750            ..Default::default()
751        };
752        let store = MemoryStore::open(config).expect("open store");
753        let result = observe_governance_with_mode(&store, GovernanceMode::FailOpen).await;
754        assert!(result.is_ok(), "fail-open mode should not error");
755        assert_eq!(
756            gate_execution(&result.unwrap()),
757            GovernanceGateResult::Allow
758        );
759    }
760
761    /// Helper: insert a governance claim into the store using raw SQL.
762    async fn insert_governance_claim(store: &MemoryStore, predicate: &str, content: &str) {
763        let id = uuid::Uuid::new_v4().to_string();
764        let claim_id = format!("gov-claim-{}", predicate);
765        let sql = format!(
766            "INSERT INTO claim_versions (
767                claim_version_id, claim_id, claim_state, projection_family,
768                subject_entity_id, predicate, object_anchor,
769                scope_namespace, scope_domain, scope_workspace_id, scope_repo_id,
770                recorded_at, preferred_open,
771                source_envelope_id, source_authority,
772                freshness, contradiction_status, content, confidence
773            ) VALUES (
774                '{}', '{}', 'active', '{}',
775                'governance-entity', '{}', '\"{}\"',
776                '{}', NULL, NULL, NULL,
777                datetime('now'), 0,
778                'gov-envelope-{}', 'governance',
779                'current', 'none', '{}', 1.0
780            )",
781            id,
782            claim_id,
783            GOVERNANCE_PROJECTION_FAMILY,
784            predicate,
785            content,
786            GOVERNANCE_SCOPE_NAMESPACE,
787            predicate,
788            content,
789        );
790        store
791            .raw_execute(&sql, vec![])
792            .await
793            .expect("insert governance claim");
794    }
795}