@riddledc/riddle-proof 0.7.202 → 0.7.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -227,6 +227,23 @@ 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.
246
+
230
247
  When promoting proof artifacts into a durable public profile, avoid guessing
231
248
  which backend or runner tokens are preserved inside `proof.json`. Derive the
232
249
  `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]",
@@ -17045,15 +17046,17 @@ function profileHasRecoveredStateReceipt(receipts) {
17045
17046
  const path7 = cliString(receipt.path) || cliString(receipt.function_name) || "";
17046
17047
  const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
17047
17048
  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");
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");
17049
17050
  if (!labelsRecovery) return false;
17050
17051
  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);
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);
17053
17054
  const hasValid = setupReturnSummaryValue(receipt, ["hasValid", "valid", "isValid"]) === true;
17054
17055
  const hasInvalid = setupReturnSummaryValue(receipt, ["hasInvalid", "invalid", "isInvalid"]);
17055
17056
  const success = setupReturnSummaryValue(receipt, ["success", "recovered", "fixed"]) === true;
17056
- 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;
17057
17060
  });
17058
17061
  }
17059
17062
  function profileMetadataHasGeneratedOutputContract(metadata) {
@@ -18911,6 +18914,78 @@ function splitViewportOutputDir(outputDir, viewportName, seen) {
18911
18914
  seen.set(base, count + 1);
18912
18915
  return import_node_path6.default.join(outputDir, count ? `${base}-${count + 1}` : base);
18913
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
+ }
18914
18989
  function splitViewportArtifactRefs(input) {
18915
18990
  return (input.result.artifacts.riddle_artifacts || []).map((artifact) => ({
18916
18991
  ...artifact,
@@ -18921,7 +18996,7 @@ function sumDefinedNumbers(values) {
18921
18996
  const numbers = values.filter((value) => typeof value === "number" && Number.isFinite(value));
18922
18997
  return numbers.length ? numbers.reduce((sum, value) => sum + value, 0) : void 0;
18923
18998
  }
18924
- function splitViewportRiddleMetadata(childRuns) {
18999
+ function splitViewportRiddleMetadata(childRuns, mode = "split-viewports") {
18925
19000
  const splitJobs = childRuns.map(({ viewport, result }) => ({
18926
19001
  viewport: viewport.name,
18927
19002
  job_id: result.riddle?.job_id,
@@ -18938,9 +19013,9 @@ function splitViewportRiddleMetadata(childRuns) {
18938
19013
  artifact_recovery: result.riddle?.artifact_recovery
18939
19014
  }));
18940
19015
  return {
18941
- mode: "split-viewports",
19016
+ mode,
18942
19017
  job_count: childRuns.length,
18943
- status: "split-viewports",
19018
+ status: mode,
18944
19019
  terminal: childRuns.every(({ result }) => result.riddle?.terminal !== false),
18945
19020
  artifact_recovery: childRuns.some(({ result }) => result.riddle?.artifact_recovery === true),
18946
19021
  queue_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.queue_elapsed_ms)),
@@ -19196,6 +19271,40 @@ async function runSplitViewportProfileForCli(profile, options, input) {
19196
19271
  });
19197
19272
  return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
19198
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
+ }
19199
19308
  async function recoverProfileForCli(profile, options) {
19200
19309
  const runner = optionString(options, "runner") || "riddle";
19201
19310
  if (runner !== "riddle") {
@@ -19300,7 +19409,7 @@ async function main() {
19300
19409
  }
19301
19410
  if (command === "run-profile") {
19302
19411
  const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
19303
- const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : await runProfileForCli(profile, options);
19412
+ const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : positional[1] === "aggregate" ? await aggregateProfileResultsForCli(profile, options) : await runProfileForCli(profile, options);
19304
19413
  writeProfileOutput(profileOutputDirOption(options), result);
19305
19414
  const diagnosticLine = profileCliDiagnosticLine(result);
19306
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 [
@@ -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]",
@@ -870,15 +871,17 @@ function profileHasRecoveredStateReceipt(receipts) {
870
871
  const path2 = cliString(receipt.path) || cliString(receipt.function_name) || "";
871
872
  const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
872
873
  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");
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");
874
875
  if (!labelsRecovery) return false;
875
876
  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);
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);
878
879
  const hasValid = setupReturnSummaryValue(receipt, ["hasValid", "valid", "isValid"]) === true;
879
880
  const hasInvalid = setupReturnSummaryValue(receipt, ["hasInvalid", "invalid", "isInvalid"]);
880
881
  const success = setupReturnSummaryValue(receipt, ["success", "recovered", "fixed"]) === true;
881
- 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;
882
885
  });
883
886
  }
884
887
  function profileMetadataHasGeneratedOutputContract(metadata) {
@@ -2736,6 +2739,78 @@ function splitViewportOutputDir(outputDir, viewportName, seen) {
2736
2739
  seen.set(base, count + 1);
2737
2740
  return path.join(outputDir, count ? `${base}-${count + 1}` : base);
2738
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
+ }
2739
2814
  function splitViewportArtifactRefs(input) {
2740
2815
  return (input.result.artifacts.riddle_artifacts || []).map((artifact) => ({
2741
2816
  ...artifact,
@@ -2746,7 +2821,7 @@ function sumDefinedNumbers(values) {
2746
2821
  const numbers = values.filter((value) => typeof value === "number" && Number.isFinite(value));
2747
2822
  return numbers.length ? numbers.reduce((sum, value) => sum + value, 0) : void 0;
2748
2823
  }
2749
- function splitViewportRiddleMetadata(childRuns) {
2824
+ function splitViewportRiddleMetadata(childRuns, mode = "split-viewports") {
2750
2825
  const splitJobs = childRuns.map(({ viewport, result }) => ({
2751
2826
  viewport: viewport.name,
2752
2827
  job_id: result.riddle?.job_id,
@@ -2763,9 +2838,9 @@ function splitViewportRiddleMetadata(childRuns) {
2763
2838
  artifact_recovery: result.riddle?.artifact_recovery
2764
2839
  }));
2765
2840
  return {
2766
- mode: "split-viewports",
2841
+ mode,
2767
2842
  job_count: childRuns.length,
2768
- status: "split-viewports",
2843
+ status: mode,
2769
2844
  terminal: childRuns.every(({ result }) => result.riddle?.terminal !== false),
2770
2845
  artifact_recovery: childRuns.some(({ result }) => result.riddle?.artifact_recovery === true),
2771
2846
  queue_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.queue_elapsed_ms)),
@@ -3021,6 +3096,40 @@ async function runSplitViewportProfileForCli(profile, options, input) {
3021
3096
  });
3022
3097
  return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
3023
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
+ }
3024
3133
  async function recoverProfileForCli(profile, options) {
3025
3134
  const runner = optionString(options, "runner") || "riddle";
3026
3135
  if (runner !== "riddle") {
@@ -3125,7 +3234,7 @@ async function main() {
3125
3234
  }
3126
3235
  if (command === "run-profile") {
3127
3236
  const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
3128
- const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : await runProfileForCli(profile, options);
3237
+ const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : positional[1] === "aggregate" ? await aggregateProfileResultsForCli(profile, options) : await runProfileForCli(profile, options);
3129
3238
  writeProfileOutput(profileOutputDirOption(options), result);
3130
3239
  const diagnosticLine = profileCliDiagnosticLine(result);
3131
3240
  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.203",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",