cito-mcp 0.3.16 → 0.3.17
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/dist/instructions.js +1 -1
- package/dist/tools/insight.js +51 -17
- package/dist/tools/live.js +7 -3
- package/dist/tools/match.js +101 -33
- package/dist/tools/meta.js +4 -3
- package/dist/tools/normalize.js +50 -3
- package/dist/tools/resolve.js +15 -1
- package/dist/tools/team.js +24 -4
- package/package.json +1 -1
package/dist/instructions.js
CHANGED
|
@@ -60,7 +60,7 @@ Live board: api_health (optional) → live_matches → match_summary for selecte
|
|
|
60
60
|
UFC empty live (count=0): read section.note / emptyReason / health (workerAlive, lag) / supervisor / nextCard — do not claim "API offline" without workerAlive/lag; never invent matchups from supervisor shells.
|
|
61
61
|
Team page: resolve_entity {type:team} → team_profile.
|
|
62
62
|
Player card: resolve_entity {type:player|fighter} → player_profile.
|
|
63
|
-
Fight night / event card: resolve_entity {type:event} → event_card {
|
|
63
|
+
Fight night / event card: resolve_entity {type:event} → event_card {includeMatches:true} → match_preview for a featured bout.
|
|
64
64
|
Match preview (named sides): resolve_entity each side (optional) → match_preview {teamA, teamB}.
|
|
65
65
|
App scaffold: list_capabilities ∥ api_health ∥ live_matches, then one composite per screen.
|
|
66
66
|
|
package/dist/tools/insight.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, unwrapPayload, } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
|
-
import { normalizeMatch, sortByCardOrder } from './normalize.js';
|
|
6
|
+
import { normalizeMatch, presentSides, sortByCardOrder } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, stringSchema, } from './types.js';
|
|
8
8
|
async function loadSide(ctx, game, side, recentLimit, includeRosters) {
|
|
9
9
|
const partial = [];
|
|
@@ -270,7 +270,9 @@ export function composeH2H(game, sideA, sideB, rows, limit) {
|
|
|
270
270
|
.slice(0, limit);
|
|
271
271
|
return {
|
|
272
272
|
meetings: meetings.length,
|
|
273
|
-
|
|
273
|
+
// Tennis has players, not teams: rename team1/team2 -> player1/player2 on
|
|
274
|
+
// the way out. Every other game keeps team1/team2 unchanged.
|
|
275
|
+
lastMeetings: meetings.slice(0, 5).map((m) => presentSides(m, game)),
|
|
274
276
|
recordNote: meetings.length ? `${meetings.length} past meetings found in window` : 'No past H2H meetings in window',
|
|
275
277
|
};
|
|
276
278
|
}
|
|
@@ -620,14 +622,17 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
620
622
|
context,
|
|
621
623
|
sideA: {
|
|
622
624
|
entity: sideARes.entity,
|
|
623
|
-
roster
|
|
624
|
-
|
|
625
|
+
// A tennis player has no roster; the key never applied here, not
|
|
626
|
+
// merely came back empty. UFC fighters get the same treatment
|
|
627
|
+
// elsewhere in this surface — omit rather than ship a foreign key.
|
|
628
|
+
...(game === 'tennis' ? {} : { roster: sideARes.roster }),
|
|
629
|
+
recentForm: sideARes.recentForm.map((m) => presentSides(m, game)),
|
|
625
630
|
keyPlayers: sideARes.keyPlayers,
|
|
626
631
|
},
|
|
627
632
|
sideB: {
|
|
628
633
|
entity: sideBRes.entity,
|
|
629
|
-
roster: sideBRes.roster,
|
|
630
|
-
recentForm: sideBRes.recentForm,
|
|
634
|
+
...(game === 'tennis' ? {} : { roster: sideBRes.roster }),
|
|
635
|
+
recentForm: sideBRes.recentForm.map((m) => presentSides(m, game)),
|
|
631
636
|
keyPlayers: sideBRes.keyPlayers,
|
|
632
637
|
},
|
|
633
638
|
h2h,
|
|
@@ -744,8 +749,12 @@ Prefer match_preview for one bout/match briefing; match_summary for completed re
|
|
|
744
749
|
|
|
745
750
|
Do not use when: you only need live scores (live_matches); single finished match recap (match_summary); no event name/id yet and game unknown.
|
|
746
751
|
|
|
752
|
+
Tennis: pass a tournament id/slug to get the draw bracket — rounds carry a round
|
|
753
|
+
code (Q1..R128, QF, SF, F) with each match's players and score. Set is the
|
|
754
|
+
primary tennis path here; there is no separate draw tool.
|
|
755
|
+
|
|
747
756
|
Parallel-safe: yes. Upstream cost: 1–4.
|
|
748
|
-
Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "
|
|
757
|
+
Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "includeStandings": false }`,
|
|
749
758
|
inputSchema: {
|
|
750
759
|
type: 'object',
|
|
751
760
|
additionalProperties: false,
|
|
@@ -757,7 +766,10 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeBouts": true, "inc
|
|
|
757
766
|
game: gameSchema({ allowAll: false, required: true }),
|
|
758
767
|
eventIdOrSlug: stringSchema('Event or tournament id/slug. Prefer over q when known. Required unless q is given. Example: "ufc-300".', 'ufc-300'),
|
|
759
768
|
q: stringSchema('Free-text event name when id/slug unknown. Example: "UFC 300". Resolves within this tool — still prefer resolve_entity when disambiguating many hits.', 'UFC 300'),
|
|
760
|
-
|
|
769
|
+
// Named for the general case (a card, a bracket, a match list), not
|
|
770
|
+
// combat-sport vocabulary — "bout" has no meaning for a tennis draw or a
|
|
771
|
+
// CS2 event hub, and this same parameter gates both.
|
|
772
|
+
includeMatches: boolSchema('Include the bout/match list, or the tennis draw bracket (default true).', true),
|
|
761
773
|
includeStandings: boolSchema('Include standings/rankings snippet when API supports event/tournament/division scope (default false). ' +
|
|
762
774
|
'For UFC this also joins divisional rank and movement onto each bout corner as team.rank / team.rankMovement — ' +
|
|
763
775
|
'bout rows themselves carry no rank. Costs one extra upstream call.', false),
|
|
@@ -785,7 +797,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeBouts": true, "inc
|
|
|
785
797
|
const game = gameParse.game;
|
|
786
798
|
let eventKey = typeof args.eventIdOrSlug === 'string' ? args.eventIdOrSlug.trim() : '';
|
|
787
799
|
const q = typeof args.q === 'string' ? args.q.trim() : '';
|
|
788
|
-
const includeBouts = args.
|
|
800
|
+
const includeBouts = args.includeMatches !== false;
|
|
789
801
|
const includeStandings = args.includeStandings === true;
|
|
790
802
|
const limit = clampInt(args.limit, 20, 1, 50);
|
|
791
803
|
if (!eventKey && !q) {
|
|
@@ -834,6 +846,10 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeBouts": true, "inc
|
|
|
834
846
|
};
|
|
835
847
|
let bouts = [];
|
|
836
848
|
let standingsSnippet = null;
|
|
849
|
+
// Tennis only: the draw bracket grouped by round, each round carrying the
|
|
850
|
+
// upstream's own round code (Q1..R128, QF, SF, F) so an agent can render
|
|
851
|
+
// the tournament tree rather than one flat, order-dependent match list.
|
|
852
|
+
let drawRounds = null;
|
|
837
853
|
if (game === 'ufc') {
|
|
838
854
|
const detail = await fetchJson(ctx, `/ufc/events/${encodeURIComponent(eventKey)}`);
|
|
839
855
|
upstreamCalls += 1;
|
|
@@ -1246,16 +1262,29 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeBouts": true, "inc
|
|
|
1246
1262
|
rateLimit = { ...rateLimit, ...draw.headers };
|
|
1247
1263
|
if (draw.ok) {
|
|
1248
1264
|
const dd = asRecord(unwrapPayload(draw.data)) ?? {};
|
|
1249
|
-
const
|
|
1265
|
+
const rawRounds = Array.isArray(dd.rounds) ? dd.rounds : [];
|
|
1250
1266
|
const flat = [];
|
|
1251
|
-
|
|
1267
|
+
drawRounds = rawRounds.map((rr) => {
|
|
1252
1268
|
const rec = asRecord(rr) ?? {};
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1269
|
+
const roundCode = pickString(rec.round_code, rec.roundCode) ?? null;
|
|
1270
|
+
const roundName = pickString(rec.round_name, rec.name, rec.round) ?? roundCode;
|
|
1271
|
+
const matches = (Array.isArray(rec.matches) ? rec.matches : []).map((mm) => {
|
|
1255
1272
|
const mr = asRecord(mm) ?? {};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1273
|
+
const row = { ...mr, id: mr.match_id, round: roundName, tournament_name: event.name, status: 'completed' };
|
|
1274
|
+
flat.push(row);
|
|
1275
|
+
// normalizeMatch gives named, id'd sides (player1/player2 once
|
|
1276
|
+
// presented); the raw score string and bracket-progression
|
|
1277
|
+
// fields are the only place the actual set score and next slot
|
|
1278
|
+
// live, so carry them alongside rather than dropping them.
|
|
1279
|
+
return presentSides({
|
|
1280
|
+
...normalizeMatch('tennis', row),
|
|
1281
|
+
score: pickString(mr.score) ?? null,
|
|
1282
|
+
matchNum: typeof mr.match_num === 'number' ? mr.match_num : null,
|
|
1283
|
+
nextMatchId: pickString(mr.next_match_id) ?? null,
|
|
1284
|
+
}, 'tennis');
|
|
1285
|
+
});
|
|
1286
|
+
return { roundCode, roundName, matches };
|
|
1287
|
+
});
|
|
1259
1288
|
bouts = flat.slice(0, limit).map((row) => normalizeMatch('tennis', row));
|
|
1260
1289
|
if (!bouts.length)
|
|
1261
1290
|
warnings.push('No draw yet: the tournament has no archived results');
|
|
@@ -1344,8 +1373,13 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeBouts": true, "inc
|
|
|
1344
1373
|
},
|
|
1345
1374
|
data: {
|
|
1346
1375
|
event,
|
|
1347
|
-
bouts: includeBouts ? bouts : [],
|
|
1376
|
+
bouts: includeBouts ? bouts.map((b) => presentSides(b, game)) : [],
|
|
1348
1377
|
boutCount: includeBouts ? bouts.length : 0,
|
|
1378
|
+
// Tennis only: the same matches grouped by round, with round codes
|
|
1379
|
+
// (Q1..R128, QF, SF, F) and each match's score/bracket progression —
|
|
1380
|
+
// this is what makes a bracket renderable; `bouts` above is the flat
|
|
1381
|
+
// list every other game already gets.
|
|
1382
|
+
...(game === 'tennis' ? { draw: drawRounds ? { rounds: drawRounds } : null } : {}),
|
|
1349
1383
|
standings: includeStandings ? standingsSnippet : null,
|
|
1350
1384
|
nextSteps: [
|
|
1351
1385
|
'For a featured bout/match: match_preview { game, matchId } or teamA+teamB',
|
package/dist/tools/live.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { clampInt, decodeCursor, encodeCursor, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, } from '../client.js';
|
|
5
5
|
import { DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT, errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
|
-
import { normalizeMatch } from './normalize.js';
|
|
6
|
+
import { normalizeMatch, presentSides } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
8
8
|
const LIVE_PATHS = {
|
|
9
9
|
lol: '/lol/live',
|
|
@@ -292,7 +292,9 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
292
292
|
label: m.label,
|
|
293
293
|
startTime: m.startTime,
|
|
294
294
|
}))
|
|
295
|
-
:
|
|
295
|
+
// Tennis has players, not teams: rename team1/team2 -> player1/player2
|
|
296
|
+
// at this boundary only. Every other game is untouched.
|
|
297
|
+
: normalized.map((m) => presentSides(m, m.game));
|
|
296
298
|
if (game === 'ufc') {
|
|
297
299
|
const health = asRecord(meta?.health) ?? asRecord(payload?.health);
|
|
298
300
|
const events = Array.isArray(payload?.events) ? payload.events : [];
|
|
@@ -759,7 +761,9 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
759
761
|
nextCursor: hasMore ? encodeCursor({ offset: offset + limit }) : null,
|
|
760
762
|
prevCursor: offset > 0 ? encodeCursor({ offset: Math.max(0, offset - limit) }) : null,
|
|
761
763
|
},
|
|
762
|
-
|
|
764
|
+
// Tennis has players, not teams: rename team1/team2 -> player1/player2
|
|
765
|
+
// at this boundary only. Every other game is untouched.
|
|
766
|
+
data: { items: items.map((m) => presentSides(m, game)) },
|
|
763
767
|
});
|
|
764
768
|
},
|
|
765
769
|
};
|
package/dist/tools/match.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, unwrapPayload, } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
|
-
import { normalizeMatch, normalizeUfcMethod } from './normalize.js';
|
|
6
|
+
import { normalizeMatch, normalizeUfcMethod, presentSides } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, parseGame, stringSchema, } from './types.js';
|
|
8
8
|
async function getSection(ctx, path, query) {
|
|
9
9
|
return fetchJson(ctx, path, { query });
|
|
@@ -61,7 +61,7 @@ function matchCore(game, matchId, raw) {
|
|
|
61
61
|
const scoreline = m.team1 || m.team2
|
|
62
62
|
? `${m.team1?.name ?? '?'} ${s1 ?? '-'} : ${s2 ?? '-'} ${m.team2?.name ?? '?'}`
|
|
63
63
|
: null;
|
|
64
|
-
|
|
64
|
+
const core = {
|
|
65
65
|
matchId: m.matchId !== 'unknown' ? m.matchId : matchId,
|
|
66
66
|
game,
|
|
67
67
|
status: m.status,
|
|
@@ -78,10 +78,17 @@ function matchCore(game, matchId, raw) {
|
|
|
78
78
|
team2: m.team2,
|
|
79
79
|
event: m.event,
|
|
80
80
|
league: m.league,
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
// method/methodRaw/weightClass/referee are combat-sport vocabulary. They
|
|
82
|
+
// stayed on every match_summary/match_details card regardless of game, so
|
|
83
|
+
// a tennis card carried referee:null and weightClass:null — foreign
|
|
84
|
+
// fields, not merely empty ones. UFC keeps them; every other game omits
|
|
85
|
+
// the keys entirely rather than shipping them as null.
|
|
86
|
+
...(game === 'ufc'
|
|
87
|
+
? {
|
|
88
|
+
method: normalizeUfcMethod(pickString(r.method, r.resultMethod)) ?? null,
|
|
89
|
+
methodRaw: pickString(r.method, r.resultMethod) ?? null,
|
|
90
|
+
}
|
|
91
|
+
: {}),
|
|
85
92
|
winner: (() => {
|
|
86
93
|
// Never name a winner while play is in progress: the score fallback
|
|
87
94
|
// below reads "who is ahead", which mid-match is a lead, not a result.
|
|
@@ -109,33 +116,37 @@ function matchCore(game, matchId, raw) {
|
|
|
109
116
|
return sideLabel(s1 > s2 ? m.team1 : m.team2);
|
|
110
117
|
return null;
|
|
111
118
|
})(),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
...(game === 'ufc'
|
|
120
|
+
? {
|
|
121
|
+
weightClass: pickString(r.weightClass, r.division) ?? null,
|
|
122
|
+
referee: (() => {
|
|
123
|
+
const nested = asRecord(r.referee);
|
|
124
|
+
if (nested) {
|
|
125
|
+
const joined = [nested.firstName, nested.lastName].filter(Boolean).join(' ').trim();
|
|
126
|
+
const name = pickString(nested.name, joined || undefined) ?? null;
|
|
127
|
+
const id = pickString(nested.id, nested.refereeId) ?? null;
|
|
128
|
+
if (name || id) {
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
name,
|
|
132
|
+
firstName: pickString(nested.firstName) ?? null,
|
|
133
|
+
lastName: pickString(nested.lastName) ?? null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const name = pickString(r.refereeName) ?? null;
|
|
138
|
+
const id = pickString(r.refereeId) ?? null;
|
|
139
|
+
if (!name && !id)
|
|
140
|
+
return null;
|
|
120
141
|
return {
|
|
121
142
|
id,
|
|
122
143
|
name,
|
|
123
|
-
firstName: pickString(
|
|
124
|
-
lastName: pickString(
|
|
144
|
+
firstName: pickString(r.refereeFirstName) ?? null,
|
|
145
|
+
lastName: pickString(r.refereeLastName) ?? null,
|
|
125
146
|
};
|
|
126
|
-
}
|
|
147
|
+
})(),
|
|
127
148
|
}
|
|
128
|
-
|
|
129
|
-
const id = pickString(r.refereeId) ?? null;
|
|
130
|
-
if (!name && !id)
|
|
131
|
-
return null;
|
|
132
|
-
return {
|
|
133
|
-
id,
|
|
134
|
-
name,
|
|
135
|
-
firstName: pickString(r.refereeFirstName) ?? null,
|
|
136
|
-
lastName: pickString(r.refereeLastName) ?? null,
|
|
137
|
-
};
|
|
138
|
-
})(),
|
|
149
|
+
: {}),
|
|
139
150
|
rawStatus: pickString(r.status, r.state) ?? null,
|
|
140
151
|
// Point-level live state (tennis): current game score like "40-AD", which
|
|
141
152
|
// side is serving, and the set in progress. Absent for other games.
|
|
@@ -150,6 +161,17 @@ function matchCore(game, matchId, raw) {
|
|
|
150
161
|
}
|
|
151
162
|
: {}),
|
|
152
163
|
};
|
|
164
|
+
return core;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Rename team1/team2 -> player1/player2 (top level and the nested score
|
|
168
|
+
* sub-object) for tennis only, at the point a matchCore object leaves this
|
|
169
|
+
* module. Kept separate from matchCore itself so every internal reader
|
|
170
|
+
* (scoreline strings, winner lookup, sideLabel) keeps working against the
|
|
171
|
+
* stable team1/team2 shape; only the client-facing copy is renamed.
|
|
172
|
+
*/
|
|
173
|
+
function toClientMatch(core, game) {
|
|
174
|
+
return presentSides({ ...core, score: presentSides(core.score, game) }, game);
|
|
153
175
|
}
|
|
154
176
|
function primaryPath(game, matchId) {
|
|
155
177
|
switch (game) {
|
|
@@ -338,6 +360,36 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
338
360
|
}
|
|
339
361
|
// games/maps
|
|
340
362
|
enrich.push((async () => {
|
|
363
|
+
// Tennis has no per-set endpoint of its own; the set-by-set score is
|
|
364
|
+
// already sitting in `primary` (the same payload matchCore built the
|
|
365
|
+
// scoreline from). Pulling it out here means match_summary keeps the
|
|
366
|
+
// set breakdown instead of shipping gamesOrMaps: [] next to a fully
|
|
367
|
+
// populated `sets` array one field over.
|
|
368
|
+
if (game === 'tennis') {
|
|
369
|
+
const payload = asRecord(asRecord(primary.data)?.data) ?? asRecord(primary.data);
|
|
370
|
+
const sets = Array.isArray(payload?.sets) ? payload.sets : [];
|
|
371
|
+
gamesOrMaps = sets.map((s) => {
|
|
372
|
+
const sr = asRecord(s) ?? {};
|
|
373
|
+
// The live route (/tennis/matches/live/{id}) spells this set_num +
|
|
374
|
+
// score + is_completed; the archive route (/tennis/matches/{id})
|
|
375
|
+
// spells the same set set_number, with no per-set score string
|
|
376
|
+
// (the match-level `score` already holds "6-4, 6-4, 2-1") and no
|
|
377
|
+
// completion flag. Read both spellings rather than nulling out
|
|
378
|
+
// real data because match_summary happened to resolve the other
|
|
379
|
+
// route this time.
|
|
380
|
+
const p1g = typeof sr.player1_games === 'number' ? sr.player1_games : null;
|
|
381
|
+
const p2g = typeof sr.player2_games === 'number' ? sr.player2_games : null;
|
|
382
|
+
return {
|
|
383
|
+
setNumber: typeof sr.set_num === 'number' ? sr.set_num : typeof sr.set_number === 'number' ? sr.set_number : null,
|
|
384
|
+
score: pickString(sr.score) ?? (p1g != null && p2g != null ? `${p1g}-${p2g}` : null),
|
|
385
|
+
player1Games: p1g,
|
|
386
|
+
player2Games: p2g,
|
|
387
|
+
tiebreak: sr.tiebreak ?? null,
|
|
388
|
+
completed: sr.is_completed === true,
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
341
393
|
let path = null;
|
|
342
394
|
if (game === 'lol')
|
|
343
395
|
path = `/lol/matches/${encodeURIComponent(matchId)}/games`;
|
|
@@ -364,11 +416,11 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
364
416
|
}
|
|
365
417
|
})());
|
|
366
418
|
await Promise.all(enrich);
|
|
367
|
-
const scoreline = {
|
|
419
|
+
const scoreline = presentSides({
|
|
368
420
|
display: `${match.team1?.name ?? '?'} ${match.score.team1 ?? '-'} : ${match.score.team2 ?? '-'} ${match.team2?.name ?? '?'}`,
|
|
369
421
|
team1: match.score.team1,
|
|
370
422
|
team2: match.score.team2,
|
|
371
|
-
};
|
|
423
|
+
}, game);
|
|
372
424
|
if (view === 'summary' && playerPerformances.length > 10) {
|
|
373
425
|
playerPerformances = playerPerformances.slice(0, 10);
|
|
374
426
|
}
|
|
@@ -382,7 +434,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
382
434
|
partial: partial.length ? partial : undefined,
|
|
383
435
|
entities: { games: [game], ids: { matchId: match.matchId } },
|
|
384
436
|
data: {
|
|
385
|
-
match,
|
|
437
|
+
match: toClientMatch(match, game),
|
|
386
438
|
scoreline,
|
|
387
439
|
keyEvents,
|
|
388
440
|
playerPerformances,
|
|
@@ -588,9 +640,25 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
588
640
|
// The header is the identity of the response, not an optional section.
|
|
589
641
|
wanted.add('base');
|
|
590
642
|
if (wanted.has('base')) {
|
|
591
|
-
|
|
643
|
+
let res = await getSection(ctx, primaryPath(game, matchId));
|
|
592
644
|
upstreamCalls += 1;
|
|
593
645
|
rateLimit = res.headers;
|
|
646
|
+
// Same fallback match_summary already has: the live board hands out
|
|
647
|
+
// s365_* ids that only resolve at /matches/live/{id} until the match
|
|
648
|
+
// finishes and archives. Without this, a matchId taken straight off
|
|
649
|
+
// live_matches 404s here even though match_summary resolves it fine.
|
|
650
|
+
if (!res.ok && game === 'tennis' && res.status === 404) {
|
|
651
|
+
res = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
|
|
652
|
+
upstreamCalls += 1;
|
|
653
|
+
rateLimit = { ...rateLimit, ...res.headers };
|
|
654
|
+
const gid = matchId.match(/^s365_(\d+)$/)?.[1];
|
|
655
|
+
if (!res.ok && gid) {
|
|
656
|
+
const year = new Date().getUTCFullYear();
|
|
657
|
+
res = await getSection(ctx, `/tennis/matches/${encodeURIComponent(`s365_${year}_${gid}`)}`);
|
|
658
|
+
upstreamCalls += 1;
|
|
659
|
+
rateLimit = { ...rateLimit, ...res.headers };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
594
662
|
if (!res.ok) {
|
|
595
663
|
return errorEnvelope({
|
|
596
664
|
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
@@ -604,7 +672,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
604
672
|
httpStatus: res.status,
|
|
605
673
|
});
|
|
606
674
|
}
|
|
607
|
-
sections.base = matchCore(game, matchId, res.data);
|
|
675
|
+
sections.base = toClientMatch(matchCore(game, matchId, res.data), game);
|
|
608
676
|
}
|
|
609
677
|
const tasks = [];
|
|
610
678
|
const load = (section, path, query) => {
|
package/dist/tools/meta.js
CHANGED
|
@@ -139,11 +139,12 @@ const TOOL_CATALOG = [
|
|
|
139
139
|
{
|
|
140
140
|
name: 'event_card',
|
|
141
141
|
outcome: 'Event / fight-night card: identity + bout list, card-ordered (main event first), ' +
|
|
142
|
-
'each corner carrying photos, record, nickname, country and weight class (+ optional standings)'
|
|
142
|
+
'each corner carrying photos, record, nickname, country and weight class (+ optional standings). ' +
|
|
143
|
+
'For tennis, pass a tournament id/slug to get the draw bracket, grouped by round (Q1..R128, QF, SF, F).',
|
|
143
144
|
parallelSafe: true,
|
|
144
145
|
games: [...PRIMARY_GAMES],
|
|
145
146
|
jobs: ['event_card', 'schedule', 'media', 'app_scaffold'],
|
|
146
|
-
exampleArgs: { game: 'ufc', eventIdOrSlug: 'ufc-300',
|
|
147
|
+
exampleArgs: { game: 'ufc', eventIdOrSlug: 'ufc-300', includeMatches: true },
|
|
147
148
|
preferOver: [
|
|
148
149
|
'call_api /ufc/events + bout expansion',
|
|
149
150
|
'N+1 match_summary for card list',
|
|
@@ -208,7 +209,7 @@ const RECIPES = [
|
|
|
208
209
|
id: 'ufc_fight_night_card',
|
|
209
210
|
steps: [
|
|
210
211
|
'resolve_entity { game: "ufc", type: "event", q } OR pass known eventIdOrSlug',
|
|
211
|
-
'event_card { game: "ufc", eventIdOrSlug,
|
|
212
|
+
'event_card { game: "ufc", eventIdOrSlug, includeMatches: true }',
|
|
212
213
|
'optional match_preview for featured bout; standings { game: "ufc", scope: "division" }',
|
|
213
214
|
],
|
|
214
215
|
},
|
package/dist/tools/normalize.js
CHANGED
|
@@ -28,6 +28,22 @@ function deriveProxiedImageUrl(rawUrl, game) {
|
|
|
28
28
|
return null;
|
|
29
29
|
return `${IMAGE_PROXY_BASE}/public/images/ufc/${Buffer.from(rawUrl, 'utf8').toString('base64url')}`;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Tennis has players, not teams. The cross-game NormalizedMatch type keeps
|
|
33
|
+
* team1/team2 internally for every game so the rest of this module stays
|
|
34
|
+
* uniform (nestedSide, sidesMatch, sortByCardOrder, …); this renames those two
|
|
35
|
+
* keys — and, when present, a nested score sub-object shaped the same way —
|
|
36
|
+
* at the JSON boundary for tennis only. Every other game passes through
|
|
37
|
+
* unchanged.
|
|
38
|
+
*/
|
|
39
|
+
export function presentSides(obj, game) {
|
|
40
|
+
if (game !== 'tennis' || !obj || typeof obj !== 'object')
|
|
41
|
+
return obj;
|
|
42
|
+
if (!('team1' in obj) && !('team2' in obj))
|
|
43
|
+
return obj;
|
|
44
|
+
const { team1, team2, ...rest } = obj;
|
|
45
|
+
return { ...rest, player1: team1, player2: team2 };
|
|
46
|
+
}
|
|
31
47
|
function sideFrom(name, id, slug, score, extra) {
|
|
32
48
|
if (!name && !id && !slug)
|
|
33
49
|
return null;
|
|
@@ -456,6 +472,29 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
456
472
|
const resultRound = numOrNull(r.resultRound);
|
|
457
473
|
const hasResultDetail = method != null || resultRound != null || winnerSlug != null || resultTime != null;
|
|
458
474
|
const isCancelled = r.isCancelled === true;
|
|
475
|
+
// Tennis live/detail rows carry a top-level sets[] (set_num/player*_games/
|
|
476
|
+
// tiebreak/is_completed) plus game_score/server/current_set. A live board
|
|
477
|
+
// that only reports "score: 1" cannot tell 6-0 6-0 from a 7-6 in progress;
|
|
478
|
+
// carry the real breakdown through instead of collapsing it to an integer.
|
|
479
|
+
let tennisSets;
|
|
480
|
+
let tennisGameScore = null;
|
|
481
|
+
let tennisServer = null;
|
|
482
|
+
let tennisCurrentSet = null;
|
|
483
|
+
if (game === 'tennis' && Array.isArray(r.sets) && r.sets.length) {
|
|
484
|
+
tennisSets = r.sets.map((s) => {
|
|
485
|
+
const sr = asRecord(s) ?? {};
|
|
486
|
+
return {
|
|
487
|
+
setNumber: numOrNull(sr.set_num),
|
|
488
|
+
player1Games: numOrNull(sr.player1_games),
|
|
489
|
+
player2Games: numOrNull(sr.player2_games),
|
|
490
|
+
tiebreak: sr.tiebreak == null ? null : (pickString(sr.tiebreak) ?? String(sr.tiebreak)),
|
|
491
|
+
completed: sr.is_completed === true,
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
tennisGameScore = pickString(r.game_score) ?? null;
|
|
495
|
+
tennisServer = typeof r.server === 'number' ? r.server : null;
|
|
496
|
+
tennisCurrentSet = typeof r.current_set === 'number' ? r.current_set : null;
|
|
497
|
+
}
|
|
459
498
|
return {
|
|
460
499
|
game,
|
|
461
500
|
matchId,
|
|
@@ -466,7 +505,9 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
466
505
|
team2,
|
|
467
506
|
event: eventName || eventId || eventSlug ? { id: eventId, slug: eventSlug, name: eventName } : null,
|
|
468
507
|
league: leagueName || leagueId || leagueSlug ? { id: leagueId, slug: leagueSlug, name: leagueName } : null,
|
|
469
|
-
|
|
508
|
+
// weightClass is UFC vocabulary; other games never set it, but never lie
|
|
509
|
+
// it into existence from a tennis/LoL row that merely shares a field name.
|
|
510
|
+
...(weightOrClass && game === 'ufc' ? { weightClass: weightOrClass } : {}),
|
|
470
511
|
...(r.titleBout != null ? { titleBout: Boolean(r.titleBout) } : {}),
|
|
471
512
|
...(hasCard
|
|
472
513
|
? {
|
|
@@ -481,16 +522,22 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
481
522
|
...(hasResultDetail
|
|
482
523
|
? {
|
|
483
524
|
result: {
|
|
484
|
-
|
|
525
|
+
// method/referee are combat-sport vocabulary with no tennis
|
|
526
|
+
// meaning; a tennis history row otherwise picks up a
|
|
527
|
+
// result.method:null / result.referee:null pair from this same
|
|
528
|
+
// object shape, which reads as "render a fight" to an agent.
|
|
529
|
+
...(game === 'ufc' ? { method: method ?? null, referee: referee ?? null } : {}),
|
|
485
530
|
methodRaw: methodRaw ?? null,
|
|
486
531
|
methodDetails: methodDetails ?? null,
|
|
487
532
|
round: resultRound,
|
|
488
533
|
time: resultTime ?? null,
|
|
489
|
-
referee: referee ?? null,
|
|
490
534
|
winnerSlug: winnerSlug ?? null,
|
|
491
535
|
},
|
|
492
536
|
}
|
|
493
537
|
: {}),
|
|
538
|
+
...(tennisSets
|
|
539
|
+
? { sets: tennisSets, gameScore: tennisGameScore, server: tennisServer, currentSet: tennisCurrentSet }
|
|
540
|
+
: {}),
|
|
494
541
|
...(isCancelled
|
|
495
542
|
? { cancelled: { isCancelled: true, reason: pickString(r.cancellationReason) ?? null } }
|
|
496
543
|
: {}),
|
package/dist/tools/resolve.js
CHANGED
|
@@ -584,6 +584,15 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
584
584
|
// while reporting total=1429 and hasMore=true. The UFC paths below fetch
|
|
585
585
|
// page 1 deliberately and re-rank in memory, so they page here instead.
|
|
586
586
|
let upstreamPaged = true;
|
|
587
|
+
// Set for a game whose dedicated search endpoint already returns results
|
|
588
|
+
// in the right order (e.g. tennis: /tennis/players/search ranks Carlos
|
|
589
|
+
// Alcaraz first). The generic re-rank below fuzzy-scores every candidate
|
|
590
|
+
// against the query string with no notion of who is actually ranked, so
|
|
591
|
+
// "alcaraz" tied four players at nearly the same score and resorted the
|
|
592
|
+
// world #3 to fourth — behind three players the upstream had already
|
|
593
|
+
// correctly placed below him. Preserving push order keeps the upstream's
|
|
594
|
+
// answer instead of a worse one computed from less information.
|
|
595
|
+
let preserveUpstreamOrder = false;
|
|
587
596
|
const pageQuery = (extra = {}) => ({
|
|
588
597
|
limit,
|
|
589
598
|
offset,
|
|
@@ -686,6 +695,11 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
686
695
|
const envelope = asRecord(res.data) ?? {};
|
|
687
696
|
const data = asRecord(envelope.data) ?? envelope;
|
|
688
697
|
items.push(...extractRows(data.items ?? data).map((r) => entityRef(r, 'player', game)));
|
|
698
|
+
// /tennis/players/search already ranks the best-known match first
|
|
699
|
+
// (it has current_rank to break ties this tool's fuzzy scorer
|
|
700
|
+
// cannot see); do not let the generic re-sort below undo that.
|
|
701
|
+
if (q)
|
|
702
|
+
preserveUpstreamOrder = true;
|
|
689
703
|
}
|
|
690
704
|
}
|
|
691
705
|
if (type === 'tournament' || type === 'event' || type === 'any') {
|
|
@@ -809,7 +823,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
809
823
|
seen.add(key);
|
|
810
824
|
return true;
|
|
811
825
|
});
|
|
812
|
-
if (q) {
|
|
826
|
+
if (q && !preserveUpstreamOrder) {
|
|
813
827
|
items.sort((a, b) => {
|
|
814
828
|
const sa = typeof a.meta?.score === 'number' ? a.meta.score : rankScore(q, a.name, a.id, a.slug, a.meta?.nickname);
|
|
815
829
|
const sb = typeof b.meta?.score === 'number' ? b.meta.score : rankScore(q, b.name, b.id, b.slug, b.meta?.nickname);
|
package/dist/tools/team.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
6
|
import { normalizeMatch } from './normalize.js';
|
|
7
|
-
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, stringSchema, } from './types.js';
|
|
7
|
+
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
8
8
|
function teamIdentity(game, raw, idHint, slugHint) {
|
|
9
9
|
const r = asRecord(raw) ?? {};
|
|
10
10
|
const nested = asRecord(r.data) ?? r;
|
|
@@ -46,7 +46,15 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
|
46
46
|
additionalProperties: false,
|
|
47
47
|
required: ['game'],
|
|
48
48
|
properties: {
|
|
49
|
-
|
|
49
|
+
// Tennis is deliberately absent: singles has players, not team rosters,
|
|
50
|
+
// and advertising it here only to throw NOT_IMPLEMENTED on every call is
|
|
51
|
+
// a trap — the schema told the caller the request was valid. player_profile
|
|
52
|
+
// and standings{game:"tennis"} cover the same ground honestly.
|
|
53
|
+
game: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
enum: PRIMARY_GAMES.filter((g) => g !== 'tennis'),
|
|
56
|
+
description: 'Game title: lol | cs2 | dota2 | cod | ufc. Tennis has players, not team rosters — use player_profile.',
|
|
57
|
+
},
|
|
50
58
|
teamId: stringSchema('Team id. Prefer for cs2. Pass teamId and/or slug (at least one required). Either works for dota2.'),
|
|
51
59
|
slug: stringSchema('Team/org slug. Prefer for lol and cod orgs. Pass teamId and/or slug (at least one required). Example: "t1".', 't1'),
|
|
52
60
|
recentLimit: limitSchema({ default: 10, max: 25 }),
|
|
@@ -903,6 +911,18 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
903
911
|
}
|
|
904
912
|
const envelope = asRecord(res.data) ?? {};
|
|
905
913
|
const h2h = asRecord(envelope.data) ?? envelope;
|
|
914
|
+
// The id-shaped shortcut above never resolves a name — sideA:"atp_207989"
|
|
915
|
+
// came in already looking like an id, so resolveTennisSide handed back
|
|
916
|
+
// {id: side, name: side} without ever asking who that id belongs to. The
|
|
917
|
+
// /tennis/h2h payload already states both names one level down (its own
|
|
918
|
+
// player1/player2, keyed by the same player1_id/player2_id this request
|
|
919
|
+
// just sent), so read from there rather than trusting the resolver's
|
|
920
|
+
// name at all: it is authoritative and always available once the H2H
|
|
921
|
+
// call itself succeeded.
|
|
922
|
+
const h2hP1 = asRecord(h2h.player1);
|
|
923
|
+
const h2hP2 = asRecord(h2h.player2);
|
|
924
|
+
const sideAName = pickString(h2hP1?.name) ?? tA.name;
|
|
925
|
+
const sideBName = pickString(h2hP2?.name) ?? tB.name;
|
|
906
926
|
return successEnvelope({
|
|
907
927
|
source: 'head_to_head',
|
|
908
928
|
game,
|
|
@@ -911,8 +931,8 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
911
931
|
upstreamCalls,
|
|
912
932
|
rateLimit,
|
|
913
933
|
data: {
|
|
914
|
-
sideA: { idOrSlug: tA.id, name:
|
|
915
|
-
sideB: { idOrSlug: tB.id, name:
|
|
934
|
+
sideA: { idOrSlug: tA.id, name: sideAName },
|
|
935
|
+
sideB: { idOrSlug: tB.id, name: sideBName },
|
|
916
936
|
h2h,
|
|
917
937
|
notes: ['Record and meetings come from the first-class /tennis/h2h endpoint'],
|
|
918
938
|
},
|
package/package.json
CHANGED