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:

json
{
  "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. |

Write descriptions like prompts

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 in violation_reasons for 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:

json
[
  { "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:

  1. Any critical + required failure → the photo lands in the hard-fail bucket (unsafe, or the policy's domain equivalent like bad_parking).
  2. Only warning failures, no critical ones → the photo lands in a soft-fail bucket (improvable).
  3. Not enough signal to judge (image too dark, blurry, or cropped to decide) → the "insufficient" bucket (lacks_info / poor_photo).
  4. 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:

json
{
  "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:

js
// 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

Get in Touch

Questions about pricing, integrations, or custom deployments? We'd love to hear from you.