Skip to content

Proactive Criterion Evaluation

Automated and specialized checks can only resolve a subset of WCAG success criteria. The rest — captions, use of color, link purpose, consistent navigation, and so on — require judgment. Rather than silently marking those "Supports" (an overclaim) or leaving them permanently "Not Evaluated", agent-a11y can proactively evaluate them: ask, for each unresolved criterion, "does this page satisfy it?" and record an honest verdict.

There are two modes, and they operate identically except for who runs the inference. Both use the same system prompt (llm.CriterionSystemPrompt), the same per-criterion prompt (llm.BuildCriterionPrompt), and the same verdict schema (llm.CriterionVerdict), so results are comparable.

Local (agent) mode API mode
Who judges An external agent (e.g. Claude Code) An LLM via the API
API key Not required Required
Entry point Result.EvaluationBundle() → verdicts JSON → Result.ConformanceWithVerdicts() Result.EvaluateCriteria(ctx, apiEvaluator)
Use when Validating, offline, or already inside an agent loop Unattended/CI runs

The verdict vocabulary

Every verdict is one of: Supports, Partially Supports, Does Not Support, Not Applicable, or Not Evaluated. The last two matter for honesty:

  • Not Applicable — the page has no content of the type the criterion governs (e.g. no audio/video for caption criteria). This is a legitimate, common outcome and is what content-level evidence most often supports.
  • Not Evaluated — the evidence is insufficient. The verdict must then state what evidence is needed (evidenceGap), e.g. "screenshot for color use", "raw DOM for alt text", "multi-page crawl", "interaction with the form".

A verdict never overrides a finding-backed Does Not Support/Partially Supports; it only resolves criteria automation left Not Evaluated. Enabling an LLM does not by itself flip anything — only an actual verdict does.

Local (agent) mode

No API key. The agent that is already running the tool acts as the judge.

res, _ := auditor.AuditPage(ctx, url)

bundle := res.EvaluationBundle()      // shared system prompt + page evidence + per-criterion prompts
// hand `bundle` to the agent; it returns verdicts on the same schema:
//   [{"id":"1.2.1","conformance":"Not Applicable","confidence":0.9,"reasoning":"no media","evidenceGap":""}]
verdicts, _ := llm.ParseVerdicts(agentOutput)

criteria := res.ConformanceWithVerdicts(verdicts)   // baseline + verdicts applied

The bundle is plain JSON, so the export/import can also cross a process boundary (write bundle to a file, agent writes verdicts to a file, import them).

API mode

Needs an LLM API key (via omnillm). Same prompts and schema, run automatically.

client := omnillm.NewChatClient(/* provider, key */)
evaluator := llm.NewAPIEvaluator(client, "claude-...")

verdicts, _ := res.EvaluateCriteria(ctx, evaluator)   // calls the model per criterion
criteria := res.ConformanceWithVerdicts(verdicts)

Because APIEvaluator implements llm.CriterionEvaluator, any alternative backend (a different provider, a cached evaluator, a test stub) can be dropped in without changing callers.

Evidence the judge needs

Content alone (title, text, links, headings) resolves media (→ Not Applicable), link purpose, headings/labels, and language. To resolve the rest, provide richer evidence in PageContext:

  • Screenshot → use of color (1.4.1), non-text contrast (1.4.11)
  • Raw DOM → non-text content / alt quality (1.1.1)
  • Multi-page crawl → consistent navigation/identification/help (3.2.3/3.2.4/3.2.6), multiple ways (2.4.5)
  • Interaction → on-input behavior (3.2.2), form error handling (3.3.1/3.3.3), authentication (3.3.8)

When evidence is missing, the honest verdict is Not Evaluated with the gap named — never a guessed Supports.