from __future__ import annotations
REQUIRED=['assessment_id','competency_id','item_id','item_alignment','item_difficulty_band','response_state','evidence_weight','estimate_before','estimate_after','uncertainty_band','selection_rule_id','next_item_or_route','stop_condition','accommodation_ref','mastery_effect','review_state','provenance']
UNKNOWN={'UNKNOWN','STALE','INCONCLUSIVE','UNVERIFIED','MISSING',''}
def empty(v): return v is None or (isinstance(v,str) and v.strip().upper() in UNKNOWN)
def evaluate(x):
    missing=[k for k in REQUIRED if k not in x]
    if missing:return {'state':'HOLD_INSUFFICIENT_EVIDENCE','reason':'MISSING_FIELDS','missing':missing}
    if x.get('baseline_oracle_reuse'):return {'state':'HOLD_BASELINE_ORACLE_REUSE','reason':'BASELINE_DECISION_REUSE_FORBIDDEN'}
    if empty(x.get('assessment_id')) or empty(x.get('competency_id')):return {'state':'HOLD_COMPETENCY_BINDING','reason':'ASSESSMENT_AND_COMPETENCY_REQUIRED'}
    if empty(x.get('item_id')) or str(x.get('item_alignment','')).upper() not in {'TARGET_PREREQUISITE','TARGET_CORE','TARGET_TRANSFER'} or x.get('alignment_valid') is False:return {'state':'HOLD_ITEM_ALIGNMENT_RECOMPUTE','reason':'ITEM_MUST_ALIGN_TO_TARGET_COMPETENCY'}
    if str(x.get('item_difficulty_band','')).upper() not in {'EASY','MEDIUM','HIGH'}:return {'state':'HOLD_ITEM_DIFFICULTY','reason':'KNOWN_DIFFICULTY_BAND_REQUIRED'}
    if str(x.get('response_state','')).upper() not in {'CORRECT','INCORRECT','PARTIAL'}:return {'state':'HOLD_RESPONSE_STATE','reason':'TYPED_RESPONSE_REQUIRED'}
    try:w=float(x.get('evidence_weight'))
    except Exception:return {'state':'HOLD_EVIDENCE_WEIGHT','reason':'NUMERIC_EVIDENCE_WEIGHT_REQUIRED'}
    if not (0 < w <= 1):return {'state':'HOLD_EVIDENCE_WEIGHT','reason':'EVIDENCE_WEIGHT_OUT_OF_RANGE'}
    try: eb=float(x.get('estimate_before')); ea=float(x.get('estimate_after'))
    except Exception:return {'state':'HOLD_ESTIMATE_RANGE','reason':'NUMERIC_ESTIMATES_REQUIRED'}
    if not (0 <= eb <= 1 and 0 <= ea <= 1):return {'state':'HOLD_ESTIMATE_RANGE','reason':'ESTIMATE_OUT_OF_RANGE'}
    if x.get('estimate_trace_valid') is False:return {'state':'HOLD_ESTIMATE_TRACE','reason':'ESTIMATE_MUST_TRACE_TO_VISIBLE_RESPONSE_EVIDENCE'}
    ub=str(x.get('uncertainty_band','')).upper()
    if ub not in {'LOW','MEDIUM','HIGH'}:return {'state':'HOLD_UNCERTAINTY','reason':'KNOWN_UNCERTAINTY_REQUIRED'}
    if empty(x.get('selection_rule_id')):return {'state':'HOLD_SELECTION_RULE','reason':'EXPLICIT_SELECTION_RULE_REQUIRED'}
    if empty(x.get('next_item_or_route')):return {'state':'HOLD_NEXT_ACTION','reason':'NEXT_ITEM_OR_ROUTE_REQUIRED'}
    stop=str(x.get('stop_condition','')).upper()
    if stop not in {'CONTINUE','STOP_BUDGET','STOP_SUFFICIENT_EVIDENCE'}:return {'state':'HOLD_STOP_CONDITION','reason':'KNOWN_STOP_CONDITION_REQUIRED'}
    if x.get('transfer_context_changed') and not x.get('decision_recomputed'):return {'state':'HOLD_TRANSFER_NOT_RECOMPUTED','reason':'CHANGED_EVIDENCE_COMPOSITION_REQUIRES_RECOMPUTATION'}
    if x.get('accommodation_changed') and x.get('evidence_semantics_changed_only_due_to_accommodation'):return {'state':'HOLD_ACCOMMODATION_SEMANTICS','reason':'ACCOMMODATION_MUST_NOT_SILENTLY_MUTATE_EVIDENCE_SEMANTICS'}
    if x.get('accommodation_changed') and x.get('underlying_response_unchanged') and x.get('weight_or_estimate_changed_without_evidence_change'):return {'state':'HOLD_ACCOMMODATION_SEMANTICS','reason':'UNCHANGED_RESPONSE_WITH_ACCOMMODATION_CANNOT_ALONE_REWEIGHT_EVIDENCE'}
    me=str(x.get('mastery_effect','')).upper()
    if me in {'MASTERED','PROMOTE_MASTERED','AUTO_PROMOTE_MASTERED','AUTO_MASTERED'} or me.startswith('PROMOTE_') or me.startswith('AUTO_PROMOTE_'):return {'state':'HOLD_MASTERY_AUTOPROMOTION','reason':'DIAGNOSTIC_ESTIMATE_CANNOT_SILENTLY_PROMOTE_MASTERY'}
    if str(x.get('review_state','')).upper() not in {'REVIEW_REQUIRED','REVIEW_READY'}:return {'state':'HOLD_REVIEW_STATE','reason':'SEPARATE_REVIEW_STATE_REQUIRED'}
    if empty(x.get('provenance')):return {'state':'HOLD_PROVENANCE','reason':'TRACEABLE_PROVENANCE_REQUIRED'}
    if stop=='STOP_BUDGET' and ub!='LOW':return {'state':'DIAGNOSTIC_STOP_REVIEW_REQUIRED','reason':'STOP_BUDGET_REACHED_BEFORE_DECISIVE_LOW_UNCERTAINTY_EVIDENCE'}
    if ub=='HIGH':return {'state':'DIAGNOSTIC_CONTINUE_ASSESSMENT','reason':'HIGH_UNCERTAINTY_REQUIRES_MORE_EVIDENCE'}
    nxt=str(x.get('next_item_or_route','')).lower()
    if 'repair' in nxt:return {'state':'DIAGNOSTIC_REPAIR_ROUTE_REQUIRED','reason':'VISIBLE_EVIDENCE_ROUTES_TO_REPAIR'}
    if stop=='STOP_SUFFICIENT_EVIDENCE' and ub=='LOW':return {'state':'DIAGNOSTIC_EVIDENCE_REVIEW_READY','reason':'DIAGNOSTIC_EVIDENCE_SUFFICIENT_FOR_SEPARATE_REVIEW_NOT_AUTOMATIC_MASTERY'}
    return {'state':'DIAGNOSTIC_CONTINUE_ASSESSMENT','reason':'CONTINUE_UNDER_EXPLICIT_SELECTION_RULE'}
