lakehouse/auditor 16 blocking issues: cloud: claim not backed — "Verified end-to-end:"
The Phase 43 scaffolds (FillValidator, EmailValidator) shipped with
TODO(phase-43 v2) markers for the actual cross-roster checks. This is
those checks landing.
The PRD calls for "the 0→85% pattern reproduces on real staffing
tasks — the iteration loop with validation in place is what made
small models successful." Worker-existence is the load-bearing check:
when the executor emits {candidate_id: "W-FAKE", name: "Imaginary"},
schema-only validation passes, and only roster lookup catches it.
Architecture:
- New `WorkerLookup` trait + `WorkerRecord` struct in lib.rs. Sync by
design — validators hold an in-memory snapshot, no per-call I/O on
the validation hot path. Production wraps a parquet snapshot;
tests use `InMemoryWorkerLookup`.
- Validators take `Arc<dyn WorkerLookup>` at construction so the
same shape covers prod + tests + future devops scaffolds.
- Contract metadata travels under JSON `_context` key alongside the
validated payload (target_count, city, state, role, client_id for
fills; candidate_id for emails). Keeps the Validator trait
signature stable and lets the executor serialize context inline.
FillValidator (11 tests, was 4):
- Schema (existing)
- Completeness — endorsed count == target_count
- Worker existence — phantom candidate_id fails Consistency
- Status — non-active worker fails Consistency
- Geo/role match — city/state/role mismatch with contract fails
Consistency
- Client blacklist — fails Policy
- Duplicate candidate_id within one fill — fails Consistency
- Name mismatch — Warning (not Error) since recruiters sometimes
send roster updates through the proposal layer
EmailValidator (11 tests, was 4):
- Schema + length (existing)
- SSN scan (NNN-NN-NNNN) — fails Policy
- Salary disclosure (keyword + $-amount within ~40 chars) — fails
Policy. Std-only scan, no regex dep added.
- Worker name consistency — when _context.candidate_id resolves,
body must contain the worker's first name (Warning if missing)
- Phantom candidate_id in _context — fails Consistency
- Phone NNN-NNN-NNNN does NOT trip the SSN detector (verified by
test); the SSN scanner explicitly rejects sequences embedded in
longer digit runs
Pre-existing issue (NOT from this change, NOT fixed here):
crates/vectord/src/pathway_memory.rs:927 has a stale PathwayTrace
struct initializer that fails `cargo check --tests` with E0063 on
6 missing fields. `cargo check --workspace` (production) is green;
only the vectord test target is broken. Tracked for a separate fix.
Verification:
cargo test -p validator 31 pass / 0 fail (was 13)
cargo check --workspace green
Next: wire `Arc<dyn WorkerLookup>` into the gateway execution loop
(generate → validate → observer-correct → retry, bounded by
max_iterations=3 per Phase 43 PRD). Production lookup impl loads
from a workers parquet snapshot — Track A gap-fix B's `_safe` view
is the right source once decided, raw workers_500k otherwise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
384 lines
15 KiB
Rust
384 lines
15 KiB
Rust
//! Fill-proposal validator (Phase 43 v2 — real consistency checks).
|
|
//!
|
|
//! PRD checks:
|
|
//! - Schema compliance (propose_done shape: `{fills: [{candidate_id, name}]}`)
|
|
//! - Completeness (endorsed count == target_count)
|
|
//! - Worker existence (every candidate_id present in workers roster)
|
|
//! - Status check (worker.status == "active")
|
|
//! - Client blacklist (worker NOT in client.blacklisted_clients)
|
|
//! - Geo/role match (worker city/state/role matches contract)
|
|
//!
|
|
//! The contract metadata (target_count, city, state, role, client_id)
|
|
//! travels alongside the JSON payload under a `_context` key:
|
|
//! `{"_context": {"target_count": 2, "city": "Toledo", "state": "OH",
|
|
//! "role": "Welder", "client_id": "CLI-00099"}, "fills": [...]}`.
|
|
//! This keeps the Validator trait signature stable while letting the
|
|
//! validator cross-check fills against contract truth.
|
|
//!
|
|
//! Worker-existence + status + geo + blacklist all share a single
|
|
//! lookup trait (`WorkerLookup`) so the validator stays decoupled
|
|
//! from queryd / parquet / catalogd transport details.
|
|
|
|
use crate::{
|
|
Artifact, Report, Validator, ValidationError, WorkerLookup, WorkerRecord,
|
|
};
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
|
|
pub struct FillValidator {
|
|
workers: Arc<dyn WorkerLookup>,
|
|
}
|
|
|
|
impl FillValidator {
|
|
pub fn new(workers: Arc<dyn WorkerLookup>) -> Self {
|
|
Self { workers }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct FillContext {
|
|
target_count: Option<usize>,
|
|
city: Option<String>,
|
|
state: Option<String>,
|
|
role: Option<String>,
|
|
client_id: Option<String>,
|
|
}
|
|
|
|
fn extract_context(value: &serde_json::Value) -> FillContext {
|
|
let ctx_obj = value.get("_context").and_then(|c| c.as_object());
|
|
let ctx = match ctx_obj {
|
|
Some(o) => o,
|
|
None => return FillContext::default(),
|
|
};
|
|
FillContext {
|
|
target_count: ctx.get("target_count").and_then(|v| v.as_u64()).map(|n| n as usize),
|
|
city: ctx.get("city").and_then(|v| v.as_str()).map(String::from),
|
|
state: ctx.get("state").and_then(|v| v.as_str()).map(String::from),
|
|
role: ctx.get("role").and_then(|v| v.as_str()).map(String::from),
|
|
client_id: ctx.get("client_id").and_then(|v| v.as_str()).map(String::from),
|
|
}
|
|
}
|
|
|
|
fn eq_ci(a: &str, b: &str) -> bool {
|
|
a.trim().eq_ignore_ascii_case(b.trim())
|
|
}
|
|
|
|
impl Validator for FillValidator {
|
|
fn name(&self) -> &'static str { "staffing.fill" }
|
|
|
|
fn validate(&self, artifact: &Artifact) -> Result<Report, ValidationError> {
|
|
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 ──
|
|
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(),
|
|
});
|
|
}
|
|
}
|
|
|
|
let ctx = extract_context(value);
|
|
|
|
// ── Completeness: count match ──
|
|
if let Some(target) = ctx.target_count {
|
|
if fills.len() != target {
|
|
return Err(ValidationError::Completeness {
|
|
reason: format!(
|
|
"endorsed count {} != target_count {target}",
|
|
fills.len()
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Cross-roster checks ──
|
|
let mut findings: Vec<crate::Finding> = vec![];
|
|
let mut seen_ids = std::collections::HashSet::new();
|
|
for (i, fill) in fills.iter().enumerate() {
|
|
let candidate_id = fill.get("candidate_id").and_then(|v| v.as_str()).unwrap_or("");
|
|
let proposed_name = fill.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
// Duplicate-ID guard inside one fill.
|
|
if !seen_ids.insert(candidate_id.to_string()) {
|
|
return Err(ValidationError::Consistency {
|
|
reason: format!(
|
|
"duplicate candidate_id {candidate_id:?} appears multiple times in fills"
|
|
),
|
|
});
|
|
}
|
|
|
|
// Worker existence — the gate that catches phantom IDs the
|
|
// model fabricates. This is the load-bearing check for
|
|
// the 0→85% pattern.
|
|
let worker: WorkerRecord = match self.workers.find(candidate_id) {
|
|
Some(w) => w,
|
|
None => return Err(ValidationError::Consistency {
|
|
reason: format!(
|
|
"fills[{i}].candidate_id {candidate_id:?} does not exist in worker roster"
|
|
),
|
|
}),
|
|
};
|
|
|
|
// Status — only "active" workers can be endorsed.
|
|
if !eq_ci(&worker.status, "active") {
|
|
return Err(ValidationError::Consistency {
|
|
reason: format!(
|
|
"fills[{i}] worker {candidate_id:?} has status {:?}, expected \"active\"",
|
|
worker.status
|
|
),
|
|
});
|
|
}
|
|
|
|
// Client blacklist.
|
|
if let Some(client) = ctx.client_id.as_deref() {
|
|
if worker.blacklisted_clients.iter().any(|b| eq_ci(b, client)) {
|
|
return Err(ValidationError::Policy {
|
|
reason: format!(
|
|
"fills[{i}] worker {candidate_id:?} blacklisted for client {client:?}"
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Geo / role match — warn-level when missing context, hard
|
|
// fail on mismatch with explicit contract values.
|
|
if let (Some(want_city), Some(have_city)) = (ctx.city.as_deref(), worker.city.as_deref()) {
|
|
if !eq_ci(want_city, have_city) {
|
|
return Err(ValidationError::Consistency {
|
|
reason: format!(
|
|
"fills[{i}] worker {candidate_id:?} city {have_city:?} doesn't match contract city {want_city:?}"
|
|
),
|
|
});
|
|
}
|
|
}
|
|
if let (Some(want_state), Some(have_state)) = (ctx.state.as_deref(), worker.state.as_deref()) {
|
|
if !eq_ci(want_state, have_state) {
|
|
return Err(ValidationError::Consistency {
|
|
reason: format!(
|
|
"fills[{i}] worker {candidate_id:?} state {have_state:?} doesn't match contract state {want_state:?}"
|
|
),
|
|
});
|
|
}
|
|
}
|
|
if let (Some(want_role), Some(have_role)) = (ctx.role.as_deref(), worker.role.as_deref()) {
|
|
if !eq_ci(want_role, have_role) {
|
|
return Err(ValidationError::Consistency {
|
|
reason: format!(
|
|
"fills[{i}] worker {candidate_id:?} role {have_role:?} doesn't match contract role {want_role:?}"
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Name-mismatch is a warning, not an error — recruiters
|
|
// sometimes send updated names through the proposal layer
|
|
// before the roster is updated.
|
|
if !proposed_name.is_empty() && !eq_ci(proposed_name, &worker.name) {
|
|
findings.push(crate::Finding {
|
|
field: format!("fills[{i}].name"),
|
|
severity: crate::Severity::Warning,
|
|
message: format!(
|
|
"proposed name {proposed_name:?} differs from roster name {:?} for {candidate_id:?}",
|
|
worker.name
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(Report {
|
|
findings,
|
|
elapsed_ms: started.elapsed().as_millis() as u64,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::InMemoryWorkerLookup;
|
|
use serde_json::json;
|
|
|
|
fn lookup(records: Vec<WorkerRecord>) -> Arc<dyn WorkerLookup> {
|
|
Arc::new(InMemoryWorkerLookup::from_records(records))
|
|
}
|
|
|
|
fn worker(id: &str, name: &str, status: &str, city: &str, state: &str, role: &str) -> WorkerRecord {
|
|
WorkerRecord {
|
|
candidate_id: id.into(),
|
|
name: name.into(),
|
|
status: status.into(),
|
|
city: Some(city.into()),
|
|
state: Some(state.into()),
|
|
role: Some(role.into()),
|
|
blacklisted_clients: vec![],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_artifact_type_fails_schema() {
|
|
let v = FillValidator::new(lookup(vec![]));
|
|
let r = v.validate(&Artifact::EmailDraft(json!({})));
|
|
assert!(matches!(r, Err(ValidationError::Schema { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn missing_fills_array_fails_schema() {
|
|
let v = FillValidator::new(lookup(vec![]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({})));
|
|
assert!(matches!(r, Err(ValidationError::Schema { field, .. }) if field == "fills"));
|
|
}
|
|
|
|
#[test]
|
|
fn fill_without_candidate_id_fails() {
|
|
let v = FillValidator::new(lookup(vec![]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({"fills": [{"name": "Jane"}]})));
|
|
assert!(matches!(r, Err(ValidationError::Schema { field, .. }) if field.contains("candidate_id")));
|
|
}
|
|
|
|
#[test]
|
|
fn well_formed_proposal_with_real_workers_passes() {
|
|
let v = FillValidator::new(lookup(vec![
|
|
worker("W-1", "Jane Doe", "active", "Toledo", "OH", "Welder"),
|
|
worker("W-2", "John Smith", "active", "Toledo", "OH", "Welder"),
|
|
]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({
|
|
"_context": {"target_count": 2, "city": "Toledo", "state": "OH", "role": "Welder"},
|
|
"fills": [
|
|
{"candidate_id": "W-1", "name": "Jane Doe"},
|
|
{"candidate_id": "W-2", "name": "John Smith"}
|
|
]
|
|
})));
|
|
assert!(r.is_ok(), "expected pass, got {:?}", r);
|
|
}
|
|
|
|
#[test]
|
|
fn phantom_candidate_id_fails_consistency() {
|
|
let v = FillValidator::new(lookup(vec![worker("W-1", "Jane", "active", "Toledo", "OH", "Welder")]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({
|
|
"_context": {"target_count": 1, "city": "Toledo", "state": "OH", "role": "Welder"},
|
|
"fills": [{"candidate_id": "W-FAKE-99999", "name": "Imaginary"}]
|
|
})));
|
|
match r {
|
|
Err(ValidationError::Consistency { reason }) => assert!(reason.contains("does not exist")),
|
|
other => panic!("expected Consistency error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn inactive_worker_fails_consistency() {
|
|
let v = FillValidator::new(lookup(vec![worker("W-1", "Jane", "inactive", "Toledo", "OH", "Welder")]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({
|
|
"_context": {"target_count": 1},
|
|
"fills": [{"candidate_id": "W-1", "name": "Jane"}]
|
|
})));
|
|
match r {
|
|
Err(ValidationError::Consistency { reason }) => assert!(reason.contains("inactive")),
|
|
other => panic!("expected Consistency error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_city_fails_consistency() {
|
|
let v = FillValidator::new(lookup(vec![worker("W-1", "Jane", "active", "Cincinnati", "OH", "Welder")]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({
|
|
"_context": {"target_count": 1, "city": "Toledo", "state": "OH", "role": "Welder"},
|
|
"fills": [{"candidate_id": "W-1", "name": "Jane"}]
|
|
})));
|
|
match r {
|
|
Err(ValidationError::Consistency { reason }) => assert!(reason.to_lowercase().contains("city")),
|
|
other => panic!("expected Consistency error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_role_fails_consistency() {
|
|
let v = FillValidator::new(lookup(vec![worker("W-1", "Jane", "active", "Toledo", "OH", "Driver")]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({
|
|
"_context": {"target_count": 1, "city": "Toledo", "state": "OH", "role": "Welder"},
|
|
"fills": [{"candidate_id": "W-1", "name": "Jane"}]
|
|
})));
|
|
match r {
|
|
Err(ValidationError::Consistency { reason }) => assert!(reason.to_lowercase().contains("role")),
|
|
other => panic!("expected Consistency error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn count_mismatch_fails_completeness() {
|
|
let v = FillValidator::new(lookup(vec![
|
|
worker("W-1", "Jane", "active", "Toledo", "OH", "Welder"),
|
|
]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({
|
|
"_context": {"target_count": 2, "city": "Toledo", "state": "OH", "role": "Welder"},
|
|
"fills": [{"candidate_id": "W-1", "name": "Jane"}]
|
|
})));
|
|
assert!(matches!(r, Err(ValidationError::Completeness { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_candidate_id_fails_consistency() {
|
|
let v = FillValidator::new(lookup(vec![
|
|
worker("W-1", "Jane", "active", "Toledo", "OH", "Welder"),
|
|
]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({
|
|
"_context": {"target_count": 2, "city": "Toledo", "state": "OH", "role": "Welder"},
|
|
"fills": [
|
|
{"candidate_id": "W-1", "name": "Jane"},
|
|
{"candidate_id": "W-1", "name": "Jane"}
|
|
]
|
|
})));
|
|
match r {
|
|
Err(ValidationError::Consistency { reason }) => assert!(reason.contains("duplicate")),
|
|
other => panic!("expected Consistency error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn blacklisted_worker_fails_policy() {
|
|
let mut w = worker("W-1", "Jane", "active", "Toledo", "OH", "Welder");
|
|
w.blacklisted_clients = vec!["CLI-00099".into()];
|
|
let v = FillValidator::new(lookup(vec![w]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({
|
|
"_context": {"target_count": 1, "city": "Toledo", "state": "OH", "role": "Welder", "client_id": "CLI-00099"},
|
|
"fills": [{"candidate_id": "W-1", "name": "Jane"}]
|
|
})));
|
|
assert!(matches!(r, Err(ValidationError::Policy { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn name_mismatch_is_warning_not_error() {
|
|
let v = FillValidator::new(lookup(vec![
|
|
worker("W-1", "Jane Doe", "active", "Toledo", "OH", "Welder"),
|
|
]));
|
|
let r = v.validate(&Artifact::FillProposal(json!({
|
|
"_context": {"target_count": 1, "city": "Toledo", "state": "OH", "role": "Welder"},
|
|
"fills": [{"candidate_id": "W-1", "name": "Janet Doe"}]
|
|
})));
|
|
let report = r.expect("name mismatch should be warning, not error");
|
|
assert_eq!(report.findings.len(), 1);
|
|
assert_eq!(report.findings[0].severity, crate::Severity::Warning);
|
|
assert!(report.findings[0].message.contains("differs from roster"));
|
|
}
|
|
}
|