import ast
import datetime
import json
import os
from pathlib import Path
import re
import subprocess

os.umask(0o077)
HERE=Path(__file__).parent
REPO=Path('/workspace/contextra-analytics-runtime-lifecycle-refactor')
JOB='20260918-contextra-web-start-prep-r01'
ROOT=Path('/tmp/codex/ltb1021/git-closeout')/JOB
def cmd(args):
    p=subprocess.run(args,text=True,capture_output=True,timeout=90)
    if p.returncode:raise RuntimeError(p.stdout+p.stderr)
    return p.stdout
def save(path,value):
    with path.open('x') as f:json.dump(value,f,indent=2,ensure_ascii=False)
    path.chmod(0o600)
before=json.loads((HERE/'before.json').read_text())
after=json.loads((HERE/'after.json').read_text())
for observation in (before,after):
    for container in observation['containers'].values():
        container['mounts']=sorted(container['mounts'],key=lambda m:m['Destination'])
for key in ('containers','states','ledger','files','neighbor','firewall'):
    assert before[key]==after[key],key
save(HERE/'preservation.json',dict(status='PASS',compared=['container id/image/start/PID/config/mounts',
    'channel state/run/PID','Event ledger fingerprint','existing config hashes','network rules'],
    note='mount arrays normalized by Destination; no runtime mutation'))
assert not cmd(['git','-C',str(REPO),'diff','--cached','--name-only']).strip()
paths=cmd(['git','-C',str(REPO),'diff','--name-only']).splitlines()+cmd(['git','-C',str(REPO),'ls-files','--others','--exclude-standard']).splitlines()
assert len(paths)==13,paths
cmd(['git','-C',str(REPO),'diff','--check'])
for name in paths:
    f=REPO/name
    if f.suffix=='.py':ast.parse(f.read_text())
    if f.suffix=='.md':
        for target in re.findall(r'\]\(([^)]+)\)',f.read_text()):
            if '://' in target or target.startswith('#'):continue
            assert (f.parent/target.split('#',1)[0]).exists(),(name,target)
receipt=dict(schema_version='ltb1021.codex_hub.report_receipt.v1',receipt_id=JOB,
    timestamp=datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).isoformat(),
    session_alias='work-session',task='Contextra Web Start 신규 등록과 운영 Artifact 설정 사전 준비',
    results=['신규 등록 기본 template과 별도 배포 overlay, Pose/Fire 준비 preset을 만들었다.',
             'ROI 미지정 정규화와 신규 camera ID 전송 정책을 보완하고 Analytics image를 준비했다.',
             '기존 container/채널 run/PID/설정/Event 원장과 네트워크 규칙을 보존했다.'],
    verify=['외부 연결 없는 표적22개PASS, image-owned source hash와 최소 계약PASS.',
            'Compose 차이 검사와 preservation PASS; 실제 Runtime 교체/채널 시험/POST 없음.'],
    commits=[],remaining=['별도 승인 후 후보 반영 및 실제 Web 신규 등록/Start/Stop/Event 시험'])
save(HERE/'report-receipt.json',receipt)
print(cmd(['codex-report-receipt','--profile','ltb1021','--session-alias','work-session','--receipt',str(HERE/'report-receipt.json')]))
portfolio=HERE/'portfolio';portfolio.mkdir(mode=0o700)
print(cmd(['python3','/workspace/users/root/codex-ops-docs/servers/test/users/ltb1021/portfolio_logs/scripts/create_portfolio_candidate.py',
    '--run-dir',str(portfolio),'--confidence','MEDIUM','--repo','contextra-analytics',
    '--track-key','contextra-analytics:ops:deployment:reproducible-deployment','--closeout-job',JOB,
    '--evidence-state','implemented','--confidentiality','internal',
    '--reason','사전 구현/후보 image와 보호 fixture 완료; 실제 Web 신규 등록 수용은 대기',
    '--verify','22 targeted tests, image source/contract, Compose and runtime preservation PASS; no live test']))
ROOT.mkdir(parents=True,mode=0o700)
mf=ROOT/'manifest.json'
args=['codex-git-closeout','prepare','--repository',str(REPO),'--output',str(mf),'--job-id',JOB,
    '--profile','ltb1021','--session-alias','work-session',
    '--title','feat(web-start): 신규 카메라 기본 설정과 연동시험 후보 준비',
    '--message','Why=Web Start 전체 설정으로 신규 카메라를 등록할 준비와 운영 Artifact 기본값을 현재 Runtime과 분리한다.',
    '--message','What=Medium 기본/Pose·Fire 준비 template, opt-in Compose overlay, ROI 누락·null 정규화와 시험용 전송 ID 제한 해제를 연결했다.',
    '--message','Impact=기존 Start/저장/no-op/CAS와 인증 경계를 재사용한다. source 및 Analytics 후보만 준비했고 현재 container/채널/Event 원장은 보존했다.',
    '--message','Verify=외부 연결 없는 표적22개PASS, image-owned import/hash 및 최소 계약PASS, Compose 허용 차이/설정·PID·원장 보존/AST·문서link·diff 검사PASS. 실제 Web 시험은 미실행이다.',
    '--message','Rollback=Runtime 반영 없음. 기존 활성 설정은 그대로 유지하며 준비 후보는 별도 경로에 보존한다. 자동 rollback/재Start는 하지 않는다.',
    '--verify','22 targeted tests PASS; image-owned source hashes and minimal ROI/delivery contract PASS',
    '--verify','Compose difference allowlist and runtime/config/ledger preservation PASS; no live Start/Stop or POST',
    '--verify','AST, Markdown links, git diff --check PASS', '--expires-in-seconds','86400']
for path in sorted(paths):args+=['--path',path]
print(cmd(args))
plan=json.loads(mf.read_text())['plan_hash']
batch=ROOT/'batch.json'
print(cmd(['codex-git-closeout','batch-prepare','--output',str(batch),'--batch-id',JOB,
    '--profile','ltb1021','--session-alias','work-session','--expected-group-count','1',
    '--repository-label','contextra-analytics','--manifest',str(mf),'--expect-plan-hash',plan,
    '--expires-in-seconds','86400']))
print('BATCH_HASH',json.loads(batch.read_text())['plan_hash'])
