Compare task coverage with the base branch - #242
Conversation
johannesjo
left a comment
There was a problem hiding this comment.
Summary
The read-only base-worktree approach and explicit coverage states are solid, but I reproduced three correctness gaps against the current head (62ee9fc6).
PR Discussion Context
This implements #236. There are no existing review threads, and both the quality and GitGuardian checks pass; the cases below are not covered by the added tests.
Issues
Important
-
src/lib/coverage-comparison.ts:116 —
impactedUnchangedFilesonly iterates task-report entries and skips zero-line counterparts. If an unchanged file covered in the base report disappears from the task report, or becomesno-executable-lines, it is omitted. Removing a low-covered file can raise the aggregate, so merge readiness reports Coveragepasswhile the regression is invisible. Iterate the union of base/task paths and retain the unavailable states for unchanged files, with base-only and available→no-lines tests. -
src/components/ChangedFilesList.tsx:312, line 839 — Badge eligibility is being used as the complete changed-path/global-display scope. A changed test/declaration file present in coverage can therefore be labeled “unchanged,” and a test-only change hides the aggregate delta and impacted-file indicator from the Changed Files panel entirely. Pass all Git-changed paths into comparison bookkeeping, keep candidate filtering only for per-row badges, and gate the global footer on report/comparison availability rather than candidate count.
-
electron/ipc/git.ts:541 — Any ordinary filename containing literal
=>is parsed as rename shorthand. With a modifiedordinary => modified.txt, Git emits that exact path in both raw and numstat output, but this parser produces a phantommodified.txtrow with the stats plus the real path with zero stats, breaking counts, navigation, and coverage matching. Prefer the exact raw-status path before parsing rename shorthand, or switch these calls to-z; add a literal-arrow filename regression test.
Strengths
- Baseline discovery and report loading remain read-only.
- The IPC channel wiring is consistent, and the ordinary numeric/new/deleted/renamed cases have focused tests.
Verdict
Needs changes. The first issue can produce a false-green readiness result, while the other two make the displayed comparison inaccurate for valid inputs.
|
Addressed the review findings in 4436846:
Added regression coverage for base-only and available-to-no-lines transitions, non-source changed paths, test-only footer visibility, and literal-arrow filenames. Local |
johannesjo
left a comment
There was a problem hiding this comment.
Summary
I rechecked the current head (4436846) against #236 and the follow-up fixes. The original reported cases are addressed, but two PR-introduced correctness gaps remain. The overall design is proportionate to the feature; I found no KISS/YAGNI premise issue or reason to add a broader abstraction or dependency.
PR Discussion Context
There are no inline review threads, and both quality and GitGuardian pass. The cases below are not covered by the current tests.
Issues
Important
-
src/components/ChangedFilesList.tsx:323 —
files()is selection-scoped UI state, not a reliable base-relative comparison inventory. Reports can resolve while it is still empty, briefly classifying changed paths asimpactedUnchangedFilesin merge readiness; if inventory loading fails, that state persists. In uncommitted-only mode it permanently contains HEAD-to-working-tree rows while the reports remain whole-task versus base, so committed branch changes are labeled “unchanged” and branch-added/renamed files edited locally lose their base-relative kind and rename delta. Keep a nullable base-relativeGetChangedFilesinventory for comparison, share the all-changes result when possible, retainfiles()for displayed rows, and suppress comparison output until the canonical inventory loads. Add delayed/failed-load and uncommitted-selection regression tests. -
src/lib/coverage-comparison.ts:62 — Treat report paths as own dictionary entries. Because this head now compares every Git-changed path, a valid root file named
toString,constructor, or__proto__that is absent from a loaded report resolves throughObject.prototype; line 64 then throws onfile.lines.total, breaking the Changed Files and merge-dialog comparison. UseObject.hasOwnfor the lookup and a null-prototype result record, with a parameterized regression test for these names.
Strengths
- Base-worktree discovery and report loading remain read-only.
- The explicit coverage-state model and pure comparison helper are justified by the acceptance criteria, and the follow-up tests correctly cover the original narrow regressions.
Verdict
Needs changes. Both fixes can remain small and local, but the current behavior can misclassify common uncommitted views and throw for valid repository paths.
|
Addressed both follow-up findings in
Validation: 1,647 tests passed; typecheck, ESLint, Knip, dependency-cruiser, security-rule tests, frontend/remote builds, Electron compilation, and MCP bundling passed. |
johannesjo
left a comment
There was a problem hiding this comment.
Summary
Re-reviewed at head 1e2ae56 with a KISS/YAGNI lens. All five items from the previous two rounds are genuinely fixed — I verified each against the code, not just the changelog:
- union of base/task report paths, with
available → no-linesand base-only states retained (coverage-comparison.ts:117-135) - all Git-changed paths feed comparison bookkeeping;
isCoverageCandidateis now only a per-row badge filter (ChangedFilesList.tsx:54-74) - literal
=>filenames resolve via the exact raw-status path first (git.ts:596-599) - nullable base-relative inventory backs the comparison (
ChangedFilesList.tsx:553-562) Object.hasOwn+ null-prototype result map, with the parameterizedtoString/constructor/__proto__test
I also confirmed the no-baseline fallback is preserved exactly: with baseCoverage === null, badges resolve to the old plain 82% + coverageColor, and deleted files still suppress no cov. That was the acceptance criterion easiest to break.
I agree with the earlier assessment that there is no premise-level KISS/YAGNI objection to the feature — #236 is a specific request and the shape of the implementation matches it. The concerns below are one design gap and several local simplifications.
Fundamental Question
Is "whatever worktree is currently on the base branch" a baseline? getBranchWorktreePath finds a worktree checked out on the base branch, and readCoverageSummary reads whatever coverage file happens to sit in it. Nothing anchors that report to a commit: it may have been generated weeks ago, at a commit far from the task's merge-base, or with that worktree dirty. The task report has the same freedom. So base 82% → task 78% (-4pp) may be entirely attributable to elapsed time rather than to the task — and it now drives a merge-readiness check.
Both reports already carry generatedAt (file mtime, coverage.ts:161), so the cheap fix is available: surface both timestamps, and hold the Coverage check at neutral when the base report predates the task's merge-base. That keeps the read-only design while making the number honest about what it is.
Issues
Important
-
merge-readiness.ts:193— The aggregate warning has no materiality threshold:aggregate.delta < 0alone flips the check towarningand overall readiness toattention. Per-file impacts are gated onMATERIAL_COVERAGE_DELTA = 1, so the two halves of the same check disagree on what "regression" means.roundPercentagekeeps two decimals, so-0.01ppof drift between two independently-generated whole-repo reports produces a standing "attention" on every task. Apply the same threshold (or a smaller, explicitly named one) to the aggregate. -
ChangedFilesList.tsx:374-402— Neither the footer label,aggregateCoverageTitle(which lists onlyreportPath), nor the merge-readiness detail exposes either report'sgeneratedAt. And because the new aggregate span replaces the old one, this removes the only placegeneratedAtwas surfaced (coverageFooterTitle, line 121). Enabling the feature strictly loses the freshness signal that existed before it. Add both timestamps toaggregateCoverageTitle. -
ChangedFilesList.tsx:920—<Show when={!aggregateCoverageLabel() && touchedCoveragePct() !== null}>hides the changed-files coverage percentage (◔ 82%) whenever a base report is found, substituting the whole-repo aggregate. #236 asks to "prioritize changed/new files, while still reporting materially impacted unchanged files" — this inverts it. For a three-file task the whole-repo aggregate barely moves, so the headline number becomes the least informative one while the actionable one disappears. Render both rather than substituting. -
ChangedFilesList.tsx:553-562and:672— In uncommitted-only mode every 5s poll now issues a second fullGetChangedFiles(pinHead+detectDiffBase+ merge-basegit diff --raw --numstat, ~5 extra processes) on top ofGetUncommittedChangedFiles, andresolveBaseCoverageRootrunsgit worktree list --porcelainon every coverage poll. None of it is gated on a coverage report existing, so projects that never run coverage pay the full cost and discard the result. Gate both oncoverage() !== null || baseCoverage() !== null.Related:
setCoverage/setBaseCoverage/setComparisonFilesreceive fresh objects each poll, socoverageComparisonrebuilds the whole comparison — Set construction over every report path, plus the sort — every 5 seconds even when nothing changed, and re-firesonCoverageComparisonChangeintobuildMergeReadiness.
KISS / YAGNI
-
coverage-comparison.ts:155-163+ChangedFilesList.tsx:76-84— Two exported wrapper layers exist to perform two null checks.buildCoverageComparisonIfReadyonly convertschangedFiles === nullintonull;buildCoverageComparisonForSelectionwraps that to add the commit-hash check. Both are exported solely so tests can reach them. Inlining into the memo deletes ~15 lines, one exported symbol, and a test block:const coverageComparison = createMemo(() => { const inventory = comparisonFiles(); if (!inventory || isCommitHashSelection(props.selectedCommit)) return null; return buildCoverageComparison(coverage(), baseCoverage(), inventory); });
-
ChangedFilesList.tsx:64-74—shouldShowCoverageFooter(selectedCommit, count, hasCoverageArtifact, hasBaseCoverageArtifact)takes four positional params, the last two both booleans and adjacent — transposing them at the call site is silent. The test readsshouldShowCoverageFooter(undefined, 0, true, true), which conveys nothing. The body is!isCommitHashSelection(sel) && (count > 0 || hasTask || hasBase); either inline it or take an options object, matchingchangedFilesFromMapsin this same PR. -
git.ts:539-561—parseNumstatPathreturnspreviousPath, but that value is already set from the--rawR/C lines atgit.ts:582-585, which is the authoritative source and always precedes the numstat block in combined output. Line 602 just rewrites the same value. The function only needs to return the destination path, which drops thepreviousPathconstruction from both branches.Broader (follow-up, not a blocker):
-zremains the simpler route. It removes rename shorthand, quoting, and escaping from the wire format entirely — deletingparseNumstatPath, thestatusMap.has(normalizedPath)ambiguity heuristic, and the quote/backslash handling innormalizeStatusPath. It would also fix a pre-existing bug this PR makes more consequential:normalizeStatusPathunescapes\\(.)without decoding octal, so with the defaultcore.quotePath=truea path like"src/caf\303\251.ts"becomessrc/caf303251.ts— which now silently reads as "not present" in coverage matching. Honest accounting:-ztouches the shared parser and all four callers, so it is a real refactor, not one flag.
Minor
-
ChangedFilesList.tsx:613— The branch fallback lost itsuncommittedOnlyfilter (wassetFiles(uncommittedOnly ? result.filter((f) => !f.committed) : result)). SinceGetChangedFilesFromBranchmarks everythingcommitted: true, that path previously yielded[]in uncommitted view and would now render the whole branch diff as "uncommitted". Reaching it needs an emptyworktreePathplus an uncommitted selection, which I could not construct from current call sites — but it is an unexplained behavior change with a free fix: keep the filter onsetFilesand pass the unfilteredresulttosetComparisonFiles. -
ChangedFilesList.test.ts:85-90—it.each(['pending', 'failed'])ignores its parameter and asserts on the identical input twice, so it reports two passing cases while covering one. This is the test standing in for the requested delayed/failed-load coverage; the failed-load path (the.catchat line 560) is not distinguished from the initial-null path. -
MergeDialog.tsx:452,TaskChangedFilesSection.tsx:155—branchNameis added at both call sites, but the coverage code only readsprojectRoot. Since the fallback is gated onprojectRoot && branchName, adding it is precisely what enables branch-based fallback — and its permanent poll shutdown pluscanOpenFilesInEditor: false— in these two surfaces for the first time. Possibly desirable, but it is an unrelated behavior change riding along; drop it or note it in the description. -
Smaller:
base 80% → task 78% (-2pp)is prose in a footer whose other items are compact glyphs (△ 3,∅ 1,▤ 7), so it dominates the row and wraps on narrow panels; the↕ N otherbadge reusesaggregateCoverageTitle(), which truncates to three files with no way to see the rest; andgetBranchWorktreePathfilters on!entry.detachedbut only the missing-branch case is tested.
Strengths
- The read-only constraint from #236 holds end-to-end — no checkout, no worktree creation, no test execution.
- The four-state
CoverageValueStatemodel is the right shape for the acceptance criteria and keeps the UI branches flat and readable. - The
previous_pathplumbing is careful, handles both{a => b}and bare-arrow numstat forms, and correctly prefers the unambiguous raw-status path.
Verdict
Needs changes. The architecture is sound and the prior rounds' findings are resolved. What remains is that a check gating merge readiness rests on two unanchored snapshots with no materiality threshold and no freshness signal. The blocking pieces are small and local: the aggregate threshold (merge-readiness.ts:193), generatedAt in the tooltip, and a one-line Show condition at ChangedFilesList.tsx:920. The KISS items are cleanup that can land alongside.
|
Addressed the latest review in b887d66. Coverage now records the task merge-base timestamp and keeps a stale base report neutral; aggregate warnings use the existing 1pp materiality threshold; both changed-file and aggregate coverage remain visible with task/base generation times; no-report projects skip the extra comparison/base-worktree Git work; unchanged polling snapshots preserve object identity; the wrapper exports and redundant numstat previous-path parsing are removed; and branch fallback retains the uncommitted filter. Verification: 96 focused tests, full suite 1,647 passed / 23 skipped, |
johannesjo
left a comment
There was a problem hiding this comment.
Summary
Re-reviewed at head b887d66. All ten items from round 3 are genuinely fixed — I verified each against the code rather than the changelog:
| Round 3 item | Resolution at b887d66 |
|---|---|
| Aggregate warning had no materiality threshold | aggregate.delta <= -MATERIAL_COVERAGE_DELTA (merge-readiness.ts:205) |
Feature removed the only generatedAt signal |
Both reports' timestamps in aggregateCoverageTitle (ChangedFilesList.tsx:387-389) |
Aggregate substituted for ◔ 82% |
!aggregateCoverageLabel() && dropped from both <Show>s (:976, :995); they co-render |
| No-coverage projects paid full poll cost | comparisonEnabled (:544) + if (taskResult) (:716) |
| Comparison rebuilt every 5s | sameChangedFiles / sameCoverageSummary preserve identity across no-op polls |
| Two wrapper layers for two null checks | Inlined into the memo (:344-348); all three exports gone repo-wide |
shouldShowCoverageFooter's 4 positional params |
Replaced by the zero-arg showCoverageFooter memo (:337-343) |
Redundant previousPath from numstat |
parseNumstatPath returns a plain string; only --raw R/C lines write previousPathMap (git.ts:575-577) |
Branch fallback lost uncommittedOnly |
Filter on setFiles, unfiltered result to setComparisonFiles (:643-654) |
| Freshness design gap | GetMergeBaseTimestamp + baseline.stale → neutral, with tests |
CI is green on this head. The remaining items are one incomplete half of the freshness fix and a set of local cleanups.
Issues
Important
-
ChangedFilesList.tsx:960-975— The staleness gate landed in merge readiness but not in the display it exists to qualify.grep -n 'stale\|baseline'over this file returns only lines 395-397, insideaggregateCoverageTitle. The footer span colors purely ondeltaColor(aggregate.delta), so a stale base report still paintsbase 82% → task 78% (-4pp)intheme.erroratfont-weight: 600. Per-file badges have the same gap (:187-192).This is reachable exactly where it hurts:
merge-readiness.ts:185is guarded by the returns at 159-184, so it only fires when task and base are bothavailableanddelta !== null— the same state that paints the red delta.Scope, to be fair to the fix: in the merge dialog
MergeReadinessPanelrenders{check.detail}as visible text (MergeReadinessPanel.tsx:93), so there the user sees a contradiction rather than a silent lie. ButChangedFilesListalso renders inTaskChangedFilesSection.tsx:152,DiffViewerDialog.tsx:436,arena/BattleScreen.tsx:138andarena/ResultsScreen.tsx:233, none of which show a readiness panel — there thetitletooltip is the only cue. Fix is local: branch the color oncoverageComparison()?.baseline?.staletotheme.fgMuted, matching theneutralstatus the check already assigns.
Minor
-
ChangedFilesList.tsx:699-735—cachedMergeBaseis keyed ontaskResult.generatedAt, which is the coverage file's mtime (coverage.ts:161). The merge-base is a function of git history and has no relationship to it. Concrete failure: the merge dialog's own "Rebase onto <base>" button (MergeDialog.tsx:265-309) rebases in place and refetches merge status, branch log and worktree status — but changes noChangedFilesListprop, so the effect doesn't restart and the closure keeps serving the pre-rebase merge-base. The rebase moves the merge-base forward, sobaseGeneratedTime < mergeBaseTimeunder-reports, and a now-genuinely-stale base report reads as fresh. Rebase-then-check-readiness is the exact workflow this check was added for. (It self-heals if coverage is re-run afterwards, which changes the key.) Caching here is right —getMergeBaseTimestampfans out to 6-8 git subprocesses — but cache it for the effect lifetime, not on an unrelated value. Related:resolveBaseCoverageRoot→git worktree list --porcelainis the one call that genuinely repeats every 5s and is uncached at every layer; andGetMainBranchmoved from hoisted-outside-refreshto inside it (cheap, sincedetectMainBranchhas a 60s TTL cache, but it's an unnecessary per-poll IPC round trip). -
git.ts:1598-1610—getMergeBaseTimestampconflates "couldn't determine" with a value, in both directions:- Fail-closed: line 1602 is
const mergeBase = picked?.sha ?? head.pickMergeBasereturnsnullatgit.ts:305(branch missing locally and asorigin/<branch>) and at:321(merge-base fails for both). Substituting the task's own HEAD yields a timestamp newer than essentially any base report, sostaleis permanentlytrueand the Coverage check sits atneutralforever, blaming report freshness for something else. The:305route mostly coincides with having no base report anyway (no worktree can be checked out on a branch that doesn't exist), so the live case is:321— unrelated histories or a merge-base failure. Returningnullwhenpickedis null would be both simpler and honest. - Fail-open: the
catchreturnsnull,coverage-comparison.ts:149-157then leavesbaselineundefined, andcoverage.baseline?.staleatmerge-readiness.ts:185is falsy — so the delta drives pass/warning completely unguarded, exactly as before the fix, andaggregateCoverageTitleguards onif (comparison.baseline)so nothing tells the user freshness couldn't be established. Narrow (needs a resolved base report plus a failed timestamp lookup), but silent.
- Fail-closed: line 1602 is
-
src/components/ChangedFilesList.test.ts— This commit deletes thebuildCoverageComparisonForSelectionandshouldShowCoverageFooterdescribe blocks along with the exports, leaving the file byte-identical tomain. Fair framing: those tests were added by this PR in1e2ae56, so this isn't a regression againstmain— but'uses the canonical whole-task inventory for an uncommitted-only display selection'was the guard for round 2's Important finding, and it's now gone. The behavior survives the inlining (:344-348for the suppression guards,:574-607for the separate base-relative fetch in uncommitted mode), but I searched all 103*.test.*files: nothing else covers it.coverage-comparison.test.tsexercises the pure builder given an inventory and cannot observe which inventory the component passes, and no test file referencesChangedFilesListat all. Agreed the deletedit.each(['pending','failed'])was low-value — it ignored its parameter — but the uncommitted-selection case was not. -
ChangedFilesList.tsx:177-183— Dead branch.comparisonBadge's'no report'case needstask.state === 'no-report' && base.state !== 'no-report'.baseResultis declarednullat:714and assigned only insideif (taskResult)(:716) →if (baseRoot)(:737), andfileValuereturns'no-report'only when the whole summary is null (a file absent from a loaded report gives'file-not-present'). So the condition is exactlycoverage() === null && baseCoverage() !== null— unreachable. Same root cause makes|| hasBaseCoverageArtifact()redundant inshowCoverageFooter(:342) and'Task: no coverage report.'unreachable inaggregateCoverageTitle(:390). Worth noting thebatch()at:745is load-bearing for this: it writessetCoveragebeforesetBaseCoverage, so without the batch Solid's per-write flush would briefly expose that exact state. -
ChangedFilesList.tsx:544—comparisonEnabledis a tracked read in the effect body, so the whole polling effect tears down and restarts when either boolean flips. Because both are boolean memos with default===equality, this is only thenull ↔ reporttransition, not every regeneration — so it costs one extra teardown/refetch on task open, withsetCanOpenFilesInEditor(false)(:548) in between. The buttons areopacity: 0until row hover (styles.css:1386-1403), so it's mostly invisible DOM churn. Flagging because the fix is oneuntrack, and because the same restart is whysetComparisonFiles(null)had to be moved out into its own effect at:529-534—setCanOpenFilesInEditor(false)was left behind. -
Carried over from round 3, still open — the footer now shows two unlabeled percentages side by side (whole-repo
base 80% → task 78% (-2pp)as prose, next to changed-files◔ 91%as a glyph), distinguishable only by hovering;↕ N other(:1026-1033) still reusesaggregateCoverageTitle(), which truncates the impacted list at 3; andbranchNameis still passed atMergeDialog.tsx:452/TaskChangedFilesSection.tsx:155— the one prop here unrelated to coverage, since it newly enables theGetChangedFilesFromBranchfallback at those two call sites — with no mention in the PR description.
Strengths
- Every round-3 item is genuinely fixed, and the fixes are the small local ones proposed rather than rewrites.
- Declining the
-zrefactor while keeping the narrow literal-=>fix was the right scope call. GetMergeBaseTimestampis wired completely and consistently — manifest,register.ts, preload allowlist — with a focused test asserting both ISO-UTC normalization and the exact argv.- The read-only constraint from #236 still holds end-to-end.
Verdict
Needs changes — one blocking item. The freshness fix landed in merge-readiness.ts but not in the footer and badges that display the number, so outside the merge dialog the panel shows a red regression the readiness check has already decided to ignore. That's a color condition. The ?? head fallback at git.ts:1602 is a second one-liner worth taking with it; the rest is cleanup that can land alongside.
|
Addressed the coverage freshness feedback in 4bd754d:
Validation: focused git IPC tests, static checks, and the full suite pass (1,649 passed, 23 skipped). |
johannesjo
left a comment
There was a problem hiding this comment.
Summary
Re-reviewed at head 4bd754d. Both round-4 items are fixed, verified against the code rather than the changelog:
| Round 4 item | Resolution at 4bd754d |
|---|---|
| Stale baseline still painted as a red regression | comparisonBadge takes baselineStale → theme.fgMuted + tooltip clause (ChangedFilesList.tsx:191-192), threaded from :907; aggregate footer greys on baseline?.stale (:969-975) |
?? head fallback in getMergeBaseTimestamp |
if (!picked) return null (git.ts:1602), with a test asserting git show never fires |
CI is green. What follows is one leftover span from the first fix, one consequence of the second that I got wrong in round 4 and want to state correctly, and a design question about what the freshness check actually measures.
Issues
Important
-
ChangedFilesList.tsx:1032-1039— The staleness gate reached two of the three coverage indicators in this footer.↕ N otheris still hardcodedtheme.warning:<Show when={(coverageComparison()?.impactedUnchangedFiles.length ?? 0) > 0}> <span title={aggregateCoverageTitle()} style={{ color: theme.warning, 'font-weight': '600' }}>
It is the indicator merge readiness discards most completely when stale: the
neutralreturn atmerge-readiness.ts:185strictly precedes the only read ofimpactedUnchangedFilesat:193, and that is the sole non-test consumer of the field. So with a stale base the readiness panel says "regenerate it before comparing" while the footer shows↕ 4 otherin warning orange next to a now-grey aggregate.Not cosmetic: set membership is itself delta-derived (
Math.abs(delta) < MATERIAL_COVERAGE_DELTA→continue,coverage-comparison.ts:132), so a stale base both invents and hides entries. This badge is exactly as untrustworthy as the deltas this commit just muted, and it is now the only warning-colored base-derived signal left. One line:color: coverageComparison()?.baseline?.stale ? theme.fgMuted : theme.warning. (The tooltip is already correct — it reusesaggregateCoverageTitle, which carries the stale sentence.) -
Unknown freshness now renders as a confident pass.
git.ts:1602-1611+ChangedFilesList.tsx:730-739+coverage-comparison.ts:149-157— I flagged this direction as narrow in round 4; having traced it properly, I had the harm model wrong and it is wider than I said. Correcting both:null→baseline: undefined→coverage.baseline?.staleis falsy atmerge-readiness.ts:185→ the delta drives pass/warning with no freshness guard, andaggregateCoverageTitle'sif (comparison.baseline)guard at:399means the tooltip says nothing either.buildCoverageComparisoncannot distinguish "no base report" from "base report present, merge-base unknown" — both areundefined.Where I was wrong in round 4: I described the
?? headremoval as trading a false-neutralfor an unguarded verdict, implying the old behavior was safer. It wasn't uniformly.neutralandpassboth roll up tooverall: 'ready'(merge-readiness.ts:219-225), so for thedelta <= -1subset this change is strictly stricter now (warning→attention). The real regression is thedelta > -1subset:b887d66showed a muted check with "regenerate it before comparing";4bd754dshows a green ✓ andBase X% → task Y% (+Npp)computed against a report of unknown provenance. Also, my "headis newer than essentially any base report" was an overstatement — any base report regenerated after the task's last commit already yieldedstale: falseand was already unguarded.Where it is wider than I said: I scoped reachability to
pickMergeBasereturning null (git.ts:305/:321, and:324is unreachable TS narrowing). ButmergeBaseAt === nullhas three further sources independent of that: an unparseable%cI(git.ts:1608), the barecatchswallowing any exec failure (:1609-1611), and.catch(() => null)on the IPC invoke (ChangedFilesList.tsx:736). AndcachedMergeBaseis keyed ontaskResult.generatedAt, so a single transient failure is cached and replayed on every 5s poll until the coverage report is regenerated — one flaky exec pins the confidently-green verdict for the rest of the session. A thirdbaselinestate ("present, unanchored") costs about whatstaledid and would let both the check and the tooltip say so. -
The freshness check is one-directional, and its vocabulary doesn't match its mechanism.
stale = baseGeneratedTime < mergeBaseTimecompares a report file's mtime (coverage.ts:161/:272,stat.mtime.toISOString()) against the merge-base commit time. But nothing anchors the base report to any commit:resolveBaseCoverageRoot→getBranchWorktreePathmatches!entry.detached && entry.branchName === branchNameand returns a path, and the report is read straight off that working tree. That worktree sits at the base branch's current HEAD.In this app that drift is the mainline, not an edge case —
mergeTaskmerges task branches into the base branch inprojectRoot(git.ts:1724-1777), i.e. into exactly the worktree that later serves as the baseline. So for any task older than the last merge, a base report regenerated today has mtime > mergeBaseTime →stale: false→ the delta ships as clean while measuring a tree containing everything merged since the task branched. Other people's work can manufacture or mask thedelta <= -1warning.The sharpest form: following the check's own instruction — "regenerate it before comparing" (
merge-readiness.ts:189) — necessarily produces a report measuring base HEAD, the tree furthest from the merge-base. The action guaranteed to clear the warning maximizes the unmeasured drift. Meanwhile the tooltip printsTask merge-base: <ts>beside the base number, which reads as anchoring that never happens.I don't think #236's read-only constraint excuses this, because the issue is detection, not measurement. Both values needed are already computed and thrown away:
parseWorktreeList(git.ts:697-733) parses theHEAD <sha>line fromgit worktree list --porcelainand discards it (your own fixture feeds it —git.test.ts:1487), andgetMergeBaseTimestampholdspicked.shaat:1603. Comparing them costs zero extra git invocations.Fair counterpoint, and I'd accept either resolution: #236 cites GitHub Code Quality, which compares a branch against the default branch's head — so base-HEAD semantics are defensible prior art, and the PR body ("an existing worktree on the configured base branch") reads that way. If that's the intent, then the wording is the defect: drop "merge-base" from the user-facing strings, say the baseline is "
<base>as currently checked out," and compare the report mtime against the base worktree's HEAD commit time rather than the merge-base's — the current reference is wrong under both readings, sincemergeBaseTime <= baseHeadCommitTimealways.
Minor
-
ChangedFilesList.tsx:730-739(carried from round 4, unchanged) — beyond the sticky-failure case above, keying the merge-base cache on a coverage-file mtime still breaks the rebase workflow. I re-verified the mechanism: the Rebase onto <base> handler (MergeDialog.tsx:273-280) callsRebaseTaskthen refetchesmergeStatus/branchLog/worktreeStatus— none of which feed any of the eight props atMergeDialog.tsx:449-458, andRebaseTaskemits nothing to the renderer. Tasks live in a fine-grainedcreateStore, so unchanged strings can't notify. The effect never restarts and the closure serves the pre-rebase merge-base. Cache for the effect lifetime instead. -
Dead code, all from the same invariant (carried, unchanged) —
baseResultis declared null at:718and assigned only insideif (taskResult)(:720-747), and the three writes are wrapped inbatch()at:749, so non-null base ⟹ non-null task with no observable transient. Therefore:comparisonBadge's'no report'branch (:178-184) can never fire,|| hasBaseCoverageArtifact()is redundant inshowCoverageFooter(:346), and'Task: no coverage report.'is unreachable inaggregateCoverageTitle(:394, guarded byif (!baseReport …) return ''at:389). -
ChangedFilesList.tsx:548(carried, unchanged) —comparisonEnabledreads twocreateMemoaccessors inside the effect body;untrackappears nowhere in the file, so the whole polling effect (interval + cleanup) tears down and restarts on thenull ↔ reportflip. -
Test coverage for the component layer.
ChangedFilesList.test.tsis byte-identical to the base (same blob OIDfef489b8), and while it does import from./ChangedFilesList, it pulls only the five pure helpers — the component is never rendered, and there is noMergeDialog.test.ts. So this commit'sbaselineStalework has no coverage at all: a repo-wide grep forbaselineStale|comparisonBadgehits onlyChangedFilesList.tsx. It also can't have any as written —comparisonBadge(:152) andFileCoverageBadge(:205) are module-private.coverage-comparison.test.ts:195-219coversstalein the pure builder andMergeReadinessPanel.test.ts:282covers the neutral check withimpactedUnchangedFiles: [], so nothing exercises the stale + non-empty-impacted combination that the Important item above turns on. This is why a fix could land on two of three indicators with a green suite. -
Carried, unchanged —
aggregateCoverageTitletruncates the impacted list at.slice(0, 3)(:409) while printing the full count (:417), and that same string is the only tooltip on↕ N other(:1034); the footer still shows two unlabeled percentages side by side; andbranchNameatMergeDialog.tsx:452/TaskChangedFilesSection.tsx:155— added by this PR, and the thing that newly enables theGetChangedFilesFromBranchfallback at those two surfaces — is still absent from the PR description.
Strengths
- Both round-4 items are genuinely fixed, with the small local changes proposed rather than rewrites —
baselineStaleis one boolean threaded through, not a refactor. if (!picked) return nullis the honest version of the fallback, and the added test asserting the absence of thegit showcall is the right shape.- I checked the branches I flagged as at-risk and they're fine:
comparisonBadge's remaining non-neutral color (:200) is reachable only whendelta === null, so it encodes the task report's own absolute coverage with zero base input — staleness cannot make it wrong. Same for△/∅and the◔footer. - The read-only constraint from #236 holds end-to-end across all five commits, and rounds 3–4's KISS deletions stayed deleted.
Verdict
Needs changes. Two items, both small: the ↕ N other color (one line, same fix already applied twice in this commit) and the unguarded-null path — which after this round presents unknown freshness as a green pass rather than a muted neutral, and sticks for the session once a transient failure is cached.
The third item is a genuine question rather than a defect I want fixed a particular way: decide whether the baseline is the merge-base or the base branch's current head, then make the code and the strings agree. If it's base-HEAD, this is a wording change plus swapping which timestamp you compare against. My apologies for the round-4 mis-scoping on the null path — the direction of the fix was right, the consequences were not what I described.
|
Addressed the two concrete follow-ups in b1837bf:
Validation: I left the merge-base versus base-HEAD baseline semantics unchanged because the review frames that as a policy choice; I can align the wording and timestamp reference once the preferred semantics are confirmed. |
johannesjo
left a comment
There was a problem hiding this comment.
Summary
Re-reviewed at head b1837bf, checked out locally: npm run typecheck is clean and the affected suites are green (87 tests across coverage-comparison, MergeReadinessPanel, git).
Both round-5 items are genuinely fixed — verified against the code, not the changelog:
| Round 5 item | Resolution at b1837bf |
|---|---|
↕ N other hardcoded theme.warning while the aggregate greys |
ChangedFilesList.tsx:1044-1049 — mutes on stale || unanchored |
| Unknown freshness rendered as a confident green pass | A third baseline state: coverage-comparison.ts:152-162 now distinguishes "no base report" (undefined) from "present, unanchored", merge-readiness.ts:192-199 returns neutral, and it is threaded to the badge (:158-163), the tooltip (:403-406), and the aggregate footer (:979-981) |
That is the right shape and the right cost: the distinction is made once in the builder rather than as a null check scattered across consumers.
The deferred question — decision: the baseline is the base branch's current HEAD
Thanks for flagging it rather than guessing. #236 cites GitHub Code Quality, which compares against the default branch's head, and base-HEAD is what the code already reads — resolveBaseCoverageRoot → getBranchWorktreePath returns the path of whatever worktree is on the base branch, and the report is read straight off that working tree. So the code is right and the vocabulary is the defect. Three changes:
-
Drop "merge-base" from the user-facing strings —
ChangedFilesList.tsx:160,:162,:405,:408,:411, andmerge-readiness.ts:189,:197. Say the baseline is "<base>as currently checked out". -
Compare the base report's mtime against the base worktree's HEAD commit time, not the merge-base's. Today's reference is systematically lenient: the merge-base sits behind what the base worktree actually contains, so a base report regenerated after any merge into
projectRoot(which is wheremergeTaskmerges, i.e. exactly the worktree that later serves as the baseline) yieldsstale: falsewhile measuring a tree that includes everything merged since the task branched. Comparing against base HEAD closes that. -
It is cheaper, not more expensive.
getMergeBaseTimestampcurrently costs 5–8 git invocations —pinHead,localBranchExists+remoteTrackingRefExists, one or twomerge-base, up to twomerge-base --is-ancestor, thengit show. Under base-HEAD semantics the sha is free:resolveBaseCoverageRootalready runsgit worktree list --porcelainon every coverage poll, and its output already contains theHEAD <sha>line —parseWorktreeListsimply ignores it today (your own fixture feeds it —git.test.ts:1484). Capture it intoListedWorktree, return it alongside the path, and onegit show -s --format=%cI <sha>yields the timestamp.
Worth documenting in the tooltip that the delta may include base-branch work merged after the task branched — that is inherent to base-HEAD semantics and fine, as long as it is stated rather than implied away.
Note the knock-on: because the sha rides along with a call that already happens every poll, the whole cachedMergeBase mechanism at ChangedFilesList.tsx:711-747 can go — which subsumes the item below and the carried rebase-cache item from round 4.
Issues
Important
-
ChangedFilesList.tsx:738-747— The merge-base cache storesnullresults.getMergeBaseTimestampreturnsnullon any exec failure or unparseable%cI(git.ts:1608-1611), and.catch(() => null)at:744swallows IPC failures too. Since the cache is keyed ontaskResult.generatedAt, one transient failure is replayed on every 5s poll until the coverage report is regenerated or the panel/dialog is deactivated and reactivated.This round changed what that costs. At
4bd754dthe stickynullproduced a green pass; now it pins the entire comparison — per-file badges, aggregate footer,↕ N other, and the readiness check — to muted "informational only", with no self-healing on the poll that is otherwise running fine. Safer direction, but still sticky. One line: only assigncachedMergeBasewhen the value is non-null, so the next tick retries. (Moot if you take the base-HEAD route above, which removes the cache.)
Minor
-
stale || unanchoredis now duplicated across four consumers —ChangedFilesList.tsx:158,:979-981,:1046-1047, andmerge-readiness.ts:185+:192as two sequentialifs. They are mutually exclusive by construction (theunanchoredbranch setsstale: false), so every consumer wants the same disjunction, and missing one of them is exactly what round 5 caught. Export a predicate fromcoverage-comparison.ts—isBaselineInformational(baseline)— and use it at all four; that also collapses the twomerge-readinessbranches into one. -
Carried from rounds 4–5, unchanged. All three dead branches still follow from the same invariant:
baseResultis declarednullat:726and assigned only insideif (taskResult), and all three writes are batched at:757, so non-null base ⟹ non-null task with no observable transient. ThereforecomparisonBadge's'no report'branch (:184-190) cannot fire,|| hasBaseCoverageArtifact()is redundant inshowCoverageFooter(:350), and'Task: no coverage report.'is unreachable at:398behind the:393guard. Also unchanged:comparisonEnabled(:556) reads twocreateMemoaccessors in the effect body anduntrackappears nowhere in the file (0 occurrences), so the wholeGetChangedFilespolling effect tears down and restarts on thenull ↔ reportflip;aggregateCoverageTitletruncates at.slice(0, 3)(:417) while printing the full count (:425), and that string is the only tooltip on↕ N other(:1043); andbranchNameatMergeDialog.tsx:452/TaskChangedFilesSection.tsx:155— which newly enables theGetChangedFilesFromBranchfallback (ChangedFilesList.tsx:647) at both surfaces, a behavior change beyond coverage — is still absent from the PR description. -
Component-layer test gap, unchanged.
ChangedFilesList.test.tsis still byte-identical tomain(same blob hash), there is noMergeDialog.test.ts, andcomparisonBadge/FileCoverageBadgeremain module-private. This round's two new tests cover the pure builder and the readiness check; all four UI call-sites the commit touched have no coverage. That is the same structure that let round 5's fix land on two of three indicators with a green suite.
Strengths
- The third baseline state is a builder-level distinction rather than a null check smeared across consumers, so the round-5 harm model — unknown provenance rendering as a confident pass — is closed at the source.
- Both fixes are the small local changes proposed, not rewrites.
- The read-only constraint from #236 holds end-to-end across all six commits, and the earlier KISS deletions stayed deleted.
Verdict
Needs changes, and the remaining work is small: realign the baseline to base-HEAD semantics per the decision above (wording plus one swapped timestamp reference, net fewer git invocations), which also lets the merge-base cache and its sticky-null behavior go.
|
Updated in fc81faa.
Validation: |
johannesjo
left a comment
There was a problem hiding this comment.
Summary
Re-reviewed at head fc81faa. CI is green, and all six round-6 items are implemented — verified against the code rather than the changelog:
| Round 6 item | Resolution at fc81faa |
|---|---|
| Drop "merge-base" from user-facing strings | All 7 sites reworded to "as currently checked out" (ChangedFilesList.tsx:162, :164, :417, :421, :428; merge-readiness.ts:192, :194) |
| Compare base mtime against base-worktree HEAD, not merge-base | stale: baseGeneratedTime < baseHeadTime (coverage-comparison.ts:164) |
Get the sha free from the existing worktree list call |
head captured in ListedWorktree (git.ts:691, :722-725); one git show -s --format=%cI replaces pinHead + 2 ref checks + up to 2 merge-base + 2 --is-ancestor + git show |
Remove cachedMergeBase and its sticky null |
Gone, along with the whole GetMergeBaseTimestamp channel, handler, and preload entry |
| Document that the delta may include post-branch base work | ChangedFilesList.tsx:423-425 |
Centralize stale || unanchored |
isBaselineInformational used at all four consumers (:159, :982, :1046, merge-readiness.ts:186) |
One correction to my own round-6 cost claim: steady state is now 2 uncached git invocations per 5s poll (worktree list + git show) versus 1 plus a cached merge-base. Cheaper on a cache miss, marginally worse in steady state. Not a defect — just accurate accounting.
Issues
Important
-
merge-readiness.ts:197-201— Base-branch drift is now attributed to the task, and it flips overall readiness toattention. Thefile.task.state !== 'available'arm ofregressedUnchangedfires when a file isavailablein the base report and absent from the task report. Under the base-HEAD semantics we just adopted, that combination has exactly one common cause: someone else added a covered source file to the base branch after this task branched. It is present in the base worktree (sonormalizeCoveragePath'sexistsSyncgate atcoverage.ts:112-114keeps it) and cannot appear in the task's report at all.It survives every filter.
coverage-comparison.ts:138doesn'tcontinue('file-not-present' !== 'available'),:139doesn't either (delta === null), so it lands inimpactedUnchangedFiles; thenregressedUnchanged.length > 0at:208-210is an independentORbeside the aggregate threshold, soMATERIAL_COVERAGE_DELTAnever applies to it. Result:Base 82% → task 82% (0pp). 3 unchanged files also regressed.→warning→overall: 'attention', for a task that changed none of those files. In an app whose whole shape is many task worktrees off one base branch, that's the mainline case, not an edge case.The task-attributable case this arm was added for in round 1 is already handled elsewhere: if the task deleted a covered file, that path is in the Git inventory, so
changedPaths.has(filePath)atcoverage-comparison.ts:134skips it and it renders as thedel 82%badge instead. By construction, nothing reachingimpactedUnchangedFileswithbase available / task not-presentcan be the task's doing. So restrict the readiness gate to deltas that are actually attributable —file.task.state === 'available' && file.delta !== null && file.delta <= -MATERIAL_COVERAGE_DELTA— and keep the wider set for the↕ N otherdisplay. The freshness check can't rescue this: it detects "base report older than base HEAD", never "task report older than base HEAD", which is normal and permanent for any task that isn't freshly branched. -
coverage-comparison.ts:166-169—baseBranchis spread into the anchored baseline (:162) but not the unanchored one, so the branch name is dropped in one of the two states that reads it. Every consumer falls back to its placeholder, and two of the three placeholders are missing an article, producing broken user-facing text:merge-readiness.ts:194→ "Base coverage report cannot be anchored to base branch as currently checked out", rendered as visible text atMergeReadinessPanel.tsx:93;comparisonBadge:164→ "cannot be anchored to base branch". (ChangedFilesList.tsx:417uses'the base branch', so the three defaults also disagree with each other.)The test that should have caught it asserts the opposite.
MergeReadinessPanel.test.ts:318-337hand-buildsbaseline: { baseBranch: 'main', stale: false, unanchored: true }and expects"...anchored to main as currently checked out"— a shapebuildCoverageComparisoncannot emit — while the builder's own unanchored test (coverage-comparison.test.ts:227-234) passes nobaseBranchand asserts its absence. Fix is the same spread on both branches, plus one builder assertion thatbaseBranchsurvives an unknown head.
Minor
-
git.ts:1598-1600—git showruns withcwd: match.path. The sha is reachable fromprojectRoot's object database (same repository), so using the worktree path only adds a failure mode: a prunable entry whose directory is gone still appears inworktree list --porcelain, andexecthen fails on the missing cwd.cwd: projectRootis strictly safer. -
git.ts:1596—if (!match?.path || !match.head) return nullcollapses "a worktree is on the base branch but its HEAD line is unreadable" into "no base worktree at all", which drops the base report and the entire comparison. Returning{ path, head, headCommittedAt: null }would degrade tounanchored— the state this PR added specifically for unknown provenance. Rare, but it's the wrong direction and it's untested. -
Test coverage for what this commit changed.
git.test.ts:1494-1512covers only the success path:git showalways returns a valid timestamp. Nothing coversheadCommittedAt: null(the sole producer ofunanchored), the!match.headguard, or an unparseable%cI. That is exactly the path broken by thebaseBranchitem above. Also dead now:buildWorktreeMockHandler'smergeBaseTimestampoption (git.test.ts:446, handler at:597) has no callers after thegetMergeBaseTimestampblock was deleted. -
ChangedFilesList.tsx:292—baseWorktree.path === taskRootis a raw string compare on a path straight out ofgit worktree list, whilelistImportableWorktreesdoes the same comparison throughsafeRealpathon both sides (git.ts:1540,:1548). On a mismatch the guard fails and the task's own report is compared against itself:base 82% → task 82% (+0pp), empty impacted set, permanentpass. Reachable when the user works directly on the base branch through a symlinked project path. -
Freshness residual (inherent — worth a sentence in the tooltip, not a fix).
stalecompares a file mtime against HEAD's committer date. Agit pull --ff-onlythat fast-forwards the base worktree onto commits authored days ago leaves a report generated before the pull reading as fresh (mtime > commit time), even though it measures the pre-pull tree. Genuinely closing it needs the report's own sha, which is out of scope for a read-only feature — but the tooltip currently warns only about the base being too new. -
MergeReadinessPanel.tsx:26-28— TheCoveragehelp text still says "task and base-branch coverage reports" without the "as currently checked out" qualification the rest of the strings gained this round. It's the one place that explains the check. -
Carried, unchanged. All three dead branches still follow from the same invariant (
baseResultisnullat:737, assigned only insideif (taskResult)at:740, all writes batched at:758, so non-null base ⟹ non-null task with no observable transient):comparisonBadge's'no report'case (:186-192),|| hasBaseCoverageArtifact()inshowCoverageFooter(:356), and'Task: no coverage report.'(:410) behind the:405guard. Also unchanged:comparisonEnabled(:573) reads two memo accessors in the effect body with nountrackanywhere in the file, so the polling effect tears down and restarts on thenull ↔ reportflip;aggregateCoverageTitletruncates at.slice(0, 3)(:434) while printing the full count (:442), and that string is the only tooltip on↕ N other(:1044); the footer still shows two unlabeled percentages side by side (:975-991prose vs:992-1010glyph);branchNameatMergeDialog.tsx:452/TaskChangedFilesSection.tsx:155— the one added prop unrelated to coverage, and the thing that newly enables theGetChangedFilesFromBranchfallback at both surfaces — is still absent from the PR description; andChangedFilesList.test.tsis still byte-identical tomain(blobfef489b8), so none of the four UI sites this round touched has component-level coverage. -
Tiny:
aggregate.delta === nullatmerge-readiness.ts:179is unreachable — the preceding guards leavetask.state === 'available', sodelta === nullimplies base isn't available, which the same condition's first clause already covers.
Strengths
- Round 6's decision landed completely and in the shape proposed: the sha rides along with a call that already happens every poll, and the merge-base IPC is gone end-to-end — channel enum, manifest, handler, preload allowlist, and tests.
isBaselineInformationalis exported once rather than re-derived at four call sites, which is what let this round's rewording touch every consumer without missing one.- The tooltip now states the base-HEAD limitation instead of implying anchoring that never happens — the wording defect from round 5 is closed at the source.
parseWorktreeListhandlesHEADbeforebranchand treats a blank line as a record terminator, so the new field doesn't perturblistImportableWorktrees.- The read-only constraint from #236 holds end-to-end across all seven commits, and rounds 3–6's KISS deletions stayed deleted.
Verdict
Needs changes — one design item, one one-liner.
The baseBranch spread is a one-line fix plus a test correction. The readiness item is the substantive one: locking in base-HEAD semantics was the right call, but it means impactedUnchangedFiles now routinely contains other people's merged work, and regressedUnchanged converts that into attention with a detail line that reads as the task's fault. The disambiguator is already present — anything in that set provably wasn't touched by the task — so the fix is narrowing one filter, not reworking the model.
|
Addressed both round-7 blockers in
Validation: targeted coverage/readiness suites (32 passed), |
johannesjo
left a comment
There was a problem hiding this comment.
Summary
Re-reviewed current head (7a8443d) against #236 and all seven prior review rounds. The round-7 fixes are present, and quality plus GitGuardian are green, but four correctness gaps remain. There are no inline review threads.
PR Discussion Context
The selected base-HEAD semantics remain reasonable for displaying an informational comparison. They do not make two divergent branch snapshots attributable to the task, and the task-side report still needs the same freshness treatment already added for the base.
Issues
Important
-
src/components/merge-readiness.ts:197-214 — When the base branch is ahead, its post-fork changes still drive the Coverage warning. A fresh base-only commit can raise aggregate coverage, making the unchanged task snapshot look like a regression; base-only tests can also improve an existing source file, leaving both entries
availablesoregressedUnchangedcounts it. Merge safety already reports that the base is ahead, so this does not independently change the overall state, but the Coverage line still falsely attributes base drift to the task and leaves the round-7 fix incomplete. Whilemain_ahead_count > 0, keep Coverage neutral/informational until rebase and regenerated task coverage, while retaining the footer comparison for reference. -
src/components/ChangedFilesList.tsx:733-766 — Only the base report is checked against a HEAD timestamp. Run coverage, then commit or rebase the task without regenerating it: the unchanged task report remains authoritative and can produce either a false pass or a false warning. Obtain task-HEAD freshness alongside the base, and keep the comparison informational whenever either report predates or cannot be anchored to its corresponding HEAD. Add a commit/rebase-after-report regression test.
-
src/components/ChangedFilesList.tsx:224-258, lines 358-360 — A delayed or failed canonical inventory removes the existing task-only per-file badge. With a valid task
summarybut nocomparison,badge()is null and the only fallback requires!props.summary, so neither branch renders. The same null is sent to merge readiness and displayed as “No task coverage report,” even though a report exists. This is transient during delayed loads and persists whileGetChangedFilesfails, especially in uncommitted mode. Restore the absolute-summary badge whenever comparison data is unavailable, and represent inventory loading/failure separately from a genuinely missing report. Add a mounted-component regression; the current tests cover only pure helpers. -
electron/ipc/git.ts:539-590 — The rename parser still relies on Git’s ambiguous human-formatted numstat shorthand. In a real repository, renaming
dir/old => literal.tstodir/new.tsemitsdir/{old => literal.ts => new.ts}; the parser assigns the stats to a phantomdir/literal.ts => new.tsrow and leaves the real rename at zero. Literal braces reproduce the same failure. The base already mishandled rename shorthand generally, but this PR addsprevious_pathand explicitly promises defined renamed-file coverage behavior, so the case is materially exposed in this feature. Use NUL-delimited--raw --numstat -zparsing rather than heuristic brace/arrow parsing, and add real Git-shaped regressions for literal-arrow and brace renames.
Strengths
- Base-worktree discovery and report loading remain read-only.
- The round-7 unavailable-state and unanchored-branch fixes are correct, and IPC wiring remains consistent.
Verdict
Needs changes. The first three items can make coverage readiness or fallback UI report a state unsupported by the available data; the rename parser still fails a defined acceptance case for legal paths.
Signed-off-by: Liang Hu <lh3057@columbia.edu>
Signed-off-by: Liang Hu <lh3057@columbia.edu>
7a8443d to
347108e
Compare
|
Addressed the four current-head findings in
Validation on the rebased branch: |
johannesjo
left a comment
There was a problem hiding this comment.
Summary
Re-reviewed current head 347108e against #236, the complete PR diff, all prior review discussion, and the latest regression tests. The four findings reported at 7a8443d are implemented: base-ahead readiness is neutral, task-report freshness is anchored to task HEAD, task-only badges survive unavailable inventory, and combined raw/numstat parsing is NUL-delimited.
One fallback-path correctness gap remains.
PR Discussion Context
There are no inline review threads. Hosted quality and GitGuardian checks are green. On an isolated export of this exact head, npm test passed (1,660 passed, 25 skipped), as did frontend typechecking, Electron compilation, ESLint, Knip, and dependency-cruiser.
Issues
Important
-
src/components/ChangedFilesList.tsx:400 — When the canonical changed-file inventory is loading or has failed, this passes
[]tobuildCoverageComparison. The builder cannot distinguish “inventory unavailable” from “there are no changed files,” so every materially different report path is classified as an impacted unchanged file. The footer then renders those entries as↕ N otherand describes them as “materially impacted unchanged files” at lines 1140–1151. In an uncommitted-only view this is visible during a delayed inventory request and persists afterGetChangedFilesfails, falsely relabeling actual changed paths as unchanged. The new readiness guard correctly keeps the merge verdict neutral, but it does not prevent this incorrect Changed Files output.I reproduced the state directly with differing task/base reports and the component's empty-inventory substitution: a known changed path is emitted in
impactedUnchangedFileswith a-20ppdelta. Keep the valid aggregate comparison and task-only badges, but suppress per-file/impactedUnchangedFilescomparison output untilinventoryState === 'available'. A mounted-component regression should cover both loading and failed inventory with a report-differing changed path and assert that↕ N otheris absent.
Strengths
- Task/base freshness and base-ahead handling now degrade safely to informational merge-readiness output.
- The NUL parser is applied consistently to all combined raw/numstat consumers and preserves exact rename paths.
Verdict
Needs changes. The remaining fix is small, but the current fallback can present materially incorrect file classification.
Signed-off-by: Liang Hu <lh3057@columbia.edu>
|
Addressed the remaining fallback-path issue in
Validation: |
Summary
Validation
npm test(1,634 passed, 23 skipped)npm run checknpm run lint:deadnpm run lint:archnpm run build:frontendCloses #236