@riddledc/riddle-proof 0.8.74 → 0.8.76

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
@@ -86,6 +86,39 @@ group may be `passed` or `product_regression`, while the after group must be
86
86
  `proof_insufficient`, and a missing or failed required delta cannot become a
87
87
  passing change proof.
88
88
 
89
+ The CLI can collect the two sides against hosted Riddle targets and write one
90
+ change receipt:
91
+
92
+ ```sh
93
+ riddle-proof-loop run-change-proof \
94
+ --profile .riddle-proof/profiles/hero-art.json \
95
+ --change-contract .riddle-proof/change-contracts/hero-art-change.json \
96
+ --before-url https://example.com \
97
+ --after-url https://preview.example.com \
98
+ --output artifacts/riddle-proof/hero-art-change
99
+ ```
100
+
101
+ It can also compare saved profile packets without starting new browser jobs:
102
+
103
+ ```sh
104
+ riddle-proof-loop run-change-proof \
105
+ --profile .riddle-proof/profiles/hero-art.json \
106
+ --change-contract .riddle-proof/change-contracts/hero-art-change.json \
107
+ --before-result artifacts/before/profile-result.json \
108
+ --after-result artifacts/after/profile-result.json \
109
+ --result-format compact-json
110
+ ```
111
+
112
+ When `--output` / `--output-dir` is set, the command writes
113
+ `change-proof-result.json`, `summary.md`, and normalized copies of the
114
+ `before/profile-result.json` and `after/profile-result.json` inputs. A
115
+ non-passing change proof exits nonzero so CI and PR-comment automation can use
116
+ the receipt as a gate.
117
+
118
+ For a small hosted-preview dogfood target, see
119
+ `examples/profiles/hosted-change-proof-preview.json` and
120
+ `examples/change-contracts/hosted-change-proof-preview.json`.
121
+
89
122
  ## Durable Loop CLI
90
123
 
91
124
  The package publishes `riddle-proof-loop` as a host-agnostic runner surface for
@@ -30,6 +30,9 @@ import {
30
30
  resolveRiddleProofProfileTargetUrl,
31
31
  resolveRiddleProofProfileTimeoutSec
32
32
  } from "./chunk-GG2D3MFZ.js";
33
+ import {
34
+ assessRiddleProofChange
35
+ } from "./chunk-6S7DZWVC.js";
33
36
  import {
34
37
  createCodexExecAgentAdapter,
35
38
  runCodexExecAgentDoctor
@@ -57,8 +60,13 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
57
60
  "attempts",
58
61
  "balancePreflight",
59
62
  "baseUrl",
63
+ "afterResult",
64
+ "afterUrl",
65
+ "beforeResult",
66
+ "beforeUrl",
60
67
  "candidateJson",
61
68
  "candidatesJson",
69
+ "changeContract",
62
70
  "checkpointMode",
63
71
  "checkpointVisibility",
64
72
  "changedFiles",
@@ -165,6 +173,7 @@ function usage() {
165
173
  " riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--base-url <base-url>] [--runner riddle] [--viewport-name <name[,name...]>] [--strict true|false; default false] [--split-viewports true|false; default false] [--balance-preflight true|false; default true] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
166
174
  " riddle-proof-loop run-profile aggregate --profile <file|json|-> --url <base-url> [--base-url <base-url>] --input-dir <dir>|--inputs <path[,path...]> [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
167
175
  " riddle-proof-loop run-profile recover --profile <file|json|-> --url <base-url> [--base-url <base-url>] --job <job-id> [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
176
+ " riddle-proof-loop run-change-proof --profile <file|json|-> --change-contract <file|json|-> (--before-url <url> --after-url <url> | --before-result <file> --after-result <file>) [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
168
177
  " riddle-proof-loop profile-suggest --route /path|--url <url> [--changed-files a,b] [--selectors .a,.b] [--changed-text-json <file|json|->] [--format json|profile]",
169
178
  " riddle-proof-loop regression-pack run [--pack oc-flow-regression|--pack-file <file>] [--local-core true|false; default true] [--hosted-riddle true|false; default false] [--format json|markdown|compact-json; default json] [--output <dir>|--output-dir <dir>]",
170
179
  " riddle-proof-loop pr-comment --proof-dir <dir>|--run-response <file> [--result-json <file>] --pr <number|url> [--repo owner/name] [--dry-run] [--body-file <file>] [--comment-mode update|append]",
@@ -1230,14 +1239,19 @@ function parseProfileViewports(value) {
1230
1239
  };
1231
1240
  });
1232
1241
  }
1233
- function normalizeProfileForCli(options) {
1234
- const rawProfile = readJsonValue(optionString(options, "profile"), "--profile");
1242
+ function readProfileRecordForCli(options) {
1243
+ return readJsonValue(optionString(options, "profile"), "--profile");
1244
+ }
1245
+ function normalizeProfileRecordForCli(rawProfile, options) {
1235
1246
  return normalizeRiddleProofProfile(rawProfile, {
1236
1247
  url: optionString(options, "url") ?? optionString(options, "baseUrl"),
1237
1248
  route: optionString(options, "route"),
1238
1249
  viewports: parseProfileViewports(optionString(options, "viewports") || optionString(options, "viewport"))
1239
1250
  });
1240
1251
  }
1252
+ function normalizeProfileForCli(options) {
1253
+ return normalizeProfileRecordForCli(readProfileRecordForCli(options), options);
1254
+ }
1241
1255
  function profileHttpStatusPreflightSummary(result) {
1242
1256
  const lines = [
1243
1257
  result.summary,
@@ -3909,6 +3923,136 @@ function writeProfileOutput(outputDir, result) {
3909
3923
  if (result.evidence?.dom_summary) writeFileSync(path.join(outputDir, "dom-summary.json"), `${JSON.stringify(result.evidence.dom_summary, null, 2)}
3910
3924
  `);
3911
3925
  }
3926
+ function readChangeContractForCli(options) {
3927
+ const parsed = readJsonValue(optionString(options, "changeContract"), "--change-contract");
3928
+ if (parsed.version !== void 0 && parsed.version !== "riddle-proof.change-contract.v1") {
3929
+ throw new Error(`Unsupported change contract version ${parsed.version}. Expected riddle-proof.change-contract.v1.`);
3930
+ }
3931
+ if (typeof parsed.name !== "string" || !parsed.name.trim()) {
3932
+ throw new Error("--change-contract must include a non-empty name.");
3933
+ }
3934
+ if (!Array.isArray(parsed.deltas) || !parsed.deltas.length) {
3935
+ throw new Error("--change-contract must include at least one delta.");
3936
+ }
3937
+ return parsed;
3938
+ }
3939
+ function changeProofSideOutputDir(outputDir, side) {
3940
+ return outputDir ? path.join(outputDir, side) : void 0;
3941
+ }
3942
+ function profileOptionsForChangeProofSide(options, url, outputDir) {
3943
+ const next = {
3944
+ ...options,
3945
+ url,
3946
+ baseUrl: url
3947
+ };
3948
+ if (outputDir) {
3949
+ next.output = outputDir;
3950
+ next.outputDir = outputDir;
3951
+ } else {
3952
+ delete next.output;
3953
+ delete next.outputDir;
3954
+ }
3955
+ return next;
3956
+ }
3957
+ function compactChangeProofResult(run, options) {
3958
+ const outputDir = profileOutputDirOption(options);
3959
+ return {
3960
+ version: "riddle-proof.change-compact-result.v1",
3961
+ contract_name: run.result.contract_name,
3962
+ profile_name: run.profile.name,
3963
+ status: run.result.status,
3964
+ summary: run.result.summary,
3965
+ before: {
3966
+ source: run.beforeSource,
3967
+ profile_name: run.beforeResult.profile_name,
3968
+ status: run.beforeResult.status,
3969
+ summary: run.beforeResult.summary
3970
+ },
3971
+ after: {
3972
+ source: run.afterSource,
3973
+ profile_name: run.afterResult.profile_name,
3974
+ status: run.afterResult.status,
3975
+ summary: run.afterResult.summary
3976
+ },
3977
+ deltas: run.result.deltas.map((delta) => ({
3978
+ type: delta.type,
3979
+ label: delta.label,
3980
+ status: delta.status,
3981
+ before_observed: delta.before_observed,
3982
+ after_observed: delta.after_observed,
3983
+ message: delta.message
3984
+ })),
3985
+ metadata: run.result.metadata,
3986
+ output_dir: outputDir,
3987
+ output_files: outputDir ? {
3988
+ change_proof_result: "change-proof-result.json",
3989
+ summary: "summary.md",
3990
+ before_profile_result: "before/profile-result.json",
3991
+ after_profile_result: "after/profile-result.json"
3992
+ } : void 0
3993
+ };
3994
+ }
3995
+ function changeProofResultMarkdown(run) {
3996
+ const lines = [
3997
+ "# Riddle Proof Change Proof",
3998
+ "",
3999
+ `Contract: ${run.result.contract_name}`,
4000
+ `Profile: ${run.profile.name}`,
4001
+ `Status: ${run.result.status}`,
4002
+ "",
4003
+ run.result.summary,
4004
+ "",
4005
+ "## Before",
4006
+ "",
4007
+ `- source: ${markdownInlineCode(run.beforeSource, 160)}`,
4008
+ `- profile: ${run.beforeResult.profile_name}`,
4009
+ `- status: ${run.beforeResult.status}`,
4010
+ `- summary: ${run.beforeResult.summary}`,
4011
+ "",
4012
+ "## After",
4013
+ "",
4014
+ `- source: ${markdownInlineCode(run.afterSource, 160)}`,
4015
+ `- profile: ${run.afterResult.profile_name}`,
4016
+ `- status: ${run.afterResult.status}`,
4017
+ `- summary: ${run.afterResult.summary}`,
4018
+ "",
4019
+ "## Deltas",
4020
+ ""
4021
+ ];
4022
+ for (const delta of run.result.deltas) {
4023
+ const observed = [
4024
+ delta.before_observed !== void 0 ? `before ${markdownInlineCode(String(delta.before_observed))}` : "",
4025
+ delta.after_observed !== void 0 ? `after ${markdownInlineCode(String(delta.after_observed))}` : ""
4026
+ ].filter(Boolean).join(" -> ");
4027
+ lines.push(`- ${delta.status}: ${delta.label}${observed ? ` (${observed})` : ""}${delta.message ? ` - ${delta.message}` : ""}`);
4028
+ }
4029
+ return `${lines.join("\n")}
4030
+ `;
4031
+ }
4032
+ function writeChangeProofOutput(outputDir, run) {
4033
+ if (!outputDir) return;
4034
+ mkdirSync(outputDir, { recursive: true });
4035
+ writeFileSync(path.join(outputDir, "change-proof-result.json"), `${JSON.stringify(run.result, null, 2)}
4036
+ `);
4037
+ writeFileSync(path.join(outputDir, "summary.md"), changeProofResultMarkdown(run));
4038
+ writeProfileOutput(changeProofSideOutputDir(outputDir, "before"), run.beforeResult);
4039
+ writeProfileOutput(changeProofSideOutputDir(outputDir, "after"), run.afterResult);
4040
+ }
4041
+ function writeRunChangeProofResult(run, options) {
4042
+ const format = runProfileResultFormatOption(options);
4043
+ if (format === "none") return;
4044
+ if (format === "summary") {
4045
+ process.stdout.write(changeProofResultMarkdown(run));
4046
+ return;
4047
+ }
4048
+ if (format === "compact-json") {
4049
+ process.stdout.write(`${JSON.stringify(compactChangeProofResult(run, options), null, 2)}
4050
+ `);
4051
+ return;
4052
+ }
4053
+ process.stdout.write(`${JSON.stringify(run.result, null, 2)}
4054
+ `);
4055
+ }
3912
4056
  function writeRiddleJobReceipt(outputDir, input) {
3913
4057
  if (!outputDir) return;
3914
4058
  mkdirSync(outputDir, { recursive: true });
@@ -4214,13 +4358,16 @@ function runProfileAggregateInputPathsOption(options) {
4214
4358
  }
4215
4359
  return uniquePaths;
4216
4360
  }
4217
- function readProfileResultForAggregate(resultPath) {
4218
- const parsed = readJsonValue(resultPath, resultPath);
4361
+ function readProfileResultForCli(resultPath, label = resultPath) {
4362
+ const parsed = readJsonValue(resultPath, label);
4219
4363
  if (parsed.version !== "riddle-proof.profile-result.v1" || !Array.isArray(parsed.checks)) {
4220
- throw new Error(`Profile aggregate input is not a riddle-proof.profile-result.v1 result: ${resultPath}`);
4364
+ throw new Error(`${label} is not a riddle-proof.profile-result.v1 result: ${resultPath}`);
4221
4365
  }
4222
4366
  return parsed;
4223
4367
  }
4368
+ function readProfileResultForAggregate(resultPath) {
4369
+ return readProfileResultForCli(resultPath, resultPath);
4370
+ }
4224
4371
  function profileResultIsAggregateParent(result) {
4225
4372
  const mode = cliString(cliRecord(result.riddle)?.mode);
4226
4373
  return (mode === "split-viewports" || mode === "named-viewport-aggregate") && (result.evidence?.viewports?.length || 0) > 1;
@@ -4751,6 +4898,58 @@ async function runProfileForCli(profile, options) {
4751
4898
  }
4752
4899
  return runSingleRiddleProfileForCli(profile, options, { client, runner, outputDir: profileOutputDirOption(options) });
4753
4900
  }
4901
+ async function runChangeProofForCli(rawProfile, options) {
4902
+ const contract = readChangeContractForCli(options);
4903
+ const beforeResultPath = optionString(options, "beforeResult");
4904
+ const afterResultPath = optionString(options, "afterResult");
4905
+ const beforeUrl = optionString(options, "beforeUrl");
4906
+ const afterUrl = optionString(options, "afterUrl");
4907
+ const hasResultInputs = Boolean(beforeResultPath || afterResultPath);
4908
+ const hasUrlInputs = Boolean(beforeUrl || afterUrl);
4909
+ if (hasResultInputs && hasUrlInputs) {
4910
+ throw new Error("run-change-proof accepts either --before-result/--after-result or --before-url/--after-url, not both.");
4911
+ }
4912
+ if (hasResultInputs && (!beforeResultPath || !afterResultPath)) {
4913
+ throw new Error("run-change-proof requires both --before-result and --after-result.");
4914
+ }
4915
+ if (!hasResultInputs && (!beforeUrl || !afterUrl)) {
4916
+ throw new Error("run-change-proof requires both --before-url and --after-url unless saved results are provided.");
4917
+ }
4918
+ const outputDir = profileOutputDirOption(options);
4919
+ let profile;
4920
+ let beforeResult;
4921
+ let afterResult;
4922
+ let beforeSource;
4923
+ let afterSource;
4924
+ if (hasResultInputs) {
4925
+ if (!beforeResultPath || !afterResultPath) {
4926
+ throw new Error("run-change-proof requires both --before-result and --after-result.");
4927
+ }
4928
+ profile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, options), options);
4929
+ beforeResult = readProfileResultForCli(beforeResultPath, "--before-result");
4930
+ afterResult = readProfileResultForCli(afterResultPath, "--after-result");
4931
+ beforeSource = beforeResultPath;
4932
+ afterSource = afterResultPath;
4933
+ } else {
4934
+ if (!beforeUrl || !afterUrl) {
4935
+ throw new Error("run-change-proof requires both --before-url and --after-url unless saved results are provided.");
4936
+ }
4937
+ const beforeOptions = profileOptionsForChangeProofSide(options, beforeUrl, changeProofSideOutputDir(outputDir, "before"));
4938
+ const afterOptions = profileOptionsForChangeProofSide(options, afterUrl, changeProofSideOutputDir(outputDir, "after"));
4939
+ const beforeProfile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, beforeOptions), beforeOptions);
4940
+ const afterProfile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, afterOptions), afterOptions);
4941
+ profile = beforeProfile;
4942
+ beforeResult = await runProfileForCli(beforeProfile, beforeOptions);
4943
+ afterResult = await runProfileForCli(afterProfile, afterOptions);
4944
+ beforeSource = beforeUrl;
4945
+ afterSource = afterUrl;
4946
+ }
4947
+ const result = assessRiddleProofChange(contract, {
4948
+ before_result: beforeResult,
4949
+ after_result: afterResult
4950
+ });
4951
+ return { profile, contract, beforeResult, afterResult, result, beforeSource, afterSource };
4952
+ }
4754
4953
  function requestForRun(options) {
4755
4954
  const statePath = optionString(options, "statePath");
4756
4955
  const withEngineModuleUrl = (request) => {
@@ -4811,6 +5010,13 @@ async function main() {
4811
5010
  process.exitCode = profileStatusExitCode(profile, result.status);
4812
5011
  return;
4813
5012
  }
5013
+ if (command === "run-change-proof") {
5014
+ const run = await runChangeProofForCli(readProfileRecordForCli(options), options);
5015
+ writeChangeProofOutput(profileOutputDirOption(options), run);
5016
+ writeRunChangeProofResult(run, options);
5017
+ process.exitCode = run.result.status === "passed" ? 0 : 1;
5018
+ return;
5019
+ }
4814
5020
  if (command === "regression-pack") {
4815
5021
  const action = positional[1] || "run";
4816
5022
  if (action !== "run") throw new Error("Only `regression-pack run` is supported.");
package/dist/cli/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import "../chunk-ANUWU3DO.js";
1
+ import "../chunk-AK2EPEVO.js";
2
2
  import "../chunk-B2DP2LET.js";
3
3
  import "../chunk-JFQXAJH2.js";
4
4
  import "../chunk-CWRIXP5H.js";
5
5
  import "../chunk-UE4I7RTI.js";
6
6
  import "../chunk-GG2D3MFZ.js";
7
+ import "../chunk-6S7DZWVC.js";
7
8
  import "../chunk-KXLEN4SA.js";
8
9
  import "../chunk-WLUMLHII.js";
9
10
  import "../chunk-CGJX7LJO.js";
package/dist/cli.cjs CHANGED
@@ -18608,6 +18608,167 @@ function suggestRiddleProofProfileChecks(input) {
18608
18608
  };
18609
18609
  }
18610
18610
 
18611
+ // src/change-proof.ts
18612
+ var RIDDLE_PROOF_CHANGE_RESULT_VERSION = "riddle-proof.change-result.v1";
18613
+ var DEFAULT_BEFORE_STATUSES = ["passed", "product_regression"];
18614
+ var DEFAULT_AFTER_STATUSES = ["passed"];
18615
+ var DEFAULT_PROFILE_STATUS_TRANSITION_BEFORE = ["product_regression"];
18616
+ var DEFAULT_PROFILE_STATUS_TRANSITION_AFTER = ["passed"];
18617
+ var DEFAULT_CHECK_STATUS_TRANSITION_BEFORE = ["failed"];
18618
+ var DEFAULT_CHECK_STATUS_TRANSITION_AFTER = ["passed"];
18619
+ function listValue(value, fallback) {
18620
+ if (Array.isArray(value)) return value;
18621
+ if (value === void 0) return fallback;
18622
+ return [value];
18623
+ }
18624
+ function includesValue(allowed, observed) {
18625
+ return observed !== void 0 && allowed.includes(observed);
18626
+ }
18627
+ function groupResult(side, contract, result) {
18628
+ const fallback = side === "before" ? DEFAULT_BEFORE_STATUSES : DEFAULT_AFTER_STATUSES;
18629
+ const requiredStatus = listValue(contract?.required_status, fallback);
18630
+ const label = contract?.label || side;
18631
+ if (!result) {
18632
+ return {
18633
+ side,
18634
+ label,
18635
+ ok: false,
18636
+ status: "missing",
18637
+ required_status: requiredStatus,
18638
+ message: `${label} profile result is missing.`
18639
+ };
18640
+ }
18641
+ const ok = includesValue(requiredStatus, result.status);
18642
+ return {
18643
+ side,
18644
+ label,
18645
+ ok,
18646
+ profile_name: result.profile_name,
18647
+ status: result.status,
18648
+ required_status: requiredStatus,
18649
+ summary: result.summary,
18650
+ message: ok ? void 0 : `${label} profile status ${result.status} did not match required status ${requiredStatus.join(", ")}.`
18651
+ };
18652
+ }
18653
+ function findCheck(result, delta) {
18654
+ return result.checks.find((check) => {
18655
+ if (delta.check_label && check.label !== delta.check_label) return false;
18656
+ if (delta.check_type && check.type !== delta.check_type) return false;
18657
+ return Boolean(delta.check_label || delta.check_type);
18658
+ });
18659
+ }
18660
+ function profileStatusTransitionResult(delta, before, after) {
18661
+ const label = delta.label || "profile-status-transition";
18662
+ if (!before || !after) {
18663
+ return {
18664
+ type: delta.type,
18665
+ label,
18666
+ status: "proof_insufficient",
18667
+ before_observed: before?.status,
18668
+ after_observed: after?.status,
18669
+ message: `${label} needs both before and after profile results.`
18670
+ };
18671
+ }
18672
+ const beforeAllowed = listValue(delta.before_status, DEFAULT_PROFILE_STATUS_TRANSITION_BEFORE);
18673
+ const afterAllowed = listValue(delta.after_status, DEFAULT_PROFILE_STATUS_TRANSITION_AFTER);
18674
+ const passed = includesValue(beforeAllowed, before.status) && includesValue(afterAllowed, after.status);
18675
+ return {
18676
+ type: delta.type,
18677
+ label,
18678
+ status: passed ? "passed" : "failed",
18679
+ before_observed: before.status,
18680
+ after_observed: after.status,
18681
+ message: passed ? void 0 : `${label} expected before ${beforeAllowed.join(", ")} and after ${afterAllowed.join(", ")}, got ${before.status} -> ${after.status}.`
18682
+ };
18683
+ }
18684
+ function checkStatusTransitionResult(delta, before, after) {
18685
+ const label = delta.label || delta.check_label || delta.check_type || "check-status-transition";
18686
+ if (!delta.check_label && !delta.check_type) {
18687
+ return {
18688
+ type: delta.type,
18689
+ label,
18690
+ status: "configuration_error",
18691
+ message: `${label} needs check_label or check_type.`
18692
+ };
18693
+ }
18694
+ if (!before || !after) {
18695
+ return {
18696
+ type: delta.type,
18697
+ label,
18698
+ status: "proof_insufficient",
18699
+ before_observed: before?.status,
18700
+ after_observed: after?.status,
18701
+ message: `${label} needs both before and after profile results.`
18702
+ };
18703
+ }
18704
+ const beforeCheck = findCheck(before, delta);
18705
+ const afterCheck = findCheck(after, delta);
18706
+ if (!beforeCheck || !afterCheck) {
18707
+ return {
18708
+ type: delta.type,
18709
+ label,
18710
+ status: "proof_insufficient",
18711
+ before_observed: beforeCheck?.status,
18712
+ after_observed: afterCheck?.status,
18713
+ message: `${label} was not present in ${!beforeCheck && !afterCheck ? "before or after" : !beforeCheck ? "before" : "after"} profile checks.`
18714
+ };
18715
+ }
18716
+ const beforeAllowed = listValue(delta.before_status, DEFAULT_CHECK_STATUS_TRANSITION_BEFORE);
18717
+ const afterAllowed = listValue(delta.after_status, DEFAULT_CHECK_STATUS_TRANSITION_AFTER);
18718
+ const passed = includesValue(beforeAllowed, beforeCheck.status) && includesValue(afterAllowed, afterCheck.status);
18719
+ return {
18720
+ type: delta.type,
18721
+ label,
18722
+ status: passed ? "passed" : "failed",
18723
+ before_observed: beforeCheck.status,
18724
+ after_observed: afterCheck.status,
18725
+ message: passed ? void 0 : `${label} expected before ${beforeAllowed.join(", ")} and after ${afterAllowed.join(", ")}, got ${beforeCheck.status} -> ${afterCheck.status}.`
18726
+ };
18727
+ }
18728
+ function assessDelta(delta, before, after) {
18729
+ if (delta.type === "profile_status_transition") {
18730
+ return profileStatusTransitionResult(delta, before, after);
18731
+ }
18732
+ return checkStatusTransitionResult(delta, before, after);
18733
+ }
18734
+ function collapsedChangeStatus(groups, deltas) {
18735
+ const groupResults = [groups.before, groups.after];
18736
+ if (groupResults.some((group) => group.status === "environment_blocked")) return "environment_blocked";
18737
+ if (groupResults.some((group) => group.status === "configuration_error")) return "configuration_error";
18738
+ if (deltas.some((delta) => delta.status === "configuration_error")) return "configuration_error";
18739
+ if (groupResults.some((group) => group.status === "needs_human_review")) return "needs_human_review";
18740
+ if (groupResults.some((group) => group.status === "missing" || group.status === "proof_insufficient")) return "proof_insufficient";
18741
+ if (!deltas.length || deltas.some((delta) => delta.status === "proof_insufficient")) return "proof_insufficient";
18742
+ if (groupResults.some((group) => !group.ok)) return "product_regression";
18743
+ if (deltas.some((delta) => delta.status === "failed")) return "product_regression";
18744
+ return "passed";
18745
+ }
18746
+ function changeSummary(name, status, deltas) {
18747
+ if (status === "passed") return `${name} passed ${deltas.length} change delta(s).`;
18748
+ if (status === "environment_blocked") return `${name} could not compare reliable evidence because an environment was blocked.`;
18749
+ if (status === "configuration_error") return `${name} has an invalid change proof contract.`;
18750
+ if (status === "needs_human_review") return `${name} needs human review before the change proof can pass.`;
18751
+ if (status === "proof_insufficient") return `${name} did not produce enough before/after evidence for a change proof.`;
18752
+ return `${name} failed ${deltas.filter((delta) => delta.status === "failed").length} change delta(s).`;
18753
+ }
18754
+ function assessRiddleProofChange(contract, input) {
18755
+ const groups = {
18756
+ before: groupResult("before", contract.before, input.before_result),
18757
+ after: groupResult("after", contract.after, input.after_result)
18758
+ };
18759
+ const deltas = (contract.deltas || []).map((delta) => assessDelta(delta, input.before_result, input.after_result));
18760
+ const status = collapsedChangeStatus(groups, deltas);
18761
+ return {
18762
+ version: RIDDLE_PROOF_CHANGE_RESULT_VERSION,
18763
+ contract_name: contract.name,
18764
+ status,
18765
+ groups,
18766
+ deltas,
18767
+ summary: changeSummary(contract.name, status, deltas),
18768
+ metadata: contract.metadata
18769
+ };
18770
+ }
18771
+
18611
18772
  // src/cli.ts
18612
18773
  var RIDDLE_PROFILE_BALANCE_PREFLIGHT_MIN_SECONDS_PER_JOB = 30;
18613
18774
  var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
@@ -18619,8 +18780,13 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
18619
18780
  "attempts",
18620
18781
  "balancePreflight",
18621
18782
  "baseUrl",
18783
+ "afterResult",
18784
+ "afterUrl",
18785
+ "beforeResult",
18786
+ "beforeUrl",
18622
18787
  "candidateJson",
18623
18788
  "candidatesJson",
18789
+ "changeContract",
18624
18790
  "checkpointMode",
18625
18791
  "checkpointVisibility",
18626
18792
  "changedFiles",
@@ -18727,6 +18893,7 @@ function usage() {
18727
18893
  " riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--base-url <base-url>] [--runner riddle] [--viewport-name <name[,name...]>] [--strict true|false; default false] [--split-viewports true|false; default false] [--balance-preflight true|false; default true] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
18728
18894
  " riddle-proof-loop run-profile aggregate --profile <file|json|-> --url <base-url> [--base-url <base-url>] --input-dir <dir>|--inputs <path[,path...]> [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
18729
18895
  " riddle-proof-loop run-profile recover --profile <file|json|-> --url <base-url> [--base-url <base-url>] --job <job-id> [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
18896
+ " riddle-proof-loop run-change-proof --profile <file|json|-> --change-contract <file|json|-> (--before-url <url> --after-url <url> | --before-result <file> --after-result <file>) [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
18730
18897
  " riddle-proof-loop profile-suggest --route /path|--url <url> [--changed-files a,b] [--selectors .a,.b] [--changed-text-json <file|json|->] [--format json|profile]",
18731
18898
  " riddle-proof-loop regression-pack run [--pack oc-flow-regression|--pack-file <file>] [--local-core true|false; default true] [--hosted-riddle true|false; default false] [--format json|markdown|compact-json; default json] [--output <dir>|--output-dir <dir>]",
18732
18899
  " riddle-proof-loop pr-comment --proof-dir <dir>|--run-response <file> [--result-json <file>] --pr <number|url> [--repo owner/name] [--dry-run] [--body-file <file>] [--comment-mode update|append]",
@@ -19792,14 +19959,19 @@ function parseProfileViewports(value) {
19792
19959
  };
19793
19960
  });
19794
19961
  }
19795
- function normalizeProfileForCli(options) {
19796
- const rawProfile = readJsonValue(optionString(options, "profile"), "--profile");
19962
+ function readProfileRecordForCli(options) {
19963
+ return readJsonValue(optionString(options, "profile"), "--profile");
19964
+ }
19965
+ function normalizeProfileRecordForCli(rawProfile, options) {
19797
19966
  return normalizeRiddleProofProfile(rawProfile, {
19798
19967
  url: optionString(options, "url") ?? optionString(options, "baseUrl"),
19799
19968
  route: optionString(options, "route"),
19800
19969
  viewports: parseProfileViewports(optionString(options, "viewports") || optionString(options, "viewport"))
19801
19970
  });
19802
19971
  }
19972
+ function normalizeProfileForCli(options) {
19973
+ return normalizeProfileRecordForCli(readProfileRecordForCli(options), options);
19974
+ }
19803
19975
  function profileHttpStatusPreflightSummary(result) {
19804
19976
  const lines = [
19805
19977
  result.summary,
@@ -22471,6 +22643,136 @@ function writeProfileOutput(outputDir, result) {
22471
22643
  if (result.evidence?.dom_summary) (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "dom-summary.json"), `${JSON.stringify(result.evidence.dom_summary, null, 2)}
22472
22644
  `);
22473
22645
  }
22646
+ function readChangeContractForCli(options) {
22647
+ const parsed = readJsonValue(optionString(options, "changeContract"), "--change-contract");
22648
+ if (parsed.version !== void 0 && parsed.version !== "riddle-proof.change-contract.v1") {
22649
+ throw new Error(`Unsupported change contract version ${parsed.version}. Expected riddle-proof.change-contract.v1.`);
22650
+ }
22651
+ if (typeof parsed.name !== "string" || !parsed.name.trim()) {
22652
+ throw new Error("--change-contract must include a non-empty name.");
22653
+ }
22654
+ if (!Array.isArray(parsed.deltas) || !parsed.deltas.length) {
22655
+ throw new Error("--change-contract must include at least one delta.");
22656
+ }
22657
+ return parsed;
22658
+ }
22659
+ function changeProofSideOutputDir(outputDir, side) {
22660
+ return outputDir ? import_node_path6.default.join(outputDir, side) : void 0;
22661
+ }
22662
+ function profileOptionsForChangeProofSide(options, url, outputDir) {
22663
+ const next = {
22664
+ ...options,
22665
+ url,
22666
+ baseUrl: url
22667
+ };
22668
+ if (outputDir) {
22669
+ next.output = outputDir;
22670
+ next.outputDir = outputDir;
22671
+ } else {
22672
+ delete next.output;
22673
+ delete next.outputDir;
22674
+ }
22675
+ return next;
22676
+ }
22677
+ function compactChangeProofResult(run, options) {
22678
+ const outputDir = profileOutputDirOption(options);
22679
+ return {
22680
+ version: "riddle-proof.change-compact-result.v1",
22681
+ contract_name: run.result.contract_name,
22682
+ profile_name: run.profile.name,
22683
+ status: run.result.status,
22684
+ summary: run.result.summary,
22685
+ before: {
22686
+ source: run.beforeSource,
22687
+ profile_name: run.beforeResult.profile_name,
22688
+ status: run.beforeResult.status,
22689
+ summary: run.beforeResult.summary
22690
+ },
22691
+ after: {
22692
+ source: run.afterSource,
22693
+ profile_name: run.afterResult.profile_name,
22694
+ status: run.afterResult.status,
22695
+ summary: run.afterResult.summary
22696
+ },
22697
+ deltas: run.result.deltas.map((delta) => ({
22698
+ type: delta.type,
22699
+ label: delta.label,
22700
+ status: delta.status,
22701
+ before_observed: delta.before_observed,
22702
+ after_observed: delta.after_observed,
22703
+ message: delta.message
22704
+ })),
22705
+ metadata: run.result.metadata,
22706
+ output_dir: outputDir,
22707
+ output_files: outputDir ? {
22708
+ change_proof_result: "change-proof-result.json",
22709
+ summary: "summary.md",
22710
+ before_profile_result: "before/profile-result.json",
22711
+ after_profile_result: "after/profile-result.json"
22712
+ } : void 0
22713
+ };
22714
+ }
22715
+ function changeProofResultMarkdown(run) {
22716
+ const lines = [
22717
+ "# Riddle Proof Change Proof",
22718
+ "",
22719
+ `Contract: ${run.result.contract_name}`,
22720
+ `Profile: ${run.profile.name}`,
22721
+ `Status: ${run.result.status}`,
22722
+ "",
22723
+ run.result.summary,
22724
+ "",
22725
+ "## Before",
22726
+ "",
22727
+ `- source: ${markdownInlineCode(run.beforeSource, 160)}`,
22728
+ `- profile: ${run.beforeResult.profile_name}`,
22729
+ `- status: ${run.beforeResult.status}`,
22730
+ `- summary: ${run.beforeResult.summary}`,
22731
+ "",
22732
+ "## After",
22733
+ "",
22734
+ `- source: ${markdownInlineCode(run.afterSource, 160)}`,
22735
+ `- profile: ${run.afterResult.profile_name}`,
22736
+ `- status: ${run.afterResult.status}`,
22737
+ `- summary: ${run.afterResult.summary}`,
22738
+ "",
22739
+ "## Deltas",
22740
+ ""
22741
+ ];
22742
+ for (const delta of run.result.deltas) {
22743
+ const observed = [
22744
+ delta.before_observed !== void 0 ? `before ${markdownInlineCode(String(delta.before_observed))}` : "",
22745
+ delta.after_observed !== void 0 ? `after ${markdownInlineCode(String(delta.after_observed))}` : ""
22746
+ ].filter(Boolean).join(" -> ");
22747
+ lines.push(`- ${delta.status}: ${delta.label}${observed ? ` (${observed})` : ""}${delta.message ? ` - ${delta.message}` : ""}`);
22748
+ }
22749
+ return `${lines.join("\n")}
22750
+ `;
22751
+ }
22752
+ function writeChangeProofOutput(outputDir, run) {
22753
+ if (!outputDir) return;
22754
+ (0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
22755
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "change-proof-result.json"), `${JSON.stringify(run.result, null, 2)}
22756
+ `);
22757
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "summary.md"), changeProofResultMarkdown(run));
22758
+ writeProfileOutput(changeProofSideOutputDir(outputDir, "before"), run.beforeResult);
22759
+ writeProfileOutput(changeProofSideOutputDir(outputDir, "after"), run.afterResult);
22760
+ }
22761
+ function writeRunChangeProofResult(run, options) {
22762
+ const format = runProfileResultFormatOption(options);
22763
+ if (format === "none") return;
22764
+ if (format === "summary") {
22765
+ process.stdout.write(changeProofResultMarkdown(run));
22766
+ return;
22767
+ }
22768
+ if (format === "compact-json") {
22769
+ process.stdout.write(`${JSON.stringify(compactChangeProofResult(run, options), null, 2)}
22770
+ `);
22771
+ return;
22772
+ }
22773
+ process.stdout.write(`${JSON.stringify(run.result, null, 2)}
22774
+ `);
22775
+ }
22474
22776
  function writeRiddleJobReceipt(outputDir, input) {
22475
22777
  if (!outputDir) return;
22476
22778
  (0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
@@ -22776,13 +23078,16 @@ function runProfileAggregateInputPathsOption(options) {
22776
23078
  }
22777
23079
  return uniquePaths;
22778
23080
  }
22779
- function readProfileResultForAggregate(resultPath) {
22780
- const parsed = readJsonValue(resultPath, resultPath);
23081
+ function readProfileResultForCli(resultPath, label = resultPath) {
23082
+ const parsed = readJsonValue(resultPath, label);
22781
23083
  if (parsed.version !== "riddle-proof.profile-result.v1" || !Array.isArray(parsed.checks)) {
22782
- throw new Error(`Profile aggregate input is not a riddle-proof.profile-result.v1 result: ${resultPath}`);
23084
+ throw new Error(`${label} is not a riddle-proof.profile-result.v1 result: ${resultPath}`);
22783
23085
  }
22784
23086
  return parsed;
22785
23087
  }
23088
+ function readProfileResultForAggregate(resultPath) {
23089
+ return readProfileResultForCli(resultPath, resultPath);
23090
+ }
22786
23091
  function profileResultIsAggregateParent(result) {
22787
23092
  const mode = cliString(cliRecord(result.riddle)?.mode);
22788
23093
  return (mode === "split-viewports" || mode === "named-viewport-aggregate") && (result.evidence?.viewports?.length || 0) > 1;
@@ -23313,6 +23618,58 @@ async function runProfileForCli(profile, options) {
23313
23618
  }
23314
23619
  return runSingleRiddleProfileForCli(profile, options, { client, runner, outputDir: profileOutputDirOption(options) });
23315
23620
  }
23621
+ async function runChangeProofForCli(rawProfile, options) {
23622
+ const contract = readChangeContractForCli(options);
23623
+ const beforeResultPath = optionString(options, "beforeResult");
23624
+ const afterResultPath = optionString(options, "afterResult");
23625
+ const beforeUrl = optionString(options, "beforeUrl");
23626
+ const afterUrl = optionString(options, "afterUrl");
23627
+ const hasResultInputs = Boolean(beforeResultPath || afterResultPath);
23628
+ const hasUrlInputs = Boolean(beforeUrl || afterUrl);
23629
+ if (hasResultInputs && hasUrlInputs) {
23630
+ throw new Error("run-change-proof accepts either --before-result/--after-result or --before-url/--after-url, not both.");
23631
+ }
23632
+ if (hasResultInputs && (!beforeResultPath || !afterResultPath)) {
23633
+ throw new Error("run-change-proof requires both --before-result and --after-result.");
23634
+ }
23635
+ if (!hasResultInputs && (!beforeUrl || !afterUrl)) {
23636
+ throw new Error("run-change-proof requires both --before-url and --after-url unless saved results are provided.");
23637
+ }
23638
+ const outputDir = profileOutputDirOption(options);
23639
+ let profile;
23640
+ let beforeResult;
23641
+ let afterResult;
23642
+ let beforeSource;
23643
+ let afterSource;
23644
+ if (hasResultInputs) {
23645
+ if (!beforeResultPath || !afterResultPath) {
23646
+ throw new Error("run-change-proof requires both --before-result and --after-result.");
23647
+ }
23648
+ profile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, options), options);
23649
+ beforeResult = readProfileResultForCli(beforeResultPath, "--before-result");
23650
+ afterResult = readProfileResultForCli(afterResultPath, "--after-result");
23651
+ beforeSource = beforeResultPath;
23652
+ afterSource = afterResultPath;
23653
+ } else {
23654
+ if (!beforeUrl || !afterUrl) {
23655
+ throw new Error("run-change-proof requires both --before-url and --after-url unless saved results are provided.");
23656
+ }
23657
+ const beforeOptions = profileOptionsForChangeProofSide(options, beforeUrl, changeProofSideOutputDir(outputDir, "before"));
23658
+ const afterOptions = profileOptionsForChangeProofSide(options, afterUrl, changeProofSideOutputDir(outputDir, "after"));
23659
+ const beforeProfile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, beforeOptions), beforeOptions);
23660
+ const afterProfile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, afterOptions), afterOptions);
23661
+ profile = beforeProfile;
23662
+ beforeResult = await runProfileForCli(beforeProfile, beforeOptions);
23663
+ afterResult = await runProfileForCli(afterProfile, afterOptions);
23664
+ beforeSource = beforeUrl;
23665
+ afterSource = afterUrl;
23666
+ }
23667
+ const result = assessRiddleProofChange(contract, {
23668
+ before_result: beforeResult,
23669
+ after_result: afterResult
23670
+ });
23671
+ return { profile, contract, beforeResult, afterResult, result, beforeSource, afterSource };
23672
+ }
23316
23673
  function requestForRun(options) {
23317
23674
  const statePath = optionString(options, "statePath");
23318
23675
  const withEngineModuleUrl = (request) => {
@@ -23373,6 +23730,13 @@ async function main() {
23373
23730
  process.exitCode = profileStatusExitCode(profile, result.status);
23374
23731
  return;
23375
23732
  }
23733
+ if (command === "run-change-proof") {
23734
+ const run = await runChangeProofForCli(readProfileRecordForCli(options), options);
23735
+ writeChangeProofOutput(profileOutputDirOption(options), run);
23736
+ writeRunChangeProofResult(run, options);
23737
+ process.exitCode = run.result.status === "passed" ? 0 : 1;
23738
+ return;
23739
+ }
23376
23740
  if (command === "regression-pack") {
23377
23741
  const action = positional[1] || "run";
23378
23742
  if (action !== "run") throw new Error("Only `regression-pack run` is supported.");
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-ANUWU3DO.js";
2
+ import "./chunk-AK2EPEVO.js";
3
3
  import "./chunk-B2DP2LET.js";
4
4
  import "./chunk-JFQXAJH2.js";
5
5
  import "./chunk-CWRIXP5H.js";
6
6
  import "./chunk-UE4I7RTI.js";
7
7
  import "./chunk-GG2D3MFZ.js";
8
+ import "./chunk-6S7DZWVC.js";
8
9
  import "./chunk-KXLEN4SA.js";
9
10
  import "./chunk-WLUMLHII.js";
10
11
  import "./chunk-CGJX7LJO.js";
package/dist/index.js CHANGED
@@ -37,11 +37,6 @@ import {
37
37
  resolveBasicGameplayProgressionCheckWithArtifactScreenshots,
38
38
  sanitizeBasicGameplayJsonString
39
39
  } from "./chunk-ELZSPOWV.js";
40
- import {
41
- RIDDLE_PROOF_CHANGE_CONTRACT_VERSION,
42
- RIDDLE_PROOF_CHANGE_RESULT_VERSION,
43
- assessRiddleProofChange
44
- } from "./chunk-6S7DZWVC.js";
45
40
  import {
46
41
  DEFAULT_RIDDLE_API_BASE_URL,
47
42
  DEFAULT_RIDDLE_API_KEY_FILE,
@@ -101,6 +96,11 @@ import {
101
96
  slugifyRiddleProofProfileName,
102
97
  summarizeRiddleProofProfileResult
103
98
  } from "./chunk-GG2D3MFZ.js";
99
+ import {
100
+ RIDDLE_PROOF_CHANGE_CONTRACT_VERSION,
101
+ RIDDLE_PROOF_CHANGE_RESULT_VERSION,
102
+ assessRiddleProofChange
103
+ } from "./chunk-6S7DZWVC.js";
104
104
  import {
105
105
  createCodexExecAgentAdapter,
106
106
  createCodexExecJsonRunner,
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": "riddle-proof.change-contract.v1",
3
+ "name": "Hosted preview marker appears after change",
4
+ "deltas": [
5
+ {
6
+ "type": "check_status_transition",
7
+ "label": "hosted preview marker becomes visible",
8
+ "check_label": "hosted-preview-marker-visible",
9
+ "before_status": "failed",
10
+ "after_status": "passed"
11
+ }
12
+ ],
13
+ "metadata": {
14
+ "example": "hosted preview before/after proof"
15
+ }
16
+ }
@@ -28,6 +28,10 @@
28
28
  <dt>Fixture</dt>
29
29
  <dd>neutral-fixture-pass</dd>
30
30
  </div>
31
+ <div>
32
+ <dt>Hosted proof</dt>
33
+ <dd data-rp-change-proof="hosted-preview">hosted preview change proof</dd>
34
+ </div>
31
35
  </dl>
32
36
  </section>
33
37
  </main>
@@ -70,7 +70,7 @@ p {
70
70
 
71
71
  dl {
72
72
  display: grid;
73
- grid-template-columns: repeat(3, minmax(0, 1fr));
73
+ grid-template-columns: repeat(4, minmax(0, 1fr));
74
74
  margin: 0;
75
75
  }
76
76
 
@@ -0,0 +1,36 @@
1
+ {
2
+ "version": "riddle-proof.profile.v1",
3
+ "name": "hosted-change-proof-preview",
4
+ "target": {
5
+ "route": "/pass.html",
6
+ "viewports": [
7
+ { "name": "phone", "width": 390, "height": 844 },
8
+ { "name": "desktop", "width": 1440, "height": 1000 }
9
+ ],
10
+ "auth": "none",
11
+ "wait_for_selector": "[data-rp-fixture=\"pass\"]",
12
+ "setup_actions": [
13
+ { "type": "screenshot", "label": "initial-fixture-state", "full_page": false },
14
+ { "type": "wait", "ms": 100 }
15
+ ],
16
+ "screenshot_full_page": false
17
+ },
18
+ "checks": [
19
+ { "type": "route_loaded", "expected_path": "/pass.html" },
20
+ { "type": "selector_visible", "selector": "[data-rp-fixture=\"pass\"]" },
21
+ {
22
+ "type": "selector_text_visible",
23
+ "label": "hosted-preview-marker-visible",
24
+ "selector": "[data-rp-change-proof=\"hosted-preview\"]",
25
+ "text": "hosted preview change proof"
26
+ },
27
+ { "type": "no_mobile_horizontal_overflow" },
28
+ { "type": "no_fatal_console_errors" }
29
+ ],
30
+ "artifacts": ["screenshot", "console", "dom_summary", "proof_json"],
31
+ "failure_policy": {
32
+ "environment_blocked": "neutral",
33
+ "proof_insufficient": "fail",
34
+ "product_regression": "fail"
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.8.74",
3
+ "version": "0.8.76",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",