"""Separate complete-chunk compute proxies from allocation/wait time.
Does not inspect correctness outcomes or native text.
"""
import json,pathlib,re,collections,datetime
ROOT=pathlib.Path(__file__).resolve().parents[1]
c=json.loads((ROOT/'notes/stable_contextual_contract.json').read_text());root=ROOT/c['output'];result={'scope':'Completed jobs only; logical matrix/attention arithmetic proxy excludes norms, embeddings, nonlinearities, transfers, optimizer, grader and serialization. Checkpoint recomputation charged. Not measured hardware FLOPs.','training':{},'evaluation':{}}
# Supplied28-layer trunk: Q16heads×128, KV8heads×128, modeldim1024, MLP3072.
# Read constants from AST-free JSON-backed model declaration only via source
# import is unnecessary: bind explicitly and verify at final reproduction.
layers=28;emb=1024;query=16*128;kv=8*128;hidden=3072;vocab=151936
trunk=layers*emb*(2*query+2*kv+3*hidden);head=emb*vocab
assert trunk+2*head+28*(2*1024+2*128)+1024==751632384

def proxy(work,evaluation=False):
 if evaluation:positions=work['trunk_positions'];heads=work['head_positions'];pairs=work['attention_pairs']
 else:
  positions=heads=pairs=0
  for prefix,factor in [('generation_',1),('support_',1),('teacher_',1),('retention_',1),('backward_',4),('diagnostic_support_',1),('diagnostic_teacher_',1),('diagnostic_backward_',4)]:
   positions+=factor*work.get(prefix+'trunk_positions',0);heads+=factor*work.get(prefix+'head_positions',0);pairs+=factor*work.get(prefix+'attention_pairs',0)
 return {'matrix_flops_proxy':2*trunk*positions+2*head*heads,'dense_attention_flops_proxy':4*layers*query*pairs,'weighted_trunk_positions':positions,'weighted_head_positions':heads,'weighted_attention_pairs':pairs}
for arm in ['G','F','E']:
 summaries=sorted((root/arm).glob('chunk*_summary.json')) if (root/arm).exists() else []
 if not summaries:continue
 reports=[json.loads(p.read_text()) for p in summaries];last=reports[-1];assert all(s['status']=='COMPLETE' for s in reports)
 allocation=0.;waitelapsed=0.;logpaths=[]
 for s in reports:
  log=ROOT/'logs'/f"stable_contextual_train_{arm}_{s['chunk']}.log";text=log.read_text();times=re.findall(r'Deallocated GPUs .*?duration: ([0-9.]+)s',text);assert len(times)==1;allocation+=float(times[0]);logpaths.append(str(log.relative_to(ROOT)))
 result['training'][arm]={'completed_steps':last['completed_steps'],'allocated_gpu_seconds':allocation,'worker_seconds':sum(s['seconds_including_setup_save'] for s in reports),'work':last['work'],'proxy':proxy(last['work']),'logs':logpaths}
for arm in ['W','G','F','E']:
 summaries=sorted((root/('eval_'+arm)).glob('chunk*_summary.json')) if (root/('eval_'+arm)).exists() else []
 if not summaries:continue
 w=collections.Counter();seconds=0.;allocated=0.
 for p in summaries:
  s=json.loads(p.read_text());assert s['status']=='COMPLETE';w.update(s['work']);seconds+=s['seconds'];log=ROOT/'logs'/f"stable_contextual_eval_{arm}_{s['chunk']}.log";times=re.findall(r'Deallocated GPUs .*?duration: ([0-9.]+)s',log.read_text());assert len(times)==1;allocated+=float(times[0])
 result['evaluation'][arm]={'completed_questions':256*len(summaries),'allocated_gpu_seconds':allocated,'worker_seconds':seconds,'work':dict(w),'proxy':proxy(w,True)}
attempts=[]
for pattern in ['stable_contextual_train_*.log','stable_contextual_eval_*.log']:
 for log in sorted((ROOT/'logs').glob(pattern)):
  times=re.findall(r'Deallocated GPUs .*?duration: ([0-9.]+)s',log.read_text())
  attempts.append({'log':str(log.relative_to(ROOT)),'allocated_gpu_seconds':sum(map(float,times)) if times else None,'completed_allocation_records':len(times)})
result['all_attempt_allocation_logs']=attempts
result['total_released_experiment_gpu_seconds']=sum(r['allocated_gpu_seconds'] or 0 for r in attempts)
result['allocation_scope']='Includes released failed attempts if any; active/waiting allocations have null duration. Completed numerical work excludes failed incomplete chunks and must be supplemented from their retained logs.'
interruptions=[]
for recordpath in sorted((ROOT/'notes').glob('stable_contextual_interruption_*/interruption.json')):
 record=json.loads(recordpath.read_text());folder=recordpath.parent
 if record.get('stage')=='eval':
  rawpath=ROOT/record['retained_records'];raw=[json.loads(s) for s in rawpath.read_text().splitlines()]
  receipt=ROOT/record['allocation_receipt'];times=re.findall(r'Deallocated GPUs .*?duration: ([0-9.]+)s',receipt.read_text())
  assert len(times)==1 and float(times[0])==record['allocated_seconds']
  assert str(receipt.relative_to(ROOT)) not in {a['log'] for a in attempts}
  interruptions.append({'record':str(recordpath.relative_to(ROOT)),
   'stage':'eval','allocated_gpu_seconds':record['allocated_seconds'],
   'allocated_gpu_seconds_bounds':[record['allocated_seconds']]*2,
   'bounds_scope':'Exact helper release receipt, archived outside successful-attempt log glob and charged once. Partial unrecorded generation work is unknown, not zero.',
   'completed_but_uncheckpointed_updates':0,'retained_partial_draws':len(raw),
   'retained_generated_tokens_lower_bound':sum(len(r['tokens']) for r in raw),
   'completed_update_work_only':{},'completed_update_proxy_lower_bound':proxy({})})
  continue
 updates=[json.loads(s) for s in (folder/f"chunk{record['chunk']}_updates.jsonl").read_text().splitlines()]
 raw=[json.loads(s) for s in (folder/f"chunk{record['chunk']}_rollouts.jsonl").read_text().splitlines()]
 lost=collections.Counter()
 for update in updates:
  for group in update['groups']:lost.update(group['work'])
  if update.get('diagnostic'):
   lost.update({'diagnostic_'+k:v for k,v in update['diagnostic']['work'].items()})
 lower=max((u['seconds'] for u in updates),default=0.)
 upper=datetime.datetime.fromisoformat(record['detected_at']).timestamp()-record['queue_job_started']
 interruptions.append({'record':str(recordpath.relative_to(ROOT)),
  'allocated_gpu_seconds':record['allocated_seconds'],
  'allocated_gpu_seconds_bounds':[lower,upper],
  'bounds_scope':'Worker elapsed through last complete update is a lower bound on allocation; detection minus queue job start is a conservative upper bound including possible wait. Exact exit/allocation receipt unavailable.',
  'completed_but_uncheckpointed_updates':len(updates),
  'retained_partial_draws':len(raw),
  'retained_generated_tokens_lower_bound':sum(len(r['tokens']) for r in raw),
  'completed_update_work_only':dict(lost),
  'completed_update_proxy_lower_bound':proxy(lost)})
result['interrupted_attempts']=interruptions
result['total_released_plus_interrupted_gpu_seconds_bounds']=[result['total_released_experiment_gpu_seconds']+sum(i['allocated_gpu_seconds_bounds'][j] for i in interruptions) for j in (0,1)]
result['total_bounds_scope']='Released jobs and archived interrupted attempts only; excludes active/waiting allocations, readiness/probes and shared ancestry. Interrupted numerical work is additional and incomplete.'
result['new_readiness_gpu_seconds']=57.0
result['fresh_warm_gpu_seconds']=460.1
result['fresh_warm_generated_tokens']=130726
result['prior_costs']='Common warm training and historical source/teacher/pilot costs remain additional sunk research costs; current training generates new own rollouts, reuses no historical training targets except fixed retention audit. See strategic/pilot ledgers.'
path=ROOT/'notes/stable_contextual_work_accounting.json';path.write_text(json.dumps(result,indent=2));print(json.dumps({k:v for k,v in result.items() if k not in ['training','evaluation']},indent=2))
