Repository navigation
fix(bc-evts): engi tap-back race, one edit-time rule for every score editor; e2e harness re-verified (bc-e2rv) - #473
Merged
Merged
Conversation
…ve is out The engi editor compared flag counts by value only, so taking a flag back to the count the server still showed read as untouched, and the earlier save's echo then wiped the tap (the race bc-ltap fixed on the individual editor). It now holds its counts against a server copy stamped before the operator's last unsent edit, cleared by landedAutosave once a save built after that edit has landed. The rule and its three owners are recorded in the match results rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new journey seeds an engi (kata pairs) competition through the wizard, scores and finishes a match on the court console and sees it on a public phone. Its second test holds a flag's save in flight, takes the flag back, and checks the echo does not restore it, in both directions; with the engi fix reverted it fails. The held-save route moves to a fixture shared with the bc-ltap test, and the wizard fixture learns the Engi option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the re-verification review of the e2e harness after PR #442: - audit rows find their spec from a path with spaces or parentheses; - mark-clearing loops are bounded (clearMarks) and fail with a message; - lineup saves wait for the save's response, not a button or a toast; - an allowlisted 409 whose body could not be read is not an error; - the fighter list closes with Escape, not a tap at a fixed point; - a harness-owned startMatch waits for Start, taps it and fails if absent; - journeyShots refuses a name that would empty the output root; - one enterTournament owner; one expect timeout constant; rosterOf bounds. The README and the Makefile comment now say what the code does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew round 1 The edit-time stamp holds a score editor's board against the echo of its own autosave, and only a landed autosave clears it. On a match that is not running nothing is autosaved, so a net-zero fiddle left a stamp nothing cleared and the editor refused another device's earlier copy. The engi and individual editors now take the stamp only while the match is running. Tests now model a landed save, and pin the edit counter (a save built before the latest tap clears nothing), a queued or refused save keeping the stamp, the scheduled match, and the stamp boundary. Harness: an unreadable 409 body is allowed only when the page had closed, under its own reason; the held-save guard asserts its answer reached the page before the next write was sent, and fails on an unreleased hold; lineup saves are matched to their team and lineup; a refused panel save sends nothing; clearMarks fails when a removed mark comes back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew round 2 A finish or send back made elsewhere inside the autosave window drops the pending write with no answer, so no landed save cleared the stamp; both editors now clear it once the match is no longer running. The rules note scopes the running-only stamp to the individual and engi editors and names the team sheet's stamps as the open question. Harness: a page close is detected from its close event as well; held-write's stop() only cleans up and assertNoSafetyRelease() runs after a passing body, so it never hides the body's own failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…engi journey The held-write guard now waits for the follow-up write and fails when it is never sent, clears its timers, and forgets a pending hold on stop. The engi journey waits for the running editor or Up next's Start, whichever shows, and checks every flag tap landed. Comment and rules wording corrected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rom the starting count Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e takes the server's copy The team sheet now takes its row and pick stamps only while the match is running and clears them when it leaves running, as the individual and engi editors do. In all three editors a refused autosave (landedAutosave's new onRefused) clears the stamp and takes the server's current copy, since no broadcast follows a refusal to bring the board back; a queued answer keeps the stamp. Tests that answered an autosave with nothing now answer a landed one, since nothing handed back reads as a refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five teams, kachinuki, pools of 3 and 2 feeding a final: winner stays on, a hikiwake retiring both, a team exhausted, a level pool encounter recorded as a draw with no daihyosen offered, the pool tables' W/L/T, IV/IL/IT and PW/PL on admin and the public viewer, and the pool winners seated in the final. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… edit, replace) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…set, locked mode)
Adds organiser-admin.spec.mjs (file mode: results workbook download, import
from a folder, logo and sponsor, forgotten-password reset) and
organiser-locked.spec.mjs (--lock-password with a hash from the app's own
hash-password command), with fixtures/organiser.mjs. A spec file may declare
its server's extra args and env with serverOptionsFor; start() takes optional
{ args, env }, unchanged when absent. One narrow console allowlist entry for
the Edit details HEAD probe of /api/branding/logo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A manifest row without a date imported as a success, yet the competition
could never be drawn ("Cannot start: invalid date in Settings tab"). The
import now gives it the tournament's Day 1 through defaultCompetitionDate,
the rule POST /competitions applies (extracted, not respelled), and refuses a
row whose date still cannot be resolved with a per-row message. The
organiser-admin journey's fixme becomes a step asserting Generate draw is
enabled and the date it took; the other fixme is renamed to its bead, bc-lblg.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BrandingManager asked HEAD /api/branding/logo whether a logo was set, and the server answers 404 on purpose while none is, so the browser logged a console error on every load. The tournament's theme now carries a derived, read-only hasLogo (omitted when false; the disk filename stays off the wire) and the page reads it, following the tournament when it changes. The HEAD and GET routes are untouched for other callers. The e2e console allowlist entry for the 404 is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hter fixmes Settle-before-read sleeps become condition waits: a state, a write's answer (nextMatchWrite/writeLanded), or, where the outcome is the thing under audit, the new rendered/untilStable settle helpers (pace.mjs). The sleeps that stay (scenario time, and negatives where an absence is the evidence) are marked "Deliberate:". The swallowed-assert probes in smoke and the team journey are real assertions. One owner each: armTwoTap (finishTeam, endMatch, armFinish, saveEngiResult), ipponOnRow (boutIppon and awardBoutIppon), correctCompleted with a live-bout reader (correctAcross), ensureCoarse (pointer.mjs). 15 s literals use EXPECT_TIMEOUT_MS. The bc-adna, bc-seqc and bc-pubq fixmes are tightened to their beads' acceptance (still fixme). The kachinuki correction step paces its strikes past each side's bounce window, which makes bout 1 M K again and the encounter total PW 8-3 (7-3 after the reopened bout 8), where an unpaced pair left it at M and the expectations had followed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions and allowlist entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
landedAutosave read a clock_skew answer as a refusal and took the server's copy, wiping a tap that recordScore deliberately leaves on the board for the next save to carry with a corrected stamp; it now keeps the stamp and the board, as for a queued answer. The individual and engi refusal adopt also requires the stamp still to be set, so a correction tapped after the match finished elsewhere is not wiped by a late refusal. The rules note states the team sheet's scope and the queued-then-refused limit. Smaller: a Go doc comment back on its function, the hasLogo test proves a PUT cannot lose a logo, and writeWasRefused names its callers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ook that must hold the result, settled reads - organiser-roster: the seeds test reads the unseeded draw, discards it, seeds two competitors it put in one pool, and asserts the redraw separates them (red when the seeding step is skipped). - organiser-admin: the Excel export is compared with one taken before the bout (winner carried into the next round, waza letter in a score cell); the bc-imdt step uses a tournament day that is not today and the page's own formatDate; the TV board's no-sponsor negative waits for the tournament payload that carries the sponsor; the bc-lblg fixme is written against an optional setting, on and off. The dated import row takes a tournament day. - untilStable takes `from` (the settled read before the tap); the probes that read after a tap use it, wait for a known target, or wait for the write. - retapWhileSaving waits for every write the taps caused before returning. - organiser-locked waits for auth-config; kachinuki-pools claims only what it tests; liveIndividualBout's 1 s read is only in the toBeNull poll; the Registration desk confirm's dwell starts after it rendered; a stale toast cannot satisfy the withdrawn competitor's Finish; the sync-pill reason in scoring.mjs and the README is corrected; small nits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…poll, listeners released in finally Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…window A pick from a fighter name list stamps the team sheet's bout list, which ignores every click inside it for TAP_BOUNCE_MS; the J3 repair pick landed on that boundary and was dropped about one run in five with --workers=1, and the step's deliberate wrong pick was always dropped. tapNameOption paces option taps from the previous one, so the wrong pick now really lands and the repair always does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixme now checks each part of bc-adna on its own (soft assertions, the network ones read in finally): Finish reads plain Finish; the Scores tab sends no start by itself (a start is a /score write carrying startOnly), after Finish or after a fusenpai; and no start is refused court_busy. Three README listing lines reflowed to 80 columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eak bout Individual Pools + Knockout, seven entrants, pool size a minimum of 3: the draw makes pools of 4 and 3 (a maximum of 3 would make 3, 2, 2). One pool is scripted so two competitors end level on W, L, D, PW and PL for the advancing 2nd place: the app adds the tie-break bout (Pool X-TB-N), shown as the only bout left to score on the court console and Scores tab, the table marks both tied until it is fought, the knockout holds both places, then the winner (picked from the one the table badged lower) ranks above the loser and is seated, on admin and public viewer. The other pool is split on points lost alone, with no tie-break bout. New helpers: playerRowFigures (pools.mjs), markDraw (scoring.mjs), playScriptedBout and ROSTER_7 (individual.mjs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first journey to run on the public project: an officiated tournament
seeded and scored from a second (operator) context through the interface,
two individual competitions with prefixes K (12 competitors) and M (6).
The phone walks the home, a competition page's tabs and the schedule;
checks the bc-nsrc number rule on the watch picker, the schedule's
filter and its text filter ('k' lists the K draw, 'k1' is K1 alone,
bare digits find nobody, names and dojos by substring); follows a
competitor and sees the watchlist hero and the match card follow the
court without a reload; and opens the match card from the Home NOW list,
the schedule, the Pools tab and the Overview while the operator keeps
scoring.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ic journeys Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…titors as tied While two pool competitors are level on every criterion and the tie decides who advances, the pool table badged one 2nd in the advancing style and the other 3rd, by draw order. tiedPlaces (viewer_standings.jsx, the one owner for the admin Pools tab and the public viewer) gives the group one shared place, shown as =2nd, with no advancing mark on any member until the tie-break bout is scored; a tie inside the advancing places keeps the mark, since it cannot change who goes through. The pools-tiebreak journey asserts it on admin and phone, and the organiser guide says it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…round 5 tiedPlaces now takes the cut the server holds a tie-break bout against (effectivePoolWinners, mirroring EffectivePoolWinners) beside the advancing cut, so a larger-pools tie over the extra place no longer reads as waiting for a bout that is never scheduled; the tooltip names the bout (or the representative bout for teams) only when one is owed. New render cases: the larger-pools tie and a 3-way tie across the cut. The organiser guide's tie paragraph now says what tiebreaker.go does. Harness: the spectator journey follows the competitor actually in both queued bouts and polls its reads; the bc-adna fixme drops dead bookkeeping; markDraw takes a Locator; the pools-tiebreak bracket read waits for the seated qualifiers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s tie-break bout The engine held tie-break bouts (and team daihyosen, chusen candidates and the requalification wait) against EffectivePoolWinners, so a tie over the extra place an oversized pool sends under Oversized send +1 got no bout and its qualifier was seated on the standings' arbitrary order. poolTieBreakCut is now the one cut, the pool's own QualifiersForPool, read by every tie path. The standings badge follows the same single cut, the docs paragraph names the extra place, and the pools-tiebreak journey plays a larger-pools tie over the 2nd place on court B. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-break bout Fill-bracket drafts some pools to send their 2nd too, but the tie cut (poolTieBreakCut) read Competition.QualifiersForPool, which cannot say which pools were drafted, so a tie for a drafted 2nd got no bout and whoever sorted 2nd took the knockout place. The cut is now read off the stored draw (drawnQualifierPlaces: the highest place of the pool the bracket's draw-time placeholders seat), the same labels the qualifier seating fills, so it covers standard, larger-pools and fill-bracket by one rule and cannot drift from the bracket when courts or seeds change after the draw. Requalification asks the same cut. The viewer detail payload carries it per pool (poolQualifiers), and the standings page reads it instead of re-deriving the larger-pools count. The pools-tiebreak journey adds a fill-bracket competition on court C. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…raw seats Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eads <t> runs CodeQL flagged the e2e fixture's XML reading: entities were decoded one after another, so "&lt;" became "<", and string text was what was left after a regex stripped tags. Decode in one pass, and read a shared or inline string's <t> runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t tests; harness polish A corrected pool result after the knockout started is now pinned to wait for the tie-break bout over a larger-pools extra place and a fill-bracket drafted 2nd (both red with the requalification cut reverted to Winners per pool). The team sheet's refused-autosave adopt of the representative bout's score and pick is pinned (red with that branch returning the local row). Harness: a single empty-marks read waits for the card's four slots, the spectator step title names the followed competitor's side correctly, the first pools-tiebreak bracket read counts roster names only, and three audit settles in knockout-mixed-individual are marked Deliberate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wyy8S51EvuQx3shdiTPwkp
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
recordScoreintends.defaultCompetitionDate); a row the tournament cannot date is refused on its own row.HEAD /api/branding/logo, which answers 404 on purpose; the tournament's theme now carries a read-onlyhasLogo.poolQualifiers), so the standings badge and advancing mark follow the server instead of re-deriving it.Why
Operator rulings 2026-10-12/13: bc-evts' plan puts this engi fix first (PR0); bc-e2rv asked for the harness to be re-planned and re-checked from main after #442; "fix and polish all detected gaps", with the coverage journeys in this PR except Swiss and except the journeys an open mp-yqxn UX-review bead owns (league mp-yqxn.3, self-run public mp-yqxn.5, engi review mp-yqxn.6, settings mp-yqxn.8; a Swiss journey is filed as bc-e2sw). The plan is on bc-e2rv's design field (rev 3). The pools tie-break journey found the tie display gap (bc-tbdg), and its review found the larger-pools and fill-bracket tie-break gaps (bc-xqtb, bc-fbtb); each was ruled "bead, fix it here" (2026-10-14).
Reviewed by Claude review rounds and independent rounds by another model family (Antigravity, Gemini) after each change set, the last two on the tie-break cut. Each round's critical and major findings were fixed, or dismissed with the code that decides them, until none remained.
Files changed
web-mobile/js/admin_scoring_engi.jsxweb-mobile/js/admin_scoring_individual.jsxweb-mobile/js/admin_scoring_team.jsxtakeServerRowsRef)web-mobile/js/admin_scoring_autosave.jsxlandedAutosavegainsonRefused; clock_skew and queued answers call neither callbackweb-mobile/js/write_result.jsxwriteWasRefusedcomment names its callersweb-mobile/js/admin_branding.jsxtheme.hasLogoinstead of probingHEAD /api/branding/logointernal/state/models.goTheme.MarshalJSONemits a derived read-onlyhasLogointernal/mobileapp/handlers_branding.gointernal/mobileapp/handlers_competition.godefaultCompetitionDate, shared by POST /competitions and the importinternal/mobileapp/handlers_import.gointernal/mobileapp/handlers_import_date_test.gointernal/mobileapp/handlers_branding_haslogo_test.gohasLogoon the wire, a PUT can neither set nor lose itinternal/mobileapp/handlers_import{,_format,_kind,_extra_qualifiers}_test.gospecs/openapi.yamlTournament.themewith read-onlyhasLogo;poolQualifierson the competition detail.claude/rules/match-results-and-marks.mdweb-mobile/js/__tests__/render/engi_editor_tap_back_kept.render.test.jsxweb-mobile/js/__tests__/render/individual_engi_refused_autosave_takes_server.render.test.jsxweb-mobile/js/__tests__/render/team_editor_stamps_only_while_running.render.test.jsxweb-mobile/js/__tests__/render/admin_branding_no_logo_probe.render.test.jsxweb-mobile/js/__tests__/render/{individual_editor_tap_back_kept,team_editor_row_stamp_cleared_on_landed_save,autosave_every_scoring_edit,changed_groups_every_editor,ippon_double_tap,kachinuki_cleared_bout_reaches_server,team_fusensho_keeps_points}.render.test.jsxundefined(now a refusal); new casesinternal/engine/tiebreaker.gopoolTieBreakCut, the one cut every pool tie path asks, read off the stored draw (drawnQualifierPlaces);PoolQualifierCutsfor the payloadinternal/engine/{chusen,daihyosen,pool_requalify}.gointernal/engine/tiebreak_extra_qualifier_test.gointernal/engine/pool_daihyosen_bandaware_test.gointernal/mobileapp/handlers_viewer.go,handlers_viewer_test.gopoolQualifierson the competition detail payloadweb-mobile/js/viewer_standings.jsxtiedPlaces: tied rows share their place (=2nd), badge title names the bout, advancing mark withheld while the tie decides it;qualifiersForPoolreads the served countweb-mobile/js/api_serializers.jsxnormalizeCompetitionDetailputs each pool's served count on the poolweb-mobile/js/__tests__/render/pools_tied_place.render.test.jsx,viewer_pools_viewer.test.jsx,api.test.jsxdocs/user-guide/organisers/run-tournament.mdscripts/e2e/journeys/pools-tiebreak.spec.mjsscripts/e2e/journeys/spectator.public.spec.mjs,scripts/e2e/fixtures/public.mjsscripts/e2e/journeys/engi.spec.mjsscripts/e2e/journeys/kachinuki-pools.spec.mjsscripts/e2e/journeys/organiser-roster.spec.mjsscripts/e2e/journeys/organiser-admin.spec.mjsbc-lblgfixmescripts/e2e/journeys/organiser-locked.spec.mjsscripts/e2e/journeys/{knockout-mixed-individual,knockout-mixed-kachinuki,knockout-mixed-team,moved-match,shared-shiaijo,smoke}.spec.mjsscripts/e2e/fixtures/{engi,held-write,pools,roster,organiser}.mjsscripts/e2e/fixtures/{audit,clumsy,competition,individual,kachinuki,pace,pointer,scoring,setup,shared-court,shots,team,test,wizard}.mjsserverOptionsFor; console allowlist (an unreadable 409 only when the page closed; the duplicate participant edit)scripts/screenshots/lib/server.mjs{ args, env }for a spec's server (capture harness callers unchanged)scripts/e2e/README.mdMakefileScreenshots
All from the e2e journeys' real-browser runs at the PR head (iPad context, 1180x820).
Engi: a flag taken back while its add was saving stays taken back (court console; the add's echo has arrived, the board still shows 0 and is Synced):
The same after a reload: the server holds 0 (the save of the take-back named the flags as changed):
Folder import: a row with no date took the tournament's date (28 Nov 2026) and can be drawn (before this PR it showed "Cannot start: invalid date in Settings tab" and Generate draw was disabled):
Edit tournament details with an uploaded logo (the page now reads
theme.hasLogo; with no logo it sends no request that answers 404):A pool tie waiting for its tie-break bout (admin Pools tab): Kei Takagi and Mina Abara are level on every criterion in Pool B, both read =2nd, neither carries the advancing mark, and the bout that decides them is still to be fought:
A tie over the extra place an oversized pool sends (Oversized send +1, public phone view): Pool A sends two; Joan Mercer and Lars Nilsen are level for 2nd, both read =2nd, and their tie-break bout (7) is scheduled. Before this PR no bout was held and whoever sorted higher took the place:
A tie over a drafted 2nd (Fit the knockout, admin): Pool A was drafted to send its 2nd, so its level pair gets a tie-break bout (4, on court C); Pool B was not, so its identical tie shares =2nd with no bout:
Test plan
make go/testpasses (lint + security scan + tests) at the PR headweb-mobile/changes): the e2e journeys drive the real app in Chromium (iPad and phone contexts); the engi tap-back race was reproduced in the browser with the fix reverted and passes with it; the folder import of a dateless row and the logo flow were walked the same waymake e2egreen locally (79 tests in 13 files): at the PR head (a2b7444) default workers 70 passed, 9 skipped (the 9 fixmes of open beads); at de5d22a (the head before the workbook reader's CodeQL fix, which onlyorganiser-adminuses and which passed twice on its own) default workers,--workers=1and a fresh clone (make e2e/deps,make e2e) each 70 passed, 9 skippeddocs/for any user-facing change: run-tournament.md describes the tied place and when a tie gets a bout (no docs page describes the import manifest; the editors' visible behaviour matches docs already);make docs/buildpassesFollow-ups filed: bc-lblg (lobby logo as an optional setting; its fixme is in organiser-admin). Not in this PR, by operator decision: league, self-run public and engi-across-formats journeys (mp-yqxn.3, .5, .6); Swiss (filed as bc-e2sw); the engi cross-device lost flag (bc-evts loss 2, removed by the event-log phases, engi in PR4).
Closes bc-e2rv, bc-imdt, bc-lgnf, bc-e2tb, bc-e2pb, bc-tbdg, bc-xqtb, bc-fbtb. Part of bc-evts (PR0 of its plan; the bead stays open).
🤖 Generated with Claude Code