@riddledc/riddle-proof 0.5.56 → 0.6.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/README.md +26 -0
- package/dist/basic-gameplay.cjs +437 -4
- package/dist/basic-gameplay.d.cts +121 -2
- package/dist/basic-gameplay.d.ts +121 -2
- package/dist/basic-gameplay.js +23 -3
- package/dist/chunk-AAIASO2C.js +699 -0
- package/dist/index.cjs +435 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -2
- package/package.json +1 -1
- package/runtime/lib/verify.py +266 -5
- package/runtime/tests/recon_verify_smoke.py +119 -8
- package/dist/chunk-53DJMNQ6.js +0 -276
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
// src/basic-gameplay.ts
|
|
2
|
+
var RIDDLE_PROOF_BASIC_GAMEPLAY_VERSION = "riddle-proof.basic-gameplay.v1";
|
|
3
|
+
var RIDDLE_PROOF_BASIC_GAMEPLAY_ASSESSMENT_VERSION = "riddle-proof.basic-gameplay.assessment.v1";
|
|
4
|
+
var RIDDLE_PROOF_BASIC_GAMEPLAY_CATCH_VERSION = "riddle-proof.basic-gameplay.catch.v1";
|
|
5
|
+
var BASIC_GAMEPLAY_ACTION_TYPES = [
|
|
6
|
+
"wait",
|
|
7
|
+
"key",
|
|
8
|
+
"key-down",
|
|
9
|
+
"key-up",
|
|
10
|
+
"hold-key",
|
|
11
|
+
"repeat",
|
|
12
|
+
"click",
|
|
13
|
+
"click-by-text",
|
|
14
|
+
"wait-for-text",
|
|
15
|
+
"window-call",
|
|
16
|
+
"evaluate"
|
|
17
|
+
];
|
|
18
|
+
var BASIC_GAMEPLAY_PROGRESS_CHECK_TYPES = [
|
|
19
|
+
"selector_count_increases",
|
|
20
|
+
"selector_count_at_least",
|
|
21
|
+
"selector_absent",
|
|
22
|
+
"selector_text_matches",
|
|
23
|
+
"number_increases",
|
|
24
|
+
"number_decreases",
|
|
25
|
+
"number_unchanged",
|
|
26
|
+
"number_stays_equal",
|
|
27
|
+
"number_equals",
|
|
28
|
+
"canvas_hash_changes",
|
|
29
|
+
"screenshot_hash_changes",
|
|
30
|
+
"visual_hash_changes",
|
|
31
|
+
"state_changes"
|
|
32
|
+
];
|
|
33
|
+
var BASIC_GAMEPLAY_CONTAINER_KEYS = [
|
|
34
|
+
"basic_gameplay",
|
|
35
|
+
"basicGameplay",
|
|
36
|
+
"basic_gameplay_evidence",
|
|
37
|
+
"basicGameplayEvidence",
|
|
38
|
+
"gameplay_proof",
|
|
39
|
+
"gameplayProof"
|
|
40
|
+
];
|
|
41
|
+
var ARTIFACT_VISUAL_CHANGE_CHECKS = /* @__PURE__ */ new Set([
|
|
42
|
+
"canvas_hash_changes",
|
|
43
|
+
"screenshot_hash_changes",
|
|
44
|
+
"state_changes",
|
|
45
|
+
"visual_hash_changes"
|
|
46
|
+
]);
|
|
47
|
+
var PHASE_SCREENSHOT_SUFFIXES = {
|
|
48
|
+
initial: "before",
|
|
49
|
+
after_action: "after",
|
|
50
|
+
after_continue: "after-continue",
|
|
51
|
+
after_restart: "after-restart",
|
|
52
|
+
revisit: "revisit"
|
|
53
|
+
};
|
|
54
|
+
function assessBasicGameplayEvidence(evidence, options = {}) {
|
|
55
|
+
const run = extractBasicGameplayEvidence(evidence);
|
|
56
|
+
if (!run) {
|
|
57
|
+
return {
|
|
58
|
+
version: RIDDLE_PROOF_BASIC_GAMEPLAY_ASSESSMENT_VERSION,
|
|
59
|
+
evidence_present: false,
|
|
60
|
+
passed: false,
|
|
61
|
+
checked_routes: 0,
|
|
62
|
+
passing_routes: 0,
|
|
63
|
+
failing_routes: [],
|
|
64
|
+
failure_counts: {},
|
|
65
|
+
warning_counts: {},
|
|
66
|
+
route_results: []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const routeResults = (run.results || []).map((route) => augmentRouteAssessmentWithProgressionChecks(
|
|
70
|
+
assessBasicGameplayRoute(route, options),
|
|
71
|
+
route
|
|
72
|
+
));
|
|
73
|
+
const failingRoutes = routeResults.filter((result) => !result.ok).map((result) => ({
|
|
74
|
+
name: result.name,
|
|
75
|
+
path: result.path,
|
|
76
|
+
failures: result.failures,
|
|
77
|
+
warnings: result.warnings,
|
|
78
|
+
suite_failures: result.suite_failures
|
|
79
|
+
}));
|
|
80
|
+
return {
|
|
81
|
+
version: RIDDLE_PROOF_BASIC_GAMEPLAY_ASSESSMENT_VERSION,
|
|
82
|
+
evidence_present: true,
|
|
83
|
+
passed: failingRoutes.length === 0,
|
|
84
|
+
checked_routes: routeResults.length,
|
|
85
|
+
passing_routes: routeResults.filter((result) => result.ok).length,
|
|
86
|
+
failing_routes: failingRoutes,
|
|
87
|
+
failure_counts: countCodes(routeResults.flatMap((result) => result.failures)),
|
|
88
|
+
warning_counts: countCodes(routeResults.flatMap((result) => result.warnings)),
|
|
89
|
+
route_results: routeResults
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function assessBasicGameplayRoute(route, options = {}) {
|
|
93
|
+
const maxMobileOverflowPx = options.maxMobileOverflowPx ?? 4;
|
|
94
|
+
const minBodyTextLength = options.minBodyTextLength ?? 20;
|
|
95
|
+
const minVisibleLargeNodes = options.minVisibleLargeNodes ?? 3;
|
|
96
|
+
const minSurfaceLargeNodes = options.minSurfaceLargeNodes ?? 8;
|
|
97
|
+
const warnOnMissingResetPath = options.warnOnMissingResetPath ?? true;
|
|
98
|
+
const warnOnConsoleError = options.warnOnConsoleError ?? true;
|
|
99
|
+
const failOnConsoleError = options.failOnConsoleError ?? false;
|
|
100
|
+
const failures = [];
|
|
101
|
+
const warnings = [];
|
|
102
|
+
const initial = route.initial || {};
|
|
103
|
+
const timed = route.timed || {};
|
|
104
|
+
const afterAction = route.after_action || route.afterAction || {};
|
|
105
|
+
const mobile = route.mobile || {};
|
|
106
|
+
const timedChange = changed(initial, timed);
|
|
107
|
+
const actionChange = changed(timed, afterAction);
|
|
108
|
+
const surfaceVisible = numberValue(initial.visible_canvas_count) > 0 || numberValue(initial.enabled_clickable_count) > 0 || numberValue(initial.visible_large_node_count) >= minSurfaceLargeNodes;
|
|
109
|
+
const actionResults = listValue(route.action_results || route.actionResults);
|
|
110
|
+
const actionAttempted = actionResults.some((result) => result.ok === true && result.action !== "wait");
|
|
111
|
+
const actionFailed = actionResults.some((result) => result.ok === false && result.action !== "wait");
|
|
112
|
+
const stateChangeObserved = actionChange.changed || timedChange.changed;
|
|
113
|
+
const resetPathPresent = numberValue(initial.reset_control_count) > 0 || numberValue(timed.reset_control_count) > 0 || numberValue(afterAction.reset_control_count) > 0;
|
|
114
|
+
const responseStatus = firstNumber(route.http_status, route.response_status, route.status);
|
|
115
|
+
const pageErrorCount = numberValue(route.page_error_count);
|
|
116
|
+
const consoleErrorCount = numberValue(route.console_error_count);
|
|
117
|
+
const mobileOverflowPx = numberValue(mobile.overflow_px);
|
|
118
|
+
if (responseStatus !== null && responseStatus >= 400) failures.push("route_http_error");
|
|
119
|
+
if (pageErrorCount > 0) failures.push("fatal_page_error");
|
|
120
|
+
if (numberValue(initial.body_text_length) < minBodyTextLength && numberValue(initial.visible_large_node_count) < minVisibleLargeNodes) {
|
|
121
|
+
failures.push("route_blank_or_thin");
|
|
122
|
+
}
|
|
123
|
+
if (!surfaceVisible) failures.push("no_game_surface");
|
|
124
|
+
if (mobileOverflowPx > maxMobileOverflowPx) failures.push("mobile_horizontal_overflow");
|
|
125
|
+
if (!actionAttempted && !timedChange.changed) failures.push("primary_control_missing");
|
|
126
|
+
if (actionAttempted && !stateChangeObserved) failures.push("primary_control_inert");
|
|
127
|
+
if (failOnConsoleError && consoleErrorCount > 0) failures.push("fatal_page_error");
|
|
128
|
+
if (numberValue(initial.visible_canvas_count) > 0 && actionAttempted && !timedChange.canvas_changed && !actionChange.canvas_changed && !actionChange.screenshot_changed) {
|
|
129
|
+
warnings.push("canvas_inert");
|
|
130
|
+
}
|
|
131
|
+
if (actionFailed) warnings.push("some_actions_failed");
|
|
132
|
+
if (warnOnMissingResetPath && !resetPathPresent && route.requires_reset !== false && actionAttempted && stateChangeObserved) {
|
|
133
|
+
warnings.push("missing_reset_path");
|
|
134
|
+
}
|
|
135
|
+
if (warnOnConsoleError && consoleErrorCount > 0) warnings.push("critical_console_error");
|
|
136
|
+
return {
|
|
137
|
+
name: route.name,
|
|
138
|
+
path: route.path,
|
|
139
|
+
ok: failures.length === 0,
|
|
140
|
+
failures,
|
|
141
|
+
warnings,
|
|
142
|
+
signals: {
|
|
143
|
+
route_loaded: !failures.includes("route_http_error") && !failures.includes("route_blank_or_thin"),
|
|
144
|
+
surface_visible: surfaceVisible,
|
|
145
|
+
action_attempted: actionAttempted,
|
|
146
|
+
timed_progression_observed: timedChange.changed,
|
|
147
|
+
first_interaction_observed: actionChange.changed,
|
|
148
|
+
state_change_observed: stateChangeObserved,
|
|
149
|
+
mobile_overflow_absent: mobileOverflowPx <= maxMobileOverflowPx,
|
|
150
|
+
reset_path_present: resetPathPresent,
|
|
151
|
+
fatal_errors_absent: pageErrorCount === 0
|
|
152
|
+
},
|
|
153
|
+
diffs: {
|
|
154
|
+
timed: timedChange,
|
|
155
|
+
after_action: actionChange
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function sanitizeBasicGameplayJsonString(value) {
|
|
160
|
+
const text = String(value ?? "");
|
|
161
|
+
let output = "";
|
|
162
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
163
|
+
const code = text.charCodeAt(index);
|
|
164
|
+
if (code >= 55296 && code <= 56319) {
|
|
165
|
+
const next = text.charCodeAt(index + 1);
|
|
166
|
+
if (next >= 56320 && next <= 57343) {
|
|
167
|
+
output += text[index] + text[index + 1];
|
|
168
|
+
index += 1;
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (code >= 56320 && code <= 57343) continue;
|
|
173
|
+
output += text[index];
|
|
174
|
+
}
|
|
175
|
+
return output;
|
|
176
|
+
}
|
|
177
|
+
function compactBasicGameplayText(value, max = 160) {
|
|
178
|
+
const compacted = sanitizeBasicGameplayJsonString(value).replace(/\s+/g, " ").trim();
|
|
179
|
+
return Array.from(compacted).slice(0, max).join("");
|
|
180
|
+
}
|
|
181
|
+
function assessBasicGameplayProgressionCheck(check) {
|
|
182
|
+
const before = metricValue(check.before);
|
|
183
|
+
const after = metricValue(check.after);
|
|
184
|
+
const type = String(check.type || "");
|
|
185
|
+
const hasExplicitResult = typeof check.ok === "boolean";
|
|
186
|
+
let ok = hasExplicitResult ? check.ok === true : true;
|
|
187
|
+
let reason = check.reason ?? null;
|
|
188
|
+
if (type === "selector_count_increases") {
|
|
189
|
+
ok = numberValue(after?.count) > numberValue(before?.count);
|
|
190
|
+
reason = ok ? null : "selector_count_did_not_increase";
|
|
191
|
+
} else if (type === "selector_count_at_least") {
|
|
192
|
+
if (numericValue(check.min) === null && hasExplicitResult) return resolveBasicGameplayProgressionCheckWithArtifactScreenshots({ ...check, ok, reason });
|
|
193
|
+
ok = numberValue(after?.count) >= numberValue(check.min);
|
|
194
|
+
reason = ok ? null : "selector_count_below_min";
|
|
195
|
+
} else if (type === "selector_absent") {
|
|
196
|
+
ok = !after?.present || numberValue(after?.count) === 0;
|
|
197
|
+
reason = ok ? null : "selector_still_present";
|
|
198
|
+
} else if (type === "selector_text_matches") {
|
|
199
|
+
if (!check.pattern && hasExplicitResult) return resolveBasicGameplayProgressionCheckWithArtifactScreenshots({ ...check, ok, reason });
|
|
200
|
+
ok = textMatches(after?.text, check.pattern, check.flags);
|
|
201
|
+
reason = ok ? null : "selector_text_did_not_match";
|
|
202
|
+
} else if (type === "number_increases") {
|
|
203
|
+
ok = numericValue(before?.number) !== null && numericValue(after?.number) !== null && numberValue(after?.number) > numberValue(before?.number);
|
|
204
|
+
reason = ok ? null : "number_did_not_increase";
|
|
205
|
+
} else if (type === "number_decreases") {
|
|
206
|
+
ok = numericValue(before?.number) !== null && numericValue(after?.number) !== null && numberValue(after?.number) < numberValue(before?.number);
|
|
207
|
+
reason = ok ? null : "number_did_not_decrease";
|
|
208
|
+
} else if (type === "number_unchanged" || type === "number_stays_equal") {
|
|
209
|
+
ok = numericValue(before?.number) !== null && numericValue(after?.number) !== null && numberValue(after?.number) === numberValue(before?.number);
|
|
210
|
+
reason = ok ? null : "number_changed";
|
|
211
|
+
} else if (type === "number_equals") {
|
|
212
|
+
const expected = numericValue(check.expected ?? check.value);
|
|
213
|
+
if (expected === null && hasExplicitResult) return resolveBasicGameplayProgressionCheckWithArtifactScreenshots({ ...check, ok, reason });
|
|
214
|
+
ok = numericValue(after?.number) !== null && expected !== null && numberValue(after?.number) === expected;
|
|
215
|
+
reason = ok ? null : "number_did_not_equal_expected";
|
|
216
|
+
} else if (type === "canvas_hash_changes") {
|
|
217
|
+
ok = hashChanged(before, after, ["first_canvas_hash", "first_canvas_visual_sample_hash", "visual_sample_hash"]);
|
|
218
|
+
reason = ok ? null : "canvas_or_visual_sample_hash_did_not_change";
|
|
219
|
+
} else if (type === "screenshot_hash_changes") {
|
|
220
|
+
ok = hashChanged(before, after, ["screenshot_hash", "visual_sample_hash", "artifact_screenshot_hash"]);
|
|
221
|
+
reason = ok ? null : "screenshot_hash_did_not_change";
|
|
222
|
+
} else if (type === "visual_hash_changes") {
|
|
223
|
+
ok = hashChanged(before, after, ["visual_sample_hash", "screenshot_hash", "artifact_screenshot_hash"]);
|
|
224
|
+
reason = ok ? null : "visual_hash_did_not_change";
|
|
225
|
+
} else if (type === "state_changes") {
|
|
226
|
+
ok = hashChanged(before, after, [
|
|
227
|
+
"dom_signature_hash",
|
|
228
|
+
"body_text_hash",
|
|
229
|
+
"screenshot_hash",
|
|
230
|
+
"visual_sample_hash",
|
|
231
|
+
"artifact_screenshot_hash",
|
|
232
|
+
"first_canvas_hash",
|
|
233
|
+
"first_canvas_visual_sample_hash"
|
|
234
|
+
]);
|
|
235
|
+
reason = ok ? null : "state_hash_did_not_change";
|
|
236
|
+
}
|
|
237
|
+
const assessed = {
|
|
238
|
+
...check,
|
|
239
|
+
ok,
|
|
240
|
+
reason
|
|
241
|
+
};
|
|
242
|
+
return resolveBasicGameplayProgressionCheckWithArtifactScreenshots(assessed);
|
|
243
|
+
}
|
|
244
|
+
function assessBasicGameplayProgressionChecks(route) {
|
|
245
|
+
return progressionChecksForRoute(route).map((check) => assessBasicGameplayProgressionCheck(check));
|
|
246
|
+
}
|
|
247
|
+
function augmentBasicGameplayAssessmentWithProgressionChecks(assessment, evidence) {
|
|
248
|
+
const run = extractBasicGameplayEvidence(evidence);
|
|
249
|
+
if (!run?.results?.length) return assessment;
|
|
250
|
+
const routeResults = assessment.route_results.map(
|
|
251
|
+
(result, index) => augmentRouteAssessmentWithProgressionChecks(result, run.results?.[index] || {})
|
|
252
|
+
);
|
|
253
|
+
const failingRoutes = routeResults.filter((result) => !result.ok).map((result) => ({
|
|
254
|
+
name: result.name,
|
|
255
|
+
path: result.path,
|
|
256
|
+
failures: result.failures,
|
|
257
|
+
warnings: result.warnings,
|
|
258
|
+
suite_failures: result.suite_failures
|
|
259
|
+
}));
|
|
260
|
+
return {
|
|
261
|
+
...assessment,
|
|
262
|
+
passed: failingRoutes.length === 0,
|
|
263
|
+
passing_routes: routeResults.filter((result) => result.ok).length,
|
|
264
|
+
failing_routes: failingRoutes,
|
|
265
|
+
failure_counts: countCodes(routeResults.flatMap((result) => result.failures)),
|
|
266
|
+
warning_counts: countCodes(routeResults.flatMap((result) => result.warnings)),
|
|
267
|
+
route_results: routeResults
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function resolveBasicGameplayProgressionCheckWithArtifactScreenshots(check) {
|
|
271
|
+
if (check.ok !== false) return check;
|
|
272
|
+
if (!ARTIFACT_VISUAL_CHANGE_CHECKS.has(String(check.type || ""))) return check;
|
|
273
|
+
const beforeHash = stringValue(metricValue(check.before)?.artifact_screenshot_hash);
|
|
274
|
+
const afterHash = stringValue(metricValue(check.after)?.artifact_screenshot_hash);
|
|
275
|
+
if (!beforeHash || !afterHash || beforeHash === afterHash) return check;
|
|
276
|
+
return {
|
|
277
|
+
...check,
|
|
278
|
+
ok: true,
|
|
279
|
+
reason: null,
|
|
280
|
+
artifact_resolution: {
|
|
281
|
+
source: "riddle_screenshot_artifacts",
|
|
282
|
+
before_hash: beforeHash,
|
|
283
|
+
after_hash: afterHash
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function attachBasicGameplayArtifactScreenshotHashes(evidence, routesOrOptions = {}, maybeArtifacts = []) {
|
|
288
|
+
const routes = Array.isArray(routesOrOptions) ? routesOrOptions : routesOrOptions.routes || [];
|
|
289
|
+
const artifacts = Array.isArray(routesOrOptions) ? maybeArtifacts : routesOrOptions.artifacts || [];
|
|
290
|
+
if (!evidence?.results?.length || !artifacts.length) return evidence;
|
|
291
|
+
const artifactsByName = screenshotArtifactIndex(artifacts);
|
|
292
|
+
if (!artifactsByName.size) return evidence;
|
|
293
|
+
for (const [index, routeEvidence] of evidence.results.entries()) {
|
|
294
|
+
const routeContract = routes[index] || {};
|
|
295
|
+
for (const phase of Object.keys(PHASE_SCREENSHOT_SUFFIXES)) {
|
|
296
|
+
attachPhaseArtifactHash(routeEvidence, routeContract, phase, artifactsByName);
|
|
297
|
+
}
|
|
298
|
+
const checks = progressionChecksForRoute(routeEvidence);
|
|
299
|
+
const resolvedChecks = checks.map((check) => {
|
|
300
|
+
const fromPhase = check.from_phase || "initial";
|
|
301
|
+
const toPhase = check.to_phase || "after_action";
|
|
302
|
+
if (check.before) attachPhaseArtifactHash({ ...routeEvidence, [fromPhase]: check.before }, routeContract, fromPhase, artifactsByName);
|
|
303
|
+
if (check.after) attachPhaseArtifactHash({ ...routeEvidence, [toPhase]: check.after }, routeContract, toPhase, artifactsByName);
|
|
304
|
+
return resolveBasicGameplayProgressionCheckWithArtifactScreenshots(check);
|
|
305
|
+
});
|
|
306
|
+
if (routeEvidence.progression_checks) routeEvidence.progression_checks = resolvedChecks;
|
|
307
|
+
else if (routeEvidence.progressionChecks) routeEvidence.progressionChecks = resolvedChecks;
|
|
308
|
+
routeEvidence.progression_failure_count = resolvedChecks.filter((check) => check.ok === false).length;
|
|
309
|
+
}
|
|
310
|
+
return evidence;
|
|
311
|
+
}
|
|
312
|
+
function createBasicGameplayCatchRecords(assessment, evidence) {
|
|
313
|
+
const run = extractBasicGameplayEvidence(evidence);
|
|
314
|
+
if (!run?.results?.length) return [];
|
|
315
|
+
const catches = [];
|
|
316
|
+
for (const route of run.results) {
|
|
317
|
+
if (numberValue(route.page_error_count) > 0) {
|
|
318
|
+
catches.push({
|
|
319
|
+
site: run.site || null,
|
|
320
|
+
route: route.name,
|
|
321
|
+
path: route.path,
|
|
322
|
+
code: "fatal_page_error",
|
|
323
|
+
label: "page runtime error",
|
|
324
|
+
type: "page_error",
|
|
325
|
+
selector: null,
|
|
326
|
+
reason: stringValue(route.first_page_error) || "page_error",
|
|
327
|
+
page_errors: listValue(route.page_errors),
|
|
328
|
+
summary: `${route.name || route.path || "Route"}: page runtime error (${stringValue(route.first_page_error) || "page_error"})`
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (numberValue(route.console_error_count) > 0) {
|
|
332
|
+
catches.push({
|
|
333
|
+
site: run.site || null,
|
|
334
|
+
route: route.name,
|
|
335
|
+
path: route.path,
|
|
336
|
+
code: "critical_console_error",
|
|
337
|
+
label: "console error",
|
|
338
|
+
type: "console_error",
|
|
339
|
+
selector: null,
|
|
340
|
+
reason: stringValue(route.first_console_error) || "console_error",
|
|
341
|
+
console_errors: listValue(route.console_errors),
|
|
342
|
+
summary: `${route.name || route.path || "Route"}: console error (${stringValue(route.first_console_error) || "console_error"})`
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
for (const [group, phase] of [
|
|
346
|
+
["action_results", "after_action"],
|
|
347
|
+
["continued_action_results", "after_continue"],
|
|
348
|
+
["restart_action_results", "after_restart"]
|
|
349
|
+
]) {
|
|
350
|
+
for (const actionResult of listValue(route[group])) {
|
|
351
|
+
if (!actionResult || actionResult.ok !== false) continue;
|
|
352
|
+
const action = stringValue(actionResult.action) || group;
|
|
353
|
+
const reason = stringValue(actionResult.reason) || "action_failed";
|
|
354
|
+
catches.push({
|
|
355
|
+
site: run.site || null,
|
|
356
|
+
route: route.name,
|
|
357
|
+
path: route.path,
|
|
358
|
+
code: "action_failed",
|
|
359
|
+
label: action,
|
|
360
|
+
type: action,
|
|
361
|
+
selector: stringValue(actionResult.selector),
|
|
362
|
+
reason,
|
|
363
|
+
phase,
|
|
364
|
+
action_result: actionResult,
|
|
365
|
+
summary: `${route.name || route.path || "Route"}: ${action} failed (${reason})`
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const check of assessBasicGameplayProgressionChecks(route).filter((item) => item.ok === false)) {
|
|
370
|
+
catches.push({
|
|
371
|
+
site: run.site || null,
|
|
372
|
+
route: route.name,
|
|
373
|
+
path: route.path,
|
|
374
|
+
code: "progression_assertion_failed",
|
|
375
|
+
label: check.label,
|
|
376
|
+
type: String(check.type || ""),
|
|
377
|
+
selector: check.selector || null,
|
|
378
|
+
state_path: check.state_path || null,
|
|
379
|
+
state_call: check.state_call || null,
|
|
380
|
+
property_path: check.property_path || null,
|
|
381
|
+
reason: check.reason || "progression_assertion_failed",
|
|
382
|
+
from_phase: check.from_phase,
|
|
383
|
+
to_phase: check.to_phase,
|
|
384
|
+
before: compactCatchMetric(check.before),
|
|
385
|
+
after: compactCatchMetric(check.after),
|
|
386
|
+
summary: `${route.name || route.path || "Route"}: ${check.label || check.type || "progression check"} failed (${check.reason || "progression_assertion_failed"})`
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!catches.length && assessment.failing_routes.length) {
|
|
391
|
+
for (const route of assessment.failing_routes) {
|
|
392
|
+
for (const code of route.failures || []) {
|
|
393
|
+
catches.push({
|
|
394
|
+
site: run.site || null,
|
|
395
|
+
route: route.name,
|
|
396
|
+
path: route.path,
|
|
397
|
+
code,
|
|
398
|
+
label: code,
|
|
399
|
+
type: code,
|
|
400
|
+
reason: code,
|
|
401
|
+
summary: `${route.name || route.path || "Route"}: ${code}`
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return catches;
|
|
407
|
+
}
|
|
408
|
+
function createBasicGameplayCatchSummary(input, options = {}) {
|
|
409
|
+
const before = summarizeAssessment(assessBasicGameplayEvidence(input.before, options));
|
|
410
|
+
const after = input.after === void 0 ? void 0 : summarizeAssessment(assessBasicGameplayEvidence(input.after, options));
|
|
411
|
+
const fixed = Boolean(after && before.notable_codes.length > 0 && after.passed && after.notable_codes.length === 0);
|
|
412
|
+
const title = input.title || [
|
|
413
|
+
input.site || "Basic gameplay",
|
|
414
|
+
input.route ? `${input.route} proof catch` : "proof catch"
|
|
415
|
+
].join(" ");
|
|
416
|
+
const beforeCodes = before.notable_codes.length ? before.notable_codes.join(", ") : "no failing or warning codes";
|
|
417
|
+
const afterCodes = after ? after.notable_codes.length ? after.notable_codes.join(", ") : "no failing or warning codes" : "not verified";
|
|
418
|
+
const summaryLines = [
|
|
419
|
+
`Before: ${before.checked_routes} checked, ${before.passing_routes} passing, codes: ${beforeCodes}.`,
|
|
420
|
+
after ? `After: ${after.checked_routes} checked, ${after.passing_routes} passing, codes: ${afterCodes}.` : "After: not provided."
|
|
421
|
+
];
|
|
422
|
+
if (input.fix?.summary) summaryLines.push(`Fix: ${input.fix.summary}`);
|
|
423
|
+
if (input.artifacts?.length) summaryLines.push(`Artifacts: ${input.artifacts.length} attached.`);
|
|
424
|
+
return {
|
|
425
|
+
version: RIDDLE_PROOF_BASIC_GAMEPLAY_CATCH_VERSION,
|
|
426
|
+
title,
|
|
427
|
+
site: input.site,
|
|
428
|
+
route: input.route,
|
|
429
|
+
detected_at: input.detected_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
430
|
+
before,
|
|
431
|
+
after,
|
|
432
|
+
fixed,
|
|
433
|
+
fix: input.fix,
|
|
434
|
+
artifacts: input.artifacts || [],
|
|
435
|
+
summary_lines: summaryLines,
|
|
436
|
+
marketing_summary: fixed ? `${title}: Riddle Proof caught ${beforeCodes}; after the fix, ${afterCodes}.` : `${title}: Riddle Proof caught ${beforeCodes}; after evidence is ${after ? "not yet clean" : "not yet attached"}.`
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function extractBasicGameplayEvidence(...sources) {
|
|
440
|
+
const seen = /* @__PURE__ */ new Set();
|
|
441
|
+
for (const source of sources) {
|
|
442
|
+
const found = findBasicGameplayEvidence(source, seen);
|
|
443
|
+
if (found) return found;
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
function summarizeAssessment(assessment) {
|
|
448
|
+
return {
|
|
449
|
+
evidence_present: assessment.evidence_present,
|
|
450
|
+
passed: assessment.passed,
|
|
451
|
+
checked_routes: assessment.checked_routes,
|
|
452
|
+
passing_routes: assessment.passing_routes,
|
|
453
|
+
failing_routes: assessment.failing_routes,
|
|
454
|
+
failure_counts: assessment.failure_counts,
|
|
455
|
+
warning_counts: assessment.warning_counts,
|
|
456
|
+
notable_codes: [
|
|
457
|
+
...Object.keys(assessment.failure_counts),
|
|
458
|
+
...Object.keys(assessment.warning_counts)
|
|
459
|
+
].sort()
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function findBasicGameplayEvidence(value, seen, depth = 0) {
|
|
463
|
+
if (depth > 6 || value === null || value === void 0) return null;
|
|
464
|
+
if (typeof value === "string") {
|
|
465
|
+
const parsed = parseJson(value);
|
|
466
|
+
return parsed === null ? null : findBasicGameplayEvidence(parsed, seen, depth + 1);
|
|
467
|
+
}
|
|
468
|
+
if (Array.isArray(value)) {
|
|
469
|
+
if (seen.has(value)) return null;
|
|
470
|
+
seen.add(value);
|
|
471
|
+
if (value.some((item) => hasRouteShape(recordValue(item)))) {
|
|
472
|
+
return { results: value.filter((item) => hasRouteShape(recordValue(item))) };
|
|
473
|
+
}
|
|
474
|
+
for (const item of value) {
|
|
475
|
+
const found = findBasicGameplayEvidence(item, seen, depth + 1);
|
|
476
|
+
if (found) return found;
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
const record = recordValue(value);
|
|
481
|
+
if (!record || seen.has(record)) return null;
|
|
482
|
+
seen.add(record);
|
|
483
|
+
if (record.version === RIDDLE_PROOF_BASIC_GAMEPLAY_VERSION || Array.isArray(record.results)) {
|
|
484
|
+
return {
|
|
485
|
+
...record,
|
|
486
|
+
results: listValue(record.results).filter((item) => Boolean(recordValue(item)))
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (hasRouteShape(record)) {
|
|
490
|
+
return { results: [record] };
|
|
491
|
+
}
|
|
492
|
+
for (const key of BASIC_GAMEPLAY_CONTAINER_KEYS) {
|
|
493
|
+
if (Object.prototype.hasOwnProperty.call(record, key)) {
|
|
494
|
+
const nested = findBasicGameplayEvidence(record[key], seen, depth + 1);
|
|
495
|
+
if (nested) return nested;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (const item of Object.values(record)) {
|
|
499
|
+
const found = findBasicGameplayEvidence(item, seen, depth + 1);
|
|
500
|
+
if (found) return found;
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
function hasRouteShape(record) {
|
|
505
|
+
return Boolean(record && (record.initial || record.after_action || record.afterAction || record.action_results || record.actionResults) && (record.path || record.name));
|
|
506
|
+
}
|
|
507
|
+
function changed(before, after) {
|
|
508
|
+
const bodyTextChanged = Boolean(before.body_text_hash && after.body_text_hash && before.body_text_hash !== after.body_text_hash);
|
|
509
|
+
const screenshotChanged = Boolean(before.screenshot_hash && after.screenshot_hash && before.screenshot_hash !== after.screenshot_hash);
|
|
510
|
+
const beforeCanvasHashes = canvasHashes(before.canvases);
|
|
511
|
+
const afterCanvasHashes = canvasHashes(after.canvases);
|
|
512
|
+
const canvasChanged = Boolean(beforeCanvasHashes && afterCanvasHashes && beforeCanvasHashes !== afterCanvasHashes);
|
|
513
|
+
return {
|
|
514
|
+
body_text_changed: bodyTextChanged,
|
|
515
|
+
screenshot_changed: screenshotChanged,
|
|
516
|
+
canvas_changed: canvasChanged,
|
|
517
|
+
changed: bodyTextChanged || screenshotChanged || canvasChanged
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function canvasHashes(canvases) {
|
|
521
|
+
return (canvases || []).map((canvas) => canvas.hash).filter(Boolean).join("|");
|
|
522
|
+
}
|
|
523
|
+
function augmentRouteAssessmentWithProgressionChecks(result, route) {
|
|
524
|
+
const progressionFailures = assessBasicGameplayProgressionChecks(route).filter((check) => check.ok === false);
|
|
525
|
+
if (!progressionFailures.length) return result;
|
|
526
|
+
const failures = result.failures.includes("progression_assertion_failed") ? result.failures : [...result.failures, "progression_assertion_failed"];
|
|
527
|
+
return {
|
|
528
|
+
...result,
|
|
529
|
+
ok: false,
|
|
530
|
+
failures,
|
|
531
|
+
suite_failures: [
|
|
532
|
+
...result.suite_failures || [],
|
|
533
|
+
...progressionFailures.map((check) => ({
|
|
534
|
+
code: "progression_assertion_failed",
|
|
535
|
+
label: check.label,
|
|
536
|
+
reason: check.reason,
|
|
537
|
+
selector: check.selector || null,
|
|
538
|
+
state_path: check.state_path || null,
|
|
539
|
+
state_call: check.state_call || null,
|
|
540
|
+
property_path: check.property_path || null
|
|
541
|
+
}))
|
|
542
|
+
]
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function progressionChecksForRoute(route) {
|
|
546
|
+
return listValue(route.progression_checks || route.progressionChecks).filter((item) => Boolean(recordValue(item)));
|
|
547
|
+
}
|
|
548
|
+
function metricValue(value) {
|
|
549
|
+
return recordValue(value);
|
|
550
|
+
}
|
|
551
|
+
function textMatches(text, pattern, flags) {
|
|
552
|
+
if (!pattern) return Boolean(text);
|
|
553
|
+
try {
|
|
554
|
+
return new RegExp(String(pattern), typeof flags === "string" ? flags : "i").test(String(text ?? ""));
|
|
555
|
+
} catch {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function hashChanged(before, after, keys) {
|
|
560
|
+
if (!before || !after) return false;
|
|
561
|
+
return keys.some((key) => {
|
|
562
|
+
const beforeValue = stringValue(before[key]);
|
|
563
|
+
const afterValue = stringValue(after[key]);
|
|
564
|
+
return Boolean(beforeValue && afterValue && beforeValue !== afterValue);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function screenshotArtifactIndex(artifacts) {
|
|
568
|
+
const index = /* @__PURE__ */ new Map();
|
|
569
|
+
for (const artifact of artifacts || []) {
|
|
570
|
+
const hash = stringValue(artifact.sha256 || artifact.hash);
|
|
571
|
+
if (!hash) continue;
|
|
572
|
+
if (String(artifact.kind || "").toLowerCase() !== "screenshot" && !/\.png($|\?)/i.test(`${artifact.name || ""} ${artifact.path || ""} ${artifact.url || ""}`)) continue;
|
|
573
|
+
const filename = artifactBasename(artifact.path || artifact.name || artifact.url);
|
|
574
|
+
if (!filename) continue;
|
|
575
|
+
index.set(filename.toLowerCase(), {
|
|
576
|
+
...artifact,
|
|
577
|
+
sha256: hash
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return index;
|
|
581
|
+
}
|
|
582
|
+
function attachPhaseArtifactHash(routeEvidence, routeContract, phase, artifactsByName) {
|
|
583
|
+
const suffix = PHASE_SCREENSHOT_SUFFIXES[phase];
|
|
584
|
+
if (!suffix) return null;
|
|
585
|
+
const metric = metricValue(routeEvidence[phase]);
|
|
586
|
+
if (!metric) return null;
|
|
587
|
+
const filename = `${routeArtifactSlug(routeEvidence, routeContract)}-${suffix}.png`.toLowerCase();
|
|
588
|
+
const artifact = artifactsByName.get(filename);
|
|
589
|
+
if (!artifact?.sha256) return null;
|
|
590
|
+
metric.artifact_screenshot_hash = artifact.sha256;
|
|
591
|
+
metric.artifact_screenshot = {
|
|
592
|
+
name: artifact.name || artifactBasename(artifact.path || filename),
|
|
593
|
+
path: artifact.path,
|
|
594
|
+
url: artifact.url,
|
|
595
|
+
kind: artifact.kind,
|
|
596
|
+
sha256: artifact.sha256
|
|
597
|
+
};
|
|
598
|
+
return artifact.sha256;
|
|
599
|
+
}
|
|
600
|
+
function routeArtifactSlug(routeEvidence, routeContract) {
|
|
601
|
+
return slug(routeContract.name || routeEvidence.name || routeContract.path || routeEvidence.path || "route");
|
|
602
|
+
}
|
|
603
|
+
function artifactBasename(value) {
|
|
604
|
+
const raw = stringValue(value);
|
|
605
|
+
if (!raw) return "";
|
|
606
|
+
try {
|
|
607
|
+
const url = new URL(raw);
|
|
608
|
+
return url.pathname.split("/").filter(Boolean).pop() || "";
|
|
609
|
+
} catch {
|
|
610
|
+
return raw.split(/[\\/]/).filter(Boolean).pop() || raw;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function compactCatchMetric(metric) {
|
|
614
|
+
const value = metricValue(metric);
|
|
615
|
+
if (!value) return null;
|
|
616
|
+
return {
|
|
617
|
+
phase: value.phase,
|
|
618
|
+
text: value.text,
|
|
619
|
+
number: value.number,
|
|
620
|
+
count: value.count,
|
|
621
|
+
present: value.present,
|
|
622
|
+
state_path: value.state_path,
|
|
623
|
+
state_call: value.state_call,
|
|
624
|
+
property_path: value.property_path,
|
|
625
|
+
screenshot_hash: value.screenshot_hash,
|
|
626
|
+
visual_sample_hash: value.visual_sample_hash,
|
|
627
|
+
viewport_screenshot_hash: value.viewport_screenshot_hash,
|
|
628
|
+
artifact_screenshot_hash: value.artifact_screenshot_hash,
|
|
629
|
+
dom_signature_hash: value.dom_signature_hash,
|
|
630
|
+
body_text_hash: value.body_text_hash,
|
|
631
|
+
first_canvas_hash: value.first_canvas_hash,
|
|
632
|
+
first_canvas_visual_sample_hash: value.first_canvas_visual_sample_hash,
|
|
633
|
+
visible_canvas_count: value.visible_canvas_count
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function slug(value) {
|
|
637
|
+
return String(value || "artifact").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96) || "artifact";
|
|
638
|
+
}
|
|
639
|
+
function countCodes(codes) {
|
|
640
|
+
const counts = {};
|
|
641
|
+
for (const code of codes) counts[code] = (counts[code] || 0) + 1;
|
|
642
|
+
return counts;
|
|
643
|
+
}
|
|
644
|
+
function firstNumber(...values) {
|
|
645
|
+
for (const value of values) {
|
|
646
|
+
const number = numericValue(value);
|
|
647
|
+
if (number !== null) return number;
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
function numberValue(value) {
|
|
652
|
+
return numericValue(value) ?? 0;
|
|
653
|
+
}
|
|
654
|
+
function numericValue(value) {
|
|
655
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
656
|
+
if (typeof value === "string" && value.trim()) {
|
|
657
|
+
const parsed = Number(value);
|
|
658
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
659
|
+
}
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
function stringValue(value) {
|
|
663
|
+
return typeof value === "string" && value.length ? value : null;
|
|
664
|
+
}
|
|
665
|
+
function recordValue(value) {
|
|
666
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
667
|
+
}
|
|
668
|
+
function listValue(value) {
|
|
669
|
+
return Array.isArray(value) ? value : [];
|
|
670
|
+
}
|
|
671
|
+
function parseJson(value) {
|
|
672
|
+
const trimmed = value.trim();
|
|
673
|
+
if (!trimmed || !/^[{[]/.test(trimmed)) return null;
|
|
674
|
+
try {
|
|
675
|
+
return JSON.parse(trimmed);
|
|
676
|
+
} catch {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export {
|
|
682
|
+
RIDDLE_PROOF_BASIC_GAMEPLAY_VERSION,
|
|
683
|
+
RIDDLE_PROOF_BASIC_GAMEPLAY_ASSESSMENT_VERSION,
|
|
684
|
+
RIDDLE_PROOF_BASIC_GAMEPLAY_CATCH_VERSION,
|
|
685
|
+
BASIC_GAMEPLAY_ACTION_TYPES,
|
|
686
|
+
BASIC_GAMEPLAY_PROGRESS_CHECK_TYPES,
|
|
687
|
+
assessBasicGameplayEvidence,
|
|
688
|
+
assessBasicGameplayRoute,
|
|
689
|
+
sanitizeBasicGameplayJsonString,
|
|
690
|
+
compactBasicGameplayText,
|
|
691
|
+
assessBasicGameplayProgressionCheck,
|
|
692
|
+
assessBasicGameplayProgressionChecks,
|
|
693
|
+
augmentBasicGameplayAssessmentWithProgressionChecks,
|
|
694
|
+
resolveBasicGameplayProgressionCheckWithArtifactScreenshots,
|
|
695
|
+
attachBasicGameplayArtifactScreenshotHashes,
|
|
696
|
+
createBasicGameplayCatchRecords,
|
|
697
|
+
createBasicGameplayCatchSummary,
|
|
698
|
+
extractBasicGameplayEvidence
|
|
699
|
+
};
|