Two endpoints, one event shape, three quiet divergences - #289
Merged
Merged
Conversation
All three found by the website building against both routes, and the first cost a reader-visible failure. **The success states differ and nothing said so.** `/api/answer` says `answered` and `nothing_found`; this one says `summarised` and `not_found`. The event shapes are otherwise nearly identical, so a consumer built one state list from both contracts, and `summarised` fell through to their unrecognised-state fallback of `failed` -- rendering a complete, correct summary as a truncated failure, with the failure text above and below the summary itself. Each contract now warns about the other by name. **`release` was a string here and a number there.** Same field, same event, different type: `/api/answer` parses it from the bundle directory and sends an int, while this one passes through the Analysis Service's text body. A consumer that required a number got null and did not notice, because null is a legitimate value for this field. Parsed to a number now, null when unparseable, with the raw string kept as the storage key where only stability matters. **This changes what the field carries on the wire** -- `97` rather than `"97"`. **The human-presence asymmetry was undocumented.** A caller token with no presence claim gets a full answer from `/api/answer` and `no_human` from here. That is deliberate and load-bearing -- it is why the stricter gate applies to one route and not the other -- and the website discovered it by measuring rather than reading. Both contracts now say it and why: one returns public pathway text, the other returns a reader's own uploaded analysis. The shape of all three is the same. Each endpoint's contract was correct about itself, and the divergence only exists when the two are held together -- which is what a consumer does and neither document did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fix for two contracts that disagreed was prose in both contracts. Prose
does not fail when the next divergence appears, which is the whole reason
this one survived: each document was correct about itself and nothing
compared them.
`tests/api/test_endpoint_contracts_agree.py` compares them. The answer
endpoint's states are read from the graph's own Literal rather than
restated, so a new state there fails here instead of quietly escaping both
documents. The two state sets must overlap in exactly `refused` and
`failed`, every divergent state must appear in its own contract, and each
contract must name the other's success state -- which is the specific
confusion that rendered a correct summary as a truncated failure. Removing
the cross-reference from either document fails the test.
Two claims in the PR were checked rather than assumed, and both held. The
answer endpoint really does send `release` as an int -- I had asserted that
from a type annotation without ever seeing a `start` event, and the
deployed build confirms `{"release": 97}`. And `nothing_found` is really
emitted, at `graph.py:527`, rather than only being described in its
contract.
The release check is honest about being half a declaration check: the answer
endpoint puts `get_release`'s value straight on the wire, so its return type
is the contract, and reading the real value needs an installed bundle. The
summary side, where the type is actually decided by parsing text, is checked
by behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
All three found by the website building against both routes. The first caused reader-visible harm.
The success states differ and nothing said so
/api/answersaysansweredandnothing_found. This one sayssummarisedandnot_found. The event shapes are otherwise nearly identical.They built one state list from both contracts, so
summarisedfell through to their unrecognised-state fallback —failed— which rendered a complete, correct summary as a truncated failure, with error text above and below the summary itself.Each contract now warns about the other by name.
releasewas a string here and a number thereSame field, same event, different type:
/api/answerparses it from the bundle directory and sends an int; this one passed through the Analysis Service's text body.A consumer that required a number got null and did not notice, because null is a legitimate value for this field.
Parsed to a number now, null when unparseable, with the raw string kept as the storage key where only stability matters. This changes the wire format —
97rather than"97".The human-presence asymmetry was undocumented
A caller token with no presence claim gets a full answer from
/api/answerandno_humanfrom here. Deliberate and load-bearing — it is why the stricter gate applies to one route and not the other — and the website found it by measuring rather than reading.Both contracts now state it and why: one returns public pathway text, the other a reader's own uploaded analysis.
The shape they share
Each contract was correct about itself. The divergence only exists when the two are held together — which is what a consumer does and neither document did. Same join problem that has produced most of this week's defects, at the level of documentation rather than code.
🤖 Generated with Claude Code