Skip to content

fix(mp-8pba): dispatch Swiss standings on engi flags and team sub-bouts - #377

Merged
gitrgoliveira merged 3 commits into
mainfrom
fix/swiss-standings-dispatch-mp-8pba
Aug 1, 2026
Merged

fix(mp-8pba): dispatch Swiss standings on engi flags and team sub-bouts#377
gitrgoliveira merged 3 commits into
mainfrom
fix/swiss-standings-dispatch-mp-8pba

Conversation

@gitrgoliveira

@gitrgoliveira gitrgoliveira commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • Swiss standings now dispatch on competition type. SwissStandings was 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.
  • Engi Swiss now ranks by Wins -> accumulated own-side Flags -> head-to-head -> name; team Swiss now accrues IV/IL/IT/PW/PL from SubResults and ranks on the full team chain -> head-to-head -> name. The individual path is unchanged, and head-to-head survives all three.
  • Frontend: the Swiss standings table hardcoded the individual W/L/D/PW/PL columns and mapped ipponsGiven->PW, so even a correct backend rendered zeros (engi has no Flags column; team populates pointsWon, not ipponsGiven). 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 on comp.Engi and isTeam), SwissStandings had a single ippon-only tally. A team parent match carries no ippons of its own (they live in SubResults) and an engi match carries FlagsA/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 through computeStandingsFrom. The pool core is structurally pool-based (groups matches by poolNameFromMatchID, applies per-pool TB/DH supplementary bouts and manual rank overrides). Swiss matches are keyed by parseSwissMatchRound (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 to engi.go to honor the hard-separation principle.

Engi keying subtlety (caught during browser UAT): Swiss matches persist no per-side UUIDs (SideAID/SideBID/WinnerID are empty in pool-matches.csv), and Swiss already treats names as unique identities. computeEngiSwissStandings therefore keys the roster by name; an engiPlayerKey ("id:") roster would never match the empty-ID matches and would tally nothing.

Files changed

File What
internal/engine/swiss.go SwissStandings dispatches: engi -> computeEngiSwissStandings; team -> inline SubResults accrual + teamStandingPoints sort; individual unchanged. Team score summary added.
internal/engine/engi.go New computeEngiSwissStandings (Wins -> own-side Flags -> head-to-head -> name, keyed by name). Imports domain.
internal/engine/scoring.go Shared helpers extracted after tri-review: accrueTeamSubResults, teamScoreSummary, individualScoreSummary (single definition of the team accrual + summary formats, used by both the pool core and Swiss).
internal/engine/swiss_test.go TestSwissStandings_Engi and TestSwissStandings_Team, using representative empty-ID Swiss matches.
web-mobile/js/viewer_standings.jsx SwissStandingsViewer reuses StandingsTableHead for engi (Flags) and team (IV/IL/IT/PW/PL) columns + paradigm-aware caption.
web-mobile/js/__tests__/swiss_standings_viewer.test.jsx New guard test for the engi Flags and team IV/PW columns.
docs/user-guide/organisers/naginata.md Engi tie-break section now covers Swiss (with its head-to-head step) + engi Swiss standings screenshot.
docs/user-guide/organisers/team-tournaments.md Team tie-break chain now scoped to Swiss too (+ head-to-head/name fallbacks) + team Swiss standings screenshot.
docs/user-guide/organisers/formats.md Swiss standings step links the kind-specific ranking detail.
docs/screenshots/swiss-standings-{engi,team}.png New docs screenshots (from this PR's browser verification).

Review

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 in 7803bacd by extracting shared helpers, plus a sibling-sweep engiScoreSummary.

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:

engi swiss standings

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:

team swiss standings

Test plan

  • make go/test passes (lint + security scan + tests); engine coverage 86.1% (>=85%)
  • npm test (JS) passes — 2473 tests; oxlint clean
  • New/updated unit tests cover the change, each confirmed red without the fix (Go TestSwissStandings_Engi/_Team; JS swiss_standings_viewer)
  • Manual browser verification: created an engi Swiss and a team Swiss competition, scored both over 2 rounds through the admin UI, and confirmed the public standings table renders the Flags / IV columns with the correct tie-break ordering (screenshots above)
  • Screenshots added above

gitrgoliveira and others added 3 commits July 31, 2026 22:31
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>
@gitrgoliveira
gitrgoliveira merged commit 24a0a05 into main Aug 1, 2026
12 checks passed
@gitrgoliveira
gitrgoliveira deleted the fix/swiss-standings-dispatch-mp-8pba branch August 1, 2026 09:04
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.

1 participant