Repository navigation
fix: stop the generated bindings from encoding states the contract forbids - #6
Merged
antoinetoussaint-byte merged 1 commit intoSep 14, 2026
Conversation
…rbids
The generated types carried the contract's optional and nullable
declarations but left three states encodable that the declaration does
not have, each of them silently.
Optional[T] exported Present and Value, so Optional[string]{Value: &s} —
the natural literal — set a value without marking it present, IsZero
reported absent, and omitzero dropped the key from the payload with no
error. The fields are now unexported and the states reachable only
through Absent, Null and Set, so no literal can spell a state the
contract does not have. MarshalJSON on an absent value is an error
rather than a null: absence is omitted with its key, and reaching the
marshaler absent means it is being encoded where a key cannot be
omitted, where null would stand in for absent without saying so.
An array was a Go slice, and a nil slice encodes as null. The scaffold
returns a zero Output, so the first payload a generated runnable
produced wrote null for a required array; decoding {} and re-encoding it
turned an absent array into an explicit null; and a null arriving for a
non-nullable array decoded without error. Arrays are now a List, which
encodes an unset array as the empty array it is and refuses to decode a
null one. A nullable array is a *List, where nil is the null state. An
optional array is a Present[List[T]]: Present, which #4 added on main
while this branch was open, refuses the null that a pointer around a
List would have swallowed before the List ever saw it.
omitzero is not gated by the module's go directive, only by the
toolchain, so an older one ignored it in silence and turned every absent
optional key into a null. Generated files now carry //go:build go1.24,
which fails the build instead.
Alongside, at the same seams:
- exported() could return a non-identifier: core allows the field name
_1, which generated the Go field 1 and surfaced as a gofmt parse error
pointing into a file the author never sees. Names are checked against
an exported-identifier pattern and the error names the contract field.
- A type-name collision reported only the generated name, which is not a
name anyone wrote. The generator now carries each type's contract path
and reports both (input.a_b and input.a.b).
- pkg and the runnable name were interpolated into source unvalidated. A
runnable name is a path component, so word-count reached gofmt as a
parse error and a newline would have injected top-level Go into a file
marked DO NOT EDIT. Both are validated at the entry points.
- Handler's doc promised the scaffold is "never rewritten over their
implementation" while the package had no writer to keep that promise.
WriteHandler creates the file with O_EXCL and reports whether it wrote
one, so a regeneration cannot overwrite an author's handler.
- Bindings takes the contract, not the Runnable, deliberately:
Runnable.Validate also demands a pinned agent, entrypoint and
execution block, none of which change a byte of output. Recorded at
the signature.
- The compile test hardcoded the go directive next to go.mod's; it now
reads go.mod, so it cannot go on compiling at a version the repository
has left.
- Generated code is read as a diff by the author whose workspace it
lands in, so a golden file pins the whole output: order, header, blank
lines and the single emission of each carrier.
- The naming rule is duplicated in runnable-python and belongs in core;
it is pinned by a test against that implementation's names until it
moves.
- CI ran with the repository's default token scope and skipped pull
requests that did not target main — with auto-merge-on-green, a stacked
pull request with no checks reads as one that passed them. Added
contents: read and dropped the branches filter.
- docs/milestone_1_bindings_review.md recorded a review verdict ("found
no blocking defect") as repository documentation; it cannot be kept
true and this change disproves it. Removed, with the tracker narrative
it duplicated in the README.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AntoineToussaint
force-pushed
the
issue-1-feat-go-runnable-agent-after-python-contract-qualification
branch
from
September 14, 2026 13:09
ea21ad1 to
4fb9283
Compare
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.
Follow-up to #3. A review of that merge found the generated types carried the contract's
optionalandnullabledeclarations but left three states encodable that the declaration does not have — each of them silently. This fixes those and the smaller findings alongside them.Verified by compiling the generated package and exercising it, not by inspection.
The two silent ones
Optional[T]dropped a value the author set.PresentandValuewere exported, soOptional[string]{Value: &s}— the natural literal — set a value without marking it present,IsZeroreported absent, andomitzerodeleted the key with no error. Fields are now unexported and the states reachable only throughAbsent,NullandSet.MarshalJSONon an absent value is an error rather than anull: absence is omitted with its key, so reaching the marshaler absent means it is being encoded where a key cannot be omitted, andnullthere would stand in for absent without saying so.A required array encoded as
null. An array was a Go slice and a nil slice encodes asnull. The scaffold returns a zeroOutput, so the first payload a generated runnable produced wrotenullfor a required array; decoding{}and re-encoding it turned an absent array into an explicitnull; and anullarriving for a non-nullable array decoded without error. Arrays are nowList, which encodes an unset array as the empty array it is and refuses to decode a null one. A nullable array is*List, where nil is the null state. An optional array stays a bareList— a nilListis already the absent stateomitzeroomits, and the pointer it would otherwise get decodes the forbiddennullas if it were absent.omitzerois gated by the toolchain, not by the module'sgodirective (verified: ago 1.21module still gets it under a 1.27 toolchain). An older toolchain ignored it in silence and turned every absent optional key into anull. Generated files now carry//go:build go1.24, which fails the build instead.Alongside, at the same seams
exported()could return a non-identifier: core allows the field name_1, which generated the Go field1and surfaced as a gofmt parse error pointing into a file the author never sees. Names are checked against an exported-identifier pattern and the error names the contract field.input.a_bandinput.a.b).pkgand the runnable name were interpolated into source unvalidated. A runnable name is a path component, soword-countreached gofmt as a parse error and a newline would have injected top-level Go into a file markedDO NOT EDIT.Handler's doc promised the scaffold is "never rewritten over their implementation" while the package had no writer to keep that promise.WriteHandlercreates the file withO_EXCLand reports whether it wrote one.Bindingstakes the contract and not theRunnabledeliberately:Runnable.Validatealso demands a pinned agent, entrypoint and execution block, none of which change a byte of output. Recorded at the signature rather than left open.godirective next togo.mod's; it readsgo.mod, so it cannot go on compiling at a version the repository has left.runnable-pythonand belongs in core; a test pins it against that implementation's names until it moves.main— with auto-merge-on-green, a stacked PR with no checks reads as one that passed them. Addedcontents: read, dropped the branches filter.docs/milestone_1_bindings_review.mdrecorded a review verdict ("found no blocking defect") as repository documentation. It cannot be kept true and this change disproves it. Removed, with the tracker narrative it duplicated in the README.Not fixed here
#4 — an optional, non-nullable scalar is a
*T, andencoding/jsonnils a pointer onnullwithout consulting any unmarshaler, so a forbiddennulldecodes as absent. Same defect class as the array case, different type family; fixing it changes the author-facing spelling of every optional scalar, so it belongs with the harness's raw-payload validation decision.Separately,
runnable-pythonhas a live silent bug this surfaced:nestedName()has no collision check, soa_b+a.bemits two classes namedInputABand the second shadows the first. Go errors on that contract; Python mistypes it. Posted to the shared blackboard with a repro.Bearing on #1
#1's checked box reads "generated code compiles and its presence/value cases are exercised". That was checked on the code this PR fixes. The box is accurate once this lands.
🤖 Generated with Claude Code