{
  "version": "0.3",
  "description": "Static browser app with multi-configurator profile adapters. PDF documents remain in the browser.",
  "files": {
    "index.html": "<!doctype html><html lang=\"it\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><meta name=\"robots\" content=\"noindex,nofollow\"><meta name=\"theme-color\" content=\"#1c332d\"><title>Secondo Occhio — Controllo ordini multi-configuratore</title><link rel=\"stylesheet\" href=\"/style.css\"></head><body>\n<header><a href=\"/\" class=\"brand\"><span class=\"mark\">SO</span><span>Secondo Occhio<small>AF UTILITY · VF ARTE DEL FERRO</small></span></a><span class=\"tag\">BETA 0.3 · MULTI-CONFIGURATORE</span></header>\n<main>\n<div class=\"intro\"><div><span class=\"eyebrow\">DALL'ORDINE ALLA CONFERMA</span><h1>Un secondo controllo,<br>prima della produzione.</h1><p>Confronta il documento di partenza con la conferma del produttore anche quando arrivano da software o configuratori differenti. Il sistema riconosce il profilo compatibile e segnala ciò che richiede verifica.</p></div><aside><strong>I PDF restano nel browser.</strong><p>Elaborazione testuale sul dispositivo. Nessun caricamento dei documenti sul server, nessuna chiave AI richiesta.</p></aside></div>\n<section class=\"panel\" aria-labelledby=\"upload-title\"><div class=\"section-head\"><h2 id=\"upload-title\">01 <span>Carica i documenti</span></h2><button id=\"demo\" class=\"text-button\">Prova un esempio sintetico</button></div>\n<div class=\"upload-grid\"><label class=\"upload\"><span class=\"eyebrow\">DOCUMENTO DI PARTENZA</span><strong>Ordine inviato</strong><span>Rilevamento automatico del formato</span><input id=\"order\" type=\"file\" accept=\"application/pdf,.pdf\"><small id=\"ordername\">PDF testuale · massimo 20 MB</small></label><label class=\"upload\"><span class=\"eyebrow\">DOCUMENTO DA CONTROLLARE</span><strong>Conferma del produttore</strong><span>Rilevamento automatico del configuratore</span><input id=\"confirmation\" type=\"file\" accept=\"application/pdf,.pdf\"><small id=\"confirmationname\">PDF testuale · massimo 20 MB</small></label></div>\n<div class=\"actions\"><button id=\"analyze\" class=\"primary\">Leggi i due PDF</button><span id=\"status\" role=\"status\" aria-live=\"polite\">Seleziona entrambi i documenti.</span></div>\n<p class=\"limits\">Secondo Occhio usa profili di lettura dedicati e un fallback generico prudente. Un formato nuovo può richiedere un adapter specifico: in quel caso i campi non riconosciuti restano «Da verificare» invece di produrre conclusioni forzate. Scansioni e note scritte a mano non sono lette.</p></section>\n<div id=\"workspace\" hidden>\n<div id=\"demo-note\" class=\"notice\" hidden>ESEMPIO SINTETICO — I dati seguenti servono a provare i comandi. Non sono il risultato dell'analisi dei tuoi PDF.</div>\n<section class=\"panel\"><h2>02 <span>Verifica l'abbinamento</span></h2><div id=\"documents\" class=\"upload-grid\"></div><div id=\"warnings\"></div><label class=\"check\"><input type=\"checkbox\" id=\"samejob\">Confermo che i due documenti riguardano la stessa commessa.</label><p class=\"limits\">Gli abbinamenti proposti usano ingombro, quantità e apertura, non solo il numero di posizione. Controllali sempre sui documenti originali.</p><div id=\"mapping\"></div><button id=\"compare\" class=\"primary\">Confronta le posizioni abbinate</button></section>\n<section id=\"results\" class=\"panel\" hidden><div class=\"section-head\"><h2>03 <span>Rapporto di controllo</span></h2><div class=\"actions\"><button id=\"export\">Scarica JSON</button><button id=\"print\">Stampa / PDF</button></div></div><div id=\"summary\" class=\"metrics\"></div><div id=\"economics\"></div><div class=\"filterbar\"><label>Mostra <select id=\"filter\"><option value=\"all\">Tutti i controlli</option><option value=\"issues\">Differenze e verifiche</option></select></label><span>«Coincide» riguarda solo il dato effettivamente confrontato.</span></div><div id=\"checks\"></div><div class=\"notice\">Questo rapporto non autorizza la produzione. La mancanza di segnalazioni non certifica l'assenza di errori. Confrontare disegni, revisioni, accordi successivi e specifiche complete.</div></section>\n</div></main><footer><span>Secondo Occhio · AF Utility by VF Arte del Ferro · beta multi-configuratore</span><span><a href=\"https://www.artedelferroveneri.com/utility\">AF Utility</a> · <a href=\"/source.json\" download=\"secondo-occhio-sorgenti.json\">Sorgenti</a></span></footer>\n<dialog id=\"viewer\"><div class=\"section-head\"><h2 id=\"viewer-title\">Pagine originali</h2><button id=\"closeviewer\">Chiudi</button></div><div class=\"view-grid\"><div><h3>Ordine</h3><div id=\"leftpage\"></div></div><div><h3>Conferma</h3><div id=\"rightpage\"></div></div></dialog>\n<script type=\"module\" src=\"/app.mjs\"></script></body></html>",
    "style.css": ":root{font-family:Arial,Helvetica,sans-serif;color:#1a1a1a;background:#f5f5f3;font-size:16px;line-height:1.55;--green:#1c332d;--green2:#213b35;--gold:#c6a86c;--gold2:#a68c52;--goldtext:#6f5420;--ivory:#f5f5f3;--line:#e7e2d8;--muted:#6a6a6a}\n*{box-sizing:border-box}body{margin:0;background:var(--ivory)}header{background:var(--green);color:white;display:flex;align-items:center;justify-content:space-between;padding:22px max(5vw,20px);border-bottom:3px solid var(--gold)}.brand{display:flex;align-items:center;gap:14px;color:white;text-decoration:none;font-size:22px;font-weight:700;line-height:1.2}.mark{border:2px solid var(--gold);padding:10px 8px;letter-spacing:-2px;color:#ead59e;background:rgba(255,255,255,.03)}.brand small{display:block;font-size:11px;font-weight:500;letter-spacing:1.6px;margin-top:6px;color:#d4ddd8}.tag{border:1px solid rgba(198,168,108,.72);border-radius:999px;padding:6px 10px;font-size:11px;letter-spacing:1px;color:#ead59e}\nmain{max-width:1220px;margin:0 auto;padding:44px 24px}.intro{display:grid;grid-template-columns:1.6fr 1fr;gap:50px;align-items:center;margin-bottom:34px}.eyebrow{font-size:12px;letter-spacing:1.5px;font-weight:700;color:var(--goldtext)}h1{font-family:Georgia,\"Times New Roman\",serif;font-size:clamp(30px,4vw,46px);line-height:1.1;letter-spacing:-1.1px;margin:12px 0 18px;color:#1a1a1a}p{margin:8px 0 0}.intro p{color:#5a5a5a;max-width:650px}.intro aside{border-left:3px solid var(--gold);padding:12px 0 12px 24px}.intro aside p{font-size:14px;color:#6a6a6a}\n.panel{background:white;border:1px solid var(--line);border-radius:12px;padding:27px;margin-bottom:24px;box-shadow:0 7px 22px rgba(28,51,45,.045)}h2{font-size:15px;color:var(--goldtext);margin:0 0 20px;letter-spacing:.5px}h2 span{font-family:Georgia,\"Times New Roman\",serif;color:#1a1a1a;margin-left:12px;font-size:21px;letter-spacing:-.2px}.section-head{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:20px}.section-head h2{margin:0}\n.upload-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.upload{border:1px dashed #bcae91;background:#fbfaf7;border-radius:9px;padding:22px;display:flex;flex-direction:column;gap:7px;min-width:0}.upload strong{font-family:Georgia,\"Times New Roman\",serif;font-size:23px}.upload>span:not(.eyebrow),small{color:#706b62;font-size:14px}.upload input{margin:10px 0;max-width:100%;font-size:14px}input::file-selector-button{background:#efe7d6;border:1px solid #dfd0ad;padding:9px 12px;border-radius:5px;margin-right:8px;color:#5f481b;cursor:pointer}\nbutton,select{font:inherit;font-size:14px;border:1px solid #cfc8bb;border-radius:6px;background:white;padding:10px 15px;color:var(--green);cursor:pointer}button:hover{background:#f3eee4}button:disabled{opacity:.5;cursor:wait}.primary{background:var(--gold);color:#1a1a1a;border-color:var(--gold);font-weight:700;padding:12px 20px}.primary:hover{background:var(--gold2);border-color:var(--gold2);color:white}.text-button{border:0;background:transparent;color:var(--goldtext);padding:6px;font-weight:700}.actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:20px}.section-head .actions{margin-top:0}#status{font-size:14px;color:#6a6a6a}.limits{font-size:14px;color:#736f68;margin-top:20px}\n.notice{border-left:4px solid var(--gold);background:#faf4e8;padding:14px 18px;margin:16px 0;font-size:14px}.error{border-color:#a93b3b;background:#fff0f0}.doccard{background:#f7f5f0;border:1px solid #ece5d8;padding:16px;border-radius:8px;overflow-wrap:anywhere}.doccard strong{display:block;font-size:17px}.doccard p{font-size:14px}.profile-chip{display:inline-block;margin-top:8px;border:1px solid #ded2b9;background:#fff;padding:4px 8px;border-radius:999px;color:#6f5420;font-size:11px;font-weight:700}\n.check{display:flex;gap:10px;align-items:flex-start;margin:20px 0;font-weight:600}.check input{width:19px;height:19px;flex-shrink:0;accent-color:var(--gold2);margin-top:3px}.maprow{display:grid;grid-template-columns:1fr 1fr auto;gap:12px;align-items:center;border-top:1px solid var(--line);padding:13px 0;font-size:14px}.maprow select{min-width:0;max-width:100%}.maprow small{display:block;font-size:12px}.metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:12px 0 25px}.metric{background:#f7f5f0;border:1px solid #ece5d8;border-radius:8px;padding:18px}.metric strong{font-family:Georgia,\"Times New Roman\",serif;font-size:30px;display:block;line-height:1.2;margin-top:5px}.metric span{font-size:14px;color:#6a6a6a}\n.tablewrap{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:14px;margin:12px 0 20px}th,td{text-align:left;padding:12px;border-bottom:1px solid var(--line);vertical-align:top;min-width:110px}th{color:#5e584d;background:#f7f5f0;font-weight:600}td small{display:block;margin-top:6px;font-size:12px;max-width:370px}td:first-child{min-width:180px}.badge{display:inline-block;border-radius:5px;padding:3px 8px;font-size:12px;font-weight:650;white-space:nowrap}.badge.ok{background:#edf3f0;color:#2b4a40}.badge.warn{background:#f7efe0;color:#77591e}.badge.diff{background:#fce5e5;color:#9a2a2a}.result-head{display:flex;gap:15px;justify-content:space-between;align-items:center;background:#f3efe6;padding:14px;border-radius:7px}.result-head h3{font-family:Georgia,\"Times New Roman\",serif;font-size:18px;margin:0}.result-head p{font-size:13px;color:#6a6a6a}\n.filterbar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:20px 0;font-size:13px;color:#6a6a6a}.filterbar label{font-size:14px}footer{max-width:1220px;margin:auto;padding:0 24px 32px;color:#6a6a6a;font-size:12px;display:flex;justify-content:space-between;gap:20px}footer a{color:var(--goldtext);font-weight:700}dialog{width:min(1400px,96vw);max-height:94vh;border:1px solid #c9bea9;border-radius:12px;padding:22px}dialog::backdrop{background:#1c332db3}.view-grid{display:grid;grid-template-columns:1fr 1fr;gap:15px}.view-grid canvas{width:100%;height:auto;border:1px solid #ddd}.view-grid pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px}.view-grid h3{font-size:14px}button:focus-visible,input:focus-visible,select:focus-visible,a:focus-visible{outline:3px solid var(--gold);outline-offset:3px}\n@media(max-width:700px){main{padding:24px 14px}.intro{grid-template-columns:1fr;gap:18px}.intro aside{padding:6px 0 6px 15px}.panel{padding:18px}.upload-grid{grid-template-columns:1fr}.maprow{grid-template-columns:1fr}.maprow button{justify-self:start}.metrics{grid-template-columns:1fr}.metric{display:flex;justify-content:space-between;align-items:center}.metric strong{font-size:26px}.view-grid{grid-template-columns:1fr}.tag{font-size:9px}header{padding:18px}.brand{font-size:19px}.filterbar{align-items:start;flex-direction:column}footer{flex-direction:column}.result-head{align-items:start;flex-direction:column}}\n@media print{header,footer,.intro,#upload-title,.upload,.actions,.filterbar,#mapping,#samejob,.check,#compare,#export,#print,.text-button,.result-head button{display:none!important}#workspace>section:first-of-type,main>section:first-of-type{display:none!important}body{background:white}main{padding:0}.panel{border:0;box-shadow:none;padding:0}table{font-size:10px}tr{break-inside:avoid}.metrics{display:flex}.metric{flex:1}dialog{display:none}h1{font-size:24px}}",
    "app.mjs": "import Engine from \"/engine.mjs\";\nconst $=id=>document.getElementById(id);\nconst esc=s=>String(s??\"Non rilevato\").replace(/[&<>\"']/g,c=>({\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\"}[c]));\nlet state=null,report=null,pdfjs=null,epoch=0;\nfunction message(s,error=false){$(\"status\").textContent=s;$(\"status\").style.color=error?\"#a92727\":\"\";}\nfunction invalidate(){epoch++;if(state)for(const d of state.docs||[])d?.destroy?.();state=null;report=null;$(\"workspace\").hidden=true;$(\"results\").hidden=true;$(\"samejob\").checked=false;}\nfor(const [input,label] of [[\"order\",\"ordername\"],[\"confirmation\",\"confirmationname\"]])$(input).addEventListener(\"change\",()=>{invalidate();$(label).textContent=$(input).files[0]?.name||\"PDF testuale · massimo 20 MB\";message(\"Documenti aggiornati. Premi «Leggi i due PDF».\");});\nasync function extract(file){\n if(!file||file.size>20*1024*1024)throw Error(\"Seleziona due PDF, ciascuno entro 20 MB.\");\n const data=new Uint8Array(await file.arrayBuffer());\n const hash=Array.from(new Uint8Array(await crypto.subtle.digest(\"SHA-256\",data))).map(v=>v.toString(16).padStart(2,\"0\")).join(\"\");\n const task=pdfjs.getDocument({data,isEvalSupported:false});\n task.onPassword=()=>task.destroy();\n let doc;\n try{doc=await task.promise;}catch(e){throw Error(\"PDF non leggibile o protetto: \"+file.name);}\n if(doc.numPages>80){await doc.destroy();throw Error(\"Limite del prototipo: 80 pagine per documento.\");}\n const pages=[];\n for(let n=1;n<=doc.numPages;n++){\n  message(\"Lettura \"+file.name+\" · pagina \"+n+\" di \"+doc.numPages);\n  const page=await doc.getPage(n),text=await page.getTextContent();\n  const items=text.items.filter(x=>typeof x.str===\"string\"&&x.str.trim()).map(x=>({s:x.str.trim(),x:x.transform[4],y:page.view[3]-x.transform[5],w:x.width||0}));\n  const grouped=[];\n  for(const it of [...items].sort((a,b)=>a.y-b.y||a.x-b.x)){\n   let row=grouped.find(x=>Math.abs(x.y-it.y)<2.5);\n   if(!row){row={y:it.y,items:[]};grouped.push(row);}row.items.push(it);\n  }\n  const rows=grouped.sort((a,b)=>a.y-b.y).map(r=>r.items.sort((a,b)=>a.x-b.x).map(i=>i.s).join(\" \"));\n  pages.push({number:n,raw:items.map(x=>x.s).join(\"\\n\"),rows,items});\n }\n if(pages.reduce((n,p)=>n+p.raw.length,0)<100){await doc.destroy();throw Error(\"PDF privo di testo leggibile. Questa versione non esegue OCR.\");}\n return {doc,pages,hash};\n}\n\nfunction visualCrop(pageData,pos,kind){\n const items=pageData?.items||[];\n if(!items.length)return null;\n if(kind===\"vf\"){\n  const anchors=items.filter(x=>x.s===String(pos.id)&&x.x<90).sort((a,b)=>a.y-b.y);\n  const anchor=anchors[0];if(!anchor)return null;\n  const figs=items.filter(x=>/^Figura$/i.test(x.s)&&x.y<anchor.y).sort((a,b)=>b.y-a.y);\n  const fig=figs[0];if(!fig)return null;\n  const desc=items.find(x=>/^Descrizione$/i.test(x.s)&&Math.abs(x.y-fig.y)<4);if(!desc)return null;\n  const laterAnchors=items.filter(x=>/^\\d{1,3}$/.test(x.s)&&x.x<90&&x.y>anchor.y+8).map(x=>x.y);\n  const notes=items.filter(x=>/^NOTE$/i.test(x.s)&&x.y>anchor.y+8).map(x=>x.y);\n  const boundary=Math.min(...laterAnchors,...notes,anchor.y+185);\n  return {x0:fig.x-40,x1:desc.x-70,y0:anchor.y+3,y1:Math.min(boundary-3,anchor.y+180)};\n }\n if(kind!==\"cosmet\")return null;\n const anchor=items.find(x=>new RegExp(\"^Pos\\\\.?\\\\s*\"+String(pos.id)+\"\\\\b\",\"i\").test(x.s));\n if(!anchor)return null;\n const next=items.filter(x=>/^Pos\\.\\s*\\d+/i.test(x.s)&&x.y>anchor.y+8).map(x=>x.y);\n const bound=Math.min(...next,anchor.y+210);\n const draw=items.filter(x=>/Disegno/i.test(x.s)&&x.y>anchor.y&&x.y<bound).sort((a,b)=>a.y-b.y)[0];\n if(!draw)return null;\n const sameRow=items.filter(x=>x.x>draw.x+65&&Math.abs(x.y-draw.y)<8).sort((a,b)=>a.x-b.x)[0];\n const elenco=items.filter(x=>/^Elenco/i.test(x.s)&&x.y>draw.y).map(x=>x.y);\n return {x0:draw.x-12,x1:(sameRow?.x??(draw.x+110))-8,y0:draw.y+10,y1:Math.min(...elenco,...next,draw.y+175)-4};\n}\nfunction longestInkRun(mask,w,h,x){\n let best=0,b0=0,b1=0,start=-1,last=-1,gap=0;\n for(let y=0;y<h;y++){\n  const on=mask[y*w+x]===1;\n  if(on){if(start<0)start=y;last=y;gap=0;}\n  else if(start>=0&&++gap>2){const len=last-start+1;if(len>best){best=len;b0=start;b1=last;}start=-1;last=-1;gap=0;}\n }\n if(start>=0){const len=last-start+1;if(len>best){best=len;b0=start;b1=last;}}\n return [best,b0,b1];\n}\nfunction drawingSignature(canvas,crop,scale){\n const sx=Math.max(0,Math.floor(crop.x0*scale)),sy=Math.max(0,Math.floor(crop.y0*scale));\n const ex=Math.min(canvas.width,Math.ceil(crop.x1*scale)),ey=Math.min(canvas.height,Math.ceil(crop.y1*scale));\n const w=ex-sx,h=ey-sy;if(w<50||h<50)return null;\n const data=canvas.getContext(\"2d\",{willReadFrequently:true}).getImageData(sx,sy,w,h).data;\n const mask=new Uint8Array(w*h);\n for(let i=0,p=0;i<data.length;i+=4,p++){\n  const r=data[i],g=data[i+1],b=data[i+2],mx=Math.max(r,g,b),mn=Math.min(r,g,b);\n  if(mx<195||((mx-mn)<45&&mx<246))mask[p]=1;\n }\n const infos=[];\n for(let x=0;x<w;x++){const [len,y0,y1]=longestInkRun(mask,w,h,x);if(len>=Math.max(20,Math.floor(h*.16)))infos.push({x,len,y0,y1});}\n const clusters=[],gap=Math.max(4,Math.floor(w*.04));\n for(const it of infos){\n  const last=clusters[clusters.length-1];\n  if(!last||it.x-last[last.length-1].x>gap)clusters.push([it]);else last.push(it);\n }\n const cs=clusters.map(c=>{\n  const best=c.reduce((a,b)=>a.len>=b.len?a:b);\n  return {x:c.reduce((s,z)=>s+z.x,0)/c.length,len:best.len,y0:best.y0,y1:best.y1};\n });\n let bestPair=null;\n for(let i=0;i<cs.length;i++)for(let j=i+2;j<cs.length;j++){\n  const a=cs[i],b=cs[j];if(a.x<w*.02||b.x>w*.95)continue;\n  const span=b.x-a.x;if(span<w*.28)continue;\n  const yover=Math.max(0,Math.min(a.y1,b.y1)-Math.max(a.y0,b.y0)+1),minlen=Math.min(a.len,b.len);\n  if(minlen<=0||yover<minlen*.7)continue;\n  const intern=cs.slice(i+1,j).filter(c=>c.len>minlen*.6);\n  if(!intern.length)continue;\n  const score=span*(yover/minlen)+yover*.4-intern.length*2;\n  if(!bestPair||score>bestPair.score)bestPair={score,a,b,intern,yover,minlen};\n }\n if(!bestPair)return null;\n const {a,b}=bestPair,fw=b.x-a.x;\n let ratios=bestPair.intern.map(c=>(c.x-a.x)/fw).filter(r=>r>.12&&r<.88);\n if(ratios.length>1)ratios=[ratios.reduce((best,r)=>Math.abs(r-.5)<Math.abs(best-.5)?r:best,ratios[0])];\n const fy0=Math.max(a.y0,b.y0),fy1=Math.min(a.y1,b.y1),bounds=[a.x,...ratios.map(r=>a.x+r*fw),b.x];\n const densities=[];\n for(let k=0;k<bounds.length-1;k++){\n  let xa=Math.floor(bounds[k]),xb=Math.ceil(bounds[k+1]),ya=fy0,yb=fy1;\n  const dx=Math.max(3,Math.floor((xb-xa)*.12)),dy=Math.max(3,Math.floor((yb-ya)*.10));\n  xa+=dx;xb-=dx;ya+=dy;yb-=dy;\n  let ink=0,total=0;\n  for(let y=ya;y<=yb;y++)for(let x=xa;x<=xb;x++){total++;if(x>=0&&x<w&&y>=0&&y<h&&mask[y*w+x])ink++;}\n  densities.push(total?ink/total:0);\n }\n const maxD=Math.max(...densities,0);\n const activeMask=densities.map(d=>maxD>.003&&d>=maxD*.25&&d>.0015);\n const confidence=Math.max(0,Math.min(.98,.72+(bestPair.yover/bestPair.minlen)*.18+(ratios.length===1?.08:0)));\n return {paneCount:ratios.length+1,splitRatios:ratios.map(r=>Math.round(r*1000)/1000),activeMask,densities:densities.map(d=>Math.round(d*10000)/10000),confidence:Math.round(confidence*100)/100,source:\"pdf-drawing\"};\n}\nasync function attachVisuals(parsed,source,kind){\n if(!source?.doc)return;\n const cache=new Map();\n for(const pos of parsed.positions){\n  if(pos.type===\"accessory\")continue;\n  try{\n   const pageData=source.pages.find(p=>p.number===pos.page),crop=visualCrop(pageData,pos,kind);\n   if(!crop)continue;\n   let rendered=cache.get(pos.page);\n   if(!rendered){\n    const page=await source.doc.getPage(pos.page),base=page.getViewport({scale:1}),scale=Math.min(2,1600/base.width),vp=page.getViewport({scale});\n    const canvas=document.createElement(\"canvas\");canvas.width=Math.ceil(vp.width);canvas.height=Math.ceil(vp.height);\n    await page.render({canvasContext:canvas.getContext(\"2d\"),viewport:vp}).promise;\n    rendered={canvas,scale};cache.set(pos.page,rendered);\n   }\n   const sig=drawingSignature(rendered.canvas,crop,rendered.scale);\n   if(sig)pos.visual=sig;\n  }catch(_){}\n }\n}\n\n$(\"analyze\").onclick=async()=>{\n const a=$(\"order\").files[0],b=$(\"confirmation\").files[0];invalidate();const run=epoch;\n $(\"analyze\").disabled=true;$(\"demo\").disabled=true;\n let da,db;\n try{\n  if(!a||!b)throw Error(\"Seleziona ordine e conferma.\");\n  message(\"Avvio del lettore PDF…\");\n  if(!pdfjs){pdfjs=await import(\"/pdf.min.mjs\");pdfjs.GlobalWorkerOptions.workerSrc=\"/pdf.worker.min.mjs\";}\n  da=await extract(a);db=await extract(b);\n  if(run!==epoch){await da.doc.destroy();await db.doc.destroy();return;}\n  if(da.hash===db.hash)throw Error(\"I due file hanno lo stesso contenuto.\");\n  const op=Engine.detectProfile(da.pages,\"order\"),cp=Engine.detectProfile(db.pages,\"confirmation\");\n  const order=Engine.parse(da.pages,op.id,a.name),conf=Engine.parse(db.pages,cp.id,b.name);\n  message(\"Analisi geometrica dei disegni…\");\n  await attachVisuals(order,da,op.id);\n  await attachVisuals(conf,db,cp.id);\n  for(const [parsed,source] of [[order,da],[conf,db]]){\n   const blanks=source.pages.filter(p=>p.raw.trim().length<35).map(p=>p.number);\n   if(blanks.length)parsed.warnings.push(\"Pagine con testo scarso o assente: \"+blanks.join(\", \")+\". Possono contenere immagini o annotazioni non lette.\");\n  }\n  if(!order.positions.length||!conf.positions.length)throw Error(\"Posizioni non riconosciute. Il formato può richiedere un adapter dedicato; non viene prodotto un rapporto vuoto.\");\n  state={a:order,b:conf,docs:[da.doc,db.doc],pages:[da.pages,db.pages],hashes:[da.hash,db.hash],demo:false};\n  showWorkspace();message(\"Lettura completata · \"+order.profileLabel+\" ↔ \"+conf.profileLabel+\". Verifica commessa, quantità e abbinamenti.\");\n }catch(e){if(!state){da?.doc?.destroy?.();db?.doc?.destroy?.();}message(e.message||\"Errore di lettura.\",true);}\n finally{$(\"analyze\").disabled=false;$(\"demo\").disabled=false;}\n};\n$(\"demo\").onclick=async()=>{\n invalidate();$(\"demo\").disabled=true;\n try{\n  const f=await fetch(\"/demo.json\").then(r=>{if(!r.ok)throw Error(\"Esempio non disponibile.\");return r.json();});\n  state={a:Engine.parse(f.pages,\"vf\",\"ESEMPIO — ordine sintetico\"),b:Engine.parse(f.bp,\"cosmet\",\"ESEMPIO — conferma sintetica\"),docs:[null,null],pages:[f.pages,f.bp],hashes:[],demo:true};\n  showWorkspace();message(\"Esempio sintetico caricato. Conferma l'abbinamento e prova il confronto.\");\n }catch(e){message(e.message,true);}finally{$(\"demo\").disabled=false;}\n};\nfunction showWorkspace(){\n $(\"workspace\").hidden=false;$(\"demo-note\").hidden=!state.demo;\n $(\"documents\").innerHTML=[state.a,state.b].map((d,i)=>'<div class=\"doccard\"><span class=\"eyebrow\">'+(i?\"CONFERMA\":\"ORDINE\")+'</span><strong>'+esc(d.name)+'</strong><span class=\"profile-chip\">'+esc(d.profileLabel||d.kind)+'</span><p>Riferimento: '+esc(d.reference)+'<br>'+d.pages+' pagine · '+d.positions.length+' posizioni estratte<br>Pezzi estratti: '+esc(d.extractedQty)+' · dichiarati: '+esc(d.declaredQty)+'</p><button class=\"doc-preview\" data-side=\"'+i+'\">Apri la prima pagina</button></div>').join(\"\");\n $(\"warnings\").innerHTML=[state.a,state.b].flatMap(d=>d.warnings.map(w=>'<div class=\"notice\">'+esc((d.profileLabel||d.kind)+\": \"+w)+'</div>')).join(\"\");\n const props=Engine.suggested(state.a.positions,state.b.positions);\n const confirmationLabel=state.b.profileLabel||\"Conferma\";\n $(\"mapping\").innerHTML=state.a.positions.map((p,i)=>'<div class=\"maprow\"><div><strong>Ordine pos. '+esc(p.id??\"?\")+'</strong> · '+p.width+' × '+p.height+' mm<small>Pagina '+p.page+' · '+esc(p.qty)+' pezzi · apertura '+esc(p.dirs.join(\"/\")||\"?\")+(p.extra!=null?' · allargatori totali '+p.extra+' mm':' · allargatori non riconosciuti')+'</small></div><select aria-label=\"Conferma per posizione ordine '+esc(p.id??i+1)+'\" data-map=\"'+i+'\"><option value=\"-1\">Non abbinata — da verificare</option>'+state.b.positions.map((q,j)=>'<option value=\"'+j+'\" '+(props[i]===j?'selected':'')+'>'+esc(confirmationLabel)+' · pos. '+esc(q.id)+' · '+q.width+' × '+q.height+' · '+esc(q.qty)+' pz · p. '+q.page+'</option>').join(\"\")+'</select><button data-preview=\"'+i+'\">Apri pagine</button></div>').join(\"\");\n $(\"mapping\").querySelectorAll(\"select\").forEach(s=>s.onchange=()=>{$(\"results\").hidden=true;report=null;});\n $(\"mapping\").querySelectorAll(\"[data-preview]\").forEach(btn=>btn.onclick=()=>{const i=Number(btn.dataset.preview),j=Number($(\"mapping\").querySelector('[data-map=\"'+i+'\"]').value);view(state.a.positions[i].page,j<0?null:state.b.positions[j].page);});\n $(\"documents\").querySelectorAll(\"[data-side]\").forEach(btn=>btn.onclick=()=>view(Number(btn.dataset.side)===0?1:null,Number(btn.dataset.side)===1?1:null));\n $(\"workspace\").scrollIntoView({behavior:\"smooth\",block:\"start\"});\n}\n$(\"samejob\").onchange=()=>{$(\"results\").hidden=true;report=null;};\n$(\"compare\").onclick=()=>{\n if(!state||!$(\"samejob\").checked){message(\"Prima conferma che i documenti riguardano la stessa commessa.\",true);$(\"samejob\").focus();return;}\n const map=[...$(\"mapping\").querySelectorAll(\"select\")].map(s=>Number(s.value)),used=map.filter(j=>j>=0);\n if(new Set(used).size!==used.length){message(\"Una posizione della conferma è abbinata più volte: correggi gli abbinamenti.\",true);return;}\n const pairs=state.a.positions.map((a,i)=>({a,b:map[i]>=0?state.b.positions[map[i]]:null})).map(p=>({...p,rows:p.b?Engine.compare(p.a,p.b):[]}));\n const economics=[];\n const econDefs=[\n  [\"gross\",\"Totale prodotti prima dello sconto\",true,3],\n  [\"discount\",\"Sconto infissi (%)\",false,0],\n  [\"discountAmount\",\"Importo sconto\",true,3],\n  [\"netItems\",\"Totale prodotti scontati\",true,3],\n  [\"transport\",\"Trasporto\",true,0],\n  [\"total\",\"Totale esclusa IVA\",true,3]\n ];\n for(const [key,label,money,tolerance] of econDefs){\n  const av=state.a.finance[key],bv=state.b.finance[key],delta=av&&bv?Math.abs(av.value-bv.value):null;\n  const status=!av||!bv?\"Da verificare\":delta<=tolerance?\"Coincide\":\"Differenza\";\n  economics.push({\n   key,name:label,a:av?.value??null,b:bv?.value??null,pages:[av?.page??null,bv?.page??null],money,status,\n   delta,rounding:money&&delta!=null&&delta>0&&delta<=tolerance,\n   info:money&&delta!=null&&delta>0&&delta<=tolerance?\"Scostamento \"+Engine.cash(delta)+\" trattato come arrotondamento contabile.\":\"\"\n  });\n }\n const moneyRows=economics.filter(r=>r.money&&r.a!=null&&r.b!=null);\n const missingMoney=economics.some(r=>r.money&&(r.a==null||r.b==null));\n const materialMoney=moneyRows.some(r=>r.status===\"Differenza\");\n const hasRounding=moneyRows.some(r=>r.rounding);\n const at=state.a.finance.total?.value??null,bt=state.b.finance.total?.value??null;\n const moneySummary={\n  status:missingMoney?\"Da verificare\":materialMoney?\"Differenza\":\"Coincide\",\n  label:missingMoney?\"IMPORTI DA VERIFICARE\":materialMoney?\"IMPORTI NON COINCIDONO\":hasRounding?\"IMPORTI COINCIDONO CON ARROTONDAMENTO\":\"IMPORTI COINCIDONO\",\n  orderTotal:at,confirmationTotal:bt,difference:at!=null&&bt!=null?bt-at:null,hasRounding\n };\n report={version:\"0.3.6\",generatedAt:new Date().toISOString(),demo:state.demo,files:[state.a.name,state.b.name],sha256:state.hashes,scope:\"Confronto di testo, configurazione grafica di base e riconciliazione economica. Il controllo disegni usa geometria, ripartizione dei campi e simboli di movimento; non certifica fattibilità, prestazioni, profili o specifiche complete.\",warnings:[...state.a.warnings,...state.b.warnings],pairs,economics,moneySummary,unmatched:state.b.positions.filter((_,j)=>!used.includes(j))};\n renderReport();message(\"Rapporto pronto. Le verifiche tecniche aperte restano evidenziate.\");$(\"results\").scrollIntoView({behavior:\"smooth\",block:\"start\"});\n};\nconst badge=s=>'<span class=\"badge '+(s===\"Coincide\"?\"ok\":s===\"Differenza\"?\"diff\":\"warn\")+'\">'+esc(s)+'</span>';\nfunction renderReport(){\n if(!report)return;\n $(\"results\").hidden=false;\n const checks=report.pairs.flatMap(p=>p.rows).concat(report.economics);\n const diff=checks.filter(x=>x.status===\"Differenza\").length;\n const verify=checks.filter(x=>x.status===\"Da verificare\").length+report.pairs.filter(p=>!p.b).length+report.unmatched.length+report.warnings.length;\n const at=state.a.finance.total?.value,bt=state.b.finance.total?.value;\n $(\"summary\").innerHTML=[[\"Differenze testuali\",String(diff)],[\"Verifiche aperte\",String(verify)],[\"Conferma meno ordine\",at!=null&&bt!=null?Engine.cash(bt-at):\"Non rilevato\"]].map(([label,v])=>'<div class=\"metric\"><span>'+esc(label)+'</span><strong>'+esc(v)+'</strong></div>').join(\"\");\n {\n  const ms=report.moneySummary||{};\n  const delta=ms.difference==null?\"Non rilevato\":Engine.cash(ms.difference);\n  const finalBox='<div class=\"notice\"><strong>Riepilogo importi — '+esc(ms.label||\"DA VERIFICARE\")+'</strong><br>Totale ordine: '+esc(Engine.cash(ms.orderTotal))+' · Totale conferma: '+esc(Engine.cash(ms.confirmationTotal))+' · Conferma meno ordine: '+esc(delta)+(ms.hasRounding?'<br><small>Gli scarti di pochi centesimi evidenziati sopra sono trattati come arrotondamenti e restano comunque visibili.</small>':'')+'</div>';\n  $(\"economics\").innerHTML='<h3>Condizioni economiche</h3><div class=\"tablewrap\"><table><thead><tr><th>Voce</th><th>Ordine</th><th>Conferma</th><th>Esito</th></tr></thead><tbody>'+report.economics.map(r=>'<tr><td>'+esc(r.name)+(r.info?'<small>'+esc(r.info)+'</small>':'')+'<small>Pagine '+esc(r.pages[0]??\"—\")+' / '+esc(r.pages[1]??\"—\")+'</small></td><td>'+esc(r.money?Engine.cash(r.a):r.a)+'</td><td>'+esc(r.money?Engine.cash(r.b):r.b)+'</td><td>'+badge(r.status)+'</td></tr>').join(\"\")+'</tbody></table></div>'+finalBox+'<p class=\"limits\">Il riepilogo indica coerenza contabile, non sostituisce la verifica di accordi commerciali, IVA, acconti o condizioni di pagamento.</p>';\n }\n const issues=$(\"filter\").value===\"issues\";\n $(\"checks\").innerHTML=report.pairs.map((p,i)=>{\n  if(!p.b)return '<div class=\"notice\">Ordine pos. '+esc(p.a.id)+' · p. '+p.a.page+': non abbinata. Nessun controllo eseguito su questa posizione.</div>';\n  const rows=p.rows.filter(r=>!issues||r.status!==\"Coincide\");\n  return '<article><div class=\"result-head\"><div><h3>Ordine '+esc(p.a.id)+' → '+esc(state.b.profileLabel||\"Conferma\")+' '+esc(p.b.id)+'</h3><p>Pagine '+p.a.page+' / '+p.b.page+' · '+esc(p.a.qty)+' pezzi</p></div><button data-pair=\"'+i+'\">Confronta le pagine</button></div><div class=\"tablewrap\"><table><thead><tr><th>Controllo</th><th>Ordine</th><th>Conferma</th><th>Esito</th></tr></thead><tbody>'+rows.map(r=>'<tr><td>'+esc(r.name)+(r.info?'<small>'+esc(r.info)+'</small>':'')+'</td><td>'+esc(r.a)+'</td><td>'+esc(r.b)+'</td><td>'+badge(r.status)+'</td></tr>').join(\"\")+'</tbody></table></div><p class=\"limits\">Da rivedere sulle pagine: colori di maniglie e cerniere, profili e codici esatti, prestazioni, note grafiche e accordi successivi.</p></article>';\n }).join(\"\")+(report.unmatched.length?'<div class=\"notice\">Posizioni della conferma non abbinate: '+esc(report.unmatched.map(x=>x.id).join(\", \"))+'. Non sono automaticamente elementi aggiunti per errore.</div>':'');\n $(\"checks\").querySelectorAll(\"[data-pair]\").forEach(btn=>btn.onclick=()=>{const p=report.pairs[Number(btn.dataset.pair)];view(p.a.page,p.b.page);});\n}\n$(\"filter\").onchange=renderReport;\n$(\"export\").onclick=()=>{\n if(!report)return;\n const url=URL.createObjectURL(new Blob([JSON.stringify(report,null,2)],{type:\"application/json\"})),a=document.createElement(\"a\");\n a.href=url;a.download=\"secondo-occhio-rapporto.json\";a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);\n};\n$(\"print\").onclick=()=>window.print();\nlet viewing=0;\nasync function view(left,right){\n const run=++viewing;$(\"leftpage\").replaceChildren();$(\"rightpage\").replaceChildren();$(\"viewer-title\").textContent=\"Ordine p. \"+(left??\"—\")+\" / Conferma p. \"+(right??\"—\");if(!$(\"viewer\").open)$(\"viewer\").showModal();\n for(const [side,n,el] of [[0,left,$(\"leftpage\")],[1,right,$(\"rightpage\")]]){\n  if(n==null){el.textContent=\"Nessuna pagina abbinata.\";continue;}\n  try{\n   if(!state.docs[side]){const pre=document.createElement(\"pre\");pre.textContent=state.pages[side].find(x=>x.number===n)?.raw||\"Pagina non presente nell'esempio sintetico.\";el.append(pre);continue;}\n   const page=await state.docs[side].getPage(n);if(run!==viewing)return;\n   const base=page.getViewport({scale:1}),vp=page.getViewport({scale:Math.min(1.6,1500/base.width,2100/base.height)});\n   const canvas=document.createElement(\"canvas\");canvas.width=vp.width;canvas.height=vp.height;canvas.setAttribute(\"aria-label\",\"Pagina \"+n+\" del documento \"+(side?\"conferma\":\"ordine\"));el.append(canvas);\n   await page.render({canvasContext:canvas.getContext(\"2d\"),viewport:vp}).promise;\n  }catch(e){el.textContent=\"Impossibile visualizzare questa pagina. Apri il PDF originale.\";}\n }\n}\n$(\"closeviewer\").onclick=()=>{viewing++;$(\"viewer\").close();};",
    "engine.mjs": "const Engine=(()=>{const norm=s=>String(s??\"\").normalize(\"NFKC\").replace(/\\s+/g,\" \").trim();\nconst compact=s=>norm(s).toLowerCase();\nconst cents=s=>{const n=String(s??\"\").replace(/[.'’\\s€]/g,\"\").replace(\",\",\".\");return /^-?\\d+(?:\\.\\d{1,4})?$/.test(n)?Math.round(Number(n)*100):null;};\nconst cash=n=>n==null?\"Non rilevato\":new Intl.NumberFormat(\"it-IT\",{style:\"currency\",currency:\"EUR\"}).format(n/100);\nconst rowText=r=>typeof r===\"string\"?r:(r?.s||\"\");\nconst rowsOnly=p=>(p.rows||[]).map(rowText).map(x=>x.trim()).filter(Boolean);\nconst pageText=p=>norm(String(p.raw||\"\")+\"\\n\"+rowsOnly(p).join(\"\\n\"));\nconst pageLines=p=>{const r=rowsOnly(p);if(r.length)return r;return String(p.raw||\"\").split(/\\r?\\n/).map(x=>x.trim()).filter(Boolean);};\nconst amounts=s=>[...String(s).matchAll(/(\\d[\\d.'’]*,\\d{2})\\s*€/g)].map(m=>cents(m[1])).filter(v=>v!=null);\nconst cleanCode=s=>s?String(s).replace(/\\D/g,\"\"):null;\nconst same=(a,b)=>a!=null&&b!=null&&JSON.stringify(a)===JSON.stringify(b);\n\nconst PROFILE_META={\n vf:{id:\"vf\",label:\"Ordine VF\",validated:true},\n cosmet:{id:\"cosmet\",label:\"Configuratore Cosmet\",validated:true},\n generic:{id:\"generic\",label:\"Formato generico\",validated:false}\n};\nfunction detectProfile(pages,role=\"any\"){\n const t=compact((pages||[]).map(pageText).join(\"\\n\"));\n const score=(tests)=>tests.reduce((n,re)=>n+(re.test(t)?1:0),0);\n const vf=score([/\\btotale voci\\b/i,/\\bnodo inferiore\\/soglia\\b/i,/\\bprofilo complementare\\b/i,/\\bpz tot(?:ale)?\\b/i,/\\baltezza maniglia\\b/i,/\\b(?:pw|fw)\\d+\\b/i]);\n const cosmet=score([/\\bmisura esecutiva\\b/i,/\\btotale infissi\\b/i,/\\bimporto totale\\b/i,/\\btel\\.inf\\b/i,/\\bmartellina\\b/i,/\\bpos\\.\\s*\\d+\\b/i]);\n let id=\"generic\",confidence=.35;\n if(vf>=3&&vf>cosmet){id=\"vf\";confidence=Math.min(.99,.55+vf*.07);}\n else if(cosmet>=3&&cosmet>vf){id=\"cosmet\";confidence=Math.min(.99,.55+cosmet*.07);}\n else if(role===\"order\"&&vf>=2){id=\"vf\";confidence=.68;}\n else if(role===\"confirmation\"&&cosmet>=2){id=\"cosmet\";confidence=.68;}\n return {...PROFILE_META[id],confidence,role,scores:{vf,cosmet}};\n}\n\nfunction field(text,label){\n const labels=\"Telaio|Nodo Inferiore/Soglia|Anta|Trattamento PVC|Colore pvc|Colore PVC interno|Colore PVC esterno|Colore esterno|Colore Interno|Colore canalina|Apertura|Maniglia Interna|Maniglia|Cerniere/Maniglie|Cerniere|Altezza Maniglia|Tipo ferramenta|Ferramenta|Fermavetri|Vetro/Pannello|Vetro/pann\\\\. C\\\\.\\\\d+|Vetro|Fissaggio telaio|Fissaggio|Profilo complementare DX|Profilo complementare SX|Accessori Maniglia|Chiusura|Misura Impostata|Misura ESECUTIVA|Prestazioni|Serratura|Colore Copricarrello|Colore Carter PSK\";\n const m=norm(text).match(new RegExp(\"(?:\"+label+\")\\\\s*:\\\\s*(.*?)(?=\\\\s+(?:\"+labels+\")\\\\s*:|$)\",\"i\"));\n return m?m[1].slice(0,260).trim():null;\n}\nfunction colour(v){\n if(!v)return null;\n if(/streifendouglasie/i.test(v))return \"Streifendouglasie\";\n if(/bianco in massa/i.test(v))return \"Bianco in massa\";\n return norm(v).replace(/^\\(\\d+\\)\\s*/,\"\").replace(/\\s*-\\s*[A-Z0-9.]{2,12}$/i,\"\").trim();\n}\nfunction canonGlass(v){\n if(!v)return null;\n const n=norm(v)\n  .replace(/\\bMQ\\s*\\d+(?:[.,]\\d+)?\\b/gi,\" \")\n  .replace(/16\\s*GAS/gi,\"16G\")\n  .replace(/16GAS/gi,\"16G\");\n const m=n.match(/33\\.1\\s*ONE\\s*-\\s*16G\\s*-\\s*33\\.1(?:\\s*OPACO)?/i);\n return m?norm(m[0]).toUpperCase().replace(/\\s*-\\s*/g,\"-\"):null;\n}\nfunction sideExtra(t){\n const vals=[...t.matchAll(/Profilo complementare (?:DX|SX)\\s*:\\s*.*?Allargatore telaio\\s+(\\d+)x/gi)].map(x=>Number(x[1]));\n return vals.length>=2?vals.slice(0,2).reduce((a,b)=>a+b,0):null;\n}\nfunction parseFinance(lines,kind,out,page){\n for(const s of lines){\n  const aa=amounts(s),d=s.match(/sconto(?:\\s+infissi)?\\s*:?\\s*(\\d+(?:[.,]\\d+)?)\\s*%/i);\n  if(d&&!out.finance.discount)out.finance.discount={value:Number(d[1].replace(\",\",\".\")),page};\n  const grossRe=kind===\"vf\"?/Totale Voci/i:kind===\"cosmet\"?/Totale Infissi\\s*\\(\\d+\\)/i:/Totale\\s+(?:Voci|Infissi|Prodotti)/i;\n  const discRe=kind===\"vf\"?/^\\s*Sconto\\s+\\d+(?:[.,]\\d+)?\\s*%/i:kind===\"cosmet\"?/sconto infissi/i:/\\bSconto\\b/i;\n  const netRe=kind===\"vf\"?/Totale scontato/i:kind===\"cosmet\"?/Totale infissi scontati/i:/Totale.*scont/i;\n  const totalRe=kind===\"vf\"?/Totale generale/i:kind===\"cosmet\"?/Importo Totale/i:/(?:Importo\\s+Totale|Totale\\s+generale|Totale\\s+ordine)/i;\n  if(aa.length&&!out.finance.gross&&grossRe.test(s))out.finance.gross={value:aa.at(-1),page};\n  if(aa.length&&!out.finance.discountAmount&&discRe.test(s))out.finance.discountAmount={value:aa.at(-1),page};\n  if(aa.length&&!out.finance.netItems&&netRe.test(s))out.finance.netItems={value:aa.at(-1),page};\n  if(aa.length&&!out.finance.total&&totalRe.test(s))out.finance.total={value:aa.at(-1),page};\n  if(aa.length&&!out.finance.transport&&/TRASPORTO|Netto Trasporto/i.test(s))out.finance.transport={value:aa.at(-1),page};\n }\n}\nfunction blockStarts(lines,kind){\n const a=[];\n lines.forEach((s,i)=>{\n  if(kind===\"vf\"){const m=s.match(/^\\s*(\\d{1,3})\\s+(.*)$/);if(m&&/€|SERIE|Profilo\\s+a\\s+barre/i.test(m[2]))a.push({i,id:+m[1]});}\n  else if(kind===\"cosmet\"){const m=s.match(/^\\s*Pos\\.\\s*(\\d{1,3})\\b/i);if(m)a.push({i,id:+m[1]});}\n  else{\n   let m=s.match(/^\\s*Pos(?:izione)?\\.?\\s*(\\d{1,3})\\b/i);\n   if(!m)m=s.match(/^\\s*(\\d{1,3})[\\s.)-]+.*?\\b\\d{3,5}\\s*[x×]\\s*\\d{3,5}\\b/i);\n   if(m)a.push({i,id:+m[1]});\n  }\n });\n return a;\n}\nfunction scopedItemText(p,kind,id){\n const items=p?.items||[];\n if(!items.length)return \"\";\n if(kind===\"vf\"){\n  const anchors=items.filter(x=>x.s===String(id)&&x.x<95).sort((a,b)=>a.y-b.y);\n  const anchor=anchors[0];if(!anchor)return \"\";\n  const next=items.filter(x=>/^\\d{1,3}$/.test(x.s)&&x.x<95&&x.y>anchor.y+8).sort((a,b)=>a.y-b.y)[0];\n  const end=next?next.y-3:Infinity;\n  return items.filter(x=>x.y>=anchor.y-3&&x.y<end).sort((a,b)=>a.y-b.y||a.x-b.x).map(x=>x.s).join(\" \");\n }\n const anchor=items.find(x=>new RegExp(\"^Pos\\\\.?\\\\s*\"+String(id)+\"\\\\b\",\"i\").test(x.s));\n if(!anchor)return \"\";\n const next=items.filter(x=>/^Pos\\.\\s*\\d+/i.test(x.s)&&x.y>anchor.y+8).sort((a,b)=>a.y-b.y)[0];\n const end=next?next.y-3:Infinity;\n return items.filter(x=>x.y>=anchor.y-3&&x.y<end).sort((a,b)=>a.y-b.y||a.x-b.x).map(x=>x.s).join(\" \");\n}\n\nfunction parseVfBlock(lines,page,id){\n const text=lines.join(\"\\n\"),t=norm(text),head=lines[0]||\"\";\n let size=t.match(/(?:-\\s*)?L\\s*(\\d+)\\s*x.{0,80}?H\\s*(\\d+)/i);if(!size)size=t.match(/\\b(\\d{3,5})\\s*x\\s*H\\s*(\\d{1,5})\\b/i);\n const aa=amounts(head),qm=head.match(/€\\s*(?:pz|PZ)\\s*(\\d+)/i)||t.match(/\\b(?:pz|PZ)\\s*(\\d+)\\b/i);\n const open=field(text,\"Apertura\")||\"\",dirs=[...open.matchAll(/\\b(SX|DX)\\b/gi)].map(x=>x[1].toUpperCase());\n const code=(t.match(/(?:Art\\.?|Profilo\\s+a\\s+barre:)\\s*([\\d.]{4,})/i)||[])[1]||null;\n const accessory=/Profilo\\s+a\\s+barre/i.test(t)||!!code||(size&&+size[2]===0);\n const handle=(t.match(/Altezza Maniglia:\\s*(\\d+)/i)||[])[1],drill=(t.match(/Foro\\s*(?:diametro|Ø)\\s*(\\d+(?:[.,]\\d+)?)/i)||[])[1],hw=field(text,\"Tipo ferramenta\");\n const c=colour(field(text,\"Colore pvc\"))||colour(field(text,\"Colore PVC esterno\"));\n return {\n  id,page,qty:qm?+qm[1]:null,width:size?+size[1]:null,height:size?+size[2]:null,extra:sideExtra(t),dirs,\n  type:accessory?\"accessory\":\"window\",itemCode:cleanCode(code),unitPrice:aa[0]??null,lineTotal:aa.length?aa.at(-1):null,\n  external:c,internal:c||colour(field(text,\"Colore PVC interno\")),handle:handle?+handle:null,drill:drill?+drill.replace(\",\",\".\"):null,\n  hardwareRaw:hw?norm(hw):null,\n  hardware:hw&&/Antieffrazione\\s+standard/i.test(hw)?\"Antieffrazione standard\":null,\n  glass:canonGlass(text),reflective:/riflett|riflet/i.test(t),threshold:field(text,\"Nodo Inferiore/Soglia\"),\n  composite:/Fisso|Scorrevole/i.test(t),description:t,rawBlock:text\n };\n}\nfunction parseCosmetBlock(lines,page,id){\n const text=lines.join(\"\\n\"),t=norm(text),head=lines[0]||\"\",size=head.match(/\\b(\\d+)\\s*x\\s*(\\d+)\\b/)||t.match(/Misura ESECUTIVA\\s*:\\s*(\\d+)\\s*x\\s*(\\d+)/i);\n const qm=head.match(/\\b(\\d+)\\s*Pz\\s*x/i),aa=amounts(head),code=(t.match(/Art\\.?\\s*([\\d.]{4,})/i)||[])[1]||null;\n const accessory=/Coprifil|P30x|Art\\./i.test(t)||!!code||(size&&+size[1]===0&&+size[2]===0),close=field(text,\"Chiusura\")||\"\",dirs=[...close.matchAll(/\\b(SX|DX)\\b/gi)].map(x=>x[1].toUpperCase());\n const handle=(t.match(/Martellina.*?\\(H\\s*(\\d+)\\)/i)||[])[1],drill=(t.match(/Foro\\s*(?:diametro|Ø)\\s*(\\d+(?:[.,]\\d+)?)/i)||[])[1],hw=field(text,\"Ferramenta\");\n return {\n  id,page,qty:qm?+qm[1]:null,width:size?+size[1]:null,height:size?+size[2]:null,extra:null,dirs,\n  type:accessory?\"accessory\":\"window\",itemCode:cleanCode(code),unitPrice:aa[0]??null,lineTotal:aa.length?aa.at(-1):null,\n  external:colour(field(text,\"Colore esterno\")),internal:colour(field(text,\"Colore Interno\")),handle:handle?+handle:null,drill:drill?+drill.replace(\",\",\".\"):null,\n  hardwareRaw:hw?norm(hw):null,\n  hardware:hw&&/Antieffrazione\\s+Standard/i.test(hw)?\"Antieffrazione standard\":null,\n  glass:canonGlass(text),reflective:/riflett|riflet/i.test(t),threshold:(t.match(/TEL\\.INF:\\s*(.*?)\\s+DX:/i)||[])[1]||null,\n  composite:/F2AR\\+F|F3AR|Scorr|fisso/i.test(t),description:t,rawBlock:text\n };\n}\nfunction parseGenericBlock(lines,page,id){\n const text=lines.join(\"\\n\"),t=norm(text),head=lines[0]||\"\";\n const size=t.match(/(?:Misura(?:\\s+ESECUTIVA)?\\s*:?\\s*)?(\\d{3,5})\\s*[x×]\\s*(\\d{3,5})/i);\n const qm=t.match(/\\b(?:Q\\.?ta|Qty|Quantit[aà]|Pz)\\s*:?\\s*(\\d+)\\b/i)||t.match(/\\b(\\d+)\\s*Pz\\b/i);\n const aa=amounts(head).length?amounts(head):amounts(t);\n const open=field(text,\"Apertura|Chiusura\")||\"\",dirs=[...open.matchAll(/\\b(SX|DX)\\b/gi)].map(x=>x[1].toUpperCase());\n const code=(t.match(/(?:Art\\.?|Cod(?:ice)?\\.?)\\s*:?\\s*([A-Z0-9.\\/-]{4,})/i)||[])[1]||null;\n const accessory=/accessor|coprifil|profilo\\s+a\\s+barre/i.test(t)||(size&&(+size[1]===0||+size[2]===0));\n const handle=(t.match(/(?:Altezza\\s+Maniglia|Martellina).*?(\\d{2,4})/i)||[])[1];\n const drill=(t.match(/Foro\\s*(?:diametro|Ø)?\\s*:?\\s*(\\d+(?:[.,]\\d+)?)/i)||[])[1];\n const hw=field(text,\"Tipo ferramenta|Ferramenta\");\n return {\n  id,page,qty:qm?+qm[1]:null,width:size?+size[1]:null,height:size?+size[2]:null,extra:null,dirs,\n  type:accessory?\"accessory\":\"window\",itemCode:code?String(code):null,unitPrice:aa[0]??null,lineTotal:aa.length?aa.at(-1):null,\n  external:colour(field(text,\"Colore esterno|Colore pvc|Colore PVC esterno\")),internal:colour(field(text,\"Colore Interno|Colore PVC interno|Colore pvc\")),\n  handle:handle?+handle:null,drill:drill?+drill.replace(\",\",\".\"):null,hardwareRaw:hw?norm(hw):null,\n  hardware:hw&&/Antieffrazione\\s+standard/i.test(hw)?\"Antieffrazione standard\":null,glass:canonGlass(text),\n  reflective:/riflett|riflet/i.test(t),threshold:field(text,\"Nodo Inferiore\\/Soglia\")||null,composite:/Fisso|Scorrevole|Scorr|fisso/i.test(t),description:t,rawBlock:text\n };\n}\n\nfunction fallbackPosition(p,kind){\n const t=pageText(p);\n if(kind===\"generic\"){\n  const x=parseGenericBlock([t],p.number,1);\n  return x&&x.width!=null?x:null;\n }\n let size,id,qty;\n if(kind===\"vf\"){\n  size=t.match(/-\\s*L\\s*(\\d+)\\s*x\\s*H\\s*(\\d+)/i)||t.match(/\\b(\\d{3,5})\\s*x\\s*H\\s*(\\d+)/i);if(!size)return null;\n  id=1;qty=+((t.match(/\\b(\\d+)\\s*pz\\b/i)||[])[1]||1);\n }else{\n  size=t.match(/Misura ESECUTIVA\\s*:\\s*(\\d+)\\s*x\\s*(\\d+)/i)||t.match(/Pos\\.\\s*\\d+.*?\\b(\\d+)\\s*x\\s*(\\d+)\\b/i);if(!size)return null;\n  id=+((t.match(/Pos\\.\\s*(\\d+)/i)||[])[1]||1);qty=+((t.match(/\\b(\\d+)\\s*Pz\\s*x/i)||[])[1]||1);\n }\n const open=kind===\"vf\"?(field(t,\"Apertura\")||\"\"):(field(t,\"Chiusura\")||\"\"),dirs=[...open.matchAll(/\\b(SX|DX)\\b/gi)].map(x=>x[1].toUpperCase());\n const handle=kind===\"vf\"?(t.match(/Altezza Maniglia:\\s*(\\d+)/i)||[])[1]:(t.match(/Martellina.*?\\(H\\s*(\\d+)\\)/i)||[])[1];\n const drill=(t.match(/Foro\\s*(?:diametro|Ø)\\s*(\\d+(?:[.,]\\d+)?)/i)||[])[1];\n const hw=kind===\"vf\"?field(t,\"Tipo ferramenta\"):field(t,\"Ferramenta\");\n const ext=kind===\"vf\"?(colour(field(t,\"Colore pvc\"))||colour(field(t,\"Colore PVC esterno\"))):colour(field(t,\"Colore esterno\"));\n const intr=kind===\"vf\"?(colour(field(t,\"Colore pvc\"))||colour(field(t,\"Colore PVC interno\"))):colour(field(t,\"Colore Interno\"));\n return {\n  id,page:p.number,qty,width:+size[1],height:+size[2],extra:kind===\"vf\"?sideExtra(t):null,dirs,type:\"window\",\n  itemCode:null,unitPrice:null,lineTotal:null,external:ext,internal:intr,handle:handle?+handle:null,drill:drill?+drill.replace(\",\",\".\"):null,\n  hardwareRaw:hw?norm(hw):null,hardware:hw&&/Antieffrazione\\s+standard/i.test(hw)?\"Antieffrazione standard\":null,\n  glass:canonGlass(t),reflective:/riflett|riflet/i.test(t),threshold:kind===\"vf\"?field(t,\"Nodo Inferiore/Soglia\"):(t.match(/TEL\\.INF:\\s*(.*?)\\s+DX:/i)||[])[1]||null,\n  composite:/Fisso|Scorrevole|Scorr|fisso/i.test(t),description:t,rawBlock:t\n };\n}\nfunction parse(pages,kind,name=\"\"){\n if(kind===\"auto\"||!kind)kind=detectProfile(pages).id;\n if(!PROFILE_META[kind])kind=\"generic\";\n const all=pages.map(pageText).join(\"\\n\"),out={kind,profile:kind,profileLabel:PROFILE_META[kind].label,name,pages:pages.length,positions:[],finance:{},warnings:[],reference:null,declaredQty:null};\n out.reference=(all.match(kind===\"vf\"?/\\b(?:PW|FW)\\d+\\b/i:kind===\"cosmet\"?/\\bC\\d{6}\\b/i:/(?:Ordine|Commessa|Rif\\.)[\\s:#-]*[A-Z0-9._\\/-]{4,}/i)||[])[0]||null;\n for(const p of pages){\n  const lines=pageLines(p);parseFinance([...lines,String(p.raw||\"\")],kind,out,p.number);\n  const starts=blockStarts(lines,kind);\n  starts.forEach((st,k)=>{\n   const block=lines.slice(st.i,k+1<starts.length?starts[k+1].i:lines.length);\n   const x=kind===\"vf\"?parseVfBlock(block,p.number,st.id):kind===\"cosmet\"?parseCosmetBlock(block,p.number,st.id):parseGenericBlock(block,p.number,st.id);\n   if(x){\n    x.profile=kind;\n    const scoped=scopedItemText(p,kind,st.id),scopedGlass=canonGlass(scoped);\n    if(scopedGlass)x.glass=scopedGlass;\n   }\n   if(x&&(x.width!=null||x.itemCode||x.unitPrice!=null))out.positions.push(x);\n  });\n  const d=pageText(p).match(kind===\"vf\"?/PZ Tot(?:ale)?\\.?\\s*=\\s*(\\d+)/i:kind===\"cosmet\"?/Totale pezzi\\s*(\\d+)/i:/(?:Totale\\s+pezzi|Pezzi\\s+totali|Q\\.?ta\\s+totale)\\s*:?\\s*(\\d+)/i);if(d)out.declaredQty=+d[1];\n }\n if(!out.positions.length)for(const p of pages){const x=fallbackPosition(p,kind);if(x){x.profile=kind;out.positions.push(x);}}\n if(kind===\"generic\")out.warnings.push(\"Formato non ancora validato: lettura generica attiva. I campi non riconosciuti restano 'Da verificare'.\");\n if(!out.positions.length)out.warnings.push(\"Nessuna posizione riconosciuta. Formato non supportato o PDF non testuale.\");\n const main=out.positions.filter(x=>x.type!==\"accessory\"),qs=main.map(x=>x.qty);out.extractedQty=qs.length&&qs.every(x=>x!=null)?qs.reduce((a,b)=>a+b,0):null;\n return out;\n}\nfunction scorePair(a,b){\n if(a.type&&b.type&&a.type!==b.type)return -1000;let s=0;\n if(a.type===\"accessory\"||b.type===\"accessory\"){\n  if(a.itemCode&&b.itemCode)s+=a.itemCode===b.itemCode?80:-40;if(same(a.qty,b.qty))s+=12;if(a.unitPrice!=null&&b.unitPrice!=null)s+=Math.abs(a.unitPrice-b.unitPrice)<=2?15:-6;return s;\n }\n if(a.width!=null&&b.width!=null&&a.height!=null&&b.height!=null){\n  const aw=a.width+(a.extra||0);if(aw===b.width&&a.height===b.height)s+=60;else if(Math.abs(aw-b.width)<=5&&Math.abs(a.height-b.height)<=5)s+=25;else s-=35;\n }\n if(same(a.qty,b.qty))s+=10;if(a.unitPrice!=null&&b.unitPrice!=null)s+=Math.abs(a.unitPrice-b.unitPrice)<=2?15:-5;if(a.handle!=null&&b.handle!=null&&a.handle===b.handle)s+=5;\n return s;\n}\nfunction suggested(a,b){\n const p=a.map(x=>{const r=b.map((y,j)=>({j,score:scorePair(x,y)})).sort((u,v)=>v.score-u.score);if(!r.length||r[0].score<45)return -1;if(r[1]&&r[1].score===r[0].score)return -1;return r[0].j;});\n return p.map(j=>j>=0&&p.filter(k=>k===j).length===1?j:-1);\n}\n\nfunction visualSummary(v){\n if(!v)return \"Non rilevato\";\n const split=v.splitRatios?.length?Math.round(v.splitRatios[0]*100)+\"/\"+Math.round((1-v.splitRatios[0])*100):\"?\";\n const active=v.activeMask||[];\n const movement=active.length===2?(active[0]&&active[1]?\"entrambi\":active[0]?\"sinistra\":active[1]?\"destra\":\"nessuno\"):\"?\";\n return (v.paneCount||\"?\")+\" campi · \"+split+\" · movimento \"+movement;\n}\nfunction visualVerdict(a,b){\n const va=a?.visual,vb=b?.visual;\n if(!va||!vb||va.confidence<.65||vb.confidence<.65)return null;\n if(va.paneCount!==vb.paneCount)return {status:\"Differenza\",info:\"I disegni mostrano un numero diverso di campi/pannelli.\"};\n const ra=va.splitRatios||[],rb=vb.splitRatios||[];\n if(ra.length!==rb.length)return {status:\"Da verificare\",info:\"La suddivisione dei campi non è stata ricostruita nello stesso modo nei due disegni.\"};\n const splitDiff=ra.length?Math.max(...ra.map((x,i)=>Math.abs(x-rb[i]))):0;\n const aa=va.activeMask||[],ab=vb.activeMask||[];\n const activeComparable=aa.length===ab.length&&aa.length===va.paneCount;\n const activeSame=activeComparable&&aa.every((x,i)=>x===ab[i]);\n if(splitDiff>.10)return {status:\"Differenza\",info:\"La ripartizione geometrica dei campi nei due disegni non coincide.\"};\n if(activeComparable&&!activeSame)return {status:\"Differenza\",info:\"I disegni collocano il campo/anta in movimento su lati diversi.\"};\n if(splitDiff<=.07&&(!activeComparable||activeSame))return {status:\"Coincide\",info:\"Confronto grafico automatico: numero campi, ripartizione e posizione del campo in movimento risultano coerenti.\"};\n return {status:\"Da verificare\",info:\"I disegni sono simili ma il confronto grafico non raggiunge confidenza sufficiente per dichiararli equivalenti.\"};\n}\n\nfunction compare(a,b){\n const rows=[],add=(name,x,y,info=\"\",money=false)=>{\n  let status;if(x==null||y==null)status=\"Da verificare\";else if(money){const d=Math.abs(x-y);status=d===0?\"Coincide\":d<=2?\"Da verificare\":\"Differenza\";}else status=JSON.stringify(x)===JSON.stringify(y)?\"Coincide\":\"Differenza\";\n  rows.push({name,a:money?cash(x):x,b:money?cash(y):y,status,info});\n };\n add(\"Quantità\",a.qty,b.qty);\n if(a.type===\"accessory\"||b.type===\"accessory\"){\n  add(\"Codice articolo\",a.itemCode,b.itemCode);add(\"Prezzo unitario\",a.unitPrice,b.unitPrice,\"\",true);add(\"Totale riga\",a.lineTotal,b.lineTotal,\"\",true);return rows;\n }\n add(\"Larghezza complessiva (mm)\",a.width==null?null:a.width+(a.extra||0),b.width);\n add(\"Altezza (mm)\",a.height,b.height);\n add(\"Prezzo unitario\",a.unitPrice,b.unitPrice,\"\",true);\n const av=a.dirs?.length?a.dirs.join(\" / \"):null,bv=b.dirs?.length?b.dirs.join(\" / \"):null;\n const sliding=/scorr|fisso/i.test((a.description||\"\")+\" \"+(b.description||\"\"));\n const vv=visualVerdict(a,b);\n if(sliding){\n  if(vv){\n   rows.push({\n    name:\"Apertura / verso\",\n    a:av,b:bv,status:vv.status,\n    info:vv.info+\" Le sigle SX/DX dei due configuratori restano informative ma non prevalgono sul disegno.\",\n    visualCheck:true,\n    visual:{a:visualSummary(a.visual),b:visualSummary(b.visual)}\n   });\n  }else if(av!=null&&bv!=null&&av!==bv){\n   rows.push({name:\"Apertura / verso\",a:av,b:bv,status:\"Da verificare\",info:\"Le sigle SX/DX possono usare convenzioni diverse; il confronto grafico automatico non è disponibile o non è abbastanza affidabile per questa posizione.\",visualCheck:true});\n  }else add(\"Sensi di apertura\",av,bv,\"Confrontare il disegno in caso di dubbio.\");\n }else{\n  add(\"Sensi di apertura\",av,bv,\"Confrontare il disegno in caso di dubbio.\");\n  if(vv)rows.push({name:\"Disegno / configurazione\",a:visualSummary(a.visual),b:visualSummary(b.visual),status:vv.status,info:vv.info,visualCheck:true});\n }\n add(\"Colore esterno (nome)\",a.external,b.external);\n add(\"Colore interno (nome)\",a.internal,b.internal);\n add(\"Altezza maniglia dichiarata (mm)\",a.handle,b.handle);\n {\n  const da=a.drill, db=b.drill;\n  const legacyPair=(a.profile===\"vf\"&&b.profile===\"cosmet\")||(a.profile===\"cosmet\"&&b.profile===\"vf\");\n  const normalPair=da!=null&&db!=null&&(\n    da===db ||\n    (legacyPair&&Math.abs(da-6)<0.001&&Math.abs(db-6.5)<0.001) ||\n    (legacyPair&&Math.abs(da-6.5)<0.001&&Math.abs(db-6)<0.001)\n  );\n  rows.push({\n    name:\"Foro di fissaggio (mm)\",\n    a:da,\n    b:db,\n    status:(da==null||db==null)?\"Da verificare\":(normalPair?\"Coincide\":\"Differenza\"),\n    info:normalPair&&da!==db&&legacyPair?\"Equivalenza tecnica prevista per questa coppia di profili: foro 6 mm e 6,5 mm non costituiscono difformità.\":\"\"\n  });\n }\n if(sliding && (a.hardwareRaw||b.hardwareRaw)){\n  rows.push({\n   name:\"Ferramenta / meccanismo\",\n   a:a.hardwareRaw||a.hardware||null,\n   b:b.hardwareRaw||b.hardware||null,\n   status:(a.hardwareRaw&&b.hardwareRaw&&compact(a.hardwareRaw)===compact(b.hardwareRaw))?\"Coincide\":\"Da verificare\",\n   info:\"Sugli scorrevoli configuratori diversi possono esporre campi non direttamente omologhi. Non viene dichiarata una difformità senza una mappa tecnica verificata per i profili riconosciuti.\"\n  });\n }else add(\"Ferramenta (categoria)\",a.hardware,b.hardware);\n add(\"Composizione vetro (testo)\",a.glass,b.glass);\n return rows;\n}\nreturn {parse,detectProfile,profiles:PROFILE_META,compare,suggested,cents,cash,scorePair};})(); export default Engine;",
    "server.cjs": "\nconst http = require(\"node:http\");\nconst fs = require(\"node:fs\");\nconst env=process.env;\nconst engine=new Function(env.SO_ENGINE)();\nconst demo=JSON.parse(env.SO_DEMO);\nconst orderProfile=engine.detectProfile(demo.pages,\"order\"),confirmationProfile=engine.detectProfile(demo.bp,\"confirmation\");\nif(orderProfile.id!==\"vf\")throw Error(\"Self-test profilo ordine fallito\");\nif(confirmationProfile.id!==\"cosmet\")throw Error(\"Self-test profilo conferma fallito\");\nconst a=engine.parse(demo.pages,orderProfile.id),b=engine.parse(demo.bp,confirmationProfile.id);\nconst rows=engine.compare(a.positions[0],b.positions[0]);\nif(rows.find(r=>r.name.startsWith(\"Larghezza\")).status!==\"Coincide\")throw Error(\"Self-test dimensioni fallito\");\nif(engine.suggested([a.positions[0],a.positions[0]],b.positions).some(i=>i>=0))throw Error(\"Self-test ambiguita fallito\");\nconst files=new Map([\n [\"/\",[\"text/html; charset=utf-8\",env.SO_HTML]],\n [\"/index.html\",[\"text/html; charset=utf-8\",env.SO_HTML]],\n [\"/style.css\",[\"text/css; charset=utf-8\",env.SO_CSS]],\n [\"/app.mjs\",[\"text/javascript; charset=utf-8\",env.SO_APP]],\n [\"/engine.mjs\",[\"text/javascript; charset=utf-8\",\"const Engine=(()=>{\"+env.SO_ENGINE+\"})(); export default Engine;\"]],\n [\"/demo.json\",[\"application/json; charset=utf-8\",env.SO_DEMO]]\n]);\nconst port=Number(env.PORT||8080);\nlet ready=false,loadError=null;\nconst source=JSON.stringify({version:\"0.3\",description:\"Static browser app with multi-configurator profile adapters. PDF documents remain in the browser.\",files:{\"index.html\":env.SO_HTML,\"style.css\":env.SO_CSS,\"app.mjs\":env.SO_APP,\"engine.mjs\":\"const Engine=(()=>{\"+env.SO_ENGINE+\"})(); export default Engine;\",\"server.cjs\":env.SO_SERVER,\"demo.json\":env.SO_DEMO},runtime:\"node:22-alpine\",start:\"node -e 'eval(process.env.SO_SERVER)'\",tests:JSON.parse(env.SO_TESTS||\"[]\")},null,2);\nconst server=http.createServer((req,res)=>{\n const pathname=new URL(req.url,\"http://localhost\").pathname;\n res.setHeader(\"X-Content-Type-Options\",\"nosniff\");\n res.setHeader(\"Referrer-Policy\",\"no-referrer\");\n res.setHeader(\"X-Frame-Options\",\"DENY\");\n res.setHeader(\"Permissions-Policy\",\"camera=(), microphone=(), geolocation=()\");\n res.setHeader(\"Content-Security-Policy\",\"default-src 'self'; script-src 'self'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'\");\n if(req.method!==\"GET\"&&req.method!==\"HEAD\"){res.writeHead(405);return res.end(\"Method not allowed\");}\n if(pathname===\"/health\"){res.writeHead(ready?200:503,{\"Content-Type\":\"application/json\",\"Cache-Control\":\"no-store\"});return res.end(JSON.stringify({status:ready?\"ok\":\"loading\",version:\"0.3\",engineTests:\"passed\",profileEngine:\"multi-configurator\",profiles:Object.keys(engine.profiles||{}),vendorReady:ready,error:loadError}));}\n if(pathname===\"/source.json\"){res.writeHead(200,{\"Content-Type\":\"application/json\",\"Content-Disposition\":\"attachment; filename=secondo-occhio-sorgenti.json\",\"Cache-Control\":\"no-store\"});return res.end(req.method===\"HEAD\"?undefined:source);}\n const file=files.get(pathname);\n if(!file){res.writeHead(404);return res.end(\"Not found\");}\n res.writeHead(200,{\"Content-Type\":file[0],\"Cache-Control\":pathname.includes(\"pdf.\")?\"public,max-age=86400\":\"no-store\"});\n res.end(req.method===\"HEAD\"?undefined:file[1]);\n});\nserver.listen(port,\"0.0.0.0\",()=>console.log(\"Secondo Occhio 0.3 listening on \"+port));\n(async()=>{\n try{\n  for(const name of [\"pdf.min.mjs\",\"pdf.worker.min.mjs\"]){\n   const response=await fetch(\"https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/build/\"+name,{signal:AbortSignal.timeout(45000)});\n   if(!response.ok)throw Error(\"PDF.js download HTTP \"+response.status);\n   const data=await response.text();\n   if(data.length<10000)throw Error(\"PDF.js asset incomplete\");\n   files.set(\"/\"+name,[\"text/javascript; charset=utf-8\",data]);\n  }\n  ready=true;console.log(\"READY: PDF.js assets loaded, engine self-tests passed\");\n }catch(e){loadError=e.message;console.error(\"STARTUP FAILED: \"+e.message);}\n})();",
    "demo.json": "{\"bp\":[{\"number\":2,\"raw\":\"Pos.1 860 x 1425 1 Pz x 694,47 € F1AR 01 Serie : Sintesi Evo Colore esterno : Streifendouglasie Vetro : 33.1 ONE-16G-33.1 Opaco Colore canalina : W-E Nera Colore Interno : Bianco in Massa Fissaggio : Foro Ø 6.5 Tassello su Muro FAP Maniglia Interna : Martellina Dk Toulon (H 500) Cerniere/Maniglie : Bianco F9010 / Bianco F9010 Ferramenta : 01 - Antieffrazione Standard | PVC Fermavetri : FV - M76 Chiusura : Sx Dk Artech Misura ESECUTIVA : 860 x 1425 (escluse alette) C261223 Totale pezzi 1\",\"rows\":[\"./. sconto infissi 50.00 % 6'075,60 €\",\"Importo Totale 6'379,39 €\"]}],\"pages\":[{\"number\":3,\"raw\":\"Telaio: Telaio ad L Colore PVC interno: (001) Bianco in massa - BNC Colore PVC esterno: (021) Streifendouglasie - 3152.009 Apertura: 1A SX-AR Maniglia: Martellina DK Toulon - Bianco Cerniere: Coperture bianche Altezza Maniglia: 500 Tipo ferramenta: Antieffrazione standard Vetro/Pannello: (002) 33.1 ONE-16GAS-33.1 OPACO Fissaggio telaio: Foro diametro 6 mm Profilo complementare DX: 114.054 - Allargatore telaio 45x76mm 1,42 ml 49,95 € Profilo complementare SX: 114.054 - Allargatore telaio 45x76mm 1,42 ml 49,95 € Descrizione Figura Item Totale Q.tà Prezzo Un. 1 694,47 € SERIE SINTESI EVO - Finestra 1 anta - L 770 x H 1425 1 pz\",\"rows\":[]},{\"number\":11,\"raw\":\"PZ Tot = 1 PW260053\",\"rows\":[\"Sconto 54%: 6.561,66 €\",\"Totale generale (escluso IVA): 5.869,05 €\"]}]}"
  },
  "runtime": "node:22-alpine",
  "start": "node -e 'eval(process.env.SO_SERVER)'",
  "tests": [
    {
      "name": "v0.3 engine suite",
      "passed": 15
    },
    {
      "name": "VF-Cosmet regression",
      "passed": true
    },
    {
      "name": "generic fail-safe",
      "passed": true
    },
    {
      "name": "VF visual palette",
      "passed": true
    }
  ]
}