@riddledc/riddle-proof 0.7.202 → 0.7.204

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 CHANGED
@@ -227,6 +227,25 @@ riddle-proof-loop run-profile recover \
227
227
  --output artifacts/riddle-proof/pricing-ipad-mini-recovered
228
228
  ```
229
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. Aggregate summaries label summed hosted timing as
246
+ child poll totals and also show the maximum child latency, so a matrix report
247
+ does not make sequential child wait time look like a single job duration.
248
+
230
249
  When promoting proof artifacts into a durable public profile, avoid guessing
231
250
  which backend or runner tokens are preserved inside `proof.json`. Derive the
232
251
  `body_contains` fragments from the artifact body first:
package/dist/cli.cjs CHANGED
@@ -16224,6 +16224,7 @@ function usage() {
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
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]",
16227
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]",
16228
16229
  " riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
16229
16230
  " riddle-proof-loop profile-http-status-preflight --profile <file|json|-> --url <base-url> [--format json|summary]",
@@ -16703,6 +16704,7 @@ function profileRiddleJobMarkdown(result) {
16703
16704
  const artifactRecovery = riddle.artifact_recovery === true;
16704
16705
  const retryCount = cliFiniteNumber(riddle.retry_count);
16705
16706
  const staleJobIds = Array.isArray(riddle.stale_job_ids) ? riddle.stale_job_ids.map((value) => cliString(value)).filter((value) => Boolean(value)) : [];
16707
+ const splitJobs = Array.isArray(riddle.split_jobs) ? riddle.split_jobs.map(cliRecord).filter((job) => Boolean(job)) : [];
16706
16708
  const parts = [
16707
16709
  mode ? `mode ${markdownInlineCode(mode)}` : "",
16708
16710
  jobCount === void 0 ? "" : `jobs ${jobCount}`,
@@ -16712,9 +16714,18 @@ function profileRiddleJobMarkdown(result) {
16712
16714
  ].filter(Boolean);
16713
16715
  const lines = parts.length ? [`- ${parts.join(", ")}`] : [];
16714
16716
  if (queueElapsedMs !== void 0 || elapsedMs3 !== void 0 || attempt !== void 0 || attempts !== void 0) {
16715
- lines.push(
16716
- `- poll: queue ${formatPollDuration(queueElapsedMs)}, elapsed ${formatPollDuration(elapsedMs3)}${preSubmissionElapsedMs === void 0 || preSubmissionElapsedMs < 1e3 ? "" : `, pre-submit ${formatPollDuration(preSubmissionElapsedMs)}`}${attempt === void 0 ? "" : `, attempt ${attempt}${attempts === void 0 ? "" : `/${attempts}`}`}`
16717
- );
16717
+ if (splitJobs.length) {
16718
+ const maxChildQueueElapsedMs = maxDefinedNumbers(splitJobs.map((job) => cliFiniteNumber(job.queue_elapsed_ms)));
16719
+ const maxChildElapsedMs = maxDefinedNumbers(splitJobs.map((job) => cliFiniteNumber(job.elapsed_ms)));
16720
+ const maxChildPreSubmissionElapsedMs = maxDefinedNumbers(splitJobs.map((job) => cliFiniteNumber(job.pre_submission_elapsed_ms)));
16721
+ lines.push(
16722
+ `- child poll totals: queue ${formatPollDuration(queueElapsedMs)}, elapsed ${formatPollDuration(elapsedMs3)}${preSubmissionElapsedMs === void 0 || preSubmissionElapsedMs < 1e3 ? "" : `, pre-submit ${formatPollDuration(preSubmissionElapsedMs)}`}; max child queue ${formatPollDuration(maxChildQueueElapsedMs)}, max child elapsed ${formatPollDuration(maxChildElapsedMs)}${maxChildPreSubmissionElapsedMs === void 0 || maxChildPreSubmissionElapsedMs < 1e3 ? "" : `, max child pre-submit ${formatPollDuration(maxChildPreSubmissionElapsedMs)}`}`
16723
+ );
16724
+ } else {
16725
+ lines.push(
16726
+ `- poll: queue ${formatPollDuration(queueElapsedMs)}, elapsed ${formatPollDuration(elapsedMs3)}${preSubmissionElapsedMs === void 0 || preSubmissionElapsedMs < 1e3 ? "" : `, pre-submit ${formatPollDuration(preSubmissionElapsedMs)}`}${attempt === void 0 ? "" : `, attempt ${attempt}${attempts === void 0 ? "" : `/${attempts}`}`}`
16727
+ );
16728
+ }
16718
16729
  }
16719
16730
  if (submittedAt || completedAt) {
16720
16731
  lines.push(`- timing:${submittedAt ? ` submitted ${markdownInlineCode(submittedAt)}` : ""}${completedAt ? ` completed ${markdownInlineCode(completedAt)}` : ""}`);
@@ -16725,7 +16736,6 @@ function profileRiddleJobMarkdown(result) {
16725
16736
  if (retryCount !== void 0 && retryCount > 0) {
16726
16737
  lines.push(`- retry recovery: replaced ${retryCount} unsubmitted job${retryCount === 1 ? "" : "s"}${staleJobIds.length ? ` (${staleJobIds.map((value) => markdownInlineCode(value)).join(", ")})` : ""}`);
16727
16738
  }
16728
- const splitJobs = Array.isArray(riddle.split_jobs) ? riddle.split_jobs.map(cliRecord).filter((job) => Boolean(job)) : [];
16729
16739
  for (const job of splitJobs.slice(0, 12)) {
16730
16740
  const viewport = cliString(job.viewport) || "viewport";
16731
16741
  const splitJobId = cliString(job.job_id);
@@ -16750,6 +16760,10 @@ function profileRiddleJobMarkdown(result) {
16750
16760
  if (splitJobs.length > 12) lines.push(`- ${splitJobs.length - 12} additional split job(s) omitted.`);
16751
16761
  return lines;
16752
16762
  }
16763
+ function maxDefinedNumbers(values) {
16764
+ const numbers = values.filter((value) => typeof value === "number" && Number.isFinite(value));
16765
+ return numbers.length ? Math.max(...numbers) : void 0;
16766
+ }
16753
16767
  function profileMetadataStringArray(value) {
16754
16768
  return Array.isArray(value) ? value.map((item) => typeof item === "string" ? item.trim() : "").filter((item) => Boolean(item)) : [];
16755
16769
  }
@@ -16838,32 +16852,64 @@ function profileIsCleanupInventoryReceipt(receipt) {
16838
16852
  const haystack = profileCleanupInventoryHaystack(receipt);
16839
16853
  return haystack.includes("cleanup") || haystack.includes("post-cleanup") || haystack.includes("stale") || haystack.includes("statehygiene") || haystack.includes("state hygiene") || haystack.includes("remained after") || haystack.includes("still present");
16840
16854
  }
16841
- function profileFailedCleanupInventoryReason(setupViewports) {
16842
- const receipts = setupViewports.flatMap((viewport) => [
16855
+ function profileIsCleanupPhaseInventoryReceipt(receipt) {
16856
+ const storedTo = (cliString(receipt.return_stored_to) || "").toLowerCase();
16857
+ const storedSegments = storedTo.split(/[.[\]/]/).filter(Boolean);
16858
+ const storedSegment = storedSegments[storedSegments.length - 1] || storedTo;
16859
+ const state = (cliString(setupReturnSummaryValue(receipt, ["state", "phase"])) || "").toLowerCase();
16860
+ const summary = (cliReturnSummaryLabel(receipt.return_summary) || "").toLowerCase();
16861
+ const markers = [storedSegment, state, summary];
16862
+ return markers.some((marker) => marker === "cleanup" || marker === "postcleanup" || marker === "post-cleanup" || marker === "aftercleanup" || marker === "after-cleanup" || marker.includes("post-cleanup") || marker.includes("after-cleanup") || marker.includes("after-clear") || marker.includes("after-reset") || marker.includes("after-undo") || marker.includes("after-discard") || marker.includes("after-new"));
16863
+ }
16864
+ function profileCleanupPhaseInventoryReceipts(setupViewports) {
16865
+ return setupViewports.flatMap((viewport) => [
16843
16866
  ...setupReceiptArray(viewport, "window_eval"),
16844
16867
  ...setupReceiptArray(viewport, "window_call")
16845
- ]);
16846
- for (const receipt of receipts) {
16847
- if (receipt.ok !== false) continue;
16848
- if (!profileIsCleanupInventoryReceipt(receipt)) continue;
16868
+ ]).filter((receipt) => profileIsCleanupInventoryReceipt(receipt) && profileIsCleanupPhaseInventoryReceipt(receipt));
16869
+ }
16870
+ function profileFailedCleanupInventoryReceiptReason(receipt) {
16871
+ if (receipt.ok === false) {
16849
16872
  const error = cliString(receipt.error);
16850
16873
  const reason = cliString(receipt.reason);
16851
16874
  return compactProfileReceiptReason(error) || compactProfileReceiptReason(reason) || "cleanup inventory failed";
16852
16875
  }
16876
+ const parts = [];
16877
+ if (setupReturnedSummaryValue(receipt, ["ok"]) === false) parts.push("ok=false");
16878
+ if (setupReturnedSummaryValue(receipt, ["success"]) === false) parts.push("success=false");
16879
+ const staleCount = cliFiniteNumber(setupReturnSummaryValue(receipt, ["staleCount"]));
16880
+ if (staleCount !== void 0 && staleCount > 0) parts.push(`staleCount=${staleCount}`);
16881
+ const staleNames = setupReturnSummaryValue(receipt, ["staleNames"]);
16882
+ if (Array.isArray(staleNames) && staleNames.length > 0) {
16883
+ const staleNamesLabel = cliValueLabel(staleNames);
16884
+ if (staleNamesLabel) parts.push(`staleNames=${compactProfileReceiptReason(staleNamesLabel, 120) ?? staleNamesLabel}`);
16885
+ }
16886
+ const productIssue = setupReturnedSummaryValue(receipt, ["productIssue", "issue"]);
16887
+ const productIssueLabel = typeof productIssue === "string" ? compactProfileReceiptReason(productIssue, 120) : void 0;
16888
+ if (parts.length && productIssueLabel) parts.push(productIssueLabel);
16889
+ return parts.length ? parts.join(", ") : void 0;
16890
+ }
16891
+ function profileFailedCleanupInventoryReason(setupViewports) {
16892
+ const receipts = profileCleanupPhaseInventoryReceipts(setupViewports);
16893
+ for (const receipt of [...receipts].reverse()) {
16894
+ const reason = profileFailedCleanupInventoryReceiptReason(receipt);
16895
+ if (reason) return reason;
16896
+ }
16853
16897
  return void 0;
16854
16898
  }
16899
+ function profilePassedCleanupInventoryReceiptReason(receipt) {
16900
+ if (receipt.ok === false) return void 0;
16901
+ if (setupReturnedSummaryValue(receipt, ["ok"]) === false) return void 0;
16902
+ if (setupReturnedSummaryValue(receipt, ["success"]) === false) return void 0;
16903
+ const staleCount = cliFiniteNumber(setupReturnSummaryValue(receipt, ["staleCount"]));
16904
+ const staleNames = setupReturnSummaryValue(receipt, ["staleNames"]);
16905
+ if (staleCount !== 0 || !Array.isArray(staleNames) || staleNames.length !== 0) return void 0;
16906
+ return "staleCount=0, staleNames=[]";
16907
+ }
16855
16908
  function profilePassedCleanupInventoryReason(setupViewports) {
16856
- const receipts = setupViewports.flatMap((viewport) => [
16857
- ...setupReceiptArray(viewport, "window_eval"),
16858
- ...setupReceiptArray(viewport, "window_call")
16859
- ]);
16860
- for (const receipt of receipts) {
16861
- if (receipt.ok === false || !profileIsCleanupInventoryReceipt(receipt)) continue;
16862
- if (setupReturnSummaryValue(receipt, ["ok"]) === false) continue;
16863
- const staleCount = cliFiniteNumber(setupReturnSummaryValue(receipt, ["staleCount"]));
16864
- const staleNames = setupReturnSummaryValue(receipt, ["staleNames"]);
16865
- if (staleCount !== 0 || !Array.isArray(staleNames) || staleNames.length !== 0) continue;
16866
- return "staleCount=0, staleNames=[]";
16909
+ const receipts = profileCleanupPhaseInventoryReceipts(setupViewports);
16910
+ for (const receipt of [...receipts].reverse()) {
16911
+ const reason = profilePassedCleanupInventoryReceiptReason(receipt);
16912
+ if (reason) return reason;
16867
16913
  }
16868
16914
  return void 0;
16869
16915
  }
@@ -17045,15 +17091,17 @@ function profileHasRecoveredStateReceipt(receipts) {
17045
17091
  const path7 = cliString(receipt.path) || cliString(receipt.function_name) || "";
17046
17092
  const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
17047
17093
  const haystack = `${storedTo} ${label} ${path7} ${summary}`.toLowerCase();
17048
- const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || 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");
17094
+ 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");
17049
17095
  if (!labelsRecovery) return false;
17050
17096
  const status = profileLowerSummaryValue(receipt, ["status", "state", "phase"]);
17051
- const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result"]);
17052
- const hasRecoveredState = ["valid", "success", "recovered", "fixed", "ready"].includes(status) || ["valid", "success", "recovered", "fixed", "ready"].includes(outcome);
17097
+ const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result", "retryOutcome", "retry_outcome"]);
17098
+ const hasRecoveredState = ["valid", "success", "recovered", "fixed", "ready"].includes(status) || ["valid", "success", "recovered", "fixed", "ready", "running_after_retry", "ready_after_retry"].includes(outcome);
17053
17099
  const hasValid = setupReturnSummaryValue(receipt, ["hasValid", "valid", "isValid"]) === true;
17054
17100
  const hasInvalid = setupReturnSummaryValue(receipt, ["hasInvalid", "invalid", "isInvalid"]);
17055
17101
  const success = setupReturnSummaryValue(receipt, ["success", "recovered", "fixed"]) === true;
17056
- return hasRecoveredState || success || hasValid && hasInvalid === false;
17102
+ const leftTerminalState = setupReturnSummaryValue(receipt, ["leftTerminalState", "left_terminal_state"]) === true;
17103
+ const retrySurfaceReady = setupReturnSummaryValue(receipt, ["retrySurfaceReady", "retry_surface_ready"]) === true;
17104
+ return hasRecoveredState || success || hasValid && hasInvalid === false || leftTerminalState && retrySurfaceReady;
17057
17105
  });
17058
17106
  }
17059
17107
  function profileMetadataHasGeneratedOutputContract(metadata) {
@@ -17606,6 +17654,9 @@ function setupReturnSummaryValue(receipt, names) {
17606
17654
  for (const name of names) {
17607
17655
  if (receipt[name] !== void 0) return receipt[name];
17608
17656
  }
17657
+ return setupReturnedSummaryValue(receipt, names);
17658
+ }
17659
+ function setupReturnedSummaryValue(receipt, names) {
17609
17660
  const returned = cliRecord(receipt.returned);
17610
17661
  for (const name of names) {
17611
17662
  if (returned?.[name] !== void 0) return returned[name];
@@ -18911,6 +18962,78 @@ function splitViewportOutputDir(outputDir, viewportName, seen) {
18911
18962
  seen.set(base, count + 1);
18912
18963
  return import_node_path6.default.join(outputDir, count ? `${base}-${count + 1}` : base);
18913
18964
  }
18965
+ function profileResultPathFromInput(inputPath) {
18966
+ if (!(0, import_node_fs6.existsSync)(inputPath)) throw new Error(`Profile aggregate input path does not exist: ${inputPath}`);
18967
+ const stat = (0, import_node_fs6.statSync)(inputPath);
18968
+ if (stat.isFile()) return [inputPath];
18969
+ if (!stat.isDirectory()) throw new Error(`Profile aggregate input path must be a file or directory: ${inputPath}`);
18970
+ 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));
18971
+ if (childProfileResults.length) return childProfileResults;
18972
+ const directProfileResult = import_node_path6.default.join(inputPath, "profile-result.json");
18973
+ if ((0, import_node_fs6.existsSync)(directProfileResult)) return [directProfileResult];
18974
+ throw new Error(`Profile aggregate input directory has no child profile-result.json files: ${inputPath}`);
18975
+ }
18976
+ function runProfileAggregateInputPathsOption(options) {
18977
+ const rawInputs = [
18978
+ optionString(options, "input"),
18979
+ optionString(options, "inputs"),
18980
+ optionString(options, "inputFile"),
18981
+ optionString(options, "inputFiles")
18982
+ ].filter((value) => Boolean(value));
18983
+ const explicitInputs = rawInputs.flatMap((raw) => raw.split(",").map((part) => part.trim()).filter(Boolean));
18984
+ const inputDir = optionString(options, "inputDir") ?? optionString(options, "resultsDir") ?? optionString(options, "runDir");
18985
+ const discoveredInputs = inputDir ? profileResultPathFromInput(inputDir) : [];
18986
+ const paths = [...explicitInputs.flatMap(profileResultPathFromInput), ...discoveredInputs];
18987
+ const uniquePaths = [...new Set(paths.map((inputPath) => import_node_path6.default.resolve(inputPath)))];
18988
+ if (!uniquePaths.length) {
18989
+ throw new Error("run-profile aggregate requires --input-dir <dir> or --inputs <path[,path...]>.");
18990
+ }
18991
+ return uniquePaths;
18992
+ }
18993
+ function readProfileResultForAggregate(resultPath) {
18994
+ const parsed = readJsonValue(resultPath, resultPath);
18995
+ if (parsed.version !== "riddle-proof.profile-result.v1" || !Array.isArray(parsed.checks)) {
18996
+ throw new Error(`Profile aggregate input is not a riddle-proof.profile-result.v1 result: ${resultPath}`);
18997
+ }
18998
+ return parsed;
18999
+ }
19000
+ function profileResultIsAggregateParent(result) {
19001
+ const mode = cliString(cliRecord(result.riddle)?.mode);
19002
+ return (mode === "split-viewports" || mode === "named-viewport-aggregate") && (result.evidence?.viewports?.length || 0) > 1;
19003
+ }
19004
+ function aggregateProfileResultViewportName(result) {
19005
+ const evidenceViewports = result.evidence?.viewports || [];
19006
+ if (evidenceViewports.length === 1) return evidenceViewports[0].name;
19007
+ const metadata = cliRecord(result.metadata);
19008
+ const splitViewport = cliString(metadata?.split_viewport);
19009
+ if (splitViewport) return splitViewport;
19010
+ const selectedViewports = Array.isArray(metadata?.selected_viewports) ? metadata.selected_viewports.map(cliString).filter((name) => Boolean(name)) : [];
19011
+ if (selectedViewports.length === 1) return selectedViewports[0];
19012
+ return void 0;
19013
+ }
19014
+ function aggregateProfileResultViewport(profile, result, resultPath) {
19015
+ const viewportName = aggregateProfileResultViewportName(result);
19016
+ const parentViewport = viewportName ? profile.target.viewports.find((viewport) => viewport.name === viewportName) : void 0;
19017
+ if (parentViewport) return parentViewport;
19018
+ const evidenceViewport = result.evidence?.viewports?.length === 1 ? result.evidence.viewports[0] : void 0;
19019
+ if (evidenceViewport && typeof evidenceViewport.name === "string" && typeof evidenceViewport.width === "number" && Number.isFinite(evidenceViewport.width) && typeof evidenceViewport.height === "number" && Number.isFinite(evidenceViewport.height)) {
19020
+ return {
19021
+ name: evidenceViewport.name,
19022
+ width: evidenceViewport.width,
19023
+ height: evidenceViewport.height
19024
+ };
19025
+ }
19026
+ throw new Error(`Profile aggregate input must be a single named viewport result or include selected viewport metadata: ${resultPath}`);
19027
+ }
19028
+ function sortAggregateChildRuns(profile, childRuns) {
19029
+ const viewportOrder = new Map(profile.target.viewports.map((viewport, index) => [viewport.name, index]));
19030
+ return [...childRuns].sort((a, b) => {
19031
+ const aIndex = viewportOrder.get(a.viewport.name) ?? Number.MAX_SAFE_INTEGER;
19032
+ const bIndex = viewportOrder.get(b.viewport.name) ?? Number.MAX_SAFE_INTEGER;
19033
+ if (aIndex !== bIndex) return aIndex - bIndex;
19034
+ return a.viewport.name.localeCompare(b.viewport.name);
19035
+ });
19036
+ }
18914
19037
  function splitViewportArtifactRefs(input) {
18915
19038
  return (input.result.artifacts.riddle_artifacts || []).map((artifact) => ({
18916
19039
  ...artifact,
@@ -18921,7 +19044,7 @@ function sumDefinedNumbers(values) {
18921
19044
  const numbers = values.filter((value) => typeof value === "number" && Number.isFinite(value));
18922
19045
  return numbers.length ? numbers.reduce((sum, value) => sum + value, 0) : void 0;
18923
19046
  }
18924
- function splitViewportRiddleMetadata(childRuns) {
19047
+ function splitViewportRiddleMetadata(childRuns, mode = "split-viewports") {
18925
19048
  const splitJobs = childRuns.map(({ viewport, result }) => ({
18926
19049
  viewport: viewport.name,
18927
19050
  job_id: result.riddle?.job_id,
@@ -18938,9 +19061,9 @@ function splitViewportRiddleMetadata(childRuns) {
18938
19061
  artifact_recovery: result.riddle?.artifact_recovery
18939
19062
  }));
18940
19063
  return {
18941
- mode: "split-viewports",
19064
+ mode,
18942
19065
  job_count: childRuns.length,
18943
- status: "split-viewports",
19066
+ status: mode,
18944
19067
  terminal: childRuns.every(({ result }) => result.riddle?.terminal !== false),
18945
19068
  artifact_recovery: childRuns.some(({ result }) => result.riddle?.artifact_recovery === true),
18946
19069
  queue_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.queue_elapsed_ms)),
@@ -19196,6 +19319,40 @@ async function runSplitViewportProfileForCli(profile, options, input) {
19196
19319
  });
19197
19320
  return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
19198
19321
  }
19322
+ async function aggregateProfileResultsForCli(profile, options) {
19323
+ const resultPaths = runProfileAggregateInputPathsOption(options);
19324
+ const seenViewports = /* @__PURE__ */ new Set();
19325
+ const childInputs = resultPaths.map((resultPath) => ({ resultPath, result: readProfileResultForAggregate(resultPath) })).filter(({ result: result2 }) => !profileResultIsAggregateParent(result2));
19326
+ if (!childInputs.length) {
19327
+ throw new Error("run-profile aggregate found no single-viewport child profile results.");
19328
+ }
19329
+ const childRuns = sortAggregateChildRuns(profile, childInputs.map(({ resultPath, result: result2 }) => {
19330
+ const viewport = aggregateProfileResultViewport(profile, result2, resultPath);
19331
+ if (seenViewports.has(viewport.name)) {
19332
+ throw new Error(`Profile aggregate received more than one result for viewport ${viewport.name}.`);
19333
+ }
19334
+ seenViewports.add(viewport.name);
19335
+ return { viewport, profile: profileForSplitViewport(profile, viewport), result: result2 };
19336
+ }));
19337
+ const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
19338
+ const blocked = childRuns.filter(({ result: result2 }) => !result2.evidence || result2.status === "environment_blocked" || result2.status === "configuration_error");
19339
+ if (blocked.length) {
19340
+ return createRiddleProofProfileEnvironmentBlockedResult({
19341
+ profile,
19342
+ runner: "riddle",
19343
+ error: splitViewportBlockedMessage(childRuns),
19344
+ riddle: splitViewportRiddleMetadata(childRuns, "named-viewport-aggregate"),
19345
+ artifacts
19346
+ });
19347
+ }
19348
+ const evidence = aggregateSplitViewportEvidence(profile, childRuns);
19349
+ const result = assessRiddleProofProfileEvidence(profile, evidence, {
19350
+ runner: "riddle",
19351
+ riddle: splitViewportRiddleMetadata(childRuns, "named-viewport-aggregate"),
19352
+ artifacts
19353
+ });
19354
+ return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
19355
+ }
19199
19356
  async function recoverProfileForCli(profile, options) {
19200
19357
  const runner = optionString(options, "runner") || "riddle";
19201
19358
  if (runner !== "riddle") {
@@ -19300,7 +19457,7 @@ async function main() {
19300
19457
  }
19301
19458
  if (command === "run-profile") {
19302
19459
  const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
19303
- const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : await runProfileForCli(profile, options);
19460
+ const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : positional[1] === "aggregate" ? await aggregateProfileResultsForCli(profile, options) : await runProfileForCli(profile, options);
19304
19461
  writeProfileOutput(profileOutputDirOption(options), result);
19305
19462
  const diagnosticLine = profileCliDiagnosticLine(result);
19306
19463
  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 [
@@ -49,6 +49,7 @@ function usage() {
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
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]",
52
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]",
53
54
  " riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
54
55
  " riddle-proof-loop profile-http-status-preflight --profile <file|json|-> --url <base-url> [--format json|summary]",
@@ -528,6 +529,7 @@ function profileRiddleJobMarkdown(result) {
528
529
  const artifactRecovery = riddle.artifact_recovery === true;
529
530
  const retryCount = cliFiniteNumber(riddle.retry_count);
530
531
  const staleJobIds = Array.isArray(riddle.stale_job_ids) ? riddle.stale_job_ids.map((value) => cliString(value)).filter((value) => Boolean(value)) : [];
532
+ const splitJobs = Array.isArray(riddle.split_jobs) ? riddle.split_jobs.map(cliRecord).filter((job) => Boolean(job)) : [];
531
533
  const parts = [
532
534
  mode ? `mode ${markdownInlineCode(mode)}` : "",
533
535
  jobCount === void 0 ? "" : `jobs ${jobCount}`,
@@ -537,9 +539,18 @@ function profileRiddleJobMarkdown(result) {
537
539
  ].filter(Boolean);
538
540
  const lines = parts.length ? [`- ${parts.join(", ")}`] : [];
539
541
  if (queueElapsedMs !== void 0 || elapsedMs !== void 0 || attempt !== void 0 || attempts !== void 0) {
540
- lines.push(
541
- `- poll: queue ${formatPollDuration(queueElapsedMs)}, elapsed ${formatPollDuration(elapsedMs)}${preSubmissionElapsedMs === void 0 || preSubmissionElapsedMs < 1e3 ? "" : `, pre-submit ${formatPollDuration(preSubmissionElapsedMs)}`}${attempt === void 0 ? "" : `, attempt ${attempt}${attempts === void 0 ? "" : `/${attempts}`}`}`
542
- );
542
+ if (splitJobs.length) {
543
+ const maxChildQueueElapsedMs = maxDefinedNumbers(splitJobs.map((job) => cliFiniteNumber(job.queue_elapsed_ms)));
544
+ const maxChildElapsedMs = maxDefinedNumbers(splitJobs.map((job) => cliFiniteNumber(job.elapsed_ms)));
545
+ const maxChildPreSubmissionElapsedMs = maxDefinedNumbers(splitJobs.map((job) => cliFiniteNumber(job.pre_submission_elapsed_ms)));
546
+ lines.push(
547
+ `- child poll totals: queue ${formatPollDuration(queueElapsedMs)}, elapsed ${formatPollDuration(elapsedMs)}${preSubmissionElapsedMs === void 0 || preSubmissionElapsedMs < 1e3 ? "" : `, pre-submit ${formatPollDuration(preSubmissionElapsedMs)}`}; max child queue ${formatPollDuration(maxChildQueueElapsedMs)}, max child elapsed ${formatPollDuration(maxChildElapsedMs)}${maxChildPreSubmissionElapsedMs === void 0 || maxChildPreSubmissionElapsedMs < 1e3 ? "" : `, max child pre-submit ${formatPollDuration(maxChildPreSubmissionElapsedMs)}`}`
548
+ );
549
+ } else {
550
+ lines.push(
551
+ `- poll: queue ${formatPollDuration(queueElapsedMs)}, elapsed ${formatPollDuration(elapsedMs)}${preSubmissionElapsedMs === void 0 || preSubmissionElapsedMs < 1e3 ? "" : `, pre-submit ${formatPollDuration(preSubmissionElapsedMs)}`}${attempt === void 0 ? "" : `, attempt ${attempt}${attempts === void 0 ? "" : `/${attempts}`}`}`
552
+ );
553
+ }
543
554
  }
544
555
  if (submittedAt || completedAt) {
545
556
  lines.push(`- timing:${submittedAt ? ` submitted ${markdownInlineCode(submittedAt)}` : ""}${completedAt ? ` completed ${markdownInlineCode(completedAt)}` : ""}`);
@@ -550,7 +561,6 @@ function profileRiddleJobMarkdown(result) {
550
561
  if (retryCount !== void 0 && retryCount > 0) {
551
562
  lines.push(`- retry recovery: replaced ${retryCount} unsubmitted job${retryCount === 1 ? "" : "s"}${staleJobIds.length ? ` (${staleJobIds.map((value) => markdownInlineCode(value)).join(", ")})` : ""}`);
552
563
  }
553
- const splitJobs = Array.isArray(riddle.split_jobs) ? riddle.split_jobs.map(cliRecord).filter((job) => Boolean(job)) : [];
554
564
  for (const job of splitJobs.slice(0, 12)) {
555
565
  const viewport = cliString(job.viewport) || "viewport";
556
566
  const splitJobId = cliString(job.job_id);
@@ -575,6 +585,10 @@ function profileRiddleJobMarkdown(result) {
575
585
  if (splitJobs.length > 12) lines.push(`- ${splitJobs.length - 12} additional split job(s) omitted.`);
576
586
  return lines;
577
587
  }
588
+ function maxDefinedNumbers(values) {
589
+ const numbers = values.filter((value) => typeof value === "number" && Number.isFinite(value));
590
+ return numbers.length ? Math.max(...numbers) : void 0;
591
+ }
578
592
  function profileMetadataStringArray(value) {
579
593
  return Array.isArray(value) ? value.map((item) => typeof item === "string" ? item.trim() : "").filter((item) => Boolean(item)) : [];
580
594
  }
@@ -663,32 +677,64 @@ function profileIsCleanupInventoryReceipt(receipt) {
663
677
  const haystack = profileCleanupInventoryHaystack(receipt);
664
678
  return haystack.includes("cleanup") || haystack.includes("post-cleanup") || haystack.includes("stale") || haystack.includes("statehygiene") || haystack.includes("state hygiene") || haystack.includes("remained after") || haystack.includes("still present");
665
679
  }
666
- function profileFailedCleanupInventoryReason(setupViewports) {
667
- const receipts = setupViewports.flatMap((viewport) => [
680
+ function profileIsCleanupPhaseInventoryReceipt(receipt) {
681
+ const storedTo = (cliString(receipt.return_stored_to) || "").toLowerCase();
682
+ const storedSegments = storedTo.split(/[.[\]/]/).filter(Boolean);
683
+ const storedSegment = storedSegments[storedSegments.length - 1] || storedTo;
684
+ const state = (cliString(setupReturnSummaryValue(receipt, ["state", "phase"])) || "").toLowerCase();
685
+ const summary = (cliReturnSummaryLabel(receipt.return_summary) || "").toLowerCase();
686
+ const markers = [storedSegment, state, summary];
687
+ return markers.some((marker) => marker === "cleanup" || marker === "postcleanup" || marker === "post-cleanup" || marker === "aftercleanup" || marker === "after-cleanup" || marker.includes("post-cleanup") || marker.includes("after-cleanup") || marker.includes("after-clear") || marker.includes("after-reset") || marker.includes("after-undo") || marker.includes("after-discard") || marker.includes("after-new"));
688
+ }
689
+ function profileCleanupPhaseInventoryReceipts(setupViewports) {
690
+ return setupViewports.flatMap((viewport) => [
668
691
  ...setupReceiptArray(viewport, "window_eval"),
669
692
  ...setupReceiptArray(viewport, "window_call")
670
- ]);
671
- for (const receipt of receipts) {
672
- if (receipt.ok !== false) continue;
673
- if (!profileIsCleanupInventoryReceipt(receipt)) continue;
693
+ ]).filter((receipt) => profileIsCleanupInventoryReceipt(receipt) && profileIsCleanupPhaseInventoryReceipt(receipt));
694
+ }
695
+ function profileFailedCleanupInventoryReceiptReason(receipt) {
696
+ if (receipt.ok === false) {
674
697
  const error = cliString(receipt.error);
675
698
  const reason = cliString(receipt.reason);
676
699
  return compactProfileReceiptReason(error) || compactProfileReceiptReason(reason) || "cleanup inventory failed";
677
700
  }
701
+ const parts = [];
702
+ if (setupReturnedSummaryValue(receipt, ["ok"]) === false) parts.push("ok=false");
703
+ if (setupReturnedSummaryValue(receipt, ["success"]) === false) parts.push("success=false");
704
+ const staleCount = cliFiniteNumber(setupReturnSummaryValue(receipt, ["staleCount"]));
705
+ if (staleCount !== void 0 && staleCount > 0) parts.push(`staleCount=${staleCount}`);
706
+ const staleNames = setupReturnSummaryValue(receipt, ["staleNames"]);
707
+ if (Array.isArray(staleNames) && staleNames.length > 0) {
708
+ const staleNamesLabel = cliValueLabel(staleNames);
709
+ if (staleNamesLabel) parts.push(`staleNames=${compactProfileReceiptReason(staleNamesLabel, 120) ?? staleNamesLabel}`);
710
+ }
711
+ const productIssue = setupReturnedSummaryValue(receipt, ["productIssue", "issue"]);
712
+ const productIssueLabel = typeof productIssue === "string" ? compactProfileReceiptReason(productIssue, 120) : void 0;
713
+ if (parts.length && productIssueLabel) parts.push(productIssueLabel);
714
+ return parts.length ? parts.join(", ") : void 0;
715
+ }
716
+ function profileFailedCleanupInventoryReason(setupViewports) {
717
+ const receipts = profileCleanupPhaseInventoryReceipts(setupViewports);
718
+ for (const receipt of [...receipts].reverse()) {
719
+ const reason = profileFailedCleanupInventoryReceiptReason(receipt);
720
+ if (reason) return reason;
721
+ }
678
722
  return void 0;
679
723
  }
724
+ function profilePassedCleanupInventoryReceiptReason(receipt) {
725
+ if (receipt.ok === false) return void 0;
726
+ if (setupReturnedSummaryValue(receipt, ["ok"]) === false) return void 0;
727
+ if (setupReturnedSummaryValue(receipt, ["success"]) === false) return void 0;
728
+ const staleCount = cliFiniteNumber(setupReturnSummaryValue(receipt, ["staleCount"]));
729
+ const staleNames = setupReturnSummaryValue(receipt, ["staleNames"]);
730
+ if (staleCount !== 0 || !Array.isArray(staleNames) || staleNames.length !== 0) return void 0;
731
+ return "staleCount=0, staleNames=[]";
732
+ }
680
733
  function profilePassedCleanupInventoryReason(setupViewports) {
681
- const receipts = setupViewports.flatMap((viewport) => [
682
- ...setupReceiptArray(viewport, "window_eval"),
683
- ...setupReceiptArray(viewport, "window_call")
684
- ]);
685
- for (const receipt of receipts) {
686
- if (receipt.ok === false || !profileIsCleanupInventoryReceipt(receipt)) continue;
687
- if (setupReturnSummaryValue(receipt, ["ok"]) === false) continue;
688
- const staleCount = cliFiniteNumber(setupReturnSummaryValue(receipt, ["staleCount"]));
689
- const staleNames = setupReturnSummaryValue(receipt, ["staleNames"]);
690
- if (staleCount !== 0 || !Array.isArray(staleNames) || staleNames.length !== 0) continue;
691
- return "staleCount=0, staleNames=[]";
734
+ const receipts = profileCleanupPhaseInventoryReceipts(setupViewports);
735
+ for (const receipt of [...receipts].reverse()) {
736
+ const reason = profilePassedCleanupInventoryReceiptReason(receipt);
737
+ if (reason) return reason;
692
738
  }
693
739
  return void 0;
694
740
  }
@@ -870,15 +916,17 @@ function profileHasRecoveredStateReceipt(receipts) {
870
916
  const path2 = cliString(receipt.path) || cliString(receipt.function_name) || "";
871
917
  const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
872
918
  const haystack = `${storedTo} ${label} ${path2} ${summary}`.toLowerCase();
873
- const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || 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");
919
+ 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");
874
920
  if (!labelsRecovery) return false;
875
921
  const status = profileLowerSummaryValue(receipt, ["status", "state", "phase"]);
876
- const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result"]);
877
- const hasRecoveredState = ["valid", "success", "recovered", "fixed", "ready"].includes(status) || ["valid", "success", "recovered", "fixed", "ready"].includes(outcome);
922
+ const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result", "retryOutcome", "retry_outcome"]);
923
+ const hasRecoveredState = ["valid", "success", "recovered", "fixed", "ready"].includes(status) || ["valid", "success", "recovered", "fixed", "ready", "running_after_retry", "ready_after_retry"].includes(outcome);
878
924
  const hasValid = setupReturnSummaryValue(receipt, ["hasValid", "valid", "isValid"]) === true;
879
925
  const hasInvalid = setupReturnSummaryValue(receipt, ["hasInvalid", "invalid", "isInvalid"]);
880
926
  const success = setupReturnSummaryValue(receipt, ["success", "recovered", "fixed"]) === true;
881
- return hasRecoveredState || success || hasValid && hasInvalid === false;
927
+ const leftTerminalState = setupReturnSummaryValue(receipt, ["leftTerminalState", "left_terminal_state"]) === true;
928
+ const retrySurfaceReady = setupReturnSummaryValue(receipt, ["retrySurfaceReady", "retry_surface_ready"]) === true;
929
+ return hasRecoveredState || success || hasValid && hasInvalid === false || leftTerminalState && retrySurfaceReady;
882
930
  });
883
931
  }
884
932
  function profileMetadataHasGeneratedOutputContract(metadata) {
@@ -1431,6 +1479,9 @@ function setupReturnSummaryValue(receipt, names) {
1431
1479
  for (const name of names) {
1432
1480
  if (receipt[name] !== void 0) return receipt[name];
1433
1481
  }
1482
+ return setupReturnedSummaryValue(receipt, names);
1483
+ }
1484
+ function setupReturnedSummaryValue(receipt, names) {
1434
1485
  const returned = cliRecord(receipt.returned);
1435
1486
  for (const name of names) {
1436
1487
  if (returned?.[name] !== void 0) return returned[name];
@@ -2736,6 +2787,78 @@ function splitViewportOutputDir(outputDir, viewportName, seen) {
2736
2787
  seen.set(base, count + 1);
2737
2788
  return path.join(outputDir, count ? `${base}-${count + 1}` : base);
2738
2789
  }
2790
+ function profileResultPathFromInput(inputPath) {
2791
+ if (!existsSync(inputPath)) throw new Error(`Profile aggregate input path does not exist: ${inputPath}`);
2792
+ const stat = statSync(inputPath);
2793
+ if (stat.isFile()) return [inputPath];
2794
+ if (!stat.isDirectory()) throw new Error(`Profile aggregate input path must be a file or directory: ${inputPath}`);
2795
+ const childProfileResults = readdirSync(inputPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(inputPath, entry.name, "profile-result.json")).filter((candidate) => existsSync(candidate));
2796
+ if (childProfileResults.length) return childProfileResults;
2797
+ const directProfileResult = path.join(inputPath, "profile-result.json");
2798
+ if (existsSync(directProfileResult)) return [directProfileResult];
2799
+ throw new Error(`Profile aggregate input directory has no child profile-result.json files: ${inputPath}`);
2800
+ }
2801
+ function runProfileAggregateInputPathsOption(options) {
2802
+ const rawInputs = [
2803
+ optionString(options, "input"),
2804
+ optionString(options, "inputs"),
2805
+ optionString(options, "inputFile"),
2806
+ optionString(options, "inputFiles")
2807
+ ].filter((value) => Boolean(value));
2808
+ const explicitInputs = rawInputs.flatMap((raw) => raw.split(",").map((part) => part.trim()).filter(Boolean));
2809
+ const inputDir = optionString(options, "inputDir") ?? optionString(options, "resultsDir") ?? optionString(options, "runDir");
2810
+ const discoveredInputs = inputDir ? profileResultPathFromInput(inputDir) : [];
2811
+ const paths = [...explicitInputs.flatMap(profileResultPathFromInput), ...discoveredInputs];
2812
+ const uniquePaths = [...new Set(paths.map((inputPath) => path.resolve(inputPath)))];
2813
+ if (!uniquePaths.length) {
2814
+ throw new Error("run-profile aggregate requires --input-dir <dir> or --inputs <path[,path...]>.");
2815
+ }
2816
+ return uniquePaths;
2817
+ }
2818
+ function readProfileResultForAggregate(resultPath) {
2819
+ const parsed = readJsonValue(resultPath, resultPath);
2820
+ if (parsed.version !== "riddle-proof.profile-result.v1" || !Array.isArray(parsed.checks)) {
2821
+ throw new Error(`Profile aggregate input is not a riddle-proof.profile-result.v1 result: ${resultPath}`);
2822
+ }
2823
+ return parsed;
2824
+ }
2825
+ function profileResultIsAggregateParent(result) {
2826
+ const mode = cliString(cliRecord(result.riddle)?.mode);
2827
+ return (mode === "split-viewports" || mode === "named-viewport-aggregate") && (result.evidence?.viewports?.length || 0) > 1;
2828
+ }
2829
+ function aggregateProfileResultViewportName(result) {
2830
+ const evidenceViewports = result.evidence?.viewports || [];
2831
+ if (evidenceViewports.length === 1) return evidenceViewports[0].name;
2832
+ const metadata = cliRecord(result.metadata);
2833
+ const splitViewport = cliString(metadata?.split_viewport);
2834
+ if (splitViewport) return splitViewport;
2835
+ const selectedViewports = Array.isArray(metadata?.selected_viewports) ? metadata.selected_viewports.map(cliString).filter((name) => Boolean(name)) : [];
2836
+ if (selectedViewports.length === 1) return selectedViewports[0];
2837
+ return void 0;
2838
+ }
2839
+ function aggregateProfileResultViewport(profile, result, resultPath) {
2840
+ const viewportName = aggregateProfileResultViewportName(result);
2841
+ const parentViewport = viewportName ? profile.target.viewports.find((viewport) => viewport.name === viewportName) : void 0;
2842
+ if (parentViewport) return parentViewport;
2843
+ const evidenceViewport = result.evidence?.viewports?.length === 1 ? result.evidence.viewports[0] : void 0;
2844
+ if (evidenceViewport && typeof evidenceViewport.name === "string" && typeof evidenceViewport.width === "number" && Number.isFinite(evidenceViewport.width) && typeof evidenceViewport.height === "number" && Number.isFinite(evidenceViewport.height)) {
2845
+ return {
2846
+ name: evidenceViewport.name,
2847
+ width: evidenceViewport.width,
2848
+ height: evidenceViewport.height
2849
+ };
2850
+ }
2851
+ throw new Error(`Profile aggregate input must be a single named viewport result or include selected viewport metadata: ${resultPath}`);
2852
+ }
2853
+ function sortAggregateChildRuns(profile, childRuns) {
2854
+ const viewportOrder = new Map(profile.target.viewports.map((viewport, index) => [viewport.name, index]));
2855
+ return [...childRuns].sort((a, b) => {
2856
+ const aIndex = viewportOrder.get(a.viewport.name) ?? Number.MAX_SAFE_INTEGER;
2857
+ const bIndex = viewportOrder.get(b.viewport.name) ?? Number.MAX_SAFE_INTEGER;
2858
+ if (aIndex !== bIndex) return aIndex - bIndex;
2859
+ return a.viewport.name.localeCompare(b.viewport.name);
2860
+ });
2861
+ }
2739
2862
  function splitViewportArtifactRefs(input) {
2740
2863
  return (input.result.artifacts.riddle_artifacts || []).map((artifact) => ({
2741
2864
  ...artifact,
@@ -2746,7 +2869,7 @@ function sumDefinedNumbers(values) {
2746
2869
  const numbers = values.filter((value) => typeof value === "number" && Number.isFinite(value));
2747
2870
  return numbers.length ? numbers.reduce((sum, value) => sum + value, 0) : void 0;
2748
2871
  }
2749
- function splitViewportRiddleMetadata(childRuns) {
2872
+ function splitViewportRiddleMetadata(childRuns, mode = "split-viewports") {
2750
2873
  const splitJobs = childRuns.map(({ viewport, result }) => ({
2751
2874
  viewport: viewport.name,
2752
2875
  job_id: result.riddle?.job_id,
@@ -2763,9 +2886,9 @@ function splitViewportRiddleMetadata(childRuns) {
2763
2886
  artifact_recovery: result.riddle?.artifact_recovery
2764
2887
  }));
2765
2888
  return {
2766
- mode: "split-viewports",
2889
+ mode,
2767
2890
  job_count: childRuns.length,
2768
- status: "split-viewports",
2891
+ status: mode,
2769
2892
  terminal: childRuns.every(({ result }) => result.riddle?.terminal !== false),
2770
2893
  artifact_recovery: childRuns.some(({ result }) => result.riddle?.artifact_recovery === true),
2771
2894
  queue_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.queue_elapsed_ms)),
@@ -3021,6 +3144,40 @@ async function runSplitViewportProfileForCli(profile, options, input) {
3021
3144
  });
3022
3145
  return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
3023
3146
  }
3147
+ async function aggregateProfileResultsForCli(profile, options) {
3148
+ const resultPaths = runProfileAggregateInputPathsOption(options);
3149
+ const seenViewports = /* @__PURE__ */ new Set();
3150
+ const childInputs = resultPaths.map((resultPath) => ({ resultPath, result: readProfileResultForAggregate(resultPath) })).filter(({ result: result2 }) => !profileResultIsAggregateParent(result2));
3151
+ if (!childInputs.length) {
3152
+ throw new Error("run-profile aggregate found no single-viewport child profile results.");
3153
+ }
3154
+ const childRuns = sortAggregateChildRuns(profile, childInputs.map(({ resultPath, result: result2 }) => {
3155
+ const viewport = aggregateProfileResultViewport(profile, result2, resultPath);
3156
+ if (seenViewports.has(viewport.name)) {
3157
+ throw new Error(`Profile aggregate received more than one result for viewport ${viewport.name}.`);
3158
+ }
3159
+ seenViewports.add(viewport.name);
3160
+ return { viewport, profile: profileForSplitViewport(profile, viewport), result: result2 };
3161
+ }));
3162
+ const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
3163
+ const blocked = childRuns.filter(({ result: result2 }) => !result2.evidence || result2.status === "environment_blocked" || result2.status === "configuration_error");
3164
+ if (blocked.length) {
3165
+ return createRiddleProofProfileEnvironmentBlockedResult({
3166
+ profile,
3167
+ runner: "riddle",
3168
+ error: splitViewportBlockedMessage(childRuns),
3169
+ riddle: splitViewportRiddleMetadata(childRuns, "named-viewport-aggregate"),
3170
+ artifacts
3171
+ });
3172
+ }
3173
+ const evidence = aggregateSplitViewportEvidence(profile, childRuns);
3174
+ const result = assessRiddleProofProfileEvidence(profile, evidence, {
3175
+ runner: "riddle",
3176
+ riddle: splitViewportRiddleMetadata(childRuns, "named-viewport-aggregate"),
3177
+ artifacts
3178
+ });
3179
+ return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
3180
+ }
3024
3181
  async function recoverProfileForCli(profile, options) {
3025
3182
  const runner = optionString(options, "runner") || "riddle";
3026
3183
  if (runner !== "riddle") {
@@ -3125,7 +3282,7 @@ async function main() {
3125
3282
  }
3126
3283
  if (command === "run-profile") {
3127
3284
  const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
3128
- const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : await runProfileForCli(profile, options);
3285
+ const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : positional[1] === "aggregate" ? await aggregateProfileResultsForCli(profile, options) : await runProfileForCli(profile, options);
3129
3286
  writeProfileOutput(profileOutputDirOption(options), result);
3130
3287
  const diagnosticLine = profileCliDiagnosticLine(result);
3131
3288
  if (diagnosticLine && optionBoolean(options, "quiet") !== true) {
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
295
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
385
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
662
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
295
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
385
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
662
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.7.202",
3
+ "version": "0.7.204",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",