Repository navigation
feat: kachinuki advisory line and recorded bouts, 3rd-place labels, form labels, and the open #442 review beads - #467
Merged
gitrgoliveira merged 34 commits intoOct 11, 2026
Conversation
Recording decisions said a team encounter's marks appear on the bracket and in the exported workbook. By the 2026-09-24 rulings the bracket card keeps only the winner's tick and (DH) (no Ht for a representative bout decided by the judges, no encounter-level (E) in kachinuki), and the workbook carries each bout's marks on its row, Kiken/Fus. beside the victory count, (DH) in the summary centre, and the representative bout's Ht on the Representative Bouts sheet. State each mark where it actually lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(bc-shcr) Two competitions on one shiaijo: the competition picker appears only with two or more and keeps an explicit pick; the Switch to nudge names the other competition's count only when the picked one has nothing running or scheduled here; the Another bout is running alert lists the extra running bouts with their competition; Finish + Start Next and the start after a withdrawal or no-show stay in the scored match's competition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o their controls (bc-lbla) The shared competition fields named no control: FieldLabel had no htmlFor, NumberField/TextField gave their input no id, and PillGroup's label named nothing while its pills showed their state only through the is-active class. The create form's and settings tab's hand-written fields (Display name, Day, Start time, Knockout qualifiers, Assigned shiaijo), the sign-in Password field and the first-run tournament form copied the same pattern, so getByLabel found none of them and a screen reader announced unnamed boxes and plain buttons. - stable_id.jsx: useStableId hoisted out of glossary.jsx into an import-only leaf; glossary.jsx and competition_fields.jsx import it. - competition_fields.jsx: FieldLabel takes htmlFor; NumberField and TextField mint an id; PillGroup's label gets an id and its row role="group" + aria-labelledby; PillButton is the one owner of a pill's is-active class and aria-pressed, both read from the same `active` (the isActive predicate, so a stored "" team match format announces Regular). - The create form and settings tab: static create-/settings- ids for Display name, Day (both the select and the date picker) and Start time; the two sanctioned hand-written pill rows become named groups of PillButtons. - app.jsx: the sign-in label points at #admin-password; the first-run fields get first-run-* ids; Tournament type names its group by its label. - The docs capture recipe finds the field by getByLabel and the pill inside its named group, checking the click through aria-pressed. - check-imports.mjs now checks competition_fields.jsx and glossary.jsx. No visible change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c-lbla sweep) The same unassociated-label pattern outside the competition-config screens: - AdminEditTournament (edit-* ids): every input; the contact rows' inputs and remove button get row-numbered names; the destructive-ops label names the new-password box and the rotation's current-password box names itself. - reset.jsx, registration.jsx, admin_schedule_page.jsx's auto-schedule, admin_branding.jsx and admin_sponsors.jsx: static ids (each renders once). - admin_announcement.jsx: the composer renders in two places, so its message and duration ids are minted with useStableId; the duration radios are a radiogroup named by their label. - admin_participants.jsx: the add and replace forms' div captions name their inputs through aria-labelledby (a label element would change the layout); the source filter pills render through PillButton. - The Scores tab's status filter is a named group whose buttons carry aria-pressed. No visible change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…face - An unfought fixed-order bout, written by the running sheet after the first point, reads as unfought when that stored match is shown again: on the score sheet (no X, no Tie switched on), the public match card and the TV board (no X in any bout centre). A Tie tap on it marks a draw on screen and on the wire, and leaves the next bout unfought. The test goes from the sheet's own write to each surface, so reverting the write rule reddens the display assertions, not only the wire one. - The score sheet's "already at" refusal on a three-person team reads "Position N" and names the position the member holds (the later one). - The Lineups page on a five-person team names positions by their FIK name on both refusal paths (Save, add row), the add row naming the position the member holds, not the one typed into. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Later (bc-bzlb) The bronze match has no match number and empty sides until the semi-finals seat their losers, so the court console's Up next card and queue row named nothing after the competition, the queue dropped it until both semi-finals were scored, and its bracket card was blank. - Up next and queue rows name a knockout match through scoreRowMatchLabel (matchLabel), the owner the Scores list and the dialogs use, so the bronze reads "the 3rd-place match" (operator decision 2026-10-11, option a). - BRONZE_MATCH_ID is exported from write_result.jsx as the one JS spelling. - bronzeSlotLabels (bracket.jsx, beside the slot-text owner) names an empty bronze side "Loser of M<n>" by semi-final position, mirroring Go propagateBracketWinner; an unnumbered semi-final reads "TBD". The admin and public 3rd-place cards and the Later row pass it; other cards are unchanged. - isPendingBracketMatch takes the competition's rounds: the bronze is pending while an empty side's semi-final is unplayed. No "Run now" on it. - Docs: the Later section says the 3rd-place match waits there. No storage or Go change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling 2026-09-24: an advisory line above the live kachinuki bout names each side's fighter on, how many fighters its lineup has left and who comes next, every fighter with their team member number (operator decision 2026-10-11), e.g. "Shiro: T1.2 Ueda on, 1 left (T1.3 Sato) · Aka: T2.1 Kudo on, last fighter". The queue is the engine's: new Engine.KachinukiRoster reads through kachinukiRemainingRosterOf, the very call MaybeAdvanceKachinuki appends the next pairing from, so the line never names a different next fighter. kachinukiRemainingRoster now reports a lineup found per side; a side with no lineup in force reads no fighters (the bout log cannot count them). Served by the public GET /api/competitions/:id/matches/:mid/kachinuki-roster (public in self-run too, since the public score sheet mounts the same editor; pinned), on the LineupEngine interface, documented in openapi. Client: API.fetchKachinukiRoster (bounded, never throws), a leaf model (kachinuki_advisory.jsx) and one muted line in the team editor, keyed on the bout log and lineups as values. Advisory only: it never ends the match, arms End match or holds back Record bout or Encho, and never asks for a lineup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-kfnl) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1+6. The server splits each side's fighter on from its queue. The roster
answer per side is {lineupFound, on: {name, memberId} | null,
remaining}, where remaining never holds the fighter on (next first).
The fighter on is the last numbered bout row's; it is taken out of the
queue by adding it to the retired set and filtering through
filterRemainingFighters (IsMemberRetired), so remaining is exactly the
advance's remaining list once the fighter on loses. The advance and the
read share kachinukiRosterQueues (one queue builder) and
RetiredPlayersFromBoutLog. The client only renders: sameFighter and the
onA/onB parameters are gone.
2. Record bout appends nothing at exhaustion, so the sheet records the
position it recorded (kachinukiRecorded) and the roster key carries it
once it reaches the live bout, with that bout's outcome; the read sends
recordedThrough and the server then drops a fighter the recorded bout
retired from `on`. Live scoring taps still read nothing.
3. A lineup in force that fields nobody (the migrated empty own lineup)
reads lineupFound:false. The advance is unchanged: it still treats such
a lineup as found, i.e. that side as exhausted.
4. The roster read loads lineups through lineupRuleOver, which returns the
lineups.yaml error (500); lineupRuleOrNone keeps logging for the advance.
5. The first roster read waits until both lineup reads have settled
(useMatchLineups' lineupsSettled), so a mount reads once.
7. bronzeSlotLabels is memoised in both bracket views (the admin view
passes its existing display model's numbers).
9. ErrTeamMatchNotFound is gone; errMatchNotFound is exported as
ErrMatchNotFound and used by KachinukiRoster.
Tests: engine (split, id-less fighter on, recorded bout, empty lineup, no
bout yet, unreadable lineups, advance pin before and after the append),
handler (on, recordedThrough, 400), JS model and render (one read on
mount, exhaustion re-read), bronzeSlotLabels numbers argument. openapi
documents on, recordedThrough and the 500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The advisory line printed "0 left" for a team whose last fighter a recorded bout took off; say it in words, and document it beside the example. CLAUDE.md follows the rename of errMatchNotFound to ErrMatchNotFound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A (findings 2, 3, 4, 7). Whether the live kachinuki bout is over is now a server rule every reader shares, not one editor's state. The running autosave already writes a lead's winner (and a 1-1's hikiwake) on the live row, and Record bout appends nothing at exhaustion, so nothing stored tells a recorded bout from one being fought. engine.KachinukiRoster therefore reads a result the last numbered bout row carries as the advance does (RetiredPlayersFromBoutLog over that row: attribution id first, state.IsDraw): the loser is off, the winner stays on, a draw takes both off. Removed: the recordedThrough query parameter (handler, deps interface, openapi), the client's kachinukiRecorded state and the Record bout handler's lastNumberedPos. kachinukiRosterKey now returns a string keyed on every bout's fighters and result (winner, winner member id, decision), the live bout's included, so a result entered, changed or recorded re-reads and a point that keeps the result does not. The advance-consistency pin also checks the decided row before the advance. B (finding 1). The roster read keys on each side's lineup as the SERVER last answered it (useMatchLineups' servedLineupsKey: a read, or a save whose answer landed), not on the lineup shown, so a save queued offline reads nothing until it lands and is announced, then once. C (finding 5). admin_shiaijo's bronzeLabelsFor guards window.bronzeSlotLabels as slotLabelFor guards window.bracketSlotLabeller. D (finding 6). bronze_feeders.jsx (a leaf) owns which semi-final feeds which bronze side; isPendingBracketMatch and bronzeSlotLabels both ask it. E (finding 8). Giving a side another team clears lineupsSettled until the new team's read settles (a start generation keeps an older start from settling it early), so a reseat reads the roster once. Tests: engine (result on the last row: winner, draw, none; same-name by id; decided-row pin), handler, JS key model, render (stored exhaustion on mount, on Record bout and on a fresh mount; result vs point; lineup announcements incl. a queued save; reseat reads once; bronze row without the bracket module), bronzeFeederSemis. Docs: the line reads the current bout as it stands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator decision (option C, after tablet mockups): round 2 read a result on the last numbered bout as final, but the running autosave writes a winner at a 1-0 lead and a hikiwake at 1-1, so the advisory line counted a fighter off mid-bout; and at exhaustion Record bout appended nothing, so nothing stored said the bout had been recorded. Record bout now marks the bout recorded on the server. - state.SubMatchResult.Recorded (json/yaml "recorded", omitempty): stored in pool-matches.csv's SubResults JSON column and in bracket.json. Absent on old rows, which read as not recorded; no migration. It is part of the row's bout:<position> group (the group projects the whole row). - engine.advanceKachinukiOnce sets it on the bout Record bout closes, in the same locked write as the appended pairing, or in a write of its own when no pairing is appended (a side has nobody left), stamping that bout's group and recording a history line (new door "kachinuki-record", worded "Kachinuki bout recorded"). A bout already recorded with nothing to append writes nothing. MaybeAdvanceKachinuki returns a non-nil KachinukiAdvance whenever it wrote; the score handler echoes it and broadcasts match_updated whenever it is non-nil, so the answer carries the mark at exhaustion too. - engine.carryRecordedBouts is the one owner of the field on a write: every incoming row takes the stored row's value, so a write that omits it keeps it and a client cannot set it. Called once, by mergeMatchWrite (every forward write, both branches, every door). noteServerBoutChanges runs before it on uncarried rows, so leaving the flag out never names a bout. The client's group comparison (match_groups.jsx groupKey) leaves the mark out too, so the first save after Record bout does not name the recorded bout and move its stamp. - What clears it: RemoveTrailingKachinukiBout, which undoes the Record bout that appended the pairing, takes the mark off the bout before it (unrecordLastBout), stamping its group in the same write. A reopen keeps the bout log and the mark. - engine.KachinukiRoster: every bout before the last retires by its result; the last retires by its result only when recorded, otherwise both its fighters are on whatever its score. The taisho rule is the advance's (a draw leaving a side with nobody appends a pairing keeping that fighter). - Client: kachinukiRosterKey keys on each row's fighters and recorded mark, and on the result of every row but an unrecorded last one, so a live point, lead or 1-1 reads nothing and Record bout reads once. The team sheet echoes a stored recorded mark on the rows it sends (storedBoutRecorded). - Docs: team-tournaments.md says the line names the fighters of the bout being fought and what Record bout and Remove this bout do to it; openapi documents SubMatchResult.recorded and the roster rule. Tests: engine (live last bout with a lead and with 1-1 keeps both on; recorded win at exhaustion on pool and bracket, idempotent replay; recorded draw with both sides out; recorded draw keeping an exhausted side's last fighter; Remove un-records; a write neither clears nor sets the mark and names no extra bout group; the advance-consistency pin), state round-trip, handler (the answer carries recorded, at exhaustion and on append; an autosave naming bout 3 names nothing else), JS key, changedGroups and storedBoutRecorded, render (a point, lead or 1-1 reads nothing; Record bout at exhaustion reads once and shows "no fighters left"; a fresh mount shows the same; the wire echoes the mark). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
15 tasks done
Root cause: the team sheet's footer End hint is chosen from kachinukiEndOutcome, which deriveKachinukiEndOutcome works out from the LAST SCORED bout (buildKachinukiEndEntries keeps played bouts only). After Record bout records a tie and the server appends the next pairing (any recorded hikiwake, seen after a tie that left Aka's taisho on), the live bout is the fresh, unscored pairing but the last scored bout is still the tie, so the outcome stays "draw" and the unscored bout read "Tied bout. End match records a drawn encounter. Record bout retires both and brings the next pair up." Only the Encho clause had been gated on the live bout being the judged one; the "Score this bout" sibling was gated to a "win" outcome. The server is not involved: the appended row carries no decision. The same lines are on main, so main reproduces it; bc-kfnl's changes are not involved. Fix: one predicate, kachinukiLiveBoutAwaitsScore (a live bout exists and is unscored, and something has been scored before it). While it holds the End hint is not shown and the footer shows the Score-this-bout hint for a draw as well as a win; in a knockout, where End match is disabled after a tie, it reads "Score this bout to enable Record bout. No draws in a knockout: End match needs a bout with a point." A tie on the live bout itself still reads "Tied bout", and a sheet with nothing scored keeps "Nothing recorded yet". Tests: kachinuki_unscored_bout_hint.render.test.jsx (pool and knockout after a recorded tie, and a live tie), red before the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ows (bc-kbrc)
Root cause: kachinukiVisiblePositions picks the first unplayed bout as the
live one, else the LAST bout. When Record bout records a bout and appends no
pairing (a side has nobody left), every bout is played, so the bout just
recorded stayed the live bout with every scoring button on, exactly as it
looked before Record bout; kachinukiDoneBoutIdxs excludes the live bout, so
it never joined the read-only rows. Nothing on screen said it was recorded.
Fix: kachinukiVisiblePositions takes closeRecordedLast and returns no live
bout when every bout is played and the last one carries the server's stored
recorded mark (bc-kfnl), so it comes from storage and a reload or a second
device shows the same. The bout then renders through the existing read-only
row and its tap to correct, like any earlier bout. It closes only while End
match can finish the encounter: a recorded knockout tie stays live, since
End is held back there and Encho on that bout is the way on. A bout added
with "+ Add next bout manually" carries no mark, so it is live again.
With no live bout (kachinukiNoLiveBout): the line in the advisory's place
reads, from the roster the advisory already reads, "Aka has no fighters
left. End match finishes the encounter." ("Shiro has ...", "Shiro and Aka
have no fighters left. ..."), or "No next pairing. End match finishes the
encounter." when no lineup says who is out (kachinukiNoPairingText, beside
the advisory). Muted advisory style, no warn colour. Record bout stays
disabled (it would record the same bout again; title "Bout recorded: no
next pairing"), "+ Add next bout manually" is enabled, "x Remove this bout"
is not offered (isKachinukiBoutRemovable takes hasLiveBout), and neither
footer hint shows, so a recorded draw never reads "Tied bout". End match and
the pinned footer are unchanged. kachinukiEndOutcome moves above the rows
it now gates. The self-run public sheet mounts the same editor.
A correction made through the tap can change the bout's result but does not
run the advance again (Record bout stays disabled); if a team then has a
fighter left, the operator adds the pairing by hand.
Tests: kachinuki_recorded_last_bout.render.test.jsx (read-only row, line,
buttons; correction tap; manual add; no roster; recorded draw; self-run;
knockout tie stays live; unrecorded stays live), five red before the fix;
the advisory render test now expects the line in place of the advisory
after Record bout at exhaustion; unit tests for kachinukiVisiblePositions,
isKachinukiBoutRemovable and kachinukiNoPairingText. Docs: team-tournaments
"Scoring a kachinuki encounter".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as nobody left (bc-kbrc) Coordinator decision 2026-10-11: the closing must not change competitions without lineups. Without a lineup the server builds the loser's queue from the bout log alone, so after every win it appends no pairing; the previous commit then closed the bout after every Record bout there and showed "No next pairing. End match finishes the encounter.", where main kept the bout live. Now the recorded last bout closes (read-only row, the no-pairing line, Record bout disabled, Remove and Encho hidden) only when the kachinuki roster read for the bout log on screen says a side's lineup has nobody left: lineupFound, no fighter on (the recorded bout retired them) and an empty remaining. kachinukiNoPairingText returns null otherwise, and the generic wording is gone. With no lineup, a failed read, or before the read lands, the bout stays live exactly as on main. A fresh mount therefore renders the bout live until the read lands and then closes it; it never shows the closed state first and reopens it. The roster read moves above the rows it now gates, and its comment says it has this second reader. Tests: no roster and a roster without lineups both stay live with Record bout enabled; a fresh mount stays live until a deferred roster read lands, then closes; the knockout-tie and unrecorded cases now run with a roster that names a side out; the self-run case mounts with the props the public page passes (no password, selfReport, team members from the payload) and reads no members. Docs updated to say the closing needs a lineup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1N8XTrYTRShcdZL8WfsRv7k
…cdZL8WfsRv7k' into worktree-bridge-cse_01N8XTrYTRShcdZL8WfsRv7k
1. Remove this bout un-records the bout before it only when the removal undoes a Record bout: the advance's own decision (recordBoutWouldAppend, over kachinukiRemainingRoster + AdvanceKachinuki, with the lineup rule read through the transaction) asked of the log without the removed row would have appended that pairing. A bout added by hand after a side had nobody left leaves the recorded bout recorded and the sheet closed. 2. The team sheet's closed state keeps the last roster answer that landed for the same recorded last bout while a newer read is out or after one fails (stale-while-revalidate); only a landed read with another answer changes it. The advisory line still shows only the read for the key on screen. A bout un-recorded or removed, another match, or a new live row is decided from the stored log at once. 3. The recorded mark takes no stamp: Record bout (and its undo) no longer advance the closed bout's bout:N stamp, so an earlier change to that bout arriving after it applies. The appended pairing and result keep their stamps. Stated where the mark is set and in carryRecordedBouts. 4. carryRecordedBouts clears the mark on a write that starts or extends encho on a recorded bout (the bout is fought again). The roster read keeps both fighters of a recorded knockout tie on, so the line never reads "no fighters left" for both while Encho is the way on. 5. models.go names engine.carryRecordedBouts. 6. The kachinuki-roster handler drops its own config.md pre-read; the engine answers an unknown competition with a NotFoundError, mapped to 404. 7. The public bracket builds the display model once and passes its match numbers to both the slot labeller and bronzeSlotLabels. Docs: the team-tournaments page says what Remove does after a manual bout and that a recorded knockout tie names both fighters on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. A recorded draw that is still the last numbered bout (the advance appended nothing, so both sides had nobody left) keeps both fighters on in EVERY phase, not only in a knockout: engine.KachinukiRoster drops the bracket-only exemption, so the bout stays live with Encho offered (the operator's call on any bout in any phase) and End match decides the rest. The team sheet never closes a recorded draw (kachinukiRecordedBoutCloses), whatever a kept roster answer says. Engine tests (pool and bracket) and the recorded-last-bout render test now pin this for both phases; the team-tournaments page says so. 3. RemoveTrailingKachinukiBout decides whether Remove undoes a Record bout from the stored fact, the match history read through the transaction (boutAppendedByRecordBout): the removed row's creator is the first line that applied its bout group since the row's last removal or the match's last reopen, and an advance line creates only its trailing bout group. It un-records the previous bout only when that creator is kachinuki-advance. No history, or no creator in it, keeps the recorded bout recorded. recordBoutWouldAppend, its lineups read inside the transaction and its AdvanceKachinuki call (and their advance logs on a removal) are gone. Tests on pool and bracket: a manual add after exhaustion then a lineup edit keeps the mark; an appended pairing then a lineup edit clears it; no history keeps it; a reused position reads its current row. 4. When AdvanceKachinuki cannot attribute the bout's winner it now says so (AdvanceKachinukiResult.OutcomeUnread), and Record bout marks nothing and writes no history line. 7. specs/openapi.yaml KachinukiRosterSide.on describes the every-phase rule. 8. kachinuki_advisory.jsx gains lastNumberedBout (the twin of Go's lastNumberedBout); the roster key, the recorded-last key and the closing check in kachinukiVisiblePositions all ask it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. A Record bout that only marks its bout recorded moves no stamp, so a
refetch read before the mark answered at an equal stamp and reopened the
bout. The team editor now holds the answer's mark in its { match, at }
override (recordedPos, recordedHoldStands) until the match carries a
strictly newer stamp, the match changes or the bout is gone; a Remove
replaces the override. While held the sheet shows the live prop with the
mark (the answer only while the prop is an older running copy). Render
tests: an equal-stamp unmarked copy after the answer keeps the bout
closed; a strictly newer one reopens it. The advisory-line test now sets
the post-record roster before the tap, since the mark shows at once.
2. CLAUDE.md: the recorded mark is the one change outside the merge that
takes no stamp (carryRecordedBouts owns it), with the client hold; the
Team Lineups & Kachinuki bullet names the mark, what sets and clears it,
and what the advisory line and the closed bout read from it. AGENTS.md
carries neither section.
3. advanceKachinukiOnce's doc: `advanced` is whether a pairing was
appended; post is non-nil whenever anything was written, a recorded-only
write included.
4. openapi MatchHistoryEntry.door lists kachinuki-record.
5. RemoveTrailingKachinukiBout reads the match history only when the bout
left last after the removal is recorded.
6. check-imports covers admin_announcement.jsx, admin_helpers.jsx and
kachinuki_advisory.jsx.
7. kachinukiRecordedBoutCloses tests a draw through isHikiwake
(api_serializers.jsx, already in the team editor's module graph).
8. Go lastNumberedBout now picks the highest positive position, first of
equals, as the JS one does; out-of-order tests in both languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view round 6, PR #467) Review rounds 5 and 6 kept finding races caused by the recorded mark moving no stamp: an equal-stamp refetch on another device (a second tablet, the public self-run sheet) dropped the mark, and the editor's own hold could outlive a server clear. The mark now lives in a server-only scalar group, `recorded` (state.ServerGroups, the shared Go/JS table and match_groups.jsx), separate from bout:N, so a point on bout N and the mark never fence each other. The advance, Remove's un-record and the encho-rise clear stamp it like every other group, so ModifiedAt moves and every device orders the marked copy after the unmarked one. A client can neither set nor clear it, and naming it in `changed` is refused. The editor's per-device hold (recordedHoldStands / withRecordedMark) is gone. Also from round 6: - openapi: the roster description states the recorded-draw rule the code applies, and `recorded` says what sets and clears it. - the roster handler answers an unknown competition or match with the engine's own not-found wording; ErrMatchNotFound is no longer reused as the public not-found. - the roster's lineup read uses ReadTeamLineups (no deep copy). - kachinukiRosterKey keeps numbered rows by lastNumberedBout's rule. Finishes work an implementer left uncommitted when it was stopped; Go state/engine/mobileapp, the whole JS unit and render suites, lint and the import check pass on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- state: setRecordedPositions clears a recorded mark on any row that is not a numbered bout, so a client-sent mark on the representative bout no longer survives the merge (test fails on the old code). - CLAUDE.md and two handler comments named ErrMatchNotFound, which round 6 un-exported again; they name errMatchNotFound. - changedGroupsError lists the valid groups from state.ScalarGroups rather than a hand-kept list that had lost repPickA and repPickB. - The announcement composer names its duration radios with its own minted id, so two mounted composers no longer share one radio group (test fails on the old code). - A stale test comment on kachinukiRemainingRoster's returns. - Docs: the closed recorded bout shows the same on another device once the lineups are read; the line never ends the encounter (it no longer claims to hold no button back, since the closed bout greys Record bout). Not changed (moderate, recorded on the PR): whether the recorded bout closes depends on the roster read, so a device whose read fails keeps it live, as on main; a migrated empty own lineup is read as exhausted by the advance but as no lineup by the roster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finding 2 (major): a recorded bout whose result a write changes is no
longer the bout Record bout closed. The merge's unrecordRaisedEncho is
renamed unrecordReopenedBouts and also takes the mark off a recorded
numbered bout whose applied bout group changes its result
(boutResultChanged: the winner side through state.SubBoutWinnerSide, ids
first, or the decision, every draw read as one and "fought" as none),
stamped in the recorded group and named in the history line exactly as
the encho clear. A waza correction with the same winner keeps the mark;
a held bout group changes nothing; a write older than the mark keeps it.
A recorded tie with both sides out scored to 1-0 now stays live with
both fighters on. Engine tests on pool and bracket, a table test for
boutResultChanged, and a render test that the scored tie does not close.
Finding 1 (moderate): boutAppendedByRecordBout counts an advance line as
the removed row's creator only when that same line applied the recorded
group (it set the mark). An advance that appended after a bout already
recorded no longer lets Remove clear an earlier Record bout's mark.
Unit and pool/bracket engine tests.
Finding 4 (minor): Record bout is disabled, titled "Bout recorded",
while the live bout is stored as recorded and the roster read says no
pairing can follow (kachinukiRosterHasNoNext: both lineups found with
nobody behind the fighter on). Without such a read it stays enabled,
since the advance may still append. A result change or encho clears the
mark (finding 2) and re-enables it. Unit and render tests.
Finding 5 (minor): bracket.jsx's makeSlotLabeller and bronzeSlotLabels
share feederMatchNumOf (the printed match number) and feederRefLabel
("<outcome> of M<n>" or "TBD"); slotDisplayName uses the latter too.
Docs: team-tournaments.md says Record bout is greyed out on a recorded
tie with both teams out, that a point scored on it makes it a bout being
fought again, and that correcting who won a recorded last bout makes it
the current bout again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…467) Round 8 renamed mergeHold.unrecordRaisedEncho to unrecordReopenedBouts and widened it to a change of the bout's result; Remove clears the mark only when the appending advance line set it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finding 1 (server invariant): a client can no longer set a kachinuki bout's recorded mark on any merge path. state.CopyGroup's bout case never moves the mark (a row the destination lacks arrives unmarked), and mergeMatchWrite re-applies the stored recorded group once every hold and S2 displacement has put its rows in place, before unrecordReopenedBouts. Pinned by a direct merge test on the displacement path (a finish whose own bout 2 row, sent with recorded: true, is brought back over a newer tombstone; mutation-verified) and a test that stored marks always land. Findings 3, 2 and 7: the roster read returns the advance's own verdict. decideKachinukiAdvance is the one Record bout decision (completed, no numbered bout, no outcome, no fighter, then AdvanceKachinuki over the lineup or bout-log queues), used by advanceKachinukiOnce and, quietly (AdvanceKachinukiInput.Quiet drops the breadcrumbs), by KachinukiRoster. The roster now answers nextPairing (a tap on the stored match would append a pairing) and, per side, exhausted (the loser of a RECORDED win whose lineup has nobody left; never after a draw, never without a lineup, never on an unrecorded bout). The sheet closes a recorded win's bout and words the no-pairing line from those flags only, and greys Record bout out on a recorded live bout whenever nextPairing is false, one side without a lineup included. kachinukiRosterHasNoNext and the queue derivation in kachinukiNoPairingText are gone; the fighter-known test is factored once (kachinukiFighterKnown in Go and JS). openapi, engine tests pinning the read against what MaybeAdvanceKachinuki then does (pool and bracket), a handler test, and JS unit and render tests. Finding 4: bronzeSlotLabels labels only a side still waiting on its semi-final (the feeding semi not completed, isPendingBracketMatch's condition); otherwise "", so the empty side renders as before. Tests for a completed semi, a completed bye semi and a played bronze. Finding 6: openapi splits WritableMergeGroup (the `changed` pattern, without `recorded`) from MergeGroup (stamp and history keys). The team sheet no longer echoes `recorded` on the rows it sends; groupKey still ignores it. Finding 8: PillButton merges a caller's className with radio-pill and is-active, and keeps type="button" and aria-pressed its own. Render test. Not fixed (coordinator decision): finding 5. Docs: team-tournaments.md says Record bout is greyed out on a recorded bout with no lineup to pair from, and comes back when a lineup saved later gives that team a fighter. CLAUDE.md's kachinuki sentence updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 9 moved the advance onto decideKachinukiAdvance, leaving kachinukiRemainingRoster with no production caller. Its four tests pin the queue-plus-retirement composition the advance still uses, so the helper moves to kachinuki_test.go and the production comments name the real owners (kachinukiRosterQueues, decideKachinukiAdvance). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. Record bout numbered the appended pairing len(SubResults)+1 on both branches while the live bout is the highest position. Both now take nextKachinukiBoutPosition (one past lastNumberedBout); the bracket branch stamps the appended bout's group by that position. Test: a gapped log [1,3] appends 4, pool and bracket. 2. RemoveTrailingKachinukiBout removed the slice-last row. It now removes the live bout (lastNumberedBout) and takes its history group from that row; the guards stay (only an unscored pairing, never the last bout, never a -1 row). Tests: an out-of-order log removes bout 2 and leaves its tombstone; a scored live bout stored first is refused. 3. Record bout's disable read kachinukiRosterForClose, which keeps an older answer after a failed re-read. It now reads only the answer for the current key (kachinukiRosterShown); comment fixed. Render test: a lineup save whose roster re-read fails re-enables Record bout. 4. unrecordReopenedBouts cleared the mark for an unstamped write (stamp 0 applies over anything). An unstamped write now never clears it. Test on both branches. 5. KachinukiRoster and decideKachinukiAdvance each built the queues and retirements. readKachinukiQueues builds them once (kachinukiQueues); decideKachinukiAdvance takes them, and the roster read passes the same reading. The advance's behaviour is unchanged. 8. unrecordLastBout repeated its only caller's check; it is removed and the caller takes the mark off the row it already found. 9. kachinuki_advisory.jsx: kachinukiAdvisorySide's doc now sits on it; kachinukiFighterKnown keeps its own. CLAUDE.md: the kachinuki paragraph names the live-bout rule, the unstamped exception and the current-read rule for greying Record bout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- team-tournaments.md: the kachinuki advisory paragraph is split into what the line reads and a list of the cases (a team with nobody left, a draw leaving one team empty, no lineup, a tie leaving both empty); Record bout pressed again before the lineups are read changes nothing; the Remove paragraph drops its implementation detail; the knockout-tie advice notes only Encho is left when both teams have nobody. - scoring-a-match.md: a 3rd-place side reads 'Loser of M<n>' only while it waits, and the row has no Run now (the final's Run now places the losers); the same knockout-tie note. - recording-decisions.md: the workbook summary centre carries X for a drawn encounter as well as (DH); 'court display', the docs' name for it; the Representative Bouts reference links its section. - openapi: MergeGroup states the result-change clear of the recorded mark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line (PR #467) make docs/media SINCE=main: kachinuki-scoring-buttons, kachinuki-correct-bout and kachinuki-knockout-tie-encho grew by the advisory line's height; the kachinuki demo shows the line too (chapter marks unchanged: 0:02, 0:08, 0:15, 0:21). Every other capture is unchanged. The scoring page's alt text for the shared scoring shot names the line as team-tournaments.md does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitrgoliveira
marked this pull request as ready for review
October 11, 2026 17:16
- team-tournaments.md: the read-only rows sentence moves up to follow the Record bout / End match list; the Remove paragraph keeps the removal cases only, and its fallback names when it happens (the match's history does not show how the removed bout was added). - team-tournaments.md: the "nobody left" bullet loses a sentence; "No lineup saved" names the losing team's lineup; a both-exhausted tie is fought again by a point or by starting Encho; the knockout tie line says Record bout adds no pairing and points to Encho or Add next bout manually; the knockout-tie screenshot alt text carries the advisory line. - scoring-a-match.md: same knockout tie wording; the 3rd-place Run now links its section and names the final's court. - recording-decisions.md: where an encounter's Kiken/Fus. marks show, including Fus. beside the winner of a match-level fusensho. - openapi.yaml: SubMatchResult.recorded no longer says the bout being fought never carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- RemoveTrailingKachinukiBout: the never-remove-the-last-bout floor counts numbered bouts only, so a [bout 1, -1] log is refused as on main; pinned on the pool and bracket branches. - admin_shiaijo.jsx: one display model per competition feeds both the slot labeller and bronzeSlotLabels, instead of each building its own. - enchoPeriodsRise moves beside its only caller, mergeHold.unrecordReopenedBouts, in match_merge.go (pure move). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitrgoliveira
deleted the
worktree-bridge-cse_01N8XTrYTRShcdZL8WfsRv7k
branch
October 11, 2026 18:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR #442's open review beads, plus two found while building this PR.
Kachinuki advisory line (bc-kfnl). In bout mode, a line above the current bout names each team's fighter on, how many fighters its lineup has left and who comes next, for example
Shiro: K2.5 Ito on, last fighter · Aka: K1.5 Nomura on, last fighter. Every fighter carries their team-member number. The line readsGET /api/competitions/:id/matches/:mid/kachinuki-roster, which returns the advance's own verdict (the same decision Record bout makes, asked without writing), so the line and the next pairing cannot disagree. It is a guide only: it never ends the encounter.recorded, stamped like every other group, so every device orders it and a client can never set or clear it. Remove, an overtime period added, or a change of result takes it off.A recorded bout reads as recorded (bc-kbrc, found here). When Record bout takes a team's last fighter (known from its lineup), the bout joins the read-only rows (correctable with ▶ as before), Record bout greys out, and one line says "Aka has no fighters left. End match finishes the encounter." A reload or another device shows the same. Before, the bout stayed open with every button live and nothing showed it had been recorded.
An unscored kachinuki bout no longer reads "Tied bout" (bc-ktih, found here; also on main). After any recorded draw followed by a pairing, the footer read "Tied bout. End match records a drawn encounter…" over a bout with no points. It now reads "Score this bout to enable Record bout…".
3rd-place match named and listed (bc-bzlb). The court console's Up next card and queue row read "… · the 3rd-place match" (the owner's words,
scoreRowMatchLabel). Before the semi-finals the bronze sits under Later with "Loser of M" on a side still waiting, and with no Run now. The admin and public 3rd Place cards show the same. Display only.Form labels and toggle pills (bc-lbla). Every operator form field is named by its visible label, and toggle pills report
aria-pressedin named groups. That covers the sign-in, first-run and wizard/settings fields, then a sweep of tournament details, reset, registration, branding, sponsors, announcements, participants and the schedule page.useStableIdmoved to an import-only leaf, andPillButtonowns the pill. No visible change: the label-driven setup capture recipe reproduced every committed screenshot unchanged.Team encounter marks docs (bc-tmdc). Recording decisions states where each mark of a team encounter lands, mark by mark: each bout's row, beside the team, on the bracket card, in the workbook, and on the Representative Bouts sheet.
Fixed on main, now pinned (bc-unfx, bc-lprf). The fixes came from fix(bc-tmfn): operator fixes: team bouts, withdrawals, knockout rounds and the Bracket page #444 and fix: iPad operator fixes; a team's lineup carries between matches #458. Tests added here fail when the fix is reverted:
Shared-court render tests (bc-shcr, partial). With two competitions on court A, render tests pin four behaviours, each checked by mutation:
The shared-shiaijo e2e journey needs PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442's harness.
Why
The beads come from the mp-yqxn.2 persona review (PR #442). Operator decisions taken for this PR, all recorded on the beads:
Review
Ten
/code-reviewrounds at high effort, then/docs-review.A final pass ran after all of that, both reviews in fresh sub-agents:
/code-review: 0 critical, 0 major, 4 minor. Three are fixed: the remove floor counts numbered bouts only, the court console builds the bracket display model once, and the encho-rise rule sits beside its caller. The fourth needs two devices editing one bout at once and is correctable with one tap./docs-review: 0 critical, 1 major, 7 minor, all fixed. The major was the Remove paragraph, now split; the minors include an alt text for the recaptured knockout-tie shot.Accepted as they are, each weighed in review:
positions: {}lineup is read as exhausted by the advance and as no lineup by the roster read.Files changed
internal/engine/kachinuki.godecideKachinukiAdvance, the one decision); a new pairing is numbered after the highest position; Remove un-records only a pairing its advance appended (history rule)internal/engine/kachinuki_roster.goEngine.KachinukiRoster: fighter on, queue,exhausted,nextPairing, from the advance's own queuesinternal/engine/match_merge.go,match_history.go,lineup_in_force.gokachinuki-recorddoor; the roster's lineup readinternal/state/match_groups.go,models.go,testdata/match_groups.jsonSubMatchResult.Recorded, therecordedserver group, the shared Go/JS group tableinternal/mobileapp/handlers_lineup.go,handlers_match.go,deps.gokachinuki-rosterroute; Record bout echoes the mark;changedrefuses the server groupinternal/**/*_test.gospecs/openapi.yamlrecorded,WritableMergeGroup, the history doorweb-mobile/js/kachinuki_advisory.jsxlastNumberedBoutweb-mobile/js/admin_scoring_team.jsxweb-mobile/js/api_client.jsx,match_groups.jsx,match_history_view.jsxfetchKachinukiRoster; the server group; the new history doorweb-mobile/js/admin_shiaijo.jsx,admin_helpers.jsx,bracket.jsx,bronze_feeders.jsx,write_result.jsx,admin_competition_bracket.jsx,viewer_competition.jsxweb-mobile/js/stable_id.jsx,competition_fields.jsx,glossary.jsx,app.jsx,admin_setup.jsx,admin_competition_settings.jsx,admin_announcement.jsx,admin_branding.jsx,admin_sponsors.jsx,admin_participants.jsx,admin_schedule_page.jsx,admin_schedule_score_editor.jsx,registration.jsx,reset.jsxaria-pressedpillsweb-mobile/css/styles.css,web-mobile/check-imports.mjsweb-mobile/js/__tests__/**scripts/screenshots/recipes/adminsetup.mjsaria-presseddocs/user-guide/**,docs/screenshots/kachinuki-*.png,docs/videos/kachinuki-demo.webmCLAUDE.mdScreenshots
Kachinuki at 1180x820 on the court console (final build). The encounter shows a 1-0 time-up win, a 1-1 tie, a 2-1 win, a 1-0 win, a 0-0 tie, and a 1-1 tie after which Aka's taisho Nomura stays on.
Live bout 7 with Ito leading 1-0: both fighters named as on.

After Record bout on Ito's 1-0 time-up win, Aka has nobody left. The bout joins the read-only rows, Record bout greys out, and the line reads "Aka has no fighters left. End match finishes the encounter." The same view shows after a reload.

The 3rd-place match: the admin bracket before the semi-finals; the court console's Later row; Up next after both semi-finals.



Test plan
make go/testpasses (lint + security scan + tests): exit 0 on the final code; engine 90.2%, mobileapp 88.4%, state 90.0%docs/(make docs/buildpasses, prose gate clean);/docs-reviewrun and its findings fixedmake docs/media SINCE=main):kachinuki-scoring-buttons,kachinuki-correct-bout,kachinuki-knockout-tie-enchoand the kachinuki demo copied (chapter marks unchanged). The 28 other captures are unchanged, so the label sweep left the layout as it was.Closes bc-bzlb
Closes bc-lbla
Closes bc-kfnl
Closes bc-tmdc
Closes bc-unfx
Closes bc-lprf
Closes bc-kbrc
Closes bc-ktih
Refs bc-shcr (the e2e journey waits for #442)
🤖 Generated with Claude Code