//! Fill-proposal validator. //! //! PRD checks: //! - Schema compliance (propose_done shape matches //! `{fills: [{candidate_id, name}]}`) //! - Completeness (endorsed count == target_count) //! - Worker existence (every candidate_id present in workers_500k) //! - Status check (active, not_on_client_blacklist) //! - Geo/role match (worker city/state/role matches contract) //! //! Today this is a scaffold — schema check is real (it's cheap); the //! worker-existence / status / geo checks need a catalog lookup and //! land in a follow-up when the catalog query helper is wired into //! this crate. use crate::{Artifact, Report, Validator, ValidationError}; use std::time::Instant; pub struct FillValidator; impl Validator for FillValidator { fn name(&self) -> &'static str { "staffing.fill" } fn validate(&self, artifact: &Artifact) -> Result { let started = Instant::now(); let value = match artifact { Artifact::FillProposal(v) => v, other => return Err(ValidationError::Schema { field: "artifact".into(), reason: format!("FillValidator expects FillProposal, got {other:?}"), }), }; // Schema check — the only real validation shipped in this // scaffold. Catches the common "model emitted prose instead of // JSON" failure mode before the consistency checks even run. let fills = value.get("fills").and_then(|f| f.as_array()).ok_or( ValidationError::Schema { field: "fills".into(), reason: "expected top-level `fills` array".into(), }, )?; for (i, fill) in fills.iter().enumerate() { if fill.get("candidate_id").is_none() { return Err(ValidationError::Schema { field: format!("fills[{i}].candidate_id"), reason: "missing".into(), }); } if fill.get("name").is_none() { return Err(ValidationError::Schema { field: format!("fills[{i}].name"), reason: "missing".into(), }); } } // TODO(phase-43 v2): worker-existence / status / geo checks. // Need a catalog query handle injected into FillValidator's // constructor — out of scope for the scaffold. Ok(Report { findings: vec![], elapsed_ms: started.elapsed().as_millis() as u64, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn wrong_artifact_type_fails_schema() { let r = FillValidator.validate(&Artifact::EmailDraft(serde_json::json!({}))); assert!(matches!(r, Err(ValidationError::Schema { .. }))); } #[test] fn missing_fills_array_fails_schema() { let r = FillValidator.validate(&Artifact::FillProposal(serde_json::json!({}))); assert!(matches!(r, Err(ValidationError::Schema { field, .. }) if field == "fills")); } #[test] fn fill_without_candidate_id_fails() { let r = FillValidator.validate(&Artifact::FillProposal(serde_json::json!({ "fills": [{"name": "Jane"}] }))); assert!(matches!(r, Err(ValidationError::Schema { field, .. }) if field.contains("candidate_id"))); } #[test] fn well_formed_proposal_passes_schema() { let r = FillValidator.validate(&Artifact::FillProposal(serde_json::json!({ "fills": [ {"candidate_id": "W-123", "name": "Jane Doe"}, {"candidate_id": "W-456", "name": "John Smith"} ] }))); assert!(r.is_ok(), "well-formed proposal should pass schema: {:?}", r); } }