fix: a refusal that blames another field is not about the value sent - #52
Merged
Conversation
Phase 7.2, and not the shape the plan predicted. Reading the probes first found the actual cause of one of the two known-wrong facts, and it is neither staleness nor a missing gate. Five mutating probes -- updateStyle, immutability, enumBoundary, normalisation and writeSideEffect -- draw their conclusions from `sc.Fixture(0)` only. The enum probe substitutes each candidate into that one body, so probing objectType=endpoint-agent sent a static-tag body with the object type swapped. The API answered, and the cassette records it verbatim in 086-post-v7-tags.json: type: Static tags are not supported for the provided object type A refusal about `type`. The probe was varying `objectType`. enumBoundary already had the guard for this -- "a refusal counts as being about *this field* only when the error says so" -- and the guard passed, because Names() normalises a field to its words and looks for them adjacently. "the provided object type" contains "object type" contiguously. So endpoint-agent was recorded as a rejected documented value at Observed confidence, and it is a perfectly good object type for a dynamic tag. Adjacency cannot separate those readings. What separates them is that a validation failure prefixes the offending field and a colon, and prose does not. Names() now treats those clause names as authoritative where any exist, and falls back to the previous match where none do -- so an API that answers in unstructured prose still gets a conclusion. This required a tool that did not exist. Facts were only ever written by a record run, so a probe that stopped drawing a wrong conclusion made `probe -mode verify` fail with no way to update the evidence short of a live run that also replaced it. `probe -mode replay -rederive` writes facts back from the committed cassette with the network denied, which is legitimate precisely because derivation is a pure function of the transcript -- the claim verify exists to keep true. The checksum is verified first, the traffic does not move, and the diff is reviewable against evidence that has not changed. Re-derived, and the set difference is exactly one fact withdrawn: objectType.rejectedValues = ["endpoint-agent"]. Nothing added, nothing otherwise changed, and a second rederive is byte-identical. Merge then folded the description change; the stale rejectedValues in Behaviour had to be removed by hand, because merge may only widen -- which is the rule working, not a gap. Two tests changed. TestUnit_Emit_AllowedValuesBecomeAValidator asserted that endpoint-agent was named as refused beside the validator; that assertion rested on the false fact. The mechanism is real, so it moved to accessType, where `system` was genuinely refused with the field named -- and a new assertion refuses the withdrawn claim, which is what would have caught this. The gate-crossing design the plan described is deliberately not here. Crossing declared gate values into one fixture would manufacture invalid bodies systematically -- it is what produced this false fact. A branch has to be a complete body a human declared, which is what a fixture already is, so 7.3's dynamic fixture is the mechanism and there is no need for a second one. Co-Authored-By: Claude Opus 5 (1M context) <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.
Phase 7.2 — and not the shape the plan predicted. Reading the probes before building anything found the actual cause of one of the two known-wrong facts, and it is neither staleness nor a missing gate.
The cause, from the cassette
Five mutating probes —
updateStyle,immutability,enumBoundary,normalisation,writeSideEffect— draw their conclusions fromsc.Fixture(0)only:So probing
objectType=endpoint-agentsent a static-tag body with the object type swapped. The API answered, and086-post-v7-tags.jsonrecords it verbatim:A refusal about
type. The probe was varyingobjectType.enumBoundaryalready had the guard — "a refusal counts as being about this field only when the error says so" — and it passed, becauseNames()normalises a field to its words and looks for them adjacently. "the provided object type" contains "object type" contiguously. Soendpoint-agentbecame a rejected documented value at Observed confidence, and it is a perfectly good object type for a dynamic tag.The fix
Adjacency cannot separate those readings. What can is that a validation failure prefixes the offending field and a colon, and prose does not:
Names()now treats those clause names as authoritative where any exist, and falls back to the previous match where none do — so an API answering in unstructured prose still gets a conclusion. Tested against eight strings a live tenant actually returned.A tool that did not exist
Facts were only ever written by a record run. So a probe that stopped drawing a wrong conclusion made
probe -mode verifyfail, with no way to update the evidence short of a live run that also replaced it.probe -mode replay -rederivewrites facts back from the committed cassette with the network denied. Legitimate precisely because derivation is a pure function of the transcript — the claimverifyexists to keep true. The checksum is verified first, the traffic does not move, and the diff is reviewable against evidence that has not changed. That is the difference between re-deriving and re-recording.The result, measured
Set difference after re-deriving is exactly one fact withdrawn:
Nothing added, nothing otherwise changed, and a second
-rederiveis byte-identical. The large textual diff is reordering.Merge then folded the description change. The stale
rejectedValuesinBehaviourhad to be removed by hand, because merge may only widen — "only the hand-authored override layer may remove". That is the rule working, not a gap.Two tests changed, and why
TestUnit_Emit_AllowedValuesBecomeAValidatorasserted thatendpoint-agentwas named as refused beside the validator. That assertion rested on the false fact. The mechanism is real, so it moved toaccessType, wheresystemwas genuinely refused with the field named — plus a new assertion that refuses the withdrawn claim, which is what would have caught this in the first place.What is deliberately not here
The plan described declaring gate values and crossing them, with cost multiplying by branch count. Crossing declared values into one fixture would manufacture invalid bodies systematically — it is precisely what produced this false fact. A branch has to be a complete body a human declared, which is what a fixture already is. So 7.3's dynamic fixture is the mechanism, and a second one would be a way to generate more wrong facts faster.
Verification
probe -mode verify-rederiveidempotencemerge -checkverifybindingsgo test ./...internal/probecoveragegofumpt -l ./golangci-lintgo build && go vet🤖 Generated with Claude Code