def evaluate(c):
    patch_class=c.get('patch_class') or 'UNKNOWN'
    rollback_ready=bool(c.get('rollback_target')) and c.get('rollback_feasibility')=='VERIFIED'
    abort_active=bool(c.get('abort_signal_active'))
    window=int(c.get('observation_window_minutes') or 0)
    latency=int(c.get('minimum_failure_latency_minutes') or 0)
    window_complete=window>=latency and window>0
    promotion_allowed=False
    reason='ALL_GATES_CLEAR'
    state='PROMOTE'
    if c.get('policy_blocked'):
        state='BLOCKED'; reason='POLICY_BLOCK'
    elif int(c.get('evidence_age_days') or 0)>45:
        state='STALE'; reason='STALE_RELEASE_EVIDENCE'
    elif c.get('unrelated_repairs') or patch_class=='COMPOSITE_UNBOUNDED':
        state='HOLD'; reason='UNATTRIBUTABLE_MIXED_REPAIRS'
    elif not c.get('reproduction_ref'):
        state='HOLD'; reason='REPRODUCTION_REF_MISSING'
    elif not c.get('change_set_hash'):
        state='UNKNOWN'; reason='CHANGE_SET_IDENTITY_MISSING'
    elif not rollback_ready:
        state='HOLD'; reason='ROLLBACK_NOT_VERIFIED'
    elif not c.get('canary_cohort') or not c.get('control_cohort'):
        state='HOLD'; reason='CANARY_CONTROL_MISSING'
    elif not c.get('promotion_gate_defined') or not c.get('rollback_trigger_defined'):
        state='HOLD'; reason='RELEASE_GATE_MISSING'
    elif abort_active:
        state='ROLLBACK'; reason='ABORT_SIGNAL_ACTIVE'
    elif not c.get('error_budget_guard_clear'):
        state='ROLLBACK'; reason='ERROR_BUDGET_GUARD_VIOLATED'
    elif not window_complete:
        state='INCONCLUSIVE'; reason='OBSERVATION_WINDOW_TOO_SHORT'
    elif not c.get('success_signals_clear'):
        state='HOLD'; reason='SUCCESS_SIGNAL_NOT_CLEAR'
    elif not c.get('post_change_verification') or c.get('post_change_verification')=='PENDING':
        state='HOLD'; reason='POST_CHANGE_VERIFICATION_PENDING'
    else:
        promotion_allowed=True
    return {
      'rollout_state':state,
      'patch_class':patch_class,
      'window_minutes':window,
      'minimum_failure_latency_minutes':latency,
      'rollback_ready':rollback_ready,
      'promotion_allowed':promotion_allowed,
      'abort_active':abort_active,
      'control_reason':reason
    }
