gamealgo-cli 0.3.9 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,20 @@ gamealgo listapi
35
35
  gamealgo docs api GameAlgo.Init --platform lua
36
36
  ```
37
37
 
38
+ Report Pack v2 uses materialized ordinary Datasets and inline Views. Common report commands are:
39
+
40
+ ```bash
41
+ gamealgo report pull --out gamealgo-report-pack.json
42
+ gamealgo report validate gamealgo-report-pack.json
43
+ gamealgo report preview --pack gamealgo-report-pack.json
44
+ gamealgo report publish gamealgo-report-pack.json
45
+ gamealgo report result --from 2026-09-04 --to 2026-09-17 --platform maker --tab-id overview
46
+ gamealgo report advanced run --query-id battle_analysis --from 2026-09-04 --to 2026-09-17 --platform maker --wait
47
+ gamealgo report advanced list
48
+ ```
49
+
50
+ All Report Packs use `schemaVersion: 2`, with custom `groups[].views` or platform-owned `groups[].standard`. Advanced query runs are explicit, are never preloaded, and remain available as immutable reports for 30 days.
51
+
38
52
  Help and documentation are fetched from the configured Server on every invocation. The CLI intentionally does not keep a documentation cache.
39
53
 
40
54
  ## Agent skill
package/dist/client.js CHANGED
@@ -168,6 +168,24 @@ export class GameAlgoAdminClient {
168
168
  const query = params.size ? `?${params.toString()}` : "";
169
169
  return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/manifest${query}`);
170
170
  }
171
+ async readReportDataset(input) {
172
+ const params = new URLSearchParams({
173
+ platform: input.platform,
174
+ startDate: input.startDate,
175
+ endDate: input.endDate,
176
+ });
177
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-datasets/${encodeURIComponent(input.datasetFingerprint)}/results?${params.toString()}`);
178
+ }
179
+ async createAdvancedReportRun(body, options = {}) {
180
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/advanced-runs`, body, options);
181
+ }
182
+ async getAdvancedReportRun(runId, includeResult = true) {
183
+ const params = new URLSearchParams({ includeResult: includeResult ? "true" : "false" });
184
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/advanced-runs/${encodeURIComponent(runId)}?${params.toString()}`);
185
+ }
186
+ async listAdvancedReportRuns(limit = 50) {
187
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/advanced-runs?limit=${encodeURIComponent(String(limit))}`);
188
+ }
171
189
  async queryReportDashboard(body, options = {}) {
172
190
  return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/query`, body, options);
173
191
  }
@@ -231,8 +249,8 @@ export class GameAlgoAdminClient {
231
249
  async listGameKeys() {
232
250
  return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys`);
233
251
  }
234
- async createGameKey(name) {
235
- return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys`, { name });
252
+ async createGameKey(name, platform) {
253
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys`, { name, ...(platform ? { platform } : {}) });
236
254
  }
237
255
  async revealGameKey(selector) {
238
256
  return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys/reveal`, selector);
@@ -129,7 +129,7 @@ export async function handleExperimentStrategy(client, args, global) {
129
129
  const platform = flags.platform === undefined ? undefined : experimentPlatform(flags.platform);
130
130
  const message = requiredMessage(flags, "experiment strategy rollback");
131
131
  if (!strategyKey || !versionId) {
132
- throw new Error("usage: gamealgo experiment strategy rollback <strategyKey> --version-id <versionId> [--platform ios|android|rest] --message <reason> --yes");
132
+ throw new Error("usage: gamealgo experiment strategy rollback <strategyKey> --version-id <versionId> [--platform ios|android|maker|web] --message <reason> --yes");
133
133
  }
134
134
  await requireYes(flags, global, "experiment strategy rollback");
135
135
  await printResult(await client.rollbackExperimentV2Strategy(strategyKey, versionId, message, platform), global);
@@ -16,7 +16,7 @@ export async function handleIntegration(client, args, global) {
16
16
  await writeIntegrationOutput(response, flags, global);
17
17
  return;
18
18
  }
19
- throw new Error("usage: gamealgo integration get-plan --environment <domestic|overseas> --platform <ios|android|maker|rest>");
19
+ throw new Error("usage: gamealgo integration get-plan --environment <domestic|overseas> --platform <ios|android|maker|web>");
20
20
  }
21
21
  export async function writeIntegrationOutput(value, flags, global) {
22
22
  if (flags.out) {
@@ -1,7 +1,7 @@
1
1
  import { parseFlags } from "../args.js";
2
2
  import { confirm, isInteractive, printResult } from "../output.js";
3
3
  import { announceTargetGame } from "../target.js";
4
- import { optionalString } from "../utils.js";
4
+ import { integrationPlatform, optionalString } from "../utils.js";
5
5
  export async function handleGameKey(client, args, global) {
6
6
  const sub = args.shift();
7
7
  if (sub === "list") {
@@ -15,8 +15,9 @@ export async function handleGameKey(client, args, global) {
15
15
  const flags = parseFlags(args);
16
16
  const name = optionalString(flags.name) || optionalString(args.shift());
17
17
  if (!name)
18
- throw new Error("usage: gamealgo key create --name <key-name>");
19
- const response = await client.createGameKey(name);
18
+ throw new Error("usage: gamealgo key create --name <key-name> [--platform ios|android|maker|web]");
19
+ const platform = flags.platform === undefined ? undefined : integrationPlatform(flags.platform);
20
+ const response = await client.createGameKey(name, platform);
20
21
  await printResult({
21
22
  ok: true,
22
23
  created: response.created !== false,
@@ -25,6 +26,7 @@ export async function handleGameKey(client, args, global) {
25
26
  prefix: response.key.keyPrefix,
26
27
  keyPrefix: response.key.keyPrefix,
27
28
  status: response.key.status,
29
+ platform: response.key.platform,
28
30
  createdAt: response.key.createdAt,
29
31
  }, global);
30
32
  return;
@@ -40,6 +42,7 @@ export async function handleGameKey(client, args, global) {
40
42
  prefix: response.key.keyPrefix,
41
43
  keyPrefix: response.key.keyPrefix,
42
44
  status: response.key.status,
45
+ platform: response.key.platform,
43
46
  createdAt: response.key.createdAt,
44
47
  }, global);
45
48
  return;
@@ -72,6 +75,7 @@ export function publicGameKeyForCli(key) {
72
75
  prefix: key.keyPrefix,
73
76
  keyPrefix: key.keyPrefix,
74
77
  status: key.status,
78
+ platform: key.platform,
75
79
  createdAt: key.createdAt,
76
80
  };
77
81
  }
@@ -4,16 +4,24 @@ import { formatDuration, markCommandFailed, printResult, reportTimeoutMs, startP
4
4
  import { reportDashboardCsv, reportDashboardCsvExports } from "../report-csv.js";
5
5
  import { normalizeReportSampleRows, summarizeReportDashboard, summarizeReportManifest, summarizeReportValidation, } from "../report-summary.js";
6
6
  import { objectValue, readJsonFile, writeTextFile } from "../resources.js";
7
+ import { queryReportPackV2Result } from "../report-v2/result.js";
8
+ import { assertWritableReportPackV2, reportPackSchemaVersion } from "../report-v2/schema.js";
9
+ import { summarizeReportPackV2Manifest } from "../report-v2/summary.js";
7
10
  import { announceTargetGame } from "../target.js";
8
11
  import { optionalString } from "../utils.js";
9
12
  export async function handleReport(client, args, global) {
10
13
  const sub = args.shift();
14
+ if (sub === "advanced") {
15
+ await handleAdvancedReport(client, args, global);
16
+ return;
17
+ }
11
18
  if (sub === "pull") {
12
19
  const flags = parseFlags(args);
13
20
  const pack = await client.getReportPack();
14
- const out = String(flags.out || "gamealgo-report-pack-v1.json");
21
+ const schemaVersion = reportPackSchemaVersion(pack.reportPack.content);
22
+ const out = String(flags.out || `gamealgo-report-pack-v${schemaVersion}.json`);
15
23
  await writeTextFile(out, JSON.stringify(pack.reportPack.content, null, 2) + "\n");
16
- await printResult({ ok: true, out }, global);
24
+ await printResult({ ok: true, out, schemaVersion }, global);
17
25
  return;
18
26
  }
19
27
  if (sub === "validate") {
@@ -23,6 +31,7 @@ export async function handleReport(client, args, global) {
23
31
  const flags = parseFlags(args);
24
32
  const full = reportFullOutput(flags);
25
33
  const content = await readJsonFile(filePath);
34
+ assertWritableReportPackV2(content, "report validate");
26
35
  const response = await client.previewReportPack(content, { responseMode: full ? "full" : "summary" });
27
36
  const summary = summarizeReportValidation(response);
28
37
  await printResult(full ? response : summary, global);
@@ -36,6 +45,7 @@ export async function handleReport(client, args, global) {
36
45
  throw new Error("usage: gamealgo report publish <report-pack.json>");
37
46
  parseFlags(args);
38
47
  const content = await readJsonFile(filePath);
48
+ assertWritableReportPackV2(content, "report publish");
39
49
  await announceTargetGame(client);
40
50
  const response = await client.putReportPack(content);
41
51
  await printResult({ ok: true, validation: response.reportPack.validation }, global);
@@ -45,7 +55,10 @@ export async function handleReport(client, args, global) {
45
55
  const flags = parseFlags(args);
46
56
  const full = reportFullOutput(flags);
47
57
  const response = await client.reportManifest({ responseMode: full ? "full" : "summary" });
48
- await printResult(full ? response : summarizeReportManifest(response), global);
58
+ const manifest = objectValue(objectValue(response)?.manifest);
59
+ await printResult(full
60
+ ? response
61
+ : manifest?.schemaVersion === 2 ? summarizeReportPackV2Manifest(response) : summarizeReportManifest(response), global);
49
62
  return;
50
63
  }
51
64
  if (sub === "result") {
@@ -54,32 +67,50 @@ export async function handleReport(client, args, global) {
54
67
  const startDate = String(flags.from || flags.start || flags.startDate || "");
55
68
  const endDate = String(flags.to || flags.end || flags.endDate || "");
56
69
  if (!startDate || !endDate)
57
- throw new Error("usage: gamealgo report result --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v] [--sample-rows 0..20] [--full] [--format json|csv] [--out file-or-directory]");
70
+ throw new Error("usage: gamealgo report result --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|maker|web] [--tab name] [--group name] [--chart name] [--selector k=v] [--sample-rows 0..20] [--full] [--format json|csv] [--out file-or-directory]");
58
71
  requireReportQueryScope(flags, "report result");
59
72
  const timeoutMs = reportTimeoutMs(flags);
60
73
  const format = reportOutputFormat(flags.format);
61
74
  const full = reportFullOutput(flags) || format === "csv";
62
75
  const sampleRows = reportSampleRows(flags, full);
63
76
  const platform = await resolveCliReportPlatform(client, flags.platform, selectors.platform, startDate, endDate);
77
+ const manifestResponse = await client.reportManifest({ responseMode: "full" });
78
+ const manifest = objectValue(objectValue(manifestResponse)?.manifest);
79
+ const schemaVersion = manifest?.schemaVersion === 2 ? 2 : 1;
64
80
  const startedAt = Date.now();
65
- const stopProgress = startProgress("Querying report results", timeoutMs);
81
+ const stopProgress = startProgress(schemaVersion === 2 ? "Reading report results" : "Querying report results", timeoutMs);
66
82
  let response;
67
83
  try {
68
- response = await client.queryReportDashboard({
69
- startDate,
70
- endDate,
71
- tab: optionalString(flags.tab),
72
- tabId: optionalString(flags["tab-id"]),
73
- group: optionalString(flags.group),
74
- groupId: optionalString(flags["group-id"]),
75
- chart: optionalString(flags.chart),
76
- chartId: optionalString(flags["chart-id"]),
77
- platform,
78
- selectors,
79
- refresh: Boolean(flags.refresh),
80
- responseMode: full ? "full" : "summary",
81
- sampleRows,
82
- }, { timeoutMs });
84
+ response = schemaVersion === 2
85
+ ? await queryReportPackV2Result(client, manifestResponse, {
86
+ startDate,
87
+ endDate,
88
+ tab: optionalString(flags.tab),
89
+ tabId: optionalString(flags["tab-id"]),
90
+ group: optionalString(flags.group),
91
+ groupId: optionalString(flags["group-id"]),
92
+ chart: optionalString(flags.chart),
93
+ chartId: optionalString(flags["chart-id"]),
94
+ platform,
95
+ selectors,
96
+ refresh: Boolean(flags.refresh),
97
+ timeoutMs,
98
+ })
99
+ : await client.queryReportDashboard({
100
+ startDate,
101
+ endDate,
102
+ tab: optionalString(flags.tab),
103
+ tabId: optionalString(flags["tab-id"]),
104
+ group: optionalString(flags.group),
105
+ groupId: optionalString(flags["group-id"]),
106
+ chart: optionalString(flags.chart),
107
+ chartId: optionalString(flags["chart-id"]),
108
+ platform,
109
+ selectors,
110
+ refresh: Boolean(flags.refresh),
111
+ responseMode: full ? "full" : "summary",
112
+ sampleRows,
113
+ }, { timeoutMs });
83
114
  }
84
115
  finally {
85
116
  stopProgress();
@@ -93,53 +124,159 @@ export async function handleReport(client, args, global) {
93
124
  return;
94
125
  }
95
126
  if (sub === "preview") {
96
- const selectors = parseSelectorFlags(args);
97
127
  const flags = parseFlags(args);
98
128
  const filePath = String(flags.pack || flags.file || args.shift() || "");
99
- const startDate = String(flags.from || flags.start || flags.startDate || "");
100
- const endDate = String(flags.to || flags.end || flags.endDate || "");
101
- if (!filePath || !startDate || !endDate) {
102
- throw new Error("usage: gamealgo report preview --pack report-pack.json --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v] [--sample-rows 0..20] [--full] [--format json|csv] [--out file-or-directory]");
103
- }
104
- requireReportQueryScope(flags, "report preview");
129
+ if (!filePath)
130
+ throw new Error("usage: gamealgo report preview --pack report-pack.json [--full]");
105
131
  const content = await readJsonFile(filePath);
132
+ assertWritableReportPackV2(content, "report preview");
106
133
  const timeoutMs = reportTimeoutMs(flags);
107
- const format = reportOutputFormat(flags.format);
108
- const full = reportFullOutput(flags) || format === "csv";
109
- const sampleRows = reportSampleRows(flags, full);
110
- const platform = await resolveCliReportPlatform(client, flags.platform, selectors.platform, startDate, endDate);
134
+ const full = reportFullOutput(flags);
111
135
  const startedAt = Date.now();
112
- const stopProgress = startProgress("Previewing report results", timeoutMs);
136
+ const stopProgress = startProgress("Validating Report Pack v2 preview", timeoutMs);
113
137
  let response;
114
138
  try {
115
- response = await client.previewReportDashboard({
116
- content,
117
- startDate,
118
- endDate,
119
- tab: optionalString(flags.tab),
120
- tabId: optionalString(flags["tab-id"]),
121
- group: optionalString(flags.group),
122
- groupId: optionalString(flags["group-id"]),
123
- chart: optionalString(flags.chart),
124
- chartId: optionalString(flags["chart-id"]),
125
- platform,
126
- selectors,
127
- responseMode: full ? "full" : "summary",
128
- sampleRows,
129
- }, { timeoutMs });
139
+ response = await client.previewReportPack(content, { responseMode: full ? "full" : "summary" });
130
140
  }
131
141
  finally {
132
142
  stopProgress();
133
143
  }
134
144
  const elapsedMs = Date.now() - startedAt;
135
145
  const rawOutputValue = withCliMeta(response, { elapsedMs, timeoutMs });
136
- const statusSummary = summarizeReportDashboard(rawOutputValue, { command: "report.preview", sampleRows });
146
+ const statusSummary = summarizeReportValidation(rawOutputValue, "report.preview");
137
147
  const outputValue = full ? rawOutputValue : statusSummary;
138
- process.stderr.write(`Report preview finished in ${formatDuration(elapsedMs)}.\n`);
139
- await writeReportQueryOutput(outputValue, flags, global, elapsedMs, statusSummary.ok);
148
+ process.stderr.write(`Report Pack preview finished in ${formatDuration(elapsedMs)}.\n`);
149
+ const out = optionalString(flags.out);
150
+ if (flags.out === true)
151
+ throw new Error("--out requires a file path");
152
+ if (out) {
153
+ await writeTextFile(out, JSON.stringify(outputValue, null, 2) + "\n");
154
+ await printResult({ ok: statusSummary.ok, out, elapsedMs }, global);
155
+ }
156
+ else {
157
+ await printResult(outputValue, global);
158
+ }
159
+ if (!statusSummary.ok)
160
+ markCommandFailed();
140
161
  return;
141
162
  }
142
- throw new Error("usage: gamealgo report <pull|validate|publish|manifest|result|preview>");
163
+ throw new Error("usage: gamealgo report <pull|validate|publish|manifest|result|preview|advanced>");
164
+ }
165
+ async function handleAdvancedReport(client, args, global) {
166
+ const action = args.shift();
167
+ if (action === "run") {
168
+ const flags = parseFlags(args);
169
+ const queryId = requiredTextFlag(flags["query-id"], "--query-id");
170
+ const startDate = requiredTextFlag(flags.from || flags.start || flags.startDate, "--from");
171
+ const endDate = requiredTextFlag(flags.to || flags.end || flags.endDate, "--to");
172
+ const platform = optionalCliReportPlatform(flags.platform, "--platform");
173
+ if (!platform)
174
+ throw new Error("--platform is required for report advanced run");
175
+ if (flags.format !== undefined && flags.format !== "json")
176
+ throw new Error("report advanced currently supports only --format json");
177
+ const packPath = optionalString(flags.pack);
178
+ if (flags.pack === true)
179
+ throw new Error("--pack requires a file path");
180
+ const pack = packPath ? await readJsonFile(packPath) : undefined;
181
+ if (pack !== undefined)
182
+ assertWritableReportPackV2(pack, "report advanced run");
183
+ const wait = booleanFlag(flags.wait, "--wait");
184
+ const timeoutMs = reportTimeoutMs(flags) ?? 15 * 60 * 1000;
185
+ await announceTargetGame(client);
186
+ const created = await client.createAdvancedReportRun({
187
+ queryId,
188
+ startDate,
189
+ endDate,
190
+ platform,
191
+ ...(pack !== undefined ? { pack } : {}),
192
+ }, { timeoutMs: Math.min(timeoutMs, 30_000) });
193
+ if (!wait) {
194
+ await writeAdvancedOutput(created, flags, global);
195
+ return;
196
+ }
197
+ const runId = requiredRunId(created.run?.id);
198
+ const startedAt = Date.now();
199
+ const stopProgress = startProgress(`Waiting for advanced report ${runId}`, timeoutMs);
200
+ let response;
201
+ try {
202
+ response = await waitForAdvancedReport(client, runId, timeoutMs, startedAt);
203
+ }
204
+ finally {
205
+ stopProgress();
206
+ }
207
+ await writeAdvancedOutput(withCliMeta(response, { elapsedMs: Date.now() - startedAt, timeoutMs }), flags, global);
208
+ if (response.run.status === "failed")
209
+ markCommandFailed();
210
+ return;
211
+ }
212
+ if (action === "get") {
213
+ const runId = args.shift();
214
+ if (!runId)
215
+ throw new Error("usage: gamealgo report advanced get <run-id> [--metadata-only] [--out file]");
216
+ const flags = parseFlags(args);
217
+ const metadataOnly = booleanFlag(flags["metadata-only"], "--metadata-only");
218
+ const response = await client.getAdvancedReportRun(runId, !metadataOnly);
219
+ await writeAdvancedOutput(response, flags, global);
220
+ if (response.run.status === "failed")
221
+ markCommandFailed();
222
+ return;
223
+ }
224
+ if (action === "list") {
225
+ const flags = parseFlags(args);
226
+ const limit = positiveIntegerFlag(flags.limit, "--limit", 50);
227
+ await writeAdvancedOutput(await client.listAdvancedReportRuns(limit), flags, global);
228
+ return;
229
+ }
230
+ throw new Error("usage: gamealgo report advanced <run|get|list>");
231
+ }
232
+ async function waitForAdvancedReport(client, runId, timeoutMs, startedAt) {
233
+ while (true) {
234
+ const response = await client.getAdvancedReportRun(runId, true);
235
+ if (response.run.status === "succeeded" || response.run.status === "failed")
236
+ return response;
237
+ if (Date.now() - startedAt >= timeoutMs) {
238
+ throw new Error(`advanced report ${runId} did not finish within ${formatDuration(timeoutMs)}; use report advanced get ${runId} later`);
239
+ }
240
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
241
+ }
242
+ }
243
+ async function writeAdvancedOutput(value, flags, global) {
244
+ if (flags.format !== undefined && flags.format !== "json")
245
+ throw new Error("report advanced currently supports only --format json");
246
+ const out = optionalString(flags.out);
247
+ if (flags.out === true)
248
+ throw new Error("--out requires a file path");
249
+ if (!out) {
250
+ await printResult(value, global);
251
+ return;
252
+ }
253
+ await writeTextFile(out, JSON.stringify(value, null, 2) + "\n");
254
+ await printResult({ ok: true, out }, global);
255
+ }
256
+ function requiredTextFlag(value, flag) {
257
+ if (typeof value !== "string" || !value.trim())
258
+ throw new Error(`${flag} is required`);
259
+ return value.trim();
260
+ }
261
+ function requiredRunId(value) {
262
+ if (typeof value !== "string" || !value)
263
+ throw new Error("advanced report create response is missing run.id");
264
+ return value;
265
+ }
266
+ function booleanFlag(value, flag) {
267
+ if (value === undefined)
268
+ return false;
269
+ if (value !== true)
270
+ throw new Error(`${flag} does not accept a value`);
271
+ return true;
272
+ }
273
+ function positiveIntegerFlag(value, flag, fallback) {
274
+ if (value === undefined)
275
+ return fallback;
276
+ if (typeof value !== "string" || !/^\d+$/.test(value) || Number(value) < 1) {
277
+ throw new Error(`${flag} must be a positive integer`);
278
+ }
279
+ return Number(value);
143
280
  }
144
281
  function reportFullOutput(flags) {
145
282
  if (flags.full === undefined)
@@ -293,7 +430,7 @@ export async function resolveCliReportPlatform(client, flagValue, selectorValue,
293
430
  const detected = objectValue(await client.countEventPlatforms(startDate, endDate));
294
431
  const detectedPlatform = optionalCliReportPlatform(detected?.recommendedPlatform, "detected report platform");
295
432
  if (!detectedPlatform) {
296
- throw new Error("report platform could not be detected for the selected date range; pass --platform ios, --platform android, or --platform rest");
433
+ throw new Error("report platform could not be detected for the selected date range; pass --platform ios, android, maker, or web");
297
434
  }
298
435
  process.stderr.write(`Report platform: ${detectedPlatform} (detected from event data)\n`);
299
436
  return detectedPlatform;
@@ -301,7 +438,7 @@ export async function resolveCliReportPlatform(client, flagValue, selectorValue,
301
438
  export function optionalCliReportPlatform(value, field) {
302
439
  if (value === undefined || value === null || value === "")
303
440
  return undefined;
304
- if (value === "ios" || value === "android" || value === "rest")
441
+ if (value === "ios" || value === "android" || value === "maker" || value === "web")
305
442
  return value;
306
- throw new Error(`${field} must be ios, android, or rest`);
443
+ throw new Error(`${field} must be ios, android, maker, or web`);
307
444
  }
@@ -1,6 +1,6 @@
1
1
  import { loadPackageMetadata } from "./config.js";
2
2
  import { requestJson } from "./http.js";
3
- export const CLI_PROTOCOL_VERSION = 1;
3
+ export const CLI_PROTOCOL_VERSION = 2;
4
4
  export async function assertServerCompatibility(host) {
5
5
  const metadata = await requestJson(host, "/admin/v1/cli/meta", {
6
6
  headers: { "Cache-Control": "no-cache" },
package/dist/docs.js CHANGED
@@ -459,12 +459,12 @@ export function parseDocsApiPlatform(value) {
459
459
  if (value === undefined || value === false || value === "")
460
460
  return undefined;
461
461
  const platform = String(value).trim().toLowerCase();
462
- if (platform === "maker")
463
- return "lua";
464
- if (platform === "lua" || platform === "ios" || platform === "android" || platform === "rest") {
462
+ if (platform === "lua")
463
+ return "maker";
464
+ if (platform === "maker" || platform === "ios" || platform === "android" || platform === "web") {
465
465
  return platform;
466
466
  }
467
- throw new Error("--platform must be lua, maker, ios, android, or rest");
467
+ throw new Error("--platform must be ios, android, maker, or web");
468
468
  }
469
469
  export async function printDocsApiResult(options) {
470
470
  const { query, platform, manifest, topic, document, webUrl, host, global } = options;
@@ -537,8 +537,8 @@ export function parseDocsApiEntries(content) {
537
537
  if (!symbols.length)
538
538
  continue;
539
539
  const platforms = parseDocsApiMetadataList(section, "平台")
540
- .map((item) => item.toLowerCase())
541
- .filter((item) => item === "lua" || item === "ios" || item === "android" || item === "rest");
540
+ .map((item) => item.toLowerCase() === "lua" ? "maker" : item.toLowerCase())
541
+ .filter((item) => item === "maker" || item === "ios" || item === "android" || item === "web");
542
542
  const summaryMatch = section.match(/^-\s*用途[::]\s*(.+)$/m);
543
543
  entries.push({
544
544
  name: stripInlineMarkdown(heading[1]),
@@ -2,7 +2,7 @@ export const REPORT_SUMMARY_SCHEMA_VERSION = 1;
2
2
  export const REPORT_SUMMARY_MAX_ITEMS = 200;
3
3
  export const REPORT_SUMMARY_MAX_ISSUES = 100;
4
4
  export const REPORT_SUMMARY_MAX_SAMPLE_ROWS = 20;
5
- export function summarizeReportValidation(value) {
5
+ export function summarizeReportValidation(value, command = "report.validate") {
6
6
  const root = objectValue(value);
7
7
  const validation = objectValue(root?.validation);
8
8
  const errorMessages = stringValues(validation?.errors);
@@ -24,7 +24,7 @@ export function summarizeReportValidation(value) {
24
24
  const items = statementItems.slice(0, REPORT_SUMMARY_MAX_ITEMS);
25
25
  return {
26
26
  schemaVersion: REPORT_SUMMARY_SCHEMA_VERSION,
27
- command: "report.validate",
27
+ command,
28
28
  ok: validation?.ok === true && errorMessages.length === 0,
29
29
  summary: {
30
30
  errorCount: errorMessages.length,
@@ -146,6 +146,13 @@ export function summarizeReportDashboard(value, options) {
146
146
  if (result?.stale === true) {
147
147
  allWarnings.push({ scope, message: "Report result is stale" });
148
148
  }
149
+ if (result?.partial === true) {
150
+ const missingDates = stringValues(result.missingDates);
151
+ allWarnings.push({
152
+ scope,
153
+ message: `Report Dataset coverage is partial${missingDates.length ? `; missing dates: ${missingDates.join(", ")}` : ""}`,
154
+ });
155
+ }
149
156
  allItems.push(compactObject({
150
157
  kind: "chart-result",
151
158
  status,
@@ -162,6 +169,9 @@ export function summarizeReportDashboard(value, options) {
162
169
  cached: typeof result?.cached === "boolean" ? result.cached : undefined,
163
170
  cachedAt: stringValue(result?.cachedAt),
164
171
  stale: typeof result?.stale === "boolean" ? result.stale : undefined,
172
+ partial: typeof result?.partial === "boolean" ? result.partial : undefined,
173
+ missingDates: stringValues(result?.missingDates),
174
+ snapshotVersion: stringValue(result?.snapshotVersion),
165
175
  truncated,
166
176
  rowLimit: finiteNonNegativeNumber(result?.rowLimit),
167
177
  error,
@@ -243,6 +253,8 @@ function reportResultStatus(result) {
243
253
  return "retrying";
244
254
  if (result?.refreshing === true)
245
255
  return "refreshing";
256
+ if (result?.partial === true)
257
+ return "partial";
246
258
  if (result?.stale === true)
247
259
  return "stale";
248
260
  return "ready";
@@ -0,0 +1,130 @@
1
+ export function parseReportFormula(source) {
2
+ const tokens = tokenize(String(source ?? ""));
3
+ let cursor = 0;
4
+ const peek = (value) => tokens[cursor]?.value === value;
5
+ const take = (value) => {
6
+ if (!peek(value))
7
+ throw new Error(`expected ${value || "expression"}`);
8
+ return tokens[cursor++];
9
+ };
10
+ const parsePrimary = () => {
11
+ const token = tokens[cursor];
12
+ if (!token)
13
+ throw new Error("unexpected end of formula");
14
+ if (token.type === "number") {
15
+ cursor += 1;
16
+ return { type: "number", value: Number(token.value) };
17
+ }
18
+ if (token.type === "identifier") {
19
+ cursor += 1;
20
+ return { type: "identifier", name: token.value };
21
+ }
22
+ if (token.value === "(") {
23
+ cursor += 1;
24
+ const expression = parseAdditive();
25
+ take(")");
26
+ return expression;
27
+ }
28
+ throw new Error(`unexpected token ${token.value}`);
29
+ };
30
+ const parseUnary = () => {
31
+ if (peek("+") || peek("-")) {
32
+ const operator = tokens[cursor++].value;
33
+ return { type: "unary", operator, argument: parseUnary() };
34
+ }
35
+ return parsePrimary();
36
+ };
37
+ const parseMultiplicative = () => {
38
+ let expression = parseUnary();
39
+ while (peek("*") || peek("/")) {
40
+ const operator = tokens[cursor++].value;
41
+ expression = { type: "binary", operator, left: expression, right: parseUnary() };
42
+ }
43
+ return expression;
44
+ };
45
+ const parseAdditive = () => {
46
+ let expression = parseMultiplicative();
47
+ while (peek("+") || peek("-")) {
48
+ const operator = tokens[cursor++].value;
49
+ expression = { type: "binary", operator, left: expression, right: parseMultiplicative() };
50
+ }
51
+ return expression;
52
+ };
53
+ if (!tokens.length)
54
+ throw new Error("formula must not be empty");
55
+ const expression = parseAdditive();
56
+ if (cursor !== tokens.length)
57
+ throw new Error(`unexpected token ${tokens[cursor].value}`);
58
+ return expression;
59
+ }
60
+ export function reportFormulaReferences(source) {
61
+ const references = new Set();
62
+ visit(parseReportFormula(source), (node) => {
63
+ if (node.type === "identifier")
64
+ references.add(node.name);
65
+ });
66
+ return [...references].sort();
67
+ }
68
+ export function evaluateReportFormula(expression, values) {
69
+ if (expression.type === "number")
70
+ return expression.value;
71
+ if (expression.type === "identifier")
72
+ return finiteNumber(values[expression.name]);
73
+ if (expression.type === "unary") {
74
+ const value = evaluateReportFormula(expression.argument, values);
75
+ if (value === null)
76
+ return null;
77
+ return expression.operator === "-" ? -value : value;
78
+ }
79
+ const left = evaluateReportFormula(expression.left, values);
80
+ const right = evaluateReportFormula(expression.right, values);
81
+ if (left === null || right === null)
82
+ return null;
83
+ const result = expression.operator === "+" ? left + right
84
+ : expression.operator === "-" ? left - right
85
+ : expression.operator === "*" ? left * right
86
+ : right === 0 ? null : left / right;
87
+ return typeof result === "number" && Number.isFinite(result) ? result : null;
88
+ }
89
+ function tokenize(source) {
90
+ const tokens = [];
91
+ let cursor = 0;
92
+ while (cursor < source.length) {
93
+ const char = source[cursor];
94
+ if (/\s/.test(char)) {
95
+ cursor += 1;
96
+ continue;
97
+ }
98
+ if ("+-*/()".includes(char)) {
99
+ tokens.push({ type: "operator", value: char });
100
+ cursor += 1;
101
+ continue;
102
+ }
103
+ const number = source.slice(cursor).match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/);
104
+ if (number) {
105
+ tokens.push({ type: "number", value: number[0] });
106
+ cursor += number[0].length;
107
+ continue;
108
+ }
109
+ const identifier = source.slice(cursor).match(/^[A-Za-z_][A-Za-z0-9_]*/);
110
+ if (identifier) {
111
+ tokens.push({ type: "identifier", value: identifier[0] });
112
+ cursor += identifier[0].length;
113
+ continue;
114
+ }
115
+ throw new Error(`unsupported formula token at position ${cursor}`);
116
+ }
117
+ return tokens;
118
+ }
119
+ function visit(node, callback) {
120
+ callback(node);
121
+ if (node.type === "unary")
122
+ visit(node.argument, callback);
123
+ if (node.type === "binary") {
124
+ visit(node.left, callback);
125
+ visit(node.right, callback);
126
+ }
127
+ }
128
+ function finiteNumber(value) {
129
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
130
+ }
@@ -0,0 +1,209 @@
1
+ import { executeReportPackV2View } from "./view-executor.js";
2
+ export async function queryReportPackV2Result(client, manifestResponse, options) {
3
+ const root = requiredObject(manifestResponse, "manifest response");
4
+ const manifest = requiredObject(root.manifest, "manifest");
5
+ if (manifest.schemaVersion !== 2)
6
+ throw new Error("Report Pack v2 manifest is required");
7
+ const selected = selectResultItems(manifest, options);
8
+ if (options.refresh && selected.standardCharts.length === 0) {
9
+ throw new Error("--refresh is only supported for standard Groups; custom v2 Views read materialized Dataset snapshots");
10
+ }
11
+ const fingerprints = new Set(selected.views.map(({ view }) => viewDatasetFingerprint(view)));
12
+ const datasets = new Map();
13
+ const standardPromise = selected.standardCharts.length
14
+ ? client.queryReportDashboard({
15
+ startDate: options.startDate,
16
+ endDate: options.endDate,
17
+ platform: options.platform,
18
+ selectors: options.selectors,
19
+ refresh: Boolean(options.refresh),
20
+ responseMode: "full",
21
+ ...queryScope(options),
22
+ }, { timeoutMs: options.timeoutMs })
23
+ : Promise.resolve(null);
24
+ const [, standardPayload] = await Promise.all([
25
+ Promise.all([...fingerprints].map(async (datasetFingerprint) => {
26
+ const payload = requiredObject(await client.readReportDataset({
27
+ datasetFingerprint,
28
+ platform: options.platform,
29
+ startDate: options.startDate,
30
+ endDate: options.endDate,
31
+ }), `Dataset ${datasetFingerprint}`);
32
+ datasets.set(datasetFingerprint, payload);
33
+ })),
34
+ standardPromise,
35
+ ]);
36
+ const viewResults = selected.views.map(({ tab, group, view }) => {
37
+ const fingerprint = viewDatasetFingerprint(view);
38
+ const dataset = datasets.get(fingerprint);
39
+ const status = String(dataset.status || "waiting");
40
+ if (status === "abnormal") {
41
+ return reportResult(tab, group, view, fingerprint, [], [], {
42
+ error: String(dataset.abnormalMessage || dataset.abnormalCode || "Dataset materialization is abnormal"),
43
+ });
44
+ }
45
+ const data = requiredObject(view.data, "view.data");
46
+ const rows = status === "waiting"
47
+ ? []
48
+ : executeReportPackV2View(view, recordRows(dataset.rows), {
49
+ selectorFilters: selectorFilters(group, data, options.selectors, recordRows(dataset.rows)),
50
+ });
51
+ const columns = [
52
+ ...stringArray(data.groupBy, "view.data.groupBy"),
53
+ ...Object.keys(requiredObject(data.metrics, "view.data.metrics")),
54
+ ];
55
+ return reportResult(tab, group, view, fingerprint, columns, rows, {
56
+ cached: true,
57
+ cachedAt: dataset.updatedAt,
58
+ pending: status === "waiting" || undefined,
59
+ partial: status === "partial" || undefined,
60
+ missingDates: dataset.missingDates,
61
+ snapshotVersion: dataset.snapshotVersion,
62
+ });
63
+ });
64
+ const standardResults = standardPayload
65
+ ? arrayValues(requiredObject(standardPayload, "standard report result").results)
66
+ : [];
67
+ return {
68
+ reportPack: root.reportPack,
69
+ dateRange: { startDate: options.startDate, endDate: options.endDate, platform: options.platform },
70
+ selectors: options.selectors,
71
+ results: [...standardResults, ...viewResults],
72
+ };
73
+ }
74
+ function selectResultItems(manifest, options) {
75
+ const views = [];
76
+ const standardCharts = [];
77
+ const groups = [];
78
+ arrayValues(manifest.tabs).forEach((rawTab) => {
79
+ const tab = requiredObject(rawTab, "manifest tab");
80
+ arrayValues(tab.groups).forEach((rawGroup) => {
81
+ const group = requiredObject(rawGroup, "manifest group");
82
+ groups.push({ tab, group });
83
+ arrayValues(group.views).forEach((rawView) => {
84
+ views.push({ tab, group, view: requiredObject(rawView, "manifest view") });
85
+ });
86
+ arrayValues(group.charts).forEach((rawChart) => {
87
+ standardCharts.push({ tab, group, chart: requiredObject(rawChart, "manifest chart") });
88
+ });
89
+ });
90
+ });
91
+ const chart = options.chartId || options.chart;
92
+ if (chart) {
93
+ const matchingViews = views.filter(({ group, view }) => matchesValue(chart, [
94
+ view.id,
95
+ view.title,
96
+ `${group.id}/${view.id}`,
97
+ `${group.id}__${view.id}`,
98
+ ]));
99
+ const matchingCharts = standardCharts.filter(({ group, chart: standardChart }) => matchesValue(chart, [
100
+ standardChart.id,
101
+ standardChart.title,
102
+ `${group.id}/${standardChart.id}`,
103
+ `${group.id}__${standardChart.id}`,
104
+ ]));
105
+ if (matchingViews.length + matchingCharts.length === 0)
106
+ throw new Error(`chart not found: ${chart}`);
107
+ if (matchingViews.length + matchingCharts.length > 1) {
108
+ const ids = [
109
+ ...matchingViews.map(({ group, view }) => `${String(group.id)}/${String(view.id)}`),
110
+ ...matchingCharts.map(({ group, chart: item }) => `${String(group.id)}/${String(item.id)}`),
111
+ ];
112
+ throw new Error(`ambiguous_chart_id: ${chart}; candidates: ${ids.join(", ")}`);
113
+ }
114
+ return { views: matchingViews, standardCharts: matchingCharts };
115
+ }
116
+ const group = options.groupId || options.group;
117
+ if (group) {
118
+ const matchingGroups = groups.filter((entry) => matches(group, [entry.group.id, entry.group.title]));
119
+ if (!matchingGroups.length)
120
+ throw new Error(`group not found: ${group}`);
121
+ const selectedGroups = new Set(matchingGroups.map((entry) => entry.group));
122
+ return {
123
+ views: views.filter((entry) => selectedGroups.has(entry.group)),
124
+ standardCharts: standardCharts.filter((entry) => selectedGroups.has(entry.group)),
125
+ };
126
+ }
127
+ const tab = options.tabId || options.tab;
128
+ if (tab) {
129
+ const matchingTabs = new Set(groups
130
+ .filter((entry) => matches(tab, [entry.tab.id, entry.tab.title]))
131
+ .map((entry) => entry.tab));
132
+ if (!matchingTabs.size)
133
+ throw new Error(`tab not found: ${tab}`);
134
+ return {
135
+ views: views.filter((entry) => matchingTabs.has(entry.tab)),
136
+ standardCharts: standardCharts.filter((entry) => matchingTabs.has(entry.tab)),
137
+ };
138
+ }
139
+ throw new Error("report result requires one of --tab, --tab-id, --group, --group-id, --chart, or --chart-id");
140
+ }
141
+ function queryScope(options) {
142
+ return Object.fromEntries(Object.entries({
143
+ tab: options.tab,
144
+ tabId: options.tabId,
145
+ group: options.group,
146
+ groupId: options.groupId,
147
+ chart: options.chart,
148
+ chartId: options.chartId,
149
+ }).filter((entry) => typeof entry[1] === "string"));
150
+ }
151
+ function matches(query, candidates) {
152
+ return matchesValue(query, candidates);
153
+ }
154
+ function matchesValue(query, candidates) {
155
+ return candidates.some((candidate) => typeof candidate === "string" && candidate.toLocaleLowerCase() === query.toLocaleLowerCase());
156
+ }
157
+ function selectorFilters(group, data, values, rows) {
158
+ const bindings = objectValue(data.selectorBindings) ?? {};
159
+ const selectors = arrayValues(group.selectors).map((value) => requiredObject(value, "selector"));
160
+ return Object.entries(bindings).flatMap(([selectorId, rawField]) => {
161
+ const selector = selectors.find((item) => item.id === selectorId);
162
+ const value = values[selectorId] ?? (typeof selector?.defaultValue === "string" ? selector.defaultValue : "all");
163
+ if (!value || value === "all")
164
+ return [];
165
+ const field = String(rawField);
166
+ const sample = rows.find((row) => row[field] !== undefined)?.[field];
167
+ const typed = typeof sample === "number" && Number.isFinite(Number(value))
168
+ ? Number(value)
169
+ : typeof sample === "boolean" ? value === "true" : value;
170
+ return [{ field, op: "eq", value: typed }];
171
+ });
172
+ }
173
+ function reportResult(tab, group, view, fingerprint, columns, rows, extra) {
174
+ return {
175
+ tab: { id: tab.id, title: tab.title },
176
+ group: { id: group.id, title: group.title },
177
+ chart: view,
178
+ reportId: `dataset:${fingerprint}`,
179
+ columns,
180
+ rows,
181
+ ...extra,
182
+ };
183
+ }
184
+ function viewDatasetFingerprint(view) {
185
+ const fingerprint = requiredObject(view.data, "view.data").datasetFingerprint;
186
+ if (typeof fingerprint !== "string" || !fingerprint)
187
+ throw new Error("v2 View is missing datasetFingerprint");
188
+ return fingerprint;
189
+ }
190
+ function recordRows(value) {
191
+ return arrayValues(value).map((row) => requiredObject(row, "Dataset row"));
192
+ }
193
+ function requiredObject(value, path) {
194
+ const object = objectValue(value);
195
+ if (!object)
196
+ throw new Error(`${path} must be an object`);
197
+ return object;
198
+ }
199
+ function objectValue(value) {
200
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
201
+ }
202
+ function arrayValues(value) {
203
+ return Array.isArray(value) ? value : [];
204
+ }
205
+ function stringArray(value, path) {
206
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
207
+ throw new Error(`${path} must be an array of strings`);
208
+ return value;
209
+ }
@@ -0,0 +1,18 @@
1
+ export function reportPackSchemaVersion(value) {
2
+ const pack = objectValue(value);
3
+ if (!pack)
4
+ throw new Error("Report Pack must be a JSON object");
5
+ if (pack.schemaVersion === undefined)
6
+ return 1;
7
+ if (pack.schemaVersion === 2)
8
+ return 2;
9
+ throw new Error(`unsupported Report Pack schemaVersion: ${String(pack.schemaVersion)}`);
10
+ }
11
+ export function assertWritableReportPackV2(value, command) {
12
+ if (reportPackSchemaVersion(value) !== 2) {
13
+ throw new Error(`${command} only accepts Report Pack schemaVersion 2; pull the current Pack and migrate it before writing`);
14
+ }
15
+ }
16
+ function objectValue(value) {
17
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
18
+ }
@@ -0,0 +1,110 @@
1
+ import { REPORT_SUMMARY_MAX_ITEMS, REPORT_SUMMARY_SCHEMA_VERSION } from "../report-summary.js";
2
+ export function summarizeReportPackV2Manifest(value) {
3
+ const root = objectValue(value);
4
+ const manifest = objectValue(root?.manifest);
5
+ if (!manifest || manifest.schemaVersion !== 2) {
6
+ return {
7
+ schemaVersion: REPORT_SUMMARY_SCHEMA_VERSION,
8
+ command: "report.manifest",
9
+ ok: false,
10
+ summary: {},
11
+ errors: [{ scope: "manifest", message: "Report Pack v2 manifest is missing" }],
12
+ warnings: [],
13
+ items: [],
14
+ truncated: false,
15
+ };
16
+ }
17
+ const tabs = arrayValues(manifest.tabs);
18
+ const advancedQueries = arrayValues(manifest.advancedQueries);
19
+ const allItems = [];
20
+ let groupCount = 0;
21
+ let viewCount = 0;
22
+ let standardChartCount = 0;
23
+ for (const [scope, containers] of [["dashboard", tabs], ["advanced", advancedQueries]]) {
24
+ for (const rawContainer of containers) {
25
+ const container = objectValue(rawContainer);
26
+ for (const rawGroup of arrayValues(container?.groups)) {
27
+ const group = objectValue(rawGroup);
28
+ groupCount += 1;
29
+ for (const rawView of arrayValues(group?.views)) {
30
+ const view = objectValue(rawView);
31
+ const data = objectValue(view?.data);
32
+ viewCount += 1;
33
+ allItems.push(compact({
34
+ kind: "view",
35
+ scope,
36
+ containerId: text(container?.id),
37
+ containerTitle: text(container?.title),
38
+ groupId: text(group?.id),
39
+ groupTitle: text(group?.title),
40
+ viewId: text(view?.id),
41
+ viewTitle: text(view?.title),
42
+ viewType: text(view?.type),
43
+ datasetId: text(data?.dataset),
44
+ datasetFingerprint: text(data?.datasetFingerprint),
45
+ selectorIds: arrayValues(group?.selectors).flatMap((rawSelector) => {
46
+ const id = text(objectValue(rawSelector)?.id);
47
+ return id ? [id] : [];
48
+ }),
49
+ }));
50
+ }
51
+ if (scope === "dashboard") {
52
+ for (const rawChart of arrayValues(group?.charts)) {
53
+ const chart = objectValue(rawChart);
54
+ standardChartCount += 1;
55
+ allItems.push(compact({
56
+ kind: "standard_chart",
57
+ scope,
58
+ containerId: text(container?.id),
59
+ containerTitle: text(container?.title),
60
+ groupId: text(group?.id),
61
+ groupTitle: text(group?.title),
62
+ standardRef: text(group?.standardRef),
63
+ chartId: text(chart?.id),
64
+ chartTitle: text(chart?.title),
65
+ chartType: text(chart?.type),
66
+ reportId: text(chart?.report),
67
+ }));
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ const datasets = objectValue(manifest.datasets) ?? {};
74
+ const items = allItems.slice(0, REPORT_SUMMARY_MAX_ITEMS);
75
+ return {
76
+ schemaVersion: REPORT_SUMMARY_SCHEMA_VERSION,
77
+ command: "report.manifest",
78
+ ok: true,
79
+ summary: compact({
80
+ reportPack: root?.reportPack,
81
+ reportPackSchemaVersion: 2,
82
+ title: text(manifest.title),
83
+ packFingerprint: text(manifest.packFingerprint),
84
+ tabCount: tabs.length,
85
+ advancedQueryCount: advancedQueries.length,
86
+ groupCount,
87
+ viewCount,
88
+ standardChartCount,
89
+ datasetCount: Object.keys(datasets).length,
90
+ datasetIds: Object.keys(datasets),
91
+ returnedItemCount: items.length,
92
+ }),
93
+ errors: [],
94
+ warnings: [],
95
+ items,
96
+ truncated: allItems.length > items.length,
97
+ };
98
+ }
99
+ function objectValue(value) {
100
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
101
+ }
102
+ function arrayValues(value) {
103
+ return Array.isArray(value) ? value : [];
104
+ }
105
+ function text(value) {
106
+ return typeof value === "string" && value ? value : undefined;
107
+ }
108
+ function compact(value) {
109
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
110
+ }
@@ -0,0 +1,198 @@
1
+ import { evaluateReportFormula, parseReportFormula, reportFormulaReferences } from "./expression.js";
2
+ const BASE_AGGREGATIONS = new Set(["sum", "min", "max", "count"]);
3
+ /** Pure v2 evaluator. It intentionally has no dependency on legacy reports. */
4
+ export function executeReportPackV2View(rawView, datasetRows, options = {}) {
5
+ const view = requiredObject(rawView, "view");
6
+ const data = requiredObject(view.data, "view.data");
7
+ const groupBy = stringArray(data.groupBy, "view.data.groupBy");
8
+ const metrics = requiredObject(data.metrics, "view.data.metrics");
9
+ if (!Object.keys(metrics).length)
10
+ throw new Error("view.data.metrics must not be empty");
11
+ const selectorFilters = Array.isArray(options.selectorFilters) ? options.selectorFilters : [];
12
+ const filteredRows = datasetRows.filter((row) => (matchesFilter(row, data.filter)
13
+ && selectorFilters.every((filter) => matchesFilter(row, filter))));
14
+ const groups = groupRows(filteredRows, groupBy);
15
+ const formulas = prepareFormulas(metrics);
16
+ const rows = [];
17
+ for (const group of groups.values()) {
18
+ const output = { ...group.dimensions };
19
+ for (const [metricId, rawDefinition] of Object.entries(metrics)) {
20
+ const definition = objectValue(rawDefinition);
21
+ if (definition?.agg)
22
+ output[metricId] = aggregateMetric(group.rows, metricId, definition);
23
+ }
24
+ evaluateFormulas(output, formulas);
25
+ rows.push(output);
26
+ }
27
+ if (!filteredRows.length && !groupBy.length) {
28
+ const output = {};
29
+ for (const [metricId, rawDefinition] of Object.entries(metrics)) {
30
+ const definition = objectValue(rawDefinition);
31
+ if (definition?.agg)
32
+ output[metricId] = aggregateMetric([], metricId, definition);
33
+ }
34
+ evaluateFormulas(output, formulas);
35
+ rows.push(output);
36
+ }
37
+ const orderBy = normalizeOrderBy(view.orderBy);
38
+ if (orderBy.length) {
39
+ const stable = [
40
+ ...orderBy,
41
+ ...groupBy.filter((field) => !orderBy.some((order) => order.field === field)).map((field) => ({ field, direction: "asc" })),
42
+ ];
43
+ rows.sort((left, right) => compareRows(left, right, stable));
44
+ }
45
+ const limit = view.limit === undefined ? undefined : positiveInteger(view.limit, "view.limit");
46
+ return limit === undefined ? rows : rows.slice(0, limit);
47
+ }
48
+ function groupRows(rows, groupBy) {
49
+ const groups = new Map();
50
+ for (const row of rows) {
51
+ const dimensions = Object.fromEntries(groupBy.map((field) => [field, row[field] ?? null]));
52
+ const key = JSON.stringify(groupBy.map((field) => [typeof row[field], row[field] ?? null]));
53
+ const existing = groups.get(key);
54
+ if (existing)
55
+ existing.rows.push(row);
56
+ else
57
+ groups.set(key, { dimensions, rows: [row] });
58
+ }
59
+ return groups;
60
+ }
61
+ function aggregateMetric(rows, metricId, definition) {
62
+ const aggregation = String(definition.agg || "");
63
+ if (!BASE_AGGREGATIONS.has(aggregation))
64
+ throw new Error(`unsupported aggregation for ${metricId}: ${aggregation}`);
65
+ if (aggregation === "count")
66
+ return rows.length;
67
+ const field = String(definition.field || "");
68
+ if (!field)
69
+ throw new Error(`view.data.metrics.${metricId}.field is required`);
70
+ const values = rows.flatMap((row) => typeof row[field] === "number" && Number.isFinite(row[field]) ? [row[field]] : []);
71
+ if (aggregation === "sum")
72
+ return values.reduce((sum, value) => sum + value, 0);
73
+ if (!values.length)
74
+ return null;
75
+ return aggregation === "min" ? Math.min(...values) : Math.max(...values);
76
+ }
77
+ function prepareFormulas(metrics) {
78
+ const formulas = new Map();
79
+ for (const [metricId, rawDefinition] of Object.entries(metrics)) {
80
+ const definition = objectValue(rawDefinition);
81
+ if (typeof definition?.formula !== "string")
82
+ continue;
83
+ formulas.set(metricId, {
84
+ expression: parseReportFormula(definition.formula),
85
+ references: reportFormulaReferences(definition.formula),
86
+ });
87
+ }
88
+ return formulas;
89
+ }
90
+ function evaluateFormulas(output, formulas) {
91
+ const pending = new Map(formulas);
92
+ while (pending.size) {
93
+ let progressed = false;
94
+ for (const [metricId, formula] of pending) {
95
+ if (formula.references.some((reference) => pending.has(reference)))
96
+ continue;
97
+ output[metricId] = evaluateReportFormula(formula.expression, output);
98
+ pending.delete(metricId);
99
+ progressed = true;
100
+ }
101
+ if (!progressed)
102
+ throw new Error(`formula dependency cycle: ${[...pending.keys()].sort().join(", ")}`);
103
+ }
104
+ }
105
+ function matchesFilter(row, rawFilter) {
106
+ if (rawFilter === undefined || rawFilter === null)
107
+ return true;
108
+ const filter = requiredObject(rawFilter, "filter");
109
+ const op = String(filter.op || "");
110
+ if (op === "and" || op === "or") {
111
+ if (!Array.isArray(filter.filters))
112
+ throw new Error(`filter ${op} requires filters`);
113
+ return op === "and"
114
+ ? filter.filters.every((child) => matchesFilter(row, child))
115
+ : filter.filters.some((child) => matchesFilter(row, child));
116
+ }
117
+ if (op === "not")
118
+ return !matchesFilter(row, filter.filter);
119
+ const field = String(filter.field || "");
120
+ if (!field)
121
+ throw new Error("filter.field is required");
122
+ const actual = row[field] ?? null;
123
+ if (op === "eq")
124
+ return Object.is(actual, filter.value ?? null);
125
+ if (op === "ne")
126
+ return !Object.is(actual, filter.value ?? null);
127
+ if (["gt", "gte", "lt", "lte"].includes(op) && (actual === null || filter.value === null || filter.value === undefined))
128
+ return false;
129
+ if (op === "gt")
130
+ return compareValues(actual, filter.value) > 0;
131
+ if (op === "gte")
132
+ return compareValues(actual, filter.value) >= 0;
133
+ if (op === "lt")
134
+ return compareValues(actual, filter.value) < 0;
135
+ if (op === "lte")
136
+ return compareValues(actual, filter.value) <= 0;
137
+ if (op === "in" || op === "not_in") {
138
+ if (!Array.isArray(filter.values))
139
+ throw new Error(`filter ${op} requires values`);
140
+ const included = filter.values.some((value) => Object.is(actual, value));
141
+ return op === "in" ? included : !included;
142
+ }
143
+ if (op === "is_null")
144
+ return actual === null;
145
+ if (op === "is_not_null")
146
+ return actual !== null;
147
+ throw new Error(`unsupported filter operation: ${op}`);
148
+ }
149
+ function normalizeOrderBy(value) {
150
+ if (value === undefined)
151
+ return [];
152
+ return (Array.isArray(value) ? value : [value]).map((raw, index) => {
153
+ const item = requiredObject(raw, `view.orderBy[${index}]`);
154
+ const field = String(item.field || "");
155
+ const direction = String(item.direction || "asc");
156
+ if (!field || (direction !== "asc" && direction !== "desc"))
157
+ throw new Error(`invalid view.orderBy[${index}]`);
158
+ return { field, direction };
159
+ });
160
+ }
161
+ function compareRows(left, right, orders) {
162
+ for (const order of orders) {
163
+ const result = compareValues(left[order.field], right[order.field]);
164
+ if (result)
165
+ return order.direction === "desc" ? -result : result;
166
+ }
167
+ return 0;
168
+ }
169
+ function compareValues(left, right) {
170
+ if (Object.is(left, right))
171
+ return 0;
172
+ if (left === null || left === undefined)
173
+ return 1;
174
+ if (right === null || right === undefined)
175
+ return -1;
176
+ if (typeof left === "number" && typeof right === "number")
177
+ return left - right;
178
+ return String(left).localeCompare(String(right));
179
+ }
180
+ function requiredObject(value, path) {
181
+ const object = objectValue(value);
182
+ if (!object)
183
+ throw new Error(`${path} must be an object`);
184
+ return object;
185
+ }
186
+ function objectValue(value) {
187
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
188
+ }
189
+ function stringArray(value, path) {
190
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item))
191
+ throw new Error(`${path} must be an array of strings`);
192
+ return value;
193
+ }
194
+ function positiveInteger(value, path) {
195
+ if (!Number.isInteger(value) || Number(value) <= 0)
196
+ throw new Error(`${path} must be a positive integer`);
197
+ return Number(value);
198
+ }
package/dist/utils.js CHANGED
@@ -10,15 +10,15 @@ export function integrationEnvironment(value) {
10
10
  }
11
11
  export function integrationPlatform(value) {
12
12
  const platform = optionalString(value);
13
- if (platform === "ios" || platform === "android" || platform === "maker" || platform === "rest")
13
+ if (platform === "ios" || platform === "android" || platform === "maker" || platform === "web")
14
14
  return platform;
15
- throw new Error("--platform must be ios, android, maker, or rest");
15
+ throw new Error("--platform must be ios, android, maker, or web");
16
16
  }
17
17
  export function experimentPlatform(value) {
18
18
  const platform = optionalString(value);
19
- if (platform === "ios" || platform === "android" || platform === "rest")
19
+ if (platform === "ios" || platform === "android" || platform === "maker" || platform === "web")
20
20
  return platform;
21
- throw new Error("--platform must be ios, android, or rest for experiments");
21
+ throw new Error("--platform must be ios, android, maker, or web for experiments");
22
22
  }
23
23
  export function optionalString(value) {
24
24
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gamealgo-cli",
3
- "version": "0.3.9",
3
+ "version": "0.4.2",
4
4
  "description": "GameAlgo command-line client for AI agents and game developers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@ Classify the requested work by the actions it requires, not only by the user's t
34
34
 
35
35
  Use each topic's section list as the router. Read the whole topic only when the task genuinely spans most of it; when that is necessary, load one full topic per tool call. If any tool marks documentation output as truncated, treat it as incomplete, retrieve the missing sections, and do not infer that unseen guidance is absent or switch workflows based on the partial output.
36
36
 
37
- - SDK installation, identity, lifecycle, telemetry, attribution, advertising, or purchase work: run `gamealgo integration get-plan --environment <domestic|overseas> --platform <ios|android|maker|rest> --json`, then list the `sdk-integration` sections and read the relevant shared and platform-specific sections.
37
+ - SDK installation, identity, lifecycle, telemetry, attribution, advertising, or purchase work: run `gamealgo integration get-plan --environment <domestic|overseas> --platform <ios|android|maker|web> --json`, then list the `sdk-integration` sections and read the relevant shared and platform-specific sections.
38
38
  - Calling or changing a concrete public SDK function: additionally run `gamealgo docs api <function> --platform <platform>` for each affected function. Never infer one platform's API from another.
39
39
  - Adding or changing events: additionally run `gamealgo events list --days 30 --json` before defining fields or names.
40
40
  - Querying an existing dashboard or report: read `gamealgo docs reports --section report-query` plus the section that defines the relevant metric or template. Creating, changing, previewing, or publishing Report Packs, charts, report SQL, or business metrics: list the `reports` sections and read every section affected by the change before interpreting data or editing definitions.
@@ -47,7 +47,17 @@ For `report validate`, `report manifest`, `report preview`, and JSON `report res
47
47
 
48
48
  When changing an existing Report Pack, run `gamealgo report pull --out <working-pack.json>` before editing and use that current published Pack as the baseline. Do not start from a remembered or stale local copy. Run `gamealgo events diff --pack <working-pack.json>` after defining the Pack events; if it reports missing fields, type conflicts, or blocked/deprecated events, inspect the actual game call sites and prepare real business descriptions rather than inventing catalog text from field names. The diff is read-only and does not authorize `events apply` or event activation.
49
49
 
50
- Validate with the compact default output, then preview every chart or group affected by the change without querying unrelated tabs. A report query must explicitly select a tab, group, or chart. A globally unique bare chart id is accepted; if the Server returns `ambiguous_chart_id`, use its candidate ids and the local Pack structure to retry without asking the developer to choose an internal chart id. Before applying event metadata or publishing the Pack, show the exact changes and impact unless the developer already authorized them. After publishing, pull the effective Pack again and verify the expected manifest or report result.
50
+ Validate with the compact default output, then use `report preview` to inspect v2 validation and generated Dataset SQL without running a warehouse query. After publishing, verify only the affected tab, group, or View with `report result`; do not query unrelated tabs. A report result request must explicitly select a tab, group, or chart/View. A globally unique bare id is accepted; if the Server returns an ambiguity error, use its candidate ids and the local Pack structure to retry without asking the developer to choose an internal id. Before applying event metadata or publishing the Pack, show the exact changes and impact unless the developer already authorized them. After publishing, pull the effective Pack again and verify the expected manifest and materialized result state.
51
+
52
+ ### Use Report Pack v2
53
+
54
+ Every Report Pack must declare `schemaVersion: 2`. v2 uses independent `datasets`, inline `groups[].views`, platform-owned `groups[].standard`, `tabs`, and optional `experimentViews` and `advancedQueries`. It has no top-level `reports` or `charts` and no author-managed Pack version. Generate, explain, validate, preview, and publish only this structure; do not expose migration-era formats to the developer.
55
+
56
+ An ordinary v2 Dataset must read only `adn.dwd_gamealgo_events_enriched_di`, include `dt` in its output grain, and be independently computable for each natural day. Before selector filtering, each `datasetFingerprint × platform × dt` must remain at or below 100 rows and 128 KiB. Estimate the output cardinality from every `groupBy` dimension before publishing. If it may exceed either limit, reduce or bucket dimensions, split the business question, or move it into `advancedQueries`; a table View, front-end selector, or `dataset.limit` does not bypass the ordinary limit.
57
+
58
+ Keep ratio and average components as additive Dataset columns and calculate the final value in the View after range aggregation. For example, materialize `wins` and `attempts`, then define `win_rate = wins / attempts` in `groups[].views[].data.metrics`. Each View references exactly one Dataset. A selector switch is a local View recalculation and must not be modeled as a new warehouse request.
59
+
60
+ Use `gamealgo report result` to read an existing ordinary v2 materialization for an explicit date range and scope. It never starts a DW query. Use `gamealgo report advanced run --query-id <id> --platform <platform> --from <date> --to <date>` only when an explicit high-dimensional or non-preloaded query is required. `--pack <file>` supplies an immutable v2 Pack snapshot for that Run without publishing it. `advanced get` and `advanced list` only read the saved Run/history. Advanced results are whole-run bounded to 20,000 rows and 20 MiB and retained for 30 days; never replace this path with local production scans or a hidden query side channel.
51
61
 
52
62
  For initial integration or business dashboard work, standard dashboards are only the baseline. During the initial integration, inspect the actual game loop and define a separate game-specific new-user flow dashboard with meaningful milestones and drop-off diagnostics. Add required milestone telemetry and event catalog fields in the same integration instead of waiting for production volume. Validate the pipeline with test data; label business conclusions as awaiting data when production samples are immature, but do not omit the dashboard design.
53
63
 
@@ -79,7 +89,7 @@ Reading documentation does not grant permission to mutate production. CLI and Se
79
89
  For SDK integration or telemetry changes, determine the actual deployment environment and runtime platform, then run:
80
90
 
81
91
  ```bash
82
- gamealgo integration get-plan --environment <domestic|overseas> --platform <ios|android|maker|rest> --json
92
+ gamealgo integration get-plan --environment <domestic|overseas> --platform <ios|android|maker|web> --json
83
93
  ```
84
94
 
85
95
  Inspect the existing project before editing. Reuse existing SDK wrappers, user identity, lifecycle, event, advertising, purchase, and attribution flows.
@@ -166,11 +176,11 @@ Before delivery, inspect changed client code for Variant or assignment-identity
166
176
 
167
177
  ## Preserve Experiment Platform Scope
168
178
 
169
- A Strategy key is shared by the game and keeps a global fallback default, while every Run belongs to exactly one runtime platform: `ios`, `android`, or `rest` (`rest` includes TapTap Maker). A promoted Run updates only its own platform default. Other platform defaults remain unchanged, and a platform without an explicit default continues to use the global fallback.
179
+ A Strategy key is shared by the game and keeps a global fallback default, while every Run belongs to exactly one runtime platform: `ios`, `android`, `maker`, or `web`. A promoted Run updates only its own platform default. Other platform defaults remain unchanged, and a platform without an explicit default continues to use the global fallback.
170
180
 
171
181
  - When a Run includes `variantId: default`, the Server snapshots the effective default for that Run's platform, falling back to the global Strategy default only when that platform has no promoted default.
172
182
  - `experiment run promote` needs no platform flag because the Run records its platform.
173
- - When rolling a platform back to a global historical version, pass `experiment strategy rollback ... --platform <ios|android|rest>` so the global version is copied into that platform default rather than replacing the global fallback.
183
+ - When rolling a platform back to a global historical version, pass `experiment strategy rollback ... --platform <ios|android|maker|web>` so the global version is copied into that platform default rather than replacing the global fallback.
174
184
  - Treat `experiment strategy publish` and `experiment strategy default` as global-fallback operations; they do not overwrite existing promoted platform defaults.
175
185
 
176
186
  ## Require Explicit Approval Before Publishing Experiments