ContractViolation takes (rule, detail, offending_output=""). The guard
passed a single string, so every retired name raised
TypeError: ContractViolation.__init__() missing 1 required positional
argument: 'detail'
instead of the clean error it was written to produce - a guard that
crashes the request it was meant to explain.
It was not caught before deployment because this machine has neither
fastapi nor psycopg, so the guard could not be imported. It was tested by
lifting its source out of the file and exercising it against a stubbed
ContractViolation - a plain Exception, which accepts one argument. The
stub is what made the test pass. Running it in the built image found it in
one command, which is the argument for running the suite where the
dependencies are rather than reasoning about it where they are not.
Also records what the error actually does: main.py logs the rule, returns
422, and deliberately keeps the detail out of the operator-facing message,
so the explanation lands in the log and in Langfuse rather than on screen.
That is the right trade - an operator asking in plain English never types
one of these names, so the audience is a developer or a stale integration.
Verified in the running container: the retired names and their lowercase
forms are rejected with rule "retired_tag_name"; the poll groups, the item
names, the instrument tags and plain English all pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>