1use crate::gherkin::{ValidationError, extract_all_metas, preprocess_truths};
26use crate::truths::{TruthDocument, TruthGovernance};
27
28#[derive(Debug, Clone)]
30pub struct SimulationConfig {
31 pub require_intent: bool,
33 pub require_authority: bool,
35 pub require_evidence: bool,
37 pub require_assertions: bool,
39 pub check_resource_availability: bool,
41 pub check_vendor_selection: bool,
44}
45
46impl Default for SimulationConfig {
47 fn default() -> Self {
48 Self {
49 require_intent: true,
50 require_authority: true,
51 require_evidence: true,
52 require_assertions: true,
53 check_resource_availability: true,
54 check_vendor_selection: true,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum Verdict {
62 Ready,
64 Risky,
66 WillNotConverge,
68}
69
70impl std::fmt::Display for Verdict {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 Self::Ready => write!(f, "ready"),
74 Self::Risky => write!(f, "risky"),
75 Self::WillNotConverge => write!(f, "will-not-converge"),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum FindingSeverity {
83 Info,
84 Warning,
85 Error,
86}
87
88impl std::fmt::Display for FindingSeverity {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 Self::Info => write!(f, "info"),
92 Self::Warning => write!(f, "warning"),
93 Self::Error => write!(f, "error"),
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct SimulationFinding {
101 pub severity: FindingSeverity,
102 pub category: &'static str,
103 pub message: String,
104 pub suggestion: Option<String>,
105}
106
107#[derive(Debug, Clone, Default)]
109pub struct VendorSelectionCoverage {
110 pub detected: bool,
112 pub evaluation_dimensions: usize,
114 pub vendor_references: Vec<String>,
116 pub has_ranking_criterion: bool,
118 pub has_commitment_gate: bool,
120}
121
122#[derive(Debug, Clone)]
124pub struct SimulationReport {
125 pub verdict: Verdict,
126 pub findings: Vec<SimulationFinding>,
127 pub governance_coverage: GovernanceCoverage,
128 pub scenario_count: usize,
129 pub resource_summary: ResourceSummary,
130 pub vendor_selection: VendorSelectionCoverage,
131}
132
133impl SimulationReport {
134 pub fn can_converge(&self) -> bool {
136 self.verdict != Verdict::WillNotConverge
137 }
138}
139
140#[derive(Debug, Clone, Default)]
142pub struct GovernanceCoverage {
143 pub has_intent: bool,
144 pub has_outcome: bool,
145 pub has_authority: bool,
146 pub has_actor: bool,
147 pub has_approval_gate: bool,
148 pub has_constraint: bool,
149 pub has_evidence: bool,
150 pub evidence_count: usize,
151 pub has_exception: bool,
152 pub has_escalation_path: bool,
153}
154
155#[derive(Debug, Clone, Default)]
157pub struct ResourceSummary {
158 pub declared_evidence: Vec<String>,
160 pub referenced_in_scenarios: Vec<String>,
162 pub missing: Vec<String>,
164}
165
166pub fn simulate(doc: &TruthDocument, config: &SimulationConfig) -> SimulationReport {
168 let mut findings = Vec::new();
169
170 let governance_coverage = check_governance(&doc.governance, config, &mut findings);
171 let scenario_count = check_scenarios(&doc.gherkin, config, &mut findings);
172 let resource_summary = check_resources(&doc.governance, &doc.gherkin, config, &mut findings);
173 let vendor_selection = if config.check_vendor_selection {
174 check_vendor_selection(&doc.governance, &doc.gherkin, &mut findings)
175 } else {
176 VendorSelectionCoverage::default()
177 };
178
179 let has_errors = findings
180 .iter()
181 .any(|f| matches!(f.severity, FindingSeverity::Error));
182 let has_warnings = findings
183 .iter()
184 .any(|f| matches!(f.severity, FindingSeverity::Warning));
185
186 let verdict = if has_errors {
187 Verdict::WillNotConverge
188 } else if has_warnings {
189 Verdict::Risky
190 } else {
191 Verdict::Ready
192 };
193
194 SimulationReport {
195 verdict,
196 findings,
197 governance_coverage,
198 scenario_count,
199 resource_summary,
200 vendor_selection,
201 }
202}
203
204pub fn simulate_spec(
206 content: &str,
207 config: &SimulationConfig,
208) -> Result<SimulationReport, ValidationError> {
209 let doc = crate::truths::parse_truth_document(content)?;
210 Ok(simulate(&doc, config))
211}
212
213fn check_governance(
214 gov: &TruthGovernance,
215 config: &SimulationConfig,
216 findings: &mut Vec<SimulationFinding>,
217) -> GovernanceCoverage {
218 let mut coverage = GovernanceCoverage::default();
219
220 if let Some(intent) = &gov.intent {
222 coverage.has_intent = true;
223 coverage.has_outcome = intent.outcome.is_some();
224 if intent.outcome.is_none() {
225 findings.push(SimulationFinding {
226 severity: FindingSeverity::Warning,
227 category: "governance",
228 message: "Intent block present but missing Outcome field.".into(),
229 suggestion: Some("Add `Outcome: <what should happen>` to the Intent block.".into()),
230 });
231 }
232 } else if config.require_intent {
233 findings.push(SimulationFinding {
234 severity: FindingSeverity::Error,
235 category: "governance",
236 message: "Missing Intent block — agents have no goal to converge toward.".into(),
237 suggestion: Some("Add an Intent block with Outcome and optionally Goal.".into()),
238 });
239 }
240
241 if let Some(authority) = &gov.authority {
243 coverage.has_authority = true;
244 coverage.has_actor = authority.actor.is_some();
245 coverage.has_approval_gate = !authority.requires_approval.is_empty();
246 if authority.actor.is_none() {
247 findings.push(SimulationFinding {
248 severity: FindingSeverity::Warning,
249 category: "governance",
250 message: "Authority block present but missing Actor field.".into(),
251 suggestion: Some("Add `Actor: <who can approve>` to the Authority block.".into()),
252 });
253 }
254 } else if config.require_authority {
255 findings.push(SimulationFinding {
256 severity: FindingSeverity::Error,
257 category: "governance",
258 message: "Missing Authority block — no one is authorized to promote decisions.".into(),
259 suggestion: Some(
260 "Add an Authority block with Actor and optionally Requires Approval.".into(),
261 ),
262 });
263 }
264
265 if let Some(constraint) = &gov.constraint {
267 coverage.has_constraint = true;
268 if constraint.budget.is_empty()
269 && constraint.cost_limit.is_empty()
270 && constraint.must_not.is_empty()
271 {
272 findings.push(SimulationFinding {
273 severity: FindingSeverity::Info,
274 category: "governance",
275 message: "Constraint block is empty — agents have no guardrails.".into(),
276 suggestion: None,
277 });
278 }
279 }
280
281 if let Some(evidence) = &gov.evidence {
283 coverage.has_evidence = true;
284 coverage.evidence_count = evidence.requires.len();
285 if evidence.requires.is_empty() {
286 findings.push(SimulationFinding {
287 severity: FindingSeverity::Warning,
288 category: "governance",
289 message: "Evidence block present but no Requires fields — nothing to audit.".into(),
290 suggestion: Some("Add `Requires: <evidence_name>` fields.".into()),
291 });
292 }
293 if evidence.audit.is_empty() {
294 findings.push(SimulationFinding {
295 severity: FindingSeverity::Info,
296 category: "governance",
297 message: "No Audit field in Evidence — decision trail may be incomplete.".into(),
298 suggestion: Some("Add `Audit: <log_name>` for traceability.".into()),
299 });
300 }
301 } else if config.require_evidence {
302 findings.push(SimulationFinding {
303 severity: FindingSeverity::Error,
304 category: "governance",
305 message: "Missing Evidence block — no proof requirements declared.".into(),
306 suggestion: Some("Add an Evidence block with Requires and Audit fields.".into()),
307 });
308 }
309
310 if let Some(exception) = &gov.exception {
312 coverage.has_exception = true;
313 coverage.has_escalation_path = !exception.escalates_to.is_empty();
314 }
315
316 if coverage.has_approval_gate && !coverage.has_evidence {
318 findings.push(SimulationFinding {
319 severity: FindingSeverity::Warning,
320 category: "coherence",
321 message:
322 "Authority requires approval but no Evidence block — approver has nothing to review."
323 .into(),
324 suggestion: Some(
325 "Add Evidence.Requires fields so the approver has artifacts to evaluate.".into(),
326 ),
327 });
328 }
329
330 if coverage.has_constraint && !coverage.has_authority {
331 findings.push(SimulationFinding {
332 severity: FindingSeverity::Warning,
333 category: "coherence",
334 message: "Constraints declared but no Authority — who enforces the limits?".into(),
335 suggestion: Some("Add an Authority block with an Actor.".into()),
336 });
337 }
338
339 coverage
340}
341
342fn check_scenarios(
343 gherkin: &str,
344 config: &SimulationConfig,
345 findings: &mut Vec<SimulationFinding>,
346) -> usize {
347 let preprocessed = preprocess_truths(gherkin);
348 let metas = extract_all_metas(&preprocessed).unwrap_or_default();
349
350 if metas.is_empty() {
351 findings.push(SimulationFinding {
352 severity: FindingSeverity::Error,
353 category: "scenario",
354 message: "No scenarios found — nothing to execute.".into(),
355 suggestion: Some("Add at least one Scenario with Given/When/Then steps.".into()),
356 });
357 return 0;
358 }
359
360 if config.require_assertions {
362 let has_then = gherkin.lines().any(|line| line.trim().starts_with("Then "));
363 if !has_then {
364 findings.push(SimulationFinding {
365 severity: FindingSeverity::Error,
366 category: "scenario",
367 message: "No Then steps found — scenarios have no success criteria.".into(),
368 suggestion: Some("Add Then steps that assert expected outcomes.".into()),
369 });
370 }
371 }
372
373 let has_given = gherkin
375 .lines()
376 .any(|line| line.trim().starts_with("Given "));
377 if !has_given {
378 findings.push(SimulationFinding {
379 severity: FindingSeverity::Warning,
380 category: "scenario",
381 message: "No Given steps — scenarios have no declared preconditions.".into(),
382 suggestion: Some("Add Given steps that establish the initial state.".into()),
383 });
384 }
385
386 let has_when = gherkin.lines().any(|line| line.trim().starts_with("When "));
388 if !has_when {
389 findings.push(SimulationFinding {
390 severity: FindingSeverity::Warning,
391 category: "scenario",
392 message: "No When steps — scenarios have no triggering action.".into(),
393 suggestion: Some("Add When steps that describe the action being governed.".into()),
394 });
395 }
396
397 metas.len()
398}
399
400fn check_resources(
401 gov: &TruthGovernance,
402 gherkin: &str,
403 config: &SimulationConfig,
404 findings: &mut Vec<SimulationFinding>,
405) -> ResourceSummary {
406 let mut summary = ResourceSummary::default();
407
408 if let Some(evidence) = &gov.evidence {
410 summary.declared_evidence.clone_from(&evidence.requires);
411 }
412
413 let resource_pattern = regex::Regex::new(r"[a-z][a-z0-9_]*(?:_[a-z0-9]+)+").ok();
415
416 if let Some(pattern) = &resource_pattern {
417 for line in gherkin.lines() {
418 let trimmed = line.trim();
419 if trimmed.starts_with("Given ")
420 || trimmed.starts_with("When ")
421 || trimmed.starts_with("Then ")
422 || trimmed.starts_with("And ")
423 {
424 for m in pattern.find_iter(trimmed) {
425 let resource = m.as_str().to_string();
426 if !summary.referenced_in_scenarios.contains(&resource) {
427 summary.referenced_in_scenarios.push(resource);
428 }
429 }
430 }
431 }
432 }
433
434 if config.check_resource_availability && !summary.declared_evidence.is_empty() {
436 for referenced in &summary.referenced_in_scenarios {
437 let looks_like_evidence = referenced.ends_with("_assessment")
438 || referenced.ends_with("_analysis")
439 || referenced.ends_with("_report")
440 || referenced.ends_with("_review")
441 || referenced.ends_with("_log")
442 || referenced.ends_with("_record")
443 || referenced.ends_with("_bundle");
444
445 if looks_like_evidence && !summary.declared_evidence.contains(referenced) {
446 summary.missing.push(referenced.clone());
447 }
448 }
449
450 if !summary.missing.is_empty() {
451 findings.push(SimulationFinding {
452 severity: FindingSeverity::Warning,
453 category: "resources",
454 message: format!(
455 "Scenario references evidence-like resources not declared in Evidence block: {}",
456 summary.missing.join(", ")
457 ),
458 suggestion: Some(
459 "Add these as `Requires:` fields in the Evidence block, or rename to avoid evidence naming patterns.".into(),
460 ),
461 });
462 }
463 }
464
465 if let Some(authority) = &gov.authority
467 && let Some(actor) = &authority.actor
468 {
469 let actor_referenced = gherkin.contains(actor);
470 if !actor_referenced {
471 findings.push(SimulationFinding {
472 severity: FindingSeverity::Info,
473 category: "resources",
474 message: format!(
475 "Authority actor `{actor}` is declared but not referenced in any scenario."
476 ),
477 suggestion: Some(
478 "Consider adding a scenario step that involves the authorized actor.".into(),
479 ),
480 });
481 }
482 }
483
484 summary
485}
486
487const VENDOR_KEYWORDS: &[&str] = &[
488 "vendor",
489 "procurement",
490 "supplier",
491 "rfp",
492 "shortlist",
493 "sourcing",
494];
495
496const EVALUATION_DIMENSIONS: &[&str] = &[
497 "compliance",
498 "cost",
499 "risk",
500 "security",
501 "capability",
502 "stability",
503 "performance",
504 "pricing",
505 "budget",
506 "certification",
507 "regulatory",
508 "timeline",
509 "delivery",
510];
511
512fn check_vendor_selection(
513 gov: &TruthGovernance,
514 gherkin: &str,
515 findings: &mut Vec<SimulationFinding>,
516) -> VendorSelectionCoverage {
517 let mut coverage = VendorSelectionCoverage::default();
518
519 let combined = format!(
520 "{} {}",
521 gov.intent
522 .as_ref()
523 .and_then(|i| i.outcome.as_deref())
524 .unwrap_or(""),
525 gherkin
526 )
527 .to_lowercase();
528
529 coverage.detected = VENDOR_KEYWORDS.iter().any(|kw| combined.contains(kw));
530 if !coverage.detected {
531 return coverage;
532 }
533
534 let dimensions: Vec<&str> = EVALUATION_DIMENSIONS
536 .iter()
537 .copied()
538 .filter(|d| combined.contains(d))
539 .collect();
540 coverage.evaluation_dimensions = dimensions.len();
541
542 if coverage.evaluation_dimensions < 3 {
543 findings.push(SimulationFinding {
544 severity: FindingSeverity::Warning,
545 category: "vendor-selection",
546 message: format!(
547 "Vendor selection spec mentions only {} evaluation dimension(s): {}. \
548 At least 3 are recommended for meaningful differentiation.",
549 coverage.evaluation_dimensions,
550 if dimensions.is_empty() {
551 "none".to_string()
552 } else {
553 dimensions.join(", ")
554 }
555 ),
556 suggestion: Some(
557 "Add evaluation criteria such as compliance, cost, risk, security, capability."
558 .into(),
559 ),
560 });
561 }
562
563 let vendor_pattern = regex::Regex::new(r#"(?i)(?:vendors?|suppliers?)\s+"([^"]+)""#).ok();
565 if let Some(pat) = &vendor_pattern {
566 for cap in pat.captures_iter(gherkin) {
567 if let Some(names) = cap.get(1) {
568 for name in names.as_str().split(',') {
569 let trimmed = name.trim().to_string();
570 if !trimmed.is_empty() && !coverage.vendor_references.contains(&trimmed) {
571 coverage.vendor_references.push(trimmed);
572 }
573 }
574 }
575 }
576 }
577
578 if coverage.vendor_references.len() < 3 {
579 findings.push(SimulationFinding {
580 severity: FindingSeverity::Info,
581 category: "vendor-selection",
582 message: format!(
583 "Only {} vendor(s) referenced in scenarios. \
584 3+ vendors recommended for meaningful comparison.",
585 coverage.vendor_references.len()
586 ),
587 suggestion: Some(
588 "Add more vendors in Given steps: Given vendors \"Acme, Beta, Gamma\"".into(),
589 ),
590 });
591 }
592
593 let scenario_text: String = gherkin
595 .lines()
596 .filter(|l| {
597 let t = l.trim();
598 t.starts_with("Then ") || t.starts_with("And ")
599 })
600 .collect::<Vec<_>>()
601 .join(" ")
602 .to_lowercase();
603
604 coverage.has_ranking_criterion = scenario_text.contains("rank")
605 || scenario_text.contains("shortlist")
606 || scenario_text.contains("scored")
607 || scenario_text.contains("recommendation");
608
609 if !coverage.has_ranking_criterion {
610 findings.push(SimulationFinding {
611 severity: FindingSeverity::Warning,
612 category: "vendor-selection",
613 message: "No ranking or shortlisting criterion detected.".into(),
614 suggestion: Some(
615 "Add a Then step asserting a ranked shortlist or recommendation is produced."
616 .into(),
617 ),
618 });
619 }
620
621 coverage.has_commitment_gate = gov
623 .authority
624 .as_ref()
625 .is_some_and(|a| !a.requires_approval.is_empty());
626
627 if !coverage.has_commitment_gate {
628 findings.push(SimulationFinding {
629 severity: FindingSeverity::Warning,
630 category: "vendor-selection",
631 message: "No commitment approval gate found. Vendor selections with financial \
632 impact should require human approval."
633 .into(),
634 suggestion: Some(
635 "Add `Requires Approval: vendor_commitment` to the Authority block.".into(),
636 ),
637 });
638 }
639
640 coverage
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646 use crate::truths::{
647 AuthorityBlock, ConstraintBlock, EvidenceBlock, ExceptionBlock, IntentBlock,
648 parse_truth_document,
649 };
650
651 fn full_spec() -> &'static str {
652 r#"Truth: Vendor selection is governed
653
654Intent:
655 Outcome: Select a vendor with auditable rationale.
656 Goal: Evaluate candidates on cost, compliance, and risk.
657
658Authority:
659 Actor: governance_review_board
660 Requires Approval: final_vendor_selection
661
662Constraint:
663 Cost Limit: first-year spend must stay within budget.
664
665Evidence:
666 Requires: security_assessment
667 Requires: pricing_analysis
668 Audit: decision_log
669
670Scenario: Vendors produce traceable outcomes
671 Given candidate vendors "Acme AI, Beta ML, Gamma LLM"
672 And each vendor has a security_assessment and pricing_analysis
673 When the governance_review_board evaluates each vendor
674 Then each vendor should produce a compliance screening result
675 And a ranked shortlist is produced
676"#
677 }
678
679 fn minimal_valid_spec() -> &'static str {
680 r"Truth: Minimal
681
682Intent:
683 Outcome: Works.
684
685Authority:
686 Actor: admin
687
688Evidence:
689 Requires: proof
690
691Scenario: It works
692 Given something exists
693 When validated
694 Then it passes
695"
696 }
697
698 #[test]
701 fn verdict_display() {
702 assert_eq!(Verdict::Ready.to_string(), "ready");
703 assert_eq!(Verdict::Risky.to_string(), "risky");
704 assert_eq!(Verdict::WillNotConverge.to_string(), "will-not-converge");
705 }
706
707 #[test]
708 fn finding_severity_display() {
709 assert_eq!(FindingSeverity::Info.to_string(), "info");
710 assert_eq!(FindingSeverity::Warning.to_string(), "warning");
711 assert_eq!(FindingSeverity::Error.to_string(), "error");
712 }
713
714 #[test]
717 fn can_converge_ready() {
718 let report = SimulationReport {
719 verdict: Verdict::Ready,
720 findings: vec![],
721 governance_coverage: GovernanceCoverage::default(),
722 scenario_count: 1,
723 resource_summary: ResourceSummary::default(),
724 vendor_selection: VendorSelectionCoverage::default(),
725 };
726 assert!(report.can_converge());
727 }
728
729 #[test]
730 fn can_converge_risky() {
731 let report = SimulationReport {
732 verdict: Verdict::Risky,
733 findings: vec![],
734 governance_coverage: GovernanceCoverage::default(),
735 scenario_count: 1,
736 resource_summary: ResourceSummary::default(),
737 vendor_selection: VendorSelectionCoverage::default(),
738 };
739 assert!(report.can_converge());
740 }
741
742 #[test]
743 fn cannot_converge_will_not() {
744 let report = SimulationReport {
745 verdict: Verdict::WillNotConverge,
746 findings: vec![],
747 governance_coverage: GovernanceCoverage::default(),
748 scenario_count: 0,
749 resource_summary: ResourceSummary::default(),
750 vendor_selection: VendorSelectionCoverage::default(),
751 };
752 assert!(!report.can_converge());
753 }
754
755 #[test]
758 fn finding_with_suggestion() {
759 let f = SimulationFinding {
760 severity: FindingSeverity::Warning,
761 category: "test",
762 message: "something is off".into(),
763 suggestion: Some("fix it".into()),
764 };
765 assert_eq!(f.severity, FindingSeverity::Warning);
766 assert_eq!(f.category, "test");
767 assert!(f.suggestion.is_some());
768 }
769
770 #[test]
771 fn finding_without_suggestion() {
772 let f = SimulationFinding {
773 severity: FindingSeverity::Info,
774 category: "test",
775 message: "just info".into(),
776 suggestion: None,
777 };
778 assert!(f.suggestion.is_none());
779 }
780
781 #[test]
784 fn complete_spec_is_ready() {
785 let doc = parse_truth_document(full_spec()).unwrap();
786 let report = simulate(&doc, &SimulationConfig::default());
787 assert_eq!(report.verdict, Verdict::Ready);
788 assert!(report.can_converge());
789 }
790
791 #[test]
792 fn complete_spec_governance_coverage() {
793 let doc = parse_truth_document(full_spec()).unwrap();
794 let report = simulate(&doc, &SimulationConfig::default());
795 assert!(report.governance_coverage.has_intent);
796 assert!(report.governance_coverage.has_outcome);
797 assert!(report.governance_coverage.has_authority);
798 assert!(report.governance_coverage.has_actor);
799 assert!(report.governance_coverage.has_constraint);
800 assert!(report.governance_coverage.has_evidence);
801 assert_eq!(report.governance_coverage.evidence_count, 2);
802 }
803
804 #[test]
805 fn complete_spec_scenario_count() {
806 let doc = parse_truth_document(full_spec()).unwrap();
807 let report = simulate(&doc, &SimulationConfig::default());
808 assert_eq!(report.scenario_count, 1);
809 }
810
811 #[test]
812 fn complete_spec_resource_summary() {
813 let doc = parse_truth_document(full_spec()).unwrap();
814 let report = simulate(&doc, &SimulationConfig::default());
815 assert_eq!(report.resource_summary.declared_evidence.len(), 2);
816 assert!(report.resource_summary.missing.is_empty());
817 }
818
819 #[test]
822 fn missing_intent_will_not_converge() {
823 let content = r"Truth: No intent
824
825Scenario: Something happens
826 Given a precondition
827 When an action occurs
828 Then a result is observed
829";
830 let doc = parse_truth_document(content).unwrap();
831 let report = simulate(&doc, &SimulationConfig::default());
832 assert_eq!(report.verdict, Verdict::WillNotConverge);
833 assert!(!report.can_converge());
834 assert!(
835 report
836 .findings
837 .iter()
838 .any(|f| f.message.contains("Missing Intent"))
839 );
840 }
841
842 #[test]
843 fn missing_authority_will_not_converge() {
844 let content = r"Truth: No authority
845
846Intent:
847 Outcome: Do a thing.
848
849Evidence:
850 Requires: proof
851
852Scenario: Action
853 Given precondition
854 When something happens
855 Then outcome
856";
857 let doc = parse_truth_document(content).unwrap();
858 let report = simulate(&doc, &SimulationConfig::default());
859 assert_eq!(report.verdict, Verdict::WillNotConverge);
860 assert!(
861 report
862 .findings
863 .iter()
864 .any(|f| f.message.contains("Missing Authority"))
865 );
866 }
867
868 #[test]
869 fn missing_evidence_will_not_converge() {
870 let content = r"Truth: No evidence
871
872Intent:
873 Outcome: Do a thing.
874
875Authority:
876 Actor: admin
877
878Scenario: Action
879 Given precondition
880 When something happens
881 Then outcome
882";
883 let doc = parse_truth_document(content).unwrap();
884 let report = simulate(&doc, &SimulationConfig::default());
885 assert_eq!(report.verdict, Verdict::WillNotConverge);
886 assert!(
887 report
888 .findings
889 .iter()
890 .any(|f| f.message.contains("Missing Evidence"))
891 );
892 }
893
894 #[test]
897 fn missing_then_steps_will_not_converge() {
898 let content = r"Truth: No assertions
899
900Intent:
901 Outcome: Do something.
902
903Authority:
904 Actor: admin
905
906Evidence:
907 Requires: report
908
909Scenario: Missing outcome
910 Given a shortlist of vendors
911 When the workflow ranks them
912";
913 let doc = parse_truth_document(content).unwrap();
914 let report = simulate(&doc, &SimulationConfig::default());
915 assert_eq!(report.verdict, Verdict::WillNotConverge);
916 assert!(
917 report
918 .findings
919 .iter()
920 .any(|f| f.message.contains("No Then steps"))
921 );
922 }
923
924 #[test]
925 fn missing_given_steps_produces_warning() {
926 let content = r"Truth: No given
927
928Intent:
929 Outcome: Do something.
930
931Authority:
932 Actor: admin
933
934Evidence:
935 Requires: report
936
937Scenario: No preconditions
938 When something happens
939 Then it works
940";
941 let doc = parse_truth_document(content).unwrap();
942 let report = simulate(&doc, &SimulationConfig::default());
943 assert!(
944 report
945 .findings
946 .iter()
947 .any(|f| f.message.contains("No Given steps"))
948 );
949 }
950
951 #[test]
952 fn missing_when_steps_produces_warning() {
953 let content = r"Truth: No when
954
955Intent:
956 Outcome: Do something.
957
958Authority:
959 Actor: admin
960
961Evidence:
962 Requires: report
963
964Scenario: No action
965 Given a state
966 Then it is fine
967";
968 let doc = parse_truth_document(content).unwrap();
969 let report = simulate(&doc, &SimulationConfig::default());
970 assert!(
971 report
972 .findings
973 .iter()
974 .any(|f| f.message.contains("No When steps"))
975 );
976 }
977
978 #[test]
979 fn no_scenarios_will_not_converge() {
980 let content = r"Truth: Empty
981
982Intent:
983 Outcome: Nothing to do.
984
985Authority:
986 Actor: admin
987
988Evidence:
989 Requires: proof
990";
991 let doc = parse_truth_document(content).unwrap();
992 let report = simulate(&doc, &SimulationConfig::default());
993 assert_eq!(report.verdict, Verdict::WillNotConverge);
994 assert!(
995 report
996 .findings
997 .iter()
998 .any(|f| f.message.contains("No scenarios found"))
999 );
1000 assert_eq!(report.scenario_count, 0);
1001 }
1002
1003 #[test]
1006 fn approval_without_evidence_is_risky() {
1007 let content = r"Truth: Approval gate without evidence
1008
1009Intent:
1010 Outcome: Approve a vendor.
1011
1012Authority:
1013 Actor: board
1014 Requires Approval: cfo_sign_off
1015
1016Scenario: Approval happens
1017 Given a vendor is shortlisted
1018 When the board reviews
1019 Then the vendor is approved
1020";
1021 let doc = parse_truth_document(content).unwrap();
1022 let report = simulate(&doc, &SimulationConfig::default());
1023 assert_eq!(report.verdict, Verdict::WillNotConverge);
1024 assert!(
1025 report
1026 .findings
1027 .iter()
1028 .any(|f| f.message.contains("approver has nothing to review"))
1029 );
1030 }
1031
1032 #[test]
1033 fn constraint_without_authority_warns() {
1034 let doc = TruthDocument {
1035 governance: TruthGovernance {
1036 intent: Some(IntentBlock {
1037 outcome: Some("Do it".into()),
1038 goal: None,
1039 }),
1040 authority: None,
1041 constraint: Some(ConstraintBlock {
1042 budget: vec!["100k".into()],
1043 cost_limit: vec![],
1044 must_not: vec![],
1045 }),
1046 evidence: Some(EvidenceBlock {
1047 requires: vec!["proof".into()],
1048 provenance: vec![],
1049 audit: vec!["log".into()],
1050 }),
1051 exception: None,
1052 },
1053 gherkin: "Scenario: Test\n Given a state\n When action\n Then result".into(),
1054 };
1055 let config = SimulationConfig {
1056 require_authority: false,
1057 check_vendor_selection: false,
1058 ..SimulationConfig::default()
1059 };
1060 let report = simulate(&doc, &config);
1061 assert!(
1062 report
1063 .findings
1064 .iter()
1065 .any(|f| f.message.contains("who enforces"))
1066 );
1067 }
1068
1069 #[test]
1072 fn intent_without_outcome_warns() {
1073 let doc = TruthDocument {
1074 governance: TruthGovernance {
1075 intent: Some(IntentBlock {
1076 outcome: None,
1077 goal: Some("A goal".into()),
1078 }),
1079 authority: Some(AuthorityBlock {
1080 actor: Some("admin".into()),
1081 ..AuthorityBlock::default()
1082 }),
1083 constraint: None,
1084 evidence: Some(EvidenceBlock {
1085 requires: vec!["proof".into()],
1086 provenance: vec![],
1087 audit: vec!["log".into()],
1088 }),
1089 exception: None,
1090 },
1091 gherkin: "Scenario: Test\n Given state\n When admin acts\n Then done".into(),
1092 };
1093 let report = simulate(&doc, &SimulationConfig::default());
1094 assert!(
1095 report
1096 .findings
1097 .iter()
1098 .any(|f| f.message.contains("missing Outcome"))
1099 );
1100 }
1101
1102 #[test]
1103 fn authority_without_actor_warns() {
1104 let doc = TruthDocument {
1105 governance: TruthGovernance {
1106 intent: Some(IntentBlock {
1107 outcome: Some("Do it".into()),
1108 goal: None,
1109 }),
1110 authority: Some(AuthorityBlock::default()),
1111 constraint: None,
1112 evidence: Some(EvidenceBlock {
1113 requires: vec!["proof".into()],
1114 provenance: vec![],
1115 audit: vec!["log".into()],
1116 }),
1117 exception: None,
1118 },
1119 gherkin: "Scenario: Test\n Given state\n When action\n Then done".into(),
1120 };
1121 let report = simulate(&doc, &SimulationConfig::default());
1122 assert!(
1123 report
1124 .findings
1125 .iter()
1126 .any(|f| f.message.contains("missing Actor"))
1127 );
1128 }
1129
1130 #[test]
1131 fn empty_evidence_requires_warns() {
1132 let doc = TruthDocument {
1133 governance: TruthGovernance {
1134 intent: Some(IntentBlock {
1135 outcome: Some("Do it".into()),
1136 goal: None,
1137 }),
1138 authority: Some(AuthorityBlock {
1139 actor: Some("admin".into()),
1140 ..AuthorityBlock::default()
1141 }),
1142 constraint: None,
1143 evidence: Some(EvidenceBlock {
1144 requires: vec![],
1145 provenance: vec![],
1146 audit: vec!["log".into()],
1147 }),
1148 exception: None,
1149 },
1150 gherkin: "Scenario: Test\n Given state\n When admin acts\n Then done".into(),
1151 };
1152 let report = simulate(&doc, &SimulationConfig::default());
1153 assert!(
1154 report
1155 .findings
1156 .iter()
1157 .any(|f| f.message.contains("no Requires fields"))
1158 );
1159 }
1160
1161 #[test]
1162 fn empty_constraint_block_info() {
1163 let doc = TruthDocument {
1164 governance: TruthGovernance {
1165 intent: Some(IntentBlock {
1166 outcome: Some("Do it".into()),
1167 goal: None,
1168 }),
1169 authority: Some(AuthorityBlock {
1170 actor: Some("admin".into()),
1171 ..AuthorityBlock::default()
1172 }),
1173 constraint: Some(ConstraintBlock::default()),
1174 evidence: Some(EvidenceBlock {
1175 requires: vec!["proof".into()],
1176 provenance: vec![],
1177 audit: vec!["log".into()],
1178 }),
1179 exception: None,
1180 },
1181 gherkin: "Scenario: Test\n Given state\n When admin acts\n Then done".into(),
1182 };
1183 let report = simulate(&doc, &SimulationConfig::default());
1184 assert!(report.findings.iter().any(|f| {
1185 f.severity == FindingSeverity::Info && f.message.contains("no guardrails")
1186 }));
1187 }
1188
1189 #[test]
1190 fn exception_with_escalation_path() {
1191 let doc = TruthDocument {
1192 governance: TruthGovernance {
1193 intent: Some(IntentBlock {
1194 outcome: Some("Do it".into()),
1195 goal: None,
1196 }),
1197 authority: Some(AuthorityBlock {
1198 actor: Some("admin".into()),
1199 ..AuthorityBlock::default()
1200 }),
1201 constraint: None,
1202 evidence: Some(EvidenceBlock {
1203 requires: vec!["proof".into()],
1204 provenance: vec![],
1205 audit: vec!["log".into()],
1206 }),
1207 exception: Some(ExceptionBlock {
1208 escalates_to: vec!["ceo".into()],
1209 requires: vec![],
1210 }),
1211 },
1212 gherkin: "Scenario: Test\n Given state\n When admin acts\n Then done".into(),
1213 };
1214 let report = simulate(&doc, &SimulationConfig::default());
1215 assert!(report.governance_coverage.has_exception);
1216 assert!(report.governance_coverage.has_escalation_path);
1217 }
1218
1219 #[test]
1222 fn relaxed_config_allows_missing_governance() {
1223 let content = r"Truth: Bare minimum
1224
1225Scenario: Just do it
1226 Given a state
1227 When an action
1228 Then a result
1229";
1230 let doc = parse_truth_document(content).unwrap();
1231 let config = SimulationConfig {
1232 require_intent: false,
1233 require_authority: false,
1234 require_evidence: false,
1235 require_assertions: false,
1236 check_resource_availability: false,
1237 check_vendor_selection: false,
1238 };
1239 let report = simulate(&doc, &config);
1240 let has_errors = report
1241 .findings
1242 .iter()
1243 .any(|f| f.severity == FindingSeverity::Error);
1244 assert!(!has_errors);
1245 assert_ne!(report.verdict, Verdict::WillNotConverge);
1246 }
1247
1248 #[test]
1249 fn default_config_is_strict() {
1250 let config = SimulationConfig::default();
1251 assert!(config.require_intent);
1252 assert!(config.require_authority);
1253 assert!(config.require_evidence);
1254 assert!(config.require_assertions);
1255 assert!(config.check_resource_availability);
1256 }
1257
1258 #[test]
1261 fn simulate_spec_convenience() {
1262 let report = simulate_spec(minimal_valid_spec(), &SimulationConfig::default()).unwrap();
1263 assert!(report.can_converge());
1264 }
1265
1266 #[test]
1267 fn simulate_spec_garbage_input() {
1268 let result = simulate_spec(
1269 "this is not a truth spec at all",
1270 &SimulationConfig::default(),
1271 );
1272 match result {
1274 Err(_) => {}
1275 Ok(report) => assert_eq!(report.verdict, Verdict::WillNotConverge),
1276 }
1277 }
1278
1279 #[test]
1280 fn simulate_spec_empty_string() {
1281 let result = simulate_spec("", &SimulationConfig::default());
1282 match result {
1283 Err(_) => {}
1284 Ok(report) => assert_eq!(report.verdict, Verdict::WillNotConverge),
1285 }
1286 }
1287
1288 #[test]
1291 fn undeclared_evidence_like_resource_warns() {
1292 let content = r"Truth: Resource mismatch
1293
1294Intent:
1295 Outcome: Check resources.
1296
1297Authority:
1298 Actor: admin
1299
1300Evidence:
1301 Requires: security_assessment
1302 Audit: decision_log
1303
1304Scenario: Uses undeclared evidence
1305 Given a vendor
1306 When admin reviews the compliance_report
1307 Then the security_assessment is valid
1308";
1309 let doc = parse_truth_document(content).unwrap();
1310 let report = simulate(&doc, &SimulationConfig::default());
1311 assert!(
1312 report
1313 .resource_summary
1314 .missing
1315 .contains(&"compliance_report".to_string())
1316 );
1317 }
1318
1319 #[test]
1320 fn actor_not_referenced_in_scenarios_info() {
1321 let content = r"Truth: Unused actor
1322
1323Intent:
1324 Outcome: Something.
1325
1326Authority:
1327 Actor: mysterious_committee
1328
1329Evidence:
1330 Requires: proof
1331
1332Scenario: Nobody calls the actor
1333 Given a state
1334 When something happens
1335 Then it works
1336";
1337 let doc = parse_truth_document(content).unwrap();
1338 let report = simulate(&doc, &SimulationConfig::default());
1339 assert!(report.findings.iter().any(|f| {
1340 f.severity == FindingSeverity::Info
1341 && f.message.contains("mysterious_committee")
1342 && f.message.contains("not referenced")
1343 }));
1344 }
1345
1346 #[test]
1349 fn multiple_scenarios_counted() {
1350 let content = r"Truth: Multi-scenario
1351
1352Intent:
1353 Outcome: Test multiple.
1354
1355Authority:
1356 Actor: admin
1357
1358Evidence:
1359 Requires: proof
1360
1361Scenario: First
1362 Given state
1363 When admin acts
1364 Then result
1365
1366Scenario: Second
1367 Given another state
1368 When admin acts again
1369 Then another result
1370";
1371 let doc = parse_truth_document(content).unwrap();
1372 let report = simulate(&doc, &SimulationConfig::default());
1373 assert_eq!(report.scenario_count, 2);
1374 }
1375
1376 #[test]
1379 fn spec_with_only_governance_no_scenarios() {
1380 let doc = TruthDocument {
1381 governance: TruthGovernance {
1382 intent: Some(IntentBlock {
1383 outcome: Some("Goal".into()),
1384 goal: None,
1385 }),
1386 authority: Some(AuthorityBlock {
1387 actor: Some("admin".into()),
1388 ..AuthorityBlock::default()
1389 }),
1390 constraint: None,
1391 evidence: Some(EvidenceBlock {
1392 requires: vec!["proof".into()],
1393 provenance: vec![],
1394 audit: vec![],
1395 }),
1396 exception: None,
1397 },
1398 gherkin: String::new(),
1399 };
1400 let report = simulate(&doc, &SimulationConfig::default());
1401 assert_eq!(report.verdict, Verdict::WillNotConverge);
1402 assert_eq!(report.scenario_count, 0);
1403 }
1404
1405 #[test]
1408 fn nil_governance_with_relaxed_config() {
1409 let content = r"Truth: Nil governance
1410
1411Scenario: Solo
1412 Given x
1413 When y
1414 Then z
1415";
1416 let doc = parse_truth_document(content).unwrap();
1417 let config = SimulationConfig {
1418 require_intent: false,
1419 require_authority: false,
1420 require_evidence: false,
1421 require_assertions: true,
1422 check_resource_availability: false,
1423 check_vendor_selection: false,
1424 };
1425 let report = simulate(&doc, &config);
1426 assert!(!report.governance_coverage.has_intent);
1427 assert!(!report.governance_coverage.has_authority);
1428 assert!(!report.governance_coverage.has_evidence);
1429 assert_eq!(report.verdict, Verdict::Ready);
1430 }
1431
1432 #[test]
1433 fn nil_governance_with_strict_config() {
1434 let content = r"Truth: Nil governance strict
1435
1436Scenario: Solo
1437 Given x
1438 When y
1439 Then z
1440";
1441 let doc = parse_truth_document(content).unwrap();
1442 let report = simulate(&doc, &SimulationConfig::default());
1443 assert_eq!(report.verdict, Verdict::WillNotConverge);
1444 let error_count = report
1445 .findings
1446 .iter()
1447 .filter(|f| f.severity == FindingSeverity::Error)
1448 .count();
1449 assert!(error_count >= 3); }
1451
1452 #[test]
1455 fn vendor_spec_detected() {
1456 let doc = parse_truth_document(full_spec()).unwrap();
1457 let report = simulate(&doc, &SimulationConfig::default());
1458 assert!(report.vendor_selection.detected);
1459 }
1460
1461 #[test]
1462 fn vendor_spec_extracts_vendor_names() {
1463 let doc = parse_truth_document(full_spec()).unwrap();
1464 let report = simulate(&doc, &SimulationConfig::default());
1465 assert!(
1466 report
1467 .vendor_selection
1468 .vendor_references
1469 .contains(&"Acme AI".to_string())
1470 );
1471 assert!(
1472 report
1473 .vendor_selection
1474 .vendor_references
1475 .contains(&"Beta ML".to_string())
1476 );
1477 }
1478
1479 #[test]
1480 fn vendor_spec_counts_evaluation_dimensions() {
1481 let doc = parse_truth_document(full_spec()).unwrap();
1482 let report = simulate(&doc, &SimulationConfig::default());
1483 assert!(report.vendor_selection.evaluation_dimensions >= 2);
1484 }
1485
1486 #[test]
1487 fn vendor_spec_detects_approval_gate() {
1488 let doc = parse_truth_document(full_spec()).unwrap();
1489 let report = simulate(&doc, &SimulationConfig::default());
1490 assert!(report.vendor_selection.has_commitment_gate);
1491 }
1492
1493 #[test]
1494 fn non_vendor_spec_not_detected() {
1495 let doc = parse_truth_document(minimal_valid_spec()).unwrap();
1496 let report = simulate(&doc, &SimulationConfig::default());
1497 assert!(!report.vendor_selection.detected);
1498 }
1499
1500 #[test]
1501 fn vendor_spec_few_dimensions_warns() {
1502 let content = r#"Truth: Thin vendor eval
1503
1504Intent:
1505 Outcome: Select a vendor.
1506
1507Authority:
1508 Actor: admin
1509 Requires Approval: commitment
1510
1511Evidence:
1512 Requires: proof
1513
1514Scenario: Pick a vendor
1515 Given vendors "Acme"
1516 When evaluated
1517 Then a recommendation is produced
1518"#;
1519 let doc = parse_truth_document(content).unwrap();
1520 let report = simulate(&doc, &SimulationConfig::default());
1521 assert!(report.vendor_selection.detected);
1522 assert!(report.findings.iter().any(|f| {
1523 f.category == "vendor-selection" && f.message.contains("evaluation dimension")
1524 }));
1525 }
1526
1527 #[test]
1528 fn vendor_spec_no_ranking_warns() {
1529 let content = r#"Truth: No ranking vendor eval
1530
1531Intent:
1532 Outcome: Select a vendor with compliance and cost and risk analysis.
1533
1534Authority:
1535 Actor: board
1536 Requires Approval: commitment
1537
1538Evidence:
1539 Requires: compliance_report
1540
1541Scenario: Evaluate vendors
1542 Given vendors "Acme, Beta, Gamma"
1543 When the board evaluates
1544 Then all vendors are screened
1545"#;
1546 let doc = parse_truth_document(content).unwrap();
1547 let report = simulate(&doc, &SimulationConfig::default());
1548 assert!(report.vendor_selection.detected);
1549 assert!(
1550 report
1551 .findings
1552 .iter()
1553 .any(|f| { f.category == "vendor-selection" && f.message.contains("ranking") })
1554 );
1555 }
1556
1557 #[test]
1558 fn vendor_spec_no_approval_gate_warns() {
1559 let content = r#"Truth: No approval vendor eval
1560
1561Intent:
1562 Outcome: Select a vendor with compliance and cost and risk.
1563
1564Authority:
1565 Actor: admin
1566
1567Evidence:
1568 Requires: report
1569
1570Scenario: Pick vendor
1571 Given vendors "Acme, Beta, Gamma"
1572 When evaluated
1573 Then a ranked shortlist is produced
1574"#;
1575 let doc = parse_truth_document(content).unwrap();
1576 let report = simulate(&doc, &SimulationConfig::default());
1577 assert!(report.vendor_selection.detected);
1578 assert!(report.findings.iter().any(|f| {
1579 f.category == "vendor-selection" && f.message.contains("commitment approval gate")
1580 }));
1581 }
1582
1583 #[test]
1584 fn vendor_check_disabled() {
1585 let content = r#"Truth: Vendor eval
1586
1587Intent:
1588 Outcome: Select a vendor.
1589
1590Authority:
1591 Actor: admin
1592
1593Evidence:
1594 Requires: proof
1595
1596Scenario: Quick
1597 Given vendors "A"
1598 When checked
1599 Then done
1600"#;
1601 let doc = parse_truth_document(content).unwrap();
1602 let config = SimulationConfig {
1603 check_vendor_selection: false,
1604 ..SimulationConfig::default()
1605 };
1606 let report = simulate(&doc, &config);
1607 assert!(!report.vendor_selection.detected);
1608 assert!(
1609 !report
1610 .findings
1611 .iter()
1612 .any(|f| f.category == "vendor-selection")
1613 );
1614 }
1615
1616 #[test]
1617 fn vendor_spec_complete_no_vendor_warnings() {
1618 let content = r#"Truth: Complete vendor selection
1619
1620Intent:
1621 Outcome: Select a vendor with compliance, cost, risk, security, and capability analysis.
1622
1623Authority:
1624 Actor: governance_review_board
1625 Requires Approval: vendor_commitment
1626
1627Constraint:
1628 Cost Limit: annual spend within budget.
1629
1630Evidence:
1631 Requires: compliance_assessment
1632 Requires: risk_assessment
1633 Requires: cost_analysis
1634 Audit: decision_log
1635
1636Scenario: Full evaluation
1637 Given vendors "Acme AI, Beta ML, Gamma LLM"
1638 And each vendor has compliance and risk data
1639 When the governance_review_board evaluates
1640 Then a ranked shortlist is produced
1641 And the recommendation has evidence from all criteria
1642"#;
1643 let doc = parse_truth_document(content).unwrap();
1644 let report = simulate(&doc, &SimulationConfig::default());
1645 assert!(report.vendor_selection.detected);
1646 assert!(report.vendor_selection.evaluation_dimensions >= 3);
1647 assert!(report.vendor_selection.has_ranking_criterion);
1648 assert!(report.vendor_selection.has_commitment_gate);
1649 assert_eq!(report.vendor_selection.vendor_references.len(), 3);
1650 assert!(!report.findings.iter().any(|f| {
1651 f.category == "vendor-selection" && f.severity == FindingSeverity::Warning
1652 }));
1653 }
1654}