"""Offline verification and publication; no additional target requests.
python3 publish.py --private-output runner/data/puppeteer-public-30-20260930 --canary runner/data/puppeteer-canary-20260930
"""
import argparse,ast,hashlib,json
from collections import Counter
from datetime import datetime,timezone
from pathlib import Path
HERE=Path(__file__).resolve().parent
sha=lambda data:hashlib.sha256(data).hexdigest()
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--private-output',type=Path,required=True);p.add_argument('--canary',type=Path,required=True);args=p.parse_args()
manifest=json.loads((HERE.parent/'public-30-v1.json').read_text())
def nodes(path):
 tree=ast.parse(path.read_text());return [n for n in tree.body if getattr(n,'name',None) in ['PageText','evaluate','result_class'] or isinstance(n,ast.Assign) and any(isinstance(t,ast.Name) and t.id in ['BLOCK_PHRASES','GATE_PHRASES'] for t in n.targets)]
reference=nodes(HERE.parent/'scrapling-public-30-20260929/run.py')
assert [ast.dump(n,include_attributes=False) for n in nodes(HERE/'run.py')]==[ast.dump(n,include_attributes=False) for n in reference]
ns={};exec('from html.parser import HTMLParser\nimport re',ns);exec(compile(ast.Module(body=reference,type_ignores=[]),'unchanged-checker','exec'),ns)
def verify(a,t,folder):
 body=(folder/(a['target_id']+'.html')).read_bytes()
 assert sha(body)==a['raw_html_sha256'] and len(body)==a['html_bytes']
 assert ns['evaluate'](body,t)==a['checks'] and ns['result_class'](a)==a['classification']
 assert a['usable_content']==(a['classification']=='usable') and a['empty_content']==(a['checks']['text_chars']==0)
 assert a['attempt_process_exit']==0 and not a['cleanup_surviving_owned_pids'] and a['request_started_at']
 readings=json.loads((folder/(a['target_id']+'-pss.json')).read_text())
 assert len(readings)==a['pss_samples'] and max(s['pss_mb'] for s in readings)==a['peak_sampled_tree_pss_mb']
 assert all(s['host_available_mb']>=3072 and s['pss_mb']<=4096 for s in readings)
 log=(folder/(a['target_id']+'-transport.json')).read_bytes();assert sha(log)==a['transport_log_sha256'];log=json.loads(log)
 assert log['requests'][0]['url']==t['url'] and all(r['method']=='GET' for r in log['requests'])
 sanitized=[{**r,'headers':{k:v for k,v in r['headers'].items() if k.lower() not in ['cookie','authorization','proxy-authorization']}} for r in log['requests']]
 assert sanitized==a['observed_request_headers'] and len(sanitized)==a['observed_document_request_count']
 assert [{k:r[k] for k in ['url','status']} for r in log['responses']]==a['document_responses']
 if log['responses']:assert log['responses'][-1]['status']==a['status']
 assert sha((folder/(a['target_id']+'-driver.log')).read_bytes())==a['driver_log_sha256']
 if a['screenshot_sha256']:assert sha((folder/(a['target_id']+'.png')).read_bytes())==a['screenshot_sha256']
 else:assert a['screenshot_error']
 assert a['initial_cookie_count']==0 and a['browser_version']=='Chrome/153.0.8010.12'
 assert '--enable-automation' in a['browser_launch_argv'] and '--no-proxy-server' in a['browser_launch_argv'] and '--no-sandbox' in a['browser_launch_argv']
 assert a['browser_observation']['webdriver'] and a['browser_observation']['viewport']=={'width':1280,'height':720}
 a['inspection_note']='Saved serialized DOM rechecked offline with unchanged rules: '+a['classification'].replace('_',' ')+'. This does not prove rendered visibility, complete field extraction or interaction.'
 if a['status']==200 and a['empty_content']:a['inspection_note']+=' HTTP 200 has no checked text.'
 if a['target_id']=='example':a['inspection_note']+=' Separate-letter markup and documentation-use wording are a known frozen-check limitation.'
 if a['target_id']=='emag':a['inspection_note']+=' RON currency stays outside the frozen $, £ or € listing pattern.'
 if a['target_id']=='target':
  a['visual_inspection']={'outcome':'verification_challenge','screenshot_sha256':a['screenshot_sha256'],'basis':'Private viewport PNG inspected; Quick verification / press and hold challenge. No interaction.'}
  a['inspection_note']+=' Private PNG shows a Quick verification challenge missed by the frozen soft-block phrases. Classification stays missing required content.'
 if a['target_id']=='instagram':
  a['visual_inspection']={'outcome':'cookie_consent_overlay_over_profile','screenshot_sha256':a['screenshot_sha256'],'basis':'Private viewport PNG inspected; cookie consent overlay covers the visible NASA profile. No interaction.'}
  a['inspection_note']+=' Private PNG shows a cookie consent overlay above the NASA profile while DOM checks pass; no consent was clicked.'
 if a['target_id']=='bestbuy':a['inspection_note']+=' Page.navigate exceeded the explicit five-second CDP protocol limit, despite a 20-second navigation deadline; DOM context was destroyed and PNG timed out. HTTP 200 is not a usable result.'
 a['request_header_observation_basis']='Puppeteer main-frame navigation events; sensitive header values removed. Not independent wire capture; browser recovery, subresources and page-script navigations are not application retries.'
 return readings
run=json.loads((args.private_output/'results.json').read_text());assert len(run['attempts'])==30 and run.get('finished_at') and not run.get('blocker')
assert [(a['target_id'],a['original_url']) for a in run['attempts']]==[(t['id'],t['url']) for t in manifest['targets']]
for field,file in [('script_sha256','run.py'),('worker_sha256','worker.mjs'),('lock_sha256','package-lock.json'),('package_sha256','package.json')]:assert run[field]==sha((HERE/file).read_bytes())
assert run['manifest_sha256']==sha((HERE.parent/'public-30-v1.json').read_bytes())
for name,digest in run['preserved_evidence_sha256'].items():assert sha(Path(name).read_bytes())==digest,name
samples={a['target_id']:verify(a,t,args.private_output) for a,t in zip(run['attempts'],manifest['targets'])}
assert run['summary']['classifications']==dict(Counter(a['classification'] for a in run['attempts']))
canary=json.loads((args.canary/'results.json').read_text());assert len(canary['attempts'])==1 and canary['script_sha256']==run['script_sha256'] and canary['worker_sha256']==run['worker_sha256'] and canary['config']==run['config']
c=canary['attempts'][0];verify(c,next(t for t in manifest['targets'] if t['id']==c['target_id']),args.canary)
startup={'canary':c,'config_same_as_batch':True,'runner_sha256':run['script_sha256'],'worker_sha256':run['worker_sha256'],'executed_command':canary['executed_command'],'counts_relation':'Separate public canary passed frozen checks; excluded. Exactly 31 explicit page.goto calls including canary, 30 counted URLs; ordinary browser subresources, redirects and page scripts remain enabled. No local smoke or other-tool benchmark.'}
(HERE/'startup-checks.json').write_text(json.dumps(startup,indent=2)+'\n')
run['summary'].update(http_200_soft_blocks=sum(a['status']==200 and a['classification']=='soft_block' for a in run['attempts']),http_200_empty_content=sum(a['status']==200 and a['empty_content'] for a in run['attempts']),observed_main_frame_requests=sum(a['observed_document_request_count'] for a in run['attempts']))
run['summary'].update(http_200_visually_observed_challenges_not_classified_soft_block=sum(a.get('visual_inspection',{}).get('outcome')=='verification_challenge' and a['classification']!='soft_block' for a in run['attempts']),http_200_met_checks_with_visually_observed_overlay=sum(a.get('visual_inspection',{}).get('outcome')=='cookie_consent_overlay_over_profile' and a['usable_content'] for a in run['attempts']))
run['publication_validation']={'checked_at':datetime.now(timezone.utc).isoformat(),'unchanged_validator_ast_verified':True,'dom_hashes_and_checks_verified':30,'pss_peaks_verified':30,'transport_metadata_and_log_hashes_verified':30,'driver_log_hashes_verified':30,'png_hashes_verified':sum(a['screenshot_sha256'] is not None for a in run['attempts']),'preserved_file_count':len(run['preserved_evidence_sha256']),'additional_target_requests':0,'surviving_owned_processes':0}
run['canary_relation']=startup['counts_relation']
run['pss_caveat']+=' Unreadable processes are omitted; samples follow the currently observable worker tree, so fast exit/reparenting can be missed.'
(HERE/'pss-samples.json').write_text(json.dumps(samples,indent=2)+'\n');run['pss_samples_sha256']=sha((HERE/'pss-samples.json').read_bytes())
(HERE/'results.json').write_text(json.dumps(run,indent=2)+'\n');print('Recomputed every private DOM/check/hash/PSS and transport/log record:',run['summary'])
