@rine-kit/pubg-domain 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/config/default-team.json +27 -0
- package/dist/src/config/chicken-index.d.ts +9 -0
- package/dist/src/config/chicken-index.js +37 -0
- package/dist/src/config/chicken-index.js.map +1 -0
- package/dist/src/config/team.d.ts +22 -0
- package/dist/src/config/team.js +69 -0
- package/dist/src/config/team.js.map +1 -0
- package/dist/src/context/context-store.d.ts +40 -0
- package/dist/src/context/context-store.js +119 -0
- package/dist/src/context/context-store.js.map +1 -0
- package/dist/src/context/team-context.d.ts +13 -0
- package/dist/src/context/team-context.js +16 -0
- package/dist/src/context/team-context.js.map +1 -0
- package/dist/src/core.d.ts +23 -0
- package/dist/src/core.js +24 -0
- package/dist/src/core.js.map +1 -0
- package/dist/src/data/model.d.ts +123 -0
- package/dist/src/data/model.js +100 -0
- package/dist/src/data/model.js.map +1 -0
- package/dist/src/data/pubg-api-client.d.ts +59 -0
- package/dist/src/data/pubg-api-client.js +441 -0
- package/dist/src/data/pubg-api-client.js.map +1 -0
- package/dist/src/engine/query-engine.d.ts +20 -0
- package/dist/src/engine/query-engine.js +765 -0
- package/dist/src/engine/query-engine.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +28 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/migration/importer.d.ts +35 -0
- package/dist/src/migration/importer.js +229 -0
- package/dist/src/migration/importer.js.map +1 -0
- package/dist/src/review/combat-scope.d.ts +11 -0
- package/dist/src/review/combat-scope.js +101 -0
- package/dist/src/review/combat-scope.js.map +1 -0
- package/dist/src/review/event-combination-engine.d.ts +17 -0
- package/dist/src/review/event-combination-engine.js +534 -0
- package/dist/src/review/event-combination-engine.js.map +1 -0
- package/dist/src/review/fight-detector.d.ts +4 -0
- package/dist/src/review/fight-detector.js +138 -0
- package/dist/src/review/fight-detector.js.map +1 -0
- package/dist/src/review/fight-integrity-validator.d.ts +13 -0
- package/dist/src/review/fight-integrity-validator.js +134 -0
- package/dist/src/review/fight-integrity-validator.js.map +1 -0
- package/dist/src/review/fun-candidate-generator.d.ts +9 -0
- package/dist/src/review/fun-candidate-generator.js +267 -0
- package/dist/src/review/fun-candidate-generator.js.map +1 -0
- package/dist/src/review/fun-event-generator.d.ts +8 -0
- package/dist/src/review/fun-event-generator.js +740 -0
- package/dist/src/review/fun-event-generator.js.map +1 -0
- package/dist/src/review/fun-intelligence.d.ts +13 -0
- package/dist/src/review/fun-intelligence.js +20 -0
- package/dist/src/review/fun-intelligence.js.map +1 -0
- package/dist/src/review/fun-ranker.d.ts +13 -0
- package/dist/src/review/fun-ranker.js +68 -0
- package/dist/src/review/fun-ranker.js.map +1 -0
- package/dist/src/review/heavy-weapon-intelligence.d.ts +5 -0
- package/dist/src/review/heavy-weapon-intelligence.js +215 -0
- package/dist/src/review/heavy-weapon-intelligence.js.map +1 -0
- package/dist/src/review/key-operation-detector.d.ts +3 -0
- package/dist/src/review/key-operation-detector.js +126 -0
- package/dist/src/review/key-operation-detector.js.map +1 -0
- package/dist/src/review/match-selector.d.ts +18 -0
- package/dist/src/review/match-selector.js +202 -0
- package/dist/src/review/match-selector.js.map +1 -0
- package/dist/src/review/review-analyzer.d.ts +2 -0
- package/dist/src/review/review-analyzer.js +652 -0
- package/dist/src/review/review-analyzer.js.map +1 -0
- package/dist/src/review/review-facts.d.ts +17 -0
- package/dist/src/review/review-facts.js +560 -0
- package/dist/src/review/review-facts.js.map +1 -0
- package/dist/src/review/review-share.d.ts +48 -0
- package/dist/src/review/review-share.js +37 -0
- package/dist/src/review/review-share.js.map +1 -0
- package/dist/src/review/selection-store.d.ts +25 -0
- package/dist/src/review/selection-store.js +80 -0
- package/dist/src/review/selection-store.js.map +1 -0
- package/dist/src/review/session-review.d.ts +72 -0
- package/dist/src/review/session-review.js +492 -0
- package/dist/src/review/session-review.js.map +1 -0
- package/dist/src/review/special-events.d.ts +3 -0
- package/dist/src/review/special-events.js +258 -0
- package/dist/src/review/special-events.js.map +1 -0
- package/dist/src/review/subgraph.d.ts +26 -0
- package/dist/src/review/subgraph.js +146 -0
- package/dist/src/review/subgraph.js.map +1 -0
- package/dist/src/review/supplemental-intelligence.d.ts +15 -0
- package/dist/src/review/supplemental-intelligence.js +431 -0
- package/dist/src/review/supplemental-intelligence.js.map +1 -0
- package/dist/src/review/team-damage.d.ts +8 -0
- package/dist/src/review/team-damage.js +180 -0
- package/dist/src/review/team-damage.js.map +1 -0
- package/dist/src/review/telemetry-events.d.ts +72 -0
- package/dist/src/review/telemetry-events.js +573 -0
- package/dist/src/review/telemetry-events.js.map +1 -0
- package/dist/src/review/telemetry.d.ts +86 -0
- package/dist/src/review/telemetry.js +266 -0
- package/dist/src/review/telemetry.js.map +1 -0
- package/dist/src/review/types.d.ts +501 -0
- package/dist/src/review/types.js +2 -0
- package/dist/src/review/types.js.map +1 -0
- package/dist/src/review/vehicle-intelligence.d.ts +3 -0
- package/dist/src/review/vehicle-intelligence.js +148 -0
- package/dist/src/review/vehicle-intelligence.js.map +1 -0
- package/dist/src/schema/query.d.ts +876 -0
- package/dist/src/schema/query.js +185 -0
- package/dist/src/schema/query.js.map +1 -0
- package/dist/src/schema/status.d.ts +66 -0
- package/dist/src/schema/status.js +49 -0
- package/dist/src/schema/status.js.map +1 -0
- package/dist/src/service/pubg-domain-service.d.ts +192 -0
- package/dist/src/service/pubg-domain-service.js +1598 -0
- package/dist/src/service/pubg-domain-service.js.map +1 -0
- package/dist/src/storage/repository-port.d.ts +84 -0
- package/dist/src/storage/repository-port.js +2 -0
- package/dist/src/storage/repository-port.js.map +1 -0
- package/dist/src/storage/sqlite-repository-adapter.d.ts +33 -0
- package/dist/src/storage/sqlite-repository-adapter.js +66 -0
- package/dist/src/storage/sqlite-repository-adapter.js.map +1 -0
- package/dist/src/storage/sqlite-repository.d.ts +73 -0
- package/dist/src/storage/sqlite-repository.js +652 -0
- package/dist/src/storage/sqlite-repository.js.map +1 -0
- package/dist/src/time/selector-resolver.d.ts +34 -0
- package/dist/src/time/selector-resolver.js +364 -0
- package/dist/src/time/selector-resolver.js.map +1 -0
- 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
|