"""Offline capture verification; never sends target requests or publishes raw captures.
python publish.py --private-output runner/data/playwright-public-30-20260930 --canary runner/data/playwright-canary-20260930
"""
import argparse,ast,hashlib,json,re,shlex
from pathlib import Path
from datetime import datetime,timezone
from importlib.metadata import version
HERE=Path(__file__).resolve().parent
sha=lambda b:hashlib.sha256(b).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):
 t=ast.parse(path.read_text());return [n for n in t.body if getattr(n,'name',None) in ['PageText','evaluate','result_class'] or isinstance(n,ast.Assign) and any(isinstance(v,ast.Name) and v.id in ['BLOCK_PHRASES','GATE_PHRASES'] for v in n.targets)]
ref=nodes(HERE.parent/'patchright-public-30-20260929/run.py')
for f in ['run.py','canary-run.py']:assert [ast.dump(n,include_attributes=False) for n in nodes(HERE/f)]==[ast.dump(n,include_attributes=False) for n in ref]
ns={};exec('from html.parser import HTMLParser\nimport re',ns);exec(compile(ast.Module(body=ref,type_ignores=[]),'unchanged-checker','exec'),ns)
def verify_attempt(a,t,folder,canary=False):
 dom=folder/(a['target_id']+'.html');b=dom.read_bytes() if dom.exists() else b''
 assert ns['evaluate'](b,t)==a['checks'] and ns['result_class'](a)==a['classification']
 assert a['raw_html_sha256']==(sha(b) if dom.exists() else None) and a['html_bytes']==len(b)
 assert a['usable_content']==(a['classification']=='usable') and a['empty_content']==(a['checks']['text_chars']==0)
 png=folder/(a['target_id']+'.png')
 if a['screenshot_sha256'] is not None:assert png.exists() and sha(png.read_bytes())==a['screenshot_sha256'] and png.read_bytes()[:8]==b'\x89PNG\r\n\x1a\n'
 else:assert a['screenshot_error'] or a['error']
 samples=json.loads((folder/(a['target_id']+'-pss.json')).read_text());assert len(samples)==a['pss_samples'] and max((s['pss_mb'] for s in samples),default=None)==a['peak_sampled_tree_pss_mb']
 assert all(s['pss_mb']<=4096 and s['host_available_mb']>=3072 for s in samples)
 assert a['cleanup_surviving_owned_pids']==[] and a['navigation_started_at'] and a['attempt_process_exit']==0
 assert a['driver_log_sha256']==sha((folder/(a['target_id']+'-driver.log')).read_bytes())
 events_path=folder/(a['target_id']+'-browser-events.json');assert sha(events_path.read_bytes())==a['browser_event_log_sha256'];events=json.loads(events_path.read_text());assert len(events)==a['browser_event_log_entries']
 assert [e['value'] for e in events if e['kind']=='main_response']==a['document_responses']
 if a['document_responses']:assert a['document_responses'][-1]['status']==a['status']
 for hop in a['redirect_history']:assert {'url':hop['previous'],'status':hop['status']} in a['document_responses']
 assert len(a['redirect_history'])<=20 and a['browser_version']=='153.0.8010.12'
 assert a['browser_cdp_version']['revision']=='@971a7443b0c9b0a9b2860529b33331b76077ec62'
 assert a['initial_cookie_count']==0 and a['browser_observation']['webdriver']==True
 assert a['browser_observation']['viewport']=={'width':1280,'height':720} and 'HeadlessChrome/153.' in a['browser_observation']['user_agent']
 if not canary:
  raw=a['effective_browser_arguments'];assert raw
  a['observed_process_cmdline']=raw
  flags=shlex.split(raw[0]) if len(raw)==1 else raw
  a['effective_browser_arguments']=flags
  a['browser_arguments_basis']='Owned main Chrome /proc cmdline before public navigation. Chromium may rewrite this as a single argument; tokens are split for readability, not an independent original argv capture.'
  assert flags and '--no-proxy-server' in flags and '--no-sandbox' in flags and '--headless' in flags
  assert '--enable-automation' not in flags and not any('AutomationControlled' in f for f in flags)
 for key in ['raw_html_private_path','screenshot_private_path']:a.pop(key,None)
 a['inspection_note']=('Complete saved DOM met all unchanged word groups, listing patterns and minimum text. No complete field extraction was verified.' if a['usable_content'] else 'Saved DOM matched frozen soft-block phrases: '+', '.join(a['checks']['soft_block_checks']['matched']) if a['classification']=='soft_block' else 'HTTP 200 contained no checked DOM text; empty checked content is not usable.' if a['status']==200 and a['empty_content'] else 'Saved DOM rechecked against unchanged rules: '+a['classification'].replace('_',' ')+'.')
 parser=ns['PageText']();parser.feed(b.decode('utf-8',errors='replace'));text=' '.join(' '.join(parser.parts).split())
 if a['target_id']=='amazon' and 'RON' in text and all(c['matches']==0 for c in a['checks']['text_regex_checks']):a['inspection_note']+=' Checked DOM text contains RON prices; the frozen price regex accepts only $, £ or €. No currency rule changed.'
 if a['target_id']=='target':
  a['inspection_note']+=' Private viewport showed Quick verification / press-and-hold. Frozen main-frame source phrases did not match; classification remains missing_required_content. No interaction occurred.'
  a['visual_inspection']={'outcome':'verification_challenge','basis':'Private viewport PNG inspected; no frozen main-frame source-text phrase match','changes_frozen_classification':False}
 if a['target_id']=='bestbuy':a['inspection_note']+=' Saved DOM text asks for a country choice rather than meeting laptop-listing rules. The viewport screenshot timed out; no rendered-view claim is made and no country was selected.'
 if a['target_id']=='walmart':a['inspection_note']+=' Private viewport showed Robot or human / press-and-hold; no interaction occurred.'
 if a['target_id']=='instagram':
  a['inspection_note']+=' Private viewport showed a cookie-consent overlay over the NASA profile. Underlying DOM content met the frozen checks; this is not completed visible interaction or consent. No overlay was clicked and the content classification is unchanged.'
  a['visual_inspection']={'outcome':'cookie_consent_overlay_over_profile','basis':'Private viewport PNG inspected; underlying DOM meets frozen checks','changes_frozen_classification':False}
 if a['target_id']=='example':a['inspection_note']+=' Frozen source parser/phrase rules are unchanged. The DOM parser does not resolve CSS visibility, separate-letter styling or iframe/shadow-root text.'
 a['screenshot_limit']='Viewport PNG was taken after DOM capture; it is not complete-page evidence and does not determine the frozen classification. A screenshot error alone cannot turn a content pass into a failure.'
 return samples
r=json.loads((args.private_output/'results.json').read_text());assert r.get('finished_at') and not r.get('blocker') and len(r['attempts'])==30
assert [(a['target_id'],a['original_url']) for a in r['attempts']]==[(t['id'],t['url']) for t in manifest['targets']]
assert r['script_sha256']==sha((HERE/'run.py').read_bytes()) and r['manifest_sha256']==sha((HERE.parent/'public-30-v1.json').read_bytes())
assert r['requirements_sha256']==sha((HERE/'requirements.txt').read_bytes())
pins=dict(line.split('==') for line in (HERE/'requirements.txt').read_text().strip().splitlines());assert all(version(n)==v for n,v in pins.items());r['installed_packages']=pins
samples={a['target_id']:verify_attempt(a,t,args.private_output) for a,t in zip(r['attempts'],manifest['targets'])}
c=json.loads((args.canary/'results.json').read_text());assert c['config']==r['config'] and len(c['attempts'])==1 and c['script_sha256']==sha((HERE/'canary-run.py').read_bytes())
a=c['attempts'][0];verify_attempt(a,next(t for t in manifest['targets'] if t['id']==a['target_id']),args.canary,True)
startup={'separate_public_canary':a,'executed_command':c['executed_command'],'canary_runner_sha256':c['script_sha256'],'batch_runner_sha256':r['script_sha256'],'config_same_as_batch':True,'change_after_canary':'Only read-only owned-child /proc command-line observation and the inaccurate self-reported default-flag description were corrected. Browser launch/context/navigation/capture/guard settings unchanged. Original exact canary code is canary-run.py; its metadata claim of enable-automation is superseded by the observed refusal and verified release source. No canary retry or mode switch.','counts_relation':'One usable HTTP 200 canary excluded from 30-target batch. 31 explicit public page.goto calls total, plus ordinary redirects/browser resources. No local smoke or other benchmark tool run.'}
(HERE/'startup-checks.json').write_text(json.dumps(startup,indent=2)+'\n')
r['baseline_sha256']={name:sha((HERE.parent/(folder+'-20260929')/'results.json').read_bytes()) for name,folder in {'botasaurus':'botasaurus-public-30','camoufox':'camoufox-public-30','curl_cffi':'curl-cffi-public-30','lightpanda':'lightpanda-public-30','patchright':'patchright-public-30','scrapling':'scrapling-public-30','scrapy':'scrapy-public-30','seleniumbase':'seleniumbase-public-30','wreq':'wreq-public-30-redirects'}.items()}
r['summary']['http_200_soft_blocks']=sum(a['status']==200 and a['classification']=='soft_block' for a in r['attempts'])
r['summary']['http_200_visually_observed_challenges_not_classified_soft_block']=sum(a['status']==200 and a['classification']!='soft_block' and a.get('visual_inspection',{}).get('outcome')=='verification_challenge' for a in r['attempts'])
r['summary']['http_200_met_checks_with_visually_observed_overlay']=sum(a['status']==200 and a['usable_content'] and a.get('visual_inspection',{}).get('outcome')=='cookie_consent_overlay_over_profile' for a in r['attempts'])
r['summary']['http_200_empty_content']=sum(a['status']==200 and a['empty_content'] for a in r['attempts'])
r['publication_validation']={'checked_at':datetime.now(timezone.utc).isoformat(),'unchanged_validator_ast_verified':True,'dom_hashes_and_checks_verified':sum(a['raw_html_sha256'] is not None for a in r['attempts']),'png_hashes_verified':sum(a['screenshot_sha256'] is not None for a in r['attempts']),'pss_peaks_verified':30,'private_browser_event_log_hashes_and_statuses_verified':30,'driver_log_hashes_verified':30,'main_browser_arguments_observed':30,'additional_target_requests':0,'surviving_owned_processes':0,'representative_private_screenshots_inspected':['walmart','target','instagram','example']}
r['latency_basis']='Immediately before page.goto through returned/error navigation, fixed two-second settle, final URL read and UTF-8 DOM capture. Excludes browser/context startup, screenshot and teardown. attempt_seconds includes those stages after imports; tree PSS also covers imports.'
r['startup_checks_url']='/evidence/playwright-public-30-20260929/startup-checks.json';r['canary_relation']=startup['counts_relation']
(HERE/'pss-samples.json').write_text(json.dumps(samples,indent=2)+'\n');r['pss_samples_sha256']=sha((HERE/'pss-samples.json').read_bytes())
(HERE/'results.json').write_text(json.dumps(r,indent=2)+'\n');print('Rechecked private captures/logs/PSS and canary:',r['summary'],r['publication_validation'])
