@playdrop/playdrop-cli 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/client-meta.json +2 -1
- package/dist/apiClient.d.ts +8 -0
- package/dist/apiClient.js +29 -1
- package/dist/captureRuntime.d.ts +13 -0
- package/dist/captureRuntime.js +21 -0
- package/dist/commandContext.js +21 -3
- package/dist/commands/captureRemote.d.ts +2 -0
- package/dist/commands/captureRemote.js +90 -0
- package/dist/commands/review.d.ts +46 -0
- package/dist/commands/review.js +353 -0
- package/dist/commands/worker/runtime.d.ts +12 -0
- package/dist/commands/worker/runtime.js +79 -35
- package/dist/commands/worker.d.ts +17 -3
- package/dist/commands/worker.js +431 -24
- package/dist/index.js +45 -0
- package/dist/workspaceAuth.d.ts +2 -0
- package/dist/workspaceAuth.js +6 -0
- package/node_modules/@playdrop/api-client/dist/client.d.ts +6 -2
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +3 -2
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.js +6 -3
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +4 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +36 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts +6 -2
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +21 -6
- package/node_modules/@playdrop/config/client-meta.json +2 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +163 -3
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.REQUIRED_REVIEW_EVIDENCE_FILES = exports.REVIEW_CRITERIA = void 0;
|
|
7
|
+
exports.validateGameReviewResult = validateGameReviewResult;
|
|
8
|
+
exports.validateReviewResultCommand = validateReviewResultCommand;
|
|
9
|
+
exports.composeReviewEvidence = composeReviewEvidence;
|
|
10
|
+
exports.createReviewRatingCard = createReviewRatingCard;
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
14
|
+
const output_1 = require("../output");
|
|
15
|
+
exports.REVIEW_CRITERIA = [
|
|
16
|
+
'Gameplay / Core Loop',
|
|
17
|
+
'Depth / Replayability',
|
|
18
|
+
'Controls / Input',
|
|
19
|
+
'UX / Usability',
|
|
20
|
+
'First Time User Experience',
|
|
21
|
+
'Visuals / Art Direction',
|
|
22
|
+
'Audio / Feedback',
|
|
23
|
+
'Store Listing & Metadata Accuracy',
|
|
24
|
+
'Safety / Age Rating / Compliance',
|
|
25
|
+
'Performance / Stability',
|
|
26
|
+
];
|
|
27
|
+
const TERMINAL_REVIEW_STATES = new Set([
|
|
28
|
+
'FAILED',
|
|
29
|
+
'LOW_QUALITY',
|
|
30
|
+
'PASSED',
|
|
31
|
+
'GOOD',
|
|
32
|
+
'EXCELLENT',
|
|
33
|
+
]);
|
|
34
|
+
const STATE_OUTCOME = {
|
|
35
|
+
EXCELLENT: 'Excellent',
|
|
36
|
+
FAILED: 'Blocked',
|
|
37
|
+
GOOD: 'Good',
|
|
38
|
+
LOW_QUALITY: 'Limited',
|
|
39
|
+
PASSED: 'Passed',
|
|
40
|
+
};
|
|
41
|
+
exports.REQUIRED_REVIEW_EVIDENCE_FILES = [
|
|
42
|
+
'core.png',
|
|
43
|
+
'win.png',
|
|
44
|
+
'loss.png',
|
|
45
|
+
'composite.png',
|
|
46
|
+
'rating-card.png',
|
|
47
|
+
];
|
|
48
|
+
const REVIEW_EVIDENCE_MAX_BYTES = 10 * 1024 * 1024;
|
|
49
|
+
function resolveFilePath(filePath, code) {
|
|
50
|
+
const normalized = typeof filePath === 'string' ? filePath.trim() : '';
|
|
51
|
+
if (!normalized) {
|
|
52
|
+
throw new Error(code);
|
|
53
|
+
}
|
|
54
|
+
return node_path_1.default.isAbsolute(normalized) ? normalized : node_path_1.default.resolve(process.cwd(), normalized);
|
|
55
|
+
}
|
|
56
|
+
async function readRequiredTextFile(filePath, code) {
|
|
57
|
+
const resolved = resolveFilePath(filePath, code);
|
|
58
|
+
const content = await node_fs_1.promises.readFile(resolved, 'utf8');
|
|
59
|
+
if (!content.trim()) {
|
|
60
|
+
throw new Error(code);
|
|
61
|
+
}
|
|
62
|
+
return content;
|
|
63
|
+
}
|
|
64
|
+
function requireLine(text, prefix) {
|
|
65
|
+
const line = text.split(/\r?\n/).find((entry) => entry.startsWith(prefix));
|
|
66
|
+
if (!line) {
|
|
67
|
+
throw new Error(`missing_review_line:${prefix}`);
|
|
68
|
+
}
|
|
69
|
+
return line.slice(prefix.length).trim();
|
|
70
|
+
}
|
|
71
|
+
function normalizeReviewText(...values) {
|
|
72
|
+
return values
|
|
73
|
+
.join(' ')
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/\s+/g, ' ')
|
|
76
|
+
.trim();
|
|
77
|
+
}
|
|
78
|
+
function parseScores(reviewMessage) {
|
|
79
|
+
if (/\/5\b/.test(reviewMessage)) {
|
|
80
|
+
throw new Error('review_scores_must_use_10_point_scale');
|
|
81
|
+
}
|
|
82
|
+
const scores = {};
|
|
83
|
+
for (const criterion of exports.REVIEW_CRITERIA) {
|
|
84
|
+
const escaped = criterion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
85
|
+
const regex = new RegExp(`^- ${escaped}: (10|[1-9])/10 \\| .+`, 'm');
|
|
86
|
+
const match = reviewMessage.match(regex);
|
|
87
|
+
if (!match) {
|
|
88
|
+
throw new Error(`missing_or_invalid_criterion_score:${criterion}`);
|
|
89
|
+
}
|
|
90
|
+
scores[criterion] = Number.parseInt(match[1], 10);
|
|
91
|
+
}
|
|
92
|
+
return scores;
|
|
93
|
+
}
|
|
94
|
+
function averageScore(scores) {
|
|
95
|
+
const values = Object.values(scores);
|
|
96
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
97
|
+
}
|
|
98
|
+
function minimumScore(scores) {
|
|
99
|
+
return Math.min(...Object.values(scores));
|
|
100
|
+
}
|
|
101
|
+
function validateOutcomeAgainstScores(outcome, scores) {
|
|
102
|
+
const average = averageScore(scores);
|
|
103
|
+
const minimum = minimumScore(scores);
|
|
104
|
+
const gameplay = scores['Gameplay / Core Loop'];
|
|
105
|
+
const controls = scores['Controls / Input'];
|
|
106
|
+
const ux = scores['UX / Usability'];
|
|
107
|
+
const ftue = scores['First Time User Experience'];
|
|
108
|
+
const visuals = scores['Visuals / Art Direction'];
|
|
109
|
+
const performance = scores['Performance / Stability'];
|
|
110
|
+
const actual = `actual=average=${average.toFixed(2)},minimum=${minimum},gameplay=${gameplay},controls=${controls},ux=${ux},ftue=${ftue},visuals=${visuals},performance=${performance}`;
|
|
111
|
+
if (outcome === 'Excellent' && (average < 8.5 || minimum < 7 || gameplay < 8 || controls < 8 || ux < 8 || ftue < 7 || visuals < 8 || performance < 8)) {
|
|
112
|
+
throw new Error(`excellent_outcome_not_supported_by_scores:requires=average>=8.5,minimum>=7,gameplay>=8,controls>=8,ux>=8,ftue>=7,visuals>=8,performance>=8:${actual}:use_good_or_lower_outcome_unless_scores_are_supported_by_evidence`);
|
|
113
|
+
}
|
|
114
|
+
if (outcome === 'Good' && (average < 7.5 || minimum < 6 || gameplay < 7 || controls < 7 || ux < 7 || ftue < 6 || visuals < 7 || performance < 7)) {
|
|
115
|
+
throw new Error(`good_outcome_not_supported_by_scores:requires=average>=7.5,minimum>=6,gameplay>=7,controls>=7,ux>=7,ftue>=6,visuals>=7,performance>=7:${actual}:use_passed_or_lower_outcome_unless_scores_are_supported_by_evidence`);
|
|
116
|
+
}
|
|
117
|
+
if (outcome === 'Passed' && (average < 6 || minimum < 4 || gameplay < 5 || controls < 5 || performance < 5)) {
|
|
118
|
+
throw new Error(`passed_outcome_not_supported_by_scores:requires=average>=6,minimum>=4,gameplay>=5,controls>=5,performance>=5:${actual}:use_low_quality_or_failed_unless_scores_are_supported_by_evidence`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function validateRequiredEvidenceLines(reviewMessage) {
|
|
122
|
+
const primaryVerb = requireLine(reviewMessage, 'Primary player verb: ');
|
|
123
|
+
const primaryEvidence = requireLine(reviewMessage, 'Primary interaction evidence: ');
|
|
124
|
+
const challengeEvidence = requireLine(reviewMessage, 'Challenge evidence: ');
|
|
125
|
+
const punchline = requireLine(reviewMessage, 'Punchline assessment: ');
|
|
126
|
+
const benchmark = requireLine(reviewMessage, 'Comparable benchmark: ');
|
|
127
|
+
if (primaryVerb.length < 3) {
|
|
128
|
+
throw new Error('primary_player_verb_too_vague');
|
|
129
|
+
}
|
|
130
|
+
if (primaryEvidence.length < 20) {
|
|
131
|
+
throw new Error('primary_interaction_evidence_too_vague');
|
|
132
|
+
}
|
|
133
|
+
if (challengeEvidence.length < 20) {
|
|
134
|
+
throw new Error('challenge_evidence_too_vague');
|
|
135
|
+
}
|
|
136
|
+
if (punchline.length < 8 || punchline.length > 160) {
|
|
137
|
+
throw new Error('punchline_assessment_invalid_length');
|
|
138
|
+
}
|
|
139
|
+
if (benchmark.length < 8) {
|
|
140
|
+
throw new Error('comparable_benchmark_too_vague');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function isNoScoreCaps(scoreCapsApplied) {
|
|
144
|
+
return scoreCapsApplied.trim().toLowerCase() === 'none';
|
|
145
|
+
}
|
|
146
|
+
function validateCapConsistency(input) {
|
|
147
|
+
const normalizedText = normalizeReviewText(input.reviewMessage, input.creatorFeedback);
|
|
148
|
+
const describesZeroChallenge = /\bzero challenge\b|\bno (?:real |meaningful )?challenge\b(?!-)|\bchallenge (?:is )?(?:absent|missing|nonexistent)\b/.test(normalizedText);
|
|
149
|
+
const describesPrimaryMissing = /\bprimary (?:interaction|verb) (?:is )?(?:absent|missing|unavailable)\b/.test(normalizedText);
|
|
150
|
+
if (isNoScoreCaps(input.scoreCapsApplied) && (describesZeroChallenge || describesPrimaryMissing)) {
|
|
151
|
+
const trigger = describesZeroChallenge ? 'zero_or_missing_challenge' : 'primary_interaction_missing';
|
|
152
|
+
throw new Error(`missing_required_score_cap:${trigger}:score_caps_applied_must_name_the_triggered_cap_or_adjust_the_evidence_wording`);
|
|
153
|
+
}
|
|
154
|
+
if (describesZeroChallenge && (['Passed', 'Good', 'Excellent'].includes(input.outcome) || input.scores['Gameplay / Core Loop'] > 3 || input.scores['Depth / Replayability'] > 3)) {
|
|
155
|
+
throw new Error('gameplay_challenge_absent_score_cap_required');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function assertPngFile(filePath) {
|
|
159
|
+
const bytes = await node_fs_1.promises.readFile(filePath);
|
|
160
|
+
if (bytes.length <= 0 || bytes.length > REVIEW_EVIDENCE_MAX_BYTES) {
|
|
161
|
+
throw new Error(`invalid_review_evidence_size:${filePath}`);
|
|
162
|
+
}
|
|
163
|
+
const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
164
|
+
for (let index = 0; index < signature.length; index += 1) {
|
|
165
|
+
if (bytes[index] !== signature[index]) {
|
|
166
|
+
throw new Error(`review_evidence_not_png:${filePath}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function validateGameReviewResult(input) {
|
|
171
|
+
const normalizedState = String(input.reviewState || '').trim().toUpperCase();
|
|
172
|
+
if (!TERMINAL_REVIEW_STATES.has(normalizedState)) {
|
|
173
|
+
return { skipped: true, reason: 'non_terminal_state' };
|
|
174
|
+
}
|
|
175
|
+
if (!input.reviewMessage.trim()) {
|
|
176
|
+
throw new Error('missing_review_message');
|
|
177
|
+
}
|
|
178
|
+
const creatorFeedback = input.creatorFeedback ?? '';
|
|
179
|
+
if (!creatorFeedback.trim()) {
|
|
180
|
+
throw new Error('missing_creator_feedback');
|
|
181
|
+
}
|
|
182
|
+
if (!input.evidenceDir.trim()) {
|
|
183
|
+
throw new Error('missing_evidence_dir');
|
|
184
|
+
}
|
|
185
|
+
const expectedOutcome = STATE_OUTCOME[normalizedState];
|
|
186
|
+
const actualOutcome = requireLine(input.reviewMessage, 'Outcome: ');
|
|
187
|
+
if (actualOutcome !== expectedOutcome) {
|
|
188
|
+
throw new Error(`review_outcome_state_mismatch:${actualOutcome}:${normalizedState}`);
|
|
189
|
+
}
|
|
190
|
+
const primarySurface = requireLine(input.reviewMessage, 'Primary reviewed surface: ');
|
|
191
|
+
if (!['MOBILE_PORTRAIT', 'MOBILE_LANDSCAPE', 'DESKTOP'].includes(primarySurface)) {
|
|
192
|
+
throw new Error(`invalid_primary_reviewed_surface:${primarySurface}`);
|
|
193
|
+
}
|
|
194
|
+
validateRequiredEvidenceLines(input.reviewMessage);
|
|
195
|
+
const scoreCapsApplied = requireLine(input.reviewMessage, 'Score caps applied: ');
|
|
196
|
+
if (!creatorFeedback.includes(`Reviewed surface: ${primarySurface}`)) {
|
|
197
|
+
throw new Error('creator_feedback_missing_reviewed_surface');
|
|
198
|
+
}
|
|
199
|
+
const scores = parseScores(input.reviewMessage);
|
|
200
|
+
validateOutcomeAgainstScores(actualOutcome, scores);
|
|
201
|
+
validateCapConsistency({
|
|
202
|
+
creatorFeedback,
|
|
203
|
+
outcome: actualOutcome,
|
|
204
|
+
reviewMessage: input.reviewMessage,
|
|
205
|
+
scoreCapsApplied,
|
|
206
|
+
scores,
|
|
207
|
+
});
|
|
208
|
+
const evidenceDir = resolveFilePath(input.evidenceDir, 'missing_evidence_dir');
|
|
209
|
+
for (const filename of exports.REQUIRED_REVIEW_EVIDENCE_FILES) {
|
|
210
|
+
await assertPngFile(node_path_1.default.join(evidenceDir, filename));
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
average: averageScore(scores),
|
|
214
|
+
evidenceDir,
|
|
215
|
+
outcome: actualOutcome,
|
|
216
|
+
primarySurface,
|
|
217
|
+
scoreCount: Object.keys(scores).length,
|
|
218
|
+
skipped: false,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async function validateReviewResultCommand(options) {
|
|
222
|
+
const result = await validateGameReviewResult({
|
|
223
|
+
creatorFeedback: options.creatorFeedbackFile ? await readRequiredTextFile(options.creatorFeedbackFile, 'invalid_creator_feedback_file') : '',
|
|
224
|
+
evidenceDir: options.evidenceDir,
|
|
225
|
+
reviewMessage: await readRequiredTextFile(options.messageFile, 'invalid_review_message_file'),
|
|
226
|
+
reviewState: options.state,
|
|
227
|
+
});
|
|
228
|
+
console.log(JSON.stringify(result, null, 2));
|
|
229
|
+
}
|
|
230
|
+
async function resizeForComposite(filePath) {
|
|
231
|
+
const image = (0, sharp_1.default)(resolveFilePath(filePath, 'invalid_review_evidence_file'));
|
|
232
|
+
const metadata = await image.metadata();
|
|
233
|
+
if (!metadata.width || !metadata.height) {
|
|
234
|
+
throw new Error('invalid_review_evidence_image');
|
|
235
|
+
}
|
|
236
|
+
const buffer = await image
|
|
237
|
+
.resize({ width: 540, height: 960, fit: 'inside', withoutEnlargement: true })
|
|
238
|
+
.png()
|
|
239
|
+
.toBuffer();
|
|
240
|
+
const resizedMetadata = await (0, sharp_1.default)(buffer).metadata();
|
|
241
|
+
if (!resizedMetadata.width || !resizedMetadata.height) {
|
|
242
|
+
throw new Error('invalid_review_evidence_image');
|
|
243
|
+
}
|
|
244
|
+
return { buffer, width: resizedMetadata.width, height: resizedMetadata.height };
|
|
245
|
+
}
|
|
246
|
+
async function composeReviewEvidence(options) {
|
|
247
|
+
const entries = [
|
|
248
|
+
options.core ? { label: 'core', path: options.core } : null,
|
|
249
|
+
options.win ? { label: 'win', path: options.win } : null,
|
|
250
|
+
options.loss ? { label: 'loss', path: options.loss } : null,
|
|
251
|
+
].filter((entry) => Boolean(entry));
|
|
252
|
+
if (entries.length === 0) {
|
|
253
|
+
throw new Error('missing_review_evidence_images');
|
|
254
|
+
}
|
|
255
|
+
const images = await Promise.all(entries.map((entry) => resizeForComposite(entry.path)));
|
|
256
|
+
const padding = 16;
|
|
257
|
+
const width = images.reduce((sum, image) => sum + image.width, padding * (images.length + 1));
|
|
258
|
+
const height = Math.max(...images.map((image) => image.height)) + padding * 2;
|
|
259
|
+
let left = padding;
|
|
260
|
+
const composites = images.map((image) => {
|
|
261
|
+
const composite = { input: image.buffer, left, top: padding };
|
|
262
|
+
left += image.width + padding;
|
|
263
|
+
return composite;
|
|
264
|
+
});
|
|
265
|
+
const outputPath = resolveFilePath(options.out, 'missing_out');
|
|
266
|
+
await node_fs_1.promises.mkdir(node_path_1.default.dirname(outputPath), { recursive: true });
|
|
267
|
+
await (0, sharp_1.default)({
|
|
268
|
+
create: {
|
|
269
|
+
width,
|
|
270
|
+
height,
|
|
271
|
+
channels: 4,
|
|
272
|
+
background: { r: 12, g: 12, b: 16, alpha: 1 },
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
.composite(composites)
|
|
276
|
+
.png()
|
|
277
|
+
.toFile(outputPath);
|
|
278
|
+
(0, output_1.printSuccess)(`Review evidence composite written to ${outputPath}`);
|
|
279
|
+
return { outputPath, imageCount: images.length };
|
|
280
|
+
}
|
|
281
|
+
function escapeXml(value) {
|
|
282
|
+
return value
|
|
283
|
+
.replace(/&/g, '&')
|
|
284
|
+
.replace(/</g, '<')
|
|
285
|
+
.replace(/>/g, '>')
|
|
286
|
+
.replace(/"/g, '"');
|
|
287
|
+
}
|
|
288
|
+
function parseCriterionRating(reviewMessage, criterion) {
|
|
289
|
+
const escaped = criterion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
290
|
+
const regex = new RegExp(`^- ${escaped}: (10|[1-9])/10 \\| .+`, 'm');
|
|
291
|
+
const match = reviewMessage.match(regex);
|
|
292
|
+
if (!match) {
|
|
293
|
+
throw new Error(`missing_or_invalid_criterion_score:${criterion}`);
|
|
294
|
+
}
|
|
295
|
+
return Number.parseInt(match[1], 10);
|
|
296
|
+
}
|
|
297
|
+
function ratingColor(score) {
|
|
298
|
+
if (score <= 1)
|
|
299
|
+
return '#ff3b30';
|
|
300
|
+
if (score <= 4)
|
|
301
|
+
return '#ff9500';
|
|
302
|
+
if (score <= 6)
|
|
303
|
+
return '#ffd60a';
|
|
304
|
+
if (score <= 9)
|
|
305
|
+
return '#34c759';
|
|
306
|
+
return '#bf5af2';
|
|
307
|
+
}
|
|
308
|
+
function outcomeColor(outcome) {
|
|
309
|
+
if (outcome === 'Blocked')
|
|
310
|
+
return '#ff3b30';
|
|
311
|
+
if (outcome === 'Limited')
|
|
312
|
+
return '#ff9500';
|
|
313
|
+
if (outcome === 'Passed')
|
|
314
|
+
return '#ffd60a';
|
|
315
|
+
if (outcome === 'Good')
|
|
316
|
+
return '#34c759';
|
|
317
|
+
if (outcome === 'Excellent')
|
|
318
|
+
return '#bf5af2';
|
|
319
|
+
return '#ff9500';
|
|
320
|
+
}
|
|
321
|
+
async function createReviewRatingCard(options) {
|
|
322
|
+
const reviewMessage = await readRequiredTextFile(options.reviewMessageFile, 'invalid_review_message_file');
|
|
323
|
+
const outcome = requireLine(reviewMessage, 'Outcome: ');
|
|
324
|
+
const punchline = (options.punchline?.trim() || requireLine(reviewMessage, 'Punchline assessment: ')).slice(0, 160);
|
|
325
|
+
const title = (options.title?.trim() || 'Game review ratings').slice(0, 80);
|
|
326
|
+
const rows = exports.REVIEW_CRITERIA.map((criterion, index) => {
|
|
327
|
+
const score = parseCriterionRating(reviewMessage, criterion);
|
|
328
|
+
const x = index < 5 ? 56 : 628;
|
|
329
|
+
const y = 170 + (index % 5) * 92;
|
|
330
|
+
return `
|
|
331
|
+
<rect x="${x}" y="${y}" width="516" height="70" rx="6" fill="#15151c" stroke="#333340"/>
|
|
332
|
+
<rect x="${x}" y="${y}" width="8" height="70" fill="${ratingColor(score)}"/>
|
|
333
|
+
<text x="${x + 24}" y="${y + 42}" fill="#f5f5f7" font-size="22" font-weight="700">${escapeXml(criterion)}</text>
|
|
334
|
+
<text x="${x + 448}" y="${y + 44}" fill="${ratingColor(score)}" font-size="32" font-weight="800" text-anchor="end">${score}/10</text>
|
|
335
|
+
`;
|
|
336
|
+
}).join('');
|
|
337
|
+
const svg = `
|
|
338
|
+
<svg width="1200" height="760" xmlns="http://www.w3.org/2000/svg">
|
|
339
|
+
<rect width="1200" height="760" fill="#000"/>
|
|
340
|
+
<text x="56" y="82" fill="#fff" font-size="38" font-weight="800">${escapeXml(title)}</text>
|
|
341
|
+
<text x="1144" y="82" fill="${outcomeColor(outcome)}" font-size="30" font-weight="800" text-anchor="end">${escapeXml(outcome)}</text>
|
|
342
|
+
<line x1="56" y1="122" x2="1144" y2="122" stroke="#333340"/>
|
|
343
|
+
${rows}
|
|
344
|
+
<line x1="56" y1="650" x2="1144" y2="650" stroke="#333340"/>
|
|
345
|
+
<text x="600" y="708" fill="${outcomeColor(outcome)}" font-size="32" font-weight="800" text-anchor="middle">${escapeXml(punchline)}</text>
|
|
346
|
+
</svg>
|
|
347
|
+
`;
|
|
348
|
+
const outputPath = resolveFilePath(options.out, 'missing_out');
|
|
349
|
+
await node_fs_1.promises.mkdir(node_path_1.default.dirname(outputPath), { recursive: true });
|
|
350
|
+
await (0, sharp_1.default)(Buffer.from(svg)).png().toFile(outputPath);
|
|
351
|
+
(0, output_1.printSuccess)(`Review rating card written to ${outputPath}`);
|
|
352
|
+
return { outputPath, width: 1200, height: 760 };
|
|
353
|
+
}
|
|
@@ -9,14 +9,25 @@ export type LoggedProcessResult = {
|
|
|
9
9
|
stderr: string;
|
|
10
10
|
outputTail: string;
|
|
11
11
|
timedOut: boolean;
|
|
12
|
+
tokenUsage: AgentTokenUsage;
|
|
12
13
|
tokensUsed: number | null;
|
|
13
14
|
};
|
|
15
|
+
export type AgentTokenUsage = {
|
|
16
|
+
inputTokens: number | null;
|
|
17
|
+
outputTokens: number | null;
|
|
18
|
+
cacheCreationInputTokens: number | null;
|
|
19
|
+
cacheReadInputTokens: number | null;
|
|
20
|
+
totalTokens: number | null;
|
|
21
|
+
rawProviderUsage: Record<string, unknown> | null;
|
|
22
|
+
usageParseError: string | null;
|
|
23
|
+
};
|
|
14
24
|
export type LoggedProcessTranscriptChunk = {
|
|
15
25
|
stream: 'stdout' | 'stderr';
|
|
16
26
|
content: string;
|
|
17
27
|
};
|
|
18
28
|
export declare function readPositiveEnvInt(name: string, fallback: number): number;
|
|
19
29
|
export declare function readEnvBoolean(name: string, fallback: boolean): boolean;
|
|
30
|
+
export declare function extractAgentTokenUsage(output: string): AgentTokenUsage;
|
|
20
31
|
export declare function extractCodexTokensUsed(output: string): number | null;
|
|
21
32
|
export declare function assertWorkerTokenUsageWithinCap(input: {
|
|
22
33
|
tokensUsed: number | null;
|
|
@@ -62,6 +73,7 @@ export declare function buildClaudeDeniedPermissionRules(denyReadRoots?: string[
|
|
|
62
73
|
export declare function buildWorkerChildEnv(input: {
|
|
63
74
|
binDir: string;
|
|
64
75
|
taskId: number;
|
|
76
|
+
attempt: number;
|
|
65
77
|
envName: string;
|
|
66
78
|
eventDir?: string;
|
|
67
79
|
devPort?: number;
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.DEFAULT_TRANSCRIPT_FLUSH_INTERVAL_MS = exports.DEFAULT_CODEX_LOG_TAIL_CHARS = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = void 0;
|
|
7
7
|
exports.readPositiveEnvInt = readPositiveEnvInt;
|
|
8
8
|
exports.readEnvBoolean = readEnvBoolean;
|
|
9
|
+
exports.extractAgentTokenUsage = extractAgentTokenUsage;
|
|
9
10
|
exports.extractCodexTokensUsed = extractCodexTokensUsed;
|
|
10
11
|
exports.assertWorkerTokenUsageWithinCap = assertWorkerTokenUsageWithinCap;
|
|
11
12
|
exports.runLoggedProcess = runLoggedProcess;
|
|
@@ -52,7 +53,45 @@ function readEnvBoolean(name, fallback) {
|
|
|
52
53
|
}
|
|
53
54
|
throw new Error(`invalid_${name.toLowerCase()}`);
|
|
54
55
|
}
|
|
55
|
-
function
|
|
56
|
+
function normalizeTokenCount(value) {
|
|
57
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return Math.ceil(value);
|
|
61
|
+
}
|
|
62
|
+
function coerceRecord(value) {
|
|
63
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function readStructuredUsage(parsed) {
|
|
69
|
+
const root = coerceRecord(parsed);
|
|
70
|
+
if (!root) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const directUsage = coerceRecord(root.usage);
|
|
74
|
+
if (directUsage) {
|
|
75
|
+
return directUsage;
|
|
76
|
+
}
|
|
77
|
+
const message = coerceRecord(root.message);
|
|
78
|
+
const messageUsage = coerceRecord(message?.usage);
|
|
79
|
+
if (messageUsage) {
|
|
80
|
+
return messageUsage;
|
|
81
|
+
}
|
|
82
|
+
const event = coerceRecord(root.event);
|
|
83
|
+
const eventUsage = coerceRecord(event?.usage);
|
|
84
|
+
if (eventUsage) {
|
|
85
|
+
return eventUsage;
|
|
86
|
+
}
|
|
87
|
+
const eventMessage = coerceRecord(event?.message);
|
|
88
|
+
const eventMessageUsage = coerceRecord(eventMessage?.usage);
|
|
89
|
+
if (eventMessageUsage) {
|
|
90
|
+
return eventMessageUsage;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function extractAgentTokenUsage(output) {
|
|
56
95
|
for (const line of output.split(/\r?\n/).reverse()) {
|
|
57
96
|
const trimmed = line.trim();
|
|
58
97
|
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
@@ -60,50 +99,49 @@ function extractCodexTokensUsed(output) {
|
|
|
60
99
|
}
|
|
61
100
|
try {
|
|
62
101
|
const parsed = JSON.parse(trimmed);
|
|
63
|
-
const usage = parsed
|
|
102
|
+
const usage = readStructuredUsage(parsed);
|
|
64
103
|
if (!usage) {
|
|
65
104
|
continue;
|
|
66
105
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
106
|
+
const inputTokens = normalizeTokenCount(usage.input_tokens);
|
|
107
|
+
const outputTokens = normalizeTokenCount(usage.output_tokens);
|
|
108
|
+
const cacheCreationInputTokens = normalizeTokenCount(usage.cache_creation_input_tokens);
|
|
109
|
+
const cacheReadInputTokens = normalizeTokenCount(usage.cache_read_input_tokens);
|
|
110
|
+
const tokenParts = [
|
|
111
|
+
inputTokens,
|
|
112
|
+
outputTokens,
|
|
113
|
+
cacheCreationInputTokens,
|
|
114
|
+
cacheReadInputTokens,
|
|
72
115
|
];
|
|
73
|
-
|
|
74
|
-
let sawTokenValue = false;
|
|
75
|
-
for (const key of tokenKeys) {
|
|
76
|
-
const value = usage[key];
|
|
77
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
total += value;
|
|
81
|
-
sawTokenValue = true;
|
|
82
|
-
}
|
|
116
|
+
const sawTokenValue = tokenParts.some((value) => value !== null);
|
|
83
117
|
if (sawTokenValue) {
|
|
84
|
-
return
|
|
118
|
+
return {
|
|
119
|
+
inputTokens,
|
|
120
|
+
outputTokens,
|
|
121
|
+
cacheCreationInputTokens,
|
|
122
|
+
cacheReadInputTokens,
|
|
123
|
+
totalTokens: tokenParts.reduce((sum, value) => sum + (value ?? 0), 0),
|
|
124
|
+
rawProviderUsage: usage,
|
|
125
|
+
usageParseError: null,
|
|
126
|
+
};
|
|
85
127
|
}
|
|
86
128
|
}
|
|
87
129
|
catch {
|
|
88
|
-
// Fall through to the plain-text patterns below.
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const patterns = [
|
|
92
|
-
/total\s+tokens\s*:?\s*([\d,]+)/i,
|
|
93
|
-
/tokens\s+used\s*:?\s*([\d,]+)/i,
|
|
94
|
-
/"total_tokens"\s*:\s*([\d,]+)/i,
|
|
95
|
-
];
|
|
96
|
-
for (const pattern of patterns) {
|
|
97
|
-
const match = pattern.exec(output);
|
|
98
|
-
if (!match?.[1]) {
|
|
99
130
|
continue;
|
|
100
131
|
}
|
|
101
|
-
const parsed = Number.parseInt(match[1].replace(/,/g, ''), 10);
|
|
102
|
-
if (Number.isInteger(parsed) && parsed >= 0) {
|
|
103
|
-
return parsed;
|
|
104
|
-
}
|
|
105
132
|
}
|
|
106
|
-
return
|
|
133
|
+
return {
|
|
134
|
+
inputTokens: null,
|
|
135
|
+
outputTokens: null,
|
|
136
|
+
cacheCreationInputTokens: null,
|
|
137
|
+
cacheReadInputTokens: null,
|
|
138
|
+
totalTokens: null,
|
|
139
|
+
rawProviderUsage: null,
|
|
140
|
+
usageParseError: 'agent_usage_not_reported',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function extractCodexTokensUsed(output) {
|
|
144
|
+
return extractAgentTokenUsage(output).totalTokens;
|
|
107
145
|
}
|
|
108
146
|
function assertWorkerTokenUsageWithinCap(input) {
|
|
109
147
|
if (input.tokensUsed === null) {
|
|
@@ -228,6 +266,7 @@ async function runLoggedProcess(input) {
|
|
|
228
266
|
throw transcriptFlushError;
|
|
229
267
|
}
|
|
230
268
|
const outputTail = tailText(combined, input.maxOutputChars);
|
|
269
|
+
const tokenUsage = extractAgentTokenUsage(`${stdout}\n${stderr}\n${outputTail}`);
|
|
231
270
|
resolve({
|
|
232
271
|
exitCode: code,
|
|
233
272
|
signal,
|
|
@@ -235,7 +274,8 @@ async function runLoggedProcess(input) {
|
|
|
235
274
|
stderr,
|
|
236
275
|
outputTail,
|
|
237
276
|
timedOut,
|
|
238
|
-
|
|
277
|
+
tokenUsage,
|
|
278
|
+
tokensUsed: tokenUsage.totalTokens,
|
|
239
279
|
});
|
|
240
280
|
})().catch(reject);
|
|
241
281
|
});
|
|
@@ -399,7 +439,11 @@ function buildWorkerChildEnv(input) {
|
|
|
399
439
|
}
|
|
400
440
|
}
|
|
401
441
|
child.PLAYDROP_WORKER_CONTEXT = '1';
|
|
442
|
+
if (!Number.isInteger(input.attempt) || input.attempt <= 0) {
|
|
443
|
+
throw new Error('invalid_worker_task_attempt');
|
|
444
|
+
}
|
|
402
445
|
child.PLAYDROP_WORKER_TASK_ID = String(input.taskId);
|
|
446
|
+
child.PLAYDROP_WORKER_TASK_ATTEMPT = String(input.attempt);
|
|
403
447
|
if (input.devPort !== undefined) {
|
|
404
448
|
if (!Number.isInteger(input.devPort) || input.devPort <= 0 || input.devPort > 65535) {
|
|
405
449
|
throw new Error('invalid_worker_task_dev_port');
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ApiClient } from '@playdrop/api-client';
|
|
2
|
-
import { type AgentExecutionTarget, type AgentRuntime, type AgentTaskResponse, type AgentWorkerCapabilities, type WorkerAgentTaskAssignmentResponse, type WorkerAgentTaskBaseSourceResponse, type WorkerAgentTaskWorkspaceFileResponse, type WorkerClaimAgentTaskResponse } from '@playdrop/types';
|
|
2
|
+
import { type AgentExecutionTarget, type AgentRuntime, type AgentTaskKind, type AgentTaskResponse, type AgentWorkerCapabilities, type WorkerAgentTaskAssignmentResponse, type WorkerAgentTaskBaseSourceResponse, type WorkerAgentTaskWorkspaceFileResponse, type WorkerClaimAgentTaskResponse } from '@playdrop/types';
|
|
3
3
|
import { type WorkerPlaydropAssetRequirement } from './upload';
|
|
4
4
|
import { type LoggedProcessResult, type LoggedProcessTranscriptChunk } from './worker/runtime';
|
|
5
|
-
export { DEFAULT_CODEX_TIMEOUT_MS, DEFAULT_WORKER_TOKEN_CAP, assertWorkerTokenUsageWithinCap, buildCodexExecArgs, buildClaudeExecArgs, buildClaudePermissionSettings, buildWorkerChildEnv, extractCodexTokensUsed, readEnvBoolean, readCodexSandboxMode, runLoggedProcess, } from './worker/runtime';
|
|
6
|
-
export type { LoggedProcessResult, LoggedProcessTranscriptChunk, } from './worker/runtime';
|
|
5
|
+
export { DEFAULT_CODEX_TIMEOUT_MS, DEFAULT_WORKER_TOKEN_CAP, assertWorkerTokenUsageWithinCap, buildCodexExecArgs, buildClaudeExecArgs, buildClaudePermissionSettings, buildWorkerChildEnv, extractAgentTokenUsage, extractCodexTokensUsed, readEnvBoolean, readCodexSandboxMode, runLoggedProcess, } from './worker/runtime';
|
|
6
|
+
export type { AgentTokenUsage, LoggedProcessResult, LoggedProcessTranscriptChunk, } from './worker/runtime';
|
|
7
7
|
export declare const WORKER_SESSION_EXPIRED_MESSAGE = "worker session expired: run \"playdrop auth login\" and start the worker again";
|
|
8
8
|
export declare const WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = "worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task upload/done/fail, project validation/build/dev/capture, read-only catalogue/documentation lookup, and PlayDrop AI generation are permitted.";
|
|
9
9
|
type WorkerStartOptions = {
|
|
@@ -33,6 +33,13 @@ type TaskFailOptions = {
|
|
|
33
33
|
env?: string;
|
|
34
34
|
message?: string;
|
|
35
35
|
};
|
|
36
|
+
type TaskSubmitReviewOptions = {
|
|
37
|
+
env?: string;
|
|
38
|
+
state?: string;
|
|
39
|
+
messageFile?: string;
|
|
40
|
+
creatorFeedbackFile?: string;
|
|
41
|
+
evidenceDir?: string;
|
|
42
|
+
};
|
|
36
43
|
export type WorkerHealthAlertInput = {
|
|
37
44
|
state: 'started' | 'stopped' | 'crashed';
|
|
38
45
|
env: string;
|
|
@@ -89,6 +96,7 @@ export declare function resolvePlaydropPluginRoot(input?: {
|
|
|
89
96
|
export declare function stagePlaydropPluginReferences(input: {
|
|
90
97
|
workspaceDir: string;
|
|
91
98
|
pluginRoot: string;
|
|
99
|
+
kind: AgentTaskKind;
|
|
92
100
|
}): Promise<string[]>;
|
|
93
101
|
export declare function discoverWorkerProjectRoot(workspaceDir: string): string;
|
|
94
102
|
export declare function readWorkerTaskState(): WorkerTaskState | null;
|
|
@@ -141,4 +149,10 @@ export declare function reportTask(options: TaskReportOptions): Promise<void>;
|
|
|
141
149
|
export declare function reportCatalogueTask(options: TaskCatalogueReportOptions): Promise<void>;
|
|
142
150
|
export declare function uploadTask(options?: TaskUploadOptions): Promise<void>;
|
|
143
151
|
export declare function completeTask(options: TaskCompleteOptions): Promise<void>;
|
|
152
|
+
export declare function readReviewEvidenceFiles(evidenceDir: string | undefined): Array<{
|
|
153
|
+
name: string;
|
|
154
|
+
contentType: string;
|
|
155
|
+
contentBase64: string;
|
|
156
|
+
}>;
|
|
157
|
+
export declare function submitReviewTask(options: TaskSubmitReviewOptions): Promise<void>;
|
|
144
158
|
export declare function failTask(options: TaskFailOptions): Promise<void>;
|