import hashlib,importlib.util,json,os,pathlib,shutil,signal,subprocess,sys,tarfile,time
os.umask(0o077)
spec=importlib.util.spec_from_file_location('old','/tmp/codex/ltb1021/2026-09-11_contextra-stop60_r01/prepare.py')
old=importlib.util.module_from_spec(spec);spec.loader.exec_module(old);g=old.g
R=pathlib.Path(__file__).parent;HEAD='0667b1dcbb0b7aa1ea14a2630dacb1ee809f51be'
EDITED=['app/api/streams.py','app/multi_pipeline.py','app/start_diagnostics.py','app/stream_lifecycle.py','app/stream_manager.py',
        'tests/test_cleanup_boundary.py','tests/test_stream_apply.py','tests/test_stream_manager_lifecycle.py','tests/sdk_stop_drop_probe.py','tests/test_async_restart.py']
def fingerprint():return {p:g.sha(g.REPO/p) for p in EDITED}
def source_cas():
 assert g.sh(['git','-C',str(g.REPO),'rev-parse','HEAD']).strip()==HEAD
 assert fingerprint()==json.loads((R/'source-manifest.json').read_text())['edited']
def gate():
 assert not (R/'gate.json').exists()
 assert g.sh(['git','-C',str(g.REPO),'rev-parse','HEAD']).strip()==HEAD
 assert sorted(l[3:] for l in g.sh(['git','-C',str(g.REPO),'status','--porcelain']).splitlines())==sorted(EDITED)
 assert not g.sh(['git','-C',str(g.REPO),'diff','--cached','--name-only']).strip()
 root=g.sh(['docker','info','--format','{{.DockerRootDir}}']).strip()
 free=min(shutil.disk_usage('/').free,shutil.disk_usage(root).free);assert free>=15*1024**3
 assert subprocess.run(['pgrep','-f',r'(^|/)docker (build|buildx build|pull)'],capture_output=True).returncode==1
 c=g.inspect();assert c['State']['Status']=='exited' and not c['State']['OOMKilled'] and c['RestartCount']==0
 assert c['Image']=='sha256:4940f9484c95fb87b3f3d811ea049d1e2c04f6a2db8629cafb0b36fa7f0fa944'
 assert c['HostConfig']['RestartPolicy']=={'Name':'on-failure','MaximumRetryCount':3}
 e=g.load_env_file(str(g.ENV));overlay=pathlib.Path(e['CONTEXTRA_RUNTIME_STATE_ROOT'])/'streams_runtime.yaml'
 assert e['CONTEXTRA_IMAGE']=='contextra-analytics:stop60-769ed3a-2c780681'
 assert g.sha(g.CFG)=='5e5a4d22786f8b5218ac821379fbec091d9e3006f62c880ee3e3da5ced2f0e0b'
 assert g.sha(g.CFG.parent/'2/model.plan')=='5fdf1e9c55ab947a3b7d5f7f6bdceaf1e36a9e52efdf86f5a15d3b99b8631d1e'
 assert g.sha(overlay)=='ff73d70010b7dbd2c0f0edba2fc9a389b151910bc74abc4b0ba80ab2fe3ac10e'
 rows=g.yaml.safe_load(overlay.read_text())['added'];assert len(rows)==1;s=rows[0]
 assert all(s[x] is False for x in ['store','active_learning','autostart']) and s['scenarios']=='off'
 assert s['inference_profile']['roles']==['det'] and s['inference_profile']['det_model']=='RFDETRMedium'
 mem={l.split(':')[0]:int(l.split()[1]) for l in pathlib.Path('/proc/meminfo').read_text().splitlines()}
 assert mem['MemAvailable']>10*1024**2 and g.gpu()<16384
 edited=fingerprint();digest=hashlib.sha256(json.dumps(edited,sort_keys=True).encode()).hexdigest()
 tag='contextra-analytics:restart-0667b1d-'+digest[:8]
 assert subprocess.run(['docker','image','inspect',tag],capture_output=True).returncode!=0
 from tools.validate_deployment import validate_deployment_inputs
 validate_deployment_inputs(str(g.ENV))
 snap=R/'snapshot';snap.mkdir(mode=0o700)
 p=subprocess.Popen(['git','-C',str(g.REPO),'archive',HEAD],stdout=subprocess.PIPE)
 with tarfile.open(fileobj=p.stdout,mode='r|') as archive:archive.extractall(snap,filter='data')
 assert p.wait()==0
 for path in EDITED:shutil.copyfile(g.REPO/path,snap/path)
 assert fingerprint()==edited
 (R/'source-manifest.json').write_text(json.dumps(dict(base=HEAD,edited=edited,digest=digest),indent=2))
 (R/'source-hashes.json').write_text(json.dumps({str(p.relative_to(snap)):g.sha(p) for p in (snap/'app').rglob('*.py')},indent=2))
 (R/'diagnostics').mkdir(mode=0o700)
 out=dict(head=HEAD,tag=tag,source_digest=digest,container_id=c['Id'],old_image=c['Image'],old_tag=e['CONTEXTRA_IMAGE'],
          env_hash=g.sha(g.ENV),config_hash=g.sha(g.CFG),plan_hash=g.sha(g.CFG.parent/'2/model.plan'),overlay_hash=g.sha(overlay),
          original_restart=c['HostConfig']['RestartPolicy'],other_containers=g.others(),gpu_mib=g.gpu(),docker_root=root,disk_free_bytes=free)
 (R/'gate.json').write_text(json.dumps(out,indent=2));print(json.dumps({k:out[k] for k in ['tag','disk_free_bytes','gpu_mib','source_digest']}))
def build():
 source_cas();assert not (R/'build.json').exists()
 assert json.loads((R/'source-verify.json').read_text())['status']=='PASS'
 data=json.loads((R/'gate.json').read_text());start=time.monotonic();reason=None
 with (R/'build.log').open('w') as log:
  p=subprocess.Popen(['docker','build','-f','docker/Dockerfile','--target','app','-t',data['tag'],'.'],cwd=R/'snapshot',stdout=log,stderr=log,start_new_session=True,env=dict(os.environ,DOCKER_BUILDKIT='1'))
  while p.poll() is None:
   if time.monotonic()-start>1200 or min(shutil.disk_usage('/').free,shutil.disk_usage(data['docker_root']).free)<15*1024**3:
    reason='TIME_OR_DISK_CAP';os.killpg(p.pid,signal.SIGTERM)
    try:p.wait(15)
    except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL)
    break
   time.sleep(2)
  code=p.wait()
 out=dict(returncode=code,abort_reason=reason,elapsed_s=round(time.monotonic()-start,2),tag=data['tag'])
 if code==0 and reason is None:out['image_id']=json.loads(g.sh(['docker','image','inspect',data['tag']]))[0]['Id']
 (R/'build.json').write_text(json.dumps(out,indent=2));print(json.dumps(out));assert code==0 and reason is None
if __name__=='__main__':{'gate':gate,'build':build}[sys.argv[1]]()
