From ded384a64903da56412802112e0141067c7f6c6a Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 13:02:30 +0100 Subject: [PATCH 01/25] refactor(mp-m4bn): remove the unreachable encho cap MaxEnchoPeriods capped how many encho (overtime) periods a match could run, returning 400 max_encho_exceeded past the limit with a force=true override. Nothing could ever set it: no create-wizard field, no settings form field, no import-manifest key, and the settings PUT whitelist in handlers_competition.go never copied the field onto the stored record. The cap was unreachable through the product, so the clamp, the "Maximum encho periods reached" banner, the 400 and its force-retry confirm dialog were all dead code. Removed rather than completed. CHK029 asked whether requirements were DEFINED for a maximum; it was answered by building a mechanism instead of recording a decision, and no functional requirement ever called for a cap (FR-032 mandates tracking the period count, not bounding it). The decision: encho is unbounded. How many overtime periods are fought, and how a still-tied match is finally resolved (hantei for an individual bout, daihyosen for a team encounter), is the shimpan's call. By the time the operator enters a period count those periods have already been fought, so a validator that rejects the entry refuses to record what physically happened, during a live event where results cannot be rolled back (constitution VII). The operator records the outcome; the software does not referee it. Encho.PeriodCount is unchanged and still required by FR-032. The force flag stays on /decision for the unrelated T103 decision-lock override. Also drops a per-request store load: the bulk-score handler loaded the competition solely for this check, along with its 500 failure path. The MAX_ENCHO axis is removed from both score-editor config matrices, halving those suites. Closes mp-m4bn --- internal/mobileapp/handlers_decision.go | 8 - internal/mobileapp/handlers_match.go | 93 ------- internal/mobileapp/handlers_match_test.go | 262 ------------------ internal/state/models.go | 5 - specs/openapi.yaml | 3 - web-mobile/css/styles.css | 5 - .../js/__tests__/admin_competition.test.jsx | 4 +- .../js/__tests__/admin_scoring_modal.test.jsx | 159 +++-------- .../admin_scoring_modal_barrel.test.jsx | 2 - ...idual_editor_config_matrix.render.test.jsx | 17 +- .../render/score_editor_matrix_axes.js | 1 - .../team_editor_config_matrix.render.test.jsx | 48 ++-- web-mobile/js/admin_competition_swiss.jsx | 4 +- web-mobile/js/admin_pools.jsx | 2 +- web-mobile/js/admin_scoring_individual.jsx | 8 +- web-mobile/js/admin_scoring_modal.jsx | 4 - web-mobile/js/admin_scoring_shared.jsx | 66 ++--- web-mobile/js/admin_scoring_team.jsx | 2 - 18 files changed, 82 insertions(+), 611 deletions(-) diff --git a/internal/mobileapp/handlers_decision.go b/internal/mobileapp/handlers_decision.go index 6ef3615f7..b8c8a7019 100644 --- a/internal/mobileapp/handlers_decision.go +++ b/internal/mobileapp/handlers_decision.go @@ -125,14 +125,6 @@ func RegisterDecisionHandlers(r *gin.RouterGroup, eng ScoringEngine, store Compe return } - // T104/CHK029: enforce MaxEnchoPeriods cap on the encho block. - // Same shape as the score handler, Force bypasses, 0 cap means - // unlimited. Done BEFORE the tx so the read is cheap and we - // don't take the lock when the request is going to 400 anyway. - if !enforceEnchoCap(c, store, id, req.Encho, req.Force) { - return - } - // T156: run the entire RecordDecision flow inside one // WithTransaction. The engine call chain, sides lookup, T103 // downstream-match check, T105 concurrent-kiken pre-check, diff --git a/internal/mobileapp/handlers_match.go b/internal/mobileapp/handlers_match.go index 901bed96f..507f8700a 100644 --- a/internal/mobileapp/handlers_match.go +++ b/internal/mobileapp/handlers_match.go @@ -150,77 +150,6 @@ func annotateBracketQueuePositions(b *state.Bracket) { } } -// enchoExceedsCap reports whether an encho block would exceed the -// competition's MaxEnchoPeriods cap. Returns false (within limit) when -// encho is unset, comp is nil, the cap is 0 (unlimited, FIK default), -// the count is within the cap, or force is set. T104/CHK029. -func enchoExceedsCap(encho *state.EnchoMetadata, comp *state.Competition, force bool) bool { - if encho == nil || encho.PeriodCount <= 0 { - return false - } - if comp == nil || comp.MaxEnchoPeriods <= 0 { - return false - } - return encho.PeriodCount > comp.MaxEnchoPeriods && !force -} - -// anySubBoutEnchoExceedsCap returns true if any sub-result's encho -// period count exceeds the competition cap. The same cap applies -// per-sub-bout because each bout is a standalone overtime bout. -func anySubBoutEnchoExceedsCap(subResults []state.SubMatchResult, comp *state.Competition, force bool) bool { - for i := range subResults { - if enchoExceedsCap(subResults[i].Encho, comp, force) { - return true - } - } - return false -} - -// anySubBoutHasEncho reports whether any sub-result carries encho with at -// least one period. Used to decide whether the cap check needs to load the -// competition at all, ordinary team scoring (every bout has SubResults but -// no encho) must not pay that store load. -func anySubBoutHasEncho(subResults []state.SubMatchResult) bool { - for i := range subResults { - if subResults[i].Encho != nil && subResults[i].Encho.PeriodCount > 0 { - return true - } - } - return false -} - -// enforceEnchoCap is the gin-handler wrapper around enchoExceedsCap for -// the single-result score / decision endpoints. Loads the competition -// once, checks the top-level encho and every sub-bout encho against the -// cap (writing 500 on store failure, 400 on cap exceeded). -// Returns true if the handler should continue. -func enforceEnchoCap(c *gin.Context, store CompetitionStore, id string, encho *state.EnchoMetadata, force bool) bool { - return enforceEnchoCapWithSubs(c, store, id, encho, nil, force) -} - -// enforceEnchoCapWithSubs is the variant used by the score endpoint. It -// checks both the top-level encho and each sub-result's encho against the -// competition cap in a single competition load. -func enforceEnchoCapWithSubs(c *gin.Context, store CompetitionStore, id string, encho *state.EnchoMetadata, subs []state.SubMatchResult, force bool) bool { - needsCheck := (encho != nil && encho.PeriodCount > 0) || anySubBoutHasEncho(subs) - if !needsCheck { - return true - } - comp, err := store.LoadCompetition(id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to validate encho limits"}) - return false - } - if enchoExceedsCap(encho, comp, force) || anySubBoutEnchoExceedsCap(subs, comp, force) { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "max_encho_exceeded", - "limit": comp.MaxEnchoPeriods, - }) - return false - } - return true -} - // tryAutoCompletePools runs the auto-complete check after a successful score // write. The score itself has already been recorded, so we don't fail the // request when the auto-complete check errors; instead we log full details @@ -314,23 +243,10 @@ func RegisterMatchHandlers(r *gin.RouterGroup, eng *engine.Engine, store Competi // the batch, mirrors the single-score handler (T085/T092). var eligibilityUpdates []*domain.CompetitorStatus - comp, err := store.LoadCompetition(id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to validate encho limits"}) - return - } - force := c.Query("force") == "true" - for i := range results { // Reject a hostile/buggy far-future or negative client timestamp so // it cannot freeze the match against later legitimate writes (mp-y3nk). results[i].ModifiedAt = clampClientModifiedAt(results[i].ModifiedAt) - // T104/CHK029: enforce MaxEnchoPeriods cap on bulk-score payload - // (top-level and each sub-bout independently). - if enchoExceedsCap(results[i].Encho, comp, force) || anySubBoutEnchoExceedsCap(results[i].SubResults, comp, force) { - errs = append(errs, scoreError{MatchID: results[i].ID, Error: "max_encho_exceeded"}) - continue - } if err := validateBulkScoreLengths(&results[i]); err != nil { errs = append(errs, scoreError{MatchID: results[i].ID, Error: err.Error()}) @@ -1007,15 +923,6 @@ func registerScoreHandler(r *gin.RouterGroup, eng ScoringEngine, store Competiti return } - // T104/CHK029: enforce MaxEnchoPeriods cap. The cap is a - // per-competition setting; 0 means unlimited (FIK default). The - // operator can override by sending ?force=true after confirming - // the warning banner, the UI's job is to surface that prompt - // when the cap is reached. - if !enforceEnchoCapWithSubs(c, store, id, req.Encho, req.SubResults, c.Query("force") == "true") { - return - } - // mp-ba3: self-run decision allowlist + result provenance. Runs // after Validate() so the request is structurally valid. resultSource, ok := enforceSelfRunPolicy(c, tl, verifier, &req) diff --git a/internal/mobileapp/handlers_match_test.go b/internal/mobileapp/handlers_match_test.go index 2ff3609bf..0d02d2d65 100644 --- a/internal/mobileapp/handlers_match_test.go +++ b/internal/mobileapp/handlers_match_test.go @@ -1330,268 +1330,6 @@ func (f failingCompetitionStore) LoadBracket(string) (*state.Bracket, error) { return nil, f.err } -// TestEnchoExceedsCap covers the pure predicate. Force, missing comp, -// 0 cap, and within-cap all return false; only an over-cap count with -// !force returns true. -func TestEnchoExceedsCap(t *testing.T) { - cases := []struct { - name string - encho *state.EnchoMetadata - comp *state.Competition - force bool - want bool - }{ - {name: "nil encho", encho: nil, comp: &state.Competition{MaxEnchoPeriods: 2}, want: false}, - {name: "zero period count", encho: &state.EnchoMetadata{PeriodCount: 0}, comp: &state.Competition{MaxEnchoPeriods: 2}, want: false}, - {name: "nil comp", encho: &state.EnchoMetadata{PeriodCount: 5}, comp: nil, want: false}, - {name: "zero cap means unlimited", encho: &state.EnchoMetadata{PeriodCount: 99}, comp: &state.Competition{MaxEnchoPeriods: 0}, want: false}, - {name: "within cap", encho: &state.EnchoMetadata{PeriodCount: 2}, comp: &state.Competition{MaxEnchoPeriods: 2}, want: false}, - {name: "at cap boundary", encho: &state.EnchoMetadata{PeriodCount: 2}, comp: &state.Competition{MaxEnchoPeriods: 3}, want: false}, - {name: "over cap without force", encho: &state.EnchoMetadata{PeriodCount: 3}, comp: &state.Competition{MaxEnchoPeriods: 2}, want: true}, - {name: "over cap with force", encho: &state.EnchoMetadata{PeriodCount: 3}, comp: &state.Competition{MaxEnchoPeriods: 2}, force: true, want: false}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - got := enchoExceedsCap(tc.encho, tc.comp, tc.force) - assert.Equal(t, tc.want, got) - }) - } -} - -// TestEnforceEnchoCap_ScoreHandler covers the gin wrapper as wired -// into the single-score endpoint: 500 on store failure (the bug this -// fix closes), 400 with limit echoed on cap exceeded, and 200 when -// the cap is unset. -func TestEnforceEnchoCap_ScoreHandler(t *testing.T) { - tempDir, err := os.MkdirTemp("", "encho-cap-*") - require.NoError(t, err) - defer os.RemoveAll(tempDir) - realStore, err := state.NewStore(tempDir) - require.NoError(t, err) - eng := engine.New(realStore) - hub := NewHub() - - compID := "encho-cap-test" - require.NoError(t, realStore.SaveCompetition(&state.Competition{ - ID: compID, Format: state.CompFormatMixed, Status: state.CompStatusPools, - MaxEnchoPeriods: 2, - })) - require.NoError(t, realStore.SaveParticipants(compID, []domain.Player{ - {Name: "Alice"}, {Name: "Bob"}, - })) - require.NoError(t, realStore.SavePoolMatches(compID, []state.MatchResult{ - {ID: "PoolA-1", SideA: "Alice", SideB: "Bob"}, - })) - - score := func(periodCount int) []byte { - body, _ := json.Marshal(state.MatchResult{ - ID: "PoolA-1", SideA: "Alice", SideB: "Bob", - Winner: "Alice", IpponsA: []string{"M"}, - Encho: &state.EnchoMetadata{PeriodCount: periodCount}, - Status: state.MatchStatusCompleted, - }) - return body - } - - t.Run("load failure returns 500", func(t *testing.T) { - gin.SetMode(gin.TestMode) - r := gin.New() - admin := r.Group("/api") - // Wire the score handler with a failing CompetitionStore so the - // cap check exercises the new fail-closed branch. The engine - // keeps the real store but never gets called, enforceEnchoCap - // aborts the request first. - registerScoreHandler(admin, eng, failingCompetitionStore{err: errors.New("disk on fire")}, realStore, hub, NewFileVerifier(realStore), realStore) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("PUT", "/api/competitions/"+compID+"/matches/PoolA-1/score", bytes.NewBuffer(score(1))) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - require.Equalf(t, http.StatusInternalServerError, w.Code, "body=%s", w.Body.String()) - assert.Contains(t, w.Body.String(), "failed to validate encho limits") - }) - - t.Run("over cap returns 400 with limit", func(t *testing.T) { - gin.SetMode(gin.TestMode) - r := gin.New() - admin := r.Group("/api") - registerScoreHandler(admin, eng, realStore, realStore, hub, NewFileVerifier(realStore), realStore) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("PUT", "/api/competitions/"+compID+"/matches/PoolA-1/score", bytes.NewBuffer(score(3))) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - require.Equalf(t, http.StatusBadRequest, w.Code, "body=%s", w.Body.String()) - var resp struct { - Error string `json:"error"` - Limit int `json:"limit"` - } - require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) - assert.Equal(t, "max_encho_exceeded", resp.Error) - assert.Equal(t, 2, resp.Limit) - }) - - t.Run("force bypasses cap", func(t *testing.T) { - gin.SetMode(gin.TestMode) - r := gin.New() - admin := r.Group("/api") - registerScoreHandler(admin, eng, realStore, realStore, hub, NewFileVerifier(realStore), realStore) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("PUT", "/api/competitions/"+compID+"/matches/PoolA-1/score?force=true", bytes.NewBuffer(score(3))) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - require.Equalf(t, http.StatusOK, w.Code, "body=%s", w.Body.String()) - }) -} - -// TestEnforceEnchoCapWithSubs covers the sub-bout encho cap path added in mp-4pc. -// anySubBoutEnchoExceedsCap inspects each subResults[].encho.periodCount; the -// single-score and bulk-score handlers must both enforce it (same cap, same 400 -// response shape). force=true bypasses the cap for both paths. -func TestEnforceEnchoCapWithSubs(t *testing.T) { - tempDir, err := os.MkdirTemp("", "sub-encho-cap-*") - require.NoError(t, err) - defer os.RemoveAll(tempDir) - realStore, err := state.NewStore(tempDir) - require.NoError(t, err) - eng := engine.New(realStore) - hub := NewHub() - - compID := "sub-encho-cap-test" - require.NoError(t, realStore.SaveCompetition(&state.Competition{ - ID: compID, Format: state.CompFormatMixed, Status: state.CompStatusPools, - MaxEnchoPeriods: 2, - })) - require.NoError(t, realStore.SaveParticipants(compID, []domain.Player{ - {Name: "TeamA"}, {Name: "TeamB"}, - })) - require.NoError(t, realStore.SavePoolMatches(compID, []state.MatchResult{ - {ID: "PoolA-1", SideA: "TeamA", SideB: "TeamB"}, - })) - - overCapSubResult := state.SubMatchResult{ - Position: state.DaihyosenSubPosition, SideA: "TeamA", SideB: "TeamB", - IpponsA: []string{"M"}, Winner: "TeamA", - Encho: &state.EnchoMetadata{PeriodCount: 3}, // exceeds cap of 2; daihyosen is the only sub-bout with encho - } - - t.Run("single-score: sub-bout encho over cap returns 400", func(t *testing.T) { - gin.SetMode(gin.TestMode) - r := gin.New() - admin := r.Group("/api") - registerScoreHandler(admin, eng, realStore, realStore, hub, NewFileVerifier(realStore), realStore) - - body, _ := json.Marshal(state.MatchResult{ - ID: "PoolA-1", SideA: "TeamA", SideB: "TeamB", - Winner: "TeamA", Status: state.MatchStatusCompleted, - SubResults: []state.SubMatchResult{overCapSubResult}, - }) - w := httptest.NewRecorder() - req, _ := http.NewRequest("PUT", "/api/competitions/"+compID+"/matches/PoolA-1/score", bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - require.Equalf(t, http.StatusBadRequest, w.Code, "body=%s", w.Body.String()) - var resp struct { - Error string `json:"error"` - Limit int `json:"limit"` - } - require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) - assert.Equal(t, "max_encho_exceeded", resp.Error) - assert.Equal(t, 2, resp.Limit) - }) - - t.Run("single-score: force=true bypasses sub-bout encho cap", func(t *testing.T) { - gin.SetMode(gin.TestMode) - r := gin.New() - admin := r.Group("/api") - registerScoreHandler(admin, eng, realStore, realStore, hub, NewFileVerifier(realStore), realStore) - - body, _ := json.Marshal(state.MatchResult{ - ID: "PoolA-1", SideA: "TeamA", SideB: "TeamB", - Winner: "TeamA", Status: state.MatchStatusCompleted, - SubResults: []state.SubMatchResult{overCapSubResult}, - }) - w := httptest.NewRecorder() - req, _ := http.NewRequest("PUT", "/api/competitions/"+compID+"/matches/PoolA-1/score?force=true", bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - require.Equalf(t, http.StatusOK, w.Code, "body=%s", w.Body.String()) - }) - - t.Run("bulk-score: sub-bout encho over cap is recorded as per-item error", func(t *testing.T) { - gin.SetMode(gin.TestMode) - r := gin.New() - admin := r.Group("/api") - RegisterMatchHandlers(admin, eng, realStore, realStore, hub, NewFileVerifier(realStore), realStore) - - body, _ := json.Marshal([]state.MatchResult{ - { - ID: "PoolA-1", SideA: "TeamA", SideB: "TeamB", - Winner: "TeamA", Status: state.MatchStatusCompleted, - SubResults: []state.SubMatchResult{overCapSubResult}, - }, - }) - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/competitions/"+compID+"/matches/bulk-score", bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - // Bulk-score always returns 200; cap violations land in the errors array. - require.Equalf(t, http.StatusOK, w.Code, "body=%s", w.Body.String()) - var resp struct { - Succeeded int `json:"succeeded"` - Errors []struct { - MatchID string `json:"matchId"` - Error string `json:"error"` - } `json:"errors"` - } - require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) - assert.Equal(t, 0, resp.Succeeded) - require.Len(t, resp.Errors, 1) - assert.Equal(t, "PoolA-1", resp.Errors[0].MatchID) - assert.Equal(t, "max_encho_exceeded", resp.Errors[0].Error) - }) -} - -// TestBulkScore_FailsClosedOnLoadError, when the cap-check load -// fails for a bulk-score request, the entire batch is rejected with -// 500 rather than silently bypassing the MaxEnchoPeriods cap on every -// entry. -func TestBulkScore_FailsClosedOnLoadError(t *testing.T) { - tempDir, err := os.MkdirTemp("", "bulk-cap-*") - require.NoError(t, err) - defer os.RemoveAll(tempDir) - realStore, err := state.NewStore(tempDir) - require.NoError(t, err) - eng := engine.New(realStore) - hub := NewHub() - - gin.SetMode(gin.TestMode) - r := gin.New() - admin := r.Group("/api") - // RegisterMatchHandlers takes the concrete *Hub; the cap check - // uses the CompetitionStore parameter (which we fault here) and - // returns 500 before any handler reaches the hub or engine. - RegisterMatchHandlers(admin, eng, failingCompetitionStore{err: errors.New("disk on fire")}, realStore, hub, NewFileVerifier(realStore), realStore) - - body, _ := json.Marshal([]state.MatchResult{ - {ID: "PoolA-1", SideA: "P1", SideB: "P2", Winner: "P1", Status: state.MatchStatusCompleted}, - }) - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/competitions/c1/matches/bulk-score", bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - require.Equalf(t, http.StatusInternalServerError, w.Code, "body=%s", w.Body.String()) - assert.Contains(t, w.Body.String(), "failed to validate encho limits") -} - // TestAnnotateQueuePositions_NonEmpty verifies that annotateQueuePositions // fills in per-court queue positions for a non-empty match list. func TestAnnotateQueuePositions_NonEmpty(t *testing.T) { diff --git a/internal/state/models.go b/internal/state/models.go index 119f0f7b5..62630b341 100644 --- a/internal/state/models.go +++ b/internal/state/models.go @@ -338,11 +338,6 @@ type Competition struct { // one written to new config.md files. PoolMatchDurationSeconds int `yaml:"pool_match_duration_seconds,omitempty" json:"poolMatchDurationSeconds,omitempty"` PlayoffMatchDurationSeconds int `yaml:"playoff_match_duration_seconds,omitempty" json:"playoffMatchDurationSeconds,omitempty"` - // MaxEnchoPeriods caps how many encho (overtime) periods one match - // may run before the operator must call daihyosen. Zero means - // unlimited (FIK general default). T104, CHK029. - MaxEnchoPeriods int `yaml:"max_encho_periods,omitempty" json:"maxEnchoPeriods,omitempty"` - // TeamMatchType selects the team-match format (FR-044). Empty value // is treated as TeamMatchTypeFixed for backward compatibility; all // N×1 bouts are pre-scheduled by position. TeamMatchTypeKachinuki diff --git a/specs/openapi.yaml b/specs/openapi.yaml index 53c769a66..c9cf0a233 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -3112,9 +3112,6 @@ components: type: string description: Team-match scoring model; `fixed` (regular position-by-position order, shown as "Regular" in the UI) or `kachinuki` (winner stays on). enum: [fixed, kachinuki] - maxEnchoPeriods: - type: integer - description: Maximum number of encho (extended time) periods permitted per match. poolMatchDurationSeconds: type: integer minimum: 0 diff --git a/web-mobile/css/styles.css b/web-mobile/css/styles.css index 847cde6dc..b6efc54a5 100644 --- a/web-mobile/css/styles.css +++ b/web-mobile/css/styles.css @@ -6947,11 +6947,6 @@ abbr.msb-lab { text-decoration: none; cursor: help; } font-family: var(--font-mono); font-weight: 700; } -.encho-row__max-banner { - color: var(--red); - font-size: 11px; - display: block; -} .encho-row__btn { min-width: 28px; min-height: 28px; diff --git a/web-mobile/js/__tests__/admin_competition.test.jsx b/web-mobile/js/__tests__/admin_competition.test.jsx index 3d289b02b..3fc48b556 100644 --- a/web-mobile/js/__tests__/admin_competition.test.jsx +++ b/web-mobile/js/__tests__/admin_competition.test.jsx @@ -1031,8 +1031,8 @@ describe('ScoreEditorModal isEngi derived synchronously (finding 10)', () => { }); it('the async fetch effect no longer calls setIsEngi', () => { - // The fetchCompetitionDetails effect still runs for maxEnchoPeriods - // and isNaginata; it must not set isEngi. + // The fetchCompetitionDetails effect still runs for isNaginata; it + // must not set isEngi. const fetchBlock = src.match(/fetchCompetitionDetails[\s\S]{0,500}catch\(\)/)?.[0] || ''; expect(fetchBlock).not.toContain('setIsEngi'); expect(fetchBlock).not.toContain('engi'); diff --git a/web-mobile/js/__tests__/admin_scoring_modal.test.jsx b/web-mobile/js/__tests__/admin_scoring_modal.test.jsx index 45d1e6277..2700a052f 100644 --- a/web-mobile/js/__tests__/admin_scoring_modal.test.jsx +++ b/web-mobile/js/__tests__/admin_scoring_modal.test.jsx @@ -4,10 +4,8 @@ import { assertRunningWritePersisted, buildDecisionBody, submitDecisionRequest, - shouldShowEnchoMaxBanner, getIpponButtons, getValidPointKeys, - canIncrementEncho, nextEnchoPeriod, prevEnchoPeriod, initialEnchoPeriodsForMatch, @@ -143,11 +141,10 @@ describe('buildDecisionBody', () => { expect(body.decision).toBe('daihyosen'); }); - describe('force flag (T103/T104 override loop)', () => { - // T103 (decision_locked) and T104 (max_encho_exceeded) both use a - // confirm-and-retry-with-force flow. The helper accepts `opts.force` - // so the parent's retry path can call back into it without - // re-implementing the body shape. + describe('force flag (T103 override loop)', () => { + // T103 (decision_locked) uses a confirm-and-retry-with-force flow. + // The helper accepts `opts.force` so the parent's retry path can + // call back into it without re-implementing the body shape. it('attaches force=true when opts.force is set', () => { const body = buildDecisionBody('kiken-voluntary', { decisionBy: 'shiro' }, 0, { force: true }); @@ -178,96 +175,23 @@ describe('buildDecisionBody', () => { }); }); -describe('shouldShowEnchoMaxBanner (T104)', () => { - // The "Maximum encho periods reached" banner surfaces once the operator - // has incremented to the cap. maxEnchoPeriods === 0 means "unlimited" - // per state.CompetitionConfig.MaxEnchoPeriods's FIK-default semantics. +describe('nextEnchoPeriod (the + button)', () => { + // mp-m4bn: encho periods are UNBOUNDED. How many overtime periods are + // fought, and how a still-tied match is finally resolved (hantei for an + // individual bout, daihyosen for a team encounter), is the shimpan's + // call. The operator only records what happened, so the + button must + // never refuse a period that was actually fought. - it('returns false when maxEnchoPeriods is 0 (unlimited)', () => { - expect(shouldShowEnchoMaxBanner(0, 0)).toBe(false); - expect(shouldShowEnchoMaxBanner(5, 0)).toBe(false); - expect(shouldShowEnchoMaxBanner(100, 0)).toBe(false); + it('increments by 1', () => { + expect(nextEnchoPeriod(1)).toBe(2); + expect(nextEnchoPeriod(2)).toBe(3); }); - it('returns false when maxEnchoPeriods is null/undefined (unlimited)', () => { - // Defensive: a missing field on the wire shouldn't surprise the - // operator with a banner; treat as unlimited. - expect(shouldShowEnchoMaxBanner(5, null)).toBe(false); - expect(shouldShowEnchoMaxBanner(5, undefined)).toBe(false); - }); - - it('returns false when enchoPeriodCount is below the cap', () => { - expect(shouldShowEnchoMaxBanner(0, 3)).toBe(false); - expect(shouldShowEnchoMaxBanner(1, 3)).toBe(false); - expect(shouldShowEnchoMaxBanner(2, 3)).toBe(false); - }); - - it('returns true when enchoPeriodCount equals the cap (at-cap warning)', () => { - expect(shouldShowEnchoMaxBanner(3, 3)).toBe(true); - expect(shouldShowEnchoMaxBanner(1, 1)).toBe(true); - }); - - it('returns true when enchoPeriodCount exceeds the cap (defensive)', () => { - // The + button is clamped, so this is unreachable through the UI. - // but pinning the `>=` so a future refactor doesn't accidentally - // narrow to `===` and leave operators staring at a non-existent - // banner if the count somehow over-shoots. - expect(shouldShowEnchoMaxBanner(4, 3)).toBe(true); - expect(shouldShowEnchoMaxBanner(10, 3)).toBe(true); - }); - - it('returns false when maxEnchoPeriods is negative (defensive)', () => { - // Negative cap is meaningless; treat as unlimited rather than - // banner-on-everything. - expect(shouldShowEnchoMaxBanner(5, -1)).toBe(false); - }); -}); - -describe('canIncrementEncho (T104 + button gate)', () => { - it('returns true when maxEnchoPeriods is 0 (unlimited)', () => { - expect(canIncrementEncho(0, 0)).toBe(true); - expect(canIncrementEncho(100, 0)).toBe(true); - }); - - it('returns true when maxEnchoPeriods is null/undefined', () => { - expect(canIncrementEncho(5, null)).toBe(true); - expect(canIncrementEncho(5, undefined)).toBe(true); - }); - - it('returns true when below the cap', () => { - expect(canIncrementEncho(0, 3)).toBe(true); - expect(canIncrementEncho(2, 3)).toBe(true); - }); - - it('returns false when at the cap', () => { - expect(canIncrementEncho(3, 3)).toBe(false); - }); - - it('returns false when above the cap (defensive)', () => { - expect(canIncrementEncho(4, 3)).toBe(false); - }); -}); - -describe('nextEnchoPeriod (T104 + button clamp)', () => { - // The + button uses this helper: increments by 1 but clamps at the - // configured cap. Combined with disabled={!canIncrementEncho(...)} - // the button can't fire over-the-cap, but pinning the clamp here so - // a future refactor that drops the disabled gate doesn't silently - // let the count run away. - - it('increments by 1 when unlimited (maxEnchoPeriods = 0)', () => { - expect(nextEnchoPeriod(1, 0)).toBe(2); - expect(nextEnchoPeriod(99, 0)).toBe(100); - }); - - it('increments by 1 when below the cap', () => { - expect(nextEnchoPeriod(1, 3)).toBe(2); - expect(nextEnchoPeriod(2, 3)).toBe(3); - }); - - it('does NOT exceed the cap (clamps at max)', () => { - expect(nextEnchoPeriod(3, 3)).toBe(3); - expect(nextEnchoPeriod(5, 3)).toBe(5); // already over → stays put + it('never clamps, however many periods were fought', () => { + expect(nextEnchoPeriod(99)).toBe(100); + let count = 1; + for (let i = 0; i < 10; i++) count = nextEnchoPeriod(count); + expect(count).toBe(11); }); }); @@ -521,11 +445,10 @@ describe('DecisionPrompt → /decision POST integration', () => { }); it('parent flow attaches force=true on the retry-after-409 path', async () => { - // T103/T104: when the server replies decision_locked or - // max_encho_exceeded the parent's submitDecision recurses with - // { force: true } after the operator confirms. The body must carry - // that flag through to the server so the second attempt isn't - // also rejected. + // T103: when the server replies decision_locked the parent's + // submitDecision recurses with { force: true } after the operator + // confirms. The body must carry that flag through to the server so + // the second attempt isn't also rejected. const onSubmit = vi.fn((payload) => { const body = buildDecisionBody('kiken-voluntary', payload, 0, { force: true }); const password = resolveDecisionPassword('pw'); @@ -578,27 +501,18 @@ describe('DecisionPrompt → /decision POST integration', () => { }); }); -describe('+ / − encho button behaviour (T104 clamp invariants)', () => { - // End-to-end pin: the clamp on the + button must not let the count - // exceed maxEnchoPeriods even across repeated clicks, and the − button - // must not drop below 1. These compose the canIncrementEncho / - // nextEnchoPeriod / prevEnchoPeriod helpers. +describe('+ / − encho button behaviour (mp-m4bn floor-only invariants)', () => { + // End-to-end pin: the + button has NO upper clamp (the shimpan decide how + // many overtime periods are fought; the operator only records them), and + // the − button must not drop below 1. These compose the nextEnchoPeriod / + // prevEnchoPeriod helpers. - it('+ button: repeated clicks stop at the cap', () => { + it('+ button: repeated clicks keep climbing, never clamp', () => { let count = 1; - const maxEnchoPeriods = 3; for (let i = 0; i < 10; i++) { - count = nextEnchoPeriod(count, maxEnchoPeriods); - } - expect(count).toBe(3); - }); - - it('+ button: when unlimited (max=0), repeated clicks keep climbing', () => { - let count = 1; - for (let i = 0; i < 5; i++) { - count = nextEnchoPeriod(count, 0); + count = nextEnchoPeriod(count); } - expect(count).toBe(6); + expect(count).toBe(11); }); it('− button: repeated clicks bottom out at 1, not 0', () => { @@ -609,16 +523,13 @@ describe('+ / − encho button behaviour (T104 clamp invariants)', () => { expect(count).toBe(1); }); - it('the +/− pair is symmetric within the [1, max] window', () => { - const maxEnchoPeriods = 5; + it('the +/− pair is symmetric above the floor of 1', () => { let count = 1; - // Climb to cap - count = nextEnchoPeriod(count, maxEnchoPeriods); // 2 - count = nextEnchoPeriod(count, maxEnchoPeriods); // 3 - count = nextEnchoPeriod(count, maxEnchoPeriods); // 4 - count = nextEnchoPeriod(count, maxEnchoPeriods); // 5 + count = nextEnchoPeriod(count); // 2 + count = nextEnchoPeriod(count); // 3 + count = nextEnchoPeriod(count); // 4 + count = nextEnchoPeriod(count); // 5 expect(count).toBe(5); - expect(canIncrementEncho(count, maxEnchoPeriods)).toBe(false); // Step down to floor count = prevEnchoPeriod(count); // 4 count = prevEnchoPeriod(count); // 3 diff --git a/web-mobile/js/__tests__/admin_scoring_modal_barrel.test.jsx b/web-mobile/js/__tests__/admin_scoring_modal_barrel.test.jsx index 995a669f1..2178dc058 100644 --- a/web-mobile/js/__tests__/admin_scoring_modal_barrel.test.jsx +++ b/web-mobile/js/__tests__/admin_scoring_modal_barrel.test.jsx @@ -46,8 +46,6 @@ const EXPECTED_EXPORTS = [ 'buildDecisionBody', 'submitDecisionRequest', 'makeSubmitDecision', - 'shouldShowEnchoMaxBanner', - 'canIncrementEncho', 'nextEnchoPeriod', 'prevEnchoPeriod', 'initialEnchoPeriodsForMatch', diff --git a/web-mobile/js/__tests__/render/individual_editor_config_matrix.render.test.jsx b/web-mobile/js/__tests__/render/individual_editor_config_matrix.render.test.jsx index ac7c3d4b4..b6fa95044 100644 --- a/web-mobile/js/__tests__/render/individual_editor_config_matrix.render.test.jsx +++ b/web-mobile/js/__tests__/render/individual_editor_config_matrix.render.test.jsx @@ -1,10 +1,10 @@ // mp-yqxn.1: individual (kendo) ScoreEditorModal config matrix — the same // matrix applied to the team editor, minus the axes that don't exist for // individuals (teamSize, teamMatchType): -// {format(+phase)} × {naginata: on, off} × {maxEnchoPeriods: 0, 2} +// {format(+phase)} × {naginata: on, off} // // Config reaches the individual editor ONLY via the async competition fetch -// (window.API.fetchCompetitionDetails → naginata + maxEnchoPeriods); the +// (window.API.fetchCompetitionDetails → naginata); the // format/phase axes ride on the match stamps. // // REGRESSION-PIN ONLY (epic mp-yqxn constraint): DOM assertions prove nothing @@ -14,7 +14,7 @@ import React from 'react'; import { render, act, screen } from '@testing-library/react'; import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; -import { FORMAT_PHASES, IMPOSSIBLE_FORMAT_PHASES, cellKey, NAGINATA, MAX_ENCHO, KENDO_SET, NAGINATA_SET } from './score_editor_matrix_axes.js'; +import { FORMAT_PHASES, IMPOSSIBLE_FORMAT_PHASES, cellKey, NAGINATA, KENDO_SET, NAGINATA_SET } from './score_editor_matrix_axes.js'; const STUBBED_GLOBALS = { isHikiwake: (_type) => false, @@ -60,13 +60,11 @@ afterAll(() => { // Shared axes + letter tables live in score_editor_matrix_axes.js (kept in // lockstep with the team-editor matrix). const CELLS = FORMAT_PHASES.flatMap(fp => - NAGINATA.flatMap(naginata => - MAX_ENCHO.map(maxEncho => ({ ...fp, naginata, maxEncho })) - ) + NAGINATA.map(naginata => ({ ...fp, naginata })) ); function cellName(c) { - return `${c.format}/${c.phase} naginata=${c.naginata} maxEncho=${c.maxEncho}`; + return `${c.format}/${c.phase} naginata=${c.naginata}`; } function makeMatch(cell, overrides = {}) { @@ -92,7 +90,6 @@ async function renderCell(cell, matchOverrides = {}, props = {}) { config: { format: cell.format, naginata: cell.naginata, - maxEnchoPeriods: cell.maxEncho, players: [], }, }); @@ -172,7 +169,7 @@ describe('individual editor IMPOSSIBLE CELLS (asserted, not skipped)', () => { it.each(IMPOSSIBLE_FORMAT_PHASES.map(fp => [`${fp.format} × phase "${fp.phase}"`, fp]))( '%s: product-impossible; the phase stamp alone decides the draw gate', async (_name, fp) => { - await renderCell({ ...fp, naginata: false, maxEncho: 0 }); + await renderCell({ ...fp, naginata: false }); expect(screen.getByTestId('scoring-modal-mark-draw').disabled).toBe(IMPOSSIBLE_EXPECT_DRAW_DISABLED[cellKey(fp)]); } ); @@ -186,7 +183,7 @@ describe('individual editor selfReport (public self-run surface)', () => { // self-run surface should be able to record a judges' decision is ruled // on by mp-yqxn.5. await renderCell( - { format: 'mixed', phase: 'pool', naginata: false, maxEncho: 0 }, + { format: 'mixed', phase: 'pool', naginata: false }, {}, { selfReport: true } ); diff --git a/web-mobile/js/__tests__/render/score_editor_matrix_axes.js b/web-mobile/js/__tests__/render/score_editor_matrix_axes.js index f93ef6182..f29d5483b 100644 --- a/web-mobile/js/__tests__/render/score_editor_matrix_axes.js +++ b/web-mobile/js/__tests__/render/score_editor_matrix_axes.js @@ -38,7 +38,6 @@ export const IMPOSSIBLE_FORMAT_PHASES = FORMATS export const cellKey = (fp) => `${fp.format}/${fp.phase}`; export const NAGINATA = [false, true]; -export const MAX_ENCHO = [0, 2]; // Independent hardcoded expectations for getIpponButtons(isNaginata) — NOT // computed from the component, so a letter-set change in diff --git a/web-mobile/js/__tests__/render/team_editor_config_matrix.render.test.jsx b/web-mobile/js/__tests__/render/team_editor_config_matrix.render.test.jsx index 47a478aea..9cb0d8bbc 100644 --- a/web-mobile/js/__tests__/render/team_editor_config_matrix.render.test.jsx +++ b/web-mobile/js/__tests__/render/team_editor_config_matrix.render.test.jsx @@ -3,11 +3,11 @@ // Mounts the REAL team editor (via the ScoreEditorModal dispatch, exactly as // every mount site does) across the full config matrix // {format(+phase)} × {teamSize: 3, 5} × {teamMatchType: fixed, kachinuki} -// × {naginata: on, off} × {maxEnchoPeriods: 0, 2} +// × {naginata: on, off} // and asserts, per cell, WHICH controls render. Config reaches the editor two // ways, mirroring production: match-level stamps (compFormat, teamMatchType — // stamped by viewer_utils.compMatches) and the async competition fetch -// (naginata, maxEnchoPeriods — via window.API.fetchCompetitionDetails). +// (naginata — via window.API.fetchCompetitionDetails). // // Formats map to phases: playoffs has ONLY bracket matches; league and swiss // have ONLY pool-shaped matches; mixed has both. Cells outside that mapping @@ -21,7 +21,7 @@ import React from 'react'; import { render, act, fireEvent, screen } from '@testing-library/react'; import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; -import { FORMAT_PHASES, IMPOSSIBLE_FORMAT_PHASES, cellKey, NAGINATA, MAX_ENCHO, KENDO_SET, NAGINATA_SET } from './score_editor_matrix_axes.js'; +import { FORMAT_PHASES, IMPOSSIBLE_FORMAT_PHASES, cellKey, NAGINATA, KENDO_SET, NAGINATA_SET } from './score_editor_matrix_axes.js'; const STUBBED_GLOBALS = { isHikiwake: (_type) => false, @@ -74,15 +74,13 @@ const MATCH_TYPES = ['fixed', 'kachinuki']; const CELLS = FORMAT_PHASES.flatMap(fp => TEAM_SIZES.flatMap(teamSize => MATCH_TYPES.flatMap(tmt => - NAGINATA.flatMap(naginata => - MAX_ENCHO.map(maxEncho => ({ ...fp, teamSize, tmt, naginata, maxEncho })) - ) + NAGINATA.map(naginata => ({ ...fp, teamSize, tmt, naginata })) ) ) ); function cellName(c) { - return `${c.format}/${c.phase} size=${c.teamSize} ${c.tmt} naginata=${c.naginata} maxEncho=${c.maxEncho}`; + return `${c.format}/${c.phase} size=${c.teamSize} ${c.tmt} naginata=${c.naginata}`; } function makeTeamMatch(cell, overrides = {}) { @@ -114,7 +112,6 @@ async function renderCell(cell, matchOverrides = {}, props = {}) { format: cell.format, teamMatchType: cell.tmt, naginata: cell.naginata, - maxEnchoPeriods: cell.maxEncho, players: [], }, }); @@ -152,8 +149,8 @@ describe('TeamScoreEditorModal config matrix (running match, admin surface)', () expect(letters).toEqual(cell.naginata ? NAGINATA_SET : KENDO_SET); // Encho affordance: always present, collapsed to the pill while no - // overtime is active. maxEnchoPeriods only caps the stepper (covered by - // the focused cap tests below). + // overtime is active. The stepper itself is unbounded above (mp-m4bn, + // covered by the focused stepper test below). expect(screen.queryByTestId('scoring-modal-encho-pill')).not.toBeNull(); // Daihyosen affordance is knockout-only (T141): phase "bracket", or the @@ -209,7 +206,7 @@ describe('TeamScoreEditorModal IMPOSSIBLE CELLS (asserted, not skipped)', () => it.each(IMPOSSIBLE_FORMAT_PHASES.map(fp => [`${fp.format} × phase "${fp.phase}"`, fp]))( '%s: product-impossible; the editor trusts the phase stamp', async (_name, fp) => { - await renderCell({ ...fp, teamSize: 5, tmt: 'fixed', naginata: false, maxEncho: 0 }); + await renderCell({ ...fp, teamSize: 5, tmt: 'fixed', naginata: false }); expect(!!screen.queryByTestId('scoring-modal-daihyosen-button')).toBe(IMPOSSIBLE_EXPECT_DAIHYOSEN[cellKey(fp)]); } ); @@ -218,7 +215,7 @@ describe('TeamScoreEditorModal IMPOSSIBLE CELLS (asserted, not skipped)', () => describe('TeamScoreEditorModal selfReport (public self-run surface)', () => { it('selfReport hides the admin decision controls', async () => { await renderCell( - { format: 'mixed', phase: 'pool', teamSize: 5, tmt: 'fixed', naginata: false, maxEncho: 0 }, + { format: 'mixed', phase: 'pool', teamSize: 5, tmt: 'fixed', naginata: false }, {}, { selfReport: true } ); @@ -229,7 +226,7 @@ describe('TeamScoreEditorModal selfReport (public self-run surface)', () => { }); }); -describe('TeamScoreEditorModal encho cap (maxEnchoPeriods)', () => { +describe('TeamScoreEditorModal encho stepper is unbounded (mp-m4bn)', () => { async function expandEncho() { await act(async () => { fireEvent.click(screen.getByTestId('scoring-modal-encho-pill')); @@ -239,24 +236,15 @@ describe('TeamScoreEditorModal encho cap (maxEnchoPeriods)', () => { }); } - it('maxEncho=2: the + stepper disables at the cap and the max banner shows', async () => { - await renderCell({ format: 'mixed', phase: 'bracket', teamSize: 5, tmt: 'fixed', naginata: false, maxEncho: 2 }); + it('the + stepper never disables, however many periods were fought', async () => { + // The shimpan decide how many overtime periods are fought and how a + // still-tied encounter is resolved (daihyosen for a team match). The + // operator only records it, so the UI must never refuse a period that + // actually happened, and there is no "maximum reached" alert to show. + await renderCell({ format: 'mixed', phase: 'bracket', teamSize: 5, tmt: 'fixed', naginata: false }); await expandEncho(); const inc = screen.getByRole('button', { name: 'Increase overtime period count' }); - expect(inc.disabled).toBe(false); // ×1 < cap - await act(async () => { fireEvent.click(inc); }); // ×2 = cap - expect(inc.disabled).toBe(true); - expect(screen.getByRole('alert').textContent).toContain('Maximum encho periods reached'); - }); - - it('maxEncho=0 means UNCAPPED: the + stepper never disables', async () => { - // canIncrementEncho treats 0 as "no cap". mp-m4bn tracks the fact that no - // UI or import path can currently SET maxEnchoPeriods, so 0/uncapped is - // the only reachable state in practice. - await renderCell({ format: 'mixed', phase: 'bracket', teamSize: 5, tmt: 'fixed', naginata: false, maxEncho: 0 }); - await expandEncho(); - const inc = screen.getByRole('button', { name: 'Increase overtime period count' }); - for (let i = 0; i < 4; i++) { + for (let i = 0; i < 6; i++) { expect(inc.disabled).toBe(false); await act(async () => { fireEvent.click(inc); }); } @@ -266,7 +254,7 @@ describe('TeamScoreEditorModal encho cap (maxEnchoPeriods)', () => { }); describe('TeamScoreEditorModal kachinuki bout navigation', () => { - const KACHI_CELL = { format: 'playoffs', phase: 'bracket', teamSize: 5, tmt: 'kachinuki', naginata: false, maxEncho: 0 }; + const KACHI_CELL = { format: 'playoffs', phase: 'bracket', teamSize: 5, tmt: 'kachinuki', naginata: false }; it('RUNNING: only the current bout renders; the operator CANNOT navigate back to a scored earlier bout', async () => { // Server bout log: bout 1 fought (won by A1), bout 2 appended by diff --git a/web-mobile/js/admin_competition_swiss.jsx b/web-mobile/js/admin_competition_swiss.jsx index c0727deec..04acfaeff 100644 --- a/web-mobile/js/admin_competition_swiss.jsx +++ b/web-mobile/js/admin_competition_swiss.jsx @@ -8,8 +8,8 @@ const { useState: useStateA, useEffect: useEffectA, useRef: useRefA } = React; // section. Extracted so the conditional logic ("which round, are // matches complete, can we generate next?") is unit-testable without // mounting AdminSwissRounds. Mirrors the admin_scoring_modal.jsx -// pattern (buildDecisionBody / shouldShowEnchoMaxBanner pure helpers -// exported for tests). +// pattern (buildDecisionBody / nextEnchoPeriod pure helpers exported +// for tests). // Returns the canonical match-ID prefix for a Swiss round. Matches // engine/swiss.go's `swissPoolName`/`swissMatchID`. Keep in sync. diff --git a/web-mobile/js/admin_pools.jsx b/web-mobile/js/admin_pools.jsx index 459f6840d..7792aab80 100644 --- a/web-mobile/js/admin_pools.jsx +++ b/web-mobile/js/admin_pools.jsx @@ -31,7 +31,7 @@ function poolMatchesForPool(poolMatches, poolName) { // raw MatchResult shape (id, status, sides, ippons, decision) with none // of the comp-level fields the modal needs: // * compKind / teamSize: picks TeamScoreEditorModal vs individual editor -// * compId: fetches competition details (maxEnchoPeriods, naginata) +// * compId: fetches competition details (naginata) // and is the path for decision/score endpoints // * compName: header eyebrow // * phase / poolName: header subtitle ("CompName · PoolName") diff --git a/web-mobile/js/admin_scoring_individual.jsx b/web-mobile/js/admin_scoring_individual.jsx index ce5f1f2f1..2feaa4527 100644 --- a/web-mobile/js/admin_scoring_individual.jsx +++ b/web-mobile/js/admin_scoring_individual.jsx @@ -94,12 +94,8 @@ export function ScoreEditorModal({ match, onClose, onSubmit, onSubmitAndNext, on // 4xx on retry): the write never landed, so we must show an explicit "not // saved" failure state rather than let the pending banner clear to "saved". const [writeFailed, setWriteFailed] = useStateA(null); // { reason } | null - // T104/CHK029: MaxEnchoPeriods cap from the competition config. - // Fetched once on open so the warning banner can fire before the - // operator submits (the server validates the same cap on PUT /score). - const [maxEnchoPeriods, setMaxEnchoPeriods] = useStateA(0); // Naginata competitions add an extra "S" (Sune) ippon button. - // Fetched from the competition config alongside maxEnchoPeriods. + // Fetched from the competition config on open. const [isNaginata, setIsNaginata] = useStateA(false); // Engi competitions use flag-count scoring; dispatched to EngiScoreEditorModal. // Derived synchronously from m.compEngi (stamped at enrichment time by @@ -150,7 +146,6 @@ export function ScoreEditorModal({ match, onClose, onSubmit, onSubmitAndNext, on let cancelled = false; window.API.fetchCompetitionDetails(m.compId).then(d => { if (!cancelled) { - setMaxEnchoPeriods(d?.config?.maxEnchoPeriods || 0); setIsNaginata(!!d?.config?.naginata); } }).catch(() => {}); @@ -677,7 +672,6 @@ export function ScoreEditorModal({ match, onClose, onSubmit, onSubmitAndNext, on diff --git a/web-mobile/js/admin_scoring_modal.jsx b/web-mobile/js/admin_scoring_modal.jsx index 011c70928..354ed806e 100644 --- a/web-mobile/js/admin_scoring_modal.jsx +++ b/web-mobile/js/admin_scoring_modal.jsx @@ -38,8 +38,6 @@ import { buildDecisionBody, submitDecisionRequest, makeSubmitDecision, - shouldShowEnchoMaxBanner, - canIncrementEncho, nextEnchoPeriod, prevEnchoPeriod, initialEnchoPeriodsForMatch, @@ -62,8 +60,6 @@ export { buildDecisionBody, submitDecisionRequest, makeSubmitDecision, - shouldShowEnchoMaxBanner, - canIncrementEncho, nextEnchoPeriod, prevEnchoPeriod, initialEnchoPeriodsForMatch, diff --git a/web-mobile/js/admin_scoring_shared.jsx b/web-mobile/js/admin_scoring_shared.jsx index 9dbf8104c..5d000fa1b 100644 --- a/web-mobile/js/admin_scoring_shared.jsx +++ b/web-mobile/js/admin_scoring_shared.jsx @@ -234,9 +234,8 @@ function assertRunningWritePersisted(saveRes) { // T093/T094: build the /decision POST body. Pure helper so we can pin the // wire shape (decision/decisionBy/decisionReason/encho/force) against a -// moving server contract. `force` is the T103/T104 override flag used when -// the server replies decision_locked or max_encho_exceeded and the operator -// confirms the override. +// moving server contract. `force` is the T103 override flag used when the +// server replies decision_locked and the operator confirms the override. function buildDecisionBody(kind, { decisionBy, decisionReason }, enchoPeriodCount, opts = {}) { const body = { decision: kind, decisionBy }; if (decisionReason) body.decisionReason = decisionReason; @@ -251,7 +250,7 @@ function buildDecisionBody(kind, { decisionBy, decisionReason }, enchoPeriodCoun // test pins the production call site (rather than re-implementing the chain // inside the test, which was how the original gap slipped through). Returns // the promise from window.API.recordDecision so callers can await + handle -// the 409 decision_locked / max_encho_exceeded retry-with-force loop. +// the 409 decision_locked retry-with-force loop. function submitDecisionRequest(compId, matchId, kind, decisionPayload, enchoPeriodCount, password, opts = {}) { const body = buildDecisionBody(kind, decisionPayload, enchoPeriodCount, opts); return window.API.recordDecision(compId, matchId, body, resolveDecisionPassword(password)); @@ -268,8 +267,8 @@ function submitDecisionRequest(compId, matchId, kind, decisionPayload, enchoPeri // - for fusenpai / other non-kiken decisions: calls onAfterDecision when // provided and the match is not a correction (item 7: starts next match), // else falls back to onClose; -// - on 409 decision_locked / max_encho_exceeded, confirms then retries with -// force (recursing into itself). +// - on 409 decision_locked, confirms then retries with force (recursing +// into itself). // Call it fresh each render so it captures the current enchoPeriodCount/password. function makeSubmitDecision({ match, @@ -352,18 +351,6 @@ function makeSubmitDecision({ if (!mountedRef.current) return; if (ok) { await submit(kind, { decisionBy, decisionReason }, { force: true }); return; } setDecisionErr('Override cancelled.'); - } else if (!opts.force && /max_encho_exceeded/i.test(msg)) { - // T104/CHK029: encho-cap override, same confirm-and-retry shape. - const ok = mountedRef.current && await window.confirmDialog({ - message: - 'This decision would exceed the configured maximum encho periods.\n\n' + - 'Record it anyway?', - confirmLabel: 'Record anyway', - danger: true, - }); - if (!mountedRef.current) return; - if (ok) { await submit(kind, { decisionBy, decisionReason }, { force: true }); return; } - setDecisionErr('Override cancelled.'); } else if (mountedRef.current) { setDecisionErr(msg); } @@ -374,25 +361,13 @@ function makeSubmitDecision({ return submit; } -// T104/CHK029: encho-period clamp + banner predicates. maxEnchoPeriods === 0 -// (or nullish) means unlimited per the FIK default -// (state.CompetitionConfig.MaxEnchoPeriods). shouldShowEnchoMaxBanner -// surfaces the "Maximum encho periods reached" warning once the operator -// has incremented to the cap; the + button uses canIncrementEncho to gate -// further increments client-side (the server enforces the same cap on PUT -// /score → 409 max_encho_exceeded). -function shouldShowEnchoMaxBanner(enchoPeriodCount, maxEnchoPeriods) { - if (!maxEnchoPeriods || maxEnchoPeriods <= 0) return false; - return enchoPeriodCount >= maxEnchoPeriods; -} - -function canIncrementEncho(enchoPeriodCount, maxEnchoPeriods) { - if (!maxEnchoPeriods || maxEnchoPeriods <= 0) return true; - return enchoPeriodCount < maxEnchoPeriods; -} - -function nextEnchoPeriod(current, maxEnchoPeriods) { - return canIncrementEncho(current, maxEnchoPeriods) ? current + 1 : current; +// mp-m4bn: encho periods are UNBOUNDED. How many overtime periods a match +// runs, and how a still-tied match is finally resolved (hantei for an +// individual bout, daihyosen for a team encounter), is the shimpan's call, +// not the software's. The operator records what was fought, so the stepper +// only guards the lower bound; there is no maximum to clamp against. +function nextEnchoPeriod(current) { + return current + 1; } function prevEnchoPeriod(current) { @@ -451,10 +426,9 @@ function shouldBlockScoringKeys({ decidedByHantei }) { // it occupies <24px of vertical space in the live scoring modal. The // full counter UI mounts only when overtime is active (enchoPeriodCount // > 0) OR the operator clicks the pill (local showCounter state). The -// counter is the existing −/×N/+ stepper plus the "Maximum encho -// periods reached" warning, preserved verbatim. Used by both -// ScoreEditorModal and TeamScoreEditorModal. -function EnchoControl({ enchoPeriodCount, setEnchoPeriodCount, maxEnchoPeriods }) { +// counter is the −/×N/+ stepper, unbounded above (see nextEnchoPeriod). +// Used by both ScoreEditorModal and TeamScoreEditorModal. +function EnchoControl({ enchoPeriodCount, setEnchoPeriodCount }) { const [showCounter, setShowCounter] = useStateA(enchoPeriodCount > 0); const expanded = showCounter || enchoPeriodCount > 0; if (!expanded) { @@ -501,17 +475,11 @@ function EnchoControl({ enchoPeriodCount, setEnchoPeriodCount, maxEnchoPeriods } )} - {shouldShowEnchoMaxBanner(enchoPeriodCount, maxEnchoPeriods) && ( - - Maximum encho periods reached - - )} ); } @@ -913,8 +881,6 @@ export { buildDecisionBody, submitDecisionRequest, makeSubmitDecision, - shouldShowEnchoMaxBanner, - canIncrementEncho, nextEnchoPeriod, prevEnchoPeriod, initialEnchoPeriodsForMatch, diff --git a/web-mobile/js/admin_scoring_team.jsx b/web-mobile/js/admin_scoring_team.jsx index 24be08072..cdd83554f 100644 --- a/web-mobile/js/admin_scoring_team.jsx +++ b/web-mobile/js/admin_scoring_team.jsx @@ -354,7 +354,6 @@ export function TeamScoreEditorModal({ match, teamSize, onClose, onSubmit, onSub // from match-level compFormat (when set by compMatches) or the comp // fetch fallback. Phase === "bracket" is the in-modal signal. const compFormat = m.compFormat || compMeta?.config?.format || ""; - const maxEnchoPeriods = compMeta?.config?.maxEnchoPeriods || 0; const isNaginataTeam = !!compMeta?.config?.naginata; // Knockout phase = a bracket match. A POOL match is never knockout, even in a // mixed/playoffs competition: pool team matches may legitimately draw @@ -827,7 +826,6 @@ export function TeamScoreEditorModal({ match, teamSize, onClose, onSubmit, onSub {/* Team header */}
From 1a466049acc17553b112f23d59f9c3fcaa43d6a5 Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 13:28:40 +0100 Subject: [PATCH 02/25] feat(mp-m4bn): surface the encho period count in result displays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every consumer of Encho.PeriodCount tested it as a boolean (> 0), so the magnitude was consumed by nothing except the stepper that produced it: a match that ran three overtime periods rendered identically to one settled in the first, and the count FR-032 mandates persisting was never shown anywhere a result is actually read. The overtime marker now carries the count when more than one period ran: "(E)" for a single period, "(E×N)" for N > 1. One period keeps the bare marker because it is the common case and the terser form keeps narrow bracket nodes and Excel cells readable. This is what makes the operator's stepper taps worth recording. It is the counterpart to removing the cap: the software should not REFEREE how many overtime periods are fought, but it should faithfully RECORD how many were, and show it. Both languages duplicated the "(E)" literal across two call sites, so the logic is extracted to a single enchoLabel() per side (internal/export/ suffix.go and web-mobile/js/bracket.jsx) rather than patched in four places. All downstream surfaces inherit it: the bracket node chip, viewer and TV displays via decisionSuffix, the schedule page and OBS lower-third via formatIpponsScore, and the results workbook via DecisionSuffix. Two existing tests used multi-period fixtures while asserting the boolean-collapsed "(E)" and went red on this change, which is the pinning signal; both are updated and explicit ×N cases added on each side. Visual Rendering Contract updated in contracts/match-decisions.md. --- internal/export/suffix.go | 30 +++++++++++++++---- internal/export/suffix_test.go | 10 +++++-- .../js/__tests__/score_display.test.jsx | 16 ++++++++-- web-mobile/js/bracket.jsx | 24 +++++++++++---- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/internal/export/suffix.go b/internal/export/suffix.go index cfd6105f0..207af452a 100644 --- a/internal/export/suffix.go +++ b/internal/export/suffix.go @@ -21,7 +21,7 @@ import ( // // Composition order: // 1. Base decision label: kiken variants -> "Kiken"; fusenpai/fusensho -> "Fus."; daihyosen -> "DH". -// 2. If enchoOn -> append " (E)". +// 2. If encho -> append " (E)", or " (E×N)" when more than one period ran. // 3. If hanteiOn -> append " Ht". // // DELIBERATE DIVERGENCE from the JS: the JS omits fusensho (the per-bout default @@ -33,7 +33,7 @@ import ( // A zero/nil Encho (or PeriodCount == 0) is treated as no encho. // Returns "" when no suffix applies. func DecisionSuffix(decision string, encho *state.EnchoMetadata, decidedByHantei bool) string { - enchoOn := encho != nil && encho.PeriodCount > 0 + enchoSfx := enchoLabel(encho) var suffix string switch { @@ -45,11 +45,11 @@ func DecisionSuffix(decision string, encho *state.EnchoMetadata, decidedByHantei suffix = "DH" } - if enchoOn { + if enchoSfx != "" { if suffix != "" { - suffix += " (E)" + suffix += " " + enchoSfx } else { - suffix = "(E)" + suffix = enchoSfx } } @@ -64,6 +64,26 @@ func DecisionSuffix(decision string, encho *state.EnchoMetadata, decidedByHantei return suffix } +// enchoLabel renders the overtime marker for an encho block: "" when no +// overtime ran, "(E)" for a single period, and "(E×N)" for N > 1. +// +// mp-m4bn: every consumer used to treat PeriodCount as a boolean, so a match +// that took three overtime periods was indistinguishable from one settled in +// the first and the count was write-only. Surfacing N is what makes the +// operator's stepper taps worth recording. One period stays the bare "(E)": +// it is the common case and the terser marker keeps narrow Excel cells and +// bracket nodes readable. Mirrors enchoLabel() in web-mobile/js/bracket.jsx +// and the editors' "· (E) Overtime ×N" eyebrow. Keep the three in sync. +func enchoLabel(encho *state.EnchoMetadata) string { + if encho == nil || encho.PeriodCount <= 0 { + return "" + } + if encho.PeriodCount > 1 { + return "(E×" + strconv.Itoa(encho.PeriodCount) + ")" + } + return "(E)" +} + // MiddleCellText composes the value for a match's centre "vs" cell from the // hikiwake draw marker and the decision suffix. When a match is a draw AND also // carries a suffix (a scoreless encho draw -> "X (E)", a hantei-decided draw -> diff --git a/internal/export/suffix_test.go b/internal/export/suffix_test.go index 1675e3b5f..470ca7d0a 100644 --- a/internal/export/suffix_test.go +++ b/internal/export/suffix_test.go @@ -39,15 +39,19 @@ func TestDecisionSuffix(t *testing.T) { // Daihyosen {name: "daihyosen", decision: "daihyosen", encho: nil, hantei: false, want: "DH"}, - // Encho only + // Encho only. mp-m4bn: one period renders the bare "(E)"; two or + // more carry the count so a three-overtime result is not displayed + // identically to one settled in the first period. {name: "encho only (fought)", decision: "fought", encho: encho(1), hantei: false, want: "(E)"}, {name: "encho nil vs zero periods", decision: "fought", encho: encho(0), hantei: false, want: ""}, + {name: "encho x2 carries the count", decision: "fought", encho: encho(2), hantei: false, want: "(E×2)"}, + {name: "encho x7 carries the count", decision: "fought", encho: encho(7), hantei: false, want: "(E×7)"}, // Hantei only {name: "hantei only (fought)", decision: "fought", encho: nil, hantei: true, want: "Ht"}, // Encho + hantei - {name: "encho + hantei (fought)", decision: "fought", encho: encho(2), hantei: true, want: "(E) Ht"}, + {name: "encho + hantei (fought)", decision: "fought", encho: encho(2), hantei: true, want: "(E×2) Ht"}, // Base label + encho {name: "Kiken + encho", decision: "kiken-voluntary", encho: encho(1), hantei: false, want: "Kiken (E)"}, @@ -60,7 +64,7 @@ func TestDecisionSuffix(t *testing.T) { // Full composition: base + encho + hantei {name: "Kiken + encho + hantei", decision: "kiken-voluntary", encho: encho(1), hantei: true, want: "Kiken (E) Ht"}, - {name: "DH + encho + hantei", decision: "daihyosen", encho: encho(3), hantei: true, want: "DH (E) Ht"}, + {name: "DH + encho + hantei", decision: "daihyosen", encho: encho(3), hantei: true, want: "DH (E×3) Ht"}, // Hikiwake (draw) produces no base label; suffix still applies {name: "hikiwake + hantei", decision: "hikiwake", encho: nil, hantei: true, want: "Ht"}, diff --git a/web-mobile/js/__tests__/score_display.test.jsx b/web-mobile/js/__tests__/score_display.test.jsx index dc16481d4..0aaa31179 100644 --- a/web-mobile/js/__tests__/score_display.test.jsx +++ b/web-mobile/js/__tests__/score_display.test.jsx @@ -138,14 +138,26 @@ describe('formatIpponsScore', () => { expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null, { periodCount: 1 })).toBe('M–K (E)'); }); - it('appends (E) to a no-score draw (X is the scoreless-draw glyph)', () => { - expect(formatIpponsScore([], [], null, 'hikiwake', { periodCount: 2 })).toBe('X (E)'); + it('appends the marker to a no-score draw (X is the scoreless-draw glyph)', () => { + expect(formatIpponsScore([], [], null, 'hikiwake', { periodCount: 2 })).toBe('X (E×2)'); }); it('does not append (E) when periodCount is 0', () => { expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 0 })).toBe('M–K'); }); + // mp-m4bn: one period stays the bare "(E)" (the common case, kept terse + // for narrow bracket nodes); two or more carry the count so a result that + // took three overtimes is distinguishable from one settled in the first. + it('carries the period count when more than one overtime ran', () => { + expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 2 })).toBe('M–K (E×2)'); + expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 5 })).toBe('M–K (E×5)'); + }); + + it('composes the counted marker with a decision label and hantei', () => { + expect(formatIpponsScore([], [], null, 'daihyosen', { periodCount: 3 }, true)).toBe('DH (E×3) Ht'); + }); + it('is a no-op when encho argument is missing entirely', () => { expect(formatIpponsScore(['M'], ['K'], null, null)).toBe('M–K'); }); diff --git a/web-mobile/js/bracket.jsx b/web-mobile/js/bracket.jsx index 8fd6e121e..e5bbdce1c 100644 --- a/web-mobile/js/bracket.jsx +++ b/web-mobile/js/bracket.jsx @@ -51,15 +51,29 @@ function sideLabel(side) { // a separate bout badge, not by this helper. Pure and DOM-free so it can be // reused by display.jsx (which builds its own scoreline) without dragging in // the rest of formatIpponsScore's bye/hantei special cases. +// mp-m4bn: the overtime marker carries the period COUNT when a match ran more +// than one encho period: "" / "(E)" / "(E×3)". Every consumer used to treat +// periodCount as a boolean, so a match that took three overtimes looked +// identical to one settled in the first and the count was write-only. +// A single period keeps the bare "(E)" — the common case, and the terser +// marker keeps narrow bracket nodes and Excel cells readable. Mirrors +// enchoLabel() in internal/export/suffix.go and the score editors' +// "· (E) Overtime ×N" eyebrow. Keep the three in sync. +function enchoLabel(encho) { + const n = encho?.periodCount || 0; + if (n <= 0) return ""; + return n > 1 ? `(E×${n})` : "(E)"; +} + function decisionSuffix(match) { if (!match) return ""; const d = match.decision || ""; - const enchoOn = !!(match.encho && match.encho.periodCount > 0); + const enchoSfx = enchoLabel(match.encho); const hanteiOn = !!match.decidedByHantei; let suffix = ""; if (isKikenDecisionBC(d)) suffix = "Kiken"; else if (DECISION_CHIPS[d]) suffix = DECISION_CHIPS[d].label; - if (enchoOn) suffix = (suffix ? suffix + " " : "") + "(E)"; + if (enchoSfx) suffix = (suffix ? suffix + " " : "") + enchoSfx; // FIK 7-5 / 29-6: judges' decision after a tied encho. Mark explicitly so // a hantei-decided final is distinguishable from an ippon-derived one // (audit + Excel + viewer parity). @@ -99,8 +113,8 @@ function formatIpponsScore(ipponsA, ipponsB, score, decision, encho, decidedByHa // lets callers that omit the arg safely get false without sending undefined. const hantei = typeof decidedByHantei === "boolean" ? decidedByHantei : false; const decSfx = decisionSuffix({ decision, encho, decidedByHantei: hantei }); - const enchoOnly = (encho && encho.periodCount > 0) ? " (E)" : ""; - const suffix = decSfx ? " " + decSfx : enchoOnly; + const enchoSfx = enchoLabel(encho); + const suffix = decSfx ? " " + decSfx : (enchoSfx ? " " + enchoSfx : ""); if (score?.type === "bye") return "BYE"; const aStr = (ipponsA || []).filter(x => x && x !== "•").join(""); const bStr = (ipponsB || []).filter(x => x && x !== "•").join(""); @@ -270,7 +284,7 @@ const MatchCard = React.memo(({ match, variant, showDojo, onClick, highlighted, {running ? ● NOW : null} {isBye ? BYE : null} {match.score?.type === "hikiwake" ? X : null} - {match.encho?.periodCount > 0 ? (E) : null} + {match.encho?.periodCount > 0 ? {enchoLabel(match.encho)} : null} {match.decidedByHantei ? Ht : null} {isKikenDecisionBC(match.decision) ? ( Kiken From cd795a6bf21dd8d35abd3571604c586b32c71d8e Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 13:45:00 +0100 Subject: [PATCH 03/25] =?UTF-8?q?docs(mp-m4bn):=20document=20encho=20overt?= =?UTF-8?q?ime=20and=20the=20(E)/(E=C3=97N)=20result=20marker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The court-operator decision guide covered kiken, fusenpai/fusensho, hikiwake, daihyosen and chusen, but never encho, so the overtime toggle and its result marker were undocumented. Documents the unbounded period counter (the shimpan decide how many periods are fought and how a still-level match is settled) and the (E) / (E×N) marker, including how it combines with other tags. --- docs/user-guide/court-operators/recording-decisions.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/user-guide/court-operators/recording-decisions.md b/docs/user-guide/court-operators/recording-decisions.md index 34295571e..477530dc8 100644 --- a/docs/user-guide/court-operators/recording-decisions.md +++ b/docs/user-guide/court-operators/recording-decisions.md @@ -26,6 +26,14 @@ An injury kiken can be reinstated later by the operator if the competitor recove A hikiwake is a draw. It applies in pool, league, and Swiss matches (not in the knockout phase) and contributes to the standings separately from wins and losses. +## Encho (overtime) + +Encho is the extra period played when a knockout match is level at the end of regulation. It follows ippon-shobu rules: the first competitor to score wins. + +In the score editor, open the **Overtime** control and tick **Encho started**. A counter appears so you can record how many overtime periods were fought, using the **+** and **-** buttons. The counter starts at 1 and has no upper limit: how many periods are fought, and how a match still level after them is finally settled (a judges' decision for an individual bout, a daihyosen for a team encounter), is the shimpan's call. Record what actually happened on court, however many periods that took. + +Completed results carry an overtime marker. A match settled in a single overtime period shows **(E)**. A match that needed more shows the count, so a result decided in the third overtime reads **(E×3)**. The marker appears on the bracket, the court console, the public viewer, and the exported results workbook, and it combines with any other tag: a withdrawal during a second overtime period reads **Kiken (E×2)**, and a match sent to a judges' decision after three reads **(E×3) Ht**. + ## Daihyosen A daihyosen is a representative bout used to break a tie when points and ranking criteria cannot separate two teams. From 9eab37737763cec66a2c59030771552e07b94264 Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 14:09:40 +0100 Subject: [PATCH 04/25] chore(mp-m4bn): ignore root PNGs and repair the dead specs screenshot negation Two ignore-rule gaps surfaced while capturing this PR's screenshots. The Playwright MCP writes browser_take_screenshot output to the repo/worktree root, and loose root PNGs were not ignored, so a `git add -A` would sweep a screenshot into a commit. The three captures for this PR sat untracked at the root for exactly that reason. Anchor the rule at the root (`/*.png`): a blanket `*.png` would silently swallow new docs/screenshots/, docs/assets/images/ and specs/*/screenshots/ files, all of which are tracked (30 PNGs today). Separately, the `!specs/*/screenshots/` negation was dead. Git cannot re-include a file whose parent directory is excluded, and `specs/*` excluded the spec directories outright, so git never descended into them and the negation could not fire. The 11 UAT screenshots stayed visible only because they were already tracked; a newly added one would have been ignored, defeating the stated intent of the comment above it. Re-including the directories with `!specs/*/` lets git descend, while `specs/*/*` still excludes their contents. Verified with git check-ignore: root PNGs ignored; docs/assets, docs/screenshots, specs/*/screenshots and specs/openapi.yaml tracked; specs plan.md and contracts/ still ignored; all 30 tracked PNGs unaffected; no previously-hidden file becomes visible in either the worktree or the main checkout. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 4b15e01cf..d036e5990 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,9 @@ node_modules/ # rule above would not match. web-mobile/node_modules specs/* +# Re-include the spec directories themselves so git descends into them; a +# directory excluded outright can never have its contents re-included below. +!specs/*/ specs/*/* !specs/openapi.yaml # UAT screenshot evidence (review.md V5): per-task PNGs captured during browser @@ -85,6 +88,12 @@ specs/*/* .specify .agent* .playwright-mcp +# The Playwright MCP writes browser_take_screenshot files to the repo/worktree +# root, where `git add -A` would sweep them into a commit. PR screenshots belong +# on the pr-assets branch, never in the tree. Root-anchored on purpose: a blanket +# *.png would silently swallow new docs/screenshots/, docs/assets/images/ and +# specs/*/screenshots/ files, which are tracked. +/*.png .claude/skills/speckit-* .claude/worktrees/* From 1ec1b82621da8bef9fb6a95040347d6d7fadb35d Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 14:33:53 +0100 Subject: [PATCH 05/25] test(mp-m4bn): drop stale comments naming the deleted encho cap (tri-review) Tri-review of PR #376 surfaced three comments still describing the removed cap machinery: - admin_scoring_modal.test.jsx header claimed the helpers implement the "T104 cap + banner" flow this PR deleted, and pinned a "seven" helper count the barrel no longer matches. - failingCompetitionStore's doc comment pointed at the deleted enforceEnchoCap / bulk-score fail-closed path; its only remaining consumer is the override-winner load-error test. - concurrency_test.go listed "LoadCompetition to check encho cap" among the contended lock steps; that per-request load is gone. Comment-only; make go/test exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/mobileapp/concurrency_test.go | 6 +++--- internal/mobileapp/handlers_match_test.go | 4 ++-- web-mobile/js/__tests__/admin_scoring_modal.test.jsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/mobileapp/concurrency_test.go b/internal/mobileapp/concurrency_test.go index ba6c96ba4..b750e9233 100644 --- a/internal/mobileapp/concurrency_test.go +++ b/internal/mobileapp/concurrency_test.go @@ -81,9 +81,9 @@ func TestConcurrentScoresPreserveOrder(t *testing.T) { RegisterMatchHandlers(admin, eng, store, store, hub, NewFileVerifier(store), store) // Goroutines all fire concurrently; each scores a different match - // so the per-comp lock is contended on every step (LoadCompetition - // to check encho cap, StartMatchTx, RecordMatchResultWithIneligibilityTx, - // MaybeAdvanceKachinuki, tryAutoCompletePools). + // so the per-comp lock is contended on every step (StartMatchTx, + // RecordMatchResultWithIneligibilityTx, MaybeAdvanceKachinuki, + // tryAutoCompletePools). var wg sync.WaitGroup wg.Add(N) for i := range N { diff --git a/internal/mobileapp/handlers_match_test.go b/internal/mobileapp/handlers_match_test.go index 0d02d2d65..0a932077a 100644 --- a/internal/mobileapp/handlers_match_test.go +++ b/internal/mobileapp/handlers_match_test.go @@ -1314,8 +1314,8 @@ func TestPostScoreKikenInvalidScoreline(t *testing.T) { } // failingCompetitionStore returns the configured error from -// LoadCompetition. Used to drive the fail-closed path in -// enforceEnchoCap / bulk-score when config.md can't be loaded. +// LoadCompetition. Used to drive the fail-closed path in the +// override-winner handler when config.md can't be loaded. type failingCompetitionStore struct{ err error } func (f failingCompetitionStore) LoadCompetition(string) (*state.Competition, error) { diff --git a/web-mobile/js/__tests__/admin_scoring_modal.test.jsx b/web-mobile/js/__tests__/admin_scoring_modal.test.jsx index 2700a052f..19b3f96e3 100644 --- a/web-mobile/js/__tests__/admin_scoring_modal.test.jsx +++ b/web-mobile/js/__tests__/admin_scoring_modal.test.jsx @@ -31,8 +31,8 @@ import { isKikenDecision } from '../api_serializers.jsx'; window.isKikenDecision = isKikenDecision; -// admin_scoring_modal.jsx ships seven module-private helpers that together -// implement the FR-033 encho-period flow (T104 cap + banner) and the +// admin_scoring_modal.jsx ships module-private helpers that together +// implement the FR-033 encho-period flow (unbounded stepper) and the // T093–T098 decision prompt path. Each helper is exported separately so // vitest can pin the behaviour without mounting either ScoreEditorModal / // TeamScoreEditorModal (those run useState/useEffect at the top and the From 7718f76eb2f0f0cb6bc8d172b1bfbad48546d65d Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 15:22:25 +0100 Subject: [PATCH 06/25] refactor(mp-m4bn): apply /simplify findings to the encho suffix path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four-lens cleanup pass (reuse / simplification / efficiency / altitude) over the PR diff. Two confirmed findings, both applied: - bracket.jsx formatIpponsScore: drop the unreachable bare-"(E)" fallback. decisionSuffix() already embeds the encho marker unconditionally, so decSfx can never be empty while enchoLabel(encho) is non-empty; the second encho path was dead and the T097 comment now names decisionSuffix() as the single source of the composed suffix. - admin_scoring_shared.jsx: pass nextEnchoPeriod directly to the state setter instead of wrapping it in an identity arrow. The helper itself stays: it is the exported test seam paired with the clamping prevEnchoPeriod and carries the no-maximum design comment. Skipped: the editors' duplicated "(E) Overtime ×N" eyebrow spans are pre-existing lines outside this diff with a deliberately more verbose format than the compact chip. make go/test exit 0; vitest 2406/2406. Co-Authored-By: Claude Opus 4.8 (1M context) --- web-mobile/js/admin_scoring_shared.jsx | 2 +- web-mobile/js/bracket.jsx | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/web-mobile/js/admin_scoring_shared.jsx b/web-mobile/js/admin_scoring_shared.jsx index 5d000fa1b..8e7a63c9b 100644 --- a/web-mobile/js/admin_scoring_shared.jsx +++ b/web-mobile/js/admin_scoring_shared.jsx @@ -475,7 +475,7 @@ function EnchoControl({ enchoPeriodCount, setEnchoPeriodCount }) {
diff --git a/web-mobile/js/bracket.jsx b/web-mobile/js/bracket.jsx index e5bbdce1c..84ea17e85 100644 --- a/web-mobile/js/bracket.jsx +++ b/web-mobile/js/bracket.jsx @@ -105,16 +105,15 @@ function ipponsFromScore(scoreStr) { // // T097: kiken / fusenpai / daihyosen append labelled suffixes alongside the // encho marker: wired through decisionSuffix() so the same string is used -// by display.jsx's hand-rolled score block. The decision-derived suffix -// supersedes the bare " (E)" so we don't double-print "(E)" alongside -// "Kiken (E)". +// by display.jsx's hand-rolled score block. decisionSuffix() is the single +// source of the composed suffix — it already embeds the encho marker, so +// there is no separate bare-"(E)" path here. function formatIpponsScore(ipponsA, ipponsB, score, decision, encho, decidedByHantei) { // decidedByHantei (positional) is the canonical flag. The `typeof` guard // lets callers that omit the arg safely get false without sending undefined. const hantei = typeof decidedByHantei === "boolean" ? decidedByHantei : false; const decSfx = decisionSuffix({ decision, encho, decidedByHantei: hantei }); - const enchoSfx = enchoLabel(encho); - const suffix = decSfx ? " " + decSfx : (enchoSfx ? " " + enchoSfx : ""); + const suffix = decSfx ? " " + decSfx : ""; if (score?.type === "bye") return "BYE"; const aStr = (ipponsA || []).filter(x => x && x !== "•").join(""); const bStr = (ipponsB || []).filter(x => x && x !== "•").join(""); From a3c452fc3b6cf850954d7c165e0fd9ec8129d596 Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 15:32:10 +0100 Subject: [PATCH 07/25] refactor(mp-m4bn): apply /simplify round-2 findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second four-lens pass over the PR diff (reuse and efficiency came back clean). Five findings applied: - bracket.jsx MatchCard: gate the encho badge on enchoLabel's own emptiness instead of a separate periodCount > 0 test, so the guard and the label cannot disagree. - bracket.jsx: update the FR-033 comment still describing the pre-PR bare-"(E)" marker; it now names the (E)/(E×N) contract. - admin_competition_bracket.jsx: drop "maxEncho" from the enrichment comment (the fetch no longer reads it) — twin of the admin_pools.jsx fix already in the diff. - validation.go: drop the "bypassing the cap check" clause from the negative-count guard rationale; the guard's real justification stands. - admin_scoring_modal.test.jsx: delete the duplicate "+ never clamps" case from the floor-invariants block; the nextEnchoPeriod unit block pins the identical loop (and more). Skipped: genericizing the docs' composed-tag examples (concrete operator examples are deliberate); inlining the single-use expandEncho test helper (harmless). make go/test exit 0; vitest 2405/2405. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/mobileapp/validation.go | 2 +- web-mobile/js/__tests__/admin_scoring_modal.test.jsx | 11 +++-------- web-mobile/js/admin_competition_bracket.jsx | 2 +- web-mobile/js/bracket.jsx | 10 ++++++---- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/internal/mobileapp/validation.go b/internal/mobileapp/validation.go index 72c79c4e2..5f6567c13 100644 --- a/internal/mobileapp/validation.go +++ b/internal/mobileapp/validation.go @@ -167,7 +167,7 @@ func validateURLHasHost(field, val string) error { func validateSubBout(prefix string, sr *state.SubMatchResult) error { // Encho period counts are bounded two ways. A negative count is never // valid on any bout (it would slip past the > 0 guards below and be - // silently treated as "no encho", bypassing the cap check). On a + // silently treated as "no encho"). On a // numbered bout, ANY non-zero count is rejected, a regular bout has // fixed regulation time and cannot go to overtime; only the daihyosen // representative bout (Position == -1) may carry encho. diff --git a/web-mobile/js/__tests__/admin_scoring_modal.test.jsx b/web-mobile/js/__tests__/admin_scoring_modal.test.jsx index 19b3f96e3..d3e8ba46d 100644 --- a/web-mobile/js/__tests__/admin_scoring_modal.test.jsx +++ b/web-mobile/js/__tests__/admin_scoring_modal.test.jsx @@ -507,14 +507,9 @@ describe('+ / − encho button behaviour (mp-m4bn floor-only invariants)', () => // the − button must not drop below 1. These compose the nextEnchoPeriod / // prevEnchoPeriod helpers. - it('+ button: repeated clicks keep climbing, never clamp', () => { - let count = 1; - for (let i = 0; i < 10; i++) { - count = nextEnchoPeriod(count); - } - expect(count).toBe(11); - }); - + // The + button's no-upper-clamp behaviour is pinned by the + // nextEnchoPeriod unit block above; this block keeps only the floor + // and symmetry invariants the pair composes into. it('− button: repeated clicks bottom out at 1, not 0', () => { let count = 3; for (let i = 0; i < 10; i++) { diff --git a/web-mobile/js/admin_competition_bracket.jsx b/web-mobile/js/admin_competition_bracket.jsx index 87365bd96..af0c878c2 100644 --- a/web-mobile/js/admin_competition_bracket.jsx +++ b/web-mobile/js/admin_competition_bracket.jsx @@ -254,7 +254,7 @@ function AdminBracket({ c, t, bracket, onMoveCourt, onEditScore, tweaks, passwor // Enrich the bracket match with the competition metadata the shared scorer // (ScoreEditorModal / MatchDetailCard) reads off the match object: the raw // bracket.rounds entries carry none of it. Mirrors enrichPoolMatchWithComp: - // compId → decision endpoints + maxEncho/naginata fetch + // compId → decision endpoints + naginata fetch // compKind / → individual vs team editor routing // teamSize // phase → "bracket" makes isKnockoutPhase true: blocks hikiwake (no diff --git a/web-mobile/js/bracket.jsx b/web-mobile/js/bracket.jsx index 84ea17e85..4b9f55224 100644 --- a/web-mobile/js/bracket.jsx +++ b/web-mobile/js/bracket.jsx @@ -99,9 +99,10 @@ function ipponsFromScore(scoreStr) { // it surfaces as an "Ht" suffix appended by decisionSuffix when // decidedByHantei=true: e.g. "M–K (E) Ht". // -// FR-033: when `encho` carries a positive periodCount, append " (E)" to the -// rendered string so operators and viewers see at a glance that the match -// went to overtime. Argument is optional and defaults to no-encho when absent. +// FR-033: when `encho` carries a positive periodCount, the overtime marker +// ("(E)" or "(E×N)", via enchoLabel) is appended so operators and viewers see +// at a glance that the match went to overtime. Argument is optional and +// defaults to no-encho when absent. // // T097: kiken / fusenpai / daihyosen append labelled suffixes alongside the // encho marker: wired through decisionSuffix() so the same string is used @@ -267,6 +268,7 @@ const MatchCard = React.memo(({ match, variant, showDojo, onClick, highlighted, const _isWatched = (typeof window !== "undefined" && window.isPlayerWatched) || (() => false); const playerHighlight = !!(highlightPlayers && (_isWatched(match.sideA, highlightPlayers) || _isWatched(match.sideB, highlightPlayers))); + const enchoBadge = enchoLabel(match.encho); return ( diff --git a/web-mobile/js/bracket.jsx b/web-mobile/js/bracket.jsx index 8c5c72fa1..8a1e3f9bc 100644 --- a/web-mobile/js/bracket.jsx +++ b/web-mobile/js/bracket.jsx @@ -40,17 +40,8 @@ function sideLabel(side) { return side === "a" ? "AKA" : "SHIRO"; } -// Decision-driven suffix appended to score strings on schedule rows, bracket -// nodes, viewer cards, and TV displays. Mirrors the Visual Rendering Contract -// in specs/003-tournament-gap-closure/contracts/match-decisions.md §Visual: -// decision == "kiken" → "Kiken" -// decision == "fusenpai" → "Fus." -// decision == "daihyosen" → "DH" -// Encho (overtime) appends " (E)" on top of any other suffix so a kiken-in- -// overtime renders "0–2 Kiken (E)". `fusensho` is per-bout only: handled by -// a separate bout badge, not by this helper. Pure and DOM-free so it can be -// reused by display.jsx (which builds its own scoreline) without dragging in -// the rest of formatIpponsScore's bye/hantei special cases. +// enchoLabel renders the overtime marker for a match's encho block: "" when no +// overtime ran, "(E)" for a single period, "(E×N)" for N > 1. // mp-m4bn: the overtime marker carries the period COUNT when a match ran more // than one encho period: "" / "(E)" / "(E×3)". Every consumer used to treat // periodCount as a boolean, so a match that took three overtimes looked @@ -67,6 +58,17 @@ function enchoLabel(encho) { return n > 1 ? `(E×${n})` : "(E)"; } +// Decision-driven suffix appended to score strings on schedule rows, bracket +// nodes, viewer cards, and TV displays. Mirrors the Visual Rendering Contract +// in specs/003-tournament-gap-closure/contracts/match-decisions.md §Visual: +// decision == "kiken" → "Kiken" +// decision == "fusenpai" → "Fus." +// decision == "daihyosen" → "DH" +// The enchoLabel marker is appended on top of any other suffix, so a kiken in +// a second overtime period renders "0–2 Kiken (E×2)". `fusensho` is per-bout +// only: handled by a separate bout badge, not by this helper. Pure and DOM-free +// so it can be reused by display.jsx (which builds its own scoreline) without +// dragging in the rest of formatIpponsScore's bye/hantei special cases. // Mirrored by DecisionSuffix in internal/export/suffix.go — keep the // composition order and single-space joins identical (its docstring records // one deliberate divergence: the export also folds fusensho into the suffix). From 847f054d8bae797ab6eea007871e0351ee02d67d Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 16:58:27 +0100 Subject: [PATCH 10/25] refactor(mp-m4bn): replace the source-grep mirror test with a shared golden table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleanup pass 5. The main finding is against round 4's own parity test. That test asserted SOURCE SUBSTRINGS of internal/export/suffix.go from the JS side (`.includes('PeriodCount > 1')`, `.toContain('"(E×"')`). Verified hole: switching Go's single-period marker to "(OT)" -- the exact divergence the case was named for -- passed green. It would also have gone red on a behaviour-preserving refactor such as fmt.Sprintf. Replaced with internal/export/testdata/encho_labels.json, one table both suites drive: TestEnchoLabel_GoldenTable table-drives the Go side, score_display.test.jsx it.each's the JS side over the same file. This pins VALUES, so it catches the drift per-language tests structurally cannot -- editing one renderer AND its own expectations in the same commit, which two independent tables both survive. Verified in both directions: mutating the Go marker reddens the Go suite, mutating the table reddens the JS suite. Also: - .gitignore: negate `*.json` for `**/testdata/*.json`. The blanket rule silently swallowed the new fixture, so it would have passed locally and failed CI on a file that never landed. No other testdata fixture was affected; the gap was latent. - bracket.jsx: decisionSuffix composes via [...].filter(Boolean).join(" "), the idiom already used in ui.jsx/display_scoreboard.jsx, replacing two copies of the ternary-space concat. Round 4 extracted joinSp on the Go side only, leaving a mirrored pair that no longer read alike; the Go docstring no longer quotes a JS shape that has since changed. - Trimmed enchoLabel's docstring in both languages (it stated the same three-branch contract three times) and the shimpan rationale duplicated across four sites down to one canonical statement plus pointers. - Merged two nextEnchoPeriod cases that pinned the same branchless property. Gates: make go/test exit 0, vitest 2408/2408, make docs/build exit 0. --- .gitignore | 4 ++ internal/export/suffix.go | 13 ++-- internal/export/suffix_test.go | 47 +++++++++++++ internal/export/testdata/encho_labels.json | 23 +++++++ .../js/__tests__/admin_scoring_modal.test.jsx | 13 ++-- .../team_editor_config_matrix.render.test.jsx | 7 +- .../js/__tests__/score_display.test.jsx | 66 ++++++++----------- web-mobile/js/bracket.jsx | 32 ++++----- 8 files changed, 129 insertions(+), 76 deletions(-) create mode 100644 internal/export/testdata/encho_labels.json diff --git a/.gitignore b/.gitignore index d036e5990..87aa49f36 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,10 @@ Thumbs.db !web-mobile/.oxlintrc.json !web/package.json !web/package-lock.json +# Go testdata fixtures. These are test INPUTS, not build output: the blanket +# *.json above would silently drop them, and a golden table that never lands +# fails CI on a checkout that works locally. +!**/testdata/*.json # Fixture data for scripts/setup_tournament.py (make mobile-app-example); # without this the demo target fails with FileNotFoundError on any checkout # other than the one that happens to have the file untracked on disk. diff --git a/internal/export/suffix.go b/internal/export/suffix.go index 2102e68c4..59ce5f9ba 100644 --- a/internal/export/suffix.go +++ b/internal/export/suffix.go @@ -54,9 +54,8 @@ func DecisionSuffix(decision string, encho *state.EnchoMetadata, decidedByHantei } // joinSp joins two display fragments with a single space, skipping empties, so -// a composed suffix never carries a leading, trailing, or doubled space. This -// is the Go spelling of the `suffix = (suffix ? suffix + " " : "") + part` -// shape decisionSuffix uses in web-mobile/js/bracket.jsx. +// a composed suffix never carries a leading, trailing, or doubled space. The JS +// mirror does the same job with [...].filter(Boolean).join(" "). func joinSp(a, b string) string { switch { case a == "": @@ -76,10 +75,10 @@ func joinSp(a, b string) string { // the first and the count was write-only. Surfacing N is what makes the // operator's stepper taps worth recording. One period stays the bare "(E)": // it is the common case and the terser marker keeps narrow Excel cells and -// bracket nodes readable. Mirrors enchoLabel() in web-mobile/js/bracket.jsx -// and the editors' "· (E) Overtime ×N" eyebrow. Keep the three in sync on -// the count-carrying contract; the eyebrow alone is a live stepper readout -// and deliberately never collapses ×1. +// bracket nodes readable. Mirrors enchoLabel() in web-mobile/js/bracket.jsx, +// pinned by the shared table in testdata/encho_labels.json, and the editors' +// "· (E) Overtime ×N" eyebrow — which is a live stepper readout and so +// deliberately never collapses ×1. func enchoLabel(encho *state.EnchoMetadata) string { if encho == nil || encho.PeriodCount <= 0 { return "" diff --git a/internal/export/suffix_test.go b/internal/export/suffix_test.go index 470ca7d0a..2d858635d 100644 --- a/internal/export/suffix_test.go +++ b/internal/export/suffix_test.go @@ -1,10 +1,14 @@ package export import ( + "encoding/json" "fmt" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/gitrgoliveira/bracket-creator/internal/state" ) @@ -172,3 +176,46 @@ func TestIpponsScore(t *testing.T) { }) } } + +// TestEnchoLabel_GoldenTable drives enchoLabel over testdata/encho_labels.json, +// the table shared with web-mobile/js/__tests__/score_display.test.jsx. +// +// mp-m4bn: the marker is implemented once per language and nothing but a +// comment used to hold the two together. The realistic drift is silent, and it +// survives per-language tests: someone edits the Go marker AND this file's +// hand-written expectations in one commit, the JS suite never runs Go code and +// stays green, and the printed workbook then disagrees with what the operator +// saw on court. Reading the expectations from a file both suites consume closes +// that: the contract can only move if the shared table moves, and moving it +// turns the other language's suite red the same run. +// +// Pin VALUES, not source shape. An earlier attempt grepped this package's +// source text from the JS side; it passed unchanged when the single-period +// marker was switched to "(OT)" (the very divergence it was named for) and went +// red on behaviour-preserving refactors like fmt.Sprintf. +func TestEnchoLabel_GoldenTable(t *testing.T) { + t.Parallel() + + raw, err := os.ReadFile(filepath.Join("testdata", "encho_labels.json")) + require.NoError(t, err, "shared Go/JS golden table is missing") + + var table struct { + Cases []struct { + PeriodCount int `json:"periodCount"` + Label string `json:"label"` + } `json:"cases"` + } + require.NoError(t, json.Unmarshal(raw, &table)) + require.NotEmpty(t, table.Cases, "golden table parsed to zero cases: it would assert nothing") + + for _, tc := range table.Cases { + t.Run(fmt.Sprintf("periodCount=%d", tc.PeriodCount), func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.Label, enchoLabel(encho(tc.PeriodCount)), + "Go enchoLabel disagrees with the shared table; update BOTH renderers, not just this one") + }) + } + + // nil is not expressible in the shared table but must render like 0. + assert.Equal(t, "", enchoLabel(nil)) +} diff --git a/internal/export/testdata/encho_labels.json b/internal/export/testdata/encho_labels.json new file mode 100644 index 000000000..bf42290b6 --- /dev/null +++ b/internal/export/testdata/encho_labels.json @@ -0,0 +1,23 @@ +{ + "_comment": [ + "mp-m4bn: the single source of truth for the overtime marker, shared by", + "BOTH language implementations of enchoLabel: internal/export/suffix.go", + "(the exported results workbook) and web-mobile/js/bracket.jsx (bracket,", + "court console, viewer, TV). internal/export/suffix_test.go table-drives", + "the Go side over these cases; web-mobile/js/__tests__/score_display.test.jsx", + "reads this same file and drives the JS side.", + "", + "Why a shared file rather than each suite restating the strings: changing", + "one language AND its own tests together is the realistic drift, and two", + "independent tables both stay green through it. Editing this file fails", + "the OTHER language's suite immediately, so the marker cannot move in one", + "renderer without the other following." + ], + "cases": [ + { "periodCount": 0, "label": "", "why": "no overtime ran" }, + { "periodCount": 1, "label": "(E)", "why": "the common case stays terse for narrow bracket nodes and Excel cells" }, + { "periodCount": 2, "label": "(E×2)", "why": "two or more carry the count" }, + { "periodCount": 3, "label": "(E×3)" }, + { "periodCount": 11, "label": "(E×11)", "why": "multi-digit N is not truncated or padded" } + ] +} diff --git a/web-mobile/js/__tests__/admin_scoring_modal.test.jsx b/web-mobile/js/__tests__/admin_scoring_modal.test.jsx index d47e754c3..44588fa89 100644 --- a/web-mobile/js/__tests__/admin_scoring_modal.test.jsx +++ b/web-mobile/js/__tests__/admin_scoring_modal.test.jsx @@ -176,18 +176,13 @@ describe('buildDecisionBody', () => { }); describe('nextEnchoPeriod (the + button)', () => { - // mp-m4bn: encho periods are UNBOUNDED. How many overtime periods are - // fought, and how a still-tied match is finally resolved (hantei for an - // individual bout, daihyosen for a team encounter), is the shimpan's - // call. The operator only records what happened, so the + button must - // never refuse a period that was actually fought. + // mp-m4bn: unbounded by design — see the nextEnchoPeriod docstring in + // admin_scoring_shared.jsx for why the shimpan, not the software, decide + // how many periods are fought. - it('increments by 1', () => { + it('increments by 1, with no upper bound', () => { expect(nextEnchoPeriod(1)).toBe(2); expect(nextEnchoPeriod(2)).toBe(3); - }); - - it('never clamps, however many periods were fought', () => { expect(nextEnchoPeriod(99)).toBe(100); }); }); diff --git a/web-mobile/js/__tests__/render/team_editor_config_matrix.render.test.jsx b/web-mobile/js/__tests__/render/team_editor_config_matrix.render.test.jsx index 9cb0d8bbc..1c3cd25f8 100644 --- a/web-mobile/js/__tests__/render/team_editor_config_matrix.render.test.jsx +++ b/web-mobile/js/__tests__/render/team_editor_config_matrix.render.test.jsx @@ -237,10 +237,9 @@ describe('TeamScoreEditorModal encho stepper is unbounded (mp-m4bn)', () => { } it('the + stepper never disables, however many periods were fought', async () => { - // The shimpan decide how many overtime periods are fought and how a - // still-tied encounter is resolved (daihyosen for a team match). The - // operator only records it, so the UI must never refuse a period that - // actually happened, and there is no "maximum reached" alert to show. + // mp-m4bn: unbounded by design — see the nextEnchoPeriod docstring in + // admin_scoring_shared.jsx. This is the end-to-end pin: the button never + // disables and there is no "maximum reached" alert to show. await renderCell({ format: 'mixed', phase: 'bracket', teamSize: 5, tmt: 'fixed', naginata: false }); await expandEncho(); const inc = screen.getByRole('button', { name: 'Increase overtime period count' }); diff --git a/web-mobile/js/__tests__/score_display.test.jsx b/web-mobile/js/__tests__/score_display.test.jsx index be324148b..eba3ad16a 100644 --- a/web-mobile/js/__tests__/score_display.test.jsx +++ b/web-mobile/js/__tests__/score_display.test.jsx @@ -316,49 +316,41 @@ describe('team score string carries IV and PW', () => { }); }); -// mp-m4bn: the "(E)" / "(E×N)" contract is implemented twice, once per -// language: enchoLabel in bracket.jsx (bracket, court console, viewer, TV) and -// enchoLabel in internal/export/suffix.go (the exported results workbook). -// Until now only reciprocal comments held them together, and the realistic -// drift is silent: someone changes the JS and its JS tests, the Go suite stays -// green, and the printed workbook disagrees with what the operator saw on -// screen. These cases derive the contract from OBSERVED JS behaviour and -// assert the Go source still matches, in the same read-the-other-language -// style as the settings round-trip test in admin_competition.test.jsx. +// mp-m4bn: the "(E)" / "(E×N)" marker is implemented once per language: +// enchoLabel in bracket.jsx (bracket, court console, viewer, TV) and enchoLabel +// in internal/export/suffix.go (the exported results workbook). Per-language +// tests do NOT catch them diverging, because the realistic drift edits one +// renderer and its own expectations in the same commit: the other suite never +// runs that code and stays green, and the printed workbook then disagrees with +// what the operator saw on court. +// +// So both suites drive the SAME table, internal/export/testdata/encho_labels.json +// (Go side: TestEnchoLabel_GoldenTable in suffix_test.go). The contract can only +// move if the shared table moves, and moving it reddens the other language the +// same run. Assertions are on VALUES: an earlier attempt grepped the Go source +// text from here and passed unchanged when the single-period marker was switched +// to "(OT)", while going red on behaviour-preserving refactors. describe('enchoLabel Go/JS mirror (mp-m4bn)', () => { - const goSrc = readFileSync( - resolve(__dirname, '..', '..', '..', 'internal', 'export', 'suffix.go'), - 'utf8' + const table = JSON.parse( + readFileSync( + resolve(__dirname, '..', '..', '..', 'internal', 'export', 'testdata', 'encho_labels.json'), + 'utf8' + ) ); - const goEnchoLabel = (goSrc.match(/func enchoLabel\([\s\S]*?\n}/) || [])[0]; - - it('the Go mirror is still where this test expects it', () => { - expect( - goEnchoLabel, - 'expected `func enchoLabel(` in internal/export/suffix.go: it moved or was renamed, so the cases below no longer guard anything' - ).toBeTruthy(); - }); - it('collapses a single period to the bare marker on both sides', () => { - const single = formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 }); - const jsCollapses = single === 'M–K (E)'; + it('the shared golden table is present and non-empty', () => { expect( - goEnchoLabel.includes('PeriodCount > 1'), - jsCollapses - ? 'JS renders one overtime period as the bare "(E)", so Go must keep its `PeriodCount > 1` guard or the workbook prints "(E×1)" where every screen shows "(E)".' - : 'JS now renders the count for a single period, so Go must drop its `PeriodCount > 1` guard to match.' - ).toBe(jsCollapses); + table.cases?.length, + 'internal/export/testdata/encho_labels.json parsed to zero cases: the mirror would assert nothing' + ).toBeGreaterThan(0); }); - it('uses the same counted-marker glyph on both sides', () => { - const counted = formatIpponsScore(['M'], ['K'], null, null, { periodCount: 2 }); - expect(counted, 'expected the counted marker on a two-period score').toBe('M–K (E×2)'); - // Derive the glyph from the JS output rather than restating it, so a - // change to the marker shape moves this expectation with it. - const prefix = counted.slice(counted.indexOf('('), counted.indexOf('2')); + it.each(table.cases)('periodCount $periodCount renders "$label"', ({ periodCount, label }) => { + // formatIpponsScore composes " ", so the marker is whatever + // trails the score: an empty label must leave the score untouched. expect( - goEnchoLabel, - `JS builds the counted marker as "${prefix}N)"; internal/export/suffix.go must concatenate the same "${prefix}" literal or the workbook and the viewer disagree.` - ).toContain(`"${prefix}"`); + formatIpponsScore(['M'], ['K'], null, null, { periodCount }), + 'JS enchoLabel disagrees with the shared table; update BOTH renderers, not just this one' + ).toBe(label ? `M–K ${label}` : 'M–K'); }); }); diff --git a/web-mobile/js/bracket.jsx b/web-mobile/js/bracket.jsx index 8a1e3f9bc..b38849971 100644 --- a/web-mobile/js/bracket.jsx +++ b/web-mobile/js/bracket.jsx @@ -41,17 +41,13 @@ function sideLabel(side) { } // enchoLabel renders the overtime marker for a match's encho block: "" when no -// overtime ran, "(E)" for a single period, "(E×N)" for N > 1. -// mp-m4bn: the overtime marker carries the period COUNT when a match ran more -// than one encho period: "" / "(E)" / "(E×3)". Every consumer used to treat -// periodCount as a boolean, so a match that took three overtimes looked -// identical to one settled in the first and the count was write-only. -// A single period keeps the bare "(E)" — the common case, and the terser -// marker keeps narrow bracket nodes and Excel cells readable. Mirrors -// enchoLabel() in internal/export/suffix.go and the score editors' -// "· (E) Overtime ×N" eyebrow. Keep the three in sync on the count-carrying -// contract; the eyebrow alone is a live stepper readout and deliberately -// never collapses ×1. +// overtime ran, "(E)" for a single period, "(E×N)" for N > 1. A single period +// stays bare because it is the common case and the terser marker keeps narrow +// bracket nodes and Excel cells readable. +// Mirrors enchoLabel() in internal/export/suffix.go, pinned by the shared table +// in internal/export/testdata/encho_labels.json, and the score editors' +// "· (E) Overtime ×N" eyebrow — which is a live stepper readout and so +// deliberately never collapses ×1. function enchoLabel(encho) { const n = encho?.periodCount || 0; if (n <= 0) return ""; @@ -75,17 +71,15 @@ function enchoLabel(encho) { function decisionSuffix(match) { if (!match) return ""; const d = match.decision || ""; - const enchoSfx = enchoLabel(match.encho); - const hanteiOn = !!match.decidedByHantei; - let suffix = ""; - if (isKikenDecisionBC(d)) suffix = "Kiken"; - else if (DECISION_CHIPS[d]) suffix = DECISION_CHIPS[d].label; - if (enchoSfx) suffix = (suffix ? suffix + " " : "") + enchoSfx; + let base = ""; + if (isKikenDecisionBC(d)) base = "Kiken"; + else if (DECISION_CHIPS[d]) base = DECISION_CHIPS[d].label; // FIK 7-5 / 29-6: judges' decision after a tied encho. Mark explicitly so // a hantei-decided final is distinguishable from an ippon-derived one // (audit + Excel + viewer parity). - if (hanteiOn) suffix = (suffix ? suffix + " " : "") + "Ht"; - return suffix; + return [base, enchoLabel(match.encho), match.decidedByHantei ? "Ht" : ""] + .filter(Boolean) + .join(" "); } // Derive an ippon array from a Go-formatted scoreA/scoreB string. From e5bc736e9f6266fe667dfde9d1578780d8175dfa Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 17:43:03 +0100 Subject: [PATCH 11/25] refactor(mp-m4bn): apply /simplify round-6 findings to the golden-table layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 6 audited round 5's additions. Reuse and efficiency lenses CLEAN (the filter/join allocation was costed at ~100ns per call behind a React.memo boundary — noise). - score_display.test.jsx: drive the golden it.each through decisionSuffix({encho:{periodCount}}) instead of formatIpponsScore. With no base label and no Ht that returns enchoLabel's output byte-for-byte, so the table value is asserted EXACTLY and an unrelated formatIpponsScore change (separator, ippon joining) can no longer redden the mirror with a false "update both renderers" diagnosis. Re-verified the pin catches table drift after the rewrite. - .gitignore: widen !**/testdata/*.json to !**/testdata/**/*.json. The one-level negation re-created the fixture-swallowing trap one directory down (testdata/golden/... would silently never land); probe-verified nested fixtures are now trackable. - Deduplicate the shared-table rationale, which was written out nearly verbatim in three places (Go test docstring, JS describe comment, JSON _comment). The JSON _comment is now canonical — it travels with the artifact both suites read — and both test comments shrink to pointers. The vitest empty-it.each guard gains a one-line why (vitest silently produces zero tests over an empty array, unlike jest). - Drop the inert "why" fields from the table (neither driver decodes them; the one non-obvious note, multi-digit N, moved into _comment). - Drop the encho x7 DecisionSuffix row (identical path and property to x2 one line above; value variety now lives in the golden table) and the duplicate periodCount 2/5 it in score_display.test.jsx. Gates: make go/test exit 0, vitest 2407/2407 (-1 = the deleted duplicate), nested-fixture probe shows ?? in git status. --- .gitignore | 5 ++- internal/export/suffix_test.go | 28 ++++-------- internal/export/testdata/encho_labels.json | 13 +++--- .../js/__tests__/score_display.test.jsx | 43 +++++++------------ 4 files changed, 35 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index 87aa49f36..ae4378039 100644 --- a/.gitignore +++ b/.gitignore @@ -67,8 +67,9 @@ Thumbs.db !web/package-lock.json # Go testdata fixtures. These are test INPUTS, not build output: the blanket # *.json above would silently drop them, and a golden table that never lands -# fails CI on a checkout that works locally. -!**/testdata/*.json +# fails CI on a checkout that works locally. /**/ matches zero or more +# directories, so nested layouts (testdata/golden/...) are covered too. +!**/testdata/**/*.json # Fixture data for scripts/setup_tournament.py (make mobile-app-example); # without this the demo target fails with FileNotFoundError on any checkout # other than the one that happens to have the file untracked on disk. diff --git a/internal/export/suffix_test.go b/internal/export/suffix_test.go index 2d858635d..5e1577407 100644 --- a/internal/export/suffix_test.go +++ b/internal/export/suffix_test.go @@ -43,13 +43,12 @@ func TestDecisionSuffix(t *testing.T) { // Daihyosen {name: "daihyosen", decision: "daihyosen", encho: nil, hantei: false, want: "DH"}, - // Encho only. mp-m4bn: one period renders the bare "(E)"; two or - // more carry the count so a three-overtime result is not displayed - // identically to one settled in the first period. + // Encho only. Marker values per count are pinned by + // TestEnchoLabel_GoldenTable; these rows cover the pass-through + // into the composed suffix. {name: "encho only (fought)", decision: "fought", encho: encho(1), hantei: false, want: "(E)"}, {name: "encho nil vs zero periods", decision: "fought", encho: encho(0), hantei: false, want: ""}, {name: "encho x2 carries the count", decision: "fought", encho: encho(2), hantei: false, want: "(E×2)"}, - {name: "encho x7 carries the count", decision: "fought", encho: encho(7), hantei: false, want: "(E×7)"}, // Hantei only {name: "hantei only (fought)", decision: "fought", encho: nil, hantei: true, want: "Ht"}, @@ -177,22 +176,11 @@ func TestIpponsScore(t *testing.T) { } } -// TestEnchoLabel_GoldenTable drives enchoLabel over testdata/encho_labels.json, -// the table shared with web-mobile/js/__tests__/score_display.test.jsx. -// -// mp-m4bn: the marker is implemented once per language and nothing but a -// comment used to hold the two together. The realistic drift is silent, and it -// survives per-language tests: someone edits the Go marker AND this file's -// hand-written expectations in one commit, the JS suite never runs Go code and -// stays green, and the printed workbook then disagrees with what the operator -// saw on court. Reading the expectations from a file both suites consume closes -// that: the contract can only move if the shared table moves, and moving it -// turns the other language's suite red the same run. -// -// Pin VALUES, not source shape. An earlier attempt grepped this package's -// source text from the JS side; it passed unchanged when the single-period -// marker was switched to "(OT)" (the very divergence it was named for) and went -// red on behaviour-preserving refactors like fmt.Sprintf. +// TestEnchoLabel_GoldenTable is the Go half of the shared Go/JS golden table +// for the overtime marker — see the `_comment` in testdata/encho_labels.json +// for why the table is shared and why it pins values, not source text. JS +// half: the "enchoLabel Go/JS mirror" describe in +// web-mobile/js/__tests__/score_display.test.jsx. func TestEnchoLabel_GoldenTable(t *testing.T) { t.Parallel() diff --git a/internal/export/testdata/encho_labels.json b/internal/export/testdata/encho_labels.json index bf42290b6..d91b57f05 100644 --- a/internal/export/testdata/encho_labels.json +++ b/internal/export/testdata/encho_labels.json @@ -11,13 +11,16 @@ "one language AND its own tests together is the realistic drift, and two", "independent tables both stay green through it. Editing this file fails", "the OTHER language's suite immediately, so the marker cannot move in one", - "renderer without the other following." + "renderer without the other following. Pin VALUES, not source shape: an", + "earlier grep-the-other-source attempt passed unchanged when the marker", + "was switched to \"(OT)\" and went red on behaviour-preserving refactors.", + "The periodCount 11 case pins that multi-digit N is not truncated." ], "cases": [ - { "periodCount": 0, "label": "", "why": "no overtime ran" }, - { "periodCount": 1, "label": "(E)", "why": "the common case stays terse for narrow bracket nodes and Excel cells" }, - { "periodCount": 2, "label": "(E×2)", "why": "two or more carry the count" }, + { "periodCount": 0, "label": "" }, + { "periodCount": 1, "label": "(E)" }, + { "periodCount": 2, "label": "(E×2)" }, { "periodCount": 3, "label": "(E×3)" }, - { "periodCount": 11, "label": "(E×11)", "why": "multi-digit N is not truncated or padded" } + { "periodCount": 11, "label": "(E×11)" } ] } diff --git a/web-mobile/js/__tests__/score_display.test.jsx b/web-mobile/js/__tests__/score_display.test.jsx index eba3ad16a..82c3d9f23 100644 --- a/web-mobile/js/__tests__/score_display.test.jsx +++ b/web-mobile/js/__tests__/score_display.test.jsx @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { readFileSync } from 'fs'; import { resolve } from 'path'; -import { formatIpponsScore, ipponsFromScore, matchStateCell } from '../bracket.jsx'; +import { decisionSuffix, formatIpponsScore, ipponsFromScore, matchStateCell } from '../bracket.jsx'; // Convention enforced across all match-list views: // SHIRO (sideB) is always displayed on the LEFT. @@ -148,14 +148,9 @@ describe('formatIpponsScore', () => { expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 0 })).toBe('M–K'); }); - // mp-m4bn: one period stays the bare "(E)" (the common case, kept terse - // for narrow bracket nodes); two or more carry the count so a result that - // took three overtimes is distinguishable from one settled in the first. - it('carries the period count when more than one overtime ran', () => { - expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 2 })).toBe('M–K (E×2)'); - expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 5 })).toBe('M–K (E×5)'); - }); - + // Marker VALUES ("(E)" / "(E×N)" per count) are pinned by the golden + // it.each at the bottom of this file; here only the composition into a + // score string is under test. it('composes the counted marker with a decision label and hantei', () => { expect(formatIpponsScore([], [], null, 'daihyosen', { periodCount: 3 }, true)).toBe('DH (E×3) Ht'); }); @@ -316,20 +311,10 @@ describe('team score string carries IV and PW', () => { }); }); -// mp-m4bn: the "(E)" / "(E×N)" marker is implemented once per language: -// enchoLabel in bracket.jsx (bracket, court console, viewer, TV) and enchoLabel -// in internal/export/suffix.go (the exported results workbook). Per-language -// tests do NOT catch them diverging, because the realistic drift edits one -// renderer and its own expectations in the same commit: the other suite never -// runs that code and stays green, and the printed workbook then disagrees with -// what the operator saw on court. -// -// So both suites drive the SAME table, internal/export/testdata/encho_labels.json -// (Go side: TestEnchoLabel_GoldenTable in suffix_test.go). The contract can only -// move if the shared table moves, and moving it reddens the other language the -// same run. Assertions are on VALUES: an earlier attempt grepped the Go source -// text from here and passed unchanged when the single-period marker was switched -// to "(OT)", while going red on behaviour-preserving refactors. +// mp-m4bn: JS half of the shared Go/JS golden table for the overtime marker — +// see the `_comment` in encho_labels.json for why the table is shared and why +// it pins values, not source text. Go half: TestEnchoLabel_GoldenTable in +// internal/export/suffix_test.go. describe('enchoLabel Go/JS mirror (mp-m4bn)', () => { const table = JSON.parse( readFileSync( @@ -338,6 +323,8 @@ describe('enchoLabel Go/JS mirror (mp-m4bn)', () => { ) ); + // Load-bearing: vitest's it.each over an empty array silently produces + // zero tests (no red), so a degraded table needs its own failure. it('the shared golden table is present and non-empty', () => { expect( table.cases?.length, @@ -345,12 +332,14 @@ describe('enchoLabel Go/JS mirror (mp-m4bn)', () => { ).toBeGreaterThan(0); }); + // decisionSuffix with a bare encho block returns enchoLabel's output + // byte-for-byte (no base label, no Ht), so the table value is asserted + // EXACTLY — an unrelated formatIpponsScore change cannot redden this + // with a misleading "update both renderers" message. it.each(table.cases)('periodCount $periodCount renders "$label"', ({ periodCount, label }) => { - // formatIpponsScore composes " ", so the marker is whatever - // trails the score: an empty label must leave the score untouched. expect( - formatIpponsScore(['M'], ['K'], null, null, { periodCount }), + decisionSuffix({ encho: { periodCount } }), 'JS enchoLabel disagrees with the shared table; update BOTH renderers, not just this one' - ).toBe(label ? `M–K ${label}` : 'M–K'); + ).toBe(label); }); }); From dff9baae8bf0f0534da02a9197f47b1d0039bf5c Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 22:08:15 +0100 Subject: [PATCH 12/25] =?UTF-8?q?docs(mp-m4bn):=20update=20CLAUDE.md=20enc?= =?UTF-8?q?ho=20suffix=20gloss=20to=20(E)/(E=C3=97N)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Match Decision Types bullet still described the visual suffix as bare (E), the exact boolean-collapsed contract this PR removed. CLAUDE.md is the instructions layer every future session reads first, and the shared golden table only pins the two existing renderers: a new surface built from this gloss would hardcode bare (E) with nothing going red. Point the gloss at enchoLabel and the shared table instead of restating the values. (Simplify round 8: three lenses clean, altitude found this in the one layer prior rounds never audited.) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index b17c73da9..e88e1c91f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,7 +119,7 @@ tournament-data/ - **Excel Layout**: Uses an **8-column per court** layout. Columns A and G (and their court-shifted counterparts) are 30 units wide; others are 5 units wide. A blank row separates pools vertically. - **Team Match Labels**: Summaries use **"IV"** (Individual Victories) and **"PW"** (Points Won). - **Court limit**: courts are labelled A–Z, so `--courts` is hard-capped at 26 and any value over that returns an error rather than silently truncating. -- **Match Decision Types** (`internal/domain/decision.go`): 10 canonical wire values: `""` (none), `"fought"`, `"hikiwake"` (draw), `"kiken"` (legacy withdrawal, maps to kiken-voluntary on YAML load), `"kiken-voluntary"` (FIK Art. 31, permanent), `"kiken-injury"` (FIK Art. 30, reinstateable), `"fusenpai"` (no-show), `"fusensho"` (per-bout default win), `"daihyosen"` (rep bout), `"kachinuki-exhaustion"`. Use `domain.IsKikenDecision(d)` / `domain.IsKikenDecisionStr(s)` to check any kiken variant. Legacy YAML `decision: true` migrates to `"hikiwake"`, `false` to `"fought"`, `"kiken"` to `"kiken-voluntary"` (Decision.UnmarshalYAML). Visual suffixes in the UI: `Kiken`, `Fus.`, `DH`, `(E)` for encho. +- **Match Decision Types** (`internal/domain/decision.go`): 10 canonical wire values: `""` (none), `"fought"`, `"hikiwake"` (draw), `"kiken"` (legacy withdrawal, maps to kiken-voluntary on YAML load), `"kiken-voluntary"` (FIK Art. 31, permanent), `"kiken-injury"` (FIK Art. 30, reinstateable), `"fusenpai"` (no-show), `"fusensho"` (per-bout default win), `"daihyosen"` (rep bout), `"kachinuki-exhaustion"`. Use `domain.IsKikenDecision(d)` / `domain.IsKikenDecisionStr(s)` to check any kiken variant. Legacy YAML `decision: true` migrates to `"hikiwake"`, `false` to `"fought"`, `"kiken"` to `"kiken-voluntary"` (Decision.UnmarshalYAML). Visual suffixes in the UI: `Kiken`, `Fus.`, `DH`, and `(E)` / `(E×N)` for encho (values pinned by `enchoLabel` in both languages via `internal/export/testdata/encho_labels.json`). - **Competitor Eligibility** (`internal/state/competitor_status.go`, `internal/engine/eligibility.go`): a kiken/fusenpai decision auto-writes a `CompetitorStatus{Eligible: false}` for the loser; `engine.StartMatch(compID, matchID)` is the pre-flight gate that returns `*IneligibleCompetitorError` (matches `errors.Is(err, ErrIneligibleCompetitor)`). Maps to HTTP 409. Kiken-injury (FIK Art. 30) sets `CompetitorStatus.Reinstateable: true`; the admin can call `POST /api/competitions/:cid/competitors/:pid/reinstate` to restore eligibility. Kiken-voluntary (Art. 31) and fusenpai are not reinstateable. - **Team Lineups & Kachinuki** (`internal/domain/team_lineup.go`, `internal/engine/kachinuki.go`): TeamLineup pins position→player for a round. FIK 5-person rule: Senpo + Taisho mandatory; 1 vacancy must be Jiho, 2 must be Jiho+Fukusho, 3+ disqualifies. Kachinuki ("winner-stays-on") dynamically appends bouts via `engine.AdvanceKachinuki` until one team is exhausted (`DecisionKachinukiExhaustion`). - **Schedule Estimator** (`internal/engine/schedule.go`): `EstimateSchedule(EstimateInput) ScheduleEstimate` produces total/per-court minutes from match duration × multiplier × slowest-court buffer. Exposed via stateless `GET /api/schedule/estimate` on both the CLI web server and the mobile app. From 83ae717c07caa817ecba3a5f0e43d4acbb2bd151 Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 23:06:06 +0100 Subject: [PATCH 13/25] refactor(mp-m4bn): encho marker is always bare (E); the count is recorded, not displayed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator decision: "Encho is just Encho. Do not add numbers to the marking. It will get confusing." The (E×N) counted marker introduced earlier in this PR is withdrawn. - enchoLabel (Go + JS): any positive periodCount renders bare "(E)"; zero/absent renders "". Docstrings now state the contract and why, and quote the rejected "(E×3)" shape as do-not-reintroduce. - The stepper is unchanged: still unbounded, still records periodCount to bracket.json for the tournament log. Only result displays change. - The editors' "(E) Overtime ×N" eyebrow stays: it is a live readout of the stepper the operator is using, not a result marking. - Golden table (encho_labels.json): all positive counts pin "(E)"; the periodCount 11 case now pins that digits never leak into the marker. _comment records the operator decision. - Test expectations updated on both sides; docs paragraph rewritten (marker is always (E), counter records the number for the log); CLAUDE.md gloss updated. Gates: make go/test exit 0, vitest 2407/2407, make docs/build exit 0. --- CLAUDE.md | 2 +- .../court-operators/recording-decisions.md | 2 +- internal/export/suffix.go | 26 ++++++++--------- internal/export/suffix_test.go | 12 ++++---- internal/export/testdata/encho_labels.json | 16 ++++++---- .../js/__tests__/score_display.test.jsx | 13 +++++---- web-mobile/js/bracket.jsx | 29 ++++++++++--------- 7 files changed, 53 insertions(+), 47 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e88e1c91f..2f7101569 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,7 +119,7 @@ tournament-data/ - **Excel Layout**: Uses an **8-column per court** layout. Columns A and G (and their court-shifted counterparts) are 30 units wide; others are 5 units wide. A blank row separates pools vertically. - **Team Match Labels**: Summaries use **"IV"** (Individual Victories) and **"PW"** (Points Won). - **Court limit**: courts are labelled A–Z, so `--courts` is hard-capped at 26 and any value over that returns an error rather than silently truncating. -- **Match Decision Types** (`internal/domain/decision.go`): 10 canonical wire values: `""` (none), `"fought"`, `"hikiwake"` (draw), `"kiken"` (legacy withdrawal, maps to kiken-voluntary on YAML load), `"kiken-voluntary"` (FIK Art. 31, permanent), `"kiken-injury"` (FIK Art. 30, reinstateable), `"fusenpai"` (no-show), `"fusensho"` (per-bout default win), `"daihyosen"` (rep bout), `"kachinuki-exhaustion"`. Use `domain.IsKikenDecision(d)` / `domain.IsKikenDecisionStr(s)` to check any kiken variant. Legacy YAML `decision: true` migrates to `"hikiwake"`, `false` to `"fought"`, `"kiken"` to `"kiken-voluntary"` (Decision.UnmarshalYAML). Visual suffixes in the UI: `Kiken`, `Fus.`, `DH`, and `(E)` / `(E×N)` for encho (values pinned by `enchoLabel` in both languages via `internal/export/testdata/encho_labels.json`). +- **Match Decision Types** (`internal/domain/decision.go`): 10 canonical wire values: `""` (none), `"fought"`, `"hikiwake"` (draw), `"kiken"` (legacy withdrawal, maps to kiken-voluntary on YAML load), `"kiken-voluntary"` (FIK Art. 31, permanent), `"kiken-injury"` (FIK Art. 30, reinstateable), `"fusenpai"` (no-show), `"fusensho"` (per-bout default win), `"daihyosen"` (rep bout), `"kachinuki-exhaustion"`. Use `domain.IsKikenDecision(d)` / `domain.IsKikenDecisionStr(s)` to check any kiken variant. Legacy YAML `decision: true` migrates to `"hikiwake"`, `false` to `"fought"`, `"kiken"` to `"kiken-voluntary"` (Decision.UnmarshalYAML). Visual suffixes in the UI: `Kiken`, `Fus.`, `DH`, and `(E)` for encho — always bare, never a count (`periodCount` is recorded but deliberately not displayed on results; pinned by `enchoLabel` in both languages via `internal/export/testdata/encho_labels.json`). - **Competitor Eligibility** (`internal/state/competitor_status.go`, `internal/engine/eligibility.go`): a kiken/fusenpai decision auto-writes a `CompetitorStatus{Eligible: false}` for the loser; `engine.StartMatch(compID, matchID)` is the pre-flight gate that returns `*IneligibleCompetitorError` (matches `errors.Is(err, ErrIneligibleCompetitor)`). Maps to HTTP 409. Kiken-injury (FIK Art. 30) sets `CompetitorStatus.Reinstateable: true`; the admin can call `POST /api/competitions/:cid/competitors/:pid/reinstate` to restore eligibility. Kiken-voluntary (Art. 31) and fusenpai are not reinstateable. - **Team Lineups & Kachinuki** (`internal/domain/team_lineup.go`, `internal/engine/kachinuki.go`): TeamLineup pins position→player for a round. FIK 5-person rule: Senpo + Taisho mandatory; 1 vacancy must be Jiho, 2 must be Jiho+Fukusho, 3+ disqualifies. Kachinuki ("winner-stays-on") dynamically appends bouts via `engine.AdvanceKachinuki` until one team is exhausted (`DecisionKachinukiExhaustion`). - **Schedule Estimator** (`internal/engine/schedule.go`): `EstimateSchedule(EstimateInput) ScheduleEstimate` produces total/per-court minutes from match duration × multiplier × slowest-court buffer. Exposed via stateless `GET /api/schedule/estimate` on both the CLI web server and the mobile app. diff --git a/docs/user-guide/court-operators/recording-decisions.md b/docs/user-guide/court-operators/recording-decisions.md index 77c685d98..0dfa7a4e0 100644 --- a/docs/user-guide/court-operators/recording-decisions.md +++ b/docs/user-guide/court-operators/recording-decisions.md @@ -32,7 +32,7 @@ Encho is the extra period played when a knockout match is level at the end of re In the score editor, open the **Overtime** control and tick **Encho started**. A counter appears so you can record how many overtime periods were fought, using the **+** and **-** buttons. The counter starts at 1 and has no upper limit: how many periods are fought, and how a match still level after them is finally settled (a judges' decision for an individual bout, a daihyosen for a team encounter), is the shimpan's call. Record what actually happened on court, however many periods that took. -Completed results carry an overtime marker. A match settled in a single overtime period shows **(E)**. A match that needed more shows the count, so a result decided in the third overtime reads **(E×3)**. For an individual match the marker appears next to the score on the bracket, the court console, the public viewer, and the exported results workbook, and it combines with any other tag: a withdrawal during a second overtime period reads **Kiken (E×2)**, and a match sent to a judges' decision after three reads **(E×3) Ht**. A team encounter shows its team totals in the score cell instead, so for those the marker appears on the bracket and in the exported workbook. +Completed results carry an overtime marker: **(E)**. The marker is the same however many overtime periods were fought. The counter records the number for the tournament log, but results never show it, so a bracket stays easy to read at a glance. For an individual match the marker appears next to the score on the bracket, the court console, the public viewer, and the exported results workbook, and it combines with any other tag: a withdrawal during overtime reads **Kiken (E)**, and a match sent to a judges' decision after overtime reads **(E) Ht**. A team encounter shows its team totals in the score cell instead, so for those the marker appears on the bracket and in the exported workbook. ## Daihyosen diff --git a/internal/export/suffix.go b/internal/export/suffix.go index 59ce5f9ba..bc13bf347 100644 --- a/internal/export/suffix.go +++ b/internal/export/suffix.go @@ -21,7 +21,7 @@ import ( // // Composition order: // 1. Base decision label: kiken variants -> "Kiken"; fusenpai/fusensho -> "Fus."; daihyosen -> "DH". -// 2. If encho -> append " (E)", or " (E×N)" when more than one period ran. +// 2. If encho -> append " (E)" (always bare, regardless of period count). // 3. If hanteiOn -> append " Ht". // // DELIBERATE DIVERGENCE from the JS: the JS omits fusensho (the per-bout default @@ -68,24 +68,22 @@ func joinSp(a, b string) string { } // enchoLabel renders the overtime marker for an encho block: "" when no -// overtime ran, "(E)" for a single period, and "(E×N)" for N > 1. +// overtime ran, "(E)" otherwise — always bare, never a count. // -// mp-m4bn: every consumer used to treat PeriodCount as a boolean, so a match -// that took three overtime periods was indistinguishable from one settled in -// the first and the count was write-only. Surfacing N is what makes the -// operator's stepper taps worth recording. One period stays the bare "(E)": -// it is the common case and the terser marker keeps narrow Excel cells and -// bracket nodes readable. Mirrors enchoLabel() in web-mobile/js/bracket.jsx, -// pinned by the shared table in testdata/encho_labels.json, and the editors' -// "· (E) Overtime ×N" eyebrow — which is a live stepper readout and so -// deliberately never collapses ×1. +// mp-m4bn: encho is just encho. The stepper records how many periods were +// fought (PeriodCount persists for the tournament log), but the result +// marking deliberately never carries the number: operator feedback is that +// counted markers ("(E×3)") confuse readers of brackets and result sheets. +// Do not reintroduce the count here. Mirrors enchoLabel() in +// web-mobile/js/bracket.jsx, pinned by the shared table in +// testdata/encho_labels.json (which includes multi-digit counts precisely to +// pin that digits never leak into the marker). The editors' "· (E) Overtime +// ×N" eyebrow is different on purpose: a live readout of the stepper the +// operator is using, not a result marking. func enchoLabel(encho *state.EnchoMetadata) string { if encho == nil || encho.PeriodCount <= 0 { return "" } - if encho.PeriodCount > 1 { - return "(E×" + strconv.Itoa(encho.PeriodCount) + ")" - } return "(E)" } diff --git a/internal/export/suffix_test.go b/internal/export/suffix_test.go index 5e1577407..8498a35ee 100644 --- a/internal/export/suffix_test.go +++ b/internal/export/suffix_test.go @@ -43,18 +43,18 @@ func TestDecisionSuffix(t *testing.T) { // Daihyosen {name: "daihyosen", decision: "daihyosen", encho: nil, hantei: false, want: "DH"}, - // Encho only. Marker values per count are pinned by - // TestEnchoLabel_GoldenTable; these rows cover the pass-through - // into the composed suffix. + // Encho only. Marker values are pinned by TestEnchoLabel_GoldenTable; + // these rows cover the pass-through into the composed suffix. The + // marker is bare "(E)" regardless of period count (mp-m4bn). {name: "encho only (fought)", decision: "fought", encho: encho(1), hantei: false, want: "(E)"}, {name: "encho nil vs zero periods", decision: "fought", encho: encho(0), hantei: false, want: ""}, - {name: "encho x2 carries the count", decision: "fought", encho: encho(2), hantei: false, want: "(E×2)"}, + {name: "encho x2 renders the same bare marker", decision: "fought", encho: encho(2), hantei: false, want: "(E)"}, // Hantei only {name: "hantei only (fought)", decision: "fought", encho: nil, hantei: true, want: "Ht"}, // Encho + hantei - {name: "encho + hantei (fought)", decision: "fought", encho: encho(2), hantei: true, want: "(E×2) Ht"}, + {name: "encho + hantei (fought)", decision: "fought", encho: encho(2), hantei: true, want: "(E) Ht"}, // Base label + encho {name: "Kiken + encho", decision: "kiken-voluntary", encho: encho(1), hantei: false, want: "Kiken (E)"}, @@ -67,7 +67,7 @@ func TestDecisionSuffix(t *testing.T) { // Full composition: base + encho + hantei {name: "Kiken + encho + hantei", decision: "kiken-voluntary", encho: encho(1), hantei: true, want: "Kiken (E) Ht"}, - {name: "DH + encho + hantei", decision: "daihyosen", encho: encho(3), hantei: true, want: "DH (E×3) Ht"}, + {name: "DH + encho + hantei", decision: "daihyosen", encho: encho(3), hantei: true, want: "DH (E) Ht"}, // Hikiwake (draw) produces no base label; suffix still applies {name: "hikiwake + hantei", decision: "hikiwake", encho: nil, hantei: true, want: "Ht"}, diff --git a/internal/export/testdata/encho_labels.json b/internal/export/testdata/encho_labels.json index d91b57f05..da42f555f 100644 --- a/internal/export/testdata/encho_labels.json +++ b/internal/export/testdata/encho_labels.json @@ -7,20 +7,26 @@ "the Go side over these cases; web-mobile/js/__tests__/score_display.test.jsx", "reads this same file and drives the JS side.", "", + "THE CONTRACT: encho is just encho. Any positive periodCount renders the", + "bare \"(E)\"; zero/absent renders \"\". The stepper records how many periods", + "were fought (periodCount persists for the tournament log), but the result", + "marking never carries the number: counted markers (\"(E×3)\") confuse", + "readers of brackets and result sheets (operator decision, PR #376). The", + "multi-digit periodCount 11 case pins that digits never leak in.", + "", "Why a shared file rather than each suite restating the strings: changing", "one language AND its own tests together is the realistic drift, and two", "independent tables both stay green through it. Editing this file fails", "the OTHER language's suite immediately, so the marker cannot move in one", "renderer without the other following. Pin VALUES, not source shape: an", "earlier grep-the-other-source attempt passed unchanged when the marker", - "was switched to \"(OT)\" and went red on behaviour-preserving refactors.", - "The periodCount 11 case pins that multi-digit N is not truncated." + "was switched to \"(OT)\" and went red on behaviour-preserving refactors." ], "cases": [ { "periodCount": 0, "label": "" }, { "periodCount": 1, "label": "(E)" }, - { "periodCount": 2, "label": "(E×2)" }, - { "periodCount": 3, "label": "(E×3)" }, - { "periodCount": 11, "label": "(E×11)" } + { "periodCount": 2, "label": "(E)" }, + { "periodCount": 3, "label": "(E)" }, + { "periodCount": 11, "label": "(E)" } ] } diff --git a/web-mobile/js/__tests__/score_display.test.jsx b/web-mobile/js/__tests__/score_display.test.jsx index 82c3d9f23..67167d89c 100644 --- a/web-mobile/js/__tests__/score_display.test.jsx +++ b/web-mobile/js/__tests__/score_display.test.jsx @@ -141,18 +141,19 @@ describe('formatIpponsScore', () => { }); it('appends the marker to a no-score draw (X is the scoreless-draw glyph)', () => { - expect(formatIpponsScore([], [], null, 'hikiwake', { periodCount: 2 })).toBe('X (E×2)'); + // mp-m4bn: bare (E) regardless of period count — results never show numbers. + expect(formatIpponsScore([], [], null, 'hikiwake', { periodCount: 2 })).toBe('X (E)'); }); it('does not append (E) when periodCount is 0', () => { expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 0 })).toBe('M–K'); }); - // Marker VALUES ("(E)" / "(E×N)" per count) are pinned by the golden - // it.each at the bottom of this file; here only the composition into a - // score string is under test. - it('composes the counted marker with a decision label and hantei', () => { - expect(formatIpponsScore([], [], null, 'daihyosen', { periodCount: 3 }, true)).toBe('DH (E×3) Ht'); + // Marker VALUES (bare "(E)" for any positive count) are pinned by the + // golden it.each at the bottom of this file; here only the composition + // into a score string is under test. + it('composes the marker with a decision label and hantei', () => { + expect(formatIpponsScore([], [], null, 'daihyosen', { periodCount: 3 }, true)).toBe('DH (E) Ht'); }); it('is a no-op when encho argument is missing entirely', () => { diff --git a/web-mobile/js/bracket.jsx b/web-mobile/js/bracket.jsx index b38849971..26a12d9d0 100644 --- a/web-mobile/js/bracket.jsx +++ b/web-mobile/js/bracket.jsx @@ -41,17 +41,18 @@ function sideLabel(side) { } // enchoLabel renders the overtime marker for a match's encho block: "" when no -// overtime ran, "(E)" for a single period, "(E×N)" for N > 1. A single period -// stays bare because it is the common case and the terser marker keeps narrow -// bracket nodes and Excel cells readable. -// Mirrors enchoLabel() in internal/export/suffix.go, pinned by the shared table -// in internal/export/testdata/encho_labels.json, and the score editors' -// "· (E) Overtime ×N" eyebrow — which is a live stepper readout and so -// deliberately never collapses ×1. +// overtime ran, "(E)" otherwise — always bare, never a count. mp-m4bn: the +// stepper records how many periods were fought (periodCount persists for the +// tournament log), but the result marking deliberately never carries the +// number: counted markers ("(E×3)") confuse readers of brackets and result +// sheets. Do not reintroduce the count here. Mirrors enchoLabel() in +// internal/export/suffix.go, pinned by the shared table in +// internal/export/testdata/encho_labels.json. The score editors' "· (E) +// Overtime ×N" eyebrow is different on purpose: a live readout of the stepper +// the operator is using, not a result marking. function enchoLabel(encho) { const n = encho?.periodCount || 0; - if (n <= 0) return ""; - return n > 1 ? `(E×${n})` : "(E)"; + return n > 0 ? "(E)" : ""; } // Decision-driven suffix appended to score strings on schedule rows, bracket @@ -61,7 +62,7 @@ function enchoLabel(encho) { // decision == "fusenpai" → "Fus." // decision == "daihyosen" → "DH" // The enchoLabel marker is appended on top of any other suffix, so a kiken in -// a second overtime period renders "0–2 Kiken (E×2)". `fusensho` is per-bout +// overtime renders "0–2 Kiken (E)". `fusensho` is per-bout // only: handled by a separate bout badge, not by this helper. Pure and DOM-free // so it can be reused by display.jsx (which builds its own scoreline) without // dragging in the rest of formatIpponsScore's bye/hantei special cases. @@ -100,10 +101,10 @@ function ipponsFromScore(scoreStr) { // it surfaces as an "Ht" suffix appended by decisionSuffix when // decidedByHantei=true: e.g. "M–K (E) Ht". // -// FR-033: when `encho` carries a positive periodCount, the overtime marker -// ("(E)" or "(E×N)", via enchoLabel) is appended so operators and viewers see -// at a glance that the match went to overtime. Argument is optional and -// defaults to no-encho when absent. +// FR-033: when `encho` carries a positive periodCount, the bare "(E)" marker +// (via enchoLabel) is appended so operators and viewers see at a glance that +// the match went to overtime. Argument is optional and defaults to no-encho +// when absent. // // T097: kiken / fusenpai / daihyosen append labelled suffixes alongside the // encho marker: wired through decisionSuffix() so the same string is used From 5dd3ecffedf395e6829f2f4215c18bb270b2aba7 Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 23:23:52 +0100 Subject: [PATCH 14/25] feat(mp-m4bn): centre the (E) marker between the two sides' scores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator requirement: "If there's an Encho, the mark (E) needs to be in the centre, between the ippon." formatIpponsScore now renders the encho marker as the separator ("M (E) ·") instead of a trailing tag ("M–· (E)"), matching the paper score sheet's centre column and the Excel export, which already writes the marker into the centre "vs" cell via MiddleCellText. Decision tags (Kiken / Fus. / DH / Ht) stay trailing; paths with no two-sided score (scoreless X draw, kiken before any ippon, 0-0 hantei) keep the whole composed decisionSuffix since there is nothing to sit between. No Go change: the Excel layout has carried the marker in the centre cell all along. decisionSuffix/enchoLabel contracts are untouched, so the shared golden table and the single-tag display surfaces (scoreboard vs-row, lobby, OBS lower-third) are unaffected. Docs and CLAUDE.md updated to state the centre-column convention. Gates: make go/test exit 0, vitest 2407/2407, make docs/build exit 0. --- CLAUDE.md | 2 +- .../court-operators/recording-decisions.md | 2 +- .../js/__tests__/score_display.test.jsx | 33 +++++++------- web-mobile/js/bracket.jsx | 45 ++++++++++++------- 4 files changed, 47 insertions(+), 35 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2f7101569..16ff71e8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,7 +119,7 @@ tournament-data/ - **Excel Layout**: Uses an **8-column per court** layout. Columns A and G (and their court-shifted counterparts) are 30 units wide; others are 5 units wide. A blank row separates pools vertically. - **Team Match Labels**: Summaries use **"IV"** (Individual Victories) and **"PW"** (Points Won). - **Court limit**: courts are labelled A–Z, so `--courts` is hard-capped at 26 and any value over that returns an error rather than silently truncating. -- **Match Decision Types** (`internal/domain/decision.go`): 10 canonical wire values: `""` (none), `"fought"`, `"hikiwake"` (draw), `"kiken"` (legacy withdrawal, maps to kiken-voluntary on YAML load), `"kiken-voluntary"` (FIK Art. 31, permanent), `"kiken-injury"` (FIK Art. 30, reinstateable), `"fusenpai"` (no-show), `"fusensho"` (per-bout default win), `"daihyosen"` (rep bout), `"kachinuki-exhaustion"`. Use `domain.IsKikenDecision(d)` / `domain.IsKikenDecisionStr(s)` to check any kiken variant. Legacy YAML `decision: true` migrates to `"hikiwake"`, `false` to `"fought"`, `"kiken"` to `"kiken-voluntary"` (Decision.UnmarshalYAML). Visual suffixes in the UI: `Kiken`, `Fus.`, `DH`, and `(E)` for encho — always bare, never a count (`periodCount` is recorded but deliberately not displayed on results; pinned by `enchoLabel` in both languages via `internal/export/testdata/encho_labels.json`). +- **Match Decision Types** (`internal/domain/decision.go`): 10 canonical wire values: `""` (none), `"fought"`, `"hikiwake"` (draw), `"kiken"` (legacy withdrawal, maps to kiken-voluntary on YAML load), `"kiken-voluntary"` (FIK Art. 31, permanent), `"kiken-injury"` (FIK Art. 30, reinstateable), `"fusenpai"` (no-show), `"fusensho"` (per-bout default win), `"daihyosen"` (rep bout), `"kachinuki-exhaustion"`. Use `domain.IsKikenDecision(d)` / `domain.IsKikenDecisionStr(s)` to check any kiken variant. Legacy YAML `decision: true` migrates to `"hikiwake"`, `false` to `"fought"`, `"kiken"` to `"kiken-voluntary"` (Decision.UnmarshalYAML). Visual suffixes in the UI: `Kiken`, `Fus.`, `DH`, and `(E)` for encho — always bare, never a count (`periodCount` is recorded but deliberately not displayed on results; pinned by `enchoLabel` in both languages via `internal/export/testdata/encho_labels.json`). In score strings `(E)` sits in the CENTRE between the two sides' scores (`M (E) ·`), replacing the `–` separator — the score sheet's centre-column convention, matching the Excel export's centre "vs" cell; the other tags trail. - **Competitor Eligibility** (`internal/state/competitor_status.go`, `internal/engine/eligibility.go`): a kiken/fusenpai decision auto-writes a `CompetitorStatus{Eligible: false}` for the loser; `engine.StartMatch(compID, matchID)` is the pre-flight gate that returns `*IneligibleCompetitorError` (matches `errors.Is(err, ErrIneligibleCompetitor)`). Maps to HTTP 409. Kiken-injury (FIK Art. 30) sets `CompetitorStatus.Reinstateable: true`; the admin can call `POST /api/competitions/:cid/competitors/:pid/reinstate` to restore eligibility. Kiken-voluntary (Art. 31) and fusenpai are not reinstateable. - **Team Lineups & Kachinuki** (`internal/domain/team_lineup.go`, `internal/engine/kachinuki.go`): TeamLineup pins position→player for a round. FIK 5-person rule: Senpo + Taisho mandatory; 1 vacancy must be Jiho, 2 must be Jiho+Fukusho, 3+ disqualifies. Kachinuki ("winner-stays-on") dynamically appends bouts via `engine.AdvanceKachinuki` until one team is exhausted (`DecisionKachinukiExhaustion`). - **Schedule Estimator** (`internal/engine/schedule.go`): `EstimateSchedule(EstimateInput) ScheduleEstimate` produces total/per-court minutes from match duration × multiplier × slowest-court buffer. Exposed via stateless `GET /api/schedule/estimate` on both the CLI web server and the mobile app. diff --git a/docs/user-guide/court-operators/recording-decisions.md b/docs/user-guide/court-operators/recording-decisions.md index 0dfa7a4e0..70b8892d1 100644 --- a/docs/user-guide/court-operators/recording-decisions.md +++ b/docs/user-guide/court-operators/recording-decisions.md @@ -32,7 +32,7 @@ Encho is the extra period played when a knockout match is level at the end of re In the score editor, open the **Overtime** control and tick **Encho started**. A counter appears so you can record how many overtime periods were fought, using the **+** and **-** buttons. The counter starts at 1 and has no upper limit: how many periods are fought, and how a match still level after them is finally settled (a judges' decision for an individual bout, a daihyosen for a team encounter), is the shimpan's call. Record what actually happened on court, however many periods that took. -Completed results carry an overtime marker: **(E)**. The marker is the same however many overtime periods were fought. The counter records the number for the tournament log, but results never show it, so a bracket stays easy to read at a glance. For an individual match the marker appears next to the score on the bracket, the court console, the public viewer, and the exported results workbook, and it combines with any other tag: a withdrawal during overtime reads **Kiken (E)**, and a match sent to a judges' decision after overtime reads **(E) Ht**. A team encounter shows its team totals in the score cell instead, so for those the marker appears on the bracket and in the exported workbook. +Completed results carry an overtime marker: **(E)**. The marker is the same however many overtime periods were fought. The counter records the number for the tournament log, but results never show it, so a bracket stays easy to read at a glance. The marker sits in the centre of the score, between the two sides' points, exactly where it goes on a paper score sheet: a match won by men in overtime reads **M (E) ·** on the court console, the public viewer, and the exported results workbook. Decision tags stay after the score: a withdrawal during overtime reads with a trailing **Kiken**, and a match sent to a judges' decision after overtime carries a trailing **Ht**. A team encounter shows its team totals in the score cell instead, so for those the marker appears on the bracket and in the exported workbook. ## Daihyosen diff --git a/web-mobile/js/__tests__/score_display.test.jsx b/web-mobile/js/__tests__/score_display.test.jsx index 67167d89c..c89f3e89e 100644 --- a/web-mobile/js/__tests__/score_display.test.jsx +++ b/web-mobile/js/__tests__/score_display.test.jsx @@ -128,16 +128,17 @@ describe('formatIpponsScore', () => { }); }); - // FR-033: encho is rendered as a trailing " (E)" so the match list and - // bracket views surface that the match went to overtime. - describe('encho suffix', () => { - it('appends (E) to a normal ippon score when encho has a positive period count', () => { - expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 })).toBe('M–K (E)'); + // FR-033: encho renders as a bare "(E)" in the CENTRE of the score string, + // replacing the "–" separator (the score sheet's centre-column convention), + // so match lists and bracket views surface overtime at a glance. + describe('encho marker', () => { + it('places (E) between the scores when encho has a positive period count', () => { + expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 })).toBe('M (E) K'); }); - it('appends (E) to a scored draw (shows points, not X)', () => { - // Item 6: scored equal draw shows techniques + encho suffix, not bare X. - expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null, { periodCount: 1 })).toBe('M–K (E)'); + it('places (E) between the points of a scored draw (shows points, not X)', () => { + // Item 6: scored equal draw shows techniques + encho marker, not bare X. + expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null, { periodCount: 1 })).toBe('M (E) K'); }); it('appends the marker to a no-score draw (X is the scoreless-draw glyph)', () => { @@ -174,12 +175,12 @@ describe('formatIpponsScore', () => { // Realistic: tied with scores, then hantei chose a winner. Backend // sends decidedByHantei=true alongside the tied ippons. const result = formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 }, true); - expect(result).toBe('M–K (E) Ht'); + expect(result).toBe('M (E) K Ht'); }); it('omits Ht when decidedByHantei is false/missing', () => { - expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 }, false)).toBe('M–K (E)'); - expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 })).toBe('M–K (E)'); + expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 }, false)).toBe('M (E) K'); + expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 })).toBe('M (E) K'); }); it('score.hantei is not read. Only the decidedByHantei param controls Ht', () => { @@ -187,8 +188,8 @@ describe('formatIpponsScore', () => { // API fields (ipponsA/B, scoreA/B). The backend never emits a `score` // object, so score.hantei can never appear in real match data. Only the // positional decidedByHantei arg matters. - expect(formatIpponsScore(['M'], ['K'], { type: 'ippon', hantei: true }, null, { periodCount: 1 })).toBe('M–K (E)'); - expect(formatIpponsScore(['M'], ['K'], { type: 'ippon', hantei: true }, null, { periodCount: 1 }, true)).toBe('M–K (E) Ht'); + expect(formatIpponsScore(['M'], ['K'], { type: 'ippon', hantei: true }, null, { periodCount: 1 })).toBe('M (E) K'); + expect(formatIpponsScore(['M'], ['K'], { type: 'ippon', hantei: true }, null, { periodCount: 1 }, true)).toBe('M (E) K Ht'); }); }); }); @@ -211,9 +212,9 @@ describe('formatIpponsScore: hikiwake draw display (item 6)', () => { expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null)).toBe('M–K'); }); - it('1–1 hikiwake with encho (periodCount>0) → "M–K (E)"', () => { - // Encho suffix must survive the scored-draw path unchanged. - expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null, { periodCount: 1 })).toBe('M–K (E)'); + it('1–1 hikiwake with encho (periodCount>0) → "M (E) K"', () => { + // The centred encho marker must survive the scored-draw path unchanged. + expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null, { periodCount: 1 })).toBe('M (E) K'); }); }); diff --git a/web-mobile/js/bracket.jsx b/web-mobile/js/bracket.jsx index 26a12d9d0..0c96d66c8 100644 --- a/web-mobile/js/bracket.jsx +++ b/web-mobile/js/bracket.jsx @@ -99,24 +99,34 @@ function ipponsFromScore(scoreStr) { // Returns something like "MM–K", "M–·", "X" (hikiwake, scored or not), "BYE". // Hantei (judges' decision after tied encho) is NOT a separate return value; // it surfaces as an "Ht" suffix appended by decisionSuffix when -// decidedByHantei=true: e.g. "M–K (E) Ht". +// decidedByHantei=true: e.g. "M (E) K Ht". // // FR-033: when `encho` carries a positive periodCount, the bare "(E)" marker -// (via enchoLabel) is appended so operators and viewers see at a glance that -// the match went to overtime. Argument is optional and defaults to no-encho -// when absent. +// (via enchoLabel) sits in the CENTRE, replacing the "–" separator ("M (E) ·"), +// so operators and viewers see at a glance that the match went to overtime — +// the score sheet's centre-column convention. Argument is optional and +// defaults to no-encho when absent. // -// T097: kiken / fusenpai / daihyosen append labelled suffixes alongside the -// encho marker: wired through decisionSuffix() so the same string is used -// by display.jsx's hand-rolled score block. decisionSuffix() is the single -// source of the composed suffix — it already embeds the encho marker, so -// there is no separate bare-"(E)" path here. +// T097: kiken / fusenpai / daihyosen append labelled TRAILING suffixes: +// wired through decisionSuffix() so the same labels are used by display.jsx's +// hand-rolled score block. Only the encho marker is pulled out of the +// composed suffix and centred; paths with no two-sided score still render +// decisionSuffix() whole. function formatIpponsScore(ipponsA, ipponsB, score, decision, encho, decidedByHantei) { // decidedByHantei (positional) is the canonical flag. The `typeof` guard // lets callers that omit the arg safely get false without sending undefined. const hantei = typeof decidedByHantei === "boolean" ? decidedByHantei : false; - const decSfx = decisionSuffix({ decision, encho, decidedByHantei: hantei }); - const suffix = decSfx ? " " + decSfx : ""; + // The (E) marker sits in the CENTRE, between the two sides' scores: the + // string analogue of the paper score sheet's centre column and of the Excel + // export, where MiddleCellText writes it into the centre "vs" cell. When a + // match went to encho, " (E) " replaces the "–" separator; decision tags + // (Kiken / Fus. / DH / Ht) remain trailing. Paths with no two-sided score + // fall back to the full composed decisionSuffix (nothing to sit between). + const enchoSfx = enchoLabel(encho); + const sep = enchoSfx ? ` ${enchoSfx} ` : "–"; + const tailSfx = decisionSuffix({ decision, decidedByHantei: hantei }); + const tail = tailSfx ? " " + tailSfx : ""; + const fullSfx = decisionSuffix({ decision, encho, decidedByHantei: hantei }); if (score?.type === "bye") return "BYE"; const aStr = (ipponsA || []).filter(x => x && x !== "•").join(""); const bStr = (ipponsB || []).filter(x => x && x !== "•").join(""); @@ -124,13 +134,14 @@ function formatIpponsScore(ipponsA, ipponsB, score, decision, encho, decidedByHa if (isDraw) { if (!aStr && !bStr) { // Scoreless draw (operator pressed X with no ippons entered): canonical - // hikiwake glyph. This is the draw marker, not the hansoku triangle. - return "X" + suffix; + // hikiwake glyph. This is the draw marker, not the hansoku triangle. X + // is itself centre-column content, so the composed suffix follows it. + return "X" + (fullSfx ? " " + fullSfx : ""); } // Scored equal draw (e.g. 1–1 M–K hikiwake): show the actual points so // the operator and viewer see what was struck, not a bare X. The hikiwake // type is still recorded on the server: this is a display-only change. - return `${aStr || "·"}–${bStr || "·"}` + suffix; + return `${aStr || "·"}${sep}${bStr || "·"}` + tail; } if (!aStr && !bStr) { // Fall back when the per-side ippon arrays are absent but a score object @@ -142,13 +153,13 @@ function formatIpponsScore(ipponsA, ipponsB, score, decision, encho, decidedByHa if (score?.type === "ippon" && (score.winnerPts > 0 || score.loserPts > 0)) { const winnerLetters = (score.ippons || []).filter(x => x && x !== "•").join(""); const winnerStr = winnerLetters || `${score.winnerPts}`; - return `${winnerStr}–${score.loserPts}` + suffix; + return `${winnerStr}${sep}${score.loserPts}` + tail; } // No scores but a decision was recorded (e.g. kiken before any ippon // was struck): still print the suffix so the operator sees "Kiken". - return suffix ? suffix.trimStart() : ""; + return fullSfx || ""; } - return `${aStr || "·"}–${bStr || "·"}` + suffix; + return `${aStr || "·"}${sep}${bStr || "·"}` + tail; } // engiFlagScore: derive an engi match's flag-count score string from From 95b63d07df26217f4458c9e7a92a0d04d37d505c Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Fri, 31 Jul 2026 23:27:03 +0100 Subject: [PATCH 15/25] docs(mp-m4bn): illustrate the encho counter and the centred (E) marker Two new screenshots for the court-operators guide, captured from a live run at 339d55cb: the score editor with the overtime counter at two periods, and a public-viewer result showing the (E) marker in the centre of the score. The encho section previously reused the generic score-editor screenshot and showed the marker nowhere. --- docs/screenshots/mobile-encho-overtime.png | Bin 0 -> 148652 bytes docs/screenshots/viewer-result-encho.png | Bin 0 -> 40625 bytes .../court-operators/recording-decisions.md | 4 ++++ 3 files changed, 4 insertions(+) create mode 100644 docs/screenshots/mobile-encho-overtime.png create mode 100644 docs/screenshots/viewer-result-encho.png diff --git a/docs/screenshots/mobile-encho-overtime.png b/docs/screenshots/mobile-encho-overtime.png new file mode 100644 index 0000000000000000000000000000000000000000..166c2971de76809c9bd3145495e4f0c748b7bfe5 GIT binary patch literal 148652 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 EA3M3vl>h($ literal 0 HcmV?d00001 diff --git a/docs/screenshots/viewer-result-encho.png b/docs/screenshots/viewer-result-encho.png new file mode 100644 index 0000000000000000000000000000000000000000..927a41687b42d85e2ef0d43caaf48ff80f9890c7 GIT binary patch literal 40625 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 bfjgzl3J5>YF~7TR2j%ibM=RU~-^70aG53k? literal 0 HcmV?d00001 diff --git a/docs/user-guide/court-operators/recording-decisions.md b/docs/user-guide/court-operators/recording-decisions.md index 70b8892d1..63c29323f 100644 --- a/docs/user-guide/court-operators/recording-decisions.md +++ b/docs/user-guide/court-operators/recording-decisions.md @@ -32,8 +32,12 @@ Encho is the extra period played when a knockout match is level at the end of re In the score editor, open the **Overtime** control and tick **Encho started**. A counter appears so you can record how many overtime periods were fought, using the **+** and **-** buttons. The counter starts at 1 and has no upper limit: how many periods are fought, and how a match still level after them is finally settled (a judges' decision for an individual bout, a daihyosen for a team encounter), is the shimpan's call. Record what actually happened on court, however many periods that took. +![The score editor during a second overtime period: the Encho started box is ticked, the period counter reads two, and the header shows the live overtime readout.](../../screenshots/mobile-encho-overtime.png) + Completed results carry an overtime marker: **(E)**. The marker is the same however many overtime periods were fought. The counter records the number for the tournament log, but results never show it, so a bracket stays easy to read at a glance. The marker sits in the centre of the score, between the two sides' points, exactly where it goes on a paper score sheet: a match won by men in overtime reads **M (E) ·** on the court console, the public viewer, and the exported results workbook. Decision tags stay after the score: a withdrawal during overtime reads with a trailing **Kiken**, and a match sent to a judges' decision after overtime carries a trailing **Ht**. A team encounter shows its team totals in the score cell instead, so for those the marker appears on the bracket and in the exported workbook. +![A completed result on the public viewer: the winner's men ippon on the left, the E overtime marker in the centre between the two scores, and the opponent on the right.](../../screenshots/viewer-result-encho.png) + ## Daihyosen A daihyosen is a representative bout used to break a tie when points and ranking criteria cannot separate two teams. From 2d773cbf23daaaf1428c6a9232a55cc822bd0659 Mon Sep 17 00:00:00 2001 From: Ricardo Oliveira Date: Sat, 1 Aug 2026 09:05:36 +0100 Subject: [PATCH 16/25] refactor(mp-m4bn): middle marks vs/X/(E)/(DH); results ride with the competitor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator spec: "The middle can only be vs, X, (E), (DH). All others are results and should be in the results column. DH bouts do not have encho." The composed-suffix model (DecisionSuffix/decisionSuffix building "Kiken (E) Ht" strings) is dissolved into two mirrored primitives: - MiddleMark / middleMark: the ONE mark the centre may carry — X (tie), (E) (overtime), (DH) (rep bout), or "" (template "vs" stays). Mutually exclusive by rule: a match that went to encho cannot end tied, so X beats (E); a daihyosen is one-point sudden death, so DH bouts have no encho and (DH) beats (E). Stale data carrying an impossible combination renders the dominant mark alone. - SideMarks / sideMarks: result marks beside the competitor they name — Ht the hantei winner, Kiken the withdrawer, Fus. the no-show (the export also marks the fusensho winner; the viewer uses a bout badge). Excel: all six builder overlay sites write side marks into the competitor's score cell (team IV cells become "2 Kiken" style strings when marked) and only the middle mark into the vs cell. Web: formatIpponsScore renders [left cell] [middle] [right cell] with a new winnerSide param ("left"/"right", derived by winnerSideLR under the SHIRO-left convention); unattributable marks trail rather than drop. A scored draw now reads "M X K" (points around the tie mark, previously "M-K" with the X dropped). A hantei overtime win reads "M Ht (E) K". MatchCard meta strip carries only X/(E)/(DH) chips; Ht/Kiken/Fus. moved to the player lines. TV scoreboard/lobby/OBS centre chips show the middle mark via window.matchMiddleMark; window.decisionSuffix is gone. Docs and CLAUDE.md restate the two-kind model; the daihyosen section records that a daihyosen has no encho. Gates: make go/test exit 0, vitest 2412/2412, make docs/build exit 0. --- CLAUDE.md | 2 +- .../court-operators/recording-decisions.md | 6 +- internal/export/builder.go | 88 ++++--- internal/export/builder_test.go | 76 +++---- internal/export/suffix.go | 110 +++++---- internal/export/suffix_test.go | 143 ++++++------ .../js/__tests__/score_display.test.jsx | 123 ++++++---- .../__tests__/streaming_overlay_dh.test.jsx | 4 +- web-mobile/js/admin_shiaijo.jsx | 3 +- web-mobile/js/bracket.jsx | 214 ++++++++++-------- web-mobile/js/display_lobby.jsx | 2 +- web-mobile/js/display_scoreboard.jsx | 4 +- web-mobile/js/streaming_overlay.jsx | 6 +- 13 files changed, 446 insertions(+), 335 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 16ff71e8a..f81c10366 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,7 +119,7 @@ tournament-data/ - **Excel Layout**: Uses an **8-column per court** layout. Columns A and G (and their court-shifted counterparts) are 30 units wide; others are 5 units wide. A blank row separates pools vertically. - **Team Match Labels**: Summaries use **"IV"** (Individual Victories) and **"PW"** (Points Won). - **Court limit**: courts are labelled A–Z, so `--courts` is hard-capped at 26 and any value over that returns an error rather than silently truncating. -- **Match Decision Types** (`internal/domain/decision.go`): 10 canonical wire values: `""` (none), `"fought"`, `"hikiwake"` (draw), `"kiken"` (legacy withdrawal, maps to kiken-voluntary on YAML load), `"kiken-voluntary"` (FIK Art. 31, permanent), `"kiken-injury"` (FIK Art. 30, reinstateable), `"fusenpai"` (no-show), `"fusensho"` (per-bout default win), `"daihyosen"` (rep bout), `"kachinuki-exhaustion"`. Use `domain.IsKikenDecision(d)` / `domain.IsKikenDecisionStr(s)` to check any kiken variant. Legacy YAML `decision: true` migrates to `"hikiwake"`, `false` to `"fought"`, `"kiken"` to `"kiken-voluntary"` (Decision.UnmarshalYAML). Visual suffixes in the UI: `Kiken`, `Fus.`, `DH`, and `(E)` for encho — always bare, never a count (`periodCount` is recorded but deliberately not displayed on results; pinned by `enchoLabel` in both languages via `internal/export/testdata/encho_labels.json`). In score strings `(E)` sits in the CENTRE between the two sides' scores (`M (E) ·`), replacing the `–` separator — the score sheet's centre-column convention, matching the Excel export's centre "vs" cell; the other tags trail. +- **Match Decision Types** (`internal/domain/decision.go`): 10 canonical wire values: `""` (none), `"fought"`, `"hikiwake"` (draw), `"kiken"` (legacy withdrawal, maps to kiken-voluntary on YAML load), `"kiken-voluntary"` (FIK Art. 31, permanent), `"kiken-injury"` (FIK Art. 30, reinstateable), `"fusenpai"` (no-show), `"fusensho"` (per-bout default win), `"daihyosen"` (rep bout), `"kachinuki-exhaustion"`. Use `domain.IsKikenDecision(d)` / `domain.IsKikenDecisionStr(s)` to check any kiken variant. Legacy YAML `decision: true` migrates to `"hikiwake"`, `false` to `"fought"`, `"kiken"` to `"kiken-voluntary"` (Decision.UnmarshalYAML). Visual marks follow the score sheet's layout, split into two kinds. MIDDLE marks (the centre "vs" cell / score-string centre) — exactly one of `X` (tie), `(E)` (overtime, always bare: `periodCount` is recorded but never displayed), `(DH)` (rep bout); mutually exclusive by rule (a match that went to encho cannot end tied, so X beats (E); a daihyosen is one-point sudden death, so DH bouts have no encho). RESULT marks ride beside the competitor they name: `Ht` (hantei winner), `Kiken` (withdrawer), `Fus.` (no-show; the Excel export also marks the fusensho winner — the JS uses a bout badge instead). E.g. `M Ht (E) K`. Mirrored helpers: `middleMark`/`sideMarks` (bracket.jsx) ↔ `MiddleMark`/`SideMarks` (internal/export/suffix.go); `enchoLabel` pinned in both languages via `internal/export/testdata/encho_labels.json`. - **Competitor Eligibility** (`internal/state/competitor_status.go`, `internal/engine/eligibility.go`): a kiken/fusenpai decision auto-writes a `CompetitorStatus{Eligible: false}` for the loser; `engine.StartMatch(compID, matchID)` is the pre-flight gate that returns `*IneligibleCompetitorError` (matches `errors.Is(err, ErrIneligibleCompetitor)`). Maps to HTTP 409. Kiken-injury (FIK Art. 30) sets `CompetitorStatus.Reinstateable: true`; the admin can call `POST /api/competitions/:cid/competitors/:pid/reinstate` to restore eligibility. Kiken-voluntary (Art. 31) and fusenpai are not reinstateable. - **Team Lineups & Kachinuki** (`internal/domain/team_lineup.go`, `internal/engine/kachinuki.go`): TeamLineup pins position→player for a round. FIK 5-person rule: Senpo + Taisho mandatory; 1 vacancy must be Jiho, 2 must be Jiho+Fukusho, 3+ disqualifies. Kachinuki ("winner-stays-on") dynamically appends bouts via `engine.AdvanceKachinuki` until one team is exhausted (`DecisionKachinukiExhaustion`). - **Schedule Estimator** (`internal/engine/schedule.go`): `EstimateSchedule(EstimateInput) ScheduleEstimate` produces total/per-court minutes from match duration × multiplier × slowest-court buffer. Exposed via stateless `GET /api/schedule/estimate` on both the CLI web server and the mobile app. diff --git a/docs/user-guide/court-operators/recording-decisions.md b/docs/user-guide/court-operators/recording-decisions.md index 63c29323f..df3e636f4 100644 --- a/docs/user-guide/court-operators/recording-decisions.md +++ b/docs/user-guide/court-operators/recording-decisions.md @@ -34,7 +34,9 @@ In the score editor, open the **Overtime** control and tick **Encho started**. A ![The score editor during a second overtime period: the Encho started box is ticked, the period counter reads two, and the header shows the live overtime readout.](../../screenshots/mobile-encho-overtime.png) -Completed results carry an overtime marker: **(E)**. The marker is the same however many overtime periods were fought. The counter records the number for the tournament log, but results never show it, so a bracket stays easy to read at a glance. The marker sits in the centre of the score, between the two sides' points, exactly where it goes on a paper score sheet: a match won by men in overtime reads **M (E) ·** on the court console, the public viewer, and the exported results workbook. Decision tags stay after the score: a withdrawal during overtime reads with a trailing **Kiken**, and a match sent to a judges' decision after overtime carries a trailing **Ht**. A team encounter shows its team totals in the score cell instead, so for those the marker appears on the bracket and in the exported workbook. +Completed results follow the paper score sheet's layout. The centre, between the two sides' points, only ever carries one mark: **vs** while nothing is decided, **X** for a tie, **(E)** for a match that went to overtime, or **(DH)** for a team encounter sent to a representative bout. The marks never combine: a match that went to encho cannot end in a tie, and a daihyosen has no overtime. A match won by men in overtime reads **M (E) ·** on the court console, the public viewer, and the exported results workbook, and the marker is the same however many overtime periods were fought: the counter records the number for the tournament log, but results never show it. + +Everything else is a result, written beside the competitor it names: **Ht** next to the winner of a judges' decision, **Kiken** next to the competitor who withdrew, **Fus.** next to a no-show. A match decided by hantei after a tied overtime reads **M Ht (E) K**, with the winner's mark on the winner's side of the centre. A team encounter shows its team totals in the score cell instead, so for those the marks appear on the bracket and in the exported workbook. ![A completed result on the public viewer: the winner's men ippon on the left, the E overtime marker in the centre between the two scores, and the opponent on the right.](../../screenshots/viewer-result-encho.png) @@ -49,7 +51,7 @@ It applies in two situations: A tie that does not affect advancement is left as a shared rank with no extra bout. In league play, running the daihyosen is the operator's choice rather than an automatic step; see [Team standings and tie-breaks](../organisers/team-tournaments.md#team-standings-and-tie-breaks) for both options. -The bout is a single-point ippon-shobu with no time limit, between one representative from each tied team. The score editor lets you pick each team's representative from its roster. On the court console, the bout appears with a **DH** tag. In pool and league standings, the team that won its daihyosen carries a **DH** badge. +The bout is a single-point ippon-shobu with no time limit, between one representative from each tied team; because it runs until someone scores, a daihyosen has no encho. The score editor lets you pick each team's representative from its roster. On the court console, the bout appears with a **(DH)** mark in the centre of the score. In pool and league standings, the team that won its daihyosen carries a **DH** badge. ## Chusen (drawing lots) diff --git a/internal/export/builder.go b/internal/export/builder.go index 5e1ae3cdd..b4ba6f9f5 100644 --- a/internal/export/builder.go +++ b/internal/export/builder.go @@ -365,7 +365,6 @@ func overlayPoolScores(f *excelize.File, pools []helper.Pool, resultByID map[str } hantei := mr.DecidedByHantei != nil && *mr.DecidedByHantei - sfx := DecisionSuffix(mr.Decision, mr.Encho, hantei) var leftScore, rightScore string if engi { @@ -374,9 +373,12 @@ func overlayPoolScores(f *excelize.File, pools []helper.Pool, resultByID map[str leftScore = IpponsScore(leftIppons) rightScore = IpponsScore(rightIppons) } - setCellStr(f, sheetName, lVCol, excelRow, leftScore) - setCellStr(f, sheetName, rVCol, excelRow, rightScore) - if mid := MiddleCellText(mr.Decision, sfx); mid != "" { + // Result marks (Kiken/Fus./Ht) go in the competitor's score + // cell; the middle carries only its single mark (X/(E)). + lMark, rMark := SideMarksLR(mr.Decision, hantei, mr.Winner, mr.SideA, mr.SideB, mirror) + setCellStr(f, sheetName, lVCol, excelRow, joinSp(leftScore, lMark)) + setCellStr(f, sheetName, rVCol, excelRow, joinSp(rightScore, rMark)) + if mid := MiddleMark(mr.Decision, mr.Encho); mid != "" { setCellStr(f, sheetName, middleCol, excelRow, mid) } } @@ -492,8 +494,10 @@ func buildCourtMatchJobs(pools []helper.Pool, numCourts int, poolOrdinals map[st // rVCol (startCol+5) = right IV, rPCol (startCol+4) = right PW // // SideA is Red (left by default), SideB is Shiro (right); mirror swaps sides. -// The middle "vs" cell carries the encounter's decision suffix (DH etc.) or the -// hikiwake "X" marker when the team encounter is a draw. +// The middle "vs" cell carries only the encounter's single middle mark +// (X for a drawn encounter, (DH) when it went to a representative bout); +// encounter-level result marks (Kiken/Fus./Ht) ride in the competitor's IV +// cell, next to their victory count. func writeTeamSummaryCells(f *excelize.File, sheetName string, courtStartCol, excelRow int, mr state.MatchResult, mirror bool) { lVCol := colNum(courtStartCol + 1) lPCol := colNum(courtStartCol + 2) @@ -501,6 +505,9 @@ func writeTeamSummaryCells(f *excelize.File, sheetName string, courtStartCol, ex rPCol := colNum(courtStartCol + 4) rVCol := colNum(courtStartCol + 5) + hantei := mr.DecidedByHantei != nil && *mr.DecidedByHantei + lMark, rMark := SideMarksLR(mr.Decision, hantei, mr.Winner, mr.SideA, mr.SideB, mirror) + line := state.TeamResultFrom(mr.SubResults, mr.SideA, mr.SideB) if line != nil { // SideA = Aka, SideB = Shiro. Left is Aka unless mirror. @@ -509,19 +516,36 @@ func writeTeamSummaryCells(f *excelize.File, sheetName string, courtStartCol, ex if mirror { leftIV, leftPW, rightIV, rightPW = rightIV, rightPW, leftIV, leftPW } - setIntCellDirect(f, sheetName, lVCol, excelRow, leftIV) + setIVCellWithMark(f, sheetName, lVCol, excelRow, leftIV, lMark) setIntCellDirect(f, sheetName, lPCol, excelRow, leftPW) - setIntCellDirect(f, sheetName, rVCol, excelRow, rightIV) + setIVCellWithMark(f, sheetName, rVCol, excelRow, rightIV, rMark) setIntCellDirect(f, sheetName, rPCol, excelRow, rightPW) + } else { + // No summary line (e.g. a forfeit before any bout was fought): + // the result marks still need a home in the competitor's cell. + if lMark != "" { + setCellStr(f, sheetName, lVCol, excelRow, lMark) + } + if rMark != "" { + setCellStr(f, sheetName, rVCol, excelRow, rMark) + } } - hantei := mr.DecidedByHantei != nil && *mr.DecidedByHantei - sfx := DecisionSuffix(mr.Decision, mr.Encho, hantei) - if mid := MiddleCellText(mr.Decision, sfx); mid != "" { + if mid := MiddleMark(mr.Decision, mr.Encho); mid != "" { setCellStr(f, sheetName, middleCol, excelRow, mid) } } +// setIVCellWithMark writes a team IV count, appending a result mark +// ("2 Kiken") when one applies to that side; a markless cell stays numeric. +func setIVCellWithMark(f *excelize.File, sheetName, col string, row, iv int, mark string) { + if mark == "" { + setIntCellDirect(f, sheetName, col, row, iv) + return + } + setCellStr(f, sheetName, col, row, joinSp(fmt.Sprintf("%d", iv), mark)) +} + // writeTeamSubMatchScores writes each sub-bout's ippon letters onto the team // sub-match rows. Left ippons -> lVCol (startCol+1), right -> rVCol (startCol+5), // middle "vs" -> tie marker / suffix. subResults are keyed by Position (1-based); @@ -545,18 +569,15 @@ func writeTeamSubMatchScores(f *excelize.File, sheetName string, courtStartCol, if mirror { leftIppons, rightIppons = sub.IpponsB, sub.IpponsA } - lScore := IpponsScore(leftIppons) - rScore := IpponsScore(rightIppons) - if lScore != "" { + lMark, rMark := SideMarksLR(sub.Decision, sub.DecidedByHantei, sub.Winner, sub.SideA, sub.SideB, mirror) + if lScore := joinSp(IpponsScore(leftIppons), lMark); lScore != "" { setCellStr(f, sheetName, lVCol, excelRow, lScore) } - if rScore != "" { + if rScore := joinSp(IpponsScore(rightIppons), rMark); rScore != "" { setCellStr(f, sheetName, rVCol, excelRow, rScore) } - hantei := sub.DecidedByHantei - sfx := DecisionSuffix(sub.Decision, sub.Encho, hantei) - if mid := MiddleCellText(sub.Decision, sfx); mid != "" { + if mid := MiddleMark(sub.Decision, sub.Encho); mid != "" { setCellStr(f, sheetName, middleCol, excelRow, mid) } } @@ -873,12 +894,12 @@ func overlayBracketScores(f *excelize.File, bracketByNum map[int]state.BracketMa } } - sfx := DecisionSuffix(bm.Decision, bm.Encho, bm.DecidedByHantei) + lMark, rMark := SideMarksLR(bm.Decision, bm.DecidedByHantei, bm.Winner, bm.SideA, bm.SideB, mirror) - setCellStr(f, sheetName, lVCol, excelRow, leftScore) - setCellStr(f, sheetName, rVCol, excelRow, rightScore) + setCellStr(f, sheetName, lVCol, excelRow, joinSp(leftScore, lMark)) + setCellStr(f, sheetName, rVCol, excelRow, joinSp(rightScore, rMark)) - if mid := MiddleCellText(bm.Decision, sfx); mid != "" { + if mid := MiddleMark(bm.Decision, bm.Encho); mid != "" { setCellStr(f, sheetName, middleCol, excelRow, mid) } @@ -950,34 +971,41 @@ func overlayTeamBracketScores(f *excelize.File, bracketByNum map[int]state.Brack if mirror { leftIppons, rightIppons = sub.IpponsB, sub.IpponsA } - if s := IpponsScore(leftIppons); s != "" { + lMark, rMark := SideMarksLR(sub.Decision, sub.DecidedByHantei, sub.Winner, sub.SideA, sub.SideB, mirror) + if s := joinSp(IpponsScore(leftIppons), lMark); s != "" { setCellStr(f, sheetName, lVCol, excelRow, s) } - if s := IpponsScore(rightIppons); s != "" { + if s := joinSp(IpponsScore(rightIppons), rMark); s != "" { setCellStr(f, sheetName, rVCol, excelRow, s) } - subSfx := DecisionSuffix(sub.Decision, sub.Encho, sub.DecidedByHantei) - if mid := MiddleCellText(sub.Decision, subSfx); mid != "" { + if mid := MiddleMark(sub.Decision, sub.Encho); mid != "" { setCellStr(f, sheetName, middleCol, excelRow, mid) } } // IV/PW summary row = H + 5 + teamSize. summaryExcelRow := headerExcelRow + 5 + teamSize + lMark, rMark := SideMarksLR(bm.Decision, bm.DecidedByHantei, bm.Winner, bm.SideA, bm.SideB, mirror) if line := state.TeamResultFrom(bm.SubResults, bm.SideA, bm.SideB); line != nil { leftIV, leftPW := line.AkaIV, line.AkaPW rightIV, rightPW := line.ShiroIV, line.ShiroPW if mirror { leftIV, leftPW, rightIV, rightPW = rightIV, rightPW, leftIV, leftPW } - setIntCellDirect(f, sheetName, lVCol, summaryExcelRow, leftIV) + setIVCellWithMark(f, sheetName, lVCol, summaryExcelRow, leftIV, lMark) setIntCellDirect(f, sheetName, lPCol, summaryExcelRow, leftPW) - setIntCellDirect(f, sheetName, rVCol, summaryExcelRow, rightIV) + setIVCellWithMark(f, sheetName, rVCol, summaryExcelRow, rightIV, rMark) setIntCellDirect(f, sheetName, rPCol, summaryExcelRow, rightPW) + } else { + if lMark != "" { + setCellStr(f, sheetName, lVCol, summaryExcelRow, lMark) + } + if rMark != "" { + setCellStr(f, sheetName, rVCol, summaryExcelRow, rMark) + } } - sfx := DecisionSuffix(bm.Decision, bm.Encho, bm.DecidedByHantei) - if mid := MiddleCellText(bm.Decision, sfx); mid != "" { + if mid := MiddleMark(bm.Decision, bm.Encho); mid != "" { setCellStr(f, sheetName, middleCol, summaryExcelRow, mid) } diff --git a/internal/export/builder_test.go b/internal/export/builder_test.go index 9848ff72a..e1189ed2f 100644 --- a/internal/export/builder_test.go +++ b/internal/export/builder_test.go @@ -272,20 +272,13 @@ func TestBuildResultsWorkbook_DecisionSuffixInSheet(t *testing.T) { rows, err := f.GetRows(helper.SheetPoolMatches) require.NoError(t, err) - // The vs/middle cell should contain "Kiken (E) Ht". - foundSuffix := false - for _, row := range rows { - for _, cell := range row { - if cell == "Kiken (E) Ht" { - foundSuffix = true - break - } - } - if foundSuffix { - break - } - } - assert.True(t, foundSuffix, "pool matches sheet must contain 'Kiken (E) Ht' suffix") + // The middle carries only its one mark ("(E)"); the result marks ride in + // the competitors' score cells: Alice (winner, scored M, hantei) "M Ht", + // Bob (withdrew) "Kiken". + assert.True(t, sheetContainsCell(rows, "(E)"), "the vs cell must carry the (E) middle mark") + assert.True(t, sheetContainsCell(rows, "M Ht"), "the winner's cell must carry her score plus the Ht mark") + assert.True(t, sheetContainsCell(rows, "Kiken"), "the withdrawing side's cell must carry the Kiken mark") + assert.False(t, sheetContainsCell(rows, "Kiken (E) Ht"), "the old composed middle suffix must be gone") } func TestBuildResultsWorkbook_BracketScores(t *testing.T) { @@ -405,11 +398,12 @@ func TestBuildResultsWorkbook_DrawMatch(t *testing.T) { assert.True(t, foundX, "pool matches sheet must contain 'X' for a hikiwake (draw) with no ippons") } -// TestBuildResultsWorkbook_DrawWithSuffixKeepsMarker is the regression test for -// the draw-marker overwrite bug: a hikiwake that also went to encho (or was -// hantei-decided) must export the COMBINED "X (E)" / "X Ht" in the vs column, not -// just the suffix. Previously the code wrote "X" then overwrote it with the -// suffix, silently dropping the draw indicator from the archived workbook. +// TestBuildResultsWorkbook_DrawWithSuffixKeepsMarker pins the draw-vs-encho +// exclusivity rule: X means a tie and a match that went to encho cannot end +// tied, so stale data carrying both exports the X ALONE — the middle column +// holds exactly one mark and the tie wins. (This test previously guarded the +// opposite, a combined "X (E)" cell, from the era when the middle carried a +// composed suffix.) func TestBuildResultsWorkbook_DrawWithSuffixKeepsMarker(t *testing.T) { t.Parallel() dir, store, eng, compID := testSetup(t) @@ -441,10 +435,12 @@ func TestBuildResultsWorkbook_DrawWithSuffixKeepsMarker(t *testing.T) { rows, err := f.GetRows(helper.SheetPoolMatches) require.NoError(t, err) - assert.True(t, sheetContainsCell(rows, "X (E)"), - "a hikiwake-in-encho must export the combined 'X (E)' marker, not just '(E)'") + assert.True(t, sheetContainsCell(rows, "X"), + "a draw with stale encho data must export the X marker") assert.False(t, sheetContainsCell(rows, "(E)"), - "the bare '(E)' cell must not appear: it means the draw marker was dropped") + "a tie can never carry the overtime marker: X wins, (E) is dropped") + assert.False(t, sheetContainsCell(rows, "X (E)"), + "the old combined draw+encho cell must be gone") } // ------------------------------------------------------------ @@ -1903,7 +1899,7 @@ func TestBuildResultsWorkbook_TeamResults(t *testing.T) { require.Greater(t, len(elimRows[vp]), vpCol+1) assert.Equal(t, "1", elimRows[vp][vpCol+1], "Red A bracket IV must be literal 1 on the summary row") - assert.True(t, sheetContainsCell(elimRows, "DH"), "daihyosen suffix 'DH' must appear on the team encounter") + assert.True(t, sheetContainsCell(elimRows, "(DH)"), "the '(DH)' middle mark must appear on the team encounter") assert.True(t, sheetContainsCell(elimRows, "Red A"), "winner 'Red A' must be written as a literal") } @@ -2595,10 +2591,12 @@ func TestBuildResultsWorkbook_NonEngiStandingsHeadersUnchanged(t *testing.T) { // TDD-5: Engi special-case characterization tests // ------------------------------------------------------------ -// TestBuildResultsWorkbook_EngiDecisionSuffix characterizes the vs-cell text for -// a kiken-voluntary engi match: the middle cell must carry "Kiken" and both -// adjacent score cells (at column offsets -2 and +2 from the vs cell) must be -// blank because FlagsScorePair returns ("", "") when neither side scored flags. +// TestBuildResultsWorkbook_EngiDecisionSuffix characterizes the result-mark +// placement for a kiken-voluntary engi match: "Kiken" rides in the WITHDRAWING +// pair's score cell (the loser, SideB, right of the vs column), the winner's +// score cell stays blank (FlagsScorePair returns ("", "") when neither side +// scored flags), and the middle cell keeps its template "vs" (kiken is a +// result, not a middle mark). func TestBuildResultsWorkbook_EngiDecisionSuffix(t *testing.T) { t.Parallel() dir, store, eng, compID := testSetup(t) @@ -2637,26 +2635,24 @@ func TestBuildResultsWorkbook_EngiDecisionSuffix(t *testing.T) { rows, err := f.GetRows(helper.SheetPoolMatches) require.NoError(t, err) - // The vs/middle cell must carry "Kiken" for a kiken-voluntary engi match. + // The withdrawing pair's score cell must carry "Kiken". assert.True(t, sheetContainsCell(rows, "Kiken"), - "Pool Matches vs-cell must render 'Kiken' for a kiken-voluntary engi match") + "the withdrawing pair's score cell must render 'Kiken' for a kiken-voluntary engi match") - // Score cells flanking the vs column must be blank: FlagsScorePair returns ("", "") for a no-flag decision. - // The vs cell sits at column offset 3 from the court band start; score cells - // are at offsets 1 (left) and 5 (right), so -2 and +2 from the vs index. + // Geometry: the loser (SideB) sits RIGHT of the vs column, so from the + // Kiken cell the middle is 2 columns left (template "vs") and the + // winner's score cell 4 columns left (blank: no flags were scored). for _, row := range rows { for j, cell := range row { if cell != "Kiken" { continue } - require.GreaterOrEqual(t, j, 2, - "Kiken vs-cell must not appear in the first two columns") - assert.Equal(t, "", row[j-2], - "left score cell (col offset -2 from vs) must be blank for kiken with FlagsA=0") - if j+2 < len(row) { - assert.Equal(t, "", row[j+2], - "right score cell (col offset +2 from vs) must be blank for kiken with FlagsB=0") - } + require.GreaterOrEqual(t, j, 4, + "Kiken must sit in the right-hand score cell, not the first columns") + assert.Contains(t, []string{"", "vs"}, row[j-2], + "the middle cell must stay untouched (template text): kiken is a result, not a middle mark") + assert.Equal(t, "", row[j-4], + "the winner's score cell must be blank for kiken with FlagsA=0") } } } diff --git a/internal/export/suffix.go b/internal/export/suffix.go index bc13bf347..e2700e3e5 100644 --- a/internal/export/suffix.go +++ b/internal/export/suffix.go @@ -14,43 +14,83 @@ import ( "github.com/gitrgoliveira/bracket-creator/internal/state" ) -// DecisionSuffix returns the display suffix for a match decision, encho, and -// hantei flag. It follows the canonical JS decisionSuffix() in -// web-mobile/js/bracket.jsx, including the "Ht" suffix mandated by the "Excel + -// viewer parity" comment there (FIK 7-5 / 29-6). +// MiddleMark returns the ONE mark the centre "vs" cell may carry for a +// completed match. The middle column of a score sheet can only ever read: // -// Composition order: -// 1. Base decision label: kiken variants -> "Kiken"; fusenpai/fusensho -> "Fus."; daihyosen -> "DH". -// 2. If encho -> append " (E)" (always bare, regardless of period count). -// 3. If hanteiOn -> append " Ht". +// vs not yet decided (the template's own text; we return "" and leave it) +// X a tie (hikiwake) +// (E) the match went to overtime +// (DH) a team encounter sent to a representative bout // -// DELIBERATE DIVERGENCE from the JS: the JS omits fusensho (the per-bout default -// WIN) here because the viewer surfaces it via a separate bout badge. A flat -// spreadsheet cell has no such badge, so this export folds fusensho into the -// suffix ("Fus.") too, preserving the defaulted-bout signal in the archive -// rather than dropping it. +// The marks are mutually exclusive by rule, not by accident: X means a tie +// and a match that went to encho cannot end tied (encho runs until someone +// scores), so X beats (E) if stale data carries both; and a daihyosen bout is +// one-point sudden death, so DH bouts do not have encho and (DH) beats (E). // -// A zero/nil Encho (or PeriodCount == 0) is treated as no encho. -// Returns "" when no suffix applies. -func DecisionSuffix(decision string, encho *state.EnchoMetadata, decidedByHantei bool) string { - enchoSfx := enchoLabel(encho) - - var suffix string +// Everything else — Kiken, Fus., Ht — is a RESULT, not a middle mark, and +// belongs beside the competitor it names: see SideMarks. Mirrors +// middleMark()/formatIpponsScore in web-mobile/js/bracket.jsx. +func MiddleMark(decision string, encho *state.EnchoMetadata) string { switch { - case domain.IsKikenDecisionStr(decision): - suffix = "Kiken" - case decision == string(domain.DecisionFusenpai), decision == string(domain.DecisionFusensho): - suffix = "Fus." + case decision == state.DecisionDraw: + return "X" case decision == string(domain.DecisionDaihyosen): - suffix = "DH" + return "(DH)" + default: + return enchoLabel(encho) } +} - suffix = joinSp(suffix, enchoSfx) +// SideMarks returns the per-side result marks for a decision: winnerMark goes +// in the winning side's score cell, loserMark in the losing side's. +// +// hantei -> winner "Ht" (FIK 7-5 / 29-6: judges picked the winner) +// kiken -> loser "Kiken" (the mark names the competitor who withdrew) +// fusenpai -> loser "Fus." (the mark names the no-show) +// fusensho -> winner "Fus." (the default WIN names the present side) +// +// The JS viewer surfaces fusensho via a separate bout badge, so its +// sideMarks() omits it; a flat spreadsheet cell has no badge, so this export +// keeps the "Fus." mark (deliberate divergence, mirrored in the JS docstring). +func SideMarks(decision string, decidedByHantei bool) (winnerMark, loserMark string) { + switch { + case domain.IsKikenDecisionStr(decision): + loserMark = "Kiken" + case decision == string(domain.DecisionFusenpai): + loserMark = "Fus." + case decision == string(domain.DecisionFusensho): + winnerMark = "Fus." + } if decidedByHantei { - suffix = joinSp(suffix, "Ht") + winnerMark = joinSp(winnerMark, "Ht") } + return winnerMark, loserMark +} - return suffix +// SideMarksLR resolves SideMarks into (left, right) on-sheet order for a +// match between sideA and sideB. Default layout is SideA (Aka) on the left; +// mirror swaps the sides physically, matching the leftIppons/rightIppons +// swaps at the call sites. A missing or unmatchable winner (a draw, an +// unfinished match, or drifted data) yields no marks: result marks hang off +// a winner by definition. +func SideMarksLR(decision string, decidedByHantei bool, winner, sideA, sideB string, mirror bool) (left, right string) { + winnerMark, loserMark := SideMarks(decision, decidedByHantei) + if winnerMark == "" && loserMark == "" || winner == "" { + return "", "" + } + var aMark, bMark string + switch winner { + case sideA: + aMark, bMark = winnerMark, loserMark + case sideB: + aMark, bMark = loserMark, winnerMark + default: + return "", "" + } + if mirror { + return bMark, aMark + } + return aMark, bMark } // joinSp joins two display fragments with a single space, skipping empties, so @@ -87,22 +127,6 @@ func enchoLabel(encho *state.EnchoMetadata) string { return "(E)" } -// MiddleCellText composes the value for a match's centre "vs" cell from the -// hikiwake draw marker and the decision suffix. When a match is a draw AND also -// carries a suffix (a scoreless encho draw -> "X (E)", a hantei-decided draw -> -// "X Ht", a team encounter drawn into a daihyosen -> "X DH"), BOTH are kept so -// the exported workbook never loses the draw indicator. This mirrors -// formatIpponsScore in web-mobile/js/bracket.jsx, which renders "X" + suffix for -// a scoreless draw. Returns "" when neither applies, so the caller can leave the -// cell untouched rather than blanking a formula. -func MiddleCellText(decision, suffix string) string { - marker := "" - if decision == state.DecisionDraw { - marker = "X" - } - return joinSp(marker, suffix) -} - // FlagsScorePair returns the display strings for both sides of an engi bout. // // Pairwise rule: when EITHER side has a positive flag count, write BOTH counts diff --git a/internal/export/suffix_test.go b/internal/export/suffix_test.go index 8498a35ee..780119c71 100644 --- a/internal/export/suffix_test.go +++ b/internal/export/suffix_test.go @@ -17,105 +17,106 @@ func encho(periods int) *state.EnchoMetadata { return &state.EnchoMetadata{PeriodCount: periods} } -func TestDecisionSuffix(t *testing.T) { +func TestMiddleMark(t *testing.T) { t.Parallel() + // The middle can only ever be "" (template keeps its "vs"), "X", "(E)" + // or "(DH)" — one mark, mutually exclusive. X beats (E) because a match + // that went to encho cannot end tied; (DH) beats (E) because a daihyosen + // bout is one-point sudden death with no overtime. tests := []struct { name string decision string encho *state.EnchoMetadata - hantei bool want string }{ - // Base: no suffix - {name: "empty decision, no encho, no hantei", decision: "", encho: nil, hantei: false, want: ""}, - {name: "fought, no extras", decision: "fought", encho: nil, hantei: false, want: ""}, - - // Kiken variants - {name: "kiken-voluntary", decision: "kiken-voluntary", encho: nil, hantei: false, want: "Kiken"}, - {name: "kiken-injury", decision: "kiken-injury", encho: nil, hantei: false, want: "Kiken"}, - {name: "kiken (legacy)", decision: "kiken", encho: nil, hantei: false, want: "Kiken"}, - - // Fusenpai / fusensho - {name: "fusenpai", decision: "fusenpai", encho: nil, hantei: false, want: "Fus."}, - {name: "fusensho", decision: "fusensho", encho: nil, hantei: false, want: "Fus."}, - - // Daihyosen - {name: "daihyosen", decision: "daihyosen", encho: nil, hantei: false, want: "DH"}, - - // Encho only. Marker values are pinned by TestEnchoLabel_GoldenTable; - // these rows cover the pass-through into the composed suffix. The - // marker is bare "(E)" regardless of period count (mp-m4bn). - {name: "encho only (fought)", decision: "fought", encho: encho(1), hantei: false, want: "(E)"}, - {name: "encho nil vs zero periods", decision: "fought", encho: encho(0), hantei: false, want: ""}, - {name: "encho x2 renders the same bare marker", decision: "fought", encho: encho(2), hantei: false, want: "(E)"}, - - // Hantei only - {name: "hantei only (fought)", decision: "fought", encho: nil, hantei: true, want: "Ht"}, - - // Encho + hantei - {name: "encho + hantei (fought)", decision: "fought", encho: encho(2), hantei: true, want: "(E) Ht"}, - - // Base label + encho - {name: "Kiken + encho", decision: "kiken-voluntary", encho: encho(1), hantei: false, want: "Kiken (E)"}, - {name: "Fus. + encho", decision: "fusenpai", encho: encho(1), hantei: false, want: "Fus. (E)"}, - {name: "DH + encho", decision: "daihyosen", encho: encho(1), hantei: false, want: "DH (E)"}, - - // Base label + hantei - {name: "Kiken + hantei", decision: "kiken-voluntary", encho: nil, hantei: true, want: "Kiken Ht"}, - {name: "DH + hantei", decision: "daihyosen", encho: nil, hantei: true, want: "DH Ht"}, - - // Full composition: base + encho + hantei - {name: "Kiken + encho + hantei", decision: "kiken-voluntary", encho: encho(1), hantei: true, want: "Kiken (E) Ht"}, - {name: "DH + encho + hantei", decision: "daihyosen", encho: encho(3), hantei: true, want: "DH (E) Ht"}, - - // Hikiwake (draw) produces no base label; suffix still applies - {name: "hikiwake + hantei", decision: "hikiwake", encho: nil, hantei: true, want: "Ht"}, - {name: "hikiwake + encho", decision: "hikiwake", encho: encho(1), hantei: false, want: "(E)"}, - {name: "hikiwake + encho + hantei", decision: "hikiwake", encho: encho(1), hantei: true, want: "(E) Ht"}, + {name: "fought, no encho", decision: "fought", encho: nil, want: ""}, + {name: "empty decision, no encho", decision: "", encho: nil, want: ""}, + {name: "encho win", decision: "fought", encho: encho(1), want: "(E)"}, + {name: "encho win, multi-period stays bare", decision: "fought", encho: encho(4), want: "(E)"}, + {name: "zero periods is no encho", decision: "fought", encho: encho(0), want: ""}, + {name: "tie", decision: "hikiwake", encho: nil, want: "X"}, + {name: "tie beats stale encho data", decision: "hikiwake", encho: encho(2), want: "X"}, + {name: "daihyosen", decision: "daihyosen", encho: nil, want: "(DH)"}, + {name: "daihyosen beats stale encho data (DH bouts have no encho)", decision: "daihyosen", encho: encho(1), want: "(DH)"}, + {name: "kiken leaves the middle alone", decision: "kiken-voluntary", encho: nil, want: ""}, + {name: "kiken during overtime keeps the (E) middle", decision: "kiken-voluntary", encho: encho(1), want: "(E)"}, + {name: "fusenpai leaves the middle alone", decision: "fusenpai", encho: nil, want: ""}, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - got := DecisionSuffix(tc.decision, tc.encho, tc.hantei) - assert.Equal(t, tc.want, got) + assert.Equal(t, tc.want, MiddleMark(tc.decision, tc.encho)) }) } } -func TestMiddleCellText(t *testing.T) { +func TestSideMarks(t *testing.T) { t.Parallel() + // Result marks name their competitor: Ht the hantei winner, Kiken the + // withdrawer, Fus. the no-show (fusenpai, loser) or the defaulted winner + // (fusensho — kept in the export because a spreadsheet has no bout badge). tests := []struct { - name string - decision string - suffix string - want string + name string + decision string + hantei bool + wantWinner string + wantLoser string + }{ + {name: "fought", decision: "fought", hantei: false, wantWinner: "", wantLoser: ""}, + {name: "hantei", decision: "fought", hantei: true, wantWinner: "Ht", wantLoser: ""}, + {name: "kiken-voluntary", decision: "kiken-voluntary", hantei: false, wantWinner: "", wantLoser: "Kiken"}, + {name: "kiken-injury", decision: "kiken-injury", hantei: false, wantWinner: "", wantLoser: "Kiken"}, + {name: "kiken (legacy)", decision: "kiken", hantei: false, wantWinner: "", wantLoser: "Kiken"}, + {name: "fusenpai marks the no-show loser", decision: "fusenpai", hantei: false, wantWinner: "", wantLoser: "Fus."}, + {name: "fusensho marks the defaulted winner", decision: "fusensho", hantei: false, wantWinner: "Fus.", wantLoser: ""}, + {name: "daihyosen is a middle mark, not a side mark", decision: "daihyosen", hantei: false, wantWinner: "", wantLoser: ""}, + {name: "hikiwake has no side marks", decision: "hikiwake", hantei: false, wantWinner: "", wantLoser: ""}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + w, l := SideMarks(tc.decision, tc.hantei) + assert.Equal(t, tc.wantWinner, w, "winner mark") + assert.Equal(t, tc.wantLoser, l, "loser mark") + }) + } +} + +func TestSideMarksLR(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + decision string + hantei bool + winner string + mirror bool + wantLeft string + wantRight string }{ - // Non-draw: just the suffix (may be empty). - {name: "fought, no suffix", decision: "fought", suffix: "", want: ""}, - {name: "fought, kiken suffix", decision: "fought", suffix: "Kiken", want: "Kiken"}, - {name: "daihyosen suffix only", decision: "daihyosen", suffix: "DH", want: "DH"}, - - // Draw alone -> bare "X". - {name: "draw, no suffix", decision: "hikiwake", suffix: "", want: "X"}, - - // The regression this helper fixes: draw AND suffix must be COMBINED, - // not overwritten. Previously the code wrote "X" then replaced it with - // the suffix, dropping the draw marker. - {name: "draw + encho", decision: "hikiwake", suffix: "(E)", want: "X (E)"}, - {name: "draw + hantei", decision: "hikiwake", suffix: "Ht", want: "X Ht"}, - {name: "draw + encho + hantei", decision: "hikiwake", suffix: "(E) Ht", want: "X (E) Ht"}, - {name: "draw + DH", decision: "hikiwake", suffix: "DH", want: "X DH"}, + // Default layout: SideA (Aka) left, SideB right. + {name: "hantei, A wins", decision: "fought", hantei: true, winner: "A", wantLeft: "Ht", wantRight: ""}, + {name: "hantei, B wins", decision: "fought", hantei: true, winner: "B", wantLeft: "", wantRight: "Ht"}, + {name: "hantei, B wins, mirrored", decision: "fought", hantei: true, winner: "B", mirror: true, wantLeft: "Ht", wantRight: ""}, + {name: "kiken, A wins marks B", decision: "kiken-voluntary", winner: "A", wantLeft: "", wantRight: "Kiken"}, + {name: "kiken, A wins, mirrored", decision: "kiken-voluntary", winner: "A", mirror: true, wantLeft: "Kiken", wantRight: ""}, + {name: "no winner recorded: marks have no home", decision: "kiken-voluntary", winner: "", wantLeft: "", wantRight: ""}, + {name: "drifted winner name: no marks rather than a guess", decision: "kiken-voluntary", winner: "C", wantLeft: "", wantRight: ""}, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - assert.Equal(t, tc.want, MiddleCellText(tc.decision, tc.suffix)) + l, r := SideMarksLR(tc.decision, tc.hantei, tc.winner, "A", "B", tc.mirror) + assert.Equal(t, tc.wantLeft, l, "left mark") + assert.Equal(t, tc.wantRight, r, "right mark") }) } } diff --git a/web-mobile/js/__tests__/score_display.test.jsx b/web-mobile/js/__tests__/score_display.test.jsx index c89f3e89e..9b0969242 100644 --- a/web-mobile/js/__tests__/score_display.test.jsx +++ b/web-mobile/js/__tests__/score_display.test.jsx @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { readFileSync } from 'fs'; import { resolve } from 'path'; -import { decisionSuffix, formatIpponsScore, ipponsFromScore, matchStateCell } from '../bracket.jsx'; +import { enchoLabel, formatIpponsScore, ipponsFromScore, matchStateCell } from '../bracket.jsx'; // Convention enforced across all match-list views: // SHIRO (sideB) is always displayed on the LEFT. @@ -54,16 +54,15 @@ describe('formatIpponsScore', () => { expect(formatIpponsScore([], [], null, 'hikiwake')).toBe('X'); }); - it('returns the points for a scored equal draw (1–1), not bare X', () => { - // Item 6: operator entered M on one side, K on the other, then toggled - // hikiwake. The ippons are preserved on the server (the score.type is - // hikiwake but ipponsA/B are non-empty). Show the techniques so the - // viewer sees what was struck rather than losing that information. - expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null)).toBe('M–K'); + it('returns the points around the X for a scored equal draw (1–1)', () => { + // Item 6 + the middle-column rule: the ippons are preserved on the + // server, so show the techniques, AND the tie's X is the middle mark, + // so the viewer sees both what was struck and that it was a tie. + expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null)).toBe('M X K'); }); it('shows scored draw with one empty side using the placeholder dot', () => { - expect(formatIpponsScore(['M'], [], { type: 'hikiwake' }, null)).toBe('M–·'); + expect(formatIpponsScore(['M'], [], { type: 'hikiwake' }, null)).toBe('M X ·'); }); it('falls back to numeric score when ippons arrays are empty AND score has no ippon letters', () => { @@ -128,54 +127,80 @@ describe('formatIpponsScore', () => { }); }); - // FR-033: encho renders as a bare "(E)" in the CENTRE of the score string, - // replacing the "–" separator (the score sheet's centre-column convention), - // so match lists and bracket views surface overtime at a glance. - describe('encho marker', () => { + // The middle of a score string carries exactly ONE mark — X (tie), (E) + // (overtime), (DH) (rep bout) — or the plain "–" separator. X beats (E) + // because a match that went to encho cannot end tied; (DH) beats (E) + // because a daihyosen bout is one-point sudden death with no overtime. + describe('middle mark', () => { it('places (E) between the scores when encho has a positive period count', () => { expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 })).toBe('M (E) K'); }); - it('places (E) between the points of a scored draw (shows points, not X)', () => { - // Item 6: scored equal draw shows techniques + encho marker, not bare X. - expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null, { periodCount: 1 })).toBe('M (E) K'); + it('a tie is X, never (E): stale draw+encho data renders the X alone', () => { + expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null, { periodCount: 1 })).toBe('M X K'); + expect(formatIpponsScore([], [], null, 'hikiwake', { periodCount: 2 })).toBe('X'); }); - it('appends the marker to a no-score draw (X is the scoreless-draw glyph)', () => { - // mp-m4bn: bare (E) regardless of period count — results never show numbers. - expect(formatIpponsScore([], [], null, 'hikiwake', { periodCount: 2 })).toBe('X (E)'); + it('a daihyosen is (DH), never (E): DH bouts have no encho', () => { + // Winner side known: Ht (the hantei winner mark) sits in the winner's + // cell, (DH) alone holds the middle. + expect(formatIpponsScore([], [], null, 'daihyosen', { periodCount: 3 }, true, 'left')).toBe('Ht (DH) ·'); }); - it('does not append (E) when periodCount is 0', () => { + it('does not mark the middle when periodCount is 0', () => { expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 0 })).toBe('M–K'); }); - // Marker VALUES (bare "(E)" for any positive count) are pinned by the - // golden it.each at the bottom of this file; here only the composition - // into a score string is under test. - it('composes the marker with a decision label and hantei', () => { - expect(formatIpponsScore([], [], null, 'daihyosen', { periodCount: 3 }, true)).toBe('DH (E) Ht'); - }); - it('is a no-op when encho argument is missing entirely', () => { expect(formatIpponsScore(['M'], ['K'], null, null)).toBe('M–K'); }); }); + // Result marks (Kiken / Fus. / Ht) ride in the cell of the competitor they + // name when the caller supplies winnerSide (matchScoreStr derives it via + // winnerSideLR); without it they trail so the result is never dropped. + describe('side result marks', () => { + it('kiken marks the withdrawing (losing) side', () => { + expect(formatIpponsScore(['M'], [], null, 'kiken-voluntary', null, false, 'left')).toBe('M–Kiken'); + expect(formatIpponsScore([], [], null, 'kiken-voluntary', null, false, 'right')).toBe('Kiken–·'); + }); + + it('fusenpai marks the no-show (losing) side', () => { + // Winner on the left → the no-show's Fus. lands in the right cell. + expect(formatIpponsScore([], [], null, 'fusenpai', null, false, 'left')).toBe('·–Fus.'); + }); + + it('kiken during overtime: loser mark plus the (E) middle', () => { + expect(formatIpponsScore(['M'], [], null, 'kiken-injury', { periodCount: 1 }, false, 'left')).toBe('M (E) Kiken'); + }); + + it('falls back to a trailing mark when the winner side is unknown', () => { + expect(formatIpponsScore(['M'], [], null, 'kiken-voluntary', null, false)).toBe('M–· Kiken'); + }); + }); + // FIK Art. 7-5 / 29-6: a knockout match that remains tied in encho is // decided by referee hantei. The renderer must mark this distinctly so // it's not confused with an ippon-derived win. - describe('hantei (judges\' decision) suffix', () => { - it('appends "(E) Ht" for a 0-0 hantei-decided overtime', () => { - // Tied 0-0 in encho, AKA awarded by hantei. No ippons → suffix only. + describe('hantei (judges\' decision) winner mark', () => { + it('a 0-0 hantei-decided overtime puts Ht in the winner\'s cell', () => { + // Tied 0-0 in encho, SHIRO (left) awarded by hantei: the winner's cell + // carries the Ht mark, the middle carries (E), the loser shows the dot. + expect(formatIpponsScore([], [], null, null, { periodCount: 1 }, true, 'left')).toBe('Ht (E) ·'); + expect(formatIpponsScore([], [], null, null, { periodCount: 1 }, true, 'right')).toBe('· (E) Ht'); + }); + + it('falls back to "(E) Ht" when the winner side is unknown', () => { expect(formatIpponsScore([], [], null, null, { periodCount: 1 }, true)).toBe('(E) Ht'); }); - it('combines (E) Ht for a hantei-decided overtime', () => { - // Realistic: tied with scores, then hantei chose a winner. Backend - // sends decidedByHantei=true alongside the tied ippons. - const result = formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 }, true); - expect(result).toBe('M (E) K Ht'); + it('rides next to the winner\'s letters on a scored hantei overtime', () => { + const result = formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 }, true, 'left'); + expect(result).toBe('M Ht (E) K'); + }); + + it('trails when the winner side is unknown (never dropped)', () => { + expect(formatIpponsScore(['M'], ['K'], null, null, { periodCount: 1 }, true)).toBe('M (E) K Ht'); }); it('omits Ht when decidedByHantei is false/missing', () => { @@ -189,14 +214,15 @@ describe('formatIpponsScore', () => { // object, so score.hantei can never appear in real match data. Only the // positional decidedByHantei arg matters. expect(formatIpponsScore(['M'], ['K'], { type: 'ippon', hantei: true }, null, { periodCount: 1 })).toBe('M (E) K'); - expect(formatIpponsScore(['M'], ['K'], { type: 'ippon', hantei: true }, null, { periodCount: 1 }, true)).toBe('M (E) K Ht'); + expect(formatIpponsScore(['M'], ['K'], { type: 'ippon', hantei: true }, null, { periodCount: 1 }, true, 'left')).toBe('M Ht (E) K'); }); }); }); // Item 6 regression suite: scored-draw rendering (formatIpponsScore). -// Pinned so future changes to the hikiwake branch can't silently revert -// the display from "M–K" back to the bare-X glyph. +// Pinned so future changes to the hikiwake branch can't silently drop the +// struck techniques (they surround the X) or the X itself (the tie's one +// legal middle mark). describe('formatIpponsScore: hikiwake draw display (item 6)', () => { it('0–0 hikiwake (score.type) → bare X', () => { expect(formatIpponsScore([], [], { type: 'hikiwake' }, null)).toBe('X'); @@ -206,15 +232,17 @@ describe('formatIpponsScore: hikiwake draw display (item 6)', () => { expect(formatIpponsScore([], [], null, 'hikiwake')).toBe('X'); }); - it('1–1 hikiwake (ipponsA=[M], ipponsB=[K]) → "M–K" (points shown, no X)', () => { + it('1–1 hikiwake (ipponsA=[M], ipponsB=[K]) → "M X K" (points around the X)', () => { // Canonical scored-equal-draw case: both sides hit one ippon, operator - // toggled hikiwake. Server keeps ippons; display must show techniques. - expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null)).toBe('M–K'); + // toggled hikiwake. Server keeps ippons; display shows the techniques + // AND the draw mark in the middle. + expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null)).toBe('M X K'); }); - it('1–1 hikiwake with encho (periodCount>0) → "M (E) K"', () => { - // The centred encho marker must survive the scored-draw path unchanged. - expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null, { periodCount: 1 })).toBe('M (E) K'); + it('1–1 hikiwake with stale encho data → still "M X K" (X beats (E))', () => { + // A tie cannot have gone to encho; if drifted data carries both, the + // draw mark wins and the overtime marker is dropped. + expect(formatIpponsScore(['M'], ['K'], { type: 'hikiwake' }, null, { periodCount: 1 })).toBe('M X K'); }); }); @@ -334,13 +362,12 @@ describe('enchoLabel Go/JS mirror (mp-m4bn)', () => { ).toBeGreaterThan(0); }); - // decisionSuffix with a bare encho block returns enchoLabel's output - // byte-for-byte (no base label, no Ht), so the table value is asserted - // EXACTLY — an unrelated formatIpponsScore change cannot redden this - // with a misleading "update both renderers" message. + // enchoLabel is driven directly, so the table value is asserted EXACTLY — + // an unrelated formatIpponsScore change cannot redden this with a + // misleading "update both renderers" message. it.each(table.cases)('periodCount $periodCount renders "$label"', ({ periodCount, label }) => { expect( - decisionSuffix({ encho: { periodCount } }), + enchoLabel({ periodCount }), 'JS enchoLabel disagrees with the shared table; update BOTH renderers, not just this one' ).toBe(label); }); diff --git a/web-mobile/js/__tests__/streaming_overlay_dh.test.jsx b/web-mobile/js/__tests__/streaming_overlay_dh.test.jsx index 8be7646d0..11e656d11 100644 --- a/web-mobile/js/__tests__/streaming_overlay_dh.test.jsx +++ b/web-mobile/js/__tests__/streaming_overlay_dh.test.jsx @@ -27,7 +27,7 @@ describe('StreamingOverlay DH signal', () => { let runtime; let StreamingOverlay; const savedGlobals = {}; - const STUBBED = ['isHikiwake', 'decisionSuffix', 'Term']; + const STUBBED = ['isHikiwake', 'matchMiddleMark', 'Term']; const runningMatch = (id) => ({ id, court: 'A', status: 'running', @@ -48,7 +48,7 @@ describe('StreamingOverlay DH signal', () => { ? { had: true, val: global.window[k] } : { had: false }; }); global.window.isHikiwake = () => false; - global.window.decisionSuffix = () => ''; + global.window.matchMiddleMark = () => ''; global.window.Term = function Term(props) { return { type: 'span', props, children: props?.children }; }; vi.resetModules(); ({ StreamingOverlay } = await import('../streaming_overlay.jsx')); diff --git a/web-mobile/js/admin_shiaijo.jsx b/web-mobile/js/admin_shiaijo.jsx index 9ac3751a1..bbc671e59 100644 --- a/web-mobile/js/admin_shiaijo.jsx +++ b/web-mobile/js/admin_shiaijo.jsx @@ -245,7 +245,8 @@ export function shiaijoScoreCell(m) { const ipponsA = m.ipponsA || (window.ipponsFromScore ? window.ipponsFromScore(m.scoreA) : []); const ipponsB = m.ipponsB || (window.ipponsFromScore ? window.ipponsFromScore(m.scoreB) : []); const s = window.formatIpponsScore - ? window.formatIpponsScore(ipponsB, ipponsA, m.score, m.decision, m.encho, m.decidedByHantei) + ? window.formatIpponsScore(ipponsB, ipponsA, m.score, m.decision, m.encho, m.decidedByHantei, + window.winnerSideLR ? window.winnerSideLR(m) : null) : ""; return s ? { kind: "ippon", ippon: s } : { kind: "none" }; } diff --git a/web-mobile/js/bracket.jsx b/web-mobile/js/bracket.jsx index 0c96d66c8..342821fe9 100644 --- a/web-mobile/js/bracket.jsx +++ b/web-mobile/js/bracket.jsx @@ -30,11 +30,6 @@ function roundLabel(roundIdx, total) { return `R${2 ** (fromEnd + 1)}`; } -const DECISION_CHIPS = { - fusenpai: { term: "fusenpai", label: "Fus." }, - daihyosen: { term: "daihyosen", label: "DH" }, -}; - // sideA = top = Aka (Red), sideB = bottom = Shiro (White) function sideLabel(side) { return side === "a" ? "AKA" : "SHIRO"; @@ -55,32 +50,63 @@ function enchoLabel(encho) { return n > 0 ? "(E)" : ""; } -// Decision-driven suffix appended to score strings on schedule rows, bracket -// nodes, viewer cards, and TV displays. Mirrors the Visual Rendering Contract -// in specs/003-tournament-gap-closure/contracts/match-decisions.md §Visual: -// decision == "kiken" → "Kiken" -// decision == "fusenpai" → "Fus." -// decision == "daihyosen" → "DH" -// The enchoLabel marker is appended on top of any other suffix, so a kiken in -// overtime renders "0–2 Kiken (E)". `fusensho` is per-bout -// only: handled by a separate bout badge, not by this helper. Pure and DOM-free -// so it can be reused by display.jsx (which builds its own scoreline) without -// dragging in the rest of formatIpponsScore's bye/hantei special cases. -// Mirrored by DecisionSuffix in internal/export/suffix.go — keep the -// composition order and single-space joins identical (its docstring records -// one deliberate divergence: the export also folds fusensho into the suffix). -function decisionSuffix(match) { +// middleMark: the ONE mark the centre of a score may carry. The middle column +// of a score sheet can only ever read: +// vs not yet decided (callers render their own "vs"/"–" placeholder) +// X a tie (hikiwake) +// (E) the match went to overtime +// (DH) a team encounter sent to a representative bout +// Mutually exclusive by rule: X means a tie and a match that went to encho +// cannot end tied (encho runs until someone scores), so X beats (E); and a +// daihyosen bout is one-point sudden death, so DH bouts do not have encho and +// (DH) beats (E). Everything else — Kiken, Fus., Ht — is a RESULT and belongs +// beside the competitor it names: see sideMarks. Mirrors MiddleMark in +// internal/export/suffix.go. +function middleMark(decision, encho) { + if (isHikiwakeBC(decision)) return "X"; + if (decision === "daihyosen") return "(DH)"; + return enchoLabel(encho); +} + +// matchMiddleMark: middleMark for a match object (TV scoreboard, lobby, OBS +// lower-third — surfaces that render the mark as a single centre chip). A +// client-derived score.type of hikiwake counts as a draw like the decision. +function matchMiddleMark(match) { if (!match) return ""; - const d = match.decision || ""; - let base = ""; - if (isKikenDecisionBC(d)) base = "Kiken"; - else if (DECISION_CHIPS[d]) base = DECISION_CHIPS[d].label; - // FIK 7-5 / 29-6: judges' decision after a tied encho. Mark explicitly so - // a hantei-decided final is distinguishable from an ippon-derived one - // (audit + Excel + viewer parity). - return [base, enchoLabel(match.encho), match.decidedByHantei ? "Ht" : ""] - .filter(Boolean) - .join(" "); + const draw = isHikiwakeBC(match.decision) || isHikiwakeBC(match.score?.type); + return draw ? "X" : middleMark(match.decision, match.encho); +} + +// sideMarks: the per-side RESULT marks. winner goes in the winning side's +// score cell, loser in the losing side's — the mark names its competitor: +// hantei → winner "Ht" (FIK 7-5 / 29-6: judges picked the winner) +// kiken → loser "Kiken" (the competitor who withdrew) +// fusenpai → loser "Fus." (the no-show) +// `fusensho` (the per-bout default WIN) is deliberately absent here: the +// viewer surfaces it via a separate bout badge. The Excel export has no +// badges, so its SideMarks (internal/export/suffix.go) folds fusensho in as +// a winner-side "Fus." — the one deliberate divergence between the mirrors. +function sideMarks(decision, decidedByHantei) { + let winner = "", loser = ""; + if (isKikenDecisionBC(decision)) loser = "Kiken"; + else if (decision === "fusenpai") loser = "Fus."; + if (decidedByHantei) winner = winner ? `${winner} Ht` : "Ht"; + return { winner, loser }; +} + +// winnerSideLR: which DISPLAY side won, under the SHIRO-left convention every +// score string uses (sideB = Shiro = left, sideA = Aka = right). Returns +// "left" | "right" | null (no winner recorded, or drifted data). Accepts both +// object sides ({id, name}) and bare name strings. +function winnerSideLR(m) { + if (!m || !m.winner) return null; + const idOf = s => (s && typeof s === "object" ? s.id : null); + const nameOf = s => (s && typeof s === "object" ? s.name : s); + const wId = idOf(m.winner); + const wName = nameOf(m.winner); + if ((wId && wId === idOf(m.sideB)) || (wName && wName === nameOf(m.sideB))) return "left"; + if ((wId && wId === idOf(m.sideA)) || (wName && wName === nameOf(m.sideA))) return "right"; + return null; } // Derive an ippon array from a Go-formatted scoreA/scoreB string. @@ -96,70 +122,70 @@ function ipponsFromScore(scoreStr) { } // Format ippons as a readable score string: ["M","K"] → "MK", [] → "" -// Returns something like "MM–K", "M–·", "X" (hikiwake, scored or not), "BYE". -// Hantei (judges' decision after tied encho) is NOT a separate return value; -// it surfaces as an "Ht" suffix appended by decisionSuffix when -// decidedByHantei=true: e.g. "M (E) K Ht". +// Returns something like "MM–K", "M (E) ·", "M X K", "X", "BYE". // -// FR-033: when `encho` carries a positive periodCount, the bare "(E)" marker -// (via enchoLabel) sits in the CENTRE, replacing the "–" separator ("M (E) ·"), -// so operators and viewers see at a glance that the match went to overtime — -// the score sheet's centre-column convention. Argument is optional and -// defaults to no-encho when absent. -// -// T097: kiken / fusenpai / daihyosen append labelled TRAILING suffixes: -// wired through decisionSuffix() so the same labels are used by display.jsx's -// hand-rolled score block. Only the encho marker is pulled out of the -// composed suffix and centred; paths with no two-sided score still render -// decisionSuffix() whole. -function formatIpponsScore(ipponsA, ipponsB, score, decision, encho, decidedByHantei) { +// The string is the flat analogue of a paper score sheet row: +// [left cell] [middle] [right cell] +// The MIDDLE carries exactly one mark — "X" (tie), "(E)" (overtime), "(DH)" +// (rep bout) — or the plain "–" separator; see middleMark for the exclusivity +// rules. RESULT marks (Kiken / Fus. / Ht) ride in the cell of the competitor +// they name ("M Ht (E) ·", "·–Kiken"), which needs `winnerSide` +// ("left" | "right", from winnerSideLR) — without it the marks fall back to +// trailing after the score, still readable but unattributed. +// Mirrors the Excel export (internal/export/suffix.go MiddleMark/SideMarks + +// builder cell writes). +function formatIpponsScore(ipponsLeft, ipponsRight, score, decision, encho, decidedByHantei, winnerSide) { // decidedByHantei (positional) is the canonical flag. The `typeof` guard // lets callers that omit the arg safely get false without sending undefined. const hantei = typeof decidedByHantei === "boolean" ? decidedByHantei : false; - // The (E) marker sits in the CENTRE, between the two sides' scores: the - // string analogue of the paper score sheet's centre column and of the Excel - // export, where MiddleCellText writes it into the centre "vs" cell. When a - // match went to encho, " (E) " replaces the "–" separator; decision tags - // (Kiken / Fus. / DH / Ht) remain trailing. Paths with no two-sided score - // fall back to the full composed decisionSuffix (nothing to sit between). - const enchoSfx = enchoLabel(encho); - const sep = enchoSfx ? ` ${enchoSfx} ` : "–"; - const tailSfx = decisionSuffix({ decision, decidedByHantei: hantei }); - const tail = tailSfx ? " " + tailSfx : ""; - const fullSfx = decisionSuffix({ decision, encho, decidedByHantei: hantei }); if (score?.type === "bye") return "BYE"; - const aStr = (ipponsA || []).filter(x => x && x !== "•").join(""); - const bStr = (ipponsB || []).filter(x => x && x !== "•").join(""); + const aStr = (ipponsLeft || []).filter(x => x && x !== "•").join(""); + const bStr = (ipponsRight || []).filter(x => x && x !== "•").join(""); const isDraw = isHikiwakeBC(decision) || isHikiwakeBC(score?.type); + if (isDraw) { - if (!aStr && !bStr) { - // Scoreless draw (operator pressed X with no ippons entered): canonical - // hikiwake glyph. This is the draw marker, not the hansoku triangle. X - // is itself centre-column content, so the composed suffix follows it. - return "X" + (fullSfx ? " " + fullSfx : ""); - } - // Scored equal draw (e.g. 1–1 M–K hikiwake): show the actual points so - // the operator and viewer see what was struck, not a bare X. The hikiwake - // type is still recorded on the server: this is a display-only change. - return `${aStr || "·"}${sep}${bStr || "·"}` + tail; + // A tie's middle is X and NOTHING else: a match that went to encho cannot + // have ended tied, and hantei picks a winner, so any such stale data is + // dropped rather than displayed as a contradiction. + if (!aStr && !bStr) return "X"; + // Scored equal draw (e.g. 1–1 M/K hikiwake): show the points around the + // draw mark so the viewer sees what was struck AND that it was a tie. + return `${aStr || "·"} X ${bStr || "·"}`; } + + const mid = middleMark(decision, encho); + const sep = mid ? ` ${mid} ` : "–"; + const marks = sideMarks(decision, hantei); + const attributed = winnerSide === "left" || winnerSide === "right"; + const leftMark = attributed ? (winnerSide === "left" ? marks.winner : marks.loser) : ""; + const rightMark = attributed ? (winnerSide === "right" ? marks.winner : marks.loser) : ""; + // Unattributable marks (no winnerSide from the caller) trail the string so + // the result is never silently dropped. + const trail = !attributed && (marks.winner || marks.loser) + ? " " + [marks.winner, marks.loser].filter(Boolean).join(" ") + : ""; + const cell = (str, mark) => (str ? (mark ? `${str} ${mark}` : str) : mark); + if (!aStr && !bStr) { // Fall back when the per-side ippon arrays are absent but a score object // exists (e.g. server-provided bracket scores). Prefer the winner's waza // LETTERS (score.ippons) over a bare count so the schedule always shows // technique letters when the data carries them: only the loser, which is - // stored as a count not letters, falls back to a number. Winner-first - // order matches the historical numeric fallback (no orientation change). + // stored as a count not letters, falls back to a number. This path is + // winner-first by construction, so the marks attach positionally. if (score?.type === "ippon" && (score.winnerPts > 0 || score.loserPts > 0)) { const winnerLetters = (score.ippons || []).filter(x => x && x !== "•").join(""); const winnerStr = winnerLetters || `${score.winnerPts}`; - return `${winnerStr}${sep}${score.loserPts}` + tail; + return `${cell(winnerStr, marks.winner)}${sep}${cell(`${score.loserPts}`, marks.loser)}`; + } + // No scores at all (kiken before any ippon, a 0-0 hantei): the cells + // hold only their result marks around the middle mark ("Ht (E) ·"). + if (leftMark || rightMark) { + return `${cell("", leftMark) || "·"}${sep}${cell("", rightMark) || "·"}`; } - // No scores but a decision was recorded (e.g. kiken before any ippon - // was struck): still print the suffix so the operator sees "Kiken". - return fullSfx || ""; + return [mid, marks.winner, marks.loser].filter(Boolean).join(" "); } - return `${aStr || "·"}${sep}${bStr || "·"}` + tail; + return `${cell(aStr, leftMark) || "·"}${sep}${cell(bStr, rightMark) || "·"}` + trail; } // engiFlagScore: derive an engi match's flag-count score string from @@ -269,8 +295,15 @@ const MatchCard = React.memo(({ match, variant, showDojo, onClick, highlighted, // engi flag editor (see engiFlagScore in this file for the shared-cell // equivalent). const isEngiMatch = match.flagsA != null || match.flagsB != null; - const aScore = isDone ? (isEngiMatch ? String(match.flagsA || 0) : (ipponsA.join("") || null)) : null; - const bScore = isDone ? (isEngiMatch ? String(match.flagsB || 0) : (ipponsB.join("") || null)) : null; + // Result marks (Kiken / Fus. / Ht) ride with the competitor they name, in + // that side's score slot — the node's "results column". The meta strip + // above carries only the middle marks (X / (E) / (DH)). + const cardMarks = isDone ? sideMarks(match.decision, !!match.decidedByHantei) : { winner: "", loser: "" }; + const aMark = aWin ? cardMarks.winner : (bWin ? cardMarks.loser : ""); + const bMark = bWin ? cardMarks.winner : (aWin ? cardMarks.loser : ""); + const withMark = (s, mk) => (mk ? (s ? `${s} ${mk}` : mk) : s); + const aScore = isDone ? withMark(isEngiMatch ? String(match.flagsA || 0) : (ipponsA.join("") || null), aMark) : null; + const bScore = isDone ? withMark(isEngiMatch ? String(match.flagsB || 0) : (ipponsB.join("") || null), bMark) : null; const aTBD = match.sideA && typeof match.sideA.id === "string" && match.sideA.id.startsWith("tbd-"); const bTBD = match.sideB && typeof match.sideB.id === "string" && match.sideB.id.startsWith("tbd-"); @@ -281,7 +314,9 @@ const MatchCard = React.memo(({ match, variant, showDojo, onClick, highlighted, const _isWatched = (typeof window !== "undefined" && window.isPlayerWatched) || (() => false); const playerHighlight = !!(highlightPlayers && (_isWatched(match.sideA, highlightPlayers) || _isWatched(match.sideB, highlightPlayers))); - const enchoBadge = enchoLabel(match.encho); + // Meta-strip middle mark: X | (E) | (DH), mutually exclusive (see + // middleMark). The X chip keeps its dedicated span below for styling. + const metaMid = matchMiddleMark(match); return (