Skip to content

fix(bc-tmfn): operator fixes: team bouts, withdrawals, knockout rounds and the Bracket page - #444

Merged
gitrgoliveira merged 21 commits into
mainfrom
bc-tmfn-operator-fixes
Sep 25, 2026
Merged

gitrgoliveira merged 21 commits into
mainfrom
bc-tmfn-operator-fixes

Conversation

@gitrgoliveira

@gitrgoliveira gitrgoliveira commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

Operator ruling 2026-09-24: every bout of a team match is fought, so a team match cannot be finished while a bout has no result. Plus four smaller operator-review fixes, and the follow-on work the operator's rulings asked for while this was built ("Save correction should just save what the operator enters"; "everything should be able to be fixed, the operator just needs to be aware of the consequences").

The five beads

  • bc-tmfn: Finish (the team editor, PUT /score and bulk-score) refuses a non-kachinuki team match while a numbered bout has no result: "Bout 3 has no result. Record a score, a Tie, or a Fusensho before finishing." No position is skipped (operator ruling: every bout has a result or a decision; a team always fields at least half+1, assumed and never checked; a bout neither team fields is recorded as a Tie). Kachinuki is out of scope. Standings count an individual draw (IT) only for a bout that has a result. Untouched bouts were being written as hikiwake, which made them indistinguishable from a marked Tie; they are now written with no decision.
  • bc-dscn: closing a running match flushes the pending autosave and closes without "Discard unsaved scoring changes?" (every edit there is autosaved). The prompt stays where closing really discards (a scheduled match, a correction, a draw toggle, a kachinuki fighter pick that the running save would drop). Esc does nothing on the court console, where the editor cannot close. The individual editor's key handler no longer fires on team and engi matches (it raised a second prompt and wrote a stray match-level ippon).
  • bc-kbhn: the keyboard hint hides under a coarse pointer (its inline display:flex beat the media query) and lists only keys that work on that host; arrow keys need a neighbour match (pressing them at the end of the Scores tab threw on scoreKeyOf(null)).
  • bc-kbrw: a double tap on a fought kachinuki bout opens that bout only (the first tap pushed the rows down and the second landed on the next row).
  • bc-cpdc: "Copy from previous match" is at the top of each side of the lineup panel, as the docs say.

Withdrawals: correct, keep, or clear

  • Save correction on a match decided by kiken or fusenpai keeps the recorded ruling and its consequences (winner, default-win marks, the withdrawn side's eligibility) and saves only what the operator entered; the winner's side is read-only and the withdrawer's letters stay editable.
  • Correction mode shows "Recorded: Kiken – Voluntary, withdrew." with Clear withdrawal and reopen (team, individual and kachinuki). It is one tap with no reason (operator decision): the consequence is stated beside the button, before it is tapped, and the tap returns the match to running with what was fought kept, and makes the withdrawn competitor or team eligible again. Switching the withdrawal to the other side works as before.
  • The eligibility record now follows the ruling: whenever a withdrawal is replaced (a fought result, a side switch through any door, a reopen, a downstream match reopened by force), the eligibility it recorded is restored. On main a fought rescore over a kiken left the competitor barred.
  • A reopen that also reopens a later knockout match says so in reopen words ("Reopening this match also reopens Match 3 (Final)…", Reopen both), keeps the editor open with a notice naming it, and clears that match's old winner out of an untouched next round.

Mixed competitions: fixing a pool after the knockout started

  • A pool correction, a pool reopen, or a rank override (chusen) that changes who qualified now warns with the affected knockout matches ("Changing this result moves Pool A's 1st place from X to Y… Match 1 (Final) was already fought with X: it will be reopened…"). Apply and reopen reopens them and seats the new qualifier. On main this was a hard 409 with no way through.
  • A running affected knockout match is refused with the fix: "Match 1 (Final) is being fought now. Finish it or send it back to the queue, then save again."
  • A change the old guard missed is now caught by comparing the corrected finishers with who sits in each bracket slot: a 1st/2nd swap, the extra-qualifier 2nd places, and a scheduled slot left holding the old qualifier once the knockout had started.
  • Chusen is accepted after the knockout has started, so a tie a correction creates can still be settled.

Chusen, and what a correction shows

  • A chusen recorded in the wrong order can be changed: the Pools tab shows a "Chusen (drawing lots) recorded" panel with Change, which reopens the entry with the recorded positions. The whole new order is one write, checked once against the knockout, so only a team whose place actually changes is named; it goes through the same requalification check as a first chusen (the warning, Apply and reopen, the running refusal).
  • Correct on a team match decided by kiken or fusenpai shows the recorded winner and decision in the result band (it read DRAW) and Kiken or Fus. beside the withdrawing team's name. The individual editor shows the same mark.
  • A tie settled by chusen, or put in a complete order by its daihyosen or ippon-shobu bouts, is no longer highlighted as a tie, in the admin and public standings.

bc-kcsh, bc-rawm, and what testing found around withdrawals

  • bc-kcsh: Correct on a match decided by kiken or fusenpai shows the recorded decision, the winner and the side mark before any edit (team and individual).
  • bc-rawm: refusals name the competitor, the match the way the operator sees it ("Pool A · Match 1") and what to do: starting a match with a withdrawn competitor, a competitor fighting on another court, a court already running a match, a reopen blocked by a later knockout match being fought, removing a kachinuki bout, bulk-score and the bracket "not ready" / "no winner" refusals. An individual knockout bout cannot be finished tied ("Needs a winner"). The Remaining matches panel's "Award default win to opponent" works again: it threw on an undefined name, on main too.
  • Team withdrawal scoring: a team match decided by kiken, fusenpai or a default win credits every bout without a result to the other team at 2–0, one individual victory and two points, as the guide records a kiken "the same way as fusensho"; bouts already fought keep their results; kachinuki gets no default bouts. It shows on the Scores list, Recent results, the viewer card, the TV board, the standings and the export, with Kiken or Fus. beside the withdrawing team's name. These matches read IV 0–0 PW 0–0 before, and a team that withdrew could rank above a team that fought.
  • A withdrawn competitor's remaining matches: Up next, Finish + Start Next, the TV next line, the lobby and on-deck alerts skip them; public rows show "Withdrawn"; operator rows offer "Record default win for " (recorded as a match-level fusensho, leaving the withdrawal the one eligibility record) and "Reinstate" for an injury kiken. "Clear default win" returns the match to the queue while the competitor is still barred, or reopens it in progress once they are reinstated. When both sides are barred, a pool or league match can be recorded as drawn (the guide: neither receives a win or points); a knockout match says to correct the earlier withdrawal or the draw.
  • Roster replace: the batch participants endpoint now keeps the id, and so the team members and lineups, of every team that stays; before the draw, a removed team's members and lineups go with it.

Hardening that came with it

  • Every reopen stamps modifiedAt, so an older queued offline save is refused as superseded instead of undoing it; Send back to queue on a pool match does the same.
  • A queued offline write never replays Proceed anyway / Apply and reopen: the confirmation is stripped on enqueue and on replay.
  • Knockout-phase saves (autosaves included) skip the pool check entirely: only a pool-match save can move a qualifier.

Code review round

  • Chusen as one write: recording or changing a chusen sends the whole order in one request (PUT .../override-rank now also takes {ranks: [...]}) and the knockout is checked once, on the final order. Sent one rank at a time, changing Kyoto 1st, Nara 2nd, Sendai 3rd to Sendai, Nara, Kyoto passed through an order nobody chose and named, then reopened, the match Nara had already fought. A refusal now records none of the order.
  • A cleared default win going back to the queue is not refused because another match is running on that court (it takes no court), and the court console lets go of the correction and returns to the running bout.
  • A withdrawal whose winner is already seated beyond the next round can be cleared again. In a five-player knockout, Hara v Fujii is a semifinal whose winner goes straight into the Final; Clear withdrawal and reopen on it takes the winner back out of the Final and answers for that match (warning if it was played, refusing if it is being fought). Before, it was refused and pointed to Save correction, which keeps the withdrawal, so a wrong kiken there could not be removed. A correction there now also warns for a played Final; it used to repaint it silently.
  • A refused correction (the knockout warning's first answer) no longer re-bars a competitor who had been reinstated.
  • An unreadable pool or bracket file is reported as such (500, naming the file) instead of "match not found", which the offline queue dropped.
  • Both sides barred, recorded as drawn: runs after the clock check and maps its refusals like every other decision (a superseded write was a 500).
  • Quick-score runs the team finish gate, with the same exemption /score makes for a correction that keeps a recorded withdrawal; bulk-score loads the competition once per request.
  • Bout order: padding a withdrawal's unfought bouts keeps the rows in position order (a stored bout 2 padded to 2, 1, 3, 4, 5 put the first fighter's name on the second bout).
  • One credit rule: the team editor's correction band credits a withdrawal with no recorded side to the winner, as the server does.
  • Dead code removed: the resolved-by-bye refusal (downstream_knockout_resolved), ChusenCandidates, the single-rank wrappers, two unused JS exports; the simultaneity gate no longer passes on an unreadable file.

Knockout rounds, as the European Championship sheets print them

  • A knockout match is placed in the round its distance from the final gives it. The 34th EKC 2026 Junior Individual Male sheet (PDF, page 4, court A) prints P2 v P3 alone in column 1 and P4 v P5 beside P1 v Winner F1 in column 2; the 33rd EKC 2025 Men Team five-team courts (PDF, page 14) and the 2026 Junior Individual Female and Men Individual sheets print the same way. An earlier reading put P4 v P5 in column 1, so a five-player knockout showed its semifinal pair (Hara v Fujii) as a quarterfinal numbered Match 1.
  • Now the app bracket, the round names, the match numbers, the order of play and the Excel columns all follow the sheets: in a five-player draw Endo v Ito is Match 1 (Quarterfinals, 09:00) and Hara v Fujii Match 2 (Semifinals, 09:05). Who meets whom is unchanged.
  • A stored bracket whose rounds or match numbers differ from the rule takes the corrected ones when it is loaded (operator decision): draws made by v2.0.0 or v2.1.0, and older draws with byes numbered by the pre-v2.0 tie-break (pinned with a real v1.1.0 bracket). The app and its Excel export then name the same bout Match N again. Pairings, results and courts are not touched; an unstarted draw still timed in storage order has its own times handed out again in match-number order, per court.
  • The bracket shows no BYE cards (operator decision, from mockups): a competitor who skips a round appears only in the match they first fight, as the sheets print them. A card fed by one match takes that line on the row the winner fills (Aka or Shiro); a card fed by two keeps the join at its centre.
  • Second code review of this work: a confirmed pool correction, or a forced knockout correction, now also clears a withdrawn qualifier from the match past a hidden bye (they could be started there before); one producer stamps a match's round, at draw time and on load; bracket.json is parsed and saved once by the load-time repairs.
  • Third code review: a match the operator moves up a court's queue stays moved after a restart. The one-off repair of old scheduling used to run on every load and could not tell that swap from the old scheduler; a bracket now records timesSettled (set by the draw, and by the repair the first time it runs), and its times are never examined on load again. A match can be reopened without any reason (operator ruling), so the both-barred draw is never refused for one; it settles the reopen in the same write and keeps the request's reason, if any (the one-tap button sends "auto: ... withdrawn"), as the correction reason, where before nothing was kept. The TV board's daihyosen row takes its tie from the same place as the viewer card (the server's figures first).
  • Ending a reopened match asks for no reason (operator ruling: a match can be reopened without any reason, and nothing is gated on that). The server no longer refuses a score or a decision that ends a match reopened without a reason; the team editor's End match no longer opens a "Reason for reopening" prompt; the withdrawal panel's reason stays optional after a reopen. Ending the match clears the stored reopen flag and keeps a reason if one is sent. The flag itself stays in storage, because the pool CSV reads its columns by position. The docs say so, the two screenshots of the retired prompts and their harness recipes are removed, and the kachinuki video is recaptured.
  • A card fed by one match is placed so the row that match fills is level with it, so the line runs straight instead of bending half a row; the stacking makes room, so no card overlaps its neighbour. The admin Bracket page (operator decision) gives the bracket the full width and puts the scoring panel below it; picking a match scrolls the panel into view. Its columns narrow to fit, 230px down to a 180px floor with tighter gaps, so a four-round tree (9 to 16 competitors) fits an iPad in landscape (1180px) without scrolling sideways, and the 3rd-place card stays under the final. The connector SVG is now sized from the cards, not from the tree's scroll size, which included the SVG and held a narrowed tree at its first width. The Bracket page also takes the whole window rather than the 1400px every other section keeps, so a six-round tree (33 to 64 competitors) fits a large monitor.

Why

  • The standings counted every unfought bout as an individual draw (IT) for both teams, so a team match could finish half fought and still feed the table.
  • Each follow-on piece exists because an earlier fix exposed a path that silently changed data the operator never touched (a correction erasing a kiken, a kiken that could no longer be removed, a correction the knockout refused outright), and the operator ruled each should save exactly what was entered and stay fixable with the consequences shown.

Files changed

File What
CLAUDE.md Reopen is no longer kachinuki-only; the retired downstream_knockout_scored guard; default-win credit, barred competitors, operator labels, the Fus. mark; review round: a queued reopen takes no court, the bye unwind, the label list; the specs line: a few plans are force-added and tracked
docs/architecture/data-model.md The bracket's TimesSettled field
docs/screenshots/decision-reason-after-reopen.png Deleted: the retired required-reason panel after a reopen
docs/screenshots/kachinuki-knockout-tie-encho.png Recaptured: the hint no longer lists prev/next where there is none
docs/screenshots/kachinuki-reopen-reason.png Deleted: the retired "Reason for reopening" prompt on End match
docs/screenshots/knockout-bracket-five.png New capture: the admin bracket of a five-player knockout; recaptured without BYE cards; recaptured: straight one-sided line, the narrower spacing
docs/screenshots/knockout-bracket-in-play.png New capture: a ten-player knockout part-way through, public Bracket tab; recaptured without BYE cards; recaptured: straight one-sided lines
docs/screenshots/viewer-bracket.png New capture: the same tree at phone width; recaptured without BYE cards; recaptured: straight one-sided lines
docs/user-guide/court-operators/recording-decisions.md Correcting a withdrawal recorded by mistake; each editor's own controls; chusen after the knockout started; changing a recorded chusen; the mark and result a withdrawal correction shows; matches after a competitor is barred; the team default-win credit; Needs a winner; review round: a changed chusen is checked as a whole; Clear default win is not stopped by a busy court; a bye rolled back with a cleared withdrawal; Clear withdrawal and reopen is one tap
docs/user-guide/court-operators/scoring-a-match.md Finish needs a result on every bout; correcting a pool result after the knockout started; unfought bouts credited to the other team; ending a reopened match asks for no reason; the two prompt screenshots removed
docs/user-guide/organisers/knockout-draw.md A pair can start in the second round; the five-competitor block described as the sheets print it; the five-player bracket image; the Byes section says which of the competitors marked BYE is the awarded bye; no BYE cards: a competitor who skips a round first appears in the match they fight; a block that fills its slots has no bye
docs/user-guide/organisers/run-tournament.md Pool corrections in the knockout phase; a roster replace keeps team members and lineups; the in-play bracket image; alt text for the recapture
docs/user-guide/organisers/team-tournaments.md Fusensho for an absent fighter; reopen wording; the default-win credit; reopening and ending again need no reason; Clear withdrawal and reopen is one tap
docs/user-guide/spectators/following.md Withdrawn chip; a withdrawn competitor's matches are not announced as next; the public Bracket tab figure; alt text for the recapture
docs/videos/kachinuki-demo.webm Re-recorded: dimmed disabled buttons, hint without prev/next; recaptured: the reopen chapter ends with no reason prompt
internal/domain/decision.go Fusensho is also the match-level default win for a barred competitor
internal/domain/glossary.go Reason patterns accept ids with a space; SplitStatusReason
internal/domain/glossary_test.go Pool ids with a space; SplitStatusReason
internal/domain/team_lineup.go PositionForBout, Position.Label()
internal/domain/team_lineup_test.go Tests for the lineup helpers
internal/engine/bc_idfx_test.go Return-arity update
internal/engine/bracket.go Numbering through state.Bracket.NumberMatches; slots assigned after numbering; comments on the round rule; rounds stamped by StampRoundsFromFeeders at draw time (the draw-tree override removed); the draw records its times as settled
internal/engine/bracket_identity_test.go Every feeder is exactly one round deeper again; 9- and 19-player cases
internal/engine/bracket_match_numbers_golden_test.go Knockout shapes discriminate the numbering tie-break again (9, 19)
internal/engine/bracket_not_ready_label_test.go Bracket refusals name the match by label
internal/engine/bracket_order_of_play_test.go New: a knockout is scheduled in match-number order on every shiaijo; a moved match keeps its time across a reload
internal/engine/bracket_rounds_restamp_test.go New: the load-time restamp leaves every freshly drawn or played bracket unchanged; a no-op restamp now pins that no second producer comes back
internal/engine/bronze_label_test.go Label assertions; comment names the moved numbering rule
internal/engine/chusen.go Chusen candidates in a mixed competition's knockout status; ChusenStatus also reports the recorded groups; ChusenCandidates removed (the handler uses ChusenStatus)
internal/engine/chusen_test.go Chusen after a correction ties a qualifying place; the recorded order is reported; on ChusenStatus; a changed chusen names only the real move
internal/engine/competition.go MaybeAutoCompletePoolsAfterWrite: pool pass only for pool writes in knockout status
internal/engine/daihyosen.go Retired downstream_knockout_scored handling; credit argument on ComputeTeamSummary; id grammar moved to state
internal/engine/daihyosen_test.go Credit argument
internal/engine/default_win_bout_credit_test.go Credit in standings and the bracket; padding; kachinuki not padded; legacy load
internal/engine/default_win_bout_padding_unreadable_test.go Padding never overwrites an unreadable bout cell
internal/engine/downstream_correction_guard_test.go A reopen through a bye unwinds it; a correction or override through a bye warns for a played match beyond it
internal/engine/downstream_guard_review_test.go Label on a hand-built refusal
internal/engine/eligibility.go BarredSides; typed simultaneity and both-barred refusals; CheckEligibility removed; lookups return load errors instead of "not found"
internal/engine/eligibility_test.go BarredSides; padded rows
internal/engine/engi.go Next-slot occupant check on re-entered winners; labelled refusals
internal/engine/errors.go QualifierChange on the played error; DownstreamKnockoutRunningError; SentenceCase; labels in refusals; comments name OverridePoolRanks and the moved numbering rule
internal/engine/excel_draw_parity_test.go The lone-entrant page exemption removed; comments
internal/engine/kachinuki.go ReopenMatch/RequeueBlockerAndReopen for withdrawal-decided matches; downstream warn-and-proceed; eligibility restore; clears an untouched next round; lineupInForce extracted for the kachinuki roster; a default win reopens to scheduled while the competitor is barred; reopen refusal copy; a reopen to the queue skips both court gates; the bye unwind (propagatedDownstreamOf); findMatchHome returns load errors; retractIntoUntouched unwinds a bye through the shared unwindChain; the reopen flag documented as asking for nothing
internal/engine/kachinuki_export.go Uses Position.Label()
internal/engine/kachinuki_export_test.go Position label table moved to TestPosition_Label
internal/engine/kachinuki_reopen_withdrawal_test.go Kachinuki reopen restores eligibility
internal/engine/kachinuki_test.go Renamed reopen doors; downstream now warns
internal/engine/knockout.go resolveSlots (pure, all-or-nothing per pool); advanceMixedPools in knockout status
internal/engine/knockout_skeleton.go Comment on the numbering the export follows
internal/engine/load_error_propagation_test.go New: an unreadable pool or bracket file is never "match not found"
internal/engine/match_label.go New: OperatorMatchLabel, the one Go owner of operator match labels
internal/engine/match_label_test.go MatchLabel against the shared round table
internal/engine/match_numbering_parity_test.go Comment on the round rule
internal/engine/pool_daihyosen_test.go Id grammar moved to state
internal/engine/pool_rank_override.go OverridePoolRanks: a group of ranks as one change, one requalification check and one undo; undo only when uncommitted
internal/engine/pool_rank_override_test.go Override warns / forces / restores; the group form
internal/engine/pool_requalify.go New: planRequalification and the forced repaint/reopen/retract; label on the played refusal
internal/engine/pool_requalify_test.go Swap, extra qualifier, bye leaf, tie, reopen-then-finish, running refusal, stale replay
internal/engine/reopen_bye_unwind_test.go New: a reopen through a bye, then a re-fought result through it; a played or running match beyond it
internal/engine/reopen_scheduled_court_test.go New: a reopen to the queue ignores a busy court, same and other competition; held to the queue by the pre-read
internal/engine/restore_touches_no_eligibility_test.go New: a refused correction leaves a reinstated competitor eligible and the status file unchanged
internal/engine/pool_write_merge_test.go Arity update
internal/engine/prior_lookup_error_test.go A failed prior lookup is propagated, not discarded
internal/engine/reopen_fusensho_status_test.go Clearing a default win: scheduled while barred, running once reinstated
internal/engine/reopen_withdrawal_test.go Reopen of withdrawal-decided matches
internal/engine/retract_through_bye_test.go New: a confirmed pool correction and a forced knockout correction clear the displaced winner past a bye; a played match past it is left alone
internal/engine/scheduler_slots.go Knockout slots in match-number order; header and the shared ScheduledAt layout constant
internal/engine/scoring.go KeepsWithdrawalRuling, preserveWithdrawalRuling, keptWithdrawalScoreline; MatchLabel; RevertMatchToQueue stamp; markTiedStandings skips a settled tie; credit in standings; KeepsWithdrawalRuling keeps any default win; labelled refusals; a rollback restore touches no eligibility; downstream guards look past a bye; the bye note on bracketMatchCarriesOwnResult; comments on the reopen flag
internal/engine/scoring_test.go A bout with no result is not an individual draw
internal/engine/scoring_tx.go Keep/restore eligibility with the ruling; requalification call; concurrent-withdrawal pre-check; pads a completed default-win team match; BarredSides at start; typed simultaneity refusal; the simultaneity gate returns load errors
internal/engine/scoring_tx_internal_test.go Arity update
internal/engine/scoring_tx_test.go Updated guard expectations
internal/engine/sub_hantei_preserve_test.go Arity update
internal/engine/swiss.go Team matches always take the team branch
internal/engine/testdata/bracket_match_numbers.json Regenerated: 5, 9, 11 and 19 players renumbered
internal/engine/testdata/match_labels.json Operator labels shared by the Go and JS tests
internal/engine/testdata/round_labels.json Round labels shared by Go and JS tests
internal/engine/tiebreaker.go pendingTieBreaks shared by both injectors; tieSettled, supplementaryGroupWins; id grammar moved to state
internal/engine/tiebreaker_test.go Id grammar moved to state
internal/engine/tied_standings_test.go A tie settled by chusen or by its supplementary bouts is not highlighted
internal/engine/withdrawal_removed_restores_test.go Eligibility follows the ruling
internal/engine/withdrawal_ruling_kept_test.go Correction keeps the ruling, pool and bracket
internal/export/builder.go Credited bouts show the default-win maru; the bracket view carries status and decisionBy
internal/export/builder_test.go Credited unfought row
internal/export/team_bracket_submatch_cells_test.go Credited bouts in the workbook
internal/export/team_name_fallback_asymmetry_test.go Signature update
internal/helper/draw.go slotDepth removed; comments: rise marks are slot geometry, rounds are distance from the final; SlotRoundMatches removed; walkLeafOffsets' visitor loses its content offset
internal/helper/draw_ekc_2026_individual_test.go Comments on the round rule
internal/helper/draw_ekc_test.go Junior Individual Male rounds as page 4 prints them
internal/helper/draw_leaf_offsets_test.go Content-window assertions removed with the offset
internal/helper/draw_seed_bye_test.go Comment: the pair is fought in column 2
internal/helper/draw_seed_count_test.go Comment
internal/helper/estimate.go Comment points at the scheduler branch, not a line range
internal/helper/excel_tree.go Tree pages use the physical depth
internal/helper/tree.go Rounds and tree columns are the distance from the root; rise marks kept for slot geometry only; comments
internal/helper/tree_test.go Wording: first-layer pairs are not all round-1 matches
internal/mobileapp/decision_both_barred_hikiwake_test.go Both sides barred: the draw is accepted for pool and league only; clock skew and superseded on the draw; a reason-less reopen never blocks the both-barred draw; a given reason is kept
internal/mobileapp/deps.go MaybeAutoCompletePoolsAfterWrite on the engine interface
internal/mobileapp/deps_test.go Stub update
internal/mobileapp/errors.go downstream_knockout_running; qualifierChange; broadcastReopenedDownstream; refusal sentences; court busy label; the barred sentence labels through OperatorMatchLabelFromStore; the resolved-by-bye mapping removed
internal/mobileapp/handlers_competition.go override-rank: planner, force, knockout status; chusen-candidates returns recorded; override-rank takes a ranks group
internal/mobileapp/handlers_competition_test.go chusen-candidates reports the recorded chusen
internal/mobileapp/handlers_daihyosen.go After-write auto-complete door; explicit credit argument
internal/mobileapp/handlers_daihyosen_test.go Expectation update
internal/mobileapp/handlers_decision.go ReopenPending for all formats; running refusal; both-sides-barred draw; refusal sentences; clock check before the both-barred draw; one error mapping for both paths; the both-barred draw settles a reason-less reopen, never refusing for a missing reason, and keeps any reason given; no decision is refused after a reason-less reopen
internal/mobileapp/handlers_decision_test.go The retired downstream_knockout_scored 409 test removed; a comment
internal/mobileapp/handlers_match.go Finish gate on /score and bulk; reopen doors, reason/force, broadcasts; ineligibleSides stamp; refusal sentences; court busy label; bulk-score sentences; quick-score runs the finish gate in a transaction; bulk-score loads the competition once; ending a reopened match is never refused for a reason; the reopen-reason message removed
internal/mobileapp/handlers_match_kachinuki_test.go Reopen handler updates; the reopen tests pin that ending again needs no reason, on every path
internal/mobileapp/handlers_match_test.go Updated guard expectations; quick-score gate and its withdrawal exemption
internal/mobileapp/pool_rank_override_group_test.go New: the group body, its refusals and validation
internal/mobileapp/handlers_participants.go Batch roster replace keeps surviving ids
internal/mobileapp/handlers_participants_test.go A kept team keeps its id and members; a removed team is pruned
internal/mobileapp/handlers_viewer.go ineligibleSides stamped before queue positions
internal/mobileapp/ineligible_sides_annotation_test.go Stamp, queue skip, removal after reinstate
internal/mobileapp/middleware_selfrun_test.go Fixture shape
internal/mobileapp/pool_requalify_handler_test.go 409 payloads at every door; forced path broadcasts
internal/mobileapp/reason_human_barred_test.go Refusal sentences name the competitor, the match and the remedy
internal/mobileapp/reopen_downstream_labels_test.go Reopen refusals and court busy carry labels, no ids
internal/mobileapp/reopen_withdrawal_handler_test.go Reopen handler for withdrawals; a reopened withdrawal is recorded again with no reason
internal/mobileapp/team_finish_gate.go New: the team Finish gate; comment for the widened KeepsWithdrawalRuling; load-free core for bulk-score and quick-score
internal/mobileapp/team_finish_gate_test.go Gate scope, a bout neither team fields needs a Tie, corrections, bulk; bulk-score loads the competition once
internal/mobileapp/testdata/unfinished_team_bouts.json Refusal copy shared by Go and JS tests
internal/state/bracket.go The cached copy keeps TimesSettled
internal/state/bracket_rounds.go New: NumberMatches (the one numbering rule) and RestampRoundsFromFeeders; StampRoundsFromFeeders (one producer); the scope described; storage-order times repaired on an unstarted draw; ScheduledAtLayout; the time repair runs once per bracket (TimesSettled); where a side can still be emptied
internal/state/bracket_rounds_schedule_test.go New: times handed out in number order per court, kept once anything is touched, a hand-moved time kept; the repair settles the bracket
internal/state/bracket_rounds_test.go New: restamp of a real v2.1.0 bracket, no-op, and every unwalkable shape left untouched; the stamping core; fixture carries no marker; the restamp settles it
internal/state/bracket_rounds_v110_test.go New: a real v1.1.0 bracket renumbered once, ids and numbers in one write; the marker
internal/state/competition.go DeleteCompetition clears the once-per-competition log marker
internal/state/default_win_bout_padding_unreadable_test.go The load pass keeps an unreadable bout cell
internal/state/legacy_team_members_filename_test.go Fixture ids for the prune
internal/state/legacy_upgrade.go Pads stored default-win team matches on load; the bracket-rounds pass for v2.0/v2.1 draws; one parse and one save for both bracket repairs; an unwalkable bracket logged once; a marker-only change is saved
internal/state/legacy_upgrade_bracket_rounds_test.go New: a v2.1.0 bracket.json is rewritten once, then left byte-identical; one write, one log; the marker alone is saved once
internal/state/match.go Queue positions skip a barred match
internal/state/match_id_grammar.go New: the pool daihyosen and tiebreaker id grammar (moved from engine)
internal/state/match_id_grammar_test.go Moved tests
internal/state/models.go SubMatchResult.HasResult(); AcceptsPoolRankOverride(); Tied documents a settled tie; ineligibleSides annotation; Attribution(); DisplayRound doc; the numbered() invariant and the repair scope; Bracket.TimesSettled; the reopen flag documented as asking for nothing
internal/state/models_test.go Table test for the new rule
internal/state/overrides.go SaveRankOverridesChanged and RestoreRankOverrides: a group of ranks in one write
internal/state/overrides_test.go Group save and restore
internal/state/participants.go Prunes removed teams' members and lineups before the draw
internal/state/pool_roundtrip_test.go ineligibleSides is never persisted
internal/state/squad.go pruneOrphanedTeamMembersLocked
internal/state/squad_test.go Pruned in setup, kept once started
internal/state/store.go The once-per-competition log marker
internal/state/sub_match_has_result_test.go HasResult
internal/state/team_lineup.go pruneOrphanedTeamLineupsLocked
internal/state/team_lineup_test.go Pruned in setup, kept once started
internal/state/team_result.go The default-win credit rule: DefaultWinCreditSide, SubBoutEffectiveResult, the padding predicate; padding keeps bout rows in position order
internal/state/team_result_test.go The credit rule; padding order
internal/state/testdata/bracket_v1.1.0_knockout9.json New: a bracket written by the v1.1.0 binary
internal/state/testdata/bracket_v2.1.0_knockout5.json New: a bracket written by the v2.1.0 binary
internal/state/testdata/participants_v1.1.0_knockout9.csv New: its roster
internal/state/transactions.go ErrTxCommitted on a post-commit Apply failure; comment
internal/state/transactions_test.go Sentinel present only after commit
knockout-example-large-seeded.xlsx Regenerated
knockout-example-large.xlsx Regenerated
knockout-example-small.xlsx Regenerated: two first-round bouts, then four in round 2
pools-example-large-seeded.xlsx Regenerated
pools-example-large-teams.xlsx Regenerated
scripts/screenshots/README.md The capture count is 33; now 31
scripts/screenshots/recipes/editors.mjs The two retired reason-prompt recipes removed
scripts/screenshots/recipes/index.mjs Registers the knockout recipes
scripts/screenshots/recipes/knockout.mjs New: two families and three bracket captures; comments: the admin page now fits the tree
scripts/screenshots/recipes/videos.mjs Kachinuki demo: the editor stays open after Reopen; End match is always the two-tap guard
scripts/screenshots/run.mjs Comments no longer carry a capture count; the recipe count
specs/007-ekc-draw/spec.md Tracked design spec: the Junior Male court tables, the derivation and the second-pass block corrected to what the PDF prints
specs/openapi.yaml Finish refusal; reopen scope, reason, force; new 409s; chusen-candidates recorded; ineligibleSides; refusal bodies; the both-barred draw; decisionBy wording; lineup positions and memberIds; override-rank ranks group; downstream_knockout_resolved removed; timesSettled; no reason is required to end a reopened match
web-mobile/css/styles.css Shortcut-hint base rule; dimmed disabled letter buttons; the Kiken/Fus. mark beside a name; Withdrawn chip; name marks in clipping cells; the BYE placeholder card styles removed; the retired reopen-reason verdict style removed; the admin bracket narrows to fit and puts the panel below; the bronze column-slot row
web-mobile/js/__tests__/api.test.jsx Override-rank and chusen-candidates parsing; the group request
web-mobile/js/__tests__/api_client_downstream_knockout.test.jsx Queue strips the confirmation; running refusal
web-mobile/js/__tests__/api_client_reopen.test.jsx Reopen reason/force
web-mobile/js/__tests__/bracket_connector_elbow.test.jsx Comment: only brackets drawn before this change skip a column; connectorTargetY
web-mobile/js/__tests__/bracket_display_model.test.jsx No placeholder cards; feeders stay real matches only; the fed row levelled with its feeder, both sides, with no overlap
web-mobile/js/__tests__/bracket_match_numbers.test.jsx Served numbers without placeholder cards
web-mobile/js/__tests__/bracket_round_label_agreement.test.jsx The placeholder filter dropped
web-mobile/js/__tests__/display_placeholders.test.jsx The next-match pick skips a barred match
web-mobile/js/__tests__/ineligible_match.test.jsx The barred-match owner
web-mobile/js/__tests__/kachinuki_bout_mode.test.jsx Played predicate update
web-mobile/js/__tests__/match_labels_golden.test.jsx JS labels against the Go fixture
web-mobile/js/__tests__/match_scoreboard.test.jsx Credited bout rows
web-mobile/js/__tests__/name_mark_survives_clipping.test.jsx Mark and name structure in clipping cells
web-mobile/js/__tests__/patch.test.jsx Queue positions skip a barred match; the stamp survives a patch
web-mobile/js/__tests__/queue_label.test.jsx No queue label for a barred match
web-mobile/js/__tests__/render/admin_bracket_layout.render.test.jsx New: the admin Bracket page narrows its tree, scrolls the panel below into view once per pick, and puts the 3rd-place card in the last column slot
web-mobile/js/__tests__/render/admin_competition.render.test.jsx The page width: none on the Bracket section, 1400px elsewhere
web-mobile/js/__tests__/render/admin_pools_chusen.render.test.jsx Chusen warning; knockout status; recorded panel and Change; one request per order
web-mobile/js/__tests__/render/admin_pools_tiebreak_buttons.render.test.jsx Chusen-candidates mock shape
web-mobile/js/__tests__/render/admin_scores_row_name_mark_clip.render.test.jsx Scores row mark placement
web-mobile/js/__tests__/render/admin_scoring_engi.render.test.jsx Engi hint class; arrows need a neighbour
web-mobile/js/__tests__/render/admin_shiaijo.render.test.jsx Reopened correction becomes live; refusal clearing; a correction cleared to the queue lets go
web-mobile/js/__tests__/render/barred_chip.render.test.jsx The Withdrawn chip on four surfaces
web-mobile/js/__tests__/render/bracket_bye_slot.render.test.jsx Deleted: pinned the BYE placeholder cards
web-mobile/js/__tests__/render/bracket_round_label.render.test.jsx Comments
web-mobile/js/__tests__/render/bracket_structural_bye.render.test.jsx New: no BYE cards for a structural skip; a one-sided join lands on the fed row, both sides; geometry read from the real placement; straight connectors; the SVG sized from the cards
web-mobile/js/__tests__/render/display_tv_dh_gating.render.test.jsx The TV DH gate uses the shared result check; the row follows the server's tie
web-mobile/js/__tests__/render/display_tv_headline_name_mark_clip.render.test.jsx TV headline mark placement
web-mobile/js/__tests__/render/display_tv_individual_barred_chip.render.test.jsx TV individual board chip
web-mobile/js/__tests__/render/editor_barred_match.render.test.jsx The editors show the notice instead of Start; the both-barred draw
web-mobile/js/__tests__/render/individual_editor_ko_tie_blocked.render.test.jsx A tied individual knockout bout blocks Finish
web-mobile/js/__tests__/render/individual_editor_live_hantei.render.test.jsx Fixtures give every bout a result
web-mobile/js/__tests__/render/kachinuki_done_bout_double_tap.render.test.jsx bc-kbrw
web-mobile/js/__tests__/render/kachinuki_reopen.render.test.jsx Shared reopen hook; End match and the withdrawal panel ask for no reason after a reopen
web-mobile/js/__tests__/render/match_lineup_side_editor_order.render.test.jsx bc-cpdc button order
web-mobile/js/__tests__/render/recorded_withdrawal_reopen.render.test.jsx Recorded line, read-only winner, reopen; the one-tap clear, no reason, the consequence shown before the tap
web-mobile/js/__tests__/render/remaining_matches_panel_award.render.test.jsx The award posts a fusensho (was a ReferenceError)
web-mobile/js/__tests__/render/score_editor_barred_match.render.test.jsx Scores tab notice, skip, landed and queued states
web-mobile/js/__tests__/render/score_editor_dismiss_prompt.render.test.jsx bc-dscn
web-mobile/js/__tests__/render/score_editor_mount_sites.render.test.jsx jsdom has no scrollIntoView: stubbed, as scrollTo already was
web-mobile/js/__tests__/render/score_editor_team_default_win_mark.render.test.jsx Scores row Kiken/Fus. mark
web-mobile/js/__tests__/render/scoring_shortcut_hint.render.test.jsx bc-kbhn
web-mobile/js/__tests__/render/team_bout_row_records_no_team_name.render.test.jsx Fixtures give every bout a result
web-mobile/js/__tests__/render/team_daihyosen_silence.render.test.jsx Fixtures give every bout a result
web-mobile/js/__tests__/render/team_editor_superseded.render.test.jsx Fixtures give every bout a result
web-mobile/js/__tests__/render/team_finish_refuses_unfought_bouts.render.test.jsx bc-tmfn client refusal
web-mobile/js/__tests__/render/use_team_lineups_squads.render.test.jsx A pool match's Round -1 reads as round 0
web-mobile/js/__tests__/render/withdrawal_correction_shows_result.render.test.jsx Recorded winner, decision and side mark on a correction
web-mobile/js/__tests__/score_display.test.jsx The match-level fusensho winner mark
web-mobile/js/__tests__/sync_queue.test.jsx Rehydrated entry strips the confirmation
web-mobile/js/__tests__/team_default_credit.test.jsx The JS credit rule; the winner fallback
web-mobile/js/__tests__/team_iv_score.test.jsx Credit in the IV fallbacks; teamMatchMarks
web-mobile/js/__tests__/unfinished_team_bouts.test.jsx unfinishedTeamBouts and its copy
web-mobile/js/__tests__/viewer_competition_bye_results.test.jsx Comments
web-mobile/js/__tests__/viewer_home_secondary_ondeck.test.jsx No on-deck for a barred match
web-mobile/js/__tests__/viewer_ondeck_alert.test.jsx No on-deck for a barred match
web-mobile/js/__tests__/write_result_downstream_knockout.test.jsx Qualifier-change and running copy
web-mobile/js/admin_competition.jsx The Bracket section takes the whole window; every other section keeps 1400px
web-mobile/js/admin_competition_bracket.jsx Editor key without status; the panel below the bracket, scrolled into view on a pick; the bronze in a column-slot row
web-mobile/js/admin_pools.jsx Chusen through confirm-and-retry; knockout status; recorded panel with Change; one write per order
web-mobile/js/admin_schedule_lineup.jsx bc-cpdc
web-mobile/js/admin_schedule_score_editor.jsx Neighbour-aware navigation; barred-match notice; skipped by navigation; row marks
web-mobile/js/admin_scoring_autosave.jsx flushPending
web-mobile/js/admin_scoring_engi.jsx Hint class; Esc/arrows per host
web-mobile/js/admin_scoring_individual.jsx Dismiss, Esc, hint, key delegation, withdrawal correction and its side mark; a tied knockout bout blocks Finish; barred-match notice
web-mobile/js/admin_scoring_shared.jsx Hint; RecordedWithdrawal, useMatchReopen, ReopenFeedback, withdrawalInForce, WithdrawalMarkedName; default win recorded and cleared; Remaining matches award fixed; court busy panel; the decision reason is always optional; the reopen presets removed; Clear withdrawal and reopen is one tap, its consequence stated beside the button; the reopen presets removed
web-mobile/js/admin_scoring_team.jsx Finish gate, unplayed rows, dismiss, tap guard, withdrawal correction with the recorded result and side mark; band credit; consequence note; barred-match notice; no reason prompt on End match or on a decision after a reopen
web-mobile/js/admin_shiaijo.jsx Reopened correction becomes live; start refusal clearing; a barred match is skipped and offers its default win; a correction that goes back to the queue lets go
web-mobile/js/api_client.jsx Reopen reason/force; 409 parsing; confirmation never queued; chusen-candidates {candidates, recorded}; refusal sentences preferred; court busy; reopen copy when a downstream match runs; overridePoolRanks; comment; comment: the clear sends no reason
web-mobile/js/barred_chip.jsx New: the Withdrawn chip
web-mobile/js/barred_match_notice.jsx New: note, Record default win, Reinstate, Record as drawn
web-mobile/js/bracket.jsx Exports sideMarks for the editors; fusensho winner mark; teamMatchMarks; Withdrawn chip on the card; comment names the moved numbering rule; no BYE placeholder cards; connectorTargetY puts a one-sided join on its row; oneSidedFeed; a one-fed card placed with its fed row level with the feeder; the connector SVG sized from the cards; bracketColumnCount
web-mobile/js/display_helpers.jsx The next-match pick and queue label skip a barred match
web-mobile/js/display_scoreboard.jsx The TV skips a barred match; marks via teamMatchMarks; credited rows; the daihyosen row's tie is the shared scoreboard's
web-mobile/js/ineligible_match.jsx New: the one JS owner of the barred-match rule
web-mobile/js/match_scoreboard.jsx Credited bout rows; name marks; a pool match's Round -1 lineup lookup
web-mobile/js/patch.jsx Queue positions skip a barred match
web-mobile/js/pool_ids.jsx scoreRowMatchLabel moved here; bout positions are 1-based
web-mobile/js/team_default_credit.jsx New: the JS credit rule and its side mapping; falls back to the winner without a recorded side
web-mobile/js/viewer_alerts.jsx No on-deck alert for a barred match
web-mobile/js/viewer_competition.jsx Up next skips a barred match; comment
web-mobile/js/viewer_home.jsx Up next skips a barred match
web-mobile/js/viewer_match.jsx Recent results marks and chip; the credited card
web-mobile/js/viewer_schedule.jsx Schedule row marks and chip
web-mobile/js/viewer_standings.jsx Exports rankOrdinal; pool rows carry marks and the chip
web-mobile/js/viewer_watchlist_core.jsx "Your next match" skips a barred match
web-mobile/js/write_result.jsx Qualifier-change, running and queue-drop copy; court busy and reopen-running copy

Screenshots

bc-tmfn: Finish refused while bout 3 has no result

bc-cpdc: Copy from previous match at the top of each side

bc-kbhn: court console hint (no arrows, no Esc) · hidden under a coarse pointer

bc-dscn: running match, M then Close at 96 ms, reopened: the M was saved, no prompt · a scheduled match still prompts

bc-kbrw: double tap on bout 3 opens bout 3 only

Withdrawal correction: recorded kiken kept, winner read-only · Clear withdrawal and reopen · after finishing the reopened match the console is on the next live match


Reopen with a later knockout match played: reopen wording and "Reopen both" · the individual editor stays open, running, with the notice · the kachinuki Reopen stays open with the notice


Knockout requalification: warning naming the round · running match refused

Chusen: Change on a recorded chusen after the knockout started · the warning naming the fought match · before: the tie awaiting chusen is highlighted · after: the order is recorded and the pool is no longer highlighted as tied, admin and public




Correcting a withdrawal shows what is recorded: team match, the result band names the recorded winner and decision, Kiken beside the team that withdrew · the Recorded line below · individual match


Team withdrawal credit: the confirm states the consequence · Scores list row (credited totals, Kiken beside the team) · the correction band · the viewer card's bouts (its Recent results row is from before the clipping fix; the next shots are on the final build) · Recent results with a long name, Shiro and Aka · a short-name row, unchanged · the Scores row with a long name · standings








A withdrawn team's remaining match: court console notice, not in Up next · public row with the Withdrawn chip · after Record default win · Clear default win confirm · back in the queue with the notice




Individual: injury kiken offers Reinstate · after Reinstate · both sides barred · recorded as drawn · Clear default win once reinstated




bc-kcsh: fusenpai correction, team and individual · kachinuki: no credit note


bc-rawm: a tied individual knockout bout · the court busy panel names the blocking match

Code review round: a cleared default win goes back to the queue while the court runs another match, and the console returns to that match · a five-player semifinal decided by kiken, reopened (the Final is back to "Winner of M2"; retaken on the final build) · changing a chusen after the knockout: only the real move is named


Knockout rounds (harness captures, final build): a five-player draw on the admin Bracket page (M1 is the only first-round bout; the semifinal pair and the third competitor first appear in the Semifinals; M1's line runs straight into the "Winner of M1" row) · a ten-player draw part-way through (each first-round line runs straight into the row its winner fills; cards fed by two matches join at their centre)

Admin Bracket page at iPad size (1180×820, a sixteen-player naginata knockout): the whole tree and the 3rd-place card fit with no sideways scroll, the panel below the bracket · after picking M1, the page has scrolled to the scoring panel

A 64-player knockout on the admin Bracket page at 1920 wide: the page takes the whole window here, so all six rounds fit at full card width

Clear withdrawal and reopen in one tap (admin Scores tab, Correct on a match decided by Kiken – Voluntary, 1180×820): the recorded withdrawal with the button and, below it, what clearing does · after the one tap, the match is in progress again with the editor open on it; no reason was asked

Reopen needs no reason (frames from the recaptured kachinuki video, a real browser run by the capture harness): the completed encounter with Reopen match · reopened, bouts kept, End match offered · End match armed by the first tap · the second tap ends it and the editor closes to the Scores list, with no reason prompt at any step

Test plan

  • make go/test on the final code (44ef2613): exit 0. Lint, import checks and security audits pass, JS 4884/4884, every Go package ok. Harness SINCE=main: 31 captured, 31 unchanged. make docs/build and the link check: OK (39 pages, 2431 links). make examples at 8c45d626 (no workbook changed); nothing since touches the export.
  • One-tap clear: the rewritten render tests fail against the previous component (31 of 41) and all pass now; in the browser, Correct on a kiken match shows the consequence beside the button, and one tap puts the match back in progress with no reason asked.
  • Straight lines and the admin layout: each new test fails against the previous code and passes now (the fed row level with its feeder on both sides, no overlap either way, a straight connector end to end in the render test, the SVG sized from the cards, the panel scrolled into view once per pick, the bronze in the last column slot). In the browser at 1440×900, 1180×820 and 1024×768 on a twelve- and a sixteen-player knockout: no sideways scroll at 1440 and 1180 (columns 230px and 187px), the 180px floor reached at 1024, where the canvas scrolls as before; the bronze card centred under the final at every width. Harness SINCE=main: 31 captured, 3 changed (the three bracket images, read and copied in), 28 unchanged.
  • Third review round, each new test run red with only its fix removed and green with it restored: the moved-times reload (the marker gate forced off), the marker-only save (the save condition forced off), and the TV daihyosen tie. The both-barred reopen test fails against each earlier handler in exactly its case: the no-reason draw against c9f595d7 (refused), the kept reason against cb251d5f (dropped).
  • Reopen reason retired: every rewritten test run against the previous handlers and editor fails exactly where the old rule refused or prompted (the no-reason ends on /score, bulk score, bracket, bronze and /decision; End match arming instead of prompting; the fusenpai panel with no required reason) and passes now; the with-reason cases pass both ways. Harness SINCE=main: 31 captured, 31 unchanged. The recaptured video read frame by frame: the reopen chapter shows Reopen match, the reopened encounter, End match armed, and the encounter ended with no prompt; chapter times unchanged (0:02, 0:08, 0:15, 0:21). Docs build and link check OK (39 pages, 2431 links).
  • New/updated unit tests cover the change. The implementing agents report each new test failing with only its fix reverted; I repeated that check myself for the reopen-notice assertion and, in the review round, for the quick-score withdrawal exemption.
  • Manual browser verification at 1180x820 through real clicks on a fresh build: all five beads (refused Finish then Finish after bout 3; lineup panel order; hint on the Scores tab, the console and under touch emulation; Close at 96 ms on a running match, Esc on the console, prompt on a scheduled match; double tap 117 ms apart); individual kiken correction; team Clear withdrawal and reopen then Finish + Start Next; knockout requalification warning, Apply and reopen, and the running refusal; reopen with a later knockout match played (individual and kachinuki): reopen wording, editor stays open, notice, "Reopened" button state. Chusen changed after the knockout started, the warning and Apply and reopen: checked on the build before the three display fixes. On the final build: chusen recorded and its recorded panel with Change; team and individual withdrawal corrections showing the recorded winner, decision and Kiken mark; a chusen-settled pool no longer highlighted as tied, admin and public. Not browser-checked, covered by render tests only: the final variant of the start-refusal clearing. Round 7 (1180x820, real clicks), on the builds of this round: team kiken credit on the Scores list, correction band, viewer card and standings; a withdrawn team's next match skipped, chip, Record default win, Clear default win back to the queue; kachinuki without credit; injury kiken with Reinstate; both sides barred recorded as drawn; Clear default win once reinstated; a tied individual knockout bout; the court busy panel; bc-kcsh fusenpai correction, team and individual. The fixes made after those runs (the confirm wording, the name-row layout) were checked again on the final build: the confirm, long names keeping their mark on Recent results (Shiro and Aka) and the Scores list, and short-name rows unchanged; the docs screenshot harness confirms the rest of the rows. Code review round, on that round's build (1180x820, real clicks): Clear default win on a fusensho while the same court runs another match (200, back to the queue, the console returns to the running match); Clear withdrawal and reopen on a five-player knockout kiken whose winner was already in the Final (200, the Final cleared), then the re-fought result reaches the Final again, retaken on the final build as the semifinal M2 (200, the Final back to "Winner of M2"); a chusen recorded as one request, then changed after both of Pool A's knockout matches were played: the confirm names only 1st place (Kyoto to Sendai), and Apply and reopen reopens that match alone while Nara's keeps its result. Knockout rounds (browser and API, final build): a five-player draw shows Endo v Ito as Match 1 at 09:00 and Hara v Fujii as Match 2 under Semifinals; a five-player draw made by a v2.1.0 build and opened by this one is corrected on the first start (logged once, the second start writes nothing), and its Excel export names the same Match 1 to 4 as the app, with Hara v Fujii in the tree's second column. make examples regenerated; the small knockout example now has two first-round bouts and four in round 2.
  • Screenshots added above.
  • Docs updated (make docs/build passes; check_links.py: 39 pages, 2430 links resolve).
  • Docs media: make docs/screenshots SINCE=main on the final build (after the knockout round change) reports all 30 captures unchanged; a full run after adding the three bracket captures reports all 33 unchanged; after the BYE-card change exactly the three bracket captures changed (recaptured, read, copied in) and the other 30 stayed unchanged (an earlier round caught five changed by a name-row alignment regression, fixed before commit). The one video, kachinuki-demo, was not re-recorded: its recipe reaches no withdrawal, default win or long-name mark, and videos are not compared by the harness.
  • No new console errors: the capture harness saw only the lineup-not-found 404 (GET .../match-lineups/:mid answers "no lineup submitted for this team and match" by design; the lookup code is identical on main), and the browser checks only the 409 of the chusen warning itself.

Notes for the reviewer

  • A team match completed on the previous build (untouched rows stored as Ties) still corrects cleanly: a stored Tie loads as draw: true, counts as played, and is written back as hikiwake, so the Finish gate does not refuse it.
  • The bc-tmfn e2e test.fixme lives in PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442 (not on main). Switched to a plain test in a scratch merge of the two branches, it passes with this branch and fails without it (Team Kame W1 IV1 IT2: the unscored bout 3 counted as a draw). It can be switched on in test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442 once this merges.
  • On an individual correction of a withdrawal the winner holds the default ○○, so the withdrawer can hold at most one point already struck; once it is recorded, every letter button on that side is disabled. The letter is changed by tapping the scored mark, as everywhere else.
  • make examples was not run: the CLI Excel path (cmd/, internal/helper, internal/excel) is unchanged.
  • Accepted risk (code review finding 7): every reopen and pool Send back to queue stamps modifiedAt with the server's clock, the fence the bracket requeue already used. A tablet whose learned clock offset has drifted by more than the seconds between a reopen and its next tap could see that tap refused as superseded; the offset is RTT-corrected and relearned on reconnect and on a 3 s heartbeat divergence, and a weaker fence would let a stale queued correction undo the reopen.
  • Out of scope, left as is: a team match left running on the previous build keeps its untouched rows as Ties; brackets drawn before per-side ids compare namesakes by name; DELETE .../overrides (the repair path) is not routed through the planner.
  • A barred competitor's remaining match is recorded as a match-level fusensho, so the withdrawal stays the one eligibility record: clearing that withdrawal later leaves those default wins as recorded (the Clear withdrawal confirm lists them), and each can be cleared on its own. "Fus." beside a fusensho winner is what the exported workbook already printed; the app now shows the same.
  • The credit is worked out when results are read, so a tournament already holding team withdrawals re-ranks on upgrade; knockout seats move only on the next pool write (the requalification check), with its warning. A completed withdrawal is padded with empty bout rows up to the team size, and older data is padded once on load; an unreadable bout cell is never padded over.
  • Correcting a team withdrawal: the bout rows stay inputs while the band shows the credited totals; a result entered on a row replaces the credit for that bout.
  • The roster stays editable after the start (an earlier operator ruling), so a roster PUT is not refused then, and team members and lineups are pruned only before the draw.
  • Observed, unchanged: after a decision the court console starts the next match on its own, as main does (it mirrors Finish + Start Next).
  • A bye is never reopened by anyone: it is the draw's hidden pass-through match, and it only changes when the match feeding it is reopened. That reopen takes the winner back out of it and leaves it as the draw made it, so it never shows as a match to play; the next result passes through it again. A pool correction that force-reopens a knockout match still leaves a bye beyond that match as it is (logged).
  • Where the admin Bracket page still scrolls sideways, measured in the browser on 16-, 32- and 64-competitor knockouts at 1180, 1440, 1600, 1920 and 2560 wide (columns stop narrowing at 180px): a four-round tree (9 to 16) fits from 1180 up and scrolls at 1024; a five-round tree (17 to 32) fits from 1440 up and scrolls at 1180; a six-round tree (33 to 64) fits from 1600 up and scrolls at 1440 and 1180. The page never scrolls sideways itself, only the bracket card. Tall trees scroll the page down as before (a 32-player page is about 2600px long, a 64-player one about 4700px). The Overview, Participants and Scores sections stay 1400px wide at 2560, measured.
  • The harness logs a 404 while capturing kachinuki-scoring-buttons; the same capture on main logs it too, so it predates this PR.
  • Knockout rounds: once any match of an old draw has been touched, its times are kept as given, so a later-numbered match can still be timed first; the operator can move it. The first of the three round commits says order of play follows the rounds; that became true with the scheduling commit after it. The EKC draw spec (specs/007-ekc-draw/spec.md, tracked) now carries the corrected Junior Male reading, checked again in the PDF: court A prints P2 v P3 alone in the first column and P4 v P5 beside P1 v Winner F1; court B prints P7 v P8 and P9 v P10 the same way.
  • The TV board's daihyosen change shows nothing different on consistent data (the server's figures and the bout rows agree), so there is no screenshot for it; the render tests pin the one case where they disagree.
  • A competitor who withdrew after qualifying from a pool is not replaced in the knockout (the guide is silent); their knockout match is skipped by Up next and offers the default win like any other.
  • Already-filed issue seen during UAT: starting a match while a correction is open hides it (bc-crpn).

Closes bc-tmfn
Closes bc-cpdc
Closes bc-kbrw
Closes bc-dscn
Closes bc-kbhn
Closes bc-kcsh
Closes bc-rawm

🤖 Generated with Claude Code

gitrgoliveira and others added 3 commits September 24, 2026 20:11
Operator ruling 2026-09-24: every bout of a team match is fought.

- bc-tmfn: Finish (client and PUT /score, bulk-score) refuses a
  non-kachinuki team match while a numbered bout has no result, naming
  the bouts. A position left empty by both teams in the lineup in force
  is skipped; a team with no saved lineup counts as fielding every
  position. Untouched bouts are no longer written as hikiwake.
- bc-dscn: closing a running match flushes the autosave instead of
  asking "Discard unsaved scoring changes?"; the prompt stays where
  closing really discards; Esc does nothing on the court console.
- bc-kbhn: the shortcut hint hides under a coarse pointer and lists only
  keys that work on the host; arrow keys need a neighbour match (fixes
  a scoreKeyOf(null) throw on the Scores tab).
- bc-kbrw: a double tap on a fought kachinuki bout opens that bout only.
- bc-cpdc: "Copy from previous match" sits at the top of each side.

Follow-on work from operator rulings ("save correction saves what the
operator enters"; "everything can be fixed, the operator is told the
consequences"):
- Save correction keeps a recorded kiken/fusenpai and its eligibility
  consequences; the withdrawer's letters stay editable.
- "Clear withdrawal and reopen" (team, individual, kachinuki) returns
  the match to running with what was fought kept and restores
  eligibility; the eligibility record now follows the ruling.
- Mixed competitions: a pool correction, reopen or rank override
  (chusen) that changes who qualified after the knockout started warns
  with the affected knockout matches and, on "Apply and reopen",
  reopens them and seats the new qualifier; a running affected match is
  refused with the fix; chusen is accepted in knockout status.
- Reopens stamp ModifiedAt; queued writes never replay a force.

Closes bc-tmfn, bc-cpdc, bc-kbrw, bc-dscn, bc-kbhn

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling 2026-09-24, superseding the both-vacant exemption in the
previous commit: every bout of a non-kachinuki team match has a result or
a decision; a team always fields at least half+1 (an assumption, never
checked); a bout neither team fields is recorded as a Tie. The finish
gate no longer skips positions, so the lineup-vacancy lookup it used
(TeamBoutsWithNoFighter, lineupRoundOfTeamMatch, TeamLineup.VacantAt)
is deleted.

Code review of PR #444:
- Standings count an individual draw (IT) only for a bout with a result.
- A confirmed reopen also clears the reopened match's winner out of an
  untouched next round, as the pool path already did.
- Moving a withdrawal to the other side through /score or bulk-score
  restores the first withdrawer, as /decision does.
- Quick-score draws are written as hikiwake.
- Two discarded prior-lookup errors are propagated.
- The reopen confirm uses reopen wording ("Reopen both"); the kachinuki
  Reopen keeps the editor open; the button reads "Reopened" once the
  server answers; a reopen notice clears when the match completes again.
- formatPositionLabel is gone in favour of Position.Label().
- The kachinuki demo recipe no longer reopens the editor after Reopen;
  the clip is re-recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sult

Chusen (drawing lots) can now be changed after it is recorded:
chusen-candidates returns the recorded groups alongside the open
ones, and the Pools page shows a "Chusen (drawing lots) recorded"
card with Change, writing through the same override-rank path and
pool requalification planner as the first entry.

Display gaps found in UAT:
- Correct on a team match decided by kiken or fusenpai shows the
  recorded winner and decision in the result band, and the Kiken or
  Fus. mark beside the withdrawing team's name, instead of DRAW.
- The individual editor shows the same side mark on a correction.
- A tie settled by chusen is no longer highlighted as tied in the
  standings (admin and public).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gitguardian

gitguardian Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

gitrgoliveira and others added 17 commits September 25, 2026 06:27
…me the match

bc-kcsh: Correct on a match decided by kiken or fusenpai shows the
recorded decision, the winner and the side mark before any edit.

bc-rawm: refusals name the competitor, the match in the operator's
terms (engine.OperatorMatchLabel, pinned to the JS labels by a shared
fixture) and what to do; an individual knockout bout cannot be
finished tied; the Remaining matches award no longer throws.

Team withdrawal credit: a completed non-kachinuki team match decided
by kiken, fusenpai or a default win credits every bout without a
result to the other team at 2-0 (one IV, two PW), one rule in Go
(state.DefaultWinCreditSide) and its JS mirror. A completed default-win
write pads empty rows to the team size; stored data is padded on load;
an unreadable bout cell is never padded over.

A barred competitor's remaining matches carry a read-only
ineligibleSides stamp, are skipped by every next-match picker, show a
Withdrawn chip, and offer a one-tap default win (match-level fusensho)
and Reinstate for an injury kiken. Clearing that default win returns
the match to the queue while the competitor is still barred. Both
sides barred: a pool or league match can be recorded as drawn.

Roster: the batch replace keeps surviving ids; removed teams' members
and lineups are pruned before the draw only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Chusen: a recorded or changed order is one write, checked against the
  knockout once on the final order; a refusal records none of it.
- Reopen: a cleared default win that goes back to the queue ignores a busy
  court; a reopen or correction whose winner passed through a bye unwinds
  the bye and answers for the match after it, so a wrong kiken there is
  fixable again.
- A refused correction's rollback no longer re-bars a reinstated competitor.
- An unreadable pool or bracket file is reported as such, not as a missing
  match (lookups and the simultaneity gate).
- Decision endpoint: the both-barred draw runs after the clock check and
  maps its errors like every other decision.
- Quick-score runs the team finish gate with /score's withdrawal exemption;
  bulk-score loads the competition once.
- Default-win padding keeps bout rows in position order.
- The court console lets go of a correction that went back to the queue.
- The JS default-win credit falls back to the winner like the server.
- Dead code removed: the resolved-by-bye refusal, single-rank wrappers,
  ChusenCandidates, two unused JS exports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bye is the draw's hidden pass-through match; nobody reopens it. The
docs now say what happens when the match feeding it is reopened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heets print

The 34th EKC 2026 Junior Individual Male sheet (page 4, court A) prints
P2 v P3 alone in column 1 and P4 v P5 beside P1 v Winner F1 in column 2;
the 33rd EKC 2025 Men Team five-team courts, and the 2026 Junior
Individual Female and Men Individual sheets, print the same way. An
earlier reading put P4 v P5 in column 1 and pulled every pair beside an
empty pair forward a round (the "risen" classification), so a
five-player knockout showed its semifinal pair as a quarterfinal.

A bout's round is now its distance from the final on every surface: the
app bracket, match numbers, order of play, and the Excel columns. The
pairings are unchanged; the rise marks stay as slot geometry only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n load

Those releases stored rounds and match numbers from the misread column
rule, while the Excel export renumbers with the corrected walk, so the
app and the printed sheet named different bouts (and the export places
scores and courts by match number). On first load a stored bracket's
rounds are recomputed from its own feeders (the final is round 1, each
feeder one deeper) and its matches renumbered by the one numbering rule,
now state.Bracket.NumberMatches, shared with generation. Pairings,
results, courts and scheduled times are untouched; a bracket that cannot
be walked is left as it is. A drift guard pins that the stored-feeder
rule leaves every freshly generated bracket unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Times were handed out in the bracket's storage order, before the matches
were numbered, so a five-player draw put its semifinal (Match 2, the pair
beside an empty pair, stored in the first row) at 09:00 ahead of the
quarterfinal (Match 1). Slots are now assigned after numbering, in
match-number order: deepest round first, then left to right, on every
shiaijo. Byes still take no court time. Draws already made keep the
times they were given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs showed no bracket at all. Three captures now do, kept current by
the harness (a new knockout recipe file, two families):

- knockout-draw.md: the admin bracket of a five-player knockout, beside
  the paragraph that describes the block (one first-round bout, three
  competitors marked BYE, the semifinal pair, the final).
- run-tournament.md: a ten-player knockout part-way through, on the
  public Bracket tab (the admin page puts the scoring panel beside the
  card, so a four-round tree never fits it without scrolling).
- following.md: the same tree on the public Bracket tab at phone width.

The Byes section now says what the bracket shows: the one bye the order
awards, and the other competitors of an odd block who simply start a
round later and are marked BYE too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A confirmed pool correction or a forced knockout correction now unwinds
  a hidden bye the old winner passed through, clearing that winner from
  the first real match past it when nobody has touched it; before, a
  competitor who no longer qualified stayed seated there and could be
  started. A played match past the bye is left for its own warning.
- One producer of a match's round: generation stamps rounds from the
  stored feeders with the same walk the load-time repair uses
  (StampRoundsFromFeeders); the draw-tree override and SlotRoundMatches
  are gone. The Excel parity suites and the numbering golden are
  unchanged.
- The load-time repair is described as what it is: any stored bracket
  whose rounds or numbers differ from the rule, which includes pre-v2.0
  brackets numbered by the old tie-break (pinned with a real v1.1.0
  bracket).
- An unstarted bracket still scheduled in storage order gets its own
  times handed out again in match-number order, per court.
- bracket.json is parsed and saved once by the load-time repairs, and a
  bracket that cannot be walked is logged once per competition.
- Comments: the numbered() invariant, the scheduler header, estimate.go's
  reference, the bye note in scoring.go; one ScheduledAt layout constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on its row

Operator decision, from mockups: a competitor who skips a round appears
only in the match they first fight, as the European Championship sheets
print them, so the placeholder BYE cards and their connectors are gone.
A card fed by one match takes that line on the row the winner fills
(Aka or Shiro); a card fed by two keeps the join at its centre
(connectorTargetY). The docs describe the bracket this way, and the
three bracket captures are recaptured by the harness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A time the operator moved is no longer taken back on load. The one-off
  repair of old storage-order scheduling ran on every load and could not
  tell a queue swap from the old scheduler, so moving Match 2 above Match 1
  in a five-player draw was reverted at the next restart. A bracket now
  records TimesSettled: set by the draw, and by the repair the first time it
  runs, which then never looks at the times again.
- Recording a both-barred match as drawn now owes a reason-less reopen's
  reason, as every other way of ending a match does: refused without one,
  and stored as the correction reason when given.
- The TV board no longer repeats the daihyosen tie test from the bout rows;
  the shared scoreboard's own test, which reads the server's figures first,
  is the one gate, as on the viewer card.
- numbered() and its two restatements say where a side can still be emptied
  (a degenerate pool's missing place) and what the load does then.
- The tracked EKC spec carries the corrected Junior Male reading in all
  three places it stated the misread, and CLAUDE.md no longer says no spec
  plan is tracked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing reason

A match can be reopened without any reason (operator ruling), so recording a
both-barred match as drawn no longer refuses a request that carries none. The
draw still settles the reopen in the same transaction, keeping the request's
reason, if any, as the correction reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling: a match can be reopened without any reason, and nothing is
gated on that. Ending a reopened match again now works like ending any match:

- the server no longer refuses a score or a decision that ends a match
  reopened without a reason (the reopen-pending refusal and its message are
  gone); ending it clears the flag and keeps a reason if one is sent;
- the team editor's End match no longer opens a "Reason for reopening"
  prompt, and the withdrawal panel's reason is optional after a reopen, as
  it is everywhere else;
- the docs say so, the two screenshots of the retired prompts and their
  harness recipes are removed, and the kachinuki video is recaptured with
  the reopen chapter ending without the prompt.

The stored reopen flag stays (the pool CSV reads its columns by position),
but nothing is refused for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A card fed by one match is placed so the row that match fills is level
  with it, so the line between them runs straight instead of bending half a
  row. The stacking makes room for the shift, so no card overlaps its
  neighbour. One helper, oneSidedFeed, now states what a one-sided card is
  for both the connector and the placement.
- The admin Bracket page (operator decision): the bracket takes the full
  width and the scoring panel sits below it; picking a match scrolls the
  panel into view. The tree's columns narrow to fit, 230px down to a 180px
  floor with tighter gaps, so a four-round tree (9 to 16 competitors) fits
  an iPad in landscape without scrolling sideways. The 3rd-place card sits
  in a row of column slots sized like the tree's, the last under the final.
- The connector SVG is sized from the cards rather than from the tree's
  scroll size, which included the SVG itself and held a narrowed tree at its
  first, wider width.
- The three bracket docs images are recaptured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The competition page keeps its 1400px width everywhere except the Bracket
section, which now takes the whole window, so a six-round tree (33 to 64
competitors) fits a large monitor: from 1600px wide, measured. Every other
section keeps its readable line length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gitrgoliveira gitrgoliveira changed the title fix(bc-tmfn): every team bout needs a result; five operator-review fixes fix(bc-tmfn): operator fixes: team bouts, withdrawals, knockout rounds and the Bracket page Sep 25, 2026
Operator decision: like Reopen match, Clear withdrawal and reopen (and
Clear default win) reopens in one tap, with no reason. What the clear does
is now stated beside the button, before it is tapped, instead of in the
confirm step it replaces: the match goes back to running or to the queue,
the withdrawn side can compete again, which points are kept, whether it may
change who qualifies, and any later default win from the same withdrawal.
The one second step left is Reopen both, when a later knockout match has
already been fought. The unused reopen reason presets are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gitrgoliveira
gitrgoliveira merged commit 83284eb into main Sep 25, 2026
13 of 14 checks passed
@gitrgoliveira
gitrgoliveira deleted the bc-tmfn-operator-fixes branch September 25, 2026 18:58
gitrgoliveira added a commit that referenced this pull request Sep 25, 2026
…n and bc-unfx

PR #444 changed three behaviours the journeys asserted:
- J2: a withdrawn competitor's next bout offers "Record default win"
  instead of Start match (e78ec2a), and a tied individual knockout
  bout's Finish reads "Needs a winner", disabled.
- J5: ending a reopened encounter asks for no reason (9df3980); the
  reload interrupt moves to the armed End match.
- J3: Copy from previous match moved to the top (bc-cpdc), so nothing
  sits beside Save lineup; bout 3 is now recorded as a Tie before
  Finish, per the every-bout-is-fought ruling.

bc-tmfn (closed by #444) and bc-unfx (fixed by #444) are plain tests
now. New fixme bc-dhas: a representative bout added inside the last
Tie's autosave window is erased by that write; J7 and bc-dhrp wait
past it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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