@riddledc/riddle-proof 0.8.76 → 0.8.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +74 -5
  2. package/dist/advanced/index.d.cts +1 -1
  3. package/dist/advanced/index.d.ts +1 -1
  4. package/dist/advanced/proof-run-engine.d.cts +1 -1
  5. package/dist/advanced/proof-run-engine.d.ts +1 -1
  6. package/dist/change-proof.cjs +991 -7
  7. package/dist/change-proof.d.cts +157 -1
  8. package/dist/change-proof.d.ts +157 -1
  9. package/dist/change-proof.js +25 -3
  10. package/dist/{chunk-B2DP2LET.js → chunk-5IFZSUPF.js} +52 -13
  11. package/dist/chunk-6VFS2JFR.js +886 -0
  12. package/dist/{chunk-CWRIXP5H.js → chunk-7N6X54WG.js} +181 -17
  13. package/dist/{chunk-GG2D3MFZ.js → chunk-FCSJZBC5.js} +26 -1
  14. package/dist/{chunk-AK2EPEVO.js → chunk-HSGZV2NK.js} +221 -63
  15. package/dist/chunk-MEVVL4TI.js +258 -0
  16. package/dist/{chunk-UE4I7RTI.js → chunk-RQPCKRKT.js} +1 -1
  17. package/dist/cli/index.js +7 -6
  18. package/dist/cli.cjs +1678 -361
  19. package/dist/cli.js +7 -6
  20. package/dist/index.cjs +1242 -42
  21. package/dist/index.d.cts +4 -3
  22. package/dist/index.d.ts +4 -3
  23. package/dist/index.js +46 -10
  24. package/dist/pr-comment.cjs +481 -17
  25. package/dist/pr-comment.d.cts +6 -1
  26. package/dist/pr-comment.d.ts +6 -1
  27. package/dist/pr-comment.js +6 -1
  28. package/dist/profile/index.cjs +26 -1
  29. package/dist/profile/index.js +1 -1
  30. package/dist/profile-suggestions.js +2 -2
  31. package/dist/profile.cjs +26 -1
  32. package/dist/profile.d.cts +7 -0
  33. package/dist/profile.d.ts +7 -0
  34. package/dist/profile.js +1 -1
  35. package/dist/{proof-run-engine-MiKZt9oY.d.ts → proof-run-engine-CsQshTUX.d.ts} +3 -3
  36. package/dist/{proof-run-engine-Baiv6l3A.d.cts → proof-run-engine-DmUqh7Cw.d.cts} +3 -3
  37. package/dist/proof-run-engine.d.cts +1 -1
  38. package/dist/proof-run-engine.d.ts +1 -1
  39. package/dist/receipts.cjs +286 -0
  40. package/dist/receipts.d.cts +115 -0
  41. package/dist/receipts.d.ts +115 -0
  42. package/dist/receipts.js +15 -0
  43. package/dist/riddle-client.cjs +98 -13
  44. package/dist/riddle-client.d.cts +18 -5
  45. package/dist/riddle-client.d.ts +18 -5
  46. package/dist/riddle-client.js +4 -1
  47. package/dist/runtime/index.cjs +98 -13
  48. package/dist/runtime/index.d.cts +5 -1
  49. package/dist/runtime/index.d.ts +5 -1
  50. package/dist/runtime/index.js +4 -1
  51. package/dist/runtime/riddle-client.cjs +98 -13
  52. package/dist/runtime/riddle-client.d.cts +5 -1
  53. package/dist/runtime/riddle-client.d.ts +5 -1
  54. package/dist/runtime/riddle-client.js +4 -1
  55. package/package.json +7 -2
  56. package/dist/chunk-6S7DZWVC.js +0 -167
package/dist/cli.cjs CHANGED
@@ -7879,6 +7879,259 @@ var import_node_child_process4 = require("child_process");
7879
7879
  var import_node_fs5 = require("fs");
7880
7880
  var import_node_os2 = require("os");
7881
7881
  var import_node_path5 = __toESM(require("path"), 1);
7882
+
7883
+ // src/receipts.ts
7884
+ var RIDDLE_PREVIEW_RECEIPT_VERSION = "riddle.preview-receipt.v1";
7885
+ var RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION = "riddle-proof.observation-receipt.v1";
7886
+ function isRecord(value) {
7887
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7888
+ }
7889
+ function requiredString(record, key, context) {
7890
+ const value = record[key];
7891
+ if (typeof value !== "string" || !value.trim()) {
7892
+ throw new Error(`${context}.${key} must be a non-empty string.`);
7893
+ }
7894
+ return value.trim();
7895
+ }
7896
+ function normalizedSourceIdentity(value) {
7897
+ if (!isRecord(value)) return {};
7898
+ return {
7899
+ git_revision: typeof value.git_revision === "string" && value.git_revision.trim() ? value.git_revision.trim() : void 0,
7900
+ repository: typeof value.repository === "string" && value.repository.trim() ? value.repository.trim() : void 0,
7901
+ dirty: typeof value.dirty === "boolean" ? value.dirty : void 0,
7902
+ label: typeof value.label === "string" && value.label.trim() ? value.label.trim() : void 0
7903
+ };
7904
+ }
7905
+ function parseRiddlePreviewReceipt(value) {
7906
+ if (!isRecord(value)) throw new Error("Preview receipt must be an object.");
7907
+ if (value.version !== RIDDLE_PREVIEW_RECEIPT_VERSION) {
7908
+ throw new Error(`Unsupported Preview receipt version ${String(value.version || "missing")}.`);
7909
+ }
7910
+ if (!isRecord(value.source)) throw new Error("Preview receipt source must be an object.");
7911
+ const expiresAt = requiredString(value, "expires_at", "preview receipt");
7912
+ const publishedAt = requiredString(value, "published_at", "preview receipt");
7913
+ const contentDigest = requiredString(value, "content_digest", "preview receipt");
7914
+ if (!contentDigest.startsWith("sha256:") || contentDigest.length <= "sha256:".length) {
7915
+ throw new Error("preview receipt.content_digest must be a sha256 digest.");
7916
+ }
7917
+ if (!Number.isFinite(Date.parse(expiresAt)) || !Number.isFinite(Date.parse(publishedAt))) {
7918
+ throw new Error("Preview receipt timestamps must be valid ISO date strings.");
7919
+ }
7920
+ return {
7921
+ version: RIDDLE_PREVIEW_RECEIPT_VERSION,
7922
+ preview_id: requiredString(value, "preview_id", "preview receipt"),
7923
+ url: requiredString(value, "url", "preview receipt"),
7924
+ expires_at: expiresAt,
7925
+ content_digest: contentDigest,
7926
+ source: normalizedSourceIdentity(value.source),
7927
+ published_at: publishedAt
7928
+ };
7929
+ }
7930
+ function comparableArtifactName(value) {
7931
+ return (value || "").split(/[/?#]/).pop()?.toLowerCase().replace(/\.(png|jpe?g|gif|webp|avif|svg)$/u, "").replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/g, "") || "";
7932
+ }
7933
+ function artifactMatchesLabel(artifact, label) {
7934
+ const expected = comparableArtifactName(label);
7935
+ if (!expected) return false;
7936
+ return [artifact.name, artifact.url, artifact.path].map(comparableArtifactName).some((name) => name === expected || name.endsWith(`-${expected}`));
7937
+ }
7938
+ function artifactLooksLikeScreenshot(ref) {
7939
+ const text = [ref.name, ref.url, ref.path, ref.kind, ref.content_type].filter(Boolean).join(" ").toLowerCase();
7940
+ return /screenshot|\bimage\b/.test(text) || /\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/.test(text);
7941
+ }
7942
+ function finalScreenshotLabels(result) {
7943
+ const explicit = result.artifacts.canonical_screenshots || [];
7944
+ if (explicit.length) return explicit;
7945
+ const fromEvidence = (result.evidence?.viewports || []).map((viewport) => viewport.screenshot_label).filter((label) => typeof label === "string" && Boolean(label.trim()));
7946
+ if (fromEvidence.length) return fromEvidence;
7947
+ return (result.artifacts.screenshots || []).slice(0, 1);
7948
+ }
7949
+ function setupScreenshotLabels(result) {
7950
+ const explicit = result.artifacts.setup_screenshots || [];
7951
+ if (explicit.length) return explicit;
7952
+ return (result.evidence?.viewports || []).flatMap(
7953
+ (viewport) => (viewport.setup_action_results || []).filter((action) => action.action === "screenshot" && action.ok !== false && typeof action.screenshot_label === "string").map((action) => String(action.screenshot_label))
7954
+ );
7955
+ }
7956
+ function observationArtifactFromRef(ref, canonicalLabels, setupLabels) {
7957
+ const base = {
7958
+ name: ref.name,
7959
+ role: ref.role || "artifact",
7960
+ url: ref.url,
7961
+ path: ref.path,
7962
+ kind: ref.kind,
7963
+ content_type: ref.content_type,
7964
+ source: ref.source
7965
+ };
7966
+ if (!artifactLooksLikeScreenshot(ref)) return base;
7967
+ if (canonicalLabels.some((label) => artifactMatchesLabel(base, label))) {
7968
+ return { ...base, role: "canonical_screenshot" };
7969
+ }
7970
+ if (setupLabels.some((label) => artifactMatchesLabel(base, label))) {
7971
+ return { ...base, role: "setup_screenshot" };
7972
+ }
7973
+ return { ...base, role: "diagnostic" };
7974
+ }
7975
+ function profileObservationArtifacts(result) {
7976
+ const canonicalLabels = finalScreenshotLabels(result);
7977
+ const setupLabels = setupScreenshotLabels(result);
7978
+ const artifacts = [];
7979
+ const seen = /* @__PURE__ */ new Set();
7980
+ const add = (artifact) => {
7981
+ const key = artifact.url || artifact.path || `${artifact.role}:${artifact.name}`;
7982
+ if (!artifact.name || seen.has(key)) return;
7983
+ seen.add(key);
7984
+ artifacts.push(artifact);
7985
+ };
7986
+ for (const ref of result.artifacts.riddle_artifacts || []) {
7987
+ add(observationArtifactFromRef(ref, canonicalLabels, setupLabels));
7988
+ }
7989
+ for (const label of canonicalLabels) {
7990
+ if (!artifacts.some((artifact) => artifactMatchesLabel(artifact, label))) {
7991
+ add({ name: label, path: label, role: "canonical_screenshot", kind: "image" });
7992
+ }
7993
+ }
7994
+ for (const label of setupLabels) {
7995
+ if (!artifacts.some((artifact) => artifactMatchesLabel(artifact, label))) {
7996
+ add({ name: label, path: label, role: "setup_screenshot", kind: "image" });
7997
+ }
7998
+ }
7999
+ for (const label of result.artifacts.screenshots || []) {
8000
+ if (!artifacts.some((artifact) => artifactMatchesLabel(artifact, label))) {
8001
+ add({ name: label, path: label, role: "diagnostic", kind: "image" });
8002
+ }
8003
+ }
8004
+ return artifacts;
8005
+ }
8006
+ function mergeObservationArtifacts(profileArtifacts, suppliedArtifacts) {
8007
+ const artifacts = [...profileArtifacts];
8008
+ for (const supplied of suppliedArtifacts) {
8009
+ const suppliedNames = [supplied.name, supplied.url, supplied.path].map(comparableArtifactName).filter(Boolean);
8010
+ const existingIndex = artifacts.findIndex((artifact) => {
8011
+ if (supplied.url && artifact.url === supplied.url) return true;
8012
+ if (supplied.path && artifact.path === supplied.path) return true;
8013
+ return [artifact.name, artifact.url, artifact.path].map(comparableArtifactName).some((name) => name && suppliedNames.includes(name));
8014
+ });
8015
+ if (existingIndex < 0) {
8016
+ artifacts.push(supplied);
8017
+ continue;
8018
+ }
8019
+ const existing = artifacts[existingIndex];
8020
+ const role = supplied.role === "canonical_screenshot" || supplied.role === "setup_screenshot" ? supplied.role : existing.role === "canonical_screenshot" || existing.role === "setup_screenshot" ? existing.role : supplied.role === "artifact" ? existing.role : supplied.role;
8021
+ artifacts[existingIndex] = {
8022
+ ...existing,
8023
+ ...supplied,
8024
+ role
8025
+ };
8026
+ }
8027
+ return artifacts;
8028
+ }
8029
+ function defaultObservationId(role, capturedAt) {
8030
+ return `obs_${role}_${capturedAt.replace(/[^0-9]/g, "").slice(0, 17) || "unknown"}`;
8031
+ }
8032
+ function profileCheckCounts(result) {
8033
+ const counts = { total: result.checks.length, passed: 0, failed: 0, skipped: 0, needs_human_review: 0 };
8034
+ for (const check of result.checks) {
8035
+ if (check.status === "passed") counts.passed += 1;
8036
+ if (check.status === "failed") counts.failed += 1;
8037
+ if (check.status === "skipped") counts.skipped += 1;
8038
+ if (check.status === "needs_human_review") counts.needs_human_review += 1;
8039
+ }
8040
+ return counts;
8041
+ }
8042
+ function createRiddleProofObservationReceipt(input) {
8043
+ const capturedAt = input.captured_at || input.profile_result?.captured_at || (/* @__PURE__ */ new Date()).toISOString();
8044
+ const profileArtifacts = input.profile_result ? profileObservationArtifacts(input.profile_result) : [];
8045
+ const requestedCanonical = input.canonical_screenshot ? { ...input.canonical_screenshot, role: "canonical_screenshot" } : void 0;
8046
+ const artifacts = mergeObservationArtifacts(
8047
+ profileArtifacts,
8048
+ [...input.artifacts || [], ...requestedCanonical ? [requestedCanonical] : []]
8049
+ );
8050
+ const canonicalScreenshot = requestedCanonical ? artifacts.find((artifact) => artifact.role === "canonical_screenshot" && artifactMatchesLabel(artifact, requestedCanonical.name)) || requestedCanonical : artifacts.find((artifact) => artifact.role === "canonical_screenshot");
8051
+ const target = input.target.kind === "preview" && input.target.preview ? { ...input.target, preview: parseRiddlePreviewReceipt(input.target.preview) } : input.target;
8052
+ return {
8053
+ version: RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION,
8054
+ observation_id: input.observation_id || defaultObservationId(input.comparison_role, capturedAt),
8055
+ comparison_role: input.comparison_role,
8056
+ executor: input.executor,
8057
+ target,
8058
+ source: normalizedSourceIdentity(input.source || input.target.preview?.source),
8059
+ captured_at: capturedAt,
8060
+ artifacts,
8061
+ canonical_screenshot: canonicalScreenshot,
8062
+ profile_summary: input.profile_result ? {
8063
+ profile_name: input.profile_result.profile_name,
8064
+ status: input.profile_result.status,
8065
+ summary: input.profile_result.summary,
8066
+ route: input.profile_result.route,
8067
+ checks: profileCheckCounts(input.profile_result)
8068
+ } : void 0,
8069
+ proof: input.profile_result ? {
8070
+ profile_name: input.profile_result.profile_name,
8071
+ result: input.profile_result
8072
+ } : void 0,
8073
+ publication: input.publication,
8074
+ execution: input.execution,
8075
+ metadata: input.metadata
8076
+ };
8077
+ }
8078
+ function parseRiddleProofObservationReceipt(value) {
8079
+ if (!isRecord(value)) throw new Error("Observation receipt must be an object.");
8080
+ if (value.version !== RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION) {
8081
+ throw new Error(`Unsupported Observation receipt version ${String(value.version || "missing")}.`);
8082
+ }
8083
+ if (!isRecord(value.executor)) throw new Error("Observation receipt executor must be an object.");
8084
+ if (!isRecord(value.target)) throw new Error("Observation receipt target must be an object.");
8085
+ if (!isRecord(value.source)) throw new Error("Observation receipt source must be an object.");
8086
+ requiredString(value, "observation_id", "observation receipt");
8087
+ requiredString(value, "captured_at", "observation receipt");
8088
+ const executorKind = requiredString(value.executor, "kind", "observation receipt executor");
8089
+ if (!"local_playwright,riddle_hosted,browser_api,other".split(",").includes(executorKind)) {
8090
+ throw new Error(`Unsupported observation executor kind ${executorKind}.`);
8091
+ }
8092
+ if (value.executor.runner !== void 0) {
8093
+ requiredString(value.executor, "runner", "observation receipt executor");
8094
+ }
8095
+ const role = requiredString(value, "comparison_role", "observation receipt");
8096
+ if (!["before", "after", "standalone"].includes(role)) {
8097
+ throw new Error(`Unsupported observation comparison role ${role}.`);
8098
+ }
8099
+ const targetKind = requiredString(value.target, "kind", "observation receipt target");
8100
+ if (targetKind !== "url" && targetKind !== "preview") {
8101
+ throw new Error(`Unsupported observation target kind ${targetKind}.`);
8102
+ }
8103
+ requiredString(value.target, "url", "observation receipt target");
8104
+ if (targetKind === "preview") {
8105
+ if (value.target.preview === void 0) {
8106
+ throw new Error("Preview Observation target must include its Preview receipt.");
8107
+ }
8108
+ parseRiddlePreviewReceipt(value.target.preview);
8109
+ }
8110
+ if (!Array.isArray(value.artifacts)) throw new Error("Observation receipt artifacts must be an array.");
8111
+ const artifactRoles = /* @__PURE__ */ new Set(["canonical_screenshot", "setup_screenshot", "data", "diagnostic", "artifact"]);
8112
+ const artifacts = value.artifacts.map((artifact, index) => {
8113
+ if (!isRecord(artifact)) throw new Error(`Observation receipt artifact ${index} must be an object.`);
8114
+ requiredString(artifact, "name", `observation receipt artifact ${index}`);
8115
+ const artifactRole = requiredString(artifact, "role", `observation receipt artifact ${index}`);
8116
+ if (!artifactRoles.has(artifactRole)) {
8117
+ throw new Error(`Unsupported observation artifact role ${artifactRole}.`);
8118
+ }
8119
+ return artifact;
8120
+ });
8121
+ if (value.canonical_screenshot !== void 0) {
8122
+ if (!isRecord(value.canonical_screenshot) || value.canonical_screenshot.role !== "canonical_screenshot") {
8123
+ throw new Error("Observation canonical_screenshot must have the canonical_screenshot role.");
8124
+ }
8125
+ const canonical = value.canonical_screenshot;
8126
+ const included = artifacts.some((artifact) => artifact.role === "canonical_screenshot" && artifact.name === canonical.name && artifact.url === canonical.url && artifact.path === canonical.path);
8127
+ if (!included) {
8128
+ throw new Error("Observation canonical_screenshot must reference an artifact in the receipt.");
8129
+ }
8130
+ }
8131
+ return value;
8132
+ }
8133
+
8134
+ // src/riddle-client.ts
7882
8135
  var DEFAULT_RIDDLE_API_BASE_URL = "https://api.riddledc.com";
7883
8136
  var DEFAULT_RIDDLE_API_KEY_FILE = "/tmp/riddle-api-key";
7884
8137
  var RIDDLE_UNSUBMITTED_WAKE_HINT = "hosted worker may still be waking or waiting for a lease";
@@ -7940,6 +8193,7 @@ async function getRiddleBalance(config = {}) {
7940
8193
  }
7941
8194
  function previewDeployResultFromRecord(input) {
7942
8195
  const { record, id, label, framework, expiresAt, publishRecovered, publishError } = input;
8196
+ const receipt = record.receipt && typeof record.receipt === "object" ? parseRiddlePreviewReceipt(record.receipt) : void 0;
7943
8197
  return {
7944
8198
  ok: true,
7945
8199
  id: String(record.id || record.preview_id || id),
@@ -7948,13 +8202,32 @@ function previewDeployResultFromRecord(input) {
7948
8202
  preview_url: String(record.preview_url || ""),
7949
8203
  file_count: typeof record.file_count === "number" ? record.file_count : void 0,
7950
8204
  total_bytes: typeof record.total_bytes === "number" ? record.total_bytes : void 0,
7951
- expires_at: expiresAt,
8205
+ expires_at: receipt?.expires_at || expiresAt,
8206
+ receipt,
7952
8207
  publish_recovered: publishRecovered || void 0,
7953
8208
  publish_error: publishError,
7954
8209
  warnings: input.warnings?.length ? input.warnings : void 0,
7955
8210
  raw: record
7956
8211
  };
7957
8212
  }
8213
+ function gitOutput(directory, args) {
8214
+ try {
8215
+ return (0, import_node_child_process4.execFileSync)("git", ["-C", directory, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8216
+ } catch {
8217
+ return "";
8218
+ }
8219
+ }
8220
+ function detectRiddlePreviewSource(directory) {
8221
+ const gitRevision = gitOutput(directory, ["rev-parse", "HEAD"]);
8222
+ if (!gitRevision) return {};
8223
+ const repository = gitOutput(directory, ["config", "--get", "remote.origin.url"]);
8224
+ const status = gitOutput(directory, ["status", "--porcelain", "--untracked-files=normal"]);
8225
+ return {
8226
+ git_revision: gitRevision,
8227
+ repository: repository || void 0,
8228
+ dirty: Boolean(status)
8229
+ };
8230
+ }
7958
8231
  function canRecoverPreviewPublish(error) {
7959
8232
  return error instanceof RiddleApiError && PREVIEW_PUBLISH_RECOVERY_STATUSES.has(error.status);
7960
8233
  }
@@ -8037,12 +8310,13 @@ async function waitForPublishedPreview(config, input) {
8037
8310
  }
8038
8311
  throw input.publishError;
8039
8312
  }
8040
- async function deployRiddlePreview(config, directory, label, framework = "static") {
8313
+ async function deployRiddlePreview(config, directory, label, framework = "static", options = {}) {
8041
8314
  if (!directory?.trim()) throw new Error("directory is required");
8042
8315
  if (!label?.trim()) throw new Error("label is required");
8043
8316
  if (framework !== "spa" && framework !== "static") throw new Error("framework must be spa or static");
8044
8317
  const startedAt = Date.now();
8045
8318
  const warnings = collectRiddlePreviewDeployWarnings(directory, framework);
8319
+ const source = options.source || detectRiddlePreviewSource(directory);
8046
8320
  const emitProgress = previewProgressEmitter(config, { label, framework, directory, startedAt, warnings });
8047
8321
  await emitProgress({ stage: "validating", message: "checking preview input directory" });
8048
8322
  const localSummary = summarizePreviewDirectory(directory);
@@ -8054,7 +8328,7 @@ async function deployRiddlePreview(config, directory, label, framework = "static
8054
8328
  });
8055
8329
  const created = await riddleRequestJson(config, "/v1/preview", {
8056
8330
  method: "POST",
8057
- body: JSON.stringify({ framework, label })
8331
+ body: JSON.stringify({ framework, label, source })
8058
8332
  });
8059
8333
  const id = String(created.id || "");
8060
8334
  const uploadUrl = String(created.upload_url || "");
@@ -8196,8 +8470,8 @@ function collectRiddlePreviewDeployWarnings(directory, framework = "static") {
8196
8470
  return [];
8197
8471
  }
8198
8472
  }
8199
- async function deployRiddleStaticPreview(config, directory, label) {
8200
- return deployRiddlePreview(config, directory, label, "static");
8473
+ async function deployRiddleStaticPreview(config, directory, label, options = {}) {
8474
+ return deployRiddlePreview(config, directory, label, "static", options);
8201
8475
  }
8202
8476
  function createTarball(directory, label, exclude = []) {
8203
8477
  const scratch = (0, import_node_fs5.mkdtempSync)(import_node_path5.default.join((0, import_node_os2.tmpdir)(), "riddle-upload-"));
@@ -8319,21 +8593,31 @@ function parseTimestampMs(value) {
8319
8593
  }
8320
8594
  function buildPollSnapshot(jobId, job, input) {
8321
8595
  const status = job?.status ? String(job.status) : null;
8596
+ const phase = job?.phase ? String(job.phase) : null;
8322
8597
  const terminal = isTerminalRiddleJobStatus(status);
8323
8598
  const createdAt = stringField(job, "created_at");
8324
8599
  const submittedAt = stringField(job, "submitted_at");
8325
8600
  const completedAt = stringField(job, "completed_at");
8326
8601
  const createdMs = parseTimestampMs(createdAt);
8327
8602
  const submittedMs = parseTimestampMs(submittedAt);
8603
+ const rawExecution = job?.execution && typeof job.execution === "object" && !Array.isArray(job.execution) ? job.execution : null;
8604
+ const enqueuedAt = rawExecution ? stringField(rawExecution, "enqueued_at") : null;
8605
+ const enqueuedMs = parseTimestampMs(enqueuedAt);
8606
+ const queueStartedMs = enqueuedMs ?? createdMs;
8607
+ const claimedAt = rawExecution ? stringField(rawExecution, "claimed_at") : null;
8608
+ const claimedMs = parseTimestampMs(claimedAt);
8328
8609
  let queueElapsedMs = null;
8329
- if (createdMs !== null && submittedMs !== null) {
8330
- queueElapsedMs = Math.max(0, submittedMs - createdMs);
8331
- } else if (createdMs !== null && !submittedAt && !terminal) {
8332
- queueElapsedMs = Math.max(0, input.observedAt - createdMs);
8610
+ if (queueStartedMs !== null && claimedMs !== null) {
8611
+ queueElapsedMs = Math.max(0, claimedMs - queueStartedMs);
8612
+ } else if (queueStartedMs !== null && submittedMs !== null) {
8613
+ queueElapsedMs = Math.max(0, submittedMs - queueStartedMs);
8614
+ } else if (queueStartedMs !== null && !submittedAt && !terminal) {
8615
+ queueElapsedMs = Math.max(0, input.observedAt - queueStartedMs);
8333
8616
  }
8334
8617
  return {
8335
8618
  job_id: jobId,
8336
8619
  status,
8620
+ phase,
8337
8621
  terminal,
8338
8622
  attempt: input.attempt,
8339
8623
  attempts: input.attempts,
@@ -8343,7 +8627,9 @@ function buildPollSnapshot(jobId, job, input) {
8343
8627
  completed_at: completedAt,
8344
8628
  queue_elapsed_ms: queueElapsedMs,
8345
8629
  pre_submission_elapsed_ms: Math.max(0, Math.floor(input.preSubmissionElapsedMs ?? 0)),
8346
- running_without_submission: Boolean(status && !terminal && !submittedAt)
8630
+ running_without_submission: Boolean(status && !terminal && !submittedAt),
8631
+ active_execution: Boolean(status === "running" && phase && phase !== "queued"),
8632
+ execution: rawExecution ? rawExecution : void 0
8347
8633
  };
8348
8634
  }
8349
8635
  function pollMessage(snapshot, timedOut) {
@@ -8352,7 +8638,7 @@ function pollMessage(snapshot, timedOut) {
8352
8638
  const wakeHint = snapshot.running_without_submission && !snapshot.created_at && !snapshot.submitted_at ? ` ${RIDDLE_UNSUBMITTED_WAKE_HINT}.` : "";
8353
8639
  const queue = snapshot.queue_elapsed_ms !== null ? ` queue_elapsed_ms=${snapshot.queue_elapsed_ms}` : "";
8354
8640
  const preSubmit = snapshot.pre_submission_elapsed_ms > 0 ? ` pre_submission_elapsed_ms=${snapshot.pre_submission_elapsed_ms}` : "";
8355
- return `Riddle job ${snapshot.job_id} did not reach a terminal status after ${snapshot.attempt} poll attempts; status=${snapshot.status || "unknown"} submitted_at=${submitted}.${wakeHint}${queue}${preSubmit}`;
8641
+ return `Riddle job ${snapshot.job_id} did not reach a terminal status after ${snapshot.attempt} poll attempts; status=${snapshot.status || "unknown"} phase=${snapshot.phase || "unknown"} submitted_at=${submitted}.${wakeHint}${queue}${preSubmit}`;
8356
8642
  }
8357
8643
  async function pollRiddleJob(config, jobId, options = {}) {
8358
8644
  if (!jobId?.trim()) throw new Error("jobId is required");
@@ -8386,6 +8672,7 @@ async function pollRiddleJob(config, jobId, options = {}) {
8386
8672
  };
8387
8673
  const progressKey = [
8388
8674
  lastSnapshot.status || "unknown",
8675
+ lastSnapshot.phase || "unknown",
8389
8676
  lastSnapshot.terminal ? "terminal" : "nonterminal",
8390
8677
  lastSnapshot.submitted_at ? "submitted" : "unsubmitted"
8391
8678
  ].join(":");
@@ -8452,8 +8739,8 @@ function createRiddleApiClient(config = {}) {
8452
8739
  apiKeySource: () => resolveRiddleApiKeySource(config),
8453
8740
  requestJson: (pathname, init) => riddleRequestJson(config, pathname, init),
8454
8741
  getBalance: () => getRiddleBalance(config),
8455
- deployPreview: (directory, label, framework = "static") => deployRiddlePreview(config, directory, label, framework),
8456
- deployStaticPreview: (directory, label) => deployRiddleStaticPreview(config, directory, label),
8742
+ deployPreview: (directory, label, framework = "static", options) => deployRiddlePreview(config, directory, label, framework, options),
8743
+ deployStaticPreview: (directory, label, options) => deployRiddleStaticPreview(config, directory, label, options),
8457
8744
  runScript: (input) => runRiddleScript(config, input),
8458
8745
  runServerPreview: (input) => runRiddleServerPreview(config, input),
8459
8746
  pollJob: (jobId, options) => pollRiddleJob(config, jobId, options)
@@ -8584,7 +8871,7 @@ function deriveRiddleProofArtifactBodyAssertions(input) {
8584
8871
  var DEFAULT_VIEWPORTS = [
8585
8872
  { name: "desktop", width: 1280, height: 800 }
8586
8873
  ];
8587
- function isRecord(value) {
8874
+ function isRecord2(value) {
8588
8875
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
8589
8876
  }
8590
8877
  function stringValue3(value) {
@@ -8615,7 +8902,7 @@ function booleanValue2(value) {
8615
8902
  return typeof value === "boolean" ? value : void 0;
8616
8903
  }
8617
8904
  function horizontalBoundsOverflowPx(value) {
8618
- if (!isRecord(value)) return 0;
8905
+ if (!isRecord2(value)) return 0;
8619
8906
  let max = maxPositiveNumber(
8620
8907
  value.overflow_px,
8621
8908
  value.overflow,
@@ -8630,7 +8917,7 @@ function horizontalBoundsOverflowPx(value) {
8630
8917
  return roundPixels(max);
8631
8918
  }
8632
8919
  function horizontalOffenderOverflowPx(value) {
8633
- if (!isRecord(value)) return 0;
8920
+ if (!isRecord2(value)) return 0;
8634
8921
  let max = maxPositiveNumber(
8635
8922
  value.overflow,
8636
8923
  value.overflow_px,
@@ -8641,8 +8928,8 @@ function horizontalOffenderOverflowPx(value) {
8641
8928
  value.leftOverflowPx,
8642
8929
  value.rightOverflowPx
8643
8930
  );
8644
- const clipped = isRecord(value.clipped || value.clip || value.clipping) ? value.clipped || value.clip || value.clipping : void 0;
8645
- if (isRecord(clipped)) {
8931
+ const clipped = isRecord2(value.clipped || value.clip || value.clipping) ? value.clipped || value.clip || value.clipping : void 0;
8932
+ if (isRecord2(clipped)) {
8646
8933
  max = Math.max(max, maxPositiveNumber(
8647
8934
  clipped.left,
8648
8935
  clipped.right,
@@ -8652,9 +8939,9 @@ function horizontalOffenderOverflowPx(value) {
8652
8939
  clipped.rightPx
8653
8940
  ));
8654
8941
  }
8655
- const rect = isRecord(value.rect || value.bounds || value.bounding_rect || value.boundingRect) ? value.rect || value.bounds || value.bounding_rect || value.boundingRect : void 0;
8942
+ const rect = isRecord2(value.rect || value.bounds || value.bounding_rect || value.boundingRect) ? value.rect || value.bounds || value.bounding_rect || value.boundingRect : void 0;
8656
8943
  const viewportWidth = numberValue2(value.viewport_width ?? value.viewportWidth);
8657
- if (isRecord(rect) && viewportWidth !== void 0) {
8944
+ if (isRecord2(rect) && viewportWidth !== void 0) {
8658
8945
  const left = numberValue2(rect.left);
8659
8946
  const right = numberValue2(rect.right);
8660
8947
  if (left !== void 0 && left < 0) max = Math.max(max, Math.abs(left));
@@ -8663,7 +8950,7 @@ function horizontalOffenderOverflowPx(value) {
8663
8950
  return roundPixels(max);
8664
8951
  }
8665
8952
  function boundsOffendersForEvidence(value) {
8666
- if (!isRecord(value)) return [];
8953
+ if (!isRecord2(value)) return [];
8667
8954
  const offenders = [
8668
8955
  ...Array.isArray(value.overflow_offenders) ? value.overflow_offenders : [],
8669
8956
  ...Array.isArray(value.overflowOffenders) ? value.overflowOffenders : [],
@@ -8674,7 +8961,7 @@ function boundsOffendersForEvidence(value) {
8674
8961
  ...Array.isArray(value.clipping_offenders) ? value.clipping_offenders : [],
8675
8962
  ...Array.isArray(value.clippingOffenders) ? value.clippingOffenders : []
8676
8963
  ];
8677
- return offenders.filter((item) => isRecord(item)).sort((a, b) => horizontalOffenderOverflowPx(b) - horizontalOffenderOverflowPx(a));
8964
+ return offenders.filter((item) => isRecord2(item)).sort((a, b) => horizontalOffenderOverflowPx(b) - horizontalOffenderOverflowPx(a));
8678
8965
  }
8679
8966
  function maxPositiveNumber(...values) {
8680
8967
  let max = 0;
@@ -8692,11 +8979,11 @@ function timeoutSecValue(value) {
8692
8979
  return number && number > 0 ? Math.ceil(number) : void 0;
8693
8980
  }
8694
8981
  function jsonRecord(value) {
8695
- if (!isRecord(value)) return void 0;
8982
+ if (!isRecord2(value)) return void 0;
8696
8983
  return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
8697
8984
  }
8698
8985
  function stringRecord(value) {
8699
- if (!isRecord(value)) return void 0;
8986
+ if (!isRecord2(value)) return void 0;
8700
8987
  const entries = Object.entries(value).map(([key, child]) => [key, String(child)]).filter(([key]) => key.trim());
8701
8988
  return entries.length ? Object.fromEntries(entries) : void 0;
8702
8989
  }
@@ -8705,7 +8992,7 @@ function toJsonValue(value) {
8705
8992
  if (typeof value === "string" || typeof value === "boolean") return value;
8706
8993
  if (typeof value === "number") return Number.isFinite(value) ? value : null;
8707
8994
  if (Array.isArray(value)) return value.map(toJsonValue);
8708
- if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
8995
+ if (isRecord2(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
8709
8996
  return String(value);
8710
8997
  }
8711
8998
  function jsonValueType(value) {
@@ -8723,7 +9010,7 @@ function compactJsonAssertionSample(value, depth = 0) {
8723
9010
  if (depth >= 2) return `[array:${value.length}]`;
8724
9011
  return value.slice(0, 3).map((item) => compactJsonAssertionSample(item, depth + 1));
8725
9012
  }
8726
- if (isRecord(value)) {
9013
+ if (isRecord2(value)) {
8727
9014
  const entries = Object.entries(value).slice(0, 8);
8728
9015
  if (depth >= 2) return `[object:${Object.keys(value).length} keys]`;
8729
9016
  return Object.fromEntries(entries.map(([key, child]) => [key, compactJsonAssertionSample(child, depth + 1)]));
@@ -8738,7 +9025,7 @@ function attachJsonAssertionObservedValue(result, value) {
8738
9025
  result.observed_sample = compactJsonAssertionSample(value);
8739
9026
  return;
8740
9027
  }
8741
- if (type === "object" && isRecord(value)) {
9028
+ if (type === "object" && isRecord2(value)) {
8742
9029
  const keyCount = Object.keys(value).length;
8743
9030
  result.observed_key_count = keyCount;
8744
9031
  result.observed_omitted_count = Math.max(0, keyCount - 8);
@@ -8756,7 +9043,7 @@ function deepJsonEqual(left, right) {
8756
9043
  if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
8757
9044
  return left.every((item, index) => deepJsonEqual(item, right[index]));
8758
9045
  }
8759
- if (!isRecord(left) || !isRecord(right)) return false;
9046
+ if (!isRecord2(left) || !isRecord2(right)) return false;
8760
9047
  const leftKeys = Object.keys(left).sort();
8761
9048
  const rightKeys = Object.keys(right).sort();
8762
9049
  if (!deepJsonEqual(leftKeys, rightKeys)) return false;
@@ -8769,7 +9056,7 @@ function jsonContains(observed, expected) {
8769
9056
  if (Array.isArray(observed)) {
8770
9057
  return observed.some((item) => deepJsonEqual(item, expected));
8771
9058
  }
8772
- if (isRecord(observed) && isRecord(expected)) {
9059
+ if (isRecord2(observed) && isRecord2(expected)) {
8773
9060
  return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
8774
9061
  }
8775
9062
  return false;
@@ -8837,7 +9124,7 @@ function resolveJsonPath(root, path7) {
8837
9124
  if (typeof segment === "number") {
8838
9125
  return { exists: false };
8839
9126
  }
8840
- if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
9127
+ if (!isRecord2(current) || !hasOwn(current, segment)) return { exists: false };
8841
9128
  current = current[segment];
8842
9129
  }
8843
9130
  return { exists: true, value: current };
@@ -8911,9 +9198,9 @@ function profileSetupFrameUrls(viewport) {
8911
9198
  const urls = [];
8912
9199
  const frames = viewport.frames || {};
8913
9200
  for (const container of Object.values(frames)) {
8914
- if (!isRecord(container) || !Array.isArray(container.frames)) continue;
9201
+ if (!isRecord2(container) || !Array.isArray(container.frames)) continue;
8915
9202
  for (const frame of container.frames) {
8916
- if (!isRecord(frame)) continue;
9203
+ if (!isRecord2(frame)) continue;
8917
9204
  const url = typeof frame.url === "string" ? frame.url : void 0;
8918
9205
  if (url && !urls.includes(url)) urls.push(url);
8919
9206
  }
@@ -9007,7 +9294,7 @@ function profileSetupReturnSummaryFields(result) {
9007
9294
  if (path8) fields.push({ path: path8 });
9008
9295
  continue;
9009
9296
  }
9010
- if (!isRecord(item)) continue;
9297
+ if (!isRecord2(item)) continue;
9011
9298
  const path7 = stringValue3(item.path) ?? stringValue3(item.key) ?? stringValue3(item.json_path) ?? stringValue3(item.jsonPath);
9012
9299
  if (!path7) continue;
9013
9300
  const label = stringValue3(item.label) ?? stringValue3(item.name) ?? stringValue3(item.title);
@@ -9243,6 +9530,12 @@ function profileScreenshotLabels(viewports) {
9243
9530
  }
9244
9531
  return labels;
9245
9532
  }
9533
+ function profileFinalScreenshotLabels(viewports) {
9534
+ return (viewports || []).map((viewport) => viewport.screenshot_label).filter((label) => typeof label === "string" && Boolean(label.trim()));
9535
+ }
9536
+ function profileAllSetupScreenshotLabels(viewports) {
9537
+ return (viewports || []).flatMap((viewport) => profileSetupScreenshotLabels(viewport.setup_action_results || []));
9538
+ }
9246
9539
  function profileSetupSummary(viewports, actionCount, expectedActionCountByViewport, finalScreenshotFullPage) {
9247
9540
  const normalizedFinalScreenshotFullPage = finalScreenshotFullPage === void 0 ? void 0 : finalScreenshotFullPage !== false;
9248
9541
  const finalScreenshotCount = viewports.filter((viewport) => typeof viewport.screenshot_label === "string" && viewport.screenshot_label.trim()).length;
@@ -9397,7 +9690,7 @@ function slugifyRiddleProofProfileName(value) {
9397
9690
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "profile";
9398
9691
  }
9399
9692
  function normalizeViewport(input, index) {
9400
- if (!isRecord(input)) throw new Error(`target.viewports[${index}] must be an object.`);
9693
+ if (!isRecord2(input)) throw new Error(`target.viewports[${index}] must be an object.`);
9401
9694
  const width = numberValue2(input.width);
9402
9695
  const height = numberValue2(input.height);
9403
9696
  if (!width || !height || width < 100 || height < 100) {
@@ -9463,7 +9756,7 @@ function normalizeReturnSummaryFields(input, index) {
9463
9756
  if (!path8) throw new Error(`target.setup_actions[${index}].return_summary_fields[${fieldIndex}] requires path.`);
9464
9757
  return { path: path8 };
9465
9758
  }
9466
- if (!isRecord(field)) {
9759
+ if (!isRecord2(field)) {
9467
9760
  throw new Error(`target.setup_actions[${index}].return_summary_fields[${fieldIndex}] must be a string path or object.`);
9468
9761
  }
9469
9762
  const path7 = stringFromOwn(field, "path", "key", "json_path", "jsonPath");
@@ -9572,7 +9865,7 @@ function normalizeSetupActionNonNegativeNumber(input, index, outputKey, ...keys)
9572
9865
  return value;
9573
9866
  }
9574
9867
  function normalizeSetupAction(input, index) {
9575
- if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
9868
+ if (!isRecord2(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
9576
9869
  const type = normalizeSetupActionType(stringValue3(input.type), index);
9577
9870
  const rawType = String(input.type || "").trim().replace(/-/g, "_").toLowerCase();
9578
9871
  const selector = stringValue3(input.selector);
@@ -9864,7 +10157,7 @@ function normalizeTargetScreenshotFullPage(input) {
9864
10157
  return values[0];
9865
10158
  }
9866
10159
  function normalizeNetworkMock(input, index) {
9867
- if (!isRecord(input)) throw new Error(`target.network_mocks[${index}] must be an object.`);
10160
+ if (!isRecord2(input)) throw new Error(`target.network_mocks[${index}] must be an object.`);
9868
10161
  const url = stringValue3(input.url) || stringValue3(input.glob) || stringValue3(input.pattern);
9869
10162
  if (!url) throw new Error(`target.network_mocks[${index}] requires url.`);
9870
10163
  const payload = normalizeNetworkMockResponsePayload(input, `target.network_mocks[${index}]`);
@@ -10019,7 +10312,7 @@ function normalizeNetworkMockResponses(value, mockIndex, defaults) {
10019
10312
  abort_error_code: defaults.abort_error_code
10020
10313
  };
10021
10314
  return value.map((response, responseIndex) => {
10022
- if (!isRecord(response)) {
10315
+ if (!isRecord2(response)) {
10023
10316
  throw new Error(`target.network_mocks[${mockIndex}].responses[${responseIndex}] must be an object.`);
10024
10317
  }
10025
10318
  return normalizeNetworkMockResponsePayload(
@@ -10116,7 +10409,7 @@ function normalizeRouteInventoryPath(value, label) {
10116
10409
  }
10117
10410
  function normalizeRouteInventoryRoute(input, index) {
10118
10411
  if (typeof input === "string") return { path: normalizeRouteInventoryPath(input, `checks route_inventory expected_routes[${index}]`) };
10119
- if (!isRecord(input)) throw new Error(`checks route_inventory expected_routes[${index}] must be a string or object.`);
10412
+ if (!isRecord2(input)) throw new Error(`checks route_inventory expected_routes[${index}] must be a string or object.`);
10120
10413
  return {
10121
10414
  name: stringValue3(input.name),
10122
10415
  path: normalizeRouteInventoryPath(input.path, `checks route_inventory expected_routes[${index}]`)
@@ -10195,7 +10488,7 @@ function normalizeHttpStatusBodyJsonAssertions(value, label) {
10195
10488
  if (!path8) throw new Error(`${itemLabel} path must not be empty.`);
10196
10489
  return { path: path8, exists: true };
10197
10490
  }
10198
- if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
10491
+ if (!isRecord2(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
10199
10492
  const path7 = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
10200
10493
  if (!path7) throw new Error(`${itemLabel}.path is required.`);
10201
10494
  const assertion = {
@@ -10233,7 +10526,7 @@ function dialogCountFieldForCheckType(type) {
10233
10526
  return "dialog_count";
10234
10527
  }
10235
10528
  function normalizeCheck(input, index) {
10236
- if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
10529
+ if (!isRecord2(input)) throw new Error(`checks[${index}] must be an object.`);
10237
10530
  const type = stringValue3(input.type);
10238
10531
  if (!type) throw new Error(`checks[${index}].type is required.`);
10239
10532
  if (!isSupportedCheckType(type)) {
@@ -10398,7 +10691,7 @@ function normalizeFailurePolicy(input) {
10398
10691
  configuration_error: "fail",
10399
10692
  needs_human_review: "fail"
10400
10693
  };
10401
- if (!isRecord(input)) return defaults;
10694
+ if (!isRecord2(input)) return defaults;
10402
10695
  const next = { ...defaults };
10403
10696
  for (const [key, value] of Object.entries(input)) {
10404
10697
  if (!RIDDLE_PROOF_PROFILE_STATUSES.includes(key)) continue;
@@ -10409,12 +10702,12 @@ function normalizeFailurePolicy(input) {
10409
10702
  return next;
10410
10703
  }
10411
10704
  function normalizeRiddleProofProfile(input, options = {}) {
10412
- if (!isRecord(input)) throw new Error("profile must be a JSON object.");
10705
+ if (!isRecord2(input)) throw new Error("profile must be a JSON object.");
10413
10706
  const version = stringValue3(input.version) || RIDDLE_PROOF_PROFILE_VERSION;
10414
10707
  if (version !== RIDDLE_PROOF_PROFILE_VERSION) {
10415
10708
  throw new Error(`Unsupported profile version ${version}. Expected ${RIDDLE_PROOF_PROFILE_VERSION}.`);
10416
10709
  }
10417
- const targetInput = isRecord(input.target) ? input.target : {};
10710
+ const targetInput = isRecord2(input.target) ? input.target : {};
10418
10711
  const checks = Array.isArray(input.checks) ? input.checks.map(normalizeCheck) : [];
10419
10712
  if (!checks.length) throw new Error("profile.checks must contain at least one check.");
10420
10713
  const targetUrl = stringValue3(options.url) || stringValue3(targetInput.url);
@@ -10545,19 +10838,19 @@ function linkStatusContentTypeOk(result, check) {
10545
10838
  function httpStatusBodyContainsFailures(result, check) {
10546
10839
  const expected = check.body_contains?.filter(Boolean) ?? [];
10547
10840
  if (!expected.length) return [];
10548
- const observed = isRecord(result.body_contains) ? result.body_contains : {};
10841
+ const observed = isRecord2(result.body_contains) ? result.body_contains : {};
10549
10842
  return expected.filter((text) => observed[text] !== true);
10550
10843
  }
10551
10844
  function httpStatusBodyNotContainsFailures(result, check) {
10552
10845
  const forbidden = check.body_not_contains?.filter(Boolean) ?? [];
10553
10846
  if (!forbidden.length) return [];
10554
- const observed = isRecord(result.body_not_contains) ? result.body_not_contains : {};
10847
+ const observed = isRecord2(result.body_not_contains) ? result.body_not_contains : {};
10555
10848
  return forbidden.filter((text) => observed[text] !== false);
10556
10849
  }
10557
10850
  function httpStatusBodyNotPatternFailures(result, check) {
10558
10851
  const forbidden = check.body_not_patterns?.filter(Boolean) ?? [];
10559
10852
  if (!forbidden.length) return [];
10560
- const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
10853
+ const observed = isRecord2(result.body_not_patterns) ? result.body_not_patterns : {};
10561
10854
  return forbidden.filter((pattern) => observed[pattern] !== false);
10562
10855
  }
10563
10856
  function httpStatusBodyJsonAssertionFailures(result, check) {
@@ -10573,7 +10866,7 @@ function httpStatusBodyJsonAssertionFailures(result, check) {
10573
10866
  errors: ["body_json_assertions evidence missing"]
10574
10867
  }));
10575
10868
  }
10576
- return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
10869
+ return result.body_json_assertions.filter((assertion) => isRecord2(assertion) && assertion.ok !== true).map((assertion) => ({
10577
10870
  label: stringValue3(assertion.label) || stringValue3(assertion.path) || "json assertion",
10578
10871
  path: stringValue3(assertion.path) || "",
10579
10872
  ok: false,
@@ -10639,7 +10932,7 @@ async function responseBodyText(response) {
10639
10932
  return { text: "", bytes: null };
10640
10933
  }
10641
10934
  function httpStatusRequestBody(check) {
10642
- const headers = isRecord(check.headers) ? Object.fromEntries(Object.entries(check.headers).map(([key, value]) => [key, String(value)]).filter(([key]) => key.trim())) : {};
10935
+ const headers = isRecord2(check.headers) ? Object.fromEntries(Object.entries(check.headers).map(([key, value]) => [key, String(value)]).filter(([key]) => key.trim())) : {};
10643
10936
  if (check.body_json !== void 0) {
10644
10937
  if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
10645
10938
  headers["content-type"] = "application/json";
@@ -10716,11 +11009,11 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
10716
11009
  content_type: stringValue3(result.content_type) ?? null,
10717
11010
  content_length: numberValue2(result.content_length) ?? null,
10718
11011
  bytes: numberValue2(result.bytes) ?? null,
10719
- body_contains: isRecord(result.body_contains) ? Object.fromEntries(Object.entries(result.body_contains).map(([key, value]) => [key, value === true])) : null,
11012
+ body_contains: isRecord2(result.body_contains) ? Object.fromEntries(Object.entries(result.body_contains).map(([key, value]) => [key, value === true])) : null,
10720
11013
  body_contains_missing: bodyContainsMissing,
10721
- body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
11014
+ body_not_contains: isRecord2(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
10722
11015
  body_not_contains_found: bodyNotContainsFound,
10723
- body_not_patterns: isRecord(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
11016
+ body_not_patterns: isRecord2(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
10724
11017
  body_not_patterns_found: bodyNotPatternsFound,
10725
11018
  body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
10726
11019
  body_json_assertions_failed: bodyJsonAssertionsFailed
@@ -10748,7 +11041,7 @@ async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}
10748
11041
  }
10749
11042
  function httpStatusEvidenceForCheck(viewport, check) {
10750
11043
  const evidence = viewport.http_statuses?.[httpStatusKey(check, viewport.url)];
10751
- return isRecord(evidence) ? evidence : void 0;
11044
+ return isRecord2(evidence) ? evidence : void 0;
10752
11045
  }
10753
11046
  function summarizeHttpStatusEvidence(viewport, check) {
10754
11047
  const key = httpStatusKey(check, viewport.url);
@@ -10804,11 +11097,11 @@ function summarizeHttpStatusEvidence(viewport, check) {
10804
11097
  content_type: stringValue3(statusEvidence.content_type) ?? null,
10805
11098
  content_length: numberValue2(statusEvidence.content_length) ?? null,
10806
11099
  bytes: linkStatusObservedBytes(statusEvidence) ?? null,
10807
- body_contains: isRecord(statusEvidence.body_contains) ? toJsonValue(statusEvidence.body_contains) : null,
11100
+ body_contains: isRecord2(statusEvidence.body_contains) ? toJsonValue(statusEvidence.body_contains) : null,
10808
11101
  body_contains_missing: bodyContainsMissing,
10809
- body_not_contains: isRecord(statusEvidence.body_not_contains) ? toJsonValue(statusEvidence.body_not_contains) : null,
11102
+ body_not_contains: isRecord2(statusEvidence.body_not_contains) ? toJsonValue(statusEvidence.body_not_contains) : null,
10810
11103
  body_not_contains_found: bodyNotContainsFound,
10811
- body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
11104
+ body_not_patterns: isRecord2(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
10812
11105
  body_not_patterns_found: bodyNotPatternsFound,
10813
11106
  body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
10814
11107
  body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
@@ -10818,7 +11111,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
10818
11111
  }
10819
11112
  function linkStatusEvidenceForCheck(viewport, check) {
10820
11113
  const evidence = viewport.link_statuses?.[linkStatusSelector(check)];
10821
- return isRecord(evidence) ? evidence : void 0;
11114
+ return isRecord2(evidence) ? evidence : void 0;
10822
11115
  }
10823
11116
  function summarizeLinkStatusEvidence(viewport, check) {
10824
11117
  const linkEvidence = linkStatusEvidenceForCheck(viewport, check);
@@ -10832,7 +11125,7 @@ function summarizeLinkStatusEvidence(viewport, check) {
10832
11125
  failures: [{ code: "link_status_evidence_missing" }]
10833
11126
  };
10834
11127
  }
10835
- const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter(isRecord) : [];
11128
+ const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter(isRecord2) : [];
10836
11129
  const totalCount = numberValue2(linkEvidence.total_count) ?? results.length;
10837
11130
  const resultCount = numberValue2(linkEvidence.result_count) ?? totalCount;
10838
11131
  const storedResultCount = numberValue2(linkEvidence.stored_result_count) ?? results.length;
@@ -10875,7 +11168,7 @@ function summarizeLinkStatusEvidence(viewport, check) {
10875
11168
  if (check.min_count !== void 0 && totalCount < check.min_count) {
10876
11169
  failures.push({ code: "link_status_count_below_minimum", min_count: check.min_count, actual: totalCount });
10877
11170
  }
10878
- const statusCounts = isRecord(linkEvidence.status_counts) ? linkEvidence.status_counts : {};
11171
+ const statusCounts = isRecord2(linkEvidence.status_counts) ? linkEvidence.status_counts : {};
10879
11172
  return {
10880
11173
  viewport: viewport.name,
10881
11174
  selector: linkStatusSelector(check),
@@ -10912,7 +11205,7 @@ function observeWithinKey(check) {
10912
11205
  function textSequenceForCheck(viewport, check) {
10913
11206
  const key = selectorKey(check);
10914
11207
  const sequence = viewport.text_sequences?.[key];
10915
- if (isRecord(sequence)) {
11208
+ if (isRecord2(sequence)) {
10916
11209
  const visibleMatchTexts = Array.isArray(sequence.visible_match_texts) ? sequence.visible_match_texts : [];
10917
11210
  const matchTexts = Array.isArray(sequence.match_texts) ? sequence.match_texts : [];
10918
11211
  const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
@@ -10942,9 +11235,9 @@ function textOrderMatch(texts, expectedTexts) {
10942
11235
  }
10943
11236
  function frameEvidenceForSelector(viewport, selector) {
10944
11237
  const container = viewport.frames?.[selector];
10945
- if (!isRecord(container)) return [];
11238
+ if (!isRecord2(container)) return [];
10946
11239
  const frames = Array.isArray(container.frames) ? container.frames : [];
10947
- return frames.filter((frame) => isRecord(frame));
11240
+ return frames.filter((frame) => isRecord2(frame));
10948
11241
  }
10949
11242
  function frameTextSample(frame) {
10950
11243
  const parts = [
@@ -10988,7 +11281,7 @@ function routeInventoryExpectedRouteSummaries(value, fallback) {
10988
11281
  const path8 = route.trim();
10989
11282
  return path8 ? { path: path8 } : void 0;
10990
11283
  }
10991
- if (!isRecord(route)) return void 0;
11284
+ if (!isRecord2(route)) return void 0;
10992
11285
  const path7 = stringValue3(route.path);
10993
11286
  if (!path7) return void 0;
10994
11287
  const name = stringValue3(route.name);
@@ -11070,11 +11363,11 @@ function textCheckFailureSamples(viewport, check) {
11070
11363
  return fallback ? [fallback] : [];
11071
11364
  }
11072
11365
  function allowedMessageSample(input) {
11073
- if (!isRecord(input)) return String(input || "");
11366
+ if (!isRecord2(input)) return String(input || "");
11074
11367
  const parts = [
11075
11368
  input.text,
11076
11369
  input.message,
11077
- isRecord(input.location) ? input.location.url : void 0
11370
+ isRecord2(input.location) ? input.location.url : void 0
11078
11371
  ];
11079
11372
  return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
11080
11373
  }
@@ -11091,12 +11384,12 @@ function matchesAllowedMessage(input, texts, patterns) {
11091
11384
  return false;
11092
11385
  }
11093
11386
  function consoleEventLocationUrl(input) {
11094
- if (!isRecord(input) || !isRecord(input.location)) return void 0;
11387
+ if (!isRecord2(input) || !isRecord2(input.location)) return void 0;
11095
11388
  return stringValue3(input.location.url);
11096
11389
  }
11097
11390
  function expectedFailedNetworkMockEvents(evidence) {
11098
11391
  return (evidence.network_mocks || []).filter((event) => {
11099
- if (!isRecord(event) || event.ok === false) return false;
11392
+ if (!isRecord2(event) || event.ok === false) return false;
11100
11393
  const status = numberValue2(event.status);
11101
11394
  const isHttpFailure = status !== void 0 && status >= 400;
11102
11395
  const isAbortedMock = event.abort === true && Boolean(stringValue3(event.abort_error_code));
@@ -11126,7 +11419,7 @@ function expectedFailedNetworkMockConsoleEventSummary(event, evidence) {
11126
11419
  abort_error_code: match ? stringValue3(match.abort_error_code) ?? null : null,
11127
11420
  label: match ? stringValue3(match.label) ?? null : null,
11128
11421
  response_label: match ? stringValue3(match.response_label) ?? null : null,
11129
- text: isRecord(event) && typeof event.text === "string" ? event.text.slice(0, 300) : sample.slice(0, 300)
11422
+ text: isRecord2(event) && typeof event.text === "string" ? event.text.slice(0, 300) : sample.slice(0, 300)
11130
11423
  };
11131
11424
  }
11132
11425
  function normalizeRoutePath2(path7) {
@@ -11647,7 +11940,7 @@ function assessCheckFromEvidence(check, evidence) {
11647
11940
  };
11648
11941
  }
11649
11942
  if (check.type === "route_inventory") {
11650
- const inventories = viewports.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory })).filter((item) => isRecord(item.inventory));
11943
+ const inventories = viewports.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory })).filter((item) => isRecord2(item.inventory));
11651
11944
  if (!inventories.length) {
11652
11945
  return {
11653
11946
  type: check.type,
@@ -12093,6 +12386,8 @@ function assessRiddleProofProfileEvidence(profile, evidence, options = {}) {
12093
12386
  const status = profileStatusFromEvidence(profile, evidence, checks);
12094
12387
  const firstViewport = evidence?.viewports?.[0];
12095
12388
  const screenshots = profileScreenshotLabels(evidence?.viewports);
12389
+ const canonicalScreenshots = profileFinalScreenshotLabels(evidence?.viewports);
12390
+ const setupScreenshots = profileAllSetupScreenshotLabels(evidence?.viewports);
12096
12391
  const result = {
12097
12392
  version: RIDDLE_PROOF_PROFILE_RESULT_VERSION,
12098
12393
  profile_name: profile.name,
@@ -12102,6 +12397,8 @@ function assessRiddleProofProfileEvidence(profile, evidence, options = {}) {
12102
12397
  route: routeForViewport(firstViewport, evidence?.target_url),
12103
12398
  artifacts: {
12104
12399
  screenshots,
12400
+ canonical_screenshots: canonicalScreenshots,
12401
+ setup_screenshots: setupScreenshots,
12105
12402
  console: "console.json",
12106
12403
  proof_json: "proof.json",
12107
12404
  dom_summary: "dom-summary.json",
@@ -13420,6 +13717,12 @@ function profileScreenshotLabels(viewports) {
13420
13717
  }
13421
13718
  return labels;
13422
13719
  }
13720
+ function profileFinalScreenshotLabels(viewports) {
13721
+ return (viewports || []).map((viewport) => viewport && viewport.screenshot_label).filter(Boolean);
13722
+ }
13723
+ function profileAllSetupScreenshotLabels(viewports) {
13724
+ return (viewports || []).flatMap((viewport) => profileSetupScreenshotLabels(viewport && viewport.setup_action_results || []));
13725
+ }
13423
13726
  function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewport, finalScreenshotFullPage) {
13424
13727
  const normalizedFinalScreenshotFullPage = finalScreenshotFullPage === undefined
13425
13728
  ? undefined
@@ -14288,6 +14591,8 @@ function assessProfile(profile, evidence) {
14288
14591
  else if (checks.some((check) => check.status === "needs_human_review")) status = "needs_human_review";
14289
14592
  else if (checks.some((check) => check.status === "failed")) status = "product_regression";
14290
14593
  const screenshotLabels = profileScreenshotLabels(viewports);
14594
+ const canonicalScreenshotLabels = profileFinalScreenshotLabels(viewports);
14595
+ const setupScreenshotLabels = profileAllSetupScreenshotLabels(viewports);
14291
14596
  const route = viewports[0] && viewports[0].route ? viewports[0].route : { requested: evidence.target_url, observed: "", matched: false, error: "missing viewport evidence" };
14292
14597
  const passedChecks = checks.filter((check) => check.status === "passed").length;
14293
14598
  const failedChecks = checks.filter((check) => check.status === "failed").length;
@@ -14304,7 +14609,14 @@ function assessProfile(profile, evidence) {
14304
14609
  status,
14305
14610
  baseline_policy: profile.baseline_policy || "invariant_only",
14306
14611
  route,
14307
- artifacts: { screenshots: screenshotLabels, console: "console.json", proof_json: "proof.json", dom_summary: "dom-summary.json" },
14612
+ artifacts: {
14613
+ screenshots: screenshotLabels,
14614
+ canonical_screenshots: canonicalScreenshotLabels,
14615
+ setup_screenshots: setupScreenshotLabels,
14616
+ console: "console.json",
14617
+ proof_json: "proof.json",
14618
+ dom_summary: "dom-summary.json"
14619
+ },
14308
14620
  checks,
14309
14621
  summary,
14310
14622
  captured_at: evidence.captured_at,
@@ -17975,7 +18287,7 @@ function collectRiddleProfileArtifactRefs(input) {
17975
18287
  const indexes = /* @__PURE__ */ new Map();
17976
18288
  const priorities = /* @__PURE__ */ new Map();
17977
18289
  function add(item, source) {
17978
- if (!isRecord(item)) return;
18290
+ if (!isRecord2(item)) return;
17979
18291
  const url = stringValue3(item.url);
17980
18292
  const path7 = stringValue3(item.path);
17981
18293
  const rawName = stringValue3(item.name) || stringValue3(item.filename) || artifactNameFromPath(url || path7) || "";
@@ -18008,7 +18320,7 @@ function collectRiddleProfileArtifactRefs(input) {
18008
18320
  for (const item of value) add(item, source);
18009
18321
  return;
18010
18322
  }
18011
- if (!isRecord(value)) return;
18323
+ if (!isRecord2(value)) return;
18012
18324
  for (const key of ["artifacts", "outputs", "screenshots", "files"]) {
18013
18325
  if (Array.isArray(value[key])) visit(value[key], key);
18014
18326
  }
@@ -18042,82 +18354,944 @@ var PROFILE_SINGLETON_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
18042
18354
  "dom-summary.json"
18043
18355
  ]);
18044
18356
  function extractRiddleProofProfileResult(input) {
18045
- if (!isRecord(input)) return void 0;
18357
+ if (!isRecord2(input)) return void 0;
18046
18358
  if (input.version === RIDDLE_PROOF_PROFILE_RESULT_VERSION) return input;
18047
18359
  const candidates = [
18048
18360
  input.result,
18049
18361
  input.return_value,
18050
18362
  input.value,
18051
18363
  input.profile_result,
18052
- isRecord(input["proof.json"]) ? input["proof.json"] : void 0,
18053
- isRecord(input._proof_json) ? input._proof_json : void 0
18364
+ isRecord2(input["proof.json"]) ? input["proof.json"] : void 0,
18365
+ isRecord2(input._proof_json) ? input._proof_json : void 0
18054
18366
  ];
18055
18367
  for (const candidate of candidates) {
18056
18368
  const result = extractRiddleProofProfileResult(candidate);
18057
18369
  if (result) return result;
18058
18370
  }
18059
- if (isRecord(input._artifact_json)) {
18371
+ if (isRecord2(input._artifact_json)) {
18060
18372
  return extractRiddleProofProfileResult(input._artifact_json["proof.json"]);
18061
18373
  }
18062
18374
  return void 0;
18063
18375
  }
18064
18376
 
18065
- // src/pr-comment.ts
18066
- var RIDDLE_PROOF_PR_COMMENT_MARKER = "<!-- riddle-proof:pr-comment:v1 -->";
18067
- function asRecord2(value) {
18068
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
18377
+ // src/change-proof.ts
18378
+ var RIDDLE_PROOF_CHANGE_RESULT_VERSION = "riddle-proof.change-result.v1";
18379
+ var RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION = "riddle-proof.change-receipt.v1";
18380
+ var RIDDLE_PROOF_CHANGE_RECEIPT_VERSION = "riddle-proof.change-receipt.v2";
18381
+ var RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION = "riddle-proof.handoff-receipt.v1";
18382
+ var DEFAULT_BEFORE_STATUSES = ["passed", "product_regression"];
18383
+ var DEFAULT_AFTER_STATUSES = ["passed"];
18384
+ var DEFAULT_PROFILE_STATUS_TRANSITION_BEFORE = ["product_regression"];
18385
+ var DEFAULT_PROFILE_STATUS_TRANSITION_AFTER = ["passed"];
18386
+ var DEFAULT_CHECK_STATUS_TRANSITION_BEFORE = ["failed"];
18387
+ var DEFAULT_CHECK_STATUS_TRANSITION_AFTER = ["passed"];
18388
+ function listValue(value, fallback) {
18389
+ if (Array.isArray(value)) return value;
18390
+ if (value === void 0) return fallback;
18391
+ return [value];
18069
18392
  }
18070
- function asArray(value) {
18071
- return Array.isArray(value) ? value : [];
18393
+ function includesValue(allowed, observed) {
18394
+ return observed !== void 0 && allowed.includes(observed);
18072
18395
  }
18073
- function stringValue4(value) {
18074
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
18396
+ function groupResult(side, contract, result) {
18397
+ const fallback = side === "before" ? DEFAULT_BEFORE_STATUSES : DEFAULT_AFTER_STATUSES;
18398
+ const requiredStatus = listValue(contract?.required_status, fallback);
18399
+ const label = contract?.label || side;
18400
+ if (!result) {
18401
+ return {
18402
+ side,
18403
+ label,
18404
+ ok: false,
18405
+ status: "missing",
18406
+ required_status: requiredStatus,
18407
+ message: `${label} profile result is missing.`
18408
+ };
18409
+ }
18410
+ const ok = includesValue(requiredStatus, result.status);
18411
+ return {
18412
+ side,
18413
+ label,
18414
+ ok,
18415
+ profile_name: result.profile_name,
18416
+ status: result.status,
18417
+ required_status: requiredStatus,
18418
+ summary: result.summary,
18419
+ message: ok ? void 0 : `${label} profile status ${result.status} did not match required status ${requiredStatus.join(", ")}.`
18420
+ };
18075
18421
  }
18076
- function numberValue3(value) {
18077
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
18422
+ function findCheck(result, delta) {
18423
+ return result.checks.find((check) => {
18424
+ if (delta.check_label && check.label !== delta.check_label) return false;
18425
+ if (delta.check_type && check.type !== delta.check_type) return false;
18426
+ return Boolean(delta.check_label || delta.check_type);
18427
+ });
18078
18428
  }
18079
- function booleanValue3(value) {
18080
- return typeof value === "boolean" ? value : void 0;
18429
+ function profileStatusTransitionResult(delta, before, after) {
18430
+ const label = delta.label || "profile-status-transition";
18431
+ if (!before || !after) {
18432
+ return {
18433
+ type: delta.type,
18434
+ label,
18435
+ status: "proof_insufficient",
18436
+ before_observed: before?.status,
18437
+ after_observed: after?.status,
18438
+ message: `${label} needs both before and after profile results.`
18439
+ };
18440
+ }
18441
+ const beforeAllowed = listValue(delta.before_status, DEFAULT_PROFILE_STATUS_TRANSITION_BEFORE);
18442
+ const afterAllowed = listValue(delta.after_status, DEFAULT_PROFILE_STATUS_TRANSITION_AFTER);
18443
+ const passed = includesValue(beforeAllowed, before.status) && includesValue(afterAllowed, after.status);
18444
+ return {
18445
+ type: delta.type,
18446
+ label,
18447
+ status: passed ? "passed" : "failed",
18448
+ before_observed: before.status,
18449
+ after_observed: after.status,
18450
+ message: passed ? void 0 : `${label} expected before ${beforeAllowed.join(", ")} and after ${afterAllowed.join(", ")}, got ${before.status} -> ${after.status}.`
18451
+ };
18081
18452
  }
18082
- function firstStringValue2(...values) {
18083
- for (const value of values) {
18084
- const text = stringValue4(value);
18085
- if (text) return text;
18453
+ function checkStatusTransitionResult(delta, before, after) {
18454
+ const label = delta.label || delta.check_label || delta.check_type || "check-status-transition";
18455
+ if (!delta.check_label && !delta.check_type) {
18456
+ return {
18457
+ type: delta.type,
18458
+ label,
18459
+ status: "configuration_error",
18460
+ message: `${label} needs check_label or check_type.`
18461
+ };
18086
18462
  }
18087
- return void 0;
18463
+ if (!before || !after) {
18464
+ return {
18465
+ type: delta.type,
18466
+ label,
18467
+ status: "proof_insufficient",
18468
+ before_observed: before?.status,
18469
+ after_observed: after?.status,
18470
+ message: `${label} needs both before and after profile results.`
18471
+ };
18472
+ }
18473
+ const beforeCheck = findCheck(before, delta);
18474
+ const afterCheck = findCheck(after, delta);
18475
+ if (!beforeCheck || !afterCheck) {
18476
+ return {
18477
+ type: delta.type,
18478
+ label,
18479
+ status: "proof_insufficient",
18480
+ before_observed: beforeCheck?.status,
18481
+ after_observed: afterCheck?.status,
18482
+ message: `${label} was not present in ${!beforeCheck && !afterCheck ? "before or after" : !beforeCheck ? "before" : "after"} profile checks.`
18483
+ };
18484
+ }
18485
+ const beforeAllowed = listValue(delta.before_status, DEFAULT_CHECK_STATUS_TRANSITION_BEFORE);
18486
+ const afterAllowed = listValue(delta.after_status, DEFAULT_CHECK_STATUS_TRANSITION_AFTER);
18487
+ const passed = includesValue(beforeAllowed, beforeCheck.status) && includesValue(afterAllowed, afterCheck.status);
18488
+ return {
18489
+ type: delta.type,
18490
+ label,
18491
+ status: passed ? "passed" : "failed",
18492
+ before_observed: beforeCheck.status,
18493
+ after_observed: afterCheck.status,
18494
+ message: passed ? void 0 : `${label} expected before ${beforeAllowed.join(", ")} and after ${afterAllowed.join(", ")}, got ${beforeCheck.status} -> ${afterCheck.status}.`
18495
+ };
18088
18496
  }
18089
- function artifactKind(name, url) {
18090
- const target = `${name} ${url}`.toLowerCase();
18091
- if (/\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/.test(target)) return "image";
18092
- if (/\.(json|har|txt|md|html|log)(\?|#|$)/.test(target)) return "data";
18093
- return "artifact";
18497
+ function assessDelta(delta, before, after) {
18498
+ if (delta.type === "profile_status_transition") {
18499
+ return profileStatusTransitionResult(delta, before, after);
18500
+ }
18501
+ return checkStatusTransitionResult(delta, before, after);
18094
18502
  }
18095
- function artifactDisplayName(value, fallback) {
18096
- const raw = stringValue4(value);
18097
- if (raw) return raw;
18098
- return fallback;
18503
+ function profileResultFromObservation(observation) {
18504
+ return observation?.proof?.result;
18099
18505
  }
18100
- function collectArtifacts(runResponse) {
18101
- const proofResult = asRecord2(runResponse.proofResult);
18102
- const outputs = asArray(proofResult.outputs);
18103
- const artifacts = [];
18104
- const seen = /* @__PURE__ */ new Set();
18105
- for (const [index, item] of outputs.entries()) {
18106
- const artifact = asRecord2(item);
18107
- const url = stringValue4(artifact.url);
18108
- if (!url || seen.has(url)) continue;
18109
- seen.add(url);
18110
- const name = artifactDisplayName(artifact.name, `artifact-${index + 1}`);
18111
- artifacts.push({
18112
- name,
18113
- url,
18114
- kind: artifactKind(name, url),
18115
- size_bytes: numberValue3(artifact.size)
18116
- });
18506
+ function previewReceiptCoversTarget(previewUrl, targetUrl) {
18507
+ try {
18508
+ const preview = new URL(previewUrl);
18509
+ const target = new URL(targetUrl);
18510
+ const previewPath = preview.pathname.replace(/\/+$/u, "");
18511
+ return preview.origin === target.origin && (target.pathname === previewPath || target.pathname.startsWith(`${previewPath}/`));
18512
+ } catch {
18513
+ return false;
18117
18514
  }
18118
- return artifacts;
18119
18515
  }
18120
- function collectProfileArtifacts(result) {
18516
+ function sourceBindingResult(side, requirement, observation, expectedRevision, evaluatedAt) {
18517
+ const expected = expectedRevision || requirement?.expected_git_revision;
18518
+ const required = Boolean(
18519
+ requirement?.preview_receipt_required || requirement?.require_clean_source || requirement?.require_content_digest || requirement?.expected_git_revision || expected
18520
+ );
18521
+ if (!required) return { side, required: false, ok: true, status: "not_required" };
18522
+ if (!observation) {
18523
+ return {
18524
+ side,
18525
+ required: true,
18526
+ ok: false,
18527
+ status: "missing",
18528
+ expected_git_revision: expected,
18529
+ message: `${side} source binding requires an Observation receipt.`
18530
+ };
18531
+ }
18532
+ const preview = observation.target.preview;
18533
+ if (requirement?.preview_receipt_required && !preview) {
18534
+ return {
18535
+ side,
18536
+ required: true,
18537
+ ok: false,
18538
+ status: "missing",
18539
+ expected_git_revision: expected,
18540
+ message: `${side} source binding requires a Preview receipt.`
18541
+ };
18542
+ }
18543
+ if (preview && !previewReceiptCoversTarget(preview.url, observation.target.url)) {
18544
+ return {
18545
+ side,
18546
+ required: true,
18547
+ ok: false,
18548
+ status: "mismatched",
18549
+ expected_git_revision: expected,
18550
+ observed_git_revision: preview.source.git_revision || observation.source.git_revision,
18551
+ content_digest: preview.content_digest,
18552
+ preview_id: preview.preview_id,
18553
+ preview_url: preview.url,
18554
+ target_url: observation.target.url,
18555
+ message: `${side} observation target is not contained by Preview ${preview.preview_id}.`
18556
+ };
18557
+ }
18558
+ const observed = preview?.source.git_revision || observation.source.git_revision;
18559
+ const digestRequired = requirement?.require_content_digest === true || requirement?.preview_receipt_required === true;
18560
+ if (digestRequired && !preview?.content_digest) {
18561
+ return {
18562
+ side,
18563
+ required: true,
18564
+ ok: false,
18565
+ status: "missing",
18566
+ expected_git_revision: expected,
18567
+ observed_git_revision: observed,
18568
+ preview_id: preview?.preview_id,
18569
+ preview_url: preview?.url,
18570
+ target_url: observation.target.url,
18571
+ message: `${side} Preview receipt is missing a content digest.`
18572
+ };
18573
+ }
18574
+ if (expected && !observed) {
18575
+ return {
18576
+ side,
18577
+ required: true,
18578
+ ok: false,
18579
+ status: "missing",
18580
+ expected_git_revision: expected,
18581
+ content_digest: preview?.content_digest,
18582
+ preview_id: preview?.preview_id,
18583
+ preview_url: preview?.url,
18584
+ target_url: observation.target.url,
18585
+ message: `${side} evidence is missing its source Git revision.`
18586
+ };
18587
+ }
18588
+ if (expected && observed !== expected) {
18589
+ return {
18590
+ side,
18591
+ required: true,
18592
+ ok: false,
18593
+ status: "mismatched",
18594
+ expected_git_revision: expected,
18595
+ observed_git_revision: observed,
18596
+ content_digest: preview?.content_digest,
18597
+ preview_id: preview?.preview_id,
18598
+ preview_url: preview?.url,
18599
+ target_url: observation.target.url,
18600
+ message: `${side} evidence came from Git revision ${observed}, not expected revision ${expected}.`
18601
+ };
18602
+ }
18603
+ const cleanRequired = requirement?.require_clean_source === true || requirement?.preview_receipt_required === true;
18604
+ const dirty = preview?.source.dirty ?? observation.source.dirty;
18605
+ if (cleanRequired && dirty !== false) {
18606
+ return {
18607
+ side,
18608
+ required: true,
18609
+ ok: false,
18610
+ status: dirty === true ? "mismatched" : "missing",
18611
+ expected_git_revision: expected,
18612
+ observed_git_revision: observed,
18613
+ content_digest: preview?.content_digest,
18614
+ preview_id: preview?.preview_id,
18615
+ preview_url: preview?.url,
18616
+ target_url: observation.target.url,
18617
+ message: dirty === true ? `${side} evidence was built from a dirty worktree and is not bound only to its Git revision.` : `${side} evidence does not record whether its source worktree was clean.`
18618
+ };
18619
+ }
18620
+ const evaluatedMs = Date.parse(evaluatedAt);
18621
+ const expiresMs = preview?.expires_at ? Date.parse(preview.expires_at) : Number.NaN;
18622
+ if (preview && Number.isFinite(evaluatedMs) && Number.isFinite(expiresMs) && expiresMs <= evaluatedMs) {
18623
+ return {
18624
+ side,
18625
+ required: true,
18626
+ ok: false,
18627
+ status: "stale",
18628
+ expected_git_revision: expected,
18629
+ observed_git_revision: observed,
18630
+ content_digest: preview.content_digest,
18631
+ preview_id: preview.preview_id,
18632
+ preview_url: preview.url,
18633
+ target_url: observation.target.url,
18634
+ expires_at: preview.expires_at,
18635
+ message: `${side} Preview receipt expired at ${preview.expires_at}.`
18636
+ };
18637
+ }
18638
+ return {
18639
+ side,
18640
+ required: true,
18641
+ ok: true,
18642
+ status: "matched",
18643
+ expected_git_revision: expected,
18644
+ observed_git_revision: observed,
18645
+ content_digest: preview?.content_digest,
18646
+ preview_id: preview?.preview_id,
18647
+ preview_url: preview?.url,
18648
+ target_url: observation.target.url,
18649
+ expires_at: preview?.expires_at
18650
+ };
18651
+ }
18652
+ function collapsedChangeStatus(groups, deltas, sourceBindings) {
18653
+ const groupResults = [groups.before, groups.after];
18654
+ if (groupResults.some((group) => group.status === "environment_blocked")) return "environment_blocked";
18655
+ if (groupResults.some((group) => group.status === "configuration_error")) return "configuration_error";
18656
+ if (deltas.some((delta) => delta.status === "configuration_error")) return "configuration_error";
18657
+ if (groupResults.some((group) => group.status === "needs_human_review")) return "needs_human_review";
18658
+ if (groupResults.some((group) => group.status === "missing" || group.status === "proof_insufficient")) return "proof_insufficient";
18659
+ if ([sourceBindings.before, sourceBindings.after].some((binding) => binding.required && !binding.ok)) return "proof_insufficient";
18660
+ if (!deltas.length || deltas.some((delta) => delta.status === "proof_insufficient")) return "proof_insufficient";
18661
+ if (groupResults.some((group) => !group.ok)) return "product_regression";
18662
+ if (deltas.some((delta) => delta.status === "failed")) return "product_regression";
18663
+ return "passed";
18664
+ }
18665
+ function changeSummary(name, status, deltas) {
18666
+ if (status === "passed") return `${name} passed ${deltas.length} change delta(s).`;
18667
+ if (status === "environment_blocked") return `${name} could not compare reliable evidence because an environment was blocked.`;
18668
+ if (status === "configuration_error") return `${name} has an invalid change proof contract.`;
18669
+ if (status === "needs_human_review") return `${name} needs human review before the change proof can pass.`;
18670
+ if (status === "proof_insufficient") return `${name} did not produce enough before/after evidence for a change proof.`;
18671
+ return `${name} failed ${deltas.filter((delta) => delta.status === "failed").length} change delta(s).`;
18672
+ }
18673
+ function assessRiddleProofChange(contract, input) {
18674
+ const beforeResult = input.before_result || profileResultFromObservation(input.before_observation);
18675
+ const afterResult = input.after_result || profileResultFromObservation(input.after_observation);
18676
+ const groups = {
18677
+ before: groupResult("before", contract.before, beforeResult),
18678
+ after: groupResult("after", contract.after, afterResult)
18679
+ };
18680
+ const deltas = (contract.deltas || []).map((delta) => assessDelta(delta, beforeResult, afterResult));
18681
+ const evaluatedAt = input.evaluated_at || (/* @__PURE__ */ new Date()).toISOString();
18682
+ const sourceBindings = {
18683
+ before: sourceBindingResult(
18684
+ "before",
18685
+ contract.source_binding?.before,
18686
+ input.before_observation,
18687
+ input.expected_source_revisions?.before,
18688
+ evaluatedAt
18689
+ ),
18690
+ after: sourceBindingResult(
18691
+ "after",
18692
+ contract.source_binding?.after,
18693
+ input.after_observation,
18694
+ input.expected_source_revisions?.after,
18695
+ evaluatedAt
18696
+ )
18697
+ };
18698
+ const status = collapsedChangeStatus(groups, deltas, sourceBindings);
18699
+ return {
18700
+ version: RIDDLE_PROOF_CHANGE_RESULT_VERSION,
18701
+ contract_name: contract.name,
18702
+ status,
18703
+ groups,
18704
+ deltas,
18705
+ source_bindings: sourceBindings,
18706
+ summary: changeSummary(contract.name, status, deltas),
18707
+ metadata: contract.metadata
18708
+ };
18709
+ }
18710
+ function changeReceiptVerdict(status) {
18711
+ if (status === "passed") return "mergeable";
18712
+ if (status === "environment_blocked") return "environment_blocked";
18713
+ if (status === "configuration_error") return "configuration_error";
18714
+ if (status === "needs_human_review") return "needs_human_review";
18715
+ if (status === "proof_insufficient") return "proof_insufficient";
18716
+ return "not_mergeable";
18717
+ }
18718
+ function profileCheckCounts2(result) {
18719
+ const counts = {
18720
+ total: result.checks.length,
18721
+ passed: 0,
18722
+ failed: 0,
18723
+ skipped: 0,
18724
+ needs_human_review: 0
18725
+ };
18726
+ for (const check of result.checks) {
18727
+ if (check.status === "passed") counts.passed += 1;
18728
+ if (check.status === "failed") counts.failed += 1;
18729
+ if (check.status === "skipped") counts.skipped += 1;
18730
+ if (check.status === "needs_human_review") counts.needs_human_review += 1;
18731
+ }
18732
+ return counts;
18733
+ }
18734
+ function artifactIsScreenshot(artifact) {
18735
+ if (artifact.kind !== "image") return false;
18736
+ return /screenshot|\.png|\.jpe?g|\.webp|\.gif|\.avif|\.svg/i.test([artifact.name, artifact.url, artifact.path].filter(Boolean).join(" "));
18737
+ }
18738
+ function metadataStringList(metadata, key) {
18739
+ const value = metadata?.[key];
18740
+ if (!Array.isArray(value)) return [];
18741
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
18742
+ }
18743
+ function changeRecommendation(verdict) {
18744
+ if (verdict === "mergeable") {
18745
+ return {
18746
+ merge_recommended: true,
18747
+ verdict,
18748
+ label: "Merge recommended",
18749
+ reason: "The declared before/after delta contract passed with sufficient bound evidence."
18750
+ };
18751
+ }
18752
+ const reason = verdict === "not_mergeable" ? "The declared change delta did not pass." : verdict === "environment_blocked" ? "The environment blocked reliable evidence collection." : verdict === "needs_human_review" ? "The evidence requires human review." : verdict === "configuration_error" ? "The change contract is invalid." : "The change proof did not produce sufficient bound evidence.";
18753
+ return { merge_recommended: false, verdict, label: "Merge not recommended", reason };
18754
+ }
18755
+ function noShippingAuthorization() {
18756
+ return { status: "not_granted", authorized: false, source: "none" };
18757
+ }
18758
+ function recommendationMatches(actual, expected) {
18759
+ return actual?.merge_recommended === expected.merge_recommended && actual.verdict === expected.verdict && actual.label === expected.label && actual.reason === expected.reason;
18760
+ }
18761
+ function assertShippingAuthorizationConsistent(authorization, receiptName) {
18762
+ if (!authorization) throw new Error(`${receiptName} shipping authorization is required.`);
18763
+ if (authorization.status !== "not_granted" && authorization.status !== "granted") {
18764
+ throw new Error(`${receiptName} shipping authorization status is invalid.`);
18765
+ }
18766
+ if (authorization.source !== "none" && authorization.source !== "human" && authorization.source !== "automation") {
18767
+ throw new Error(`${receiptName} shipping authorization source is invalid.`);
18768
+ }
18769
+ const granted = authorization.status === "granted";
18770
+ if (authorization.authorized !== granted) {
18771
+ throw new Error(`${receiptName} shipping authorization status and authorized flag must agree.`);
18772
+ }
18773
+ if (authorization.source === "none" === granted) {
18774
+ throw new Error(`${receiptName} shipping authorization source must identify an authorizer only when granted.`);
18775
+ }
18776
+ }
18777
+ function observationArtifactMatches(actual, expected) {
18778
+ if (!actual || !expected) return actual === expected;
18779
+ return actual.name === expected.name && actual.role === expected.role && actual.url === expected.url && actual.path === expected.path && actual.kind === expected.kind && actual.content_type === expected.content_type && actual.source === expected.source;
18780
+ }
18781
+ function observationExecutor(result) {
18782
+ return result.runner === "local-playwright" ? { kind: "local_playwright", runner: result.runner } : {
18783
+ kind: "riddle_hosted",
18784
+ runner: result.runner,
18785
+ job_id: result.riddle?.job_id
18786
+ };
18787
+ }
18788
+ function observationForReceiptSide(side, result, source, sourceIdentity) {
18789
+ if (!result) throw new Error(`${side}_result or ${side}_observation is required.`);
18790
+ if (!source) throw new Error(`${side}_source or ${side}_observation is required.`);
18791
+ return createRiddleProofObservationReceipt({
18792
+ comparison_role: side,
18793
+ executor: observationExecutor(result),
18794
+ target: { kind: "url", url: source },
18795
+ source: sourceIdentity,
18796
+ profile_result: result,
18797
+ execution: result.riddle?.execution,
18798
+ publication: source.startsWith("http") ? { kind: result.runner === "local-playwright" ? "other" : "riddle_cdn", url: source } : { kind: "local", path: source }
18799
+ });
18800
+ }
18801
+ function createRiddleProofChangeReceipt(input) {
18802
+ const before = input.before_observation ? parseRiddleProofObservationReceipt(input.before_observation) : observationForReceiptSide("before", input.before_result, input.before_source, input.before_source_identity);
18803
+ const after = input.after_observation ? parseRiddleProofObservationReceipt(input.after_observation) : observationForReceiptSide("after", input.after_result, input.after_source, input.after_source_identity);
18804
+ const verdict = changeReceiptVerdict(input.result.status);
18805
+ const shippingAuthorization = input.shipping_authorization || noShippingAuthorization();
18806
+ assertShippingAuthorizationConsistent(shippingAuthorization, "Change receipt");
18807
+ return {
18808
+ version: RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
18809
+ contract_name: input.result.contract_name,
18810
+ profile_name: input.profile_name,
18811
+ status: input.result.status,
18812
+ verdict,
18813
+ summary: input.result.summary,
18814
+ before,
18815
+ after,
18816
+ groups: input.result.groups,
18817
+ source_bindings: input.result.source_bindings,
18818
+ deltas: input.result.deltas.map((delta) => ({
18819
+ type: delta.type,
18820
+ label: delta.label,
18821
+ status: delta.status,
18822
+ before_observed: delta.before_observed,
18823
+ after_observed: delta.after_observed,
18824
+ message: delta.message
18825
+ })),
18826
+ recommendation: changeRecommendation(verdict),
18827
+ shipping_authorization: shippingAuthorization,
18828
+ proves: metadataStringList(input.contract.metadata, "required_receipts"),
18829
+ does_not_prove: metadataStringList(input.contract.metadata, "does_not_prove"),
18830
+ metadata: input.result.metadata
18831
+ };
18832
+ }
18833
+ function isRecord3(value) {
18834
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
18835
+ }
18836
+ function migratedObservationFromLegacySide(side, legacy) {
18837
+ const legacyArtifacts = legacy.artifacts.map((artifact) => ({
18838
+ name: artifact.name,
18839
+ url: artifact.url,
18840
+ path: artifact.path,
18841
+ source: artifact.source,
18842
+ kind: artifact.kind,
18843
+ role: legacy.screenshots[0] && artifactTarget(artifact) === artifactTarget(legacy.screenshots[0]) ? "canonical_screenshot" : artifact.kind === "image" ? "diagnostic" : artifact.kind === "data" ? "data" : "artifact"
18844
+ }));
18845
+ const canonicalScreenshot = legacyArtifacts.find((artifact) => artifact.role === "canonical_screenshot");
18846
+ return {
18847
+ version: "riddle-proof.observation-receipt.v1",
18848
+ observation_id: `obs_${side}_migrated`,
18849
+ comparison_role: side,
18850
+ executor: { kind: "other", runner: "legacy-change-receipt" },
18851
+ target: { kind: "url", url: legacy.source },
18852
+ source: {},
18853
+ captured_at: legacy.captured_at || (/* @__PURE__ */ new Date(0)).toISOString(),
18854
+ artifacts: legacyArtifacts,
18855
+ canonical_screenshot: canonicalScreenshot,
18856
+ profile_summary: {
18857
+ profile_name: legacy.profile_name,
18858
+ status: legacy.status,
18859
+ summary: legacy.summary,
18860
+ route: legacy.route,
18861
+ checks: legacy.checks
18862
+ },
18863
+ metadata: { migrated_from: RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION }
18864
+ };
18865
+ }
18866
+ function migrateRiddleProofChangeReceipt(legacy) {
18867
+ const recommendation = changeRecommendation(legacy.verdict);
18868
+ return {
18869
+ version: RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
18870
+ contract_name: legacy.contract_name,
18871
+ profile_name: legacy.profile_name,
18872
+ status: legacy.status,
18873
+ verdict: legacy.verdict,
18874
+ summary: legacy.summary,
18875
+ before: migratedObservationFromLegacySide("before", legacy.before),
18876
+ after: migratedObservationFromLegacySide("after", legacy.after),
18877
+ groups: {
18878
+ before: {
18879
+ side: "before",
18880
+ label: "before",
18881
+ ok: legacy.before.status === "passed" || legacy.before.status === "product_regression",
18882
+ profile_name: legacy.before.profile_name,
18883
+ status: legacy.before.status,
18884
+ required_status: DEFAULT_BEFORE_STATUSES,
18885
+ summary: legacy.before.summary
18886
+ },
18887
+ after: {
18888
+ side: "after",
18889
+ label: "after",
18890
+ ok: legacy.after.status === "passed",
18891
+ profile_name: legacy.after.profile_name,
18892
+ status: legacy.after.status,
18893
+ required_status: DEFAULT_AFTER_STATUSES,
18894
+ summary: legacy.after.summary
18895
+ }
18896
+ },
18897
+ source_bindings: {
18898
+ before: { side: "before", required: false, ok: true, status: "not_required" },
18899
+ after: { side: "after", required: false, ok: true, status: "not_required" }
18900
+ },
18901
+ deltas: legacy.deltas,
18902
+ recommendation,
18903
+ shipping_authorization: noShippingAuthorization(),
18904
+ proves: legacy.proves,
18905
+ does_not_prove: legacy.does_not_prove,
18906
+ metadata: legacy.metadata
18907
+ };
18908
+ }
18909
+ function parseRiddleProofChangeReceipt(value) {
18910
+ if (!isRecord3(value)) throw new Error("Change receipt must be an object.");
18911
+ if (value.version === RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION) {
18912
+ return migrateRiddleProofChangeReceipt(value);
18913
+ }
18914
+ if (value.version !== RIDDLE_PROOF_CHANGE_RECEIPT_VERSION) {
18915
+ throw new Error(`Unsupported Change receipt version ${String(value.version || "missing")}.`);
18916
+ }
18917
+ const receipt = value;
18918
+ if (!RIDDLE_PROOF_PROFILE_STATUSES.includes(receipt.status)) {
18919
+ throw new Error(`Unsupported Change receipt status ${String(receipt.status || "missing")}.`);
18920
+ }
18921
+ parseRiddleProofObservationReceipt(receipt.before);
18922
+ parseRiddleProofObservationReceipt(receipt.after);
18923
+ if (receipt.before.comparison_role !== "before" || receipt.after.comparison_role !== "after") {
18924
+ throw new Error("Change receipt observations must preserve their before and after comparison roles.");
18925
+ }
18926
+ const expectedVerdict = changeReceiptVerdict(receipt.status);
18927
+ if (receipt.verdict !== expectedVerdict) {
18928
+ throw new Error("Change receipt verdict must match its evaluated status.");
18929
+ }
18930
+ if (!recommendationMatches(receipt.recommendation, changeRecommendation(expectedVerdict))) {
18931
+ throw new Error("Change receipt recommendation must be derived from the Change receipt verdict.");
18932
+ }
18933
+ assertShippingAuthorizationConsistent(receipt.shipping_authorization, "Change receipt");
18934
+ return receipt;
18935
+ }
18936
+ function createRiddleProofHandoffReceipt(changeReceipt, options = {}) {
18937
+ const change = parseRiddleProofChangeReceipt(changeReceipt);
18938
+ const authorization = options.shipping_authorization || change.shipping_authorization;
18939
+ assertShippingAuthorizationConsistent(authorization, "Handoff receipt");
18940
+ return {
18941
+ version: RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION,
18942
+ change,
18943
+ verdict: change.verdict,
18944
+ recommendation: change.recommendation,
18945
+ shipping_authorization: authorization,
18946
+ canonical_pair: {
18947
+ before: change.before.canonical_screenshot,
18948
+ after: change.after.canonical_screenshot
18949
+ },
18950
+ created_at: options.created_at || (/* @__PURE__ */ new Date()).toISOString()
18951
+ };
18952
+ }
18953
+ function parseRiddleProofHandoffReceipt(value) {
18954
+ if (!isRecord3(value) || value.version !== RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION) {
18955
+ throw new Error(`Unsupported Handoff receipt version ${String(isRecord3(value) ? value.version || "missing" : "missing")}.`);
18956
+ }
18957
+ const receipt = value;
18958
+ if (!Number.isFinite(Date.parse(receipt.created_at))) {
18959
+ throw new Error("Handoff receipt created_at must be a valid timestamp.");
18960
+ }
18961
+ const change = parseRiddleProofChangeReceipt(receipt.change);
18962
+ if (receipt.verdict !== change.verdict || !recommendationMatches(receipt.recommendation, change.recommendation)) {
18963
+ throw new Error("Handoff receipt must preserve the Change receipt verdict and recommendation.");
18964
+ }
18965
+ if (!observationArtifactMatches(receipt.canonical_pair?.before, change.before.canonical_screenshot) || !observationArtifactMatches(receipt.canonical_pair?.after, change.after.canonical_screenshot)) {
18966
+ throw new Error("Handoff receipt canonical pair must project the Change receipt observations.");
18967
+ }
18968
+ assertShippingAuthorizationConsistent(receipt.shipping_authorization, "Handoff receipt");
18969
+ return receipt;
18970
+ }
18971
+ function markdownTableCell(value) {
18972
+ return String(value ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
18973
+ }
18974
+ function artifactTarget(artifact) {
18975
+ return artifact.url || artifact.path;
18976
+ }
18977
+ function observationArtifactKind(artifact) {
18978
+ if (artifact.role === "canonical_screenshot" || artifact.role === "setup_screenshot" || artifact.kind === "image") return "image";
18979
+ if (artifact.role === "data" || /\.(json|md|txt|html|log|har)(\?|#|$)/i.test(artifact.name)) return "data";
18980
+ return "artifact";
18981
+ }
18982
+ function receiptSideView(side, observation) {
18983
+ const artifacts = observation.artifacts.map((artifact) => ({
18984
+ side,
18985
+ name: artifact.name,
18986
+ kind: observationArtifactKind(artifact),
18987
+ url: artifact.url,
18988
+ path: artifact.path,
18989
+ source: artifact.source
18990
+ }));
18991
+ const summary = observation.profile_summary;
18992
+ const canonical = observation.canonical_screenshot;
18993
+ const screenshots = artifacts.filter(artifactIsScreenshot);
18994
+ if (canonical) {
18995
+ const canonicalTarget = canonical.url || canonical.path;
18996
+ screenshots.sort((left, right) => {
18997
+ const leftCanonical = (left.url || left.path) === canonicalTarget || left.name === canonical.name;
18998
+ const rightCanonical = (right.url || right.path) === canonicalTarget || right.name === canonical.name;
18999
+ return Number(rightCanonical) - Number(leftCanonical);
19000
+ });
19001
+ }
19002
+ return {
19003
+ side,
19004
+ source: observation.target.url,
19005
+ profile_name: summary?.profile_name || observation.proof?.profile_name || "profile",
19006
+ status: summary?.status || observation.proof?.result.status || "proof_insufficient",
19007
+ summary: summary?.summary || observation.proof?.result.summary || "No profile summary recorded.",
19008
+ route: summary?.route,
19009
+ captured_at: observation.captured_at,
19010
+ checks: summary?.checks || (observation.proof ? profileCheckCounts2(observation.proof.result) : {
19011
+ total: 0,
19012
+ passed: 0,
19013
+ failed: 0,
19014
+ skipped: 0,
19015
+ needs_human_review: 0
19016
+ }),
19017
+ screenshots,
19018
+ artifacts
19019
+ };
19020
+ }
19021
+ function markdownLink(label, target) {
19022
+ return `[${label.replace(/\]/g, "\\]")}](${target})`;
19023
+ }
19024
+ function appendMarkdownArtifacts(lines, title, artifacts) {
19025
+ lines.push(`### ${title}`, "");
19026
+ if (!artifacts.length) {
19027
+ lines.push("- No screenshot artifacts recorded.", "");
19028
+ return;
19029
+ }
19030
+ for (const artifact of artifacts.slice(0, 6)) {
19031
+ const target = artifactTarget(artifact);
19032
+ if (target && artifact.kind === "image") {
19033
+ lines.push(`![${artifact.name.replace(/\]/g, "\\]")}](${target})`);
19034
+ } else if (target) {
19035
+ lines.push(`- ${markdownLink(artifact.name, target)}`);
19036
+ } else {
19037
+ lines.push(`- ${artifact.name}`);
19038
+ }
19039
+ }
19040
+ if (artifacts.length > 6) lines.push(`- ${artifacts.length - 6} more artifact(s) omitted`);
19041
+ lines.push("");
19042
+ }
19043
+ function riddleProofChangeReceiptMarkdown(receipt) {
19044
+ const before = receiptSideView("before", receipt.before);
19045
+ const after = receiptSideView("after", receipt.after);
19046
+ const lines = [
19047
+ "# Riddle Proof Change Receipt",
19048
+ "",
19049
+ `**Verdict:** ${receipt.verdict}`,
19050
+ `**Status:** ${receipt.status}`,
19051
+ `**Contract:** ${receipt.contract_name}`,
19052
+ receipt.profile_name ? `**Profile:** ${receipt.profile_name}` : "",
19053
+ `**Recommendation:** ${receipt.recommendation.label}`,
19054
+ `**Shipping authorization:** ${receipt.shipping_authorization.status}`,
19055
+ "",
19056
+ receipt.summary,
19057
+ "",
19058
+ "## Evidence Pair",
19059
+ "",
19060
+ "| Side | Source | Status | Checks |",
19061
+ "| --- | --- | --- | --- |",
19062
+ `| Before | ${markdownTableCell(before.source)} | ${before.status}${receipt.groups.before.ok ? " (matched baseline)" : " (baseline mismatch)"} | ${before.checks.passed} passed / ${before.checks.failed} failed |`,
19063
+ `| After | ${markdownTableCell(after.source)} | ${after.status} | ${after.checks.passed} passed / ${after.checks.failed} failed |`,
19064
+ "",
19065
+ "## Source Binding",
19066
+ "",
19067
+ "| Side | Status | Git revision | Preview | Digest |",
19068
+ "| --- | --- | --- | --- | --- |",
19069
+ `| Before | ${receipt.source_bindings.before.status} | ${markdownTableCell(receipt.source_bindings.before.observed_git_revision || "not recorded")} | ${markdownTableCell(receipt.source_bindings.before.preview_id || "not required")} | ${markdownTableCell(receipt.source_bindings.before.content_digest || "not required")} |`,
19070
+ `| After | ${receipt.source_bindings.after.status} | ${markdownTableCell(receipt.source_bindings.after.observed_git_revision || "not recorded")} | ${markdownTableCell(receipt.source_bindings.after.preview_id || "not required")} | ${markdownTableCell(receipt.source_bindings.after.content_digest || "not required")} |`,
19071
+ "",
19072
+ "## Delta Checks",
19073
+ "",
19074
+ "| Delta | Status | Before | After |",
19075
+ "| --- | --- | --- | --- |"
19076
+ ].filter(Boolean);
19077
+ for (const delta of receipt.deltas) {
19078
+ lines.push(`| ${markdownTableCell(delta.label)} | ${delta.status} | ${markdownTableCell(delta.before_observed)} | ${markdownTableCell(delta.after_observed)} |`);
19079
+ if (delta.message) lines.push(`| ${markdownTableCell(`${delta.label} message`)} | ${markdownTableCell(delta.message)} | | |`);
19080
+ }
19081
+ lines.push("");
19082
+ appendMarkdownArtifacts(lines, "Before Canonical Screenshot", before.screenshots.slice(0, 1));
19083
+ appendMarkdownArtifacts(lines, "After Canonical Screenshot", after.screenshots.slice(0, 1));
19084
+ if (receipt.proves.length) {
19085
+ lines.push("## What This Proves", "");
19086
+ for (const claim of receipt.proves) lines.push(`- ${claim}`);
19087
+ lines.push("");
19088
+ }
19089
+ if (receipt.does_not_prove.length) {
19090
+ lines.push("## What This Does Not Prove", "");
19091
+ for (const claim of receipt.does_not_prove) lines.push(`- ${claim}`);
19092
+ lines.push("");
19093
+ }
19094
+ const linkedArtifacts = [...before.artifacts, ...after.artifacts].filter((artifact) => artifactTarget(artifact)).slice(0, 24);
19095
+ if (linkedArtifacts.length) {
19096
+ lines.push("## Artifacts", "");
19097
+ for (const artifact of linkedArtifacts) {
19098
+ lines.push(`- ${artifact.side}: ${markdownLink(artifact.name, artifactTarget(artifact) || "")}`);
19099
+ }
19100
+ lines.push("");
19101
+ }
19102
+ return `${lines.join("\n").trim()}
19103
+ `;
19104
+ }
19105
+ function escapeHtml(value) {
19106
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
19107
+ }
19108
+ function htmlArtifactCard(artifact) {
19109
+ const target = artifactTarget(artifact);
19110
+ if (!target) return `<p>${escapeHtml(artifact.name)}</p>`;
19111
+ if (artifact.kind === "image") {
19112
+ return `<figure><img src="${escapeHtml(target)}" alt="${escapeHtml(artifact.name)}"><figcaption>${escapeHtml(artifact.name)}</figcaption></figure>`;
19113
+ }
19114
+ return `<p><a href="${escapeHtml(target)}">${escapeHtml(artifact.name)}</a></p>`;
19115
+ }
19116
+ function htmlArtifactLink(artifact) {
19117
+ const target = artifactTarget(artifact);
19118
+ if (!target) return escapeHtml(artifact.name);
19119
+ return `<a href="${escapeHtml(target)}">${escapeHtml(artifact.name)}</a>`;
19120
+ }
19121
+ function htmlList(items, emptyText) {
19122
+ if (!items.length) return `<p class="muted">${escapeHtml(emptyText)}</p>`;
19123
+ return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
19124
+ }
19125
+ function riddleProofChangeReceiptHtml(receipt) {
19126
+ const before = receiptSideView("before", receipt.before);
19127
+ const after = receiptSideView("after", receipt.after);
19128
+ const artifactLinks = [...before.artifacts, ...after.artifacts].filter((artifact) => artifactTarget(artifact)).slice(0, 32);
19129
+ const deltaRows = receipt.deltas.map((delta) => `
19130
+ <tr>
19131
+ <td>${escapeHtml(delta.label)}</td>
19132
+ <td><span class="status ${escapeHtml(delta.status)}">${escapeHtml(delta.status)}</span></td>
19133
+ <td>${escapeHtml(delta.before_observed)}</td>
19134
+ <td>${escapeHtml(delta.after_observed)}</td>
19135
+ </tr>
19136
+ ${delta.message ? `<tr><td class="muted">${escapeHtml(delta.label)} message</td><td colspan="3">${escapeHtml(delta.message)}</td></tr>` : ""}`).join("");
19137
+ return `<!doctype html>
19138
+ <html lang="en">
19139
+ <head>
19140
+ <meta charset="utf-8">
19141
+ <meta name="viewport" content="width=device-width, initial-scale=1">
19142
+ <title>${escapeHtml(receipt.contract_name)} - Riddle Proof Change Receipt</title>
19143
+ <style>
19144
+ :root { color-scheme: light dark; --bg: #0f141b; --panel: #171f29; --text: #eef4f8; --muted: #aebbc8; --border: #314152; --ok: #69d089; --bad: #ff7a7a; --warn: #ffd166; }
19145
+ body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); }
19146
+ main { max-width: 1120px; margin: 0 auto; padding: 32px 20px 48px; }
19147
+ header, section { border: 1px solid var(--border); background: var(--panel); border-radius: 8px; padding: 20px; margin: 0 0 18px; }
19148
+ h1, h2, h3, p { margin-top: 0; }
19149
+ h1 { font-size: clamp(1.6rem, 3vw, 2.4rem); }
19150
+ h2 { font-size: 1.1rem; margin-bottom: 14px; }
19151
+ .muted { color: var(--muted); }
19152
+ .verdict { display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 999px; border: 1px solid var(--border); font-weight: 700; }
19153
+ .mergeable, .passed { color: var(--ok); }
19154
+ .not_mergeable, .failed, .product_regression { color: var(--bad); }
19155
+ .proof_insufficient, .environment_blocked, .needs_human_review, .configuration_error { color: var(--warn); }
19156
+ .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
19157
+ .side { border: 1px solid var(--border); border-radius: 8px; padding: 14px; min-width: 0; }
19158
+ code { overflow-wrap: anywhere; color: var(--muted); }
19159
+ table { width: 100%; border-collapse: collapse; }
19160
+ th, td { border-bottom: 1px solid var(--border); padding: 10px 8px; text-align: left; vertical-align: top; }
19161
+ figure { margin: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: #0b1017; }
19162
+ img { display: block; width: 100%; height: auto; }
19163
+ figcaption { padding: 8px 10px; color: var(--muted); font-size: 0.9rem; }
19164
+ ul { margin-bottom: 0; }
19165
+ a { color: #8bc7ff; }
19166
+ @media (max-width: 760px) { .grid { grid-template-columns: 1fr; } main { padding: 18px 12px 32px; } }
19167
+ </style>
19168
+ </head>
19169
+ <body>
19170
+ <main>
19171
+ <header>
19172
+ <p class="verdict ${escapeHtml(receipt.verdict)}">${escapeHtml(receipt.verdict)}</p>
19173
+ <h1>${escapeHtml(receipt.contract_name)}</h1>
19174
+ <p>${escapeHtml(receipt.summary)}</p>
19175
+ <p class="muted">Status: ${escapeHtml(receipt.status)}${receipt.profile_name ? ` | Profile: ${escapeHtml(receipt.profile_name)}` : ""}</p>
19176
+ <p>${escapeHtml(receipt.recommendation.label)}. Shipping authorization: ${escapeHtml(receipt.shipping_authorization.status)}.</p>
19177
+ </header>
19178
+ <section>
19179
+ <h2>Evidence Pair</h2>
19180
+ <div class="grid">
19181
+ <div class="side">
19182
+ <h3>Before</h3>
19183
+ <p><code>${escapeHtml(before.source)}</code></p>
19184
+ <p>Status: <strong class="${escapeHtml(before.status)}">${escapeHtml(before.status)}</strong>${receipt.groups.before.ok ? " (matched baseline)" : " (baseline mismatch)"}</p>
19185
+ <p>${escapeHtml(before.summary)}</p>
19186
+ <p class="muted">${before.checks.passed} passed / ${before.checks.failed} failed checks</p>
19187
+ </div>
19188
+ <div class="side">
19189
+ <h3>After</h3>
19190
+ <p><code>${escapeHtml(after.source)}</code></p>
19191
+ <p>Status: <strong class="${escapeHtml(after.status)}">${escapeHtml(after.status)}</strong></p>
19192
+ <p>${escapeHtml(after.summary)}</p>
19193
+ <p class="muted">${after.checks.passed} passed / ${after.checks.failed} failed checks</p>
19194
+ </div>
19195
+ </div>
19196
+ </section>
19197
+ <section>
19198
+ <h2>Source Binding</h2>
19199
+ <table>
19200
+ <thead><tr><th>Side</th><th>Status</th><th>Git revision</th><th>Preview</th><th>Digest</th></tr></thead>
19201
+ <tbody>
19202
+ <tr><td>Before</td><td>${escapeHtml(receipt.source_bindings.before.status)}</td><td>${escapeHtml(receipt.source_bindings.before.observed_git_revision || "not recorded")}</td><td>${escapeHtml(receipt.source_bindings.before.preview_id || "not required")}</td><td>${escapeHtml(receipt.source_bindings.before.content_digest || "not required")}</td></tr>
19203
+ <tr><td>After</td><td>${escapeHtml(receipt.source_bindings.after.status)}</td><td>${escapeHtml(receipt.source_bindings.after.observed_git_revision || "not recorded")}</td><td>${escapeHtml(receipt.source_bindings.after.preview_id || "not required")}</td><td>${escapeHtml(receipt.source_bindings.after.content_digest || "not required")}</td></tr>
19204
+ </tbody>
19205
+ </table>
19206
+ </section>
19207
+ <section>
19208
+ <h2>Delta Checks</h2>
19209
+ <table>
19210
+ <thead><tr><th>Delta</th><th>Status</th><th>Before</th><th>After</th></tr></thead>
19211
+ <tbody>${deltaRows}</tbody>
19212
+ </table>
19213
+ </section>
19214
+ <section>
19215
+ <h2>Screenshots</h2>
19216
+ <div class="grid">
19217
+ <div>${before.screenshots.slice(0, 1).map(htmlArtifactCard).join("") || `<p class="muted">No canonical before screenshot recorded.</p>`}</div>
19218
+ <div>${after.screenshots.slice(0, 1).map(htmlArtifactCard).join("") || `<p class="muted">No canonical after screenshot recorded.</p>`}</div>
19219
+ </div>
19220
+ </section>
19221
+ <section>
19222
+ <h2>What This Proves</h2>
19223
+ ${htmlList(receipt.proves, "No explicit proof claims were recorded in contract metadata.")}
19224
+ </section>
19225
+ <section>
19226
+ <h2>What This Does Not Prove</h2>
19227
+ ${htmlList(receipt.does_not_prove, "No explicit limits were recorded in contract metadata.")}
19228
+ </section>
19229
+ <section>
19230
+ <h2>Artifacts</h2>
19231
+ ${artifactLinks.length ? `<ul>${artifactLinks.map((artifact) => `<li>${escapeHtml(artifact.side)}: ${htmlArtifactLink(artifact)}</li>`).join("")}</ul>` : `<p class="muted">No linked artifacts recorded.</p>`}
19232
+ </section>
19233
+ </main>
19234
+ </body>
19235
+ </html>
19236
+ `;
19237
+ }
19238
+
19239
+ // src/pr-comment.ts
19240
+ var RIDDLE_PROOF_PR_COMMENT_MARKER = "<!-- riddle-proof:pr-comment:v1 -->";
19241
+ function asRecord2(value) {
19242
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
19243
+ }
19244
+ function asArray(value) {
19245
+ return Array.isArray(value) ? value : [];
19246
+ }
19247
+ function stringValue4(value) {
19248
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
19249
+ }
19250
+ function numberValue3(value) {
19251
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
19252
+ }
19253
+ function booleanValue3(value) {
19254
+ return typeof value === "boolean" ? value : void 0;
19255
+ }
19256
+ function firstStringValue2(...values) {
19257
+ for (const value of values) {
19258
+ const text = stringValue4(value);
19259
+ if (text) return text;
19260
+ }
19261
+ return void 0;
19262
+ }
19263
+ function artifactKind(name, url) {
19264
+ const target = `${name} ${url}`.toLowerCase();
19265
+ if (/\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/.test(target)) return "image";
19266
+ if (/\.(json|har|txt|md|html|log)(\?|#|$)/.test(target)) return "data";
19267
+ return "artifact";
19268
+ }
19269
+ function artifactDisplayName(value, fallback) {
19270
+ const raw = stringValue4(value);
19271
+ if (raw) return raw;
19272
+ return fallback;
19273
+ }
19274
+ function collectArtifacts(runResponse) {
19275
+ const proofResult = asRecord2(runResponse.proofResult);
19276
+ const outputs = asArray(proofResult.outputs);
19277
+ const artifacts = [];
19278
+ const seen = /* @__PURE__ */ new Set();
19279
+ for (const [index, item] of outputs.entries()) {
19280
+ const artifact = asRecord2(item);
19281
+ const url = stringValue4(artifact.url);
19282
+ if (!url || seen.has(url)) continue;
19283
+ seen.add(url);
19284
+ const name = artifactDisplayName(artifact.name, `artifact-${index + 1}`);
19285
+ artifacts.push({
19286
+ name,
19287
+ url,
19288
+ kind: artifactKind(name, url),
19289
+ size_bytes: numberValue3(artifact.size)
19290
+ });
19291
+ }
19292
+ return artifacts;
19293
+ }
19294
+ function collectProfileArtifacts(result) {
18121
19295
  const resultArtifacts = asRecord2(result.artifacts);
18122
19296
  const artifacts = [];
18123
19297
  const seen = /* @__PURE__ */ new Set();
@@ -18137,6 +19311,34 @@ function collectProfileArtifacts(result) {
18137
19311
  }
18138
19312
  return artifacts;
18139
19313
  }
19314
+ function collectChangeReceiptArtifacts(result) {
19315
+ const artifacts = [];
19316
+ const seen = /* @__PURE__ */ new Set();
19317
+ const collectSide = (sideName, side) => {
19318
+ const candidates = [
19319
+ side.canonical_screenshot,
19320
+ ...asArray(side.screenshots),
19321
+ ...asArray(side.artifacts)
19322
+ ].filter(Boolean);
19323
+ for (const [index, item] of candidates.entries()) {
19324
+ const artifact = asRecord2(item);
19325
+ const url = stringValue4(artifact.url) || stringValue4(artifact.path);
19326
+ if (!url || seen.has(url)) continue;
19327
+ seen.add(url);
19328
+ const fallbackName = `${sideName}-artifact-${index + 1}`;
19329
+ const name = `${sideName}/${artifactDisplayName(artifact.name, fallbackName)}`;
19330
+ artifacts.push({
19331
+ name,
19332
+ url,
19333
+ kind: artifactKind(name, url),
19334
+ size_bytes: numberValue3(artifact.size_bytes) ?? numberValue3(artifact.size)
19335
+ });
19336
+ }
19337
+ };
19338
+ collectSide("before", asRecord2(result.before));
19339
+ collectSide("after", asRecord2(result.after));
19340
+ return artifacts;
19341
+ }
18140
19342
  function mergeArtifacts(...artifactLists) {
18141
19343
  const artifacts = [];
18142
19344
  const seen = /* @__PURE__ */ new Set();
@@ -18163,7 +19365,7 @@ function pageSummaries(result) {
18163
19365
  }
18164
19366
  return pages;
18165
19367
  }
18166
- function profileCheckCounts(result) {
19368
+ function profileCheckCounts3(result) {
18167
19369
  const checks = asArray(result.checks);
18168
19370
  if (checks.length) {
18169
19371
  let passed2 = 0;
@@ -18192,6 +19394,33 @@ function profilePageSummaries(result, counts) {
18192
19394
  failed: counts.failed
18193
19395
  }];
18194
19396
  }
19397
+ function changeReceiptCheckCounts(result) {
19398
+ const deltas = asArray(result.deltas);
19399
+ if (!deltas.length) return void 0;
19400
+ let passed = 0;
19401
+ let failed = 0;
19402
+ for (const item of deltas) {
19403
+ const status = stringValue4(asRecord2(item).status);
19404
+ if (status === "passed") passed += 1;
19405
+ if (status === "failed" || status === "proof_insufficient" || status === "configuration_error") failed += 1;
19406
+ }
19407
+ return { passed, failed };
19408
+ }
19409
+ function changeReceiptPageSummaries(result) {
19410
+ const pages = [];
19411
+ for (const sideName of ["before", "after"]) {
19412
+ const side = asRecord2(result[sideName]);
19413
+ if (!Object.keys(side).length) continue;
19414
+ const profileSummary = asRecord2(side.profile_summary);
19415
+ const checks = Object.keys(asRecord2(side.checks)).length ? asRecord2(side.checks) : asRecord2(profileSummary.checks);
19416
+ pages.push({
19417
+ route: firstStringValue2(asRecord2(side.target).url, side.source, profileSummary.profile_name, side.profile_name) || sideName,
19418
+ passed: numberValue3(checks.passed) ?? 0,
19419
+ failed: numberValue3(checks.failed) ?? 0
19420
+ });
19421
+ }
19422
+ return pages;
19423
+ }
18195
19424
  function summarizeExplicitChecks(value) {
18196
19425
  let passed = 0;
18197
19426
  let failed = 0;
@@ -18247,11 +19476,27 @@ function isProfileResult(result) {
18247
19476
  const version = stringValue4(result.version);
18248
19477
  return version === "riddle-proof.profile-result.v1" || version === "riddle-proof.profile-compact-result.v1";
18249
19478
  }
19479
+ function isChangeReceiptResult(result) {
19480
+ const version = stringValue4(result.version);
19481
+ return version === RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION || version === RIDDLE_PROOF_CHANGE_RECEIPT_VERSION;
19482
+ }
19483
+ function isHandoffReceiptResult(result) {
19484
+ return stringValue4(result.version) === RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION;
19485
+ }
19486
+ function isChangeResult(result) {
19487
+ const version = stringValue4(result.version);
19488
+ return version === "riddle-proof.change-result.v1" || version === "riddle-proof.change-compact-result.v1";
19489
+ }
18250
19490
  function summarizeRiddleProofPrComment(input) {
18251
19491
  const runResponse = asRecord2(input.runResponse);
18252
- const result = asRecord2(input.result);
19492
+ const inputResult = asRecord2(input.result);
19493
+ const handoffReceipt = isHandoffReceiptResult(inputResult) ? parseRiddleProofHandoffReceipt(inputResult) : void 0;
19494
+ const parsedChangeReceipt = handoffReceipt?.change || (isChangeReceiptResult(inputResult) ? parseRiddleProofChangeReceipt(inputResult) : void 0);
19495
+ const result = parsedChangeReceipt ? asRecord2(parsedChangeReceipt) : inputResult;
18253
19496
  const proofResult = asRecord2(runResponse.proofResult);
18254
19497
  const profileResult = isProfileResult(result);
19498
+ const changeReceiptResult = Boolean(parsedChangeReceipt);
19499
+ const changeResult = changeReceiptResult || isChangeResult(result);
18255
19500
  const profileRiddle = asRecord2(result.riddle);
18256
19501
  const preview = asRecord2(runResponse.preview);
18257
19502
  const resultRunCard = asRecord2(result.run_card);
@@ -18259,16 +19504,19 @@ function summarizeRiddleProofPrComment(input) {
18259
19504
  const resultDetails = asRecord2(result.details);
18260
19505
  const resultRaw = asRecord2(result.raw);
18261
19506
  const rawDetails = asRecord2(resultRaw.details);
18262
- const profileChecks = profileResult ? profileCheckCounts(result) : void 0;
19507
+ const profileChecks = profileResult ? profileCheckCounts3(result) : void 0;
19508
+ const changeChecks = changeResult ? changeReceiptCheckCounts(result) : void 0;
18263
19509
  const artifacts = mergeArtifacts(
18264
19510
  collectArtifacts(runResponse),
18265
- profileResult ? collectProfileArtifacts(result) : []
19511
+ profileResult ? collectProfileArtifacts(result) : [],
19512
+ changeReceiptResult ? collectChangeReceiptArtifacts(result) : []
18266
19513
  );
18267
- const pages = profileResult ? profilePageSummaries(result, profileChecks) : pageSummaries(result);
19514
+ const pages = changeReceiptResult ? changeReceiptPageSummaries(result) : profileResult ? profilePageSummaries(result, profileChecks) : pageSummaries(result);
18268
19515
  const checkSource = { ...result };
18269
19516
  delete checkSource.ok;
18270
19517
  const nestedChecks = summarizeExplicitChecks(checkSource);
18271
- const ok = booleanValue3(result.ok) ?? booleanValue3(runResponse.ok) ?? null;
19518
+ const resultStatus = firstStringValue2(result.status, stopCondition.status);
19519
+ const ok = changeReceiptResult ? parsedChangeReceipt?.recommendation.merge_recommended ?? false : changeResult ? resultStatus === "passed" : booleanValue3(result.ok) ?? booleanValue3(runResponse.ok) ?? null;
18272
19520
  const checkpointSummary = checkpointSummaryFrom2(
18273
19521
  result.checkpoint_summary,
18274
19522
  stopCondition.checkpoint_summary,
@@ -18276,19 +19524,19 @@ function summarizeRiddleProofPrComment(input) {
18276
19524
  rawDetails.checkpoint_summary,
18277
19525
  proofResult.checkpoint_summary
18278
19526
  );
18279
- const publicState = summarizeRiddleProofPublicState({
19527
+ const publicState = changeReceiptResult ? void 0 : summarizeRiddleProofPublicState({
18280
19528
  ...result,
18281
- status: firstStringValue2(result.status, stopCondition.status),
19529
+ status: resultStatus,
18282
19530
  checkpoint_summary: checkpointSummary || result.checkpoint_summary
18283
19531
  });
18284
- const mergeRecommendation = riddleProofPublicStateMergeRecommendation(
19532
+ const mergeRecommendation = publicState ? riddleProofPublicStateMergeRecommendation(
18285
19533
  publicState,
18286
19534
  firstStringValue2(result.merge_recommendation, stopCondition.merge_recommendation, resultRaw.merge_recommendation)
18287
- );
19535
+ ) : void 0;
18288
19536
  return {
18289
19537
  ok,
18290
19538
  status: firstStringValue2(proofResult.status, profileRiddle.status),
18291
- result_status: publicState.status,
19539
+ result_status: changeResult ? resultStatus : publicState?.status,
18292
19540
  job_id: firstStringValue2(proofResult.job_id, profileRiddle.job_id),
18293
19541
  duration_ms: numberValue3(proofResult.duration_ms) ?? numberValue3(profileRiddle.elapsed_ms),
18294
19542
  proof_url: stringValue4(runResponse.proofUrl),
@@ -18296,17 +19544,17 @@ function summarizeRiddleProofPrComment(input) {
18296
19544
  preview_url: stringValue4(preview.preview_url) || stringValue4(preview.url),
18297
19545
  preview_publish_recovered: booleanValue3(preview.publish_recovered),
18298
19546
  preview_publish_error: stringValue4(preview.publish_error),
18299
- ship_held: publicState.ship_held,
18300
- shipping_disabled: publicState.shipping_disabled,
18301
- ship_authorized: publicState.ship_authorized,
18302
- merge_ready: publicState.merge_ready,
18303
- sync_allowed: publicState.sync_allowed,
19547
+ ship_held: publicState?.ship_held,
19548
+ shipping_disabled: publicState?.shipping_disabled,
19549
+ ship_authorized: changeReceiptResult ? parsedChangeReceipt?.shipping_authorization.authorized : publicState?.ship_authorized,
19550
+ merge_ready: publicState?.merge_ready,
19551
+ sync_allowed: publicState?.sync_allowed,
18304
19552
  proof_decision: firstStringValue2(result.proof_decision, stopCondition.proof_decision, resultRaw.proof_decision),
18305
- merge_recommendation: mergeRecommendation,
19553
+ merge_recommendation: changeReceiptResult ? parsedChangeReceipt?.recommendation.label : mergeRecommendation,
18306
19554
  checkpoint_summary: checkpointSummary,
18307
19555
  public_state: publicState,
18308
- passed_checks: profileChecks?.passed ?? nestedChecks.passed,
18309
- failed_checks: profileChecks?.failed ?? nestedChecks.failed,
19556
+ passed_checks: changeChecks?.passed ?? profileChecks?.passed ?? nestedChecks.passed,
19557
+ failed_checks: changeChecks?.failed ?? profileChecks?.failed ?? nestedChecks.failed,
18310
19558
  pages,
18311
19559
  artifacts,
18312
19560
  primary_image: selectPrimaryImage(artifacts)
@@ -18319,7 +19567,7 @@ function formatDuration(ms) {
18319
19567
  const remainder = seconds % 60;
18320
19568
  return minutes > 0 ? `${minutes}m${String(remainder).padStart(2, "0")}s` : `${seconds}s`;
18321
19569
  }
18322
- function markdownLink(label, url) {
19570
+ function markdownLink2(label, url) {
18323
19571
  return `[${label.replace(/\]/g, "\\]")}](${url})`;
18324
19572
  }
18325
19573
  function resultLabel(summary) {
@@ -18370,7 +19618,88 @@ function checkpointSummaryLine(summary) {
18370
19618
  summary.latest_decision ? `latest decision \`${summary.latest_decision}\`` : ""
18371
19619
  ].filter(Boolean).join("; ");
18372
19620
  }
19621
+ function markdownTableValue(value) {
19622
+ return String(value ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
19623
+ }
19624
+ function observationArtifactTarget(artifact) {
19625
+ return artifact?.url || artifact?.path;
19626
+ }
19627
+ function canonicalScreenshotCell(artifact, fallback) {
19628
+ const target = observationArtifactTarget(artifact);
19629
+ if (!artifact || !target) return fallback;
19630
+ if (artifact.url) return `![${artifact.name.replace(/\]/g, "\\]")}](${artifact.url})`;
19631
+ return markdownTableValue(artifact.path || artifact.name);
19632
+ }
19633
+ function shortRevision(value) {
19634
+ return value ? value.slice(0, 12) : "not recorded";
19635
+ }
19636
+ function buildRiddleProofHandoffPrCommentMarkdown(handoff, input = {}) {
19637
+ const receipt = parseRiddleProofHandoffReceipt(handoff);
19638
+ const change = receipt.change;
19639
+ const beforeStatus = change.before.profile_summary?.status || "not recorded";
19640
+ const afterStatus = change.after.profile_summary?.status || "not recorded";
19641
+ const beforeBaseline = change.groups.before.ok ? "matched baseline" : "baseline mismatch";
19642
+ const lines = [
19643
+ RIDDLE_PROOF_PR_COMMENT_MARKER,
19644
+ `## ${input.title?.trim() || "Riddle Change Proof"}`,
19645
+ "",
19646
+ `**Result:** ${receipt.recommendation.label}`,
19647
+ `**Evidence verdict:** \`${receipt.verdict}\``,
19648
+ `**Shipping authorization:** ${receipt.shipping_authorization.status}`,
19649
+ `**Contract:** ${change.contract_name}`
19650
+ ];
19651
+ if (input.goal?.trim()) lines.push(`**Goal:** ${input.goal.trim()}`);
19652
+ if (input.successCriteria?.trim()) lines.push(`**Success criteria:** ${input.successCriteria.trim()}`);
19653
+ lines.push("", change.summary, "", "### Canonical Before / After", "");
19654
+ lines.push("| Before | After |");
19655
+ lines.push("| --- | --- |");
19656
+ lines.push(`| ${beforeBaseline}: \`${markdownTableValue(beforeStatus)}\` | \`${markdownTableValue(afterStatus)}\` |`);
19657
+ lines.push(`| ${canonicalScreenshotCell(receipt.canonical_pair.before, "No canonical before screenshot")} | ${canonicalScreenshotCell(receipt.canonical_pair.after, "No canonical after screenshot")} |`);
19658
+ lines.push("", "### Source Binding", "");
19659
+ lines.push("| Side | Target | Git revision | Binding | Preview | Digest |");
19660
+ lines.push("| --- | --- | --- | --- | --- | --- |");
19661
+ for (const side of ["before", "after"]) {
19662
+ const observation = change[side];
19663
+ const binding = change.source_bindings[side];
19664
+ lines.push(`| ${side} | ${markdownTableValue(observation.target.url)} | \`${shortRevision(binding.observed_git_revision || observation.source.git_revision)}\` | ${binding.status} | ${markdownTableValue(binding.preview_id || observation.target.preview?.preview_id || "not required")} | \`${markdownTableValue(binding.content_digest || observation.target.preview?.content_digest || "not required")}\` |`);
19665
+ }
19666
+ lines.push("", "### Declared Delta", "");
19667
+ lines.push("| Measurement | Status | Before | After |");
19668
+ lines.push("| --- | --- | --- | --- |");
19669
+ for (const delta of change.deltas) {
19670
+ lines.push(`| ${markdownTableValue(delta.label)} | ${delta.status} | ${markdownTableValue(delta.before_observed)} | ${markdownTableValue(delta.after_observed)} |`);
19671
+ }
19672
+ if (change.proves.length) {
19673
+ lines.push("", "### What This Proves", "");
19674
+ for (const claim of change.proves) lines.push(`- ${claim}`);
19675
+ }
19676
+ if (change.does_not_prove.length) {
19677
+ lines.push("", "### Limits", "");
19678
+ for (const claim of change.does_not_prove) lines.push(`- ${claim}`);
19679
+ }
19680
+ const linkedArtifacts = [
19681
+ ...change.before.artifacts.map((artifact) => ({ side: "before", artifact })),
19682
+ ...change.after.artifacts.map((artifact) => ({ side: "after", artifact }))
19683
+ ].filter(({ artifact }) => Boolean(artifact.url));
19684
+ if (linkedArtifacts.length) {
19685
+ lines.push("", "### Artifacts", "");
19686
+ for (const { side, artifact } of linkedArtifacts.slice(0, 24)) {
19687
+ lines.push(`- ${side}: ${markdownLink2(artifact.name, artifact.url || "")}`);
19688
+ }
19689
+ }
19690
+ lines.push("", input.source?.trim() ? `_Source: ${input.source.trim()}_` : "_Rendered from the Handoff receipt without recomputing its verdict._");
19691
+ return `${lines.join("\n").trim()}
19692
+ `;
19693
+ }
18373
19694
  function buildRiddleProofPrCommentMarkdown(input) {
19695
+ const result = asRecord2(input.result);
19696
+ if (isHandoffReceiptResult(result)) {
19697
+ return buildRiddleProofHandoffPrCommentMarkdown(parseRiddleProofHandoffReceipt(result), input);
19698
+ }
19699
+ if (isChangeReceiptResult(result)) {
19700
+ const handoff = createRiddleProofHandoffReceipt(parseRiddleProofChangeReceipt(result));
19701
+ return buildRiddleProofHandoffPrCommentMarkdown(handoff, input);
19702
+ }
18374
19703
  const summary = summarizeRiddleProofPrComment(input);
18375
19704
  const title = input.title?.trim() || "Riddle Proof Evidence";
18376
19705
  const lines = [
@@ -18394,10 +19723,10 @@ function buildRiddleProofPrCommentMarkdown(input) {
18394
19723
  if (summary.proof_decision) lines.push(`**Proof decision:** \`${summary.proof_decision}\``);
18395
19724
  if (shouldRenderMergeRecommendation(summary)) lines.push(`**Merge recommendation:** ${summary.merge_recommendation}`);
18396
19725
  if (summary.checkpoint_summary) lines.push(`**Checkpoints:** ${checkpointSummaryLine(summary.checkpoint_summary)}`);
18397
- if (summary.proof_url) lines.push(`**Proof URL:** ${markdownLink(summary.proof_url, summary.proof_url)}`);
19726
+ if (summary.proof_url) lines.push(`**Proof URL:** ${markdownLink2(summary.proof_url, summary.proof_url)}`);
18398
19727
  if (summary.preview_id || summary.preview_url) {
18399
19728
  const previewLabel = summary.preview_id ? `\`${summary.preview_id}\`` : "preview";
18400
- lines.push(`**Preview:** ${summary.preview_url ? markdownLink(previewLabel, summary.preview_url) : previewLabel}`);
19729
+ lines.push(`**Preview:** ${summary.preview_url ? markdownLink2(previewLabel, summary.preview_url) : previewLabel}`);
18401
19730
  }
18402
19731
  if (summary.preview_publish_recovered) {
18403
19732
  const detail = summary.preview_publish_error ? `: ${summary.preview_publish_error}` : "";
@@ -18422,7 +19751,7 @@ function buildRiddleProofPrCommentMarkdown(input) {
18422
19751
  if (linkedArtifacts.length) {
18423
19752
  lines.push("### Artifacts");
18424
19753
  for (const artifact of linkedArtifacts) {
18425
- lines.push(`- ${markdownLink(artifact.name, artifact.url)}`);
19754
+ lines.push(`- ${markdownLink2(artifact.name, artifact.url)}`);
18426
19755
  }
18427
19756
  if (summary.artifacts.length - (summary.primary_image ? 1 : 0) > linkedArtifacts.length) {
18428
19757
  lines.push(`- ${summary.artifacts.length - (summary.primary_image ? 1 : 0) - linkedArtifacts.length} more artifact(s) omitted`);
@@ -18608,167 +19937,6 @@ function suggestRiddleProofProfileChecks(input) {
18608
19937
  };
18609
19938
  }
18610
19939
 
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
-
18772
19940
  // src/cli.ts
18773
19941
  var RIDDLE_PROFILE_BALANCE_PREFLIGHT_MIN_SECONDS_PER_JOB = 30;
18774
19942
  var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
@@ -18781,8 +19949,14 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
18781
19949
  "balancePreflight",
18782
19950
  "baseUrl",
18783
19951
  "afterResult",
19952
+ "afterObservation",
19953
+ "afterPreviewReceipt",
19954
+ "afterSourceRevision",
18784
19955
  "afterUrl",
18785
19956
  "beforeResult",
19957
+ "beforeObservation",
19958
+ "beforePreviewReceipt",
19959
+ "beforeSourceRevision",
18786
19960
  "beforeUrl",
18787
19961
  "candidateJson",
18788
19962
  "candidatesJson",
@@ -18836,6 +20010,7 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
18836
20010
  "pack",
18837
20011
  "packFile",
18838
20012
  "profile",
20013
+ "previewReceipt",
18839
20014
  "proofDir",
18840
20015
  "pr",
18841
20016
  "progressEveryMs",
@@ -18859,6 +20034,9 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
18859
20034
  "scriptFile",
18860
20035
  "selectors",
18861
20036
  "sourceKind",
20037
+ "sourceDirty",
20038
+ "sourceRepository",
20039
+ "sourceRevision",
18862
20040
  "splitViewports",
18863
20041
  "stateDir",
18864
20042
  "statePath",
@@ -18890,16 +20068,16 @@ function usage() {
18890
20068
  " riddle-proof-loop respond --state-path <path> --response-json <file|json|->",
18891
20069
  " riddle-proof-loop respond --state-path <path> --decision <decision> --summary <text> [--payload-json <file|json|->]",
18892
20070
  " riddle-proof-loop status --state-path <path>",
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]",
20071
+ " riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--preview-receipt <file|json>] [--source-revision <sha>] [--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]",
18894
20072
  " 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]",
18895
20073
  " 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]",
20074
+ " 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> | --before-observation <file> --after-observation <file>) [--before-preview-receipt <file|json>] [--after-preview-receipt <file|json>] [--before-source-revision <sha>] [--after-source-revision <sha>] [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
18897
20075
  " riddle-proof-loop profile-suggest --route /path|--url <url> [--changed-files a,b] [--selectors .a,.b] [--changed-text-json <file|json|->] [--format json|profile]",
18898
20076
  " 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>]",
18899
20077
  " 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]",
18900
20078
  " riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
18901
20079
  " riddle-proof-loop profile-http-status-preflight --profile <file|json|-> --url <base-url> [--format json|summary]",
18902
- " riddle-proof-loop riddle-preview-deploy <build-dir> <label> [--framework spa|static] [--quiet]",
20080
+ " riddle-proof-loop riddle-preview-deploy <build-dir> <label> [--framework spa|static] [--output <dir>] [--quiet]",
18903
20081
  " riddle-proof-loop riddle-server-preview <directory> --script-file <file> [--path /route] [--wait-for-selector selector]",
18904
20082
  " riddle-proof-loop riddle-run-script --url <url> --script-file <file> [--viewport 1280x720] [--strict true|false]",
18905
20083
  " riddle-proof-loop riddle-poll <job-id> [--wait] [--attempts n] [--quiet]",
@@ -19059,6 +20237,7 @@ function compactRunProfileResult(result, options) {
19059
20237
  output_dir: outputDir,
19060
20238
  output_files: outputDir ? {
19061
20239
  profile_result: "profile-result.json",
20240
+ observation_receipt: "observation-receipt.json",
19062
20241
  summary: "summary.md",
19063
20242
  proof_json: result.evidence ? "proof.json" : void 0,
19064
20243
  console: result.evidence?.console ? "console.json" : void 0,
@@ -19687,12 +20866,13 @@ function formatByteCount(bytes) {
19687
20866
  }
19688
20867
  function riddlePollProgressLine(snapshot) {
19689
20868
  const submittedAt = snapshot.submitted_at || "not-submitted";
19690
- const queuePart = snapshot.running_without_submission ? ` waiting_for_worker_submit=${formatPollDuration(snapshot.pre_submission_elapsed_ms)} worker_wake=possible${snapshot.queue_elapsed_ms !== null ? ` queued_for=${formatPollDuration(snapshot.queue_elapsed_ms)}` : ""}` : snapshot.queue_elapsed_ms !== null ? ` queue=${formatPollDuration(snapshot.queue_elapsed_ms)}` : "";
20869
+ const queuePart = snapshot.active_execution ? `${snapshot.queue_elapsed_ms !== null ? ` queue=${formatPollDuration(snapshot.queue_elapsed_ms)}` : ""} active_execution=${formatPollDuration(snapshot.elapsed_ms)}` : snapshot.running_without_submission ? ` waiting_for_worker_claim=${formatPollDuration(snapshot.pre_submission_elapsed_ms)} worker_wake=possible${snapshot.queue_elapsed_ms !== null ? ` queued_for=${formatPollDuration(snapshot.queue_elapsed_ms)}` : ""}` : snapshot.queue_elapsed_ms !== null ? ` queue=${formatPollDuration(snapshot.queue_elapsed_ms)}` : "";
19691
20870
  const terminalPart = snapshot.terminal ? " terminal=true" : "";
19692
20871
  return [
19693
20872
  "[riddle-poll]",
19694
20873
  snapshot.job_id,
19695
20874
  `status=${snapshot.status || "unknown"}`,
20875
+ `phase=${snapshot.phase || "unknown"}`,
19696
20876
  `attempt=${snapshot.attempt}/${snapshot.attempts}`,
19697
20877
  `elapsed=${formatPollDuration(snapshot.elapsed_ms)}`,
19698
20878
  `submitted_at=${submittedAt}${queuePart}${terminalPart}`
@@ -22630,10 +23810,48 @@ function profileHttpStatusSummaryMarkdown(result) {
22630
23810
  }
22631
23811
  return lines;
22632
23812
  }
22633
- function writeProfileOutput(outputDir, result) {
23813
+ function profileResultTargetUrl(result) {
23814
+ const legacyRoute = result.route;
23815
+ return result.route.observed || result.route.requested || legacyRoute.url || result.evidence?.target_url || "unknown";
23816
+ }
23817
+ function profileObservationForOutput(result, outputDir, options) {
23818
+ if (options.observation) return options.observation;
23819
+ const preview = options.previewReceipt;
23820
+ const targetUrl = profileResultTargetUrl(result);
23821
+ return createRiddleProofObservationReceipt({
23822
+ comparison_role: options.comparisonRole || "standalone",
23823
+ executor: result.runner === "local-playwright" ? { kind: "local_playwright", runner: result.runner } : {
23824
+ kind: "riddle_hosted",
23825
+ runner: result.runner,
23826
+ job_id: result.riddle?.job_id,
23827
+ worker_id: typeof result.riddle?.execution?.worker_id === "string" ? result.riddle.execution.worker_id : void 0
23828
+ },
23829
+ target: preview ? { kind: "preview", url: targetUrl, preview } : { kind: "url", url: targetUrl },
23830
+ source: options.source || preview?.source,
23831
+ profile_result: result,
23832
+ publication: { kind: "local", path: outputDir },
23833
+ execution: result.riddle?.execution
23834
+ });
23835
+ }
23836
+ function profileOutputOptionsForCli(options) {
23837
+ const previewInput = optionString(options, "previewReceipt");
23838
+ const previewReceipt = previewInput ? parseRiddlePreviewReceipt(readJsonValue(previewInput, "--preview-receipt")) : void 0;
23839
+ const sourceDirty = optionBoolean(options, "sourceDirty");
23840
+ const source = {
23841
+ ...previewReceipt?.source || {},
23842
+ ...optionString(options, "sourceRevision") ? { git_revision: optionString(options, "sourceRevision") } : {},
23843
+ ...optionString(options, "sourceRepository") ? { repository: optionString(options, "sourceRepository") } : {},
23844
+ ...typeof sourceDirty === "boolean" ? { dirty: sourceDirty } : {}
23845
+ };
23846
+ return { previewReceipt, source };
23847
+ }
23848
+ function writeProfileOutput(outputDir, result, options = {}) {
22634
23849
  if (!outputDir) return;
22635
23850
  (0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
22636
23851
  (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "profile-result.json"), `${JSON.stringify(result, null, 2)}
23852
+ `);
23853
+ const observation = profileObservationForOutput(result, outputDir, options);
23854
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "observation-receipt.json"), `${JSON.stringify(observation, null, 2)}
22637
23855
  `);
22638
23856
  (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "summary.md"), profileResultMarkdown(result));
22639
23857
  if (result.evidence) (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "proof.json"), `${JSON.stringify(result, null, 2)}
@@ -22674,6 +23892,12 @@ function profileOptionsForChangeProofSide(options, url, outputDir) {
22674
23892
  }
22675
23893
  return next;
22676
23894
  }
23895
+ function observationForChangeSide(observation, side) {
23896
+ if (observation.comparison_role !== "standalone" && observation.comparison_role !== side) {
23897
+ throw new Error(`${side} observation has incompatible comparison_role ${observation.comparison_role}.`);
23898
+ }
23899
+ return observation.comparison_role === side ? observation : { ...observation, comparison_role: side };
23900
+ }
22677
23901
  function compactChangeProofResult(run, options) {
22678
23902
  const outputDir = profileOutputDirOption(options);
22679
23903
  return {
@@ -22706,57 +23930,37 @@ function compactChangeProofResult(run, options) {
22706
23930
  output_dir: outputDir,
22707
23931
  output_files: outputDir ? {
22708
23932
  change_proof_result: "change-proof-result.json",
23933
+ change_proof_receipt: "change-proof-receipt.json",
23934
+ change_proof_receipt_markdown: "change-proof-receipt.md",
23935
+ change_proof_receipt_html: "change-proof-receipt.html",
23936
+ handoff_receipt: "handoff-receipt.json",
23937
+ handoff_receipt_markdown: "handoff-receipt.md",
22709
23938
  summary: "summary.md",
22710
23939
  before_profile_result: "before/profile-result.json",
22711
- after_profile_result: "after/profile-result.json"
23940
+ after_profile_result: "after/profile-result.json",
23941
+ before_observation_receipt: "before/observation-receipt.json",
23942
+ after_observation_receipt: "after/observation-receipt.json"
22712
23943
  } : void 0
22713
23944
  };
22714
23945
  }
22715
23946
  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
- `;
23947
+ return riddleProofChangeReceiptMarkdown(run.receipt);
22751
23948
  }
22752
23949
  function writeChangeProofOutput(outputDir, run) {
22753
23950
  if (!outputDir) return;
22754
23951
  (0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
22755
23952
  (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "change-proof-result.json"), `${JSON.stringify(run.result, null, 2)}
22756
23953
  `);
23954
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "change-proof-receipt.json"), `${JSON.stringify(run.receipt, null, 2)}
23955
+ `);
23956
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "change-proof-receipt.md"), riddleProofChangeReceiptMarkdown(run.receipt));
23957
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "change-proof-receipt.html"), riddleProofChangeReceiptHtml(run.receipt));
23958
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "handoff-receipt.json"), `${JSON.stringify(run.handoff, null, 2)}
23959
+ `);
23960
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "handoff-receipt.md"), buildRiddleProofPrCommentMarkdown({ result: run.handoff }));
22757
23961
  (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);
23962
+ writeProfileOutput(changeProofSideOutputDir(outputDir, "before"), run.beforeResult, { observation: run.beforeObservation });
23963
+ writeProfileOutput(changeProofSideOutputDir(outputDir, "after"), run.afterResult, { observation: run.afterObservation });
22760
23964
  }
22761
23965
  function writeRunChangeProofResult(run, options) {
22762
23966
  const format = runProfileResultFormatOption(options);
@@ -22855,6 +24059,7 @@ function withRiddleMetadata(result, input) {
22855
24059
  ...result.riddle || {},
22856
24060
  job_id: input.job_id || result.riddle?.job_id,
22857
24061
  status: input.status ?? result.riddle?.status,
24062
+ phase: poll?.phase ?? result.riddle?.phase,
22858
24063
  terminal: input.terminal ?? result.riddle?.terminal,
22859
24064
  created_at: poll?.created_at ?? result.riddle?.created_at,
22860
24065
  submitted_at: poll?.submitted_at ?? result.riddle?.submitted_at,
@@ -22867,7 +24072,8 @@ function withRiddleMetadata(result, input) {
22867
24072
  timed_out: poll?.timed_out ?? result.riddle?.timed_out,
22868
24073
  retry_count: input.retryCount ?? result.riddle?.retry_count,
22869
24074
  stale_job_ids: staleJobIds?.length ? staleJobIds : result.riddle?.stale_job_ids,
22870
- artifact_recovery: input.artifactRecovery ?? result.riddle?.artifact_recovery
24075
+ artifact_recovery: input.artifactRecovery ?? result.riddle?.artifact_recovery,
24076
+ execution: poll?.execution ?? result.riddle?.execution
22871
24077
  },
22872
24078
  artifacts: {
22873
24079
  ...result.artifacts,
@@ -22935,6 +24141,7 @@ function riddleMetadataFromPoll(jobId, poll) {
22935
24141
  return {
22936
24142
  job_id: jobId,
22937
24143
  status: poll.status,
24144
+ phase: poll.poll?.phase,
22938
24145
  terminal: poll.terminal,
22939
24146
  created_at: poll.poll?.created_at,
22940
24147
  submitted_at: poll.poll?.submitted_at,
@@ -22944,7 +24151,8 @@ function riddleMetadataFromPoll(jobId, poll) {
22944
24151
  elapsed_ms: poll.poll?.elapsed_ms,
22945
24152
  attempt: poll.poll?.attempt,
22946
24153
  attempts: poll.poll?.attempts,
22947
- timed_out: poll.poll?.timed_out
24154
+ timed_out: poll.poll?.timed_out,
24155
+ execution: poll.poll?.execution
22948
24156
  };
22949
24157
  }
22950
24158
  function profileUnsubmittedRetryTimeoutMs(options) {
@@ -23490,7 +24698,7 @@ async function runSplitViewportProfileForCli(profile, options, input) {
23490
24698
  const childProfile = profileForSplitViewport(profile, viewport);
23491
24699
  const childOutputDir = outputDir ? splitViewportOutputDir(outputDir, viewport.name, seenOutputNames) : void 0;
23492
24700
  const result2 = await runSingleRiddleProfileForCli(childProfile, options, { ...input, outputDir: childOutputDir });
23493
- if (childOutputDir) writeProfileOutput(childOutputDir, result2);
24701
+ if (childOutputDir) writeProfileOutput(childOutputDir, result2, profileOutputOptionsForCli(options));
23494
24702
  childRuns.push({ viewport, profile: childProfile, result: result2 });
23495
24703
  }
23496
24704
  const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
@@ -23622,18 +24830,34 @@ async function runChangeProofForCli(rawProfile, options) {
23622
24830
  const contract = readChangeContractForCli(options);
23623
24831
  const beforeResultPath = optionString(options, "beforeResult");
23624
24832
  const afterResultPath = optionString(options, "afterResult");
24833
+ const beforeObservationPath = optionString(options, "beforeObservation");
24834
+ const afterObservationPath = optionString(options, "afterObservation");
23625
24835
  const beforeUrl = optionString(options, "beforeUrl");
23626
24836
  const afterUrl = optionString(options, "afterUrl");
24837
+ const beforePreviewInput = optionString(options, "beforePreviewReceipt");
24838
+ const afterPreviewInput = optionString(options, "afterPreviewReceipt");
24839
+ const beforePreview = beforePreviewInput ? parseRiddlePreviewReceipt(readJsonValue(beforePreviewInput, "--before-preview-receipt")) : void 0;
24840
+ const afterPreview = afterPreviewInput ? parseRiddlePreviewReceipt(readJsonValue(afterPreviewInput, "--after-preview-receipt")) : void 0;
23627
24841
  const hasResultInputs = Boolean(beforeResultPath || afterResultPath);
24842
+ const hasObservationInputs = Boolean(beforeObservationPath || afterObservationPath);
23628
24843
  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.");
24844
+ if ([hasResultInputs, hasObservationInputs, hasUrlInputs].filter(Boolean).length > 1) {
24845
+ throw new Error("run-change-proof accepts one before/after input pair: result, observation, or URL.");
23631
24846
  }
23632
24847
  if (hasResultInputs && (!beforeResultPath || !afterResultPath)) {
23633
24848
  throw new Error("run-change-proof requires both --before-result and --after-result.");
23634
24849
  }
23635
- if (!hasResultInputs && (!beforeUrl || !afterUrl)) {
23636
- throw new Error("run-change-proof requires both --before-url and --after-url unless saved results are provided.");
24850
+ if (hasObservationInputs && (!beforeObservationPath || !afterObservationPath)) {
24851
+ throw new Error("run-change-proof requires both --before-observation and --after-observation.");
24852
+ }
24853
+ if (hasUrlInputs && (!beforeUrl || !afterUrl)) {
24854
+ throw new Error("run-change-proof requires both --before-url and --after-url.");
24855
+ }
24856
+ if (!hasResultInputs && !hasObservationInputs && !hasUrlInputs) {
24857
+ throw new Error("run-change-proof requires a before/after result, observation, or URL pair.");
24858
+ }
24859
+ if ((beforePreview || afterPreview) && !hasUrlInputs) {
24860
+ throw new Error("--before-preview-receipt and --after-preview-receipt require the URL input mode.");
23637
24861
  }
23638
24862
  const outputDir = profileOutputDirOption(options);
23639
24863
  let profile;
@@ -23641,15 +24865,53 @@ async function runChangeProofForCli(rawProfile, options) {
23641
24865
  let afterResult;
23642
24866
  let beforeSource;
23643
24867
  let afterSource;
23644
- if (hasResultInputs) {
24868
+ let beforeObservation;
24869
+ let afterObservation;
24870
+ if (hasObservationInputs) {
24871
+ if (!beforeObservationPath || !afterObservationPath) {
24872
+ throw new Error("run-change-proof requires both --before-observation and --after-observation.");
24873
+ }
24874
+ profile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, options), options);
24875
+ beforeObservation = observationForChangeSide(
24876
+ parseRiddleProofObservationReceipt(readJsonValue(beforeObservationPath, "--before-observation")),
24877
+ "before"
24878
+ );
24879
+ afterObservation = observationForChangeSide(
24880
+ parseRiddleProofObservationReceipt(readJsonValue(afterObservationPath, "--after-observation")),
24881
+ "after"
24882
+ );
24883
+ if (!beforeObservation.proof?.result || !afterObservation.proof?.result) {
24884
+ throw new Error("Change Proof observations must include their profile results.");
24885
+ }
24886
+ beforeResult = beforeObservation.proof.result;
24887
+ afterResult = afterObservation.proof.result;
24888
+ beforeSource = beforeObservation.target.url;
24889
+ afterSource = afterObservation.target.url;
24890
+ } else if (hasResultInputs) {
23645
24891
  if (!beforeResultPath || !afterResultPath) {
23646
24892
  throw new Error("run-change-proof requires both --before-result and --after-result.");
23647
24893
  }
23648
24894
  profile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, options), options);
23649
24895
  beforeResult = readProfileResultForCli(beforeResultPath, "--before-result");
23650
24896
  afterResult = readProfileResultForCli(afterResultPath, "--after-result");
23651
- beforeSource = beforeResultPath;
23652
- afterSource = afterResultPath;
24897
+ beforeSource = profileResultTargetUrl(beforeResult);
24898
+ afterSource = profileResultTargetUrl(afterResult);
24899
+ beforeObservation = createRiddleProofObservationReceipt({
24900
+ comparison_role: "before",
24901
+ executor: { kind: beforeResult.runner === "local-playwright" ? "local_playwright" : "riddle_hosted", runner: beforeResult.runner, job_id: beforeResult.riddle?.job_id },
24902
+ target: { kind: "url", url: beforeSource },
24903
+ profile_result: beforeResult,
24904
+ execution: beforeResult.riddle?.execution,
24905
+ publication: { kind: "local", path: beforeResultPath }
24906
+ });
24907
+ afterObservation = createRiddleProofObservationReceipt({
24908
+ comparison_role: "after",
24909
+ executor: { kind: afterResult.runner === "local-playwright" ? "local_playwright" : "riddle_hosted", runner: afterResult.runner, job_id: afterResult.riddle?.job_id },
24910
+ target: { kind: "url", url: afterSource },
24911
+ profile_result: afterResult,
24912
+ execution: afterResult.riddle?.execution,
24913
+ publication: { kind: "local", path: afterResultPath }
24914
+ });
23653
24915
  } else {
23654
24916
  if (!beforeUrl || !afterUrl) {
23655
24917
  throw new Error("run-change-proof requires both --before-url and --after-url unless saved results are provided.");
@@ -23663,12 +24925,54 @@ async function runChangeProofForCli(rawProfile, options) {
23663
24925
  afterResult = await runProfileForCli(afterProfile, afterOptions);
23664
24926
  beforeSource = beforeUrl;
23665
24927
  afterSource = afterUrl;
24928
+ beforeObservation = createRiddleProofObservationReceipt({
24929
+ comparison_role: "before",
24930
+ executor: { kind: "riddle_hosted", runner: beforeResult.runner, job_id: beforeResult.riddle?.job_id },
24931
+ target: beforePreview ? { kind: "preview", url: beforeSource, preview: beforePreview } : { kind: "url", url: beforeSource },
24932
+ source: beforePreview?.source,
24933
+ profile_result: beforeResult,
24934
+ execution: beforeResult.riddle?.execution
24935
+ });
24936
+ afterObservation = createRiddleProofObservationReceipt({
24937
+ comparison_role: "after",
24938
+ executor: { kind: "riddle_hosted", runner: afterResult.runner, job_id: afterResult.riddle?.job_id },
24939
+ target: afterPreview ? { kind: "preview", url: afterSource, preview: afterPreview } : { kind: "url", url: afterSource },
24940
+ source: afterPreview?.source,
24941
+ profile_result: afterResult,
24942
+ execution: afterResult.riddle?.execution
24943
+ });
23666
24944
  }
23667
24945
  const result = assessRiddleProofChange(contract, {
23668
24946
  before_result: beforeResult,
23669
- after_result: afterResult
24947
+ after_result: afterResult,
24948
+ before_observation: beforeObservation,
24949
+ after_observation: afterObservation,
24950
+ expected_source_revisions: {
24951
+ before: optionString(options, "beforeSourceRevision"),
24952
+ after: optionString(options, "afterSourceRevision")
24953
+ }
24954
+ });
24955
+ const receipt = createRiddleProofChangeReceipt({
24956
+ contract,
24957
+ result,
24958
+ before_observation: beforeObservation,
24959
+ after_observation: afterObservation,
24960
+ profile_name: profile.name
23670
24961
  });
23671
- return { profile, contract, beforeResult, afterResult, result, beforeSource, afterSource };
24962
+ const handoff = createRiddleProofHandoffReceipt(receipt);
24963
+ return {
24964
+ profile,
24965
+ contract,
24966
+ beforeResult,
24967
+ afterResult,
24968
+ result,
24969
+ receipt,
24970
+ handoff,
24971
+ beforeObservation,
24972
+ afterObservation,
24973
+ beforeSource,
24974
+ afterSource
24975
+ };
23672
24976
  }
23673
24977
  function requestForRun(options) {
23674
24978
  const statePath = optionString(options, "statePath");
@@ -23720,7 +25024,7 @@ async function main() {
23720
25024
  if (command === "run-profile") {
23721
25025
  const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
23722
25026
  const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : positional[1] === "aggregate" ? await aggregateProfileResultsForCli(profile, options) : await runProfileForCli(profile, options);
23723
- writeProfileOutput(profileOutputDirOption(options), result);
25027
+ writeProfileOutput(profileOutputDirOption(options), result, profileOutputOptionsForCli(options));
23724
25028
  const diagnosticLine = profileCliDiagnosticLine(result);
23725
25029
  if (diagnosticLine && optionBoolean(options, "quiet") !== true) {
23726
25030
  process.stderr.write(`${diagnosticLine}
@@ -23751,7 +25055,10 @@ async function main() {
23751
25055
  const explicitResultJsonPath = optionString(options, "resultJson");
23752
25056
  const runResponsePath = explicitRunResponsePath || defaultProofDirJsonPath(proofDir, "riddle-run-response.json");
23753
25057
  const resultJsonPaths = explicitResultJsonPath ? [explicitResultJsonPath] : [
25058
+ defaultProofDirJsonPath(proofDir, "handoff-receipt.json"),
23754
25059
  defaultProofDirJsonPath(proofDir, "result.json"),
25060
+ defaultProofDirJsonPath(proofDir, "change-proof-receipt.json"),
25061
+ defaultProofDirJsonPath(proofDir, "change-proof-result.json"),
23755
25062
  defaultProofDirJsonPath(proofDir, "profile-result.json")
23756
25063
  ];
23757
25064
  const runResponse = readJsonFileIfExists(runResponsePath);
@@ -23870,6 +25177,16 @@ async function main() {
23870
25177
  };
23871
25178
  }
23872
25179
  const result = await createRiddleApiClient(clientConfig).deployPreview(buildDir, label, previewFrameworkOption(options));
25180
+ const outputDir = profileOutputDirOption(options);
25181
+ if (outputDir) {
25182
+ (0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
25183
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "preview-deploy-result.json"), `${JSON.stringify(result, null, 2)}
25184
+ `);
25185
+ if (result.receipt) {
25186
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "preview-receipt.json"), `${JSON.stringify(result.receipt, null, 2)}
25187
+ `);
25188
+ }
25189
+ }
23873
25190
  for (const warning of result.warnings ?? []) {
23874
25191
  process.stderr.write(`Warning: ${warning}
23875
25192
  `);