#!/usr/bin/env python3
"""Validate captured events and package them without changing application checkouts."""
import datetime, html, json, platform, shlex, subprocess
from collections import defaultdict
from pathlib import Path

ROOT=Path('/tmp/lead-32880-r1/evidence-capture')
BATCH=Path((ROOT/'batch-location.txt').read_text())
OUT=ROOT/('publish-'+datetime.datetime.now(datetime.timezone.utc).isoformat().replace(':','-'))
OUT.mkdir()
ENTRIES=json.loads((BATCH/'results.json').read_text())
BEFORE='fa2f4ff2a2bac5ac92e07749f6b83b0003d642a0'
AFTER='df3ffc1249ca06ef43aaebe6a534f96034354700'
COMMIT=lambda sha:f'[{sha[:10]}](https://git.cafe/pingdotgg/t3code/commit/{sha})'
ATTRIBUTION='AI-assisted; verified with the tests and tools listed above.'
ENV='Node v26.12.1; Linux '+platform.release()+' x86_64; synthetic Codex home, schema per Codex 0.163.0. Installed binary: codex-cli 0.163.0.'
GROUPS=defaultdict(dict)
for e in ENTRIES:
 assert e['exitCode']==0, e
 assert e['inputFilesUnchanged'], e
 events=[json.loads(line) for line in Path(e['log']).read_text().splitlines() if line.startswith('{')]
 e['events']=events;e['byEvent']={x['event']:x['output'] for x in events}
 assert e['byEvent']['environment']['sha']==e['sha']
 assert e['byEvent']['environment']['node']=='v26.12.1'
 assert e['byEvent']['cold.scanner.created']=={'instanceNumber':2,'distinctFromPriorInstances':True,'firstCall':'recentThreads(P)'}
 for label in e['byEvent']['fixture']['roots']:
  assert e['byEvent'][f'warm.recent({label})']==e['byEvent'][f'cold.recent({label})'], (e['fixture'],e['side'],label)
 GROUPS[e['fixture']][e['side']]=e
assert len(GROUPS)==15 and len(ENTRIES)==30
for fixture,sides in GROUPS.items():
 assert sides['before']['fixtureHashesBefore']==sides['after']['fixtureHashesBefore'],fixture
def result(f,side,event='warm.recent(P)'):return GROUPS[f][side]['byEvent'][event]
def titles(outcomes):return [o['title'] for o in outcomes if o['tag']=='Importable']
assert titles(result('M1-composite','before'))==['R01 direct control']
assert set(titles(result('M1-composite','after')))=={'R01 direct control','R02 assigned scratch','R05 keep original request'}
rewound=next(o for o in result('M1-composite','after') if o.get('title')=='R05 keep original request')
assert len(rewound['messages'])==6 and all('ABANDON' not in m['text'] for m in rewound['messages'])
assert result('M2-corrupt-db','before')==result('M2-corrupt-db','after')
assert result('M3-root-order','after','warm.recent(Q)')==[]
assert titles(result('M3-root-order','after'))==['R07 position zero wins']
assert titles(result('R11-age','after'))==['R11 exact cutoff inclusive']
assert result('R11-row-cap','after')==[]
assert result('R11-row-cap','after','warm.scan').get('truncated') is None
assert [o['tag'] for o in result('R12-repeat','after')]==['Importable','Duplicate']
assert [o['tag'] for o in result('R12-repeat','after','warm.recent(P; completedSources=initial Importable sources)')]==['AlreadyImported','Duplicate']
changed=result('R12-repeat','after','warm.recent(P; changed file; old completedSources)')
assert changed[0]['tag']=='Importable' and changed[0]['messages'][-1]['text']=='R12 appended after completed import'
assert [o['tag'] for o in result('R13-invalid','after')]==['Skipped','Skipped']
assert result('R14-cache-mutation','after','warm.cached-after-mutation.recent(P)')==result('R14-cache-mutation','after')
assert result('R14-cache-mutation','after','warm.refreshed-after-mutation.recent(P)')==[]
assert titles(result('R14-cache-mutation','after','warm.refreshed-after-mutation.recent(Q)'))==['R14 cache mutation']

OUTCOMES={
'R01':('Direct P control imports; scan P:1.','Direct P control imports; composite P:5 includes additional mapped files.'),
'R02':('Assigned scratch contributes no outcome.','Assigned scratch imports under P; no rollout cwd rewrite.'),
'R03':('Unassigned scratch absent from all candidates and outcomes.','Unassigned scratch remains absent; control survives.'),
'R04':('Q:1; imports only for Q.','Q:1; imports only for Q despite assignment P.'),
'R05':('Scratch rewind files excluded.','Three files count under P; one logical import + two Duplicate; six inherited/current messages, no abandoned text.'),
'R06':('Corrupt DB ignored; P:1 and only direct control imports.','Same P:1 and control-only import; no scanner failure.'),
'R07':('No candidates or recent outcomes.','Only P:1 and P imports; Q empty despite its row being inserted first.'),
'R08':('Relative/excluded/symlink-to-excluded mappings have no outcomes.','Same empty scan and recent results for all three variants.'),
'R09':('Absent/file/linked-worktree destinations have no outcomes.','Scan omits all three; direct recent(destination) imports; P stays empty, so later valid root is not fallback.'),
'R10':('Claude P:1 and Pi Q:1 remain; scratch Codex chats absent.','P:2 and Q:2; same UUID imports with distinct Codex owners/mappings; alias adds no count, disabled home absent; Claude/Pi unchanged.'),
'R11':('Mapped scratch variants excluded.','Scan counts old/cutoff/future; recent accepts only exact cutoff. Thread beyond 100000 joined rows stays excluded without truncation flag.'),
'R12':('Mapped scratch files excluded across all calls.','Importable + Duplicate; completed replay AlreadyImported + Duplicate; changed file reparses with appended message.'),
'R13':('All scratch/no-cwd/malformed/subagent/no-conversation inputs excluded.','Only valid-cwd non-importable files add P:2, then two Skipped; no-cwd/malformed/subagent remain absent.'),
'R14':('No scratch membership in warm or cold calls.','Warm and cold agree in every fixture. Mapping mutation remains cached at P, then rescan moves membership to Q. Persisted symlink root identity retained initially.'),
}
def shell_command(e):
 f=e['byEvent']['fixture'];home=f['home']
 return f"cd {shlex.quote(e['cwd'])}\nHOME='<fakehome>' CODEX_HOME='<fakehome>/.codex' T3CODE_DEV_SEED=0 "+' '.join(shlex.quote(x) for x in e['command'])
def section(name):
 sides=GROUPS[name];a=sides['before'];b=sides['after'];fixture=a['byEvent']['fixture']
 text=f'## {name}: '+', '.join(fixture['rows'])+'\n\n'+fixture['description']+'\n\n'
 text+='Fixture: **synthetic Codex home, schema per Codex 0.163.0**. P and Q are ordinary directories. Viewport/theme: N/A. Real filesystem, subprocess runner and scanner; only settings, project-store records, host home and clock are injected.\n\n'
 text+='Seed command: `python3 /tmp/lead-32880-r1/evidence-capture/seed-fixtures.py`. The fixture below records exact JSONL content, timestamps, thread assignments and root insertion order. Read-only database inspection is included in each output.\n\n'
 text+='```json\n'+json.dumps(fixture,indent=2)+'\n```\n\n'
 text+='Commands, before then after:\n\n```sh\n'+shell_command(a)+'\n\n'+shell_command(b)+'\n```\n\n'
 text+='Each invocation builds a warm scanner, calls scan then recent(P)/recent(Q), and builds a separate cold scanner that starts with recent(P). Additional calls retain their event order. Each invocation writes a fresh timestamped run directory; both sides consume identical fixture bytes.\n\n'
 text+='<table><tr><th>Before <a href="https://git.cafe/pingdotgg/t3code/commit/'+BEFORE+'">'+BEFORE+'</a></th><th>After <a href="https://git.cafe/pingdotgg/t3code/commit/'+AFTER+'">'+AFTER+'</a></th></tr><tr>'
 for e in (a,b):
  text+='<td><pre>'+html.escape('\n\n'.join(x['event']+'\n'+json.dumps(x['output'],indent=2) for x in e['events'] if x['event']!='fixture'))+'</pre></td>'
 text+='</tr></table>\n\n'
 text+='Run receipt: both exit 0; fixture SHA-256 maps match between sides and before/after each invocation. Warm and cold stable outcomes match exactly, including source identities and messages.\n\n'
 return text
def document(filename,names):
 text='# Runtime evidence for [#32880](https://git.cafe/pingdotgg/t3code/pull/32880)\n\n'+ENV+'\n\nBefore '+COMMIT(BEFORE)+'; after '+COMMIT(AFTER)+'.\n\n'
 text+='The isolated Codex app-server initialized and accepted a turn, then returned 401 Unauthorized because the synthetic home has no credentials. No real Codex home was read. The fallback SQL schema was extracted from the database that codex-cli 0.163.0 created in that isolated probe. All conversations here are synthetic; no application server or live T3 state was used.\n\n'
 text+=''.join(section(n) for n in names)
 text+='Attribution: '+ATTRIBUTION+'\n'
 (OUT/filename).write_text(text)
document('M1-R01-R05-R14.md',['M1-composite'])
document('M2-R06-R14.md',['M2-corrupt-db'])
document('M3-R07-R14.md',['M3-root-order'])
document('supplemental-R08-R14.md',[n for n in GROUPS if n not in ['M1-composite','M2-corrupt-db','M3-root-order']])

# Retain complete scanner events and command receipts without unredacted synthetic-home paths.
receipts=[]
for e in ENTRIES:
 home=next(x['output']['home'] for x in e['events'] if x['event']=='fixture')
 originalHome=json.loads(Path(e['fixturePath']).read_text())['home']
 item={k:v for k,v in e.items() if k not in ['byEvent']}
 receipts.append(json.loads(json.dumps(item).replace(originalHome,'<fakehome>')))
(OUT/'runtime-receipts.json').write_text(json.dumps(receipts,indent=2)+'\n')
(ROOT/'publish-location.txt').write_text(str(OUT))
print('Validated 30 runtime invocations, 15 fixtures, 14 matrix rows; all warm/cold outcomes agree and all fixture bytes restored.')
print(OUT)
