"""MSI protocol v0.2 reference calculator. Technical tool, not scientific validation.
Usage: python calculator-v0.2.py profile.json
Input: {"jurisdiction":"...", "period":"2025", "basketVersion":"...", "observations":[...]}
Each observation: id, numerator, denominator, evidenceGrade A/B, source, period,
assessor, reviewer, adjudication; optional lower and upper percentages.
Missing entries yield coverage without a composite. No imputation or country lookup.
"""
import json,math,random,sys
IDS=[p+str(n) for p in ['G','S','M','C','I','T','P','R','H','F'] for n in range(1,4)]
DOMAINS=['Governance','Science','Manufacturing','Supply chain','Investment','Technology','Geopolitics','Regulation','People','Future dynamics']
def finite(x):return isinstance(x,(int,float)) and not isinstance(x,bool) and math.isfinite(x)
def calculate(profile):
 for key in ['jurisdiction','period','basketVersion']:
  if not isinstance(profile.get(key),str) or not profile[key].strip():raise ValueError('Missing '+key)
 if len(profile['period'])!=4 or not profile['period'].isdigit():raise ValueError('Invalid observation year')
 rows=profile.get('observations',[])
 if not isinstance(rows,list):raise ValueError('Observations must be a list')
 values={};bounds={};seen=set();excluded=[]
 for row in rows:
  id=row.get('id')
  if id not in IDS or id in seen:raise ValueError('Unknown or duplicate indicator')
  seen.add(id)
  n,d=row.get('numerator'),row.get('denominator')
  if n is None or d is None:excluded.append(id);continue
  if not finite(n) or not finite(d) or d<=0 or n<0 or n>d:raise ValueError('Invalid numerator or denominator '+id)
  if row.get('evidenceGrade') not in ['A','B']:excluded.append(id);continue
  for field in ['source','assessor','reviewer','adjudication']:
   if not isinstance(row.get(field),str) or not row[field].strip():raise ValueError('Missing evidence metadata '+id+' '+field)
  if row['assessor'].strip().casefold()==row['reviewer'].strip().casefold():raise ValueError('Distinct second assessor required')
  if row.get('period')!=profile['period']:raise ValueError('Mixed observation years')
  value=100*n/d;lo=row.get('lower');hi=row.get('upper')
  if (lo is None)!=(hi is None):raise ValueError('Both uncertainty bounds required')
  if lo is not None:
   if not finite(lo) or not finite(hi) or not 0<=lo<=value<=hi<=100:raise ValueError('Invalid uncertainty bounds')
   bounds[id]=(lo,hi)
  values[id]=value
 domain={name:(sum(values[id] for id in IDS[k*3:k*3+3])/3 if all(id in values for id in IDS[k*3:k*3+3]) else None) for k,name in enumerate(DOMAINS)}
 complete=len(values)==30
 return dict(version='0.2',status='Research calculation only; independent validation pending',jurisdiction=profile['jurisdiction'],period=profile['period'],basketVersion=profile['basketVersion'],coverage=len(values)/30,missing=[id for id in IDS if id not in values],excluded=excluded,indicators=values,domains=domain,composite=sum(values.values())/30 if complete else None,minimumDomain=min(domain.values()) if complete else None,provisionalFloorFlags=[name for name,v in domain.items() if v is not None and v<40],measurementBounds=([sum(x[0] for x in bounds.values())/30,sum(x[1] for x in bounds.values())/30] if complete and len(bounds)==30 else None))
def weight_sensitivity(domain_scores,draws=10000,seed=20261022):
 if len(domain_scores)!=10 or any(not finite(v) or not 0<=v<=100 for v in domain_scores):raise ValueError('Ten complete domain scores required')
 if not isinstance(draws,int) or draws<1:raise ValueError('Positive draw count required')
 rng=random.Random(seed);values=[]
 for _ in range(draws):
  weights=[rng.uniform(.5,1.5) for _ in range(10)];values.append(sum(w*s for w,s in zip(weights,domain_scores))/sum(weights))
 values.sort()
 return dict(seed=seed,draws=draws,p05=values[int((draws-1)*.05)],p95=values[int((draws-1)*.95)],minimum=values[0],maximum=values[-1],interpretation='Weight scenarios only; not confidence intervals')
def alternatives(scores):
 weight_sensitivity(scores,1)
 return dict(arithmetic=sum(scores)/10,geometric=(0 if 0 in scores else math.exp(sum(math.log(s) for s in scores)/10)),leaveOneDomainOut=[sum(scores[:k]+scores[k+1:])/9 for k in range(10)])
if __name__=='__main__':
 if len(sys.argv)!=2:raise SystemExit('Usage: python calculator-v0.2.py profile.json')
 result=calculate(json.load(open(sys.argv[1],encoding='utf-8')))
 if result['composite'] is not None:
  scores=list(result['domains'].values());result['weightSensitivity']=weight_sensitivity(scores);result['aggregationSensitivity']=alternatives(scores)
 print(json.dumps(result,indent=2,allow_nan=False))
