fix(review): join the changeset to native-separator graph paths - #646
Conversation
reviewRulepackMatches builds the changed-file key correctly and then
looks it up with filepath.Clean, which rewrites the one separator a graph
path keeps as '/': the repo prefix. Probed on windows/amd64:
changed map key "repo-a/pkg\widget.go"
target.GraphPath "repo-a/pkg\widget.go" <- identical
filepath.Clean(GraphPath) "repo-a\pkg\widget.go" <- prefix slash lost
hit false
So `review` narrowed the rulepack to an empty target set and reported
zero findings on code its own detector bundle flags — the same false
clean the join was written to fix, reintroduced by the Clean.
Compare in graphpath.Norm form on both sides instead. Norm is the
canonical comparison spelling for a path that is "<prefix>/" plus a
native remainder, and it is filepath.ToSlash, so this is the identity on
POSIX.
Second half, same contract: reviewRepoRelPath feeds the `.gortex.yaml`
rule globs, rankFileRisk's row keys and the forge comment API, all of
which speak '/'. It returned `pkg\widget.go` on Windows, which matches no
glob and anchors no comment. Normalize there too.
Whole package on windows before/after: 27 -> 24 failures, exactly the
three rulepack tests flip, nothing else changes state. Each half is
separately load-bearing: reverting the join fails all three, reverting
the output normalization fails the two that assert m.File.
graphpath.Norm is deliberately a no-op on POSIX — a backslash is an
ordinary filename byte there — so no test can bind this on the
linux/macos matrix. The three names join the existing windows
native-separator step, whose package list already carries internal/mcp.
zzet
left a comment
There was a problem hiding this comment.
Requesting changes after an additional path-provenance validation.
Blocking correctness issue at internal/mcp/tools_review.go:700:
With repoPrefix repo-a and a legitimate Git-relative changed path repo-a/pkg/widget.go, normalization produces the same string. The HasPrefix check then treats it as already graph-prefixed, although the correct graph key is repo-a/repo-a/pkg/widget.go. This misses the changed target and, when pkg/widget.go also exists, can scan that unchanged shadow target instead. The ambiguity already exists on POSIX and this change newly exposes the wrong-target behavior on Windows.
Please address the following:
- Carry explicit path provenance for Git-relative versus graph-keyed paths instead of inferring it with HasPrefix.
- Prefix production MapGitDiff paths unconditionally, or otherwise make the input domain explicit.
- Add a regression fixture containing both pkg/widget.go and repo-a/pkg/widget.go, mark only the nested path changed, and assert that only the changed target is scanned.
- Rerun the focused Windows native-separator validation and the existing CI suite.
Adding both possible graph keys is not a safe workaround because it can still select unchanged code.
Addresses the review on zzet#646. reviewChangedGraphPaths inferred "this path is already graph-keyed" from strings.HasPrefix(f, repoPrefix+"/"). The two domains overlap, so that is not recoverable by inspection: in a repo whose tree carries a top-level directory named like the repo prefix, `repo-a/pkg/widget.go` is a valid git-relative path AND a valid graph key for a different file. The inference skips the real key `repo-a/repo-a/pkg/widget.go`, so the changed file is never scanned - and when a same-named `pkg/widget.go` exists, that unchanged shadow is scanned in its place. Make the domain explicit and travel with the data: changedPathsRepoRelative git's spelling, relative to the working tree changedPathsGraphKeyed the graph's "<prefix>/<rel>" node key All four production callers pass DiffResult.ChangedFiles, which MapGitDiff documents as keeping "the diff-relative paths (callers re-join them with git pathspecs)", so they pass changedPathsRepoRelative and the prefix is now applied unconditionally. Only the test covering a caller that already holds node keys passes changedPathsGraphKeyed. The Norm-based join and output normalization from the first revision are unchanged; this only replaces the inference. Regression test: a fixture carrying both pkg/widget.go and repo-a/pkg/widget.go, only the nested path marked changed, asserting every match reports the changed target. Both files carry the detector fixture, so a wrong-target scan still returns matches and only the reported path separates the outcomes - restoring the HasPrefix inference fails it with "pkg/widget.go" is the unchanged shadow. The ci.yml selector hunk is dropped: zzet#652 removes that job outright.
|
You were right, and the failure mode reproduces exactly as you described. Pushed I verified the claim before changing anything: restoring the — the changed 1 + 2 — explicit provenance, unconditional prefixingThe domain now travels with the data instead of being recovered from it: changedPathsRepoRelative // git's spelling, relative to the working tree
changedPathsGraphKeyed // the graph's "<prefix>/<rel>" node key
On "prefix production MapGitDiff paths unconditionally" — that is now what happens, and the contract already supported it. All four production callers ( 3 — regression fixture
Both files carry the detector fixture deliberately — otherwise a wrong-target scan would return empty and the test would pass for the wrong reason. With both seeded, the wrong target still produces matches and only the reported path separates the outcomes. 4 — validation
One thing I removedThe Also worth flagging since it is upstream of this: the ambiguity is in the data model, not only in this function. |
zzet
left a comment
There was a problem hiding this comment.
Second review round — requesting changes.
The rulepack narrowing fix itself is correct, but the prefix-shadow case is still incorrect end to end.
- ChangedSymbols still bind to the unchanged shadow.
MapGitDiff passes Git-relative hunk paths to analysis.JoinFileNodes (internal/analysis/diffmap.go:109-117). JoinFileNodes tries the raw path first and returns it before trying the prefixed key. With repoPrefix repo-a and changed Git path repo-a/pkg/widget.go, the raw key repo-a/pkg/widget.go belongs to the unchanged pkg/widget.go shadow; the real changed graph key is repo-a/repo-a/pkg/widget.go. If the shadow exists it is returned, and if it does not exist the HasPrefix branch returns nil. The correct key is never tried.
That contaminates diff.ChangedSymbols. review and review_pack then derive impact, contracts, guards, test targets, classification, previews, receipts, and potentially the verdict from the unchanged file or from no symbols at all.
- File-risk normalization repeats the domain collision.
internal/review/report.go:123-129 uses one normalizer that strips repoPrefix from graph-keyed ChangedSymbol.FilePath values and from already repo-relative findings and ChangedFiles. In the same case, the legitimate repo-relative repo-a/pkg/widget.go becomes pkg/widget.go. This attributes risk to the shadow path; after fixing JoinFileNodes it can also produce two inconsistent risk rows and incorrect finding counts/verdict input.
The new TestReviewRulepackMatches_PrefixShadowedPathScansOnlyTheChangedTarget directly calls reviewRulepackMatches, so it bypasses MapGitDiff, JoinFileNodes, rankFileRisk, and review_pack gates. The existing end-to-end test has no prefix-shadow fixture. Green CI therefore does not exercise either failure.
Please address before merge:
- Carry explicit repo-relative provenance through MapGitDiff/joinHunksToSymbols and prefix that domain unconditionally when resolving graph nodes. Audit JoinFilePath and the other JoinFileNodes callers rather than changing its mixed-domain contract implicitly.
- Normalize file-risk inputs by domain: strip the graph prefix only from graph-keyed symbol paths, not from repo-relative findings or ChangedFiles.
- Add end-to-end review and review_pack tests containing both pkg/widget.go and repo-a/pkg/widget.go, changing only the nested file. Assert the finding, ChangedSymbols, FileRisk, contracts/guards/test targets, previews, receipt, and verdict all refer only to repo-a/pkg/widget.go. Include deleted/renamed old-side paths if the join remains shared with those flows.
Non-blocking cleanup: changedPathsGraphKeyed currently has no production caller; only its test reaches that branch.
… file risk Second review round on zzet#646. 1. JoinFileNodes resolved the wrong file. It tried the raw key first and the prefixed key second, so a git-relative "<prefix>/<rel>" resolved against the same-named top-level file — and when that shadow did not exist the HasPrefix branch returned nil, never trying the real key "<prefix>/<prefix>/<rel>". That contaminated DiffResult.ChangedSymbols, which review and review_pack derive impact, classification, risk, receipts and the verdict from. The domain now travels with the path, as analysis.PathDomain, and a repo-relative path is prefixed unconditionally. All five production call sites were audited and every one is repo-relative: joinHunksToSymbols (hunks and vanished paths), changedSymbolsForFiles (forge file list) and suggest_reviewers (its own comment already said so). Nothing depended on the mixed contract. GraphKey also converts to native separators. The key's shape is "<prefix>/" + the remainder as the indexing machine spells it (see internal/graphpath), so a '/'-spelled git path missed every file below the repo root on Windows — the shadow fixture proved it, resolving 0 nodes before and 4 after. JoinFilePath collapsed into GraphKey: with the domain explicit it was the same function, and probing the store to pick between two candidate keys is what made it ambiguous. 2. File risk repeated the collision. rankFileRisk ran one normalizer over every input, stripping the repo prefix from graph-keyed symbol paths and from repo-relative findings and ChangedFiles alike, so a legitimate "repo-a/pkg/widget.go" became "pkg/widget.go" and the risk landed on the shadow. Split by domain: fromGraphKey strips, fromRepoRel does not. 3. Tests. End-to-end review and review_pack tests drive MapGitDiff, JoinFileNodes, rankFileRisk and the pack gates over a repo whose tree carries a top-level directory named like the repo prefix. Both files carry the flagged source at the base commit and only the nested one is edited — inside the function, so the hunk overlaps a symbol — so a wrong-target join still produces findings and only the attributed path separates the outcomes. Fixture paths are deliberately flat: a nested spelling differs by separator on Windows and would mask the defect there. Existing tests that encoded the old tolerance were rewritten rather than deleted: TestJoinFileNodes and TestChangedSymbolsForFiles_RepoPrefixJoin now assert the shadow resolves to the nested file, and their fixtures build graph keys the way the indexer does instead of hard-coding the '/'-joined form. changedPathDomain folded into analysis.PathDomain so one vocabulary spans both packages. Windows: internal/mcp 27 -> 24 against main, failing test names diffed, newly-broken set empty. The seven internal/review failures are unchanged and pre-existing (identical with this change stashed). Lint: 6 staticcheck findings, also pre-existing.
|
Both are real and both are fixed. Pushed 1 —
|
| call site | source |
|---|---|
joinHunksToSymbols — hunk paths |
diff parsing |
joinHunksToSymbols — vanished |
FileChange.Path / PreviousPath |
changedSymbolsForFiles (tools_prs.go) |
forge file list |
suggest_reviewers ×2 |
its own comment already said "the changed-file paths are repo-relative (git / forge)" |
The domain now travels as analysis.PathDomain and a repo-relative path is prefixed unconditionally. JoinFilePath collapsed into GraphKey — with the domain explicit it was the same function, and probing the store to choose between two candidate keys is what made it ambiguous in the first place.
One thing I found while building the fixture that is worth flagging: GraphKey also has to convert separators. The key is "<prefix>/" + the remainder as the indexing machine spells it, so a /-spelled git path misses everything below the repo root on Windows. Probed on the real fixture:
GetFileNodes("repo-a/repo-a/pkg/widget.go") -> 0 nodes
GetFileNodes("repo-a/repo-a\pkg\widget.go") -> 4 nodes <- what the indexer wrote
Without that, ChangedSymbols came back empty rather than wrong — a different failure, same root.
2 — file-risk normalization
Split by domain: fromGraphKey strips the prefix (ChangedSymbols), fromRepoRel does not (findings, ChangedFiles). TestRankFileRiskNormalizesRepoPrefix still merges the two spellings onto one row, which was the behaviour that normalizer existed for.
3 — end-to-end tests
TestReview_PrefixShadowAttributesOnlyTheChangedFile and TestReviewPack_PrefixShadowAttributesOnlyTheChangedFile drive the real pipeline — MapGitDiff → JoinFileNodes → rankFileRisk → the pack gates — over a repo whose tree carries a top-level directory named like the prefix. Both files carry the flagged source at the base commit and only the nested one is edited, inside the function so the hunk overlaps a symbol, so a wrong-target join still yields findings and only the attributed path separates the outcomes. They assert findings, ChangedSymbols (against the real graph key, not a /-joined guess), FileRisk and verdict.
Two fixture decisions worth stating, because both were mistakes I made first:
- Flat paths, not nested. With
pkg/widget.gothe changed git path and the shadow's graph key differ by separator on Windows, so the raw-lookup defect is masked here. One segment makes them coincide. - The edit is inside
Load. My first version touched line 1; the file changed, no symbol did, andChangedSymbolswas empty for a reason unrelated to the join.
Limitation I would rather state than have you find: on Windows I cannot sabotage-verify the e2e pair — hunk.FilePath is filepath.Cleaned to repo-a\widget.go, which never matches a /-joined key, so restoring raw-first is masked locally. The linux/macos shards are where they bind. What I can verify here is the unit level: restoring raw-first fails TestJoinFileNodes with "prefix-shadowed repo-relative path must resolve to the nested file", and that fixture is platform-independent.
Existing tests
Rewritten, not deleted. TestJoinFileNodes and TestChangedSymbolsForFiles_RepoPrefixJoin asserted the old "already-prefixed hits raw" tolerance — the defect itself — so they now assert the shadow resolves to the nested file. Their fixtures also build graph keys the way the indexer does instead of hard-coding the /-joined form, which is a POSIX-only spelling.
Validation
internal/mcpon windows: 27 → 24 againstmain, failing test names diffed, newly-broken set empty.internal/review: 7 failures, unchanged and pre-existing — byte-identical with this change stashed. They arecleanPath/filepath.Cleanpath-spelling assertions, not this change.- Focused native-separator selector:
okacrossanalysis,store_sqlite,mcp,resolver,persistence. golangci-lint: 6 staticcheck findings, also pre-existing.
Non-blocking cleanup
changedPathsGraphKeyed is gone — folded into analysis.PathDomain so one vocabulary spans both packages, and GraphKeyedPath now has a real consumer in JoinFileNodes' contract.
|
CI is red but not on this change. It needs a re-run, which I cannot trigger from a fork. The failure is a wall-clock ratio assertion in a package this PR does not touch:
The run's own numbers show the measurement was unstable. Locally on windows/amd64, go1.26.6,
Three passes, right at the expected ~10x, and the naive walk consistently grows more than the iterator — the opposite of what CI recorded. Two notes on reading that run, since one of them cost me a detour:
If you would rather not re-run on faith, I am happy to send the scaling test's stabilisation as its own PR — the honest fix is to compare against a same-machine baseline rather than an absolute ratio, or to raise |
zzet
left a comment
There was a problem hiding this comment.
Third review round — requesting changes.
The previous prefix-shadow fixes are correct in the main review paths, but two blocking correctness issues remain at head a7ca67b.
-
High —
suggest_reviewers(ids=…)double-prefixes graph-keyed paths.resolveReviewerChangeset'sidsbranch returnsn.FilePathverbatim (internal/mcp/tools_suggest_reviewers.go:142-153), so those entries are already graph-keyed. The new ownership and co-change lookups at lines 100 and 111 nevertheless forceanalysis.RepoRelativePath.With prefix
repo-a, a node pathrepo-a/pkg/a.gois looked up asrepo-a/repo-a/pkg/a.go. Recent-author and co-change reviewer signals disappear. The existing IDs test uses an unprefixed graph, so it cannot expose this regression.Please either normalize the IDs branch to repo-relative paths or return/carry
PathDomainwith the files. Add a prefixed-graph IDs test that verifies CODEOWNERS, recent-author, and co-change signals. -
High — Windows file-risk normalization still produces duplicate/wrong rows.
In
internal/review/report.go:131-138,fromGraphKeycallscleanPathbeforestrings.TrimPrefix(file, repoPrefix+"/").cleanPathusesfilepath.Clean. On Windows:repo-a/repo-a\widget.go -> repo-a\repo-a\widget.goThe forward-slash prefix no longer matches. Changed-symbol impact/coverage stays on a graph-prefixed row while findings and
ChangedFilescreate a second repo-relative row.Normalize both domains to slash/
graphpath.Normform before removing the normalized prefix, and return the documented repo-relative/spelling. Strengthen both prefix-shadow end-to-end tests to require exactly oneFileRiskrow equal to the changed file; the currentNotEqual(shadow)assertion permits both erroneous rows. -
Windows CI does not exercise these contracts.
The selector in
.github/workflows/ci.yml:101-106does not runTestGraphKey,TestJoinFileNodes,TestChangedSymbolsForFiles_RepoPrefixJoin, or either new prefix-shadow end-to-end test. It also omits./internal/review, where the second blocker lives. Please extend the Windows coverage accordingly.
Non-blocking: GraphKeyedPath currently has only test callers; fixing the IDs path should give it a real production use. No new security/authentication/secret issue was found.
… stripping Third review round on zzet#646. 1. suggest_reviewers(ids=...) double-prefixed graph-keyed paths. resolveReviewerChangeset's three sources do not share a vocabulary: ids returns graph node FilePaths, base (git) and number (forge) return repo-relative paths. Forcing analysis.RepoRelativePath on all three looked up repo-a/pkg/auth/login.go as repo-a/repo-a/pkg/auth/login.go, so the ownership and co-change signals disappeared while CODEOWNERS - which matches the repo-relative spelling - still answered and the tool looked healthy. The resolver now returns the domain with the files and the two lookups use it. That also gives GraphKeyedPath its first production caller. My round-two audit missed this: I read the call site's comment, which says the paths are repo-relative, without walking all three producers feeding it. The comment was true for two of them. 2. File risk stripped the prefix after filepath.Clean. fromGraphKey called cleanPath first, and cleanPath ends in filepath.Clean, so on Windows repo-a/repo-a\widget.go became repo-a\repo-a\widget.go and the '/'-joined prefix no longer matched. Changed-symbol impact stayed on a graph-prefixed row while findings and ChangedFiles produced a second repo-relative row for the same file. Both domains now normalize through graphpath.Norm before the strip and leave in the documented repo-relative '/' spelling. This also fixes three pre-existing Windows failures in internal/review: TestRankFileRiskUsesImpact, TestRankFileRiskNormalizesRepoPrefix and TestRankFileRiskExemptsTestFilesFromCoverageDebt. The package goes from seven failures to four; the remaining four are untouched by this change. 3. Windows coverage. The selector gains GraphKey, JoinFileNodes, ChangedSymbolsForFiles, both prefix-shadow end-to-end tests, the new ids test and RankFileRisk, and ./internal/review joins the package list. Tests. The new prefixed-graph ids test asserts all three reviewer signals survive; the existing ids tests use an unprefixed graph where both domains coincide, which is why they could not see the regression. Both prefix-shadow end-to-end tests now require exactly one FileRisk row equal to the changed file - the previous NotEqual(shadow) assertion passed even when both erroneous rows were present. Each fix is sabotage-verified separately: restoring the ids branch to RepoRelativePath leaves only the CODEOWNERS reviewer, and restoring cleanPath-before-strip produces exactly the two rows described above. Also audited, and clean: every other JoinFileNodes / GraphKey caller. changedSymbolsForFiles is fed only by forge PR file lists through resolvePRFiles and prImpactForNumber, so its repo-relative domain is correct. The only two remaining repoPrefix strips both normalize first and both have an established domain.
|
Both blockers confirmed and fixed, plus the coverage gap. Pushed 1 —
|
| source | domain |
|---|---|
ids |
graph node FilePath — graph-keyed |
base |
diff.ChangedFiles — repo-relative |
number |
forge file list — repo-relative |
My round-two audit read the call site's comment ("the changed-file paths are repo-relative (git / forge)") and stopped there. That comment is true for two of the three producers, and I never walked the third.
The resolver now returns the domain with the files and both lookups use it. That also gives GraphKeyedPath its first production caller, which closes your non-blocking note.
New TestSuggestReviewers_IdsOnPrefixedGraphKeepsAllSignals builds a prefixed graph and asserts CODEOWNERS, recent-author and co-change all survive. Sabotage-verified — forcing the ids branch back to RepoRelativePath:
recent-author signal missing … [{Reviewer:org/secteam Kind:team Reasons:[CODEOWNERS for 1 file]}]
Only CODEOWNERS survives, exactly as you predicted — and exactly why the tool looked healthy.
2 — file-risk normalization
fromGraphKey called cleanPath (which ends in filepath.Clean) before the strip. Both domains now go through graphpath.Norm first and leave in the documented repo-relative / spelling.
Sabotage-verified with the strengthened assertion — restoring cleanPath-before-strip:
[{repo-a\repo-a\widget.go MEDIUM 0} {repo-a/widget.go LOW 1}]
should have 1 item(s), but has 2
Two rows for one file: impact/coverage on the graph-prefixed row, the finding on the repo-relative one. Your point about NotEqual(shadow) was the important half — the old assertion passed on that output.
Bonus: this also fixes three pre-existing Windows failures in internal/review — TestRankFileRiskUsesImpact, TestRankFileRiskNormalizesRepoPrefix, TestRankFileRiskExemptsTestFilesFromCoverageDebt. The package goes 7 → 4; the remaining four (TestVerificationCommand, TestRunRelocatesAndBlocks, both TestRenderSummary_*Golden) are untouched by this change and were already red.
3 — Windows coverage
Selector gains GraphKey|JoinFileNodes|ChangedSymbolsForFiles|PrefixShadow|IdsOnPrefixedGraph|RankFileRisk, and ./internal/review joins the package list.
A correction to what I told you last round: I said the ci.yml selector hunk was dropped from this branch. It was not — it was still there from the first revision, which is what you were reading at ci.yml:101-106. I misreported that; sorry.
The audit you asked for, done properly this time
Rather than trusting one comment again, I walked every producer for every consumer of the domain-carrying API:
changedSymbolsForFilesis fed only byresolvePRFilesand the forge fetch inprImpactForNumber— four call sites, all forge PR file lists, all repo-relative. Its hardcodedRepoRelativePathis correct.- The only two remaining
TrimPrefix(…, repoPrefix+"/")sites arereport.go'sfromGraphKeyandreviewRepoRelPath; both normalize first and both have an established domain.
Validation
- Exact Windows selector, all six packages:
ok. internal/mcpwhole package: 27 → 24 againstmain, failing test names diffed, newly-broken set empty.internal/analysis:ok.internal/review: 4, down from 7.golangci-lint: 6 staticcheck findings, all pre-existing.
One environment note on my numbers: Go 1.27 landed on this machine mid-session (for #654), so I pinned every figure above with GOTOOLCHAIN=go1.26.6 to match this branch's go.mod and your CI, rather than reporting results from a toolchain CI does not run.
zzet
left a comment
There was a problem hiding this comment.
Fourth review round — requesting changes at head a1a5b87.
The previous IDs-mode double-prefixing and file-risk normalization blockers are fixed. Two path-domain correctness gaps remain:
-
High — internal/analysis/diffmap.go:113: GraphKey returns immediately when repoPrefix is empty, before filepath.FromSlash. On standalone Windows, forge paths such as pkg/auth/x.go remain slash-spelled and miss graph keys stored as pkg\auth\x.go. This silently drops changed symbols and reviewer signals in get_pr_impact and suggest_reviewers(number=...). Only GraphKeyedPath should return unchanged; RepoRelativePath should always pass through filepath.FromSlash and then conditionally receive the prefix. Add a multi-segment empty-prefix Windows regression test; the current a.go fixture cannot expose this.
-
Medium — internal/mcp/tools_suggest_reviewers.go:82: IDs mode returns graph-keyed paths, but CODEOWNERS still receives that spelling through relForRepo, which only strips an absolute repo root. A path such as repo-a/pkg/auth/login.go is passed instead of pkg/auth/login.go, so root-anchored rules such as /pkg/auth/ do not match. Convert the path to repo-relative form according to fileDomain before MatchFile and matched_files, and cover it with a root-anchored CODEOWNERS rule. The current unanchored pkg/auth/ fixture masks the mismatch.
Test gap: the deleted-file and renamed PreviousPath joins still have no prefixed/prefix-shadow regression cases. Their existing tests use an empty prefix, so they do not bind the new RepoRelativePath contract. Add prefixed delete and rename cases that resolve only the nested old-side graph key and include them in the Windows selector.
The first two behaviors also existed on the base branch, but they are directly inside the GraphKey/PathDomain contract introduced and exercised by this PR, whose purpose is cross-platform native-separator correctness. Current CI is green because the fixtures do not cover these cases. No new security or dead-code concern was found.
…nd CODEOWNERS the repo-relative spelling Fourth review round on zzet#646. 1. GraphKey returned early on an empty repo prefix, before filepath.FromSlash. A standalone index carries no prefix but its keys still use native separators, so a '/'-spelled forge or git path missed them on Windows and get_pr_impact / suggest_reviewers(number=...) silently lost changed symbols and reviewer signals. Only GraphKeyedPath returns unchanged now; RepoRelativePath always converts, then takes the prefix when one applies. 2. CODEOWNERS received a graph key in ids mode. relForRepo only strips an absolute repo root, so repo-a/pkg/auth/login.go reached MatchFile with the prefix still on and a root-anchored rule such as /pkg/auth/ matched nothing. matched_files reported the same spelling back to the caller. Both now go through the new analysis.RepoRelPath, GraphKey's inverse: it strips the prefix only for a GraphKeyedPath, because that is the one domain that carries it by construction. 3. Tests. - Empty-prefix multi-segment cases for GraphKey and JoinFileNodes. The previous a.go fixture has no separator, so it cannot fail either way. - The ids CODEOWNERS fixture is now root-anchored (/pkg/auth/). The unanchored rule also matched the graph-keyed spelling and masked the bug. - TestJoinHunksToSymbolsPrefixedDeleteAndRename covers the two hunk-less change kinds against a prefix-shadowed graph: the delete's Path and the rename's PreviousPath must resolve the nested old-side key and never the same-named shadow. Their existing tests use an empty prefix and so did not bind this contract. - The Windows selector gains PrefixedDeleteAndRename. Each fix is sabotage-verified separately: forcing the ids branch back to a raw graph key leaves only the CODEOWNERS-less reviewers, passing the graph key to MatchFile drops the codeowner signal entirely, and declaring the vanished path GraphKeyedPath makes the delete/rename test miss both old-side symbols. Four fixtures needed the same correction as TestChangedSymbolsForFiles did earlier: prToolsTestServer, conflictsTestServer and conflictsBudgetServer hard-coded '/'-spelled graph keys while supplying the same strings as forge paths, which describes an index a Windows daemon never writes. They now store native and supply the forge spelling, which is the shape under test. Windows: internal/mcp 27 -> 24 against main, failing test names diffed, newly-broken set empty. internal/analysis passes clean. internal/review keeps its four pre-existing failures. Lint: 6 staticcheck findings, pre-existing.
|
Both blockers and the test gap are addressed. Pushed 1 —
|
Problem
reviewRulepackMatchesbuilds the changed-file key correctly, then looks it up withfilepath.Clean— which rewrites the one separator a graph path deliberately keeps as/: the repo prefix.Probed on windows/amd64 with the existing fixture:
The narrowing therefore produced an empty target set and
reviewreported zero findings on code its own detector bundle flags underanalyze --kind review. That is the same false cleanreviewChangedGraphPathswas written to fix — its doc-comment says so — reintroduced one line later by theClean.Change
Two halves of one contract, both in
tools_review.go:The join. Compare in
graphpath.Normform on both sides.Normis the canonical comparison spelling for a path shaped"<prefix>/"+ native remainder, and it isfilepath.ToSlash, so this is the identity on POSIX.The output.
reviewRepoRelPathfeeds the.gortex.yamlrule globs,rankFileRisk's row keys, and the forge comment API — the function's own doc-comment lists them, and all three speak/. On Windows it returnedpkg\widget.go, which matches no glob and anchors no comment. Normalize there too.Effect
Whole
internal/mcppackage on windows/amd64, go1.26.6,-count=1:main(c982cf52)Diffing failing test names: exactly three flip, newly-broken set empty.
Verification
Each half sabotage-verified separately, because a fix that only looks necessary is not:
m.Filefail onNot equal.golangci-lint run ./internal/mcp/...reports 6staticcheckSA5011 findings; pre-existing, identical with this change stashed.git diff --checkclean.Why there is no cross-platform test
graphpath.Normisfilepath.ToSlash, deliberately a no-op on POSIX — the package doc is explicit that a backslash is an ordinary filename byte there and must survive. So a test feeding a backslash path and asserting a slash result would fail on linux/macos, and one built withfilepath.FromSlashasserts nothing on either. The three names therefore join the existing windows native-separator step, whose package list already carriesinternal/mcp— no new step, no new compile.Declared, not swept under
TestReviewPackNeverClaimsNoTestSymbolsWithTestTargetsfails on windows too and I deliberately left it out: it fails ontest_targetsbeing empty, not on the path join, andtestpath.IsTestFilealready normalizes separators itself — so it is a different cause and belongs in its own change rather than riding along here.Branched from
mainatc982cf52. Windows 11, go1.26.6.