ADR 0037: One exception type for a dimension mismatch

Status

Accepted.

Relates to ADR 0012,

which stands. Relates to

ADR 0017, which settles that

EmbeddingProvider declares its own dimension — that page stands; this

ADR is about what happens when two declared dimensions disagree, not about

the port itself.

Context

Two composition entry points refuse an embedding provider and a vector store

whose dimensions disagree, and until now they refused it with two different

exception types. Retriever.__init__ raised DimensionMismatchError.

build_graph's internal wiring check raised a bare ValueError, for the same

condition. DimensionMismatchError extends RedstringError, which extends

Exception directly — it is not a ValueError subclass, so an except

written around either type does not catch the other.

Nothing asserted that the two entry points agreed, which is how the

divergence went unnoticed: each had its own test, and each test only checked

its own entry point's behaviour against itself. A caller wiring both build_graph

and Retriever behind one try/except around the configuration step

catches one mismatch and crashes on the other. The two checks are not

identical in shape — build_graph takes its provider and store as optional

and has a second failure mode, one collaborator supplied without the other,

that Retriever cannot have, since all three of its collaborators are

required. So "make them agree" was two separate questions: whether the

half-configured case and the mismatched-dimension case ought to share a type

at all, and which type the dimension case gets.

Decision

The exception type that names the condition wins. A dimension mismatch

between an embedding provider and a vector store is now always

DimensionMismatchError, at every composition entry point that performs the

check. build_graph raises it in place of the ValueError it used to raise;

Retriever.__init__ is unchanged.

The half-configured case keeps ValueError. A provider supplied without

a store, or the reverse, is a different mistake from a dimension

disagreement — there is no dimension to have disagreed on when one

collaborator is entirely absent — and it has no equivalent at any other entry

point to disagree with.

**The gate that would have caught this is introspective, not enumerated by

hand.** A test parametrised over a manually written list of entry points is

exactly the shape that let the original two diverge — each existing test

already covered its own entry point and none of them looked sideways. The

new test instead walks redstring.composition's public surface for every

callable whose signature names an EmbeddingProvider, asserts that set

against the enumerated cases, and fails loudly when the two disagree. A

fourth entry point that takes an EmbeddingProvider is covered by

construction: the introspective half of the gate fails first, before its

case can be written wrong.

Consequences

This is a breaking change. Any caller catching ValueError around a

build_graph call that mismatches embedding dimensions no longer catches it;

DimensionMismatchError must be caught instead. It ships with a version

bump, not a quiet patch release.

The half-configured case is deliberately excluded from this decision and

remains a ValueError. Widening DimensionMismatchError to cover it, or

introducing a shared base for both failure modes, is a separate decision this

ADR does not make.

A future composition entry point that accepts an EmbeddingProvider and a

store is covered by the introspective gate the moment it is added to

redstring.composition's public surface; its own test case still has to be

written, but the gate fails until it is, rather than passing silently on a

stale enumeration.