@riddledc/riddle-proof 0.8.77 → 0.8.79

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 (48) hide show
  1. package/README.md +65 -3
  2. package/dist/change-proof.cjs +744 -85
  3. package/dist/change-proof.d.cts +100 -8
  4. package/dist/change-proof.d.ts +100 -8
  5. package/dist/change-proof.js +15 -1
  6. package/dist/{chunk-B2DP2LET.js → chunk-5IFZSUPF.js} +52 -13
  7. package/dist/chunk-6VFS2JFR.js +886 -0
  8. package/dist/{chunk-IY4W6STC.js → chunk-7N6X54WG.js} +116 -17
  9. package/dist/{chunk-GG2D3MFZ.js → chunk-FCSJZBC5.js} +26 -1
  10. package/dist/chunk-MEVVL4TI.js +258 -0
  11. package/dist/{chunk-H25IDX76.js → chunk-NEXWITV4.js} +221 -35
  12. package/dist/{chunk-UE4I7RTI.js → chunk-RQPCKRKT.js} +1 -1
  13. package/dist/cli/index.js +7 -6
  14. package/dist/cli.cjs +2026 -1060
  15. package/dist/cli.js +7 -6
  16. package/dist/index.cjs +932 -122
  17. package/dist/index.d.cts +4 -3
  18. package/dist/index.d.ts +4 -3
  19. package/dist/index.js +42 -14
  20. package/dist/pr-comment.cjs +416 -17
  21. package/dist/pr-comment.d.cts +6 -1
  22. package/dist/pr-comment.d.ts +6 -1
  23. package/dist/pr-comment.js +6 -1
  24. package/dist/profile/index.cjs +26 -1
  25. package/dist/profile/index.js +1 -1
  26. package/dist/profile-suggestions.js +2 -2
  27. package/dist/profile.cjs +26 -1
  28. package/dist/profile.d.cts +7 -0
  29. package/dist/profile.d.ts +7 -0
  30. package/dist/profile.js +1 -1
  31. package/dist/receipts.cjs +286 -0
  32. package/dist/receipts.d.cts +115 -0
  33. package/dist/receipts.d.ts +115 -0
  34. package/dist/receipts.js +15 -0
  35. package/dist/riddle-client.cjs +98 -13
  36. package/dist/riddle-client.d.cts +18 -5
  37. package/dist/riddle-client.d.ts +18 -5
  38. package/dist/riddle-client.js +4 -1
  39. package/dist/runtime/index.cjs +98 -13
  40. package/dist/runtime/index.d.cts +5 -1
  41. package/dist/runtime/index.d.ts +5 -1
  42. package/dist/runtime/index.js +4 -1
  43. package/dist/runtime/riddle-client.cjs +98 -13
  44. package/dist/runtime/riddle-client.d.cts +5 -1
  45. package/dist/runtime/riddle-client.d.ts +5 -1
  46. package/dist/runtime/riddle-client.js +4 -1
  47. package/package.json +7 -2
  48. package/dist/chunk-BILL3UC2.js +0 -488
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,1113 +18354,1587 @@ 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) {
18121
- const resultArtifacts = asRecord2(result.artifacts);
18122
- const artifacts = [];
18123
- const seen = /* @__PURE__ */ new Set();
18124
- for (const [index, item] of asArray(resultArtifacts.riddle_artifacts).entries()) {
18125
- const artifact = asRecord2(item);
18126
- const url = stringValue4(artifact.url);
18127
- if (!url || seen.has(url)) continue;
18128
- seen.add(url);
18129
- const fallbackName = stringValue4(artifact.kind) || `artifact-${index + 1}`;
18130
- const name = artifactDisplayName(artifact.name, fallbackName);
18131
- artifacts.push({
18132
- name,
18133
- url,
18134
- kind: artifactKind(name, url),
18135
- size_bytes: numberValue3(artifact.size_bytes) ?? numberValue3(artifact.size)
18136
- });
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
+ };
18137
18531
  }
18138
- return artifacts;
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
+ };
18139
18651
  }
18140
- function collectChangeReceiptArtifacts(result) {
18141
- const artifacts = [];
18142
- const seen = /* @__PURE__ */ new Set();
18143
- const collectSide = (sideName, side) => {
18144
- const candidates = [
18145
- ...asArray(side.screenshots),
18146
- ...asArray(side.artifacts)
18147
- ];
18148
- for (const [index, item] of candidates.entries()) {
18149
- const artifact = asRecord2(item);
18150
- const url = stringValue4(artifact.url) || stringValue4(artifact.path);
18151
- if (!url || seen.has(url)) continue;
18152
- seen.add(url);
18153
- const fallbackName = `${sideName}-artifact-${index + 1}`;
18154
- const name = `${sideName}/${artifactDisplayName(artifact.name, fallbackName)}`;
18155
- artifacts.push({
18156
- name,
18157
- url,
18158
- kind: artifactKind(name, url),
18159
- size_bytes: numberValue3(artifact.size_bytes) ?? numberValue3(artifact.size)
18160
- });
18161
- }
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
18162
18708
  };
18163
- collectSide("before", asRecord2(result.before));
18164
- collectSide("after", asRecord2(result.after));
18165
- return artifacts;
18166
18709
  }
18167
- function mergeArtifacts(...artifactLists) {
18168
- const artifacts = [];
18169
- const seen = /* @__PURE__ */ new Set();
18170
- for (const artifact of artifactLists.flat()) {
18171
- if (seen.has(artifact.url)) continue;
18172
- seen.add(artifact.url);
18173
- artifacts.push(artifact);
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;
18174
18731
  }
18175
- return artifacts;
18732
+ return counts;
18176
18733
  }
18177
- function pageSummaries(result) {
18178
- const pages = [];
18179
- for (const page of asArray(result.pages)) {
18180
- const record = asRecord2(page);
18181
- const route = stringValue4(record.route) || stringValue4(record.url) || "page";
18182
- const checks = asRecord2(record.checks);
18183
- let passed = 0;
18184
- let failed = 0;
18185
- for (const value of Object.values(checks)) {
18186
- if (value === true) passed += 1;
18187
- if (value === false) failed += 1;
18188
- }
18189
- pages.push({ route, passed, failed });
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
+ };
18190
18751
  }
18191
- return pages;
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 };
18192
18754
  }
18193
- function profileCheckCounts(result) {
18194
- const checks = asArray(result.checks);
18195
- if (checks.length) {
18196
- let passed2 = 0;
18197
- let failed2 = 0;
18198
- for (const item of checks) {
18199
- const status = stringValue4(asRecord2(item).status);
18200
- if (status === "passed") passed2 += 1;
18201
- if (status === "failed") failed2 += 1;
18202
- }
18203
- return { passed: passed2, failed: failed2 };
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.`);
18204
18765
  }
18205
- const checkCounts = asRecord2(result.check_counts);
18206
- const passed = numberValue3(checkCounts.passed);
18207
- const failed = numberValue3(checkCounts.failed);
18208
- if (typeof passed === "number" || typeof failed === "number") {
18209
- return { passed: passed ?? 0, failed: failed ?? 0 };
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.`);
18210
18775
  }
18211
- return void 0;
18212
18776
  }
18213
- function profilePageSummaries(result, counts) {
18214
- if (!counts) return [];
18215
- const route = asRecord2(result.route);
18216
- return [{
18217
- route: firstStringValue2(route.observed, route.expected_path, route.requested, result.profile_name) || "profile",
18218
- passed: counts.passed,
18219
- failed: counts.failed
18220
- }];
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;
18221
18780
  }
18222
- function changeReceiptCheckCounts(result) {
18223
- const deltas = asArray(result.deltas);
18224
- if (!deltas.length) return void 0;
18225
- let passed = 0;
18226
- let failed = 0;
18227
- for (const item of deltas) {
18228
- const status = stringValue4(asRecord2(item).status);
18229
- if (status === "passed") passed += 1;
18230
- if (status === "failed" || status === "proof_insufficient" || status === "configuration_error") failed += 1;
18231
- }
18232
- return { passed, failed };
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
+ };
18233
18787
  }
18234
- function changeReceiptPageSummaries(result) {
18235
- const pages = [];
18236
- for (const sideName of ["before", "after"]) {
18237
- const side = asRecord2(result[sideName]);
18238
- if (!Object.keys(side).length) continue;
18239
- const checks = asRecord2(side.checks);
18240
- pages.push({
18241
- route: firstStringValue2(side.source, side.profile_name) || sideName,
18242
- passed: numberValue3(checks.passed) ?? 0,
18243
- failed: numberValue3(checks.failed) ?? 0
18244
- });
18245
- }
18246
- return pages;
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
+ });
18247
18800
  }
18248
- function summarizeExplicitChecks(value) {
18249
- let passed = 0;
18250
- let failed = 0;
18251
- const visit = (current, inChecks = false) => {
18252
- if (current === true && inChecks) {
18253
- passed += 1;
18254
- return;
18255
- }
18256
- if (current === false && inChecks) {
18257
- failed += 1;
18258
- return;
18259
- }
18260
- if (Array.isArray(current)) {
18261
- for (const item of current) visit(item, inChecks);
18262
- return;
18263
- }
18264
- if (current && typeof current === "object") {
18265
- for (const [key, item] of Object.entries(current)) {
18266
- visit(item, inChecks || key === "checks");
18267
- }
18268
- }
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
18269
18831
  };
18270
- visit(value);
18271
- return { passed, failed };
18272
- }
18273
- function selectPrimaryImage(artifacts) {
18274
- const images = artifacts.filter((artifact) => artifact.kind === "image");
18275
- return images.find((artifact) => /after|proof|screenshot/i.test(artifact.name)) || images[0];
18276
18832
  }
18277
- function firstRecordValue2(...values) {
18278
- for (const value of values) {
18279
- const record = asRecord2(value);
18280
- if (Object.keys(record).length) return record;
18281
- }
18282
- return void 0;
18833
+ function isRecord3(value) {
18834
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
18283
18835
  }
18284
- function checkpointSummaryFrom2(...values) {
18285
- const record = firstRecordValue2(...values);
18286
- if (!record) return void 0;
18287
- const summary = {
18288
- pending: booleanValue3(record.pending),
18289
- response_count: numberValue3(record.response_count),
18290
- rejected_response_count: numberValue3(record.rejected_response_count),
18291
- ignored_response_count: numberValue3(record.ignored_response_count),
18292
- duplicate_response_count: numberValue3(record.duplicate_response_count),
18293
- latest_decision: stringValue4(record.latest_decision),
18294
- latest_packet_id: stringValue4(record.latest_packet_id),
18295
- latest_resume_token: stringValue4(record.latest_resume_token)
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 }
18296
18864
  };
18297
- return Object.values(summary).some((value) => typeof value !== "undefined") ? summary : void 0;
18298
- }
18299
- function isProfileResult(result) {
18300
- const version = stringValue4(result.version);
18301
- return version === "riddle-proof.profile-result.v1" || version === "riddle-proof.profile-compact-result.v1";
18302
- }
18303
- function isChangeReceiptResult(result) {
18304
- return stringValue4(result.version) === "riddle-proof.change-receipt.v1";
18305
- }
18306
- function isChangeResult(result) {
18307
- const version = stringValue4(result.version);
18308
- return version === "riddle-proof.change-result.v1" || version === "riddle-proof.change-compact-result.v1";
18309
18865
  }
18310
- function summarizeRiddleProofPrComment(input) {
18311
- const runResponse = asRecord2(input.runResponse);
18312
- const result = asRecord2(input.result);
18313
- const proofResult = asRecord2(runResponse.proofResult);
18314
- const profileResult = isProfileResult(result);
18315
- const changeReceiptResult = isChangeReceiptResult(result);
18316
- const changeResult = changeReceiptResult || isChangeResult(result);
18317
- const profileRiddle = asRecord2(result.riddle);
18318
- const preview = asRecord2(runResponse.preview);
18319
- const resultRunCard = asRecord2(result.run_card);
18320
- const stopCondition = asRecord2(resultRunCard.stop_condition);
18321
- const resultDetails = asRecord2(result.details);
18322
- const resultRaw = asRecord2(result.raw);
18323
- const rawDetails = asRecord2(resultRaw.details);
18324
- const profileChecks = profileResult ? profileCheckCounts(result) : void 0;
18325
- const changeChecks = changeResult ? changeReceiptCheckCounts(result) : void 0;
18326
- const artifacts = mergeArtifacts(
18327
- collectArtifacts(runResponse),
18328
- profileResult ? collectProfileArtifacts(result) : [],
18329
- changeReceiptResult ? collectChangeReceiptArtifacts(result) : []
18330
- );
18331
- const pages = changeReceiptResult ? changeReceiptPageSummaries(result) : profileResult ? profilePageSummaries(result, profileChecks) : pageSummaries(result);
18332
- const checkSource = { ...result };
18333
- delete checkSource.ok;
18334
- const nestedChecks = summarizeExplicitChecks(checkSource);
18335
- const resultStatus = firstStringValue2(result.status, stopCondition.status);
18336
- const ok = changeResult ? resultStatus === "passed" : booleanValue3(result.ok) ?? booleanValue3(runResponse.ok) ?? null;
18337
- const checkpointSummary = checkpointSummaryFrom2(
18338
- result.checkpoint_summary,
18339
- stopCondition.checkpoint_summary,
18340
- resultDetails.checkpoint_summary,
18341
- rawDetails.checkpoint_summary,
18342
- proofResult.checkpoint_summary
18343
- );
18344
- const publicState = summarizeRiddleProofPublicState({
18345
- ...result,
18346
- status: resultStatus,
18347
- checkpoint_summary: checkpointSummary || result.checkpoint_summary
18348
- });
18349
- const mergeRecommendation = riddleProofPublicStateMergeRecommendation(
18350
- publicState,
18351
- firstStringValue2(result.merge_recommendation, stopCondition.merge_recommendation, resultRaw.merge_recommendation)
18352
- );
18866
+ function migrateRiddleProofChangeReceipt(legacy) {
18867
+ const recommendation = changeRecommendation(legacy.verdict);
18353
18868
  return {
18354
- ok,
18355
- status: firstStringValue2(proofResult.status, profileRiddle.status),
18356
- result_status: changeResult ? resultStatus : publicState.status,
18357
- job_id: firstStringValue2(proofResult.job_id, profileRiddle.job_id),
18358
- duration_ms: numberValue3(proofResult.duration_ms) ?? numberValue3(profileRiddle.elapsed_ms),
18359
- proof_url: stringValue4(runResponse.proofUrl),
18360
- preview_id: stringValue4(preview.id),
18361
- preview_url: stringValue4(preview.preview_url) || stringValue4(preview.url),
18362
- preview_publish_recovered: booleanValue3(preview.publish_recovered),
18363
- preview_publish_error: stringValue4(preview.publish_error),
18364
- ship_held: publicState.ship_held,
18365
- shipping_disabled: publicState.shipping_disabled,
18366
- ship_authorized: publicState.ship_authorized,
18367
- merge_ready: publicState.merge_ready,
18368
- sync_allowed: publicState.sync_allowed,
18369
- proof_decision: firstStringValue2(result.proof_decision, stopCondition.proof_decision, resultRaw.proof_decision),
18370
- merge_recommendation: changeReceiptResult && resultStatus ? stringValue4(result.verdict) : mergeRecommendation,
18371
- checkpoint_summary: checkpointSummary,
18372
- public_state: publicState,
18373
- passed_checks: changeChecks?.passed ?? profileChecks?.passed ?? nestedChecks.passed,
18374
- failed_checks: changeChecks?.failed ?? profileChecks?.failed ?? nestedChecks.failed,
18375
- pages,
18376
- artifacts,
18377
- primary_image: selectPrimaryImage(artifacts)
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
18378
18907
  };
18379
18908
  }
18380
- function formatDuration(ms) {
18381
- if (typeof ms !== "number" || !Number.isFinite(ms)) return "";
18382
- const seconds = Math.max(0, Math.round(ms / 1e3));
18383
- const minutes = Math.floor(seconds / 60);
18384
- const remainder = seconds % 60;
18385
- return minutes > 0 ? `${minutes}m${String(remainder).padStart(2, "0")}s` : `${seconds}s`;
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;
18386
18935
  }
18387
- function markdownLink(label, url) {
18388
- return `[${label.replace(/\]/g, "\\]")}](${url})`;
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
+ };
18389
18952
  }
18390
- function resultLabel(summary) {
18391
- if (summary.public_state?.result_label) return summary.public_state.result_label;
18392
- if (summary.ok === true) {
18393
- if (summary.result_status === "shipped") return "shipped";
18394
- if (summary.result_status === "completed") return "completed";
18395
- if (summary.ship_held === true) return "proof passed; ship held";
18396
- if (summary.ship_authorized === true) return "passed; ship authorized";
18397
- return "passed";
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")}.`);
18398
18956
  }
18399
- if (summary.ok === false) return "failed";
18400
- return summary.result_status || summary.status || "recorded";
18401
- }
18402
- function artifactRank(artifact) {
18403
- const name = artifact.name.toLowerCase();
18404
- if (name === "proof.json") return 0;
18405
- if (name === "result.json") return 1;
18406
- if (name.includes("proof") && name.endsWith(".json") && !name.includes("layout")) return 2;
18407
- if (name === "console.json") return 3;
18408
- if (artifact.kind === "data") return 10;
18409
- if (artifact.kind === "image") return 20;
18410
- return 30;
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;
18411
18970
  }
18412
- function formatBool(value) {
18413
- return typeof value === "boolean" ? String(value) : "unknown";
18971
+ function markdownTableCell(value) {
18972
+ return String(value ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
18414
18973
  }
18415
- function hasShipControl(summary) {
18416
- return typeof summary.ship_held === "boolean" || typeof summary.shipping_disabled === "boolean" || typeof summary.ship_authorized === "boolean";
18974
+ function artifactTarget(artifact) {
18975
+ return artifact.url || artifact.path;
18417
18976
  }
18418
- function hasHandoffControl(summary) {
18419
- return typeof summary.merge_ready === "boolean" || typeof summary.sync_allowed === "boolean";
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";
18420
18981
  }
18421
- function shouldRenderMergeRecommendation(summary) {
18422
- if (!summary.merge_recommendation) return false;
18423
- return riddleProofPublicStateAllowsMergeRecommendation(summary.public_state);
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
+ };
18424
19020
  }
18425
- function checkpointSummaryLine(summary) {
18426
- const accepted = summary.response_count ?? 0;
18427
- const rejected = summary.rejected_response_count ?? 0;
18428
- const ignored = summary.ignored_response_count ?? 0;
18429
- const parts = [`${accepted} accepted`, `${rejected} rejected`, `${ignored} ignored`];
18430
- if ((summary.duplicate_response_count ?? 0) > 0) parts.push(`${summary.duplicate_response_count} duplicate`);
18431
- const state = summary.pending === true ? "pending" : summary.pending === false ? "complete" : "";
18432
- return [
18433
- parts.join(" / "),
18434
- state,
18435
- summary.latest_decision ? `latest decision \`${summary.latest_decision}\`` : ""
18436
- ].filter(Boolean).join("; ");
19021
+ function markdownLink(label, target) {
19022
+ return `[${label.replace(/\]/g, "\\]")}](${target})`;
18437
19023
  }
18438
- function buildRiddleProofPrCommentMarkdown(input) {
18439
- const summary = summarizeRiddleProofPrComment(input);
18440
- const title = input.title?.trim() || "Riddle Proof Evidence";
18441
- const lines = [
18442
- RIDDLE_PROOF_PR_COMMENT_MARKER,
18443
- `## ${title}`,
18444
- "",
18445
- `**Result:** ${resultLabel(summary)}`
18446
- ];
18447
- if (input.goal?.trim()) lines.push(`**Goal:** ${input.goal.trim()}`);
18448
- if (input.successCriteria?.trim()) lines.push(`**Success criteria:** ${input.successCriteria.trim()}`);
18449
- if (summary.result_status) lines.push(`**Evidence status:** ${summary.result_status}`);
18450
- if (summary.status) lines.push(`**Riddle job status:** ${summary.status}`);
18451
- if (summary.job_id) lines.push(`**Riddle job:** \`${summary.job_id}\``);
18452
- if (summary.duration_ms) lines.push(`**Duration:** ${formatDuration(summary.duration_ms)}`);
18453
- if (hasShipControl(summary)) {
18454
- lines.push(`**Ship control:** held=${formatBool(summary.ship_held)}, shipping_disabled=${formatBool(summary.shipping_disabled)}, authorized=${formatBool(summary.ship_authorized)}`);
18455
- }
18456
- if (hasHandoffControl(summary)) {
18457
- lines.push(`**Handoff:** merge_ready=${formatBool(summary.merge_ready)}, sync_allowed=${formatBool(summary.sync_allowed)}`);
18458
- }
18459
- if (summary.proof_decision) lines.push(`**Proof decision:** \`${summary.proof_decision}\``);
18460
- if (shouldRenderMergeRecommendation(summary)) lines.push(`**Merge recommendation:** ${summary.merge_recommendation}`);
18461
- if (summary.checkpoint_summary) lines.push(`**Checkpoints:** ${checkpointSummaryLine(summary.checkpoint_summary)}`);
18462
- if (summary.proof_url) lines.push(`**Proof URL:** ${markdownLink(summary.proof_url, summary.proof_url)}`);
18463
- if (summary.preview_id || summary.preview_url) {
18464
- const previewLabel = summary.preview_id ? `\`${summary.preview_id}\`` : "preview";
18465
- lines.push(`**Preview:** ${summary.preview_url ? markdownLink(previewLabel, summary.preview_url) : previewLabel}`);
19024
+ function appendMarkdownArtifacts(lines, title, artifacts) {
19025
+ lines.push(`### ${title}`, "");
19026
+ if (!artifacts.length) {
19027
+ lines.push("- No screenshot artifacts recorded.", "");
19028
+ return;
18466
19029
  }
18467
- if (summary.preview_publish_recovered) {
18468
- const detail = summary.preview_publish_error ? `: ${summary.preview_publish_error}` : "";
18469
- lines.push(`**Preview publish recovery:** recovered after publish error${detail}`);
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
+ }
18470
19039
  }
18471
- lines.push(`**Checks:** ${summary.passed_checks} passed / ${summary.failed_checks} failed`);
19040
+ if (artifacts.length > 6) lines.push(`- ${artifacts.length - 6} more artifact(s) omitted`);
18472
19041
  lines.push("");
18473
- if (summary.primary_image) {
18474
- lines.push("### Screenshot");
18475
- lines.push(`![${summary.primary_image.name}](${summary.primary_image.url})`);
18476
- lines.push("");
18477
- }
18478
- if (summary.pages.length) {
18479
- lines.push("### Page Checks");
18480
- for (const page of summary.pages.slice(0, 12)) {
18481
- lines.push(`- \`${page.route}\`: ${page.passed} passed / ${page.failed} failed`);
18482
- }
18483
- if (summary.pages.length > 12) lines.push(`- ${summary.pages.length - 12} more page(s) omitted`);
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}`);
18484
19087
  lines.push("");
18485
19088
  }
18486
- const linkedArtifacts = summary.artifacts.filter((artifact) => artifact.url !== summary.primary_image?.url).sort((left, right) => artifactRank(left) - artifactRank(right) || left.name.localeCompare(right.name)).slice(0, 20);
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);
18487
19095
  if (linkedArtifacts.length) {
18488
- lines.push("### Artifacts");
19096
+ lines.push("## Artifacts", "");
18489
19097
  for (const artifact of linkedArtifacts) {
18490
- lines.push(`- ${markdownLink(artifact.name, artifact.url)}`);
18491
- }
18492
- if (summary.artifacts.length - (summary.primary_image ? 1 : 0) > linkedArtifacts.length) {
18493
- lines.push(`- ${summary.artifacts.length - (summary.primary_image ? 1 : 0) - linkedArtifacts.length} more artifact(s) omitted`);
19098
+ lines.push(`- ${artifact.side}: ${markdownLink(artifact.name, artifactTarget(artifact) || "")}`);
18494
19099
  }
18495
19100
  lines.push("");
18496
19101
  }
18497
- if (input.source?.trim()) {
18498
- lines.push(`_Source: ${input.source.trim()}_`);
18499
- } else {
18500
- lines.push("_Updated by `riddle-proof-loop pr-comment`._");
18501
- }
18502
19102
  return `${lines.join("\n").trim()}
18503
19103
  `;
18504
19104
  }
18505
-
18506
- // src/profile-suggestions.ts
18507
- var RIDDLE_PROOF_PROFILE_SUGGESTIONS_VERSION = "riddle-proof.profile-suggestions.v1";
18508
- var UI_FILE_PATTERN = /\.(css|html?|jsx?|tsx?|vue|svelte|mdx)$/i;
18509
- var VISUAL_ASSET_PATTERN = /\.(avif|gif|jpe?g|png|svg|webp)$/i;
18510
- function nonEmptyStrings(values) {
18511
- return Array.from(new Set(
18512
- (values || []).filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
18513
- ));
18514
- }
18515
- function normalizeRoute(value) {
18516
- if (!value?.trim()) return void 0;
18517
- const trimmed = value.trim();
18518
- if (/^https?:\/\//i.test(trimmed)) return void 0;
18519
- return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
19105
+ function escapeHtml(value) {
19106
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
18520
19107
  }
18521
- function routeFromUrl(value) {
18522
- if (!value?.trim()) return void 0;
18523
- try {
18524
- const url = new URL(value);
18525
- return normalizeRoute(`${url.pathname}${url.search || ""}`);
18526
- } catch {
18527
- return void 0;
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>`;
18528
19113
  }
19114
+ return `<p><a href="${escapeHtml(target)}">${escapeHtml(artifact.name)}</a></p>`;
18529
19115
  }
18530
- function slugify(value) {
18531
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "suggested-profile";
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>`;
18532
19120
  }
18533
- function inferRouteFromFile(file) {
18534
- const normalized = file.replace(/\\/g, "/");
18535
- const routeMatch = normalized.match(/(?:^|\/)(?:pages|routes|app)\/(.+?)(?:\.[^.\/]+)?$/);
18536
- if (!routeMatch) return void 0;
18537
- const route = routeMatch[1].replace(/\/index$/i, "").replace(/\/page$/i, "").replace(/\[[^/\]]+\]/g, ":param");
18538
- return normalizeRoute(route || "/");
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>`;
18539
19124
  }
18540
- function inferRouteFromFiles(files) {
18541
- for (const file of files) {
18542
- const route = inferRouteFromFile(file);
18543
- if (route) return route;
18544
- }
18545
- return void 0;
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
+ `;
18546
19237
  }
18547
- function uniqueChecks(checks) {
18548
- const seen = /* @__PURE__ */ new Set();
18549
- return checks.filter((check) => {
18550
- const key = JSON.stringify(check);
18551
- if (seen.has(key)) return false;
18552
- seen.add(key);
18553
- return true;
18554
- });
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 : {};
18555
19243
  }
18556
- function changedTextEntryText(entry) {
18557
- return typeof entry === "string" ? entry.trim() : (entry.expected_text || entry.text || "").trim();
19244
+ function asArray(value) {
19245
+ return Array.isArray(value) ? value : [];
18558
19246
  }
18559
- function changedTextEntrySelector(entry) {
18560
- return typeof entry === "string" ? void 0 : entry.selector?.trim() || void 0;
19247
+ function stringValue4(value) {
19248
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
18561
19249
  }
18562
- function changedTextEntryLabel(entry) {
18563
- return typeof entry === "string" ? void 0 : entry.label?.trim() || void 0;
19250
+ function numberValue3(value) {
19251
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
18564
19252
  }
18565
- function suggestionProfileName(input, route, files) {
18566
- if (input.name?.trim()) return input.name.trim();
18567
- return `suggested-${slugify(route || files[0] || "profile")}`;
19253
+ function booleanValue3(value) {
19254
+ return typeof value === "boolean" ? value : void 0;
18568
19255
  }
18569
- function defaultViewports(includeMobile) {
18570
- const viewports = [
18571
- { name: "desktop", width: 1280, height: 800 }
18572
- ];
18573
- if (includeMobile !== false) {
18574
- viewports.push({ name: "mobile", width: 390, height: 844 });
19256
+ function firstStringValue2(...values) {
19257
+ for (const value of values) {
19258
+ const text = stringValue4(value);
19259
+ if (text) return text;
18575
19260
  }
18576
- return viewports;
19261
+ return void 0;
18577
19262
  }
18578
- function suggestRiddleProofProfileChecks(input) {
18579
- const changedFiles = nonEmptyStrings(input.changed_files);
18580
- const selectors = nonEmptyStrings(input.selectors);
18581
- const changedText = input.changed_text || [];
18582
- const route = normalizeRoute(input.route) || inferRouteFromFiles(changedFiles) || routeFromUrl(input.url);
18583
- const uiFiles = changedFiles.filter((file) => UI_FILE_PATTERN.test(file));
18584
- const visualAssets = changedFiles.filter((file) => VISUAL_ASSET_PATTERN.test(file));
18585
- const warnings = [];
18586
- const suggestions = [];
18587
- const setupActions = [];
18588
- if (!route && !input.url?.trim()) {
18589
- warnings.push("No route or URL was supplied; the draft profile defaults to route /.");
18590
- }
18591
- if ((uiFiles.length || visualAssets.length) && !changedText.length && !selectors.length) {
18592
- warnings.push("UI or visual files changed, but no changed text or selectors were supplied; add focused checks before trusting this profile.");
18593
- }
18594
- const routeOrDefault = route || "/";
18595
- const routeChecks = [
18596
- { type: "route_loaded", expected_path: routeOrDefault }
18597
- ];
18598
- suggestions.push({
18599
- id: "route-loaded",
18600
- reason: "Every browser proof should first prove that the intended route loaded.",
18601
- checks: routeChecks
18602
- });
18603
- const hygieneChecks = [
18604
- { type: "no_fatal_console_errors" },
18605
- { type: "no_mobile_horizontal_overflow" }
18606
- ];
18607
- suggestions.push({
18608
- id: "runtime-hygiene",
18609
- reason: "UI changes should not introduce fatal console errors or mobile overflow.",
18610
- checks: hygieneChecks
18611
- });
18612
- for (const selector of selectors) {
18613
- suggestions.push({
18614
- id: `selector-${slugify(selector)}`,
18615
- reason: `Changed selector ${selector} should remain visible.`,
18616
- checks: [{ type: "selector_visible", selector }]
18617
- });
18618
- }
18619
- for (const entry of changedText) {
18620
- const text = changedTextEntryText(entry);
18621
- if (!text) continue;
18622
- const selector = changedTextEntrySelector(entry);
18623
- const label = changedTextEntryLabel(entry);
18624
- const check = selector ? { type: "selector_text_visible", selector, text, label } : { type: "text_visible", text, label };
18625
- suggestions.push({
18626
- id: `text-${slugify(`${selector || "page"}-${text}`)}`,
18627
- reason: selector ? `Expected text should be visible inside ${selector}.` : "Expected text should be visible on the route.",
18628
- checks: [check]
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)
18629
19290
  });
18630
19291
  }
18631
- if (input.include_screenshot !== false && (uiFiles.length || visualAssets.length || selectors.length || changedText.length)) {
18632
- setupActions.push({ type: "screenshot", label: "suggested-proof-screenshot", full_page: true });
18633
- suggestions.push({
18634
- id: "screenshot-artifact",
18635
- reason: "A screenshot artifact keeps the proof packet inspectable by humans and hosted renderers.",
18636
- checks: [],
18637
- setup_actions: setupActions
19292
+ return artifacts;
19293
+ }
19294
+ function collectProfileArtifacts(result) {
19295
+ const resultArtifacts = asRecord2(result.artifacts);
19296
+ const artifacts = [];
19297
+ const seen = /* @__PURE__ */ new Set();
19298
+ for (const [index, item] of asArray(resultArtifacts.riddle_artifacts).entries()) {
19299
+ const artifact = asRecord2(item);
19300
+ const url = stringValue4(artifact.url);
19301
+ if (!url || seen.has(url)) continue;
19302
+ seen.add(url);
19303
+ const fallbackName = stringValue4(artifact.kind) || `artifact-${index + 1}`;
19304
+ const name = artifactDisplayName(artifact.name, fallbackName);
19305
+ artifacts.push({
19306
+ name,
19307
+ url,
19308
+ kind: artifactKind(name, url),
19309
+ size_bytes: numberValue3(artifact.size_bytes) ?? numberValue3(artifact.size)
18638
19310
  });
18639
19311
  }
18640
- const checks = uniqueChecks(suggestions.flatMap((suggestion) => suggestion.checks));
18641
- const target = {
18642
- ...input.url?.trim() ? { url: input.url.trim() } : { route: routeOrDefault },
18643
- viewports: defaultViewports(input.include_mobile),
18644
- setup_actions: setupActions.length ? setupActions : void 0
18645
- };
18646
- const profile = {
18647
- version: RIDDLE_PROOF_PROFILE_VERSION,
18648
- name: suggestionProfileName(input, routeOrDefault, changedFiles),
18649
- target,
18650
- checks,
18651
- artifacts: ["screenshot", "console", "proof_json"],
18652
- baseline_policy: "invariant_only",
18653
- failure_policy: {
18654
- product_regression: "fail",
18655
- proof_insufficient: "review",
18656
- environment_blocked: "neutral",
18657
- configuration_error: "fail",
18658
- needs_human_review: "review"
18659
- },
18660
- metadata: {
18661
- suggestion_input: {
18662
- changed_files: changedFiles,
18663
- selectors,
18664
- changed_text_count: changedText.length
18665
- }
18666
- }
18667
- };
18668
- return {
18669
- version: RIDDLE_PROOF_PROFILE_SUGGESTIONS_VERSION,
18670
- profile,
18671
- suggestions,
18672
- warnings
18673
- };
18674
- }
18675
-
18676
- // src/change-proof.ts
18677
- var RIDDLE_PROOF_CHANGE_RESULT_VERSION = "riddle-proof.change-result.v1";
18678
- var RIDDLE_PROOF_CHANGE_RECEIPT_VERSION = "riddle-proof.change-receipt.v1";
18679
- var DEFAULT_BEFORE_STATUSES = ["passed", "product_regression"];
18680
- var DEFAULT_AFTER_STATUSES = ["passed"];
18681
- var DEFAULT_PROFILE_STATUS_TRANSITION_BEFORE = ["product_regression"];
18682
- var DEFAULT_PROFILE_STATUS_TRANSITION_AFTER = ["passed"];
18683
- var DEFAULT_CHECK_STATUS_TRANSITION_BEFORE = ["failed"];
18684
- var DEFAULT_CHECK_STATUS_TRANSITION_AFTER = ["passed"];
18685
- function listValue(value, fallback) {
18686
- if (Array.isArray(value)) return value;
18687
- if (value === void 0) return fallback;
18688
- return [value];
18689
- }
18690
- function includesValue(allowed, observed) {
18691
- return observed !== void 0 && allowed.includes(observed);
19312
+ return artifacts;
18692
19313
  }
18693
- function groupResult(side, contract, result) {
18694
- const fallback = side === "before" ? DEFAULT_BEFORE_STATUSES : DEFAULT_AFTER_STATUSES;
18695
- const requiredStatus = listValue(contract?.required_status, fallback);
18696
- const label = contract?.label || side;
18697
- if (!result) {
18698
- return {
18699
- side,
18700
- label,
18701
- ok: false,
18702
- status: "missing",
18703
- required_status: requiredStatus,
18704
- message: `${label} profile result is missing.`
18705
- };
18706
- }
18707
- const ok = includesValue(requiredStatus, result.status);
18708
- return {
18709
- side,
18710
- label,
18711
- ok,
18712
- profile_name: result.profile_name,
18713
- status: result.status,
18714
- required_status: requiredStatus,
18715
- summary: result.summary,
18716
- message: ok ? void 0 : `${label} profile status ${result.status} did not match required status ${requiredStatus.join(", ")}.`
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
+ }
18717
19337
  };
19338
+ collectSide("before", asRecord2(result.before));
19339
+ collectSide("after", asRecord2(result.after));
19340
+ return artifacts;
18718
19341
  }
18719
- function findCheck(result, delta) {
18720
- return result.checks.find((check) => {
18721
- if (delta.check_label && check.label !== delta.check_label) return false;
18722
- if (delta.check_type && check.type !== delta.check_type) return false;
18723
- return Boolean(delta.check_label || delta.check_type);
18724
- });
18725
- }
18726
- function profileStatusTransitionResult(delta, before, after) {
18727
- const label = delta.label || "profile-status-transition";
18728
- if (!before || !after) {
18729
- return {
18730
- type: delta.type,
18731
- label,
18732
- status: "proof_insufficient",
18733
- before_observed: before?.status,
18734
- after_observed: after?.status,
18735
- message: `${label} needs both before and after profile results.`
18736
- };
19342
+ function mergeArtifacts(...artifactLists) {
19343
+ const artifacts = [];
19344
+ const seen = /* @__PURE__ */ new Set();
19345
+ for (const artifact of artifactLists.flat()) {
19346
+ if (seen.has(artifact.url)) continue;
19347
+ seen.add(artifact.url);
19348
+ artifacts.push(artifact);
18737
19349
  }
18738
- const beforeAllowed = listValue(delta.before_status, DEFAULT_PROFILE_STATUS_TRANSITION_BEFORE);
18739
- const afterAllowed = listValue(delta.after_status, DEFAULT_PROFILE_STATUS_TRANSITION_AFTER);
18740
- const passed = includesValue(beforeAllowed, before.status) && includesValue(afterAllowed, after.status);
18741
- return {
18742
- type: delta.type,
18743
- label,
18744
- status: passed ? "passed" : "failed",
18745
- before_observed: before.status,
18746
- after_observed: after.status,
18747
- message: passed ? void 0 : `${label} expected before ${beforeAllowed.join(", ")} and after ${afterAllowed.join(", ")}, got ${before.status} -> ${after.status}.`
18748
- };
19350
+ return artifacts;
18749
19351
  }
18750
- function checkStatusTransitionResult(delta, before, after) {
18751
- const label = delta.label || delta.check_label || delta.check_type || "check-status-transition";
18752
- if (!delta.check_label && !delta.check_type) {
18753
- return {
18754
- type: delta.type,
18755
- label,
18756
- status: "configuration_error",
18757
- message: `${label} needs check_label or check_type.`
18758
- };
19352
+ function pageSummaries(result) {
19353
+ const pages = [];
19354
+ for (const page of asArray(result.pages)) {
19355
+ const record = asRecord2(page);
19356
+ const route = stringValue4(record.route) || stringValue4(record.url) || "page";
19357
+ const checks = asRecord2(record.checks);
19358
+ let passed = 0;
19359
+ let failed = 0;
19360
+ for (const value of Object.values(checks)) {
19361
+ if (value === true) passed += 1;
19362
+ if (value === false) failed += 1;
19363
+ }
19364
+ pages.push({ route, passed, failed });
18759
19365
  }
18760
- if (!before || !after) {
18761
- return {
18762
- type: delta.type,
18763
- label,
18764
- status: "proof_insufficient",
18765
- before_observed: before?.status,
18766
- after_observed: after?.status,
18767
- message: `${label} needs both before and after profile results.`
18768
- };
19366
+ return pages;
19367
+ }
19368
+ function profileCheckCounts3(result) {
19369
+ const checks = asArray(result.checks);
19370
+ if (checks.length) {
19371
+ let passed2 = 0;
19372
+ let failed2 = 0;
19373
+ for (const item of checks) {
19374
+ const status = stringValue4(asRecord2(item).status);
19375
+ if (status === "passed") passed2 += 1;
19376
+ if (status === "failed") failed2 += 1;
19377
+ }
19378
+ return { passed: passed2, failed: failed2 };
18769
19379
  }
18770
- const beforeCheck = findCheck(before, delta);
18771
- const afterCheck = findCheck(after, delta);
18772
- if (!beforeCheck || !afterCheck) {
18773
- return {
18774
- type: delta.type,
18775
- label,
18776
- status: "proof_insufficient",
18777
- before_observed: beforeCheck?.status,
18778
- after_observed: afterCheck?.status,
18779
- message: `${label} was not present in ${!beforeCheck && !afterCheck ? "before or after" : !beforeCheck ? "before" : "after"} profile checks.`
18780
- };
19380
+ const checkCounts = asRecord2(result.check_counts);
19381
+ const passed = numberValue3(checkCounts.passed);
19382
+ const failed = numberValue3(checkCounts.failed);
19383
+ if (typeof passed === "number" || typeof failed === "number") {
19384
+ return { passed: passed ?? 0, failed: failed ?? 0 };
18781
19385
  }
18782
- const beforeAllowed = listValue(delta.before_status, DEFAULT_CHECK_STATUS_TRANSITION_BEFORE);
18783
- const afterAllowed = listValue(delta.after_status, DEFAULT_CHECK_STATUS_TRANSITION_AFTER);
18784
- const passed = includesValue(beforeAllowed, beforeCheck.status) && includesValue(afterAllowed, afterCheck.status);
18785
- return {
18786
- type: delta.type,
18787
- label,
18788
- status: passed ? "passed" : "failed",
18789
- before_observed: beforeCheck.status,
18790
- after_observed: afterCheck.status,
18791
- message: passed ? void 0 : `${label} expected before ${beforeAllowed.join(", ")} and after ${afterAllowed.join(", ")}, got ${beforeCheck.status} -> ${afterCheck.status}.`
18792
- };
19386
+ return void 0;
18793
19387
  }
18794
- function assessDelta(delta, before, after) {
18795
- if (delta.type === "profile_status_transition") {
18796
- return profileStatusTransitionResult(delta, before, after);
18797
- }
18798
- return checkStatusTransitionResult(delta, before, after);
19388
+ function profilePageSummaries(result, counts) {
19389
+ if (!counts) return [];
19390
+ const route = asRecord2(result.route);
19391
+ return [{
19392
+ route: firstStringValue2(route.observed, route.expected_path, route.requested, result.profile_name) || "profile",
19393
+ passed: counts.passed,
19394
+ failed: counts.failed
19395
+ }];
18799
19396
  }
18800
- function collapsedChangeStatus(groups, deltas) {
18801
- const groupResults = [groups.before, groups.after];
18802
- if (groupResults.some((group) => group.status === "environment_blocked")) return "environment_blocked";
18803
- if (groupResults.some((group) => group.status === "configuration_error")) return "configuration_error";
18804
- if (deltas.some((delta) => delta.status === "configuration_error")) return "configuration_error";
18805
- if (groupResults.some((group) => group.status === "needs_human_review")) return "needs_human_review";
18806
- if (groupResults.some((group) => group.status === "missing" || group.status === "proof_insufficient")) return "proof_insufficient";
18807
- if (!deltas.length || deltas.some((delta) => delta.status === "proof_insufficient")) return "proof_insufficient";
18808
- if (groupResults.some((group) => !group.ok)) return "product_regression";
18809
- if (deltas.some((delta) => delta.status === "failed")) return "product_regression";
18810
- return "passed";
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 };
18811
19408
  }
18812
- function changeSummary(name, status, deltas) {
18813
- if (status === "passed") return `${name} passed ${deltas.length} change delta(s).`;
18814
- if (status === "environment_blocked") return `${name} could not compare reliable evidence because an environment was blocked.`;
18815
- if (status === "configuration_error") return `${name} has an invalid change proof contract.`;
18816
- if (status === "needs_human_review") return `${name} needs human review before the change proof can pass.`;
18817
- if (status === "proof_insufficient") return `${name} did not produce enough before/after evidence for a change proof.`;
18818
- return `${name} failed ${deltas.filter((delta) => delta.status === "failed").length} change delta(s).`;
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;
18819
19423
  }
18820
- function assessRiddleProofChange(contract, input) {
18821
- const groups = {
18822
- before: groupResult("before", contract.before, input.before_result),
18823
- after: groupResult("after", contract.after, input.after_result)
18824
- };
18825
- const deltas = (contract.deltas || []).map((delta) => assessDelta(delta, input.before_result, input.after_result));
18826
- const status = collapsedChangeStatus(groups, deltas);
18827
- return {
18828
- version: RIDDLE_PROOF_CHANGE_RESULT_VERSION,
18829
- contract_name: contract.name,
18830
- status,
18831
- groups,
18832
- deltas,
18833
- summary: changeSummary(contract.name, status, deltas),
18834
- metadata: contract.metadata
19424
+ function summarizeExplicitChecks(value) {
19425
+ let passed = 0;
19426
+ let failed = 0;
19427
+ const visit = (current, inChecks = false) => {
19428
+ if (current === true && inChecks) {
19429
+ passed += 1;
19430
+ return;
19431
+ }
19432
+ if (current === false && inChecks) {
19433
+ failed += 1;
19434
+ return;
19435
+ }
19436
+ if (Array.isArray(current)) {
19437
+ for (const item of current) visit(item, inChecks);
19438
+ return;
19439
+ }
19440
+ if (current && typeof current === "object") {
19441
+ for (const [key, item] of Object.entries(current)) {
19442
+ visit(item, inChecks || key === "checks");
19443
+ }
19444
+ }
18835
19445
  };
19446
+ visit(value);
19447
+ return { passed, failed };
18836
19448
  }
18837
- function changeReceiptVerdict(status) {
18838
- if (status === "passed") return "mergeable";
18839
- if (status === "environment_blocked") return "environment_blocked";
18840
- if (status === "configuration_error") return "configuration_error";
18841
- if (status === "needs_human_review") return "needs_human_review";
18842
- if (status === "proof_insufficient") return "proof_insufficient";
18843
- return "not_mergeable";
19449
+ function selectPrimaryImage(artifacts) {
19450
+ const images = artifacts.filter((artifact) => artifact.kind === "image");
19451
+ return images.find((artifact) => /after|proof|screenshot/i.test(artifact.name)) || images[0];
18844
19452
  }
18845
- function profileCheckCounts2(result) {
18846
- const counts = {
18847
- total: result.checks.length,
18848
- passed: 0,
18849
- failed: 0,
18850
- skipped: 0,
18851
- needs_human_review: 0
18852
- };
18853
- for (const check of result.checks) {
18854
- if (check.status === "passed") counts.passed += 1;
18855
- if (check.status === "failed") counts.failed += 1;
18856
- if (check.status === "skipped") counts.skipped += 1;
18857
- if (check.status === "needs_human_review") counts.needs_human_review += 1;
19453
+ function firstRecordValue2(...values) {
19454
+ for (const value of values) {
19455
+ const record = asRecord2(value);
19456
+ if (Object.keys(record).length) return record;
18858
19457
  }
18859
- return counts;
19458
+ return void 0;
18860
19459
  }
18861
- function artifactKind2(ref) {
18862
- const text = [ref.name, ref.url, ref.path, ref.kind, ref.content_type].filter(Boolean).join(" ").toLowerCase();
18863
- if (/\bimage\b/.test(text) || /\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/.test(text)) return "image";
18864
- if (/\bjson\b|\btext\b|\bmarkdown\b|\bhtml\b|\blog\b/.test(text) || /\.(json|md|txt|html|log|har)(\?|#|$)/.test(text)) return "data";
18865
- return "artifact";
19460
+ function checkpointSummaryFrom2(...values) {
19461
+ const record = firstRecordValue2(...values);
19462
+ if (!record) return void 0;
19463
+ const summary = {
19464
+ pending: booleanValue3(record.pending),
19465
+ response_count: numberValue3(record.response_count),
19466
+ rejected_response_count: numberValue3(record.rejected_response_count),
19467
+ ignored_response_count: numberValue3(record.ignored_response_count),
19468
+ duplicate_response_count: numberValue3(record.duplicate_response_count),
19469
+ latest_decision: stringValue4(record.latest_decision),
19470
+ latest_packet_id: stringValue4(record.latest_packet_id),
19471
+ latest_resume_token: stringValue4(record.latest_resume_token)
19472
+ };
19473
+ return Object.values(summary).some((value) => typeof value !== "undefined") ? summary : void 0;
19474
+ }
19475
+ function isProfileResult(result) {
19476
+ const version = stringValue4(result.version);
19477
+ return version === "riddle-proof.profile-result.v1" || version === "riddle-proof.profile-compact-result.v1";
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";
18866
19489
  }
18867
- function receiptArtifactFromRef(side, ref) {
19490
+ function summarizeRiddleProofPrComment(input) {
19491
+ const runResponse = asRecord2(input.runResponse);
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;
19496
+ const proofResult = asRecord2(runResponse.proofResult);
19497
+ const profileResult = isProfileResult(result);
19498
+ const changeReceiptResult = Boolean(parsedChangeReceipt);
19499
+ const changeResult = changeReceiptResult || isChangeResult(result);
19500
+ const profileRiddle = asRecord2(result.riddle);
19501
+ const preview = asRecord2(runResponse.preview);
19502
+ const resultRunCard = asRecord2(result.run_card);
19503
+ const stopCondition = asRecord2(resultRunCard.stop_condition);
19504
+ const resultDetails = asRecord2(result.details);
19505
+ const resultRaw = asRecord2(result.raw);
19506
+ const rawDetails = asRecord2(resultRaw.details);
19507
+ const profileChecks = profileResult ? profileCheckCounts3(result) : void 0;
19508
+ const changeChecks = changeResult ? changeReceiptCheckCounts(result) : void 0;
19509
+ const artifacts = mergeArtifacts(
19510
+ collectArtifacts(runResponse),
19511
+ profileResult ? collectProfileArtifacts(result) : [],
19512
+ changeReceiptResult ? collectChangeReceiptArtifacts(result) : []
19513
+ );
19514
+ const pages = changeReceiptResult ? changeReceiptPageSummaries(result) : profileResult ? profilePageSummaries(result, profileChecks) : pageSummaries(result);
19515
+ const checkSource = { ...result };
19516
+ delete checkSource.ok;
19517
+ const nestedChecks = summarizeExplicitChecks(checkSource);
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;
19520
+ const checkpointSummary = checkpointSummaryFrom2(
19521
+ result.checkpoint_summary,
19522
+ stopCondition.checkpoint_summary,
19523
+ resultDetails.checkpoint_summary,
19524
+ rawDetails.checkpoint_summary,
19525
+ proofResult.checkpoint_summary
19526
+ );
19527
+ const publicState = changeReceiptResult ? void 0 : summarizeRiddleProofPublicState({
19528
+ ...result,
19529
+ status: resultStatus,
19530
+ checkpoint_summary: checkpointSummary || result.checkpoint_summary
19531
+ });
19532
+ const mergeRecommendation = publicState ? riddleProofPublicStateMergeRecommendation(
19533
+ publicState,
19534
+ firstStringValue2(result.merge_recommendation, stopCondition.merge_recommendation, resultRaw.merge_recommendation)
19535
+ ) : void 0;
18868
19536
  return {
18869
- side,
18870
- name: ref.name,
18871
- kind: artifactKind2(ref),
18872
- url: ref.url,
18873
- path: ref.path,
18874
- source: ref.source
19537
+ ok,
19538
+ status: firstStringValue2(proofResult.status, profileRiddle.status),
19539
+ result_status: changeResult ? resultStatus : publicState?.status,
19540
+ job_id: firstStringValue2(proofResult.job_id, profileRiddle.job_id),
19541
+ duration_ms: numberValue3(proofResult.duration_ms) ?? numberValue3(profileRiddle.elapsed_ms),
19542
+ proof_url: stringValue4(runResponse.proofUrl),
19543
+ preview_id: stringValue4(preview.id),
19544
+ preview_url: stringValue4(preview.preview_url) || stringValue4(preview.url),
19545
+ preview_publish_recovered: booleanValue3(preview.publish_recovered),
19546
+ preview_publish_error: stringValue4(preview.publish_error),
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,
19552
+ proof_decision: firstStringValue2(result.proof_decision, stopCondition.proof_decision, resultRaw.proof_decision),
19553
+ merge_recommendation: changeReceiptResult ? parsedChangeReceipt?.recommendation.label : mergeRecommendation,
19554
+ checkpoint_summary: checkpointSummary,
19555
+ public_state: publicState,
19556
+ passed_checks: changeChecks?.passed ?? profileChecks?.passed ?? nestedChecks.passed,
19557
+ failed_checks: changeChecks?.failed ?? profileChecks?.failed ?? nestedChecks.failed,
19558
+ pages,
19559
+ artifacts,
19560
+ primary_image: selectPrimaryImage(artifacts)
18875
19561
  };
18876
19562
  }
18877
- function comparableArtifactName(value) {
18878
- return (value || "").split(/[/?#]/).pop()?.toLowerCase().replace(/\.(png|jpe?g|gif|webp|avif|svg)$/u, "").replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/g, "") || "";
19563
+ function formatDuration(ms) {
19564
+ if (typeof ms !== "number" || !Number.isFinite(ms)) return "";
19565
+ const seconds = Math.max(0, Math.round(ms / 1e3));
19566
+ const minutes = Math.floor(seconds / 60);
19567
+ const remainder = seconds % 60;
19568
+ return minutes > 0 ? `${minutes}m${String(remainder).padStart(2, "0")}s` : `${seconds}s`;
18879
19569
  }
18880
- function artifactMatchesScreenshotLabel(artifact, screenshot) {
18881
- const screenshotName = comparableArtifactName(screenshot);
18882
- if (!screenshotName) return false;
18883
- return [artifact.name, artifact.url, artifact.path].map(comparableArtifactName).some((name) => name === screenshotName || name.endsWith(`-${screenshotName}`));
19570
+ function markdownLink2(label, url) {
19571
+ return `[${label.replace(/\]/g, "\\]")}](${url})`;
18884
19572
  }
18885
- function collectReceiptArtifacts(side, result) {
18886
- const artifacts = [];
18887
- const seen = /* @__PURE__ */ new Set();
18888
- const add = (artifact) => {
18889
- const key = artifact.url || artifact.path || `${artifact.kind}:${artifact.name}`;
18890
- if (!artifact.name || seen.has(key)) return;
18891
- seen.add(key);
18892
- artifacts.push(artifact);
18893
- };
18894
- for (const ref of result.artifacts.riddle_artifacts || []) {
18895
- add(receiptArtifactFromRef(side, ref));
18896
- }
18897
- for (const screenshot of result.artifacts.screenshots || []) {
18898
- if (artifacts.some((artifact) => artifactMatchesScreenshotLabel(artifact, screenshot))) continue;
18899
- add({
18900
- side,
18901
- name: screenshot,
18902
- kind: "image"
18903
- });
19573
+ function resultLabel(summary) {
19574
+ if (summary.public_state?.result_label) return summary.public_state.result_label;
19575
+ if (summary.ok === true) {
19576
+ if (summary.result_status === "shipped") return "shipped";
19577
+ if (summary.result_status === "completed") return "completed";
19578
+ if (summary.ship_held === true) return "proof passed; ship held";
19579
+ if (summary.ship_authorized === true) return "passed; ship authorized";
19580
+ return "passed";
18904
19581
  }
18905
- return artifacts;
19582
+ if (summary.ok === false) return "failed";
19583
+ return summary.result_status || summary.status || "recorded";
18906
19584
  }
18907
- function artifactIsScreenshot(artifact) {
18908
- if (artifact.kind !== "image") return false;
18909
- return /screenshot|\.png|\.jpe?g|\.webp|\.gif|\.avif|\.svg/i.test([artifact.name, artifact.url, artifact.path].filter(Boolean).join(" "));
19585
+ function artifactRank(artifact) {
19586
+ const name = artifact.name.toLowerCase();
19587
+ if (name === "proof.json") return 0;
19588
+ if (name === "result.json") return 1;
19589
+ if (name.includes("proof") && name.endsWith(".json") && !name.includes("layout")) return 2;
19590
+ if (name === "console.json") return 3;
19591
+ if (artifact.kind === "data") return 10;
19592
+ if (artifact.kind === "image") return 20;
19593
+ return 30;
18910
19594
  }
18911
- function receiptSide(side, source, result) {
18912
- const artifacts = collectReceiptArtifacts(side, result);
18913
- return {
18914
- side,
18915
- source,
18916
- profile_name: result.profile_name,
18917
- status: result.status,
18918
- summary: result.summary,
18919
- route: result.route,
18920
- captured_at: result.captured_at,
18921
- checks: profileCheckCounts2(result),
18922
- screenshots: artifacts.filter(artifactIsScreenshot),
18923
- artifacts
18924
- };
19595
+ function formatBool(value) {
19596
+ return typeof value === "boolean" ? String(value) : "unknown";
18925
19597
  }
18926
- function metadataStringList(metadata, key) {
18927
- const value = metadata?.[key];
18928
- if (!Array.isArray(value)) return [];
18929
- return value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
19598
+ function hasShipControl(summary) {
19599
+ return typeof summary.ship_held === "boolean" || typeof summary.shipping_disabled === "boolean" || typeof summary.ship_authorized === "boolean";
18930
19600
  }
18931
- function createRiddleProofChangeReceipt(input) {
18932
- return {
18933
- version: RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
18934
- contract_name: input.result.contract_name,
18935
- profile_name: input.profile_name,
18936
- status: input.result.status,
18937
- verdict: changeReceiptVerdict(input.result.status),
18938
- summary: input.result.summary,
18939
- before: receiptSide("before", input.before_source, input.before_result),
18940
- after: receiptSide("after", input.after_source, input.after_result),
18941
- deltas: input.result.deltas.map((delta) => ({
18942
- type: delta.type,
18943
- label: delta.label,
18944
- status: delta.status,
18945
- before_observed: delta.before_observed,
18946
- after_observed: delta.after_observed,
18947
- message: delta.message
18948
- })),
18949
- proves: metadataStringList(input.contract.metadata, "required_receipts"),
18950
- does_not_prove: metadataStringList(input.contract.metadata, "does_not_prove"),
18951
- metadata: input.result.metadata
18952
- };
19601
+ function hasHandoffControl(summary) {
19602
+ return typeof summary.merge_ready === "boolean" || typeof summary.sync_allowed === "boolean";
18953
19603
  }
18954
- function markdownTableCell(value) {
19604
+ function shouldRenderMergeRecommendation(summary) {
19605
+ if (!summary.merge_recommendation) return false;
19606
+ return riddleProofPublicStateAllowsMergeRecommendation(summary.public_state);
19607
+ }
19608
+ function checkpointSummaryLine(summary) {
19609
+ const accepted = summary.response_count ?? 0;
19610
+ const rejected = summary.rejected_response_count ?? 0;
19611
+ const ignored = summary.ignored_response_count ?? 0;
19612
+ const parts = [`${accepted} accepted`, `${rejected} rejected`, `${ignored} ignored`];
19613
+ if ((summary.duplicate_response_count ?? 0) > 0) parts.push(`${summary.duplicate_response_count} duplicate`);
19614
+ const state = summary.pending === true ? "pending" : summary.pending === false ? "complete" : "";
19615
+ return [
19616
+ parts.join(" / "),
19617
+ state,
19618
+ summary.latest_decision ? `latest decision \`${summary.latest_decision}\`` : ""
19619
+ ].filter(Boolean).join("; ");
19620
+ }
19621
+ function markdownTableValue(value) {
18955
19622
  return String(value ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
18956
19623
  }
18957
- function artifactTarget(artifact) {
18958
- return artifact.url || artifact.path;
19624
+ function observationArtifactTarget(artifact) {
19625
+ return artifact?.url || artifact?.path;
18959
19626
  }
18960
- function markdownLink2(label, target) {
18961
- return `[${label.replace(/\]/g, "\\]")}](${target})`;
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);
18962
19632
  }
18963
- function appendMarkdownArtifacts(lines, title, artifacts) {
18964
- lines.push(`### ${title}`, "");
18965
- if (!artifacts.length) {
18966
- lines.push("- No screenshot artifacts recorded.", "");
18967
- return;
18968
- }
18969
- for (const artifact of artifacts.slice(0, 6)) {
18970
- const target = artifactTarget(artifact);
18971
- if (target && artifact.kind === "image") {
18972
- lines.push(`![${artifact.name.replace(/\]/g, "\\]")}](${target})`);
18973
- } else if (target) {
18974
- lines.push(`- ${markdownLink2(artifact.name, target)}`);
18975
- } else {
18976
- lines.push(`- ${artifact.name}`);
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 || "")}`);
18977
19688
  }
18978
19689
  }
18979
- if (artifacts.length > 6) lines.push(`- ${artifacts.length - 6} more artifact(s) omitted`);
18980
- lines.push("");
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
+ `;
18981
19693
  }
18982
- function riddleProofChangeReceiptMarkdown(receipt) {
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
+ }
19703
+ const summary = summarizeRiddleProofPrComment(input);
19704
+ const title = input.title?.trim() || "Riddle Proof Evidence";
18983
19705
  const lines = [
18984
- "# Riddle Proof Change Receipt",
18985
- "",
18986
- `**Verdict:** ${receipt.verdict}`,
18987
- `**Status:** ${receipt.status}`,
18988
- `**Contract:** ${receipt.contract_name}`,
18989
- receipt.profile_name ? `**Profile:** ${receipt.profile_name}` : "",
18990
- "",
18991
- receipt.summary,
18992
- "",
18993
- "## Evidence Pair",
18994
- "",
18995
- "| Side | Source | Status | Checks |",
18996
- "| --- | --- | --- | --- |",
18997
- `| Before | ${markdownTableCell(receipt.before.source)} | ${receipt.before.status} | ${receipt.before.checks.passed} passed / ${receipt.before.checks.failed} failed |`,
18998
- `| After | ${markdownTableCell(receipt.after.source)} | ${receipt.after.status} | ${receipt.after.checks.passed} passed / ${receipt.after.checks.failed} failed |`,
18999
- "",
19000
- "## Delta Checks",
19706
+ RIDDLE_PROOF_PR_COMMENT_MARKER,
19707
+ `## ${title}`,
19001
19708
  "",
19002
- "| Delta | Status | Before | After |",
19003
- "| --- | --- | --- | --- |"
19004
- ].filter(Boolean);
19005
- for (const delta of receipt.deltas) {
19006
- lines.push(`| ${markdownTableCell(delta.label)} | ${delta.status} | ${markdownTableCell(delta.before_observed)} | ${markdownTableCell(delta.after_observed)} |`);
19007
- if (delta.message) lines.push(`| ${markdownTableCell(`${delta.label} message`)} | ${markdownTableCell(delta.message)} | | |`);
19709
+ `**Result:** ${resultLabel(summary)}`
19710
+ ];
19711
+ if (input.goal?.trim()) lines.push(`**Goal:** ${input.goal.trim()}`);
19712
+ if (input.successCriteria?.trim()) lines.push(`**Success criteria:** ${input.successCriteria.trim()}`);
19713
+ if (summary.result_status) lines.push(`**Evidence status:** ${summary.result_status}`);
19714
+ if (summary.status) lines.push(`**Riddle job status:** ${summary.status}`);
19715
+ if (summary.job_id) lines.push(`**Riddle job:** \`${summary.job_id}\``);
19716
+ if (summary.duration_ms) lines.push(`**Duration:** ${formatDuration(summary.duration_ms)}`);
19717
+ if (hasShipControl(summary)) {
19718
+ lines.push(`**Ship control:** held=${formatBool(summary.ship_held)}, shipping_disabled=${formatBool(summary.shipping_disabled)}, authorized=${formatBool(summary.ship_authorized)}`);
19719
+ }
19720
+ if (hasHandoffControl(summary)) {
19721
+ lines.push(`**Handoff:** merge_ready=${formatBool(summary.merge_ready)}, sync_allowed=${formatBool(summary.sync_allowed)}`);
19722
+ }
19723
+ if (summary.proof_decision) lines.push(`**Proof decision:** \`${summary.proof_decision}\``);
19724
+ if (shouldRenderMergeRecommendation(summary)) lines.push(`**Merge recommendation:** ${summary.merge_recommendation}`);
19725
+ if (summary.checkpoint_summary) lines.push(`**Checkpoints:** ${checkpointSummaryLine(summary.checkpoint_summary)}`);
19726
+ if (summary.proof_url) lines.push(`**Proof URL:** ${markdownLink2(summary.proof_url, summary.proof_url)}`);
19727
+ if (summary.preview_id || summary.preview_url) {
19728
+ const previewLabel = summary.preview_id ? `\`${summary.preview_id}\`` : "preview";
19729
+ lines.push(`**Preview:** ${summary.preview_url ? markdownLink2(previewLabel, summary.preview_url) : previewLabel}`);
19730
+ }
19731
+ if (summary.preview_publish_recovered) {
19732
+ const detail = summary.preview_publish_error ? `: ${summary.preview_publish_error}` : "";
19733
+ lines.push(`**Preview publish recovery:** recovered after publish error${detail}`);
19008
19734
  }
19735
+ lines.push(`**Checks:** ${summary.passed_checks} passed / ${summary.failed_checks} failed`);
19009
19736
  lines.push("");
19010
- appendMarkdownArtifacts(lines, "Before Screenshot", receipt.before.screenshots);
19011
- appendMarkdownArtifacts(lines, "After Screenshot", receipt.after.screenshots);
19012
- if (receipt.proves.length) {
19013
- lines.push("## What This Proves", "");
19014
- for (const claim of receipt.proves) lines.push(`- ${claim}`);
19737
+ if (summary.primary_image) {
19738
+ lines.push("### Screenshot");
19739
+ lines.push(`![${summary.primary_image.name}](${summary.primary_image.url})`);
19015
19740
  lines.push("");
19016
19741
  }
19017
- if (receipt.does_not_prove.length) {
19018
- lines.push("## What This Does Not Prove", "");
19019
- for (const claim of receipt.does_not_prove) lines.push(`- ${claim}`);
19742
+ if (summary.pages.length) {
19743
+ lines.push("### Page Checks");
19744
+ for (const page of summary.pages.slice(0, 12)) {
19745
+ lines.push(`- \`${page.route}\`: ${page.passed} passed / ${page.failed} failed`);
19746
+ }
19747
+ if (summary.pages.length > 12) lines.push(`- ${summary.pages.length - 12} more page(s) omitted`);
19020
19748
  lines.push("");
19021
19749
  }
19022
- const linkedArtifacts = [...receipt.before.artifacts, ...receipt.after.artifacts].filter((artifact) => artifactTarget(artifact)).slice(0, 24);
19750
+ const linkedArtifacts = summary.artifacts.filter((artifact) => artifact.url !== summary.primary_image?.url).sort((left, right) => artifactRank(left) - artifactRank(right) || left.name.localeCompare(right.name)).slice(0, 20);
19023
19751
  if (linkedArtifacts.length) {
19024
- lines.push("## Artifacts", "");
19752
+ lines.push("### Artifacts");
19025
19753
  for (const artifact of linkedArtifacts) {
19026
- lines.push(`- ${artifact.side}: ${markdownLink2(artifact.name, artifactTarget(artifact) || "")}`);
19754
+ lines.push(`- ${markdownLink2(artifact.name, artifact.url)}`);
19755
+ }
19756
+ if (summary.artifacts.length - (summary.primary_image ? 1 : 0) > linkedArtifacts.length) {
19757
+ lines.push(`- ${summary.artifacts.length - (summary.primary_image ? 1 : 0) - linkedArtifacts.length} more artifact(s) omitted`);
19027
19758
  }
19028
19759
  lines.push("");
19029
19760
  }
19761
+ if (input.source?.trim()) {
19762
+ lines.push(`_Source: ${input.source.trim()}_`);
19763
+ } else {
19764
+ lines.push("_Updated by `riddle-proof-loop pr-comment`._");
19765
+ }
19030
19766
  return `${lines.join("\n").trim()}
19031
19767
  `;
19032
19768
  }
19033
- function escapeHtml(value) {
19034
- return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
19769
+
19770
+ // src/profile-suggestions.ts
19771
+ var RIDDLE_PROOF_PROFILE_SUGGESTIONS_VERSION = "riddle-proof.profile-suggestions.v1";
19772
+ var UI_FILE_PATTERN = /\.(css|html?|jsx?|tsx?|vue|svelte|mdx)$/i;
19773
+ var VISUAL_ASSET_PATTERN = /\.(avif|gif|jpe?g|png|svg|webp)$/i;
19774
+ function nonEmptyStrings(values) {
19775
+ return Array.from(new Set(
19776
+ (values || []).filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
19777
+ ));
19778
+ }
19779
+ function normalizeRoute(value) {
19780
+ if (!value?.trim()) return void 0;
19781
+ const trimmed = value.trim();
19782
+ if (/^https?:\/\//i.test(trimmed)) return void 0;
19783
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
19784
+ }
19785
+ function routeFromUrl(value) {
19786
+ if (!value?.trim()) return void 0;
19787
+ try {
19788
+ const url = new URL(value);
19789
+ return normalizeRoute(`${url.pathname}${url.search || ""}`);
19790
+ } catch {
19791
+ return void 0;
19792
+ }
19793
+ }
19794
+ function slugify(value) {
19795
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "suggested-profile";
19796
+ }
19797
+ function inferRouteFromFile(file) {
19798
+ const normalized = file.replace(/\\/g, "/");
19799
+ const routeMatch = normalized.match(/(?:^|\/)(?:pages|routes|app)\/(.+?)(?:\.[^.\/]+)?$/);
19800
+ if (!routeMatch) return void 0;
19801
+ const route = routeMatch[1].replace(/\/index$/i, "").replace(/\/page$/i, "").replace(/\[[^/\]]+\]/g, ":param");
19802
+ return normalizeRoute(route || "/");
19803
+ }
19804
+ function inferRouteFromFiles(files) {
19805
+ for (const file of files) {
19806
+ const route = inferRouteFromFile(file);
19807
+ if (route) return route;
19808
+ }
19809
+ return void 0;
19810
+ }
19811
+ function uniqueChecks(checks) {
19812
+ const seen = /* @__PURE__ */ new Set();
19813
+ return checks.filter((check) => {
19814
+ const key = JSON.stringify(check);
19815
+ if (seen.has(key)) return false;
19816
+ seen.add(key);
19817
+ return true;
19818
+ });
19819
+ }
19820
+ function changedTextEntryText(entry) {
19821
+ return typeof entry === "string" ? entry.trim() : (entry.expected_text || entry.text || "").trim();
19822
+ }
19823
+ function changedTextEntrySelector(entry) {
19824
+ return typeof entry === "string" ? void 0 : entry.selector?.trim() || void 0;
19035
19825
  }
19036
- function htmlArtifactCard(artifact) {
19037
- const target = artifactTarget(artifact);
19038
- if (!target) return `<p>${escapeHtml(artifact.name)}</p>`;
19039
- if (artifact.kind === "image") {
19040
- return `<figure><img src="${escapeHtml(target)}" alt="${escapeHtml(artifact.name)}"><figcaption>${escapeHtml(artifact.name)}</figcaption></figure>`;
19041
- }
19042
- return `<p><a href="${escapeHtml(target)}">${escapeHtml(artifact.name)}</a></p>`;
19826
+ function changedTextEntryLabel(entry) {
19827
+ return typeof entry === "string" ? void 0 : entry.label?.trim() || void 0;
19043
19828
  }
19044
- function htmlArtifactLink(artifact) {
19045
- const target = artifactTarget(artifact);
19046
- if (!target) return escapeHtml(artifact.name);
19047
- return `<a href="${escapeHtml(target)}">${escapeHtml(artifact.name)}</a>`;
19829
+ function suggestionProfileName(input, route, files) {
19830
+ if (input.name?.trim()) return input.name.trim();
19831
+ return `suggested-${slugify(route || files[0] || "profile")}`;
19048
19832
  }
19049
- function htmlList(items, emptyText) {
19050
- if (!items.length) return `<p class="muted">${escapeHtml(emptyText)}</p>`;
19051
- return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
19833
+ function defaultViewports(includeMobile) {
19834
+ const viewports = [
19835
+ { name: "desktop", width: 1280, height: 800 }
19836
+ ];
19837
+ if (includeMobile !== false) {
19838
+ viewports.push({ name: "mobile", width: 390, height: 844 });
19839
+ }
19840
+ return viewports;
19052
19841
  }
19053
- function riddleProofChangeReceiptHtml(receipt) {
19054
- const artifactLinks = [...receipt.before.artifacts, ...receipt.after.artifacts].filter((artifact) => artifactTarget(artifact)).slice(0, 32);
19055
- const deltaRows = receipt.deltas.map((delta) => `
19056
- <tr>
19057
- <td>${escapeHtml(delta.label)}</td>
19058
- <td><span class="status ${escapeHtml(delta.status)}">${escapeHtml(delta.status)}</span></td>
19059
- <td>${escapeHtml(delta.before_observed)}</td>
19060
- <td>${escapeHtml(delta.after_observed)}</td>
19061
- </tr>
19062
- ${delta.message ? `<tr><td class="muted">${escapeHtml(delta.label)} message</td><td colspan="3">${escapeHtml(delta.message)}</td></tr>` : ""}`).join("");
19063
- return `<!doctype html>
19064
- <html lang="en">
19065
- <head>
19066
- <meta charset="utf-8">
19067
- <meta name="viewport" content="width=device-width, initial-scale=1">
19068
- <title>${escapeHtml(receipt.contract_name)} - Riddle Proof Change Receipt</title>
19069
- <style>
19070
- :root { color-scheme: light dark; --bg: #0f141b; --panel: #171f29; --text: #eef4f8; --muted: #aebbc8; --border: #314152; --ok: #69d089; --bad: #ff7a7a; --warn: #ffd166; }
19071
- body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); }
19072
- main { max-width: 1120px; margin: 0 auto; padding: 32px 20px 48px; }
19073
- header, section { border: 1px solid var(--border); background: var(--panel); border-radius: 8px; padding: 20px; margin: 0 0 18px; }
19074
- h1, h2, h3, p { margin-top: 0; }
19075
- h1 { font-size: clamp(1.6rem, 3vw, 2.4rem); }
19076
- h2 { font-size: 1.1rem; margin-bottom: 14px; }
19077
- .muted { color: var(--muted); }
19078
- .verdict { display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 999px; border: 1px solid var(--border); font-weight: 700; }
19079
- .mergeable, .passed { color: var(--ok); }
19080
- .not_mergeable, .failed, .product_regression { color: var(--bad); }
19081
- .proof_insufficient, .environment_blocked, .needs_human_review, .configuration_error { color: var(--warn); }
19082
- .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
19083
- .side { border: 1px solid var(--border); border-radius: 8px; padding: 14px; min-width: 0; }
19084
- code { overflow-wrap: anywhere; color: var(--muted); }
19085
- table { width: 100%; border-collapse: collapse; }
19086
- th, td { border-bottom: 1px solid var(--border); padding: 10px 8px; text-align: left; vertical-align: top; }
19087
- figure { margin: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: #0b1017; }
19088
- img { display: block; width: 100%; height: auto; }
19089
- figcaption { padding: 8px 10px; color: var(--muted); font-size: 0.9rem; }
19090
- ul { margin-bottom: 0; }
19091
- a { color: #8bc7ff; }
19092
- @media (max-width: 760px) { .grid { grid-template-columns: 1fr; } main { padding: 18px 12px 32px; } }
19093
- </style>
19094
- </head>
19095
- <body>
19096
- <main>
19097
- <header>
19098
- <p class="verdict ${escapeHtml(receipt.verdict)}">${escapeHtml(receipt.verdict)}</p>
19099
- <h1>${escapeHtml(receipt.contract_name)}</h1>
19100
- <p>${escapeHtml(receipt.summary)}</p>
19101
- <p class="muted">Status: ${escapeHtml(receipt.status)}${receipt.profile_name ? ` | Profile: ${escapeHtml(receipt.profile_name)}` : ""}</p>
19102
- </header>
19103
- <section>
19104
- <h2>Evidence Pair</h2>
19105
- <div class="grid">
19106
- <div class="side">
19107
- <h3>Before</h3>
19108
- <p><code>${escapeHtml(receipt.before.source)}</code></p>
19109
- <p>Status: <strong class="${escapeHtml(receipt.before.status)}">${escapeHtml(receipt.before.status)}</strong></p>
19110
- <p>${escapeHtml(receipt.before.summary)}</p>
19111
- <p class="muted">${receipt.before.checks.passed} passed / ${receipt.before.checks.failed} failed checks</p>
19112
- </div>
19113
- <div class="side">
19114
- <h3>After</h3>
19115
- <p><code>${escapeHtml(receipt.after.source)}</code></p>
19116
- <p>Status: <strong class="${escapeHtml(receipt.after.status)}">${escapeHtml(receipt.after.status)}</strong></p>
19117
- <p>${escapeHtml(receipt.after.summary)}</p>
19118
- <p class="muted">${receipt.after.checks.passed} passed / ${receipt.after.checks.failed} failed checks</p>
19119
- </div>
19120
- </div>
19121
- </section>
19122
- <section>
19123
- <h2>Delta Checks</h2>
19124
- <table>
19125
- <thead><tr><th>Delta</th><th>Status</th><th>Before</th><th>After</th></tr></thead>
19126
- <tbody>${deltaRows}</tbody>
19127
- </table>
19128
- </section>
19129
- <section>
19130
- <h2>Screenshots</h2>
19131
- <div class="grid">
19132
- <div>${receipt.before.screenshots.slice(0, 4).map(htmlArtifactCard).join("") || `<p class="muted">No before screenshots recorded.</p>`}</div>
19133
- <div>${receipt.after.screenshots.slice(0, 4).map(htmlArtifactCard).join("") || `<p class="muted">No after screenshots recorded.</p>`}</div>
19134
- </div>
19135
- </section>
19136
- <section>
19137
- <h2>What This Proves</h2>
19138
- ${htmlList(receipt.proves, "No explicit proof claims were recorded in contract metadata.")}
19139
- </section>
19140
- <section>
19141
- <h2>What This Does Not Prove</h2>
19142
- ${htmlList(receipt.does_not_prove, "No explicit limits were recorded in contract metadata.")}
19143
- </section>
19144
- <section>
19145
- <h2>Artifacts</h2>
19146
- ${artifactLinks.length ? `<ul>${artifactLinks.map((artifact) => `<li>${escapeHtml(artifact.side)}: ${htmlArtifactLink(artifact)}</li>`).join("")}</ul>` : `<p class="muted">No linked artifacts recorded.</p>`}
19147
- </section>
19148
- </main>
19149
- </body>
19150
- </html>
19151
- `;
19842
+ function suggestRiddleProofProfileChecks(input) {
19843
+ const changedFiles = nonEmptyStrings(input.changed_files);
19844
+ const selectors = nonEmptyStrings(input.selectors);
19845
+ const changedText = input.changed_text || [];
19846
+ const route = normalizeRoute(input.route) || inferRouteFromFiles(changedFiles) || routeFromUrl(input.url);
19847
+ const uiFiles = changedFiles.filter((file) => UI_FILE_PATTERN.test(file));
19848
+ const visualAssets = changedFiles.filter((file) => VISUAL_ASSET_PATTERN.test(file));
19849
+ const warnings = [];
19850
+ const suggestions = [];
19851
+ const setupActions = [];
19852
+ if (!route && !input.url?.trim()) {
19853
+ warnings.push("No route or URL was supplied; the draft profile defaults to route /.");
19854
+ }
19855
+ if ((uiFiles.length || visualAssets.length) && !changedText.length && !selectors.length) {
19856
+ warnings.push("UI or visual files changed, but no changed text or selectors were supplied; add focused checks before trusting this profile.");
19857
+ }
19858
+ const routeOrDefault = route || "/";
19859
+ const routeChecks = [
19860
+ { type: "route_loaded", expected_path: routeOrDefault }
19861
+ ];
19862
+ suggestions.push({
19863
+ id: "route-loaded",
19864
+ reason: "Every browser proof should first prove that the intended route loaded.",
19865
+ checks: routeChecks
19866
+ });
19867
+ const hygieneChecks = [
19868
+ { type: "no_fatal_console_errors" },
19869
+ { type: "no_mobile_horizontal_overflow" }
19870
+ ];
19871
+ suggestions.push({
19872
+ id: "runtime-hygiene",
19873
+ reason: "UI changes should not introduce fatal console errors or mobile overflow.",
19874
+ checks: hygieneChecks
19875
+ });
19876
+ for (const selector of selectors) {
19877
+ suggestions.push({
19878
+ id: `selector-${slugify(selector)}`,
19879
+ reason: `Changed selector ${selector} should remain visible.`,
19880
+ checks: [{ type: "selector_visible", selector }]
19881
+ });
19882
+ }
19883
+ for (const entry of changedText) {
19884
+ const text = changedTextEntryText(entry);
19885
+ if (!text) continue;
19886
+ const selector = changedTextEntrySelector(entry);
19887
+ const label = changedTextEntryLabel(entry);
19888
+ const check = selector ? { type: "selector_text_visible", selector, text, label } : { type: "text_visible", text, label };
19889
+ suggestions.push({
19890
+ id: `text-${slugify(`${selector || "page"}-${text}`)}`,
19891
+ reason: selector ? `Expected text should be visible inside ${selector}.` : "Expected text should be visible on the route.",
19892
+ checks: [check]
19893
+ });
19894
+ }
19895
+ if (input.include_screenshot !== false && (uiFiles.length || visualAssets.length || selectors.length || changedText.length)) {
19896
+ setupActions.push({ type: "screenshot", label: "suggested-proof-screenshot", full_page: true });
19897
+ suggestions.push({
19898
+ id: "screenshot-artifact",
19899
+ reason: "A screenshot artifact keeps the proof packet inspectable by humans and hosted renderers.",
19900
+ checks: [],
19901
+ setup_actions: setupActions
19902
+ });
19903
+ }
19904
+ const checks = uniqueChecks(suggestions.flatMap((suggestion) => suggestion.checks));
19905
+ const target = {
19906
+ ...input.url?.trim() ? { url: input.url.trim() } : { route: routeOrDefault },
19907
+ viewports: defaultViewports(input.include_mobile),
19908
+ setup_actions: setupActions.length ? setupActions : void 0
19909
+ };
19910
+ const profile = {
19911
+ version: RIDDLE_PROOF_PROFILE_VERSION,
19912
+ name: suggestionProfileName(input, routeOrDefault, changedFiles),
19913
+ target,
19914
+ checks,
19915
+ artifacts: ["screenshot", "console", "proof_json"],
19916
+ baseline_policy: "invariant_only",
19917
+ failure_policy: {
19918
+ product_regression: "fail",
19919
+ proof_insufficient: "review",
19920
+ environment_blocked: "neutral",
19921
+ configuration_error: "fail",
19922
+ needs_human_review: "review"
19923
+ },
19924
+ metadata: {
19925
+ suggestion_input: {
19926
+ changed_files: changedFiles,
19927
+ selectors,
19928
+ changed_text_count: changedText.length
19929
+ }
19930
+ }
19931
+ };
19932
+ return {
19933
+ version: RIDDLE_PROOF_PROFILE_SUGGESTIONS_VERSION,
19934
+ profile,
19935
+ suggestions,
19936
+ warnings
19937
+ };
19152
19938
  }
19153
19939
 
19154
19940
  // src/cli.ts
@@ -19163,8 +19949,14 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
19163
19949
  "balancePreflight",
19164
19950
  "baseUrl",
19165
19951
  "afterResult",
19952
+ "afterObservation",
19953
+ "afterPreviewReceipt",
19954
+ "afterSourceRevision",
19166
19955
  "afterUrl",
19167
19956
  "beforeResult",
19957
+ "beforeObservation",
19958
+ "beforePreviewReceipt",
19959
+ "beforeSourceRevision",
19168
19960
  "beforeUrl",
19169
19961
  "candidateJson",
19170
19962
  "candidatesJson",
@@ -19218,6 +20010,7 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
19218
20010
  "pack",
19219
20011
  "packFile",
19220
20012
  "profile",
20013
+ "previewReceipt",
19221
20014
  "proofDir",
19222
20015
  "pr",
19223
20016
  "progressEveryMs",
@@ -19241,6 +20034,9 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
19241
20034
  "scriptFile",
19242
20035
  "selectors",
19243
20036
  "sourceKind",
20037
+ "sourceDirty",
20038
+ "sourceRepository",
20039
+ "sourceRevision",
19244
20040
  "splitViewports",
19245
20041
  "stateDir",
19246
20042
  "statePath",
@@ -19272,16 +20068,16 @@ function usage() {
19272
20068
  " riddle-proof-loop respond --state-path <path> --response-json <file|json|->",
19273
20069
  " riddle-proof-loop respond --state-path <path> --decision <decision> --summary <text> [--payload-json <file|json|->]",
19274
20070
  " riddle-proof-loop status --state-path <path>",
19275
- " 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]",
19276
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]",
19277
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]",
19278
- " 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]",
19279
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]",
19280
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>]",
19281
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]",
19282
20078
  " riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
19283
20079
  " riddle-proof-loop profile-http-status-preflight --profile <file|json|-> --url <base-url> [--format json|summary]",
19284
- " 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]",
19285
20081
  " riddle-proof-loop riddle-server-preview <directory> --script-file <file> [--path /route] [--wait-for-selector selector]",
19286
20082
  " riddle-proof-loop riddle-run-script --url <url> --script-file <file> [--viewport 1280x720] [--strict true|false]",
19287
20083
  " riddle-proof-loop riddle-poll <job-id> [--wait] [--attempts n] [--quiet]",
@@ -19441,6 +20237,7 @@ function compactRunProfileResult(result, options) {
19441
20237
  output_dir: outputDir,
19442
20238
  output_files: outputDir ? {
19443
20239
  profile_result: "profile-result.json",
20240
+ observation_receipt: "observation-receipt.json",
19444
20241
  summary: "summary.md",
19445
20242
  proof_json: result.evidence ? "proof.json" : void 0,
19446
20243
  console: result.evidence?.console ? "console.json" : void 0,
@@ -20069,12 +20866,13 @@ function formatByteCount(bytes) {
20069
20866
  }
20070
20867
  function riddlePollProgressLine(snapshot) {
20071
20868
  const submittedAt = snapshot.submitted_at || "not-submitted";
20072
- 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)}` : "";
20073
20870
  const terminalPart = snapshot.terminal ? " terminal=true" : "";
20074
20871
  return [
20075
20872
  "[riddle-poll]",
20076
20873
  snapshot.job_id,
20077
20874
  `status=${snapshot.status || "unknown"}`,
20875
+ `phase=${snapshot.phase || "unknown"}`,
20078
20876
  `attempt=${snapshot.attempt}/${snapshot.attempts}`,
20079
20877
  `elapsed=${formatPollDuration(snapshot.elapsed_ms)}`,
20080
20878
  `submitted_at=${submittedAt}${queuePart}${terminalPart}`
@@ -23012,10 +23810,63 @@ function profileHttpStatusSummaryMarkdown(result) {
23012
23810
  }
23013
23811
  return lines;
23014
23812
  }
23015
- function writeProfileOutput(outputDir, result) {
23813
+ function profileResultTargetUrl(result) {
23814
+ const legacyRoute = result.route;
23815
+ const observed = result.route.observed;
23816
+ const requested = result.route.requested || legacyRoute.url || result.evidence?.target_url;
23817
+ if (observed) {
23818
+ try {
23819
+ return new URL(observed, requested).href;
23820
+ } catch {
23821
+ }
23822
+ }
23823
+ if (requested) {
23824
+ try {
23825
+ return new URL(requested).href;
23826
+ } catch {
23827
+ return requested;
23828
+ }
23829
+ }
23830
+ return observed || "unknown";
23831
+ }
23832
+ function profileObservationForOutput(result, outputDir, options) {
23833
+ if (options.observation) return options.observation;
23834
+ const preview = options.previewReceipt;
23835
+ const targetUrl = profileResultTargetUrl(result);
23836
+ return createRiddleProofObservationReceipt({
23837
+ comparison_role: options.comparisonRole || "standalone",
23838
+ executor: result.runner === "local-playwright" ? { kind: "local_playwright", runner: result.runner } : {
23839
+ kind: "riddle_hosted",
23840
+ runner: result.runner,
23841
+ job_id: result.riddle?.job_id,
23842
+ worker_id: typeof result.riddle?.execution?.worker_id === "string" ? result.riddle.execution.worker_id : void 0
23843
+ },
23844
+ target: preview ? { kind: "preview", url: targetUrl, preview } : { kind: "url", url: targetUrl },
23845
+ source: options.source || preview?.source,
23846
+ profile_result: result,
23847
+ publication: { kind: "local", path: outputDir },
23848
+ execution: result.riddle?.execution
23849
+ });
23850
+ }
23851
+ function profileOutputOptionsForCli(options) {
23852
+ const previewInput = optionString(options, "previewReceipt");
23853
+ const previewReceipt = previewInput ? parseRiddlePreviewReceipt(readJsonValue(previewInput, "--preview-receipt")) : void 0;
23854
+ const sourceDirty = optionBoolean(options, "sourceDirty");
23855
+ const source = {
23856
+ ...previewReceipt?.source || {},
23857
+ ...optionString(options, "sourceRevision") ? { git_revision: optionString(options, "sourceRevision") } : {},
23858
+ ...optionString(options, "sourceRepository") ? { repository: optionString(options, "sourceRepository") } : {},
23859
+ ...typeof sourceDirty === "boolean" ? { dirty: sourceDirty } : {}
23860
+ };
23861
+ return { previewReceipt, source };
23862
+ }
23863
+ function writeProfileOutput(outputDir, result, options = {}) {
23016
23864
  if (!outputDir) return;
23017
23865
  (0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
23018
23866
  (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "profile-result.json"), `${JSON.stringify(result, null, 2)}
23867
+ `);
23868
+ const observation = profileObservationForOutput(result, outputDir, options);
23869
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "observation-receipt.json"), `${JSON.stringify(observation, null, 2)}
23019
23870
  `);
23020
23871
  (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "summary.md"), profileResultMarkdown(result));
23021
23872
  if (result.evidence) (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "proof.json"), `${JSON.stringify(result, null, 2)}
@@ -23056,6 +23907,12 @@ function profileOptionsForChangeProofSide(options, url, outputDir) {
23056
23907
  }
23057
23908
  return next;
23058
23909
  }
23910
+ function observationForChangeSide(observation, side) {
23911
+ if (observation.comparison_role !== "standalone" && observation.comparison_role !== side) {
23912
+ throw new Error(`${side} observation has incompatible comparison_role ${observation.comparison_role}.`);
23913
+ }
23914
+ return observation.comparison_role === side ? observation : { ...observation, comparison_role: side };
23915
+ }
23059
23916
  function compactChangeProofResult(run, options) {
23060
23917
  const outputDir = profileOutputDirOption(options);
23061
23918
  return {
@@ -23091,9 +23948,13 @@ function compactChangeProofResult(run, options) {
23091
23948
  change_proof_receipt: "change-proof-receipt.json",
23092
23949
  change_proof_receipt_markdown: "change-proof-receipt.md",
23093
23950
  change_proof_receipt_html: "change-proof-receipt.html",
23951
+ handoff_receipt: "handoff-receipt.json",
23952
+ handoff_receipt_markdown: "handoff-receipt.md",
23094
23953
  summary: "summary.md",
23095
23954
  before_profile_result: "before/profile-result.json",
23096
- after_profile_result: "after/profile-result.json"
23955
+ after_profile_result: "after/profile-result.json",
23956
+ before_observation_receipt: "before/observation-receipt.json",
23957
+ after_observation_receipt: "after/observation-receipt.json"
23097
23958
  } : void 0
23098
23959
  };
23099
23960
  }
@@ -23109,9 +23970,12 @@ function writeChangeProofOutput(outputDir, run) {
23109
23970
  `);
23110
23971
  (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "change-proof-receipt.md"), riddleProofChangeReceiptMarkdown(run.receipt));
23111
23972
  (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "change-proof-receipt.html"), riddleProofChangeReceiptHtml(run.receipt));
23973
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "handoff-receipt.json"), `${JSON.stringify(run.handoff, null, 2)}
23974
+ `);
23975
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "handoff-receipt.md"), buildRiddleProofPrCommentMarkdown({ result: run.handoff }));
23112
23976
  (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "summary.md"), changeProofResultMarkdown(run));
23113
- writeProfileOutput(changeProofSideOutputDir(outputDir, "before"), run.beforeResult);
23114
- writeProfileOutput(changeProofSideOutputDir(outputDir, "after"), run.afterResult);
23977
+ writeProfileOutput(changeProofSideOutputDir(outputDir, "before"), run.beforeResult, { observation: run.beforeObservation });
23978
+ writeProfileOutput(changeProofSideOutputDir(outputDir, "after"), run.afterResult, { observation: run.afterObservation });
23115
23979
  }
23116
23980
  function writeRunChangeProofResult(run, options) {
23117
23981
  const format = runProfileResultFormatOption(options);
@@ -23210,6 +24074,7 @@ function withRiddleMetadata(result, input) {
23210
24074
  ...result.riddle || {},
23211
24075
  job_id: input.job_id || result.riddle?.job_id,
23212
24076
  status: input.status ?? result.riddle?.status,
24077
+ phase: poll?.phase ?? result.riddle?.phase,
23213
24078
  terminal: input.terminal ?? result.riddle?.terminal,
23214
24079
  created_at: poll?.created_at ?? result.riddle?.created_at,
23215
24080
  submitted_at: poll?.submitted_at ?? result.riddle?.submitted_at,
@@ -23222,7 +24087,8 @@ function withRiddleMetadata(result, input) {
23222
24087
  timed_out: poll?.timed_out ?? result.riddle?.timed_out,
23223
24088
  retry_count: input.retryCount ?? result.riddle?.retry_count,
23224
24089
  stale_job_ids: staleJobIds?.length ? staleJobIds : result.riddle?.stale_job_ids,
23225
- artifact_recovery: input.artifactRecovery ?? result.riddle?.artifact_recovery
24090
+ artifact_recovery: input.artifactRecovery ?? result.riddle?.artifact_recovery,
24091
+ execution: poll?.execution ?? result.riddle?.execution
23226
24092
  },
23227
24093
  artifacts: {
23228
24094
  ...result.artifacts,
@@ -23290,6 +24156,7 @@ function riddleMetadataFromPoll(jobId, poll) {
23290
24156
  return {
23291
24157
  job_id: jobId,
23292
24158
  status: poll.status,
24159
+ phase: poll.poll?.phase,
23293
24160
  terminal: poll.terminal,
23294
24161
  created_at: poll.poll?.created_at,
23295
24162
  submitted_at: poll.poll?.submitted_at,
@@ -23299,7 +24166,8 @@ function riddleMetadataFromPoll(jobId, poll) {
23299
24166
  elapsed_ms: poll.poll?.elapsed_ms,
23300
24167
  attempt: poll.poll?.attempt,
23301
24168
  attempts: poll.poll?.attempts,
23302
- timed_out: poll.poll?.timed_out
24169
+ timed_out: poll.poll?.timed_out,
24170
+ execution: poll.poll?.execution
23303
24171
  };
23304
24172
  }
23305
24173
  function profileUnsubmittedRetryTimeoutMs(options) {
@@ -23845,7 +24713,7 @@ async function runSplitViewportProfileForCli(profile, options, input) {
23845
24713
  const childProfile = profileForSplitViewport(profile, viewport);
23846
24714
  const childOutputDir = outputDir ? splitViewportOutputDir(outputDir, viewport.name, seenOutputNames) : void 0;
23847
24715
  const result2 = await runSingleRiddleProfileForCli(childProfile, options, { ...input, outputDir: childOutputDir });
23848
- if (childOutputDir) writeProfileOutput(childOutputDir, result2);
24716
+ if (childOutputDir) writeProfileOutput(childOutputDir, result2, profileOutputOptionsForCli(options));
23849
24717
  childRuns.push({ viewport, profile: childProfile, result: result2 });
23850
24718
  }
23851
24719
  const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
@@ -23977,18 +24845,34 @@ async function runChangeProofForCli(rawProfile, options) {
23977
24845
  const contract = readChangeContractForCli(options);
23978
24846
  const beforeResultPath = optionString(options, "beforeResult");
23979
24847
  const afterResultPath = optionString(options, "afterResult");
24848
+ const beforeObservationPath = optionString(options, "beforeObservation");
24849
+ const afterObservationPath = optionString(options, "afterObservation");
23980
24850
  const beforeUrl = optionString(options, "beforeUrl");
23981
24851
  const afterUrl = optionString(options, "afterUrl");
24852
+ const beforePreviewInput = optionString(options, "beforePreviewReceipt");
24853
+ const afterPreviewInput = optionString(options, "afterPreviewReceipt");
24854
+ const beforePreview = beforePreviewInput ? parseRiddlePreviewReceipt(readJsonValue(beforePreviewInput, "--before-preview-receipt")) : void 0;
24855
+ const afterPreview = afterPreviewInput ? parseRiddlePreviewReceipt(readJsonValue(afterPreviewInput, "--after-preview-receipt")) : void 0;
23982
24856
  const hasResultInputs = Boolean(beforeResultPath || afterResultPath);
24857
+ const hasObservationInputs = Boolean(beforeObservationPath || afterObservationPath);
23983
24858
  const hasUrlInputs = Boolean(beforeUrl || afterUrl);
23984
- if (hasResultInputs && hasUrlInputs) {
23985
- throw new Error("run-change-proof accepts either --before-result/--after-result or --before-url/--after-url, not both.");
24859
+ if ([hasResultInputs, hasObservationInputs, hasUrlInputs].filter(Boolean).length > 1) {
24860
+ throw new Error("run-change-proof accepts one before/after input pair: result, observation, or URL.");
23986
24861
  }
23987
24862
  if (hasResultInputs && (!beforeResultPath || !afterResultPath)) {
23988
24863
  throw new Error("run-change-proof requires both --before-result and --after-result.");
23989
24864
  }
23990
- if (!hasResultInputs && (!beforeUrl || !afterUrl)) {
23991
- throw new Error("run-change-proof requires both --before-url and --after-url unless saved results are provided.");
24865
+ if (hasObservationInputs && (!beforeObservationPath || !afterObservationPath)) {
24866
+ throw new Error("run-change-proof requires both --before-observation and --after-observation.");
24867
+ }
24868
+ if (hasUrlInputs && (!beforeUrl || !afterUrl)) {
24869
+ throw new Error("run-change-proof requires both --before-url and --after-url.");
24870
+ }
24871
+ if (!hasResultInputs && !hasObservationInputs && !hasUrlInputs) {
24872
+ throw new Error("run-change-proof requires a before/after result, observation, or URL pair.");
24873
+ }
24874
+ if ((beforePreview || afterPreview) && !hasUrlInputs) {
24875
+ throw new Error("--before-preview-receipt and --after-preview-receipt require the URL input mode.");
23992
24876
  }
23993
24877
  const outputDir = profileOutputDirOption(options);
23994
24878
  let profile;
@@ -23996,15 +24880,53 @@ async function runChangeProofForCli(rawProfile, options) {
23996
24880
  let afterResult;
23997
24881
  let beforeSource;
23998
24882
  let afterSource;
23999
- if (hasResultInputs) {
24883
+ let beforeObservation;
24884
+ let afterObservation;
24885
+ if (hasObservationInputs) {
24886
+ if (!beforeObservationPath || !afterObservationPath) {
24887
+ throw new Error("run-change-proof requires both --before-observation and --after-observation.");
24888
+ }
24889
+ profile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, options), options);
24890
+ beforeObservation = observationForChangeSide(
24891
+ parseRiddleProofObservationReceipt(readJsonValue(beforeObservationPath, "--before-observation")),
24892
+ "before"
24893
+ );
24894
+ afterObservation = observationForChangeSide(
24895
+ parseRiddleProofObservationReceipt(readJsonValue(afterObservationPath, "--after-observation")),
24896
+ "after"
24897
+ );
24898
+ if (!beforeObservation.proof?.result || !afterObservation.proof?.result) {
24899
+ throw new Error("Change Proof observations must include their profile results.");
24900
+ }
24901
+ beforeResult = beforeObservation.proof.result;
24902
+ afterResult = afterObservation.proof.result;
24903
+ beforeSource = beforeObservation.target.url;
24904
+ afterSource = afterObservation.target.url;
24905
+ } else if (hasResultInputs) {
24000
24906
  if (!beforeResultPath || !afterResultPath) {
24001
24907
  throw new Error("run-change-proof requires both --before-result and --after-result.");
24002
24908
  }
24003
24909
  profile = profileWithSelectedViewportNamesForCli(normalizeProfileRecordForCli(rawProfile, options), options);
24004
24910
  beforeResult = readProfileResultForCli(beforeResultPath, "--before-result");
24005
24911
  afterResult = readProfileResultForCli(afterResultPath, "--after-result");
24006
- beforeSource = beforeResultPath;
24007
- afterSource = afterResultPath;
24912
+ beforeSource = profileResultTargetUrl(beforeResult);
24913
+ afterSource = profileResultTargetUrl(afterResult);
24914
+ beforeObservation = createRiddleProofObservationReceipt({
24915
+ comparison_role: "before",
24916
+ executor: { kind: beforeResult.runner === "local-playwright" ? "local_playwright" : "riddle_hosted", runner: beforeResult.runner, job_id: beforeResult.riddle?.job_id },
24917
+ target: { kind: "url", url: beforeSource },
24918
+ profile_result: beforeResult,
24919
+ execution: beforeResult.riddle?.execution,
24920
+ publication: { kind: "local", path: beforeResultPath }
24921
+ });
24922
+ afterObservation = createRiddleProofObservationReceipt({
24923
+ comparison_role: "after",
24924
+ executor: { kind: afterResult.runner === "local-playwright" ? "local_playwright" : "riddle_hosted", runner: afterResult.runner, job_id: afterResult.riddle?.job_id },
24925
+ target: { kind: "url", url: afterSource },
24926
+ profile_result: afterResult,
24927
+ execution: afterResult.riddle?.execution,
24928
+ publication: { kind: "local", path: afterResultPath }
24929
+ });
24008
24930
  } else {
24009
24931
  if (!beforeUrl || !afterUrl) {
24010
24932
  throw new Error("run-change-proof requires both --before-url and --after-url unless saved results are provided.");
@@ -24018,21 +24940,54 @@ async function runChangeProofForCli(rawProfile, options) {
24018
24940
  afterResult = await runProfileForCli(afterProfile, afterOptions);
24019
24941
  beforeSource = beforeUrl;
24020
24942
  afterSource = afterUrl;
24943
+ beforeObservation = createRiddleProofObservationReceipt({
24944
+ comparison_role: "before",
24945
+ executor: { kind: "riddle_hosted", runner: beforeResult.runner, job_id: beforeResult.riddle?.job_id },
24946
+ target: beforePreview ? { kind: "preview", url: beforeSource, preview: beforePreview } : { kind: "url", url: beforeSource },
24947
+ source: beforePreview?.source,
24948
+ profile_result: beforeResult,
24949
+ execution: beforeResult.riddle?.execution
24950
+ });
24951
+ afterObservation = createRiddleProofObservationReceipt({
24952
+ comparison_role: "after",
24953
+ executor: { kind: "riddle_hosted", runner: afterResult.runner, job_id: afterResult.riddle?.job_id },
24954
+ target: afterPreview ? { kind: "preview", url: afterSource, preview: afterPreview } : { kind: "url", url: afterSource },
24955
+ source: afterPreview?.source,
24956
+ profile_result: afterResult,
24957
+ execution: afterResult.riddle?.execution
24958
+ });
24021
24959
  }
24022
24960
  const result = assessRiddleProofChange(contract, {
24023
24961
  before_result: beforeResult,
24024
- after_result: afterResult
24962
+ after_result: afterResult,
24963
+ before_observation: beforeObservation,
24964
+ after_observation: afterObservation,
24965
+ expected_source_revisions: {
24966
+ before: optionString(options, "beforeSourceRevision"),
24967
+ after: optionString(options, "afterSourceRevision")
24968
+ }
24025
24969
  });
24026
24970
  const receipt = createRiddleProofChangeReceipt({
24027
24971
  contract,
24028
24972
  result,
24029
- before_result: beforeResult,
24030
- after_result: afterResult,
24031
- before_source: beforeSource,
24032
- after_source: afterSource,
24973
+ before_observation: beforeObservation,
24974
+ after_observation: afterObservation,
24033
24975
  profile_name: profile.name
24034
24976
  });
24035
- return { profile, contract, beforeResult, afterResult, result, receipt, beforeSource, afterSource };
24977
+ const handoff = createRiddleProofHandoffReceipt(receipt);
24978
+ return {
24979
+ profile,
24980
+ contract,
24981
+ beforeResult,
24982
+ afterResult,
24983
+ result,
24984
+ receipt,
24985
+ handoff,
24986
+ beforeObservation,
24987
+ afterObservation,
24988
+ beforeSource,
24989
+ afterSource
24990
+ };
24036
24991
  }
24037
24992
  function requestForRun(options) {
24038
24993
  const statePath = optionString(options, "statePath");
@@ -24084,7 +25039,7 @@ async function main() {
24084
25039
  if (command === "run-profile") {
24085
25040
  const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
24086
25041
  const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : positional[1] === "aggregate" ? await aggregateProfileResultsForCli(profile, options) : await runProfileForCli(profile, options);
24087
- writeProfileOutput(profileOutputDirOption(options), result);
25042
+ writeProfileOutput(profileOutputDirOption(options), result, profileOutputOptionsForCli(options));
24088
25043
  const diagnosticLine = profileCliDiagnosticLine(result);
24089
25044
  if (diagnosticLine && optionBoolean(options, "quiet") !== true) {
24090
25045
  process.stderr.write(`${diagnosticLine}
@@ -24115,6 +25070,7 @@ async function main() {
24115
25070
  const explicitResultJsonPath = optionString(options, "resultJson");
24116
25071
  const runResponsePath = explicitRunResponsePath || defaultProofDirJsonPath(proofDir, "riddle-run-response.json");
24117
25072
  const resultJsonPaths = explicitResultJsonPath ? [explicitResultJsonPath] : [
25073
+ defaultProofDirJsonPath(proofDir, "handoff-receipt.json"),
24118
25074
  defaultProofDirJsonPath(proofDir, "result.json"),
24119
25075
  defaultProofDirJsonPath(proofDir, "change-proof-receipt.json"),
24120
25076
  defaultProofDirJsonPath(proofDir, "change-proof-result.json"),
@@ -24236,6 +25192,16 @@ async function main() {
24236
25192
  };
24237
25193
  }
24238
25194
  const result = await createRiddleApiClient(clientConfig).deployPreview(buildDir, label, previewFrameworkOption(options));
25195
+ const outputDir = profileOutputDirOption(options);
25196
+ if (outputDir) {
25197
+ (0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
25198
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "preview-deploy-result.json"), `${JSON.stringify(result, null, 2)}
25199
+ `);
25200
+ if (result.receipt) {
25201
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "preview-receipt.json"), `${JSON.stringify(result.receipt, null, 2)}
25202
+ `);
25203
+ }
25204
+ }
24239
25205
  for (const warning of result.warnings ?? []) {
24240
25206
  process.stderr.write(`Warning: ${warning}
24241
25207
  `);