@voltro/plugin-leaderboards 0.0.0-stage → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +24492 -0
  2. package/LICENSE +61 -0
  3. package/README.md +1087 -2
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +5603 -0
  6. package/dist/administration.js +4 -0
  7. package/dist/bracketAdmission.js +293 -0
  8. package/dist/bracketAdmissionPolicy.js +21 -0
  9. package/dist/bracketInstallation.js +132 -0
  10. package/dist/bracketMatchPorts.js +4 -0
  11. package/dist/bracketMatches.js +545 -0
  12. package/dist/brackets.d.ts +149 -0
  13. package/dist/brackets.js +378 -0
  14. package/dist/bucketStore.js +92 -0
  15. package/dist/buckets.js +8 -0
  16. package/dist/errors.d.ts +13 -0
  17. package/dist/errors.js +16 -0
  18. package/dist/glicko2.js +71 -0
  19. package/dist/index.d.ts +1208 -0
  20. package/dist/index.js +15 -0
  21. package/dist/inspect.js +83 -0
  22. package/dist/json.js +38 -0
  23. package/dist/leaderboard.d.ts +49 -0
  24. package/dist/leaderboard.js +56 -0
  25. package/dist/leagueSchedule.js +39 -0
  26. package/dist/leagues.js +30 -0
  27. package/dist/openskill.js +73 -0
  28. package/dist/plugin.js +337 -0
  29. package/dist/portLifetime.js +66 -0
  30. package/dist/ports.d.ts +294 -0
  31. package/dist/ports.js +6 -0
  32. package/dist/rankQuery.js +23 -0
  33. package/dist/ranking.d.ts +42 -0
  34. package/dist/ranking.js +70 -0
  35. package/dist/rating.d.ts +103 -0
  36. package/dist/rating.js +49 -0
  37. package/dist/ratingPools.js +133 -0
  38. package/dist/ratingStore.js +333 -0
  39. package/dist/ratingStrategies.js +107 -0
  40. package/dist/records.d.ts +116 -0
  41. package/dist/records.js +87 -0
  42. package/dist/rpc.d.ts +321 -0
  43. package/dist/rpc.js +305 -0
  44. package/dist/score.d.ts +13 -0
  45. package/dist/score.js +26 -0
  46. package/dist/server.d.ts +1477 -0
  47. package/dist/server.js +17 -0
  48. package/dist/service.js +948 -0
  49. package/dist/subjectFences.js +43 -0
  50. package/dist/tables.entity.js +187 -0
  51. package/dist/testing.d.ts +872 -0
  52. package/dist/testing.js +367 -0
  53. package/dist/tournament-app/tournament.advance.workflow.server.tsx +3 -0
  54. package/dist/tournament-app/tournament.advance.workflow.tsx +12 -0
  55. package/dist/tournament-app/tournament.stage.workflow.server.tsx +3 -0
  56. package/dist/tournament-app/tournament.stage.workflow.tsx +10 -0
  57. package/dist/tournamentInstallation.js +167 -0
  58. package/dist/tournamentPrizes.js +33 -0
  59. package/dist/tournamentStage.js +150 -0
  60. package/dist/tournamentStagePorts.js +4 -0
  61. package/dist/tournamentWorkflow.js +100 -0
  62. package/dist/tournamentWorkflowSchemas.js +53 -0
  63. package/dist/tournaments.d.ts +32 -0
  64. package/dist/tournaments.js +20 -0
  65. package/dist/web.d.ts +175 -0
  66. package/dist/web.js +16 -0
  67. package/package.json +113 -4
package/README.md CHANGED
@@ -1,3 +1,1088 @@
1
- # Temporary Holding Version
1
+ # Leaderboard foundations
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This source is **built**. It provides pure board/score/rank/rating functions
4
+ plus a borrowed SQL adapter. The actual `leaderboardsPlugin(options)` installs
5
+ this adapter, tables, privacy hooks, reader RPCs and durable manual period
6
+ rolls. Automatic reset/season workflows remain unfinished.
7
+
8
+ ## Take
9
+
10
+ `defineLeaderboard` describes a board; the root and `./leaderboard` export it.
11
+ `./tournaments` exports captured `defineTournament`; `./score` exports `scoreValue`, `scoreString`, `applyScore`; `./ranking` exports
12
+ `rankLeaderboardRecords`, `leaderboardAround`, `defaultLeaderboardTiebreak`,
13
+ `defaultPeriodSummary`, `hashBucket`. `./rating` exports `elo`, `glicko2`, `openskill`. The root now includes server-only installation;
14
+ descriptors import `./rpc`/`./errors`, never the root. Pure leaf subpaths remain
15
+ browser-safe; `./server` is the borrowed SQL/administration entry. Player tournament joining is an explicit intent; score writing remains machine-only.
16
+
17
+ ```ts
18
+ import { Schema } from 'effect'
19
+ import { defineLeaderboard } from '@voltro/plugin-leaderboards/leaderboard'
20
+ import { applyScore } from '@voltro/plugin-leaderboards/score'
21
+
22
+ const weekly = defineLeaderboard({
23
+ name: 'weekly-score', sort: 'desc', operator: 'best',
24
+ metadata: Schema.Struct({ character: Schema.String }),
25
+ reset: { cron: '0 0 * * 1', timezone: 'UTC', keepHistory: 12 },
26
+ })
27
+ const next = applyScore('9007199254740993', '2', 'increment')
28
+ // next === '9007199254740995'; no JavaScript Number rounding.
29
+ ```
30
+
31
+ The descriptor and arithmetic above do not write SQL or activate a schedule.
32
+ A player score-submit route does not exist. The server `leaderboards.submit`
33
+ Action has a scope guard and the adapter independently refuses human identities;
34
+ only its trusted system calls pass this installed route. Verified cross-app
35
+ service names can call the standalone adapter after explicit allowlisting; plugin
36
+ route service-audience forwarding is still unfinished, so no external service-RPC
37
+ claim follows. Browser RPC imports expose reads only. Service-only submit is the captured
38
+ policy. The server adapter enforces trusted system tasks or explicitly configured
39
+ verified service names; no service is allowed by default and humans cannot submit.
40
+
41
+ ## Configure
42
+
43
+ | Option | Default | Source budget or meaning |
44
+ |---|---|---|
45
+ | `sort` | `desc` | `asc` makes lower scores better |
46
+ | `operator` | `best` | `best`, `set`, `increment`, `decrement` |
47
+ | `metadata` | required | Actual Effect Schema; trusted JSON metadata must decode without coercion |
48
+ | `submit.via` | `service-only` | Client score submission refuses in this descriptor |
49
+ | `reset` | absent | Captured cron/timezone/history, not an active reset |
50
+ | `maxRankRecords` | 10,000 | Complete supplied pure rank snapshot; at most 100,000 |
51
+ | `maxPageSize` | 100 | Descriptor budget; at most 1,000 and no larger than rank budget |
52
+
53
+ Cron parsing reuses Effect Cron, with an explicit valid timezone and bounded
54
+ positive history count. Callback/data selection and numeric descriptor values
55
+ are captured. Pure helpers own no score registry, store, timer or hidden authority.
56
+ The adapter retains captured descriptor configuration and persists score authority in SQL. `best` retains the better value for sort direction; `set`
57
+ replaces; increment/decrement use zero for a missing score. Overflow refuses.
58
+ Scores/subscores are canonical decimal strings in the signed SQL BIGINT range
59
+ [-9223372036854775808, 9223372036854775807]. Unsafe Numbers, fractional/exponent/
60
+ leading-zero strings, whitespace and negative zero refuse. SQL idempotency is
61
+ not inferred from pure arithmetic. The adapter additionally preserves winning
62
+ Best subscore/metadata/time when a later score loses or ties without improving
63
+ subscore; the accepted submit counter still increments.
64
+
65
+ ## Hook
66
+
67
+ `./server` exports `dataStoreLeaderboards(store, options)` and twelve table handles.
68
+ The adapter borrows one DataStore, captures board/provider/clock selections and
69
+ requires verified trusted machine authority for score writes. A subject writer
70
+ fence precedes the board fence; submission receipt, record update and initial
71
+ period creation share one transaction. Exact retries return the original receipt
72
+ before validation and never reapply an increment; changed input refuses. The
73
+ `validate(input, previous, tx)` slot runs inside that transaction; its default
74
+ `acceptLeaderboardSubmission` accepts already validated signed-64-bit scores,
75
+ without inventing game-specific plausibility. Audit retains room/match references.
76
+
77
+ `top`, `me` and `around` read a complete bounded current-period SQL snapshot for
78
+ the verified human tenant and use exact pure sorting. The installed
79
+ top/me/around Query routes declare the record table as reactive source and use
80
+ these same methods. More rows than `maxRankRecords` refuse, even for Top-1. The
81
+ initial period is created on a first submission; manual rolls close it and
82
+ activate the next period; changing an existing board’s operator/sort refuses
83
+ until a proper configuration/period migration is added. The trusted `rollPeriod`
84
+ and human `history` methods now persist period close/summary/activation.
85
+ Automatic reset workflows, `leaderboard.periodClosed`, automatic match-finished
86
+ event wiring, cron registration and listener privacy remain unfinished.
87
+
88
+ `./web` exports `useLeaderboardTop(board, limit, apiName)`,
89
+ `useLeaderboardMe(board, apiName)`, `useLeaderboardAround(board, radius, apiName)`
90
+ and `useLeaderboardHistory(board, limit, apiName)`
91
+ through existing subscriptions and installed plugin aliases, without polling.
92
+ `./server` exposes the actual `LeaderboardAdministration` service Tag for
93
+ server-side handlers, not an invented game-stack leaf Port. The owned default
94
+ service clears on deactivation; one plugin instance cannot cross app stores. A pure rank snapshot causes no reward or season side effect.
95
+ SQL is the adapter score truth and the future HotStore rebuild source.
96
+
97
+ ## Durable manual periods
98
+
99
+ The server adapter and installed internal `LeaderboardAdministration` expose
100
+ `rollPeriod(subject, { tenantId, board, requestId, expectedPeriodId, closedAt })`.
101
+ It requires the same trusted machine authority as submit; no player roll route
102
+ exists. SQL serializes the board, checks the expected active period, creates a
103
+ bounded summary, closes it and activates a deterministic successor in one
104
+ transaction. Stable request identity and full input digest are retained in the
105
+ successor. Recreated adapters and concurrent retries return that stored result;
106
+ changed intention or stale expected period refuses. A close boundary before any
107
+ included score update refuses; this does not implement late cron admission.
108
+
109
+ `periodSummary(records)` is a captured pure slot. Its default is
110
+ `defaultPeriodSummary`, selecting the first ten ranked records and participant
111
+ count. Overrides may select at most 1,000 exact original rank records, without
112
+ extra metadata or fabricated owners/scores. The byte budget still applies.
113
+ `maxPeriodRows` defaults to 10,000 (at most 100,000), `maxSummaryBytes` to 16,384
114
+ (at most 65,536). Oversized snapshots, history, summaries and privacy work refuse
115
+ instead of truncating work. A declared `keepHistory` marks older closed periods
116
+ archived; records and retry receipts are retained, not garbage-collected.
117
+
118
+ `leaderboards.history` is a human-tenant reactive reader sourced from the period
119
+ table; the web history hook uses the normal subscription. Current-period score
120
+ views start empty after a roll. Historical summaries are read from SQL. Export
121
+ adds own `periodSummaries`; erase locks persisted tenant boards in stable order,
122
+ removes the owner from summaries and deletes own records/receipts. Retrying an
123
+ old roll returns the currently redacted summary and cannot revive erased data.
124
+ Participant counts remain aggregate history. No schedule or reward is activated
125
+ by this method.
126
+
127
+ The installed factory also exposes `plugin.leaderboards.service()` as a trusted
128
+ server facade of the same internal service object, useful in ordinary workflow
129
+ steps. It does not construct another adapter or grant browser score authority.
130
+ Calls still require the real machine/human policy for their method, and default
131
+ methods fail unavailable before binding or after deactivation.
132
+
133
+ For a reset board, active SQL periods now persist the next Effect Cron/timezone
134
+ boundary. Fresh submissions at or after that boundary refuse before validation;
135
+ exact old receipts still return. The next rolled period retains its own next
136
+ boundary. Manual roll can close earlier but cannot skip past a persisted
137
+ deadline. Reset configuration belongs to immutable board configuration, and a
138
+ tournament cannot span beyond its board's next reset boundary. This does not
139
+ install a cron worker; it prevents late admission from silently mixing seasons
140
+ while a worker is unavailable.
141
+
142
+ ## Time-window tournament admission
143
+
144
+ `defineTournament` on `./tournaments` captures the supplied window and admission
145
+ budgets. These fixed dates are a declaration example, not a running tournament
146
+ or a measured capacity claim. No prize or price is invented.
147
+
148
+ ```ts
149
+ import { Schema } from 'effect'
150
+ import { defineLeaderboard } from '@voltro/plugin-leaderboards/leaderboard'
151
+ import { defineTournament } from '@voltro/plugin-leaderboards/tournaments'
152
+ import { leaderboardsPlugin } from '@voltro/plugin-leaderboards'
153
+
154
+ const score = defineLeaderboard({ name: 'score', operator: 'best', metadata: Schema.Struct({}) })
155
+ const tournament = defineTournament({
156
+ name: 'weekend', board: 'score', startsAt: Date.UTC(2026, 9, 10), endsAt: Date.UTC(2026, 9, 12),
157
+ maxEntries: 100, attemptsPerEntry: 3, joinRequired: true,
158
+ })
159
+ const plugin = leaderboardsPlugin({ boards: [score], tournaments: [tournament] })
160
+ ```
161
+
162
+ At most 256 declared tournaments are accepted, each with a distinct name and
163
+ board present in the board list. Windows use safe integral UTC milliseconds;
164
+ end must follow start. Entry budgets are 1–1,000,000, attempts 1–2,147,483,647,
165
+ with one attempt and explicit join required by default. These remain budgets.
166
+ The captured tournament declaration joins the persisted board configuration. Concurrent changed declarations refuse on reads and writes; rotation needs an explicit migration. Dynamic tournament rotation, automatic settlement and prizes are unfinished.
167
+
168
+ Players send only `leaderboards.joinTournament({ tournament })`. Owner and tenant
169
+ come from the verified human request. The SQL transaction acquires subject,
170
+ board and tournament fences, binds an empty period and serializes entry capacity.
171
+ Repeated joins retain the original joined time and remaining attempts, including
172
+ an already joined ended-window retry; erased identities refuse. A scored period
173
+ cannot be silently adopted when a tournament is first materialized.
174
+
175
+ A tournament board requires the trusted score input's matching `tournamentId`.
176
+ Only machine score submissions can consume attempts. With `joinRequired:false`,
177
+ a trusted score atomically admits its owner; otherwise an entry must exist.
178
+ Admission, attempt decrement, score and submission receipt share one transaction;
179
+ a failed policy or overflow rolls them back. Exact submission retries return the
180
+ original receipt before deadline/attempt checks and never spend another attempt.
181
+ New work requires `startsAt <= now < endsAt` and the originally bound active
182
+ period. A manual period roll ends new tournament attempts. Joining another
183
+ owner, supplying attempts or submitting scores from a player is not an API.
184
+
185
+ `closeTournament(subject, { tenantId, tournament, requestId })` is an actual
186
+ trusted server method on the adapter/internal service. It refuses early closes
187
+ and humans, prepares an empty declared tournament if needed, then calls the same
188
+ SQL-owned period roll at the declared end boundary. Preparation and roll are two
189
+ transactions: uncertain or failed roll work remains retriable under the same
190
+ identity and does not return a successful close. The namespace/digest and stored
191
+ bound period make concurrent/recreated retries use the original transition.
192
+ Changed close identities after closure refuse; empty tournaments produce zero
193
+ participants, without entries or rewards. Later erase redacts the close summary
194
+ through the normal period privacy path. This is a manual operation; cron,
195
+ event/reward outbox and automatic settlement remain unfinished.
196
+
197
+ `leaderboards.tournamentStatus` returns only the caller's entry plus a dated
198
+ window observation; it reacts to entry/tournament/board writes. Clock-deadline
199
+ invalidation is not installed, so a retained subscription value is not proof the
200
+ window is still open. Every new write checks the actual clock independently. Web
201
+ hooks provide explicit joining and the normal status subscription. SQL table
202
+ roles additionally include `_voltro_lb_tournaments` and
203
+ `_voltro_lb_tournament_entries`; all twelve roles are declared by plugin
204
+ privacy. Export adds own `tournamentEntries`; erase takes tournament fences
205
+ after board fences, removes entries and updates the admitted count in the same
206
+ transaction. Recreated adapters retain the owner tombstone and cannot rejoin
207
+ that identity.
208
+
209
+ ## Override
210
+
211
+ `rankLeaderboardRecords(records, { sort, tiebreak, maxRecords })` receives a
212
+ complete single-tenant/board/period snapshot, with unique record and owner IDs.
213
+ It detaches/freezes only projected values and returns consecutive ordinal ranks.
214
+ Primary score follows sort; default tie order is higher subscore, earlier
215
+ `updatedAt`, binary owner/record ID. This is not SQL RANK/dense-rank semantics.
216
+ A custom `tiebreak` is called on equal primary scores; non-finite results refuse.
217
+ `leaderboardAround` slices around an owner; missing owners return an empty view.
218
+ `defaultPeriodSummary` returns participant count and bounded top records.
219
+ `hashBucket` gives a deterministic UTF-8 FNV-1a partition for an explicit bucket
220
+ count; it does not establish fixed-size cohort admission or durable assignments.
221
+ Indexed dialect rank queries and full periodSummary policies remain open.
222
+ Persisted capacity-bounded buckets are described below.
223
+
224
+ `elo({ k: 32, scale: 400 })` returns a pure `rate({ teams, ranks })` function.
225
+ Each player supplies server-owned `id`, finite rating, optional weight in [0,1].
226
+ Ranks start at 1; equal ranks draw. Team strength is its weighted rating mean.
227
+ For each team pair, logistic expected score uses `10 ** ((other - own)/scale)`;
228
+ team delta is `k/(teamCount-1) * (actual - expected)`. Opposite deltas are
229
+ summed and each team's delta is distributed in proportion to participation
230
+ weight. Zero-play members retain ratings; a whole zero-play team refuses. This
231
+ explicit multi-team extension conserves total pairwise delta up to
232
+ floating-point error. Options are captured, inputs are not mutated, results are
233
+ detached/frozen. Budgets: 2–64 teams, 1–256 players/team, at most 4,096 unique
234
+ players; ratings bounded to magnitude 1e9, positive `k`/`scale` at most 1e6.
235
+ These are author budgets, not rating-quality or load evidence. Pure Glicko-2 and
236
+ Weng–Lin Plackett–Luce functions and persisted placement/decay/rate slots are
237
+ included; automatic match-finished event wiring remains unfinished.
238
+
239
+ ### Glicko-2 and Weng–Lin
240
+
241
+ `glicko2({ tau, epsilon, maxIterations, maxOpponents })` applies one simultaneous
242
+ period to `{ player: { rating, deviation, volatility }, opponents: [{ rating,
243
+ deviation, score }] }`. Scores are0/0.5/1; opponent values are pre-period values.
244
+ No opponents preserves strength/volatility and grows deviation. The bounded
245
+ Illinois solver implements [Glickman's revised algorithm](https://glicko.net/glicko/glicko2.pdf);
246
+ budget exhaustion or non-finite arithmetic refuses the entire computation.
247
+ It is a head-to-head period API, not a fabricated team model.
248
+
249
+ `openskill({ beta, tau, kappa, ordinalZ })` applies the [Weng–Lin Plackett–Luce
250
+ equations](https://jmlr.org/papers/volume12/weng11a/weng11a.pdf) to `{ teams:
251
+ [[{ id, mu, sigma, weight? }]], ranks }`, preserving supplied team order and
252
+ ties. Its fractional participation extension uses weighted Gaussian team
253
+ means/variances; zero participation keeps the exact prior. It does not claim the
254
+ OpenSkill library's contribution normalization or all of that library's models.
255
+ Risk sets use independent log-sum-exp normalization.
256
+
257
+ ## Extend or replace
258
+
259
+ The pure tiebreak/summary/rating exports and fixtures are present. Leaderboards
260
+ is a leaf system, so no invented LeaderboardsPort is introduced. Tables are
261
+ `_voltro_lb_boards`, `_voltro_lb_records`, `_voltro_lb_periods`,
262
+ `_voltro_lb_submissions`, `_voltro_lb_subject_admission`,
263
+ `_voltro_lb_tournaments`, `_voltro_lb_tournament_entries`. The adapter’s
264
+ export/erase methods serialize with the same owner fence. Export projects only
265
+ own records and submission audit; erase deletes those and redacts the hashed
266
+ fence’s plain subject ID. Repeated erase/export and adapter recreation retain
267
+ the tombstone; peers and owner-free configuration/period rows survive.
268
+ Historical summaries are scrubbed under the same board fences; anonymous
269
+ historical participant counts remain. No compliance claim follows.
270
+ `maxMetadataBytes` defaults to 16,384 (at most 65,536), `maxExportRows` and
271
+ `maxSubmissionsPerOwner` to 10,000 (at most 100,000 and submissions no larger
272
+ than export budget). Incomplete work refuses.
273
+
274
+ ## Panel and GM
275
+
276
+ `GET /status` is an actual process-scoped Inspect endpoint with `inspect:read`.
277
+ It returns captured board/operator/sort/service-only policies, snapshot/page/
278
+ metadata/privacy/period/summary budgets and declared tournament windows/admission rules, declared reset/timezone/history, binding and actual
279
+ Default/Custom validation/tiebreak/period-summary selection. It reads no SQL and returns no
280
+ owner, score, metadata schema or allowed service names. Reset scheduling and hot
281
+ index installation are explicitly unfinished; declarations are not live seasons.
282
+
283
+ The transport-independent `LeaderboardsPage` is connected to Local HTTP
284
+ inspection and the tenant-authorized Cloud proxy/page, with plugin-instance and
285
+ replica selection owned by each host. Both languages, loading/empty/error states
286
+ and focus-preserving guarded reload are present in source. Invalid or failed
287
+ reads hide previous configuration. Live rank/period inspection, bans and GM
288
+ reset/delete actions remain unfinished. No health, capacity or support claim
289
+ follows. Templates and complete manifests remain open.
290
+
291
+ ## Tests
292
+
293
+ `./testing` exports actual `leaderboardPureFixture`, `leaderboardRecordFixture`
294
+ and `eloFixture`. Adapter tests additionally author actual SQL-port transactions
295
+ on memory: service-only authority, captured metadata, exact original receipts,
296
+ concurrent increments, Best metadata retention, real callback rollback, overflow
297
+ and privacy. Six native dialect variants
298
+ (PostgreSQL/MySQL/MariaDB/MSSQL/SQLite/Turso) each author
299
+ signed-BIGINT/JSON/date/null roundtrip, all four operators, independent
300
+ writer-pool retries, rollback, exact rank ties and successor privacy. Missing
301
+ required fixture credentials fail instead of becoming acceptance skips.
302
+ Installed-plugin fixtures additionally author reader descriptor/source parity,
303
+ real server score submission/player refusal, actual internal Layer, captured
304
+ configuration, privacy and deactivation. Five manual-period memory fixtures and
305
+ two additional scenarios per native dialect author durable retry/races,
306
+ rollback, history, archival and summary privacy. A strict JSON budget fixture is
307
+ also authored.
308
+
309
+ ## SQL-owned rating matches
310
+
311
+ `ratingPools` captures named algorithm/initial/tuning declarations before
312
+ binding; it adds no player write route. After normal plugin installation, the
313
+ same trusted `leaderboards.leaderboards.service()` provides
314
+ `prepareRatingMatch`, `applyRatingMatch` and `pendingRatingMatches`.
315
+
316
+ ```ts
317
+ import { leaderboardsPlugin } from '@voltro/plugin-leaderboards'
318
+
319
+ export const leaderboards = leaderboardsPlugin({
320
+ boards: [],
321
+ ratingPools: [
322
+ { name: 'ranked', algorithm: 'elo', initial: { rating: 1000 }, options: { k: 32 } },
323
+ { name: 'bayesian', algorithm: 'openskill' },
324
+ { name: 'duel', algorithm: 'glicko2' },
325
+ ],
326
+ })
327
+ ```
328
+
329
+ Preparation takes `{ tenantId, pool, matchId, teams: [[{ ownerId, weight? }]],
330
+ ranks }` from a trusted system/explicitly allowed verified service. Players supply
331
+ no prior/new ratings or result states. SQL stores the detached intended roster,
332
+ result and request digest under the existing subject fences followed by the pool
333
+ writer fence. A changed retry conflicts. Pool configuration is immutable; fresh
334
+ work and profile reads refuse conflicting declarations.
335
+
336
+ Application takes only `{ tenantId, pool, matchId }`. It rechecks the stored
337
+ intention under the same subject/pool admission, reads actual SQL priors and runs
338
+ the selected pure algorithm. All owner states, per-owner previous/current receipts
339
+ and match completion commit together. Completion clears the roster. Concurrent/
340
+ recreated exact applies return the original aggregate receipt without spending
341
+ rating updates again. Zero-participation states retain their prior and game count.
342
+ Glicko-2 application accepts two single-player full-participation teams: each match
343
+ is one head-to-head period; a team Glicko model is refused. Rating floats remain
344
+ JSON floats, not rounded leaderboard BIGINT scores.
345
+
346
+ `leaderboards.myRating` resolves the caller's own tenant/owner; `useMyLeaderboardRating`
347
+ is its alias-aware live hook. An unplayed profile reports the configured initial
348
+ state and zero games. Machine preparation/application/queue access have no browser
349
+ RPC descriptors. This does not complete external service-audience forwarding.
350
+
351
+ Four additional roles are `_voltro_lb_rating_pools`, `_voltro_lb_ratings`,
352
+ `_voltro_lb_rating_matches` and `_voltro_lb_rating_results`: twelve roles total. Privacy
353
+ locks persisted pools after the original subject/board/tournament fences, including
354
+ pools removed from current configuration. Export includes only owned states/
355
+ receipts and the owner's part of prepared evidence. Erase deletes owned states/
356
+ results, clears affected SQL match links and cancels the whole prepared match
357
+ if it contains that owner. Completed aggregate retries write nothing and cannot
358
+ revive the erased owner; peers keep their already-applied strength. The existing
359
+ subject tombstone refuses fresh preparation for that identity. Bounded privacy
360
+ work refuses instead of truncating retained data.
361
+
362
+ The process-only four-layer panel now reports declared pool names/algorithms and
363
+ twelve table roles. It never opens SQL or reports player ratings, match
364
+ outcomes, queue counts or active workflows. Authored memory and all-six-native
365
+ fixtures cover concurrent/reopened receipts, JSON/date state, privacy after
366
+ config removal and a real SQL result-key rollback.
367
+
368
+ The `api-leaderboards` source starter supplies internal `rating.applyMatch` and a
369
+ minute schedule reading a bounded page of SQL-prepared match identities. Workflow
370
+ payload/result contain no owners or ratings. The actual Rooms match.finished
371
+ bridge, durable event settlement, rating-to-board publication, periodic idle decay,
372
+ full journal privacy and cluster crash/replay execution remain open.
373
+
374
+ ## Override rank queries
375
+
376
+ `rankQuery` is a captured server strategy. `defaultLeaderboardRankQuery` is
377
+ exported from the root and `./server`; `./testing` supplies
378
+ `leaderboardRankQueryFixture(query)` using the actual supplied read capability.
379
+ The default ranks the complete admitted SQL snapshot using exact scores and the
380
+ captured tiebreak. An override may compose it or use `context.query` with an
381
+ app-owned dialect implementation. This is the ordinary DataStore query
382
+ capability; it exposes no write methods or native SQL client.
383
+
384
+ ```ts
385
+ import { Effect } from 'effect'
386
+ import { leaderboardsPlugin, defaultLeaderboardRankQuery } from '@voltro/plugin-leaderboards'
387
+ import { defineLeaderboard } from '@voltro/plugin-leaderboards/leaderboard'
388
+ import { Schema } from 'effect'
389
+
390
+ const board = defineLeaderboard({ name: 'weekly', metadata: Schema.Struct({}) })
391
+ const plugin = leaderboardsPlugin({
392
+ boards: [board],
393
+ rankQuery: context => defaultLeaderboardRankQuery(context).pipe(
394
+ Effect.map(ids => [...ids].reverse()),
395
+ ),
396
+ })
397
+ ```
398
+
399
+ This example deliberately reverses the complete order. The context contains
400
+ frozen ranking fields and explicit tenant, board, period, sort and record budget;
401
+ metadata and mutable storage rows stay inside the adapter. Return all original
402
+ record IDs exactly once. The adapter refuses fabricated/duplicate IDs, sparse
403
+ arrays and incomplete prefixes, supplies consecutive ordinal ranks and takes
404
+ scores/metadata from SQL. The same slot drives top, me, around and period summary
405
+ creation. Historical summary reads retain their SQL receipt. Typed refusals
406
+ retain their error code; existing privacy and service-only write admission stay
407
+ in effect. Both dashboard hosts display actual default/custom slot selection.
408
+
409
+ This foundation still loads one complete bounded SQL snapshot before the slot.
410
+ It does not yet implement indexed COUNT/window ranking or the large-board
411
+ capacity targets.
412
+
413
+ ## Override persisted rating rules
414
+
415
+ `ratingStrategies` captures three pure slots: `rate`, `placementPolicy` and
416
+ `decay`. The root and `./server` export their single actual defaults,
417
+ `defaultRatingRate`, `defaultRatingPlacement` and `defaultRatingDecay`.
418
+ `./testing` exports `ratingStrategiesFixture()` for all three. The rate default
419
+ uses the declared Elo, PL or head-to-head Glicko implementation; placement uses
420
+ the captured pool initial state; decay preserves the exact prior.
421
+
422
+ ```ts
423
+ import { leaderboardsPlugin, defaultRatingRate, defaultRatingDecay } from '@voltro/plugin-leaderboards'
424
+
425
+ const plugin = leaderboardsPlugin({
426
+ boards: [],
427
+ ratingPools: [{ name: 'ranked', algorithm: 'elo' }],
428
+ ratingStrategies: {
429
+ version: 'game-rating-v1',
430
+ placementPolicy: () => ({ algorithm: 'elo', rating: 1200 }),
431
+ decay: defaultRatingDecay,
432
+ rate: context => defaultRatingRate(context),
433
+ },
434
+ })
435
+ ```
436
+
437
+ Any custom callback requires an application-owned `version`; the `voltro:`
438
+ prefix is reserved for defaults. The SQL pool persists this version with its
439
+ captured configuration. Fresh reads/preparation/application refuse conflicting
440
+ versions across processes. Changing rule meaning needs a new version and an
441
+ explicit pool-data migration or a new pool; no automatic migration is implied.
442
+ A completed aggregate retry returns its SQL receipt without reevaluating rules,
443
+ even if a recreated caller has different rule configuration.
444
+
445
+ During fresh participated application, a missing SQL state goes through
446
+ placement, and an existing state through decay with actual prior games/time and
447
+ `elapsedMs`. Then rate receives the frozen teams, SQL-derived/placed/decayed
448
+ states, weights, ranks and trusted match/tenant/pool/time. It returns every
449
+ original owner exactly once with the same algorithm and valid bounded numerical
450
+ state. Missing, duplicated or foreign owners, extra fields, sparse arrays,
451
+ non-finite values and algorithm changes refuse the whole application. The adapter
452
+ still owns games, timestamps, SQL results and match completion. It stores actual
453
+ previous SQL state in the audit, rather than an invented client prior. The owner-
454
+ free workflow receipt and existing privacy coverage remain unchanged.
455
+
456
+ Zero-play owners bypass placement/decay; rate must retain their exact prior.
457
+ Existing state, games and update time are unchanged. New zero-play rows start
458
+ with the base initial state and zero games. Before first participated placement,
459
+ `myRating` displays that configured base initial state without evaluating a rule.
460
+ Decay runs only on fresh participated application, never on reads or retries;
461
+ there is no periodic idle decay worker. Both dashboards show each actual slot's
462
+ default/custom selection without running callbacks or exposing owner values.
463
+
464
+ Source fixtures cover the pipeline, actual SQL priors/results, captured rules,
465
+ version conflicts, invalid output and zero play, with two additional scenarios
466
+ per native dialect variant. Room result bridging, rating-to-board settlement,
467
+ timer decay, journal privacy and cluster crash/replay execution remain
468
+ unfinished or unverified.
469
+
470
+ ## Persisted capacity-bounded buckets
471
+
472
+ `buckets: { size, assign, skillWidth? }` now admits real SQL membership in
473
+ `_voltro_lb_bucket_assignments`; the adapter and installed privacy declare twelve
474
+ roles. Size is 1–10,000 and no larger than `maxRankRecords`. `assign` is `hash`,
475
+ `skill` or `cohort`. Skill defaults to width 100 with an explicit finite width
476
+ between 1e-6 and 1e9. These are configuration bounds, not capacity evidence.
477
+
478
+ ```ts
479
+ import { Schema } from 'effect'
480
+ import { defineLeaderboard } from '@voltro/plugin-leaderboards/leaderboard'
481
+ import { leaderboardsPlugin, defaultLeaderboardBucketAssign } from '@voltro/plugin-leaderboards'
482
+
483
+ const board = defineLeaderboard({
484
+ name: 'weekly', metadata: Schema.Struct({}),
485
+ buckets: { size: 50, assign: 'skill', skillWidth: 100 },
486
+ })
487
+ const plugin = leaderboardsPlugin({ boards: [board], bucketAssign: defaultLeaderboardBucketAssign })
488
+ ```
489
+
490
+ Trusted score submission supplies `bucketSkill` for skill grouping or
491
+ `bucketCohort` for cohort grouping. Hash accepts neither. Skills are finite with
492
+ magnitude at most 1e9; cohort identifiers are bounded nonblank strings. Skill
493
+ bands use `floor(skill / skillWidth)`, including negative skills. Cohorts are
494
+ explicit trusted grouping, not a client-supplied rank/score. No human score route
495
+ is added. First admitted membership is fixed for that owner/board/period; later
496
+ trusted skill/cohort changes do not move that membership. Submission digests
497
+ still distinguish changed intent.
498
+
499
+ The original subject then board writer fence serializes capacity. The adapter
500
+ reads complete bounded period membership, checks group/candidate identity and
501
+ current occupancy, supplies only non-full candidates and creates a new ordinal
502
+ only when the group has no free candidate. `defaultLeaderboardBucketAssign`
503
+ hash-selects an eligible candidate. It is exported from root/`./server`, with
504
+ `leaderboardBucketAssignFixture()` in `./testing`. A custom pure `bucketAssign`
505
+ can choose only one actual admitted candidate; invented/full choices refuse.
506
+ Its required application-owned `bucketStrategyVersion` joins immutable board
507
+ configuration and must not use the reserved `voltro:` prefix. Changing rule
508
+ meaning requires explicit data migration/new board. The callback receives frozen
509
+ scope/group/candidates, no store or mutable membership data.
510
+
511
+ Records now carry nullable `bucket`. `me`/`around` rank within the owner's
512
+ current bucket on configured boards. `top` defaults to the global order; its
513
+ optional fourth `scope: 'bucket'` argument returns the requesting owner's bucket.
514
+ The browser Top descriptor accepts only `scope: 'global' | 'bucket'`, never a
515
+ foreign owner/tenant/bucket identifier. The web hook is `useLeaderboardTop(board, limit, apiName, 'bucket')`. The rank strategy receives the actual `bucketId` or null for
516
+ global ranking. Reader sources include records, board-period changes and
517
+ membership, so source invalidation covers new periods and erasure. Wire delivery
518
+ still requires execution evidence.
519
+
520
+ A period roll uses the global summary and starts fresh membership; old
521
+ membership remains in SQL. Export adds only own `bucketAssignments` with
522
+ group/time; erase locks persisted boards and deletes own membership even after
523
+ declaration removal. Remaining members retain their IDs and capacity can be
524
+ reused; erased identities stay fenced. Membership, score and submission receipt
525
+ commit together. Later arithmetic failure rolls back an already inserted
526
+ assignment. Reset rewards, retention and cohort administration remain
527
+ unfinished; no hot bucket cache exists. The current rank/query admission still
528
+ scans a bounded complete snapshot, with `maxRankRecords` as the period
529
+ membership budget. No indexed/large-board/throughput claim follows.
530
+
531
+ ## Pure league, Swiss and elimination plans
532
+
533
+ `@voltro/plugin-leaderboards/brackets` now contains `roundRobin`,
534
+ `swissPairings`, `seedSingleElimination` and `advanceSingleElimination`. They
535
+ create detached frozen plans/observations, with no SQL, timer, Room reservation,
536
+ workflow, client result authority or reward. The existing `defineTournament`
537
+ still declares a time-window board tournament; these independent primitives do
538
+ not silently enable a new installed format.
539
+
540
+ ```ts
541
+ import { roundRobin, swissPairings, seedSingleElimination, advanceSingleElimination } from '@voltro/plugin-leaderboards/brackets'
542
+
543
+ const entrants = [
544
+ { entrantId: 'a', seed: 1 }, { entrantId: 'b', seed: 2 },
545
+ { entrantId: 'c', seed: 3 }, { entrantId: 'd', seed: 4 },
546
+ ]
547
+ const league = roundRobin(entrants, { doubleRound: true })
548
+ const swiss = swissPairings(entrants.map(entrant => ({
549
+ ...entrant, points: 0, byes: 0, opponents: [],
550
+ })))
551
+ const bracket = seedSingleElimination(entrants)
552
+ const progress = advanceSingleElimination(bracket, [
553
+ { matchId: 'w1.1', winnerId: 'a' },
554
+ { matchId: 'w1.2', winnerId: 'b' },
555
+ { matchId: 'w2.1', winnerId: 'a' },
556
+ ])
557
+ ```
558
+
559
+ Entrants carry distinct bounded opaque IDs and a complete seed sequence1..N.
560
+ Input order does not change plans. Inputs reuse strict detached Leaderboards JSON
561
+ admission; accessors, sparse arrays, classes, extra fields and unknown settings
562
+ refuse. Defaults256 entrants,65536 planned matches,100000 search nodes and65536
563
+ input bytes are budgets, never measured tournament capacity. Input/output IDs
564
+ carry no authorization; a server coordinator supplies trusted membership/results.
565
+
566
+ `roundRobin` uses a fixed-first-seed circle schedule. Every unordered pair appears
567
+ once per leg; every odd roster gets one bye per entrant. `doubleRound:true`
568
+ appends a second leg with swapped orientation. This rotation does not claim a
569
+ particular league's home/away balancing or federation rules.
570
+
571
+ Swiss takes trusted `points`, `byes` and symmetric distinct `opponents` history.
572
+ Points are nonnegative safe whole scoring units chosen by the studio (win2/draw1,
573
+ for example). Up to128 entrants are admitted under the configured input budget.
574
+ Players sort by points then seed; bounded backtracking tries close-score and seed
575
+ distance partners, refusing rematches. It finds the first complete assignment in
576
+ that order, not a globally optimal or federation-standard pairing. Gap comparison
577
+ uses BigInt; extreme safe scores do not overflow. Odd rosters select only entrants
578
+ with the minimum prior bye count, then lowest score and weakest seed, trying later
579
+ eligible bye choices only when a complete pairing is impossible for earlier ones.
580
+ No solution returns `conflict`; exhausted search returns `limit_exceeded`, never
581
+ a partial plan or proof of impossibility. Search-node counts are algorithm values.
582
+
583
+ Single Elimination uses conventional mirrored seeds (eight entrants begin1–8,
584
+ 4–5,2–7,3–6) and fills a power-of-two graph with explicit byes. Advancement rebuilds
585
+ the exact canonical graph from its roster before accepting winner reports; forged
586
+ edges, duplicate/unknown reports and unresolved/fabricated winners refuse. Byes
587
+ advance automatically, ready matches accept only one of their actual entrants,
588
+ and the last winner becomes champion. Results may be supplied in any order when
589
+ all required predecessors are present. There is no SQL receipt, authority, event
590
+ or exactly-once prize guarantee in this pure reconstruction.
591
+
592
+ `@voltro/plugin-leaderboards/testing` exports `tournamentPairingFixture` with
593
+ these actual functions and roster/standings. Each rule can be composed in an
594
+ application-owned coordinator. Installed workflow strategy/version/panel
595
+ bindings are still separate work.
596
+
597
+ No new plugin table or configuration-panel installed capability is claimed.
598
+
599
+ ### Pure Double Elimination graph and conditional final reset
600
+
601
+ The same entry now also contains `seedDoubleElimination` and
602
+ `advanceDoubleElimination`. Winners reuse mirrored seeding. The lower bracket
603
+ alternates crossed drops from winner rounds and consolidation rounds. Two
604
+ entrants need no lower match. Bye losers propagate as explicit empty slots
605
+ without a loss; only an actual completed match increments its loser. A player
606
+ with two losses cannot enter another ready match.
607
+
608
+ ```ts
609
+ import { seedDoubleElimination, advanceDoubleElimination } from '@voltro/plugin-leaderboards/brackets'
610
+
611
+ const bracket = seedDoubleElimination([
612
+ { entrantId: 'a', seed: 1 }, { entrantId: 'b', seed: 2 },
613
+ ])
614
+ const resetReady = advanceDoubleElimination(bracket, [
615
+ { matchId: 'w1.1', winnerId: 'a' },
616
+ { matchId: 'g1.1', winnerId: 'b' },
617
+ ])
618
+ const complete = advanceDoubleElimination(bracket, [
619
+ { matchId: 'w1.1', winnerId: 'a' },
620
+ { matchId: 'g1.1', winnerId: 'b' },
621
+ { matchId: 'g2.1', winnerId: 'a' },
622
+ ])
623
+ ```
624
+
625
+ `g1.1` pairs the undefeated winner finalist and the lower finalist. A win by the
626
+ undefeated finalist makes `g2.1` inactive and establishes the champion. A lower
627
+ finalist win activates the reset between the same entrants, now with one loss
628
+ each; its winner becomes champion. Premature/inactive reset reports, fabricated
629
+ winners, duplicate results and modified canonical dependencies refuse. The graph
630
+ is reconstructed from its roster before any result advances it.
631
+
632
+ Returned observations include pending/ready/walkover/empty/inactive/complete,
633
+ actual winner/loser and per-entrant loss counts. These are pure reconstruction
634
+ values, not committed SQL or live Room evidence. The actual fixture exports both
635
+ functions too. Test expectations are not measured tournament capacity or runtime
636
+ proof. Durable final/Room/reward/ dispute/check-in workflows and UI remain open.
637
+
638
+ ## Leagues: promotion and relegation
639
+
640
+ A league is a set of division boards, best first. `defineLeague({ name, divisions,
641
+ promote, relegate })` declares one, and `leaderboardsPlugin({ leagues })` installs
642
+ it. A division must be a declared board without buckets, and a board belongs to at
643
+ most one league.
644
+
645
+ ```ts
646
+ import { Schema } from 'effect'
647
+ import { defineLeaderboard, defineLeague, leaderboardsPlugin } from '@voltro/plugin-leaderboards'
648
+
649
+ const division = (name: string) => defineLeaderboard({ name, operator: 'increment', metadata: Schema.Struct({}) })
650
+ export const ladder = defineLeague({ name: 'ladder', divisions: ['gold', 'silver', 'bronze'], promote: 2, relegate: 2 })
651
+ export const leaderboards = leaderboardsPlugin({ boards: ['gold', 'silver', 'bronze'].map(division), leagues: [ladder] })
652
+ ```
653
+
654
+ Server code works through `leaderboards.service()`:
655
+
656
+ - `assignLeague(system, { tenantId, league, ownerId, division })` places an owner,
657
+ for sign-up, seeding or a staff correction.
658
+ - `myLeague(player, league)` returns the caller's division. An owner without one
659
+ starts in the bottom division.
660
+ - `rollLeague(system, { tenantId, league, requestId, closedAt })` closes every
661
+ division's current period at `closedAt` and then moves members by the closed
662
+ ranks. In each division the best `promote` go up and the worst `relegate` go
663
+ down. A member without a record ranks last, the top division promotes nobody,
664
+ the bottom division relegates nobody, and nobody moves twice. It returns the
665
+ closed and new period of each division and the moves.
666
+ - A retry of the same request returns the recorded moves and changes nothing.
667
+ - Export lists a player's division and moves; erase removes both.
668
+
669
+ A league closes when you call `rollLeague`, or on a schedule.
670
+ `defineLeagueRollSchedule({ league, cron, timezone?, pageSize?, maxPages? })` from
671
+ `@voltro/plugin-leaderboards/server` is an ordinary schedule. Each firing closes the
672
+ league for every tenant with members, at the firing instant, which also names the
673
+ request: a rerun of the same firing moves nobody twice. A tenant that fails does
674
+ not stop the others; the run then fails, and the next run closes what is still
675
+ open. Export it as the default of a file in `schedules/`:
676
+
677
+ ```ts
678
+ import { defineLeagueRollSchedule } from '@voltro/plugin-leaderboards/server'
679
+
680
+ // Mondays at midnight, Berlin time.
681
+ export default defineLeagueRollSchedule({ league: 'ladder', cron: '0 0 * * 1', timezone: 'Europe/Berlin' })
682
+ ```
683
+
684
+ Staff place or correct a player with the GM action `leagues.assign`
685
+ ([GM console](/docs/plugins/gm)).
686
+
687
+ Checked: the pure move rule, a three-division close with retry and privacy, the
688
+ system contract for both new tables, and the schedule (paging, rerun, a failing
689
+ tenant).
690
+
691
+ ## Explicit SQL bracket registration and check-in
692
+
693
+ `defineBracketAdmission` on `./tournaments` captures exact author-owned registration
694
+ and check-in windows plus a two-to-256 entry budget. Unknown/accessor data and
695
+ overlapping/non-increasing windows refuse. This is configuration, not a running
696
+ schedule or tournament capacity claim.
697
+
698
+ ```ts
699
+ import { defineBracketAdmission } from '@voltro/plugin-leaderboards/tournaments'
700
+
701
+ export const bracketWindow = defineBracketAdmission({
702
+ registrationOpensAt: Date.UTC(2026, 9, 12, 10),
703
+ registrationClosesAt: Date.UTC(2026, 9, 12, 11),
704
+ checkInOpensAt: Date.UTC(2026, 9, 12, 11),
705
+ checkInClosesAt: Date.UTC(2026, 9, 12, 12),
706
+ maxEntries: 128,
707
+ })
708
+ ```
709
+
710
+ `./server` exports `dataStoreBracketAdmissions(store, options)`, its types and
711
+ `bracketAdmissionsTable` (`_voltro_tournament_admissions`). This table must be
712
+ included in the application's normal migrated schema inventory; the optional
713
+ installed SQL binding described below contributes it to factory/privacy/inspect. The adapter
714
+ borrows SQL and current clock/guards, with no timer/HotStore/pool/global roster.
715
+ `create` and `seal` accept only trusted system or explicitly authorized service
716
+ Subjects; the supplied current `authorize` policy still runs. `register`,
717
+ `checkIn` and `status` accept verified user/guest self-intentions in their exact
718
+ tenant, never caller-supplied owner state. All callbacks/data are captured before
719
+ lazy execution; no successful authorization or withdrawal default is provided.
720
+
721
+ Scoped SQL row locks serialize capacity/entry and check-in receipts. Creation
722
+ retry must preserve the complete window/configuration; old registration/check-in
723
+ receipts do not allocate another entry or timestamp. Fresh admission checks real
724
+ half-open deadlines. Seal runs only after check-in close and returns persisted
725
+ checked-in/no-show identities to the authorized machine; it does not invent
726
+ rating/seed, apply a no-show penalty, create Rooms, advance a graph or grant prizes.
727
+ Human status contains counts and only its own entry. Recreated adapters read SQL;
728
+ there is no in-process authority or fallback. Accepted SQL promises are awaited
729
+ even when the surrounding Effect is interrupted.
730
+
731
+ `exportSubject` validates a complete bounded tenant inventory and projects only
732
+ the selected subject's association, no peer identity. `eraseSubject` requires
733
+ actual permanent app Subject withdrawal/quiescence, then journals erasing before
734
+ external tournament withdrawal, outside SQL. Exact request plus target digest
735
+ fences a conflicting cleanup. Failure retains participant data and receipt; a
736
+ fresh adapter retries. Only confirmed withdrawal removes the selected identity
737
+ and cancels further admission to that tournament. Other participants remain;
738
+ the real app must cancel/await Rooms, timers, rewards and external copies. Receipt
739
+ digests are pseudonymous data; retention/removal policy is still open. A callback
740
+ or phase is no proof that distributed withdrawal happened. Oversized/corrupt
741
+ inventories refuse, never truncate privacy work.
742
+
743
+ The existing six native SQL fixture lanes now migrate this actual table through
744
+ normal schema apply and invoke the same body with an independent pool; no native
745
+ checks ran. Supplied Subjects/guards/withdrawal are explicit fixture policies.
746
+
747
+ ## Installed bracket admission and replacement port
748
+
749
+ The optional `brackets` binding installs the same registration/check-in kernel.
750
+ SQL binding contributes `_voltro_tournament_admissions` to ordinary schema apply,
751
+ declares it in the shared subject privacy inventory and provides the seven-method
752
+ `BracketAdmissionPort` from `./ports`. The installed server facade is
753
+ `plugin.leaderboards.brackets()`; `plugin.services` provides that same facade to
754
+ the ordinary application Effect context. No separate Runtime or SQL pool exists.
755
+ Standalone `bracketAdmissionLayer(store, options)` on `./server` uses the same
756
+ adapter. Explicit guards, permanent subject withdrawal, external tournament
757
+ withdrawal, export authorization and a clock remain required owner bindings.
758
+
759
+ ```ts
760
+ import { leaderboardsPlugin } from '@voltro/plugin-leaderboards'
761
+ import type { BracketAdmissionOptions } from '@voltro/plugin-leaderboards/server'
762
+
763
+ export const tournamentPlugin = (admission: BracketAdmissionOptions) =>
764
+ leaderboardsPlugin({ boards: [], brackets: { backend: 'sql', options: admission } })
765
+ ```
766
+
767
+ With no binding the optional table, routes and Tag are absent. The server facade
768
+ refuses work. Existing score/rating/time-window behavior remains separate.
769
+ With a binding, browser-safe RPC descriptors and `./web` hooks provide
770
+ `useRegisterBracket`, `useCheckInBracket` and `useBracketAdmissionStatus`.
771
+ Players send only `{ tournamentId }`; the authenticated user/guest identity and
772
+ tenant come from the request. Creation/sealing have no browser mutation route.
773
+ The current supplied policy still admits or refuses each operation. The SQL
774
+ status subscription invalidates from the actual admission table.
775
+
776
+ An entire replacement uses `{ backend: 'custom', version, service, sources }`.
777
+ `service` must implement all seven methods, including privacy. The captured
778
+ non-reserved version names the app implementation, and `sources` explicitly
779
+ names its one-to-32 normal application table invalidation sources. This factory
780
+ does not install a pretend SQL admission table for a replacement. The app owns
781
+ those tables, normal migrations, privacy and source invalidation. Replacing the
782
+ Tag through a normal Layer permits independent consumers; replacing the factory
783
+ binding keeps these built-in routes and hooks on the same implementation.
784
+
785
+ Installed export combines the current score/rating export with own bracket
786
+ associations. Erasure awaits the bracket withdrawal/journal before score data
787
+ erasure; a failed bracket hook leaves the combined hook incomplete. Receipt
788
+ digest retention and real distributed Rooms/timer/reward quiescence still need
789
+ their full app contract. Deactivation refuses new Port work and awaits locally
790
+ admitted whole Effects. Custom services must await their actual accepted external
791
+ work; the counter is no all-node withdrawal proof. Base score-service teardown
792
+ and complete workflow/history privacy remain separate open work.
793
+
794
+ The existing read-only `/status` endpoint and both dashboards now carry the
795
+ required `brackets` configuration or `null`: backend, version, binding and complete
796
+ tenant inventory budget. The conditional thirteen-table SQL inventory is exact;
797
+ replacements show app-owned bounds and no default admission role. Stage workflow
798
+ installation is explicitly false. No owner, roster, result, SQL count, credentials
799
+ or source table names are shown. The shared EN/DE section inherits current reload,
800
+ error, scope and focus behavior. Binding does not prove health or a live tournament.
801
+
802
+ `./testing` exports `fakeBracketAdmissionService`, `bracketAdmissionFakeLayer`
803
+ and the actual `bracketAdmissionPortFixture`; omitted fake methods fail
804
+ unavailable.
805
+
806
+ ## Durable elimination results, disputes and external-work journals
807
+
808
+ `./server` exports `dataStoreTournamentBrackets`, `tournamentBracketLayer` and
809
+ `tournamentBracketsTable` (`_voltro_brackets`). This explicit server coordinator
810
+ uses canonical Single/Double Elimination and a two-to-64 entrant source budget.
811
+ The application ordinarily migrates that table and the shared
812
+ `leaderboardSubjectsTable`; it supplies this table's full privacy hooks and the
813
+ eight-method `TournamentBracketPort` from `./ports`. Automatic factory binding,
814
+ public bracket/schedule UI and workflow registration are still open. No hidden
815
+ scheduler, pool, cache, Tick I/O, Room result or grant is generated.
816
+
817
+ ```ts
818
+ import { tournamentBracketLayer } from '@voltro/plugin-leaderboards/server'
819
+ import type { TournamentBracketOptions } from '@voltro/plugin-leaderboards/server'
820
+ import type { DataStore } from '@voltro/database'
821
+
822
+ export const bracketLayer = (store: DataStore, bindings: TournamentBracketOptions) =>
823
+ tournamentBracketLayer(store, bindings)
824
+ ```
825
+
826
+ Creation captures the complete contiguous seeded roster, format and immutable
827
+ application `ruleVersion`. Only authorized machines create/report/resolve/advance/
828
+ read the internal status. The caller obtains this roster from actual sealed
829
+ check-in/rating policy; this adapter does not invent rating seeding or no-show
830
+ penalties. Actual canonical pairing implementations own graph progression.
831
+
832
+ `advance` prepares at most one external work item under SQL admission. A ready
833
+ match journals its exact private roster and deterministic request identity before
834
+ the supplied `provisionRoom` runs outside the transaction. A confirmed receipt
835
+ contains only roomId/epoch, no credential or fabricated player Subject. Lost
836
+ replies retain pending SQL work; recreated adapters offer the same identity and
837
+ roster again. The actual app Room participant must make that retry idempotent,
838
+ enforce private metadata/roster/current grants and await accepted reservations.
839
+ SQL confirmation refuses a changed receipt or a pause/erase fence. Concurrent
840
+ callers can offer the same key; this journal alone cannot promise exactly-once
841
+ Room creation. No success default exists for that external service.
842
+
843
+ Only a trusted machine can report a result for the current ready match and its
844
+ confirmed Room/epoch. The supplied current `verifyResult` policy verifies real
845
+ Room authority. Fresh work rechecks admission after verification. The original
846
+ report identity/winner are retained separately from a later GM decision; exact
847
+ confirmed retries do not reverify or rewrite a result. Altered/foreign/unready
848
+ reports refuse. Participants send only their own dispute intent with an allowed
849
+ reason code. A dispute pauses further work before a GM resolution. Corrections
850
+ that would invalidate any admitted descendant Room/result refuse; the app must
851
+ withdraw/cancel that work rather than silently rewrite the tournament. Settlement
852
+ already pending/completed closes this local dispute path; appeal timing is an
853
+ explicit application policy, not a guessed timer.
854
+
855
+ A canonical champion journals one deterministic settlement identity before the
856
+ supplied real economy/inventory/Inbox participant runs. Its retry must enforce
857
+ durable idempotency and whole participant outcomes; no price, currency or reward
858
+ is invented here. The bracket becomes complete only after confirmed settlement.
859
+ Accepted Effects/SQL promises are awaited on interruption. Retention, event
860
+ delivery, reminders, scheduled stage progression and complete prize/provider
861
+ integration remain open; a fixture service is no production participant proof.
862
+
863
+ Scores/ratings, bracket admission and this coordinator share one current SQL
864
+ subject-fence implementation with unchanged persisted identity keys. Registration/
865
+ check-in acquire subject before tournament; seal and graph writers acquire the
866
+ complete sorted subject roster first and refuse changed/erased rosters. Creation
867
+ cannot associate an already erased owner, even under a machine request. Standalone
868
+ admission now also needs `leaderboardSubjectsTable` in normal migration.
869
+
870
+ Own export projects seed, own result outcome/Room links and own dispute metadata,
871
+ never a peer roster. Erase requires permanent app withdrawal, then records the
872
+ same SQL subject tombstone before complete bounded inventory. A per-tournament
873
+ erase receipt precedes actual external withdrawal, including pending identities
874
+ whose Room replies were lost. Failure retains graph/associations; a recreated
875
+ adapter retries. Confirmed withdrawal cancels the whole shared graph, clears
876
+ Room/result/dispute/settlement associations and removes the target roster entry.
877
+ Peer roster entries remain until their own erasure; repeated erasure handles a
878
+ partially scrubbed cancelled roster without reinterpreting it as a new bracket.
879
+ Exact completed retries skip confirmed external cancellation. Digests/fence keys
880
+ are pseudonymous data; retention/removal is still open. SQL fencing does not
881
+ withdraw actual remote resources; the app must prevent late external work and
882
+ await every accepted Room/reward/workflow association.
883
+
884
+ Its Room/grant consumers are explicit lost-reply fault models; they prove
885
+ nothing about actual Rooms/economy/Inbox. `./testing` exports the actual
886
+ fail-closed eight-method service/Layer/Port fixture.
887
+
888
+ ## Ordinary durable tournament stage and advance workflows
889
+
890
+ `./server` exports the optional `leaderboardsTournamentSources` app-shaped root,
891
+ `tournamentStageExecutor` and `tournamentAdvanceExecutor`. It contains ordinary
892
+ browser-safe `tournament.stage`/`tournament.advance` descriptors and paired server
893
+ executors. The normal library build copies the actual source folder into the
894
+ published package. These are explicit discovery sources; exporting them does not
895
+ install a worker, start a timer or provide missing app services. The app must add
896
+ this root through normal plugin discovery and provide actual Stage/Bracket Ports,
897
+ schema/privacy and external participants. The optional factory graph/Stage binding below contributes these sources when Stage is configured; actual default external participants remain open.
898
+
899
+ ```ts
900
+ import { tournamentStageLayer, leaderboardsTournamentSources } from '@voltro/plugin-leaderboards/server'
901
+ import type { TournamentStageOptions } from '@voltro/plugin-leaderboards/server'
902
+ import type { DataStore } from '@voltro/database'
903
+
904
+ export const stageLayer = (store: DataStore, bindings: TournamentStageOptions) =>
905
+ tournamentStageLayer(store, bindings)
906
+
907
+ export const tournamentSourceRoot = leaderboardsTournamentSources
908
+ ```
909
+
910
+ Both workflows are internal, operator-control scope `tournaments:operate`, and
911
+ singleton-skip per exact tenant/tournament for the stage; advance queues (`concurrency: { limit: 1 }`). Start keys include the explicit runKey;
912
+ stage also includes format/rule version. JSON tuple keys avoid delimiter ambiguity.
913
+ The ordinary run AppContext supplies the actual machine identity, which server
914
+ executors capture with payload before lazy work. No fabricated user or service
915
+ identity is introduced. Current adapters still authorize every operation; a start
916
+ guard does not grant permanent authority after a long sleep.
917
+
918
+ The stage checkpoints only admission configuration, sleeps to absolute
919
+ registration/check-in/close instants with the shipped `sleepUntil`, calls actual
920
+ notification participants through steps, then seals and seeds in one step. A late
921
+ attempt or sealed/cancelled window skips obsolete reminders. A successful notify
922
+ flag follows the actual awaited callback, not merely a timer wakeup. The
923
+ `TournamentStagePort`/`tournamentStageLayer` borrows the existing SQL header reader,
924
+ actual Admission/Bracket services, current clock/authorization and explicit
925
+ `notifyEntrants`/`seedRoster` participants. No separate store/pool/Runtime exists.
926
+
927
+ `seedRoster` receives actual sealed checked-in/no-show identities only inside the
928
+ server Effect. It must recover a stable durable seed/no-show/rating decision under
929
+ the same deterministic request identity across retries and fresh adapters. The
930
+ returned complete seed roster must contain exactly the checked-in participants;
931
+ foreign/missing/duplicate or invalid seeds refuse before graph creation. No-show
932
+ penalties and rating tie rules are author policy. Fewer than two valid participants
933
+ cannot be silently fabricated into a bracket. The actual graph adapter rechecks
934
+ the shared SQL Subject fences, so an erased participant cannot be re-associated.
935
+
936
+ Advance checkpoints one real graph/external-work operation. Workflow payloads,
937
+ step inputs/results and final summaries contain only scope/configuration/counts;
938
+ no roster, winner, prior/result ratings, Room credential or reward outcome. Unknown,
939
+ duplicate or inconsistent status observations refuse rather than turning into
940
+ zero counts. SQL remains graph truth; workflow recovery still delegates exact
941
+ resource keys to real downstream participants. Event-triggered starts, automatic
942
+ repeated advancement, pauses/schedule UI and real scheduler/engine replay are
943
+ not established by these sources.
944
+
945
+ Stage notifications/seeding can create durable app associations before graph
946
+ creation. Full withdrawal must cancel/await workflows and those participants as
947
+ well as Rooms/rewards, including pending request identities; hook receipt or a
948
+ count is no all-node proof. Subject export/erase of engine journals, notification
949
+ receipts and seeding records belongs to their actual owners and remains open.
950
+ The read-only factory panel correctly keeps workflowInstalled:false until the
951
+ factory really installs that worker; explicit app sources are not claimed there.
952
+
953
+ Seed snapshots/notifications in that body are explicit participant models, not
954
+ real rating/Inbox proof. No clock/Activity engine was faked into a workflow
955
+ pass.
956
+
957
+
958
+ ### Advancing a bracket and the player read
959
+
960
+ One `tournament.advance` run does all open work: one journaled step per ready match
961
+ without a Room (`advance-1`, `advance-2`, …), and the champion's settlement once
962
+ the bracket is decided. It ends when nothing is left or a step makes no progress.
963
+ A paused bracket (an open dispute) refuses with `conflict` until staff resolve it.
964
+ `tournament.stage` runs the same steps right after seeding, so the first round's
965
+ Rooms exist when the stage ends.
966
+
967
+ Later rounds become ready when results arrive. After `report` (or `resolve`),
968
+ use `reportTournamentResult` (reports and queues `tournament.advance`, report ID as `runKey`);
969
+ nothing runs on a timer. The workflow summary adds `waiting`: ready matches without a
970
+ Room, plus one while the champion awaits settlement.
971
+
972
+ Bracket status lists each match's `roomId` once provisioned. Players read their
973
+ own matches with `leaderboards.myBracketMatches({ tournamentId })` /
974
+ `useMyBracketMatches(tournamentId)`, live from the bracket table: phase, whether
975
+ the caller is champion, and per match the round, status, opponent, Room and
976
+ result. Only verified users can read it, and only their own matches. The route
977
+ reads status as the system subject `leaderboards.myBracketMatches`, so your
978
+ `authorize` policy must allow `status` for it.
979
+
980
+ ```tsx
981
+ import { useMyBracketMatches } from '@voltro/plugin-leaderboards/web'
982
+
983
+ export function MyMatches({ tournamentId }: { tournamentId: string }) {
984
+ const { data } = useMyBracketMatches(tournamentId)
985
+ if (data === undefined) return null
986
+ return (
987
+ <ul>
988
+ {data.matches.map(match => (
989
+ <li key={match.matchId}>
990
+ Round {match.round}: {match.opponentId ?? 'bye'}, {match.roomId === null ? 'waiting for a Room' : 'Room ' + match.roomId}
991
+ </li>
992
+ ))}
993
+ </ul>
994
+ )
995
+ }
996
+ ```
997
+
998
+ Checked on the in-memory workflow engine: a round robin's six Rooms in one run,
999
+ settlement in a later run, a run that waits for results, a paused bracket, the
1000
+ stage's first round, and the installed player read.
1001
+
1002
+ ## Optional installed tournament graph and stage
1003
+
1004
+ `leaderboardsPlugin` accepts `tournamentCoordination` in addition to optional
1005
+ `brackets` admission. A SQL graph binding is `{ backend: 'sql', options }`, using
1006
+ actual `TournamentBracketOptions`. It contributes `_voltro_brackets` to the
1007
+ ordinary schema and declared subject privacy. The factory already contributes
1008
+ the shared Subject fence table; no second pool, runtime, clock loop or cache is
1009
+ created. Without coordination, its table, Tags, intent route and discovery roots
1010
+ are absent; its server facades fail unavailable.
1011
+
1012
+ ```ts
1013
+ import { leaderboardsPlugin } from '@voltro/plugin-leaderboards'
1014
+ import type { LeaderboardsPluginOptions } from '@voltro/plugin-leaderboards'
1015
+
1016
+ export const tournamentPlugin = (bindings: LeaderboardsPluginOptions) =>
1017
+ leaderboardsPlugin(bindings)
1018
+ ```
1019
+
1020
+ A full replacement is `{ backend: 'custom', version, service, sources }`: all eight
1021
+ `TournamentBracketPort` methods, a non-reserved application version and complete
1022
+ actual invalidation source names are required. It contributes no default graph
1023
+ SQL table. Methods/options/source names are captured before app binding; accessor
1024
+ bindings, partial services and reserved custom versions refuse. Replacement owners
1025
+ must still implement current authorization, SQL rebuild, privacy, exact downstream
1026
+ idempotency and withdrawal; capture alone is no conformance proof.
1027
+
1028
+ The same installed services are available through `plugin.leaderboards.tournaments()`
1029
+ and actual `TournamentBracketPort` Layers. `plugin.leaderboards.tournamentStages()`
1030
+ and `TournamentStagePort` exist when `stage` is configured. A SQL stage binding is
1031
+ `{ backend: 'sql', options, privacy }`; its four options are `epochNow`, `authorize`,
1032
+ `seedRoster`, `notifyEntrants`. The factory captures the actual graph and admission
1033
+ services. SQL Stage requires the SQL admission binding, because its window reader
1034
+ uses that registered table. A custom stage binding is
1035
+ `{ backend: 'custom', version, service, privacy }`, with all three Stage methods.
1036
+ Every stage binding requires real `privacy.exportSubject` and `privacy.eraseSubject`
1037
+ participants for seed/reminder associations, including accepted pending work.
1038
+ No successful empty participant is supplied by the factory.
1039
+
1040
+ With Stage configured, the factory contributes the ordinary app-shaped stage and
1041
+ advance source root under the stable `tournaments` discovery name. Optional
1042
+ `workflowSources` replaces that root with an absolute application directory;
1043
+ normal discovery checks directory existence, overlap and procedure collisions.
1044
+ An override without Stage refuses. These are regular descriptor/server pairs,
1045
+ using existing workflow primitives; no parallel scheduler or hidden immediate
1046
+ start. No Stage means no workflow source. Configured discovery is distinct from
1047
+ observed execution and real downstream delivery.
1048
+
1049
+ The browser write is `leaderboards.disputeBracket`/`useDisputeBracket`:
1050
+ tournament, match, dispute identity and a reason code. The actual request supplies
1051
+ human identity and tenant; machine browser calls refuse. Results, winners, scores,
1052
+ Rooms and rewards cannot be submitted through this operation. The actual graph
1053
+ adapter checks own match participation/current authority, pauses disputed work,
1054
+ and returns only aggregate phase/counts. Reporting, resolution, creation and
1055
+ advance remain trusted server Ports. The player read is `myBracketMatches` (above).
1056
+ Public bracket screens remain open.
1057
+
1058
+ Export awaits actual Stage participant export, graph-own export, admission and
1059
+ score export. Erase withdraws Stage participants before graph, admission and scores;
1060
+ a failed Stage withdrawal leaves downstream SQL cleanup incomplete. Each graph
1061
+ adapter retains its own external withdrawal journal. Callbacks must enforce
1062
+ permanent all-node withdrawal, not merely delete a local timer. Custom Stage data
1063
+ must join its supplied privacy participant; engine journals belong to workflow
1064
+ privacy separately. Neither registry coverage nor a declared hook proves erasure.
1065
+
1066
+ Local shutdown closes every optional facade before waiting for admitted whole
1067
+ Effects. An already admitted SQL Stage borrows the captured real seal/create
1068
+ services until its own Effect settles, so later dependency calls survive facade
1069
+ closure. No new work or app rebinding follows closure. This is local lifecycle
1070
+ coordination, not distributed actor/worker/provider quiescence or score-service
1071
+ teardown proof.
1072
+
1073
+ Four-layer process inspection now requires `coordination:null/config`, exact
1074
+ conditional thirteen/fourteen table roles, SQL/custom version and budget semantics,
1075
+ Stage backend/binding and default/custom workflow source selection. SQL Stage
1076
+ without SQL admission and contradictory bound/source shapes refuse in Shared,
1077
+ Cloud and Local decoding. The existing bilingual page shows bindings/configuration,
1078
+ never participants, graph results, winners, provider credentials, source paths,
1079
+ prize outcomes or live worker state. `workflowRuntimeObserved:false` explicitly
1080
+ retains the missing runtime observation. Failed refresh hides stale configuration.
1081
+
1082
+ External Room, prize, notification and seed callbacks in fixtures are explicit
1083
+ models.
1084
+
1085
+
1086
+ ## Production license admission
1087
+
1088
+ Declare the actual `licensingPlugin()` before this plugin in the API app. Production and staging activation require its verified active or grace deployment-wide license to grant `leaderboards`. Explicit development/test mode stays free without recurring per-instance notices; CLI owns the actual template first-boot receipt. Tenant-scoped licenses refuse until actual tenant-bound authority exists. This server-only gate preserves normal schema, store binding and cleanup; it runs before the original activation hook, not before schema/store binding.