use crate::gherkin::{ValidationError, extract_all_metas, preprocess_truths};
use crate::truths::{TruthDocument, TruthGovernance};
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SimulationConfig {
pub require_intent: bool,
pub require_authority: bool,
pub require_evidence: bool,
pub require_assertions: bool,
pub check_resource_availability: bool,
pub domain_profiles: Vec<DomainProfile>,
}
impl Default for SimulationConfig {
fn default() -> Self {
Self {
require_intent: true,
require_authority: true,
require_evidence: true,
require_assertions: true,
check_resource_availability: true,
domain_profiles: Vec::new(),
}
}
}
impl SimulationConfig {
#[must_use]
pub fn with_domain_profile(mut self, profile: DomainProfile) -> Self {
if !self.domain_profiles.contains(&profile) {
self.domain_profiles.push(profile);
}
self.domain_profiles = normalized_domain_profiles(&self.domain_profiles);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DomainProfile {
VendorSelection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Ready,
Risky,
WillNotConverge,
}
impl std::fmt::Display for Verdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ready => write!(f, "ready"),
Self::Risky => write!(f, "risky"),
Self::WillNotConverge => write!(f, "will-not-converge"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FindingSeverity {
Info,
Warning,
Error,
}
impl std::fmt::Display for FindingSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Info => write!(f, "info"),
Self::Warning => write!(f, "warning"),
Self::Error => write!(f, "error"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SimulationFinding {
pub severity: FindingSeverity,
pub category: &'static str,
pub message: String,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VendorSelectionCoverage {
pub detected: bool,
pub evaluation_dimensions: usize,
pub vendor_references: Vec<String>,
pub has_ranking_criterion: bool,
pub has_commitment_gate: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DomainProfileCoverage {
VendorSelection(VendorSelectionCoverage),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DomainProfileReport {
pub profile: DomainProfile,
pub findings: Vec<SimulationFinding>,
pub coverage: DomainProfileCoverage,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SimulationReport {
pub verdict: Verdict,
pub findings: Vec<SimulationFinding>,
pub governance_coverage: GovernanceCoverage,
pub scenario_count: usize,
pub resource_summary: ResourceSummary,
pub deterministic_trace: DeterministicTrace,
pub domain_profiles: Vec<DomainProfileReport>,
}
impl SimulationReport {
pub fn can_converge(&self) -> bool {
self.verdict != Verdict::WillNotConverge
}
#[must_use]
pub fn vendor_selection(&self) -> Option<&VendorSelectionCoverage> {
self.domain_profiles
.iter()
.map(|report| match &report.coverage {
DomainProfileCoverage::VendorSelection(coverage) => coverage,
})
.next()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GovernanceCoverage {
pub has_intent: bool,
pub has_outcome: bool,
pub has_authority: bool,
pub has_actor: bool,
pub has_approval_gate: bool,
pub has_constraint: bool,
pub has_evidence: bool,
pub evidence_count: usize,
pub has_exception: bool,
pub has_escalation_path: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ResourceSummary {
pub declared_evidence: Vec<String>,
pub referenced_in_scenarios: Vec<String>,
pub missing: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DeterministicTrace {
pub trace_hash: String,
pub step_count: usize,
pub replayable: bool,
pub non_replayable_steps: Vec<TraceStep>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceStep {
pub scenario_index: usize,
pub step_index: usize,
pub keyword: String,
pub text: String,
}
pub fn simulate(doc: &TruthDocument, config: &SimulationConfig) -> SimulationReport {
let mut findings = Vec::new();
let governance_coverage = check_governance(&doc.governance, config, &mut findings);
let scenario_count = check_scenarios(&doc.gherkin, config, &mut findings);
let resource_summary = check_resources(&doc.governance, &doc.gherkin, config, &mut findings);
let deterministic_trace =
check_determinism(&doc.governance, &doc.gherkin, config, &mut findings);
let domain_profiles =
check_domain_profiles(&doc.governance, &doc.gherkin, &config.domain_profiles);
for profile_report in &domain_profiles {
findings.extend(profile_report.findings.iter().cloned());
}
let has_errors = findings
.iter()
.any(|f| matches!(f.severity, FindingSeverity::Error));
let has_warnings = findings
.iter()
.any(|f| matches!(f.severity, FindingSeverity::Warning));
let verdict = if has_errors {
Verdict::WillNotConverge
} else if has_warnings {
Verdict::Risky
} else {
Verdict::Ready
};
let mut report = SimulationReport {
verdict,
findings,
governance_coverage,
scenario_count,
resource_summary,
deterministic_trace,
domain_profiles,
};
canonicalize_report(&mut report);
report
}
pub fn simulate_spec(
content: &str,
config: &SimulationConfig,
) -> Result<SimulationReport, ValidationError> {
let doc = crate::truths::parse_truth_document(content)?;
Ok(simulate(&doc, config))
}
fn check_governance(
gov: &TruthGovernance,
config: &SimulationConfig,
findings: &mut Vec<SimulationFinding>,
) -> GovernanceCoverage {
let mut coverage = GovernanceCoverage::default();
if let Some(intent) = &gov.intent {
coverage.has_intent = true;
coverage.has_outcome = intent.outcome.is_some();
if intent.outcome.is_none() {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "governance",
message: "Intent block present but missing Outcome field.".into(),
suggestion: Some("Add `Outcome: <what should happen>` to the Intent block.".into()),
});
}
} else if config.require_intent {
findings.push(SimulationFinding {
severity: FindingSeverity::Error,
category: "governance",
message: "Missing Intent block — agents have no goal to converge toward.".into(),
suggestion: Some("Add an Intent block with Outcome and optionally Goal.".into()),
});
}
if let Some(authority) = &gov.authority {
coverage.has_authority = true;
coverage.has_actor = authority.actor.is_some();
coverage.has_approval_gate = !authority.requires_approval.is_empty();
if authority.actor.is_none() {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "governance",
message: "Authority block present but missing Actor field.".into(),
suggestion: Some("Add `Actor: <who can approve>` to the Authority block.".into()),
});
}
} else if config.require_authority {
findings.push(SimulationFinding {
severity: FindingSeverity::Error,
category: "governance",
message: "Missing Authority block — no one is authorized to promote decisions.".into(),
suggestion: Some(
"Add an Authority block with Actor and optionally Requires Approval.".into(),
),
});
}
if let Some(constraint) = &gov.constraint {
coverage.has_constraint = true;
if constraint.budget.is_empty()
&& constraint.cost_limit.is_empty()
&& constraint.must_not.is_empty()
{
findings.push(SimulationFinding {
severity: FindingSeverity::Info,
category: "governance",
message: "Constraint block is empty — agents have no guardrails.".into(),
suggestion: None,
});
}
}
if let Some(evidence) = &gov.evidence {
coverage.has_evidence = true;
coverage.evidence_count = evidence.requires.len();
if evidence.requires.is_empty() {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "governance",
message: "Evidence block present but no Requires fields — nothing to audit.".into(),
suggestion: Some("Add `Requires: <evidence_name>` fields.".into()),
});
}
if evidence.audit.is_empty() {
findings.push(SimulationFinding {
severity: FindingSeverity::Info,
category: "governance",
message: "No Audit field in Evidence — decision trail may be incomplete.".into(),
suggestion: Some("Add `Audit: <log_name>` for traceability.".into()),
});
}
} else if config.require_evidence {
findings.push(SimulationFinding {
severity: FindingSeverity::Error,
category: "governance",
message: "Missing Evidence block — no proof requirements declared.".into(),
suggestion: Some("Add an Evidence block with Requires and Audit fields.".into()),
});
}
if let Some(exception) = &gov.exception {
coverage.has_exception = true;
coverage.has_escalation_path = !exception.escalates_to.is_empty();
}
if coverage.has_approval_gate && !coverage.has_evidence {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "coherence",
message:
"Authority requires approval but no Evidence block — approver has nothing to review."
.into(),
suggestion: Some(
"Add Evidence.Requires fields so the approver has artifacts to evaluate.".into(),
),
});
}
if coverage.has_constraint && !coverage.has_authority {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "coherence",
message: "Constraints declared but no Authority — who enforces the limits?".into(),
suggestion: Some("Add an Authority block with an Actor.".into()),
});
}
coverage
}
fn check_scenarios(
gherkin: &str,
config: &SimulationConfig,
findings: &mut Vec<SimulationFinding>,
) -> usize {
let preprocessed = preprocess_truths(gherkin);
let metas = extract_all_metas(&preprocessed).unwrap_or_default();
if metas.is_empty() {
findings.push(SimulationFinding {
severity: FindingSeverity::Error,
category: "scenario",
message: "No scenarios found — nothing to execute.".into(),
suggestion: Some("Add at least one Scenario with Given/When/Then steps.".into()),
});
return 0;
}
if config.require_assertions {
let has_then = gherkin.lines().any(|line| line.trim().starts_with("Then "));
if !has_then {
findings.push(SimulationFinding {
severity: FindingSeverity::Error,
category: "scenario",
message: "No Then steps found — scenarios have no success criteria.".into(),
suggestion: Some("Add Then steps that assert expected outcomes.".into()),
});
}
}
let has_given = gherkin
.lines()
.any(|line| line.trim().starts_with("Given "));
if !has_given {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "scenario",
message: "No Given steps — scenarios have no declared preconditions.".into(),
suggestion: Some("Add Given steps that establish the initial state.".into()),
});
}
let has_when = gherkin.lines().any(|line| line.trim().starts_with("When "));
if !has_when {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "scenario",
message: "No When steps — scenarios have no triggering action.".into(),
suggestion: Some("Add When steps that describe the action being governed.".into()),
});
}
metas.len()
}
fn check_resources(
gov: &TruthGovernance,
gherkin: &str,
config: &SimulationConfig,
findings: &mut Vec<SimulationFinding>,
) -> ResourceSummary {
let mut summary = ResourceSummary::default();
if let Some(evidence) = &gov.evidence {
summary.declared_evidence.clone_from(&evidence.requires);
}
let resource_pattern = regex::Regex::new(r"[a-z][a-z0-9_]*(?:_[a-z0-9]+)+").ok();
let mut referenced = BTreeSet::new();
if let Some(pattern) = &resource_pattern {
for line in gherkin.lines() {
let trimmed = line.trim();
if trimmed.starts_with("Given ")
|| trimmed.starts_with("When ")
|| trimmed.starts_with("Then ")
|| trimmed.starts_with("And ")
{
for m in pattern.find_iter(trimmed) {
referenced.insert(m.as_str().to_string());
}
}
}
}
summary.referenced_in_scenarios = referenced.into_iter().collect();
if config.check_resource_availability && !summary.declared_evidence.is_empty() {
for referenced in &summary.referenced_in_scenarios {
let looks_like_evidence = referenced.ends_with("_assessment")
|| referenced.ends_with("_analysis")
|| referenced.ends_with("_report")
|| referenced.ends_with("_review")
|| referenced.ends_with("_log")
|| referenced.ends_with("_record")
|| referenced.ends_with("_bundle");
if looks_like_evidence && !summary.declared_evidence.contains(referenced) {
summary.missing.push(referenced.clone());
}
}
if !summary.missing.is_empty() {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "resources",
message: format!(
"Scenario references evidence-like resources not declared in Evidence block: {}",
summary.missing.join(", ")
),
suggestion: Some(
"Add these as `Requires:` fields in the Evidence block, or rename to avoid evidence naming patterns.".into(),
),
});
}
}
if let Some(authority) = &gov.authority
&& let Some(actor) = &authority.actor
{
let actor_referenced = gherkin.contains(actor);
if !actor_referenced {
findings.push(SimulationFinding {
severity: FindingSeverity::Info,
category: "resources",
message: format!(
"Authority actor `{actor}` is declared but not referenced in any scenario."
),
suggestion: Some(
"Consider adding a scenario step that involves the authorized actor.".into(),
),
});
}
}
summary
}
fn check_determinism(
gov: &TruthGovernance,
gherkin: &str,
config: &SimulationConfig,
findings: &mut Vec<SimulationFinding>,
) -> DeterministicTrace {
let steps = extract_trace_steps(gherkin);
let non_replayable_steps: Vec<TraceStep> = steps
.iter()
.filter(|step| contains_non_replayable_reference(&step.text))
.cloned()
.collect();
if !non_replayable_steps.is_empty() {
let labels = non_replayable_steps
.iter()
.map(|step| format!("scenario {} step {}", step.scenario_index, step.step_index))
.collect::<Vec<_>>()
.join(", ");
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "determinism",
message: format!("Scenario language references non-replayable runtime state: {labels}."),
suggestion: Some(
"Use declared Evidence fields for snapshots, or rewrite the step to avoid current time, randomness, latest external state, or live network calls.".into(),
),
});
}
let mut canonical = String::new();
canonical.push_str("simulation-trace-v1\n");
canonical.push_str(&canonical_config(config));
canonical.push('\n');
canonical.push_str(&canonical_governance(gov));
canonical.push('\n');
for step in &steps {
canonical.push_str(&format!(
"{}:{}:{}:{}\n",
step.scenario_index, step.step_index, step.keyword, step.text
));
}
DeterministicTrace {
trace_hash: deterministic_hash(canonical.as_bytes()),
step_count: steps.len(),
replayable: non_replayable_steps.is_empty(),
non_replayable_steps,
}
}
fn extract_trace_steps(gherkin: &str) -> Vec<TraceStep> {
let mut steps = Vec::new();
let mut scenario_index = 0;
let mut step_index = 0;
for line in gherkin.lines() {
let trimmed = line.trim();
if starts_scenario(trimmed) {
scenario_index += 1;
step_index = 0;
continue;
}
let Some((keyword, text)) = parse_step_line(trimmed) else {
continue;
};
if scenario_index == 0 {
scenario_index = 1;
}
step_index += 1;
steps.push(TraceStep {
scenario_index,
step_index,
keyword: keyword.to_string(),
text: normalize_whitespace(text),
});
}
steps
}
fn starts_scenario(trimmed: &str) -> bool {
trimmed.starts_with("Scenario:")
|| trimmed.starts_with("Scenario Outline:")
|| trimmed.starts_with("Example:")
}
fn parse_step_line(trimmed: &str) -> Option<(&'static str, &str)> {
for keyword in ["Given", "When", "Then", "And", "But"] {
if let Some(text) = trimmed
.strip_prefix(keyword)
.and_then(|rest| rest.strip_prefix(' '))
{
return Some((keyword, text));
}
}
None
}
fn normalize_whitespace(value: &str) -> String {
value.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn contains_non_replayable_reference(text: &str) -> bool {
let lower = text.to_ascii_lowercase();
lower.contains("current time")
|| lower.contains("current date")
|| lower.contains("external api")
|| lower.contains("remote api")
|| lower.contains("live api")
|| lower.contains("live service")
|| lower.contains("network call")
|| lower.contains("webhook")
|| lower.contains("real-time")
|| lower.contains("realtime")
|| contains_word(&lower, "today")
|| contains_word(&lower, "now")
|| contains_word(&lower, "latest")
|| contains_word(&lower, "random")
}
fn contains_word(text: &str, word: &str) -> bool {
text.split(|ch: char| !ch.is_ascii_alphanumeric())
.any(|part| part == word)
}
fn check_domain_profiles(
gov: &TruthGovernance,
gherkin: &str,
profiles: &[DomainProfile],
) -> Vec<DomainProfileReport> {
normalized_domain_profiles(profiles)
.into_iter()
.map(|profile| match profile {
DomainProfile::VendorSelection => {
let mut findings = Vec::new();
let coverage = check_vendor_selection(gov, gherkin, &mut findings);
DomainProfileReport {
profile,
findings,
coverage: DomainProfileCoverage::VendorSelection(coverage),
}
}
})
.collect()
}
fn normalized_domain_profiles(profiles: &[DomainProfile]) -> Vec<DomainProfile> {
let mut profiles = profiles.to_vec();
profiles.sort_unstable();
profiles.dedup();
profiles
}
fn canonical_config(config: &SimulationConfig) -> String {
let profiles = normalized_domain_profiles(&config.domain_profiles)
.iter()
.map(|profile| match profile {
DomainProfile::VendorSelection => "vendor-selection",
})
.collect::<Vec<_>>()
.join(",");
format!(
"require_intent={};require_authority={};require_evidence={};require_assertions={};check_resource_availability={};domain_profiles={profiles}",
config.require_intent,
config.require_authority,
config.require_evidence,
config.require_assertions,
config.check_resource_availability
)
}
fn canonical_governance(gov: &TruthGovernance) -> String {
let mut lines = Vec::new();
if let Some(intent) = &gov.intent {
push_optional(&mut lines, "intent.outcome", intent.outcome.as_deref());
push_optional(&mut lines, "intent.goal", intent.goal.as_deref());
}
if let Some(authority) = &gov.authority {
push_optional(&mut lines, "authority.actor", authority.actor.as_deref());
push_values(&mut lines, "authority.may", &authority.may);
push_values(&mut lines, "authority.must_not", &authority.must_not);
push_values(
&mut lines,
"authority.requires_approval",
&authority.requires_approval,
);
push_optional(
&mut lines,
"authority.expires",
authority.expires.as_deref(),
);
}
if let Some(constraint) = &gov.constraint {
push_values(&mut lines, "constraint.budget", &constraint.budget);
push_values(&mut lines, "constraint.cost_limit", &constraint.cost_limit);
push_values(&mut lines, "constraint.must_not", &constraint.must_not);
}
if let Some(evidence) = &gov.evidence {
push_values(&mut lines, "evidence.requires", &evidence.requires);
push_values(&mut lines, "evidence.provenance", &evidence.provenance);
push_values(&mut lines, "evidence.audit", &evidence.audit);
}
if let Some(exception) = &gov.exception {
push_values(
&mut lines,
"exception.escalates_to",
&exception.escalates_to,
);
push_values(&mut lines, "exception.requires", &exception.requires);
}
lines.sort();
lines.join("\n")
}
fn push_optional(lines: &mut Vec<String>, key: &str, value: Option<&str>) {
if let Some(value) = value {
lines.push(format!("{key}={}", normalize_whitespace(value)));
}
}
fn push_values(lines: &mut Vec<String>, key: &str, values: &[String]) {
for value in sorted_unique(values) {
lines.push(format!("{key}={}", normalize_whitespace(&value)));
}
}
fn sorted_unique(values: &[String]) -> Vec<String> {
values
.iter()
.map(|value| normalize_whitespace(value))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
fn deterministic_hash(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let hash = Sha256::digest(bytes);
let hash_bytes: &[u8] = hash.as_ref();
let mut encoded = String::with_capacity("sha256:".len() + (hash_bytes.len() * 2));
encoded.push_str("sha256:");
for &byte in hash_bytes {
encoded.push(char::from(HEX[usize::from(byte >> 4)]));
encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
encoded
}
fn canonicalize_report(report: &mut SimulationReport) {
canonicalize_findings(&mut report.findings);
canonicalize_strings(&mut report.resource_summary.declared_evidence);
canonicalize_strings(&mut report.resource_summary.referenced_in_scenarios);
canonicalize_strings(&mut report.resource_summary.missing);
report.domain_profiles.sort_by_key(|domain| domain.profile);
for domain in &mut report.domain_profiles {
canonicalize_findings(&mut domain.findings);
match &mut domain.coverage {
DomainProfileCoverage::VendorSelection(coverage) => {
canonicalize_strings(&mut coverage.vendor_references);
}
}
}
report
.deterministic_trace
.non_replayable_steps
.sort_by(|left, right| {
left.scenario_index
.cmp(&right.scenario_index)
.then_with(|| left.step_index.cmp(&right.step_index))
.then_with(|| left.keyword.cmp(&right.keyword))
.then_with(|| left.text.cmp(&right.text))
});
}
fn canonicalize_strings(values: &mut Vec<String>) {
*values = values
.iter()
.map(|value| normalize_whitespace(value))
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
}
fn canonicalize_findings(findings: &mut Vec<SimulationFinding>) {
findings.sort_by(|left, right| {
severity_rank(left.severity)
.cmp(&severity_rank(right.severity))
.then_with(|| left.category.cmp(right.category))
.then_with(|| left.message.cmp(&right.message))
.then_with(|| left.suggestion.cmp(&right.suggestion))
});
findings.dedup_by(|left, right| {
left.severity == right.severity
&& left.category == right.category
&& left.message == right.message
&& left.suggestion == right.suggestion
});
}
fn severity_rank(severity: FindingSeverity) -> u8 {
match severity {
FindingSeverity::Error => 0,
FindingSeverity::Warning => 1,
FindingSeverity::Info => 2,
}
}
const VENDOR_KEYWORDS: &[&str] = &[
"vendor",
"procurement",
"supplier",
"rfp",
"shortlist",
"sourcing",
];
const EVALUATION_DIMENSIONS: &[&str] = &[
"compliance",
"cost",
"risk",
"security",
"capability",
"stability",
"performance",
"pricing",
"budget",
"certification",
"regulatory",
"timeline",
"delivery",
];
fn check_vendor_selection(
gov: &TruthGovernance,
gherkin: &str,
findings: &mut Vec<SimulationFinding>,
) -> VendorSelectionCoverage {
let mut coverage = VendorSelectionCoverage::default();
let combined = format!(
"{} {}",
gov.intent
.as_ref()
.and_then(|i| i.outcome.as_deref())
.unwrap_or(""),
gherkin
)
.to_lowercase();
coverage.detected = VENDOR_KEYWORDS.iter().any(|kw| combined.contains(kw));
if !coverage.detected {
return coverage;
}
let dimensions: Vec<&str> = EVALUATION_DIMENSIONS
.iter()
.copied()
.filter(|d| combined.contains(d))
.collect();
coverage.evaluation_dimensions = dimensions.len();
if coverage.evaluation_dimensions < 3 {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "vendor-selection",
message: format!(
"Vendor selection spec mentions only {} evaluation dimension(s): {}. \
At least 3 are recommended for meaningful differentiation.",
coverage.evaluation_dimensions,
if dimensions.is_empty() {
"none".to_string()
} else {
dimensions.join(", ")
}
),
suggestion: Some(
"Add evaluation criteria such as compliance, cost, risk, security, capability."
.into(),
),
});
}
let vendor_pattern = regex::Regex::new(r#"(?i)(?:vendors?|suppliers?)\s+"([^"]+)""#).ok();
if let Some(pat) = &vendor_pattern {
for cap in pat.captures_iter(gherkin) {
if let Some(names) = cap.get(1) {
for name in names.as_str().split(',') {
let trimmed = name.trim().to_string();
if !trimmed.is_empty() && !coverage.vendor_references.contains(&trimmed) {
coverage.vendor_references.push(trimmed);
}
}
}
}
}
if coverage.vendor_references.len() < 3 {
findings.push(SimulationFinding {
severity: FindingSeverity::Info,
category: "vendor-selection",
message: format!(
"Only {} vendor(s) referenced in scenarios. \
3+ vendors recommended for meaningful comparison.",
coverage.vendor_references.len()
),
suggestion: Some(
"Add more vendors in Given steps: Given vendors \"Acme, Beta, Gamma\"".into(),
),
});
}
let scenario_text: String = gherkin
.lines()
.filter(|l| {
let t = l.trim();
t.starts_with("Then ") || t.starts_with("And ")
})
.collect::<Vec<_>>()
.join(" ")
.to_lowercase();
coverage.has_ranking_criterion = scenario_text.contains("rank")
|| scenario_text.contains("shortlist")
|| scenario_text.contains("scored")
|| scenario_text.contains("recommendation");
if !coverage.has_ranking_criterion {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "vendor-selection",
message: "No ranking or shortlisting criterion detected.".into(),
suggestion: Some(
"Add a Then step asserting a ranked shortlist or recommendation is produced."
.into(),
),
});
}
coverage.has_commitment_gate = gov
.authority
.as_ref()
.is_some_and(|a| !a.requires_approval.is_empty());
if !coverage.has_commitment_gate {
findings.push(SimulationFinding {
severity: FindingSeverity::Warning,
category: "vendor-selection",
message: "No commitment approval gate found. Vendor selections with financial \
impact should require human approval."
.into(),
suggestion: Some(
"Add `Requires Approval: vendor_commitment` to the Authority block.".into(),
),
});
}
coverage
}
#[cfg(test)]
mod tests {
use super::*;
use crate::truths::{
AuthorityBlock, ConstraintBlock, EvidenceBlock, ExceptionBlock, IntentBlock,
parse_truth_document,
};
fn full_spec() -> &'static str {
r#"Truth: Vendor selection is governed
Intent:
Outcome: Select a vendor with auditable rationale.
Goal: Evaluate candidates on cost, compliance, and risk.
Authority:
Actor: governance_review_board
Requires Approval: final_vendor_selection
Constraint:
Cost Limit: first-year spend must stay within budget.
Evidence:
Requires: security_assessment
Requires: pricing_analysis
Audit: decision_log
Scenario: Vendors produce traceable outcomes
Given candidate vendors "Acme AI, Beta ML, Gamma LLM"
And each vendor has a security_assessment and pricing_analysis
When the governance_review_board evaluates each vendor
Then each vendor should produce a compliance screening result
And a ranked shortlist is produced
"#
}
fn minimal_valid_spec() -> &'static str {
r"Truth: Minimal
Intent:
Outcome: Works.
Authority:
Actor: admin
Evidence:
Requires: proof
Scenario: It works
Given something exists
When validated
Then it passes
"
}
fn vendor_config() -> SimulationConfig {
SimulationConfig::default().with_domain_profile(DomainProfile::VendorSelection)
}
fn vendor_selection(report: &SimulationReport) -> &VendorSelectionCoverage {
report
.vendor_selection()
.expect("vendor selection profile should be enabled")
}
#[test]
fn verdict_display() {
assert_eq!(Verdict::Ready.to_string(), "ready");
assert_eq!(Verdict::Risky.to_string(), "risky");
assert_eq!(Verdict::WillNotConverge.to_string(), "will-not-converge");
}
#[test]
fn finding_severity_display() {
assert_eq!(FindingSeverity::Info.to_string(), "info");
assert_eq!(FindingSeverity::Warning.to_string(), "warning");
assert_eq!(FindingSeverity::Error.to_string(), "error");
}
#[test]
fn can_converge_ready() {
let report = SimulationReport {
verdict: Verdict::Ready,
findings: vec![],
governance_coverage: GovernanceCoverage::default(),
scenario_count: 1,
resource_summary: ResourceSummary::default(),
deterministic_trace: DeterministicTrace::default(),
domain_profiles: vec![],
};
assert!(report.can_converge());
}
#[test]
fn can_converge_risky() {
let report = SimulationReport {
verdict: Verdict::Risky,
findings: vec![],
governance_coverage: GovernanceCoverage::default(),
scenario_count: 1,
resource_summary: ResourceSummary::default(),
deterministic_trace: DeterministicTrace::default(),
domain_profiles: vec![],
};
assert!(report.can_converge());
}
#[test]
fn cannot_converge_will_not() {
let report = SimulationReport {
verdict: Verdict::WillNotConverge,
findings: vec![],
governance_coverage: GovernanceCoverage::default(),
scenario_count: 0,
resource_summary: ResourceSummary::default(),
deterministic_trace: DeterministicTrace::default(),
domain_profiles: vec![],
};
assert!(!report.can_converge());
}
#[test]
fn finding_with_suggestion() {
let f = SimulationFinding {
severity: FindingSeverity::Warning,
category: "test",
message: "something is off".into(),
suggestion: Some("fix it".into()),
};
assert_eq!(f.severity, FindingSeverity::Warning);
assert_eq!(f.category, "test");
assert!(f.suggestion.is_some());
}
#[test]
fn finding_without_suggestion() {
let f = SimulationFinding {
severity: FindingSeverity::Info,
category: "test",
message: "just info".into(),
suggestion: None,
};
assert!(f.suggestion.is_none());
}
#[test]
fn complete_spec_is_ready() {
let doc = parse_truth_document(full_spec()).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.verdict, Verdict::Ready);
assert!(report.can_converge());
}
#[test]
fn complete_spec_governance_coverage() {
let doc = parse_truth_document(full_spec()).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert!(report.governance_coverage.has_intent);
assert!(report.governance_coverage.has_outcome);
assert!(report.governance_coverage.has_authority);
assert!(report.governance_coverage.has_actor);
assert!(report.governance_coverage.has_constraint);
assert!(report.governance_coverage.has_evidence);
assert_eq!(report.governance_coverage.evidence_count, 2);
}
#[test]
fn complete_spec_scenario_count() {
let doc = parse_truth_document(full_spec()).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.scenario_count, 1);
}
#[test]
fn complete_spec_resource_summary() {
let doc = parse_truth_document(full_spec()).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.resource_summary.declared_evidence.len(), 2);
assert!(report.resource_summary.missing.is_empty());
}
#[test]
fn missing_intent_will_not_converge() {
let content = r"Truth: No intent
Scenario: Something happens
Given a precondition
When an action occurs
Then a result is observed
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.verdict, Verdict::WillNotConverge);
assert!(!report.can_converge());
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("Missing Intent"))
);
}
#[test]
fn missing_authority_will_not_converge() {
let content = r"Truth: No authority
Intent:
Outcome: Do a thing.
Evidence:
Requires: proof
Scenario: Action
Given precondition
When something happens
Then outcome
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.verdict, Verdict::WillNotConverge);
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("Missing Authority"))
);
}
#[test]
fn missing_evidence_will_not_converge() {
let content = r"Truth: No evidence
Intent:
Outcome: Do a thing.
Authority:
Actor: admin
Scenario: Action
Given precondition
When something happens
Then outcome
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.verdict, Verdict::WillNotConverge);
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("Missing Evidence"))
);
}
#[test]
fn missing_then_steps_will_not_converge() {
let content = r"Truth: No assertions
Intent:
Outcome: Do something.
Authority:
Actor: admin
Evidence:
Requires: report
Scenario: Missing outcome
Given a shortlist of vendors
When the workflow ranks them
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.verdict, Verdict::WillNotConverge);
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("No Then steps"))
);
}
#[test]
fn missing_given_steps_produces_warning() {
let content = r"Truth: No given
Intent:
Outcome: Do something.
Authority:
Actor: admin
Evidence:
Requires: report
Scenario: No preconditions
When something happens
Then it works
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("No Given steps"))
);
}
#[test]
fn missing_when_steps_produces_warning() {
let content = r"Truth: No when
Intent:
Outcome: Do something.
Authority:
Actor: admin
Evidence:
Requires: report
Scenario: No action
Given a state
Then it is fine
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("No When steps"))
);
}
#[test]
fn no_scenarios_will_not_converge() {
let content = r"Truth: Empty
Intent:
Outcome: Nothing to do.
Authority:
Actor: admin
Evidence:
Requires: proof
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.verdict, Verdict::WillNotConverge);
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("No scenarios found"))
);
assert_eq!(report.scenario_count, 0);
}
#[test]
fn approval_without_evidence_is_risky() {
let content = r"Truth: Approval gate without evidence
Intent:
Outcome: Approve a vendor.
Authority:
Actor: board
Requires Approval: cfo_sign_off
Scenario: Approval happens
Given a vendor is shortlisted
When the board reviews
Then the vendor is approved
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.verdict, Verdict::WillNotConverge);
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("approver has nothing to review"))
);
}
#[test]
fn constraint_without_authority_warns() {
let doc = TruthDocument {
governance: TruthGovernance {
intent: Some(IntentBlock {
outcome: Some("Do it".into()),
goal: None,
}),
authority: None,
constraint: Some(ConstraintBlock {
budget: vec!["100k".into()],
cost_limit: vec![],
must_not: vec![],
}),
evidence: Some(EvidenceBlock {
requires: vec!["proof".into()],
provenance: vec![],
audit: vec!["log".into()],
}),
exception: None,
},
gherkin: "Scenario: Test\n Given a state\n When action\n Then result".into(),
};
let config = SimulationConfig {
require_authority: false,
..SimulationConfig::default()
};
let report = simulate(&doc, &config);
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("who enforces"))
);
}
#[test]
fn intent_without_outcome_warns() {
let doc = TruthDocument {
governance: TruthGovernance {
intent: Some(IntentBlock {
outcome: None,
goal: Some("A goal".into()),
}),
authority: Some(AuthorityBlock {
actor: Some("admin".into()),
..AuthorityBlock::default()
}),
constraint: None,
evidence: Some(EvidenceBlock {
requires: vec!["proof".into()],
provenance: vec![],
audit: vec!["log".into()],
}),
exception: None,
},
gherkin: "Scenario: Test\n Given state\n When admin acts\n Then done".into(),
};
let report = simulate(&doc, &SimulationConfig::default());
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("missing Outcome"))
);
}
#[test]
fn authority_without_actor_warns() {
let doc = TruthDocument {
governance: TruthGovernance {
intent: Some(IntentBlock {
outcome: Some("Do it".into()),
goal: None,
}),
authority: Some(AuthorityBlock::default()),
constraint: None,
evidence: Some(EvidenceBlock {
requires: vec!["proof".into()],
provenance: vec![],
audit: vec!["log".into()],
}),
exception: None,
},
gherkin: "Scenario: Test\n Given state\n When action\n Then done".into(),
};
let report = simulate(&doc, &SimulationConfig::default());
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("missing Actor"))
);
}
#[test]
fn empty_evidence_requires_warns() {
let doc = TruthDocument {
governance: TruthGovernance {
intent: Some(IntentBlock {
outcome: Some("Do it".into()),
goal: None,
}),
authority: Some(AuthorityBlock {
actor: Some("admin".into()),
..AuthorityBlock::default()
}),
constraint: None,
evidence: Some(EvidenceBlock {
requires: vec![],
provenance: vec![],
audit: vec!["log".into()],
}),
exception: None,
},
gherkin: "Scenario: Test\n Given state\n When admin acts\n Then done".into(),
};
let report = simulate(&doc, &SimulationConfig::default());
assert!(
report
.findings
.iter()
.any(|f| f.message.contains("no Requires fields"))
);
}
#[test]
fn empty_constraint_block_info() {
let doc = TruthDocument {
governance: TruthGovernance {
intent: Some(IntentBlock {
outcome: Some("Do it".into()),
goal: None,
}),
authority: Some(AuthorityBlock {
actor: Some("admin".into()),
..AuthorityBlock::default()
}),
constraint: Some(ConstraintBlock::default()),
evidence: Some(EvidenceBlock {
requires: vec!["proof".into()],
provenance: vec![],
audit: vec!["log".into()],
}),
exception: None,
},
gherkin: "Scenario: Test\n Given state\n When admin acts\n Then done".into(),
};
let report = simulate(&doc, &SimulationConfig::default());
assert!(report.findings.iter().any(|f| {
f.severity == FindingSeverity::Info && f.message.contains("no guardrails")
}));
}
#[test]
fn exception_with_escalation_path() {
let doc = TruthDocument {
governance: TruthGovernance {
intent: Some(IntentBlock {
outcome: Some("Do it".into()),
goal: None,
}),
authority: Some(AuthorityBlock {
actor: Some("admin".into()),
..AuthorityBlock::default()
}),
constraint: None,
evidence: Some(EvidenceBlock {
requires: vec!["proof".into()],
provenance: vec![],
audit: vec!["log".into()],
}),
exception: Some(ExceptionBlock {
escalates_to: vec!["ceo".into()],
requires: vec![],
}),
},
gherkin: "Scenario: Test\n Given state\n When admin acts\n Then done".into(),
};
let report = simulate(&doc, &SimulationConfig::default());
assert!(report.governance_coverage.has_exception);
assert!(report.governance_coverage.has_escalation_path);
}
#[test]
fn relaxed_config_allows_missing_governance() {
let content = r"Truth: Bare minimum
Scenario: Just do it
Given a state
When an action
Then a result
";
let doc = parse_truth_document(content).unwrap();
let config = SimulationConfig {
require_intent: false,
require_authority: false,
require_evidence: false,
require_assertions: false,
check_resource_availability: false,
domain_profiles: vec![],
};
let report = simulate(&doc, &config);
let has_errors = report
.findings
.iter()
.any(|f| f.severity == FindingSeverity::Error);
assert!(!has_errors);
assert_ne!(report.verdict, Verdict::WillNotConverge);
}
#[test]
fn default_config_is_strict() {
let config = SimulationConfig::default();
assert!(config.require_intent);
assert!(config.require_authority);
assert!(config.require_evidence);
assert!(config.require_assertions);
assert!(config.check_resource_availability);
assert!(config.domain_profiles.is_empty());
}
#[test]
fn simulate_spec_convenience() {
let report = simulate_spec(minimal_valid_spec(), &SimulationConfig::default()).unwrap();
assert!(report.can_converge());
}
#[test]
fn simulate_spec_garbage_input() {
let result = simulate_spec(
"this is not a truth spec at all",
&SimulationConfig::default(),
);
match result {
Err(_) => {}
Ok(report) => assert_eq!(report.verdict, Verdict::WillNotConverge),
}
}
#[test]
fn simulate_spec_empty_string() {
let result = simulate_spec("", &SimulationConfig::default());
match result {
Err(_) => {}
Ok(report) => assert_eq!(report.verdict, Verdict::WillNotConverge),
}
}
#[test]
fn undeclared_evidence_like_resource_warns() {
let content = r"Truth: Resource mismatch
Intent:
Outcome: Check resources.
Authority:
Actor: admin
Evidence:
Requires: security_assessment
Audit: decision_log
Scenario: Uses undeclared evidence
Given a vendor
When admin reviews the compliance_report
Then the security_assessment is valid
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert!(
report
.resource_summary
.missing
.contains(&"compliance_report".to_string())
);
}
#[test]
fn actor_not_referenced_in_scenarios_info() {
let content = r"Truth: Unused actor
Intent:
Outcome: Something.
Authority:
Actor: mysterious_committee
Evidence:
Requires: proof
Scenario: Nobody calls the actor
Given a state
When something happens
Then it works
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert!(report.findings.iter().any(|f| {
f.severity == FindingSeverity::Info
&& f.message.contains("mysterious_committee")
&& f.message.contains("not referenced")
}));
}
#[test]
fn simulation_report_is_reproducible_across_runs() {
let doc = parse_truth_document(full_spec()).unwrap();
let config = SimulationConfig {
domain_profiles: vec![
DomainProfile::VendorSelection,
DomainProfile::VendorSelection,
],
..SimulationConfig::default()
};
let first = simulate(&doc, &config);
let second = simulate(&doc, &config);
assert_eq!(first, second);
assert!(first.deterministic_trace.trace_hash.starts_with("sha256:"));
assert_eq!(first.deterministic_trace.step_count, 5);
assert!(first.deterministic_trace.replayable);
}
#[test]
fn resource_summary_is_canonicalized() {
let content = r"Truth: Resource order
Intent:
Outcome: Check resources.
Authority:
Actor: admin
Evidence:
Requires: zeta_report
Requires: alpha_report
Requires: alpha_report
Audit: decision_log
Scenario: Uses resources
Given zeta_report exists
And alpha_report exists
When admin reviews beta_assessment
Then alpha_report passes
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(
report.resource_summary.declared_evidence,
vec!["alpha_report".to_string(), "zeta_report".to_string()]
);
assert_eq!(
report.resource_summary.referenced_in_scenarios,
vec![
"alpha_report".to_string(),
"beta_assessment".to_string(),
"zeta_report".to_string()
]
);
assert_eq!(
report.resource_summary.missing,
vec!["beta_assessment".to_string()]
);
}
#[test]
fn non_replayable_language_warns_and_marks_trace() {
let content = r"Truth: Live data
Intent:
Outcome: Check latest state.
Authority:
Actor: admin
Evidence:
Requires: snapshot_report
Scenario: Uses live data
Given snapshot_report exists
When admin calls the remote API for the latest value
Then the value is accepted
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert!(!report.deterministic_trace.replayable);
assert_eq!(report.deterministic_trace.non_replayable_steps.len(), 1);
assert!(report.findings.iter().any(|finding| {
finding.category == "determinism" && finding.message.contains("non-replayable")
}));
}
#[test]
fn multiple_scenarios_counted() {
let content = r"Truth: Multi-scenario
Intent:
Outcome: Test multiple.
Authority:
Actor: admin
Evidence:
Requires: proof
Scenario: First
Given state
When admin acts
Then result
Scenario: Second
Given another state
When admin acts again
Then another result
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.scenario_count, 2);
}
#[test]
fn spec_with_only_governance_no_scenarios() {
let doc = TruthDocument {
governance: TruthGovernance {
intent: Some(IntentBlock {
outcome: Some("Goal".into()),
goal: None,
}),
authority: Some(AuthorityBlock {
actor: Some("admin".into()),
..AuthorityBlock::default()
}),
constraint: None,
evidence: Some(EvidenceBlock {
requires: vec!["proof".into()],
provenance: vec![],
audit: vec![],
}),
exception: None,
},
gherkin: String::new(),
};
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.verdict, Verdict::WillNotConverge);
assert_eq!(report.scenario_count, 0);
}
#[test]
fn nil_governance_with_relaxed_config() {
let content = r"Truth: Nil governance
Scenario: Solo
Given x
When y
Then z
";
let doc = parse_truth_document(content).unwrap();
let config = SimulationConfig {
require_intent: false,
require_authority: false,
require_evidence: false,
require_assertions: true,
check_resource_availability: false,
domain_profiles: vec![],
};
let report = simulate(&doc, &config);
assert!(!report.governance_coverage.has_intent);
assert!(!report.governance_coverage.has_authority);
assert!(!report.governance_coverage.has_evidence);
assert_eq!(report.verdict, Verdict::Ready);
}
#[test]
fn nil_governance_with_strict_config() {
let content = r"Truth: Nil governance strict
Scenario: Solo
Given x
When y
Then z
";
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert_eq!(report.verdict, Verdict::WillNotConverge);
let error_count = report
.findings
.iter()
.filter(|f| f.severity == FindingSeverity::Error)
.count();
assert!(error_count >= 3); }
#[test]
fn default_config_does_not_run_domain_profiles() {
let doc = parse_truth_document(full_spec()).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert!(report.domain_profiles.is_empty());
assert!(report.vendor_selection().is_none());
assert!(
!report
.findings
.iter()
.any(|f| f.category == "vendor-selection")
);
}
#[test]
fn domain_profile_deduplicates() {
let config = SimulationConfig::default()
.with_domain_profile(DomainProfile::VendorSelection)
.with_domain_profile(DomainProfile::VendorSelection);
assert_eq!(config.domain_profiles.len(), 1);
}
#[test]
fn vendor_spec_detected() {
let doc = parse_truth_document(full_spec()).unwrap();
let report = simulate(&doc, &vendor_config());
assert!(vendor_selection(&report).detected);
}
#[test]
fn vendor_spec_extracts_vendor_names() {
let doc = parse_truth_document(full_spec()).unwrap();
let report = simulate(&doc, &vendor_config());
assert!(
vendor_selection(&report)
.vendor_references
.contains(&"Acme AI".to_string())
);
assert!(
vendor_selection(&report)
.vendor_references
.contains(&"Beta ML".to_string())
);
}
#[test]
fn vendor_spec_counts_evaluation_dimensions() {
let doc = parse_truth_document(full_spec()).unwrap();
let report = simulate(&doc, &vendor_config());
assert!(vendor_selection(&report).evaluation_dimensions >= 2);
}
#[test]
fn vendor_spec_detects_approval_gate() {
let doc = parse_truth_document(full_spec()).unwrap();
let report = simulate(&doc, &vendor_config());
assert!(vendor_selection(&report).has_commitment_gate);
}
#[test]
fn non_vendor_spec_not_detected() {
let doc = parse_truth_document(minimal_valid_spec()).unwrap();
let report = simulate(&doc, &vendor_config());
assert!(!vendor_selection(&report).detected);
}
#[test]
fn vendor_spec_few_dimensions_warns() {
let content = r#"Truth: Thin vendor eval
Intent:
Outcome: Select a vendor.
Authority:
Actor: admin
Requires Approval: commitment
Evidence:
Requires: proof
Scenario: Pick a vendor
Given vendors "Acme"
When evaluated
Then a recommendation is produced
"#;
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &vendor_config());
assert!(vendor_selection(&report).detected);
assert!(report.findings.iter().any(|f| {
f.category == "vendor-selection" && f.message.contains("evaluation dimension")
}));
}
#[test]
fn vendor_spec_no_ranking_warns() {
let content = r#"Truth: No ranking vendor eval
Intent:
Outcome: Select a vendor with compliance and cost and risk analysis.
Authority:
Actor: board
Requires Approval: commitment
Evidence:
Requires: compliance_report
Scenario: Evaluate vendors
Given vendors "Acme, Beta, Gamma"
When the board evaluates
Then all vendors are screened
"#;
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &vendor_config());
assert!(vendor_selection(&report).detected);
assert!(
report
.findings
.iter()
.any(|f| { f.category == "vendor-selection" && f.message.contains("ranking") })
);
}
#[test]
fn vendor_spec_no_approval_gate_warns() {
let content = r#"Truth: No approval vendor eval
Intent:
Outcome: Select a vendor with compliance and cost and risk.
Authority:
Actor: admin
Evidence:
Requires: report
Scenario: Pick vendor
Given vendors "Acme, Beta, Gamma"
When evaluated
Then a ranked shortlist is produced
"#;
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &vendor_config());
assert!(vendor_selection(&report).detected);
assert!(report.findings.iter().any(|f| {
f.category == "vendor-selection" && f.message.contains("commitment approval gate")
}));
}
#[test]
fn vendor_profile_not_enabled() {
let content = r#"Truth: Vendor eval
Intent:
Outcome: Select a vendor.
Authority:
Actor: admin
Evidence:
Requires: proof
Scenario: Quick
Given vendors "A"
When checked
Then done
"#;
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &SimulationConfig::default());
assert!(report.vendor_selection().is_none());
assert!(
!report
.findings
.iter()
.any(|f| f.category == "vendor-selection")
);
}
#[test]
fn vendor_spec_complete_no_vendor_warnings() {
let content = r#"Truth: Complete vendor selection
Intent:
Outcome: Select a vendor with compliance, cost, risk, security, and capability analysis.
Authority:
Actor: governance_review_board
Requires Approval: vendor_commitment
Constraint:
Cost Limit: annual spend within budget.
Evidence:
Requires: compliance_assessment
Requires: risk_assessment
Requires: cost_analysis
Audit: decision_log
Scenario: Full evaluation
Given vendors "Acme AI, Beta ML, Gamma LLM"
And each vendor has compliance and risk data
When the governance_review_board evaluates
Then a ranked shortlist is produced
And the recommendation has evidence from all criteria
"#;
let doc = parse_truth_document(content).unwrap();
let report = simulate(&doc, &vendor_config());
let coverage = vendor_selection(&report);
assert!(coverage.detected);
assert!(coverage.evaluation_dimensions >= 3);
assert!(coverage.has_ranking_criterion);
assert!(coverage.has_commitment_gate);
assert_eq!(coverage.vendor_references.len(), 3);
assert!(!report.findings.iter().any(|f| {
f.category == "vendor-selection" && f.severity == FindingSeverity::Warning
}));
}
}