Validators (validators.*)

One of the poq.toml spec sections.

Who does the reviewing and under what terms: default validator count and pay, validator classes (human or AI), per-item routing and escalation, claim duration, qualification gates, and task-page actions. Read together, these knobs form the project's Assurance Policy.

Assurance Policy

The Assurance Policy is everything in the spec that determines how much you can trust a verified datapoint. It is not a separate table — it is the existing validators.* and rubric knobs read as one concept:

What it controlsKnobWhere
Who reviewsclasses, priority[[validators.classes]]
How many, and on which itemsnum_validators, routes, escalation[validators], [[validators.routes]]
Who may claimqualification gates, claim TTL[[validators.qualification]], [validators.claims]
How agreement is judgedclass consensus_weight, dimension consensus_strength_required + consensus_weight[[validators.classes]], Dimension-level verdict

Run-time quality controls (batching, checkpoints, spot audits, calibration) are part of the same policy conceptually but are not configurable in poq.toml yet.

Validators are accountable by identity: they are named and qualified before they review, so the audit trail and their reputation are on the line.

validators

Default validator count and pay. Declare [[validators.classes]] and [[validators.routes]] separately when you need personas or item-specific routing.


[validators]
num_validators = 3
reward_usd = "1.00"
optionacceptsrequireddescriptionexample
num_validatorsintyesDefault validators per item when no route matches.3
reward_usdstringyesDefault reward per completed review (quoted USD decimal string; compiled to cents)."1.00"

validators.classes

Under [validators.*], classes are validator personas (pay, consensus weight, and optionally an LLM agent) — not rubric answer types.

Each class is filled either by humans claiming slots in the portal (type = "human", the default when type is omitted) or by an AI agent (type = "ai") backed by a synthetic system user and the server AI worker. Routing, composition, and escalation treat both kinds the same — you can compose multiple AI classes on the default route and escalate to a human senior reviewer when consensus is low.

Class fields

optionacceptsrequireddescriptionexample
idstringyesClass id used in routes."senior"
labelstringnoDisplay name in admin UI."Senior auditor"
typehuman or aino (default human)Who fills slots for this class. See AI agent classes."ai"
modelstringwhen type = "ai"OpenRouter model slug — must be one of Allowed model values."anthropic/claude-sonnet-4.5"
promptstringwhen type = "ai"System prompt sent to the model (max 64 KiB); use '''…''' literals for verbatim text.see AI example below
reward_usdstringyesPer-review reward for this class."10.00"
priorityintyesTie-break priority (higher = stronger).10
consensus_weightnumberno (default 1.0)Per-class vote weight in weighted consensus.1.0

Human example:

[[validators.classes]]
id         = "senior"
label      = "Senior reviewer"
reward_usd = "20.00"
priority   = 10

AI agent classes

Set type = "ai" and declare model plus prompt to fill a class with an LLM instead of human validators. The worker polls unclaimed validations for that class, packages the same evidence and rubric a human would see, calls the model, and submits via the standard validation path (audit and consensus unchanged).

[[validators.classes]]
id         = "opus-reviewer"
label      = "Opus 4.7 reviewer"
type       = "ai"
model      = "anthropic/claude-opus-4.7"
prompt     = '''
You are an expert reviewer. Score each rubric dimension using only the discrete
anchors provided. Ground every judgment in the evidence; if evidence is
insufficient, pick the midpoint and say so in the rationale.
'''
priority   = 30
reward_usd = "0.00"

Constraints

  • model must be an OpenRouter slug from the allowlist below. Compile-time validation rejects unknown slugs and echoes this list in the error.
  • prompt is required for type = "ai" and is sent verbatim as the system message (max 64 KiB).
  • type = "human" (or omitted type) must not set model or prompt — misplaced AI-only fields are rejected.
  • AI classes have no wallet. reward_usd defaults to "0.00" when omitted.
  • consensus_weight defaults to 1.0 unless you explicitly weight AI votes differently.
  • Rubric scales must use integer-only anchors when any AI class is declared (see The scale block).

Allowed model values

Each slug is an OpenRouter model id. All entries support multimodal evidence (images in messages) and strict JSON-schema response_format for rubric vectors.

model slugContext (tokens)Notes
anthropic/claude-haiku-4.5200,000Lowest-latency Anthropic option; good for high-volume first-pass classes.
anthropic/claude-opus-4.6200,000Prior Opus generation; kept for reproducibility of votes pinned to this version.
anthropic/claude-opus-4.7200,000Latest Opus; highest-quality reasoning; high-stakes or final-step reviewers.
anthropic/claude-sonnet-4.5200,000Default for most AI reviewer classes — strong vision, fast, cheaper than Opus.
google/gemini-2.5-flash1,000,000Fast and cheap with full multimodal support.
google/gemini-2.5-pro1,000,000Long context, strong vision; best when evidence payloads are large.
x-ai/grok-4.31,000,000xAI flagship; vision + structured outputs; vendor diversity.
meta-llama/llama-4-maverick1,048,576Open-weight multimodal flagship; useful for diversity in multi-agent compositions.
mistralai/mistral-medium-3.1131,072European-hosted multimodal option.
qwen/qwen3-vl-235b-a22b-instruct131,072Open-weight multimodal (Alibaba); broad provider coverage.
openai/gpt-4o128,000Strong multimodal generalist; diversity in multi-agent compositions.
openai/gpt-4o-mini128,000Cheap, fast multimodal; first-pass or budget-bound classes.

Example route mixing AI panel + human escalation:

[[validators.routes]]
total = 3
[[validators.routes.composition]]
class = "opus-reviewer"
count = 1
[[validators.routes.composition]]
class = "sonnet-reviewer"
count = 1
[[validators.routes.composition]]
class = "maverick-reviewer"
count = 1

[[validators.routes.escalation]]
add = 1
[[validators.routes.escalation.composition]]
class = "senior"
count = 1

Fixture: server/internal/projectspec/testdata/validators_ai.poq.toml. Design notes: issue #633; implementation server/internal/aivalidator.

validators.routes

Under [validators.*], routes assign validator counts and class slots to matching items — not HTTP or API routes.

Route fields

Use match to select items by [ingestion.fields] column names. A key may be a scalar (exact match) or an array (any listed value). Omit match on exactly one catch-all route (or use match = {}).


[[validators.routes]]
match = { proposed_severity = ["high", "critical"], image_type = "skin_lesion" }
total = 5
[[validators.routes.composition]]
class = "senior"
count = 2

[[validators.routes]]
total = 3   # catch-all — no match

[[validators.routes.escalation]]
add = 2

Route match keys must match a [ingestion.fields] key. Escalation steps take no match — see below.

optionacceptsrequireddescriptionexample
matchtableno (omit on exactly one default route)Match review items: [ingestion.fields] key → scalar or array of allowed values.match = { difficulty = "hard" }
totalintyesValidators per matching item.5
compositionarray of tablesyesClass slots: { class, count }. Use class = "*" for any class.{ class = "senior", count = 2 }
escalationarray of tablesnoOrdered ladder of extra-validator steps; each fires while the datapoint is not yet verified. No match key.add = 2, composition = [...]

validators.qualification

Validators fill out [[validators.qualification]] before claiming items.


[[validators.qualification]]
key = "audit_firm"
label = "Audit firm"
type = "text"
required = true
optionacceptsrequireddescriptionexample
keystringyesStored profile key (snake_case, unique)."audit_firm"
labelstringyesForm question text."Audit firm"
typeenumno (default text)Input type — only text today."text"
requiredboolno (default false)Must be filled before claim when true.true

validators.claims

Optional claim duration — how long a validator has to finish one item after claiming it. Default 5 minutes.


[validators.claims]
duration_minutes = 45
optionacceptsrequireddescriptionexample
duration_minutesintno (default 5)Minutes before an unfinished claim expires. Max 24 hours.45

validators.actions

Optional validator task-page actions. Today: conflict-of-interest self-decline.


[validators.actions]
allow_conflict_of_interest_self_decline = true
optionacceptsrequireddescriptionexample
allow_conflict_of_interest_self_declineboolnoWhen true, validators can decline a claimed item due to conflict of interest.true
conflict_of_interest_decline_button_labelstringnoButton label override."Decline due to conflict"
conflict_of_interest_decline_confirm_titlestringnoConfirmation dialog title override."Decline this validation?"
conflict_of_interest_decline_confirm_bodystringnoConfirmation dialog body override."Use this when you have a conflict of interest."