truth: split staffing + devops into dedicated modules (Phase 42 PRD)

Phase 42 PRD (docs/CONTROL_PLANE_PRD.md:137) specified:
  - crates/truth/src/staffing.rs — staffing rule shapes
  - crates/truth/src/devops.rs — scaffold for DevOps long-horizon

PHASES.md marked Phase 42 done, but the rule sets lived inline in
default_truth_store() in lib.rs. Worked, but doesn't match the PRD's
module separation — and that separation matters when the long-horizon
phase fleshes out devops rules: "Keeps the dispatcher signature stable
so no refactor needed later."

Fix: extract staffing_rules() into staffing.rs (5 rules, unchanged
behavior) + create devops.rs with an empty scaffold. default_truth_store
becomes a one-line composition:
    devops::devops_rules(staffing::staffing_rules(TruthStore::new()))

4 new tests in the submodules cover:
  - staffing_rules registers expected count (regression guard)
  - blacklisted worker fails the client-not-blacklisted rule
  - missing deadline fires Reject via FieldEmpty condition
  - devops scaffold is a no-op for now

Total truth tests: 28 → 32. Workspace warnings still at 0.

Still open from Phase 42 (flagged, not in this commit):
  - `truth/` dir at repo root for file-backed rule loading (TOML/YAML).
    Rules are in-code today; loader work is a separate feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-24 13:25:54 -05:00
co-authored by Claude Opus 4.7
parent ed85620558
commit 049a4b69fb
3 changed files with 189 additions and 58 deletions
+15 -58
View File
@@ -1,6 +1,9 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub mod staffing;
pub mod devops;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TruthRule {
pub id: String,
@@ -204,65 +207,19 @@ pub fn sql_query_guard_store() -> TruthStore {
store
}
/// Phase 42 default store: staffing rules + DevOps scaffold composed
/// onto an empty TruthStore. Per the PRD: "Staffing rules ship first;
/// Terraform/Ansible rule shapes are scaffolded but unpopulated until
/// the long-horizon phase." The composition order is irrelevant here
/// (DevOps is empty) but preserved so the shape matches the PRD's
/// expected "compose on top" pattern.
///
/// Moved out of inline in-function rule registration (2026-04-24) to
/// land the Phase 42 module split the PRD called for: `staffing.rs` +
/// `devops.rs` each owns their task-class rule sets. Behavior unchanged
/// for existing callers.
pub fn default_truth_store() -> TruthStore {
let mut store = TruthStore::new();
store.add_rule(TruthRule {
id: "worker-active".to_string(),
task_class: "staffing.fill".to_string(),
description: "Worker must be active".to_string(),
condition: RuleCondition::FieldEquals {
field: "worker.status".to_string(),
value: "active".to_string(),
},
action: RuleAction::Pass,
});
store.add_rule(TruthRule {
id: "client-not-blacklisted".to_string(),
task_class: "staffing.fill".to_string(),
description: "Worker cannot be blacklisted for client".to_string(),
condition: RuleCondition::FieldEquals {
field: "worker.client_blacklisted".to_string(),
value: "false".to_string(),
},
action: RuleAction::Pass,
});
store.add_rule(TruthRule {
id: "deadline-required".to_string(),
task_class: "staffing.fill".to_string(),
description: "Contract must have deadline".to_string(),
condition: RuleCondition::FieldEmpty {
field: "contract.deadline".to_string(),
},
action: RuleAction::Reject {
message: "Contract deadline is required".to_string(),
},
});
store.add_rule(TruthRule {
id: "budget-required".to_string(),
task_class: "staffing.fill".to_string(),
description: "Budget must be non-negative".to_string(),
condition: RuleCondition::FieldGreater {
field: "contract.budget_per_hour_max".to_string(),
threshold: 0,
},
action: RuleAction::Pass,
});
store.add_rule(TruthRule {
id: "pii-redact".to_string(),
task_class: "staffing.any".to_string(),
description: "Redact PII before cloud calls".to_string(),
condition: RuleCondition::Always,
action: RuleAction::Redact {
fields: vec!["ssn".to_string(), "salary".to_string()],
},
});
store
devops::devops_rules(staffing::staffing_rules(TruthStore::new()))
}
#[cfg(test)]