Criteria
A criterion is a single rule the model checks against a photo. A policy is mostly a list of criteria plus the categories they roll up into. When a verification runs, the model evaluates every criterion individually, returns a pass/fail for each, and the policy engine combines those results into one outcome.
Anatomy of a criterion
Each criterion is a small object on the policy's criteria array:
{
"id": "not_blocking_entrance",
"label": "Not blocking entrance",
"description": "Only FAIL if the vehicle is physically positioned in a doorway, ramp, or emergency exit opening such that it prevents people from entering or exiting. Being parked near, beside, or in front of a building that has an entrance is NOT a violation — the vehicle must actually obstruct the doorway opening itself.",
"severity": "critical",
"required": true
}| Field | Type | Description |
| ------------- | -------- | ------------------------------------------------------------------------------------------ |
| id | string | Stable identifier. This is exactly what appears in violation_reasons when it fails. |
| label | string | Short human-readable name, shown in dashboards and reports. |
| description | string | The actual rule. The model reads this verbatim — write it like a prompt fragment. |
| severity | string | critical, warning, or info. Drives which category the result resolves to. |
| required | boolean | When true, this criterion contributes to category resolution. Optional ones are advisory. |
The description is fed to the model as-is, so be concrete: name the
visual features that PASS, name the ones that FAIL, and disambiguate
edge cases explicitly. Vague rules ("looks safe") produce vague,
inconsistent outputs. The hardened production policies spell out
exactly what does and does not count as a violation.
Severity
Severity controls how much weight a failed criterion carries when the engine picks a category. There are three levels:
| Severity | Meaning |
| ---------- | ------------------------------------------------------------------------------------------------ |
| critical | A hard rule. A failure here typically forces the result into a non-compliant bucket (unsafe). |
| warning | A soft rule. Failures suggest a retake but don't necessarily block the action (improvable). |
| info | Advisory only. Recorded for analytics; does not by itself change the compliance outcome. |
Think of severity as the answer to "if only this criterion fails, how bad is it?" Critical failures block; warnings nudge; info just notes.
Required vs optional
The required flag controls whether a criterion participates in
category resolution at all:
required: true— the criterion's pass/fail counts toward the final category. Most meaningful rules are required.required: false— the criterion is evaluated and can appear inviolation_reasonsfor analytics, but on its own it won't flip the result. Useful for "nice to know" signals you want tracked without letting them block the workflow.
severity and required work together. A critical + required
criterion is the strongest possible rule: failing it almost always
produces a non-compliant outcome. A warning + required criterion
contributes a softer signal. An info criterion is recorded but never
decisive.
A real example
The hardened scooter/e-bike parking policy uses a mix of severities and required flags so that genuine violations block a ride while photo-angle quirks don't cause false positives:
[
{ "id": "vehicle_visible", "severity": "critical", "required": true },
{ "id": "not_blocking_entrance", "severity": "critical", "required": true },
{ "id": "not_in_roadway", "severity": "critical", "required": true },
{ "id": "not_blocking_sidewalk", "severity": "warning", "required": false },
{ "id": "vehicle_stable", "severity": "warning", "required": false },
{ "id": "image_clear", "severity": "warning", "required": true }
]The three critical + required rules (a vehicle is present, it isn't
blocking a doorway, it isn't in traffic) are the ones that can fail a
ride. The sidewalk and stability checks are warnings that flag a problem
without hard-blocking, and image_clear is a required warning that
catches unusable photos.
How criteria roll up to a category
Once the model has a pass/fail for every criterion, the policy engine resolves a single category. The logic is, roughly:
- Any
critical+requiredfailure → the photo lands in the hard-fail bucket (unsafe, or the policy's domain equivalent likebad_parking). - Only
warningfailures, no critical ones → the photo lands in a soft-fail bucket (improvable). - Not enough signal to judge (image too dark, blurry, or cropped to
decide) → the "insufficient" bucket (
lacks_info/poor_photo). - Every required criterion passes → the compliant bucket
(
compliant/good_parking).
The resolved category's isCompliant flag becomes the top-level
is_compliant on the response, and every failed criterion id is
collected into violation_reasons:
{
"is_compliant": false,
"category": "unsafe",
"violation_reasons": ["blocking_sidewalk", "kickstand_up"]
}The full resolution logic lives in
lib/verify-ai/policy-engine.ts.
Criteria and violation_reasons
The relationship is direct: violation_reasons is the list of failed
criterion ids. Because the IDs are stable, you can build durable logic
and analytics on top of them:
// Branch on a specific failure
if (result.violation_reasons.includes("not_blocking_entrance")) {
showEntranceReminder();
}
// Tally failure modes over time for ops dashboards
for (const reason of result.violation_reasons) {
counts[reason] = (counts[reason] ?? 0) + 1;
}When a verification is compliant, violation_reasons is an empty array.
What's next
- Concepts: Categories — the outcome
buckets that criteria roll up into, and what
is_compliantresolves to. - Concepts: Policies — how criteria, categories, retries, and UI copy combine into a policy.
- API reference: Verify — where
violation_reasonsandcategoryshow up on the response.