@riddledc/riddle-proof 0.7.80 → 0.7.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.cts CHANGED
@@ -9,6 +9,20 @@ type RiddleProofVerificationMode = "proof" | "visual" | "interaction" | "playabl
9
9
  type RiddleProofDecision = "ready_to_ship" | "needs_richer_proof" | "revise_capture" | "needs_recon" | "needs_implementation" | (string & {});
10
10
  type RiddleProofStage = "preflight" | "setup" | "recon" | "author" | "implement" | "prove" | "verify" | "ship" | "notify" | (string & {});
11
11
  type RiddleProofArtifactRole = "baseline" | "after_proof" | "incidental" | "diagnostic" | (string & {});
12
+ interface RiddleProofViewportCapture {
13
+ name?: string;
14
+ slug?: string;
15
+ width?: number;
16
+ height?: number;
17
+ screenshot_label?: string;
18
+ screenshot_url?: string;
19
+ }
20
+ interface RiddleProofViewportMatrixStatus {
21
+ status?: "not_requested" | "not_run" | "complete" | "incomplete" | (string & {});
22
+ requested?: RiddleProofViewportCapture[];
23
+ executed?: RiddleProofViewportCapture[];
24
+ missing?: RiddleProofViewportCapture[];
25
+ }
12
26
  interface RiddleProofRunParams {
13
27
  repo?: string;
14
28
  branch?: string;
@@ -201,6 +215,7 @@ interface RiddleProofRunCard {
201
215
  visual_delta?: Record<string, unknown> | null;
202
216
  evidence_issue_code?: string | null;
203
217
  proof_evidence_present?: boolean;
218
+ viewport_matrix?: RiddleProofViewportMatrixStatus;
204
219
  artifacts?: RiddleProofCheckpointArtifact[];
205
220
  };
206
221
  observability?: {
@@ -285,6 +300,7 @@ interface RiddleProofRunState {
285
300
  implementation_detection_summary?: string | null;
286
301
  implementation_detection?: Record<string, unknown> | null;
287
302
  proof_session?: RiddleProofVisualSession;
303
+ viewport_matrix_status?: RiddleProofViewportMatrixStatus;
288
304
  finalized?: boolean;
289
305
  blocker?: RiddleProofBlocker;
290
306
  checkpoint_packet?: RiddleProofCheckpointPacket;
@@ -338,6 +354,7 @@ interface RiddleProofRunResult {
338
354
  proof_contract?: RiddleProofProofContract;
339
355
  run_card?: RiddleProofRunCard;
340
356
  proof_session?: RiddleProofVisualSession;
357
+ viewport_matrix_status?: RiddleProofViewportMatrixStatus;
341
358
  evidence_bundle?: RiddleProofEvidenceBundle;
342
359
  raw?: Record<string, unknown>;
343
360
  }
@@ -373,6 +390,7 @@ interface RiddleProofRunStatusSnapshot {
373
390
  state_paths?: RiddleProofStatePaths;
374
391
  proof_contract?: RiddleProofProofContract;
375
392
  run_card?: RiddleProofRunCard;
393
+ viewport_matrix_status?: RiddleProofViewportMatrixStatus;
376
394
  latest_event?: RiddleProofEvent;
377
395
  }
378
396
  interface RiddleProofEvidenceBundle {
@@ -381,6 +399,7 @@ interface RiddleProofEvidenceBundle {
381
399
  after?: EvidenceReference;
382
400
  artifacts?: EvidenceArtifact[];
383
401
  proof_session?: RiddleProofVisualSession;
402
+ viewport_matrix?: RiddleProofViewportMatrixStatus;
384
403
  proof_evidence?: unknown;
385
404
  playability_evidence?: unknown;
386
405
  proof_evidence_sample?: unknown;
@@ -572,6 +591,7 @@ interface RiddleProofVisualSession {
572
591
  };
573
592
  evidence?: {
574
593
  visual_delta?: JsonValue;
594
+ viewport_matrix?: RiddleProofViewportMatrixStatus;
575
595
  semantic_context?: JsonValue;
576
596
  artifact_contract?: JsonValue;
577
597
  artifact_usage?: JsonValue;
@@ -593,4 +613,4 @@ interface RiddleProofVisualSessionFingerprintBasis {
593
613
  capture_script_hash?: string;
594
614
  }
595
615
 
596
- export type { EvidenceArtifact, EvidenceReference, ImplementationAdapter, ImplementationAdapterInput, ImplementationAdapterResult, IntegrationContext, JsonObject, JsonPrimitive, JsonValue, JudgeAdapter, NotificationAdapter, PreflightAdapter, PreflightAdapterInput, PreflightAdapterResult, ProofAdapter, ProofAdapterInput, ProofAdapterResult, RiddleProofArtifactRole, RiddleProofAssessment, RiddleProofBlocker, RiddleProofCheckpointArtifact, RiddleProofCheckpointPacket, RiddleProofCheckpointResponse, RiddleProofCheckpointRole, RiddleProofCheckpointRoutingHint, RiddleProofCheckpointSummary, RiddleProofCheckpointVisibility, RiddleProofDecision, RiddleProofEvent, RiddleProofEvidenceBundle, RiddleProofPrLifecycleState, RiddleProofPrLifecycleStatus, RiddleProofProofContract, RiddleProofRunCard, RiddleProofRunParams, RiddleProofRunResult, RiddleProofRunState, RiddleProofRunStatusSnapshot, RiddleProofStage, RiddleProofStatePaths, RiddleProofStatus, RiddleProofTerminalMetadata, RiddleProofVerificationMode, RiddleProofVisualSession, RiddleProofVisualSessionFingerprintBasis, SetupAdapter, SetupAdapterInput, SetupAdapterResult, ShipAdapter };
616
+ export type { EvidenceArtifact, EvidenceReference, ImplementationAdapter, ImplementationAdapterInput, ImplementationAdapterResult, IntegrationContext, JsonObject, JsonPrimitive, JsonValue, JudgeAdapter, NotificationAdapter, PreflightAdapter, PreflightAdapterInput, PreflightAdapterResult, ProofAdapter, ProofAdapterInput, ProofAdapterResult, RiddleProofArtifactRole, RiddleProofAssessment, RiddleProofBlocker, RiddleProofCheckpointArtifact, RiddleProofCheckpointPacket, RiddleProofCheckpointResponse, RiddleProofCheckpointRole, RiddleProofCheckpointRoutingHint, RiddleProofCheckpointSummary, RiddleProofCheckpointVisibility, RiddleProofDecision, RiddleProofEvent, RiddleProofEvidenceBundle, RiddleProofPrLifecycleState, RiddleProofPrLifecycleStatus, RiddleProofProofContract, RiddleProofRunCard, RiddleProofRunParams, RiddleProofRunResult, RiddleProofRunState, RiddleProofRunStatusSnapshot, RiddleProofStage, RiddleProofStatePaths, RiddleProofStatus, RiddleProofTerminalMetadata, RiddleProofVerificationMode, RiddleProofViewportCapture, RiddleProofViewportMatrixStatus, RiddleProofVisualSession, RiddleProofVisualSessionFingerprintBasis, SetupAdapter, SetupAdapterInput, SetupAdapterResult, ShipAdapter };
package/dist/types.d.ts CHANGED
@@ -9,6 +9,20 @@ type RiddleProofVerificationMode = "proof" | "visual" | "interaction" | "playabl
9
9
  type RiddleProofDecision = "ready_to_ship" | "needs_richer_proof" | "revise_capture" | "needs_recon" | "needs_implementation" | (string & {});
10
10
  type RiddleProofStage = "preflight" | "setup" | "recon" | "author" | "implement" | "prove" | "verify" | "ship" | "notify" | (string & {});
11
11
  type RiddleProofArtifactRole = "baseline" | "after_proof" | "incidental" | "diagnostic" | (string & {});
12
+ interface RiddleProofViewportCapture {
13
+ name?: string;
14
+ slug?: string;
15
+ width?: number;
16
+ height?: number;
17
+ screenshot_label?: string;
18
+ screenshot_url?: string;
19
+ }
20
+ interface RiddleProofViewportMatrixStatus {
21
+ status?: "not_requested" | "not_run" | "complete" | "incomplete" | (string & {});
22
+ requested?: RiddleProofViewportCapture[];
23
+ executed?: RiddleProofViewportCapture[];
24
+ missing?: RiddleProofViewportCapture[];
25
+ }
12
26
  interface RiddleProofRunParams {
13
27
  repo?: string;
14
28
  branch?: string;
@@ -201,6 +215,7 @@ interface RiddleProofRunCard {
201
215
  visual_delta?: Record<string, unknown> | null;
202
216
  evidence_issue_code?: string | null;
203
217
  proof_evidence_present?: boolean;
218
+ viewport_matrix?: RiddleProofViewportMatrixStatus;
204
219
  artifacts?: RiddleProofCheckpointArtifact[];
205
220
  };
206
221
  observability?: {
@@ -285,6 +300,7 @@ interface RiddleProofRunState {
285
300
  implementation_detection_summary?: string | null;
286
301
  implementation_detection?: Record<string, unknown> | null;
287
302
  proof_session?: RiddleProofVisualSession;
303
+ viewport_matrix_status?: RiddleProofViewportMatrixStatus;
288
304
  finalized?: boolean;
289
305
  blocker?: RiddleProofBlocker;
290
306
  checkpoint_packet?: RiddleProofCheckpointPacket;
@@ -338,6 +354,7 @@ interface RiddleProofRunResult {
338
354
  proof_contract?: RiddleProofProofContract;
339
355
  run_card?: RiddleProofRunCard;
340
356
  proof_session?: RiddleProofVisualSession;
357
+ viewport_matrix_status?: RiddleProofViewportMatrixStatus;
341
358
  evidence_bundle?: RiddleProofEvidenceBundle;
342
359
  raw?: Record<string, unknown>;
343
360
  }
@@ -373,6 +390,7 @@ interface RiddleProofRunStatusSnapshot {
373
390
  state_paths?: RiddleProofStatePaths;
374
391
  proof_contract?: RiddleProofProofContract;
375
392
  run_card?: RiddleProofRunCard;
393
+ viewport_matrix_status?: RiddleProofViewportMatrixStatus;
376
394
  latest_event?: RiddleProofEvent;
377
395
  }
378
396
  interface RiddleProofEvidenceBundle {
@@ -381,6 +399,7 @@ interface RiddleProofEvidenceBundle {
381
399
  after?: EvidenceReference;
382
400
  artifacts?: EvidenceArtifact[];
383
401
  proof_session?: RiddleProofVisualSession;
402
+ viewport_matrix?: RiddleProofViewportMatrixStatus;
384
403
  proof_evidence?: unknown;
385
404
  playability_evidence?: unknown;
386
405
  proof_evidence_sample?: unknown;
@@ -572,6 +591,7 @@ interface RiddleProofVisualSession {
572
591
  };
573
592
  evidence?: {
574
593
  visual_delta?: JsonValue;
594
+ viewport_matrix?: RiddleProofViewportMatrixStatus;
575
595
  semantic_context?: JsonValue;
576
596
  artifact_contract?: JsonValue;
577
597
  artifact_usage?: JsonValue;
@@ -593,4 +613,4 @@ interface RiddleProofVisualSessionFingerprintBasis {
593
613
  capture_script_hash?: string;
594
614
  }
595
615
 
596
- export type { EvidenceArtifact, EvidenceReference, ImplementationAdapter, ImplementationAdapterInput, ImplementationAdapterResult, IntegrationContext, JsonObject, JsonPrimitive, JsonValue, JudgeAdapter, NotificationAdapter, PreflightAdapter, PreflightAdapterInput, PreflightAdapterResult, ProofAdapter, ProofAdapterInput, ProofAdapterResult, RiddleProofArtifactRole, RiddleProofAssessment, RiddleProofBlocker, RiddleProofCheckpointArtifact, RiddleProofCheckpointPacket, RiddleProofCheckpointResponse, RiddleProofCheckpointRole, RiddleProofCheckpointRoutingHint, RiddleProofCheckpointSummary, RiddleProofCheckpointVisibility, RiddleProofDecision, RiddleProofEvent, RiddleProofEvidenceBundle, RiddleProofPrLifecycleState, RiddleProofPrLifecycleStatus, RiddleProofProofContract, RiddleProofRunCard, RiddleProofRunParams, RiddleProofRunResult, RiddleProofRunState, RiddleProofRunStatusSnapshot, RiddleProofStage, RiddleProofStatePaths, RiddleProofStatus, RiddleProofTerminalMetadata, RiddleProofVerificationMode, RiddleProofVisualSession, RiddleProofVisualSessionFingerprintBasis, SetupAdapter, SetupAdapterInput, SetupAdapterResult, ShipAdapter };
616
+ export type { EvidenceArtifact, EvidenceReference, ImplementationAdapter, ImplementationAdapterInput, ImplementationAdapterResult, IntegrationContext, JsonObject, JsonPrimitive, JsonValue, JudgeAdapter, NotificationAdapter, PreflightAdapter, PreflightAdapterInput, PreflightAdapterResult, ProofAdapter, ProofAdapterInput, ProofAdapterResult, RiddleProofArtifactRole, RiddleProofAssessment, RiddleProofBlocker, RiddleProofCheckpointArtifact, RiddleProofCheckpointPacket, RiddleProofCheckpointResponse, RiddleProofCheckpointRole, RiddleProofCheckpointRoutingHint, RiddleProofCheckpointSummary, RiddleProofCheckpointVisibility, RiddleProofDecision, RiddleProofEvent, RiddleProofEvidenceBundle, RiddleProofPrLifecycleState, RiddleProofPrLifecycleStatus, RiddleProofProofContract, RiddleProofRunCard, RiddleProofRunParams, RiddleProofRunResult, RiddleProofRunState, RiddleProofRunStatusSnapshot, RiddleProofStage, RiddleProofStatePaths, RiddleProofStatus, RiddleProofTerminalMetadata, RiddleProofVerificationMode, RiddleProofViewportCapture, RiddleProofViewportMatrixStatus, RiddleProofVisualSession, RiddleProofVisualSessionFingerprintBasis, SetupAdapter, SetupAdapterInput, SetupAdapterResult, ShipAdapter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.7.80",
3
+ "version": "0.7.82",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -15,6 +15,7 @@ from util import (
15
15
  invoke,
16
16
  load_package_json,
17
17
  load_proof_session_source,
18
+ normalize_viewport_matrix,
18
19
  save_state,
19
20
  validate_proof_session_resume,
20
21
  )
@@ -130,6 +131,12 @@ if raw_assertions:
130
131
  raise SystemExit('assertions_json is not valid JSON: ' + str(e))
131
132
  s['parsed_assertions'] = parsed_assertions
132
133
  s['viewport_matrix'] = parse_json_arg('viewport_matrix_json', (dict, list), None)
134
+ s['viewport_matrix_status'] = {
135
+ 'status': 'not_run' if normalize_viewport_matrix(s.get('viewport_matrix')) else 'not_requested',
136
+ 'requested': normalize_viewport_matrix(s.get('viewport_matrix')),
137
+ 'executed': [],
138
+ 'missing': [],
139
+ }
133
140
  s['deterministic_setup'] = parse_json_arg('deterministic_setup_json', (dict, list), None)
134
141
 
135
142
  # Generate branch if not provided. The riddle-proof/* namespace is reserved for
@@ -19,6 +19,7 @@ from util import ( # noqa: E402
19
19
  apply_auth_context,
20
20
  build_capture_script,
21
21
  capture_static_preview,
22
+ capture_viewport_matrix_status,
22
23
  enrich_capture_payload,
23
24
  has_auth_context,
24
25
  invoke_retry,
@@ -27,6 +28,9 @@ from util import ( # noqa: E402
27
28
  save_state,
28
29
  should_use_static_preview,
29
30
  summarize_capture_artifacts,
31
+ viewport_matrix_return_js,
32
+ viewport_matrix_screenshot_js,
33
+ viewport_matrix_setup_js,
30
34
  )
31
35
  import subprocess as sp
32
36
 
@@ -514,9 +518,10 @@ def route_matches_expected(expected_path, observed_path):
514
518
  return True
515
519
 
516
520
 
517
- def build_probe_capture_script(base_script='', screenshot_label=''):
521
+ def build_probe_capture_script(base_script='', screenshot_label='', viewport_matrix=None):
518
522
  pieces = []
519
523
  script = (base_script or '').strip()
524
+ pieces.extend(viewport_matrix_setup_js(viewport_matrix))
520
525
  if script:
521
526
  pieces.append(script.rstrip(';') + ';')
522
527
  pieces.extend([
@@ -556,9 +561,12 @@ def build_probe_capture_script(base_script='', screenshot_label=''):
556
561
  'console.log(' + json.dumps(PAGE_STATE_PREFIX) + ' + JSON.stringify(pageState));',
557
562
  ])
558
563
  label = (screenshot_label or '').strip()
559
- if label and 'saveScreenshot' not in script:
564
+ viewport_screenshot_lines = viewport_matrix_screenshot_js(label, viewport_matrix)
565
+ if label and viewport_screenshot_lines:
566
+ pieces.extend(viewport_screenshot_lines)
567
+ elif label and 'saveScreenshot' not in script:
560
568
  pieces.append('await saveScreenshot(' + json.dumps(label) + ');')
561
- pieces.append('return { pageState };')
569
+ pieces.append('return { pageState, viewportMatrix: ' + viewport_matrix_return_js() + ' };')
562
570
  return ' '.join(pieces)
563
571
 
564
572
 
@@ -727,7 +735,7 @@ def capture_workspace_baseline(project_dir, label, plan, capture_script=''):
727
735
  'exclude': server_exclude,
728
736
  'path': target_path,
729
737
  'readiness_path': '/' if has_auth_context(s) else target_path,
730
- 'script': build_probe_capture_script(capture_script, label),
738
+ 'script': build_probe_capture_script(capture_script, label, s.get('viewport_matrix')),
731
739
  }
732
740
  if s.get('color_scheme'):
733
741
  server_args['color_scheme'] = s['color_scheme']
@@ -757,7 +765,7 @@ def capture_workspace_baseline(project_dir, label, plan, capture_script=''):
757
765
  print('Recon capture (' + label + ') using static preview fallback: ' + static_reason)
758
766
  capture_phase = label + '_capture'
759
767
  record_recon_phase(capture_phase, 'running', 'Capturing ' + label + ' recon baseline evidence.')
760
- capture = capture_static_preview(state_for_capture, project_dir, label, build_probe_capture_script(capture_script, label), timeout=300, target_path=target_path)
768
+ capture = capture_static_preview(state_for_capture, project_dir, label, build_probe_capture_script(capture_script, label, s.get('viewport_matrix')), timeout=300, target_path=target_path)
761
769
  raw = (capture.get('raw') or {}).get('capture') or {}
762
770
  append_capture_diagnostic(
763
771
  s,
@@ -787,7 +795,7 @@ def capture_prod_baseline(prod_url, plan, capture_script=''):
787
795
  if not target_url:
788
796
  raise SystemExit('Requested prod baseline in recon, but prod_url is missing.')
789
797
  wait_for_selector = (plan.get('wait_for_selector') or '').strip()
790
- script = build_capture_script(target_url, build_probe_capture_script(capture_script, 'prod'), 'prod', wait_for_selector)
798
+ script = build_capture_script(target_url, build_probe_capture_script(capture_script, 'prod', s.get('viewport_matrix')), 'prod', wait_for_selector, s.get('viewport_matrix'))
791
799
  args = {'script': script, 'timeout_sec': 60}
792
800
  apply_auth_context(s, args)
793
801
  print('Recon capture (prod) at ' + target_url)
@@ -815,6 +823,7 @@ def baseline_record(capture, observation):
815
823
  'url': capture.get('url'),
816
824
  'static_fallback_reason': capture.get('static_fallback_reason', ''),
817
825
  'artifact_summary': summarize_capture_artifacts(capture.get('raw') or {}),
826
+ 'viewport_matrix': capture_viewport_matrix_status(s, capture.get('raw') or {}, str(observation.get('label') or '')),
818
827
  'observation': observation,
819
828
  }
820
829
 
@@ -833,6 +842,24 @@ def build_observation_packet(label, expected_path, capture=None, error=''):
833
842
 
834
843
  payload = (capture or {}).get('raw') or {}
835
844
  quality = evaluate_capture_quality(payload, expected_path)
845
+ viewport_matrix = capture_viewport_matrix_status(s, payload, label)
846
+ if viewport_matrix.get('status') == 'incomplete':
847
+ missing_names = [
848
+ str(item.get('name') or item.get('slug') or '').strip()
849
+ for item in viewport_matrix.get('missing') or []
850
+ if str(item.get('name') or item.get('slug') or '').strip()
851
+ ]
852
+ missing_text = ', '.join(missing_names[:8]) or 'requested viewport screenshots'
853
+ quality['valid'] = False
854
+ quality['telemetry_ready'] = False
855
+ quality['reason'] = (
856
+ quality.get('reason') + '; missing requested viewport evidence: ' + missing_text
857
+ if quality.get('reason') and quality.get('reason') != 'ok'
858
+ else 'missing requested viewport evidence: ' + missing_text
859
+ )
860
+ details = quality.get('details') if isinstance(quality.get('details'), dict) else {}
861
+ details['viewport_matrix'] = viewport_matrix
862
+ quality['details'] = details
836
863
  return {
837
864
  'label': label,
838
865
  'ok': bool((capture or {}).get('url')) and quality['valid'],
@@ -1042,6 +1069,13 @@ attempt_record = {
1042
1069
  'captured_baselines': attempt_captured_baselines,
1043
1070
  'result': attempt_result,
1044
1071
  }
1072
+ if attempt_captured_baselines:
1073
+ latest_viewport_status = None
1074
+ for baseline in attempt_captured_baselines.values():
1075
+ if isinstance(baseline, dict) and isinstance(baseline.get('viewport_matrix'), dict):
1076
+ latest_viewport_status = baseline.get('viewport_matrix')
1077
+ if latest_viewport_status:
1078
+ s['viewport_matrix_status'] = latest_viewport_status
1045
1079
  attempt_history.append(attempt_record)
1046
1080
  recon_results['attempt_history'] = attempt_history
1047
1081
  recon_results['baselines'] = {}
@@ -948,8 +948,153 @@ def join_url_path(base_url, target_path=''):
948
948
  return urljoin(base.rstrip('/') + '/', path.lstrip('/'))
949
949
 
950
950
 
951
- def build_capture_script(url, capture_script, label, wait_for_selector=''):
951
+ def slugify_viewport_name(value, fallback='viewport'):
952
+ text = re.sub(r'[^a-zA-Z0-9]+', '-', str(value or '').strip().lower()).strip('-')
953
+ return text or fallback
954
+
955
+
956
+ def normalize_viewport_matrix(value):
957
+ matrix = value
958
+ if isinstance(matrix, dict):
959
+ for key in ('viewports', 'viewport_matrix', 'matrix'):
960
+ if isinstance(matrix.get(key), list):
961
+ matrix = matrix.get(key)
962
+ break
963
+ else:
964
+ matrix = [matrix]
965
+ if not isinstance(matrix, list):
966
+ return []
967
+
968
+ normalized = []
969
+ for index, item in enumerate(matrix):
970
+ if not isinstance(item, dict):
971
+ continue
972
+ try:
973
+ width = int(item.get('width'))
974
+ height = int(item.get('height'))
975
+ except Exception:
976
+ continue
977
+ if width <= 0 or height <= 0:
978
+ continue
979
+ raw_name = str(item.get('name') or item.get('label') or '').strip()
980
+ name = raw_name or f'{width}x{height}'
981
+ slug = slugify_viewport_name(name, f'viewport-{index + 1}')
982
+ normalized.append({
983
+ 'name': name,
984
+ 'slug': slug,
985
+ 'width': width,
986
+ 'height': height,
987
+ })
988
+ return normalized
989
+
990
+
991
+ def viewport_screenshot_label(label, viewport):
992
+ return (str(label or 'screenshot').strip() or 'screenshot') + '-' + str(viewport.get('slug') or viewport.get('name') or 'viewport')
993
+
994
+
995
+ def viewport_matrix_setup_js(viewport_matrix):
996
+ viewports = normalize_viewport_matrix(viewport_matrix)
997
+ if not viewports:
998
+ return []
999
+ return [
1000
+ 'const __riddleProofViewportMatrix = ' + json.dumps(viewports) + ';',
1001
+ 'const __riddleProofViewportResults = [];',
1002
+ 'const __riddleProofApplyViewport = async (vp) => {',
1003
+ ' if (!vp || !vp.width || !vp.height) return;',
1004
+ ' await page.setViewportSize({ width: Number(vp.width), height: Number(vp.height) });',
1005
+ '};',
1006
+ 'await __riddleProofApplyViewport(__riddleProofViewportMatrix[0]);',
1007
+ ]
1008
+
1009
+
1010
+ def viewport_matrix_screenshot_js(label, viewport_matrix):
1011
+ viewports = normalize_viewport_matrix(viewport_matrix)
1012
+ if not viewports:
1013
+ return []
1014
+ base_label = str(label or 'screenshot').strip() or 'screenshot'
1015
+ return [
1016
+ 'for (const __riddleProofViewport of __riddleProofViewportMatrix) {',
1017
+ ' await __riddleProofApplyViewport(__riddleProofViewport);',
1018
+ ' await page.waitForTimeout(250);',
1019
+ ' const __riddleProofViewportLabel = ' + json.dumps(base_label) + ' + "-" + (__riddleProofViewport.slug || __riddleProofViewport.name || "viewport");',
1020
+ ' await saveScreenshot(__riddleProofViewportLabel);',
1021
+ ' __riddleProofViewportResults.push({',
1022
+ ' name: __riddleProofViewport.name || "",',
1023
+ ' slug: __riddleProofViewport.slug || "",',
1024
+ ' width: Number(__riddleProofViewport.width) || null,',
1025
+ ' height: Number(__riddleProofViewport.height) || null,',
1026
+ ' screenshot_label: __riddleProofViewportLabel,',
1027
+ ' });',
1028
+ '}',
1029
+ ]
1030
+
1031
+
1032
+ def viewport_matrix_return_js():
1033
+ return 'typeof __riddleProofViewportMatrix !== "undefined" ? { requested: __riddleProofViewportMatrix, executed: __riddleProofViewportResults } : null'
1034
+
1035
+
1036
+ def capture_viewport_matrix_status(state, payload, label=''):
1037
+ requested = normalize_viewport_matrix((state or {}).get('viewport_matrix'))
1038
+ if not requested:
1039
+ return {
1040
+ 'status': 'not_requested',
1041
+ 'requested': [],
1042
+ 'executed': [],
1043
+ 'missing': [],
1044
+ }
1045
+
1046
+ artifact_summary = summarize_capture_artifacts(payload)
1047
+ image_items = []
1048
+ for key in ('outputs', 'screenshots', 'artifacts'):
1049
+ for item in artifact_summary.get(key) or []:
1050
+ if isinstance(item, dict):
1051
+ image_items.append(item)
1052
+
1053
+ executed = []
1054
+ missing = []
1055
+ for viewport in requested:
1056
+ expected_label = viewport_screenshot_label(label, viewport)
1057
+ expected_names = {
1058
+ expected_label,
1059
+ expected_label + '.png',
1060
+ expected_label + '.jpg',
1061
+ expected_label + '.jpeg',
1062
+ expected_label + '.webp',
1063
+ expected_label + '.gif',
1064
+ }
1065
+ match = None
1066
+ for item in image_items:
1067
+ name = str(item.get('name') or '')
1068
+ url = str(item.get('url') or '')
1069
+ if name in expected_names and url:
1070
+ match = item
1071
+ break
1072
+ record = {
1073
+ 'name': viewport.get('name'),
1074
+ 'slug': viewport.get('slug'),
1075
+ 'width': viewport.get('width'),
1076
+ 'height': viewport.get('height'),
1077
+ 'screenshot_label': expected_label,
1078
+ 'screenshot_url': str((match or {}).get('url') or ''),
1079
+ }
1080
+ if match:
1081
+ executed.append(record)
1082
+ else:
1083
+ missing.append(record)
1084
+
1085
+ return {
1086
+ 'status': 'complete' if not missing else 'incomplete',
1087
+ 'requested': requested,
1088
+ 'executed': executed,
1089
+ 'missing': missing,
1090
+ }
1091
+
1092
+
1093
+ def build_capture_script(url, capture_script, label, wait_for_selector='', viewport_matrix=None):
1094
+ script_handles_viewport_matrix = '__riddleProofViewportMatrix' in (capture_script or '')
1095
+ effective_viewport_matrix = None if script_handles_viewport_matrix else viewport_matrix
952
1096
  pieces = [
1097
+ *viewport_matrix_setup_js(effective_viewport_matrix),
953
1098
  'await page.goto(' + json.dumps(url) + ');',
954
1099
  ]
955
1100
  selector = (wait_for_selector or '').strip()
@@ -958,8 +1103,11 @@ def build_capture_script(url, capture_script, label, wait_for_selector=''):
958
1103
  pieces.append('await page.waitForTimeout(1500);')
959
1104
  if (capture_script or '').strip():
960
1105
  pieces.append((capture_script or '').strip().rstrip(';') + ';')
961
- if not capture_script_saves_screenshot(capture_script):
1106
+ if normalize_viewport_matrix(effective_viewport_matrix):
1107
+ pieces.extend(viewport_matrix_screenshot_js(label, effective_viewport_matrix))
1108
+ elif not capture_script_saves_screenshot(capture_script):
962
1109
  pieces.append('await saveScreenshot(' + json.dumps(label) + ');')
1110
+ pieces.append('return { viewportMatrix: ' + viewport_matrix_return_js() + ' };')
963
1111
  return ' '.join(pieces)
964
1112
 
965
1113
 
@@ -987,7 +1135,7 @@ def capture_static_preview(state, project_dir, label, capture_script, timeout=30
987
1135
  preview_id = preview.get('id', '')
988
1136
  capture_url = join_url_path(preview_url, target_path or state.get('server_path', ''))
989
1137
 
990
- script = build_capture_script(capture_url, capture_script, label, state.get('wait_for_selector', ''))
1138
+ script = build_capture_script(capture_url, capture_script, label, state.get('wait_for_selector', ''), state.get('viewport_matrix'))
991
1139
  args = {'script': script, 'timeout_sec': 60}
992
1140
  apply_auth_context(state, args)
993
1141
  shot = invoke_retry('riddle_script', args, retries=3, timeout=max(timeout, 120))