"""Publish inspected metadata and samples only; never raw DOM/screenshots/logs.
Usage: python publish.py --private-output runner/data/browser-resource-slice-20260929
Requires the immutable versioned runners and official-wheel documentation JSON.
"""
import argparse,ast,hashlib,json
from datetime import datetime,timezone
from pathlib import Path
HERE=Path(__file__).resolve().parent
sha=lambda data:hashlib.sha256(data).hexdigest()
def save(name,data): (HERE/name).write_text(json.dumps(data,indent=2)+'\n')
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--private-output',type=Path,required=True)
parser.add_argument('--documentation-dir',type=Path,required=True)
args=parser.parse_args();private=args.private_output
run=json.loads((private/'results.json').read_text())
assert sha((HERE/'run.py').read_bytes())==run['script_sha256']
assert sha((HERE/'targets.json').read_bytes())==run['manifest_sha256']
parent=json.loads((HERE.parent/'public-30-v1.json').read_text())
assert sha((HERE.parent/'public-30-v1.json').read_bytes())==run['manifest']['parent_manifest_sha256']
for target in run['manifest']['targets']:assert target==next(t for t in parent['targets'] if t['id']==target['id'])
validators={}
for tool in ['patchright','camoufox']:
 source=ast.parse((HERE.parent/f'{tool}-public-30-20260929'/'run.py').read_text())
 selected=[n for n in source.body if isinstance(n,(ast.ClassDef,ast.FunctionDef)) and getattr(n,'name','') 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)]
 namespace={};exec('from html.parser import HTMLParser\nimport re',namespace);exec(compile(ast.Module(body=selected,type_ignores=[]),'frozen-validator','exec'),namespace);validators[tool]=namespace
assert len(run['groups'])==16 and not run.get('blocker')
samples={};attempts=[]
for g in run['groups']:
 assert not g['blocker'] and g['barrier_released_at']
 assert len(g['attempts'])==(3 if g['mode']=='concurrent' else 1)
 samples[g['id']]=g.pop('samples')
 for s in samples[g['id']]:
  assert s['host_available_mib']>=run['policy']['min_host_available_mib']
  assert s['aggregate_pss_mib']==round(sum(v for v in s['tree_pss_mib'].values() if v is not None),3) or s['aggregate_pss_mib'] is None
 assert g['peak_sampled_aggregate_pss_mib']==max(s['aggregate_pss_mib'] for s in samples[g['id']] if s['aggregate_pss_mib'] is not None)
 g['sample_count']=len(samples[g['id']]);g['max_sweep_seconds']=max(s['sweep_seconds'] for s in samples[g['id']])
 events=[]
 for a in g['attempts']:
  target=next(t for t in run['manifest']['targets'] if t['id']==a['target_id']);body=(private/g['id']/(a['target_id']+'.html')).read_bytes();png=(private/g['id']/(a['target_id']+'.png')).read_bytes()
  assert sha(body)==a['raw_html_sha256'] and sha(png)==a['screenshot_sha256']
  assert len(body)==a['html_bytes'] and validators[g['tool']]['evaluate'](body,target)==a['checks']
  assert validators[g['tool']]['result_class'](a)==a['classification'] and a['usable_content']==(a['classification']=='usable')
  assert not a['cleanup_surviving_owned_pids'] and a['worker_process_exit']==0
  assert a['peak_sampled_tree_pss_mib']==max(s['tree_pss_mib'][a['target_id']] for s in samples[g['id']] if s['tree_pss_mib'][a['target_id']] is not None)
  assert a['pss_sample_count']==sum(s['tree_pss_mib'][a['target_id']] is not None for s in samples[g['id']])
  assert a['navigation_started_at']>=g['barrier_released_at']
  start=datetime.fromisoformat(a['navigation_started_at']).timestamp();events.extend([(start,1),(start+a['navigation_seconds'],-1)])
  a['inspection_note']=('Complete DOM met the unchanged words, text length and listing patterns; not a verified field extraction.' if a['usable_content'] else 'HTTP 200 saved an empty DOM and a blank viewport screenshot; zero checked text.' if a['html_bytes']==0 else 'HTTP 200 challenge page; checked phrase: '+', '.join(a['checks']['soft_block_checks']['matched']) if a['classification']=='soft_block' else 'HTTP 403 denied response retained. Required content checks were not met.')
  a['elapsed_spawn_to_group_cleanup_seconds']=a.pop('worker_lifecycle_seconds')
  for field in ['raw_html_private_path','screenshot_private_path']:a.pop(field,None)
  attempts.append(a)
 active=maximum=0
 for _,delta in sorted(events):active+=delta;maximum=max(maximum,active)
 g['max_overlapping_navigation_intervals']=maximum
 assert maximum==(3 if g['mode']=='concurrent' else 1)
assert len(attempts)==24
for tool in ['patchright','camoufox']:
 run['versions'][tool]['documentation']=json.loads((args.documentation_dir/f'{tool}-documentation.json').read_text())
 run['versions'][tool]['baseline_runner_url']=f'/evidence/{tool}-public-30-20260929/run.py'
# Source review concerns current repositories; release-specific behavior comes from
# the installed API files matched to official versioned wheels.
camoufox_baseline=json.loads((HERE.parent/'camoufox-public-30-20260929/results.json').read_text())
run['versions']['camoufox']['runtime_packages']={k:camoufox_baseline[k+'_version'] for k in ['playwright','browserforge']}
run['documentation_note']='Focused DeepWiki questions checked current repository behavior. Release-specific calls were inspected in the installed APIs and matched byte for byte to official PyPI wheels. This run uses ordinary browser.new_context, not the separate Camoufox AsyncNewContext helper discussed by current repository docs.'
run['deepwiki_sources']=['https://deepwiki.com/search/for-a-bounded-resource-test-us_a174a6cf-8ff9-4520-9435-c2925246140c','https://deepwiki.com/search/for-a-bounded-resource-test-us_f0538006-f7f6-427a-97e9-f3ba3c28a8eb']
run['public_capture_policy']='Raw UTF-8 DOM, viewport PNGs and driver logs remain private under ignored runner/data. Public metadata retains original capture hashes. Sample trajectories are public in samples.json. elapsed_spawn_to_group_cleanup_seconds includes time after an individual worker exits until group cleanup; it is not individual request latency.'
run['latency_basis']='latency_seconds runs from page.goto start through DOMContentLoaded navigation, fixed two-second settle and DOM serialization. It excludes browser startup, ready-barrier wait, screenshot and teardown. navigation_seconds ends at DOMContentLoaded. attempt_seconds includes startup after Python imports, barrier, screenshot and teardown.'
run['summary']={'attempts':len(attempts),'distinct_targets':3,'repetitions_per_mode':2,'met_checks':{tool:sum(a['usable_content'] for a in attempts if a['tool']==tool) for tool in ['patchright','camoufox']},'minimum_sampled_host_available_mib':min(s['host_available_mib'] for rows in samples.values() for s in rows)}
run['publication_validation']={'checked_at':datetime.now(timezone.utc).isoformat(),'raw_dom_hashes_and_frozen_checks_verified':24,'private_png_hashes_verified':24,'sampled_group_peaks_verified':16,'sampled_attempt_peaks_verified':24,'triggered_guards':0,'surviving_owned_processes':0,'additional_target_requests':0}
save('samples.json',samples);run['samples_sha256']=sha((HERE/'samples.json').read_bytes());save('results.json',run)
print('Published 24 inspected attempts and all resource samples; no raw captures or additional target requests.')
