use std::fmt;
#[derive(Debug, Clone)]
pub struct ConstraintCheckResult {
pub constraint_key: String,
pub constraint_text: String,
pub status: ConstraintStatus,
pub reason: String,
pub relevant_facts: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintStatus {
Pass,
Fail,
Unknown,
}
impl fmt::Display for ConstraintStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConstraintStatus::Pass => write!(f, "PASS"),
ConstraintStatus::Fail => write!(f, "FAIL"),
ConstraintStatus::Unknown => write!(f, "UNKNOWN"),
}
}
}
#[derive(Debug, Clone)]
pub struct FactValue {
pub key: String,
pub value: String,
}
pub fn check_constraints(
constraints: &[FactValue],
facts: &[FactValue],
) -> Vec<ConstraintCheckResult> {
constraints.iter().map(|c| check_one(c, facts)).collect()
}
fn check_one(constraint: &FactValue, facts: &[FactValue]) -> ConstraintCheckResult {
let c_text = &constraint.value;
let c_lower = c_text.to_lowercase();
if is_budget_constraint(&c_lower) {
if let Some(limit) = extract_dollar_amount(c_text) {
for fact in facts {
let f_lower = fact.value.to_lowercase();
if f_lower.contains("cost")
|| f_lower.contains("spend")
|| f_lower.contains("price")
|| f_lower.contains("total")
|| f_lower.contains("budget")
{
if let Some(amount) = extract_dollar_amount(&fact.value) {
let status = if amount <= limit {
ConstraintStatus::Pass
} else {
ConstraintStatus::Fail
};
return ConstraintCheckResult {
constraint_key: constraint.key.clone(),
constraint_text: c_text.clone(),
status,
reason: format!(
"${:.0} {} ${:.0} limit",
amount,
if amount <= limit { "within" } else { "exceeds" },
limit,
),
relevant_facts: vec![fact.key.clone()],
};
}
}
}
}
}
if is_capacity_constraint(&c_lower) {
if let Some(limit) = extract_count(&c_lower) {
for fact in facts {
let f_lower = fact.value.to_lowercase();
if let Some(count) = extract_count(&f_lower) {
if fact.key.to_lowercase().contains(
&constraint
.key
.to_lowercase()
.replace("_limit", "")
.replace("_cap", "")
.replace("_max", ""),
) || f_lower.contains("team")
|| f_lower.contains("staff")
|| f_lower.contains("assigned")
|| f_lower.contains("current")
{
let status = if count <= limit {
ConstraintStatus::Pass
} else {
ConstraintStatus::Fail
};
return ConstraintCheckResult {
constraint_key: constraint.key.clone(),
constraint_text: c_text.clone(),
status,
reason: format!(
"{} {} {} limit",
count,
if count <= limit { "within" } else { "exceeds" },
limit,
),
relevant_facts: vec![fact.key.clone()],
};
}
}
}
}
}
if is_approval_constraint(&c_lower) {
for fact in facts {
let f_lower = fact.value.to_lowercase();
if f_lower.contains("approved")
|| f_lower.contains("signed off")
|| f_lower.contains("authorized")
{
return ConstraintCheckResult {
constraint_key: constraint.key.clone(),
constraint_text: c_text.clone(),
status: ConstraintStatus::Pass,
reason: "approval found".into(),
relevant_facts: vec![fact.key.clone()],
};
}
}
}
ConstraintCheckResult {
constraint_key: constraint.key.clone(),
constraint_text: c_text.clone(),
status: ConstraintStatus::Unknown,
reason: "cannot evaluate deterministically".into(),
relevant_facts: vec![],
}
}
pub fn format_checklist(results: &[ConstraintCheckResult]) -> String {
if results.is_empty() {
return String::new();
}
let mut lines = vec!["## Constraint Pre-Check".to_string()];
for r in results {
let icon = match r.status {
ConstraintStatus::Pass => "✅",
ConstraintStatus::Fail => "❌",
ConstraintStatus::Unknown => "❓",
};
lines.push(format!(
"{} {} [{}]: {}",
icon, r.status, r.constraint_key, r.reason
));
}
lines.join("\n")
}
fn extract_dollar_amount(text: &str) -> Option<f64> {
let re_m = regex_lite_find(text, r"\$\s*([\d,]+(?:\.\d+)?)\s*[Mm]");
if let Some(s) = re_m {
return parse_number(&s).map(|n| n * 1_000_000.0);
}
let re_k = regex_lite_find(text, r"\$\s*([\d,]+(?:\.\d+)?)\s*[Kk]");
if let Some(s) = re_k {
return parse_number(&s).map(|n| n * 1000.0);
}
let chars: Vec<char> = text.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c == '$' {
let start = i + 1;
let start = chars[start..]
.iter()
.position(|c| !c.is_whitespace())
.map(|p| start + p)
.unwrap_or(start);
let end = chars[start..]
.iter()
.position(|c| !c.is_ascii_digit() && *c != ',' && *c != '.')
.map(|p| start + p)
.unwrap_or(chars.len());
let num_str: String = chars[start..end].iter().collect();
return parse_number(&num_str);
}
}
None
}
fn extract_count(text: &str) -> Option<u64> {
let words: Vec<&str> = text.split_whitespace().collect();
let count_contexts = [
"people",
"engineers",
"staff",
"team",
"members",
"seats",
"units",
"items",
"projects",
"employees",
];
for (i, word) in words.iter().enumerate() {
if let Ok(n) = word.parse::<u64>() {
if i + 1 < words.len()
&& count_contexts
.iter()
.any(|ctx| words[i + 1].starts_with(ctx))
{
return Some(n);
}
}
}
for word in &words {
if let Ok(n) = word.parse::<u64>() {
return Some(n);
}
}
None
}
fn is_budget_constraint(text: &str) -> bool {
[
"budget",
"limit",
"cap",
"maximum",
"cannot exceed",
"spending",
"allocation",
"ceiling",
]
.iter()
.any(|kw| text.contains(kw))
}
fn is_capacity_constraint(text: &str) -> bool {
[
"capacity",
"maximum",
"headcount",
"limit",
"no more than",
"at most",
]
.iter()
.any(|kw| text.contains(kw))
}
fn is_approval_constraint(text: &str) -> bool {
[
"approval required",
"needs approval",
"must be approved",
"requires sign-off",
"authorization required",
]
.iter()
.any(|kw| text.contains(kw))
}
fn parse_number(s: &str) -> Option<f64> {
s.replace(',', "").parse::<f64>().ok()
}
fn regex_lite_find(text: &str, _pattern: &str) -> Option<String> {
if _pattern.contains("[Mm]") {
if let Some(dollar_pos) = text.find('$') {
let after = &text[dollar_pos + 1..];
let after = after.trim_start();
let end = after
.find(|c: char| !c.is_ascii_digit() && c != ',' && c != '.')
.unwrap_or(after.len());
let num = &after[..end];
let suffix = after[end..].trim_start();
if suffix.starts_with('M') || suffix.starts_with('m') {
return Some(num.to_string());
}
}
} else if _pattern.contains("[Kk]") {
if let Some(dollar_pos) = text.find('$') {
let after = &text[dollar_pos + 1..];
let after = after.trim_start();
let end = after
.find(|c: char| !c.is_ascii_digit() && c != ',' && c != '.')
.unwrap_or(after.len());
let num = &after[..end];
let suffix = after[end..].trim_start();
if suffix.starts_with('K') || suffix.starts_with('k') {
return Some(num.to_string());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_plain_dollar() {
assert_eq!(extract_dollar_amount("budget is $150,000"), Some(150_000.0));
assert_eq!(extract_dollar_amount("costs $2,500"), Some(2500.0));
}
#[test]
fn extract_dollar_k() {
assert_eq!(extract_dollar_amount("budget: $200K"), Some(200_000.0));
}
#[test]
fn extract_dollar_m() {
assert_eq!(extract_dollar_amount("total $1.5M"), Some(1_500_000.0));
}
#[test]
fn budget_constraint_pass() {
let constraints = vec![FactValue {
key: "project_budget".into(),
value: "Budget limit: $200,000".into(),
}];
let facts = vec![FactValue {
key: "current_spending".into(),
value: "Total cost so far: $150,000".into(),
}];
let results = check_constraints(&constraints, &facts);
assert_eq!(results.len(), 1);
assert_eq!(results[0].status, ConstraintStatus::Pass);
}
#[test]
fn budget_constraint_fail() {
let constraints = vec![FactValue {
key: "project_budget".into(),
value: "Budget limit: $100,000".into(),
}];
let facts = vec![FactValue {
key: "current_spending".into(),
value: "Total cost: $150,000".into(),
}];
let results = check_constraints(&constraints, &facts);
assert_eq!(results[0].status, ConstraintStatus::Fail);
}
#[test]
fn unknown_when_no_matching_facts() {
let constraints = vec![FactValue {
key: "approval".into(),
value: "Requires VP approval".into(),
}];
let results = check_constraints(&constraints, &[]);
assert_eq!(results[0].status, ConstraintStatus::Unknown);
}
#[test]
fn format_checklist_output() {
let results = vec![
ConstraintCheckResult {
constraint_key: "budget".into(),
constraint_text: "Budget $200K".into(),
status: ConstraintStatus::Pass,
reason: "$150000 within $200000 limit".into(),
relevant_facts: vec!["spending".into()],
},
ConstraintCheckResult {
constraint_key: "approval".into(),
constraint_text: "Needs VP sign-off".into(),
status: ConstraintStatus::Unknown,
reason: "cannot evaluate deterministically".into(),
relevant_facts: vec![],
},
];
let output = format_checklist(&results);
assert!(output.contains("PASS"));
assert!(output.contains("UNKNOWN"));
assert!(output.contains("Constraint Pre-Check"));
}
}