Skip to content

Matchmaking Pipelines — Status Tracker

Status: Active — living tracker, update in place as fixes ship Last reviewed: 2026-07-18 Purpose: the single place to check how healthy match generation is and what the next highest-leverage fix is — one row per pipeline at a glance below, one detail section per pipeline further down. Scope: the 7 pipelines as they stand after Slices 0–3 shipped (rotation core · feasibility forecast · availability v1 · the 4 tournament engines wired · the minimal tournament-experience wedge · the pluggable registry, now activated). Score-driven formats (P7): Americano + Mexicano + the 순위 standings/rating-impact surface + the 친선 rating-gate + King-of-the-Court all shipped — P7 complete. Grounding: 3 read-only adversarial audit passes over the shipped system, verified against source — ~45 findings regrouped into the 7 pipelines below. Live against preview OTA 559eaaa9 (HEAD a8aab7b5, 2026-07-15). Companion artifacts + the Slice 1 spec + match-generation canon: see Links.


How to update this doc

This tracker is living — edit it directly as fixes ship. Don't wait for a new audit pass to keep it honest.

  • A fix ships: move the finding(s) it closes from that pipeline's Gaps list to a Resolved list directly under it (add one if the section doesn't have one yet). Keep the finding ID and add a one-line "what changed" + commit ref. Delete the matching line from Fixes → contribution once every finding it closed is resolved, or narrow the line to whatever's left.
  • Health %: bump the number in both places it appears — the pipeline's row in the status table and the bold status line directly under its ## PN heading (e.g. **Mature · 85% · 0 high**) — moving toward that pipeline's stated Target. The heading text itself (## P1 — Rotation generation) stays fixed on purpose, so its anchor link never breaks. Health is a judgment call weighted toward High/Correctness findings, not a formula; don't over-precision it.
  • High-count: after any fix, recount the remaining [High …] tags in that pipeline's Gaps list and update the status-table column.
  • Status label: move along the same ladder the 7 rows already use — Not startedv1 · incomplete / Wedge · unfinishedWired · sound core / Built · half-liveFunctional · IA pending / N of M · <remainder> pendingSolidShipped / ActivatedMature. Reuse a rung rather than inventing a new label; if none fits, add the new label to this sentence too.
  • A pipeline hits its Target: flip Status to Mature (or Shipped/Complete as fits), set Health to the target %, confirm Gaps is empty, and leave the Fixes/Target lines in place as a record of what closed it.
  • A new finding surfaces (regression, or the next audit pass): add it to the right pipeline's Gaps with a severity tag and an ID that doesn't collide with existing ones in that letter series (A#/B#/C#/D#/E#/T-#, matching the flat audit inventory's numbering).
  • Keep Recommended sequence in sync if a batch's scope changes — mark a step DONE rather than deleting it, so the doc still reads as history.

Severity tags: High (fix before real use) · Correctness (wrong output/copy today) · Moderate (UX/rough edge) · Behavioral (latent trap, invisible until triggered) · Cosmetic (dead code / doc-drift).


Status at a glance

PipelineStatusHealthHigh-countNote
P1 · Rotation generationMature100%0A2b/E1 closed (2026-07-18): fixed_pairs is deterministic circle-method + shared-scorer ELO/gender — no random search, no flaky coverage.
P2 · Feasibility forecastMature100%0T-9/T-10 closed (2026-07-18, approved wireframes): all-graphic stat tiles + 2-row court grid to 8 courts — the readout is symmetric, no cliff.
P3 · Availability / late-arrivalMature100%0D4a/D3a/D5a + D1a closed (2026-07-18): session-relative rounds, scoped host RLS (00433), rollback, and the inline arrival round/time picker.
P4 · Tournament enginesMature95%0A6 closed (2026-07-18): fake 0점 column removed, Buchholz draws-corrected + surfaced, head-to-head + competition ranking — honest standings.
P5 · Tournament experienceComplete95%0Chooser ship closed T-3/T-4; cosmetic-tail wave (2026-07-18) closed D3 (live board header) + the 조별리그 knockout-knob threading.
P6 · Pluggable registryActivated100%0styleId now threads into generateRound's resolveRoundStrategy; registry-driven selection is live on the round path — P7's prerequisite is met.
P7 · Score-driven formatsComplete100%0All formats shipped; T-17 closed (2026-07-18) — dead results_driven policy values retired, canon corrected to the shipped styleId/registry seam.

P1 — Rotation generation

Mature · 100% · 0 high

The core round-by-round 경기 만들기 loop — resolve pool → apply constraints → strategy builds the round → preview → commit. The mature spine; every session's matches come through here.

Shipped

  • Shared pipeline + registry + constraint model (Slice 1)
  • 혼복 (mixed-doubles) gender fix (rotate_all/skill_balanced, Slice 1 soft courtScore penalty)
  • fixed_pairs opponent-variety fix
  • 혼복+랜덤 gender-first pairing (quick-win batch, 2026-07-15) — generateRandom now forms every doubles team as 1M+1F via pairDoublesTeamsGenderFirst when requireMixedGender + real gender data are present, with best-effort maximization on skewed rosters (rotation.strategy.ts; tests in rotation.rules.test.ts)

Resolved

  • [Correctness A4] 혼복+랜덤 yields same-gender teams — FIXED: generateRandom threads ctx+constraints and routes doubles through gender-first pairing instead of the flat gender-blind shuffle-and-slice; falls back to plain random when there's no gender data (quick-win batch, 2026-07-15; rotation.strategy.ts)

  • [Cosmetic] Dead-code sweep — CLOSED (2026-07-18): skill_balanced needed NO code change — the tracker's "= rotate_all alias" phrasing was the stale part; the code already self-documents the alias and the 실력별 preset IS behaviorally differentiated at the config layer (balanceByElo/eloTolerance consumed by the shared courtScore). benchMode:'volunteer' stays as a documented future seam, but its orphaned benchModes i18n dict (zero consumers, ko+en) was deleted. MatchFormatConfig 'mixed' deleted from union + zod (verified unpersisted — RotationConfig is in-memory only, never crosses a mapper/adapter). avoidPairs/lockedPairs deleted from AssignmentConstraints/buildConstraintSet/3 test fixtures (no partial wiring existed to preserve). Stale comments fixed: constraints.ts "not yet wired" (it runs unconditionally in generateRound), types.ts phantom scoreArrangement/applyConstraints names, resolve-pool.ts "dormant" framing (the availability seam is live)

  • [Moderate A2b] + [Behavioral E1] — FIXED together (2026-07-18): generateFixedPairs's bounded RANDOM shuffle-and-score replaced with the deterministic enumerate-and-score template freedomGreedySingles already established — circleMethodPairings over synthetic per-PAIR tokens enumerates every pair-vs-pair grouping, each candidate scored via the shared courtScore(pairA, pairB, history, config, ratings, genderMap), lowest wins (ties → first-enumerated; zero Math.random() in the path). This routes balanceByElo/eloTolerance (and the grouping-invariant requireMixedGender term, documented) through the shared scorer — closing A2b — AND makes opponent coverage exact rather than ~99.2%-probable — closing E1 (the flaky exact-coverage test now holds with probability 1; the old "does-NOT-minimize" probabilistic test was replaced by a strictly stronger byte-equality assertion; +ELO-balance and determinism tests)

Gaps

None.

Fixes → contribution

  • Route fixed_pairs gender/ELO through the shared scorer → closes A2b — DONE 2026-07-18 (see Resolved)
  • Harden fixed_pairs coverage, or emit a repeat warning → closes E1 — DONE 2026-07-18 (deterministic construction; see Resolved)
  • Dead-code sweep — honest aliases, remove/wire the dead flags — DONE 2026-07-18 (see Resolved)

Target: consistent constraint model, zero silent drops, no dead scaffolding → 100% — HIT.


P2 — Feasibility forecast

Mature · 100% · 0 high

The pre-generation "will this session work" readout — games/player, sit-outs, max-late budget, "need N more." A market-first surface (no competitor ships it).

Shipped

  • computeAttendanceViability + the graphical readout
  • computeFairnessReport surfaced
  • Availability-aware games/player estimate (quick-win batch, 2026-07-15) — gamesPerPlayer now accumulates each player's own per-round slot share (respecting fromRound) instead of dividing one full-pool figure evenly; unchanged when no availability map is passed (feasibility.ts)

Resolved

  • [Correctness C1] "예상 게임 수" (estimated games) was availability-blind — FIXED: gamesPerPlayer is built round-by-round from the same present/courtsUsed numbers perRound reports, crediting each player only for rounds they were actually present for (quick-win batch, 2026-07-15; feasibility.ts)

  • [Behavioral C3] A second, dead MatchmakingStrategy.feasibility() — FIXED (2026-07-18): deleted the losing (dead) side of the name collision wholesale — the narrow Slice-1 stub FeasibilityReport interface, the feasibility() method from both RoundStrategy/TournamentStrategy contracts, all 8 strategy implementations (which carried hardcoded raw-Korean strings, a domain-layer violation), the StrategyFeasibilityReport barrel alias, and the 7 test blocks exercising it. Grep-verified ZERO production call sites existed (add-round-sheet.tsx calls computeAttendanceViability directly; the chooser's per-format gates are deliberately UI-owned constants). feasibility.ts's rich shape is now the single FeasibilityReport

  • [Moderate T-9/T-10] Readout asymmetry + >4-court cliff — FIXED (2026-07-18, owner-approved wireframe options A/A): all three stat tiles now carry a 28px graphic + bold value + micro label (games → ascending range bars, filled = min; 지각 여유 → head-count dots) so the CapacityRing stops being the odd one out; the court strip wraps 5-8 courts into a 4-col×2-row grid of 16px CourtLinesTiles and only falls back to ratio+bar above 8 — a deliberate, commented ceiling instead of a cliff at 4. 불가 state untouched

Gaps

None.

Fixes → contribution

  • Consolidate to one feasibility source — wire or delete the dead one → closes C3 — DONE 2026-07-18 (deleted; see Resolved)
  • UI polish (tile symmetry, large-court treatment) → closes T-9, T-10 — DONE 2026-07-18 (see Resolved)

Target: accurate, late-arrival-aware forecast from one source → 100% — HIT.


P3 — Availability / late-arrival

Mature · 100% · 0 high

Per-player arrival → the per-round generation pool. A player marked "from round 3" is benched until then. The pool-constraint seam Slice 1 built, with a Slice 2 marking UI. The 2026-07-18 wave closed the round-anchoring, RLS-scope, and rollback gaps; the wall-clock time-picker (D1a) is in wireframe review.

Shipped

  • available_from_round + host tap-to-cycle roster
  • resolvePool wiring
  • startRoundNumber fix
  • Honest catch-up copy (quick-win batch, 2026-07-15) — the late-arrival hint always shows the real catch-up behavior regardless of the stored late_arrival_policy value, via the new lateArrivalHint() (presentation/utils/late-arrival-hint.ts); the dead/dishonest lateArrivalPolicyEqualRest i18n key is removed; the schema default site documents that equal_rest_forward isn't implemented yet (session.entity.ts)

Resolved

  • [Correctness D2] Every session showed "균등 휴식" (equal rest) late-arrival copy, but the engine only ever does catch-up — FIXED via the "fix the copy" branch (not "make the policy real" — resolvePool still only implements catch-up, now documented inline): the UI always shows the honest catch-up hint (quick-win batch, 2026-07-15; presentation/utils/late-arrival-hint.ts, add-round-sheet.tsx)

  • [Behavioral D4a] Absolute round constants — FIXED (2026-07-18): computeAttendanceViability gained an optional startRound = 1 param (defaults byte-identical for every pre-existing call); add-round-sheet passes nextRoundNumber, so the forecast window and the host arrival cycle are anchored at the session's REAL next round ([nextRoundNumber+1 … +5], wrap → 도착). New pure cycleArrivalRound helper + tests; the wrap maps to the round-1 "not late" sentinel so the roster badge convention holds, and a stale out-of-window fromRound clamps cleanly into the window on the next tap

  • [Behavioral D3a] Whole-row host UPDATE — FIXED (2026-07-18): migration 00433_rsvp_host_availability_scope (ON PROD) adds a private.protect_rsvp_columns_from_non_admin_host() BEFORE UPDATE trigger following 00074's protect_columns silent-revert idiom with actor-conditional branching (SECDEF/self/club-admin bypass; a non-admin host's writes to anything except available_from_round/arrives_at silently revert). RLS WITH CHECK can't see OLD — a trigger is the only column-scoping mechanism. pgTAP rsvp_host_availability_scope.test.sql (17 asserts; suite now 27 files / 269 asserts). NOTE: 00303's session_payments host policy is the same whole-row shape but its host flow legitimately writes status — deliberately not remediated, flagged in the migration header

  • [Behavioral D5a] No rollback on the availability tap — FIXED (2026-07-18): updateAvailability.mutate now passes a per-call onError that deletes the optimistic override key — effectiveAvailability falls back to the server-sourced map, so deletion IS the rollback

  • [Behavioral D1a] Wall-clock arrival never wired — FIXED (2026-07-18, owner-approved wireframe option A + alignment directive): the tap-to-cycle badge is replaced by an inline expansion card (flush with the roster's avatar/badge columns — no arbitrary indent, per owner) with session-relative round chips (도착 + next 5) and a 시간으로 chip opening the already-linked native DateTimePickerModal (mirrors create-session-step1; OTA-safe). A picked time writes arrives_at AND derives available_from_round via the new arrivesAtToRound(arrivesAt, sessionStart, rotationMinutes ?? 30) util (+tests); a round chip writes the round and clears the time; the badge then shows 시간 · N라운드부터 via formatTimeOfDayKo12. Same onError override-delete rollback; cycleArrivalRound deleted with its sole consumer. Modal-over-ModalPanel is first-of-its-kind — flagged for on-device QA

Gaps

None.

Fixes → contribution

  • Session-relative round numbering → closes D4a — DONE 2026-07-18 (see Resolved)
  • Build the wall-clock time-picker → closes D1a — DONE 2026-07-18 (see Resolved)
  • Column-scoped host RLS + pgTAP + optimistic rollback → closes D3a, D5a — DONE 2026-07-18 (see Resolved)

Target: honest policy, round+time marking, correct mid-session numbering, tight auth → 95%


P4 — Tournament engines

Mature · 95% · 0 high

The four bracket/swiss algorithms (Swiss · Knockout · Round-Robin · Compass) + their round math + standings — the compute layer behind a tournament.

Shipped

  • 4 engines wrapped as strategies
  • 3 latent bugs FIXED in the wiring: bracket silent-drop, tournament dead-end knockout, compass non-8-player

Resolved

  • [High A2] Round-robin group-stage overflow stamped overflow pairings to a permanent courtNumber: 0 (invisible, no way to enter a result — so the group stage could never see every match 'completed' and never reached knockout) — FIXED: generateGroupStage now splits each schedule round into ceil(pairings/courtCount) wave-slots, the same fix shape already applied to the bracket engine; the round-robin-groups strategy inherits it transitively via generateTournamentBracket (commit 58319217; tournament.rules.ts:103-158)

  • [High A3] "다음 라운드" (next round) with partial results silently orphaned matches and dropped players — FIXED: Swiss/Compass advance() now throws "Cannot advance: N matches unfinished" instead of silently continuing past them (commit 58319217; swiss.rules.ts:348-353; compass.rules.ts:268-271,384-389)

  • [Moderate A6] Fake standings — FIXED (2026-07-18) without adding score entry (winner-pick-only is deliberate, spec §13.5): (a) the always-0 "득점" column REMOVED from the board (pointsFor is structurally unpopulatable — TournamentMatch carries no score); (b) calculateBuchholz now counts opponents' draws (wins + 0.5*draws, matching the file's own score convention) and swiss.strategy.ts's toGroupStanding forwards the already-persisted buchholz to the new optional GroupStanding.buchholz (read-side only, zero migration); Swiss rows render a 부흐홀츠 meta chip; (c) round-robin calculateStandings breaks exactly-2-way score ties by head-to-head (3+-way/cyclic falls through unchanged); (d) the board uses competition ranking (competitionRanks helper — equal key ⇒ shared rank, 1-2-2-4) so bracket/compass ties stop implying false precision

Gaps

None — score-based standings remain intentionally out of scope while the board is winner-pick-only; a seed/rating tiebreak for bracket/compass would need a state-shape change and stays unscoped.

  • [Cosmetic E2] Compass gets stuck at small N (compass.rules.ts); orphaned second recommender — recommendFormat (4-tier, bracket.rules.ts:514) is fully orphaned while only recommendStrategy (3-tier, recommend-format.ts:48) is wired

Fixes → contribution

  • Wire real standings — show Buchholz, stable tiebreak → closes A6 — DONE 2026-07-18 (see Resolved; score entry deliberately NOT added)

Target: complete, safe engines with real standings → 95% — HIT.


P5 — Tournament experience

Complete · 95% · 0 high

The run-loop a host actually touches: create → board → advance → standings — plus how it lives in the app's IA and who's allowed to use it. Wave A+B (2026-07-15) closed the return-path/dedup/RLS/entitlement/concurrency/pairing gaps; the 2026-07-17 chooser ship closed the IA consolidation (T-3/T-4); the 2026-07-18 cosmetic-tail wave closed D3 + the 조별리그 knob threading.

Shipped (deliberately minimal wedge — "enough to inspect, not pixel-final")

  • Board + create/advance mutations
  • Create-tournament entry point
  • Tournaments-real Wave A+B (2026-07-15) — return path (이어보기) + dedup + host-only cancel/delete recovery, host-OR-admin RLS, compare-and-swap advance, entitlement-gated create, an advance confirm dialog, ELO-balanced doubles pairing, and the bracket ko-label fix (commits 58319217 + 27cbea02)
  • P5 chooser ship (2026-07-17) — the unified 경기 방식 chooser (format-chooser-sheet.tsx: ONE registry-backed ModalPanel, 라운드 방식 presets+score-driven group · 토너먼트 strategy group, eligibility badges/gates) replacing session-detail's triple-CTA; CreateTournamentModal gained a 대회 setup step (조별 인원 · 진출 방식 top-N/전원 시드 · 조 편성 레이팅/랜덤 · read-only 경기 시간 → match-rules hint; knobs render ONLY for bracket ≥9 entrants — round_robin_groups.init has no options param, so knobs there would be silently ignored); the persistent 이어보기 chip renders for any ACTIVE tournament at EVERY session status; engine knobs (TournamentConfig advancementPolicy/seeding + snakeSeedByCapacity, defaults byte-identical); naming v3 (research-grounded region-friendly names: KDK 복식/단식 · 짝바꿈 복식 · 순위매칭전 · 코트승급전 · 전적매칭 토너먼트 · 전원부활전 · 조별리그/토너먼트 kept); session.style retired (SessionStyle narrowed to 'social'|'competitive', SessionTemplate vertical deleted — generation_style_id is the real matchmaking field)

Resolved

  • [High T-1] One-way door — no return path to a live tournament; findBySession existed end-to-end but nothing called it — FIXED: session detail resolves useTournamentBySession and shows a "토너먼트 이어보기" button straight to the board whenever one exists (commit 27cbea02; session-detail-screen.tsx:265-267,1473-1483)

  • [High T-2/E3] No duplicate-tournament guard (re-tap → raw Postgres unique-violation toast) + no delete/cancel mutation anywhere, so a stuck/1-player tournament was permanently undeletable — FIXED: the same useTournamentBySession lookup resolves re-tap to "이어보기" instead of a second create, and the new useCancelTournament mutation (wraps the previously-uncalled TournamentRepositoryPort.delete) gives the host a destructive-confirm escape hatch back to a fresh create (commits 58319217+27cbea02; use-cancel-tournament.ts; tournament-board-screen.tsx:210-229)

  • [High D6] RLS required club role owner/admin/match_director, but the button showed for the literal session host, so a non-admin 번개 host hit a raw Postgres RLS-denial toast — FIXED: migration 00427 (APPLIED TO PROD) adds additive permissive host policies (tournaments_insertable_by_host/_updatable_by_host/_deletable_by_host, WITH CHECK pinned to club_id), mirroring 00303's pattern, plus pgTAP 00029 (8 assertions) (commit 58319217; supabase/migrations/00427_tournament_host_rls.sql:54-94)

  • [High C5] No concurrency control on advance — two devices/tabs could read-then-write with no version predicate, so the second submit silently clobbered the first — FIXED: updateIfCurrentRound(id, expectedRound, input) is now an atomic WHERE id AND current_round = expectedRound compare-and-swap that throws ConflictError instead of clobbering (commit 58319217; tournament.repository.port.ts:10-19; tournament.supabase.ts:146-175; use-advance-tournament.ts:68)

  • [High T-6] tournamentsEnabled paid-tier entitlement was defined but unenforced — every plan could create real brackets — FIXED: the create branch (not continue/이어보기) is now gated behind useFeatureGate(session.clubId).entitlements.tournamentsEnabled (commit 27cbea02; session-detail-screen.tsx:267,1484)

  • [Moderate D1] Doubles tournament pairing was sequential-RSVP, not ELO-balanced, able to stack two high-rated players on one team — FIXED: pairForDoubles now sorts entrants by buildEloSnapshot rating and cross-pairs strong+weak (rank 1 with rank N, etc.) — the standard balance technique; an odd player out still sits out unpaired (unchanged rough edge) (commit 27cbea02; create-tournament-modal.tsx:73-82,105)

  • [Moderate D4] Winner-pick had no confirm and was only zero-blocked (any single pick, not all picks, unblocked "다음 라운드") — FIXED: allWinnersPicked now requires every pending match to have a pick before the button is even pressable, and advancing routes through a useConfirm dialog first (commit 27cbea02; tournament-board-screen.tsx:137-158,191-201,380-381)

  • [Moderate T-5] 토너먼트↔토너먼트 Korean name collision between the screen and the bracket-format label — FIXED: the bracket strategy's Korean label changed from "토너먼트" to "녹아웃" (commit 27cbea02; bracket.strategy.ts:375-377) — superseded 2026-07-17: naming v3 restored 토너먼트 (owner-locked — in Korean it specifically means knockout); disambiguation now comes from the chooser's group structure + the 한 번 지면 탈락 subtitle

  • [High T-3/T-4] Triple-CTA + dead session.style + open/locked-scoped 이어보기 — FIXED (2026-07-17): session detail's two full-width buttons collapsed into ONE 경기 방식 선택 NavRow (gated host/canManageSchedule/canManageMatches × open/locked/in_progress) opening the unified FormatChooserSheet; a round pick seeds this screen's own AddRoundSheet mount (settle-before-play-gated), a tournament pick preselect-skips into CreateTournamentModal's setup step; the 이어보기 chip (TournamentContinueChip, live PulseDot + current round) renders for any active tournament at EVERY session status — a running tournament can never strand again; session.style retired outright (entity narrowed, mapper coerces legacy rows to 'social')

  • [Moderate D3] Static "토너먼트" board header — FIXED (2026-07-18): DetailShell now carries a live subtitle + status-gated PulseDot titleLeading, mirroring session-detail-screen's canonical header treatment exactly. in_progress reuses tournaments.continueChipLabel(formatLabel, latestRoundNumber) so the header and the session-detail 이어보기 chip say the same words about the same tournament; every other status composes formatLabel · status.<key> from keys that already existed — zero new i18n (tournament-board-screen.tsx)

  • [Cosmetic] 조별리그 silently discarded the 대회 knobs — FIXED (2026-07-18): round_robin_groups.strategy.ts's init now accepts TournamentInitOptions and honors the ONE honestly-applicable axis — advancementPolicy/advancersPerGroup map onto the pre-existing generateKnockoutStage(standings, topN) parameter via a new optional TournamentBracket.knockoutFieldSize ('all_seeded' → full-field seeded knockout; default ?? 4 byte-identical to the legacy semis+final). groupSize/seeding are deliberately NOT read — the kernel runs ONE exhaustive round-robin (no pools to size or seed), so wiring them would be theater. UI knob exposure for 조별리그 stays deferred (a wireframe-first design decision: the shared panel's advancers default 2 / Stepper max 3 can't represent this strategy's default 4)

Gaps

None — the moderate/cosmetic tails above are closed; the only remaining product decision is whether to expose 조별리그's knockout-size knob in the setup UI (deferred, wireframe-first).

Fixes → contribution

  • IA cleanup — one lifecycle-aware entry point (covering every session status, not just open/locked), decouple/fix the style label → closes T-3, T-4, D5 — DONE 2026-07-17 (the P5 chooser ship above)
  • Live board header (D3) + 조별리그 knob threading — DONE 2026-07-18 (see Resolved)

Target: a recoverable, correctly-gated, coherent tournament experience → 95% — HIT. No High-severity items remain in any pipeline.


P6 — Pluggable registry

Activated · 100% · 0 high

The config-driven strategy architecture — the owner's original ask ("choose from algorithms, keep adding"). Adding an engine = one registry entry.

Shipped

  • Split RoundStrategy | TournamentStrategy contract
  • Registry + 5 strategies
  • recommendStrategy
  • Registry-driven round-path activation (2026-07-15) — GenerateOptions.styleId now threads into generateRound's resolveRoundStrategy (falls back to round_robin when unset, so today's output is byte-identical); proven live end-to-end by P7's americano/mexicano registrations (commit 8413c450; rotation.rules.ts:99,237-239)

Resolved

  • [Behavioral B1] generateRound hardcoded DEFAULT_GENERATION_POLICY; styleId was never set anywhere, so the round path couldn't actually select a strategy — FIXED: GenerateOptions.styleId now threads into generateRound's resolveRoundStrategy; registry-driven selection is live on the round path (byte-identical when no styleId is set) (commit 8413c450; rotation.rules.ts:99,237-239)

Gaps

None — B1 was the only gap and it's resolved above.

Fixes → contribution

  • Thread the session's GenerationPolicy (with styleId) into generateRound → closed B1 — DONE (commit 8413c450): activated registry-driven selection on the live path; was the exact seam P7 needed

Target: fully config-driven selection → 100% — HIT. Was the prerequisite for P7 — now satisfied.


P7 — Score-driven formats

Complete · 100% · 0 high

Americano · Mexicano · King-of-the-Court — the net-new formats where pairings adapt to accumulated points and courts move by result.

Shipped

  • Americano — rotate-all generation (delegates to rotationStrategy's rotate_all path, americano.strategy.ts:31-42); its market identity is individual-point scoring, computed by the shared computePlayerPoints ledger (score-driven.ts:33) (backend, commit 8413c450)
  • Mexicano — rank-adaptive: ranks the pool by pointsMap each round and pairs rank-crossover (1&3 vs 2&4 in doubles, adjacent-rank in singles); falls back to rotate_all on round 1 / before any points exist (backend, commit 8413c450; mexicano.strategy.ts:36-41,66-83)
  • The styleId selection seam — both formats registered in STRATEGY_REGISTRY and selectable in the 방식 picker alongside the rotation presets (UI wave, uncommitted-but-verified; add-round-sheet.tsx:116-117,128-132)
  • The rolling GenerationLifecycle enum value — KOTC's future foundation; no strategy sets it yet (commit 8413c450; generation-policy.entity.ts:38)
  • King-of-the-Court (2026-07-16) — the rolling court-ladder: king-of-court.strategy.ts snake-seeds round 1 by ELO (balances court strength), then each round's ascender/descender is derived from the prior court results; a 5:5 draw tiebreaks for MOVEMENT ONLY (higher cumulative points → deterministic 가위바위보; the match stays a real draw for score/points/ELO), partners reshuffle within each court. Court-primary 킹코트 ladder scorecard tab (reuses MatchScoreboard + an ElapsedClock ambient timer) with a ▲승격/▼강등/👑방어 movement preview + 가위바위보/점수 badges; 방식 picker entry gated to 2+ courts; kingOfCourtMatchRules (allowDraw + no tiebreak). Rides the score-driven 순위 tab + the draw-aware foundation — no new migration.
  • The 순위 standings tab (2026-07-15) — the session points ladder (computePlayerPoints) + a personal "오늘의 영향" rating-impact cascade (복식/단식 레이팅 before→after · 클럽 내 순위 movement · 전국 순위 movement), read from stored elo_history (no new rating math); composed only from shared atoms (PlayerIdentityChip/EloTierBadge, no sibling-feature import), friendly sessions render ladder-only (commit f51e9f7b; standings-tab.tsx)
  • sessions.generation_style_id persistence (migration 00429) + the isScoreDrivenStyle predicate that gates the tab; set-on-generate wires the host's chosen style onto the session record (commit f51e9f7b)
  • get_session_rating_impact RPC (migration 00430, SECURITY DEFINER + search_path='' + a union session-visibility guard) + useSessionRatingImpact — per-participant session ELO delta + national/club rank movement, the cascade's data source (commit f51e9f7b)
  • The 친선 rating-gate (migration 00428) — apply_match_ratings now skips ELO on is_friendly sessions, so the long-promised "ELO 미반영" label is finally true; the correctness prerequisite that makes honest impact rendering possible, and it also covers the recompute_all_ratings replay. pgTAP-proven 8/8 (commit fcb4bcb1)

Resolved

  • [Cosmetic T-14] Registry had only a stub comment for americano/mexicano, and rolling wasn't a GenerationLifecycle value yet — FIXED: both strategies are real STRATEGY_REGISTRY entries now (only King-of-the-Court remains a stub comment) and rolling is a real enum + zod value (commit 8413c450; registry.ts:51-53; generation-policy.entity.ts:38)

  • [Moderate T-15] No points-standings surface + scoring mode not persisted — FIXED: the 순위 tab renders the points ladder + the personal rating-impact cascade; sessions.generation_style_id persists the host's chosen score-driven style, and the impact reads real elo_history deltas (migrations 00428/00429/00430; commit f51e9f7b)

  • [Cosmetic T-17] Dead results_driven policy value — FIXED (2026-07-18): RETIRED the never-assigned/never-read 'results_driven' member from the composition axis AND its identically-dead twin on the trigger axis (generation-policy.entity.ts types + zod). Routing was ruled out on evidence: the only reader of any GenerationPolicy field is resolveStrategy, which reads .styleId exclusively, and the live classification already exists in three consumed places (each strategy's family field, isScoreDrivenStyle(), the picker's SelectedStyle.kind) — a policy-layer mirror would just re-drift. The axes themselves stay (six-axis canon intact); docs/canon/match-generation.md corrected — its planned KOTC-via-composition activation was superseded by the styleId/registry seam that actually shipped 2026-07-16

Gaps

None.

Build set → contribution

  • Points-standings/leaderboard surface + persist the scoring mode → closes T-15 — DONE (commit f51e9f7b; migrations 00428/00429/00430; the 순위 tab + rating-impact cascade)
  • King-of-the-Court — the rolling lifecycle's court-movement rules — DONE 2026-07-16 (snake-seed → ascender/descender movement → 가위바위보 draw-tiebreak; court-primary ladder tab)
  • Route Americano/Mexicano through the composition:'results_driven' axis, or retire the axis → closes T-17 — DONE 2026-07-18 (retired; see Resolved)

Target: full format coverage → HIT. Americano + Mexicano + the 순위 standings/rating-impact surface + the 친선 gate + King-of-the-Court (rolling court-ladder) all shipped. P5's T-3/T-4 IA consolidation closed 2026-07-17 — no design-led items remain across the 7 pipelines.


Correctness quick-win (P1 · P2 · P3) — DONE (2026-07-15). A4 (혼복+random gender), C1 (games estimate), D2 (late-arrival copy) — all three fixed + unit-tested in one batch: rotation.strategy.ts + rotation.rules.test.ts (A4), feasibility.ts + feasibility.test.ts (C1), presentation/utils/late-arrival-hint.ts + add-round-sheet.tsx (D2). All JS-only, no migration.

Make tournaments real (P5 + P4's A2/A3) — DONE (2026-07-15; T-3/T-4 IA consolidation closed 2026-07-17 via the wireframe-approved chooser ship). Two waves: Wave A (backend/data layer, commit 58319217) — round-robin wave-slots (A2), the Swiss/Compass all-results-in gate (A3), the useTournamentBySession/useCancelTournament seam (T-2/E3), compare-and-swap advance (C5), and host-OR-admin RLS (D6, migration 00427, applied to prod). Wave B (UI, commit 27cbea02) — wired Wave A's hooks into session detail (T-1/T-2, the T-6 entitlement gate), the advance confirm dialog (D4), ELO-balanced doubles pairing (D1), and the bracket ko-label fix (T-5). Several were copy-paste of patterns already in the codebase (00303 RLS, the bracket overflow fix), as planned. Moved P5 from unusable to usable — remaining: T-3/T-4's fuller IA consolidation, deferred to a wireframe pass.

Activate the registry (P6) — DONE (2026-07-15). Threaded styleId into the round path — the pluggable spine is fully live (per-session algorithm choice); the seam is proven by Americano/Mexicano's own registrations. One focused change (commit 8413c450).

Score-driven formats (P7) — COMPLETE. Americano + Mexicano + registry (commit 8413c450), the 순위 standings + rating-impact + generation_style_id + 친선 gate (f51e9f7b/fcb4bcb1; migrations 00428/00429/00430), then King-of-the-Court — the rolling court-ladder + 가위바위보 draw-tiebreak — 2026-07-16. All shipped; no design-led remainder in P7.

Cosmetic tail — DONE (2026-07-18). One research-grounded wave over the remaining cosmetic/dead-code/doc-drift items: D3 live board header, 조별리그 knockout-knob threading, T-17 dead results_driven retirement (+ canon correction), the P1 dead-code sweep (dead i18n dict, 'mixed', avoidPairs/lockedPairs, stale comments; skill_balanced confirmed already-honest — no code change), and the C3 dead feasibility-stub deletion. The 2026-07-18 behavior wave then closed A2b/E1, D4a/D3a/D5a, A6, and — after the owner approved the wireframes — T-9/T-10 and D1a. Every finding from the original audit inventory is now closed; what remains anywhere is deliberately-scoped future work (score-based tournament standings if score entry ever ships, seed-persisted bracket tiebreaks, 조별리그 knob UI exposure).


Companion artifacts (claude.ai Artifacts — require the same claude.ai account access as the one that published them; not publicly hosted):

Repo docs:


Source: 3 read-only adversarial audit passes over the shipped system (Slices 0–3), verified against source, live on preview OTA 559eaaa9. This tracker regroups ~45 findings into the 7 pipelines above — see the flat audit inventory (linked above) for the complete, ungrouped list with the full surface/inspection map.

Markdown remains the source of truth. Run yarn docs:check before handoff.