cito-mcp 0.6.0 → 0.6.1
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/client.js +8 -8
- package/dist/envelope.js +4 -4
- package/dist/install.js +15 -15
- package/dist/pacing.js +2 -2
- package/dist/scrub.js +58 -20
- package/dist/tools/combat.js +96 -96
- package/dist/tools/insight.js +36 -36
- package/dist/tools/leaderboard.js +4 -4
- package/dist/tools/live.js +41 -41
- package/dist/tools/match.js +25 -21
- package/dist/tools/meta.js +69 -69
- package/dist/tools/player.js +23 -23
- package/dist/tools/rankings.js +7 -7
- package/dist/tools/resolve.js +53 -53
- package/dist/tools/schedule.js +8 -8
- package/dist/tools/standings.js +23 -23
- package/dist/tools/team.js +88 -88
- package/dist/tools/tournaments.js +2 -2
- package/dist/tools/webhooks.js +2 -2
- package/package.json +51 -52
package/dist/tools/resolve.js
CHANGED
|
@@ -376,20 +376,20 @@ async function searchGame(ctx, game, q, type, limit) {
|
|
|
376
376
|
}
|
|
377
377
|
export const resolveEntity = {
|
|
378
378
|
name: 'resolve_entity',
|
|
379
|
-
description: `Natural-language / fuzzy query → best typed entity ID(s) + game (player, team, event, tournament, match, fighter).
|
|
380
|
-
|
|
381
|
-
When to use:
|
|
382
|
-
- User named an entity without an ID ("T1", "s1mple", "IEM Cologne", "Islam Makhachev")
|
|
383
|
-
- Need a canonical id/slug before profile or match tools
|
|
384
|
-
|
|
385
|
-
Prefer over search_entities when you want one best match (or small ranked set) to chain.
|
|
386
|
-
Prefer search_entities when browsing many results with pagination.
|
|
387
|
-
|
|
388
|
-
Do not use when: you already have a stable id/slug from a prior tool.
|
|
389
|
-
|
|
390
|
-
Empty/ambiguous results still return ok:true with best=null or needsDisambiguation=true — pick from candidates or refine q/game/type. Does not emit AMBIGUOUS_ENTITY as a hard error.
|
|
391
|
-
|
|
392
|
-
Parallel-safe: yes. API calls: 1–5.
|
|
379
|
+
description: `Natural-language / fuzzy query → best typed entity ID(s) + game (player, team, event, tournament, match, fighter).
|
|
380
|
+
|
|
381
|
+
When to use:
|
|
382
|
+
- User named an entity without an ID ("T1", "s1mple", "IEM Cologne", "Islam Makhachev")
|
|
383
|
+
- Need a canonical id/slug before profile or match tools
|
|
384
|
+
|
|
385
|
+
Prefer over search_entities when you want one best match (or small ranked set) to chain.
|
|
386
|
+
Prefer search_entities when browsing many results with pagination.
|
|
387
|
+
|
|
388
|
+
Do not use when: you already have a stable id/slug from a prior tool.
|
|
389
|
+
|
|
390
|
+
Empty/ambiguous results still return ok:true with best=null or needsDisambiguation=true — pick from candidates or refine q/game/type. Does not emit AMBIGUOUS_ENTITY as a hard error.
|
|
391
|
+
|
|
392
|
+
Parallel-safe: yes. API calls: 1–5.
|
|
393
393
|
Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
394
394
|
inputSchema: {
|
|
395
395
|
type: 'object',
|
|
@@ -441,11 +441,11 @@ Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
|
441
441
|
? [gameParse.game]
|
|
442
442
|
: [...PRIMARY_GAMES];
|
|
443
443
|
const partial = [];
|
|
444
|
-
let
|
|
444
|
+
let apiCalls = 0;
|
|
445
445
|
const all = [];
|
|
446
446
|
const results = await Promise.all(games.map((g) => searchGame(ctx, g, q, type, limit)));
|
|
447
447
|
for (const r of results) {
|
|
448
|
-
|
|
448
|
+
apiCalls += r.calls;
|
|
449
449
|
all.push(...r.candidates);
|
|
450
450
|
if (r.error)
|
|
451
451
|
partial.push(r.error);
|
|
@@ -469,7 +469,7 @@ Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
|
469
469
|
game: best?.game ?? gameParse.game ?? null,
|
|
470
470
|
requestId,
|
|
471
471
|
tookMs: Date.now() - started,
|
|
472
|
-
|
|
472
|
+
apiCalls,
|
|
473
473
|
partial: partial.length ? partial : undefined,
|
|
474
474
|
entities: best
|
|
475
475
|
? {
|
|
@@ -492,22 +492,22 @@ Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
|
492
492
|
};
|
|
493
493
|
export const searchEntities = {
|
|
494
494
|
name: 'search_entities',
|
|
495
|
-
description: `Browse/search teams, players, tournaments, events, fighters with type filter and pagination.
|
|
496
|
-
|
|
497
|
-
When to use:
|
|
498
|
-
- Typeahead / pickers
|
|
499
|
-
- "List teams matching…"
|
|
500
|
-
- Exploring entities without committing to one ID
|
|
501
|
-
- UFC fighter lookup by name/nickname (uses /ufc/search + client re-rank)
|
|
502
|
-
|
|
503
|
-
Prefer over resolve_entity when the user wants a list.
|
|
504
|
-
Prefer resolve_entity when chaining one name into a profile tool.
|
|
505
|
-
|
|
506
|
-
Do not use when: fetching a known entity profile — use team_profile or player_profile.
|
|
507
|
-
|
|
508
|
-
UFC: with q set, results are ranked (exact name > multi-token match > nickname). "Jon Jones" should return jon-jones first — never the generic P4P list.
|
|
509
|
-
|
|
510
|
-
Parallel-safe: yes. API calls: 1–3.
|
|
495
|
+
description: `Browse/search teams, players, tournaments, events, fighters with type filter and pagination.
|
|
496
|
+
|
|
497
|
+
When to use:
|
|
498
|
+
- Typeahead / pickers
|
|
499
|
+
- "List teams matching…"
|
|
500
|
+
- Exploring entities without committing to one ID
|
|
501
|
+
- UFC fighter lookup by name/nickname (uses /ufc/search + client re-rank)
|
|
502
|
+
|
|
503
|
+
Prefer over resolve_entity when the user wants a list.
|
|
504
|
+
Prefer resolve_entity when chaining one name into a profile tool.
|
|
505
|
+
|
|
506
|
+
Do not use when: fetching a known entity profile — use team_profile or player_profile.
|
|
507
|
+
|
|
508
|
+
UFC: with q set, results are ranked (exact name > multi-token match > nickname). "Jon Jones" should return jon-jones first — never the generic P4P list.
|
|
509
|
+
|
|
510
|
+
Parallel-safe: yes. API calls: 1–3.
|
|
511
511
|
Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
512
512
|
inputSchema: {
|
|
513
513
|
type: 'object',
|
|
@@ -549,10 +549,10 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
549
549
|
const offset = typeof cursor?.offset === 'number' ? cursor.offset : 0;
|
|
550
550
|
const activeOnly = args.activeOnly !== false;
|
|
551
551
|
let items = [];
|
|
552
|
-
let
|
|
552
|
+
let apiCalls = 0;
|
|
553
553
|
let total = null;
|
|
554
|
-
let
|
|
555
|
-
let
|
|
554
|
+
let apiPaged = true;
|
|
555
|
+
let preserveApiOrder = false;
|
|
556
556
|
const pageQuery = (extra = {}) => ({
|
|
557
557
|
limit,
|
|
558
558
|
offset,
|
|
@@ -563,7 +563,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
563
563
|
});
|
|
564
564
|
async function listPath(path, entityType, extra = {}) {
|
|
565
565
|
const res = await fetchJson(ctx, path, { query: pageQuery(extra) });
|
|
566
|
-
|
|
566
|
+
apiCalls += 1;
|
|
567
567
|
if (!res.ok) {
|
|
568
568
|
throw Object.assign(new Error(`HTTP ${res.status}`), {
|
|
569
569
|
status: res.status,
|
|
@@ -592,7 +592,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
592
592
|
else if (game === 'cs2') {
|
|
593
593
|
if (q && (type === 'any' || !['team', 'player', 'event'].includes(type))) {
|
|
594
594
|
const res = await fetchJson(ctx, '/cs2/search', { query: { q, limit } });
|
|
595
|
-
|
|
595
|
+
apiCalls += 1;
|
|
596
596
|
if (res.ok) {
|
|
597
597
|
const envelope = asRecord(res.data) ?? {};
|
|
598
598
|
const data = asRecord(envelope.data) ?? envelope;
|
|
@@ -625,7 +625,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
625
625
|
else if (game === 'dota2') {
|
|
626
626
|
if (q) {
|
|
627
627
|
const res = await fetchJson(ctx, '/dota2/search', { query: { q, limit } });
|
|
628
|
-
|
|
628
|
+
apiCalls += 1;
|
|
629
629
|
if (res.ok) {
|
|
630
630
|
const data = asRecord(res.data) ?? {};
|
|
631
631
|
if (type === 'team' || type === 'any')
|
|
@@ -648,18 +648,18 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
648
648
|
else if (game === 'tennis') {
|
|
649
649
|
if (type === 'player' || type === 'any') {
|
|
650
650
|
const res = await fetchJson(ctx, '/tennis/players/search', { query: { q, limit } });
|
|
651
|
-
|
|
651
|
+
apiCalls += 1;
|
|
652
652
|
if (res.ok) {
|
|
653
653
|
const envelope = asRecord(res.data) ?? {};
|
|
654
654
|
const data = asRecord(envelope.data) ?? envelope;
|
|
655
655
|
items.push(...extractRows(data.items ?? data).map((r) => entityRef(r, 'player', game)));
|
|
656
656
|
if (q)
|
|
657
|
-
|
|
657
|
+
preserveApiOrder = true;
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
if (type === 'tournament' || type === 'event' || type === 'any') {
|
|
661
661
|
const res = await fetchJson(ctx, '/tennis/competitions', { query: { q, page_size: limit } });
|
|
662
|
-
|
|
662
|
+
apiCalls += 1;
|
|
663
663
|
if (res.ok) {
|
|
664
664
|
const envelope = asRecord(res.data) ?? {};
|
|
665
665
|
const data = asRecord(envelope.data) ?? envelope;
|
|
@@ -669,11 +669,11 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
669
669
|
}
|
|
670
670
|
else if (game === 'cod') {
|
|
671
671
|
if (q) {
|
|
672
|
-
|
|
672
|
+
apiPaged = false;
|
|
673
673
|
const res = await fetchJson(ctx, '/cod/search', {
|
|
674
674
|
query: { q, limit, ...(type !== 'any' ? { type: type === 'team' ? 'org' : type } : {}) },
|
|
675
675
|
});
|
|
676
|
-
|
|
676
|
+
apiCalls += 1;
|
|
677
677
|
if (res.ok)
|
|
678
678
|
items.push(...extractRows(res.data).map((r) => entityRef(r, type === 'any' ? 'unknown' : type, game)));
|
|
679
679
|
}
|
|
@@ -688,9 +688,9 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
688
688
|
}
|
|
689
689
|
else if (game === 'ufc') {
|
|
690
690
|
if (q) {
|
|
691
|
-
|
|
691
|
+
apiPaged = false;
|
|
692
692
|
const res = await fetchJson(ctx, '/ufc/search', { query: { q } });
|
|
693
|
-
|
|
693
|
+
apiCalls += 1;
|
|
694
694
|
if (!res.ok) {
|
|
695
695
|
throw Object.assign(new Error(`HTTP ${res.status}`), {
|
|
696
696
|
status: res.status,
|
|
@@ -761,7 +761,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
761
761
|
source: 'search_entities',
|
|
762
762
|
requestId,
|
|
763
763
|
tookMs: Date.now() - started,
|
|
764
|
-
|
|
764
|
+
apiCalls,
|
|
765
765
|
httpStatus: e.status,
|
|
766
766
|
rateLimit: e.headers,
|
|
767
767
|
});
|
|
@@ -777,7 +777,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
777
777
|
const scoreOf = (it) => typeof it.meta?.score === 'number'
|
|
778
778
|
? it.meta.score
|
|
779
779
|
: rankScore(q, it.name, it.id, it.slug, it.meta?.nickname);
|
|
780
|
-
const
|
|
780
|
+
const unfilteredTotal = total;
|
|
781
781
|
let droppedBelowFloor = [];
|
|
782
782
|
const ENTITY_TYPES = new Set(['player', 'team', 'fighter']);
|
|
783
783
|
const protectedRow = q ? items.find((it) => ENTITY_TYPES.has(it.type)) ?? null : null;
|
|
@@ -803,15 +803,15 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
803
803
|
if (droppedBelowFloor.length)
|
|
804
804
|
items = items.filter(keep);
|
|
805
805
|
}
|
|
806
|
-
if (q && !
|
|
806
|
+
if (q && !preserveApiOrder) {
|
|
807
807
|
items.sort((a, b) => {
|
|
808
808
|
const am = a.meta?.matchedBy === 'top-hit' ? 1 : 0;
|
|
809
809
|
const bm = b.meta?.matchedBy === 'top-hit' ? 1 : 0;
|
|
810
810
|
return bm - am || scoreOf(b) - scoreOf(a) || a.name.localeCompare(b.name);
|
|
811
811
|
});
|
|
812
812
|
}
|
|
813
|
-
const pageItems =
|
|
814
|
-
const hasMore =
|
|
813
|
+
const pageItems = apiPaged ? items.slice(0, limit) : items.slice(offset, offset + limit);
|
|
814
|
+
const hasMore = apiPaged
|
|
815
815
|
? (total != null ? offset + pageItems.length < total : pageItems.length >= limit)
|
|
816
816
|
: offset + pageItems.length < items.length;
|
|
817
817
|
const floorWarnings = [];
|
|
@@ -829,7 +829,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
829
829
|
game,
|
|
830
830
|
requestId,
|
|
831
831
|
tookMs: Date.now() - started,
|
|
832
|
-
|
|
832
|
+
apiCalls,
|
|
833
833
|
warnings: floorWarnings.length ? floorWarnings : undefined,
|
|
834
834
|
pagination: {
|
|
835
835
|
limit,
|
|
@@ -849,7 +849,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
849
849
|
matched: items.length,
|
|
850
850
|
droppedBelowFloor: droppedBelowFloor.length,
|
|
851
851
|
dropped: droppedBelowFloor.slice(0, 25),
|
|
852
|
-
unfilteredTotal:
|
|
852
|
+
unfilteredTotal: unfilteredTotal ?? null,
|
|
853
853
|
},
|
|
854
854
|
}
|
|
855
855
|
: {}),
|
package/dist/tools/schedule.js
CHANGED
|
@@ -134,13 +134,13 @@ Parallel-safe: yes. API calls: 1 or 2.`,
|
|
|
134
134
|
const limit = clampInt(args.limit, 20, 1, 50);
|
|
135
135
|
const wantScheduled = includeRaw === 'both' || includeRaw === 'scheduled';
|
|
136
136
|
const wantCompleted = includeRaw === 'both' || includeRaw === 'completed';
|
|
137
|
-
const
|
|
137
|
+
const apiPage = 500;
|
|
138
138
|
const settled = await Promise.allSettled([
|
|
139
139
|
wantScheduled
|
|
140
|
-
? fetchJson(ctx, '/tennis/schedule', { query: { date, limit:
|
|
140
|
+
? fetchJson(ctx, '/tennis/schedule', { query: { date, limit: apiPage } })
|
|
141
141
|
: Promise.resolve(null),
|
|
142
142
|
wantCompleted
|
|
143
|
-
? fetchJson(ctx, '/tennis/matches/completed', { query: { date, limit:
|
|
143
|
+
? fetchJson(ctx, '/tennis/matches/completed', { query: { date, limit: apiPage } })
|
|
144
144
|
: Promise.resolve(null),
|
|
145
145
|
]);
|
|
146
146
|
const rejected = [];
|
|
@@ -208,7 +208,7 @@ Parallel-safe: yes. API calls: 1 or 2.`,
|
|
|
208
208
|
source: 'tennis_schedule',
|
|
209
209
|
requestId,
|
|
210
210
|
tookMs: Date.now() - started,
|
|
211
|
-
|
|
211
|
+
apiCalls: rejected.length,
|
|
212
212
|
recover: ['Check the date is a real calendar day', 'Retry, or widen include to both'],
|
|
213
213
|
});
|
|
214
214
|
}
|
|
@@ -255,14 +255,14 @@ Parallel-safe: yes. API calls: 1 or 2.`,
|
|
|
255
255
|
}
|
|
256
256
|
: {}),
|
|
257
257
|
};
|
|
258
|
-
const
|
|
258
|
+
const apiCalls = (wantScheduled ? 1 : 0) + (wantCompleted ? 1 : 0);
|
|
259
259
|
if (rejected.length > 0) {
|
|
260
260
|
return successEnvelope({
|
|
261
261
|
source: 'tennis_schedule',
|
|
262
262
|
game,
|
|
263
263
|
requestId,
|
|
264
264
|
tookMs: Date.now() - started,
|
|
265
|
-
|
|
265
|
+
apiCalls,
|
|
266
266
|
data,
|
|
267
267
|
partial: rejected.map((r) => partialFromRejection(r.section, {
|
|
268
268
|
code: r.code,
|
|
@@ -270,7 +270,7 @@ Parallel-safe: yes. API calls: 1 or 2.`,
|
|
|
270
270
|
...(r.httpStatus !== undefined ? { httpStatus: r.httpStatus } : {}),
|
|
271
271
|
})),
|
|
272
272
|
warnings: [
|
|
273
|
-
`${rejected.length} of ${
|
|
273
|
+
`${rejected.length} of ${apiCalls} schedule section(s) failed; the rest of the day is still returned.`,
|
|
274
274
|
],
|
|
275
275
|
});
|
|
276
276
|
}
|
|
@@ -279,7 +279,7 @@ Parallel-safe: yes. API calls: 1 or 2.`,
|
|
|
279
279
|
game,
|
|
280
280
|
requestId,
|
|
281
281
|
tookMs: Date.now() - started,
|
|
282
|
-
|
|
282
|
+
apiCalls,
|
|
283
283
|
data,
|
|
284
284
|
});
|
|
285
285
|
},
|
package/dist/tools/standings.js
CHANGED
|
@@ -271,7 +271,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
271
271
|
source: 'standings',
|
|
272
272
|
requestId,
|
|
273
273
|
tookMs: Date.now() - started,
|
|
274
|
-
|
|
274
|
+
apiCalls: 1,
|
|
275
275
|
httpStatus: res.status,
|
|
276
276
|
rateLimit: res.headers,
|
|
277
277
|
});
|
|
@@ -293,7 +293,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
293
293
|
source: 'standings',
|
|
294
294
|
requestId,
|
|
295
295
|
tookMs: Date.now() - started,
|
|
296
|
-
|
|
296
|
+
apiCalls: 1,
|
|
297
297
|
recover: [
|
|
298
298
|
'Use team_profile with radar for style/form',
|
|
299
299
|
'Use call_api if a standings path is added later',
|
|
@@ -305,7 +305,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
305
305
|
game,
|
|
306
306
|
requestId,
|
|
307
307
|
tookMs: Date.now() - started,
|
|
308
|
-
|
|
308
|
+
apiCalls: 1,
|
|
309
309
|
rateLimit: res.headers,
|
|
310
310
|
warnings: ['Dota standings composed from team worldRanking fields; sparse'],
|
|
311
311
|
data: {
|
|
@@ -327,7 +327,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
327
327
|
source: 'standings',
|
|
328
328
|
requestId,
|
|
329
329
|
tookMs: Date.now() - started,
|
|
330
|
-
|
|
330
|
+
apiCalls: 1,
|
|
331
331
|
httpStatus: res.status,
|
|
332
332
|
rateLimit: res.headers,
|
|
333
333
|
});
|
|
@@ -361,13 +361,13 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
361
361
|
});
|
|
362
362
|
}
|
|
363
363
|
const obj = asRecord(res.data);
|
|
364
|
-
const
|
|
365
|
-
const
|
|
364
|
+
const apiMeta = asRecord(obj?.meta) ?? {};
|
|
365
|
+
const unfilteredTotal = typeof obj?.total === 'number' ? obj.total : null;
|
|
366
366
|
const rankedPlayers = typeof obj?.ranked_players === 'number' ? obj.ranked_players : null;
|
|
367
367
|
const totalPages = typeof obj?.total_pages === 'number' ? obj.total_pages : null;
|
|
368
|
-
const
|
|
369
|
-
const effectiveTotal =
|
|
370
|
-
const effectiveHasMore =
|
|
368
|
+
const apiHasNext = typeof obj?.has_next === 'boolean' ? obj.has_next : null;
|
|
369
|
+
const effectiveTotal = unfilteredTotal ?? allRows.length;
|
|
370
|
+
const effectiveHasMore = apiHasNext ?? offset + rows.length < effectiveTotal;
|
|
371
371
|
const standingsPagination = {
|
|
372
372
|
limit,
|
|
373
373
|
offset,
|
|
@@ -376,7 +376,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
376
376
|
nextCursor: effectiveHasMore ? String(offset + rows.length) : null,
|
|
377
377
|
prevCursor: offset > 0 ? String(Math.max(0, offset - limit)) : null,
|
|
378
378
|
};
|
|
379
|
-
const dataQuality = asRecord(
|
|
379
|
+
const dataQuality = asRecord(apiMeta.dataQuality) ?? {};
|
|
380
380
|
const warnings = [];
|
|
381
381
|
const pushWarning = (value) => {
|
|
382
382
|
if (typeof value !== 'string')
|
|
@@ -385,23 +385,23 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
385
385
|
if (trimmed && !warnings.includes(trimmed))
|
|
386
386
|
warnings.push(trimmed);
|
|
387
387
|
};
|
|
388
|
-
pushWarning(
|
|
388
|
+
pushWarning(apiMeta.warning);
|
|
389
389
|
pushWarning(dataQuality.warning);
|
|
390
390
|
const tennisCoverage = game === 'tennis' ? asRecord(obj?.list_coverage) : null;
|
|
391
391
|
if (tennisCoverage && tennisCoverage.status === 'partial')
|
|
392
392
|
pushWarning(tennisCoverage.note);
|
|
393
|
-
if (Array.isArray(
|
|
394
|
-
for (const w of
|
|
393
|
+
if (Array.isArray(apiMeta.warnings)) {
|
|
394
|
+
for (const w of apiMeta.warnings)
|
|
395
395
|
pushWarning(w);
|
|
396
396
|
}
|
|
397
|
-
const dataFreshness = pickString(
|
|
398
|
-
const status = pickString(
|
|
397
|
+
const dataFreshness = pickString(apiMeta.dataFreshness, apiMeta.freshnessStatus, dataQuality.dataFreshness);
|
|
398
|
+
const status = pickString(apiMeta.status, dataQuality.status);
|
|
399
399
|
if (status === 'fallback') {
|
|
400
400
|
pushWarning('Standings served from last-known-good rankings (fallback).');
|
|
401
401
|
}
|
|
402
|
-
if (dataFreshness === 'cached' ||
|
|
403
|
-
pushWarning(typeof
|
|
404
|
-
? `Rankings dataFreshness=cached (ageHours=${
|
|
402
|
+
if (dataFreshness === 'cached' || apiMeta.stale === true) {
|
|
403
|
+
pushWarning(typeof apiMeta.dataAgeHours === 'number'
|
|
404
|
+
? `Rankings dataFreshness=cached (ageHours=${apiMeta.dataAgeHours}).`
|
|
405
405
|
: 'Rankings dataFreshness=cached or stale.');
|
|
406
406
|
}
|
|
407
407
|
return successEnvelope({
|
|
@@ -410,7 +410,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
410
410
|
game,
|
|
411
411
|
requestId,
|
|
412
412
|
tookMs: Date.now() - started,
|
|
413
|
-
|
|
413
|
+
apiCalls: 1,
|
|
414
414
|
rateLimit: res.headers,
|
|
415
415
|
warnings: warnings.length ? warnings : undefined,
|
|
416
416
|
data: {
|
|
@@ -432,16 +432,16 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
432
432
|
? {
|
|
433
433
|
rankingDate: pickString(obj?.ranking_date) ?? null,
|
|
434
434
|
listCoverage: tennisCoverage ? normalizeCoverage(tennisCoverage) : null,
|
|
435
|
-
updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated,
|
|
435
|
+
updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated, apiMeta.syncedAt, apiMeta.fetchedAt, obj?.ranking_date) ?? null,
|
|
436
436
|
}
|
|
437
437
|
: {
|
|
438
|
-
updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated,
|
|
438
|
+
updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated, apiMeta.syncedAt, apiMeta.fetchedAt) ??
|
|
439
439
|
null,
|
|
440
440
|
}),
|
|
441
441
|
...(dataFreshness ? { dataFreshness } : {}),
|
|
442
442
|
...(status ? { status } : {}),
|
|
443
|
-
...(
|
|
444
|
-
? { warning: pickString(
|
|
443
|
+
...(apiMeta.warning || dataQuality.warning
|
|
444
|
+
? { warning: pickString(apiMeta.warning, dataQuality.warning) }
|
|
445
445
|
: {}),
|
|
446
446
|
},
|
|
447
447
|
});
|