@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.
Files changed (4) hide show
  1. package/README.md +42 -0
  2. package/dist/cli.cjs +345 -15
  3. package/dist/cli.js +346 -16
  4. 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
- return hasRecoveredState || success || hasValid && hasInvalid === false;
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: "split-viewports",
19016
+ mode,
18772
19017
  job_count: childRuns.length,
18773
- status: "split-viewports",
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 result2 = await runSingleRiddleProfileForCli(childProfile, options, input);
18999
- if (outputDir) {
19000
- writeProfileOutput(splitViewportOutputDir(outputDir, viewport.name, seenOutputNames), result2);
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
- return hasRecoveredState || success || hasValid && hasInvalid === false;
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: "split-viewports",
2841
+ mode,
2597
2842
  job_count: childRuns.length,
2598
- status: "split-viewports",
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 result2 = await runSingleRiddleProfileForCli(childProfile, options, input);
2824
- if (outputDir) {
2825
- writeProfileOutput(splitViewportOutputDir(outputDir, viewport.name, seenOutputNames), result2);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.7.201",
3
+ "version": "0.7.203",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",