@riddledc/riddle-proof 0.5.42 → 0.5.43

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.
@@ -96,14 +96,16 @@ function summarizeCaptureArtifacts(payload) {
96
96
  const record = isRecord(payload) ? payload : {};
97
97
  const artifactJson = isRecord(record._artifact_json) ? record._artifact_json : {};
98
98
  const proofJson = isRecord(record._proof_json) ? record._proof_json : isRecord(artifactJson["proof.json"]) ? artifactJson["proof.json"] : {};
99
+ const visualDiffJson = isRecord(record.visual_diff) ? record.visual_diff : isRecord(record.visualDiff) ? record.visualDiff : isRecord(artifactJson["visual-diff.json"]) ? artifactJson["visual-diff.json"] : {};
99
100
  const consoleJson = isRecord(record.console) ? record.console : isRecord(artifactJson["console.json"]) ? artifactJson["console.json"] : {};
100
101
  const result = isRecord(record.result) ? record.result : isRecord(proofJson.result) ? proofJson.result : isRecord(proofJson.script_result) ? proofJson.script_result : isRecord(proofJson.return_value) ? proofJson.return_value : isRecord(proofJson.value) ? proofJson.value : {};
101
102
  const consoleSummary = isRecord(consoleJson.summary) ? redactForProofDiagnostics(consoleJson.summary, { string_limit: 500 }) : void 0;
103
+ const resultKeys = /* @__PURE__ */ new Set([...sortedKeys(result), ...sortedKeys(visualDiffJson)]);
102
104
  return {
103
105
  outputs: artifactItems(record.outputs).slice(0, 20).map((item) => artifactSummary(item, "outputs")),
104
106
  screenshots: artifactItems(record.screenshots).slice(0, 10).map((item) => artifactSummary(item, "screenshots")),
105
107
  artifacts: artifactItems(record.artifacts).slice(0, 20).map((item) => artifactSummary(item, "artifacts")),
106
- result_keys: sortedKeys(result),
108
+ result_keys: Array.from(resultKeys).sort(),
107
109
  artifact_json: sortedKeys(artifactJson),
108
110
  artifact_errors: artifactErrorMap(record._artifact_errors),
109
111
  proof_script_error: Boolean(proofJson.script_error),
@@ -127,14 +127,16 @@ function summarizeCaptureArtifacts(payload) {
127
127
  const record = isRecord(payload) ? payload : {};
128
128
  const artifactJson = isRecord(record._artifact_json) ? record._artifact_json : {};
129
129
  const proofJson = isRecord(record._proof_json) ? record._proof_json : isRecord(artifactJson["proof.json"]) ? artifactJson["proof.json"] : {};
130
+ const visualDiffJson = isRecord(record.visual_diff) ? record.visual_diff : isRecord(record.visualDiff) ? record.visualDiff : isRecord(artifactJson["visual-diff.json"]) ? artifactJson["visual-diff.json"] : {};
130
131
  const consoleJson = isRecord(record.console) ? record.console : isRecord(artifactJson["console.json"]) ? artifactJson["console.json"] : {};
131
132
  const result = isRecord(record.result) ? record.result : isRecord(proofJson.result) ? proofJson.result : isRecord(proofJson.script_result) ? proofJson.script_result : isRecord(proofJson.return_value) ? proofJson.return_value : isRecord(proofJson.value) ? proofJson.value : {};
132
133
  const consoleSummary = isRecord(consoleJson.summary) ? redactForProofDiagnostics(consoleJson.summary, { string_limit: 500 }) : void 0;
134
+ const resultKeys = /* @__PURE__ */ new Set([...sortedKeys(result), ...sortedKeys(visualDiffJson)]);
133
135
  return {
134
136
  outputs: artifactItems(record.outputs).slice(0, 20).map((item) => artifactSummary(item, "outputs")),
135
137
  screenshots: artifactItems(record.screenshots).slice(0, 10).map((item) => artifactSummary(item, "screenshots")),
136
138
  artifacts: artifactItems(record.artifacts).slice(0, 20).map((item) => artifactSummary(item, "artifacts")),
137
- result_keys: sortedKeys(result),
139
+ result_keys: Array.from(resultKeys).sort(),
138
140
  artifact_json: sortedKeys(artifactJson),
139
141
  artifact_errors: artifactErrorMap(record._artifact_errors),
140
142
  proof_script_error: Boolean(proofJson.script_error),
@@ -7,7 +7,7 @@ import {
7
7
  createCaptureDiagnostic,
8
8
  redactForProofDiagnostics,
9
9
  summarizeCaptureArtifacts
10
- } from "./chunk-DWQK7J4U.js";
10
+ } from "./chunk-XNRMMQAY.js";
11
11
  export {
12
12
  DEFAULT_DIAGNOSTIC_ARRAY_LIMIT,
13
13
  DEFAULT_DIAGNOSTIC_HISTORY_LIMIT,
package/dist/index.cjs CHANGED
@@ -5172,14 +5172,16 @@ function summarizeCaptureArtifacts(payload) {
5172
5172
  const record = isRecord(payload) ? payload : {};
5173
5173
  const artifactJson = isRecord(record._artifact_json) ? record._artifact_json : {};
5174
5174
  const proofJson = isRecord(record._proof_json) ? record._proof_json : isRecord(artifactJson["proof.json"]) ? artifactJson["proof.json"] : {};
5175
+ const visualDiffJson = isRecord(record.visual_diff) ? record.visual_diff : isRecord(record.visualDiff) ? record.visualDiff : isRecord(artifactJson["visual-diff.json"]) ? artifactJson["visual-diff.json"] : {};
5175
5176
  const consoleJson = isRecord(record.console) ? record.console : isRecord(artifactJson["console.json"]) ? artifactJson["console.json"] : {};
5176
5177
  const result = isRecord(record.result) ? record.result : isRecord(proofJson.result) ? proofJson.result : isRecord(proofJson.script_result) ? proofJson.script_result : isRecord(proofJson.return_value) ? proofJson.return_value : isRecord(proofJson.value) ? proofJson.value : {};
5177
5178
  const consoleSummary = isRecord(consoleJson.summary) ? redactForProofDiagnostics(consoleJson.summary, { string_limit: 500 }) : void 0;
5179
+ const resultKeys = /* @__PURE__ */ new Set([...sortedKeys(result), ...sortedKeys(visualDiffJson)]);
5178
5180
  return {
5179
5181
  outputs: artifactItems(record.outputs).slice(0, 20).map((item) => artifactSummary(item, "outputs")),
5180
5182
  screenshots: artifactItems(record.screenshots).slice(0, 10).map((item) => artifactSummary(item, "screenshots")),
5181
5183
  artifacts: artifactItems(record.artifacts).slice(0, 20).map((item) => artifactSummary(item, "artifacts")),
5182
- result_keys: sortedKeys(result),
5184
+ result_keys: Array.from(resultKeys).sort(),
5183
5185
  artifact_json: sortedKeys(artifactJson),
5184
5186
  artifact_errors: artifactErrorMap(record._artifact_errors),
5185
5187
  proof_script_error: Boolean(proofJson.script_error),
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  createCaptureDiagnostic,
21
21
  redactForProofDiagnostics,
22
22
  summarizeCaptureArtifacts
23
- } from "./chunk-DWQK7J4U.js";
23
+ } from "./chunk-XNRMMQAY.js";
24
24
  import {
25
25
  createDisabledRiddleProofAgentAdapter,
26
26
  readRiddleProofRunStatus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.5.42",
3
+ "version": "0.5.43",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -154,6 +154,7 @@ def author_request_payload(state, reference, baselines, current_plan, hypothesis
154
154
  'For playable/gameplay proof, start the experience, send keyboard or pointer input, sample state before/after, measure non-HUD playfield/canvas pixel deltas across time, and set window.__riddleProofEvidence.playability or playability_evidence with version riddle-proof.playability.v1.',
155
155
  'For data/audio/log/metric/custom proof, screenshots are optional; set window.__riddleProofEvidence inside page.evaluate to a JSON-serializable object with the measured observations the verifier should judge.',
156
156
  'Do not assign globalThis.__riddleProofEvidence, window.__riddleProofEvidence, or self.__riddleProofEvidence outside page.evaluate; the Riddle worker context may not expose those globals safely.',
157
+ 'When page.evaluate needs data from the outer Playwright script, pass exactly one serializable argument object; do not pass multiple positional arguments.',
157
158
  'Do not begin capture_script with page.goto unless an in-app navigation is genuinely required after the preview opens the target route.',
158
159
  'Only escalate to the human after the supervising agent concludes the workflow is genuinely stuck or not converging.',
159
160
  ],
@@ -136,6 +136,28 @@ async function run(tool, args) {
136
136
  });
137
137
  }
138
138
 
139
+ if (tool === "riddle_visual_diff") {
140
+ const {
141
+ async,
142
+ options,
143
+ stealth,
144
+ timeout_sec,
145
+ ...visualDiffOptions
146
+ } = args;
147
+ const script = `return await visualDiff(${JSON.stringify(visualDiffOptions)});`;
148
+ return core.runWithDefaults(config, {
149
+ url: args.url_before,
150
+ script,
151
+ options: { ...(options ?? {}), returnResult: true },
152
+ stealth,
153
+ timeout_sec: timeout_sec ?? 60,
154
+ async,
155
+ }, {
156
+ include: ["result", "console", "visual_diff"],
157
+ returnAsync: !!async,
158
+ });
159
+ }
160
+
139
161
  if (tool === "riddle_run") {
140
162
  return core.runWithDefaults(config, args.payload ?? args, {
141
163
  include: ["screenshot", "console", "result"],
@@ -14,6 +14,7 @@ RIDDLE_DIRECT_TOOLS = {
14
14
  'riddle_build_preview',
15
15
  'riddle_build_preview_status',
16
16
  'riddle_script',
17
+ 'riddle_visual_diff',
17
18
  'riddle_run',
18
19
  }
19
20
  CAPTURE_ARTIFACT_JSON_LIMIT = 256 * 1024
@@ -767,7 +768,7 @@ def enrich_capture_payload(payload):
767
768
  artifact_json = dict(enriched.get('_artifact_json') or {})
768
769
  artifact_errors = dict(enriched.get('_artifact_errors') or {})
769
770
 
770
- for name in ('console.json', 'proof.json'):
771
+ for name in ('console.json', 'proof.json', 'visual-diff.json'):
771
772
  if name in artifact_json or name in artifact_errors:
772
773
  continue
773
774
  item = capture_output_item(enriched, name)
@@ -798,6 +799,10 @@ def enrich_capture_payload(payload):
798
799
  enriched['result'] = result
799
800
  break
800
801
 
802
+ visual_diff_json = artifact_json.get('visual-diff.json')
803
+ if isinstance(visual_diff_json, dict) and not enriched.get('visual_diff'):
804
+ enriched['visual_diff'] = visual_diff_json
805
+
801
806
  return enriched
802
807
 
803
808
 
@@ -55,13 +55,24 @@ MIN_VISUAL_CHANGED_PIXELS = 5000
55
55
  VISUAL_DELTA_PERCENT_KEYS = {
56
56
  'change_pct', 'change_percent', 'changed_percent', 'percent_changed',
57
57
  'diff_percent', 'visual_delta_percent', 'pixel_change_percent',
58
+ 'changepercent', 'changedpercent', 'percentchanged', 'diffpercent',
59
+ 'visualdeltapercent', 'pixelchangepercent', 'difference_percent',
60
+ 'differencepercent', 'diff_percentage', 'diffpercentage',
61
+ 'mismatch_percent', 'mismatchpercent', 'percentage', 'percent',
58
62
  }
59
63
  VISUAL_DELTA_RATIO_KEYS = {
60
64
  'change_ratio', 'changed_ratio', 'diff_ratio', 'visual_delta_ratio',
65
+ 'changeratio', 'changedratio', 'diffratio', 'visualdeltaratio',
66
+ 'difference_ratio', 'differenceratio', 'mismatch_ratio', 'mismatchratio',
61
67
  }
62
68
  VISUAL_CHANGED_PIXEL_KEYS = {
63
69
  'changed_pixels', 'changed_pixel_count', 'changedpixels',
64
70
  'diff_pixels', 'pixel_delta', 'visual_delta_pixels',
71
+ 'diffpixels', 'pixeldelta', 'visualdeltapixels', 'different_pixels',
72
+ 'differentpixels', 'mismatch_pixels', 'mismatchpixels', 'pixels_changed',
73
+ 'pixelschanged', 'diff_pixel_count', 'diffpixelcount',
74
+ 'pixel_diff_count', 'pixeldiffcount', 'different_pixel_count',
75
+ 'differentpixelcount', 'num_different_pixels', 'numdifferentpixels',
65
76
  }
66
77
  VISUAL_TOTAL_PIXEL_KEYS = {
67
78
  'total_pixels', 'total_pixel_count', 'pixel_count', 'totalpixels',
@@ -800,6 +811,7 @@ def extract_visual_delta(payload):
800
811
  payload = enrich_capture_payload(payload)
801
812
  result = payload.get('result') if isinstance(payload, dict) else {}
802
813
  proof_json = payload.get('_proof_json') if isinstance(payload, dict) else {}
814
+ artifact_json = payload.get('_artifact_json') if isinstance(payload, dict) else {}
803
815
  proof_evidence = extract_proof_evidence(payload)
804
816
  screenshot_url = extract_screenshot_url(payload)
805
817
  artifact_summary = summarize_capture_artifacts(payload)
@@ -807,6 +819,11 @@ def extract_visual_delta(payload):
807
819
  payload if isinstance(payload, dict) else {},
808
820
  result if isinstance(result, dict) else {},
809
821
  proof_json if isinstance(proof_json, dict) else {},
822
+ payload.get('visual_diff') if isinstance(payload, dict) and isinstance(payload.get('visual_diff'), dict) else {},
823
+ payload.get('visualDiff') if isinstance(payload, dict) and isinstance(payload.get('visualDiff'), dict) else {},
824
+ result.get('visual_diff') if isinstance(result, dict) and isinstance(result.get('visual_diff'), dict) else {},
825
+ result.get('visualDiff') if isinstance(result, dict) and isinstance(result.get('visualDiff'), dict) else {},
826
+ artifact_json.get('visual-diff.json') if isinstance(artifact_json, dict) and isinstance(artifact_json.get('visual-diff.json'), dict) else {},
810
827
  proof_evidence,
811
828
  ]
812
829
 
@@ -899,6 +916,114 @@ def extract_visual_delta(payload):
899
916
  }
900
917
 
901
918
 
919
+ def visual_delta_baseline_candidate(state, results):
920
+ reference = str(state.get('requested_reference') or state.get('reference') or 'both').strip().lower()
921
+ baseline = (results.get('baseline') or {}) if isinstance(results, dict) else {}
922
+ candidates = []
923
+ if reference in ('before', 'both', ''):
924
+ candidates.append(('before', (baseline.get('before') or {}).get('url') if isinstance(baseline.get('before'), dict) else ''))
925
+ if reference in ('prod', 'production', 'both'):
926
+ candidates.append(('prod', (baseline.get('prod') or {}).get('url') if isinstance(baseline.get('prod'), dict) else ''))
927
+ candidates.extend([
928
+ ('before', state.get('before_cdn') or ''),
929
+ ('prod', state.get('prod_cdn') or ''),
930
+ ])
931
+ for label, url in candidates:
932
+ text = str(url or '').strip()
933
+ if text:
934
+ return {'label': label, 'url': text}
935
+ return {'label': '', 'url': ''}
936
+
937
+
938
+ def add_visual_delta_diagnostic(visual_delta, key, value):
939
+ updated = dict(visual_delta or {})
940
+ diagnostic = dict(updated.get('diagnostic') or {})
941
+ diagnostic[key] = value
942
+ updated['diagnostic'] = diagnostic
943
+ return updated
944
+
945
+
946
+ def measure_visual_delta_against_baseline(state, results, after_payload, current_visual_delta):
947
+ if not visual_delta_applies(state.get('verification_mode')):
948
+ return current_visual_delta
949
+ if isinstance(current_visual_delta, dict) and current_visual_delta.get('status') == 'measured':
950
+ return current_visual_delta
951
+
952
+ baseline = visual_delta_baseline_candidate(state, results)
953
+ before_url = baseline.get('url') or ''
954
+ after_url = extract_screenshot_url(after_payload, 'after-proof') or str(state.get('after_cdn') or '').strip()
955
+ if not before_url or not after_url:
956
+ missing = []
957
+ if not before_url:
958
+ missing.append('baseline screenshot')
959
+ if not after_url:
960
+ missing.append('after screenshot')
961
+ return add_visual_delta_diagnostic(
962
+ current_visual_delta,
963
+ 'visual_diff_fallback',
964
+ {
965
+ 'status': 'skipped',
966
+ 'reason': 'missing ' + ' and '.join(missing),
967
+ 'baseline_label': baseline.get('label') or '',
968
+ 'before_url_present': bool(before_url),
969
+ 'after_url_present': bool(after_url),
970
+ },
971
+ )
972
+
973
+ args = {
974
+ 'url_before': before_url,
975
+ 'url_after': after_url,
976
+ 'delay_ms': 250,
977
+ 'timeout_sec': 60,
978
+ }
979
+ try:
980
+ payload = invoke_retry('riddle_visual_diff', args, retries=2, timeout=180)
981
+ append_capture_diagnostic(state, 'visual_delta', 'riddle_visual_diff', args, payload)
982
+ except Exception as exc:
983
+ return add_visual_delta_diagnostic(
984
+ current_visual_delta,
985
+ 'visual_diff_fallback',
986
+ {
987
+ 'status': 'error',
988
+ 'error': type(exc).__name__ + ': ' + str(exc)[:300],
989
+ 'baseline_label': baseline.get('label') or '',
990
+ },
991
+ )
992
+
993
+ measured = extract_visual_delta(payload)
994
+ if isinstance(measured, dict) and measured.get('status') == 'measured':
995
+ measured['source'] = 'riddle_visual_diff'
996
+ measured['comparison'] = {
997
+ 'baseline_label': baseline.get('label') or '',
998
+ 'before_url': before_url,
999
+ 'after_url': after_url,
1000
+ }
1001
+ if isinstance(payload, dict):
1002
+ diff_url = extract_screenshot_url(payload, 'visual-diff') or extract_screenshot_url(payload, 'diff')
1003
+ if diff_url:
1004
+ measured['comparison']['diff_url'] = diff_url
1005
+ measured['artifact_summary'] = summarize_capture_artifacts(payload)
1006
+ return measured
1007
+
1008
+ updated = add_visual_delta_diagnostic(
1009
+ current_visual_delta,
1010
+ 'visual_diff_fallback',
1011
+ {
1012
+ 'status': 'unmeasured',
1013
+ 'baseline_label': baseline.get('label') or '',
1014
+ 'before_url': before_url,
1015
+ 'after_url': after_url,
1016
+ 'payload_ok': payload.get('ok') if isinstance(payload, dict) else None,
1017
+ 'payload_error': str(payload.get('error') or payload.get('stderr') or '')[:500] if isinstance(payload, dict) else '',
1018
+ 'artifact_summary': summarize_capture_artifacts(payload) if isinstance(payload, dict) else {},
1019
+ },
1020
+ )
1021
+ reason = str(updated.get('reason') or '').strip()
1022
+ suffix = ' Riddle visual_diff fallback ran but did not publish a recognizable numeric delta.'
1023
+ updated['reason'] = (reason + suffix).strip() if reason else suffix.strip()
1024
+ return updated
1025
+
1026
+
902
1027
  def visual_delta_applies(verification_mode):
903
1028
  return screenshot_required_for_mode(verification_mode)
904
1029
 
@@ -1443,6 +1568,7 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
1443
1568
  if visual_delta_applies(state.get('verification_mode'))
1444
1569
  else {'status': 'not_applicable', 'passed': None, 'reason': 'Verification mode does not require visual delta gating.'}
1445
1570
  )
1571
+ visual_delta = measure_visual_delta_against_baseline(state, results, after_payload, visual_delta)
1446
1572
  semantic_context = build_semantic_context(state, results, after_observation, expected_path)
1447
1573
  artifact_contract = artifact_contract_for_mode(state.get('verification_mode'))
1448
1574
  artifact_production = artifact_production_summary(after_payload, supporting)
@@ -569,6 +569,50 @@ def run_verify_quality_ignores_proof_telemetry_console_text():
569
569
  assert unmeasured_delta['diagnostic']['after_screenshot_present'] is True
570
570
  assert 'After screenshot artifact is present' in unmeasured_delta['reason']
571
571
 
572
+ visual_diff_delta = namespace['extract_visual_delta']({
573
+ 'ok': True,
574
+ 'outputs': [{'name': 'visual-diff.json', 'url': 'https://cdn.example.com/visual-diff.json'}],
575
+ '_artifact_json': {
576
+ 'visual-diff.json': {
577
+ 'changePercent': '1.45',
578
+ 'diffPixelCount': 14094,
579
+ 'totalPixels': 972000,
580
+ },
581
+ },
582
+ })
583
+ assert visual_diff_delta['status'] == 'measured'
584
+ assert visual_diff_delta['passed'] is True
585
+ assert visual_diff_delta['change_percent'] == 1.45
586
+ assert visual_diff_delta['changed_pixels'] == 14094
587
+
588
+ fallback_calls = []
589
+
590
+ def fake_invoke_retry(tool, args, retries=3, timeout=180):
591
+ fallback_calls.append({'tool': tool, 'args': args, 'retries': retries, 'timeout': timeout})
592
+ return {
593
+ 'ok': True,
594
+ 'visual_diff': {
595
+ 'diffPercentage': 0.82,
596
+ 'differentPixels': 7970,
597
+ 'totalPixels': 972000,
598
+ },
599
+ 'outputs': [{'name': 'visual-diff.png', 'url': 'https://cdn.example.com/visual-diff.png'}],
600
+ }
601
+
602
+ namespace['invoke_retry'] = fake_invoke_retry
603
+ namespace['append_capture_diagnostic'] = lambda *args, **kwargs: None
604
+ fallback_delta = namespace['measure_visual_delta_against_baseline'](
605
+ {'verification_mode': 'visual', 'requested_reference': 'before', 'before_cdn': 'https://cdn.example.com/before.png'},
606
+ {'baseline': {'before': {'url': 'https://cdn.example.com/before.png'}}},
607
+ {'ok': True, 'screenshots': [{'name': 'after-proof.png', 'url': 'https://cdn.example.com/after.png'}]},
608
+ unmeasured_delta,
609
+ )
610
+ assert fallback_delta['status'] == 'measured'
611
+ assert fallback_delta['source'] == 'riddle_visual_diff'
612
+ assert fallback_delta['comparison']['before_url'] == 'https://cdn.example.com/before.png'
613
+ assert fallback_delta['comparison']['after_url'] == 'https://cdn.example.com/after.png'
614
+ assert fallback_calls[0]['tool'] == 'riddle_visual_diff'
615
+
572
616
  canvas_payload = {
573
617
  'bodyTextLength': 7,
574
618
  'visibleTextSample': 'Luge',