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.
validators(required) — Default validator count and pay.validators.classes(optional) — Validator personas (human or AI), pay, and consensus weight.validators.routes(optional) — Route items to validator counts and classes.validators.qualification(optional) — Profile fields required before claim.validators.claims(optional) — Claim TTL before a task expires.validators.actions(optional) — Task-page actions (e.g. conflict-of-interest self-decline).
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 controls | Knob | Where |
|---|---|---|
| Who reviews | classes, priority | [[validators.classes]] |
| How many, and on which items | num_validators, routes, escalation | [validators], [[validators.routes]] |
| Who may claim | qualification gates, claim TTL | [[validators.qualification]], [validators.claims] |
| How agreement is judged | class 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"
| option | accepts | required | description | example |
|---|---|---|---|---|
num_validators | int | yes | Default validators per item when no route matches. | 3 |
reward_usd | string | yes | Default 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
| option | accepts | required | description | example |
|---|---|---|---|---|
id | string | yes | Class id used in routes. | "senior" |
label | string | no | Display name in admin UI. | "Senior auditor" |
type | human or ai | no (default human) | Who fills slots for this class. See AI agent classes. | "ai" |
model | string | when type = "ai" | OpenRouter model slug — must be one of Allowed model values. | "anthropic/claude-sonnet-4.5" |
prompt | string | when type = "ai" | System prompt sent to the model (max 64 KiB); use '''…''' literals for verbatim text. | see AI example below |
reward_usd | string | yes | Per-review reward for this class. | "10.00" |
priority | int | yes | Tie-break priority (higher = stronger). | 10 |
consensus_weight | number | no (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
modelmust be an OpenRouter slug from the allowlist below. Compile-time validation rejects unknown slugs and echoes this list in the error.promptis required fortype = "ai"and is sent verbatim as the system message (max 64 KiB).type = "human"(or omittedtype) must not setmodelorprompt— misplaced AI-only fields are rejected.- AI classes have no wallet.
reward_usddefaults to"0.00"when omitted. consensus_weightdefaults to1.0unless 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 slug | Context (tokens) | Notes |
|---|---|---|
anthropic/claude-haiku-4.5 | 200,000 | Lowest-latency Anthropic option; good for high-volume first-pass classes. |
anthropic/claude-opus-4.6 | 200,000 | Prior Opus generation; kept for reproducibility of votes pinned to this version. |
anthropic/claude-opus-4.7 | 200,000 | Latest Opus; highest-quality reasoning; high-stakes or final-step reviewers. |
anthropic/claude-sonnet-4.5 | 200,000 | Default for most AI reviewer classes — strong vision, fast, cheaper than Opus. |
google/gemini-2.5-flash | 1,000,000 | Fast and cheap with full multimodal support. |
google/gemini-2.5-pro | 1,000,000 | Long context, strong vision; best when evidence payloads are large. |
x-ai/grok-4.3 | 1,000,000 | xAI flagship; vision + structured outputs; vendor diversity. |
meta-llama/llama-4-maverick | 1,048,576 | Open-weight multimodal flagship; useful for diversity in multi-agent compositions. |
mistralai/mistral-medium-3.1 | 131,072 | European-hosted multimodal option. |
qwen/qwen3-vl-235b-a22b-instruct | 131,072 | Open-weight multimodal (Alibaba); broad provider coverage. |
openai/gpt-4o | 128,000 | Strong multimodal generalist; diversity in multi-agent compositions. |
openai/gpt-4o-mini | 128,000 | Cheap, 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.
| option | accepts | required | description | example |
|---|---|---|---|---|
match | table | no (omit on exactly one default route) | Match review items: [ingestion.fields] key → scalar or array of allowed values. | match = { difficulty = "hard" } |
total | int | yes | Validators per matching item. | 5 |
composition | array of tables | yes | Class slots: { class, count }. Use class = "*" for any class. | { class = "senior", count = 2 } |
escalation | array of tables | no | Ordered 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
| option | accepts | required | description | example |
|---|---|---|---|---|
key | string | yes | Stored profile key (snake_case, unique). | "audit_firm" |
label | string | yes | Form question text. | "Audit firm" |
type | enum | no (default text) | Input type — only text today. | "text" |
required | bool | no (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
| option | accepts | required | description | example |
|---|---|---|---|---|
duration_minutes | int | no (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
| option | accepts | required | description | example |
|---|---|---|---|---|
allow_conflict_of_interest_self_decline | bool | no | When true, validators can decline a claimed item due to conflict of interest. | true |
conflict_of_interest_decline_button_label | string | no | Button label override. | "Decline due to conflict" |
conflict_of_interest_decline_confirm_title | string | no | Confirmation dialog title override. | "Decline this validation?" |
conflict_of_interest_decline_confirm_body | string | no | Confirmation dialog body override. | "Use this when you have a conflict of interest." |