@riddledc/riddle-proof 0.5.35 → 0.5.37
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/README.md +16 -0
- package/dist/chunk-NSWT3VSV.js +361 -0
- package/dist/engine-harness.js +2 -2
- package/dist/index.cjs +364 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +23 -11
- package/dist/playability.cjs +389 -0
- package/dist/playability.d.cts +44 -0
- package/dist/playability.d.ts +44 -0
- package/dist/playability.js +14 -0
- package/dist/types.d.cts +2 -1
- package/dist/types.d.ts +2 -1
- package/package.json +7 -2
- package/runtime/lib/author.py +2 -1
- package/runtime/lib/riddle_core_call.mjs +12 -0
- package/runtime/lib/util.py +2 -0
- package/runtime/lib/verify.py +333 -2
- package/dist/{chunk-7R6ZQE3X.js → chunk-GN7HSZ6G.js} +3 -3
package/runtime/lib/verify.py
CHANGED
|
@@ -47,6 +47,7 @@ VISUAL_FIRST_MODES = {
|
|
|
47
47
|
'visual', 'render', 'interaction', 'ui', 'layout', 'screenshot',
|
|
48
48
|
'canvas', 'animation',
|
|
49
49
|
}
|
|
50
|
+
PLAYABILITY_MODES = {'playable', 'gameplay', 'game'}
|
|
50
51
|
PROOF_EVIDENCE_REQUIRED_MODES = {'audio'}
|
|
51
52
|
MIN_VISUAL_DELTA_PERCENT = 0.5
|
|
52
53
|
MIN_VISUAL_CHANGED_PIXELS = 5000
|
|
@@ -77,11 +78,13 @@ def normalized_verification_mode(value):
|
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
def proof_evidence_required_for_mode(verification_mode):
|
|
80
|
-
|
|
81
|
+
mode = normalized_verification_mode(verification_mode)
|
|
82
|
+
return mode in PROOF_EVIDENCE_REQUIRED_MODES or mode in PLAYABILITY_MODES
|
|
81
83
|
|
|
82
84
|
|
|
83
85
|
def screenshot_required_for_mode(verification_mode):
|
|
84
|
-
|
|
86
|
+
mode = normalized_verification_mode(verification_mode)
|
|
87
|
+
return mode in VISUAL_FIRST_MODES or mode in PLAYABILITY_MODES
|
|
85
88
|
|
|
86
89
|
|
|
87
90
|
def auto_screenshot_for_mode(verification_mode):
|
|
@@ -408,6 +411,306 @@ def extract_proof_evidence(payload):
|
|
|
408
411
|
return evidence
|
|
409
412
|
|
|
410
413
|
|
|
414
|
+
PLAYABILITY_EVIDENCE_VERSION = 'riddle-proof.playability.v1'
|
|
415
|
+
PLAYABILITY_ASSESSMENT_VERSION = 'riddle-proof.playability.assessment.v1'
|
|
416
|
+
PLAYABILITY_CONTAINER_KEYS = (
|
|
417
|
+
'playability', 'playability_evidence', 'playabilityEvidence',
|
|
418
|
+
'playable', 'gameplay', 'gameplay_evidence', 'gameplayEvidence',
|
|
419
|
+
)
|
|
420
|
+
PLAYABILITY_INPUT_KEYS = (
|
|
421
|
+
'inputAccepted', 'inputObserved', 'inputReceived', 'controlsWorked',
|
|
422
|
+
'keyboardWorked', 'pointerWorked', 'touchWorked', 'steeringInputAccepted',
|
|
423
|
+
'userInputObserved',
|
|
424
|
+
)
|
|
425
|
+
PLAYABILITY_STATE_KEYS = (
|
|
426
|
+
'stateChanged', 'gameStateChanged', 'playStarted', 'simulationAdvanced',
|
|
427
|
+
'distanceAdvanced', 'scoreChanged', 'hudChanged', 'positionChanged',
|
|
428
|
+
'speedChanged',
|
|
429
|
+
)
|
|
430
|
+
PLAYABILITY_MOTION_KEYS = (
|
|
431
|
+
'motionObserved', 'visualMotion', 'canvasChanged', 'pixelChanged',
|
|
432
|
+
'playfieldMoved', 'playfieldPixelsChanged', 'nonHudPixelsChanged',
|
|
433
|
+
'animationAdvanced', 'frameChanged', 'framesChanged',
|
|
434
|
+
)
|
|
435
|
+
PLAYABILITY_TIME_KEYS = (
|
|
436
|
+
'timeProgressed', 'clockAdvanced', 'animationAdvanced',
|
|
437
|
+
'simulationAdvanced', 'playStarted',
|
|
438
|
+
)
|
|
439
|
+
PLAYABILITY_PERCENT_KEYS = (
|
|
440
|
+
'changed_percent', 'change_percent', 'percent_changed', 'diff_percent',
|
|
441
|
+
'motion_percent', 'pixel_change_percent',
|
|
442
|
+
)
|
|
443
|
+
PLAYABILITY_RATIO_KEYS = (
|
|
444
|
+
'changed_ratio', 'change_ratio', 'diff_ratio', 'motion_ratio',
|
|
445
|
+
'pixel_change_ratio',
|
|
446
|
+
)
|
|
447
|
+
PLAYABILITY_PIXEL_KEYS = (
|
|
448
|
+
'changed_pixels', 'changed_pixel_count', 'diff_pixels', 'motion_pixels',
|
|
449
|
+
'pixel_delta', 'changedPixels',
|
|
450
|
+
)
|
|
451
|
+
PLAYABILITY_AVG_DELTA_KEYS = (
|
|
452
|
+
'average_delta', 'avg_delta', 'mean_delta', 'avg_abs_delta',
|
|
453
|
+
'mean_abs_delta',
|
|
454
|
+
)
|
|
455
|
+
PLAYABILITY_TIME_DELTA_KEYS = (
|
|
456
|
+
'time_delta_ms', 'elapsed_ms', 'duration_ms', 'sample_duration_ms',
|
|
457
|
+
'animation_delta_ms',
|
|
458
|
+
)
|
|
459
|
+
PLAYABILITY_THRESHOLDS = {
|
|
460
|
+
'min_changed_percent': 0.5,
|
|
461
|
+
'min_changed_pixels': 1000,
|
|
462
|
+
'min_average_delta': 1,
|
|
463
|
+
'min_time_delta_ms': 250,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _numeric(value):
|
|
468
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
469
|
+
return float(value)
|
|
470
|
+
if isinstance(value, str) and value.strip():
|
|
471
|
+
try:
|
|
472
|
+
return float(value)
|
|
473
|
+
except Exception:
|
|
474
|
+
return None
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _numeric_from_keys(record, keys):
|
|
479
|
+
if not isinstance(record, dict):
|
|
480
|
+
return None
|
|
481
|
+
for key in keys:
|
|
482
|
+
value = _numeric(record.get(key))
|
|
483
|
+
if value is not None:
|
|
484
|
+
return value
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _true_for_any_key(record, keys):
|
|
489
|
+
return isinstance(record, dict) and any(record.get(key) is True for key in keys)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _percent_from(record):
|
|
493
|
+
percent = _numeric_from_keys(record, PLAYABILITY_PERCENT_KEYS)
|
|
494
|
+
if percent is not None:
|
|
495
|
+
return percent
|
|
496
|
+
ratio = _numeric_from_keys(record, PLAYABILITY_RATIO_KEYS)
|
|
497
|
+
if ratio is not None:
|
|
498
|
+
return ratio * 100 if ratio <= 1 else ratio
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _parse_json_if_possible(value):
|
|
503
|
+
if not isinstance(value, str):
|
|
504
|
+
return None
|
|
505
|
+
text = value.strip()
|
|
506
|
+
if not text or text[0] not in '[{':
|
|
507
|
+
return None
|
|
508
|
+
try:
|
|
509
|
+
return json.loads(text)
|
|
510
|
+
except Exception:
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _has_playability_shape(record):
|
|
515
|
+
if not isinstance(record, dict):
|
|
516
|
+
return False
|
|
517
|
+
if record.get('version') == PLAYABILITY_EVIDENCE_VERSION:
|
|
518
|
+
return True
|
|
519
|
+
shape_keys = (
|
|
520
|
+
'input_events', 'state_delta', 'pixel_delta', 'canvas_delta',
|
|
521
|
+
'motion_delta', 'playfield_delta', 'time_delta_ms',
|
|
522
|
+
)
|
|
523
|
+
if any(key in record for key in shape_keys):
|
|
524
|
+
return True
|
|
525
|
+
assertions = record.get('assertions') if isinstance(record.get('assertions'), dict) else record
|
|
526
|
+
return any(
|
|
527
|
+
key in assertions
|
|
528
|
+
for key in (
|
|
529
|
+
PLAYABILITY_INPUT_KEYS
|
|
530
|
+
+ PLAYABILITY_STATE_KEYS
|
|
531
|
+
+ PLAYABILITY_MOTION_KEYS
|
|
532
|
+
+ PLAYABILITY_TIME_KEYS
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def extract_playability_evidence(value, seen=None, depth=0):
|
|
538
|
+
if depth > 6 or value is None:
|
|
539
|
+
return None
|
|
540
|
+
if seen is None:
|
|
541
|
+
seen = set()
|
|
542
|
+
parsed = _parse_json_if_possible(value)
|
|
543
|
+
if parsed is not None:
|
|
544
|
+
return extract_playability_evidence(parsed, seen, depth + 1)
|
|
545
|
+
if isinstance(value, list):
|
|
546
|
+
ident = id(value)
|
|
547
|
+
if ident in seen:
|
|
548
|
+
return None
|
|
549
|
+
seen.add(ident)
|
|
550
|
+
for item in value:
|
|
551
|
+
found = extract_playability_evidence(item, seen, depth + 1)
|
|
552
|
+
if found is not None:
|
|
553
|
+
return found
|
|
554
|
+
return None
|
|
555
|
+
if not isinstance(value, dict):
|
|
556
|
+
return None
|
|
557
|
+
ident = id(value)
|
|
558
|
+
if ident in seen:
|
|
559
|
+
return None
|
|
560
|
+
seen.add(ident)
|
|
561
|
+
if _has_playability_shape(value):
|
|
562
|
+
return value
|
|
563
|
+
for key in PLAYABILITY_CONTAINER_KEYS:
|
|
564
|
+
if key in value:
|
|
565
|
+
found = extract_playability_evidence(value.get(key), seen, depth + 1)
|
|
566
|
+
if found is not None:
|
|
567
|
+
return found
|
|
568
|
+
if isinstance(value.get(key), bool):
|
|
569
|
+
return {'assertions': {key: value.get(key)}}
|
|
570
|
+
for item in value.values():
|
|
571
|
+
found = extract_playability_evidence(item, seen, depth + 1)
|
|
572
|
+
if found is not None:
|
|
573
|
+
return found
|
|
574
|
+
return None
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def assess_playability_evidence(value):
|
|
578
|
+
evidence = extract_playability_evidence(value)
|
|
579
|
+
metrics = {}
|
|
580
|
+
required = {
|
|
581
|
+
'input': True,
|
|
582
|
+
'state_change': True,
|
|
583
|
+
'motion': True,
|
|
584
|
+
'time_progression': True,
|
|
585
|
+
}
|
|
586
|
+
if not isinstance(evidence, dict):
|
|
587
|
+
return {
|
|
588
|
+
'version': PLAYABILITY_ASSESSMENT_VERSION,
|
|
589
|
+
'evidence_present': False,
|
|
590
|
+
'passed': False,
|
|
591
|
+
'input_observed': False,
|
|
592
|
+
'state_changed': False,
|
|
593
|
+
'motion_observed': False,
|
|
594
|
+
'time_progressed': False,
|
|
595
|
+
'concerns': ['playability evidence is missing'],
|
|
596
|
+
'metrics': metrics,
|
|
597
|
+
'thresholds': dict(PLAYABILITY_THRESHOLDS),
|
|
598
|
+
'required': required,
|
|
599
|
+
'evidence_keys': [],
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
assertions = evidence.get('assertions') if isinstance(evidence.get('assertions'), dict) else evidence
|
|
603
|
+
input_events = []
|
|
604
|
+
for key in ('input_events', 'inputs', 'interactions'):
|
|
605
|
+
if isinstance(evidence.get(key), list):
|
|
606
|
+
input_events.extend(evidence.get(key))
|
|
607
|
+
input_count = _numeric_from_keys(evidence, ('input_event_count', 'input_count', 'interaction_count'))
|
|
608
|
+
input_observed = bool(
|
|
609
|
+
_true_for_any_key(assertions, PLAYABILITY_INPUT_KEYS)
|
|
610
|
+
or input_events
|
|
611
|
+
or (input_count is not None and input_count > 0)
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
state_delta = evidence.get('state_delta') if isinstance(evidence.get('state_delta'), dict) else evidence.get('stateDelta')
|
|
615
|
+
changed_keys = []
|
|
616
|
+
if isinstance(state_delta, dict):
|
|
617
|
+
raw_keys = state_delta.get('changed_keys') or state_delta.get('changedKeys') or []
|
|
618
|
+
if isinstance(raw_keys, list):
|
|
619
|
+
changed_keys = raw_keys
|
|
620
|
+
state_numeric_delta = _numeric_from_keys(evidence, (
|
|
621
|
+
'distance_delta', 'score_delta', 'position_delta', 'speed_delta', 'hud_delta',
|
|
622
|
+
))
|
|
623
|
+
if changed_keys:
|
|
624
|
+
metrics['state_changed_keys'] = changed_keys
|
|
625
|
+
if state_numeric_delta is not None:
|
|
626
|
+
metrics['state_numeric_delta'] = state_numeric_delta
|
|
627
|
+
state_changed = bool(
|
|
628
|
+
_true_for_any_key(assertions, PLAYABILITY_STATE_KEYS)
|
|
629
|
+
or (isinstance(state_delta, dict) and state_delta.get('changed') is True)
|
|
630
|
+
or changed_keys
|
|
631
|
+
or (state_numeric_delta is not None and abs(state_numeric_delta) > 0)
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
motion_observed = _true_for_any_key(assertions, PLAYABILITY_MOTION_KEYS)
|
|
635
|
+
if not motion_observed:
|
|
636
|
+
for source in (
|
|
637
|
+
evidence.get('playfield_delta'), evidence.get('playfieldDelta'),
|
|
638
|
+
evidence.get('non_hud_delta'), evidence.get('nonHudDelta'),
|
|
639
|
+
evidence.get('pixel_delta'), evidence.get('pixelDelta'),
|
|
640
|
+
evidence.get('canvas_delta'), evidence.get('canvasDelta'),
|
|
641
|
+
evidence.get('motion_delta'), evidence.get('motionDelta'),
|
|
642
|
+
evidence.get('visual_delta'), evidence.get('visualDelta'),
|
|
643
|
+
evidence,
|
|
644
|
+
):
|
|
645
|
+
if not isinstance(source, dict):
|
|
646
|
+
continue
|
|
647
|
+
percent = _percent_from(source)
|
|
648
|
+
pixels = _numeric_from_keys(source, PLAYABILITY_PIXEL_KEYS)
|
|
649
|
+
average_delta = _numeric_from_keys(source, PLAYABILITY_AVG_DELTA_KEYS)
|
|
650
|
+
if percent is not None:
|
|
651
|
+
metrics['changed_percent'] = percent
|
|
652
|
+
if pixels is not None:
|
|
653
|
+
metrics['changed_pixels'] = pixels
|
|
654
|
+
if average_delta is not None:
|
|
655
|
+
metrics['average_delta'] = average_delta
|
|
656
|
+
motion_observed = bool(
|
|
657
|
+
(percent is not None and percent >= PLAYABILITY_THRESHOLDS['min_changed_percent'])
|
|
658
|
+
or (pixels is not None and pixels >= PLAYABILITY_THRESHOLDS['min_changed_pixels'])
|
|
659
|
+
or (average_delta is not None and average_delta >= PLAYABILITY_THRESHOLDS['min_average_delta'])
|
|
660
|
+
)
|
|
661
|
+
if motion_observed:
|
|
662
|
+
break
|
|
663
|
+
|
|
664
|
+
time_delta = _numeric_from_keys(evidence, PLAYABILITY_TIME_DELTA_KEYS)
|
|
665
|
+
if time_delta is None and isinstance(state_delta, dict):
|
|
666
|
+
time_delta = _numeric_from_keys(state_delta, PLAYABILITY_TIME_DELTA_KEYS)
|
|
667
|
+
if time_delta is not None:
|
|
668
|
+
metrics['time_delta_ms'] = time_delta
|
|
669
|
+
time_progressed = bool(
|
|
670
|
+
_true_for_any_key(assertions, PLAYABILITY_TIME_KEYS)
|
|
671
|
+
or (time_delta is not None and time_delta >= PLAYABILITY_THRESHOLDS['min_time_delta_ms'])
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
explicit_failure = bool(
|
|
675
|
+
evidence.get('passed') is False
|
|
676
|
+
or evidence.get('playable') is False
|
|
677
|
+
or assertions.get('playabilityPassed') is False
|
|
678
|
+
or any(assertions.get(key) is False for key in (
|
|
679
|
+
PLAYABILITY_INPUT_KEYS
|
|
680
|
+
+ PLAYABILITY_STATE_KEYS
|
|
681
|
+
+ PLAYABILITY_MOTION_KEYS
|
|
682
|
+
+ PLAYABILITY_TIME_KEYS
|
|
683
|
+
))
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
concerns = []
|
|
687
|
+
if not input_observed:
|
|
688
|
+
concerns.append('no accepted player input was observed')
|
|
689
|
+
if not state_changed:
|
|
690
|
+
concerns.append('game state did not measurably change')
|
|
691
|
+
if not motion_observed:
|
|
692
|
+
concerns.append('playfield/canvas pixels did not measurably change')
|
|
693
|
+
if not time_progressed:
|
|
694
|
+
concerns.append('play time or animation time did not measurably progress')
|
|
695
|
+
if explicit_failure:
|
|
696
|
+
concerns.append('playability evidence includes an explicit failed assertion')
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
'version': PLAYABILITY_ASSESSMENT_VERSION,
|
|
700
|
+
'evidence_present': True,
|
|
701
|
+
'passed': bool(input_observed and state_changed and motion_observed and time_progressed and not explicit_failure),
|
|
702
|
+
'input_observed': input_observed,
|
|
703
|
+
'state_changed': state_changed,
|
|
704
|
+
'motion_observed': motion_observed,
|
|
705
|
+
'time_progressed': time_progressed,
|
|
706
|
+
'concerns': concerns,
|
|
707
|
+
'metrics': metrics,
|
|
708
|
+
'thresholds': dict(PLAYABILITY_THRESHOLDS),
|
|
709
|
+
'required': required,
|
|
710
|
+
'evidence_keys': list(evidence.keys()),
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
|
|
411
714
|
def first_failed_proof_evidence(value):
|
|
412
715
|
if isinstance(value, dict):
|
|
413
716
|
if value.get('proof_evidence_present') is False:
|
|
@@ -698,6 +1001,7 @@ def collect_supporting_artifacts(payload):
|
|
|
698
1001
|
structured_result_keys = [k for k in result_keys if k not in ('pageState', 'page_state')]
|
|
699
1002
|
console_entries = payload.get('console') or []
|
|
700
1003
|
proof_evidence = extract_proof_evidence(payload)
|
|
1004
|
+
playability_assessment = assess_playability_evidence(proof_evidence)
|
|
701
1005
|
|
|
702
1006
|
return {
|
|
703
1007
|
'image_outputs': image_outputs,
|
|
@@ -708,6 +1012,9 @@ def collect_supporting_artifacts(payload):
|
|
|
708
1012
|
'console_entries': len(console_entries),
|
|
709
1013
|
'proof_evidence_present': proof_evidence is not None,
|
|
710
1014
|
'proof_evidence_sample': compact_value(proof_evidence) if proof_evidence is not None else '',
|
|
1015
|
+
'playability_evidence_present': bool(playability_assessment.get('evidence_present')),
|
|
1016
|
+
'playability_ready': bool(playability_assessment.get('passed')),
|
|
1017
|
+
'playability_assessment': playability_assessment,
|
|
711
1018
|
'has_structured_payload': bool(data_outputs or structured_result_keys or proof_evidence is not None),
|
|
712
1019
|
}
|
|
713
1020
|
|
|
@@ -721,6 +1028,7 @@ def artifact_contract_for_mode(verification_mode):
|
|
|
721
1028
|
'route_semantics': True,
|
|
722
1029
|
'screenshot': screenshot_required_for_mode(mode),
|
|
723
1030
|
'proof_evidence': proof_evidence_required_for_mode(mode),
|
|
1031
|
+
'playability': mode in PLAYABILITY_MODES,
|
|
724
1032
|
'visual_delta': visual_delta_applies(mode),
|
|
725
1033
|
},
|
|
726
1034
|
'preferred': {
|
|
@@ -748,6 +1056,8 @@ def artifact_production_summary(payload, supporting):
|
|
|
748
1056
|
'console_entries': int(supporting.get('console_entries') or 0),
|
|
749
1057
|
'structured_result_keys': list(supporting.get('structured_result_keys') or []),
|
|
750
1058
|
'proof_evidence_present': bool(supporting.get('proof_evidence_present')),
|
|
1059
|
+
'playability_evidence_present': bool(supporting.get('playability_evidence_present')),
|
|
1060
|
+
'playability_ready': bool(supporting.get('playability_ready')),
|
|
751
1061
|
'has_structured_payload': bool(supporting.get('has_structured_payload')),
|
|
752
1062
|
}
|
|
753
1063
|
|
|
@@ -768,6 +1078,7 @@ def artifact_signal_availability(state, after_observation, supporting, visual_de
|
|
|
768
1078
|
),
|
|
769
1079
|
'structured_payload': bool(supporting.get('has_structured_payload')),
|
|
770
1080
|
'proof_evidence': bool(supporting.get('proof_evidence_present')),
|
|
1081
|
+
'playability': bool(supporting.get('playability_ready')),
|
|
771
1082
|
'visual_delta': visual_delta_passes_ship_gate(visual_delta),
|
|
772
1083
|
'console_summary': bool(supporting.get('console_entries')),
|
|
773
1084
|
'json_artifacts': bool(supporting.get('data_outputs')),
|
|
@@ -799,6 +1110,8 @@ def artifact_usage_summary(state, after_observation, supporting, visual_delta, r
|
|
|
799
1110
|
capture_quality.append('structured_payload')
|
|
800
1111
|
if available.get('proof_evidence'):
|
|
801
1112
|
capture_quality.append('proof_evidence')
|
|
1113
|
+
if available.get('playability'):
|
|
1114
|
+
capture_quality.append('playability')
|
|
802
1115
|
if available.get('visual_delta'):
|
|
803
1116
|
capture_quality.append('visual_delta')
|
|
804
1117
|
|
|
@@ -1048,6 +1361,8 @@ def build_semantic_context(state, results, after_observation, expected_path):
|
|
|
1048
1361
|
def build_evidence_bundle(state, results, after_payload, after_observation, required_baseline_present, expected_path):
|
|
1049
1362
|
supporting = collect_supporting_artifacts(after_payload)
|
|
1050
1363
|
proof_evidence = extract_proof_evidence(after_payload)
|
|
1364
|
+
playability_assessment = assess_playability_evidence(proof_evidence)
|
|
1365
|
+
playability_evidence = extract_playability_evidence(proof_evidence)
|
|
1051
1366
|
visual_delta = (
|
|
1052
1367
|
extract_visual_delta(after_payload)
|
|
1053
1368
|
if visual_delta_applies(state.get('verification_mode'))
|
|
@@ -1089,6 +1404,7 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1089
1404
|
},
|
|
1090
1405
|
evidence={
|
|
1091
1406
|
'visual_delta': visual_delta,
|
|
1407
|
+
'playability_assessment': playability_assessment,
|
|
1092
1408
|
'semantic_context': semantic_context,
|
|
1093
1409
|
'artifact_contract': artifact_contract,
|
|
1094
1410
|
'artifact_usage': artifact_usage,
|
|
@@ -1110,10 +1426,14 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1110
1426
|
'observation': after_observation,
|
|
1111
1427
|
'supporting_artifacts': supporting,
|
|
1112
1428
|
'proof_evidence': proof_evidence,
|
|
1429
|
+
'playability_evidence': playability_evidence,
|
|
1430
|
+
'playability_assessment': playability_assessment,
|
|
1113
1431
|
'proof_evidence_sample': compact_value(proof_evidence) if proof_evidence is not None else '',
|
|
1114
1432
|
'visual_delta': visual_delta,
|
|
1115
1433
|
},
|
|
1116
1434
|
'proof_evidence': proof_evidence,
|
|
1435
|
+
'playability_evidence': playability_evidence,
|
|
1436
|
+
'playability_assessment': playability_assessment,
|
|
1117
1437
|
'proof_evidence_sample': compact_value(proof_evidence) if proof_evidence is not None else '',
|
|
1118
1438
|
'success_criteria': (state.get('success_criteria') or '').strip(),
|
|
1119
1439
|
'assertions': state.get('parsed_assertions') or None,
|
|
@@ -1135,6 +1455,8 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
|
|
|
1135
1455
|
evidence_basis.append('screenshots')
|
|
1136
1456
|
if supporting['has_structured_payload']:
|
|
1137
1457
|
evidence_basis.append('structured-artifacts')
|
|
1458
|
+
if supporting.get('playability_ready'):
|
|
1459
|
+
evidence_basis.append('playability')
|
|
1138
1460
|
visual_delta = ((evidence_bundle or {}).get('after') or {}).get('visual_delta') or {}
|
|
1139
1461
|
if visual_delta.get('status') == 'measured':
|
|
1140
1462
|
evidence_basis.append('visual-delta')
|
|
@@ -1167,6 +1489,14 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
|
|
|
1167
1489
|
evidence_bundle['artifact_usage'] = artifact_usage
|
|
1168
1490
|
visual_delta_blocker = visual_delta_blocker_for_mode(verification_mode, visual_delta)
|
|
1169
1491
|
hard_blockers = [visual_delta_blocker] if visual_delta_blocker else []
|
|
1492
|
+
if verification_mode in PLAYABILITY_MODES and not supporting.get('playability_ready'):
|
|
1493
|
+
assessment = supporting.get('playability_assessment') or {}
|
|
1494
|
+
concerns = assessment.get('concerns') if isinstance(assessment, dict) else []
|
|
1495
|
+
detail = '; '.join(str(item) for item in concerns[:4]) if isinstance(concerns, list) else ''
|
|
1496
|
+
hard_blockers.append(
|
|
1497
|
+
'playability evidence blocks ready_to_ship for playable/gameplay proof'
|
|
1498
|
+
+ (f': {detail}' if detail else '.')
|
|
1499
|
+
)
|
|
1170
1500
|
|
|
1171
1501
|
return {
|
|
1172
1502
|
'status': 'needs_supervising_agent_assessment',
|
|
@@ -1191,6 +1521,7 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
|
|
|
1191
1521
|
'Use semantic_context.route plus headings/buttons/text anchors to ground route and content judgment before treating a screenshot as wrong-route.',
|
|
1192
1522
|
'For visual/UI modes, use screenshots plus after_observation.details.visible_text_sample, headings, buttons, links, canvas_count, and large_visible_elements to explain what the proof actually shows.',
|
|
1193
1523
|
'For visual/UI polish, capture success is not proof. If visual_delta.status is unmeasured, missing, not_applicable, or measured with passed=false, choose needs_implementation or needs_richer_proof instead of ready_to_ship.',
|
|
1524
|
+
'For playable/gameplay proof, screenshots are supporting evidence only. Do not mark ready_to_ship unless playability_assessment.passed is true and the proof shows accepted input, state/time progression, and playfield/canvas pixel motion.',
|
|
1194
1525
|
'For data/audio/log/metrics/custom modes, judge the structured evidence bundle and proof_evidence_sample directly; screenshots are optional supporting context.',
|
|
1195
1526
|
'The summary must name the concrete change, the target route/UI, what changed in after evidence, and why the stop condition is satisfied.',
|
|
1196
1527
|
'Only set escalation_target=human when you conclude the workflow has hit a real wall or is not converging.',
|
|
@@ -6,9 +6,6 @@ import {
|
|
|
6
6
|
normalizeRunParams,
|
|
7
7
|
setRunStatus
|
|
8
8
|
} from "./chunk-OASB3CYU.js";
|
|
9
|
-
import {
|
|
10
|
-
visualDeltaShipGateReason
|
|
11
|
-
} from "./chunk-4YCWZVBN.js";
|
|
12
9
|
import {
|
|
13
10
|
applyTerminalMetadata,
|
|
14
11
|
compactRecord,
|
|
@@ -17,6 +14,9 @@ import {
|
|
|
17
14
|
normalizeTerminalMetadata,
|
|
18
15
|
recordValue
|
|
19
16
|
} from "./chunk-J2MERROF.js";
|
|
17
|
+
import {
|
|
18
|
+
visualDeltaShipGateReason
|
|
19
|
+
} from "./chunk-4YCWZVBN.js";
|
|
20
20
|
|
|
21
21
|
// src/engine-harness.ts
|
|
22
22
|
import { execFileSync } from "child_process";
|