@rine-kit/pubg-domain 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -2
  3. package/dist/config/default-team.json +27 -0
  4. package/dist/src/config/chicken-index.d.ts +9 -0
  5. package/dist/src/config/chicken-index.js +37 -0
  6. package/dist/src/config/chicken-index.js.map +1 -0
  7. package/dist/src/config/team.d.ts +22 -0
  8. package/dist/src/config/team.js +69 -0
  9. package/dist/src/config/team.js.map +1 -0
  10. package/dist/src/context/context-store.d.ts +40 -0
  11. package/dist/src/context/context-store.js +119 -0
  12. package/dist/src/context/context-store.js.map +1 -0
  13. package/dist/src/context/team-context.d.ts +13 -0
  14. package/dist/src/context/team-context.js +16 -0
  15. package/dist/src/context/team-context.js.map +1 -0
  16. package/dist/src/core.d.ts +23 -0
  17. package/dist/src/core.js +24 -0
  18. package/dist/src/core.js.map +1 -0
  19. package/dist/src/data/model.d.ts +123 -0
  20. package/dist/src/data/model.js +100 -0
  21. package/dist/src/data/model.js.map +1 -0
  22. package/dist/src/data/pubg-api-client.d.ts +59 -0
  23. package/dist/src/data/pubg-api-client.js +441 -0
  24. package/dist/src/data/pubg-api-client.js.map +1 -0
  25. package/dist/src/engine/query-engine.d.ts +20 -0
  26. package/dist/src/engine/query-engine.js +765 -0
  27. package/dist/src/engine/query-engine.js.map +1 -0
  28. package/dist/src/index.d.ts +27 -0
  29. package/dist/src/index.js +28 -0
  30. package/dist/src/index.js.map +1 -0
  31. package/dist/src/migration/importer.d.ts +35 -0
  32. package/dist/src/migration/importer.js +229 -0
  33. package/dist/src/migration/importer.js.map +1 -0
  34. package/dist/src/review/combat-scope.d.ts +11 -0
  35. package/dist/src/review/combat-scope.js +101 -0
  36. package/dist/src/review/combat-scope.js.map +1 -0
  37. package/dist/src/review/event-combination-engine.d.ts +17 -0
  38. package/dist/src/review/event-combination-engine.js +534 -0
  39. package/dist/src/review/event-combination-engine.js.map +1 -0
  40. package/dist/src/review/fight-detector.d.ts +4 -0
  41. package/dist/src/review/fight-detector.js +138 -0
  42. package/dist/src/review/fight-detector.js.map +1 -0
  43. package/dist/src/review/fight-integrity-validator.d.ts +13 -0
  44. package/dist/src/review/fight-integrity-validator.js +134 -0
  45. package/dist/src/review/fight-integrity-validator.js.map +1 -0
  46. package/dist/src/review/fun-candidate-generator.d.ts +9 -0
  47. package/dist/src/review/fun-candidate-generator.js +267 -0
  48. package/dist/src/review/fun-candidate-generator.js.map +1 -0
  49. package/dist/src/review/fun-event-generator.d.ts +8 -0
  50. package/dist/src/review/fun-event-generator.js +740 -0
  51. package/dist/src/review/fun-event-generator.js.map +1 -0
  52. package/dist/src/review/fun-intelligence.d.ts +13 -0
  53. package/dist/src/review/fun-intelligence.js +20 -0
  54. package/dist/src/review/fun-intelligence.js.map +1 -0
  55. package/dist/src/review/fun-ranker.d.ts +13 -0
  56. package/dist/src/review/fun-ranker.js +68 -0
  57. package/dist/src/review/fun-ranker.js.map +1 -0
  58. package/dist/src/review/heavy-weapon-intelligence.d.ts +5 -0
  59. package/dist/src/review/heavy-weapon-intelligence.js +215 -0
  60. package/dist/src/review/heavy-weapon-intelligence.js.map +1 -0
  61. package/dist/src/review/key-operation-detector.d.ts +3 -0
  62. package/dist/src/review/key-operation-detector.js +126 -0
  63. package/dist/src/review/key-operation-detector.js.map +1 -0
  64. package/dist/src/review/match-selector.d.ts +18 -0
  65. package/dist/src/review/match-selector.js +202 -0
  66. package/dist/src/review/match-selector.js.map +1 -0
  67. package/dist/src/review/review-analyzer.d.ts +2 -0
  68. package/dist/src/review/review-analyzer.js +652 -0
  69. package/dist/src/review/review-analyzer.js.map +1 -0
  70. package/dist/src/review/review-facts.d.ts +17 -0
  71. package/dist/src/review/review-facts.js +560 -0
  72. package/dist/src/review/review-facts.js.map +1 -0
  73. package/dist/src/review/review-share.d.ts +48 -0
  74. package/dist/src/review/review-share.js +37 -0
  75. package/dist/src/review/review-share.js.map +1 -0
  76. package/dist/src/review/selection-store.d.ts +25 -0
  77. package/dist/src/review/selection-store.js +80 -0
  78. package/dist/src/review/selection-store.js.map +1 -0
  79. package/dist/src/review/session-review.d.ts +72 -0
  80. package/dist/src/review/session-review.js +492 -0
  81. package/dist/src/review/session-review.js.map +1 -0
  82. package/dist/src/review/special-events.d.ts +3 -0
  83. package/dist/src/review/special-events.js +258 -0
  84. package/dist/src/review/special-events.js.map +1 -0
  85. package/dist/src/review/subgraph.d.ts +26 -0
  86. package/dist/src/review/subgraph.js +146 -0
  87. package/dist/src/review/subgraph.js.map +1 -0
  88. package/dist/src/review/supplemental-intelligence.d.ts +15 -0
  89. package/dist/src/review/supplemental-intelligence.js +431 -0
  90. package/dist/src/review/supplemental-intelligence.js.map +1 -0
  91. package/dist/src/review/team-damage.d.ts +8 -0
  92. package/dist/src/review/team-damage.js +180 -0
  93. package/dist/src/review/team-damage.js.map +1 -0
  94. package/dist/src/review/telemetry-events.d.ts +72 -0
  95. package/dist/src/review/telemetry-events.js +573 -0
  96. package/dist/src/review/telemetry-events.js.map +1 -0
  97. package/dist/src/review/telemetry.d.ts +86 -0
  98. package/dist/src/review/telemetry.js +266 -0
  99. package/dist/src/review/telemetry.js.map +1 -0
  100. package/dist/src/review/types.d.ts +501 -0
  101. package/dist/src/review/types.js +2 -0
  102. package/dist/src/review/types.js.map +1 -0
  103. package/dist/src/review/vehicle-intelligence.d.ts +3 -0
  104. package/dist/src/review/vehicle-intelligence.js +148 -0
  105. package/dist/src/review/vehicle-intelligence.js.map +1 -0
  106. package/dist/src/schema/query.d.ts +876 -0
  107. package/dist/src/schema/query.js +185 -0
  108. package/dist/src/schema/query.js.map +1 -0
  109. package/dist/src/schema/status.d.ts +66 -0
  110. package/dist/src/schema/status.js +49 -0
  111. package/dist/src/schema/status.js.map +1 -0
  112. package/dist/src/service/pubg-domain-service.d.ts +192 -0
  113. package/dist/src/service/pubg-domain-service.js +1598 -0
  114. package/dist/src/service/pubg-domain-service.js.map +1 -0
  115. package/dist/src/storage/repository-port.d.ts +84 -0
  116. package/dist/src/storage/repository-port.js +2 -0
  117. package/dist/src/storage/repository-port.js.map +1 -0
  118. package/dist/src/storage/sqlite-repository-adapter.d.ts +33 -0
  119. package/dist/src/storage/sqlite-repository-adapter.js +66 -0
  120. package/dist/src/storage/sqlite-repository-adapter.js.map +1 -0
  121. package/dist/src/storage/sqlite-repository.d.ts +73 -0
  122. package/dist/src/storage/sqlite-repository.js +652 -0
  123. package/dist/src/storage/sqlite-repository.js.map +1 -0
  124. package/dist/src/time/selector-resolver.d.ts +34 -0
  125. package/dist/src/time/selector-resolver.js +364 -0
  126. package/dist/src/time/selector-resolver.js.map +1 -0
  127. package/package.json +42 -3
@@ -0,0 +1,765 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { parseTeamContext } from '../context/team-context.js';
3
+ import { CanonicalQuerySchema } from '../schema/query.js';
4
+ import { normalizeRecords, numberOr } from '../data/model.js';
5
+ import { CHICKEN_INDEX_WEIGHTS, normalizeChickenIndexWeights, } from '../config/chicken-index.js';
6
+ import { BUSINESS_DAY_START, DEFAULT_TIMEZONE, businessDayLabel, describeRange, resolveQuerySelectors, resolveSelector, } from '../time/selector-resolver.js';
7
+ function emptyAccumulator() {
8
+ return {
9
+ matchIds: new Set(),
10
+ kills: 0,
11
+ assists: 0,
12
+ damage: 0,
13
+ dbnos: 0,
14
+ revives: 0,
15
+ headshotKills: 0,
16
+ survivalTime: 0,
17
+ longestKill: 0,
18
+ deaths: 0,
19
+ unknownDeathCount: 0,
20
+ rankValues: [],
21
+ wins: 0,
22
+ top10: 0,
23
+ deathSemantics: new Set(),
24
+ };
25
+ }
26
+ function addPlayer(accumulator, match, player) {
27
+ accumulator.matchIds.add(match.matchId);
28
+ accumulator.kills += numberOr(player.kills);
29
+ accumulator.assists += numberOr(player.assists);
30
+ accumulator.damage += numberOr(player.damage);
31
+ accumulator.dbnos += numberOr(player.dbnos);
32
+ accumulator.revives += numberOr(player.revives);
33
+ accumulator.headshotKills += numberOr(player.headshotKills);
34
+ accumulator.survivalTime += numberOr(player.survivalTime);
35
+ accumulator.longestKill = Math.max(accumulator.longestKill, numberOr(player.longestKill));
36
+ if (typeof player.deaths === 'number' && Number.isFinite(player.deaths))
37
+ accumulator.deaths += Math.max(0, player.deaths);
38
+ else
39
+ accumulator.unknownDeathCount += 1;
40
+ if (player.deathSemantics)
41
+ accumulator.deathSemantics.add(player.deathSemantics);
42
+ if (player.rank !== null) {
43
+ accumulator.rankValues.push(player.rank);
44
+ if (player.rank === 1)
45
+ accumulator.wins += 1;
46
+ if (player.rank <= 10)
47
+ accumulator.top10 += 1;
48
+ }
49
+ }
50
+ function round(value, digits = 2) {
51
+ if (value === null || !Number.isFinite(value))
52
+ return value;
53
+ const scale = 10 ** digits;
54
+ return Math.round(value * scale) / scale;
55
+ }
56
+ function metricNullableNumber(row, metric) {
57
+ if (!row)
58
+ return null;
59
+ const value = row.metrics[metric];
60
+ if (typeof value === 'number')
61
+ return Number.isFinite(value) ? value : null;
62
+ if (typeof value === 'string' && value.trim()) {
63
+ const parsed = Number(value);
64
+ return Number.isFinite(parsed) ? parsed : null;
65
+ }
66
+ return null;
67
+ }
68
+ function ratio(numerator, denominator) {
69
+ if (numerator === null || denominator === null || denominator === 0)
70
+ return null;
71
+ const value = numerator / denominator;
72
+ return Number.isFinite(value) ? round(value) : null;
73
+ }
74
+ function metricValue(accumulator, metric) {
75
+ const matches = accumulator.matchIds.size;
76
+ const avgRank = accumulator.rankValues.length
77
+ ? accumulator.rankValues.reduce((sum, value) => sum + value, 0) / accumulator.rankValues.length
78
+ : null;
79
+ const kd = accumulator.unknownDeathCount > 0
80
+ ? null
81
+ : accumulator.deaths > 0
82
+ ? accumulator.kills / accumulator.deaths
83
+ : null;
84
+ switch (metric) {
85
+ case 'matches':
86
+ return matches;
87
+ case 'kills':
88
+ return accumulator.kills;
89
+ case 'assists':
90
+ return accumulator.assists;
91
+ case 'damage':
92
+ return accumulator.damage;
93
+ case 'avg_damage':
94
+ return matches ? accumulator.damage / matches : 0;
95
+ case 'kd':
96
+ return kd;
97
+ case 'deaths':
98
+ return accumulator.unknownDeathCount > 0 ? null : accumulator.deaths;
99
+ case 'wins':
100
+ return accumulator.wins;
101
+ case 'top10':
102
+ return accumulator.top10;
103
+ case 'rank':
104
+ return avgRank === null ? null : avgRank;
105
+ case 'dbnos':
106
+ return accumulator.dbnos;
107
+ case 'revives':
108
+ return accumulator.revives;
109
+ case 'headshot_kills':
110
+ return accumulator.headshotKills;
111
+ case 'survival_time':
112
+ return accumulator.survivalTime;
113
+ case 'longest_kill':
114
+ return accumulator.longestKill;
115
+ case 'performance_score':
116
+ return null;
117
+ case 'chicken_index':
118
+ return null;
119
+ }
120
+ }
121
+ function playerIdsForSubject(query, team) {
122
+ const ids = query.subject.ids.map(String);
123
+ if (query.subject.type === 'team' && (ids.length === 0 || ids.includes(team.id)))
124
+ return team.players.map((player) => player.id);
125
+ return ids;
126
+ }
127
+ function aliasesForTeam(team) {
128
+ const result = new Map();
129
+ for (const player of team.players) {
130
+ result.set(player.id, player.name);
131
+ result.set(player.name, player.name);
132
+ }
133
+ return result;
134
+ }
135
+ function createRow(key, label, groupBy, accumulator, metadata = {}) {
136
+ const metrics = {};
137
+ const metricNames = [
138
+ 'matches',
139
+ 'kills',
140
+ 'assists',
141
+ 'damage',
142
+ 'avg_damage',
143
+ 'kd',
144
+ 'deaths',
145
+ 'wins',
146
+ 'top10',
147
+ 'rank',
148
+ 'dbnos',
149
+ 'revives',
150
+ 'headshot_kills',
151
+ 'survival_time',
152
+ 'longest_kill',
153
+ ];
154
+ for (const metric of metricNames)
155
+ metrics[metric] = metricValue(accumulator, metric);
156
+ metrics.avg_kills = accumulator.matchIds.size ? accumulator.kills / accumulator.matchIds.size : 0;
157
+ metrics.avg_rank = accumulator.rankValues.length
158
+ ? accumulator.rankValues.reduce((sum, value) => sum + value, 0) / accumulator.rankValues.length
159
+ : null;
160
+ metrics.top10_rate = accumulator.matchIds.size
161
+ ? accumulator.top10 / accumulator.matchIds.size
162
+ : 0;
163
+ metrics.denominatorZero =
164
+ accumulator.matchIds.size === 0 || accumulator.unknownDeathCount > 0
165
+ ? null
166
+ : accumulator.deaths === 0
167
+ ? 1
168
+ : 0;
169
+ return {
170
+ key,
171
+ label,
172
+ groupBy,
173
+ metrics,
174
+ activityStatus: accumulator.matchIds.size ? 'ACTIVE' : 'NO_ACTIVITY',
175
+ bestRank: accumulator.rankValues.length ? Math.min(...accumulator.rankValues) : null,
176
+ ...metadata,
177
+ };
178
+ }
179
+ function selectedPlayers(match, ids) {
180
+ const byId = new Map();
181
+ for (const player of match.players)
182
+ if (ids.has(player.accountId))
183
+ byId.set(player.accountId, player);
184
+ return [...byId.values()];
185
+ }
186
+ function selectBySelector(records, selector, subjectIds, options) {
187
+ if (selector.type === 'result_set') {
188
+ const ids = new Set(options.resultSetMatchIds ?? []);
189
+ return records.filter((record) => ids.has(record.matchId));
190
+ }
191
+ const hasSubject = (record) => selectedPlayers(record, subjectIds).length > 0;
192
+ const competitive = records.filter((record) => record.isCompetitive !== false && hasSubject(record));
193
+ if (selector.type === 'last_n_matches') {
194
+ const ordered = [...competitive].sort((left, right) => right.timestamp - left.timestamp || right.matchId.localeCompare(left.matchId));
195
+ return ordered.slice(selector.offset, selector.offset + selector.count);
196
+ }
197
+ const resolved = resolveSelector(selector, options);
198
+ const start = Date.parse(resolved.start);
199
+ const end = Date.parse(resolved.end);
200
+ return competitive.filter((record) => record.timestamp >= start && record.timestamp < end);
201
+ }
202
+ function aggregatePlayerRows(records, ids, team) {
203
+ const buckets = new Map();
204
+ for (const id of ids)
205
+ buckets.set(id, emptyAccumulator());
206
+ const idSet = new Set(ids);
207
+ for (const match of records) {
208
+ for (const player of selectedPlayers(match, idSet)) {
209
+ const accumulator = buckets.get(player.accountId);
210
+ if (accumulator)
211
+ addPlayer(accumulator, match, player);
212
+ }
213
+ }
214
+ const aliasMap = aliasesForTeam(team);
215
+ const rows = [];
216
+ for (const id of ids)
217
+ rows.push(createRow(id, aliasMap.get(id) ?? id, 'player', buckets.get(id) ?? emptyAccumulator()));
218
+ return rows;
219
+ }
220
+ function aggregateTeamRow(records, ids, team) {
221
+ const accumulator = emptyAccumulator();
222
+ const seenMatches = new Set();
223
+ for (const match of records) {
224
+ const players = selectedPlayers(match, ids);
225
+ if (!players.length || seenMatches.has(match.matchId))
226
+ continue;
227
+ seenMatches.add(match.matchId);
228
+ for (const player of players)
229
+ addPlayer(accumulator, match, player);
230
+ }
231
+ const row = createRow(team.id, team.label, 'team', accumulator);
232
+ const teamRanks = records.flatMap((match) => selectedPlayers(match, ids)
233
+ .map((player) => player.rank)
234
+ .filter((rank) => rank !== null));
235
+ const bestRankByMatch = new Map();
236
+ for (const match of records) {
237
+ const ranks = selectedPlayers(match, ids)
238
+ .map((player) => player.rank)
239
+ .filter((rank) => rank !== null);
240
+ if (ranks.length)
241
+ bestRankByMatch.set(match.matchId, Math.min(...ranks));
242
+ }
243
+ const teamWins = [...bestRankByMatch.values()].filter((rank) => rank === 1).length;
244
+ const teamTop10 = [...bestRankByMatch.values()].filter((rank) => rank <= 10).length;
245
+ row.metrics.wins = teamWins;
246
+ row.metrics.top10 = teamTop10;
247
+ row.metrics.rank = teamRanks.length
248
+ ? round(teamRanks.reduce((sum, value) => sum + value, 0) / teamRanks.length)
249
+ : null;
250
+ row.metrics.avg_damage = accumulator.matchIds.size
251
+ ? round(accumulator.damage / accumulator.matchIds.size)
252
+ : 0;
253
+ row.metrics.teamCombinedKD =
254
+ accumulator.unknownDeathCount > 0 || accumulator.deaths === 0
255
+ ? null
256
+ : round(accumulator.kills / accumulator.deaths);
257
+ row.metrics.deathSemantics = [...accumulator.deathSemantics].join(',') || 'unknown';
258
+ return row;
259
+ }
260
+ function aggregateMatchRows(records, ids) {
261
+ return records.map((match) => {
262
+ const players = selectedPlayers(match, ids);
263
+ const accumulator = emptyAccumulator();
264
+ for (const player of players)
265
+ addPlayer(accumulator, match, player);
266
+ const row = createRow(match.matchId, match.matchId, 'match', accumulator, {
267
+ matchId: match.matchId,
268
+ timestamp: match.timestamp,
269
+ createdAt: match.createdAt,
270
+ mapName: match.mapName,
271
+ gameMode: match.gameMode,
272
+ duration: match.duration,
273
+ players,
274
+ });
275
+ row.metrics.rank =
276
+ players
277
+ .map((player) => player.rank)
278
+ .filter((rank) => rank !== null)
279
+ .sort((left, right) => left - right)[0] ?? null;
280
+ row.metrics.wins = row.metrics.rank === 1 ? 1 : 0;
281
+ row.metrics.top10 = typeof row.metrics.rank === 'number' && row.metrics.rank <= 10 ? 1 : 0;
282
+ row.metrics.avg_damage = row.metrics.damage ?? 0;
283
+ return row;
284
+ });
285
+ }
286
+ function aggregateDayRows(records, ids, team, timezone, businessDayStart) {
287
+ const groups = new Map();
288
+ for (const record of records) {
289
+ const label = businessDayLabel(record.timestamp, timezone, businessDayStart);
290
+ const bucket = groups.get(label) ?? [];
291
+ bucket.push(record);
292
+ groups.set(label, bucket);
293
+ }
294
+ return [...groups.entries()]
295
+ .sort(([left], [right]) => left.localeCompare(right))
296
+ .map(([label, matches]) => {
297
+ const row = aggregateTeamRow(matches, ids, team);
298
+ return { ...row, key: label, label, groupBy: 'day' };
299
+ });
300
+ }
301
+ function aggregateCategoryRows(records, ids, team, groupBy) {
302
+ const groups = new Map();
303
+ for (const record of records) {
304
+ const label = groupBy === 'map' ? record.mapName : record.gameMode;
305
+ const bucket = groups.get(label) ?? [];
306
+ bucket.push(record);
307
+ groups.set(label, bucket);
308
+ }
309
+ return [...groups.entries()]
310
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
311
+ .map(([label, matches]) => {
312
+ const row = aggregateTeamRow(matches, ids, team);
313
+ return { ...row, key: label, label, groupBy };
314
+ });
315
+ }
316
+ function normalizeComponent(value, values, higherIsBetter) {
317
+ const finiteValues = values.filter((item) => Number.isFinite(item));
318
+ if (!finiteValues.length)
319
+ return 0;
320
+ const min = Math.min(...finiteValues);
321
+ const max = Math.max(...finiteValues);
322
+ if (max === min)
323
+ return 100;
324
+ const normalized = higherIsBetter ? (value - min) / (max - min) : (max - value) / (max - min);
325
+ return Math.max(0, Math.min(100, normalized * 100));
326
+ }
327
+ function applyChickenIndex(rows, override = {}) {
328
+ const weights = normalizeChickenIndexWeights(override);
329
+ const eligible = rows.filter((row) => row.activityStatus !== 'NO_ACTIVITY');
330
+ const knownValues = (metric) => eligible.flatMap((row) => {
331
+ const value = metricNullableNumber(row, metric);
332
+ return value === null ? [] : [value];
333
+ });
334
+ const rateValues = (metric) => eligible.flatMap((row) => {
335
+ const value = metricNullableNumber(row, metric);
336
+ const matches = metricNullableNumber(row, 'matches');
337
+ return value === null || matches === null || matches <= 0 ? [] : [value / matches];
338
+ });
339
+ const kdValues = knownValues('kd');
340
+ const damageValues = knownValues('avg_damage');
341
+ const killsValues = rateValues('kills');
342
+ const rankValues = knownValues('rank');
343
+ const top10Values = rateValues('top10');
344
+ for (const row of rows) {
345
+ if (row.activityStatus === 'NO_ACTIVITY') {
346
+ row.metrics.performance_score = null;
347
+ row.metrics.chicken_index = null;
348
+ continue;
349
+ }
350
+ const kd = metricNullableNumber(row, 'kd');
351
+ const avgDamage = metricNullableNumber(row, 'avg_damage');
352
+ const kills = metricNullableNumber(row, 'kills');
353
+ const matches = metricNullableNumber(row, 'matches');
354
+ const avgRank = metricNullableNumber(row, 'rank');
355
+ const top10 = metricNullableNumber(row, 'top10');
356
+ if (kd === null ||
357
+ avgDamage === null ||
358
+ kills === null ||
359
+ matches === null ||
360
+ matches <= 0 ||
361
+ avgRank === null ||
362
+ top10 === null) {
363
+ row.metrics.performance_score = null;
364
+ row.metrics.chicken_index = null;
365
+ continue;
366
+ }
367
+ const avgKills = kills / matches;
368
+ const top10Rate = top10 / matches;
369
+ const score = normalizeComponent(kd, kdValues, true) * weights.kd +
370
+ normalizeComponent(avgDamage, damageValues, true) * weights.avgDamage +
371
+ normalizeComponent(avgKills, killsValues, true) * weights.avgKills +
372
+ normalizeComponent(avgRank, rankValues, false) * weights.placement +
373
+ normalizeComponent(top10Rate, top10Values, true) * weights.top10Rate;
374
+ row.metrics.performance_score = round(score);
375
+ row.metrics.chicken_index = round(Math.max(0, Math.min(100, 100 - score)));
376
+ }
377
+ }
378
+ function sortRows(rows, metric, direction) {
379
+ return [...rows].sort((left, right) => {
380
+ const leftValue = metricNullableNumber(left, metric);
381
+ const rightValue = metricNullableNumber(right, metric);
382
+ if (leftValue === null || rightValue === null) {
383
+ if (leftValue === null && rightValue === null)
384
+ return left.label.localeCompare(right.label);
385
+ return leftValue === null ? 1 : -1;
386
+ }
387
+ if (leftValue !== rightValue)
388
+ return direction === 'desc' ? rightValue - leftValue : leftValue - rightValue;
389
+ return left.label.localeCompare(right.label);
390
+ });
391
+ }
392
+ function positionRows(rows, metric) {
393
+ let previous = null;
394
+ let position = 0;
395
+ return rows.map((row, index) => {
396
+ const value = metricNullableNumber(row, metric);
397
+ if (value === null)
398
+ return { ...row, tied: false };
399
+ if (previous === null || value !== previous)
400
+ position = index + 1;
401
+ previous = value;
402
+ return {
403
+ ...row,
404
+ position,
405
+ tied: index > 0 && value === metricNullableNumber(rows[index - 1], metric),
406
+ };
407
+ });
408
+ }
409
+ function highlights(rows) {
410
+ const eligible = rows.filter((row) => row.activityStatus !== 'NO_ACTIVITY');
411
+ const result = {};
412
+ const highest = (metric) => {
413
+ const known = eligible.flatMap((row) => {
414
+ const value = metricNullableNumber(row, metric);
415
+ return value === null ? [] : [value];
416
+ });
417
+ if (!known.length)
418
+ return [];
419
+ const max = Math.max(...known);
420
+ return eligible.filter((row) => metricNullableNumber(row, metric) === max);
421
+ };
422
+ result.kd = highest('kd');
423
+ result.kills = highest('kills');
424
+ result.damage = highest('damage');
425
+ result.dbnos = highest('dbnos');
426
+ result.revives = highest('revives');
427
+ result.longest_kill = highest('longest_kill');
428
+ const chickenValues = eligible.flatMap((row) => {
429
+ const value = metricNullableNumber(row, 'chicken_index');
430
+ return value === null ? [] : [value];
431
+ });
432
+ const maxChicken = chickenValues.length ? Math.max(...chickenValues) : null;
433
+ result.chicken_index =
434
+ maxChicken === null
435
+ ? []
436
+ : eligible.filter((row) => metricNullableNumber(row, 'chicken_index') === maxChicken);
437
+ return result;
438
+ }
439
+ function selectorTimeOptions(selector, options) {
440
+ if (selector.type === 'time_range')
441
+ return { timezone: selector.timezone, businessDayStart: selector.businessDayStart };
442
+ return {
443
+ timezone: options.timezone ?? DEFAULT_TIMEZONE,
444
+ businessDayStart: options.businessDayStart ?? BUSINESS_DAY_START,
445
+ };
446
+ }
447
+ function statusFor(coverage, selectedCount) {
448
+ if (coverage.status === 'SOURCE_UNAVAILABLE') {
449
+ if (coverage.queryCovered === true ||
450
+ (coverage.queryCovered === undefined && (coverage.complete || selectedCount > 0)))
451
+ return 'STALE';
452
+ return 'SOURCE_UNAVAILABLE';
453
+ }
454
+ if (coverage.status === 'PARTIAL')
455
+ return 'PARTIAL';
456
+ if (coverage.status === 'COVERAGE_GAP')
457
+ return 'COVERAGE_GAP';
458
+ if (coverage.status === 'STALE')
459
+ return 'STALE';
460
+ return coverage.complete ? 'OK' : 'COVERAGE_GAP';
461
+ }
462
+ function evidence(records, _query) {
463
+ const fields = new Set([
464
+ 'matchId',
465
+ 'timestamp',
466
+ 'players.kills',
467
+ 'players.assists',
468
+ 'players.damage',
469
+ 'players.deaths',
470
+ 'players.rank',
471
+ 'players.dbnos',
472
+ 'players.revives',
473
+ 'players.headshotKills',
474
+ 'players.survivalTime',
475
+ 'players.longestKill',
476
+ ]);
477
+ return {
478
+ matchIds: records.map((record) => record.matchId),
479
+ playerIds: [
480
+ ...new Set(records.flatMap((record) => record.players.map((player) => player.accountId))),
481
+ ],
482
+ fields: [...fields],
483
+ calculation: 'deterministic_query_engine_v3',
484
+ };
485
+ }
486
+ export class DeterministicQueryEngine {
487
+ defaults;
488
+ constructor(defaults = {}) {
489
+ this.defaults = defaults;
490
+ }
491
+ execute(input, sourceRecords, coverage, source, options = {}) {
492
+ const parsed = CanonicalQuerySchema.safeParse(input);
493
+ const queryId = typeof input === 'object' && input !== null && 'queryId' in input
494
+ ? String(input.queryId ?? '')
495
+ : 'unknown-query';
496
+ const sessionId = typeof input === 'object' && input !== null && 'reference' in input
497
+ ? String(input.reference?.sessionId ??
498
+ 'unknown-session')
499
+ : 'unknown-session';
500
+ if (!parsed.success)
501
+ return this.invalid(queryId, sessionId, 'INVALID_QUERY', parsed.error.issues.map((issue) => issue.message), coverage, source);
502
+ const query = parsed.data;
503
+ if (query.reference.unsupportedCapability)
504
+ return this.invalid(query.queryId, sessionId, 'UNSUPPORTED_CAPABILITY', [query.reference.unsupportedCapability], coverage, source);
505
+ const mergedOptions = { ...this.defaults, ...options };
506
+ const resolved = resolveQuerySelectors(query, mergedOptions);
507
+ if (!mergedOptions.teamContext)
508
+ return this.invalid(query.queryId, sessionId, 'INVALID_QUERY', ['TEAM_CONTEXT_REQUIRED'], coverage, source);
509
+ let teamContext;
510
+ try {
511
+ teamContext = parseTeamContext(mergedOptions.teamContext);
512
+ }
513
+ catch {
514
+ return this.invalid(query.queryId, sessionId, 'INVALID_QUERY', ['TEAM_CONTEXT_INVALID'], coverage, source);
515
+ }
516
+ const team = teamContext.team;
517
+ const ids = playerIdsForSubject(resolved, team);
518
+ const knownIds = new Set(team.players.map((player) => player.id));
519
+ const unknown = resolved.subject.type !== 'team' ? ids.filter((id) => !knownIds.has(id)) : [];
520
+ if (unknown.length)
521
+ return this.invalid(query.queryId, sessionId, 'UNKNOWN_PLAYER', unknown, coverage, source);
522
+ const records = normalizeRecords(sourceRecords);
523
+ const subjectIds = new Set(ids);
524
+ const querySessionId = String(resolved.reference.sessionId ?? sessionId);
525
+ const selected = resolved.operation === 'compare'
526
+ ? normalizeRecords(resolved.segments.flatMap((segment) => selectBySelector(records, segment.selector, subjectIds, mergedOptions)))
527
+ : selectBySelector(records, resolved.selector, subjectIds, mergedOptions);
528
+ const baseStatus = statusFor(coverage, selected.length);
529
+ if (resolved.operation === 'compare')
530
+ return this.executeCompare(resolved, records, coverage, source, team, subjectIds, mergedOptions, baseStatus, querySessionId);
531
+ if (resolved.selector.type === 'result_set' && !mergedOptions.resultSetMatchIds)
532
+ return this.invalid(resolved.queryId, querySessionId, 'INVALID_QUERY', ['RESULT_SET_NOT_FOUND'], coverage, source);
533
+ const selectorTime = selectorTimeOptions(resolved.selector, mergedOptions);
534
+ const data = this.executeSingle(resolved, selected, subjectIds, ids, team, selectorTime.timezone, selectorTime.businessDayStart, mergedOptions.chickenIndexWeights ?? CHICKEN_INDEX_WEIGHTS);
535
+ const status = selected.length === 0 && coverage.complete && baseStatus === 'OK' ? 'NO_MATCHES' : baseStatus;
536
+ const result = {
537
+ queryId: resolved.queryId,
538
+ sessionId: querySessionId,
539
+ status,
540
+ data,
541
+ coverage,
542
+ source,
543
+ evidence: evidence(selected, resolved),
544
+ diagnostics: {
545
+ selectedMatchCount: selected.length,
546
+ resolvedSelector: resolved.selector,
547
+ selectorDescription: describeRange(resolved.selector),
548
+ calculation: 'deterministic_query_engine_v3',
549
+ },
550
+ };
551
+ return result;
552
+ }
553
+ executeSingle(query, selected, subjectIds, playerIds, team, timezone, businessDayStart, chickenIndexWeights = CHICKEN_INDEX_WEIGHTS) {
554
+ let rows;
555
+ if (query.groupBy === 'player')
556
+ rows = aggregatePlayerRows(selected, playerIds, team);
557
+ else if (query.groupBy === 'match')
558
+ rows = aggregateMatchRows(selected, subjectIds);
559
+ else if (query.groupBy === 'day')
560
+ rows = aggregateDayRows(selected, subjectIds, team, timezone, businessDayStart);
561
+ else if (query.groupBy === 'map' || query.groupBy === 'mode')
562
+ rows = aggregateCategoryRows(selected, subjectIds, team, query.groupBy);
563
+ else
564
+ rows = [aggregateTeamRow(selected, subjectIds, team)];
565
+ if (query.groupBy !== 'match') {
566
+ applyChickenIndex(rows, chickenIndexWeights);
567
+ }
568
+ const orderMetric = query.operation === 'strongest'
569
+ ? 'performance_score'
570
+ : query.operation === 'weakest'
571
+ ? 'chicken_index'
572
+ : query.orderBy.metric;
573
+ const orderDirection = query.operation === 'weakest'
574
+ ? 'desc'
575
+ : query.operation === 'strongest'
576
+ ? 'desc'
577
+ : query.orderBy.direction;
578
+ let outputRows = rows;
579
+ if (query.operation === 'rank' ||
580
+ query.operation === 'strongest' ||
581
+ query.operation === 'weakest' ||
582
+ query.operation === 'report') {
583
+ outputRows = sortRows(rows, orderMetric, orderDirection);
584
+ outputRows = positionRows(outputRows, orderMetric);
585
+ if (query.operation !== 'report' && query.limit)
586
+ outputRows = outputRows
587
+ .filter((row) => row.activityStatus !== 'NO_ACTIVITY')
588
+ .slice(0, query.limit);
589
+ }
590
+ if (query.operation === 'list')
591
+ outputRows = rows.slice(0, query.limit ?? 20);
592
+ if (query.operation === 'detail' && query.limit)
593
+ outputRows = outputRows.slice(0, query.limit);
594
+ const teamRow = query.groupBy === 'team' ? outputRows[0] : aggregateTeamRow(selected, subjectIds, team);
595
+ const result = {
596
+ operation: query.operation,
597
+ groupBy: query.groupBy,
598
+ ...(query.operation === 'rank' ||
599
+ query.operation === 'strongest' ||
600
+ query.operation === 'weakest'
601
+ ? { metric: orderMetric, direction: orderDirection }
602
+ : {}),
603
+ rows: outputRows,
604
+ summary: {
605
+ periodLabel: query.selector.label ?? describeRange(query.selector),
606
+ uniqueMatchCount: new Set(selected.map((record) => record.matchId)).size,
607
+ team: teamRow?.metrics ?? {},
608
+ },
609
+ };
610
+ if (query.operation === 'report')
611
+ result.highlights = highlights(rows);
612
+ if (query.operation === 'trend') {
613
+ const ordered = [...rows].sort((left, right) => (left.label ?? '').localeCompare(right.label ?? ''));
614
+ const first = ordered[0];
615
+ const last = ordered[ordered.length - 1];
616
+ const metricNames = ['kd', 'avg_damage', 'kills'];
617
+ const changes = {};
618
+ for (const metric of metricNames) {
619
+ const from = first ? metricNullableNumber(first, metric) : null;
620
+ const to = last ? metricNullableNumber(last, metric) : null;
621
+ const delta = from === null || to === null ? null : round(to - from);
622
+ changes[metric] = { from: round(from), to: round(to), delta };
623
+ }
624
+ const positive = [changes.kd?.delta, changes.avg_damage?.delta, changes.kills?.delta].some((value) => typeof value === 'number' && value > 0);
625
+ const negative = [changes.kd?.delta, changes.avg_damage?.delta].every((value) => typeof value === 'number' && value < 0);
626
+ result.dailySeries = ordered;
627
+ result.change = {
628
+ direction: positive && !negative ? 'up' : negative ? 'down' : 'stable',
629
+ metrics: changes,
630
+ };
631
+ }
632
+ return result;
633
+ }
634
+ executeCompare(query, records, coverage, source, team, ids, options, baseStatus, sessionId) {
635
+ const segmentData = query.segments.map((segment) => {
636
+ const selected = selectBySelector(records, segment.selector, ids, options);
637
+ const segmentQuery = {
638
+ ...query,
639
+ operation: 'report',
640
+ selector: segment.selector,
641
+ segments: [],
642
+ };
643
+ const segmentTime = selectorTimeOptions(segment.selector, options);
644
+ const data = this.executeSingle(segmentQuery, selected, ids, [...ids], team, segmentTime.timezone, segmentTime.businessDayStart, options.chickenIndexWeights ?? CHICKEN_INDEX_WEIGHTS);
645
+ return {
646
+ label: segment.label,
647
+ selector: segment.selector,
648
+ rows: data.rows,
649
+ summary: data.summary,
650
+ };
651
+ });
652
+ const first = segmentData[0];
653
+ const second = segmentData[1];
654
+ const deltaRows = [];
655
+ if (first && second) {
656
+ const secondByKey = new Map(second.rows.map((row) => [row.key, row]));
657
+ const keys = new Set([
658
+ ...first.rows.map((row) => row.key),
659
+ ...second.rows.map((row) => row.key),
660
+ ]);
661
+ for (const key of keys) {
662
+ const firstRow = first.rows.find((row) => row.key === key);
663
+ const secondRow = secondByKey.get(key);
664
+ const template = firstRow ?? secondRow;
665
+ if (!template)
666
+ continue;
667
+ const metrics = {};
668
+ const comparisonMetrics = [
669
+ 'matches',
670
+ 'kills',
671
+ 'assists',
672
+ 'damage',
673
+ 'avg_damage',
674
+ 'kd',
675
+ 'deaths',
676
+ 'wins',
677
+ 'top10',
678
+ 'rank',
679
+ 'dbnos',
680
+ 'revives',
681
+ 'performance_score',
682
+ 'chicken_index',
683
+ ];
684
+ const comparisonRatios = {};
685
+ for (const metric of comparisonMetrics) {
686
+ const firstKnown = metricNullableNumber(firstRow, metric);
687
+ const secondKnown = metricNullableNumber(secondRow, metric);
688
+ metrics[metric] =
689
+ firstKnown === null || secondKnown === null ? null : round(firstKnown - secondKnown);
690
+ comparisonRatios[metric] = ratio(firstKnown, secondKnown);
691
+ }
692
+ deltaRows.push({
693
+ ...template,
694
+ activityStatus: firstRow?.activityStatus === 'ACTIVE' || secondRow?.activityStatus === 'ACTIVE'
695
+ ? 'ACTIVE'
696
+ : 'NO_ACTIVITY',
697
+ metrics,
698
+ comparisonRatios,
699
+ });
700
+ }
701
+ }
702
+ const selectedForEvidence = normalizeRecords(query.segments.flatMap((segment) => selectBySelector(records, segment.selector, ids, options)));
703
+ return {
704
+ queryId: query.queryId,
705
+ sessionId,
706
+ status: baseStatus,
707
+ data: {
708
+ operation: 'compare',
709
+ groupBy: query.groupBy,
710
+ rows: deltaRows,
711
+ summary: {
712
+ segmentCount: segmentData.length,
713
+ comparisonRatios: Object.fromEntries(deltaRows.map((row) => [row.key, row.comparisonRatios ?? {}])),
714
+ },
715
+ segments: segmentData,
716
+ },
717
+ coverage,
718
+ source,
719
+ evidence: evidence(selectedForEvidence, query),
720
+ diagnostics: {
721
+ selectedMatchCount: selectedForEvidence.length,
722
+ resolvedSegments: query.segments,
723
+ calculation: 'deterministic_query_engine_v3',
724
+ },
725
+ };
726
+ }
727
+ invalid(queryId, sessionId, status, errors, coverage, source) {
728
+ return {
729
+ queryId,
730
+ sessionId,
731
+ status,
732
+ data: { operation: 'report', groupBy: 'player', rows: [], summary: { errors } },
733
+ coverage,
734
+ source,
735
+ evidence: {
736
+ matchIds: [],
737
+ playerIds: [],
738
+ fields: [],
739
+ calculation: 'deterministic_query_engine_v3',
740
+ },
741
+ diagnostics: { errors },
742
+ };
743
+ }
744
+ }
745
+ export function resultSetFromResult(result, query, sessionId, ttlMs = 24 * 60 * 60 * 1000) {
746
+ const createdAt = new Date();
747
+ return {
748
+ id: `rs_${randomUUID()}`,
749
+ queryId: result.queryId,
750
+ sessionId,
751
+ resolvedQuery: query,
752
+ resolvedSelector: query.selector,
753
+ playerIds: result.evidence.playerIds,
754
+ matchIds: result.evidence.matchIds,
755
+ rows: result.data.rows,
756
+ aggregates: result.data.summary,
757
+ rankings: result.data.rows.filter((row) => row.position !== undefined),
758
+ coverage: result.coverage,
759
+ status: result.status,
760
+ source: result.source,
761
+ createdAt: createdAt.toISOString(),
762
+ expiresAt: new Date(createdAt.getTime() + ttlMs).toISOString(),
763
+ };
764
+ }
765
+ //# sourceMappingURL=query-engine.js.map