gamealgo-cli 0.3.5 → 0.3.7
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 +2 -0
- package/dist/client.js +24 -0
- package/dist/commands/dynamic-config.js +71 -0
- package/dist/commands/events.js +25 -1
- package/dist/commands/report.js +99 -21
- package/dist/docs.js +154 -1
- package/dist/main.js +5 -0
- package/dist/report-csv.js +87 -0
- package/package.json +1 -1
- package/skills/gamealgo/SKILL.md +69 -5
package/README.md
CHANGED
package/dist/client.js
CHANGED
|
@@ -133,6 +133,21 @@ export class GameAlgoAdminClient {
|
|
|
133
133
|
async getScriptVersion(name, versionId) {
|
|
134
134
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/scripts/${encodeURIComponent(name)}/versions/${encodeURIComponent(versionId)}`);
|
|
135
135
|
}
|
|
136
|
+
async listDynamicConfigs() {
|
|
137
|
+
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs`);
|
|
138
|
+
}
|
|
139
|
+
async getDynamicConfig(key) {
|
|
140
|
+
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs/${encodeURIComponent(key)}`);
|
|
141
|
+
}
|
|
142
|
+
async putDynamicConfig(key, body) {
|
|
143
|
+
return await this.put(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs/${encodeURIComponent(key)}`, body);
|
|
144
|
+
}
|
|
145
|
+
async queueDynamicConfigRun(key) {
|
|
146
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs/${encodeURIComponent(key)}/run`, {});
|
|
147
|
+
}
|
|
148
|
+
async listDynamicConfigRuns(key, limit = 50) {
|
|
149
|
+
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs/${encodeURIComponent(key)}/runs?limit=${encodeURIComponent(String(limit))}`);
|
|
150
|
+
}
|
|
136
151
|
async getReportPack() {
|
|
137
152
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-pack`);
|
|
138
153
|
}
|
|
@@ -173,6 +188,15 @@ export class GameAlgoAdminClient {
|
|
|
173
188
|
async deprecateEventCatalog(eventType) {
|
|
174
189
|
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog/${encodeURIComponent(eventType)}/deprecate`, {});
|
|
175
190
|
}
|
|
191
|
+
async blockEventCatalog(eventType, reason) {
|
|
192
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog/${encodeURIComponent(eventType)}/block`, { reason });
|
|
193
|
+
}
|
|
194
|
+
async activateEventCatalog(eventType, reason, confirmEventType) {
|
|
195
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog/${encodeURIComponent(eventType)}/activate`, {
|
|
196
|
+
reason,
|
|
197
|
+
confirmEventType,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
176
200
|
async getIntegrationPlan(environment, platform) {
|
|
177
201
|
const params = new URLSearchParams({ environment, platform });
|
|
178
202
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/integration/plan?${params.toString()}`);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { parseFlags } from "../args.js";
|
|
4
|
+
import { printResult } from "../output.js";
|
|
5
|
+
import { contentTypeForFileName, objectValue, readJsonFile, sanitizeRemoteFileName } from "../resources.js";
|
|
6
|
+
import { announceTargetGame } from "../target.js";
|
|
7
|
+
import { optionalString } from "../utils.js";
|
|
8
|
+
export async function handleDynamicConfig(client, args, global) {
|
|
9
|
+
const sub = args.shift();
|
|
10
|
+
if (sub === "list") {
|
|
11
|
+
parseFlags(args);
|
|
12
|
+
await printResult(await client.listDynamicConfigs(), global);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (sub === "get") {
|
|
16
|
+
const key = args.shift();
|
|
17
|
+
if (!key)
|
|
18
|
+
throw new Error("usage: gamealgo dynamic-config get <key>");
|
|
19
|
+
parseFlags(args);
|
|
20
|
+
await printResult(await client.getDynamicConfig(key), global);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (sub === "runs") {
|
|
24
|
+
const key = args.shift();
|
|
25
|
+
if (!key)
|
|
26
|
+
throw new Error("usage: gamealgo dynamic-config runs <key> [--limit 50]");
|
|
27
|
+
const flags = parseFlags(args);
|
|
28
|
+
await printResult(await client.listDynamicConfigRuns(key, Number(flags.limit || 50)), global);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (sub === "run") {
|
|
32
|
+
const key = args.shift();
|
|
33
|
+
if (!key)
|
|
34
|
+
throw new Error("usage: gamealgo dynamic-config run <key>");
|
|
35
|
+
parseFlags(args);
|
|
36
|
+
await announceTargetGame(client);
|
|
37
|
+
await printResult(await client.queueDynamicConfigRun(key), global);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (sub === "publish") {
|
|
41
|
+
const definitionPath = args.shift();
|
|
42
|
+
if (!definitionPath) {
|
|
43
|
+
throw new Error("usage: gamealgo dynamic-config publish <definition.json> [--script transform.js] [--message text]");
|
|
44
|
+
}
|
|
45
|
+
const flags = parseFlags(args);
|
|
46
|
+
const definition = objectValue(await readJsonFile(definitionPath));
|
|
47
|
+
if (!definition)
|
|
48
|
+
throw new Error("dynamic config definition must be a JSON object");
|
|
49
|
+
const key = optionalString(definition.key);
|
|
50
|
+
if (!key)
|
|
51
|
+
throw new Error("dynamic config definition.key is required");
|
|
52
|
+
const body = { ...definition };
|
|
53
|
+
delete body.key;
|
|
54
|
+
const scriptPath = optionalString(flags.script);
|
|
55
|
+
await announceTargetGame(client);
|
|
56
|
+
if (scriptPath) {
|
|
57
|
+
const scriptName = sanitizeRemoteFileName(optionalString(definition.scriptName) || basename(scriptPath));
|
|
58
|
+
const content = await readFile(scriptPath, "utf8");
|
|
59
|
+
const created = await client.createScriptVersion(scriptName, content, contentTypeForFileName(scriptName), optionalString(flags.message));
|
|
60
|
+
body.scriptVersionId = created.scriptVersion.versionId;
|
|
61
|
+
}
|
|
62
|
+
delete body.scriptName;
|
|
63
|
+
if (!optionalString(body.scriptVersionId)) {
|
|
64
|
+
throw new Error("definition.scriptVersionId or --script is required");
|
|
65
|
+
}
|
|
66
|
+
const response = await client.putDynamicConfig(key, body);
|
|
67
|
+
await printResult({ ok: true, ...response }, global);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw new Error("usage: gamealgo dynamic-config <list|get|publish|run|runs>");
|
|
71
|
+
}
|
package/dist/commands/events.js
CHANGED
|
@@ -41,8 +41,32 @@ export async function handleEvents(client, args, global) {
|
|
|
41
41
|
await writeEventCatalogOutput(await client.deprecateEventCatalog(eventType), flags, global);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
|
+
if (sub === "block") {
|
|
45
|
+
const eventType = args.shift();
|
|
46
|
+
const reason = optionalString(flags.reason);
|
|
47
|
+
if (!eventType || args.length || !reason)
|
|
48
|
+
throw new Error("usage: gamealgo events block <eventType> --reason <reason> [--yes]");
|
|
49
|
+
if (!flags.yes && !(await confirm(`Block event ${eventType}? New occurrences will be dropped before Kafka.`)))
|
|
50
|
+
throw new Error("Cancelled");
|
|
51
|
+
await announceTargetGame(client);
|
|
52
|
+
await writeEventCatalogOutput(await client.blockEventCatalog(eventType, reason), flags, global);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (sub === "activate") {
|
|
56
|
+
const eventType = args.shift();
|
|
57
|
+
const reason = optionalString(flags.reason);
|
|
58
|
+
const confirmation = optionalString(flags.confirm);
|
|
59
|
+
if (!eventType || args.length || !reason || !confirmation) {
|
|
60
|
+
throw new Error("usage: gamealgo events activate <eventType> --reason <reason> --confirm <eventType>");
|
|
61
|
+
}
|
|
62
|
+
if (confirmation !== eventType)
|
|
63
|
+
throw new Error("--confirm must exactly match eventType");
|
|
64
|
+
await announceTargetGame(client);
|
|
65
|
+
await writeEventCatalogOutput(await client.activateEventCatalog(eventType, reason, confirmation), flags, global);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
44
68
|
if (sub !== "count")
|
|
45
|
-
throw new Error("usage: gamealgo events <list|get|apply|deprecate|count>");
|
|
69
|
+
throw new Error("usage: gamealgo events <list|get|apply|deprecate|block|activate|count>");
|
|
46
70
|
const timeoutMs = reportTimeoutMs(flags);
|
|
47
71
|
const startedAt = Date.now();
|
|
48
72
|
const stopProgress = startProgress("Querying event counts", timeoutMs);
|
package/dist/commands/report.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { extname, join } from "node:path";
|
|
1
2
|
import { parseFlags, parseSelectorFlags } from "../args.js";
|
|
2
3
|
import { formatDuration, printResult, reportTimeoutMs, startProgress, withCliMeta } from "../output.js";
|
|
4
|
+
import { reportDashboardCsv, reportDashboardCsvExports } from "../report-csv.js";
|
|
3
5
|
import { objectValue, readJsonFile, writeTextFile } from "../resources.js";
|
|
4
6
|
import { announceTargetGame } from "../target.js";
|
|
5
7
|
import { optionalString } from "../utils.js";
|
|
@@ -43,7 +45,7 @@ export async function handleReport(client, args, global) {
|
|
|
43
45
|
const startDate = String(flags.from || flags.start || flags.startDate || "");
|
|
44
46
|
const endDate = String(flags.to || flags.end || flags.endDate || "");
|
|
45
47
|
if (!startDate || !endDate)
|
|
46
|
-
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]");
|
|
48
|
+
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] [--format json|csv] [--out file-or-directory]");
|
|
47
49
|
const timeoutMs = reportTimeoutMs(flags);
|
|
48
50
|
const platform = await resolveCliReportPlatform(client, flags.platform, selectors.platform, startDate, endDate);
|
|
49
51
|
const startedAt = Date.now();
|
|
@@ -70,17 +72,7 @@ export async function handleReport(client, args, global) {
|
|
|
70
72
|
const elapsedMs = Date.now() - startedAt;
|
|
71
73
|
const outputValue = withCliMeta(response, { elapsedMs, timeoutMs });
|
|
72
74
|
process.stderr.write(`Report query finished in ${formatDuration(elapsedMs)}.\n`);
|
|
73
|
-
|
|
74
|
-
await writeTextFile(String(flags.out), JSON.stringify(outputValue, null, 2) + "\n");
|
|
75
|
-
await printResult({
|
|
76
|
-
ok: true,
|
|
77
|
-
out: String(flags.out),
|
|
78
|
-
results: Array.isArray(outputValue.results) ? outputValue.results.length : 0,
|
|
79
|
-
elapsedMs,
|
|
80
|
-
}, global);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
await printResult(outputValue, global);
|
|
75
|
+
await writeReportQueryOutput(outputValue, flags, global, elapsedMs);
|
|
84
76
|
return;
|
|
85
77
|
}
|
|
86
78
|
if (sub === "preview") {
|
|
@@ -90,7 +82,7 @@ export async function handleReport(client, args, global) {
|
|
|
90
82
|
const startDate = String(flags.from || flags.start || flags.startDate || "");
|
|
91
83
|
const endDate = String(flags.to || flags.end || flags.endDate || "");
|
|
92
84
|
if (!filePath || !startDate || !endDate) {
|
|
93
|
-
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]");
|
|
85
|
+
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] [--format json|csv] [--out file-or-directory]");
|
|
94
86
|
}
|
|
95
87
|
const content = await readJsonFile(filePath);
|
|
96
88
|
const timeoutMs = reportTimeoutMs(flags);
|
|
@@ -119,20 +111,106 @@ export async function handleReport(client, args, global) {
|
|
|
119
111
|
const elapsedMs = Date.now() - startedAt;
|
|
120
112
|
const outputValue = withCliMeta(response, { elapsedMs, timeoutMs });
|
|
121
113
|
process.stderr.write(`Report preview finished in ${formatDuration(elapsedMs)}.\n`);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
114
|
+
await writeReportQueryOutput(outputValue, flags, global, elapsedMs);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
throw new Error("usage: gamealgo report <pull|validate|publish|manifest|result|preview>");
|
|
118
|
+
}
|
|
119
|
+
export function reportOutputFormat(value) {
|
|
120
|
+
if (value === undefined || value === null || value === "")
|
|
121
|
+
return "json";
|
|
122
|
+
if (typeof value !== "string")
|
|
123
|
+
throw new Error("--format requires json or csv");
|
|
124
|
+
const normalized = value.trim().toLowerCase();
|
|
125
|
+
if (normalized === "json" || normalized === "csv")
|
|
126
|
+
return normalized;
|
|
127
|
+
throw new Error("--format must be json or csv");
|
|
128
|
+
}
|
|
129
|
+
async function writeReportQueryOutput(outputValue, flags, global, elapsedMs) {
|
|
130
|
+
const format = reportOutputFormat(flags.format);
|
|
131
|
+
const out = optionalString(flags.out);
|
|
132
|
+
if (flags.out === true)
|
|
133
|
+
throw new Error("--out requires a file path");
|
|
134
|
+
if (format === "csv") {
|
|
135
|
+
if (!out && global.json)
|
|
136
|
+
throw new Error("--format csv cannot be combined with --json unless --out is provided");
|
|
137
|
+
if (out) {
|
|
138
|
+
const exports = reportDashboardCsvExports(outputValue);
|
|
139
|
+
const files = reportCsvOutputFiles(exports, out);
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
warnTruncatedCsv(file.exported);
|
|
142
|
+
await writeTextFile(file.out, file.exported.content);
|
|
143
|
+
}
|
|
144
|
+
const first = files[0];
|
|
145
|
+
const summary = files.length === 1 ? {
|
|
125
146
|
ok: true,
|
|
126
|
-
out:
|
|
127
|
-
|
|
147
|
+
out: first.out,
|
|
148
|
+
format,
|
|
149
|
+
reportId: first.exported.reportId,
|
|
150
|
+
chartId: first.exported.chartId,
|
|
151
|
+
chartTitle: first.exported.chartTitle,
|
|
152
|
+
rows: first.exported.rowCount,
|
|
153
|
+
columns: first.exported.columns.length,
|
|
154
|
+
truncated: first.exported.truncated,
|
|
128
155
|
elapsedMs,
|
|
129
|
-
}
|
|
156
|
+
} : {
|
|
157
|
+
ok: true,
|
|
158
|
+
out,
|
|
159
|
+
format,
|
|
160
|
+
results: files.length,
|
|
161
|
+
files: files.map((file) => ({
|
|
162
|
+
out: file.out,
|
|
163
|
+
reportId: file.exported.reportId,
|
|
164
|
+
chartId: file.exported.chartId,
|
|
165
|
+
chartTitle: file.exported.chartTitle,
|
|
166
|
+
rows: file.exported.rowCount,
|
|
167
|
+
columns: file.exported.columns.length,
|
|
168
|
+
truncated: file.exported.truncated,
|
|
169
|
+
})),
|
|
170
|
+
elapsedMs,
|
|
171
|
+
};
|
|
172
|
+
await printResult(summary, global);
|
|
130
173
|
return;
|
|
131
174
|
}
|
|
132
|
-
|
|
175
|
+
const exported = reportDashboardCsv(outputValue);
|
|
176
|
+
warnTruncatedCsv(exported);
|
|
177
|
+
process.stdout.write(exported.content);
|
|
133
178
|
return;
|
|
134
179
|
}
|
|
135
|
-
|
|
180
|
+
if (out) {
|
|
181
|
+
await writeTextFile(out, JSON.stringify(outputValue, null, 2) + "\n");
|
|
182
|
+
await printResult({
|
|
183
|
+
ok: true,
|
|
184
|
+
out,
|
|
185
|
+
results: Array.isArray(outputValue.results) ? outputValue.results.length : 0,
|
|
186
|
+
elapsedMs,
|
|
187
|
+
}, global);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await printResult(outputValue, global);
|
|
191
|
+
}
|
|
192
|
+
function reportCsvOutputFiles(exports, out) {
|
|
193
|
+
if (exports.length === 1)
|
|
194
|
+
return [{ out, exported: exports[0] }];
|
|
195
|
+
if (extname(out).toLowerCase() === ".csv") {
|
|
196
|
+
throw new Error(`CSV export matched ${exports.length} charts; use --chart-id to select one chart or pass a directory to --out`);
|
|
197
|
+
}
|
|
198
|
+
return exports.map((exported, index) => ({
|
|
199
|
+
out: join(out, `${String(index + 1).padStart(2, "0")}-${reportCsvBaseName(exported, index)}.csv`),
|
|
200
|
+
exported,
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
function reportCsvBaseName(exported, index) {
|
|
204
|
+
const candidate = exported.chartId || exported.reportId || `report-${index + 1}`;
|
|
205
|
+
return candidate
|
|
206
|
+
.replace(/[^A-Za-z0-9_.-]+/g, "-")
|
|
207
|
+
.replace(/^-+|-+$/g, "")
|
|
208
|
+
|| `report-${index + 1}`;
|
|
209
|
+
}
|
|
210
|
+
function warnTruncatedCsv(exported) {
|
|
211
|
+
if (!exported.truncated)
|
|
212
|
+
return;
|
|
213
|
+
process.stderr.write(`Warning: CSV contains a truncated report result${exported.rowLimit ? ` (limit ${exported.rowLimit})` : ""}.\n`);
|
|
136
214
|
}
|
|
137
215
|
export async function resolveCliReportPlatform(client, flagValue, selectorValue, startDate, endDate) {
|
|
138
216
|
const flagPlatform = optionalCliReportPlatform(flagValue, "--platform");
|
package/dist/docs.js
CHANGED
|
@@ -257,8 +257,17 @@ export async function handleDocs(args, global) {
|
|
|
257
257
|
const flags = parseFlags(args);
|
|
258
258
|
const topicId = String(args.shift() || "").trim();
|
|
259
259
|
const apiQuery = topicId === "api" ? args.join(" ").trim() : "";
|
|
260
|
+
const listSections = flags.list === true || flags.toc === true;
|
|
261
|
+
const sectionId = optionalDocsSectionId(flags.section);
|
|
262
|
+
if (listSections && sectionId)
|
|
263
|
+
throw new Error("--list and --section cannot be used together");
|
|
264
|
+
if (flags.open && (listSections || sectionId))
|
|
265
|
+
throw new Error("--open cannot be combined with --list or --section");
|
|
266
|
+
if (topicId === "api" && (listSections || sectionId)) {
|
|
267
|
+
throw new Error("gamealgo docs api uses its own function list; --list and --section only apply to documentation topics");
|
|
268
|
+
}
|
|
260
269
|
if (topicId !== "api" && args.length) {
|
|
261
|
-
throw new Error("usage: gamealgo docs [topic] [--open] [--json] [--host <admin-url>]");
|
|
270
|
+
throw new Error("usage: gamealgo docs [topic] [--list | --section <section-id>] [--open] [--json] [--host <admin-url>]");
|
|
262
271
|
}
|
|
263
272
|
const saved = await loadConfig();
|
|
264
273
|
const host = normalizeHost(global.host || saved?.host || "");
|
|
@@ -307,6 +316,28 @@ export async function handleDocs(args, global) {
|
|
|
307
316
|
console.error("Warning: using cached documentation index because the Admin host could not be reached");
|
|
308
317
|
return;
|
|
309
318
|
}
|
|
319
|
+
if (listSections) {
|
|
320
|
+
if (!topic.sections.length)
|
|
321
|
+
throw new Error(`Documentation topic ${topic.id} does not publish addressable sections`);
|
|
322
|
+
if (global.json) {
|
|
323
|
+
await printResult({
|
|
324
|
+
ok: true,
|
|
325
|
+
version: manifest.version,
|
|
326
|
+
updatedAt: manifest.updatedAt,
|
|
327
|
+
topic: topic.id,
|
|
328
|
+
title: topic.title,
|
|
329
|
+
cached: loaded.cached,
|
|
330
|
+
webUrl,
|
|
331
|
+
rawUrl: docsRawUrl(host, topic.file),
|
|
332
|
+
sections: topic.sections,
|
|
333
|
+
}, global);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
printDocsSectionIndex(topic, manifest, topic.sections);
|
|
337
|
+
if (loaded.cached)
|
|
338
|
+
console.error("Warning: using cached documentation index because the Admin host could not be reached");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
310
341
|
const document = await loadDocsDocument(host, manifest, topic, loaded.cached);
|
|
311
342
|
if (topicId === "api") {
|
|
312
343
|
await printDocsApiResult({
|
|
@@ -321,6 +352,34 @@ export async function handleDocs(args, global) {
|
|
|
321
352
|
});
|
|
322
353
|
return;
|
|
323
354
|
}
|
|
355
|
+
if (sectionId) {
|
|
356
|
+
const section = topic.sections.find((item) => item.id === sectionId);
|
|
357
|
+
if (!section) {
|
|
358
|
+
const available = topic.sections.map((item) => item.id);
|
|
359
|
+
const suffix = available.length ? ` Available sections: ${available.join(", ")}` : "";
|
|
360
|
+
throw new Error(`Unknown documentation section: ${topic.id}/${sectionId}.${suffix}`);
|
|
361
|
+
}
|
|
362
|
+
const content = extractDocsSection(document.content, section);
|
|
363
|
+
if (global.json) {
|
|
364
|
+
await printResult({
|
|
365
|
+
ok: true,
|
|
366
|
+
version: manifest.version,
|
|
367
|
+
updatedAt: manifest.updatedAt,
|
|
368
|
+
topic: topic.id,
|
|
369
|
+
title: topic.title,
|
|
370
|
+
section,
|
|
371
|
+
cached: document.cached,
|
|
372
|
+
webUrl,
|
|
373
|
+
rawUrl: docsRawUrl(host, topic.file),
|
|
374
|
+
content,
|
|
375
|
+
}, global);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
console.log(content.trimEnd());
|
|
379
|
+
if (document.cached)
|
|
380
|
+
console.error("Warning: using cached documentation because the Admin host could not be reached");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
324
383
|
if (global.json) {
|
|
325
384
|
await printResult({
|
|
326
385
|
ok: true,
|
|
@@ -340,6 +399,62 @@ export async function handleDocs(args, global) {
|
|
|
340
399
|
if (document.cached)
|
|
341
400
|
console.error("Warning: using cached documentation because the Admin host could not be reached");
|
|
342
401
|
}
|
|
402
|
+
export function optionalDocsSectionId(value) {
|
|
403
|
+
if (value === undefined || value === false || value === "")
|
|
404
|
+
return undefined;
|
|
405
|
+
if (value === true)
|
|
406
|
+
throw new Error("--section requires a section id");
|
|
407
|
+
const sectionId = String(value).trim();
|
|
408
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(sectionId)) {
|
|
409
|
+
throw new Error("--section must use a stable lowercase section id");
|
|
410
|
+
}
|
|
411
|
+
return sectionId;
|
|
412
|
+
}
|
|
413
|
+
export function extractDocsSection(content, section) {
|
|
414
|
+
const lines = content.split(/\r?\n/);
|
|
415
|
+
const headingLines = markdownHeadingLines(lines);
|
|
416
|
+
const matches = headingLines.filter((item) => (item.level === section.level
|
|
417
|
+
&& normalizeDocsSectionTitle(item.title) === normalizeDocsSectionTitle(section.title)));
|
|
418
|
+
if (!matches.length)
|
|
419
|
+
throw new Error(`Documentation section heading not found: ${section.id} (${section.title})`);
|
|
420
|
+
if (matches.length > 1)
|
|
421
|
+
throw new Error(`Documentation section heading is ambiguous: ${section.id} (${section.title})`);
|
|
422
|
+
const startLine = matches[0].line;
|
|
423
|
+
const endLine = headingLines.find((item) => item.line > startLine && item.level <= section.level)?.line ?? lines.length;
|
|
424
|
+
return lines.slice(startLine, endLine).join("\n").trimEnd();
|
|
425
|
+
}
|
|
426
|
+
function normalizeDocsSectionTitle(value) {
|
|
427
|
+
return stripInlineMarkdown(value).replace(/\s+/g, " ").trim();
|
|
428
|
+
}
|
|
429
|
+
function markdownHeadingLines(lines) {
|
|
430
|
+
const headings = [];
|
|
431
|
+
let fence = "";
|
|
432
|
+
for (let line = 0; line < lines.length; line += 1) {
|
|
433
|
+
const fenceMatch = lines[line].match(/^\s{0,3}(```+|~~~+)/);
|
|
434
|
+
if (fenceMatch) {
|
|
435
|
+
const marker = fenceMatch[1][0];
|
|
436
|
+
if (!fence)
|
|
437
|
+
fence = marker;
|
|
438
|
+
else if (fence === marker)
|
|
439
|
+
fence = "";
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (fence)
|
|
443
|
+
continue;
|
|
444
|
+
const heading = lines[line].match(/^(#{1,6})[ \t]+(.+?)\s*$/);
|
|
445
|
+
if (heading)
|
|
446
|
+
headings.push({ line, level: heading[1].length, title: heading[2] });
|
|
447
|
+
}
|
|
448
|
+
return headings;
|
|
449
|
+
}
|
|
450
|
+
export function printDocsSectionIndex(topic, manifest, sections) {
|
|
451
|
+
console.log(`${topic.title}章节 ${manifest.version}${manifest.updatedAt ? ` (${manifest.updatedAt})` : ""}`);
|
|
452
|
+
for (const section of sections) {
|
|
453
|
+
const indent = " ".repeat(Math.max(1, section.level - 1));
|
|
454
|
+
console.log(`\n${indent}${section.id}\n${indent} ${section.title}${section.summary ? `:${section.summary}` : ""}`);
|
|
455
|
+
}
|
|
456
|
+
console.log(`\n读取章节: gamealgo docs ${topic.id} --section <section-id>`);
|
|
457
|
+
}
|
|
343
458
|
export function parseDocsApiPlatform(value) {
|
|
344
459
|
if (value === undefined || value === false || value === "")
|
|
345
460
|
return undefined;
|
|
@@ -556,6 +671,7 @@ export function parseDocsManifest(text) {
|
|
|
556
671
|
group: String(topic.group || "其他"),
|
|
557
672
|
file,
|
|
558
673
|
hidden: topic.hidden === true,
|
|
674
|
+
sections: parseDocsManifestSections(topic.sections, id),
|
|
559
675
|
};
|
|
560
676
|
});
|
|
561
677
|
const ids = new Set(topics.map((topic) => topic.id));
|
|
@@ -572,6 +688,43 @@ export function parseDocsManifest(text) {
|
|
|
572
688
|
topics,
|
|
573
689
|
};
|
|
574
690
|
}
|
|
691
|
+
function parseDocsManifestSections(value, topicId) {
|
|
692
|
+
if (value === undefined)
|
|
693
|
+
return [];
|
|
694
|
+
if (!Array.isArray(value))
|
|
695
|
+
throw new Error(`Documentation topic ${topicId} sections must be an array`);
|
|
696
|
+
const sections = [];
|
|
697
|
+
const parents = [];
|
|
698
|
+
const ids = new Set();
|
|
699
|
+
for (const item of value) {
|
|
700
|
+
const section = objectValue(item);
|
|
701
|
+
const id = optionalString(section?.id);
|
|
702
|
+
const title = optionalString(section?.title);
|
|
703
|
+
const summary = optionalString(section?.summary);
|
|
704
|
+
const level = Number(section?.level);
|
|
705
|
+
if (!id || !/^[a-z][a-z0-9-]{0,63}$/.test(id) || !title || !summary || !Number.isInteger(level) || level < 2 || level > 6) {
|
|
706
|
+
throw new Error(`Documentation topic ${topicId} contains an invalid section: ${id || "unknown"}`);
|
|
707
|
+
}
|
|
708
|
+
if (ids.has(id))
|
|
709
|
+
throw new Error(`Documentation topic ${topicId} contains duplicate section id: ${id}`);
|
|
710
|
+
ids.add(id);
|
|
711
|
+
while (parents.length && parents[parents.length - 1].level >= level)
|
|
712
|
+
parents.pop();
|
|
713
|
+
if (level > 2 && (!parents.length || level > parents[parents.length - 1].level + 1)) {
|
|
714
|
+
throw new Error(`Documentation topic ${topicId} section ${id} has no valid parent`);
|
|
715
|
+
}
|
|
716
|
+
const parsed = {
|
|
717
|
+
id,
|
|
718
|
+
title,
|
|
719
|
+
summary,
|
|
720
|
+
level,
|
|
721
|
+
...(parents.length ? { parentId: parents[parents.length - 1].id } : {}),
|
|
722
|
+
};
|
|
723
|
+
sections.push(parsed);
|
|
724
|
+
parents.push(parsed);
|
|
725
|
+
}
|
|
726
|
+
return sections;
|
|
727
|
+
}
|
|
575
728
|
export function docsWebUrl(host, topicId) {
|
|
576
729
|
const url = new URL(`${normalizeHost(host)}/dashboard/docs/`);
|
|
577
730
|
if (topicId)
|
package/dist/main.js
CHANGED
|
@@ -12,6 +12,7 @@ import { handleFileResource } from "./commands/resources.js";
|
|
|
12
12
|
import { handleSkill } from "./commands/skill.js";
|
|
13
13
|
import { loadPackageMetadata } from "./config.js";
|
|
14
14
|
import { handleDocs, handleHelp } from "./docs.js";
|
|
15
|
+
import { handleDynamicConfig } from "./commands/dynamic-config.js";
|
|
15
16
|
import { printResult } from "./output.js";
|
|
16
17
|
export async function main() {
|
|
17
18
|
const args = process.argv.slice(2);
|
|
@@ -76,6 +77,10 @@ export async function main() {
|
|
|
76
77
|
await handleFileResource(client, args, global, "config");
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
80
|
+
if (command === "dynamic-config") {
|
|
81
|
+
await handleDynamicConfig(client, args, global);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
79
84
|
if (command === "report") {
|
|
80
85
|
await handleReport(client, args, global);
|
|
81
86
|
return;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function reportDashboardCsv(value) {
|
|
2
|
+
const exports = reportDashboardCsvExports(value);
|
|
3
|
+
if (exports.length !== 1) {
|
|
4
|
+
throw new Error(`CSV output to stdout or one file requires exactly one matching chart, but the query returned ${exports.length}`);
|
|
5
|
+
}
|
|
6
|
+
return exports[0];
|
|
7
|
+
}
|
|
8
|
+
export function reportDashboardCsvExports(value) {
|
|
9
|
+
const dashboard = recordValue(value);
|
|
10
|
+
const results = Array.isArray(dashboard?.results) ? dashboard.results : [];
|
|
11
|
+
if (!results.length)
|
|
12
|
+
throw new Error("CSV export did not receive any matching chart results");
|
|
13
|
+
return results.map(reportResultCsv);
|
|
14
|
+
}
|
|
15
|
+
function reportResultCsv(value) {
|
|
16
|
+
const result = recordValue(value);
|
|
17
|
+
if (!result)
|
|
18
|
+
throw new Error("CSV export received an invalid report result");
|
|
19
|
+
if (typeof result.error === "string" && result.error.trim()) {
|
|
20
|
+
throw new Error(`cannot export failed report result: ${result.error.trim()}`);
|
|
21
|
+
}
|
|
22
|
+
const rows = Array.isArray(result.rows)
|
|
23
|
+
? result.rows.map((row) => recordValue(row) || {})
|
|
24
|
+
: [];
|
|
25
|
+
const declaredColumns = Array.isArray(result.columns)
|
|
26
|
+
? result.columns.filter((column) => typeof column === "string" && Boolean(column.trim()))
|
|
27
|
+
: [];
|
|
28
|
+
const columns = uniqueStrings(declaredColumns.length ? declaredColumns : rows.flatMap((row) => Object.keys(row)));
|
|
29
|
+
if (!columns.length) {
|
|
30
|
+
throw new Error("CSV export result is not ready because the Server returned no columns; retry with --refresh or wait for the background refresh");
|
|
31
|
+
}
|
|
32
|
+
const lines = [
|
|
33
|
+
columns.map(csvCell).join(","),
|
|
34
|
+
...rows.map((row) => columns.map((column) => csvCell(row[column])).join(",")),
|
|
35
|
+
];
|
|
36
|
+
const chart = recordValue(result.chart);
|
|
37
|
+
const rowLimit = positiveIntegerOrUndefined(result.rowLimit);
|
|
38
|
+
return {
|
|
39
|
+
content: `\uFEFF${lines.join("\r\n")}\r\n`,
|
|
40
|
+
columns,
|
|
41
|
+
rowCount: rows.length,
|
|
42
|
+
reportId: optionalString(result.reportId),
|
|
43
|
+
chartId: optionalString(chart?.id),
|
|
44
|
+
chartTitle: optionalString(chart?.title),
|
|
45
|
+
truncated: result.truncated === true,
|
|
46
|
+
...(rowLimit ? { rowLimit } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function csvCell(value) {
|
|
50
|
+
let text;
|
|
51
|
+
if (value === null || value === undefined)
|
|
52
|
+
text = "";
|
|
53
|
+
else if (typeof value === "string")
|
|
54
|
+
text = protectSpreadsheetFormula(value);
|
|
55
|
+
else if (typeof value === "number")
|
|
56
|
+
text = Number.isFinite(value) ? String(value) : "";
|
|
57
|
+
else if (typeof value === "boolean" || typeof value === "bigint")
|
|
58
|
+
text = String(value);
|
|
59
|
+
else {
|
|
60
|
+
try {
|
|
61
|
+
text = JSON.stringify(value) ?? "";
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
text = String(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (/[",\r\n]/.test(text) || /^\s|\s$/.test(text))
|
|
68
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
function protectSpreadsheetFormula(value) {
|
|
72
|
+
return /^\s*[=+\-@]/.test(value) ? `'${value}` : value;
|
|
73
|
+
}
|
|
74
|
+
function recordValue(value) {
|
|
75
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
76
|
+
? value
|
|
77
|
+
: null;
|
|
78
|
+
}
|
|
79
|
+
function uniqueStrings(values) {
|
|
80
|
+
return [...new Set(values)];
|
|
81
|
+
}
|
|
82
|
+
function optionalString(value) {
|
|
83
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
84
|
+
}
|
|
85
|
+
function positiveIntegerOrUndefined(value) {
|
|
86
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
87
|
+
}
|
package/package.json
CHANGED
package/skills/gamealgo/SKILL.md
CHANGED
|
@@ -22,7 +22,7 @@ When the user asks what GameAlgo can do, asks for ongoing iteration, or names a
|
|
|
22
22
|
1. Run `gamealgo optimize list --json` to load the complete current task catalog from the Server.
|
|
23
23
|
2. Treat the result as an unranked capability catalog. Do not claim that a task is recommended or ready until its prerequisites have been checked against the current game.
|
|
24
24
|
3. Once the user selects a task, or their request clearly identifies one, run `gamealgo optimize get <task-id> --json`.
|
|
25
|
-
4.
|
|
25
|
+
4. For each topic named in the task's `docs` field, run `gamealgo docs <topic> --list --json`, then read the sections relevant to the selected task with `gamealgo docs <topic> --section <section-id>` before implementation.
|
|
26
26
|
5. Follow the returned `agentSteps` and use `deliverables` as the completion checklist.
|
|
27
27
|
|
|
28
28
|
Do not run every catalog task in sequence. Complete the selected task and report unmet prerequisites instead of manufacturing missing data.
|
|
@@ -31,14 +31,16 @@ Do not run every catalog task in sequence. Complete the selected task and report
|
|
|
31
31
|
|
|
32
32
|
Classify the requested work by the actions it requires, not only by the user's task title. Before implementation or interpreting results, load every applicable source below from the current Server:
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
- 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.
|
|
35
37
|
- 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.
|
|
36
38
|
- Adding or changing events: additionally run `gamealgo events list --days 30 --json` before defining fields or names.
|
|
37
|
-
-
|
|
38
|
-
- Creating or changing remote configuration, strategies, experiments, variants, objectives, rollout, or assessment:
|
|
39
|
+
- 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.
|
|
40
|
+
- Creating or changing remote configuration, dynamic config pipelines, strategies, experiments, variants, objectives, rollout, or assessment: list the `experiments` sections and read every section affected by the change; also load the integration plan and exact API documentation when client code changes.
|
|
39
41
|
- Adjust or marketing attribution work: load the selected optimization task when one applies, read every topic in its `docs`, and load the integration plan and exact identifier/attribution APIs for the target platform.
|
|
40
42
|
|
|
41
|
-
If work crosses domains, combine their requirements. For example, an advertising event used by an experiment report requires the integration plan, event inventory, SDK API documentation, report
|
|
43
|
+
If work crosses domains, combine their requirements. For example, an advertising event used by an experiment report requires the integration plan, event inventory, SDK API documentation, relevant report sections, and relevant experiment sections. Read a section once per turn unless the task expands or the Server documentation version changes.
|
|
42
44
|
|
|
43
45
|
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.
|
|
44
46
|
|
|
@@ -51,6 +53,18 @@ For initial integration or business dashboard work, standard dashboards are only
|
|
|
51
53
|
5. Before publishing reports, remote configuration, scripts, strategies, experiments, rollout changes, or other externally visible mutations, show the relevant diff and impact unless the user has already explicitly authorized that exact operation.
|
|
52
54
|
6. After execution, verify with real configuration retrieval, accepted non-debug events, report results, or experiment state as appropriate. A code diff or successful request alone is not business verification.
|
|
53
55
|
|
|
56
|
+
## Keep Production Analysis Off The Developer Workstation
|
|
57
|
+
|
|
58
|
+
For production telemetry investigations, execute partition-scale scans and aggregations with managed Alibaba Cloud compute such as DataWorks/MaxCompute or Kyuubi/Spark, and return only the small aggregated result needed for diagnosis.
|
|
59
|
+
|
|
60
|
+
The built-in domestic Data Bridge Paimon backend is not a distributed SQL pushdown engine: PyPaimon plans Paimon splits, reads OSS data into an Arrow table inside the Bridge pod, and only then executes the SQL with DuckDB. The current runner also does not translate SQL predicates into `read_builder.with_filter(...)`, so SQL `dt`, `game`, or `platform` filters do not guarantee Paimon split pruning. Do not use this backend for production raw/enriched partition scans or describe it as Paimon aggregating inside the storage service. Limit it to tightly bounded summary-table reads or emergency diagnostics until the query path is backed by managed compute.
|
|
61
|
+
|
|
62
|
+
- Never use the local PyPaimon/DuckDB launcher to calculate production counts, DAU, hourly volume, event coverage, or source-to-enriched reconciliation. That launcher materializes selected Paimon partition data on the local machine even when the submitted SQL contains only aggregates.
|
|
63
|
+
- Do not pull production detail rows or whole partitions to the local workspace and aggregate them afterward.
|
|
64
|
+
- Put partition filters, game filters, grouping, distinct counts, joins, and same-elapsed-window comparisons in the SQL sent to the managed query engine, and verify its execution plan actually prunes partitions and columns.
|
|
65
|
+
- Local Paimon access is limited to catalog/schema/partition metadata and a deliberately tiny `LIMIT` sample when the schema cannot otherwise be established.
|
|
66
|
+
- If no managed query path is available, stop and give the developer a bounded SQL query to run. Do not silently fall back to a local or Bridge-Pod partition scan.
|
|
67
|
+
|
|
54
68
|
Reading documentation does not grant permission to mutate production. CLI and Server validation, scopes, project binding, and explicit approval requirements still apply.
|
|
55
69
|
|
|
56
70
|
## Integration Details
|
|
@@ -67,6 +81,39 @@ Reuse events with the same business meaning, register new fields with their real
|
|
|
67
81
|
|
|
68
82
|
For every game, prefer stable, human-readable Chinese values for business dimensions that will appear directly in report legends, axes, selectors, or tables. This includes values such as level or stage type, game mode, placement, item name, and business outcome or failure reason. Keep event names, field names, IDs, SQL aliases, and protocol-defined enums in their required technical format. Do not rename values already used by a live game solely to translate them, because that would split one historical dimension into multiple report values.
|
|
69
83
|
|
|
84
|
+
## Bound Custom Event Volume
|
|
85
|
+
|
|
86
|
+
Before adding or changing a custom event, prefer a protocol-standard event or an existing custom event with the same meaning. Keep the event type stable and put variable values in payload fields; never generate event types from item IDs, levels, timestamps, user input, or other runtime values.
|
|
87
|
+
|
|
88
|
+
Inspect every call site for render/update loops, timers, polling, transport retries, duplicate lifecycle callbacks, and re-entrant listeners. State the trigger boundary and estimate the maximum calls per user, session, context, and active user-day. Use the game's active-user estimate to describe the expected daily volume when it is available.
|
|
89
|
+
|
|
90
|
+
- Standard SDK semantic events are governed by their protocol semantics. Do not disguise custom telemetry as a standard event to bypass safeguards.
|
|
91
|
+
- The SDK limits custom events to 1,000 occurrences per `context × eventType`, 5,000 total occurrences per context, and 100 distinct custom event types per context.
|
|
92
|
+
- A custom event with an unbounded trigger or an expected rate above 1,000 calls per context requires explicit developer review before implementation. Explain the expected volume, storage and processing impact, and the guard that makes the trigger bounded.
|
|
93
|
+
- Never add client-side retries around `track`, rename an event after rejection, remove its custom-event prefix, declare it standard, raise a safety limit, or call a lower-level transport to bypass a quota.
|
|
94
|
+
- Treat an SDK `event_guard` diagnostic as evidence of a telemetry bug or unexpectedly high-frequency design. Find and fix the emission path before asking to change a limit.
|
|
95
|
+
|
|
96
|
+
Before delivery, report the estimated bound, normal-flow tests, the limiting loop or callback, and behavior once the local quota is reached.
|
|
97
|
+
|
|
98
|
+
## Treat Dynamic Config As A Production Publisher
|
|
99
|
+
|
|
100
|
+
A dynamic config pipeline is Server-side automation that turns aggregated Report Pack inputs into a live config file. It does not authorize SDK changes, detailed production-data downloads, or live publication by itself.
|
|
101
|
+
|
|
102
|
+
- Define bounded aggregate inputs with Report Pack syntax. Keep filters, joins, aggregation, and data reduction in the managed query rather than passing event detail to JavaScript.
|
|
103
|
+
- Keep transforms synchronous and deterministic. They may use only the supplied datasets, previous output, and scheduled metadata; never depend on network, filesystem, environment variables, wall-clock APIs, randomness, async functions, or Promise output.
|
|
104
|
+
- The output file name and content type belong to the reviewed pipeline definition. The script may return only file content and JSON diagnostics.
|
|
105
|
+
- Before publishing or materially updating an active pipeline, show the target game, input reports and windows, complete script diff, output file and content type, schedule, expected behavior, failure behavior, and user-facing or operational risks. Obtain explicit approval for that exact publication unless the user already gave it after seeing those details.
|
|
106
|
+
- Prefer a disabled pipeline or a local script test while details are still under discussion. After an authorized publish, trigger one run, inspect the persisted run result and generated file version, and confirm that an unchanged second run is hash-deduplicated before treating the automation as verified.
|
|
107
|
+
- Do not raise the 1 MiB script/input/output caps, the 5,000-row per-dataset cap, the 5-second execution cap, or the minimum 60-minute schedule to bypass a failing design. Reduce data in the Report Pack or simplify the transform.
|
|
108
|
+
|
|
109
|
+
Use `gamealgo dynamic-config list|get|publish|run|runs` for lifecycle operations. Dynamic config output continues through the ordinary SDK config-file delivery mechanism, so do not change client SDK code merely to consume a generated file that already uses the existing config API.
|
|
110
|
+
|
|
111
|
+
## Respect Blocked Events
|
|
112
|
+
|
|
113
|
+
A blocked event is a persistent ingestion safety state. You may inspect its diagnostics, analyze data read-only, fix the client call site, and run local validation, but you must not reactivate it, register an equivalent replacement event, declare it standard, switch credentials, call a lower-level transport, or otherwise bypass the block without fresh explicit approval. Applying ordinary catalog metadata must not reactivate a blocked or deprecated event.
|
|
114
|
+
|
|
115
|
+
Before asking to reactivate an event, tell the developer why it was blocked, the faulty call path, the implemented fix, the previous and expected event rate per context and active user-day, the validation performed, and the remaining risk. Only an unambiguous instruction given after that disclosure authorizes the exact `blocked` to `active` transition. Use the dedicated CLI activation command with its required reason and exact event-name confirmation, then verify both the catalog state and newly observed event health. Do not edit the database or repeatedly reactivate an event that becomes blocked again without a new diagnosis and approval.
|
|
116
|
+
|
|
70
117
|
## Initialize At The Earliest Safe Lifecycle Point
|
|
71
118
|
|
|
72
119
|
Initialize GameAlgo at the beginning of the earliest runtime lifecycle hook where its minimal required dependencies are available. Do not place it after save loading, scene creation, asset loading, UI construction, ad initialization, or a broad game-module bootstrap merely for organizational convenience. Any unhandled failure before the call would otherwise exclude an opened user from telemetry, and an SDK initialization watchdog cannot report a failure when the SDK was never started.
|
|
@@ -98,6 +145,23 @@ When client code implements an experiment or remote Strategy, define a stable, t
|
|
|
98
145
|
|
|
99
146
|
Before delivery, inspect changed client code for Variant or assignment-identity access and verify none of it reaches business conditionals. Test active control and treatment configs, missing configuration, and the post-promotion shape where the Variant becomes `default` while the promoted config remains active; unchanged parameters must produce unchanged game behavior regardless of the Variant name.
|
|
100
147
|
|
|
148
|
+
## Require Explicit Approval Before Publishing Experiments
|
|
149
|
+
|
|
150
|
+
Treat publishing a live experiment as a two-step workflow. Before approval, limit work to read-only analysis, experiment ideas, and local drafts. Do not publish an experiment-specific Strategy or Strategy version, create or start a Run, allocate traffic, or otherwise mutate live experiment state.
|
|
151
|
+
|
|
152
|
+
Before asking for approval, tell the developer:
|
|
153
|
+
|
|
154
|
+
- the target game and environment, eligible audience, and exclusions;
|
|
155
|
+
- the hypothesis, primary metric, and decision criterion;
|
|
156
|
+
- the exact control and treatment differences, including parameter values or scripts;
|
|
157
|
+
- the traffic allocation and planned duration, sample size, or review checkpoint;
|
|
158
|
+
- the expected benefit and realistic product, revenue, retention, user-experience, and technical risks that apply; and
|
|
159
|
+
- the stop-loss conditions and rollback or cancellation plan.
|
|
160
|
+
|
|
161
|
+
Then explicitly ask whether to publish that exact experiment. Only an unambiguous confirmation given after this disclosure authorizes publication. Earlier broad instructions such as “优化一下”, “做个实验”, or “按你判断”, and authorization for code, report, or configuration work do not authorize publishing an experiment. If a material detail changes after approval—including the target game, audience, variants, parameters or scripts, traffic, duration, objective, decision criterion, or risk profile—disclose the change and obtain fresh approval before publishing.
|
|
162
|
+
|
|
163
|
+
After an authorized publication, retrieve and report the actual Strategy and Run IDs, status, scope, and rollback path. Do not silently create another experiment, Run, round, or rollout. A direct request to pause, cancel, or roll back an existing experiment is sufficient authority for that protective action and must not be blocked by this publication gate.
|
|
164
|
+
|
|
101
165
|
## Protect D0 Monetization In Maker Experiments
|
|
102
166
|
|
|
103
167
|
For TapTap Maker and other Maker games, do not autonomously design, create, or start an experiment that removes all ads from a user's first local day, delays the first ad until D1 or later, or otherwise moves the game's core D0 ad inventory to a later day. Many Maker games lose a large share of users before D1, so this is a direct D0 revenue-risk decision rather than an ordinary ad-timing adjustment. A broad request to optimize ads is not approval for this design.
|