@zmnatz/usa-club-rugby-data 0.2.0 → 0.4.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/README.md CHANGED
@@ -23,7 +23,7 @@ const players = await client.players('rocky-gorge'); // a verdict per player per
23
23
  const report = await client.eligibility('rocky-gorge'); // the same, grouped by side
24
24
  ```
25
25
 
26
- A player record carries `participation` (Match Participation: matches started plus matches entered as a substitute), `eligible`, `reasons` for a failure, `pending` for the matches an eligible player still needs, and per-round detail in `rows[].rounds`.
26
+ A player record carries `participation` (Match Participation: matches started plus matches entered as a substitute), `eligible`, `reasons` for a failure, `pending` for the matches an eligible player still needs, `cautions` when one more upper-side match would end an eligible player's eligibility for the rest of the season, and per-round detail in `rows[].rounds`.
27
27
 
28
28
  Everything the client does is also a pure function: `clubEligibility(snapshot, fixtures, club, { rules, now })`, `computeBreakdown`, `parseTeamSide`, `possibleFixtures` and so on. The same inputs always give the same verdicts. Pass your own `EligibilityRules` to apply a different season's thresholds; `DEFAULT_RULES` are the 2026-27 Senior Club Regulations (§3.4.2, §3.5).
29
29
 
@@ -35,3 +35,11 @@ export declare function clubsInRows(rows: PlayerRow[]): Club[];
35
35
  * are combined into a single total; rows with no id cannot be joined and are
36
36
  * excluded. */
37
37
  export declare function computeBreakdown(rows: PlayerRow[], sides: Side[], rules: EligibilityRules, remaining?: RemainingMatches): EligibilityBreakdown;
38
+ /** Where a player stands on a side, most urgent first. */
39
+ export declare const VERDICT_STATUSES: readonly ['ineligible', 'caution', 'on-track', 'eligible'];
40
+ export type VerdictStatus = (typeof VERDICT_STATUSES)[number];
41
+ /** A verdict as one status. Ineligible outranks Caution, and Caution outranks On
42
+ * Track, which a player can still recover from. */
43
+ export declare function verdictStatus(player: Pick<PlayerEligibility, 'eligible' | 'pending' | 'cautions'>): VerdictStatus;
44
+ /** A status's place in `VERDICT_STATUSES`, for sorting: lowest is most urgent. */
45
+ export declare function verdictRank(player: Pick<PlayerEligibility, 'eligible' | 'pending' | 'cautions'>): number;
@@ -99,8 +99,10 @@ function evaluate(row, index, rosters, rules, remaining) {
99
99
  const isLowest = index === rosters.length - 1;
100
100
  const ownLabel = isLowest ? own.label : `${own.label} or lower`;
101
101
  const fixturesLeft = sum(rosters.slice(index).map((roster) => remaining[roster.side.teamName] ?? 0));
102
+ const fixturesKnown = rosters.slice(index).every((roster) => remaining[roster.side.teamName] !== undefined);
102
103
  const reasons = [];
103
104
  const pending = [];
105
+ const cautions = [];
104
106
  // A two-match minimum is met if the player can still reach it by Super
105
107
  // Regionals (Reg 3.5.1(a)); see docs/adr/008-eligibility-rules.md.
106
108
  const checkMinimum = (count, matchLabel) => {
@@ -123,10 +125,16 @@ function evaluate(row, index, rosters, rules, remaining) {
123
125
  if (count >= rules.upperMatchLimit) {
124
126
  reasons.push(`Played ${count} ${rosters[aboveIndex].side.label} matches — playing ${rules.upperMatchLimit} for an upper side loses ${own.label} eligibility`);
125
127
  }
128
+ else if (count === rules.upperMatchLimit - 1) {
129
+ cautions.push(`One more ${rosters[aboveIndex].side.label} match (${count} played) loses ${own.label} eligibility for the rest of the season`);
130
+ }
126
131
  }
127
132
  if (atOrBelow < sum(above)) {
128
133
  reasons.push(`Upper-side participation (${sum(above)}) exceeds ${ownLabel} participation (${atOrBelow})`);
129
134
  }
135
+ else if (atOrBelow === sum(above) && fixturesLeft === 0 && fixturesKnown) {
136
+ cautions.push(`One more upper-side match loses ${own.label} eligibility for the rest of the season (level at ${atOrBelow} with no ${ownLabel} fixtures left)`);
137
+ }
130
138
  }
131
139
  return {
132
140
  playerId: row.playerId,
@@ -146,6 +154,7 @@ function evaluate(row, index, rosters, rules, remaining) {
146
154
  eligible: reasons.length === 0,
147
155
  reasons,
148
156
  pending,
157
+ cautions: reasons.length === 0 ? cautions : [],
149
158
  };
150
159
  }
151
160
  const byName = (a, b) => a.lastName.localeCompare(b.lastName) || a.firstName.localeCompare(b.firstName);
@@ -234,6 +243,7 @@ export function computeBreakdown(rows, sides, rules, remaining = {}) {
234
243
  total: players.length,
235
244
  eligible: players.filter((player) => player.eligible).length,
236
245
  onTrack: players.filter((player) => player.eligible && player.pending.length > 0).length,
246
+ caution: players.filter((player) => player.cautions.length > 0).length,
237
247
  competitions: [...roster.competitions].sort(),
238
248
  },
239
249
  };
@@ -241,3 +251,18 @@ export function computeBreakdown(rows, sides, rules, remaining = {}) {
241
251
  ignoredTeamNames: [...new Set(rows.map((row) => row.teamName).filter((name) => !teamNames.has(name)))],
242
252
  };
243
253
  }
254
+ /** Where a player stands on a side, most urgent first. */
255
+ export const VERDICT_STATUSES = ['ineligible', 'caution', 'on-track', 'eligible'];
256
+ /** A verdict as one status. Ineligible outranks Caution, and Caution outranks On
257
+ * Track, which a player can still recover from. */
258
+ export function verdictStatus(player) {
259
+ if (!player.eligible)
260
+ return 'ineligible';
261
+ if (player.cautions.length > 0)
262
+ return 'caution';
263
+ return player.pending.length > 0 ? 'on-track' : 'eligible';
264
+ }
265
+ /** A status's place in `VERDICT_STATUSES`, for sorting: lowest is most urgent. */
266
+ export function verdictRank(player) {
267
+ return VERDICT_STATUSES.indexOf(verdictStatus(player));
268
+ }
package/dist/types.d.ts CHANGED
@@ -73,6 +73,10 @@ export interface PlayerEligibility {
73
73
  /** Matches an eligible player still needs by Super Regionals; empty when the
74
74
  * minimum is already met. These players are "on track". */
75
75
  pending: string[];
76
+ /** How one more upper-side match would end an eligible player's eligibility
77
+ * for this side for the rest of the season; empty otherwise. These players
78
+ * are in "Caution". */
79
+ cautions: string[];
76
80
  }
77
81
  /** One side's verdicts and summary. */
78
82
  export interface SideBreakdown extends Side {
@@ -90,6 +94,8 @@ export interface DivisionSummary {
90
94
  eligible: number;
91
95
  /** How many of the eligible players still need matches to reach a minimum. */
92
96
  onTrack: number;
97
+ /** How many of the eligible players are in Caution. */
98
+ caution: number;
93
99
  /** Distinct competitions this side's roster spans, sorted. */
94
100
  competitions: string[];
95
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zmnatz/usa-club-rugby-data",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "USA Club Rugby senior club teams, players and NCS eligibility from the Rugby Xplorer match record: typed readers for the nightly published data, a rate-limited CMS client, and the reproducible eligibility rules.",
5
5
  "keywords": [
6
6
  "rugby",