Skip to content

Feat/eng 2516#577

Merged
bao-byterover merged 5 commits intomainfrom
feat/ENG-2516
Apr 29, 2026
Merged

Feat/eng 2516#577
bao-byterover merged 5 commits intomainfrom
feat/ENG-2516

Conversation

@bao-byterover
Copy link
Copy Markdown
Collaborator

Summary

  • Problem: brv vc deadlocked when status reported the working directory clean while pull errored "would be overwritten" and push errored "Run brv vc pull first." User had no actionable signal and no documented escape.
  • Why it matters: trivial workflows trigger this (edit → vc add → editor autosave reverts disk content). [1,1,3] is the canonical isomorphic-git encoding for "WORKDIR matches HEAD, INDEX diverges from both" and was silently dropped by the status whitelist.
  • What changed:
    • Replaced the 11-tuple hardcoded whitelist in parseMatrix with a single classifyTuple helper derived from the isomorphic-git encoding spec, routed through every statusMatrix consumer (status, pull dirty-check, staged classifier, unstaged classifier, guardStagedConflicts, resetUnstage).
    • Pull "would be overwritten" error now lists the affected files so users can run brv vc reset <file>.
    • Extracted overwrite messages (pull / merge / checkout) into a shared formatOverwriteMessage helper with unified "commit or discard" trailer.
    • Added tuple-reachability fuzz + pull error format-lock tests.
  • What did NOT change (scope boundary):
    • No new vc commands, no transport schema changes, no daemon protocol changes.
    • The "would be overwritten" anchor string is preserved verbatim because vc-handler pattern-matches on it to map GitErrorVcError(UNCOMMITTED_CHANGES).
    • Out-of-scope per ticket was "unify the five ad-hoc dirty-check implementations." This PR did unify them via classifyTuple because the bug fix itself required deriving classification from the encoding rules instead of an ad-hoc whitelist; flagging here so reviewers know to expect more diff than a one-tuple addition.

Type of change

  • Bug fix
  • New feature
  • Refactor (no behavior change)
  • Documentation
  • Test
  • Chore (build, dependencies, CI)

Scope (select all touched areas)

  • TUI / REPL
  • Agent / Tools
  • LLM Providers
  • Server / Daemon
  • Shared (constants, types, transport events)
  • CLI Commands (oclif)
  • Hub / Connectors
  • Cloud Sync
  • CI/CD / Infra

Linked issues

Root cause (bug fixes only, otherwise write N/A)

  • Root cause: parseMatrix in src/server/infra/git/isomorphic-git-service.ts was a hardcoded whitelist of 11 [head, workdir, stage] tuples. Tuple [1,1,3] (workdir matches HEAD, stage diverges from both) is reachable via vc add followed by a disk-only revert (editor undo, agent revert, two-editor race) and was missing from the list, so status silently treated it as clean. pull used a generic row[2] !== 1 || row[3] !== 1 filter, caught [1,1,3], and refused.
  • Why this was not caught earlier: no coverage over the full encoding state space. The 11 enumerated tuples were the ones encountered organically; the encoding can produce 24 valid tuples and several reachable ones (including [1,1,3]) had no test.

Test plan

  • Coverage added:
    • Unit test
    • Integration test
    • Manual verification only
  • Test file(s):
    • test/unit/infra/git/status-row-classifier.test.ts (new, 245 lines) - exhaustive per-tuple expectations for the classifier.
    • test/unit/infra/git/git-error-messages.test.ts (new, 58 lines) - format-lock for each operation × file-count combo.
    • test/integration/infra/git/status-tuple-reachability.test.ts (new, 316 lines) - fuzz that constructs every reachable tuple via real isomorphic-git operations and asserts the cross-property invariant.
    • test/integration/infra/git/isomorphic-git-service.test.ts (+101 lines) - adds [1,1,3] repro and pull-error-names-files assertion.
    • test/unit/infra/transport/handlers/vc-handler.test.ts (+36 lines) - confirms anchor string still maps to VcError(UNCOMMITTED_CHANGES).
  • Key scenario(s) covered:
    • Reproduce recipe: vc add then disk-only revert, status now reports the file as staged + modified instead of clean.
    • Pull error names the affected files (matches the ticket acceptance "pull error names abcdef.md").
    • Cross-property invariant: status.isClean === true implies pull never throws "would be overwritten", verified across all reachable tuples.
    • Pull / checkout / merge overwrite messages share a single formatter and preserve the "would be overwritten" anchor for vc-handler mapping.

User-visible changes

  • brv vc status now reports [1,1,3] files (added then disk-reverted) as staged + modified instead of silently treating them as clean.
  • brv vc pull overwrite error now lists the affected files so users know which paths to brv vc reset. Old format: "Local changes would be overwritten by pull. Commit or discard your changes first." New format when files are known: "Your local changes to the following files would be overwritten by pull:\n\t\nPlease commit or discard your changes before you pull."
  • Pull / checkout / merge overwrite messages share a unified phrasing ("commit or discard your changes"). Behavior unchanged; only wording is normalized.

Evidence

  • Failing test/log before + passing after: [1,1,3] repro test in isomorphic-git-service.test.ts fails on main, passes on this branch. Tuple-reachability fuzz exercises every reachable encoding.
  • Trace/log snippets: pull error before/after string comparison in test/unit/infra/git/git-error-messages.test.ts.
  • Screenshot/recording

Checklist

  • Tests added or updated and passing (npm test)
  • Lint passes (npm run lint)
  • Type check passes (npm run typecheck)
  • Build succeeds (npm run build)
  • Commits follow Conventional Commits format
  • Documentation updated (if applicable) - N/A
  • No breaking changes (or clearly documented above)
  • Branch is up to date with main (merge commit 674d061)

Risks and mitigations

  • Risk: Routing six call sites through one classifier could introduce a behavior change for tuples that the old per-site code handled implicitly.
    • Mitigation: status-tuple-reachability.test.ts fuzz constructs every reachable tuple via real git operations and asserts old behavior for the 11 previously-whitelisted tuples is preserved. validateEncoding throws if isomorphic-git ever emits an out-of-range column, surfacing future encoding drift loudly instead of silently misclassifying.
  • Risk: Pull error message format changed; downstream consumers may pattern-match on the old phrasing.
    • Mitigation: the "would be overwritten" anchor substring is preserved verbatim; vc-handler.test.ts adds an explicit format-lock case that the anchor still maps to VcError(UNCOMMITTED_CHANGES).

…rite messages

Replace the whitelist tuple enumeration in classifyTuple with rule-based
stagedDiff/unstagedDiff/untracked helpers that are total over the
statusMatrix encoding's value range. Out-of-range columns still throw
GitError so a real encoding shift surfaces loudly.

Surfaces two silent-drops the previous whitelist missed:
- [1,1,3]: now reports both staged + unstaged modifications, matching
  native git (was reporting only the staged side after the prior fix)
- [1,0,2]: now reports both staged-modified + unstaged-deleted (was
  dropping the staged side entirely; same bug class as [1,1,3])

Extract formatOverwriteMessage into git-error-messages.ts, used by all
five overwrite paths (checkout / merge / pull, with and without file
lists). Drop the "stash them" trailer because brv has no stash command;
trailer now reads "commit or discard". The "would be overwritten" anchor
is preserved so vc-handler's UNCOMMITTED_CHANGES mapping still works.

Cross-property invariants in the unit suite now run exhaustively over
the full (h, w, s) cartesian product (24 tuples x 6 invariants = 144
assertions) so any future algorithmic regression is caught structurally.
Drops the brittle source-scan format-lock test in favor of helper unit
tests against the extracted formatter.
@bao-byterover bao-byterover requested a review from leehpham as a code owner April 29, 2026 02:06
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

Claude finished @bao-byterover's task in 5m 46s —— View job


PR Review: feat/ENG-2516

Tasks

  • Gather context and read changed files
  • Review status-row-classifier.ts (new file)
  • Review git-error-messages.ts (new file)
  • Review isomorphic-git-service.ts (modified)
  • Review test files
  • Post inline comments
  • Post final summary

Summary

Score: 4 / 5

This is a well-scoped, well-reasoned fix. The root cause ([1,1,3] silently dropped by the 11-tuple whitelist) is correctly diagnosed; the principled approach of replacing the whitelist with a spec-derived classifier is strictly better than appending a 12th hardcoded entry. The six consumer call sites now share a single source of truth, so future encoding changes will surface loudly via validateEncoding rather than causing another silent-drop class of bug.


What works well

  • classifyTuple correctness — traced all 24 valid tuples through stagedDiffFor and unstagedDiffFor. Every reachable tuple produces the right (stagedDiff, unstagedDiff, dirty, staged) projection, including the previously-missing [1,1,3] (staged=true, stagedDiff='modified', unstagedDiff='modified').
  • validateEncoding fail-fast — throwing a GitError on out-of-range column values is the right signal boundary. Better than a silent wrong classification.
  • staged invariantstaged === (stage !== head) holds for all in-range tuples; the unit tests verify this structurally.
  • formatOverwriteMessage extraction — the three call sites (pull, merge, checkout) now share the "would be overwritten" anchor string, and vc-handler pattern-matching is preserved verbatim. The format-lock test in vc-handler.test.ts is exactly the right regression guard.
  • Integration fuzz harness — constructs every reachable tuple via real isomorphic-git operations, including the utimes bump for [1,1,3] to force stat-cache revalidation. Without the mtime manipulation the scenario would silently produce [1,2,2] instead.
  • Pull error now names files — directly actionable. The OID comparison to determine which dirty files would actually be overwritten is the right approach (avoids false positives for dirty files that don't conflict with the incoming diff).

Issues

Suggestion — integration test uses old pull filter, not new classifier
isomorphic-git-service.test.ts line ~456, the "cross-property invariant" test reconstructs the pull dirty-set with row[2] !== 1 || row[3] !== 1 (the removed formula) rather than classifyTuple().dirty (what pull() now calls). The test validates consistency with the old code path. If classifyTuple is later changed, the discrepancy in pull() won't be caught here. Fix this →

Nitpick — [1,0,2] scenario name is unverified
status-tuple-reachability.test.ts line ~107: the scenario for [1,0,2] (stage 'v2', then unlink) has no assertion that the resulting tuple is actually [1,0,2] vs. [1,0,3] or another value. The [1,1,3] scenario correctly asserts expect(matrix).to.deep.equal(...). The same pattern should be applied here to make the name self-verifying.

Nitpick — shared state in fuzz harness summary test
status-tuple-reachability.test.ts line ~309: the final 'reachable set is non-empty and fully classifiable' test depends on the scenario it blocks above having populated observedTuples and unclassifiableTuples. If tests are filtered via --grep or scenarios are .skipped, the summary passes vacuously. A short comment above the summary block noting this dependency would help future readers.

Minor — unstagedDiffFor edge on (s=2, w=0) is unreachable but undocumented
status-row-classifier.ts line ~53: s=2 means "INDEX matches WORKDIR"; if WORKDIR is absent, INDEX is also absent and the encoding would use s=0, not s=2. The w === 0 early-return covers this correctly, but a brief comment that (s=2, w=0) is unreachable by the encoding would help future readers understand why the guard order doesn't need to worry about it.


No security issues found. Correctness is solid for all reachable tuples.

Comment thread src/server/infra/git/status-row-classifier.ts
Comment thread src/server/infra/git/status-row-classifier.ts
Comment thread test/integration/infra/git/isomorphic-git-service.test.ts
Comment thread test/integration/infra/git/status-tuple-reachability.test.ts
Comment thread test/integration/infra/git/status-tuple-reachability.test.ts
Comment thread src/server/infra/git/isomorphic-git-service.ts
Comment thread src/server/infra/git/git-error-messages.ts
- Switch cross-property invariant test to classifyTuple().dirty so it
  tracks pull()'s live filter instead of the removed raw formula
- Add expectedTuple identity assertions to every named-tuple scenario
  in the reachability fuzz, locking scenario names to actual matrix rows
- Fix [1,2,1] and [1,2,3] recipes that silently produced [1,1,1] and
  [1,2,2] due to stat-cache aliasing on same-size payloads (utimes bump,
  same workaround the existing [1,1,3] scenario uses)
- Remove misnamed [1,0,2] scenario; the tuple is unreachable by the
  encoding (w=0 forces s in {0,3}, never 2) and the recipe duplicated [1,0,3]
- Add comments noting the (s=2,w=0) and [0,0,0] unreachable edges in
  status-row-classifier
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

Claude finished @bao-byterover's task in 2m 12s —— View job


PR Review: feat/ENG-2516 (Re-review after synchronize)

Tasks

  • Gather context and understand the PR
  • Check what changed since the last review
  • Read updated changed files
  • Check if previous review issues were addressed
  • Post final review summary

Re-Review Summary

Score: 5 / 5

All four issues from the previous review have been addressed cleanly, plus two bonus fixes surfaced by the work. This is ready to merge.


Previous issues — all resolved

Suggestion (was: critical) — integration test used old pull filter
isomorphic-git-service.test.ts:462 now correctly imports and calls classifyTuple(head, workdir, stage).dirty instead of the removed row[2] !== 1 || row[3] !== 1 formula. The cross-property invariant test now exercises the live code path, not an in-test re-implementation of the old one. ✅

Nitpick — [1,0,2] scenario was unverified
The scenario has been removed entirely with a comment in status-tuple-reachability.test.ts:120–123 explaining why [1,0,2] is unreachable by the encoding (w=0 forces s to {0,3}, never 2). Removing the recipe is strictly better than adding a tuple assertion — it removes a misleading test rather than silently locking the wrong thing. ✅

Nitpick — shared state dependency in summary test
status-tuple-reachability.test.ts:338–341 now has a clear comment before the summary it block documenting that the per-scenario tests above populate the shared sets, and that the summary passes vacuously if scenarios are skipped. ✅

Minor — (s=2,w=0) unreachable edge undocumented
status-row-classifier.ts:56–57 now has an inline comment: "(s=2,w=0) is unreachable by the encoding (s=2 means INDEX==WORKDIR, so WORKDIR absent forces INDEX absent => s=0); the w===0 guard above handles it safely either way." The [0,0,0] unreachable case is also documented at line 99–101. ✅


Bonus fixes in commit 724a54b

[1,2,1] and [1,2,3] recipes had silent stat-cache aliasing
Both scenarios write same-size payloads, so isomorphic-git's stat cache can report stale results and the scenarios silently collapsed to [1,1,1] and [1,2,2] respectively. Both now apply a utimes bump (same pattern as the existing [1,1,3] fix), and both carry expectedTuple assertions that would fail loudly if the aliasing returned. These were latent test-correctness bugs not flagged in the first review; good catch.

pull-error-format.test.tsgit-error-messages.test.ts
The old file used source-file scraping (reading the raw .ts file and checking for string literals) — a brittle approach that would silently pass after a refactor. The replacement tests formatOverwriteMessage directly, covering all three operation × file-count combos. The rename is accurate since the helper covers pull, merge, and checkout.


No new issues found. Correctness, test coverage, and documentation are all solid.

Copy link
Copy Markdown
Collaborator

@cuongdo-byterover cuongdo-byterover left a comment

Choose a reason for hiding this comment

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

lgtm!

@bao-byterover bao-byterover merged commit f6e6b53 into main Apr 29, 2026
6 checks passed
@bao-byterover bao-byterover deleted the feat/ENG-2516 branch April 29, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants