#!/usr/bin/env python3
"""Synthetic-only evidence. Usage: python3 repro.py FIXED_CHECKOUT
Fixed revision: a007595b42; baseline: a12d6ed942. Requires installed Node 24 and checkout dependencies. Creates only /tmp data;
never starts a server or accesses ~/.t3. Creates a detached temporary baseline git worktree. Leaves evidence for inspection.
"""
import sys, pathlib, tempfile, subprocess, shlex, sqlite3, hashlib, json, os
fixed=pathlib.Path(sys.argv[1]).resolve()
root=pathlib.Path(tempfile.mkdtemp(prefix='issue19018-synthetic-'))
log=[]
def emit(s):
 print(s,flush=True); log.append(s)
def run(args,cwd=fixed,expected=0):
 emit('\n$ (cd '+shlex.quote(str(cwd))+' && '+shlex.join(list(map(str,args)))+')')
 p=subprocess.run(list(map(str,args)),cwd=cwd,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
 emit(p.stdout.rstrip()); emit('EXIT_STATUS='+str(p.returncode))
 if expected is not None: assert p.returncode==expected,(args,p.returncode)
 return p
try:
 emit('Synthetic-only run directory: '+str(root))
 run(['node','--version'])
 run(['git','rev-parse','HEAD'])
 assert subprocess.check_output(['git','rev-parse','HEAD'],cwd=fixed,text=True).strip()=='a007595b42e2237ac5b8d91b1f294d24af2446e2'
 run(['git','status','--porcelain'])
 assert not subprocess.check_output(['git','status','--porcelain','--untracked-files=no'],cwd=fixed,text=True).strip(), 'Use a clean checkout of the fixed revision; preserve concurrent edits.'
 run(['git','rev-parse','70e5e384da'])
 baseline=root/'baseline-worktree'
 run(['git','worktree','add','--quiet','--detach',baseline,'a12d6ed942'])
 run(['git','rev-parse','HEAD'],baseline)
 run(['git','diff','--exit-code','70e5e384da','a12d6ed942','--','apps/server/scripts/migrate-dev-db.ts'])
 # Reuse installed dependencies without changing tracked files in either checkout.
 for parent in [fixed,* (fixed/'apps').iterdir(),*(fixed/'packages').iterdir()]:
  dep=parent/'node_modules'
  for checkout in [baseline]:
   target=checkout/dep.relative_to(fixed)
   if dep.exists() and not target.exists():
    target.parent.mkdir(parents=True,exist_ok=True); target.symlink_to(dep,target_is_directory=True)
 fixture=root/'fixture.mjs'
 fixture.write_text('''import * as Effect from %s;
import {runMigrations} from %s;
import * as DB from %s;
import {DatabaseSync} from 'node:sqlite';
const file=process.argv[2];
await Effect.runPromise(runMigrations({toMigrationInclusive:54}).pipe(Effect.provide(DB.layer({filename:file})),Effect.scoped));
const db=new DatabaseSync(file);
db.exec(`INSERT INTO projection_projects(project_id,title,workspace_root,scripts_json,created_at,updated_at) VALUES('p','Synthetic Repro','/tmp/synthetic','[]','2026-10-01','2026-10-01');
INSERT INTO projection_threads(thread_id,project_id,title,model_selection_json,runtime_mode,created_at,updated_at) VALUES('t','p','Synthetic legacy thread','{"instanceId":"codex","model":"gpt-5.4"}','full-access','2026-10-01','2026-10-01');`);
db.close();
''' % tuple(json.dumps(str(fixed/p)) for p in ['apps/server/node_modules/effect/dist/Effect.js','apps/server/src/persistence/Migrations.ts','packages/shared/src/nodeSqliteClient.ts']))
 source=root/'populated-v1.sqlite'; empty=root/'empty-v1.sqlite'
 run(['node',fixture,source])
 with sqlite3.connect(source) as src, sqlite3.connect(empty) as dst: src.backup(dst)
 with sqlite3.connect(empty) as db: db.execute('DELETE FROM projection_threads')
 def inspect(file):
  result={'path':str(file),'exists':file.exists()}
  if file.exists():
   result['sha256']=hashlib.sha256(file.read_bytes()).hexdigest()
   with sqlite3.connect('file:'+str(file)+'?mode=ro',uri=True) as db:
    for table in ['projection_projects','projection_threads','orchestration_v2_projection_threads','orchestration_events','evidence_sentinel']:
     if db.execute('SELECT 1 FROM sqlite_master WHERE type="table" AND name=?',(table,)).fetchone(): result[table]=db.execute('SELECT COUNT(*) FROM '+table).fetchone()[0]
  emit('SQLITE_INSPECTION '+json.dumps(result,sort_keys=True));return result
 original=inspect(source); inspect(empty)
 def cli(checkout,src,dest,expected):
  return run(['node','apps/server/scripts/migrate-dev-db.ts','--source',src,'--base-dir',dest,'--projects','5','--threads-per-project','10'],checkout,expected)
 before=root/'before'; cli(baseline,source,before,0)
 beforestate=inspect(before/'userdata/statev2.sqlite');assert beforestate['orchestration_v2_projection_threads']==0
 # Build schema-V2 with a pending legacy shell using migrations only, never a server.
 pending=root/'v2-schema-pending-legacy.sqlite'
 with sqlite3.connect(source) as src, sqlite3.connect(pending) as dst: src.backup(dst)
 migrate=root/'migrate.mjs'
 migrate.write_text('import * as Effect from '+json.dumps(str(fixed/'apps/server/node_modules/effect/dist/Effect.js'))+';\nimport {runMigrations} from '+json.dumps(str(fixed/'apps/server/src/persistence/Migrations.ts'))+';\nimport * as DB from '+json.dumps(str(fixed/'packages/shared/src/nodeSqliteClient.ts'))+';\nawait Effect.runPromise(runMigrations().pipe(Effect.provide(DB.layer({filename:process.argv[2]})),Effect.scoped));')
 run(['node',migrate,pending])
 pendingoriginal=inspect(pending)
 assert pendingoriginal['projection_threads']==1 and pendingoriginal['orchestration_v2_projection_threads']==0
 for label,src in [('populated-v1',source),('v2-schema-pending-legacy',pending)]:
  after=root/('after-'+label);(after/'userdata').mkdir(parents=True)
  destination=after/'userdata/statev2.sqlite'
  db=sqlite3.connect(destination)
  try:
   db.execute('PRAGMA journal_mode=WAL');db.execute('PRAGMA wal_autocheckpoint=0')
   db.execute('CREATE TABLE evidence_sentinel(value TEXT)');db.commit()
   db.execute("INSERT INTO evidence_sentinel VALUES('synthetic uncheckpointed destination')");db.commit()
   def hashes():
    return {str(f):hashlib.sha256(f.read_bytes()).hexdigest() for f in [destination,pathlib.Path(str(destination)+'-wal')]}
   previous=hashes(); assert pathlib.Path(str(destination)+'-wal').stat().st_size>0
   emit('DESTINATION_BEFORE '+json.dumps(previous,sort_keys=True))
   rejection=cli(fixed,src,after,1)
   assert 'MigrateDevDbLegacySourceError' in rejection.stdout and 'userdata/statev2.sqlite' in rejection.stdout
   assert hashes()==previous
   emit('DESTINATION_AFTER '+json.dumps(hashes(),sort_keys=True))
   assert db.execute('SELECT COUNT(*) FROM evidence_sentinel').fetchone()[0]==1
   emit('ASSERTION: '+label+' rejection preserves database and live WAL SHA-256 and sentinel')
  finally: db.close()
 assert inspect(pending)==pendingoriginal
 emptydest=root/'empty-success';cli(fixed,empty,emptydest,0)
 assert inspect(emptydest/'userdata/statev2.sqlite')['orchestration_v2_projection_threads']==0
 assert inspect(source)==original
 emit('ASSERTION: populated V1 source bytes/counts unchanged across both CLI runs')
 red=root/'red-worktree'
 run(['git','worktree','add','--quiet','--detach',red,'44bd595e77'])
 run(['git','rev-parse','HEAD'],red)
 run(['git','diff','--exit-code','44bd595e77','HEAD','--','apps/server/scripts/migrate-dev-db.test.ts'])
 for parent in [fixed,*(fixed/'apps').iterdir(),*(fixed/'packages').iterdir()]:
  dep=parent/'node_modules'; target=red/dep.relative_to(fixed)
  if dep.exists() and not target.exists():
   target.parent.mkdir(parents=True,exist_ok=True); target.symlink_to(dep,target_is_directory=True)
 redresult=run(['./node_modules/.bin/vp','test','run','apps/server/scripts/migrate-dev-db.test.ts','--reporter=verbose'],red,1)
 assert 'refuses a legacy thread committed after the guard' in redresult.stdout
 assert '1 failed' in redresult.stdout and '9 passed' in redresult.stdout
 emit('ASSERTION: deterministic injected filesystem boundary commits a V1 thread; red suite has exactly 1 failed / 9 passed; no sleeps.')
 test=['./node_modules/.bin/vp','test','run','apps/server/scripts/migrate-dev-db.test.ts','--reporter=verbose']
 greenresult=run(test,fixed,0)
 assert '10 passed' in greenresult.stdout
 assert 'refuses a legacy thread committed after the guard' in greenresult.stdout
 emit('V2 fixture oracle (committed test): 9 source threads -> 3 eligible retained threads: fork-thread, settled-thread, stopped-thread; pending auth/tasks/transfers counts -> 0. Full focused suite above validates exact IDs/counts.')
 run(['git','status','--porcelain'],fixed)
 emit('PASS: all evidence assertions completed; no servers, browser, private state, or forge writes used.')
finally:
 (root/'transcript.txt').write_text('\n'.join(log)+'\n')
 print('TRANSCRIPT_PATH='+str(root/'transcript.txt'),flush=True)
