The gates were breaking the files they were repairing
Every spec in the suite, one run each, no retries: 19 of 22 green. That is the headline, and it is not the interesting part. The interesting part is what the three failures turned out to be — because the last of them was not a model failure at all. The fix loop kept dying on a line that no model had written. The gates had written it.
The line nobody wrote
taskapipro's fix loop burned every round it had on this:
tk1, _ := models.Task{ID: "1", Title: "task1", Status: "todo"}
A composite literal yields one value. There is no circumstance in which that line
is valid Go, and no model produced it — the artifact it came from had
tk1 := models.Task{…}, which is fine. Tracing the gate chain pass by
pass:
_requalify_undefinedadds two imports to the top of the file.- Every line below them shifts down by two.
-
_fix_assignment_arity— still holding the line numbers the compiler handed it before the insert — applies its repair to line 40, which is no longer theif err := svc.Create(…)it was aimed at, but thetk1 := …above it.
Almost every gate is indexed by a compiler line number. A gate that inserts a line silently invalidates those numbers for every gate behind it in the same pass. This has been true since the day there were two kinds of gate. It corrupts a file whenever an import is added alongside an in-place repair, and it presents as "the fix loop failed to converge" — which reads, from the outside, exactly like a model that isn't good enough.
The fix, and what the fix taught
The chain now runs in two phases. Phase one is the repairs that rewrite a line in place — safe together, because none of them changes how many lines a file has — and it returns the moment any of them fires, so the caller re-compiles and the next pass gets fresh numbers. Phase two is the line-shifting repairs: an import added, a struct field inserted, a method appended to an interface, a statement hoisted, a file reformatted. One per pass. Nothing runs behind one.
I classified the gates into those two phases by reading them. Then I wrote a property test to pin the classification — phase one must not contain a gate that changes a file's line count — and it failed on its first run, three times over:
| Gate | Looks like | Actually |
|---|---|---|
_fix_string_int_conversion | a one-line rewrite | string(42) → strconv.Itoa(42) needs an import |
_fix_errors_wrap | a one-line rewrite | errors.Wrap → fmt.Errorf needs an import |
_fix_unknown_struct_fields | a one-line rewrite | inserts a field into a struct |
All three look like single-line repairs. That is precisely why reading the code was not good enough, and why the test now reads the phase-one list out of the chain itself: a gate filed in the wrong phase fails there, instead of quietly corrupting a file six months from now.
A second gate bug fell out of the same trace. _fix_assignment_arity,
padding a lone err, appended the blank —
err, _ := svc.Create(…) — which assigns the Task to
err, so the err != nil beside it is a type mismatch. Go
puts the error last. The gate was manufacturing the exact defect that
_fix_swapped_error_assignment exists to repair: one gate breaking what
the next one fixes, and the project only surviving because the second gate happened
to run.
With both fixed, the gate chain takes the red taskapipro artifact — the one the sweep gave up on — to green on its own: seven passes, four packages race-clean, no model involved at any point.
The last hard compile class
taskapipro's actual build error had been in my notes for months, labelled "gate candidate, but the physics are different":
return paginate(s.store.ListProjects(ctx), limit, offset), nil
// multiple-value s.store.ListProjects(ctx) (value of type
// ([]models.Project, error)) in single-value context
ListProjects returns (items, error) and the model used the
call as an argument, dropping the error on the floor. Go cannot express that. Every
other gate rewrites one line; this one has to introduce statements,
which is why it was left alone.
What makes it tractable is that the enclosing function's signature settles everything — the error goes to the last result, each earlier result takes its zero value — so the repair is read off the AST rather than invented:
items, err := s.store.ListProjects(ctx)
if err != nil {
return nil, err
}
return paginate(items, limit, offset), nil
And it refuses whenever the signature does not settle it. The guard that matters is
the zero value it cannot prove: models.Project{} looks obvious
and is not — if models.Project were an interface, that literal would not
compile, and the file cannot see the other package to know. A named type is given a
composite literal only when this file declares it a struct. Everything else
refuses.
A green build is not the project you asked for
One more, found while reading a successful run. workapi's green artifact
contains internal/store/memory.go. It is one line long:
package store. The spec asked it to hold the MemStore; the model wrote
MemStore in store.go instead, so memory.go had nothing left to declare and shipped
empty. Go's compilation unit is the package, not the file — so the build is green,
the tests pass, and nothing complains. All four multi-package artifacts carry one of
these.
A prompt telling the model to stay in its lane did not stop it. So it gets a repair, and the repair is safe for a reason that is a fact about the language rather than a clever argument: moving a declaration between files of the same package cannot change what the program means. The type, its methods and its constructor move back to the file the plan gave them; each half is given exactly the imports it uses; the whole project is re-checked, and if it is not still green the move is reverted. It was reverted twice while I got it right, and both reverts taught me something I had wrong.
Nine mechanisms this week that looked like they worked and did not — and the last two are the ones I would least have predicted: the gates corrupting the files they were repairing, and a green build hiding a file the spec asked for and never got. Neither was a model limitation. Neither was visible until something counted. The suite stands at 281 tests, the model has not changed, and the bill is still $0.