@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 +17 -0
- package/dist/cli.cjs +117 -8
- package/dist/cli.js +118 -9
- package/dist/proof-run-engine.d.cts +3 -3
- package/dist/proof-run-engine.d.ts +3 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
|
19016
|
+
mode,
|
|
18942
19017
|
job_count: childRuns.length,
|
|
18943
|
-
status:
|
|
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
|
-
|
|
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
|
|
2841
|
+
mode,
|
|
2767
2842
|
job_count: childRuns.length,
|
|
2768
|
-
status:
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
662
|
+
action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
|
|
663
663
|
state_path: string;
|
|
664
664
|
stage: any;
|
|
665
665
|
summary: string;
|