@mpgd/cli 0.39.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asset-pack-build.js +4 -1
- package/dist/asset-pack-native-audit.d.ts +5 -0
- package/dist/asset-pack-native-audit.js +137 -0
- package/dist/asset-pack-publish.d.ts +43 -0
- package/dist/asset-pack-publish.js +309 -0
- package/dist/asset-pack-target.d.ts +42 -0
- package/dist/asset-pack-target.js +251 -0
- package/dist/game-acceptance.d.ts +4 -0
- package/dist/game-acceptance.js +49 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +127 -6
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +3404 -621
- package/dist/performance-acceptance.d.ts +56 -0
- package/dist/performance-acceptance.js +269 -0
- package/package.json +15 -10
- package/templates/phaser-game/agent/acceptance.md +7 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type GameplayE2EPathEvidence } from './gameplay-e2e.js';
|
|
2
|
+
export declare const maximumPerformanceEvidenceBytes: number;
|
|
3
|
+
export interface PerformanceMetricBudget {
|
|
4
|
+
readonly p95?: number;
|
|
5
|
+
readonly p99?: number;
|
|
6
|
+
readonly max?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PerformanceBudgetProfile {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly target: string;
|
|
11
|
+
readonly scenario: string;
|
|
12
|
+
readonly renderer: string;
|
|
13
|
+
readonly device: string;
|
|
14
|
+
readonly minSamples: number;
|
|
15
|
+
readonly metrics: Readonly<Record<string, PerformanceMetricBudget>>;
|
|
16
|
+
readonly maxExcludedFrames?: number;
|
|
17
|
+
readonly maxHeapGrowthBytes?: number;
|
|
18
|
+
readonly hitchThresholdMs?: number;
|
|
19
|
+
readonly maxHitches?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface PerformanceBudget {
|
|
22
|
+
readonly schemaVersion: 1;
|
|
23
|
+
readonly profiles: readonly PerformanceBudgetProfile[];
|
|
24
|
+
}
|
|
25
|
+
export interface PerformanceAcceptanceInput {
|
|
26
|
+
readonly budgetFile: string;
|
|
27
|
+
readonly reportFile: string;
|
|
28
|
+
readonly stepId: string;
|
|
29
|
+
readonly targets: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export interface PreparedPerformanceAcceptance {
|
|
32
|
+
readonly budget: PerformanceBudget;
|
|
33
|
+
readonly budgetEvidence: GameplayE2EPathEvidence;
|
|
34
|
+
readonly reportFile: string;
|
|
35
|
+
}
|
|
36
|
+
export interface PerformanceAcceptanceEvidence {
|
|
37
|
+
readonly file: string;
|
|
38
|
+
readonly found: boolean;
|
|
39
|
+
readonly validationError: string | null;
|
|
40
|
+
readonly budget: GameplayE2EPathEvidence;
|
|
41
|
+
readonly report: GameplayE2EPathEvidence | null;
|
|
42
|
+
readonly value: unknown;
|
|
43
|
+
}
|
|
44
|
+
/** Consumer-authored budgets: no universal device/FPS thresholds are supplied. */
|
|
45
|
+
export declare function validatePerformanceBudget(value: unknown): PerformanceBudget;
|
|
46
|
+
export declare function preparePerformanceAcceptance(gameRoot: string, input: PerformanceAcceptanceInput): PreparedPerformanceAcceptance;
|
|
47
|
+
/** Accept only fresh untraced foreground measurements with matching authored
|
|
48
|
+
* context, adequate sample counts and current artifact/budget provenance. */
|
|
49
|
+
export declare function readPerformanceAcceptanceEvidence(input: {
|
|
50
|
+
readonly gameRoot: string;
|
|
51
|
+
readonly prepared: PreparedPerformanceAcceptance;
|
|
52
|
+
readonly startedAtMs: number | undefined;
|
|
53
|
+
readonly finishedAtMs: number;
|
|
54
|
+
readonly profile: string | undefined;
|
|
55
|
+
readonly releaseManifestFile?: string | undefined;
|
|
56
|
+
}): PerformanceAcceptanceEvidence;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { formatError, readBoundedUtf8File, relativeOrAbsolute } from './evidence-io.js';
|
|
4
|
+
import { collectGameplayE2EPathEvidence, resolveGameplayE2EPathInsideGameRoot, } from './gameplay-e2e.js';
|
|
5
|
+
export const maximumPerformanceEvidenceBytes = 1_024 * 1_024;
|
|
6
|
+
function record(value, label) {
|
|
7
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
8
|
+
throw new Error(`${label} must be an object.`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function text(value, label) {
|
|
13
|
+
if (typeof value !== 'string' || value.trim().length === 0 || value.length > 256) {
|
|
14
|
+
throw new Error(`${label} must be a bounded non-empty string.`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function number(value, label, signed = false) {
|
|
19
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || (!signed && value < 0)) {
|
|
20
|
+
throw new Error(`${label} must be a finite ${signed ? '' : 'non-negative '}number.`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function count(value, label, min = 0) {
|
|
25
|
+
const n = number(value, label);
|
|
26
|
+
if (!Number.isSafeInteger(n) || n < min || n > 1_000_000_000) {
|
|
27
|
+
throw new Error(`${label} must be an integer between ${min} and 1000000000.`);
|
|
28
|
+
}
|
|
29
|
+
return n;
|
|
30
|
+
}
|
|
31
|
+
function keys(value, allowed, label) {
|
|
32
|
+
if (Object.keys(value).some((key) => !allowed.includes(key))) {
|
|
33
|
+
throw new Error(`${label} contains an unknown field.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Consumer-authored budgets: no universal device/FPS thresholds are supplied. */
|
|
37
|
+
export function validatePerformanceBudget(value) {
|
|
38
|
+
const input = record(value, 'Performance budget');
|
|
39
|
+
keys(input, ['schemaVersion', 'profiles'], 'Performance budget');
|
|
40
|
+
if (input.schemaVersion !== 1 || !Array.isArray(input.profiles)
|
|
41
|
+
|| input.profiles.length === 0 || input.profiles.length > 64) {
|
|
42
|
+
throw new Error('Performance budget needs schemaVersion 1 and 1–64 profiles.');
|
|
43
|
+
}
|
|
44
|
+
const ids = new Set();
|
|
45
|
+
const profiles = input.profiles.map((item) => {
|
|
46
|
+
const profile = record(item, 'Performance profile');
|
|
47
|
+
keys(profile, ['id', 'target', 'scenario', 'renderer', 'device', 'minSamples', 'metrics',
|
|
48
|
+
'maxExcludedFrames', 'maxHeapGrowthBytes', 'hitchThresholdMs', 'maxHitches'], 'Performance profile');
|
|
49
|
+
for (const name of ['id', 'target', 'scenario', 'renderer', 'device']) {
|
|
50
|
+
text(profile[name], `Performance profile ${name}`);
|
|
51
|
+
}
|
|
52
|
+
const id = text(profile.id, 'Performance profile id');
|
|
53
|
+
if (ids.has(id)) {
|
|
54
|
+
throw new Error(`Duplicate performance profile: ${id}`);
|
|
55
|
+
}
|
|
56
|
+
ids.add(id);
|
|
57
|
+
count(profile.minSamples, `${id} minSamples`, 2);
|
|
58
|
+
const metrics = record(profile.metrics, `${id} metrics`);
|
|
59
|
+
const metricNames = Object.keys(metrics);
|
|
60
|
+
const builtInMetrics = ['intervalMs', 'totalCpuMs'];
|
|
61
|
+
const customNames = metricNames.filter((field) => !builtInMetrics.includes(field));
|
|
62
|
+
if (metricNames.length === 0 || customNames.length > 64) {
|
|
63
|
+
throw new Error(`${id} needs 1–66 metric budgets.`);
|
|
64
|
+
}
|
|
65
|
+
if (customNames.some((field) => field.length > 64 || !/^[a-z][a-zA-Z0-9]*$/.test(field)
|
|
66
|
+
|| ['constructor', 'prototype'].includes(field))) {
|
|
67
|
+
throw new Error(`${id} has an invalid profiler metric name.`);
|
|
68
|
+
}
|
|
69
|
+
for (const [field, itemBudget] of Object.entries(metrics)) {
|
|
70
|
+
text(field, 'Performance metric name');
|
|
71
|
+
const budget = record(itemBudget, `${id}/${field} budget`);
|
|
72
|
+
keys(budget, ['p95', 'p99', 'max'], `${id}/${field} budget`);
|
|
73
|
+
if (Object.keys(budget).length === 0) {
|
|
74
|
+
throw new Error(`${id}/${field} needs a threshold.`);
|
|
75
|
+
}
|
|
76
|
+
for (const [quantile, threshold] of Object.entries(budget)) {
|
|
77
|
+
number(threshold, `${id}/${field}/${quantile}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const name of ['maxHeapGrowthBytes', 'hitchThresholdMs']) {
|
|
81
|
+
if (profile[name] !== undefined) {
|
|
82
|
+
number(profile[name], `${id} ${name}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const name of ['maxExcludedFrames', 'maxHitches']) {
|
|
86
|
+
if (profile[name] !== undefined) {
|
|
87
|
+
count(profile[name], `${id} ${name}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if ((profile.maxHitches === undefined) !== (profile.hitchThresholdMs === undefined)
|
|
91
|
+
|| profile.hitchThresholdMs === 0) {
|
|
92
|
+
throw new Error(`${id} hitch budgets need maxHitches and a positive hitchThresholdMs.`);
|
|
93
|
+
}
|
|
94
|
+
const snapshot = structuredClone(profile);
|
|
95
|
+
for (const metric of Object.values(snapshot.metrics)) {
|
|
96
|
+
Object.freeze(metric);
|
|
97
|
+
}
|
|
98
|
+
Object.freeze(snapshot.metrics);
|
|
99
|
+
return Object.freeze(snapshot);
|
|
100
|
+
});
|
|
101
|
+
return Object.freeze({ schemaVersion: 1, profiles: Object.freeze(profiles) });
|
|
102
|
+
}
|
|
103
|
+
function readJson(file, label) {
|
|
104
|
+
const content = readBoundedUtf8File(file, maximumPerformanceEvidenceBytes);
|
|
105
|
+
if (content === null) {
|
|
106
|
+
throw new Error(`${label} exceeds its byte limit.`);
|
|
107
|
+
}
|
|
108
|
+
return JSON.parse(content);
|
|
109
|
+
}
|
|
110
|
+
function evidence(gameRoot, value, label) {
|
|
111
|
+
const input = record(value, label);
|
|
112
|
+
const file = text(input.file, `${label} file`);
|
|
113
|
+
const current = collectGameplayE2EPathEvidence(gameRoot, file, label);
|
|
114
|
+
if (current.kind === 'symbolic-link' || current.kind !== input.kind || current.sha256 !== input.sha256) {
|
|
115
|
+
throw new Error(`${label} no longer matches its current file/directory hash.`);
|
|
116
|
+
}
|
|
117
|
+
return current;
|
|
118
|
+
}
|
|
119
|
+
export function preparePerformanceAcceptance(gameRoot, input) {
|
|
120
|
+
const budgetFile = resolveGameplayE2EPathInsideGameRoot(gameRoot, input.budgetFile, 'Performance budget');
|
|
121
|
+
const reportFile = resolveGameplayE2EPathInsideGameRoot(gameRoot, input.reportFile, 'Performance report');
|
|
122
|
+
if (budgetFile === reportFile) {
|
|
123
|
+
throw new Error('Performance report must not overwrite its budget.');
|
|
124
|
+
}
|
|
125
|
+
const budget = validatePerformanceBudget(readJson(budgetFile, 'Performance budget'));
|
|
126
|
+
if (budget.profiles.some((profile) => !input.targets.includes(profile.target))) {
|
|
127
|
+
throw new Error('Performance budget targets must belong to the selected acceptance targets.');
|
|
128
|
+
}
|
|
129
|
+
const budgetEvidence = collectGameplayE2EPathEvidence(gameRoot, budgetFile, 'Performance budget');
|
|
130
|
+
if (budgetEvidence.kind !== 'file') {
|
|
131
|
+
throw new Error('Performance budget must be a regular file.');
|
|
132
|
+
}
|
|
133
|
+
return Object.freeze({ budget, budgetEvidence: Object.freeze(budgetEvidence), reportFile });
|
|
134
|
+
}
|
|
135
|
+
/** Accept only fresh untraced foreground measurements with matching authored
|
|
136
|
+
* context, adequate sample counts and current artifact/budget provenance. */
|
|
137
|
+
export function readPerformanceAcceptanceEvidence(input) {
|
|
138
|
+
const { gameRoot, prepared } = input;
|
|
139
|
+
const result = {
|
|
140
|
+
file: relativeOrAbsolute(gameRoot, prepared.reportFile),
|
|
141
|
+
found: false,
|
|
142
|
+
validationError: null,
|
|
143
|
+
budget: prepared.budgetEvidence,
|
|
144
|
+
report: null,
|
|
145
|
+
value: null,
|
|
146
|
+
};
|
|
147
|
+
let value;
|
|
148
|
+
try {
|
|
149
|
+
const file = resolveGameplayE2EPathInsideGameRoot(gameRoot, prepared.reportFile, 'Performance report');
|
|
150
|
+
if (!existsSync(file)) {
|
|
151
|
+
throw new Error('Performance collector did not produce its report.');
|
|
152
|
+
}
|
|
153
|
+
value = readJson(file, 'Performance report');
|
|
154
|
+
const reportEvidence = collectGameplayE2EPathEvidence(gameRoot, file, 'Performance report');
|
|
155
|
+
const report = record(value, 'Performance report');
|
|
156
|
+
if (report.schemaVersion !== 1 || input.startedAtMs === undefined) {
|
|
157
|
+
throw new Error('Performance report needs schemaVersion 1 and a completed collection step.');
|
|
158
|
+
}
|
|
159
|
+
text(report.profile, 'Performance report profile');
|
|
160
|
+
const at = Date.parse(text(report.generatedAt, 'Performance generatedAt'));
|
|
161
|
+
if (!Number.isFinite(at) || new Date(at).toISOString() !== report.generatedAt
|
|
162
|
+
|| at < input.startedAtMs || at > input.finishedAtMs) {
|
|
163
|
+
throw new Error('Performance report timestamp must be inside this collection run.');
|
|
164
|
+
}
|
|
165
|
+
if (input.profile !== undefined && report.profile !== input.profile) {
|
|
166
|
+
throw new Error('Performance report profile must match acceptance.');
|
|
167
|
+
}
|
|
168
|
+
const currentBudget = evidence(gameRoot, report.budget, 'Performance budget');
|
|
169
|
+
if (currentBudget.file !== prepared.budgetEvidence.file
|
|
170
|
+
|| currentBudget.sha256 !== prepared.budgetEvidence.sha256) {
|
|
171
|
+
throw new Error('Performance budget changed during acceptance.');
|
|
172
|
+
}
|
|
173
|
+
if (!Array.isArray(report.measurements) || report.measurements.length !== prepared.budget.profiles.length) {
|
|
174
|
+
throw new Error('Performance report must cover every budget profile exactly once.');
|
|
175
|
+
}
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
const release = input.releaseManifestFile === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: record(readJson(input.releaseManifestFile, 'Release manifest'), 'Release manifest');
|
|
180
|
+
for (const item of report.measurements) {
|
|
181
|
+
const measurement = record(item, 'Performance measurement');
|
|
182
|
+
const id = text(measurement.id, 'Performance measurement id');
|
|
183
|
+
const budget = prepared.budget.profiles.find((profile) => profile.id === id);
|
|
184
|
+
if (budget === undefined || seen.has(id)) {
|
|
185
|
+
throw new Error('Performance measurements have missing/duplicate profiles.');
|
|
186
|
+
}
|
|
187
|
+
seen.add(id);
|
|
188
|
+
const context = record(measurement.context, `${id} context`);
|
|
189
|
+
for (const name of ['target', 'scenario', 'renderer', 'device']) {
|
|
190
|
+
if (context[name] !== budget[name]) {
|
|
191
|
+
throw new Error(`${id} ${name} does not match its budget.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const dpr = number(context.devicePixelRatio, `${id} devicePixelRatio`);
|
|
195
|
+
if (dpr === 0 || dpr > 64 || context.foreground !== true) {
|
|
196
|
+
throw new Error(`${id} must record a positive DPR and foreground measurement.`);
|
|
197
|
+
}
|
|
198
|
+
const instrumentation = record(context.instrumentation, `${id} instrumentation`);
|
|
199
|
+
if (instrumentation.trace !== false || instrumentation.screenshots !== false) {
|
|
200
|
+
throw new Error(`${id} needs an untraced measurement without screenshot capture.`);
|
|
201
|
+
}
|
|
202
|
+
for (const name of ['logicalWidth', 'logicalHeight', 'backingWidth', 'backingHeight']) {
|
|
203
|
+
if (number(context[name], `${id} ${name}`) === 0) {
|
|
204
|
+
throw new Error(`${id} dimensions must be positive.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const artifact = evidence(gameRoot, measurement.artifact, `${id} target artifact`);
|
|
208
|
+
if (release !== undefined) {
|
|
209
|
+
const target = record(record(release.targets, 'Release targets')[budget.target], `${id} release target`);
|
|
210
|
+
if (typeof target.artifact !== 'string'
|
|
211
|
+
|| path.resolve(gameRoot, target.artifact) !== path.resolve(gameRoot, artifact.file)
|
|
212
|
+
|| (target.profile !== undefined && target.profile !== report.profile)) {
|
|
213
|
+
throw new Error(`${id} artifact/profile differs from the release target.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const snapshot = record(measurement.snapshot, `${id} snapshot`);
|
|
217
|
+
const metrics = record(snapshot.metrics, `${id} metrics`);
|
|
218
|
+
const maximums = record(snapshot.maximums, `${id} maximums`);
|
|
219
|
+
const samples = record(snapshot.sampleCounts, `${id} sampleCounts`);
|
|
220
|
+
const retained = count(snapshot.retainedSamples, `${id} retainedSamples`);
|
|
221
|
+
const frames = count(snapshot.frames, `${id} frames`);
|
|
222
|
+
const excluded = count(snapshot.excludedFrames, `${id} excludedFrames`);
|
|
223
|
+
if (frames < retained || (budget.maxExcludedFrames !== undefined && excluded > budget.maxExcludedFrames)) {
|
|
224
|
+
throw new Error(`${id} has inconsistent frame counts or too many excluded frames.`);
|
|
225
|
+
}
|
|
226
|
+
for (const [field, thresholds] of Object.entries(budget.metrics)) {
|
|
227
|
+
const n = count(samples[field], `${id}/${field} sampleCount`);
|
|
228
|
+
if (n < budget.minSamples || n > retained) {
|
|
229
|
+
throw new Error(`${id}/${field} has insufficient/inconsistent samples.`);
|
|
230
|
+
}
|
|
231
|
+
const quantiles = record(metrics[field], `${id}/${field} quantiles`);
|
|
232
|
+
const p50 = number(quantiles.p50, `${id}/${field} p50`);
|
|
233
|
+
const p95 = number(quantiles.p95, `${id}/${field} p95`);
|
|
234
|
+
const p99 = number(quantiles.p99, `${id}/${field} p99`);
|
|
235
|
+
const max = number(maximums[field], `${id}/${field} max`);
|
|
236
|
+
if (p50 > p95 || p95 > p99 || p99 > max) {
|
|
237
|
+
throw new Error(`${id}/${field} quantiles are inconsistent.`);
|
|
238
|
+
}
|
|
239
|
+
for (const [key, threshold] of Object.entries(thresholds)) {
|
|
240
|
+
const measured = key === 'max' ? max : number(quantiles[key], `${id}/${field}/${key}`);
|
|
241
|
+
if (measured > threshold) {
|
|
242
|
+
throw new Error(`${id}/${field} ${key} exceeds its budget.`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (budget.maxHeapGrowthBytes !== undefined
|
|
247
|
+
&& number(measurement.heapGrowthBytes, `${id} heapGrowthBytes`, true) > budget.maxHeapGrowthBytes) {
|
|
248
|
+
throw new Error(`${id} heap growth exceeds its budget.`);
|
|
249
|
+
}
|
|
250
|
+
if (budget.maxHitches !== undefined) {
|
|
251
|
+
const hitches = record(measurement.hitches, `${id} hitches`);
|
|
252
|
+
const n = count(hitches.count, `${id} hitch count`);
|
|
253
|
+
const total = count(hitches.samples, `${id} hitch samples`, budget.minSamples);
|
|
254
|
+
if (n > total || n > budget.maxHitches || hitches.thresholdMs !== budget.hitchThresholdMs) {
|
|
255
|
+
throw new Error(`${id} hitches exceed or do not match their budget.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { ...result, found: true, report: reportEvidence, value };
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return {
|
|
263
|
+
...result,
|
|
264
|
+
found: existsSync(prepared.reportFile),
|
|
265
|
+
validationError: formatError(error),
|
|
266
|
+
value: value ?? null,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
"mpgd": "./dist/bin.js"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@aws-sdk/client-s3": "3.1147.0",
|
|
44
|
+
"@smithy/node-http-handler": "4.12.1",
|
|
43
45
|
"@googleapis/androidpublisher": "42.0.0",
|
|
44
46
|
"@gunshi/plugin-completion": "^0.35.1",
|
|
45
47
|
"@gunshi/plugin-i18n": "^0.35.1",
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
"@xmldom/xmldom": "0.9.12",
|
|
48
50
|
"acorn": "8.17.0",
|
|
49
51
|
"entities": "^8.0.0",
|
|
52
|
+
"fflate": "0.8.3",
|
|
50
53
|
"esbuild": "^0.28.1",
|
|
51
54
|
"gunshi": "^0.35.1",
|
|
52
55
|
"semver": "^7.8.5",
|
|
@@ -54,26 +57,28 @@
|
|
|
54
57
|
"typia": "13.0.2",
|
|
55
58
|
"undici": "^7.29.1",
|
|
56
59
|
"vite": "8.1.3",
|
|
60
|
+
"yauzl": "3.4.0",
|
|
57
61
|
"@mpgd/catalog": "0.8.0",
|
|
58
|
-
"@mpgd/
|
|
59
|
-
"@mpgd/
|
|
62
|
+
"@mpgd/phaser-assets": "0.9.0",
|
|
63
|
+
"@mpgd/target-config": "0.21.0"
|
|
60
64
|
},
|
|
61
65
|
"devDependencies": {
|
|
62
66
|
"@types/node": "^24.0.0",
|
|
63
67
|
"@types/semver": "^7.8.0",
|
|
68
|
+
"@types/yauzl": "3.4.0",
|
|
64
69
|
"ttsc": "0.30.4",
|
|
65
70
|
"typescript": "7.0.2",
|
|
66
|
-
"@mpgd/adapter-ait": "0.13.
|
|
71
|
+
"@mpgd/adapter-ait": "0.13.3",
|
|
67
72
|
"@mpgd/adapter-browser": "0.9.0",
|
|
68
|
-
"@mpgd/adapter-capacitor": "0.7.
|
|
73
|
+
"@mpgd/adapter-capacitor": "0.7.4",
|
|
74
|
+
"@mpgd/adapter-devvit": "0.9.17",
|
|
75
|
+
"@mpgd/adapter-verse8": "0.4.3",
|
|
69
76
|
"@mpgd/analytics": "0.4.3",
|
|
70
|
-
"@mpgd/adapter-verse8": "0.4.2",
|
|
71
77
|
"@mpgd/game-services": "0.19.0",
|
|
72
|
-
"@mpgd/game-runtime": "0.5.0",
|
|
73
|
-
"@mpgd/adapter-devvit": "0.9.17",
|
|
74
78
|
"@mpgd/bridge": "0.11.0",
|
|
75
|
-
"@mpgd/
|
|
76
|
-
"@mpgd/i18n": "0.6.8"
|
|
79
|
+
"@mpgd/game-runtime": "0.6.0",
|
|
80
|
+
"@mpgd/i18n": "0.6.8",
|
|
81
|
+
"@mpgd/platform": "0.17.0"
|
|
77
82
|
},
|
|
78
83
|
"main": "./dist/index.js",
|
|
79
84
|
"types": "./dist/index.d.ts",
|
|
@@ -82,3 +82,10 @@ the snapshot bounds for persistent controls.
|
|
|
82
82
|
Build and smoke the configured ONE play target using the game-owned kit path. Validate server-issued reward/checkout binding, raw-byte signed SSV, purchase token API verification, ledger-before-consume/acknowledge, retries without duplicate grants, signed PNS orphan-purchase recovery and atomic cancellation fences. Missing finalization must keep PNS pending; a busy SDK must report native not-started before rejecting a new checkout. Permit five minutes of provider/server clock skew while rejecting timestamps outside the intent window.
|
|
83
83
|
|
|
84
84
|
Use an authenticated server session rather than the SDK player pseudonym. Confirm ad IDs, real payments, lifecycle, HTTPS iframe hosting and PNS/SSV in ONE store with issued ONEconsole credentials. Generic fixture and artifact smoke do not certify real-app commerce.
|
|
85
|
+
|
|
86
|
+
Performance evidence can be required with `mpgd game accept . --performance`.
|
|
87
|
+
Provide a game-owned `performance:e2e` script and `agent/performance.budget.json`;
|
|
88
|
+
collect a fresh foreground report at `MPGD_PERFORMANCE_REPORT_FILE`. Calibrate
|
|
89
|
+
per-device/scenario CPU and cadence budgets independently, link the tested
|
|
90
|
+
artifact/budget hashes and keep trace/screenshots in separate diagnostic runs.
|
|
91
|
+
See the [report contract](https://github.com/imjlk/mpgd-kit/blob/main/docs/GAME_PERFORMANCE_ACCEPTANCE.md).
|