Skip to content

fix(bc-rfsw,bc-offl,bc-rboq,bc-flst,bc-kheb,bc-mrgc,bc-hlck): merge match writes by change, held results visible, correction refusals, kachinuki encho - #453

Merged
gitrgoliveira merged 48 commits into
mainfrom
worktree-bridge-cse_015HAZRVnwzTHJEA71fKRqXm
Oct 5, 2026
Merged

gitrgoliveira merged 48 commits into
mainfrom
worktree-bridge-cse_015HAZRVnwzTHJEA71fKRqXm

Conversation

@gitrgoliveira

@gitrgoliveira gitrgoliveira commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

Seven beads, plus operator requests made during review.

  • Merge changes, never drop (bc-mrgc). Operator ruling 2026-10-03: "Nothing should be dropped. All events must be ordered." A match write now applies only the groups it changes (points, result, overtime, engi flags, representative players, each bout row), each ordered by when the change was made, never by when it arrived. Whatever does not apply is kept in a per-match history, written in the same transaction as the match and shown in every score editor under History. The rules, each an operator ruling:

    • Points scored after a withdrawal of that match mean the withdrawal was a mistake: it is cleared and kept in the history.
    • A late point on a finished match applies, the match stays finished, and the winner is worked out again.
    • A change that would leave a finished knockout match without a winner, or an engi match without a valid flag count, is held, and the operator is told to correct the result with a winner. In a pool or league the match becomes a draw.
    • A match closed by a kiken, fusenpai or fusensho keeps that result against a court still scoring it, and the notice names the decision.
    • For engi, the newest valid count decides, whichever write arrives first.
    • The final state does not depend on the order in which writes arrive.

    The offline queue keeps every write: one entry per write, each sent on its own, nothing dropped for its age. A write that keeps failing holds back no other, and a write the server keeps refusing can be discarded on its own.

  • No write stamped before the match it was made against (bc-hlck). A device stamps a write no earlier than the match it saw, so a correction made just after a write from a device whose clock runs ahead is applied rather than kept in the history.

  • Correction refused while a later match is being fought (bc-rfsw). A knockout correction that would change a side of a running later match is refused with a 409 naming that match and its shiaijo, instead of re-seating a bout in progress.

  • Held results are visible (bc-offl, bc-rboq). The top bar reads "Offline: N result(s) not sent", the sync pill "Reconnecting…", and a toast confirms "1 finished result sent." Every queued notice reads "Not sent yet: saved on this device, and sent when the connection returns." The top-bar count opens a list of everything held on the device, with Discard for a write the server keeps refusing. Kachinuki Record bout returns its result, so a bout held offline shows "Not sent yet".

  • Fighter list (bc-flst). The list commits on click, swallows a bounced second tap, and reopens when its name box takes focus.

  • Kachinuki overtime on the bout (bc-kheb). A kachinuki encounter carries no match-level (E): the mark lives on the bout row. Writes strip it, and stored marks are repaired once on load.

  • Remove kiken, fusenpai or fusensho. Operator ruling 2026-10-03: "an operator can fix a mistake, but the fix must leave the match resolved". A decision recorded by mistake on a match fought to a result is removed in one save: Remove takes it off the board, the operator enters the real result, and Save correction sends it. The match stays finished, never needs the court, and the barred competitor can fight again. Clear and reopen stays beside it for a match with fighting left. Kachinuki is not offered Remove: a finished encounter has no Save correction.

  • The decision is named. Operator ruling 2026-10-04: every finished match has a result, a scoreline or a registered decision. Every label, notice, message and docs page names the recorded decision ("Record fusensho for ", "Remove kiken", "Not applied: this match was closed with a fusensho"). Three prose gates keep the old term out of Go strings, web-mobile strings and the docs.

  • Shared courts (operator request 2026-10-03). The court-move confirm names both competitors. A court whose competition still has matches names another competition's waiting matches in a quiet line ("1 Individual match also waiting on this court."), and the console switches only on a tap.

  • Gaps closed (operator request 2026-10-04): a court console back online refetches its queue; each pool bout has one number on every surface, its place in the playing order; a write the disk refused after its transaction log committed is retried by the device and logged as a disk error; the server refuses to start on a data folder it cannot write, naming each folder; the pdfcpu 0.16.0 bump on main (every call takes a context) is fixed.

  • Two defects found while closing the review's gaps. Release archives and packages embedded no Preact, so the app's UI rendered blank: the published v2.1.0 answers 404 for all four of its vendor scripts. The release hooks now fetch them, and the package smoke test starts the app and fetches its UI assets, so a missing one fails the release. And the armed state of a two-tap button had no text colour of its own, so kachinuki's armed End match read dark on navy; it is white now.

Why

  • bc-mrgc: a write older than the stored result was dropped whole and kept nowhere, even when it changed something else. A running write could replace a queued Finish, queued writes expired after 12 hours, and two read-modify-write windows could lose an entry.
  • bc-hlck: two devices' estimates of server time differ by up to the 5 s the server accepts, so a correction could be stamped older than the write it corrected.
  • bc-rfsw: the guard judged only later matches already played, so a correction reaching a running match got through.
  • bc-offl, bc-rboq: a queued write and a landed write looked the same to the operator.
  • bc-flst: the list closed on blur before the click registered, and a bounce picked the wrong fighter.
  • bc-kheb: one bout fought on in encho put the whole encounter in overtime.
  • Remove: undoing a mistaken withdrawal needed a reopen, which takes the court and leaves the match unresolved.
  • Gaps: a reconnected console kept a stale queue, because the stream reopening raises no error and its reconnect refetch never ran; a stuck autosave, lineup or hand-set winner had no way out but signing out.
  • Release: web-mobile/vendor is gitignored and only the Makefile's vendor-frontend target fetches it; goreleaser never ran that target, and its smoke test only ran bracket-creator version. Docker images and the Homebrew formula build through other paths and were not affected.
  • Armed button: .btn--confirm set a navy fill and relied on .btn--primary for white text; End match is a plain .btn.

Upgrade and rollback

Upgrading in the middle of a tournament needs no action:

  • pool-matches.csv gains a GroupStamps column, appended last, and bracket.json a groupStamps field. A match without them reads every group as stamped at its ModifiedAt, which is how the previous release ordered writes.
  • Each competition gains a history/ folder, created when a match is first written.
  • A kachinuki competition's stored encounter overtime is repaired once, on first load, and config.md records it (kachinuki_encounter_encho_cleared).
  • Writes a browser queued under the previous release load and replay unchanged.
  • Pool bout ids follow the playing order in draws made from now on; an existing draw keeps its ids.
  • One behaviour change at startup: the server refuses to start if it cannot write to the data folder or any competition folder, and names each one. A deployment that ran with such a folder before now stops at startup.

Rolling back to the previous release: it reads these files, ignoring the extra column, the extra field and history/. Its next save of a match file drops the stamps, so those matches fall back to one stamp per match. Its writes are not added to the history, and its Discard draw leaves history/ in place, so a draw discarded and regenerated under it shows the earlier draw's history for the reused match ids after upgrading again.

Checked in a browser against v2.1.0 on the same data: a finish this build's page held offline was sent to v2.1.0 and saved; after a reload as the v2.1.0 client, a finish this build had queued (the new entry format, with changed) was read, sent and saved, and the queue emptied. v2.1.0 rewrote pool-matches.csv without the stamps column and added no history line. The published v2.1.0 binary serves no UI (the release defect above), so the old client was v2.1.0 built from its tag.

Files changed

File What
.github/workflows/release.yaml the package smoke test also starts the app and fetches /dist/app.js, /dist/glossary_data.js and the four /vendor/*.js files
.goreleaser.yaml the release hooks run make vendor-frontend, so archives and packages embed Preact
CLAUDE.md the rules this PR settles: match groups, stamps and history, the held-write answers, the offline queue, Remove , the decision named, the startup write check, pool bout numbers
docs/architecture/data-model.md group stamps, the per-match history file and every reason it records, write ordering, pool bout ids in playing order
docs/architecture/infrastructure-architecture.md the startup write check
docs/architecture/network-architecture.md how writes are queued on the device, sent and ordered; held and moved changes; a match closed by a decision keeps it
docs/check_prose.py a rule refusing the term this PR removes from the docs
docs/screenshots/kachinuki-reopen.png, kachinuki-correct-bout.png, kachinuki-knockout-tie-encho.png, kachinuki-scoring-buttons.png, mobile-encho-overtime.png, mobile-score-editor.png recaptured: the History line under the board (kachinuki-reopen also no longer clips its footer)
docs/screenshots/mobile-draw-preview.png, mobile-pool-standings.png recaptured: the standings legend names the decisions
docs/videos/kachinuki-demo.webm recaptured: the armed End match is readable (chapter marks unchanged)
docs/user-guide/court-operators/recording-decisions.md buttons named by the recorded decision; Remove beside Clear and reopen
docs/user-guide/court-operators/scoring-a-match.md match history; results held offline; changes kept rather than applied, with the notices quoted as the editor shows them; correction refused while a later match is fought; the shared-court line
docs/user-guide/install/install.md the startup write check
docs/user-guide/organisers/team-tournaments.md the decision named
running_a_kendo_tournament.md, specs/003-tournament-gap-closure/inputs/gaps_spec_brief.md, gaps_tournament_spec.md, gaps_webui.md the decision named
specs/openapi.yaml changed, heldGroups, displacedGroups, heldReason, heldDecision, clearWithdrawal; the history route and every reason it records; the new 409 shapes, with the runningMatches body on every door that returns it
internal/domain/decision.go DecisionWord: the one Go owner of a decision's word
internal/domain/glossary.go, web-mobile/js/glossary_data.js (generated) the kiken and fusenpai tooltips say the match is awarded to the opponent
internal/engine/bracket_result.go projects group stamps so a rollback restores them; bracketMatchCourtAndSchedule for a replayed decision's answer
internal/engine/competition.go discarding a draw deletes its match history
internal/engine/daihyosen.go pool daihyosen injection reads, decides and saves in one transaction
internal/engine/eligibility.go lookupExistingResultIn: one read answers the prior and which file holds it
internal/engine/engi.go the engi finish ordered by stamp, never a draw, the newest valid count deciding; the override guard runs the running-match check even when forced
internal/engine/engine.go test-only read seam for the lost-update tests
internal/engine/errors.go the running-downstream refusal; sentenceList lists "A, B and C" as the SPA does
internal/engine/kachinuki.go runningDownstreamRefusal; encounter overtime stripped; the advance re-checked under the lock, re-read on a stale check, stamped no older than the match, and written with its history entry in one transaction (KachinukiAdvance hands back its stamps)
internal/engine/league_tiebreak.go league tie-break injection in one transaction
internal/engine/match_history.go new: history entries for every write door
internal/engine/match_merge.go new: the merge owner (groups, stamps, R2 to R4, moved changes, the finish kept with its scoreline, the decision hold)
internal/engine/pools.go numberPoolMatchesInPlayingOrder: a pool's ids follow the playing order
internal/engine/scoring.go the shared correction guards; KeepsWithdrawalRuling does not keep a ruling the write clears; both write branches merge; the hand-set winner and the requeue write and append history in one transaction
internal/engine/scoring_tx.go the decision pre-check (stale replay, the score door's rules); history points; held writes; exact decision replay with the bracket match's court; kachinuki overtime stripped in recordDecisionTx
internal/engine/swiss.go, tiebreaker.go the next Swiss round and tie-break injection each in one transaction
internal/engine/testdata/downstream_running_messages.json new: the refusal sentences, read by the Go and JS tests
internal/mobileapp/deps.go the history store interface; MaybeAdvanceKachinuki returns *engine.KachinukiAdvance
internal/mobileapp/errors.go the running refusal as a 409 with reasonHuman; a committed but unapplied write logged as a disk error
internal/mobileapp/handlers_branding.go, handlers_sponsors.go folder names from state
internal/mobileapp/handlers_competition.go a new kachinuki competition starts with the repair marker
internal/mobileapp/handlers_daihyosen.go changed and held groups on the representative-bout routes
internal/mobileapp/handlers_decision.go the handler-side clear removed (the engine owns it)
internal/mobileapp/handlers_match.go /score reads changed and clearWithdrawal; withHeldGroups, the one owner of the held answer keys; the running revision guard holds; messageForHeldReason; the Record-bout answer carries the advance's stamps
internal/mobileapp/handlers_match_history.go new: GET .../matches/:mid/history
internal/mobileapp/middleware.go, server.go the history route, main-gated in self-run
internal/mobileapp/testdata/default_win_cases.json new: the case table the Go and JS prose gates both read
internal/pdf/generate.go, pagenum.go, pdfops.go pdfcpu 0.16.0: every call takes the generator's context
internal/state/bracket.go groupStamps on bracket matches; MatchByID
internal/state/legacy_upgrade.go the one-time encounter overtime repair; a read failure leaves the marker unset
internal/state/match_groups.go new: the group owner, stamps and merge report
internal/state/match_history.go new: the per-match history file, staged in the write's transaction; a damaged line, or one the scanner cannot read past, is logged with the match
internal/state/models.go GroupStamps; the request-only fields; ClearKachinukiEncounterEncho and its marker
internal/state/pools.go the GroupStamps CSV column; request-only fields never cached
internal/state/store.go, writable.go the startup write check (checkDataWritable, ErrDataNotWritable), run before the transaction log replay
internal/state/testdata/match_groups.json new: the group names, read by TestMatchGroups_SharedTable and match_groups_table.test.jsx
internal/state/transactions.go history methods on StoreTx
internal/**/*_test.go (55 files) new and updated Go tests: the merge rules, history and its crash atomicity (match_history_crash_test.go), lost-update windows, the refusals, the repair, the prose gates
web-mobile/check-helpers.mjs, check-imports.mjs, check-write-result.mjs the prose gate reads the comment-stripped whole file; admin_shell.jsx may import from write_result.jsx
web-mobile/css/styles.css the top-bar offline chip, the fighter list, the quiet "also waiting" line; .btn--confirm owns its text colour
web-mobile/js/__tests__/** (44 files) new and updated unit and render tests
web-mobile/js/admin.jsx the closing toast for a held or moved change
web-mobile/js/admin_schedule_score_editor.jsx a start names only the result group
web-mobile/js/admin_scoring_autosave.jsx the changed-groups hook; the kept-in-history note; each tap records the shown match's stamp
web-mobile/js/admin_scoring_engi.jsx, admin_scoring_individual.jsx the shared not-sent notice; Remove unlocks the board and sends clearWithdrawal; a discarded held write disarms Finish and the engi save (dropHeldWrite)
web-mobile/js/admin_scoring_shared.jsx the fighter list; RecordedWithdrawal with Remove and Undo; useWithdrawalRemoval; the held-write hooks and notice; a decision carries the stamp it was recorded against; comments on RecordedWithdrawal and the fighter list's focus brought up to date
web-mobile/js/admin_scoring_team.jsx Record bout returns its result and adopts the answer's stamp; the fighter list; Remove on a team match; a discarded held write disarms Finish and End match (dropHeldWrite)
web-mobile/js/admin_shell.jsx the top-bar held count, a button opening the held-writes list
web-mobile/js/admin_shiaijo.jsx the move confirm names the match; the "also waiting" line; refetch on reconnect and on returning to the foreground
web-mobile/js/api_client.jsx one queued entry per write, sent on its own, never dropped; the held-writes list and Discard; _stampAfterSeen on every stamped write
web-mobile/js/api_serializers.jsx forwards clearWithdrawal
web-mobile/js/app.jsx the "Reconnecting…" pill; the queue alert names the decision
web-mobile/js/barred_match_notice.jsx, ineligible_match.jsx "Record fusensho for "; the shared queued notice
web-mobile/js/match_groups.jsx new: the client group owner, pinned to the server's by the shared table
web-mobile/js/match_history_view.jsx new: the History disclosure (held overtime reads "overtime, started" or "not started")
web-mobile/js/match_scoreboard.jsx, viewer_standings.jsx the circle's tooltip and the standings legend name the decisions
web-mobile/js/viewer_utils.jsx "Match N of M" reads the bout's number from its id
web-mobile/js/write_result.jsx the queued, refused, held, needs-a-winner, moved-to-history and decision-hold copy; decisionWord

Screenshots

bc-rfsw: correction refused, running final named

bc-rfsw: correction refused, running final named

bc-offl: finished offline, the top bar shows the held result

bc-offl: finished offline, the top bar shows the held result

bc-offl: the reconnecting pill while a write is held

bc-offl: reconnecting pill

bc-offl: the sent toast, once the held result lands and the court moves on

bc-offl: sent toast

bc-rboq: a kachinuki bout held offline shows Not sent yet

bc-rboq: kachinuki bout held offline shows Not sent yet

bc-flst: the fighter list, and the list reopened by a focused name box

bc-flst: fighter list

bc-flst: list reopened by a focused name box

bc-flst: a typed name committed by tapping an ippon button scores it (bout 1); a double tap on a list option picks once (bout 2, Fusensho untouched)

Typed name and double tap

bc-kheb: a tied bout fought on in encho and decided: (E) on the bout row, none on the encounter

bc-kheb: encho on the bout, not the encounter

The armed End match is readable (it was dark text on navy)

Armed End match

Remove: the recorded kiken offers both fixes, Clear kiken and reopen and Remove kiken (Aoki withdrew; court A is scoring Mori vs Sato meanwhile)

Remove offered

Removed: the circles gone, the real result entered (Sato K, Aoki M K), a reason asked as for any correction

Real result entered

Saved: the match reads K vs MK, Aoki's next match can start, Mori vs Sato was never interrupted

After the fix

While the removal is pending, the name carries no Kiken mark (Undo brings it back)

No Kiken mark while removed

Team match: removed, bouts 2 and 3 become rows to fill (Save refuses until they have a result)

Team removal

Team match saved: Kyoto wins 2-1 on bouts and can compete again

Team after

Buttons name the recorded decision

Decision named buttons

Shared court: the move confirm names the match

Move confirm

Shared court: court B, open in another tab, names the moved match about 0.3 s after the move, with no reload, and stays on Teams

Also waiting

Shared court: one tap switches court B to Individual, the moved match is Up next

Switched

Merge: a kiken made offline before a point recorded on another device. The point stands, the match keeps running, the kiken is kept in the history and nobody is barred

Device A after reconnecting

The match history, oldest change first

Merge: a late point that would leave a finished knockout tied. The match keeps its finish, the point is kept in the history, and the operator is told it needs a winner (the Scores tab's editor, which stays open; on the court console the editor closes when the match finishes and the same message comes as an alert)

Needs a winner

Merge: a court offline ticked overtime after another device recorded a kiken. Back online, the overtime is held and the court is told which decision closed the match and where to change it; the history records "a kiken closed this match" (the Scores tab's editor, as above)

Decision hold notice

Kachinuki Record bout, then the next tap: the winner stays on against Red's next fighter, and the first point on the new bout saves cleanly ("Synced", no held note)

Kachinuki Record bout and next tap

Gaps: a court console back online moved on by itself (Match 1 had been started on another device)

Court console after reconnecting

Gaps: the held-writes list from the top bar, with Discard for a write the server keeps refusing

Held on this device

Gaps: the editor's banner once the server keeps refusing a held result

Not saved yet: the server keeps refusing it

Gaps: the first bout to be played is Match 1 on the court console and in the scores list

Court console: Match 1 of 6

Scores list: League table · Match 1

Gaps: a result finished while the browser's storage is full

Keep this page open

Review history

Every finding was checked against the code before anything changed. Each fix comes with a test that fails without it, except where noted.

  • Code review 1. A stale queued decision is answered superseded, before any of its refusals. A typed fighter name committed by tapping an ippon button keeps that tap. Go lists three or more matches "A, B and C", as the SPA does. The encho repair retries after a read failure, and a new kachinuki competition starts with its marker. (The docs sweep has no test.)
  • Code review 2, Remove and shared courts. The court console no longer switches competition by itself. Every competition waiting on a court is named. Undo of a removal disarms a hantei armed during it. A tied knockout team match after Remove points to Clear and reopen. A correction held offline offers no Undo.
  • Code review 3, the merge work and the owner's threads. R4 revised: a change that would leave a finished knockout without a winner is held, never sent back to running. Both arrival orders end in one state (displacedGroups). A write that does not name the result keeps a hantei mark. The queue holds nothing behind a failing write. Request-only fields never reach the cached copy. CodeQL's allocation alert fixed.
  • Code review 4, the gaps and the owner's threads. A second failing held write is published when it crosses the threshold. An editor's banner goes once nothing is held for its match. A hand-set winner resent after a clock refusal keeps its floor. A team match decided by its representative bout alone no longer reads tied.
  • Code review 5. The kachinuki advance hands back the stamps it left and reads the match again rather than standing down. A verdict sent again records no new status, so a reinstatement stands. Points clear only a withdrawal of the match itself; a fusensho stands. A damaged history line is logged.
  • Code review 6. An engi finish echoing the stored count applies. An echo keeps its stored stamp. A finish is kept together with the scoreline it stood on. The decision hold has its own answer and notice. A replayed decision keeps its court. The advance, the hand-set winner and the requeue append history in the same transaction as their write (their tests pin the entry; crash atomicity of the shared transaction path is pinned by the crash test added for the docs review's gaps). A line the history scanner cannot read past is logged.
  • Review loop, rounds 1 to 4. A group the writer names takes the write's stamp. No stamper lowers a stamp, and the engi recorder writes the stamps the merge decided. Overtime from a court still scoring a match a decision closed is held with its points. The held notice names no button the kachinuki editor lacks. The engi ruling (the newest count decides). The three prose gates and their shared case table. The loop stopped at the owner's direction once only gate wording and docs truth remained.
  • Docs review. The history reasons listed in full in the data model and the OpenAPI spec; the held notices quoted as the editor shows them; the runningMatches body on every 409 that returns it; a table cross-reference by name; this body rebuilt with the upgrade and rollback notes and every changed file. (Docs only, no test.)
  • The docs review's open gaps. A crash test for the history append (match_history_crash_test.go): a real transaction whose file apply fails on the match file or on the history file, a crash before the apply and one between the two files, each replayed on restart with both landing, and an abort that leaves neither; it fails when AppendMatchHistory bypasses the transaction log. The OpenAPI reopen text said a force-reopened match loses its bouts and that a bye is refused (it keeps its points and the bye is unwound). Two stale code comments. The four untested items below were checked in a browser. Found on the way: the release packaging defect and the armed-button colour (the colour has a CSS test that fails without the fix; the release fix was checked with a goreleaser snapshot build, whose binary served all six UI assets, against the v2.1.0 release, where the new smoke step stops at Preact's 404). All PR screenshots were recaptured on this build.
  • Owner decisions, 2026-10-05. A discarded held result disarms the two-tap commit (Finish, Finish + Start Next, End match, the engi save), whichever door discarded it: each editor's one dropHeldWrite serves its own Discard held result and the edge a discard from the top bar produces. A render test covers both doors in all three editors and fails without the disarm; in the browser, after the server refused a finish 11 times and the operator discarded it, the button read "Finish + Start Next →" and the queue was empty. The held overtime line reads "Kept in history: overtime, started" rather than "overtime, overtime".

Dismissed with evidence: "re-parses the same files" (config, pool and bracket loads are served by the version and mtime cache); "the kept-in-history note can leak across matches" (every host keys the editor on its match).

Notes for review

  • Accepted residuals, each recorded in CLAUDE.md: a lineup save made while one is queued reads {queued: true} until the flush sends it; a finish that carries none of its own scoreline cannot be replayed under a newer tying change and is held; the history file is rewritten whole on each write to its match; two writes floored by the same seen stamp share it; a correction held whole when a court still scoring moved the result stamp first; a write the disk refused after its log committed is answered 500 and retried, and a Discard does not stop it applying at restart.
  • An echoed engi count's history outcome reads "applied" rather than "unchanged". The History view lists held groups only, so no operator sees the difference.
  • The four items earlier listed as untested are now covered: crash atomicity of the history append by match_history_crash_test.go; queued writes across a rollback in a browser (refer to Upgrade and rollback); the lineup panel's Save lineup with the fighter list in a browser (a click commits, a double tap places one fighter, a focused box reopens the list, and the saved lineup carries each member's id); and the Record-bout floor with the page's server time set 2.5 s behind, where the next tap's own estimate was older than the appended bout and it went out at the bout's stamp plus 1 ms and applied.
  • A finish that did not land (queued) still leaves the two-tap button armed while the write is held, by design: a second tap re-sends into the same queued entry, and the banner offers Retry now. A discard disarms it (owner decision above).
  • bc-kheb: the repair writes config.md once per kachinuki competition on first startup. On a tied level bout X outranks (E), so only "Undo encho" shows the overtime until a point lands.
  • bc-rfsw: runningDownstreamRefusal may go if bc-dlck removes the T103 block. bc-offl's e2e fixme flip lives on PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442.
  • Seven commits name the term this PR removes: five from before the 2026-10-04 ruling (76ffb044, 37d4187e, 0ec2e3ea, 47d739da, 8b90315c) and two that quote it to say it was removed (aa81d5c7, 53f54164). Rewording them needs a force-push; a squash merge leaves none of them.

Test plan

  • make go/test passes (lint, security scan, tests), re-run after the gap fixes
  • New and updated unit tests cover the change
  • Manual browser verification of each bead: the knockout correction refused on a running final; finish offline, reconnect and toast on a court; kachinuki Record bout offline; the fighter list's click, double tap and reopen; kachinuki tied bout encho
  • Manual browser verification of Remove: individual (the court busy with another match, which kept running) and team (refused with empty bouts, then saved); the stored result, eligibility and wire body checked; the Kiken mark hidden while pending. Shared court checked with the receiving court open in a second tab: updated with no reload, no automatic switch, switched on a tap
  • Manual browser verification of the merge with two devices, one offline: a kiken made offline before a point on the other device is kept in the history and the match keeps running; points made offline after the other device finished apply on reconnect and the winner is worked out again; an offline Finish after an autosave loses nothing; a late tying point on a finished knockout is held with "needs a winner" and the match keeps its finish; the history lists every write in order
  • Browser checks of the gaps: a console offline while another device started its match moves on once back online (a control build without the fix stayed stale); a stuck autosave and a stuck finished result listed and discarded from the top-bar list, the editor's own Discard held result, the banner going with the write; the storage-full notice and alert, and the result landing on reconnect; with one device's clock 3 s ahead, the other device's point made 1 s after seeing it applied
  • Browser check of the numbering: a new league's first bout played is "Match 1 of 6" on the console and "League table · Match 1" in the scores list
  • Browser check of the decision hold, on a fresh build with two browser contexts: the editor, the queue alert and the history name the kiken; with the overtime tapped before the kiken it is held as a plain newer change; the box reads Clear kiken and reopen and Remove kiken; kachinuki Record bout then the next tap applies
  • Browser checks of the docs review's gaps: queued writes across a rollback to v2.1.0 (both the open page and a reload as the old client), the lineup panel's Save lineup with the fighter list, the Record-bout floor with the page's server time 2.5 s behind, and the armed End match readable (computed colour white on navy); after the owner's decisions, a discarded held result leaves Finish unarmed, and the held overtime line in the recaptured decision-hold screenshot reads "overtime, started"
  • Release packaging: a goreleaser snapshot build of this branch in a clean copy without the vendor files serves all six UI assets; the same build without the fix, like the v2.1.0 release, answers 404 for Preact, and the new smoke step stops there
  • The binary started on a data folder with a read-only competition folder refuses to start and names that folder (mobile-app --folder on a scratch folder)
  • Screenshots added above, all recaptured on this build
  • Docs updated under docs/ (make docs/build and the link check pass)
  • Docs screenshots recaptured with make docs/screenshots SINCE=main: the six editor shots gaining the History line and the two standings shots naming the decisions; the other 23 unchanged. Remove changes no docs screenshot: none shows the recorded-decision box. After the gap fixes, make docs/media SINCE=main again: all 31 screenshots unchanged (two needed a second run after a seeding timeout), and kachinuki-demo.webm recaptured because it shows the armed End match
  • No new console errors or warnings (the only one is the expected 409 from the bc-rfsw refusal)

Closes bc-rfsw
Closes bc-offl
Closes bc-rboq
Closes bc-flst
Closes bc-kheb
Closes bc-mrgc
Closes bc-hlck

🤖 Generated with Claude Code

gitrgoliveira and others added 9 commits October 2, 2026 22:34
…ued bout shows "Not sent yet"

Co-Authored-By: Claude Code <noreply@anthropic.com>
… the bounce

Co-Authored-By: Claude Code <noreply@anthropic.com>
…they land

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…er match

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ir stored legacy marks on load

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…i encho inside recordDecisionTx

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 22:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

A stale /decision replay is reported superseded instead of refused for a
running later match; the decision pre-check shares the score door's
winner-changed test and refusal tail. A typed fighter name committed by
tapping an ippon button no longer swallows that tap. Running and played
refusals list three or more matches "A, B and C" as the SPA does. The
kachinuki encho repair keeps its marker unset when a match file cannot be
read, and a new kachinuki competition starts with the marker set; the
decision write fails rather than silently miscounting when the competition
cannot be loaded.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 22:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

A reopen blocked by a running downstream knockout match showed "...then
reopen again." in the SPA while the server's message said "...then reopen
this match again." Both now say the latter.

The sentence is pinned by one table both languages load
(internal/engine/testdata/downstream_running_messages.json), covering the
save and reopen doors for one match and three, with and without a court,
in place of copies of the same literals in each language's tests, which
is how the two drifted. Stale comments naming the old wording are updated.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 07:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Operator ruling 2026-10-03: an operator can fix any mistake, but the fix
must leave the match resolved. A withdrawal or default win recorded by
mistake on a match fought to a result was only removable by Clear
withdrawal and reopen, which puts the match back to running, needs a free
court and leaves it unresolved until finished again.

The score editors' recorded-withdrawal box now also offers Remove
withdrawal (Remove default win). It takes the ruling off the board: the
default-win circles go, the withdrawer's struck points stay, and the
operator enters the real result, a win or a pool/league draw. Save
correction sends it with the request-only clearWithdrawal, which makes
engine.KeepsWithdrawalRuling replace the ruling instead of keeping it:
the match stays completed, never takes the court, and the competitor the
withdrawal barred is eligible again. A team match needs a result on every
bout, as on any finish; a knockout winner change goes through the usual
downstream checks. Undo puts the recorded ruling back before saving.

Clear withdrawal and reopen stays for a match with fighting left.
Kachinuki is not offered Remove withdrawal: a finished kachinuki
encounter has no Save correction.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 14:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

After Remove withdrawal the board and the box say the withdrawal is going,
but the header still showed "Kiken" beside the withdrawn competitor, two
answers on one screen. WithdrawalMarkedName takes the editor's pending
removal and drops the mark until Undo or the save.

Also pins a hantei decided after Remove: its own write (submitHantei)
carries clearWithdrawal like Save correction does.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 14:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

- useWithdrawalRemoval (admin_scoring_shared.jsx, beside useMatchReopen)
  owns Remove withdrawal for both editors: the state, one reset rule (a
  change of match id, decision or side ends a pending removal), the
  RecordedWithdrawal prop, the clearWithdrawal patch block and one
  rulingShown value the board, the summary and the name mark read. The
  team editor states the kachinuki exclusion once. On a reset the
  individual board re-seeds; the team editor keeps its bout edits.
- WithdrawalMarkedName takes rulingShown, so a surface that forgets it
  shows no mark rather than a stale one.
- The team editor's knockout tie block and its button label read one
  input: a removal whose bouts tie in a knockout reads "Needs a winner".
- RecordedWithdrawal works out one eligibility outcome for both buttons'
  copy and renders one consequence paragraph; a fusensho whose competitor
  is already eligible now says so after Remove too.
- The individual editor's completed writes share one tail.
- The shared refusal table gains the 3rd-place cases; the JS copies go.
- Tests reuse collectEvents and one subtest-name helper.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 14:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…onfirm names the match

Operator request 2026-10-03 (PR #453). Two gaps on the court console when a
match is moved to another shiaijo:

- The move confirm named one competitor ("Ito Rin leaves Shiaijo A"),
  which reads as a person moving. It now names the match, both
  competitors, through confirmMatchLabel, the naming Send back to queue
  already used.
- On the receiving court, the moved match was visible only through the
  competition picker while that court's own competition still had matches
  waiting. The court now names the other competition's waiting matches in
  a quiet neutral line ("1 Individual match also waiting on this court.
  Switch"), keeps the amber "Switch to" for when its own competition has
  none left, and still switches only on a tap (one operator per court).

Checked in the browser with the receiving court open in a second tab: the
line and the picker appear there about 0.5 s after the move, with no
reload.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 16:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Court console (admin_shiaijo.jsx):
- The console no longer switches competition by itself. The default only
  chooses the first competition shown; it is then held as if picked, so a
  bout finishing (or a match moved onto the court) cannot swap it. Only
  the picker and the banner change it (one operator per court, ruling
  2026-09-26). The amber "Switch to" is now reached through that flow,
  not by picking the finished competition by hand in the test.
- Every other competition waiting on the court is named, each line with
  its own switch, and each line's accessible name carries its text.

Editors:
- Undo of a removal puts back the recorded hantei verdict too, so a hantei
  armed on the tie the removal made no longer leaves every control
  disabled once the circles return.
- A tied knockout team match after Remove no longer offers a
  representative bout the server would refuse (the stored match still
  holds the withdrawal): the panel and the block's title point to Clear
  withdrawal and reopen, which puts the match back on the court.
- A correction held offline reads as saved on the device and offers no
  Undo, which could not recall it.

Docs and the ClearsWithdrawal comment follow.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 20:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

- The engi recorder writes the stamps the merge decided, so a finish that
  moved a newer invalid recount out leaves its flags at the finish's own
  time, and both arrival orders reach the same winner and stamps.
- Points that would clear a kiken or fusenpai but leave a knockout tied
  do not clear it: the decision stands and the points and overtime are
  held together under its reason, never "needs a winner".
- Overtime enters that rule only when it actually changes, so an echoed
  (E) is a no-op again instead of a false "newer change" answer, and a
  hold records a reason only when it held something.
- heldDecision travels only with its heldReason (one owner).
- The team editor's two hints name the real button for the recorded
  decision and no longer promise the court; "again" is said only of a
  fusensho; the queue alert compares decisions by their word; two docs
  sentences match each button state; the decision-hold message says
  "change", not "scoring"; the OpenAPI representative-bout answers list
  their held fields; the glossary no longer says a side wins by default.
- The three prose gates share one pattern pair, catch text wrapped
  across lines or split across literals, and pass identifiers.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 21:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

- Operator ruling 2026-10-04: for an engi match the newest count
  decides, in either arrival order. A finish whose result applies over
  a newer stored valid count closes the match with that count's winner;
  the finish's own count is kept in the history as held. A stale finish
  over a completed match stays held whole, now pinned by its own test.
- specs/openapi.yaml named the result of a withdrawal wrongly; it now
  names the decisions, and a gate reads the spec as well.
- The three prose gates share one pattern pair that also covers the
  past and participle forms, the other side of the same wording, text
  wrapped across a line, and HTML or emphasis split text in the docs,
  while passing identifiers and attribute runs.
- The kiken glossary entries say the withdrawer's points stand rather
  than a fixed score; two docs sentences name the fusenpai; the
  recording-decisions page describes each button state truthfully,
  including a competitor barred by a separate match.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 23:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

- A refused engi pool correction leaves no history entry: the entry is
  written after the requalification check, as every other door does.
- An engi write answers with the stamp the merge settled on, so the
  next tap's floor matches the stored match.
- The prose gates share three patterns and one case table read by the
  Go and JS tests: a phrase split across a line on either side is
  caught, the noun form is caught, and ordinary settings and connection
  prose passes. A git-tracked scan reads the person-read files outside
  docs/, and the wording it found in the tournament guide and the
  tracked spec inputs now names the decision.
- The reopen description in the spec, the recording-decisions page and
  CLAUDE.md match the code: the still-barred check applies to every
  kiken, fusenpai and fusensho, a reinstatement lifts it, and clearing
  the match that recorded a bar moves it onto a chained fusenpai.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 23:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

A docs review of this PR found the history-reason lists in the data
model and the OpenAPI spec named four of the six reasons the server
records, leaving out the late finish kept with its scoreline and a
kiken, fusenpai or fusensho that already closed the match.

The scoring guide quoted the short note where the editor shows the
full banner, and cut one note before its last sentence. The override
and decision 409s named runningMatches without describing that body.
A table cell pointed at "the next row".

All now match the code.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 08:40
@gitrgoliveira gitrgoliveira changed the title fix(bc-rfsw,bc-offl,bc-rboq,bc-flst,bc-kheb): running-match correction refusal, offline visibility, fighter list, kachinuki encho fix(bc-rfsw,bc-offl,bc-rboq,bc-flst,bc-kheb,bc-mrgc,bc-hlck): merge match writes by change, held results visible, correction refusals, kachinuki encho Oct 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The armed state of a two-tap button (.btn--confirm) set a navy fill
but no text colour, so kachinuki's End match, a plain .btn, showed
"Tap again" in dark ink on navy. The rule now owns its text colour.

Release builds embedded no Preact: web-mobile/vendor is gitignored and
only the Makefile's vendor-frontend target fetches it, which goreleaser
never ran, so every release archive and package served a blank UI. The
release hooks now run that target, and the package smoke test starts
the app and fetches its UI assets, so a missing one fails the release.

A crash test pins that a match write and its history line land
together through the transaction log, whichever file fails to apply.
The reopen's OpenAPI text said a force-reopened match loses its bouts
and that a bye is refused; it keeps its points and the bye is unwound.
The RecordedWithdrawal comment described a reason asked before a
reopen, which was removed on 2026-09-25. The kachinuki demo video is
recaptured with the readable armed button.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 10:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

A browser check found the comment's premise wrong for the team editor:
it disables the name box while a pick's lineup write is out, which
drops focus, so the next tap reopens the list through onFocus. The
onClick path is for a box that kept focus (after Escape, or in the
lineup panel, which keeps the box enabled).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 11:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Operator ruling 2026-10-05: after an operator discards a held result,
the two-tap commit goes back to its first tap, so a discarded result is
not one stray tap from being sent again. Each editor has one function
for "this device holds no write for the match", used by its own Discard
held result and by the edge a discard elsewhere (the top bar's list)
produces; it disarms Finish, Finish + Start Next, End match and the engi
save.

A held overtime change read "Kept in history: overtime, overtime"; it
now reads "overtime, started" or "overtime, not started".

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 11:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitrgoliveira
gitrgoliveira merged commit e26a349 into main Oct 5, 2026
13 checks passed
@gitrgoliveira
gitrgoliveira deleted the worktree-bridge-cse_015HAZRVnwzTHJEA71fKRqXm branch October 5, 2026 12:05
gitrgoliveira added a commit that referenced this pull request Oct 7, 2026
…nl's

Main now carries #447, #450 and #453. Their beads' fixmes were run unskipped
on the merged build:

- Plain tests now, passing as written: bc-kfup, bc-htcr, bc-plcl (individual),
  bc-fsnp, bc-dhas (team), bc-emsl, bc-kheb, bc-sync (kachinuki).
- bc-offl: the court header reads "Offline: 1 result not sent" while the
  Finish is queued (captured offline); the test looked for "Not saved yet".
- bc-sbq: #447 made Send back to queue keep the score, and the confirm now
  says "Any score entered is kept". The test asserts that, then starts the
  bout again and finds its point still on the board.
- bc-kpnl's two tests are removed: the panel they wait for was taken out by
  the 2026-09-26 ruling (recording a withdrawal changes only that match).

Follow-on edits:
- The two 800 ms waits that kept bc-dhas from erasing the daihyosen row are
  gone (J7 and bc-dhrp); bc-dhas's own test proves the fix.
- bc-fsnp waits for the sync pill to read Synced instead of 1.5 s: since
  bc-sync the pill reads Syncing from the first tap.
- J5 final plays on from the first unfought bout: bc-kheb, which runs before
  it in this serial file, records the final's bout 1.

Five fixmes remain, all on open beads: bc-otpl, bc-kosc, bc-crpn, bc-lprf,
bc-dhrp. make e2e: 28 passed, 5 skipped, EXIT 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitrgoliveira added a commit that referenced this pull request Oct 11, 2026
…, shared-court journeys

Replan revision 2 of PR #442 (operator decisions 2026-10-11).

Harness review against main (review only, then fixed):
- finishMatch/finishTeam judge the server's answer to the completing write
  (finishLanded): a refused or unanswered Finish fails instead of reading as
  landed.
- ensureRunning returns null only when the court has nothing startable and
  throws when a tapped Start ran nothing; its callers assert, not branch.
- Forms are found by label and role, pills read by aria-pressed, the lineup
  panel by its dialog role; stale comments about the app corrected.
- Same-side ippon taps are paced (tapOnSide); slotMarks reads locked slots.
- One owner each for the court-console and team-sheet selectors; dead exports
  removed; README layout, tap-guard and Finish notes corrected.

Console policy: a console error or uncaught page error in any page of any
context fails the test unless it is on CONSOLE_ALLOWLIST (fixtures/test.mjs),
each entry narrow and with its reason (deliberate offline steps; 409 refusals
the app shows as refused); warnings are recorded.

New journeys (seeded through the UI by fixtures/shared-court.mjs):
- shared-shiaijo.spec.mjs (bc-shcr): two competitions on one shiaijo, walked
  as the clumsy operator; asserts one bout at a time, no competition switch by
  itself, the advance staying in the scored competition, Send back keeping
  the score.
- moved-match.spec.mjs (bc-mvsj): a waiting match moved to another court seen
  live on that court's console (two devices), with #453's decisions asserted.
Suspected defects they found are recorded as audit rows, pending the
operator's rulings.

make e2e: 38 passed. make go/test: EXIT 0.

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.

3 participants