#!/usr/bin/env python3
"""COMPLEXA C221–C230: source-derived computational complexity and organization SVG/CSV illustrations.

Pure Python 3.12+ standard library; deterministic charts, numerical audits and ZIP.
Run: python make_figures.py --out /path/to/content/supplementary
     python make_figures.py --check /path/to/content/supplementary
Every panel derives from the manuscript-defined examples, never unreported observations.
"""
from __future__ import annotations
import argparse
import csv
import hashlib
import io
import math
from pathlib import Path
from xml.sax.saxutils import escape
from zipfile import ZipFile, ZipInfo, ZIP_STORED

BASE = Path(__file__).resolve().parent
TEAL = '#087f8c'; BLUE = '#326d98'; ORANGE = '#c26c38'; PLUM = '#754c96'
INK = '#183348'; MUTED = '#566c78'; GRID = '#dae4eb'; BG = '#ffffff'; LT = '#f3f7fa'; GREEN='#2d7865'
COLORS=[BLUE,TEAL,ORANGE,PLUM,GREEN]
W,H=1120,680

def h(p):
    if p<=0 or p>=1:
        assert 0<=p<=1
        return 0.
    return -p*math.log2(p)-(1-p)*math.log2(1-p)

def entropy(p):
    return -sum(v*math.log2(v) for v in p if v>0)

def kl(p,q):
    return sum(a*math.log2(a/b) for a,b in zip(p,q) if a>0)

def fmt(x,n=6):
    return f'{x:.{n}f}'.rstrip('0').rstrip('.') if isinstance(x,float) else str(x)

def number(x):
    return f'{x:.10f}' if isinstance(x,float) else str(x)

def csv_content(cols,rows):
    out=io.StringIO(newline='')
    writer=csv.writer(out,lineterminator='\n');writer.writerow(cols)
    for row in rows: writer.writerow([number(x) for x in row])
    return out.getvalue().encode('utf-8')

class Figure:
    def __init__(self,n,title,subtitle,detail):
        self.n=n;self.a=[]
        self.add(f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}" role="img" aria-labelledby="title desc">')
        self.add(f'<title id="title">{escape(title)}</title><desc id="desc">{escape(detail)}</desc>')
        self.rect(0,0,W,H,BG);self.rect(0,0,W,9,TEAL)
        self.txt(65,47,f'COMPLEXA · FIGURA {n}.1',14,TEAL,True)
        self.txt(65,87,title,26,INK,True)
        self.txt(65,119,subtitle,14,MUTED)
        self.line(65,139,W-65,139,GRID,1.5)
    def add(self,s): self.a.append(s)
    def rect(self,x,y,w,h,color,r=0,stroke=None):
        b=f' stroke="{stroke}"' if stroke else ''
        self.add(f'<rect x="{x:.2f}" y="{y:.2f}" width="{w:.2f}" height="{h:.2f}" rx="{r:.1f}" fill="{color}"{b}/>')
    def line(self,x,y,x2,y2,color=GRID,width=1,dash=None):
        dd=f' stroke-dasharray="{dash}"' if dash else ''
        self.add(f'<line x1="{x:.2f}" y1="{y:.2f}" x2="{x2:.2f}" y2="{y2:.2f}" stroke="{color}" stroke-width="{width:.2f}"{dd}/>')
    def txt(self,x,y,s,size=14,color=INK,bold=False,anchor='start'):
        self.add(f'<text x="{x:.2f}" y="{y:.2f}" text-anchor="{anchor}" fill="{color}" font-family="Arial,Helvetica,sans-serif" font-size="{size:.1f}" font-weight="{700 if bold else 400}">{escape(str(s))}</text>')
    def circle(self,x,y,r,fill):self.add(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="{r:.2f}" fill="{fill}" stroke="#fff" stroke-width="1.6"/>')
    def poly(self,xy,col,w=3,dash=None):
        pts=' '.join(f'{x:.2f},{y:.2f}' for x,y in xy)
        d=f' stroke-dasharray="{dash}"' if dash else ''
        self.add(f'<polyline points="{pts}" fill="none" stroke="{col}" stroke-width="{w:.2f}" stroke-linejoin="round" stroke-linecap="round"{d}/>')
    def legend(self,parts,x=735,y=166):
        for i,(name,color) in enumerate(parts):
            yy=y+i*23
            self.rect(x,yy-12,24,9,color,2);self.txt(x+34,yy-3,name,13,MUTED)
    def note(self,lead,txt):
        self.rect(65,583,W-130,68,LT,8,GRID)
        self.txt(82,610,lead,14,TEAL,True)
        self.txt(82,635,txt,13,MUTED)
    def output(self):return ('\n'.join(self.a+['</svg>'])+'\n').encode('utf-8')

class Axes:
    def __init__(self,f,x0=119,y0=188,w=847,h=322,xmin=0,xmax=1,ymin=0,ymax=1,xlabel='',ylabel=''):
        assert xmax>xmin and ymax>ymin
        self.f=f;self.x0=x0;self.y0=y0;self.w=w;self.h=h
        self.xmin=xmin;self.xmax=xmax;self.ymin=ymin;self.ymax=ymax
        f.rect(x0,y0,w,h,BG,4,GRID)
        f.line(x0,y0+h,x0+w,y0+h,INK,1.8);f.line(x0,y0,x0,y0+h,INK,1.8)
        f.txt(x0+w/2,y0+h+53,xlabel,15,INK,anchor='middle')
        f.txt(x0-13,y0-14,ylabel,13,MUTED)
    def xy(self,x,y):return (self.x0+(x-self.xmin)/(self.xmax-self.xmin)*self.w,
                                  self.y0+self.h-(y-self.ymin)/(self.ymax-self.ymin)*self.h)
    def ticks(self,xs,ys):
        for x,s in xs:
            px,_=self.xy(x,self.ymin);self.f.line(px,self.y0,px,self.y0+self.h,GRID,1)
            self.f.txt(px,self.y0+self.h+24,s,12,MUTED,anchor='middle')
        for y,s in ys:
            _,py=self.xy(self.xmin,y);self.f.line(self.x0,py,self.x0+self.w,py,GRID,1)
            self.f.txt(self.x0-12,py+4,s,12,MUTED,anchor='end')
    def curve(self,pts,col,width=3):self.f.poly([self.xy(*xy) for xy in pts],col,width)
    def dot(self,x,y,color=ORANGE,r=5):self.f.circle(*self.xy(x,y),r,color)
    def dotted(self,x,y,col=ORANGE):
        p1=self.xy(x,self.ymin);p2=self.xy(x,y);self.f.line(*p1,*p2,col,1.5,'4 6')



# Explicit mathematical examples of C221–C230. These numbers do not describe observations.

def bar(f,a,x,height,w,color):
    x0,y0=a.xy(x-w/2,0);x1,yt=a.xy(x+w/2,height)
    if height>0:f.rect(x0,yt,x1-x0,y0-yt,color,4)

def build221():
    def g(n):return 2*(n.bit_length()-1)+1
    literal=2+g(1024)+1024
    constant=2+1+g(1024)
    repeat=2+g(4)+g(256)+4
    doubled=2+g(512)+g(2)+512
    assert (literal,constant,repeat,doubled)==(1047,24,28,536)
    rows=[('Literal 1.024 bits',literal,1024),('Constante 1.024 bits',constant,1024),('Patrón 0011 × 256',repeat,1024),('Bloque de 512 × 2',doubled,1024)]
    f=Figure(221,'La descripción más corta depende del repertorio',
      'Tres instrucciones fijas y código entero g(n)=2⌊log₂n⌋+1; salida de 1.024 bits',
      'Longitudes exactas: literal 1.047, constante 24, patrón 0011 repetido 256 veces 28, bloque arbitrario de 512 bits duplicado 536. Son cotas de un lenguaje limitado, no complejidad de Kolmogorov.')
    a=Axes(f,xmin=.5,xmax=4.5,ymin=0,ymax=1160,xlabel='Objeto y descripción admitida',ylabel='Longitud de código (bits)')
    a.ticks([(i,t) for i,t in enumerate(['Literal','Constante','0011×256','512×2'],1)],[(v,str(v)) for v in (0,256,512,768,1024)])
    for i,(lab,n,out) in enumerate(rows,1):
        bar(f,a,i,n,.52,COLORS[(i-1)%len(COLORS)])
        xx,yy=a.xy(i,n);f.txt(xx,yy-9,str(n),17,INK,True,'middle')
    f.note('No es una estimación universal','Los costos exactos incluyen metadatos del formato didáctico, pero no un decodificador universal.')
    return f.output(),csv_content(['objeto','bits_descripcion','bits_salida','ratio_descripcion_a_salida'],[(name,n,length,n/length) for name,n,length in rows])

def build222():
    rows=[(L,2**(L+1)-1,(2**(L+1)-1)*1_000_000,math.log10(2**(L+1)-1)) for L in range(21)]
    assert rows[20][1]==2097151 and rows[20][2]==2097151000000
    f=Figure(222,'Una búsqueda finita puede ser inmensa',
       'N(L)=2^(L+1)−1 programas binarios de longitud hasta L (incluida la vacía)',
       'Número exacto de programas candidatos para el límite de longitud L de 0 a 20; la escala vertical es logaritmo decimal de candidatos, no probabilidad ni complejidad universal.')
    a=Axes(f,xmin=0,xmax=20,ymin=0,ymax=6.5,xlabel='Límite de longitud L (bits)',ylabel='log₁₀ del número de programas')
    a.ticks([(v,str(v)) for v in (0,5,10,15,20)],[(v,str(v)) for v in (0,1,2,3,4,5,6)])
    a.curve([(L,v) for L,N,steps,v in rows],TEAL)
    for L in (0,5,10,15,20):a.dot(L,rows[L][3],ORANGE)
    f.note('L = 20','2.097.151 candidatos y hasta 2.097.151.000.000 pasos si se prueban 10⁶ pasos por candidato.')
    return f.output(),csv_content(['L_bits','programas_hasta_L','pasos_con_T_1000000','log10_programas'],rows)

def build223():
    # The literal LZ78 example 001001011: phrases 0,01,00,1,011.
    steps=[(1,0,'0','0',0,1,1),(2,1,'1','01',1,3,3),(3,1,'0','00',2,6,5),(4,0,'1','1',2,9,6),(5,2,'1','011',3,13,9)]
    assert ''.join(r[3] for r in steps)=='001001011'
    assert sum(r[4]+1 for r in steps)==13
    f=Figure(223,'LZ78: las referencias también cuestan bits',
       'Secuencia 001001011; índices variables de 0, 1, 2, 2 y 3 bits',
       'El costo parcial de LZ78 es trece bits para nueve símbolos originales, antes de marcas de final; se muestran las cinco entradas nuevas del diccionario.')
    a=Axes(f,xmin=.7,xmax=5.3,ymin=0,ymax=14.4,xlabel='Número de emisiones LZ78',ylabel='Bits acumulados (sin cabecera)')
    a.ticks([(x,str(x)) for x in range(1,6)],[(y,str(y)) for y in (0,3,6,9,12)])
    a.curve([(n,bits) for n,idx,s,phr,ib,bits,source in steps],TEAL)
    a.curve([(n,source) for n,idx,s,phr,ib,bits,source in steps],BLUE)
    for n,idx,s,phr,ib,bits,source in steps:a.dot(n,bits,TEAL,5)
    f.legend([('LZ78: índice + símbolo',TEAL),('Texto literal procesado',BLUE)],690,165)
    f.note('Reconstrucción verificada','Frases: 0 | 01 | 00 | 1 | 011. El formato parcial usa 13 bits frente a 9 originales.')
    return f.output(),csv_content(['paso','indice_previo','simbolo_anadido','frase_nueva','bits_indice','bits_acumulados','bits_originales'],steps)

def build224():
    rows=[(t,t.bit_count(),1<<t.bit_count(),math.log2(1<<t.bit_count())) for t in range(1,33)]
    assert rows[6][2]==8 and rows[7][2]==2 and (1024).bit_count()==1
    f=Figure(224,'Regla 90: el número de consultas depende del tiempo binario',
        'Autómata XOR: términos iniciales = 2^(número de unos en el tiempo t)',
        'Para una celda en una línea infinita de regla 90, consultar los términos impares del desarrollo algebraico evita actualizar todos los pasos: t7 requiere ocho valores, t8 solo dos, t1024 dos.')
    a=Axes(f,xmin=1,xmax=32,ymin=0,ymax=33,xlabel='Horizonte t (pasos)',ylabel='Consultas a celdas iniciales')
    a.ticks([(v,str(v)) for v in (1,7,8,16,24,32)],[(v,str(v)) for v in (0,8,16,24,32)])
    a.curve([(t,terms) for t,k,terms,lv in rows],TEAL)
    for t in (7,8,15,16,31,32):a.dot(t,rows[t-1][2],ORANGE,5)
    f.note('Dos horizontes próximos','t=7: ocho consultas; t=8: dos consultas; t=1.024: también dos, con accesos arbitrarios.')
    return f.output(),csv_content(['tiempo_t','unos_en_binario','consultas_iniciales','log2_consultas'],rows)

def build225():
    rows=[(n,n*n,2**n,math.log10(n*n),n*math.log10(2)) for n in range(5,41)]
    assert [(n,sq,exp) for n,sq,exp,_,_ in rows if n in (10,20,40)]==[(10,100,1024),(20,400,1048576),(40,1600,1099511627776)]
    f=Figure(225,'El costo exponencial termina dominando',
       'Dos cuentas hipotéticas del manuscrito: n² operaciones frente a 2ⁿ candidatas',
       'Las funciones cuadrática y exponencial se muestran en escala vertical logaritmo decimal; para n40 son 1600 frente a 1.099.511.627.776 operaciones.')
    a=Axes(f,xmin=5,xmax=40,ymin=0,ymax=13,xlabel='Tamaño de entrada n (bits o elementos del ejemplo)',ylabel='log₁₀ del costo (operaciones)')
    a.ticks([(v,str(v)) for v in (5,10,20,30,40)],[(v,str(v)) for v in (0,3,6,9,12)])
    a.curve([(n,lq) for n,sq,exp,lq,le in rows],BLUE)
    a.curve([(n,le) for n,sq,exp,lq,le in rows],TEAL)
    for n in (10,20,40):a.dot(n,rows[n-5][4],ORANGE,5)
    f.legend([('n²',BLUE),('2ⁿ',TEAL)],795,165)
    f.note('n = 40','Cuadrático: 1.600; enumeración completa: 1.099.511.627.776. La escala es logarítmica.')
    return f.output(),csv_content(['tamano_n','n_cuadrado','dos_a_n','log10_n_cuadrado','log10_dos_a_n'],rows)

def build226():
    rows=[('Literal',2,16,18),('Cuatro unos',7,11,18),('Periódico 0010',11,0,11),('Periódico + 1 error',11,4,15)]
    assert all(a+b==t for _,a,b,t in rows)
    f=Figure(226,'Separar costo del modelo y detalle residual',
       'Cadena de 16 bits, metadatos del formato explícito (C226.7–8)',
       'Barras apiladas: literal 2+16, cuatro unos 7+11, repetición 0010 11+0 y repetición con excepción 11+4 bits; no es complejidad algorítmica universal.')
    a=Axes(f,xmin=.4,xmax=4.6,ymin=0,ymax=20,xlabel='Descripción del registro de 16 bits',ylabel='Bits de longitud total')
    a.ticks([(i,t) for i,t in enumerate(['Literal','Conteo','Periódico','+ excepción'],1)],[(v,str(v)) for v in (0,5,10,15,20)])
    for i,(label,model,resid,total) in enumerate(rows,1):
        bar(f,a,i,model,.44,TEAL)
        x0,top=a.xy(i-.22,model+resid);x1,base=a.xy(i+.22,model)
        if resid:f.rect(x0,top,x1-x0,base-top,ORANGE,3)
        xp,yp=a.xy(i,total);f.txt(xp,yp-9,str(total),15,INK,True,'middle')
    f.legend([('Estructura o modelo',TEAL),('Detalle residual',ORANGE)],750,166)
    f.note('Longitudes del código, no K(x)','La excepción cambia el repertorio. Transmitir una regla compacta no demuestra minimalidad universal.')
    return f.output(),csv_content(['formato','bits_modelo','bits_residuo','bits_totales'],rows)

def build227():
    p=.2;rate=h(p)
    rows=[('Markov persistente',1.,rate,1.-rate),('Bernoulli sesgada',0.,rate,0.)]
    assert abs(rate-.721928094887)<1e-12
    f=Figure(227,'Igual tasa, distinta memoria predictiva',
       'Cadena de Markov estacionaria y moneda sesgada: h = h₂(0,2) = 0,721928 bits/símbolo',
       'Comparación teórica: la cadena persistente necesita 1 bit de estado predictivo y comparte 0,278072 bits entre pasado y futuro; la moneda sesgada requiere 0 bits, aunque ambas tienen la misma tasa.')
    a=Axes(f,xmin=.5,xmax=2.5,ymin=0,ymax=1.2,xlabel='Tipo de fuente',ylabel='Cantidad informacional (bits)')
    a.ticks([(1,'Markov'),(2,'Moneda sesgada')],[(v,fmt(v,1)) for v in (0,.25,.5,.75,1)])
    for i,(lab,Cmu,hh,E) in enumerate(rows,1):
        for d,val,col in [(-.25,Cmu,ORANGE),(0,hh,TEAL),(.25,E,BLUE)]:bar(f,a,i+d,val,.21,col)
    f.legend([('Cμ: memoria mínima',ORANGE),('h: tasa de entropía',TEAL),('E: memoria compartida',BLUE)],765,166)
    f.note('Misma novedad, memoria diferente','Markov: Cμ=1, E=0,2781 bits; Bernoulli: Cμ=E=0. No inferir estructura solo desde h.')
    return f.output(),csv_content(['proceso','Cmu_bits','tasa_entropia_bits_simbolo','E_bits'],rows)

def build228():
    words=['00','01','10','11']
    gm=[0,1/3,1/3,1/3]
    ev=[1/6,1/6,1/6,1/2]
    assert abs(sum(gm)-1)<1e-12 and abs(sum(ev)-1)<1e-12
    f=Figure(228,'Dos máquinas con igual memoria y distintas palabras',
       'Golden Mean y Even con P(A)=2/3, P(B)=1/3: probabilidades de pares de símbolos',
       'La máquina Golden Mean prohíbe 00 y distribuye 01, 10, 11 igualmente; Even admite 00, 01, 10 con probabilidad 1/6 cada una y 11 con probabilidad 1/2.')
    a=Axes(f,xmin=.5,xmax=4.5,ymin=0,ymax=.6,xlabel='Palabra observada de dos símbolos',ylabel='Probabilidad teórica')
    a.ticks([(i,w) for i,w in enumerate(words,1)],[(v,fmt(v,2)) for v in (0,.1,.2,.3,.4,.5)])
    for i,w in enumerate(words,1):
        bar(f,a,i-.15,gm[i-1],.26,TEAL)
        bar(f,a,i+.15,ev[i-1],.26,ORANGE)
    f.legend([('Golden Mean',TEAL),('Even',ORANGE)],815,164)
    f.note('Diferente ley de observaciones','Golden Mean: P(00)=0; Even: P(00)=1/6. Ambas tienen Cμ≈0,9183 bits.')
    return f.output(),csv_content(['palabra','P_Golden_Mean','P_Even'],[(w,a,b) for w,a,b in zip(words,gm,ev)])

def build229():
    from math import comb
    fair=2**(-10)
    integrated=math.factorial(6)*math.factorial(4)/math.factorial(11)
    factor=fair/integrated
    assert math.isclose(integrated,1/2310,rel_tol=1e-14) and math.isclose(factor,2310/1024,rel_tol=1e-14)
    rows=[('Moneda fija',fair,fair*10_000),('Moneda con prior uniforme',integrated,integrated*10_000)]
    f=Figure(229,'La evidencia penaliza flexibilidad no necesaria',
      'Secuencia de diez bits con seis unos: verosimilitud puntual fija frente a marginal integrada',
      'Barras de probabilidad de una secuencia binaria concreta bajo moneda justa p=0,5 y bajo probabilidad desconocida con prior uniforme; razón de evidencias 2310/1024 ≈2,25586.')
    a=Axes(f,xmin=.5,xmax=2.5,ymin=0,ymax=12,xlabel='Modelo probabilístico y prior',ylabel='Probabilidad de secuencia × 10.000')
    a.ticks([(1,'p=0,5'),(2,'Prior uniforme')],[(v,str(v)) for v in (0,3,6,9,12)])
    for i,(lab,p,val) in enumerate(rows,1):
        bar(f,a,i,val,.48,COLORS[i-1]);xp,yp=a.xy(i,val);f.txt(xp,yp-13,fmt(val,4),18,INK,True,'middle')
    f.note('Un cálculo de evidencia, no una prueba causal','P fija=1/1024; P integrada=1/2310. Razón fija/flexible = 2,25586.')
    return f.output(),csv_content(['modelo','evidencia_secuencia','evidencia_multiplicada_10000'],rows)

def build230():
    rows=[('Independiente 0,5',1.,1.,0.,0.),('Independiente 0,2',h(.2),h(.2),0.,0.),
      ('Markov 0,8',1.,h(.2),1.,1-h(.2)),('Ciclo 0011',1.,0.,2.,2.),
      ('Golden Mean',h(1/3),2/3,h(1/3),h(1/3)-2/3),('Even',h(1/3),2/3,h(1/3),h(1/3))]
    assert math.isclose(rows[4][3],rows[5][3]) and math.isclose(rows[4][2],rows[5][2])
    f=Figure(230,'Seis generadores: azar, estructura y memoria',
       'Teoría exacta del capítulo de síntesis: H₁, tasa h, complejidad Cμ y exceso E',
       'Seis modelos analíticos muestran tasas de entropía y memorias diferentes; Golden Mean y Even comparten tasa y Cmu, pero discrepan en E. Todas son cantidades teóricas, no estimaciones simuladas.')
    a=Axes(f,xmin=.45,xmax=6.55,ymin=0,ymax=2.25,xlabel='Proceso (enumeración 1–6, ver CSV y nota)',ylabel='Magnitud de información (bits)')
    a.ticks([(i,t) for i,t in enumerate(['IID 1/2','IID 1/5','Markov','Ciclo','Golden','Even'],1)],[(v,fmt(v,1)) for v in (0,.5,1,1.5,2)])
    for i,(name,H1,hh,Cmu,E) in enumerate(rows,1):
        for dx,v,col in [(-.18,hh,TEAL),(0,Cmu,ORANGE),(.18,E,PLUM)]:bar(f,a,i+dx,v,.17,col)
    f.legend([('h: incertidumbre nueva',TEAL),('Cμ: memoria de estados',ORANGE),('E: pasado–futuro',PLUM)],790,163)
    f.note('Modelos 5 y 6: Golden Mean frente a Even','Ambos h=2/3 y Cμ≈0,9183; E≈0,2516 frente a 0,9183 bits.')
    return f.output(),csv_content(['proceso','H1_bits','tasa_h_bits_simbolo','Cmu_bits','E_bits'],rows)

CASES={n:globals()[f'build{n}'] for n in range(221,231)}

def generated():
    result={}
    for n,fn in CASES.items():
        svg,data=fn()
        result[f'figures/complexa-c{n}-fig01.svg']=svg
        result[f'repro/c221-230/c{n}.csv']=data
    return result

def bundle(files):
    buf=io.BytesIO()
    with ZipFile(buf,'w',compression=ZIP_STORED) as z:
        for path,content in sorted(files.items()):
            name=path.removeprefix('figures/').removeprefix('repro/c221-230/')
            inf=ZipInfo(name,(2026,1,1,0,0,0));inf.compress_type=ZIP_STORED
            inf.create_system=3;inf.external_attr=0o644<<16
            z.writestr(inf,content)
    return buf.getvalue()

def run(check:bool,root:Path):
    files=generated()
    import xml.etree.ElementTree as ET
    for n in CASES:
        ET.fromstring(files[f'figures/complexa-c{n}-fig01.svg'])
        data=list(csv.reader(io.StringIO(files[f'repro/c221-230/c{n}.csv'].decode())))
        assert len(data)>=3 and len(data[0])>=3,n
        assert all(len(x)==len(data[0]) for x in data[1:]),n
    extra={'repro/c221-230/make_figures.py':BASE.joinpath('make_figures.py').read_bytes(),
           'repro/c221-230/README.md':BASE.joinpath('README.md').read_bytes()}
    zip_bytes=bundle({**files,**extra})
    if check:
        for path,expected in files.items():
            target=root/path;assert target.is_file() and target.read_bytes()==expected,f'CHECK FAIL: {target}'
        zp=root/'zip-data/complexa-c221-c230-reproducible.zip.b64'
        import base64
        assert zp.is_file() and base64.b64decode(zp.read_text().strip())==zip_bytes,'CHECK FAIL: zip'
        print('PASS C221–C230: 10 SVG, 10 CSV, ZIP bitwise reproducible; numerical checks.');return
    for path,data in files.items():
        target=root/path;target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(data)
    import base64
    zp=root/'zip-data/complexa-c221-c230-reproducible.zip.b64';zp.parent.mkdir(parents=True,exist_ok=True)
    zp.write_text(base64.b64encode(zip_bytes).decode()+'\n')
    direct=root/'complexa-c221-c230-reproducible.zip';direct.write_bytes(zip_bytes)
    print(f'PASS C221–C230: {len(files)} SVG+CSV; ZIP {len(zip_bytes)} bytes; SHA256 {hashlib.sha256(zip_bytes).hexdigest()}')

if __name__=='__main__':
    ap=argparse.ArgumentParser();g=ap.add_mutually_exclusive_group(required=True)
    g.add_argument('--out',type=Path);g.add_argument('--check',type=Path)
    args=ap.parse_args()
    run(args.check is not None,args.check or args.out)
