-
Notifications
You must be signed in to change notification settings - Fork 0
fix: make reviewer coverage resilient (exempt lockfiles; degrade over-long constraints) #567
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
41fcdf5
6d0772e
7157adb
5fdbceb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1576,6 +1576,12 @@ func ensureSelectedGlobCoverage(selection llm.Selection, catalog agents.Catalog, | |
| if covered[file] { | ||
| continue | ||
| } | ||
| // Generated lockfiles are not a coverage obligation (see | ||
| // buildReviewerCoverage): don't force-assign one to an agent, or its | ||
| // prompt scope would list a file the accounting layer then exempts. | ||
| if isGeneratedLockfile(file) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This revision adds two new lockfile-exemption branches beyond the one covered by TestBuildReviewerCoverageExemptsGeneratedLockfiles, and neither has a regression test: (1) here in ensureSelectedGlobCoverage, an uncovered lockfile matching a full-scope agent's globs is now deliberately skipped instead of force-assigned — no test drives a changed-file set where a lockfile (e.g. Cargo.lock) matches an agent's FileGlobs, so a future edit that removes or reorders this Reply inline to this comment. |
||
| continue | ||
| } | ||
| for i := range selection.SelectedAgents { | ||
| selected := &selection.SelectedAgents[i] | ||
| candidate, ok := agentByID[selected.AgentID] | ||
|
|
@@ -2437,10 +2443,56 @@ func reviewerToolEvidenceByAgent(sessions []sessionDraft) map[string]*llm.Review | |
| return out | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. File-level note: internal/pipeline/pipeline.go docs/checkout-native-review-contract.md is the documented source of truth for reviewer coverage semantics: it defines "readable files" as "all changed files in the workbench" and states unassigned/skipped files "must not turn into a clean approval silently." This diff's buildReviewerCoverage now filters generated lockfiles out of changedFiles (and each agent's scope) before computing coverage, so lockfiles are silently excluded from the readable-files universe the doc describes — but the doc's coverage-status section (lines ~337-354) was not updated to mention the lockfile exemption or the new filterReviewableFiles/isGeneratedLockfile step. A future agent or engineer reading that doc to understand what "complete" coverage guarantees will get a materially misleading picture of what the harness actually enforces. Update docs/checkout-native-review-contract.md to document the lockfile exemption alongside the existing coverage-status definitions. Reply inline to this comment. |
||
| } | ||
|
|
||
| // generatedLockfiles are dependency lockfiles: machine-written by a package | ||
| // manager and reviewed (if at all) through the manifest change that produced | ||
| // them, never line by line. A reviewer that skips one is behaving correctly, so | ||
| // they are excluded from the coverage universe — otherwise a skipped lockfile | ||
| // marks the reviewer incomplete_skipped and blocks approval on an otherwise | ||
| // clean review. (A real PR stalled exactly this way: a Cargo.lock churned by a | ||
| // dependency bump was the only file left "unreviewed".) | ||
| var generatedLockfiles = map[string]bool{ | ||
| "Cargo.lock": true, | ||
| "package-lock.json": true, | ||
| "npm-shrinkwrap.json": true, | ||
| "yarn.lock": true, | ||
| "pnpm-lock.yaml": true, | ||
| "bun.lockb": true, | ||
| "go.sum": true, | ||
| "Gemfile.lock": true, | ||
| "poetry.lock": true, | ||
| "Pipfile.lock": true, | ||
| "composer.lock": true, | ||
| "Podfile.lock": true, | ||
| "flake.lock": true, | ||
| "mix.lock": true, | ||
| } | ||
|
|
||
| // isGeneratedLockfile reports whether path is a dependency lockfile a reviewer | ||
| // is not expected to read line by line. | ||
| func isGeneratedLockfile(path string) bool { | ||
| return generatedLockfiles[filepath.Base(path)] | ||
| } | ||
|
|
||
| // filterReviewableFiles drops generated lockfiles from a file list so they do | ||
| // not become a coverage obligation. | ||
| func filterReviewableFiles(files []string) []string { | ||
| out := make([]string, 0, len(files)) | ||
| for _, file := range files { | ||
| if isGeneratedLockfile(file) { | ||
| continue | ||
| } | ||
| out = append(out, file) | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| func buildReviewerCoverage(selected []llm.SelectedAgent, results []llm.Findings, failures []ReviewerFailure, changedFiles []string, toolEvidence ...map[string]*llm.ReviewerToolEvidence) []reviewplan.ReviewerCoverageSummary { | ||
| if len(selected) == 0 && len(changedFiles) == 0 { | ||
| return nil | ||
| } | ||
| // Generated lockfiles are not a review obligation: exclude them so neither a | ||
| // reviewer that skips one nor an unassigned lockfile blocks approval. | ||
| changedFiles = filterReviewableFiles(changedFiles) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. U-S1: the exemption is applied at the accounting layer only, so the repo now answers "what is in the coverage universe?" in two places that disagree. Choosing the accounting layer is the right call — filtering Reply inline to this comment. |
||
| resultByAgent := make(map[string]llm.Findings, len(results)) | ||
| for _, result := range results { | ||
| resultByAgent[result.AgentID] = result | ||
|
|
@@ -2452,7 +2504,9 @@ func buildReviewerCoverage(selected []llm.SelectedAgent, results []llm.Findings, | |
| assigned := map[string]bool{} | ||
| out := make([]reviewplan.ReviewerCoverageSummary, 0, len(selected)+1) | ||
| for _, agent := range selected { | ||
| scope := reviewerAssignmentScope(agent, changedFiles) | ||
| // A lockfile explicitly assigned to an agent is exempt too — the scope | ||
| // is what the reviewer is held to, and lockfiles are not reviewable. | ||
| scope := filterReviewableFiles(reviewerAssignmentScope(agent, changedFiles)) | ||
| for _, file := range scope { | ||
| assigned[file] = true | ||
| } | ||
|
|
@@ -2473,7 +2527,10 @@ func buildReviewerCoverage(selected []llm.SelectedAgent, results []llm.Findings, | |
| out = append(out, entry) | ||
| continue | ||
| } | ||
| entry.InspectedFiles = copySortedStrings(result.InspectedFiles) | ||
| // Draw both scope and coverage rows from the same lockfile-exempt set, | ||
| // so a reviewer that did read a lockfile doesn't emit an inspected file | ||
| // outside its scope. | ||
| entry.InspectedFiles = filterReviewableFiles(copySortedStrings(result.InspectedFiles)) | ||
| entry.SkippedFiles = sortedIntersection(result.SkippedFiles, scope) | ||
| entry.Constraints = copySortedStrings(result.Constraints) | ||
| if evidence := reviewerToolEvidenceForAgent(toolEvidence, agent.AgentID); evidence != nil && evidence.DiffStatus != llm.DiffToolStatusSucceeded { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
U-S1: sanitize-before-clamp is the right order (marker.SanitizeModelContent replaces
<!-- codereview:with a 3-rune-longer<!-- codereview:, so the old order could exceed the cap; truncation can only delete a suffix, so it cannot re-form a marker opening). The residue is thatdefaultMaxCoverageConstraintRunes-3encodes a private detail of a helper in another file —truncateRunesappends a literal"..."(adapter.go:401) — so changing that ellipsis to"…"silently makes every caller that budgets for it wrong. The comment documents the coupling rather than removing it. Consider a sibling helper next totruncateRunesthat takes a total budget (e.g.truncateRunesTotal(value, max)returning a string of at mostmaxrunes including the marker), leaving the arithmetic with the code that appends. Defensible as-is given the test at contracts_test.go:213-216 now asserts the ≤300 postcondition.Reply inline to this comment.