@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
@@ -1,3 +1,11 @@
1
+ import {
2
+ RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION,
3
+ RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
4
+ RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION,
5
+ createRiddleProofHandoffReceipt,
6
+ parseRiddleProofChangeReceipt,
7
+ parseRiddleProofHandoffReceipt
8
+ } from "./chunk-6VFS2JFR.js";
1
9
  import {
2
10
  riddleProofPublicStateAllowsMergeRecommendation,
3
11
  riddleProofPublicStateMergeRecommendation,
@@ -84,9 +92,10 @@ function collectChangeReceiptArtifacts(result) {
84
92
  const seen = /* @__PURE__ */ new Set();
85
93
  const collectSide = (sideName, side) => {
86
94
  const candidates = [
95
+ side.canonical_screenshot,
87
96
  ...asArray(side.screenshots),
88
97
  ...asArray(side.artifacts)
89
- ];
98
+ ].filter(Boolean);
90
99
  for (const [index, item] of candidates.entries()) {
91
100
  const artifact = asRecord(item);
92
101
  const url = stringValue(artifact.url) || stringValue(artifact.path);
@@ -178,9 +187,10 @@ function changeReceiptPageSummaries(result) {
178
187
  for (const sideName of ["before", "after"]) {
179
188
  const side = asRecord(result[sideName]);
180
189
  if (!Object.keys(side).length) continue;
181
- const checks = asRecord(side.checks);
190
+ const profileSummary = asRecord(side.profile_summary);
191
+ const checks = Object.keys(asRecord(side.checks)).length ? asRecord(side.checks) : asRecord(profileSummary.checks);
182
192
  pages.push({
183
- route: firstStringValue(side.source, side.profile_name) || sideName,
193
+ route: firstStringValue(asRecord(side.target).url, side.source, profileSummary.profile_name, side.profile_name) || sideName,
184
194
  passed: numberValue(checks.passed) ?? 0,
185
195
  failed: numberValue(checks.failed) ?? 0
186
196
  });
@@ -243,7 +253,11 @@ function isProfileResult(result) {
243
253
  return version === "riddle-proof.profile-result.v1" || version === "riddle-proof.profile-compact-result.v1";
244
254
  }
245
255
  function isChangeReceiptResult(result) {
246
- return stringValue(result.version) === "riddle-proof.change-receipt.v1";
256
+ const version = stringValue(result.version);
257
+ return version === RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION || version === RIDDLE_PROOF_CHANGE_RECEIPT_VERSION;
258
+ }
259
+ function isHandoffReceiptResult(result) {
260
+ return stringValue(result.version) === RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION;
247
261
  }
248
262
  function isChangeResult(result) {
249
263
  const version = stringValue(result.version);
@@ -251,10 +265,13 @@ function isChangeResult(result) {
251
265
  }
252
266
  function summarizeRiddleProofPrComment(input) {
253
267
  const runResponse = asRecord(input.runResponse);
254
- const result = asRecord(input.result);
268
+ const inputResult = asRecord(input.result);
269
+ const handoffReceipt = isHandoffReceiptResult(inputResult) ? parseRiddleProofHandoffReceipt(inputResult) : void 0;
270
+ const parsedChangeReceipt = handoffReceipt?.change || (isChangeReceiptResult(inputResult) ? parseRiddleProofChangeReceipt(inputResult) : void 0);
271
+ const result = parsedChangeReceipt ? asRecord(parsedChangeReceipt) : inputResult;
255
272
  const proofResult = asRecord(runResponse.proofResult);
256
273
  const profileResult = isProfileResult(result);
257
- const changeReceiptResult = isChangeReceiptResult(result);
274
+ const changeReceiptResult = Boolean(parsedChangeReceipt);
258
275
  const changeResult = changeReceiptResult || isChangeResult(result);
259
276
  const profileRiddle = asRecord(result.riddle);
260
277
  const preview = asRecord(runResponse.preview);
@@ -275,7 +292,7 @@ function summarizeRiddleProofPrComment(input) {
275
292
  delete checkSource.ok;
276
293
  const nestedChecks = summarizeExplicitChecks(checkSource);
277
294
  const resultStatus = firstStringValue(result.status, stopCondition.status);
278
- const ok = changeResult ? resultStatus === "passed" : booleanValue(result.ok) ?? booleanValue(runResponse.ok) ?? null;
295
+ const ok = changeReceiptResult ? parsedChangeReceipt?.recommendation.merge_recommended ?? false : changeResult ? resultStatus === "passed" : booleanValue(result.ok) ?? booleanValue(runResponse.ok) ?? null;
279
296
  const checkpointSummary = checkpointSummaryFrom(
280
297
  result.checkpoint_summary,
281
298
  stopCondition.checkpoint_summary,
@@ -283,19 +300,19 @@ function summarizeRiddleProofPrComment(input) {
283
300
  rawDetails.checkpoint_summary,
284
301
  proofResult.checkpoint_summary
285
302
  );
286
- const publicState = summarizeRiddleProofPublicState({
303
+ const publicState = changeReceiptResult ? void 0 : summarizeRiddleProofPublicState({
287
304
  ...result,
288
305
  status: resultStatus,
289
306
  checkpoint_summary: checkpointSummary || result.checkpoint_summary
290
307
  });
291
- const mergeRecommendation = riddleProofPublicStateMergeRecommendation(
308
+ const mergeRecommendation = publicState ? riddleProofPublicStateMergeRecommendation(
292
309
  publicState,
293
310
  firstStringValue(result.merge_recommendation, stopCondition.merge_recommendation, resultRaw.merge_recommendation)
294
- );
311
+ ) : void 0;
295
312
  return {
296
313
  ok,
297
314
  status: firstStringValue(proofResult.status, profileRiddle.status),
298
- result_status: changeResult ? resultStatus : publicState.status,
315
+ result_status: changeResult ? resultStatus : publicState?.status,
299
316
  job_id: firstStringValue(proofResult.job_id, profileRiddle.job_id),
300
317
  duration_ms: numberValue(proofResult.duration_ms) ?? numberValue(profileRiddle.elapsed_ms),
301
318
  proof_url: stringValue(runResponse.proofUrl),
@@ -303,13 +320,13 @@ function summarizeRiddleProofPrComment(input) {
303
320
  preview_url: stringValue(preview.preview_url) || stringValue(preview.url),
304
321
  preview_publish_recovered: booleanValue(preview.publish_recovered),
305
322
  preview_publish_error: stringValue(preview.publish_error),
306
- ship_held: publicState.ship_held,
307
- shipping_disabled: publicState.shipping_disabled,
308
- ship_authorized: publicState.ship_authorized,
309
- merge_ready: publicState.merge_ready,
310
- sync_allowed: publicState.sync_allowed,
323
+ ship_held: publicState?.ship_held,
324
+ shipping_disabled: publicState?.shipping_disabled,
325
+ ship_authorized: changeReceiptResult ? parsedChangeReceipt?.shipping_authorization.authorized : publicState?.ship_authorized,
326
+ merge_ready: publicState?.merge_ready,
327
+ sync_allowed: publicState?.sync_allowed,
311
328
  proof_decision: firstStringValue(result.proof_decision, stopCondition.proof_decision, resultRaw.proof_decision),
312
- merge_recommendation: changeReceiptResult && resultStatus ? stringValue(result.verdict) : mergeRecommendation,
329
+ merge_recommendation: changeReceiptResult ? parsedChangeReceipt?.recommendation.label : mergeRecommendation,
313
330
  checkpoint_summary: checkpointSummary,
314
331
  public_state: publicState,
315
332
  passed_checks: changeChecks?.passed ?? profileChecks?.passed ?? nestedChecks.passed,
@@ -377,7 +394,88 @@ function checkpointSummaryLine(summary) {
377
394
  summary.latest_decision ? `latest decision \`${summary.latest_decision}\`` : ""
378
395
  ].filter(Boolean).join("; ");
379
396
  }
397
+ function markdownTableValue(value) {
398
+ return String(value ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
399
+ }
400
+ function observationArtifactTarget(artifact) {
401
+ return artifact?.url || artifact?.path;
402
+ }
403
+ function canonicalScreenshotCell(artifact, fallback) {
404
+ const target = observationArtifactTarget(artifact);
405
+ if (!artifact || !target) return fallback;
406
+ if (artifact.url) return `![${artifact.name.replace(/\]/g, "\\]")}](${artifact.url})`;
407
+ return markdownTableValue(artifact.path || artifact.name);
408
+ }
409
+ function shortRevision(value) {
410
+ return value ? value.slice(0, 12) : "not recorded";
411
+ }
412
+ function buildRiddleProofHandoffPrCommentMarkdown(handoff, input = {}) {
413
+ const receipt = parseRiddleProofHandoffReceipt(handoff);
414
+ const change = receipt.change;
415
+ const beforeStatus = change.before.profile_summary?.status || "not recorded";
416
+ const afterStatus = change.after.profile_summary?.status || "not recorded";
417
+ const beforeBaseline = change.groups.before.ok ? "matched baseline" : "baseline mismatch";
418
+ const lines = [
419
+ RIDDLE_PROOF_PR_COMMENT_MARKER,
420
+ `## ${input.title?.trim() || "Riddle Change Proof"}`,
421
+ "",
422
+ `**Result:** ${receipt.recommendation.label}`,
423
+ `**Evidence verdict:** \`${receipt.verdict}\``,
424
+ `**Shipping authorization:** ${receipt.shipping_authorization.status}`,
425
+ `**Contract:** ${change.contract_name}`
426
+ ];
427
+ if (input.goal?.trim()) lines.push(`**Goal:** ${input.goal.trim()}`);
428
+ if (input.successCriteria?.trim()) lines.push(`**Success criteria:** ${input.successCriteria.trim()}`);
429
+ lines.push("", change.summary, "", "### Canonical Before / After", "");
430
+ lines.push("| Before | After |");
431
+ lines.push("| --- | --- |");
432
+ lines.push(`| ${beforeBaseline}: \`${markdownTableValue(beforeStatus)}\` | \`${markdownTableValue(afterStatus)}\` |`);
433
+ lines.push(`| ${canonicalScreenshotCell(receipt.canonical_pair.before, "No canonical before screenshot")} | ${canonicalScreenshotCell(receipt.canonical_pair.after, "No canonical after screenshot")} |`);
434
+ lines.push("", "### Source Binding", "");
435
+ lines.push("| Side | Target | Git revision | Binding | Preview | Digest |");
436
+ lines.push("| --- | --- | --- | --- | --- | --- |");
437
+ for (const side of ["before", "after"]) {
438
+ const observation = change[side];
439
+ const binding = change.source_bindings[side];
440
+ 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")}\` |`);
441
+ }
442
+ lines.push("", "### Declared Delta", "");
443
+ lines.push("| Measurement | Status | Before | After |");
444
+ lines.push("| --- | --- | --- | --- |");
445
+ for (const delta of change.deltas) {
446
+ lines.push(`| ${markdownTableValue(delta.label)} | ${delta.status} | ${markdownTableValue(delta.before_observed)} | ${markdownTableValue(delta.after_observed)} |`);
447
+ }
448
+ if (change.proves.length) {
449
+ lines.push("", "### What This Proves", "");
450
+ for (const claim of change.proves) lines.push(`- ${claim}`);
451
+ }
452
+ if (change.does_not_prove.length) {
453
+ lines.push("", "### Limits", "");
454
+ for (const claim of change.does_not_prove) lines.push(`- ${claim}`);
455
+ }
456
+ const linkedArtifacts = [
457
+ ...change.before.artifacts.map((artifact) => ({ side: "before", artifact })),
458
+ ...change.after.artifacts.map((artifact) => ({ side: "after", artifact }))
459
+ ].filter(({ artifact }) => Boolean(artifact.url));
460
+ if (linkedArtifacts.length) {
461
+ lines.push("", "### Artifacts", "");
462
+ for (const { side, artifact } of linkedArtifacts.slice(0, 24)) {
463
+ lines.push(`- ${side}: ${markdownLink(artifact.name, artifact.url || "")}`);
464
+ }
465
+ }
466
+ lines.push("", input.source?.trim() ? `_Source: ${input.source.trim()}_` : "_Rendered from the Handoff receipt without recomputing its verdict._");
467
+ return `${lines.join("\n").trim()}
468
+ `;
469
+ }
380
470
  function buildRiddleProofPrCommentMarkdown(input) {
471
+ const result = asRecord(input.result);
472
+ if (isHandoffReceiptResult(result)) {
473
+ return buildRiddleProofHandoffPrCommentMarkdown(parseRiddleProofHandoffReceipt(result), input);
474
+ }
475
+ if (isChangeReceiptResult(result)) {
476
+ const handoff = createRiddleProofHandoffReceipt(parseRiddleProofChangeReceipt(result));
477
+ return buildRiddleProofHandoffPrCommentMarkdown(handoff, input);
478
+ }
381
479
  const summary = summarizeRiddleProofPrComment(input);
382
480
  const title = input.title?.trim() || "Riddle Proof Evidence";
383
481
  const lines = [
@@ -448,5 +546,6 @@ function buildRiddleProofPrCommentMarkdown(input) {
448
546
  export {
449
547
  RIDDLE_PROOF_PR_COMMENT_MARKER,
450
548
  summarizeRiddleProofPrComment,
549
+ buildRiddleProofHandoffPrCommentMarkdown,
451
550
  buildRiddleProofPrCommentMarkdown
452
551
  };
@@ -781,6 +781,12 @@ function profileScreenshotLabels(viewports) {
781
781
  }
782
782
  return labels;
783
783
  }
784
+ function profileFinalScreenshotLabels(viewports) {
785
+ return (viewports || []).map((viewport) => viewport.screenshot_label).filter((label) => typeof label === "string" && Boolean(label.trim()));
786
+ }
787
+ function profileAllSetupScreenshotLabels(viewports) {
788
+ return (viewports || []).flatMap((viewport) => profileSetupScreenshotLabels(viewport.setup_action_results || []));
789
+ }
784
790
  function profileSetupSummary(viewports, actionCount, expectedActionCountByViewport, finalScreenshotFullPage) {
785
791
  const normalizedFinalScreenshotFullPage = finalScreenshotFullPage === void 0 ? void 0 : finalScreenshotFullPage !== false;
786
792
  const finalScreenshotCount = viewports.filter((viewport) => typeof viewport.screenshot_label === "string" && viewport.screenshot_label.trim()).length;
@@ -3631,6 +3637,8 @@ function assessRiddleProofProfileEvidence(profile, evidence, options = {}) {
3631
3637
  const status = profileStatusFromEvidence(profile, evidence, checks);
3632
3638
  const firstViewport = evidence?.viewports?.[0];
3633
3639
  const screenshots = profileScreenshotLabels(evidence?.viewports);
3640
+ const canonicalScreenshots = profileFinalScreenshotLabels(evidence?.viewports);
3641
+ const setupScreenshots = profileAllSetupScreenshotLabels(evidence?.viewports);
3634
3642
  const result = {
3635
3643
  version: RIDDLE_PROOF_PROFILE_RESULT_VERSION,
3636
3644
  profile_name: profile.name,
@@ -3640,6 +3648,8 @@ function assessRiddleProofProfileEvidence(profile, evidence, options = {}) {
3640
3648
  route: routeForViewport(firstViewport, evidence?.target_url),
3641
3649
  artifacts: {
3642
3650
  screenshots,
3651
+ canonical_screenshots: canonicalScreenshots,
3652
+ setup_screenshots: setupScreenshots,
3643
3653
  console: "console.json",
3644
3654
  proof_json: "proof.json",
3645
3655
  dom_summary: "dom-summary.json",
@@ -4958,6 +4968,12 @@ function profileScreenshotLabels(viewports) {
4958
4968
  }
4959
4969
  return labels;
4960
4970
  }
4971
+ function profileFinalScreenshotLabels(viewports) {
4972
+ return (viewports || []).map((viewport) => viewport && viewport.screenshot_label).filter(Boolean);
4973
+ }
4974
+ function profileAllSetupScreenshotLabels(viewports) {
4975
+ return (viewports || []).flatMap((viewport) => profileSetupScreenshotLabels(viewport && viewport.setup_action_results || []));
4976
+ }
4961
4977
  function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewport, finalScreenshotFullPage) {
4962
4978
  const normalizedFinalScreenshotFullPage = finalScreenshotFullPage === undefined
4963
4979
  ? undefined
@@ -5826,6 +5842,8 @@ function assessProfile(profile, evidence) {
5826
5842
  else if (checks.some((check) => check.status === "needs_human_review")) status = "needs_human_review";
5827
5843
  else if (checks.some((check) => check.status === "failed")) status = "product_regression";
5828
5844
  const screenshotLabels = profileScreenshotLabels(viewports);
5845
+ const canonicalScreenshotLabels = profileFinalScreenshotLabels(viewports);
5846
+ const setupScreenshotLabels = profileAllSetupScreenshotLabels(viewports);
5829
5847
  const route = viewports[0] && viewports[0].route ? viewports[0].route : { requested: evidence.target_url, observed: "", matched: false, error: "missing viewport evidence" };
5830
5848
  const passedChecks = checks.filter((check) => check.status === "passed").length;
5831
5849
  const failedChecks = checks.filter((check) => check.status === "failed").length;
@@ -5842,7 +5860,14 @@ function assessProfile(profile, evidence) {
5842
5860
  status,
5843
5861
  baseline_policy: profile.baseline_policy || "invariant_only",
5844
5862
  route,
5845
- artifacts: { screenshots: screenshotLabels, console: "console.json", proof_json: "proof.json", dom_summary: "dom-summary.json" },
5863
+ artifacts: {
5864
+ screenshots: screenshotLabels,
5865
+ canonical_screenshots: canonicalScreenshotLabels,
5866
+ setup_screenshots: setupScreenshotLabels,
5867
+ console: "console.json",
5868
+ proof_json: "proof.json",
5869
+ dom_summary: "dom-summary.json"
5870
+ },
5846
5871
  checks,
5847
5872
  summary,
5848
5873
  captured_at: evidence.captured_at,
@@ -0,0 +1,258 @@
1
+ // src/receipts.ts
2
+ var RIDDLE_PREVIEW_RECEIPT_VERSION = "riddle.preview-receipt.v1";
3
+ var RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION = "riddle-proof.observation-receipt.v1";
4
+ function isRecord(value) {
5
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+ function requiredString(record, key, context) {
8
+ const value = record[key];
9
+ if (typeof value !== "string" || !value.trim()) {
10
+ throw new Error(`${context}.${key} must be a non-empty string.`);
11
+ }
12
+ return value.trim();
13
+ }
14
+ function normalizedSourceIdentity(value) {
15
+ if (!isRecord(value)) return {};
16
+ return {
17
+ git_revision: typeof value.git_revision === "string" && value.git_revision.trim() ? value.git_revision.trim() : void 0,
18
+ repository: typeof value.repository === "string" && value.repository.trim() ? value.repository.trim() : void 0,
19
+ dirty: typeof value.dirty === "boolean" ? value.dirty : void 0,
20
+ label: typeof value.label === "string" && value.label.trim() ? value.label.trim() : void 0
21
+ };
22
+ }
23
+ function parseRiddlePreviewReceipt(value) {
24
+ if (!isRecord(value)) throw new Error("Preview receipt must be an object.");
25
+ if (value.version !== RIDDLE_PREVIEW_RECEIPT_VERSION) {
26
+ throw new Error(`Unsupported Preview receipt version ${String(value.version || "missing")}.`);
27
+ }
28
+ if (!isRecord(value.source)) throw new Error("Preview receipt source must be an object.");
29
+ const expiresAt = requiredString(value, "expires_at", "preview receipt");
30
+ const publishedAt = requiredString(value, "published_at", "preview receipt");
31
+ const contentDigest = requiredString(value, "content_digest", "preview receipt");
32
+ if (!contentDigest.startsWith("sha256:") || contentDigest.length <= "sha256:".length) {
33
+ throw new Error("preview receipt.content_digest must be a sha256 digest.");
34
+ }
35
+ if (!Number.isFinite(Date.parse(expiresAt)) || !Number.isFinite(Date.parse(publishedAt))) {
36
+ throw new Error("Preview receipt timestamps must be valid ISO date strings.");
37
+ }
38
+ return {
39
+ version: RIDDLE_PREVIEW_RECEIPT_VERSION,
40
+ preview_id: requiredString(value, "preview_id", "preview receipt"),
41
+ url: requiredString(value, "url", "preview receipt"),
42
+ expires_at: expiresAt,
43
+ content_digest: contentDigest,
44
+ source: normalizedSourceIdentity(value.source),
45
+ published_at: publishedAt
46
+ };
47
+ }
48
+ function comparableArtifactName(value) {
49
+ return (value || "").split(/[/?#]/).pop()?.toLowerCase().replace(/\.(png|jpe?g|gif|webp|avif|svg)$/u, "").replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/g, "") || "";
50
+ }
51
+ function artifactMatchesLabel(artifact, label) {
52
+ const expected = comparableArtifactName(label);
53
+ if (!expected) return false;
54
+ return [artifact.name, artifact.url, artifact.path].map(comparableArtifactName).some((name) => name === expected || name.endsWith(`-${expected}`));
55
+ }
56
+ function artifactLooksLikeScreenshot(ref) {
57
+ const text = [ref.name, ref.url, ref.path, ref.kind, ref.content_type].filter(Boolean).join(" ").toLowerCase();
58
+ return /screenshot|\bimage\b/.test(text) || /\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/.test(text);
59
+ }
60
+ function finalScreenshotLabels(result) {
61
+ const explicit = result.artifacts.canonical_screenshots || [];
62
+ if (explicit.length) return explicit;
63
+ const fromEvidence = (result.evidence?.viewports || []).map((viewport) => viewport.screenshot_label).filter((label) => typeof label === "string" && Boolean(label.trim()));
64
+ if (fromEvidence.length) return fromEvidence;
65
+ return (result.artifacts.screenshots || []).slice(0, 1);
66
+ }
67
+ function setupScreenshotLabels(result) {
68
+ const explicit = result.artifacts.setup_screenshots || [];
69
+ if (explicit.length) return explicit;
70
+ return (result.evidence?.viewports || []).flatMap(
71
+ (viewport) => (viewport.setup_action_results || []).filter((action) => action.action === "screenshot" && action.ok !== false && typeof action.screenshot_label === "string").map((action) => String(action.screenshot_label))
72
+ );
73
+ }
74
+ function observationArtifactFromRef(ref, canonicalLabels, setupLabels) {
75
+ const base = {
76
+ name: ref.name,
77
+ role: ref.role || "artifact",
78
+ url: ref.url,
79
+ path: ref.path,
80
+ kind: ref.kind,
81
+ content_type: ref.content_type,
82
+ source: ref.source
83
+ };
84
+ if (!artifactLooksLikeScreenshot(ref)) return base;
85
+ if (canonicalLabels.some((label) => artifactMatchesLabel(base, label))) {
86
+ return { ...base, role: "canonical_screenshot" };
87
+ }
88
+ if (setupLabels.some((label) => artifactMatchesLabel(base, label))) {
89
+ return { ...base, role: "setup_screenshot" };
90
+ }
91
+ return { ...base, role: "diagnostic" };
92
+ }
93
+ function profileObservationArtifacts(result) {
94
+ const canonicalLabels = finalScreenshotLabels(result);
95
+ const setupLabels = setupScreenshotLabels(result);
96
+ const artifacts = [];
97
+ const seen = /* @__PURE__ */ new Set();
98
+ const add = (artifact) => {
99
+ const key = artifact.url || artifact.path || `${artifact.role}:${artifact.name}`;
100
+ if (!artifact.name || seen.has(key)) return;
101
+ seen.add(key);
102
+ artifacts.push(artifact);
103
+ };
104
+ for (const ref of result.artifacts.riddle_artifacts || []) {
105
+ add(observationArtifactFromRef(ref, canonicalLabels, setupLabels));
106
+ }
107
+ for (const label of canonicalLabels) {
108
+ if (!artifacts.some((artifact) => artifactMatchesLabel(artifact, label))) {
109
+ add({ name: label, path: label, role: "canonical_screenshot", kind: "image" });
110
+ }
111
+ }
112
+ for (const label of setupLabels) {
113
+ if (!artifacts.some((artifact) => artifactMatchesLabel(artifact, label))) {
114
+ add({ name: label, path: label, role: "setup_screenshot", kind: "image" });
115
+ }
116
+ }
117
+ for (const label of result.artifacts.screenshots || []) {
118
+ if (!artifacts.some((artifact) => artifactMatchesLabel(artifact, label))) {
119
+ add({ name: label, path: label, role: "diagnostic", kind: "image" });
120
+ }
121
+ }
122
+ return artifacts;
123
+ }
124
+ function mergeObservationArtifacts(profileArtifacts, suppliedArtifacts) {
125
+ const artifacts = [...profileArtifacts];
126
+ for (const supplied of suppliedArtifacts) {
127
+ const suppliedNames = [supplied.name, supplied.url, supplied.path].map(comparableArtifactName).filter(Boolean);
128
+ const existingIndex = artifacts.findIndex((artifact) => {
129
+ if (supplied.url && artifact.url === supplied.url) return true;
130
+ if (supplied.path && artifact.path === supplied.path) return true;
131
+ return [artifact.name, artifact.url, artifact.path].map(comparableArtifactName).some((name) => name && suppliedNames.includes(name));
132
+ });
133
+ if (existingIndex < 0) {
134
+ artifacts.push(supplied);
135
+ continue;
136
+ }
137
+ const existing = artifacts[existingIndex];
138
+ 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;
139
+ artifacts[existingIndex] = {
140
+ ...existing,
141
+ ...supplied,
142
+ role
143
+ };
144
+ }
145
+ return artifacts;
146
+ }
147
+ function defaultObservationId(role, capturedAt) {
148
+ return `obs_${role}_${capturedAt.replace(/[^0-9]/g, "").slice(0, 17) || "unknown"}`;
149
+ }
150
+ function profileCheckCounts(result) {
151
+ const counts = { total: result.checks.length, passed: 0, failed: 0, skipped: 0, needs_human_review: 0 };
152
+ for (const check of result.checks) {
153
+ if (check.status === "passed") counts.passed += 1;
154
+ if (check.status === "failed") counts.failed += 1;
155
+ if (check.status === "skipped") counts.skipped += 1;
156
+ if (check.status === "needs_human_review") counts.needs_human_review += 1;
157
+ }
158
+ return counts;
159
+ }
160
+ function createRiddleProofObservationReceipt(input) {
161
+ const capturedAt = input.captured_at || input.profile_result?.captured_at || (/* @__PURE__ */ new Date()).toISOString();
162
+ const profileArtifacts = input.profile_result ? profileObservationArtifacts(input.profile_result) : [];
163
+ const requestedCanonical = input.canonical_screenshot ? { ...input.canonical_screenshot, role: "canonical_screenshot" } : void 0;
164
+ const artifacts = mergeObservationArtifacts(
165
+ profileArtifacts,
166
+ [...input.artifacts || [], ...requestedCanonical ? [requestedCanonical] : []]
167
+ );
168
+ const canonicalScreenshot = requestedCanonical ? artifacts.find((artifact) => artifact.role === "canonical_screenshot" && artifactMatchesLabel(artifact, requestedCanonical.name)) || requestedCanonical : artifacts.find((artifact) => artifact.role === "canonical_screenshot");
169
+ const target = input.target.kind === "preview" && input.target.preview ? { ...input.target, preview: parseRiddlePreviewReceipt(input.target.preview) } : input.target;
170
+ return {
171
+ version: RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION,
172
+ observation_id: input.observation_id || defaultObservationId(input.comparison_role, capturedAt),
173
+ comparison_role: input.comparison_role,
174
+ executor: input.executor,
175
+ target,
176
+ source: normalizedSourceIdentity(input.source || input.target.preview?.source),
177
+ captured_at: capturedAt,
178
+ artifacts,
179
+ canonical_screenshot: canonicalScreenshot,
180
+ profile_summary: input.profile_result ? {
181
+ profile_name: input.profile_result.profile_name,
182
+ status: input.profile_result.status,
183
+ summary: input.profile_result.summary,
184
+ route: input.profile_result.route,
185
+ checks: profileCheckCounts(input.profile_result)
186
+ } : void 0,
187
+ proof: input.profile_result ? {
188
+ profile_name: input.profile_result.profile_name,
189
+ result: input.profile_result
190
+ } : void 0,
191
+ publication: input.publication,
192
+ execution: input.execution,
193
+ metadata: input.metadata
194
+ };
195
+ }
196
+ function parseRiddleProofObservationReceipt(value) {
197
+ if (!isRecord(value)) throw new Error("Observation receipt must be an object.");
198
+ if (value.version !== RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION) {
199
+ throw new Error(`Unsupported Observation receipt version ${String(value.version || "missing")}.`);
200
+ }
201
+ if (!isRecord(value.executor)) throw new Error("Observation receipt executor must be an object.");
202
+ if (!isRecord(value.target)) throw new Error("Observation receipt target must be an object.");
203
+ if (!isRecord(value.source)) throw new Error("Observation receipt source must be an object.");
204
+ requiredString(value, "observation_id", "observation receipt");
205
+ requiredString(value, "captured_at", "observation receipt");
206
+ const executorKind = requiredString(value.executor, "kind", "observation receipt executor");
207
+ if (!"local_playwright,riddle_hosted,browser_api,other".split(",").includes(executorKind)) {
208
+ throw new Error(`Unsupported observation executor kind ${executorKind}.`);
209
+ }
210
+ if (value.executor.runner !== void 0) {
211
+ requiredString(value.executor, "runner", "observation receipt executor");
212
+ }
213
+ const role = requiredString(value, "comparison_role", "observation receipt");
214
+ if (!["before", "after", "standalone"].includes(role)) {
215
+ throw new Error(`Unsupported observation comparison role ${role}.`);
216
+ }
217
+ const targetKind = requiredString(value.target, "kind", "observation receipt target");
218
+ if (targetKind !== "url" && targetKind !== "preview") {
219
+ throw new Error(`Unsupported observation target kind ${targetKind}.`);
220
+ }
221
+ requiredString(value.target, "url", "observation receipt target");
222
+ if (targetKind === "preview") {
223
+ if (value.target.preview === void 0) {
224
+ throw new Error("Preview Observation target must include its Preview receipt.");
225
+ }
226
+ parseRiddlePreviewReceipt(value.target.preview);
227
+ }
228
+ if (!Array.isArray(value.artifacts)) throw new Error("Observation receipt artifacts must be an array.");
229
+ const artifactRoles = /* @__PURE__ */ new Set(["canonical_screenshot", "setup_screenshot", "data", "diagnostic", "artifact"]);
230
+ const artifacts = value.artifacts.map((artifact, index) => {
231
+ if (!isRecord(artifact)) throw new Error(`Observation receipt artifact ${index} must be an object.`);
232
+ requiredString(artifact, "name", `observation receipt artifact ${index}`);
233
+ const artifactRole = requiredString(artifact, "role", `observation receipt artifact ${index}`);
234
+ if (!artifactRoles.has(artifactRole)) {
235
+ throw new Error(`Unsupported observation artifact role ${artifactRole}.`);
236
+ }
237
+ return artifact;
238
+ });
239
+ if (value.canonical_screenshot !== void 0) {
240
+ if (!isRecord(value.canonical_screenshot) || value.canonical_screenshot.role !== "canonical_screenshot") {
241
+ throw new Error("Observation canonical_screenshot must have the canonical_screenshot role.");
242
+ }
243
+ const canonical = value.canonical_screenshot;
244
+ const included = artifacts.some((artifact) => artifact.role === "canonical_screenshot" && artifact.name === canonical.name && artifact.url === canonical.url && artifact.path === canonical.path);
245
+ if (!included) {
246
+ throw new Error("Observation canonical_screenshot must reference an artifact in the receipt.");
247
+ }
248
+ }
249
+ return value;
250
+ }
251
+
252
+ export {
253
+ RIDDLE_PREVIEW_RECEIPT_VERSION,
254
+ RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION,
255
+ parseRiddlePreviewReceipt,
256
+ createRiddleProofObservationReceipt,
257
+ parseRiddleProofObservationReceipt
258
+ };