@voltro/plugin-leaderboards 0.0.0-stage → 0.87.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.
- package/CHANGELOG.md +24329 -0
- package/LICENSE +61 -0
- package/README.md +1087 -2
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +5007 -0
- package/dist/administration.js +5 -0
- package/dist/bracketAdmission.js +294 -0
- package/dist/bracketAdmissionPolicy.js +22 -0
- package/dist/bracketInstallation.js +133 -0
- package/dist/bracketMatchPorts.js +5 -0
- package/dist/bracketMatches.js +546 -0
- package/dist/brackets.d.ts +149 -0
- package/dist/brackets.js +379 -0
- package/dist/bucketStore.js +93 -0
- package/dist/buckets.js +9 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +17 -0
- package/dist/glicko2.js +72 -0
- package/dist/index.d.ts +1208 -0
- package/dist/index.js +15 -0
- package/dist/inspect.js +84 -0
- package/dist/json.js +39 -0
- package/dist/leaderboard.d.ts +49 -0
- package/dist/leaderboard.js +57 -0
- package/dist/leagueSchedule.js +40 -0
- package/dist/leagues.js +31 -0
- package/dist/openskill.js +74 -0
- package/dist/plugin.js +338 -0
- package/dist/portLifetime.js +67 -0
- package/dist/ports.d.ts +294 -0
- package/dist/ports.js +7 -0
- package/dist/rankQuery.js +24 -0
- package/dist/ranking.d.ts +42 -0
- package/dist/ranking.js +71 -0
- package/dist/rating.d.ts +103 -0
- package/dist/rating.js +50 -0
- package/dist/ratingPools.js +134 -0
- package/dist/ratingStore.js +334 -0
- package/dist/ratingStrategies.js +108 -0
- package/dist/records.d.ts +116 -0
- package/dist/records.js +88 -0
- package/dist/rpc.d.ts +321 -0
- package/dist/rpc.js +306 -0
- package/dist/score.d.ts +13 -0
- package/dist/score.js +27 -0
- package/dist/server.d.ts +1477 -0
- package/dist/server.js +17 -0
- package/dist/service.js +949 -0
- package/dist/subjectFences.js +44 -0
- package/dist/tables.entity.js +188 -0
- package/dist/testing.d.ts +872 -0
- package/dist/testing.js +368 -0
- package/dist/tournament-app/tournament.advance.workflow.server.tsx +3 -0
- package/dist/tournament-app/tournament.advance.workflow.tsx +12 -0
- package/dist/tournament-app/tournament.stage.workflow.server.tsx +3 -0
- package/dist/tournament-app/tournament.stage.workflow.tsx +10 -0
- package/dist/tournamentInstallation.js +168 -0
- package/dist/tournamentPrizes.js +34 -0
- package/dist/tournamentStage.js +151 -0
- package/dist/tournamentStagePorts.js +5 -0
- package/dist/tournamentWorkflow.js +101 -0
- package/dist/tournamentWorkflowSchemas.js +54 -0
- package/dist/tournaments.d.ts +32 -0
- package/dist/tournaments.js +21 -0
- package/dist/web.d.ts +175 -0
- package/dist/web.js +17 -0
- package/package.json +113 -4
package/README.md
CHANGED
|
@@ -1,3 +1,1088 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Leaderboard foundations
|
|
2
2
|
|
|
3
|
-
This
|
|
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.
|