Fix order-dependent PR-stack grouping via leaf-rooting#815
Conversation
buildStacks walked only downward (toward the base) and marked PRs consumed as it went, so a descendant discovered after its parent chain was already built could not attach and was dumped into loose. A 3-deep stack A(base=main) <- B(base=A) <- C(base=B) grouped correctly only when the input arrived leaf-first ([C,B,A]); for base-first or interleaved orderings the chain split and orphaned PRs into loose. The result depended on the order the API returned PRs. Root every chain from a leaf (a PR whose head branch is not any other PR's base branch) and walk down toward its base. A leaf-rooted walk captures the full chain in one pass regardless of input order. Output shape is unchanged: stacks (chains > 1, base->leaf, '#<base> -> #<leaf>' labels) and loose (everything else, in original order). Extracted buildStacks and the PRStack interface into a pure module (no React imports) so it is unit-testable, and added a table-driven vitest suite asserting that every permutation of the same stack yields identical grouping, plus cycle and fork edge cases.
…ch param Addresses the 4-model review of #815: - Remove the unused `_defaultBranch` param from `buildStacks` and drop it from the `LandingPage` useMemo + deps, fixing the latent stale-default split (UI held "main" while the real default was "master"). - Make fork grouping deterministic: visit candidate leaves in ascending PR number so the lowest-numbered leaf claims a shared ancestor regardless of input order; the sibling falls through to loose. - Resolve duplicate head branches deterministically in `byHead` (prefer open, then lower number) so chain-following no longer depends on input order. - Sort the returned `stacks` (by base PR number) and `loose` (by number) so independent stacks never swap positions between 30s polls. - Tidy the length-1 release to `stacked.delete(chain[0].id)`. - Tests: rewrite the fork test to assert a deterministic outcome across ALL permutations; add duplicate-head and leaf-into-cycle permutation tests; drop the sorted() wrappers so tests assert the now-deterministic output order. - Soften the doc comment + backlog: order-independent grouping including the fork tiebreak, explicitly stated as a 'one child wins' policy, not full fork collapse.
|
Fix pass applied from the 4-model interrogate review. Pushed to Fix 1 — drop dead Fix 2 — deterministic forks (UNANIMOUS). Candidate leaves are now visited in ascending PR- Fix 3 — deterministic Fix 4 — stable output order (Consider). Fix 5 — cycle test + loop tidy (Consider). Added a leaf-into-cycle permutation test pinning current bounded behaviour (terminates via the Verification.
|
The bug
buildStacks(project dashboard PR grouping inLandingPage.tsx) walked PR chains only downward (toward the base) and marked each PR consumed as it went. A descendant discovered after its parent chain was already built could not attach and was dumped intoloose.Concretely, a 3-deep stack
A(base=main) ← B(base=A) ← C(base=B)grouped correctly only when the input arrived leaf-first ([C, B, A]). For base-first ([A, B, C]) or interleaved orderings,Bwas consumed into[A, B]first; thenCwalked down, hit already-consumedB, had length 1, and orphaned intoloose. The grouping depended on the order the API returned PRs — which is wrong.The fix
Root every chain from a leaf — a PR whose
headBranchis not any other PR'sbaseBranch— then walk down from each leaf followingbaseBranch ← headBranchedges. A leaf-rooted walk captures the full chain in one pass regardless of input order.Output shape is unchanged:
stacks: chains of length > 1, ordered base → leaf, labelled#<base> → #<leaf>.loose: everything else, in original input order.The base PR (whose base is the default branch) is still included via the walk. Cycles fall through to
loose(their members are never leaves, and the walk guard prevents infinite loops). Forks (a branch that is the base of two PRs) degrade to one stack plus a loose sibling without double-counting the shared ancestor.To make it testable,
buildStacksand thePRStackinterface were extracted into a new pure moduleapps/frontend/src/components/landing/buildStacks.ts(no React imports).LandingPage.tsxnow imports both; the call-site signature is unchanged.Test coverage
New table-driven vitest suite
apps/frontend/src/components/landing/buildStacks.test.ts:#1 → #3stack with 3 PRs.The suite was confirmed to fail against the old downward-only algorithm (12 failures, including the base-first 3-deep case) and pass against the fix (18 passing). A separate parity check over 2000 random leaf-first forests confirmed the new algorithm agrees with the old one wherever the old one was correct, i.e. the fix is a strict generalization.
Verification
In
apps/frontend:bun run test→ 18 passed.bun run typecheck→ exit 0 (the only diagnostic is a pre-existing@vitest/browser-playwrightmodule-resolution error invitest.browser.config.ts, present on the base branch and unrelated to this change).Also marked the "PR stack splitting is order-dependent" backlog entry as DONE in
goals/frontend-session-lifecycle/backlog.md.