"""Bounded SDK selection and project inspection, not a compiler or .NET host."""
import hashlib,json,re,xml.etree.ElementTree as ET

def digest(value):return hashlib.sha256(json.dumps(value,ensure_ascii=False,sort_keys=True,separators=(',',':')).encode()).hexdigest()
def version(value):
    if not isinstance(value,str) or not re.fullmatch(r'(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)',value):raise ValueError('Use a full stable major.minor.patch version.')
    return tuple(map(int,value.split('.')))
def resolve_sdk(installed,global_json):
    if not isinstance(installed,list) or not all(isinstance(v,str) for v in installed):raise ValueError('installed_sdks must be a list of stable versions.')
    parsed=[(version(v),v) for v in installed]
    if not isinstance(global_json,dict) or set(global_json)!={'sdk'} or not isinstance(global_json['sdk'],dict):raise ValueError('The model accepts one explicit sdk object.')
    config=global_json['sdk']
    if set(config)!={'version','rollForward','allowPrerelease'} or config['allowPrerelease'] is not False:raise ValueError('Explicit stable-only selection is required.')
    requested=version(config['version']);policy=config['rollForward']
    if policy=='disable':candidates=[v for t,v in parsed if t==requested]
    elif policy=='latestPatch':candidates=[v for t,v in parsed if t[:2]==requested[:2] and t[2]//100==requested[2]//100 and t>=requested]
    else:raise ValueError('UNSUPPORTED: this teaching model implements only disable and latestPatch.')
    return max(candidates,key=version) if candidates else None

def inspect_project(xml):
    root=ET.fromstring(xml)
    if root.tag!='Project':raise ValueError('Expected Project XML.')
    result={}
    for name in ['TargetFramework','LangVersion','Nullable','TreatWarningsAsErrors']:
        nodes=root.findall('./PropertyGroup/'+name)
        if len(nodes)!=1:raise ValueError('One unconditioned '+name+' required by this bounded inspector.')
        node=nodes[0]
        if node.attrib or any('Condition' in g.attrib for g in root.findall('./PropertyGroup')):raise ValueError('Conditional MSBuild evaluation is outside this inspector.')
        result[name]=node.text
    return result

def field_changes(before,after):
    return [k for k in sorted(set(before)|set(after)) if json.dumps(before.get(k),sort_keys=True)!=json.dumps(after.get(k),sort_keys=True)]
