package validator import ( "fmt" "strings" "time" ) // FillValidator is the Go port of Rust's FillValidator. Per // `crates/validator/src/staffing/fill.rs`: // // - 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) // // Contract metadata travels alongside the JSON payload under a // `_context` key: // // {"_context": {"target_count": 2, "city": "Toledo", "state": "OH", // "role": "Welder", "client_id": "CLI-00099"}, "fills": [...]} // // The duplicate-ID guard inside one fill catches the LLM mistake // of repeating the same candidate twice to satisfy a higher // target_count. type FillValidator struct { workers WorkerLookup } // NewFillValidator constructs a FillValidator with the given lookup. // Lookup must be non-nil; pass NewInMemoryWorkerLookup(nil) for // tests that don't exercise existence checks. func NewFillValidator(workers WorkerLookup) *FillValidator { return &FillValidator{workers: workers} } // Name satisfies Validator. Stable string used for audit // trail / receipts. Matches Rust output "staffing.fill" so // cross-runtime audit logs share the same name. func (v *FillValidator) Name() string { return "staffing.fill" } // fillContext is the optional contract metadata extracted from // _context. Each field is independently nil-able (Rust's Option // pattern) — validators only enforce a check when both contract // and roster sides have a value. type fillContext struct { TargetCount *int City *string State *string Role *string ClientID *string } func extractContext(value map[string]any) fillContext { ctx, ok := value["_context"].(map[string]any) if !ok { return fillContext{} } out := fillContext{} if v, ok := ctx["target_count"]; ok { if n, ok := toInt(v); ok { out.TargetCount = &n } } if s, ok := ctx["city"].(string); ok { out.City = &s } if s, ok := ctx["state"].(string); ok { out.State = &s } if s, ok := ctx["role"].(string); ok { out.Role = &s } if s, ok := ctx["client_id"].(string); ok { out.ClientID = &s } return out } // toInt accepts JSON numbers (float64) and integers, returning // the int form when the value is a whole number ≥ 0. func toInt(v any) (int, bool) { switch n := v.(type) { case int: return n, true case int64: return int(n), true case float64: // JSON unmarshals all numbers as float64; whole-number check // is mandatory because target_count=2.5 makes no sense. i := int(n) if float64(i) == n { return i, true } return 0, false } return 0, false } // eqCI is the case-insensitive equality used everywhere validators // compare strings (status, role, city, etc.). Trim+lowercase mirrors // Rust's `.trim().eq_ignore_ascii_case(other.trim())`. func eqCI(a, b string) bool { return strings.EqualFold(strings.TrimSpace(a), strings.TrimSpace(b)) } // Validate implements the Validator interface. Mirrors the Rust // validation order exactly: schema → completeness → cross-roster // per-fill checks. func (v *FillValidator) Validate(artifact Artifact) (Report, error) { started := time.Now() value := artifact.FillProposal if value == nil { return Report{}, &ValidationError{ Kind: ErrSchema, Field: "artifact", Reason: fmt.Sprintf("FillValidator expects FillProposal, got %s", artifact.Kind()), } } // ── Schema check ── fillsRaw, ok := value["fills"].([]any) if !ok { return Report{}, &ValidationError{ Kind: ErrSchema, Field: "fills", Reason: "expected top-level `fills` array", } } for i, fillRaw := range fillsRaw { fill, ok := fillRaw.(map[string]any) if !ok { return Report{}, &ValidationError{ Kind: ErrSchema, Field: fmt.Sprintf("fills[%d]", i), Reason: "expected object", } } if _, ok := fill["candidate_id"]; !ok { return Report{}, &ValidationError{ Kind: ErrSchema, Field: fmt.Sprintf("fills[%d].candidate_id", i), Reason: "missing", } } if _, ok := fill["name"]; !ok { return Report{}, &ValidationError{ Kind: ErrSchema, Field: fmt.Sprintf("fills[%d].name", i), Reason: "missing", } } } ctx := extractContext(value) // ── Completeness ── if ctx.TargetCount != nil && len(fillsRaw) != *ctx.TargetCount { return Report{}, &ValidationError{ Kind: ErrCompleteness, Reason: fmt.Sprintf("endorsed count %d != target_count %d", len(fillsRaw), *ctx.TargetCount), } } // ── Cross-roster checks ── var findings []Finding seenIDs := make(map[string]bool, len(fillsRaw)) for i, fillRaw := range fillsRaw { fill := fillRaw.(map[string]any) // already type-checked in schema pass candidateID, _ := fill["candidate_id"].(string) proposedName, _ := fill["name"].(string) // Duplicate-ID guard inside one fill. if seenIDs[candidateID] { return Report{}, &ValidationError{ Kind: ErrConsistency, Reason: fmt.Sprintf( "duplicate candidate_id %q appears multiple times in fills", candidateID, ), } } seenIDs[candidateID] = true // Worker existence — load-bearing check for the 0→85% pattern. worker, ok := v.workers.Find(candidateID) if !ok { return Report{}, &ValidationError{ Kind: ErrConsistency, Reason: fmt.Sprintf( "fills[%d].candidate_id %q does not exist in worker roster", i, candidateID, ), } } // Status — only "active" workers can be endorsed. if !eqCI(worker.Status, "active") { return Report{}, &ValidationError{ Kind: ErrConsistency, Reason: fmt.Sprintf( "fills[%d] worker %q has status %q, expected \"active\"", i, candidateID, worker.Status, ), } } // Client blacklist. if ctx.ClientID != nil { for _, b := range worker.BlacklistedClients { if eqCI(b, *ctx.ClientID) { return Report{}, &ValidationError{ Kind: ErrPolicy, Reason: fmt.Sprintf( "fills[%d] worker %q blacklisted for client %q", i, candidateID, *ctx.ClientID, ), } } } } // Geo / role match — only when BOTH sides have a value. if ctx.City != nil && worker.City != nil && !eqCI(*ctx.City, *worker.City) { return Report{}, &ValidationError{ Kind: ErrConsistency, Reason: fmt.Sprintf( "fills[%d] worker %q city %q doesn't match contract city %q", i, candidateID, *worker.City, *ctx.City, ), } } if ctx.State != nil && worker.State != nil && !eqCI(*ctx.State, *worker.State) { return Report{}, &ValidationError{ Kind: ErrConsistency, Reason: fmt.Sprintf( "fills[%d] worker %q state %q doesn't match contract state %q", i, candidateID, *worker.State, *ctx.State, ), } } if ctx.Role != nil && worker.Role != nil && !eqCI(*ctx.Role, *worker.Role) { return Report{}, &ValidationError{ Kind: ErrConsistency, Reason: fmt.Sprintf( "fills[%d] worker %q role %q doesn't match contract role %q", i, candidateID, *worker.Role, *ctx.Role, ), } } // Name-mismatch is a warning, not an error — recruiters // sometimes send updated names through the proposal layer // before the roster catches up. if proposedName != "" && !eqCI(proposedName, worker.Name) { findings = append(findings, Finding{ Field: fmt.Sprintf("fills[%d].name", i), Severity: SeverityWarning, Message: fmt.Sprintf( "proposed name %q differs from roster name %q for %q", proposedName, worker.Name, candidateID, ), }) } } return Report{ Findings: findings, ElapsedMs: elapsed(started), }, nil }