cocoindex-io/cocoindex · error · ValueError

resolve_pair returned matched={decision.matched!r}, which is

Error message

resolve_pair returned matched={decision.matched!r}, which is not in candidates={candidates!r}. This is a contract violation (see requirement.md).

What it means

This error is raised when a user-supplied `resolve_pair` callback for entity resolution returns a `PairDecision` whose `matched` value is not one of the candidate entity names (or equals the entity itself). It enforces the library contract documented in requirement.md: the callback may only merge an entity into one of the candidates it was offered. It is an internal contract-validation guard, not a library bug.

Source

Thrown at python/cocoindex/ops/entity_resolution/__init__.py:282

def _chain_walk(dedup: dict[str, str | None], name: str) -> str:
    current = name
    while True:
        target = dedup.get(current)
        if target is None:
            return current
        current = target


def _validate_pair_decision(
    *,
    entity: str,
    candidates: list[str],
    decision: PairDecision,
) -> None:
    if decision.matched is not None and (
        decision.matched not in candidates or decision.matched == entity
    ):
        raise ValueError(
            f"resolve_pair returned matched={decision.matched!r}, "
            f"which is not in candidates={candidates!r}. This is a "
            f"contract violation (see requirement.md)."
        )


def _apply_pair_decision(
    *,
    info: _EntityInfo,
    decision: PairDecision,
    entity_map: dict[str, _EntityInfo],
    dedup: dict[str, str | None],
    existing_policy: ExistingCanonicalPolicy,
) -> _DecisionApplication:
    if decision.matched is None:
        dedup[info.name] = None
        return _DecisionApplication(canonical=info.name)

View on GitHub (pinned to e84aa99b32)

Solutions

  1. In your resolve_pair, only return matched values taken directly from the `candidates` argument (exact string identity), or None for no match.
  2. If matching an entity to a normalized/external form, map it back to the corresponding candidate string before constructing PairDecision.
  3. Never set matched to the entity itself; use matched=None when no candidate matches.

Example fix

// before
def resolve_pair(entity, candidates):
    best = fuzzy_lookup(entity)  # returns an external canonical name
    return PairDecision(matched=best)
// after
def resolve_pair(entity, candidates):
    best = fuzzy_lookup(entity)
    if best is None or best not in candidates or best == entity:
        return PairDecision(matched=None)
    return PairDecision(matched=best)
Defensive patterns

Strategy: validation

Validate before calling

def make_decision(entity, candidates, key):
    m = key(entity, candidates)
    assert m is None or (m in candidates and m != entity), f"matched={m!r} invalid for candidates={candidates!r}"
    return PairDecision(matched=m)

Type guard

def is_valid_matched(m, candidates):
    return m is None or (isinstance(m, str) and m in candidates)

Try / catch

try:
    handle = await coco.use_mount(resolve_component, ...)
except ValueError as e:
    if 'contract violation' in str(e):
        logging.error('resolve_pair returned an invalid matched value: %s', e)
    else:
        raise

Prevention

When it happens

Trigger: A custom `resolve_pair` function returns a `PairDecision` whose `matched` field is set to a string that is not in the `candidates` list passed to it, or sets `matched` equal to the entity being resolved (self-match), instead of `None` for no match.

Common situations: Hand-rolling a resolve_pair that fuzzy-matches against an external dictionary or LLM and returns the matched term verbatim from the dictionary rather than the candidate string; typos in candidate names; case-normalizing candidates but returning the original-cased external value; confusing 'matched' semantics and echoing the entity itself.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of cocoindex-io/cocoindex@e84aa99b32 (2026-09-08). Data as JSON: /api/errors/d008606f43857645. Report an issue: GitHub.