import {modelRoles} from './semantic-models.mjs'; import fs from 'node:fs/promises';import path from 'node:path';import {fileURLToPath,pathToFileURL} from 'node:url'; const P=path.dirname(fileURLToPath(import.meta.url)),O=path.join(P,'output'),A=JSON.parse(await fs.readFile(path.join(O,'analyses.json'),'utf8')); const C={ink:'#102B46',muted:'#4D6379',blue:'#1259A7',teal:'#08777B',slate:'#6C8197',line:'#CCD8E4',wash:'#F0F5F9',paper:'#FFFFFF'}; const esc=x=>String(x).replaceAll('&','&').replaceAll('<','<').replaceAll('"','"'); const node=(type,geometry={},children=[],key)=>{if(type==='ellipse'){let{cx,cy,rx,ry,...g}=geometry;type='path';geometry={...g,d:'M'+(cx-rx)+' '+cy+' a'+rx+' '+ry+' 0 1 0 '+(2*rx)+' 0 a'+rx+' '+ry+' 0 1 0 '+(-2*rx)+' 0'};}if('fill-opacity' in geometry){geometry={...geometry,fill:geometry.fill===C.teal?'#E1EEEE':geometry.fill===C.slate?'#E8EDF1':'#E0EAF4'};delete geometry['fill-opacity'];}return{type,roles:[],geometry:Object.fromEntries(Object.entries(geometry).map(([k,v])=>[k.toLowerCase(),String(v)])),children,...(key?{key}:{})};}; const T=(x,y,s,size=26,color=C.ink,weight=400,anchor='start')=>node('text',{x,y,'font-size':size,'font-family':'Pretendard Variable','font-weight':weight,fill:color,'text-anchor':anchor},[s]); const R=(x,y,w,h,fill=C.wash,stroke='none')=>node('rect',{x,y,width:w,height:h,fill,stroke,'stroke-width':1}); const L=(x,y,xx,yy,color=C.line,width=1,dash)=>node('line',{x1:x,y1:y,x2:xx,y2:yy,stroke:color,'stroke-width':width,...(dash?{'stroke-dasharray':dash}:{})}); const G=(children,key)=>node('g',{},children,key); const D=(d,color=C.blue,width=3,fill='none',extra={})=>node('path',{d,stroke:color,'stroke-width':width,fill,...extra}); const AH=(x,y,angle,color=C.blue)=>{let z=12,a=angle*Math.PI/180;return D('M'+(x-z*Math.cos(a-.5))+' '+(y-z*Math.sin(a-.5))+' L'+x+' '+y+' L'+(x-z*Math.cos(a+.5))+' '+(y-z*Math.sin(a+.5)),color,2);}; const Q=(x,y,r=8,fill=C.blue,stroke=fill)=>node('circle',{cx:x,cy:y,r,fill,stroke,'stroke-width':2}); const fmt=(x,d=2)=>x!==0&&Math.abs(x)<10**(-d)?Number(x).toExponential(2):Number(x).toFixed(d).replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,''); const signed=(x,d=2)=>(x>0?'+':'')+fmt(x,d);const pv=x=>x<.001?'p < .001':'p = '+fmt(x,3); const xml=n=>typeof n==='string'?esc(n):'<'+n.type+' '+Object.entries(n.geometry||{}).map(([k,v])=>(k==='viewbox'?'viewBox':k)+'="'+esc(v)+'"').join(' ')+(n.type==='svg'?' xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Scientific analysis figure"':'')+(n.key?' data-evidence="'+esc(n.key)+'" aria-label="'+esc(A.details[n.key]?.label||n.key)+'"':'')+'>'+n.children.map(xml).join('')+''; const SVG=children=>node('svg',{viewBox:'0 0 1600 650',width:1600,height:650},[R(0,0,1600,650,C.paper),...children]); const extent=rows=>{let lo=Math.min(0,...rows.map(r=>r.lo)),hi=Math.max(0,...rows.map(r=>r.hi));let p=(hi-lo)*.08||.1;return[lo-p,hi+p]}; function forest(rows,{x=0,y=0,w=1000,title='',unit='',labelW=300,rowH=86,range,color=C.blue,sign=true}={}){ let a=[T(x,y+33,title,30,C.ink,700)],r=range||extent(rows),left=x+labelW,right=x+w-225,X=v=>left+(v-r[0])/(r[1]-r[0])*(right-left),top=y+95,bt=top+(rows.length-1)*rowH; for(let k=0;k<5;k++){let t=r[0]+(r[1]-r[0])*k/4;a.push(L(X(t),top-28,X(t),bt+28),T(X(t),bt+74,fmt(t),24,C.muted,400,'middle'))} if(r[0]<=0&&r[1]>=0)a.push(L(X(0),top-28,X(0),bt+28,C.slate,2,'5 5')); rows.forEach((z,i)=>{let yy=top+i*rowH,cross=z.lo<=0&&z.hi>=0,col=cross?C.slate:color;a.push(G([T(x+8,yy+7,z.label,26),L(X(z.lo),yy,X(z.hi),yy,col,4),L(X(z.lo),yy-9,X(z.lo),yy+9,col,2),L(X(z.hi),yy-9,X(z.hi),yy+9,col,2),Q(X(z.b),yy,8,cross?C.paper:col,col),T(x+w-10,yy,sign?signed(z.b):fmt(z.b),32,col,700,'end'),T(x+w-10,yy+34,'['+fmt(z.lo)+', '+fmt(z.hi)+']',23,C.muted,400,'end')],z.key))}); a.push(T((left+right)/2,bt+115,unit,24,C.muted,400,'middle'));return a; } function plotFrame({x,y,w,h,xRange,yRange,xTicks,yTicks,xLabel,yLabel}){let a=[],X=v=>x+(v-xRange[0])/(xRange[1]-xRange[0])*w,Y=v=>y+h-(v-yRange[0])/(yRange[1]-yRange[0])*h;for(const t of yTicks)a.push(L(x,Y(t),x+w,Y(t)),T(x-14,Y(t)+8,fmt(t),24,C.muted,400,'end'));for(const t of xTicks)a.push(T(X(t),y+h+40,fmt(t),24,C.muted,400,'middle'));a.push(T(x+w/2,y+h+78,xLabel,24,C.muted,400,'middle'),T(x,y-20,yLabel,24,C.muted));return{a,X,Y};} function curve(points,X,Y,color=C.blue,band=true){const line=points.map((p,i)=>(i?'L':'M')+X(p.x)+' '+Y(p.y)).join(' '),a=[];if(band){const poly=points.map(p=>X(p.x)+' '+Y(p.lo)).concat([...points].reverse().map(p=>X(p.x)+' '+Y(p.hi))).join(' L');a.push(D('M'+poly+' Z','none',0,color,{'fill-opacity':.11}))}a.push(D(line,color,4));return a;} const short={Democrat:'Democrat',Independent:'Independent',Republican:'Republican','High school or less':'High school or less','Some college':'Some college','Degree or more':'Degree or more',age10:'Age (+10 yr)',educ:'Education code',logpopul:'Urban log(pop)',"C(party3)[T.Independent]":'Independent vs Dem.',"C(party3)[T.Republican]":'Republican vs Dem.'}; const figures=[];const pele=await import(pathToFileURL(process.argv[2])); for(const f of A.families)for(const v of f.variants){let a=[],note='',source='',map={}; if(v.kind==='chi'){ a.push(T(10,35,'A Expected vote within each party group',30,C.ink,700),T(1000,35,'B Deviation from independence',30,C.ink,700),T(1135,82,'Clinton',24,C.muted,600,'middle'),T(1425,82,'Dole',24,C.muted,600,'middle'));let rh=v.rows.length>3?65:145,top=105; v.rows.forEach((r,i)=>{let yy=top+i*rh,cs=v.cells.filter(c=>c.i===i);a.push(T(10,yy+24,v.rows.length>3?['0 Strong Dem.','1 Weak Dem.','2 Lean Dem.','3 Independent','4 Lean Rep.','5 Weak Rep.','6 Strong Rep.'][+r]:r,26,C.ink,600),T(10,yy+56,'n = '+cs.reduce((n,c)=>n+c.obs,0),23,C.muted));let xx=230;for(const c of cs){let width=c.pct*6.4,col=c.j===0?C.blue:C.teal;a.push(G([R(xx,yy,Math.max(1,width),46,col),...(width>112?[T(xx+width/2,yy+31,fmt(c.pct,1)+'%',25,'#FFFFFF',600,'middle')]:[])],c.key));xx+=width;let cx=1000+c.j*290;let strength=Math.min(1,Math.abs(c.residual)/15),fill=c.residual>0?'#DCE9F3':'#EFF2F4';a.push(G([R(cx,yy,270,65,fill),T(cx+10,yy+27,c.obs+' / E '+fmt(c.expected,1),25),T(cx+260,yy+54,'res '+signed(c.residual,1),23,C.muted,400,'end')],c.key))}}); a.push(R(12,587,18,18,C.blue),T(40,605,'Clinton',25),R(180,587,18,18,C.teal),T(208,605,'Dole',25),T(450,605,'χ²('+v.df+') = '+fmt(v.stat,1)+' · '+pv(v.p),28,C.ink,650),G([T(1000,605,'V '+fmt(v.effect,3)+' ['+v.effectCI.map(x=>fmt(x,3)).join(', ')+']',30,C.ink,700)],v.key));note='Counts are observed. E = expected counts. Residual color is descriptive; no cellwise significance claim.';source='flowchart LR\n P["Party identification"] --- V["Expected vote"]\n P --> C["Observed × expected counts"]\n V --> C\n C --> R["Residuals + Cramer V"]';map={P:v.key,V:v.cells[0].key,C:v.cells[1].key,R:v.key}; }else if(v.kind==='t'){ const rows=v.groups.map(g=>({label:g.label,b:g.mean,lo:g.ci[0],hi:g.ci[1],key:g.key}));a.push(...forest(rows,{x:0,y:8,w:1000,title:'A Group means · pointwise 95% CI',unit:v.unit,rowH:155,labelW:260,sign:false,range:v.id==='paired'?[1,7]:[Math.min(...rows.map(r=>r.lo))-1,Math.max(...rows.map(r=>r.hi))+1]}),T(1100,47,'B Difference',32,C.ink,700),G([T(1100,145,signed(v.difference),66,C.blue,750),T(1100,190,v.unit,28,C.muted),T(1100,258,'95% CI',26,C.muted),T(1100,305,'['+v.ci.map(x=>fmt(x,3)).join(', ')+']',36,C.ink,650),T(1100,377,'t('+fmt(v.df,1)+') = '+fmt(v.stat,2),27),T(1100,423,pv(v.p),27),T(1100,491,v.effectName+' = '+fmt(v.effect,3),30,C.teal,700),T(1100,533,'effect CI ['+v.effectCI.map(x=>fmt(x,3)).join(', ')+']',24,C.muted)],v.key),T(15,580,'Group n: '+v.groups.map(g=>g.n).join(' / ')+' · '+(v.id==='paired'?'same 944 respondents':'independent groups'),27,C.muted),...(v.id==='paired'?[T(15,625,'Scale: 1 very liberal → 7 very conservative',26,C.muted)]:[]));note=v.id==='paired'?'DoleLR−ClinLR on the same 1–7 numeric-coded scale. Not a causal candidate effect.':'Mean age differs by expected vote; Welch df reflects unequal variances. Nonrandom groups.';source=v.id==='paired'?'flowchart LR\n R["Same respondent"] --> A["Clinton impression"]\n R --> B["Dole impression"]\n A --> D["Within-person difference"]\n B --> D':'flowchart LR\n A["Clinton expected"] --> D["Mean-age difference"]\n B["Dole expected"] --> D\n D --> W["Welch CI"]';map={R:v.key,A:v.groups[0].key,B:v.groups[1].key,D:v.key,W:v.key}; }else if(v.kind==='correlation'){ a.push(T(0,35,'A Observed age × TV-news frequency',30,C.ink,700),T(1000,35,'B '+v.label+' correlation',30,C.ink,700),T(1000,62,(v.id==='pearson'?'r':'ρ')+' [95% CI]',20,C.muted));let pp=plotFrame({x:80,y:100,w:760,h:395,xRange:[15,95],yRange:[0,7],xTicks:[20,40,60,80],yTicks:[0,2,4,6],xLabel:'Age (years)',yLabel:'TV news (times/week)'});a.push(...pp.a);for(const b of v.bubbles)a.push(G([node('circle',{cx:pp.X(b.x),cy:pp.Y(b.y),r:Math.sqrt(b.count)*3.6,fill:C.blue,'fill-opacity':.38,stroke:C.blue,'stroke-width':.6})],b.key));let labs=['Age','News','Educ.','Urban'];for(let i=0;i<4;i++){a.push(T(1000+i*150+65,90,labs[i],25,C.ink,650,'middle'),T(970,153+i*100,labs[i],23,C.muted,400,'end'));for(let j=0;j<4;j++){let r=i===j?1:v.pairs.find(p=>p.i===Math.min(i,j)&&p.j===Math.max(i,j));let val=i===j?1:r.r;a.push(G([R(995+j*150,110+i*100,140,90,i===j?C.wash:val>0?'#DCE9F3':'#EAF2F1'),T(1065+j*150,153+i*100,fmt(val,2),32,C.ink,650,'middle'),...(i===j?[]:[T(1065+j*150,184+i*100,'['+r.ci.map(x=>fmt(x,2)).join(', ')+']',21,C.muted,400,'middle')])],i===j?undefined:r.key))}}a.push(T(995,557,'All pairs n = 944 · point area = observed count',25,C.muted),T(10,622,'No jitter or fabricated observations. Correlation does not identify a causal relation.',26,C.muted));note=v.id==='spearman'?'Ordinal sensitivity:rank-pair percentile bootstrap, 2000 draws. Matrix intervals are pointwise,not simultaneous.':'Pearson intervals use Fisher-z. Numeric-coded education is not an equal-interval measurement.';source='flowchart LR\n A["Age"] --- N["TV news"]\n E["Education"] --- N\n U["Urban scale"] --- N\n N --> R["Association + uncertainty"]';map={A:v.key,N:v.key,E:v.pairs.find(x=>x.i===1&&x.j===2)?.key,U:v.pairs.find(x=>x.i===1&&x.j===3)?.key,R:v.key}; }else if(v.kind==='regression'){ let rows=v.coef.filter(x=>x.term!=='Intercept').map(x=>({...x,label:short[x.term]||x.term}));a.push(...forest(rows,{x:0,y:0,w:980,labelW:300,rowH:86,title:'A Unstandardized B ·95% CI',unit:'Times/week per term unit'}));a.push(T(1080,33,'B Adjusted mean response',30,C.ink,700));let pp=plotFrame({x:1120,y:100,w:440,h:340,xRange:[15,95],yRange:[Math.min(2,...v.predictions.map(p=>p.lo)),Math.max(7,...v.predictions.map(p=>p.hi))],xTicks:[20,40,60,80],yTicks:[2,3,4,5,6,7],xLabel:'Age (years)',yLabel:'TV news (times/week)'});a.push(...pp.a,G(curve(v.predictions,pp.X,pp.Y),v.key),T(1095,575,'R² = '+fmt(v.r2,3)+' · N =944',28,C.teal,700));note='Age: 10 years; education: 1–7 code. Urban:log(population in1000s+.1). Other covariates averaged over this sample; band is a mean CI.';source='flowchart LR\n A["Age"] --> Y["TV news"]\n E["Education code"] --> Y\n U["Urban scale"] --> Y\n P["Party group"] --> Y\n Y --> R["Conditional association"]';map={A:v.key,E:rows.find(x=>x.term==='educ')?.key,U:rows.find(x=>x.term==='logpopul')?.key,P:rows.find(x=>x.term.includes('Independent'))?.key,Y:v.key,R:v.key}; }else if(v.kind==='anova'){ a.push(T(0,35,'A Actual0–7 times distributions',30,C.ink,700),T(900,35,'B Familywise mean contrasts',30,C.ink,700));const cols=[C.blue,C.teal,C.slate];v.groups.forEach((g,i)=>{let yy=105+i*155;a.push(T(0,yy,g.label,28,C.ink,650),T(0,yy+37,'n '+g.n+' · mean '+fmt(g.mean,2),25,C.muted));for(let d=0;d<8;d++){let n=g.hist.find(a=>a[0]===d)?.[1]||0,h=n/g.n*155;a.push(G([R(370+d*56,yy+85-h,40,h,cols[i]),T(390+d*56,yy+118,String(d),22,C.muted,400,'middle')],g.key))}});a.push(...forest(v.contrasts,{x:900,y:50,w:700,labelW:130,rowH:115,title:'',unit:'Times/week · corrected95% CI'}),T(905,560,'F('+v.df.map(x=>fmt(x,1)).join(', ')+') = '+fmt(v.F,3),27,C.ink,650),T(905,605,pv(v.p)+' · descriptive η² '+fmt(v.eta2Descriptive,4),26,C.muted));note='Groups: 1 = high school or less; 2 = some college; 3 = degree+. Bars show within-group frequencies; contrasts use the selected correction.';source='flowchart LR\n E["Education3groups"] --> Y["TV-news distribution"]\n Y --> F["Overall F"]\n Y --> P["Corrected pair contrasts"]\n P --> C["Uncertainty retained"]';map={E:v.groups[0].key,Y:v.groups[1].key,F:v.key,P:v.key,C:v.key}; }else if(v.kind==='two-way'){ a.push(T(0,35,'A Full-interaction cell means ·95% CI',30,C.ink,700),T(1010,35,'B '+v.label+' tests',30,C.ink,700));const ps=['Democrat','Independent','Republican'],bs=['18–34','35–54','55+'],cols=[C.blue,C.teal,C.slate];let pp=plotFrame({x:80,y:110,w:750,h:360,xRange:[-.2,2.2],yRange:[0,8],xTicks:[],yTicks:[0,2,4,6,8],xLabel:'Age band',yLabel:'TV news (times/week)'});a.push(...pp.a);bs.forEach((b,i)=>a.push(T(pp.X(i),510,b,26,C.muted,400,'middle')));ps.forEach((p,i)=>{const cells=v.cells.filter(c=>c.party===p),xx=j=>pp.X(j)+(i-1)*12;a.push(D(cells.map((c,j)=>(j?'L':'M')+xx(j)+' '+pp.Y(c.mean)).join(' '),cols[i],3));cells.forEach((c,j)=>a.push(G([L(xx(j),pp.Y(c.lo),xx(j),pp.Y(c.hi),cols[i],3),Q(xx(j),pp.Y(c.mean),7,cols[i])],c.key)));a.push(R(40+i*300,585,16,16,cols[i]),T(68+i*300,604,p,25),T(68+i*300,640,'n: '+cells.map(c=>c.n).join(' / '),22,C.muted))});v.terms.forEach((t,i)=>{let yy=125+i*135;a.push(T(1015,yy,['Party group','Age band','Party × age'][i],29,C.ink,650),T(1015,yy+43,'F('+t.df+', '+v.dfResidual+') = '+fmt(t.F,3),28),T(1015,yy+83,pv(t.p),26,C.muted))});note='Unbalanced 3×3 design, sum contrasts. Cell n and pointwise intervals remain visible. Joint interaction test is not inferred from line crossings.';source='flowchart LR\n P["Party group"] --> Y["TV news"]\n A["Age band"] --> Y\n I["Party × age"] --> Y\n Y --> C["Cell means + joint tests"]';map={P:v.cells[0].key,A:v.cells[1].key,I:v.key,Y:v.key,C:v.key}; }else if(v.kind==='moderation'){ a.push(T(0,35,'A Conditional mean response',30,C.ink,700),T(840,35,'B Conditional age slope ·95% CI',30,C.ink,700));let p1=plotFrame({x:60,y:100,w:620,h:300,xRange:[15,95],yRange:[Math.min(2,...v.curves.flat().map(p=>p.lo)),Math.max(7,...v.curves.flat().map(p=>p.hi))],xTicks:[20,40,60,80],yTicks:[2,3,4,5,6,7],xLabel:'Age (years)',yLabel:'TV news (times/week)'}),p2=plotFrame({x:900,y:100,w:630,h:300,xRange:[Math.min(...v.jn.map(x=>x.x)),Math.max(...v.jn.map(x=>x.x))],yRange:[Math.min(-.1,...v.jn.map(x=>x.lo)),Math.max(.7,...v.jn.map(x=>x.hi))],xTicks:[0,4,8],yTicks:[0,.2,.4,.6],xLabel:'Urban log(population in1000s+.1)',yLabel:'Times/week per10years'});a.push(...p1.a,...p2.a);v.curves.forEach((c,i)=>a.push(G(curve(c,p1.X,p1.Y,[C.blue,C.teal,C.slate][i],true).slice(0,1),v.slopes[i].key)));v.curves.forEach((c,i)=>{let col=[C.blue,C.teal,C.slate][i];for(const edge of ['lo','hi'])a.push(G([D(c.map((p,j)=>(j?'L':'M')+p1.X(p.x)+' '+p1.Y(p[edge])).join(' '),col,1.5,'none',{'stroke-dasharray':'5 5'})],v.slopes[i].key));a.push(G(curve(c,p1.X,p1.Y,col,false),v.slopes[i].key));});a.push(G(curve(v.jn,p2.X,p2.Y),v.key),L(900,p2.Y(0),1530,p2.Y(0),C.slate,2,'5 5'));v.slopes.forEach((s,i)=>{let xx=30+i*530;a.push(G([T(xx,535,'Urban '+s.label,28,[C.blue,C.teal,C.slate][i],700),T(xx,580,signed(s.b,3)+' ['+fmt(s.lo,3)+', '+fmt(s.hi,3)+']',28)],s.key))});a.push(G([T(15,625,'Interaction B '+signed(v.interaction.value,3)+' · 95% CI ['+v.interaction.ci.map(x=>fmt(x,3)).join(', ')+'] · '+pv(v.interaction.p),26,C.muted)],v.key));note='Observed moderator range; P10/P50/P90 lines; dashed bounds show each mean CI. Intervals retain full covariance and are pointwise,not simultaneous.';source='flowchart LR\n A["Age"] --> N["TV news"]\n U["Urban scale"] --> N\n I["Age × urban"] --> N\n N --> S["Conditional slopes + CI"]';map={A:v.slopes[0].key,U:v.slopes[2].key,I:v.key,N:v.key,S:v.slopes[1].key}; }else if(v.kind==='mediation'){ a.push(T(0,35,'A Structural path overview',30,C.ink,700),T(760,35,'B Direct,indirect and total association',30,C.ink,700));const boxes=[[40,270,'Industrialization','1960'],[250,90,'Democracy','1960'],[470,270,'Democracy','1965']];boxes.forEach(([x,y,s,t])=>a.push(R(x,y,190,85,C.wash),T(x+95,y+33,s,25,C.ink,650,'middle'),T(x+95,y+65,t,25,C.muted,400,'middle')));a.push(D('M180 270 L290 175',C.blue,3),AH(290,175,-41),D('M400 175 L520 270',C.blue,3),AH(520,270,38),D('M230 310 L470 310',C.teal,3),AH(470,310,0,C.teal),T(60,205,'a = '+fmt(v.a,3),28,C.blue,650),T(460,205,'b = '+fmt(v.b,3),28,C.blue,650),T(350,350,"c′ = "+fmt(v.c,3),28,C.teal,650,'middle'),T(70,470,'Indirect a×b = '+fmt(v.a*v.b,3),33,C.ink,700),T(70,520,'Same measurement + residual model',25,C.muted),T(70,559,'N = 75 countries · MLW fit',25,C.muted),...forest(v.effects,{x:760,y:60,w:840,labelW:220,rowH:118,title:'',unit:'Unstandardized latent-scale B'}));note=v.id==='med-mc'?'Parametric Monte Carlo 200,000 draws from full parameter covariance; not a nonparametric bootstrap. Observational indirect association.':'First-order delta normal intervals retain cross-parameter covariance. Path direction is model structure,not causal proof.';source='flowchart LR\n I["Industrialization1960"] -->|"a '+fmt(v.a,3)+'"| M["Democracy1960"]\n M -->|"b '+fmt(v.b,3)+'"| Y["Democracy1965"]\n I -->|"c direct '+fmt(v.c,3)+'"| Y';map={I:v.effects[0].key,M:v.effects[0].key,Y:v.effects[1].key}; }else if(v.kind==='sem'){ const latent=['ind60','dem60','dem65'],centers=[220,780,1340],indicators=[['x1','x2','x3'],['y1','y2','y3','y4'],['y5','y6','y7','y8']],pos={},find=(l,r,op='~')=>v.params.find(p=>p.lval===l&&p.rval===r&&p.op===op); // Residual nodes make covariance distinct from covariance of observed variables. latent.forEach((l,k)=>{let x=centers[k];a.push(node('ellipse',{cx:x,cy:95,rx:120,ry:42,fill:C.wash,stroke:C.blue,'stroke-width':2}),T(x,93,l==='ind60'?'Industrialization':'Democracy',24,C.ink,700,'middle'),T(x,122,l==='dem65'?'1965':'1960',23,C.muted,400,'middle'));indicators[k].forEach((name,j)=>{let yy=225+j*95;pos[name]={x:x+150,y:yy};let e=find(name,l);a.push(G([D('M'+(x-105)+' 113 C'+(x-175)+' '+(yy-60)+' '+(x-180)+' '+yy+' '+(x-92)+' '+yy,C.blue,2),AH(x-92,yy,0),R(x-92,yy-26,190,52,C.wash),T(x+3,yy+10,name+' · '+({x1:'GNP/capita',x2:'Energy',x3:'Industry',y1:'Press',y2:'Opposition',y3:'Elections',y4:'Legislature',y5:'Press',y6:'Opposition',y7:'Elections',y8:'Legislature'}[name]),23,C.ink,650,'middle'),T(x-137,yy-15,fmt(e.b,2),23,C.blue,600,'middle'),Q(x+150,yy,16,C.paper,C.slate),T(x+150,yy+6,'ε',22,C.muted,400,'middle'),L(x+134,yy,x+100,yy,C.slate,2),AH(x+100,yy,180,C.slate)],e.key))})}); for(const [l,r,start,end] of [['dem60','ind60',340,660],['dem65','dem60',900,1220]]){let e=find(l,r);a.push(G([L(start,95,end,95,C.blue,3),D('M'+(end-12)+' 87 L'+end+' 95 L'+(end-12)+' 103',C.blue,2),T((start+end)/2,70,fmt(e.b,3),28,C.blue,700,'middle'),T((start+end)/2,130,'['+e.ci.map(x=>fmt(x,2)).join(', ')+']',23,C.muted,400,'middle')],e.key))} let direct=find('dem65','ind60');a.push(G([D('M220 50 C450 2 1090 2 1340 50',C.teal,2),AH(1340,50,11,C.teal),R(550,4,460,32,C.paper),T(780,30,"Direct "+fmt(direct.b,3)+' ['+direct.ci.map(x=>fmt(x,2)).join(', ')+']',25,C.teal,600,'middle')],direct.key)); if(v.id==='sem-full')for(const [u,w] of [['y1','y5'],['y2','y4'],['y2','y6'],['y3','y7'],['y4','y8'],['y6','y8']]){let e=v.params.find(p=>p.op==='~~'&&((p.lval===u&&p.rval===w)||(p.lval===w&&p.rval===u))),s=pos[u],t=pos[w];if(!e)throw Error('missing covariance '+u+w);let d=s.x===t.x?'M'+s.x+' '+s.y+' C'+(s.x+70)+' '+s.y+' '+(t.x+70)+' '+t.y+' '+t.x+' '+t.y:'M'+s.x+' '+s.y+' C'+(s.x+150)+' '+(s.y-52)+' '+(t.x-160)+' '+(t.y-52)+' '+t.x+' '+t.y;let tx=s.x===t.x?s.x+64:(s.x+t.x)/2-100,ty=s.x===t.x?(s.y+t.y)/2:s.y-30;a.push(G([D(d,C.slate,2,'none',{'stroke-dasharray':'5 4'}),AH(s.x+16,s.y,s.x===t.x?180:160,C.slate),AH(t.x-16,t.y,s.x===t.x?0:20,C.slate),T(tx,ty,fmt(e.b,2),23,C.muted,400,'middle')],e.key))} a.push(T(25,555,'χ²('+v.fit.DoF+') '+fmt(v.fit.chi2,2)+' · '+pv(v.fit['chi2 p-value']),27,C.ink,650),T(25,597,'CFI '+fmt(v.fit.CFI,3)+' · RMSEA '+fmt(v.fit.RMSEA,3),27,C.teal,700),T(760,585,'B unstandardized · first loadings fixed at 1',25,C.muted),T(760,625,'Dashed: residual covariance · ε: indicator error',25,C.muted));note='75 countries, 11 indicators, 3 latent variables. Structural path CIs are normal expected-Fisher intervals. Good fit is not causal identification.';source='flowchart LR\n I(["Industrialization1960"]) --> D(["Democracy1960"])\n D --> Y(["Democracy1965"])\n I --> Y\n X["x1 x2 x3"] --- I\n A["y1 y2 y3 y4"] --- D\n B["y5 y6 y7 y8"] --- Y';map={I:v.key,D:v.key,Y:v.key,X:find('x2','ind60').key,A:find('y2','dem60').key,B:find('y6','dem65').key}; } map=modelRoles(f,v,A);const tree=SVG(a),svg=xml(tree).replace(/(]*>)/,'$1'+esc(f.name+' · '+v.label)+''+esc(f.claim+' '+note)+'');await fs.writeFile(path.join(O,v.id+'.svg'),svg);await fs.writeFile(path.join(O,v.id+'.ast.json'),JSON.stringify(tree));await fs.writeFile(path.join(O,v.id+'.pele'),source);const native=pele.render(source,{fontFamily:'Pretendard Variable',fontSize:23,idPrefix:v.id+'-'}).svg;await fs.writeFile(path.join(O,v.id+'-native.svg'),native);figures.push({family:f.id,variant:v.id,title:f.claim,note,source,map,svg:v.id+'.svg',native:v.id+'-native.svg'}); } await fs.writeFile(path.join(O,'analyses.json'),JSON.stringify(A)); await fs.writeFile(path.join(O,'figures.json'),JSON.stringify(figures)); console.log(JSON.stringify({scientific:figures.length,native:figures.length,sourceBytes:figures.reduce((n,f)=>n+Buffer.byteLength(f.source),0)}));