fix(mp-8pba): dispatch Swiss standings on engi flags and team sub-bouts - #377
Merged
Conversation
SwissStandings was the only standings routine that never dispatched on competition type: it unconditionally tallied match-level ippons, so engi Swiss (flag-scored) and team Swiss (sub-bout-scored) competitions lost all their tie-break data and fell through to head-to-head then name order. Root cause: unlike computeStandingsFrom (the pool/league core, which branches on comp.Engi and isTeam), SwissStandings had a single ippon-only tally. Team parent matches carry no ippons of their own (they live in SubResults) and engi matches carry FlagsA/FlagsB, so both surfaced as zeros. Fix: duplicate the dispatch inside SwissStandings rather than route through computeStandingsFrom. The pool core is structurally pool-based (groups by poolNameFromMatchID, applies per-pool TB/DH bouts and manual overrides); Swiss matches are keyed by parseSwissMatchRound, form one cumulative cross-round group with bye-as-win semantics and a head-to-head tiebreak the pool path lacks. Routing through it would drop every Swiss match. - engi -> computeEngiSwissStandings (engi.go, hard-separation principle): ranks Wins -> own-side Flags -> head-to-head -> name. Keyed by name, not engiPlayerKey: Swiss matches persist no per-side UUIDs, and Swiss already treats names as unique identities. Keying by "id:<uuid>" would never match the empty-ID matches and tally nothing. - team -> inline SubResults accrual (IV/IL/IT/PW/PL), ranked by the packed teamStandingPoints chain -> head-to-head -> name. - individual path unchanged. Head-to-head survives all three. Frontend: SwissStandingsViewer hardcoded the individual W/L/D/PW/PL table and mapped ipponsGiven->PW, so even with the backend fix the tie-break columns rendered as zeros (engi has no Flags column; team populates pointsWon, not ipponsGiven). It now reuses StandingsTableHead to show Flags (engi) and IV/IL/IT/PW/PL (team), with a paradigm-aware "Ranked by" caption. Tests (all confirmed red without the fix): Go TestSwissStandings_Engi/_Team using representative empty-ID Swiss matches, and JS swiss_standings_viewer covering the engi Flags and team IV/PW columns. Closes mp-8pba Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ats (tri-review)
Tri-review round 1: the one confirmed finding (simplify, low) was that the
team SubResults accrual loop and the team ScoreSummary format string in the
new SwissStandings branch were near-verbatim copies of computeStandingsFrom's
team branch. This is distinct from the recorded dispatch-duplication decision:
the inner tally math has no dependency on the pool grouping/overrides wrapper,
so it can live in one place without touching the dispatch seam.
- accrueTeamSubResults(sA, sB, m): single definition of the IV/IL/IT/PW/PL
accrual, called from both the pool core and SwissStandings.
- teamScoreSummary / individualScoreSummary: single definition of each
format string, both assembly loops updated.
- Sibling-sweep: engiScoreSummary extracted in engi.go ("W:%d Flags:%d" was
duplicated between computeEngiStandings and computeEngiSwissStandings).
Behavior-neutral: identical output strings and accrual order; make go/test
green, engine coverage unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engi and team tie-break docs scoped themselves to "pools and leagues" / "pools and league", which was only accurate while Swiss standings ignored flags and sub-bout results. Now that Swiss ranks both correctly: - naginata.md: engi ranking applies to Swiss too, with the Swiss-only head-to-head step inserted before the name-order fallback; adds a screenshot of the engi Swiss final standings table. - team-tournaments.md: the eight-criteria chain applies to Swiss as well, followed by head-to-head then name order; adds a screenshot of the team Swiss standings table with the full tie-break columns. - formats.md: the Swiss "review the standings" step now points at the kind-specific ranking detail instead of implying points-scored for all. Verified with make docs/build (strict); anchors checked in the rendered HTML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SwissStandingswas the only standings routine that never branched on engi vs team vs individual: it unconditionally tallied match-level ippons, so engi Swiss (flag-scored) and team Swiss (sub-bout-scored) competitions lost all their tie-break data and fell through to head-to-head, then name order.SubResultsand ranks on the full team chain -> head-to-head -> name. The individual path is unchanged, and head-to-head survives all three.W/L/D/PW/PLcolumns and mappedipponsGiven->PW, so even a correct backend rendered zeros (engi has no Flags column; team populatespointsWon, notipponsGiven). It now shows Flags (engi) and IV/IL/IT/PW/PL (team), with a paradigm-aware "Ranked by" caption.Why
Root cause: unlike
computeStandingsFrom(the pool/league core, which branches oncomp.EngiandisTeam),SwissStandingshad a single ippon-only tally. A team parent match carries no ippons of its own (they live inSubResults) and an engi match carriesFlagsA/FlagsB, so both surfaced as all-zero columns and the tie-break collapsed to name order. Swiss exists specifically to separate a large field over a fixed number of rounds, so its tie-break does more work than any other format's.Decision (recorded per acceptance criteria): duplicate the dispatch inside
SwissStandings, NOT route throughcomputeStandingsFrom. The pool core is structurally pool-based (groups matches bypoolNameFromMatchID, applies per-pool TB/DH supplementary bouts and manual rank overrides). Swiss matches are keyed byparseSwissMatchRound(Swiss-R{n}-{i}), form one cumulative cross-round group with bye-as-win semantics and a head-to-head tiebreak the pool path lacks. Routing Swiss through the pool core would drop every Swiss match (no pool ID). So the fix mirrors the pool path's dispatch while keeping Swiss's bye + head-to-head logic; the engi branch delegates toengi.goto honor the hard-separation principle.Engi keying subtlety (caught during browser UAT): Swiss matches persist no per-side UUIDs (
SideAID/SideBID/WinnerIDare empty inpool-matches.csv), and Swiss already treats names as unique identities.computeEngiSwissStandingstherefore keys the roster by name; anengiPlayerKey("id:") roster would never match the empty-ID matches and would tally nothing.Files changed
internal/engine/swiss.goSwissStandingsdispatches: engi ->computeEngiSwissStandings; team -> inlineSubResultsaccrual +teamStandingPointssort; individual unchanged. Team score summary added.internal/engine/engi.gocomputeEngiSwissStandings(Wins -> own-side Flags -> head-to-head -> name, keyed by name). Importsdomain.internal/engine/scoring.goaccrueTeamSubResults,teamScoreSummary,individualScoreSummary(single definition of the team accrual + summary formats, used by both the pool core and Swiss).internal/engine/swiss_test.goTestSwissStandings_EngiandTestSwissStandings_Team, using representative empty-ID Swiss matches.web-mobile/js/viewer_standings.jsxSwissStandingsViewerreusesStandingsTableHeadfor engi (Flags) and team (IV/IL/IT/PW/PL) columns + paradigm-aware caption.web-mobile/js/__tests__/swiss_standings_viewer.test.jsxdocs/user-guide/organisers/naginata.mddocs/user-guide/organisers/team-tournaments.mddocs/user-guide/organisers/formats.mddocs/screenshots/swiss-standings-{engi,team}.pngReview
Tri-review (simplify + correctness + security, adversarially verified) ran post-CI: correctness and security lenses clean; the one confirmed simplify finding (team accrual/summary duplicated from
computeStandingsFrom) is fixed in7803bacdby extracting shared helpers, plus a sibling-sweepengiScoreSummary.Screenshots
Engi Swiss standings (2 rounds) — Bravo (1W, 7 flags) correctly ranks above Alpha (1W, 5 flags) on the flag tie-break; before the fix both showed 0 flags and the tie fell to name order:
Team Swiss standings (2 rounds) — Bara (1W, IV 3) correctly ranks above Aoi (1W, IV 2) on the individual-victory tie-break; before the fix IV/PW rendered as zeros:
Test plan
make go/testpasses (lint + security scan + tests); engine coverage 86.1% (>=85%)npm test(JS) passes — 2473 tests;oxlintcleanTestSwissStandings_Engi/_Team; JSswiss_standings_viewer)