diff --git a/multiplayer-todo.md b/multiplayer-todo.md index e324029a..b75feb50 100644 --- a/multiplayer-todo.md +++ b/multiplayer-todo.md @@ -1202,7 +1202,7 @@ the local/CI/community transport, not a silent production fallback. | 8.22 `[D:8.21]` | **IN PROGRESS.** Pure Go ranked profile exposes the first ten games as provisional and keeps casual ratings outside the API | `RankedIsProvisional` covers the 0–9/10 boundary; authoritative tier derivation and UI remain | | 8.23 `[D:8.21]` | **IN PROGRESS.** Pure Go ranked-only season rollover compresses 25% toward 1500, clamps RD to 200–350, preserves volatility/history and is idempotent by season ID | `ApplySeasonRollover` covers compression, floor/cap and duplicate replay; PostgreSQL transaction locking and 12-week scheduler remain | | 8.24 `[D:8.9,8.20,8.21]` | **IN PROGRESS.** Pure Go ranked connection policy binds match/server/player/team/slot/protocol, supports 60 s reclaim with server-owned generations, fences old connections, and applies the rolling 7-day 5 m/15 m/1 h/24 h abandon ladder | `server/domain/reconnect.go` covers repeated backend-independent reclaim, binding rejection, old-generation fencing, grace boundary and deterministic cooldown audit ordering; signed authorisations, persistent lease fencing, join transport and full match/result integration remain | -| 8.25 `[D:8.10,8.24]` | **IN PROGRESS.** Pure Go result policy binds match/server/workload identity, hashes canonical payloads, makes identical retries idempotent, leaves conflicts inert, separates integrity eligibility, validates annotation signatures/digests, and exposes 5 m alert/30 m review delivery thresholds; Go store SQL now defines conflict-safe receipt insert, deterministic match/rating locks and atomic completion/outbox boundaries | `server/domain/result.go` plus `server/store/result_sql.go` and adversarial fixtures cover binding, duplicate/conflict, annotation forgery, commit, lock ordering and delivery-health invariants; production credential verification, Agones annotation persistence/reconciliation, live PostgreSQL execution and integrity-classification adapters remain | +| 8.25 `[D:8.10,8.24]` | **IN PROGRESS.** Pure Go result policy binds match/server/workload identity, hashes canonical payloads, makes identical retries idempotent, leaves conflicts inert, separates integrity eligibility, classifies roster/simulation/result/fairness evidence, validates annotation signatures/digests, and exposes 5 m alert/30 m review delivery thresholds; Go store SQL defines conflict-safe receipt insert, deterministic match/rating locks and atomic completion/outbox boundaries | `server/domain/result.go` plus `server/store/result_sql.go` and adversarial fixtures cover binding, duplicate/conflict, annotation forgery, delivery-outage-versus-integrity classification, commit and lock ordering; production credential verification, Agones annotation persistence/reconciliation, live PostgreSQL execution and integrity evidence adapters remain | #### 8D — Agones, allocation and regional scaling diff --git a/server/domain/result.go b/server/domain/result.go index b08970d8..9dd1b27e 100644 --- a/server/domain/result.go +++ b/server/domain/result.go @@ -20,6 +20,24 @@ const ( IntegrityReview IntegrityState = "REVIEW" ) +type IntegrityEvidence struct { + RosterAuthoritative bool + SimulationAuthoritative bool + ResultAuthoritative bool + RegionalPlayFair bool + DeliveryAvailable bool +} + +// ClassifyIntegrity deliberately ignores DeliveryAvailable when deciding +// rating eligibility: a healthy match remains rated while the control plane +// is temporarily unable to acknowledge its result. +func ClassifyIntegrity(evidence IntegrityEvidence) IntegrityState { + if !evidence.RosterAuthoritative || !evidence.SimulationAuthoritative || !evidence.ResultAuthoritative || !evidence.RegionalPlayFair { + return IntegritySuppressed + } + return IntegrityCertified +} + var ( ErrResultBinding = fmt.Errorf("result workload binding rejected") ErrResultConflict = fmt.Errorf("conflicting result") diff --git a/server/domain/result_test.go b/server/domain/result_test.go index d7be2f0c..69d0cf81 100644 --- a/server/domain/result_test.go +++ b/server/domain/result_test.go @@ -94,3 +94,20 @@ func TestResultDeliveryHealthSeparatesOutageFromIntegrity(t *testing.T) { t.Fatalf("committed delivery status = %+v err=%v", committed, err) } } + +func TestIntegrityClassifierDoesNotSuppressHealthyResultForDeliveryOutage(t *testing.T) { + healthy := IntegrityEvidence{RosterAuthoritative: true, SimulationAuthoritative: true, ResultAuthoritative: true, RegionalPlayFair: true, DeliveryAvailable: false} + if got := ClassifyIntegrity(healthy); got != IntegrityCertified { + t.Fatalf("delivery outage changed integrity: %s", got) + } + for _, evidence := range []IntegrityEvidence{ + {SimulationAuthoritative: true, ResultAuthoritative: true, RegionalPlayFair: true}, + {RosterAuthoritative: true, ResultAuthoritative: true, RegionalPlayFair: true}, + {RosterAuthoritative: true, SimulationAuthoritative: true, RegionalPlayFair: true}, + {RosterAuthoritative: true, SimulationAuthoritative: true, ResultAuthoritative: true}, + } { + if got := ClassifyIntegrity(evidence); got != IntegritySuppressed { + t.Fatalf("incomplete integrity was certified: %+v -> %s", evidence, got) + } + } +}