Skip to content

Candidates & Ambiguity

This content is for v0.5.0. Switch to the latest version for up-to-date documentation.

One input can legitimately mean two different things. Paxman does not guess — it shows you the disagreement. This page explains what a candidate is, how AMBIGUOUS differs from other statuses, and what to do about it.


A candidate is one validated answer: a canonical string plus the evidence that produced it.

candidate.value # e.g. "2026-01-02"
candidate.recognition_rule # which grammar spotted it
candidate.validation_rule # which rule (spec section) validated it
candidate.span # where in the input it sat
candidate.provenance # which authority vouches for it

Candidates are deduplicated by (value, recognition_rule, validation_rule). If two rules happen to converge on the same string, they collapse to one distinct value — that is agreement, not ambiguity (see Execution Result).


AMBIGUOUS means: one mention, two or more distinct canonical values, each validated by a different specification.

Concrete examples:

InputCapabilityWhat happensStatus
01/02/2026DateUS reads 2026-01-02, European reads 2026-02-01AMBIGUOUS
metre per secondSI UnitWord form is not a compound — words recognized separately, rules disagree on groupingAMBIGUOUS
2026-01-15DateOnly ISO grammar’s reading validatesSUCCESS

AMBIGUOUS is a domain signal, not a failure. The input is real; the specs genuinely conflict. Contrast with:

  • MISSING — no grammar matched, or a recognized match was suppressed (suppressed_count > 0 tells the two apart — see Execution Result).
  • INVALID — a grammar matched but no spec accepted it (looks like the entity but is malformed). This includes refused ghosts: a well-formed-but-unregistered tag (xx-yyyyy) or a bare abbreviation (EST) is recognized yet validates nowhere, so it is INVALID, not a silent pick.
  • MultipleMentionsError — two separate mentions with different values in one call (see the Segmentation Recipe). That raises an exception rather than returning a status, because it signals you need to split the input first.

Qualification: syntax alone never validates (ADR-0012)

Section titled “Qualification: syntax alone never validates (ADR-0012)”

A well-formedness check is not authority validation. After validation and before counting, a PARSER-strategy candidate survives only when a LOOKUP_TABLE rule validates the same recognition — e.g. Serbo-Croatian → SUCCESS "sh" (the BCP 47 syntax ghost serbo-croatian is disqualified, the English-name mapping stands); a lone ghost such as xx-yyyyy → INVALID (was wrongly SUCCESS on syntax alone); en-x-private without include_private → INVALID (with the flag it stays SUCCESS). Contracts filtering the lookup authority out preserve parser candidates per the vacuity clause.


You have three tools, all through the contract (see Contracts):

If you know which interpretation you want, narrow the rules:

import paxman
from paxman.capabilities import Date
paxman.register_all_shipped()
# Only the US reading
contract = Date.create_contract(pinned_rules=["Derived-US-date-format"])
result = paxman.canonicalize("01/02/2026", contract)
# may become SUCCESS, or INVALID if no pinned rule validates

Use pinned_rules when you want to enforce a single authority. Note that pinned_rules overrides excluded_rules and that year still filters after pinning.

from paxman.capabilities import Date
contract = Date.create_contract(year=2019) # only rules published ≤ 2019

3. Surface the disagreement to the user or log

Section titled “3. Surface the disagreement to the user or log”

Often the right behavior is to surface the candidates, not to suppress them:

import paxman
from paxman.capabilities import Date
from paxman.core.domain import Resolution
paxman.register_all_shipped()
result = paxman.canonicalize("01/02/2026", Date.create_contract())
if result.status == Resolution.AMBIGUOUS:
for c in result.candidates:
p = c.provenance[0]
print(
f" {c.value!r} via {c.validation_rule} ({p.authority}: {p.specification_name}) span={c.span}"
)

Output:

'2026-01-02' via Section X ... (Authority A: Spec A) span=(0, 10)
'2026-02-01' via Section Y ... (Authority B: Spec B) span=(0, 10)

That evidence is the provenance story for each reading (see Provenance) — it is what lets you or your user decide, rather than Paxman deciding silently.

Notebook pattern — keep ambiguous rows for review

Section titled “Notebook pattern — keep ambiguous rows for review”
import paxman
from paxman.capabilities import Date
from paxman.core.domain import Resolution
paxman.register_all_shipped()
contract = Date.create_contract()
rows = ["2026-01-15", "01/02/2026", "not a date"]
for text in rows:
r = paxman.canonicalize(text, contract)
if r.status == Resolution.SUCCESS:
print(f"{text!r:15} → {r.canonicalized_value}")
elif r.status == Resolution.AMBIGUOUS:
vals = sorted({c.value for c in r.candidates})
print(f"{text!r:15} → AMBIGUOUS {vals} (candidates={len(r.candidates)})")
else:
print(f"{text!r:15} → {r.status.value}")

Candidates are like second opinions from different experts looking at the same X-ray. If both experts agree, you have one answer. If they disagree and both are credible, the honest report is these experts disagree, here is why — not a silent pick. AMBIGUOUS is that honest report, and candidates is the list of opinions with their citations so you can decide.

Next: Errors → — what raises an exception instead of returning a status.