Validator veto is the pre-resolver stage that turns validator-backed recognizer failures into typed audit metadata. It replaces the old regex self-drop path: recognizers now emit shape matches, and the core registry decides whether a validator-backed candidate survives to conflict resolution.
The stage runs inside RecognizerRegistry::detect_all_resolved after locale and
minimum-score filtering and before resolver::resolve_candidates. Its input is
the candidate list, the registry, and the normalized text used for matching.
For each candidate:
- Look up
candidate.recognizer_idin the registry's recognizer-id index. - Call
Recognizer::validator_kind(). - If the recognizer has no validator, keep the candidate. No audit row is
emitted for this
NotApplicablepath. - If a validator exists, re-slice the matched bytes from normalized input and
call
ValidatorKind::validate. ValidatorOutcome::Pass { canonical_form }keeps the candidate and fillscandidate.canonical_formonly when it was absent.ValidatorOutcome::Fail { reason }removes the candidate before conflict resolution and returnsVetoedCandidate { candidate, reason }for audit emission.
resolver::resolve_candidates is unchanged. Existing
ConflictTier::Validator still means the same-class containment tie-breaker.
ConflictTier::ValidatorVeto is only used for this pre-resolver drop.
The pipeline logs one loser-only RedactionEntry per vetoed candidate:
RedactionEntry {
conflict_loser: true,
decided_by: ConflictTier::ValidatorVeto,
validator_fail_reason: Some(reason),
..
}No token is emitted, no manifest entry is created, and restore round-trip semantics do not change. The row is metadata-only: source, class, action, document kind, conflict tier, session id, and typed failure reason. Raw matched bytes never enter the audit entry.
ValidatorKind, ValidatorOutcome, and ValidatorFailReason live in
gaze-types because both recognizers and the core pipeline consume them.
gaze-recognizers re-exports ValidatorKind and Region for source
compatibility.
ValidatorFailReason is a closed typed image of current validators:
EmailRfcRejectedPhoneE164RejectedPhoneNationalRegionMismatchLuhnFailedIbanMod97FailedIpv4ParseFailedIpv6ParseFailedEthEip55ChecksumFailed
Phone reasons are always present in the type. They are emitted only when the
phone-parser feature makes the corresponding validators available.
- Axis 1, reliability: invalid validator-backed candidates still fail closed before token emission.
- Axis 2, reversibility: vetoed candidates never touch the manifest or token session, so restore behavior is unchanged.
- Axis 4, auditability: previously silent drops now produce typed loser-only audit rows.
This stage intentionally increases audit volume. Any invalid validator-backed
shape that was previously dropped inside RegexDetector now emits one
validator_veto row. Adopters with high invalid-candidate rates should expect
redaction logs to grow in proportion to those rejects.
- Safety nets remain observer-only and post-clean. They do not participate in validator veto.
- The resolver signature and conflict-resolution order are unchanged.
- Raw document shape, clean document shape, manifest token format, and restore behavior are unchanged.