@playdrop/playdrop-cli 0.13.6 → 0.13.8
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/config/client-meta.json +2 -2
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/loadCheck.d.ts +6 -6
- package/dist/apps/loadCheck.js +109 -199
- package/dist/apps/upload.d.ts +1 -1
- package/dist/apps/upload.js +2 -5
- package/dist/commands/check.js +16 -24
- package/dist/commands/upload.d.ts +2 -5
- package/dist/commands/upload.js +4 -179
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +7 -0
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +9 -0
- package/node_modules/@playdrop/types/dist/version.d.ts +18 -1
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/apps/playtestContract.d.ts +0 -2
- package/dist/apps/playtestContract.js +0 -77
package/dist/commands/upload.js
CHANGED
|
@@ -5,11 +5,9 @@ exports.resolveDefaultUploadCreator = resolveDefaultUploadCreator;
|
|
|
5
5
|
exports.upload = upload;
|
|
6
6
|
exports.resolveWorkerPlaydropAssetRequirement = resolveWorkerPlaydropAssetRequirement;
|
|
7
7
|
exports.assertAgentNewGamePlaytestContract = assertAgentNewGamePlaytestContract;
|
|
8
|
-
exports.assertTaskPlaytestEvidenceManifest = assertTaskPlaytestEvidenceManifest;
|
|
9
8
|
exports.assertWorkerAppLocalUploadPreflight = assertWorkerAppLocalUploadPreflight;
|
|
10
9
|
exports.publishWorkerAppProject = publishWorkerAppProject;
|
|
11
10
|
exports.publishStaticHtmlProject = publishStaticHtmlProject;
|
|
12
|
-
const node_crypto_1 = require("node:crypto");
|
|
13
11
|
const node_fs_1 = require("node:fs");
|
|
14
12
|
const node_path_1 = require("node:path");
|
|
15
13
|
const semver_1 = require("semver");
|
|
@@ -1271,12 +1269,6 @@ function assertAgentNewGamePlaytestContract(task) {
|
|
|
1271
1269
|
if (!tape) {
|
|
1272
1270
|
throw new Error(`agent_task_playtest_tape_missing:${surface}`);
|
|
1273
1271
|
}
|
|
1274
|
-
if (tape.startOnlyEventCount <= 0) {
|
|
1275
|
-
throw new Error(`agent_task_playtest_start_only_required:${surface}: the tape must include an explicit startup action.`);
|
|
1276
|
-
}
|
|
1277
|
-
if (tape.events[0]?.atMs !== 0) {
|
|
1278
|
-
throw new Error(`agent_task_playtest_ready_relative_timing_required:${surface}: the first startup event must run at 0 ms after sdk.host.ready().`);
|
|
1279
|
-
}
|
|
1280
1272
|
}
|
|
1281
1273
|
}
|
|
1282
1274
|
const WORKER_SUPERVISOR_SOURCE_EXCLUDES = [
|
|
@@ -1710,23 +1702,6 @@ async function assertNewGameDeclaredAssetsAreUsedAtRuntime(input) {
|
|
|
1710
1702
|
throw new Error('agent_task_game_asset_fallback_disallowed: new game tasks must fail clearly when declared gameplay assets cannot load instead of rendering placeholder or fallback visuals.');
|
|
1711
1703
|
}
|
|
1712
1704
|
}
|
|
1713
|
-
function readPlaytestEvidenceManifest(task) {
|
|
1714
|
-
const manifestPath = (0, node_path_1.join)(task.projectDir, 'playtest-evidence.json');
|
|
1715
|
-
if (!(0, node_fs_1.existsSync)(manifestPath)) {
|
|
1716
|
-
throw new Error(formatMissingPlaytestEvidenceManifestError('task uploads must include playtest-evidence.json before upload.'));
|
|
1717
|
-
}
|
|
1718
|
-
try {
|
|
1719
|
-
const value = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
|
|
1720
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1721
|
-
throw new Error('manifest must be a JSON object');
|
|
1722
|
-
}
|
|
1723
|
-
return value;
|
|
1724
|
-
}
|
|
1725
|
-
catch (error) {
|
|
1726
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
1727
|
-
throw new Error(formatPlaytestEvidenceManifestError(`playtest-evidence.json must be valid JSON. ${reason}`));
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
1705
|
function assertGreyboxReport(task) {
|
|
1731
1706
|
const reportPath = (0, node_path_1.join)(task.projectDir, 'greybox-report.json');
|
|
1732
1707
|
if (!(0, node_fs_1.existsSync)(reportPath)) {
|
|
@@ -1791,153 +1766,10 @@ async function collectUploadPreflightErrors(checks) {
|
|
|
1791
1766
|
}
|
|
1792
1767
|
return errors;
|
|
1793
1768
|
}
|
|
1794
|
-
const PLAYTEST_EVIDENCE_MANIFEST_HELP = 'Expected shape: {"version":1,"entries":[{"environment":"local","url":"http://localhost:8080/...","surface":"desktop","checkedAt":"2026-07-05T00:00:00.000Z","captures":["evidence/input.png","evidence/win.png","evidence/loss.png"],"actions":["press ArrowRight","complete the final round","allow health to reach zero"],"statesObserved":["player moved right","victory overlay","defeat overlay"],"consoleErrors":[]}],"proof":{"primaryInput":{"action":"press ArrowRight","observed":"player moved right","capture":"evidence/input.png"},"win":{"action":"complete the final round","observed":"victory overlay appeared","capture":"evidence/win.png"},"loss":{"action":"allow health to reach zero","observed":"defeat overlay appeared","capture":"evidence/loss.png"}}}. NEW_GAME proof requires three distinct captures from a current non-final entry. The uploader appends hashes and the final published-route entry automatically.';
|
|
1795
|
-
function formatMissingPlaytestEvidenceManifestError(message) {
|
|
1796
|
-
return `agent_task_playtest_manifest_missing: ${message} ${PLAYTEST_EVIDENCE_MANIFEST_HELP}`;
|
|
1797
|
-
}
|
|
1798
|
-
function formatPlaytestEvidenceManifestError(message) {
|
|
1799
|
-
return `agent_task_playtest_manifest_invalid: ${message} ${PLAYTEST_EVIDENCE_MANIFEST_HELP}`;
|
|
1800
|
-
}
|
|
1801
|
-
function assertStringArrayField(value, field, options) {
|
|
1802
|
-
if (!Array.isArray(value)) {
|
|
1803
|
-
throw new Error(formatPlaytestEvidenceManifestError(`${field} must be an array.`));
|
|
1804
|
-
}
|
|
1805
|
-
const strings = value.map((entry) => typeof entry === 'string' ? entry.trim() : '');
|
|
1806
|
-
if (strings.some((entry) => entry.length === 0)) {
|
|
1807
|
-
throw new Error(formatPlaytestEvidenceManifestError(`${field} entries must be non-empty strings.`));
|
|
1808
|
-
}
|
|
1809
|
-
if (!options?.allowEmpty && strings.length === 0) {
|
|
1810
|
-
throw new Error(formatPlaytestEvidenceManifestError(`${field} must include at least one entry.`));
|
|
1811
|
-
}
|
|
1812
|
-
return strings;
|
|
1813
|
-
}
|
|
1814
|
-
function assertPlaytestCapturePaths(task, captures) {
|
|
1815
|
-
for (const capture of captures) {
|
|
1816
|
-
const capturePath = (0, node_path_1.resolve)(task.projectDir, capture);
|
|
1817
|
-
const relativePath = (0, node_path_1.relative)((0, node_path_1.resolve)(task.projectDir), capturePath).replace(/\\/g, '/');
|
|
1818
|
-
if (relativePath === '' || relativePath === '..' || relativePath.startsWith('../') || relativePath.startsWith('/')) {
|
|
1819
|
-
throw new Error(formatPlaytestEvidenceManifestError('capture paths must stay inside the project.'));
|
|
1820
|
-
}
|
|
1821
|
-
if (!(0, node_fs_1.existsSync)(capturePath) || !(0, node_fs_1.statSync)(capturePath).isFile()) {
|
|
1822
|
-
throw new Error(formatPlaytestEvidenceManifestError(`capture file "${capture}" does not exist.`));
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
function assertExactObjectKeys(record, allowedKeys, field) {
|
|
1827
|
-
const allowed = new Set(allowedKeys);
|
|
1828
|
-
const unexpected = Object.keys(record).find((key) => !allowed.has(key));
|
|
1829
|
-
if (unexpected) {
|
|
1830
|
-
throw new Error(formatPlaytestEvidenceManifestError(`${field} contains unsupported key "${unexpected}".`));
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
function buildRequiredTaskPlaytestProof(task, manifest, currentSelfPlaytestCaptures) {
|
|
1834
|
-
if (!manifest.proof || typeof manifest.proof !== 'object' || Array.isArray(manifest.proof)) {
|
|
1835
|
-
throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json proof must demonstrate primaryInput, win, and loss for NEW_GAME tasks.'));
|
|
1836
|
-
}
|
|
1837
|
-
const rawProof = manifest.proof;
|
|
1838
|
-
assertExactObjectKeys(rawProof, ['primaryInput', 'win', 'loss'], 'proof');
|
|
1839
|
-
const proof = { version: 1 };
|
|
1840
|
-
for (const momentName of ['primaryInput', 'win', 'loss']) {
|
|
1841
|
-
const rawMoment = rawProof[momentName];
|
|
1842
|
-
if (!rawMoment || typeof rawMoment !== 'object' || Array.isArray(rawMoment)) {
|
|
1843
|
-
throw new Error(formatPlaytestEvidenceManifestError(`proof.${momentName} must include action, observed, and capture.`));
|
|
1844
|
-
}
|
|
1845
|
-
const moment = rawMoment;
|
|
1846
|
-
assertExactObjectKeys(moment, ['action', 'observed', 'capture'], `proof.${momentName}`);
|
|
1847
|
-
const action = typeof moment.action === 'string' ? moment.action.trim() : '';
|
|
1848
|
-
const observed = typeof moment.observed === 'string' ? moment.observed.trim() : '';
|
|
1849
|
-
const capture = typeof moment.capture === 'string' ? moment.capture.trim() : '';
|
|
1850
|
-
if (!action || !observed || !capture) {
|
|
1851
|
-
throw new Error(formatPlaytestEvidenceManifestError(`proof.${momentName} must include non-empty action, observed, and capture strings.`));
|
|
1852
|
-
}
|
|
1853
|
-
if (!currentSelfPlaytestCaptures.has(capture)) {
|
|
1854
|
-
throw new Error(formatPlaytestEvidenceManifestError(`proof.${momentName}.capture must be listed by a current non-final playtest entry.`));
|
|
1855
|
-
}
|
|
1856
|
-
const capturePath = (0, node_path_1.resolve)(task.projectDir, capture);
|
|
1857
|
-
const captureSha256 = (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(capturePath)).digest('hex');
|
|
1858
|
-
proof[momentName] = { action, observed, capture, captureSha256 };
|
|
1859
|
-
}
|
|
1860
|
-
const parsed = (0, types_1.normalizeAgentTaskPlaytestProof)(proof);
|
|
1861
|
-
if (!parsed.ok) {
|
|
1862
|
-
throw new Error(formatPlaytestEvidenceManifestError(parsed.error));
|
|
1863
|
-
}
|
|
1864
|
-
return parsed.value;
|
|
1865
|
-
}
|
|
1866
|
-
function assertTaskPlaytestEvidenceManifest(task, options = {}) {
|
|
1867
|
-
const manifest = readPlaytestEvidenceManifest(task);
|
|
1868
|
-
if (manifest.version !== 1) {
|
|
1869
|
-
throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json version must be 1.'));
|
|
1870
|
-
}
|
|
1871
|
-
const entries = Array.isArray(manifest.entries) ? manifest.entries : [];
|
|
1872
|
-
if (entries.length === 0) {
|
|
1873
|
-
throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json entries must include a non-final self-playtest before upload.'));
|
|
1874
|
-
}
|
|
1875
|
-
const sourceLastModifiedAt = readRuntimeSourceLastModifiedAt(task.projectDir);
|
|
1876
|
-
const latestSourceMs = sourceLastModifiedAt ? Date.parse(sourceLastModifiedAt) : null;
|
|
1877
|
-
let hasCurrentSelfPlaytest = false;
|
|
1878
|
-
const currentSelfPlaytestCaptures = new Set();
|
|
1879
|
-
entries.forEach((entry, index) => {
|
|
1880
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1881
|
-
throw new Error(formatPlaytestEvidenceManifestError(`entries[${index}] must be an object.`));
|
|
1882
|
-
}
|
|
1883
|
-
const record = entry;
|
|
1884
|
-
const environment = typeof record.environment === 'string' ? record.environment.trim() : '';
|
|
1885
|
-
const url = typeof record.url === 'string' ? record.url.trim() : '';
|
|
1886
|
-
const surface = typeof record.surface === 'string' ? record.surface.trim() : '';
|
|
1887
|
-
const checkedAt = typeof record.checkedAt === 'string' ? record.checkedAt.trim() : '';
|
|
1888
|
-
if (!environment || !url || !surface || !checkedAt) {
|
|
1889
|
-
throw new Error(formatPlaytestEvidenceManifestError(`entries[${index}] must include environment, url, surface, and checkedAt.`));
|
|
1890
|
-
}
|
|
1891
|
-
const checkedAtMs = Date.parse(checkedAt);
|
|
1892
|
-
if (!Number.isFinite(checkedAtMs)) {
|
|
1893
|
-
throw new Error(formatPlaytestEvidenceManifestError(`entries[${index}].checkedAt must be an ISO date string.`));
|
|
1894
|
-
}
|
|
1895
|
-
const isFinalGateEntry = environment === 'final';
|
|
1896
|
-
const captures = assertStringArrayField(record.captures, `entries[${index}].captures`, { allowEmpty: isFinalGateEntry });
|
|
1897
|
-
assertStringArrayField(record.actions, `entries[${index}].actions`);
|
|
1898
|
-
assertStringArrayField(record.statesObserved, `entries[${index}].statesObserved`, { allowEmpty: isFinalGateEntry });
|
|
1899
|
-
assertStringArrayField(record.consoleErrors, `entries[${index}].consoleErrors`, { allowEmpty: true });
|
|
1900
|
-
assertPlaytestCapturePaths(task, captures);
|
|
1901
|
-
if (!isFinalGateEntry && (latestSourceMs === null || checkedAtMs >= latestSourceMs)) {
|
|
1902
|
-
hasCurrentSelfPlaytest = true;
|
|
1903
|
-
captures.forEach((capture) => currentSelfPlaytestCaptures.add(capture));
|
|
1904
|
-
}
|
|
1905
|
-
});
|
|
1906
|
-
if (!hasCurrentSelfPlaytest) {
|
|
1907
|
-
throw new Error(formatPlaytestEvidenceManifestError(`playtest-evidence.json must include a non-final self-playtest checkedAt after the last source change${sourceLastModifiedAt ? ` (${sourceLastModifiedAt})` : ''}.`));
|
|
1908
|
-
}
|
|
1909
|
-
return options.requireOutcomeProof
|
|
1910
|
-
? buildRequiredTaskPlaytestProof(task, manifest, currentSelfPlaytestCaptures)
|
|
1911
|
-
: null;
|
|
1912
|
-
}
|
|
1913
|
-
function readRuntimeSourceLastModifiedAt(rootDir) {
|
|
1914
|
-
const root = (0, node_path_1.resolve)(rootDir);
|
|
1915
|
-
let latest = 0;
|
|
1916
|
-
const walk = (dir) => {
|
|
1917
|
-
for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
1918
|
-
const absolute = (0, node_path_1.join)(dir, entry.name);
|
|
1919
|
-
const relativePath = absolute.slice(root.length + 1).replace(/\\/g, '/');
|
|
1920
|
-
if (entry.isDirectory()) {
|
|
1921
|
-
if (RUNTIME_SOURCE_EXCLUDED_DIRS.has(entry.name) || RUNTIME_SOURCE_EXCLUDED_DIRS.has(relativePath)) {
|
|
1922
|
-
continue;
|
|
1923
|
-
}
|
|
1924
|
-
walk(absolute);
|
|
1925
|
-
continue;
|
|
1926
|
-
}
|
|
1927
|
-
if (!entry.isFile() || !RUNTIME_SOURCE_EXTENSIONS.has((0, node_path_1.extname)(entry.name).toLowerCase())) {
|
|
1928
|
-
continue;
|
|
1929
|
-
}
|
|
1930
|
-
latest = Math.max(latest, (0, node_fs_1.statSync)(absolute).mtimeMs);
|
|
1931
|
-
}
|
|
1932
|
-
};
|
|
1933
|
-
walk(root);
|
|
1934
|
-
return latest > 0 ? new Date(latest).toISOString() : null;
|
|
1935
|
-
}
|
|
1936
1769
|
async function assertWorkerAppLocalUploadPreflight(input) {
|
|
1937
1770
|
if (input.kind === 'GAME_UPDATE') {
|
|
1938
|
-
return
|
|
1771
|
+
return;
|
|
1939
1772
|
}
|
|
1940
|
-
let playtestProof = null;
|
|
1941
1773
|
const errors = await collectUploadPreflightErrors([
|
|
1942
1774
|
() => assertGreyboxReport(input.task),
|
|
1943
1775
|
() => (0, listingPreflight_1.assertAgentGameHeroListingPreflight)(input.task),
|
|
@@ -1962,11 +1794,6 @@ async function assertWorkerAppLocalUploadPreflight(input) {
|
|
|
1962
1794
|
() => (0, listingPreflight_1.assertAgentGameCaptureReportPreflight)(input.task, {
|
|
1963
1795
|
requireCaptureReport: input.executionTarget === 'FIRST_PARTY',
|
|
1964
1796
|
}),
|
|
1965
|
-
() => {
|
|
1966
|
-
playtestProof = assertTaskPlaytestEvidenceManifest(input.task, {
|
|
1967
|
-
requireOutcomeProof: input.kind === 'NEW_GAME',
|
|
1968
|
-
});
|
|
1969
|
-
},
|
|
1970
1797
|
]);
|
|
1971
1798
|
if (errors.length === 1) {
|
|
1972
1799
|
throw new Error(errors[0]);
|
|
@@ -1974,7 +1801,6 @@ async function assertWorkerAppLocalUploadPreflight(input) {
|
|
|
1974
1801
|
if (errors.length > 1) {
|
|
1975
1802
|
throw new Error(`agent_task_upload_preflight_failed:\n${errors.map((error, index) => `${index + 1}. ${error}`).join('\n')}`);
|
|
1976
1803
|
}
|
|
1977
|
-
return playtestProof;
|
|
1978
1804
|
}
|
|
1979
1805
|
// Task upload path used by "playdrop task upload": same internal pipeline as
|
|
1980
1806
|
// "playdrop project publish", but always as one PRIVATE draft version for the
|
|
@@ -2022,7 +1848,7 @@ async function publishWorkerAppProject(input) {
|
|
|
2022
1848
|
throw new Error(`agent_task_remix_ref_mismatch:${catalogueRemixRef}:${expectedRemixRef}`);
|
|
2023
1849
|
}
|
|
2024
1850
|
}
|
|
2025
|
-
|
|
1851
|
+
await assertWorkerAppLocalUploadPreflight({
|
|
2026
1852
|
task,
|
|
2027
1853
|
kind: input.kind,
|
|
2028
1854
|
executionTarget: input.executionTarget,
|
|
@@ -2063,9 +1889,8 @@ async function publishWorkerAppProject(input) {
|
|
|
2063
1889
|
runStagedUploadLoadCheck: true,
|
|
2064
1890
|
ensureRegisteredAppShell: true,
|
|
2065
1891
|
agentTaskId: input.taskId,
|
|
2066
|
-
playtestProof: playtestProof ?? undefined,
|
|
2067
1892
|
mediaCaptureRequired: input.executionTarget === 'FIRST_PARTY',
|
|
2068
|
-
|
|
1893
|
+
playtestSmokeCheckRequired: input.kind === 'NEW_GAME',
|
|
2069
1894
|
captureSession,
|
|
2070
1895
|
});
|
|
2071
1896
|
if (!uploadResult.versionCreated || !uploadResult.version) {
|
|
@@ -2140,7 +1965,7 @@ async function publishStaticHtmlProject(input) {
|
|
|
2140
1965
|
ensureRegisteredAppShell: true,
|
|
2141
1966
|
agentTaskId: input.taskId,
|
|
2142
1967
|
mediaCaptureRequired: false,
|
|
2143
|
-
|
|
1968
|
+
playtestSmokeCheckRequired: false,
|
|
2144
1969
|
captureSession,
|
|
2145
1970
|
artifacts: prepared.artifacts,
|
|
2146
1971
|
});
|