from pathlib import Path
import json,csv,io,hashlib,subprocess,sys,datetime
r=Path(__file__).resolve().parent
raw=b'[{"case":"missing"},{"case":"null","value":null},{"case":"empty","value":""},{"case":"number-zero","value":0},{"case":"string-zero","value":"0"},{"case":"boolean-false","value":false},{"case":"string-false","value":"false"},{"case":"large-number","value":9007199254740993},{"case":"large-string","value":"9007199254740993"},{"case":"quoted","value":"a,\\"b\\"\\nCaf\\u00e9","later":"tail"}]'
(r/'input.json').write_bytes(raw)
node=r/'node-path.cjs'
node.write_text('''const fs=require('fs');const p=require('path');const rows=JSON.parse(fs.readFileSync(p.join(__dirname,'input.json'),'utf8'));const headers=[...new Set(rows.flatMap(Object.keys))];const cell=v=>v==null?'':typeof v==='object'?JSON.stringify(v):String(v);const quote=s=>/[",\\r\\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;let out=[headers.join(','),...rows.map(r=>headers.map(k=>quote(cell(r[k]))).join(','))].join('\\r\\n')+'\\r\\n';fs.writeFileSync(p.join(__dirname,'node.csv'),out);fs.writeFileSync(p.join(__dirname,'node-parsed.json'),JSON.stringify({node:process.version,headers,rows:rows.map(r=>({case:r.case,present:Object.hasOwn(r,'value'),type:!Object.hasOwn(r,'value')?'missing':r.value===null?'null':typeof r.value,value:r.value}))},null,2));''',encoding='utf-8')
q=subprocess.run(['node',str(node)],capture_output=True,text=True);assert q.returncode==0,q.stderr
rows=json.loads(raw);headers=list(dict.fromkeys(k for row in rows for k in row));s=io.StringIO(newline='');w=csv.writer(s,lineterminator='\r\n');w.writerow(headers)
def cell(v):
 if v is None:return ''
 if isinstance(v,bool):return str(v).lower()
 if isinstance(v,(dict,list)):return json.dumps(v,ensure_ascii=False,separators=(',',':'))
 return str(v)
for row in rows:w.writerow([cell(row.get(k)) for k in headers])
(r/'python.csv').write_bytes(s.getvalue().encode())
pyrows=[{'case':row['case'],'present':'value'in row,'type':'missing'if'value'not in row else 'null'if row['value']is None else type(row['value']).__name__,'value':row.get('value')}for row in rows]
n=json.loads((r/'node-parsed.json').read_text());reports=[]
for name in ['python.csv','node.csv']:
 b=(r/name).read_bytes();back=list(csv.DictReader(io.StringIO(b.decode(),newline='')));reports.append({'path':name,'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest(),'headers':list(back[0]),'reread':back,'all_reloaded_values_are_strings':all(type(v)is str for row in back for v in row.values())})
a,b=reports;assert [x['value']for x in a['reread'][:3]]==['','',''];assert a['reread'][3]['value']==a['reread'][4]['value']=='0';assert a['reread'][5]['value']==a['reread'][6]['value']=='false';assert a['reread'][7]['value']=='9007199254740993';assert b['reread'][7]['value']=='9007199254740992';assert b['reread'][8]['value']=='9007199254740993';assert a['reread'][9]['value']==b['reread'][9]['value']=='a,"b"\nCafé';assert a['headers']==b['headers']==['case','value','later']
d={'at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'python':sys.version.split()[0],'node':n['node'],'input_bytes':len(raw),'input_sha256':hashlib.sha256(raw).hexdigest(),'method':'Python stdlib json + csv.writer vs Node JSON.parse + explicit minimal CSV encoder; same union headers and CRLF, no type sidecar in CSV. Independent Python csv.DictReader rereads both files. No online tool or Excel test.','python_parsed':pyrows,'node_parsed':n['rows'],'outputs':reports,'difference_offsets':[i for i,(x,y)in enumerate(zip((r/'python.csv').read_bytes(),(r/'node.csv').read_bytes()))if x!=y]};(r/'results.json').write_text(json.dumps(d,indent=2,ensure_ascii=False),encoding='utf-8');print(json.dumps({k:d[k]for k in ['python','node','input_bytes','input_sha256']}));print(json.dumps({'outputs':[{k:x[k]for k in ['path','bytes','sha256']}for x in reports],'difference_offsets':d['difference_offsets']}))
