1use schemars::JsonSchema;
43use semantic_memory::{MemoryStore, ProjectionClaimVersion, ProjectionQuery};
44use serde::{Deserialize, Serialize};
45use stack_ids::ScopeKey;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
54#[serde(rename_all = "snake_case")]
55pub enum GovernanceMode {
56 #[default]
58 FailOpen,
59 Strict,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
65pub enum GovernanceGateError {
66 #[error("governance observation failed in strict mode: {reason}")]
68 ObservationFailed { reason: String },
69 #[error("no governance claims found in strict mode")]
71 NoGovernanceClaims,
72}
73
74#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
76pub struct GovernanceObservation {
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub effect_preflight_status: Option<String>,
80 #[serde(default)]
82 pub assurance_ready: bool,
83 #[serde(default)]
85 pub authority_delegation_valid: bool,
86 #[serde(default)]
88 pub continuity_incident_active: bool,
89 #[serde(default)]
91 pub constitutional_amendment_pending: bool,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub mechanism_fit_disposition: Option<String>,
95 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub governance_degradations: Vec<GovernanceDegradation>,
98}
99
100#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
110#[serde(rename_all = "snake_case")]
111pub enum GovernanceGateResult {
112 Allow,
114 AdvisoryOnly { reason: String },
116 Blocked { reason: String },
118}
119
120#[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#[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
142pub const GOVERNANCE_PROJECTION_FAMILY: &str = "governance";
145
146pub const GOVERNANCE_SCOPE_NAMESPACE: &str = "governance";
148
149pub mod predicates {
151 pub const EFFECT_PREFLIGHT: &str = "effect_preflight_status";
153 pub const ASSURANCE_READY: &str = "assurance_ready";
155 pub const AUTHORITY_DELEGATION_VALID: &str = "authority_chain_validity";
157 pub const CONTINUITY_INCIDENT_ACTIVE: &str = "continuity_incident_active";
159 pub const CONSTITUTIONAL_AMENDMENT_PENDING: &str = "constitutional_amendment_pending";
161 pub const MECHANISM_FIT: &str = "mechanism_fit_disposition";
163}
164
165pub 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
196pub 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 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
238fn 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
249async 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 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 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
354fn parse_bool_claim(content: &str) -> bool {
357 matches!(content.trim().to_lowercase().as_str(), "true" | "1" | "yes")
358}
359
360pub fn gate_execution(observation: &GovernanceObservation) -> GovernanceGateResult {
365 if observation.continuity_incident_active {
367 return GovernanceGateResult::Blocked {
368 reason: "continuity incident is active".into(),
369 };
370 }
371 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 if observation.constitutional_amendment_pending {
379 return GovernanceGateResult::AdvisoryOnly {
380 reason: "constitutional amendment is pending".into(),
381 };
382 }
383 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
396pub 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
416pub 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 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 #[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 assert_eq!(gate_execution(&obs), GovernanceGateResult::Allow);
564 }
565
566 #[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_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 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 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 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 #[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 #[test]
651 fn strict_mode_blocks_on_blocked_gate_result() {
652 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 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 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 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 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 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 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 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}