@riddledc/riddle-proof 0.7.201 → 0.7.203
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 +42 -0
- package/dist/cli.cjs +345 -15
- package/dist/cli.js +346 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,6 +202,48 @@ generic inline-script warning threshold. Use `--strict=true` when you
|
|
|
202
202
|
deliberately want Riddle's non-critical script-safety warnings to block the run.
|
|
203
203
|
Critical script-safety violations remain blocked by Riddle either way.
|
|
204
204
|
|
|
205
|
+
Use `--viewport-name <name>` to run only one named viewport from a
|
|
206
|
+
multi-viewport profile while preserving viewport-scoped setup actions and
|
|
207
|
+
checks:
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
riddle-proof-loop run-profile \
|
|
211
|
+
--profile .riddle-proof/profiles/pricing.json \
|
|
212
|
+
--url https://example.com \
|
|
213
|
+
--viewport-name ipad-mini \
|
|
214
|
+
--output artifacts/riddle-proof/pricing-ipad-mini
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
When `--output` / `--output-dir` is set, hosted profile runs write
|
|
218
|
+
`riddle-job.json` as soon as Riddle returns a job id. If the local process is
|
|
219
|
+
interrupted or pruned, recover the profile artifacts from the hosted job:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
riddle-proof-loop run-profile recover \
|
|
223
|
+
--profile .riddle-proof/profiles/pricing.json \
|
|
224
|
+
--url https://example.com \
|
|
225
|
+
--job job_abc123 \
|
|
226
|
+
--viewport-name ipad-mini \
|
|
227
|
+
--output artifacts/riddle-proof/pricing-ipad-mini-recovered
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
When a matrix run is executed as separate named-viewport jobs, aggregate the
|
|
231
|
+
saved child outputs back into one profile judgment:
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
riddle-proof-loop run-profile aggregate \
|
|
235
|
+
--profile .riddle-proof/profiles/pricing.json \
|
|
236
|
+
--url https://example.com \
|
|
237
|
+
--input-dir artifacts/riddle-proof/pricing-matrix \
|
|
238
|
+
--output artifacts/riddle-proof/pricing-matrix
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`--input-dir` reads child `*/profile-result.json` files such as
|
|
242
|
+
`desktop/profile-result.json` and `ipad-mini/profile-result.json`, then writes
|
|
243
|
+
the combined `profile-result.json` and `summary.md`. Use `--inputs` with a
|
|
244
|
+
comma-separated list of files or directories when the child outputs do not
|
|
245
|
+
share one parent directory.
|
|
246
|
+
|
|
205
247
|
When promoting proof artifacts into a durable public profile, avoid guessing
|
|
206
248
|
which backend or runner tokens are preserved inside `proof.json`. Derive the
|
|
207
249
|
`body_contains` fragments from the artifact body first:
|
package/dist/cli.cjs
CHANGED
|
@@ -16223,7 +16223,9 @@ function usage() {
|
|
|
16223
16223
|
" riddle-proof-loop respond --state-path <path> --response-json <file|json|->",
|
|
16224
16224
|
" riddle-proof-loop respond --state-path <path> --decision <decision> --summary <text> [--payload-json <file|json|->]",
|
|
16225
16225
|
" riddle-proof-loop status --state-path <path>",
|
|
16226
|
-
" riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--runner riddle] [--strict true|false; default false] [--split-viewports true|false; default false] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
|
|
16226
|
+
" riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--runner riddle] [--viewport-name <name[,name...]>] [--strict true|false; default false] [--split-viewports true|false; default false] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
|
|
16227
|
+
" riddle-proof-loop run-profile aggregate --profile <file|json|-> --url <base-url> --input-dir <dir>|--inputs <path[,path...]> [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
|
|
16228
|
+
" riddle-proof-loop run-profile recover --profile <file|json|-> --url <base-url> --job <job-id> [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
|
|
16227
16229
|
" riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
|
|
16228
16230
|
" riddle-proof-loop profile-http-status-preflight --profile <file|json|-> --url <base-url> [--format json|summary]",
|
|
16229
16231
|
" riddle-proof-loop riddle-preview-deploy <build-dir> <label> [--framework spa|static]",
|
|
@@ -16283,6 +16285,11 @@ function runProfileStrictOption(options) {
|
|
|
16283
16285
|
function runProfileSplitViewportsOption(options) {
|
|
16284
16286
|
return optionBoolean(options, "splitViewports") ?? false;
|
|
16285
16287
|
}
|
|
16288
|
+
function runProfileViewportNamesOption(options) {
|
|
16289
|
+
const raw = optionString(options, "viewportName") ?? optionString(options, "viewportNames");
|
|
16290
|
+
if (!raw) return [];
|
|
16291
|
+
return raw.split(",").map((part) => part.trim()).filter(Boolean);
|
|
16292
|
+
}
|
|
16286
16293
|
var DEFAULT_PROFILE_UNSUBMITTED_RETRY_TIMEOUT_MS = 9e4;
|
|
16287
16294
|
var DEFAULT_PROFILE_UNSUBMITTED_RETRIES = 2;
|
|
16288
16295
|
function optionNumber(options, ...keys) {
|
|
@@ -16883,6 +16890,61 @@ function profileHasRouteExitAffordanceReceipt(receipts) {
|
|
|
16883
16890
|
return routeFields.some((name) => setupReturnSummaryValue(receipt, [name]) !== void 0) || haystack.includes("route=") || haystack.includes("browserpath=");
|
|
16884
16891
|
});
|
|
16885
16892
|
}
|
|
16893
|
+
function profileCleanupLabelMatches(value) {
|
|
16894
|
+
if (!value) return false;
|
|
16895
|
+
return /\b(cleanup|clean|clear|reset|undo|discard|new)\b/i.test(value);
|
|
16896
|
+
}
|
|
16897
|
+
function profileHasCleanupBoundaryAffordanceReceipt(receipts) {
|
|
16898
|
+
const visibleFields = [
|
|
16899
|
+
"cleanupControlVisible",
|
|
16900
|
+
"cleanupVisible",
|
|
16901
|
+
"clearControlVisible",
|
|
16902
|
+
"resetControlVisible",
|
|
16903
|
+
"undoVisible",
|
|
16904
|
+
"discardVisible",
|
|
16905
|
+
"newControlVisible",
|
|
16906
|
+
"exitControlVisible"
|
|
16907
|
+
];
|
|
16908
|
+
const textFields = [
|
|
16909
|
+
"cleanupControlText",
|
|
16910
|
+
"cleanupText",
|
|
16911
|
+
"clearControlText",
|
|
16912
|
+
"resetControlText",
|
|
16913
|
+
"undoText",
|
|
16914
|
+
"discardText",
|
|
16915
|
+
"newControlText",
|
|
16916
|
+
"exitControlText",
|
|
16917
|
+
"controlText",
|
|
16918
|
+
"affordanceText"
|
|
16919
|
+
];
|
|
16920
|
+
return receipts.some((receipt) => {
|
|
16921
|
+
const storedTo = cliString(receipt.return_stored_to) || "";
|
|
16922
|
+
const label = cliString(receipt.label) || "";
|
|
16923
|
+
const path7 = cliString(receipt.path) || cliString(receipt.function_name) || "";
|
|
16924
|
+
const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
|
|
16925
|
+
const haystack = `${storedTo} ${label} ${path7} ${summary}`.toLowerCase();
|
|
16926
|
+
const mentionsCleanupBoundary = haystack.includes("cleanup") || haystack.includes("precleanup") || haystack.includes("pre-cleanup") || haystack.includes("boundary") || haystack.includes("undo") || haystack.includes("clear") || haystack.includes("reset") || haystack.includes("discard");
|
|
16927
|
+
const visibleControl = visibleFields.some((name) => setupReturnSummaryValue(receipt, [name]) === true);
|
|
16928
|
+
const controlText = textFields.map((name) => cliString(setupReturnSummaryValue(receipt, [name]))).find((value) => profileCleanupLabelMatches(value));
|
|
16929
|
+
return mentionsCleanupBoundary && (visibleControl || Boolean(controlText));
|
|
16930
|
+
});
|
|
16931
|
+
}
|
|
16932
|
+
function profileVisibleCleanupActionCount(setupViewports) {
|
|
16933
|
+
const keys = /* @__PURE__ */ new Set();
|
|
16934
|
+
const clickedReceipts = setupViewports.flatMap((viewport) => [
|
|
16935
|
+
...setupReceiptArray(viewport, "clicked"),
|
|
16936
|
+
...setupReceiptArray(viewport, "tap"),
|
|
16937
|
+
...setupReceiptArray(viewport, "tap_until")
|
|
16938
|
+
]);
|
|
16939
|
+
clickedReceipts.forEach((receipt, index) => {
|
|
16940
|
+
if (receipt.ok === false) return;
|
|
16941
|
+
const text = cliString(receipt.text) || cliString(receipt.label) || cliString(receipt.target) || cliString(receipt.selector);
|
|
16942
|
+
if (!profileCleanupLabelMatches(text)) return;
|
|
16943
|
+
const ordinal = cliFiniteNumber(receipt.ordinal);
|
|
16944
|
+
keys.add(ordinal === void 0 ? `idx:${index}:${text}` : `ord:${ordinal}:${text}`);
|
|
16945
|
+
});
|
|
16946
|
+
return keys.size;
|
|
16947
|
+
}
|
|
16886
16948
|
function profileHasOfflineAudioMetricsReceipt(receipts) {
|
|
16887
16949
|
const metricFields = [
|
|
16888
16950
|
"mixPeak",
|
|
@@ -16984,17 +17046,51 @@ function profileHasRecoveredStateReceipt(receipts) {
|
|
|
16984
17046
|
const path7 = cliString(receipt.path) || cliString(receipt.function_name) || "";
|
|
16985
17047
|
const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
|
|
16986
17048
|
const haystack = `${storedTo} ${label} ${path7} ${summary}`.toLowerCase();
|
|
16987
|
-
const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || haystack.includes("try fix") || haystack.includes("tryfix") || haystack.includes("after-fix") || haystack.includes("fixed");
|
|
17049
|
+
const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || haystack.includes("retry") || haystack.includes("restart") || haystack.includes("play again") || haystack.includes("playagain") || haystack.includes("play-again") || haystack.includes("try fix") || haystack.includes("tryfix") || haystack.includes("after-fix") || haystack.includes("fixed");
|
|
16988
17050
|
if (!labelsRecovery) return false;
|
|
16989
17051
|
const status = profileLowerSummaryValue(receipt, ["status", "state", "phase"]);
|
|
16990
|
-
const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result"]);
|
|
16991
|
-
const hasRecoveredState = ["valid", "success", "recovered", "fixed", "ready"].includes(status) || ["valid", "success", "recovered", "fixed", "ready"].includes(outcome);
|
|
17052
|
+
const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result", "retryOutcome", "retry_outcome"]);
|
|
17053
|
+
const hasRecoveredState = ["valid", "success", "recovered", "fixed", "ready"].includes(status) || ["valid", "success", "recovered", "fixed", "ready", "running_after_retry", "ready_after_retry"].includes(outcome);
|
|
16992
17054
|
const hasValid = setupReturnSummaryValue(receipt, ["hasValid", "valid", "isValid"]) === true;
|
|
16993
17055
|
const hasInvalid = setupReturnSummaryValue(receipt, ["hasInvalid", "invalid", "isInvalid"]);
|
|
16994
17056
|
const success = setupReturnSummaryValue(receipt, ["success", "recovered", "fixed"]) === true;
|
|
16995
|
-
|
|
17057
|
+
const leftTerminalState = setupReturnSummaryValue(receipt, ["leftTerminalState", "left_terminal_state"]) === true;
|
|
17058
|
+
const retrySurfaceReady = setupReturnSummaryValue(receipt, ["retrySurfaceReady", "retry_surface_ready"]) === true;
|
|
17059
|
+
return hasRecoveredState || success || hasValid && hasInvalid === false || leftTerminalState && retrySurfaceReady;
|
|
16996
17060
|
});
|
|
16997
17061
|
}
|
|
17062
|
+
function profileMetadataHasGeneratedOutputContract(metadata) {
|
|
17063
|
+
const contract = cliRecord(metadata.declared_state_contract);
|
|
17064
|
+
if (!contract) return false;
|
|
17065
|
+
const keys = Object.keys(contract).join(" ").toLowerCase();
|
|
17066
|
+
const values = Object.values(contract).map((value) => cliString(value)?.toLowerCase() || "").join(" ");
|
|
17067
|
+
const haystack = `${keys} ${values}`;
|
|
17068
|
+
return haystack.includes("generated_output") || haystack.includes("generated output") || haystack.includes("output-size") || haystack.includes("output size") || haystack.includes("output result") || haystack.includes("optimizer size");
|
|
17069
|
+
}
|
|
17070
|
+
function profileHasGeneratedOutputReceipt(receipts) {
|
|
17071
|
+
let outputReady = false;
|
|
17072
|
+
let outputChanged = false;
|
|
17073
|
+
for (const receipt of receipts) {
|
|
17074
|
+
const storedTo = cliString(receipt.return_stored_to) || "";
|
|
17075
|
+
const label = cliString(receipt.label) || "";
|
|
17076
|
+
const path7 = cliString(receipt.path) || cliString(receipt.function_name) || "";
|
|
17077
|
+
const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
|
|
17078
|
+
const haystack = `${storedTo} ${label} ${path7} ${summary}`.toLowerCase();
|
|
17079
|
+
const readySignal = setupReturnSummaryValue(receipt, ["outputReady", "outputStillReady"]) === true || cliFiniteNumber(setupReturnSummaryValue(receipt, ["surfaceCount", "before.surfaceCount", "after.surfaceCount"])) !== void 0 || setupReturnSummaryValue(receipt, ["size", "before.size", "after.size", "size.text", "before.size.text", "after.size.text"]) !== void 0;
|
|
17080
|
+
if (readySignal && (haystack.includes("output") || haystack.includes("size") || haystack.includes("result"))) {
|
|
17081
|
+
outputReady = true;
|
|
17082
|
+
}
|
|
17083
|
+
const beforeBytes = cliFiniteNumber(setupReturnSummaryValue(receipt, ["before.size.outputBytes", "before.outputBytes", "beforeBytes"]));
|
|
17084
|
+
const afterBytes = cliFiniteNumber(setupReturnSummaryValue(receipt, ["after.size.outputBytes", "after.outputBytes", "afterBytes"]));
|
|
17085
|
+
const beforeText = cliString(setupReturnSummaryValue(receipt, ["before.size.text", "before.outputText", "beforeText"]));
|
|
17086
|
+
const afterText = cliString(setupReturnSummaryValue(receipt, ["after.size.text", "after.outputText", "afterText"]));
|
|
17087
|
+
const explicitChange = setupReturnSummaryValue(receipt, ["sizeChanged", "outputChanged", "resultChanged"]) === true;
|
|
17088
|
+
const byteChange = beforeBytes !== void 0 && afterBytes !== void 0 && beforeBytes !== afterBytes;
|
|
17089
|
+
const textChange = Boolean(beforeText && afterText && beforeText !== afterText);
|
|
17090
|
+
if (explicitChange || byteChange || textChange) outputChanged = true;
|
|
17091
|
+
}
|
|
17092
|
+
return outputReady && outputChanged;
|
|
17093
|
+
}
|
|
16998
17094
|
function profilePackReceiptStatus(result, metadata, receipt) {
|
|
16999
17095
|
const text = receipt.toLowerCase();
|
|
17000
17096
|
const setupSummary = profileSetupSummaryRecord(result);
|
|
@@ -17025,6 +17121,7 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
17025
17121
|
const clickFallbackTapCount = clickFallbackTapKeys.size;
|
|
17026
17122
|
const tapUntilCount = profileSetupReceiptTotal(setupViewports, "tap_until");
|
|
17027
17123
|
const visibleUiActionCount = clickCount + profileSetupReceiptTotal(setupViewports, "tap") + tapUntilCount;
|
|
17124
|
+
const visibleCleanupActionCount = profileVisibleCleanupActionCount(setupViewports);
|
|
17028
17125
|
const setupFailureCount = profileSetupFailureCount(setupViewports);
|
|
17029
17126
|
const setupObstructionCount = profileSetupObstructionCount(setupViewports);
|
|
17030
17127
|
const inputDispatchCount = profileSetupReceiptTotal(setupViewports, "drag") + profileSetupReceiptTotal(setupViewports, "tap") + tapUntilCount + profileSetupReceiptTotal(setupViewports, "press") + profileSetupReceiptTotal(setupViewports, "keyboard_sequence");
|
|
@@ -17060,6 +17157,7 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
17060
17157
|
const hasTextAbsence = profileHasPassedCheck(result, ["text_absent", "selector_text_absent"]);
|
|
17061
17158
|
const hasMeasuredStateChange = hasNaturalInput || hasCanvasChange || valueReceipts.some((item) => setupReturnSummaryValue(item, ["changed"]) === true || setupReturnSummaryValue(item, ["nonWhiteDelta", "darkDelta", "pixelDelta", "movementDelta"]) !== void 0);
|
|
17062
17159
|
const hasRouteExitAffordanceReceipt = profileHasRouteExitAffordanceReceipt(valueReceipts);
|
|
17160
|
+
const hasCleanupBoundaryAffordanceReceipt = profileHasCleanupBoundaryAffordanceReceipt(valueReceipts);
|
|
17063
17161
|
const hasOfflineAudioMetricsReceipt = profileHasOfflineAudioMetricsReceipt(valueReceipts);
|
|
17064
17162
|
const hasActiveRouteLocalProofReceipt = profileHasActiveRouteLocalProofReceipt(valueReceipts);
|
|
17065
17163
|
const hasTerminalLossReceipt = profileHasTerminalLossReceipt(valueReceipts);
|
|
@@ -17068,6 +17166,8 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
17068
17166
|
const hasControlledSuccessLaunchReceipt = profileHasControlledLaunchReceipt(valueReceipts, "success");
|
|
17069
17167
|
const hasRouteContinuationReceipt = profileHasRouteContinuationReceipt(valueReceipts);
|
|
17070
17168
|
const hasRecoveredStateReceipt = profileHasRecoveredStateReceipt(valueReceipts);
|
|
17169
|
+
const hasGeneratedOutputContract = profileMetadataHasGeneratedOutputContract(metadata);
|
|
17170
|
+
const hasGeneratedOutputReceipt = profileHasGeneratedOutputReceipt(valueReceipts);
|
|
17071
17171
|
const failedCleanupInventoryReason = profileFailedCleanupInventoryReason(setupViewports);
|
|
17072
17172
|
const passedCleanupInventoryReason = profilePassedCleanupInventoryReason(setupViewports);
|
|
17073
17173
|
if (text.includes("artifact link") || text.includes("artifact path")) {
|
|
@@ -17121,6 +17221,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
17121
17221
|
}
|
|
17122
17222
|
return profileReceiptSignalStatus(hasTextAbsence, "absence check passed", "absence check missing");
|
|
17123
17223
|
}
|
|
17224
|
+
if (text.includes("generated-output") || text.includes("generated output") || text.includes("output-size") || text.includes("output size") || (text.includes("output") || text.includes("result")) && (text.includes("mutation") || text.includes("final"))) {
|
|
17225
|
+
return profileReceiptSignalStatus(
|
|
17226
|
+
hasGeneratedOutputContract && hasGeneratedOutputReceipt,
|
|
17227
|
+
"generated-output mutation receipt present",
|
|
17228
|
+
"generated-output mutation receipt missing"
|
|
17229
|
+
);
|
|
17230
|
+
}
|
|
17124
17231
|
if (text.includes("recovered") || text.includes("final state")) {
|
|
17125
17232
|
return profileReceiptSignalStatus(hasStateContract || hasTextVisibility, "final state receipt present", "final state receipt missing");
|
|
17126
17233
|
}
|
|
@@ -17177,6 +17284,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
17177
17284
|
"route continuation receipt missing"
|
|
17178
17285
|
);
|
|
17179
17286
|
}
|
|
17287
|
+
if (text.includes("cleanup") && text.includes("action") && (text.includes("visible ui") || text.includes("visible"))) {
|
|
17288
|
+
return profileReceiptSignalStatus(
|
|
17289
|
+
visibleCleanupActionCount > 0,
|
|
17290
|
+
`visible cleanup action receipt present (${visibleCleanupActionCount})`,
|
|
17291
|
+
"visible cleanup action receipt missing"
|
|
17292
|
+
);
|
|
17293
|
+
}
|
|
17180
17294
|
if (text.includes("through visible ui") || text.includes("visible ui action") || text.includes("ui-routed") || text.includes("ui routed") || text.includes("visible") && text.includes("route") && text.includes("exit") && text.includes("action") || text.includes("visible") && text.includes("mode") && text.includes("exit") && text.includes("action")) {
|
|
17181
17295
|
return profileReceiptSignalStatus(
|
|
17182
17296
|
visibleUiActionCount > 0,
|
|
@@ -17191,6 +17305,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
17191
17305
|
"affordance receipt missing"
|
|
17192
17306
|
);
|
|
17193
17307
|
}
|
|
17308
|
+
if (text.includes("cleanup") && (text.includes("affordance") || text.includes("control") || text.includes("boundary") || text.includes("inventory"))) {
|
|
17309
|
+
return profileReceiptSignalStatus(
|
|
17310
|
+
hasCleanupBoundaryAffordanceReceipt,
|
|
17311
|
+
"visible cleanup affordance receipt present",
|
|
17312
|
+
"visible cleanup affordance receipt missing"
|
|
17313
|
+
);
|
|
17314
|
+
}
|
|
17194
17315
|
if (text.includes("retry") || text.includes("repair") || text.includes("reset") || text.includes("affordance")) {
|
|
17195
17316
|
return profileReceiptSignalStatus(hasStateContract || clickCount > 0, "affordance or transition receipt present", "affordance receipt missing");
|
|
17196
17317
|
}
|
|
@@ -18528,6 +18649,21 @@ function writeProfileOutput(outputDir, result) {
|
|
|
18528
18649
|
if (result.evidence?.dom_summary) (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "dom-summary.json"), `${JSON.stringify(result.evidence.dom_summary, null, 2)}
|
|
18529
18650
|
`);
|
|
18530
18651
|
}
|
|
18652
|
+
function writeRiddleJobReceipt(outputDir, input) {
|
|
18653
|
+
if (!outputDir) return;
|
|
18654
|
+
(0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
|
|
18655
|
+
(0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "riddle-job.json"), `${JSON.stringify({
|
|
18656
|
+
version: "riddle-proof.riddle-job-receipt.v1",
|
|
18657
|
+
profile_name: input.profile.name,
|
|
18658
|
+
job_id: input.jobId,
|
|
18659
|
+
target_url: input.targetUrl,
|
|
18660
|
+
viewport: input.viewport || null,
|
|
18661
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18662
|
+
created: input.created || null,
|
|
18663
|
+
recovery_command: `riddle-proof-loop run-profile recover --profile <profile> --job ${input.jobId} --output-dir ${outputDir}`
|
|
18664
|
+
}, null, 2)}
|
|
18665
|
+
`);
|
|
18666
|
+
}
|
|
18531
18667
|
async function readArtifactJson(artifact) {
|
|
18532
18668
|
const target = artifact.url || artifact.path;
|
|
18533
18669
|
if (!target) return void 0;
|
|
@@ -18731,6 +18867,43 @@ function profileForSplitViewport(profile, viewport) {
|
|
|
18731
18867
|
}
|
|
18732
18868
|
};
|
|
18733
18869
|
}
|
|
18870
|
+
function profileItemAppliesToAnySelectedViewport(item, viewports) {
|
|
18871
|
+
if (!item.viewports?.length) return true;
|
|
18872
|
+
const names = new Set(viewports.map((viewport) => viewport.name).filter(Boolean));
|
|
18873
|
+
return item.viewports.some((name) => names.has(name));
|
|
18874
|
+
}
|
|
18875
|
+
function profileForSelectedViewports(profile, viewports) {
|
|
18876
|
+
const suffix = viewports.map((viewport) => viewport.name || `${viewport.width}x${viewport.height}`).join("-");
|
|
18877
|
+
const setupActions = profile.target.setup_actions?.filter((action) => profileItemAppliesToAnySelectedViewport(action, viewports));
|
|
18878
|
+
return {
|
|
18879
|
+
...profile,
|
|
18880
|
+
name: `${profile.name}-${suffix}`,
|
|
18881
|
+
checks: profile.checks.filter((check) => profileItemAppliesToAnySelectedViewport(check, viewports)),
|
|
18882
|
+
target: {
|
|
18883
|
+
...profile.target,
|
|
18884
|
+
viewports,
|
|
18885
|
+
...setupActions ? { setup_actions: setupActions } : {}
|
|
18886
|
+
},
|
|
18887
|
+
metadata: {
|
|
18888
|
+
...profile.metadata || {},
|
|
18889
|
+
selected_parent_profile: profile.name,
|
|
18890
|
+
selected_viewports: viewports.map((viewport) => viewport.name || `${viewport.width}x${viewport.height}`)
|
|
18891
|
+
}
|
|
18892
|
+
};
|
|
18893
|
+
}
|
|
18894
|
+
function profileWithSelectedViewportNamesForCli(profile, options) {
|
|
18895
|
+
const names = runProfileViewportNamesOption(options);
|
|
18896
|
+
if (!names.length) return profile;
|
|
18897
|
+
const requested = new Set(names);
|
|
18898
|
+
const viewports = profile.target.viewports.filter((viewport) => viewport.name && requested.has(viewport.name));
|
|
18899
|
+
const matched = new Set(viewports.map((viewport) => viewport.name).filter(Boolean));
|
|
18900
|
+
const missing = names.filter((name) => !matched.has(name));
|
|
18901
|
+
if (missing.length) {
|
|
18902
|
+
const available = profile.target.viewports.map((viewport) => viewport.name).filter(Boolean).join(", ") || "none";
|
|
18903
|
+
throw new Error(`Unknown --viewport-name ${missing.join(", ")}. Available viewport names: ${available}.`);
|
|
18904
|
+
}
|
|
18905
|
+
return profileForSelectedViewports(profile, viewports);
|
|
18906
|
+
}
|
|
18734
18907
|
function safeProfileOutputSegment(value) {
|
|
18735
18908
|
const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
18736
18909
|
return safe || "viewport";
|
|
@@ -18741,6 +18914,78 @@ function splitViewportOutputDir(outputDir, viewportName, seen) {
|
|
|
18741
18914
|
seen.set(base, count + 1);
|
|
18742
18915
|
return import_node_path6.default.join(outputDir, count ? `${base}-${count + 1}` : base);
|
|
18743
18916
|
}
|
|
18917
|
+
function profileResultPathFromInput(inputPath) {
|
|
18918
|
+
if (!(0, import_node_fs6.existsSync)(inputPath)) throw new Error(`Profile aggregate input path does not exist: ${inputPath}`);
|
|
18919
|
+
const stat = (0, import_node_fs6.statSync)(inputPath);
|
|
18920
|
+
if (stat.isFile()) return [inputPath];
|
|
18921
|
+
if (!stat.isDirectory()) throw new Error(`Profile aggregate input path must be a file or directory: ${inputPath}`);
|
|
18922
|
+
const childProfileResults = (0, import_node_fs6.readdirSync)(inputPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => import_node_path6.default.join(inputPath, entry.name, "profile-result.json")).filter((candidate) => (0, import_node_fs6.existsSync)(candidate));
|
|
18923
|
+
if (childProfileResults.length) return childProfileResults;
|
|
18924
|
+
const directProfileResult = import_node_path6.default.join(inputPath, "profile-result.json");
|
|
18925
|
+
if ((0, import_node_fs6.existsSync)(directProfileResult)) return [directProfileResult];
|
|
18926
|
+
throw new Error(`Profile aggregate input directory has no child profile-result.json files: ${inputPath}`);
|
|
18927
|
+
}
|
|
18928
|
+
function runProfileAggregateInputPathsOption(options) {
|
|
18929
|
+
const rawInputs = [
|
|
18930
|
+
optionString(options, "input"),
|
|
18931
|
+
optionString(options, "inputs"),
|
|
18932
|
+
optionString(options, "inputFile"),
|
|
18933
|
+
optionString(options, "inputFiles")
|
|
18934
|
+
].filter((value) => Boolean(value));
|
|
18935
|
+
const explicitInputs = rawInputs.flatMap((raw) => raw.split(",").map((part) => part.trim()).filter(Boolean));
|
|
18936
|
+
const inputDir = optionString(options, "inputDir") ?? optionString(options, "resultsDir") ?? optionString(options, "runDir");
|
|
18937
|
+
const discoveredInputs = inputDir ? profileResultPathFromInput(inputDir) : [];
|
|
18938
|
+
const paths = [...explicitInputs.flatMap(profileResultPathFromInput), ...discoveredInputs];
|
|
18939
|
+
const uniquePaths = [...new Set(paths.map((inputPath) => import_node_path6.default.resolve(inputPath)))];
|
|
18940
|
+
if (!uniquePaths.length) {
|
|
18941
|
+
throw new Error("run-profile aggregate requires --input-dir <dir> or --inputs <path[,path...]>.");
|
|
18942
|
+
}
|
|
18943
|
+
return uniquePaths;
|
|
18944
|
+
}
|
|
18945
|
+
function readProfileResultForAggregate(resultPath) {
|
|
18946
|
+
const parsed = readJsonValue(resultPath, resultPath);
|
|
18947
|
+
if (parsed.version !== "riddle-proof.profile-result.v1" || !Array.isArray(parsed.checks)) {
|
|
18948
|
+
throw new Error(`Profile aggregate input is not a riddle-proof.profile-result.v1 result: ${resultPath}`);
|
|
18949
|
+
}
|
|
18950
|
+
return parsed;
|
|
18951
|
+
}
|
|
18952
|
+
function profileResultIsAggregateParent(result) {
|
|
18953
|
+
const mode = cliString(cliRecord(result.riddle)?.mode);
|
|
18954
|
+
return (mode === "split-viewports" || mode === "named-viewport-aggregate") && (result.evidence?.viewports?.length || 0) > 1;
|
|
18955
|
+
}
|
|
18956
|
+
function aggregateProfileResultViewportName(result) {
|
|
18957
|
+
const evidenceViewports = result.evidence?.viewports || [];
|
|
18958
|
+
if (evidenceViewports.length === 1) return evidenceViewports[0].name;
|
|
18959
|
+
const metadata = cliRecord(result.metadata);
|
|
18960
|
+
const splitViewport = cliString(metadata?.split_viewport);
|
|
18961
|
+
if (splitViewport) return splitViewport;
|
|
18962
|
+
const selectedViewports = Array.isArray(metadata?.selected_viewports) ? metadata.selected_viewports.map(cliString).filter((name) => Boolean(name)) : [];
|
|
18963
|
+
if (selectedViewports.length === 1) return selectedViewports[0];
|
|
18964
|
+
return void 0;
|
|
18965
|
+
}
|
|
18966
|
+
function aggregateProfileResultViewport(profile, result, resultPath) {
|
|
18967
|
+
const viewportName = aggregateProfileResultViewportName(result);
|
|
18968
|
+
const parentViewport = viewportName ? profile.target.viewports.find((viewport) => viewport.name === viewportName) : void 0;
|
|
18969
|
+
if (parentViewport) return parentViewport;
|
|
18970
|
+
const evidenceViewport = result.evidence?.viewports?.length === 1 ? result.evidence.viewports[0] : void 0;
|
|
18971
|
+
if (evidenceViewport && typeof evidenceViewport.name === "string" && typeof evidenceViewport.width === "number" && Number.isFinite(evidenceViewport.width) && typeof evidenceViewport.height === "number" && Number.isFinite(evidenceViewport.height)) {
|
|
18972
|
+
return {
|
|
18973
|
+
name: evidenceViewport.name,
|
|
18974
|
+
width: evidenceViewport.width,
|
|
18975
|
+
height: evidenceViewport.height
|
|
18976
|
+
};
|
|
18977
|
+
}
|
|
18978
|
+
throw new Error(`Profile aggregate input must be a single named viewport result or include selected viewport metadata: ${resultPath}`);
|
|
18979
|
+
}
|
|
18980
|
+
function sortAggregateChildRuns(profile, childRuns) {
|
|
18981
|
+
const viewportOrder = new Map(profile.target.viewports.map((viewport, index) => [viewport.name, index]));
|
|
18982
|
+
return [...childRuns].sort((a, b) => {
|
|
18983
|
+
const aIndex = viewportOrder.get(a.viewport.name) ?? Number.MAX_SAFE_INTEGER;
|
|
18984
|
+
const bIndex = viewportOrder.get(b.viewport.name) ?? Number.MAX_SAFE_INTEGER;
|
|
18985
|
+
if (aIndex !== bIndex) return aIndex - bIndex;
|
|
18986
|
+
return a.viewport.name.localeCompare(b.viewport.name);
|
|
18987
|
+
});
|
|
18988
|
+
}
|
|
18744
18989
|
function splitViewportArtifactRefs(input) {
|
|
18745
18990
|
return (input.result.artifacts.riddle_artifacts || []).map((artifact) => ({
|
|
18746
18991
|
...artifact,
|
|
@@ -18751,7 +18996,7 @@ function sumDefinedNumbers(values) {
|
|
|
18751
18996
|
const numbers = values.filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
18752
18997
|
return numbers.length ? numbers.reduce((sum, value) => sum + value, 0) : void 0;
|
|
18753
18998
|
}
|
|
18754
|
-
function splitViewportRiddleMetadata(childRuns) {
|
|
18999
|
+
function splitViewportRiddleMetadata(childRuns, mode = "split-viewports") {
|
|
18755
19000
|
const splitJobs = childRuns.map(({ viewport, result }) => ({
|
|
18756
19001
|
viewport: viewport.name,
|
|
18757
19002
|
job_id: result.riddle?.job_id,
|
|
@@ -18768,9 +19013,9 @@ function splitViewportRiddleMetadata(childRuns) {
|
|
|
18768
19013
|
artifact_recovery: result.riddle?.artifact_recovery
|
|
18769
19014
|
}));
|
|
18770
19015
|
return {
|
|
18771
|
-
mode
|
|
19016
|
+
mode,
|
|
18772
19017
|
job_count: childRuns.length,
|
|
18773
|
-
status:
|
|
19018
|
+
status: mode,
|
|
18774
19019
|
terminal: childRuns.every(({ result }) => result.riddle?.terminal !== false),
|
|
18775
19020
|
artifact_recovery: childRuns.some(({ result }) => result.riddle?.artifact_recovery === true),
|
|
18776
19021
|
queue_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.queue_elapsed_ms)),
|
|
@@ -18919,6 +19164,13 @@ async function runSingleRiddleProfileForCli(profile, options, input) {
|
|
|
18919
19164
|
const directResult = extractRiddleProofProfileResult(created);
|
|
18920
19165
|
return directResult ? withRiddleMetadata(withProfileMetadata(profile, directResult), { artifacts: collectRiddleProfileArtifactRefs(created) }) : createRiddleProofProfileInsufficientResult({ profile, runner, error: "Riddle run response was missing job_id.", artifacts: collectRiddleProfileArtifactRefs(created) });
|
|
18921
19166
|
}
|
|
19167
|
+
writeRiddleJobReceipt(input.outputDir, {
|
|
19168
|
+
profile,
|
|
19169
|
+
jobId,
|
|
19170
|
+
targetUrl,
|
|
19171
|
+
viewport: profile.target.viewports[0],
|
|
19172
|
+
created
|
|
19173
|
+
});
|
|
18922
19174
|
poll = await client.pollJob(jobId, pollOptions);
|
|
18923
19175
|
if (attempt < retryLimit && shouldRetryUnsubmittedRiddleJob(poll)) {
|
|
18924
19176
|
const recoveredResult = await recoverProfileResultFromRiddleArtifacts(profile, {
|
|
@@ -18995,10 +19247,9 @@ async function runSplitViewportProfileForCli(profile, options, input) {
|
|
|
18995
19247
|
const childRuns = [];
|
|
18996
19248
|
for (const viewport of profile.target.viewports) {
|
|
18997
19249
|
const childProfile = profileForSplitViewport(profile, viewport);
|
|
18998
|
-
const
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
}
|
|
19250
|
+
const childOutputDir = outputDir ? splitViewportOutputDir(outputDir, viewport.name, seenOutputNames) : void 0;
|
|
19251
|
+
const result2 = await runSingleRiddleProfileForCli(childProfile, options, { ...input, outputDir: childOutputDir });
|
|
19252
|
+
if (childOutputDir) writeProfileOutput(childOutputDir, result2);
|
|
19002
19253
|
childRuns.push({ viewport, profile: childProfile, result: result2 });
|
|
19003
19254
|
}
|
|
19004
19255
|
const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
|
|
@@ -19020,6 +19271,85 @@ async function runSplitViewportProfileForCli(profile, options, input) {
|
|
|
19020
19271
|
});
|
|
19021
19272
|
return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
|
|
19022
19273
|
}
|
|
19274
|
+
async function aggregateProfileResultsForCli(profile, options) {
|
|
19275
|
+
const resultPaths = runProfileAggregateInputPathsOption(options);
|
|
19276
|
+
const seenViewports = /* @__PURE__ */ new Set();
|
|
19277
|
+
const childInputs = resultPaths.map((resultPath) => ({ resultPath, result: readProfileResultForAggregate(resultPath) })).filter(({ result: result2 }) => !profileResultIsAggregateParent(result2));
|
|
19278
|
+
if (!childInputs.length) {
|
|
19279
|
+
throw new Error("run-profile aggregate found no single-viewport child profile results.");
|
|
19280
|
+
}
|
|
19281
|
+
const childRuns = sortAggregateChildRuns(profile, childInputs.map(({ resultPath, result: result2 }) => {
|
|
19282
|
+
const viewport = aggregateProfileResultViewport(profile, result2, resultPath);
|
|
19283
|
+
if (seenViewports.has(viewport.name)) {
|
|
19284
|
+
throw new Error(`Profile aggregate received more than one result for viewport ${viewport.name}.`);
|
|
19285
|
+
}
|
|
19286
|
+
seenViewports.add(viewport.name);
|
|
19287
|
+
return { viewport, profile: profileForSplitViewport(profile, viewport), result: result2 };
|
|
19288
|
+
}));
|
|
19289
|
+
const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
|
|
19290
|
+
const blocked = childRuns.filter(({ result: result2 }) => !result2.evidence || result2.status === "environment_blocked" || result2.status === "configuration_error");
|
|
19291
|
+
if (blocked.length) {
|
|
19292
|
+
return createRiddleProofProfileEnvironmentBlockedResult({
|
|
19293
|
+
profile,
|
|
19294
|
+
runner: "riddle",
|
|
19295
|
+
error: splitViewportBlockedMessage(childRuns),
|
|
19296
|
+
riddle: splitViewportRiddleMetadata(childRuns, "named-viewport-aggregate"),
|
|
19297
|
+
artifacts
|
|
19298
|
+
});
|
|
19299
|
+
}
|
|
19300
|
+
const evidence = aggregateSplitViewportEvidence(profile, childRuns);
|
|
19301
|
+
const result = assessRiddleProofProfileEvidence(profile, evidence, {
|
|
19302
|
+
runner: "riddle",
|
|
19303
|
+
riddle: splitViewportRiddleMetadata(childRuns, "named-viewport-aggregate"),
|
|
19304
|
+
artifacts
|
|
19305
|
+
});
|
|
19306
|
+
return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
|
|
19307
|
+
}
|
|
19308
|
+
async function recoverProfileForCli(profile, options) {
|
|
19309
|
+
const runner = optionString(options, "runner") || "riddle";
|
|
19310
|
+
if (runner !== "riddle") {
|
|
19311
|
+
throw new Error(`Unsupported --runner ${runner}. The current CLI supports --runner riddle.`);
|
|
19312
|
+
}
|
|
19313
|
+
const jobId = optionString(options, "job") ?? optionString(options, "jobId");
|
|
19314
|
+
if (!jobId) throw new Error("run-profile recover requires --job <job-id>.");
|
|
19315
|
+
const client = createRiddleApiClient(riddleClientConfig(options));
|
|
19316
|
+
let artifactPayload;
|
|
19317
|
+
try {
|
|
19318
|
+
artifactPayload = await client.requestJson(`/v1/jobs/${jobId}/artifacts`);
|
|
19319
|
+
} catch (error) {
|
|
19320
|
+
return createRiddleProofProfileEnvironmentBlockedResult({
|
|
19321
|
+
profile,
|
|
19322
|
+
runner,
|
|
19323
|
+
error,
|
|
19324
|
+
riddle: { job_id: jobId, terminal: false }
|
|
19325
|
+
});
|
|
19326
|
+
}
|
|
19327
|
+
const artifacts = collectRiddleProfileArtifactRefs(artifactPayload);
|
|
19328
|
+
const artifactStatus = riddleArtifactsPayloadStatus(artifactPayload);
|
|
19329
|
+
const terminal = artifactStatus ? isTerminalRiddleJobStatus(artifactStatus) : artifacts.length > 0;
|
|
19330
|
+
const recovered = await profileResultFromRiddleArtifacts(profile, artifacts, [artifactPayload]);
|
|
19331
|
+
if (recovered) {
|
|
19332
|
+
return withRiddleMetadata(recovered, {
|
|
19333
|
+
job_id: jobId,
|
|
19334
|
+
status: artifactStatus,
|
|
19335
|
+
terminal,
|
|
19336
|
+
artifacts,
|
|
19337
|
+
artifactRecovery: true
|
|
19338
|
+
});
|
|
19339
|
+
}
|
|
19340
|
+
return createRiddleProofProfileInsufficientResult({
|
|
19341
|
+
profile,
|
|
19342
|
+
runner,
|
|
19343
|
+
error: artifacts.length ? `Riddle job ${jobId} artifacts were recovered without a proof result.` : `Riddle job ${jobId} had no recoverable artifacts.`,
|
|
19344
|
+
riddle: {
|
|
19345
|
+
job_id: jobId,
|
|
19346
|
+
status: artifactStatus,
|
|
19347
|
+
terminal,
|
|
19348
|
+
artifact_recovery: artifacts.length > 0
|
|
19349
|
+
},
|
|
19350
|
+
artifacts
|
|
19351
|
+
});
|
|
19352
|
+
}
|
|
19023
19353
|
async function runProfileForCli(profile, options) {
|
|
19024
19354
|
const runner = optionString(options, "runner") || "riddle";
|
|
19025
19355
|
if (runner !== "riddle") {
|
|
@@ -19029,7 +19359,7 @@ async function runProfileForCli(profile, options) {
|
|
|
19029
19359
|
if (runProfileSplitViewportsOption(options) && profile.target.viewports.length > 1) {
|
|
19030
19360
|
return runSplitViewportProfileForCli(profile, options, { client, runner });
|
|
19031
19361
|
}
|
|
19032
|
-
return runSingleRiddleProfileForCli(profile, options, { client, runner });
|
|
19362
|
+
return runSingleRiddleProfileForCli(profile, options, { client, runner, outputDir: profileOutputDirOption(options) });
|
|
19033
19363
|
}
|
|
19034
19364
|
function requestForRun(options) {
|
|
19035
19365
|
const statePath = optionString(options, "statePath");
|
|
@@ -19078,8 +19408,8 @@ async function main() {
|
|
|
19078
19408
|
return;
|
|
19079
19409
|
}
|
|
19080
19410
|
if (command === "run-profile") {
|
|
19081
|
-
const profile = normalizeProfileForCli(options);
|
|
19082
|
-
const result = await runProfileForCli(profile, options);
|
|
19411
|
+
const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
|
|
19412
|
+
const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : positional[1] === "aggregate" ? await aggregateProfileResultsForCli(profile, options) : await runProfileForCli(profile, options);
|
|
19083
19413
|
writeProfileOutput(profileOutputDirOption(options), result);
|
|
19084
19414
|
const diagnosticLine = profileCliDiagnosticLine(result);
|
|
19085
19415
|
if (diagnosticLine && optionBoolean(options, "quiet") !== true) {
|
package/dist/cli.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
import "./chunk-VY4Y5U57.js";
|
|
39
39
|
|
|
40
40
|
// src/cli.ts
|
|
41
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
41
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
42
42
|
import path from "path";
|
|
43
43
|
function usage() {
|
|
44
44
|
return [
|
|
@@ -48,7 +48,9 @@ function usage() {
|
|
|
48
48
|
" riddle-proof-loop respond --state-path <path> --response-json <file|json|->",
|
|
49
49
|
" riddle-proof-loop respond --state-path <path> --decision <decision> --summary <text> [--payload-json <file|json|->]",
|
|
50
50
|
" riddle-proof-loop status --state-path <path>",
|
|
51
|
-
" riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--runner riddle] [--strict true|false; default false] [--split-viewports true|false; default false] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
|
|
51
|
+
" riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--runner riddle] [--viewport-name <name[,name...]>] [--strict true|false; default false] [--split-viewports true|false; default false] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
|
|
52
|
+
" riddle-proof-loop run-profile aggregate --profile <file|json|-> --url <base-url> --input-dir <dir>|--inputs <path[,path...]> [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
|
|
53
|
+
" riddle-proof-loop run-profile recover --profile <file|json|-> --url <base-url> --job <job-id> [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
|
|
52
54
|
" riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
|
|
53
55
|
" riddle-proof-loop profile-http-status-preflight --profile <file|json|-> --url <base-url> [--format json|summary]",
|
|
54
56
|
" riddle-proof-loop riddle-preview-deploy <build-dir> <label> [--framework spa|static]",
|
|
@@ -108,6 +110,11 @@ function runProfileStrictOption(options) {
|
|
|
108
110
|
function runProfileSplitViewportsOption(options) {
|
|
109
111
|
return optionBoolean(options, "splitViewports") ?? false;
|
|
110
112
|
}
|
|
113
|
+
function runProfileViewportNamesOption(options) {
|
|
114
|
+
const raw = optionString(options, "viewportName") ?? optionString(options, "viewportNames");
|
|
115
|
+
if (!raw) return [];
|
|
116
|
+
return raw.split(",").map((part) => part.trim()).filter(Boolean);
|
|
117
|
+
}
|
|
111
118
|
var DEFAULT_PROFILE_UNSUBMITTED_RETRY_TIMEOUT_MS = 9e4;
|
|
112
119
|
var DEFAULT_PROFILE_UNSUBMITTED_RETRIES = 2;
|
|
113
120
|
function optionNumber(options, ...keys) {
|
|
@@ -708,6 +715,61 @@ function profileHasRouteExitAffordanceReceipt(receipts) {
|
|
|
708
715
|
return routeFields.some((name) => setupReturnSummaryValue(receipt, [name]) !== void 0) || haystack.includes("route=") || haystack.includes("browserpath=");
|
|
709
716
|
});
|
|
710
717
|
}
|
|
718
|
+
function profileCleanupLabelMatches(value) {
|
|
719
|
+
if (!value) return false;
|
|
720
|
+
return /\b(cleanup|clean|clear|reset|undo|discard|new)\b/i.test(value);
|
|
721
|
+
}
|
|
722
|
+
function profileHasCleanupBoundaryAffordanceReceipt(receipts) {
|
|
723
|
+
const visibleFields = [
|
|
724
|
+
"cleanupControlVisible",
|
|
725
|
+
"cleanupVisible",
|
|
726
|
+
"clearControlVisible",
|
|
727
|
+
"resetControlVisible",
|
|
728
|
+
"undoVisible",
|
|
729
|
+
"discardVisible",
|
|
730
|
+
"newControlVisible",
|
|
731
|
+
"exitControlVisible"
|
|
732
|
+
];
|
|
733
|
+
const textFields = [
|
|
734
|
+
"cleanupControlText",
|
|
735
|
+
"cleanupText",
|
|
736
|
+
"clearControlText",
|
|
737
|
+
"resetControlText",
|
|
738
|
+
"undoText",
|
|
739
|
+
"discardText",
|
|
740
|
+
"newControlText",
|
|
741
|
+
"exitControlText",
|
|
742
|
+
"controlText",
|
|
743
|
+
"affordanceText"
|
|
744
|
+
];
|
|
745
|
+
return receipts.some((receipt) => {
|
|
746
|
+
const storedTo = cliString(receipt.return_stored_to) || "";
|
|
747
|
+
const label = cliString(receipt.label) || "";
|
|
748
|
+
const path2 = cliString(receipt.path) || cliString(receipt.function_name) || "";
|
|
749
|
+
const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
|
|
750
|
+
const haystack = `${storedTo} ${label} ${path2} ${summary}`.toLowerCase();
|
|
751
|
+
const mentionsCleanupBoundary = haystack.includes("cleanup") || haystack.includes("precleanup") || haystack.includes("pre-cleanup") || haystack.includes("boundary") || haystack.includes("undo") || haystack.includes("clear") || haystack.includes("reset") || haystack.includes("discard");
|
|
752
|
+
const visibleControl = visibleFields.some((name) => setupReturnSummaryValue(receipt, [name]) === true);
|
|
753
|
+
const controlText = textFields.map((name) => cliString(setupReturnSummaryValue(receipt, [name]))).find((value) => profileCleanupLabelMatches(value));
|
|
754
|
+
return mentionsCleanupBoundary && (visibleControl || Boolean(controlText));
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
function profileVisibleCleanupActionCount(setupViewports) {
|
|
758
|
+
const keys = /* @__PURE__ */ new Set();
|
|
759
|
+
const clickedReceipts = setupViewports.flatMap((viewport) => [
|
|
760
|
+
...setupReceiptArray(viewport, "clicked"),
|
|
761
|
+
...setupReceiptArray(viewport, "tap"),
|
|
762
|
+
...setupReceiptArray(viewport, "tap_until")
|
|
763
|
+
]);
|
|
764
|
+
clickedReceipts.forEach((receipt, index) => {
|
|
765
|
+
if (receipt.ok === false) return;
|
|
766
|
+
const text = cliString(receipt.text) || cliString(receipt.label) || cliString(receipt.target) || cliString(receipt.selector);
|
|
767
|
+
if (!profileCleanupLabelMatches(text)) return;
|
|
768
|
+
const ordinal = cliFiniteNumber(receipt.ordinal);
|
|
769
|
+
keys.add(ordinal === void 0 ? `idx:${index}:${text}` : `ord:${ordinal}:${text}`);
|
|
770
|
+
});
|
|
771
|
+
return keys.size;
|
|
772
|
+
}
|
|
711
773
|
function profileHasOfflineAudioMetricsReceipt(receipts) {
|
|
712
774
|
const metricFields = [
|
|
713
775
|
"mixPeak",
|
|
@@ -809,17 +871,51 @@ function profileHasRecoveredStateReceipt(receipts) {
|
|
|
809
871
|
const path2 = cliString(receipt.path) || cliString(receipt.function_name) || "";
|
|
810
872
|
const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
|
|
811
873
|
const haystack = `${storedTo} ${label} ${path2} ${summary}`.toLowerCase();
|
|
812
|
-
const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || haystack.includes("try fix") || haystack.includes("tryfix") || haystack.includes("after-fix") || haystack.includes("fixed");
|
|
874
|
+
const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || haystack.includes("retry") || haystack.includes("restart") || haystack.includes("play again") || haystack.includes("playagain") || haystack.includes("play-again") || haystack.includes("try fix") || haystack.includes("tryfix") || haystack.includes("after-fix") || haystack.includes("fixed");
|
|
813
875
|
if (!labelsRecovery) return false;
|
|
814
876
|
const status = profileLowerSummaryValue(receipt, ["status", "state", "phase"]);
|
|
815
|
-
const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result"]);
|
|
816
|
-
const hasRecoveredState = ["valid", "success", "recovered", "fixed", "ready"].includes(status) || ["valid", "success", "recovered", "fixed", "ready"].includes(outcome);
|
|
877
|
+
const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result", "retryOutcome", "retry_outcome"]);
|
|
878
|
+
const hasRecoveredState = ["valid", "success", "recovered", "fixed", "ready"].includes(status) || ["valid", "success", "recovered", "fixed", "ready", "running_after_retry", "ready_after_retry"].includes(outcome);
|
|
817
879
|
const hasValid = setupReturnSummaryValue(receipt, ["hasValid", "valid", "isValid"]) === true;
|
|
818
880
|
const hasInvalid = setupReturnSummaryValue(receipt, ["hasInvalid", "invalid", "isInvalid"]);
|
|
819
881
|
const success = setupReturnSummaryValue(receipt, ["success", "recovered", "fixed"]) === true;
|
|
820
|
-
|
|
882
|
+
const leftTerminalState = setupReturnSummaryValue(receipt, ["leftTerminalState", "left_terminal_state"]) === true;
|
|
883
|
+
const retrySurfaceReady = setupReturnSummaryValue(receipt, ["retrySurfaceReady", "retry_surface_ready"]) === true;
|
|
884
|
+
return hasRecoveredState || success || hasValid && hasInvalid === false || leftTerminalState && retrySurfaceReady;
|
|
821
885
|
});
|
|
822
886
|
}
|
|
887
|
+
function profileMetadataHasGeneratedOutputContract(metadata) {
|
|
888
|
+
const contract = cliRecord(metadata.declared_state_contract);
|
|
889
|
+
if (!contract) return false;
|
|
890
|
+
const keys = Object.keys(contract).join(" ").toLowerCase();
|
|
891
|
+
const values = Object.values(contract).map((value) => cliString(value)?.toLowerCase() || "").join(" ");
|
|
892
|
+
const haystack = `${keys} ${values}`;
|
|
893
|
+
return haystack.includes("generated_output") || haystack.includes("generated output") || haystack.includes("output-size") || haystack.includes("output size") || haystack.includes("output result") || haystack.includes("optimizer size");
|
|
894
|
+
}
|
|
895
|
+
function profileHasGeneratedOutputReceipt(receipts) {
|
|
896
|
+
let outputReady = false;
|
|
897
|
+
let outputChanged = false;
|
|
898
|
+
for (const receipt of receipts) {
|
|
899
|
+
const storedTo = cliString(receipt.return_stored_to) || "";
|
|
900
|
+
const label = cliString(receipt.label) || "";
|
|
901
|
+
const path2 = cliString(receipt.path) || cliString(receipt.function_name) || "";
|
|
902
|
+
const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
|
|
903
|
+
const haystack = `${storedTo} ${label} ${path2} ${summary}`.toLowerCase();
|
|
904
|
+
const readySignal = setupReturnSummaryValue(receipt, ["outputReady", "outputStillReady"]) === true || cliFiniteNumber(setupReturnSummaryValue(receipt, ["surfaceCount", "before.surfaceCount", "after.surfaceCount"])) !== void 0 || setupReturnSummaryValue(receipt, ["size", "before.size", "after.size", "size.text", "before.size.text", "after.size.text"]) !== void 0;
|
|
905
|
+
if (readySignal && (haystack.includes("output") || haystack.includes("size") || haystack.includes("result"))) {
|
|
906
|
+
outputReady = true;
|
|
907
|
+
}
|
|
908
|
+
const beforeBytes = cliFiniteNumber(setupReturnSummaryValue(receipt, ["before.size.outputBytes", "before.outputBytes", "beforeBytes"]));
|
|
909
|
+
const afterBytes = cliFiniteNumber(setupReturnSummaryValue(receipt, ["after.size.outputBytes", "after.outputBytes", "afterBytes"]));
|
|
910
|
+
const beforeText = cliString(setupReturnSummaryValue(receipt, ["before.size.text", "before.outputText", "beforeText"]));
|
|
911
|
+
const afterText = cliString(setupReturnSummaryValue(receipt, ["after.size.text", "after.outputText", "afterText"]));
|
|
912
|
+
const explicitChange = setupReturnSummaryValue(receipt, ["sizeChanged", "outputChanged", "resultChanged"]) === true;
|
|
913
|
+
const byteChange = beforeBytes !== void 0 && afterBytes !== void 0 && beforeBytes !== afterBytes;
|
|
914
|
+
const textChange = Boolean(beforeText && afterText && beforeText !== afterText);
|
|
915
|
+
if (explicitChange || byteChange || textChange) outputChanged = true;
|
|
916
|
+
}
|
|
917
|
+
return outputReady && outputChanged;
|
|
918
|
+
}
|
|
823
919
|
function profilePackReceiptStatus(result, metadata, receipt) {
|
|
824
920
|
const text = receipt.toLowerCase();
|
|
825
921
|
const setupSummary = profileSetupSummaryRecord(result);
|
|
@@ -850,6 +946,7 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
850
946
|
const clickFallbackTapCount = clickFallbackTapKeys.size;
|
|
851
947
|
const tapUntilCount = profileSetupReceiptTotal(setupViewports, "tap_until");
|
|
852
948
|
const visibleUiActionCount = clickCount + profileSetupReceiptTotal(setupViewports, "tap") + tapUntilCount;
|
|
949
|
+
const visibleCleanupActionCount = profileVisibleCleanupActionCount(setupViewports);
|
|
853
950
|
const setupFailureCount = profileSetupFailureCount(setupViewports);
|
|
854
951
|
const setupObstructionCount = profileSetupObstructionCount(setupViewports);
|
|
855
952
|
const inputDispatchCount = profileSetupReceiptTotal(setupViewports, "drag") + profileSetupReceiptTotal(setupViewports, "tap") + tapUntilCount + profileSetupReceiptTotal(setupViewports, "press") + profileSetupReceiptTotal(setupViewports, "keyboard_sequence");
|
|
@@ -885,6 +982,7 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
885
982
|
const hasTextAbsence = profileHasPassedCheck(result, ["text_absent", "selector_text_absent"]);
|
|
886
983
|
const hasMeasuredStateChange = hasNaturalInput || hasCanvasChange || valueReceipts.some((item) => setupReturnSummaryValue(item, ["changed"]) === true || setupReturnSummaryValue(item, ["nonWhiteDelta", "darkDelta", "pixelDelta", "movementDelta"]) !== void 0);
|
|
887
984
|
const hasRouteExitAffordanceReceipt = profileHasRouteExitAffordanceReceipt(valueReceipts);
|
|
985
|
+
const hasCleanupBoundaryAffordanceReceipt = profileHasCleanupBoundaryAffordanceReceipt(valueReceipts);
|
|
888
986
|
const hasOfflineAudioMetricsReceipt = profileHasOfflineAudioMetricsReceipt(valueReceipts);
|
|
889
987
|
const hasActiveRouteLocalProofReceipt = profileHasActiveRouteLocalProofReceipt(valueReceipts);
|
|
890
988
|
const hasTerminalLossReceipt = profileHasTerminalLossReceipt(valueReceipts);
|
|
@@ -893,6 +991,8 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
893
991
|
const hasControlledSuccessLaunchReceipt = profileHasControlledLaunchReceipt(valueReceipts, "success");
|
|
894
992
|
const hasRouteContinuationReceipt = profileHasRouteContinuationReceipt(valueReceipts);
|
|
895
993
|
const hasRecoveredStateReceipt = profileHasRecoveredStateReceipt(valueReceipts);
|
|
994
|
+
const hasGeneratedOutputContract = profileMetadataHasGeneratedOutputContract(metadata);
|
|
995
|
+
const hasGeneratedOutputReceipt = profileHasGeneratedOutputReceipt(valueReceipts);
|
|
896
996
|
const failedCleanupInventoryReason = profileFailedCleanupInventoryReason(setupViewports);
|
|
897
997
|
const passedCleanupInventoryReason = profilePassedCleanupInventoryReason(setupViewports);
|
|
898
998
|
if (text.includes("artifact link") || text.includes("artifact path")) {
|
|
@@ -946,6 +1046,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
946
1046
|
}
|
|
947
1047
|
return profileReceiptSignalStatus(hasTextAbsence, "absence check passed", "absence check missing");
|
|
948
1048
|
}
|
|
1049
|
+
if (text.includes("generated-output") || text.includes("generated output") || text.includes("output-size") || text.includes("output size") || (text.includes("output") || text.includes("result")) && (text.includes("mutation") || text.includes("final"))) {
|
|
1050
|
+
return profileReceiptSignalStatus(
|
|
1051
|
+
hasGeneratedOutputContract && hasGeneratedOutputReceipt,
|
|
1052
|
+
"generated-output mutation receipt present",
|
|
1053
|
+
"generated-output mutation receipt missing"
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
949
1056
|
if (text.includes("recovered") || text.includes("final state")) {
|
|
950
1057
|
return profileReceiptSignalStatus(hasStateContract || hasTextVisibility, "final state receipt present", "final state receipt missing");
|
|
951
1058
|
}
|
|
@@ -1002,6 +1109,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
1002
1109
|
"route continuation receipt missing"
|
|
1003
1110
|
);
|
|
1004
1111
|
}
|
|
1112
|
+
if (text.includes("cleanup") && text.includes("action") && (text.includes("visible ui") || text.includes("visible"))) {
|
|
1113
|
+
return profileReceiptSignalStatus(
|
|
1114
|
+
visibleCleanupActionCount > 0,
|
|
1115
|
+
`visible cleanup action receipt present (${visibleCleanupActionCount})`,
|
|
1116
|
+
"visible cleanup action receipt missing"
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1005
1119
|
if (text.includes("through visible ui") || text.includes("visible ui action") || text.includes("ui-routed") || text.includes("ui routed") || text.includes("visible") && text.includes("route") && text.includes("exit") && text.includes("action") || text.includes("visible") && text.includes("mode") && text.includes("exit") && text.includes("action")) {
|
|
1006
1120
|
return profileReceiptSignalStatus(
|
|
1007
1121
|
visibleUiActionCount > 0,
|
|
@@ -1016,6 +1130,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
|
|
|
1016
1130
|
"affordance receipt missing"
|
|
1017
1131
|
);
|
|
1018
1132
|
}
|
|
1133
|
+
if (text.includes("cleanup") && (text.includes("affordance") || text.includes("control") || text.includes("boundary") || text.includes("inventory"))) {
|
|
1134
|
+
return profileReceiptSignalStatus(
|
|
1135
|
+
hasCleanupBoundaryAffordanceReceipt,
|
|
1136
|
+
"visible cleanup affordance receipt present",
|
|
1137
|
+
"visible cleanup affordance receipt missing"
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1019
1140
|
if (text.includes("retry") || text.includes("repair") || text.includes("reset") || text.includes("affordance")) {
|
|
1020
1141
|
return profileReceiptSignalStatus(hasStateContract || clickCount > 0, "affordance or transition receipt present", "affordance receipt missing");
|
|
1021
1142
|
}
|
|
@@ -2353,6 +2474,21 @@ function writeProfileOutput(outputDir, result) {
|
|
|
2353
2474
|
if (result.evidence?.dom_summary) writeFileSync(path.join(outputDir, "dom-summary.json"), `${JSON.stringify(result.evidence.dom_summary, null, 2)}
|
|
2354
2475
|
`);
|
|
2355
2476
|
}
|
|
2477
|
+
function writeRiddleJobReceipt(outputDir, input) {
|
|
2478
|
+
if (!outputDir) return;
|
|
2479
|
+
mkdirSync(outputDir, { recursive: true });
|
|
2480
|
+
writeFileSync(path.join(outputDir, "riddle-job.json"), `${JSON.stringify({
|
|
2481
|
+
version: "riddle-proof.riddle-job-receipt.v1",
|
|
2482
|
+
profile_name: input.profile.name,
|
|
2483
|
+
job_id: input.jobId,
|
|
2484
|
+
target_url: input.targetUrl,
|
|
2485
|
+
viewport: input.viewport || null,
|
|
2486
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2487
|
+
created: input.created || null,
|
|
2488
|
+
recovery_command: `riddle-proof-loop run-profile recover --profile <profile> --job ${input.jobId} --output-dir ${outputDir}`
|
|
2489
|
+
}, null, 2)}
|
|
2490
|
+
`);
|
|
2491
|
+
}
|
|
2356
2492
|
async function readArtifactJson(artifact) {
|
|
2357
2493
|
const target = artifact.url || artifact.path;
|
|
2358
2494
|
if (!target) return void 0;
|
|
@@ -2556,6 +2692,43 @@ function profileForSplitViewport(profile, viewport) {
|
|
|
2556
2692
|
}
|
|
2557
2693
|
};
|
|
2558
2694
|
}
|
|
2695
|
+
function profileItemAppliesToAnySelectedViewport(item, viewports) {
|
|
2696
|
+
if (!item.viewports?.length) return true;
|
|
2697
|
+
const names = new Set(viewports.map((viewport) => viewport.name).filter(Boolean));
|
|
2698
|
+
return item.viewports.some((name) => names.has(name));
|
|
2699
|
+
}
|
|
2700
|
+
function profileForSelectedViewports(profile, viewports) {
|
|
2701
|
+
const suffix = viewports.map((viewport) => viewport.name || `${viewport.width}x${viewport.height}`).join("-");
|
|
2702
|
+
const setupActions = profile.target.setup_actions?.filter((action) => profileItemAppliesToAnySelectedViewport(action, viewports));
|
|
2703
|
+
return {
|
|
2704
|
+
...profile,
|
|
2705
|
+
name: `${profile.name}-${suffix}`,
|
|
2706
|
+
checks: profile.checks.filter((check) => profileItemAppliesToAnySelectedViewport(check, viewports)),
|
|
2707
|
+
target: {
|
|
2708
|
+
...profile.target,
|
|
2709
|
+
viewports,
|
|
2710
|
+
...setupActions ? { setup_actions: setupActions } : {}
|
|
2711
|
+
},
|
|
2712
|
+
metadata: {
|
|
2713
|
+
...profile.metadata || {},
|
|
2714
|
+
selected_parent_profile: profile.name,
|
|
2715
|
+
selected_viewports: viewports.map((viewport) => viewport.name || `${viewport.width}x${viewport.height}`)
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
function profileWithSelectedViewportNamesForCli(profile, options) {
|
|
2720
|
+
const names = runProfileViewportNamesOption(options);
|
|
2721
|
+
if (!names.length) return profile;
|
|
2722
|
+
const requested = new Set(names);
|
|
2723
|
+
const viewports = profile.target.viewports.filter((viewport) => viewport.name && requested.has(viewport.name));
|
|
2724
|
+
const matched = new Set(viewports.map((viewport) => viewport.name).filter(Boolean));
|
|
2725
|
+
const missing = names.filter((name) => !matched.has(name));
|
|
2726
|
+
if (missing.length) {
|
|
2727
|
+
const available = profile.target.viewports.map((viewport) => viewport.name).filter(Boolean).join(", ") || "none";
|
|
2728
|
+
throw new Error(`Unknown --viewport-name ${missing.join(", ")}. Available viewport names: ${available}.`);
|
|
2729
|
+
}
|
|
2730
|
+
return profileForSelectedViewports(profile, viewports);
|
|
2731
|
+
}
|
|
2559
2732
|
function safeProfileOutputSegment(value) {
|
|
2560
2733
|
const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2561
2734
|
return safe || "viewport";
|
|
@@ -2566,6 +2739,78 @@ function splitViewportOutputDir(outputDir, viewportName, seen) {
|
|
|
2566
2739
|
seen.set(base, count + 1);
|
|
2567
2740
|
return path.join(outputDir, count ? `${base}-${count + 1}` : base);
|
|
2568
2741
|
}
|
|
2742
|
+
function profileResultPathFromInput(inputPath) {
|
|
2743
|
+
if (!existsSync(inputPath)) throw new Error(`Profile aggregate input path does not exist: ${inputPath}`);
|
|
2744
|
+
const stat = statSync(inputPath);
|
|
2745
|
+
if (stat.isFile()) return [inputPath];
|
|
2746
|
+
if (!stat.isDirectory()) throw new Error(`Profile aggregate input path must be a file or directory: ${inputPath}`);
|
|
2747
|
+
const childProfileResults = readdirSync(inputPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(inputPath, entry.name, "profile-result.json")).filter((candidate) => existsSync(candidate));
|
|
2748
|
+
if (childProfileResults.length) return childProfileResults;
|
|
2749
|
+
const directProfileResult = path.join(inputPath, "profile-result.json");
|
|
2750
|
+
if (existsSync(directProfileResult)) return [directProfileResult];
|
|
2751
|
+
throw new Error(`Profile aggregate input directory has no child profile-result.json files: ${inputPath}`);
|
|
2752
|
+
}
|
|
2753
|
+
function runProfileAggregateInputPathsOption(options) {
|
|
2754
|
+
const rawInputs = [
|
|
2755
|
+
optionString(options, "input"),
|
|
2756
|
+
optionString(options, "inputs"),
|
|
2757
|
+
optionString(options, "inputFile"),
|
|
2758
|
+
optionString(options, "inputFiles")
|
|
2759
|
+
].filter((value) => Boolean(value));
|
|
2760
|
+
const explicitInputs = rawInputs.flatMap((raw) => raw.split(",").map((part) => part.trim()).filter(Boolean));
|
|
2761
|
+
const inputDir = optionString(options, "inputDir") ?? optionString(options, "resultsDir") ?? optionString(options, "runDir");
|
|
2762
|
+
const discoveredInputs = inputDir ? profileResultPathFromInput(inputDir) : [];
|
|
2763
|
+
const paths = [...explicitInputs.flatMap(profileResultPathFromInput), ...discoveredInputs];
|
|
2764
|
+
const uniquePaths = [...new Set(paths.map((inputPath) => path.resolve(inputPath)))];
|
|
2765
|
+
if (!uniquePaths.length) {
|
|
2766
|
+
throw new Error("run-profile aggregate requires --input-dir <dir> or --inputs <path[,path...]>.");
|
|
2767
|
+
}
|
|
2768
|
+
return uniquePaths;
|
|
2769
|
+
}
|
|
2770
|
+
function readProfileResultForAggregate(resultPath) {
|
|
2771
|
+
const parsed = readJsonValue(resultPath, resultPath);
|
|
2772
|
+
if (parsed.version !== "riddle-proof.profile-result.v1" || !Array.isArray(parsed.checks)) {
|
|
2773
|
+
throw new Error(`Profile aggregate input is not a riddle-proof.profile-result.v1 result: ${resultPath}`);
|
|
2774
|
+
}
|
|
2775
|
+
return parsed;
|
|
2776
|
+
}
|
|
2777
|
+
function profileResultIsAggregateParent(result) {
|
|
2778
|
+
const mode = cliString(cliRecord(result.riddle)?.mode);
|
|
2779
|
+
return (mode === "split-viewports" || mode === "named-viewport-aggregate") && (result.evidence?.viewports?.length || 0) > 1;
|
|
2780
|
+
}
|
|
2781
|
+
function aggregateProfileResultViewportName(result) {
|
|
2782
|
+
const evidenceViewports = result.evidence?.viewports || [];
|
|
2783
|
+
if (evidenceViewports.length === 1) return evidenceViewports[0].name;
|
|
2784
|
+
const metadata = cliRecord(result.metadata);
|
|
2785
|
+
const splitViewport = cliString(metadata?.split_viewport);
|
|
2786
|
+
if (splitViewport) return splitViewport;
|
|
2787
|
+
const selectedViewports = Array.isArray(metadata?.selected_viewports) ? metadata.selected_viewports.map(cliString).filter((name) => Boolean(name)) : [];
|
|
2788
|
+
if (selectedViewports.length === 1) return selectedViewports[0];
|
|
2789
|
+
return void 0;
|
|
2790
|
+
}
|
|
2791
|
+
function aggregateProfileResultViewport(profile, result, resultPath) {
|
|
2792
|
+
const viewportName = aggregateProfileResultViewportName(result);
|
|
2793
|
+
const parentViewport = viewportName ? profile.target.viewports.find((viewport) => viewport.name === viewportName) : void 0;
|
|
2794
|
+
if (parentViewport) return parentViewport;
|
|
2795
|
+
const evidenceViewport = result.evidence?.viewports?.length === 1 ? result.evidence.viewports[0] : void 0;
|
|
2796
|
+
if (evidenceViewport && typeof evidenceViewport.name === "string" && typeof evidenceViewport.width === "number" && Number.isFinite(evidenceViewport.width) && typeof evidenceViewport.height === "number" && Number.isFinite(evidenceViewport.height)) {
|
|
2797
|
+
return {
|
|
2798
|
+
name: evidenceViewport.name,
|
|
2799
|
+
width: evidenceViewport.width,
|
|
2800
|
+
height: evidenceViewport.height
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
throw new Error(`Profile aggregate input must be a single named viewport result or include selected viewport metadata: ${resultPath}`);
|
|
2804
|
+
}
|
|
2805
|
+
function sortAggregateChildRuns(profile, childRuns) {
|
|
2806
|
+
const viewportOrder = new Map(profile.target.viewports.map((viewport, index) => [viewport.name, index]));
|
|
2807
|
+
return [...childRuns].sort((a, b) => {
|
|
2808
|
+
const aIndex = viewportOrder.get(a.viewport.name) ?? Number.MAX_SAFE_INTEGER;
|
|
2809
|
+
const bIndex = viewportOrder.get(b.viewport.name) ?? Number.MAX_SAFE_INTEGER;
|
|
2810
|
+
if (aIndex !== bIndex) return aIndex - bIndex;
|
|
2811
|
+
return a.viewport.name.localeCompare(b.viewport.name);
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2569
2814
|
function splitViewportArtifactRefs(input) {
|
|
2570
2815
|
return (input.result.artifacts.riddle_artifacts || []).map((artifact) => ({
|
|
2571
2816
|
...artifact,
|
|
@@ -2576,7 +2821,7 @@ function sumDefinedNumbers(values) {
|
|
|
2576
2821
|
const numbers = values.filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
2577
2822
|
return numbers.length ? numbers.reduce((sum, value) => sum + value, 0) : void 0;
|
|
2578
2823
|
}
|
|
2579
|
-
function splitViewportRiddleMetadata(childRuns) {
|
|
2824
|
+
function splitViewportRiddleMetadata(childRuns, mode = "split-viewports") {
|
|
2580
2825
|
const splitJobs = childRuns.map(({ viewport, result }) => ({
|
|
2581
2826
|
viewport: viewport.name,
|
|
2582
2827
|
job_id: result.riddle?.job_id,
|
|
@@ -2593,9 +2838,9 @@ function splitViewportRiddleMetadata(childRuns) {
|
|
|
2593
2838
|
artifact_recovery: result.riddle?.artifact_recovery
|
|
2594
2839
|
}));
|
|
2595
2840
|
return {
|
|
2596
|
-
mode
|
|
2841
|
+
mode,
|
|
2597
2842
|
job_count: childRuns.length,
|
|
2598
|
-
status:
|
|
2843
|
+
status: mode,
|
|
2599
2844
|
terminal: childRuns.every(({ result }) => result.riddle?.terminal !== false),
|
|
2600
2845
|
artifact_recovery: childRuns.some(({ result }) => result.riddle?.artifact_recovery === true),
|
|
2601
2846
|
queue_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.queue_elapsed_ms)),
|
|
@@ -2744,6 +2989,13 @@ async function runSingleRiddleProfileForCli(profile, options, input) {
|
|
|
2744
2989
|
const directResult = extractRiddleProofProfileResult(created);
|
|
2745
2990
|
return directResult ? withRiddleMetadata(withProfileMetadata(profile, directResult), { artifacts: collectRiddleProfileArtifactRefs(created) }) : createRiddleProofProfileInsufficientResult({ profile, runner, error: "Riddle run response was missing job_id.", artifacts: collectRiddleProfileArtifactRefs(created) });
|
|
2746
2991
|
}
|
|
2992
|
+
writeRiddleJobReceipt(input.outputDir, {
|
|
2993
|
+
profile,
|
|
2994
|
+
jobId,
|
|
2995
|
+
targetUrl,
|
|
2996
|
+
viewport: profile.target.viewports[0],
|
|
2997
|
+
created
|
|
2998
|
+
});
|
|
2747
2999
|
poll = await client.pollJob(jobId, pollOptions);
|
|
2748
3000
|
if (attempt < retryLimit && shouldRetryUnsubmittedRiddleJob(poll)) {
|
|
2749
3001
|
const recoveredResult = await recoverProfileResultFromRiddleArtifacts(profile, {
|
|
@@ -2820,10 +3072,9 @@ async function runSplitViewportProfileForCli(profile, options, input) {
|
|
|
2820
3072
|
const childRuns = [];
|
|
2821
3073
|
for (const viewport of profile.target.viewports) {
|
|
2822
3074
|
const childProfile = profileForSplitViewport(profile, viewport);
|
|
2823
|
-
const
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
}
|
|
3075
|
+
const childOutputDir = outputDir ? splitViewportOutputDir(outputDir, viewport.name, seenOutputNames) : void 0;
|
|
3076
|
+
const result2 = await runSingleRiddleProfileForCli(childProfile, options, { ...input, outputDir: childOutputDir });
|
|
3077
|
+
if (childOutputDir) writeProfileOutput(childOutputDir, result2);
|
|
2827
3078
|
childRuns.push({ viewport, profile: childProfile, result: result2 });
|
|
2828
3079
|
}
|
|
2829
3080
|
const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
|
|
@@ -2845,6 +3096,85 @@ async function runSplitViewportProfileForCli(profile, options, input) {
|
|
|
2845
3096
|
});
|
|
2846
3097
|
return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
|
|
2847
3098
|
}
|
|
3099
|
+
async function aggregateProfileResultsForCli(profile, options) {
|
|
3100
|
+
const resultPaths = runProfileAggregateInputPathsOption(options);
|
|
3101
|
+
const seenViewports = /* @__PURE__ */ new Set();
|
|
3102
|
+
const childInputs = resultPaths.map((resultPath) => ({ resultPath, result: readProfileResultForAggregate(resultPath) })).filter(({ result: result2 }) => !profileResultIsAggregateParent(result2));
|
|
3103
|
+
if (!childInputs.length) {
|
|
3104
|
+
throw new Error("run-profile aggregate found no single-viewport child profile results.");
|
|
3105
|
+
}
|
|
3106
|
+
const childRuns = sortAggregateChildRuns(profile, childInputs.map(({ resultPath, result: result2 }) => {
|
|
3107
|
+
const viewport = aggregateProfileResultViewport(profile, result2, resultPath);
|
|
3108
|
+
if (seenViewports.has(viewport.name)) {
|
|
3109
|
+
throw new Error(`Profile aggregate received more than one result for viewport ${viewport.name}.`);
|
|
3110
|
+
}
|
|
3111
|
+
seenViewports.add(viewport.name);
|
|
3112
|
+
return { viewport, profile: profileForSplitViewport(profile, viewport), result: result2 };
|
|
3113
|
+
}));
|
|
3114
|
+
const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
|
|
3115
|
+
const blocked = childRuns.filter(({ result: result2 }) => !result2.evidence || result2.status === "environment_blocked" || result2.status === "configuration_error");
|
|
3116
|
+
if (blocked.length) {
|
|
3117
|
+
return createRiddleProofProfileEnvironmentBlockedResult({
|
|
3118
|
+
profile,
|
|
3119
|
+
runner: "riddle",
|
|
3120
|
+
error: splitViewportBlockedMessage(childRuns),
|
|
3121
|
+
riddle: splitViewportRiddleMetadata(childRuns, "named-viewport-aggregate"),
|
|
3122
|
+
artifacts
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
const evidence = aggregateSplitViewportEvidence(profile, childRuns);
|
|
3126
|
+
const result = assessRiddleProofProfileEvidence(profile, evidence, {
|
|
3127
|
+
runner: "riddle",
|
|
3128
|
+
riddle: splitViewportRiddleMetadata(childRuns, "named-viewport-aggregate"),
|
|
3129
|
+
artifacts
|
|
3130
|
+
});
|
|
3131
|
+
return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
|
|
3132
|
+
}
|
|
3133
|
+
async function recoverProfileForCli(profile, options) {
|
|
3134
|
+
const runner = optionString(options, "runner") || "riddle";
|
|
3135
|
+
if (runner !== "riddle") {
|
|
3136
|
+
throw new Error(`Unsupported --runner ${runner}. The current CLI supports --runner riddle.`);
|
|
3137
|
+
}
|
|
3138
|
+
const jobId = optionString(options, "job") ?? optionString(options, "jobId");
|
|
3139
|
+
if (!jobId) throw new Error("run-profile recover requires --job <job-id>.");
|
|
3140
|
+
const client = createRiddleApiClient(riddleClientConfig(options));
|
|
3141
|
+
let artifactPayload;
|
|
3142
|
+
try {
|
|
3143
|
+
artifactPayload = await client.requestJson(`/v1/jobs/${jobId}/artifacts`);
|
|
3144
|
+
} catch (error) {
|
|
3145
|
+
return createRiddleProofProfileEnvironmentBlockedResult({
|
|
3146
|
+
profile,
|
|
3147
|
+
runner,
|
|
3148
|
+
error,
|
|
3149
|
+
riddle: { job_id: jobId, terminal: false }
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
const artifacts = collectRiddleProfileArtifactRefs(artifactPayload);
|
|
3153
|
+
const artifactStatus = riddleArtifactsPayloadStatus(artifactPayload);
|
|
3154
|
+
const terminal = artifactStatus ? isTerminalRiddleJobStatus(artifactStatus) : artifacts.length > 0;
|
|
3155
|
+
const recovered = await profileResultFromRiddleArtifacts(profile, artifacts, [artifactPayload]);
|
|
3156
|
+
if (recovered) {
|
|
3157
|
+
return withRiddleMetadata(recovered, {
|
|
3158
|
+
job_id: jobId,
|
|
3159
|
+
status: artifactStatus,
|
|
3160
|
+
terminal,
|
|
3161
|
+
artifacts,
|
|
3162
|
+
artifactRecovery: true
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
return createRiddleProofProfileInsufficientResult({
|
|
3166
|
+
profile,
|
|
3167
|
+
runner,
|
|
3168
|
+
error: artifacts.length ? `Riddle job ${jobId} artifacts were recovered without a proof result.` : `Riddle job ${jobId} had no recoverable artifacts.`,
|
|
3169
|
+
riddle: {
|
|
3170
|
+
job_id: jobId,
|
|
3171
|
+
status: artifactStatus,
|
|
3172
|
+
terminal,
|
|
3173
|
+
artifact_recovery: artifacts.length > 0
|
|
3174
|
+
},
|
|
3175
|
+
artifacts
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
2848
3178
|
async function runProfileForCli(profile, options) {
|
|
2849
3179
|
const runner = optionString(options, "runner") || "riddle";
|
|
2850
3180
|
if (runner !== "riddle") {
|
|
@@ -2854,7 +3184,7 @@ async function runProfileForCli(profile, options) {
|
|
|
2854
3184
|
if (runProfileSplitViewportsOption(options) && profile.target.viewports.length > 1) {
|
|
2855
3185
|
return runSplitViewportProfileForCli(profile, options, { client, runner });
|
|
2856
3186
|
}
|
|
2857
|
-
return runSingleRiddleProfileForCli(profile, options, { client, runner });
|
|
3187
|
+
return runSingleRiddleProfileForCli(profile, options, { client, runner, outputDir: profileOutputDirOption(options) });
|
|
2858
3188
|
}
|
|
2859
3189
|
function requestForRun(options) {
|
|
2860
3190
|
const statePath = optionString(options, "statePath");
|
|
@@ -2903,8 +3233,8 @@ async function main() {
|
|
|
2903
3233
|
return;
|
|
2904
3234
|
}
|
|
2905
3235
|
if (command === "run-profile") {
|
|
2906
|
-
const profile = normalizeProfileForCli(options);
|
|
2907
|
-
const result = await runProfileForCli(profile, options);
|
|
3236
|
+
const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
|
|
3237
|
+
const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : positional[1] === "aggregate" ? await aggregateProfileResultsForCli(profile, options) : await runProfileForCli(profile, options);
|
|
2908
3238
|
writeProfileOutput(profileOutputDirOption(options), result);
|
|
2909
3239
|
const diagnosticLine = profileCliDiagnosticLine(result);
|
|
2910
3240
|
if (diagnosticLine && optionBoolean(options, "quiet") !== true) {
|