docs: record the backfill roster-delivery decision and remaining work

A backfilled player's join authorisation is issued after their server
started, but the supervisor fetches the roster once before launching the
game child and the game process has no reload path, so backfill cannot
work end to end regardless of how good the selection rule is.

Decided: the control plane marks the roster changed, the supervisor
re-fetches and rewrites the roster file, then signals the game process to
reload. Chosen because it reuses the authenticated channel and roster
endpoint that already exist -- no inbound path into the game pod, no new
trust boundary -- and keeps the roster an allowlist the server was told
to expect rather than admitting anyone holding a valid signature.
Signature verification is untouched and already binds match, server, slot
and generation.

Recorded in docs/MATCHMAKING.md, which the repo treats as the design
source of truth, so the decision is not re-litigated from a task row.
Remaining implementation is tracked in #32 and summarised in §7 8.19.
This commit is contained in:
Josh Creek
2026-09-05 18:09:10 +01:00
parent 1becfb4f3f
commit 654f20e28f
2 changed files with 12 additions and 1 deletions
+11
View File
@@ -259,6 +259,17 @@ only after the same transition commits.
- An original casual participant gets 30 seconds to reconnect; leaving after
that applies a 60-second queue cooldown. The match's ordinary hidden-rating
result still applies, with no extra rating penalty.
- **Late roster delivery.** A backfilled player's join authorisation is issued
after their server started, but the supervisor fetches the roster once before
launching the game child and the game process has no reload path. The agreed
model is: the control plane marks the roster changed, the supervisor -- which
already holds an authenticated channel to the control plane and already owns
the roster file -- re-fetches and rewrites it, then signals the game process
to reload. This deliberately adds no inbound path into the game pod and no new
trust boundary; the roster stays an allowlist the server is told to expect,
rather than admitting anyone holding a valid signature. Signature
verification is unchanged and already covers match, server, slot and
generation.
- An accepted casual initial-connect no-show gets the same 60-second cooldown.
The match proceeds with a bot only if at least one human connected on each
team; otherwise it cancels and restores every innocent ticket with original
+1 -1
View File
@@ -249,7 +249,7 @@ are done; everything below is what's left on the tasks still open.
| 8.16 `[D:8.14,8.15]` | Candidate/team formation, matcher worker | The matcher is a real long-running poll loop and now has casual and ranked Deployments in `deploy/k8s/base`; what remains is live soak against a cluster rather than the integration itself ([#17](https://github.com/jcreek/CosmicClash/issues/17)) |
| 8.17 `[D:8.14,8.16]` | Proposal policy (response window, cooldowns, offender/innocent split) | Live PostgreSQL execution and allocation integration remain |
| 8.18 `[D:8.5,8.14,8.17]` | Store layer (serializable retries, claim SQL, atomic promotion) | Allocation runtime integration remains |
| 8.19 `[D:8.18]` | Casual lineup (26 humans, bot backfill) | Casual lineup formation is built and wired (`domain.BuildCasualLineup`, called from `domain/formation.go` and `domain/noshow.go`), and all four penalty kinds are durably written (`PROPOSAL_DECLINED`, `PROPOSAL_TIMEOUT`, `INITIAL_CONNECT_NO_SHOW`, `MATCH_ABANDONED`). What genuinely remains is the opt-in 10 s **backfill proposal** path: `domain.CanCasualBackfill`/`CasualBackfillPenalty` exist as policy, but no matcher code path ever creates a proposal to fill a slot in an in-progress match. Live integration also remains |
| 8.19 `[D:8.18]` | Casual lineup (26 humans, bot backfill) | Candidate selection landed (`domain.SelectCasualBackfillCandidate`: oldest ordinary casual ticket meeting build/region/tolerance, ties by ticket ID, deterministic across replicas). Casual lineup formation was already built and wired, and all four penalty kinds are written durably. What remains is the backfill proposal itself, the matcher pass that finds vacated kickoff slots, the client offer UI, and **late roster delivery** — a backfilled player's authorisation is issued after their server started, and the supervisor fetches the roster once before launching the game child with no reload path. That delivery design is now decided (supervisor re-fetches and signals a reload; see `docs/MATCHMAKING.md` § Casual) and the remaining work is tracked in [#32](https://github.com/jcreek/CosmicClash/issues/32). End-to-end verification needs a live cluster ([#17](https://github.com/jcreek/CosmicClash/issues/17)) |
| 8.20 `[D:8.18]` | Ranked admission (six unique verified humans) | Done. Allocation wiring was already complete end to end (allocator sets the `cosmic-clash.io/arena-path` annotation → `supervisor.withAllocatedCompatibility` maps it to `--arena-path``server_boot.gd``ServerMatchLoop.allocated_arena_path`), with coverage at each hop. `ArenaRegistry` integration is now a cross-language guard rather than a shared list: `server/domain/ranked.go` must keep its own ranked-eligible subset (the choice is server-authoritative and made before any Godot process exists), so `arena_registry_sync_test.go` parses `arena_registry.gd` and fails if the two disagree in either direction, if rotation order diverges, or if a ranked path has no scene behind it. Verified against four drift scenarios including promoting an elevated variant, which the registry's own comment anticipates. Live ranked admission against a real cluster remains ([#17](https://github.com/jcreek/CosmicClash/issues/17)) |
| 8.21 `[D:8.5,8.20]` | Rating core (Glicko-2, weights, transactional updates) | Live maintenance/DB execution remains |
| 8.22 `[D:8.21]` | Ranked profile (provisional games, tiers) | Persisted tier policy done: bands live in `tier_bands`, seeded with the exact compiled launch policy so storage changed without behaviour changing, loaded at startup with a malformed policy failing startup rather than silently mis-tiering, and an empty table falling back to the compiled default so an operator can truncate back to known-good. Retuning is now a rolling restart rather than a rebuilt image. `PROVISIONAL` is rejected as a durable band, being derived from game count rather than rating. Client UI was already built (`RankedProfileState.display_text()` renders tier, provisional status, ranked games and the season countdown). Reconnect transport is tracked by 8.42 and depends on live auth/backend events |