Skip to content

feat: kachinuki advisory line and recorded bouts, 3rd-place labels, form labels, and the open #442 review beads - #467

Merged
gitrgoliveira merged 34 commits into
mainfrom
worktree-bridge-cse_01N8XTrYTRShcdZL8WfsRv7k
Oct 11, 2026
Merged

gitrgoliveira merged 34 commits into
mainfrom
worktree-bridge-cse_01N8XTrYTRShcdZL8WfsRv7k

Conversation

@gitrgoliveira

@gitrgoliveira gitrgoliveira commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

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 reads GET /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.

    • The bout being fought keeps both fighters on, whatever the score (operator decision, option C, after tablet mockups of a mixed encounter). A bout can be won with one point at time-up, or tied, so a 1-0 lead during play is not a result.
    • Record bout marks the bout it closes as recorded. The mark lives in a new server-only merge group, 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-pressed in 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. useStableId moved to an import-only leaf, and PillButton owns 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:

    • an unfought team bout reads as unfought on the sheet, the viewer and the TV board;
    • the duplicate-member refusal names the held position, in one wording on all three surfaces.
  • Shared-court render tests (bc-shcr, partial). With two competitions on court A, render tests pin four behaviours, each checked by mutation:

    • the competition picker;
    • the switch nudge;
    • the "Another bout is running" alert;
    • same-competition Finish + Start Next and the start after a withdrawal.

    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:

  • bc-bzlb: use the owner's wording.
  • bc-kfnl:
    • Wording: show the player's number.
    • Live bout: option C.
    • Rejected alternatives:
      • count a fighter out at any lead;
      • name a beaten last fighter until End match;
      • add Correct/Start buttons.
  • bc-lbla: sweep every operator form.
  • bc-shcr: render tests now; its journey after test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442.
  • Beads found during this PR are solved in it: bc-kbrc and bc-ktih.

Review

Ten /code-review rounds at high effort, then /docs-review.

  • Every critical and major finding was fixed.
  • Rounds 7, 9 and 10 found no major. Round 8's one major was fixed.
  • The docs review's critical finding (alt text describing a line the old screenshots lacked) is fixed by the recapture. Its majors are fixed:
    • the split kachinuki paragraph;
    • the bronze row's missing Run now;
    • an incomplete spec sentence.

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:

  • A recorded bout closes only once the roster has been read. A device whose roster read fails keeps it live, as on main.
  • A migrated empty lineup reads differently in two places. A match's own positions: {} lineup is read as exhausted by the advance and as no lineup by the roster read.
  • End match does not mark the last bout recorded. This matters only when an encounter is reopened.
  • Removing a bout added with "Add next bout manually" after exhaustion keeps the recorded mark. It does so when the history says Record bout did not append that bout. If the history is missing, the bout stays recorded and can still be corrected.
  • The verdict comes from a separate read. It could instead ride on the match payload as a request-time annotation; that would be a follow-up, not a defect.

Files changed

File What
internal/engine/kachinuki.go Record bout marks the closed bout recorded (decideKachinukiAdvance, 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.go Engine.KachinukiRoster: fighter on, queue, exhausted, nextPairing, from the advance's own queues
internal/engine/match_merge.go, match_history.go, lineup_in_force.go The recorded group in the merge (server-only, stamped; cleared by an overtime period or a result change); the kachinuki-record door; the roster's lineup read
internal/state/match_groups.go, models.go, testdata/match_groups.json SubMatchResult.Recorded, the recorded server group, the shared Go/JS group table
internal/mobileapp/handlers_lineup.go, handlers_match.go, deps.go The public kachinuki-roster route; Record bout echoes the mark; changed refuses the server group
internal/**/*_test.go Engine, state and handler tests (roster verdict, recorded merge invariants, live-bout rules, history rule)
specs/openapi.yaml The roster route and schemas, recorded, WritableMergeGroup, the history door
web-mobile/js/kachinuki_advisory.jsx The advisory model, read key and lastNumberedBout
web-mobile/js/admin_scoring_team.jsx The line, the closed recorded bout, Record bout greying, the unscored-bout hint
web-mobile/js/api_client.jsx, match_groups.jsx, match_history_view.jsx fetchKachinukiRoster; the server group; the new history door
web-mobile/js/admin_shiaijo.jsx, admin_helpers.jsx, bracket.jsx, bronze_feeders.jsx, write_result.jsx, admin_competition_bracket.jsx, viewer_competition.jsx 3rd-place label, Later row and "Loser of M" placeholders
web-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.jsx Form labels and aria-pressed pills
web-mobile/css/styles.css, web-mobile/check-imports.mjs The advisory line style; the import check covers the new ES imports
web-mobile/js/__tests__/** Unit and render tests for every bead, including the shared-court and pinned-fix tests
scripts/screenshots/recipes/adminsetup.mjs Fields by label, pills by group role and aria-pressed
docs/user-guide/**, docs/screenshots/kachinuki-*.png, docs/videos/kachinuki-demo.webm The advisory line, the recorded bout, the 3rd-place match and team-encounter marks; recaptured media
CLAUDE.md The recorded mark, its group and its clear rules

Screenshots

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/test passes (lint + security scan + tests): exit 0 on the final code; engine 90.2%, mobileapp 88.4%, state 90.0%
  • New/updated unit tests cover the change: Go engine, state and handler tests; JS unit 6846 and render 1896, all passing. New tests were red-first or checked by mutation.
  • Manual browser verification at 1180x820 on the court console:
    • Kachinuki: the mixed encounter played through the UI (line during play, after each tie, the taisho staying on, Record bout at exhaustion, reload). bc-ktih's hint after ties.
    • Bronze: Later row, then Up next after both semi-finals.
    • Labels: first-run and wizard fields and pill groups resolved through the accessibility tree.
  • Screenshots added above
  • Docs updated under docs/ (make docs/build passes, prose gate clean); /docs-review run and its findings fixed
  • Docs screenshots and videos recaptured (make docs/media SINCE=main): kachinuki-scoring-buttons, kachinuki-correct-bout, kachinuki-knockout-tie-encho and the kachinuki demo copied (chapter marks unchanged). The 28 other captures are unchanged, so the label sweep left the layout as it was.
  • No new console errors or warnings (0 errors, 0 warnings across the browser runs)

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

gitrgoliveira and others added 14 commits October 11, 2026 01:08
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>
gitrgoliveira and others added 15 commits October 11, 2026 12:01
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>
…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>
gitrgoliveira and others added 3 commits October 11, 2026 17:57
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 gitrgoliveira changed the title feat: kachinuki advisory line, 3rd-place match labels, form labels, and four more #442 review beads feat: kachinuki advisory line and recorded bouts, 3rd-place labels, form labels, and the open #442 review beads Oct 11, 2026
@gitrgoliveira
gitrgoliveira marked this pull request as ready for review October 11, 2026 17:16
Copilot AI balanced review requested due to automatic review settings October 11, 2026 17:16

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 and others added 2 commits October 11, 2026 18:52
- 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>
Copilot AI balanced review requested due to automatic review settings October 11, 2026 18: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.

@gitrgoliveira
gitrgoliveira merged commit f443f6b into main Oct 11, 2026
13 checks passed
@gitrgoliveira
gitrgoliveira deleted the worktree-bridge-cse_01N8XTrYTRShcdZL8WfsRv7k branch October 11, 2026 18:59
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.

2 participants