fix(measure): say whether a refusal was the part's doing or partspec's - #390
Merged
Merged
Conversation
`refused` carried two things that mean opposite things. "the part defeated this measurement" is a finding about the design; "partspec could not perform it" is not a statement about the design at all. Both landed in one dict[str, str], so an agent had to string-match English to tell them apart -- and the run exited 0 either way, in the block whose other entries are about the part. Two changes, and both are needed to close it. A discriminator alone would let a consumer branch while the default answer still read as success. `refused_by` maps each refused name to "part" or "tool". A separate block rather than widening `refused`'s value type, because adding a field is non-breaking under SPEC-report 7.1 and changing dict[str, str] to dict[str, object] is not. Emitted exactly when `refused` is and keyed identically. A "tool" refusal now exits 4. That is the code this verb already returns when it could not measure what it was given, and exit 0 asserted the run was fine while partspec had failed at its job for a name. The payload is still emitted in full, so #369's property is untouched: one raising backend costs one name and not the other thirteen. A part that merely defeats a measurement still exits 0 -- that is an answer about the part, and measure decides nothing about parts. This creates a state no consumer has seen: exit 4 with a complete payload. It is distinguishable from the failure shape at a glance -- that carries `error` and no `measurements`, this one the reverse -- and AGENT-CONTRACT 2.4 said measure exits 4 only on a build failure, with empty stdout as the discriminator for "partspec itself failed". Both statements are now corrected there rather than left to be discovered. Closes #371
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.
refusedcarried two things that mean opposite things, as prose, at exit0.Reproduced side by side on
main:Both
dict[str, str], both exit0. The only discriminator was the English — in a blockwhose other entries mean the part's defect, so a bug in this tool read as a finding
about the part.
Two decisions, stated plainly because both change a released contract
1. Additive discriminator, not a widened
refused.refused_bymaps each refusedname to
"part"or"tool". #371 offered "§6.1's vocabulary or a narrowerrefused_by";§6.1's
originisenvironment | model | nulland none of those fits "the backendraised", so the narrower one is the honest spelling. A separate block because adding a
field is non-breaking under §7.1 while changing
dict[str, str]todict[str, object]is not — a consumer written before this reads
refusedunchanged.2. A
"tool"refusal now exits 4. #371 left this open ("worth deciding whether thebackstop path should exit 0 at all"). I think it must not: a discriminator alone lets a
consumer branch while the default answer still reads as success, which is half a fix.
Exit
4is the code this verb already returns when it could not measure what it wasgiven. A part that merely defeats a measurement still exits
0— that is an answer aboutthe part, and
measuredecides nothing about parts.#369's property is untouched. The payload is still emitted in full: one raising
backend costs one name and not the other thirteen. Verified — the tool-fault run emits 8
measurements alongside its exit 4. That gain is a property of the output; the exit code
is a separate channel and now says something true.
After
A new state for consumers, documented rather than discovered
Exit
4with a complete payload did not previously exist.AGENT-CONTRACT.md§2.4said
measureexits 4 only on a build failure, and that "exit 4 and stdout is empty"means the contract raised or partspec itself failed — with empty as the discriminator.
Both statements are corrected there. The two shapes stay distinguishable at a glance: the
failure shape carries
errorand nomeasurements, this one the reverse.SPEC-report.md§7.3 gainsrefused_bywith its emission rule and the exit-codeconsequence.
Tests
Three added, plus two existing ones updated — both of which failed for the right reasons
(
_measureasserts exit 0; the minimal-key-set test correctly saw one more key).With
cli.pyreverted and tests kept, all 5 go red. The standing control ispre-existing and untouched:
test_one_unmeasurable_quantity_does_not_suppress_the_other_thirteendrives a part-fault run through
_measure, which asserts exit 0, and it still passes — so"exit 4 for every refusal" would not survive.
just checkclean,1396 passed(baseline 1393).Closes #371