Repository navigation
Read the governing go.work in Go discovery - #2011
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
Conversation
Go discovery read only a go.work at the project root and only members inside the root. With GOWORK unset it ignored an ancestor go.work and read the root go.mod in module mode, so in a member of a parent workspace it could attest a go.mod replace that the workspace overrides, doesn't `use`, or makes inert by version selection. Hosted and vendored modes resolve the workspace go itself would use and refuse to write such a replace. On disk, discovery now loads the governing workspace with the same go_workspace::load_with loader hosted mode uses: GOWORK (environment, then the go env -w file), else the nearest go.work at or above the root, with `use` members anywhere. Out-of-root members' requires and replaces now count toward selection and conflicts. In-root members are still re-read through the view, so recognition and parse diagnostics stay the same. A wired Socket replace that this workspace keeps from the build, at exactly the points where hosted mode's host_conflict check refuses, stays a ref (list, rollback and remove still find it) but is marked unattested with the new UnattestedKind::GoWorkspaceOverride. The same applies to a replace in the root go.work while GOWORK makes go build another one. vex_sources reports these under the existing `go_workspace_override` reason, the one vex's own go.mod gate already uses. A replace that workspace selection makes inert is still diagnosed, as in module mode. A GOWORK that go rejects still withholds every ref as patched_ref_invalid. In memory there is no directory to search from and no environment the project builds under. The root go.work and its in-root members are the workspace there, as the in-memory hosted engine reads them, and the same conflict gate applies. GOWORK keeps its old effect in memory and GOFLAGS is not read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
Version-selection inertness in replaced_purl, and the hosted selection checks, ran before Workspace::bypass. A Socket replace that the governing go.work also refuses (not used, overridden by a go.work replace, member conflict) was dropped as inert with a diagnostic instead of being kept as a GoWorkspaceOverride ref, so list, rollback and remove could not find wiring that re-running scan or vendor would never clear. Compute the bypass first and skip the inertness checks for a bypassed replace; it stays a ref and is reported unattested. Found by Cursor Bugbot on #2011. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
|
Labeled Ready for review (burn-down agent).
Generated by Claude Code |
This fixes the confirmed /code-review high findings on #2011. Each fix has a test that fails on the previous commit. - A Go workspace mark now applies to one file only. The new UnattestedKind::file_scoped is true only for GoWorkspaceOverride. vex_sources drops the bypassed ref from the candidate's basis and vetoes the patch only when no live ref of it remains. wiring_conflicts ignores bypassed refs. So a root go.work replace that go never reads no longer withholds the root go.mod's mirror of it. A leftover member pin that Socket's own go.work replace overrides no longer turns into a wiring conflict that also withholds the go.work patch, which is what vex.rs's WorkHosted exemption intends. - A replace of a module that is itself a `use` member is diagnosed as inert again, both on disk and in memory, hosted and vendored. This check now runs before the workspace bypass. - SumPins now also reads the sum files the loader already read: the governing go.work.sum and each out-of-root member's go.sum. They are used only when they lie inside the git repository. - In memory, workspace conflicts are evaluated in go_workspace::PathSpace::Memory. Paths are compared lexically against the view's files and never against the filesystem or the working directory. conflict/work_conflict delegate to the new *_in variants, so disk behavior is unchanged. - The snapshot probe list is now built by one shared helper (workspace_probes), spelled the way resolve_with spells it. - A root go.work that go doesn't read is no longer judged unparseable, and its wiring stays an unattested ref. - The go_workspace_override omission phrase now covers every case. - read_loaded_workspace uses members_within. The view re-reads only the in-root members' go.mod, which recognition and parse diagnostics need. The duplicate go.sum and go.work.sum reads are gone. - Tests that use a nested project now create a .git, so they keep working once reads are bounded to the repository root (#2025). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2984629 dropped every workspace-bypassed ref from wiring_conflicts, so a leftover member pin that Socket's go.work replace overrides would not withhold the go.work patch. That also caught the root go.mod. A GOWORK=off build from the root reads the root go.mod, so a different patch there still disagrees with go.work about what the build links. The behavior is the same reasoning ReplaceHost::mirrors_root_go_mod uses. e2e_vex_lockfile golang_go_mod_and_go_work_disagreeing_attest_neither went red in coverage (2): it got go_workspace_override plus go_version_not_selected skips, not one wiring_conflict skip. Count bypassed refs in the root go.mod as wiring again. Member go.mod pins are still exempt, as a_bypassed_leftover_pin_is_no_wiring_conflict requires. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
|
Burn-down agent: removed Ready for review. The head had moved to Failure: Fix ( Local runs: e2e_vex_lockfile (354), e2e_vex / e2e_vex_vendor / e2e_vex_redirect, CLI lib (870) all pass. Clippy on lib/bins is clean. Reviewer: check that the root-vs-member split is the intended product behavior. If go.work should also win over a different root go.mod pin, the e2e test should change instead. Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 1529210. Configure here.
|
Ready for review at head
For a reviewer: when the root go.mod is itself a wiring conflict, discovery still treats it that way (see commit 1529210). Generated by Claude Code |
Final review briefWhat it does. Before this PR, Go VEX discovery read only the project's root Risk: medium. It changes what gets attested for any Go project that sits in a workspace or has an ancestor or Look here
Verified
Open questions (non-blocking)
Auto-merge is not armed: ci-ok has to go green first. Once it's green it gets armed, and approving then sends it straight to the merge queue. Generated by Claude Code |
Problem (G1: which go.work and which members count)
Go VEX discovery read only a
go.workat the project root, and only the members inside the root. WithGOWORKunset it ignored an ancestorgo.workand read the rootgo.modin module mode. Hosted mode resolves the workspace go itself would use and refuses to write a replace onhost_conflict(patch/redirect/mod.rs,hosted/engine.rs::read_go_workspace), and vendored mode runs the same check (vendor/golang.rs→go_consumer_sync::replace_refusal). So in a member of a parent workspace, discovery could attest a rootgo.modreplace that the workspace overrides, doesn'tuse, or makes inert by version selection. Discovery also never read ause ../svcmember outside the root, so that member's higherrequireor conflicting replace was invisible.Fix
On disk,
vex/discover/golang.rsloads the governing workspace withgo_workspace::load_with, the loader hosted mode uses. It resolvesGOWORK(environment, then thego env -wfile, read the same wayextractalready read it), else the nearestgo.workat or above the root, with members anywhere. It then reads the module graph withread_graph, as hosted mode does.requires now count toward selection, and their replaces toward conflicts.go.modis re-read through the view, so Socket-identity recognition andlockfile_unparseablediagnostics don't change. The project's own sum files are read once, bySumPins. Every other text the loader read is used as it read it.resolve_withprobes are declared to a tracked snapshot. A loaded workspace goes raw, as before, because the module graph reads the module cache.Gate: before a hosted or vendored ref is pushed,
Workspace::bypassruns:go.modrefs:GoWorkspace::conflict(<module dir>, …);go.workrefs:work_conflict.These are the checks
Loaded::host_conflictapplies before hosted or vendored mode writes. A rootgo.workthat go doesn't read, becauseGOWORKnames another file, also counts as a bypass.In memory (
ProjectView::Memory) there is no directory to search from and no environment the project builds under:go.workand its in-root members, exactly as the in-memory hosted engine builds it, and the same gate runs on it. That gate is evaluated ingo_workspace::PathSpace::Memory: paths are compared lexically against the view's files and never against the filesystem or the working directory.GOWORKkeeps its previous in-memory effect:offreads nogo.work, and any other explicit value withholds every ref.GOFLAGSis still not read there.withhold(every ref becomespatched_ref_invalid) now covers onlyLoaded::Invalid, where go refuses to build: a relativeGOWORK, ago.workit can't read or parse (including an unparseable rootgo.work, which used to fall back to module mode), and the existing-modfilecase (Go apply, apply --check and vex ignore GOFLAGS=-modfile, so they report a patch as applied while go builds from another go.mod and links the unpatched module #1799).Why unattested instead of diagnosed
The gated replaces are live Socket wiring. Hosted and vendored modes refuse to rewrite them while the workspace conflicts, so re-running
scan/vendorcan never clear the evidence. That is the caseUnattestedexists for.So the ref is kept (
list,rollbackandremovestill find it) and marked with a newUnattestedKind::GoWorkspaceOverride. The CLI note arm invex_sources.rsreports it under the existinggo_workspace_overridereason. That is the reasonvex's own go.mod gate (vex.rs,workspace_audit) already gives for the same cause, so users see one code for one cause. The const moved tovex_sources.rsandvex.rsimports it.Version-selection inertness (an out-of-root or ancestor member raising the module) is still diagnosed, never a ref, as in module mode. A stale pin is something the writers treat as dead: re-running
scanrewires the selected version.Tests
New tests. Each fails on
origin/main(verified by running them against main'sgolang.rs) and passes here:ancestor_go_work_governs_the_root_module: an ancestorgo.workthat overrides the root replace, doesn'tusethe module, or has a member raising it to v1.5.0 (inert diagnostic); plus a clean case and aGOWORK=offcase.gowork_naming_another_workspace_governs:GOWORKnames an existing workspace elsewhere that overrides, doesn't use, or cleanly uses the module. A rootgo.workholding the wiring whileGOWORKpoints elsewhere is unattested.workspace_member_outside_the_root_is_read: ause ../svcmember that raises the module (inert), replaces it elsewhere (MemberConflict, unattested), or requires the same version (attested).in_memory_root_go_work_governs_the_root_module: an in-memory rootgo.workthat doesn'tusethe root module.Changed test:
gowork_naming_another_workspace_attests_nothing→gowork_go_refuses_attests_nothing. AGOWORKnaming a missing file now reports the loader's "cannot be read" reason instead of "other than".cargo test -p socket-patch-core --lib -- vex::discover:: vendor::go patch::redirect:: hosted::: 1689 passed, 0 failed (after the review fixes)cargo test -p socket-patch-cli --lib -- commands::vex: 35 passedvex::discover::testing::golden::tests::committed_fixture_corpus_matches_golden: passes, no golden changescargo clippy --locked --workspace --all-features -- -D warnings: cleancargo fmt --all -- --check: cleanNotes / not done
GOWORKpointing elsewhere. On main, discovery never attested in this case: it withheld every ref aspatched_ref_invalid, so the new test fails on main because the ref is dropped rather than kept as unattested. Now discovery reads that workspace. Where hosted mode would write the replace, it attests; where it wouldn't, the ref stays and is unattested.vexcommand level, the existinggo_workspace_overrideand selection gates invex.rsalready withheld most of the go.mod cases. They rebuild the env-aware workspace themselves. This PR fixes the discovery contract that other consumers rely on, for example the hosted rollout's recorded pins and the VEX plan.-modfileand an invalidGOWORKstill withhold refs as diagnostics (Go apply, apply --check and vex ignore GOFLAGS=-modfile, so they report a patch as applied while go builds from another go.mod and links the unpatched module #1799 behavior) instead of marking them unattested. Changing them would alter an existing contract this PR doesn't need to touch.GoWorkspace::conflictwith an empty root, which probes the working directory. Discovery no longer does (review finding 5); moving hosted toPathSpace::Memoryis a follow-up.Review findings (/code-review high, fixed in 2984629)
I checked each finding first. Every behavior fix has a test that fails on 3f452d1 and passes now.
Confirmed and fixed: a go.work mark vetoed the live go.mod mirror.
UnattestedKind::file_scoped()is true only forGoWorkspaceOverride. Invex_sources.rs, such a mark now:wiring_conflicts.Test:
a_go_workspace_mark_withholds_only_its_own_file(CLI).Confirmed and fixed: a replace of a
use-member module became an unattested ref. The ordering came from 3f452d1, which moved the bypass ahead of selection. A replace of a module that is itself ausemember is now diagnosed as inert ("the build selects (the workspace module in …)") before the bypass runs. This covers disk and memory, hosted and vendored. Other bypassed replaces still skip the inertness check, as 3f452d1 intended. Test:replace_of_a_workspace_module_is_inert.Confirmed and fixed: sum files outside the project were ignored.
SumPinsnow also takes the loader's already-read texts of the governinggo.work.sum(when that go.work isn't the root's) and each out-of-root member'sgo.sum. They are used only when they lie inside the git checkout (find_git_repo, or the project itself when there is none). Test:sum_files_outside_the_project_still_decide_the_pin, which puts a malformed line in the ancestor go.work.sum or the out-of-root member's go.sum.Aligned, by a different route than suggested. The mark itself stays: a member pin that a Socket go.work replace of another target overrides is not what the build links, so marking that file is accurate. What
vex.rsexempts is the purl-wide veto, and with finding 1's file scoping the mark can no longer veto or conflict the go.work ref. The go.work patch now attests instead of becoming awiring_conflict. Test:a_bypassed_leftover_pin_is_no_wiring_conflict(CLI).Confirmed and fixed: in-memory checks touched the disk. New
go_workspace::PathSpace { Disk, Memory { files } }.conflict,work_conflictandrooted_atdelegate to*_invariants, andDiskbehaves exactly as before. In memory (and underwithhold), paths are matched lexically against the view's files, so nothing touches the filesystem or the working directory. Test:in_memory_workspace_check_reads_only_the_view. It used to say "has nowhere to go" because the crate directory has no go.mod; it now says "go.work does notusethis module".Partly fixed. The probe list is built by one shared helper,
workspace_probes, used by bothextract_goenvandextract_gowork. It is spelled the wayresolve_withprobes (current_dir().join(root)), so a relative root's real ancestors are declared too.resolve_withstats, and Never read or rewrite project files outside the git repository root #2025 keeps that stat above the root ("Only a stat above the root: the file is never opened there"). If the list were bounded, a go.work created above the repository would not invalidate a cached snapshot, even though after Never read or rewrite project files outside the git repository root #2025 it makes the workspaceInvalid.chdir.Confirmed and fixed: a go.work go never reads was judged unparseable. When
GOWORKnames another workspace, the root go.work is only normalized, never checked for block structure. Its wiring stays an unattested ref and nolockfile_unparseableis reported. Test:unread_root_go_work_is_not_diagnosed_unparseable, with an unclosedreplace (block.Fixed as wording.
omission_phrase(go_workspace_override)now covers every case: "keeps its replace from the build (overrides, ignores or rejects it, or go builds from a go.work other than the one holding it) … the run warning names which". The per-case detail already rides the run warning. Passing it throughfailed[].reasonwould mean adding a detail field tovex::FailedPatch, which is shared by every reason and out of scope here.Fixed.
read_loaded_workspacecallsmembers_withinand maps each member back to its index.Fixed. Dropped the view re-reads of member
go.sumandgo.work.suminto the model; discovery uses neither, sinceMember::go_sumonly feedsreplace_host. The view still reads:go.modand rootgo.work: recognition (rule 11), and they are the refs' source;go.mod: recognition, parse diagnostics, and refs when the root has no go.mod;SumPins: recognition and the snapshot's read set.Everything else uses the loader's text.
Compatibility with #2025: this PR adds no reads above the repository root. Outside sums are bounded by the checkout. The tests that use a nested project now create a
.gitbeside it, so they stay valid once ancestor go.work files outside the repository are refused.New tests: 4 core and 2 CLI. All 6 fail on 3f452d1 and pass now.
Note
Medium Risk
Changes Go patch discovery and VEX attestation semantics for workspace layouts; incorrect logic would mis-state which builds run patched code, though the design keeps wiring visible while withholding statements when the workspace blocks the replace.
Overview
Go VEX discovery now loads the workspace the same way hosted/vendored modes do (
go_workspace::load_with): nearest/GOWORKworkspace, members outside the project, module graph, and sum files inside the repo that govern pins. Replaces the old behavior that only read the rootgo.workand could attest rootgo.modwiring the real workspace overrides or ignores.Socket replaces the workspace keeps from the build stay discoverable for
list/rollback/remove, but areUnattestedKind::GoWorkspaceOverridesovexomits them withgo_workspace_overrideinstead of treating them as fully invalid or falsely attested.GOWORKpointing at another readable workspace now follows that model (attest when the writers would write; otherwise unattested) rather than withholding every ref aspatched_ref_invalid.The VEX plan treats that mark as file-scoped: only the bypassed file drops out of the attestation basis; a live
go.modmirror can still attest, and bypassed refs no longer triggerwiring_conflict. Replaces of a module that is itself ausemember are diagnosed inert before bypass logic runs.PathSpace::Memoryruns workspace conflict checks lexically on in-memory project views without touching the filesystem. CLI contract and ecosystem docs document the new Go workspace and omission behavior;GO_WORKSPACE_OVERRIDEis centralized invex_sources.Reviewed by Cursor Bugbot for commit 1529210. Configure here.
Generated by Claude Code