Skip to content

Fix Go replace for unselected versions (#391, #392, #509) - #1403

Merged
Mikola Lysenko (mikolalysenko) merged 6 commits into
mainfrom
agent/fix-go-selected-version-gate
Oct 12, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 6 commits into
mainfrom
agent/fix-go-selected-version-gate

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #391
Fixes #392
Fixes #509

Summary

Go keys a replace by module and version. When the build list
selects a different version, a replace pinned to the patched version is
silently ignored and the build links the unpatched module. Every Go path
used its own partial go.mod/go.sum heuristic to decide whether a version
is built, so each reported a patch as applied or attested when it wasn't:

Path Before After
agent apply (#392) no gate: wrote the inert replace and exited 0 refuses (nothing written)
vendor (#392, vendored variant in a comment) same refuses vendor_go_version_not_selected
apply --check (#392, go 1.16) "absent from require" counted as harmless ResolvedVersionMismatch
vex (#391) attested not_affected from the intact copy omits with go_version_not_selected (agent, vendored and hosted replaces)
hosted gate (#509) a go.sum /go.mod-only line counted as "in the graph" only a zip line counts; warns redirect_golang_version_mismatch

Root cause and fix

There was no shared answer to "does the build list select M@v?". The new
vendor::go_build_list answers it offline from go.mod and go.sum, and every
call site above uses it:

  • go ≥ 1.17 (pruned graph): require is authoritative. If the module
    is absent, it is built only if go.sum has its zip line.
  • go < 1.17 (or no go directive): require holds only lower bounds
    and leaves out transitive modules. The selected version is the highest of
    the required version and the go.sum zip-line versions. A /go.mod
    line is written for every version MVS reads, so it doesn't count. With
    no evidence at or above the patched version (as after go mod tidy
    drops a directory-replaced original's lines), the answer is unknown and
    nothing is flagged, unless a zip line below the patched version shows
    that lower version is built.
  • In workspace mode, "not in graph" degrades to unknown, because another
    member may require the module.

Docs: CLI_CONTRACT.md (golang agent section and the hosted rule) and
docs/ecosystems.md (Go).

Tests (red on main, green here)

Each new regression test was applied to unmodified origin/main and failed
there:

Commands run locally (Linux, go 1.24.7):

  • cargo fmt --all -- --check: clean
  • cargo clippy --locked --workspace --all-features -- -D warnings: clean
  • cargo test -p socket-patch-core --all-features --lib -- go: 848 passed
  • cargo test -p socket-patch-cli --all-features --lib: 913 passed
  • CLI suites covgap_commands_vex, covgap_commands_rollback, e2e_golang, e2e_vex_vendor, in_process_vendor, e2e_golang_build, e2e_golang_hosted_build, e2e_golang_hosted_state, e2e_golang_workspace_build and e2e_vendor_golang_build (with --include-ignored, real go): all pass

The full cargo test --workspace couldn't finish locally because the session
ran out of disk, so CI's full matrix is the check for the rest.

tests/equivalence/golang_rewrite.golden was re-blessed. To show only the
warning wording changed, I restored the old not-in-graph message and checked
that the old golden still matched.

/code-review high findings not fixed

  • Stale higher zip line in a go < 1.17 go.sum (a downgrade with no
    go mod tidy): this reads as "the build selects the higher version". It
    fails closed: apply refuses, vex omits, and hosted drops its own
    replace. go.mod and go.sum alone can't tell a stale line from a transitive
    requirement. go mod tidy clears it.
  • apply fails rather than skips, and leaves an already-written inert
    replace in place:
    a failure is the right signal, because the patch isn't
    applied, and apply --check names the drift. Removing go.mod lines from a
    failing apply would be a larger behavior change.
  • project_selection re-reads go.mod/go.sum per replace entry in
    apply --check and vex: there are a handful of entries per project, so
    I didn't add the extra plumbing.
  • read_required_versions is no longer used outside tests: left in place
    to keep this PR scoped. Its tests and a discovery guard still reference it.
  • The equivalence golden doesn't exercise go < 1.17: that path is covered
    by the targeted unit tests above instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DH6BEwGj8nVAeveDy6x5fc


Note

Medium Risk
Changes fail-closed behavior across apply, vendor, hosted redirect, and VEX for Go patches—users with stale version-pinned replaces may see new refusals or omitted attestations until they re-scan for the selected version.

Overview
Go replace directives are keyed by module and version, so a pin for a version the build list does not select is ignored and the unpatched module is linked. This PR adds shared offline logic in go_build_list (from go.mod / go.sum) and uses it everywhere that used to guess from partial heuristics.

Agent apply / vendor now refuse before writing copies or replace lines when the patched version is not selected (including go 1.16 graphs where selection comes from go.sum zip lines, not /go.mod-only entries). apply --check flags ResolvedVersionMismatch on the same basis instead of treating “missing from require” as harmless.

Hosted redirect applies the same selection rules: version mismatches warn redirect_golang_version_mismatch; “in graph” checks ignore /go.mod-only lines (#509). vex drops attestation for socket-owned replaces with go_version_not_selected when go.mod/go.sum show another version is built.

CLI and ecosystem docs describe the behavior; regression tests and updated golang rewrite golden cover apply, vendor, hosted, vex, and the helper.

Reviewed by Cursor Bugbot for commit 598055a. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A Go replace names an exact module version. When the build list
selects another version (a later go get, or a go 1.16 graph where a
transitive dependency asks for a newer one), the replace is ignored
and the build links the unpatched module. Agent apply wrote such a
replace and reported it applied, apply --check passed it in go 1.16
graphs, vex attested not_affected, and the hosted gate took a
go.sum /go.mod-only line as proof the version was built.

All four now ask one helper that reads go.mod and go.sum: require
for go 1.17+, and for older graphs the highest of require and the
go.sum zip-line versions. apply refuses, apply --check flags, vex
omits with go_version_not_selected, and hosted warns and skips.

Fixes #391, #392, #509.

Assisted-by: Claude Code:claude-opus-5-5
The not-in-graph warning now says go.sum has no zip line for the
version, since a /go.mod-only line no longer counts. Checked that
the old golden still matches with the old wording, so only the
message changed. The +incompatible reconcile test now requires the
version it applies.

Assisted-by: Claude Code:claude-opus-5-5
A go 1.16 go.sum zip line below the patched version now shows that
lower version is built, so apply and apply --check catch it again.
Vendored mode gets the same build-list gate before it fetches or
wires a module. The hosted not-in-graph warning no longer claims
go.mod lacks a require it may have, and the zip-line rule lives in
one place.

Assisted-by: Claude Code:claude-opus-5-5
The download plan has to name exactly the modules vendor asks the
service for, so it now skips a version the build doesn't select,
as vendor itself does. Adds a regression test for vendoring an
unselected version in a go 1.16 graph.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 11, 2026 23:25
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

The vendor suite pinned apply exiting 0 when go.mod requires a newer
version than the patch, though the replace it wrote was ignored. It
now expects the refusal that names the selected version (#392);
apply --check still reports the drift.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 598055a. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 12, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review (burn-down agent)

  • Head: 598055a
  • CI: 35/35 non-skipped checks green (19 skipped by path filters); mergeable, no conflicts.
  • Bugbot: reviewed 598055a, no findings; no open review threads.
  • Already approved by Wenxin Jiang (@Wenxin-Jiang) on this head. For a reviewer: the new shared vendor::go_build_list (go ≥ 1.17 vs < 1.17 selection rules) is the core of the change; every Go path (apply, vendor, apply --check, vex, hosted gate) now routes through it.

Generated by Claude Code

Merged via the queue into main with commit 1d523d3 Oct 12, 2026
54 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-go-selected-version-gate branch October 12, 2026 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment