""" Verificări mecanice pe diagramele SVG dintr-un fișier de sumarizare. Prinde exact clasele de bug găsite manual în 01_MODUL_1/05_patternuri_candele.md: scară inconsistentă între niveluri, text tăiat de viewBox, text suprapus, linii goale care rup blocul HTML. NU înlocuiește verificarea vizuală — nu vede dacă un text stă peste o linie de preț. Rulează: .venv/bin/python audit_diagrams.py summaries//.md Selftest: .venv/bin/python audit_diagrams.py --selftest """ import re import sys from pathlib import Path CHAR_W = 5.7 # latime medie per caracter la 12px sans-serif (calibrat pe randare) LINE_H = 13 # înălțimea unei linii de text _TRANSFORM = re.compile(r'translate\(\s*([-\d.]+)\s*[, ]\s*([-\d.]+)\s*\)') _ELEMENT = re.compile( r']*)>||]*)>(.*?)|]*?)/>|<[^>]+>', re.S) _ROLE = re.compile(r'SL\b|stop', re.I) _ROLE_ENTRY = re.compile(r'intrar|entry', re.I) _ROLE_TP = re.compile(r'\bTP\d*|\bțint|\btarget', re.I) def _elements(svg: str): """Evenimentele SVG în ordine, cu translația `` acumulată aplicată. Fiecare `` deschide un cadru; elementele dinăuntru moștenesc deplasarea, oricât de adânc ar fi imbricate. """ stack = [(0.0, 0.0)] for m in _ELEMENT.finditer(svg): if m.group(1) is not None: # t = _TRANSFORM.search(m.group(1)) dx, dy = (float(t.group(1)), float(t.group(2))) if t else (0.0, 0.0) px, py = stack[-1] stack.append((px + dx, py + dy)) elif m.group(0) == "": if len(stack) > 1: stack.pop() else: tx, ty = stack[-1] if m.group(3) is not None: # ... attrs = m.group(2) x = float(re.search(r'\bx="([-\d.]+)"', attrs).group(1)) if re.search(r'\bx="', attrs) else 0.0 y = float(re.search(r'\by="([-\d.]+)"', attrs).group(1)) if re.search(r'\by="', attrs) else 0.0 yield "text", x + tx, y + ty, attrs, m.group(3) else: # sau alt tag y = re.search(r'\by1="([-\d.]+)"', m.group(4) or "") if y: yield "line", float(y.group(1)) + ty, m.group(4) else: yield "other", 0.0, 0.0 def texts(svg: str): for ev in _elements(svg): if ev[0] != "text": continue _, x, y, attrs, raw = ev inner = re.sub(r'<[^>]+>', '', raw) anchor = (re.search(r'text-anchor="(\w+)"', attrs) or [None, "start"])[1] w = len(inner) * CHAR_W x0 = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) yield x0, x0 + w, y, inner.strip() def levels(svg: str): """Perechile line+text care joacă rolurile SL / intrare / TP, cu y absolut.""" out = [] ev = list(_elements(svg)) for a, b in zip(ev, ev[1:]): if a[0] != "line" or b[0] != "text": continue label = re.sub(r'<[^>]+>', '', b[4]).strip() price = re.search(r'(\d+\.\d\d)', label) if not price: continue role = ("sl" if _ROLE.search(label) else "entry" if _ROLE_ENTRY.search(label) else "tp" if _ROLE_TP.search(label) else None) if role: out.append((float(price.group(1)), a[1], role)) return out def _audit_one(i: int, svg: str) -> list[str]: problems = [] vb = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg) if not vb: return [f"svg {i}: fără viewBox"] vw, vh = float(vb.group(1)), float(vb.group(2)) if re.search(r"\n[ \t]*\n", svg): problems.append(f"svg {i}: linie goală înăuntru — markdown rupe blocul") tx = list(texts(svg)) for x0, x1, y, s in tx: if x1 > vw + 2: problems.append(f'svg {i}: text depășește marginea dreaptă ({x1:.0f} > {vw:.0f}): "{s[:45]}"') if y > vh + 2 or y < 0: problems.append(f'svg {i}: text în afara înălțimii (y={y:.0f}, viewBox {vh:.0f}): "{s[:45]}"') for a in range(len(tx)): for b in range(a + 1, len(tx)): xa0, xa1, ya, sa = tx[a] xb0, xb1, yb, sb = tx[b] if abs(ya - yb) < LINE_H and xa0 < xb1 - 4 and xb0 < xa1 - 4: problems.append(f'svg {i}: texte suprapuse — "{sa[:28]}" / "{sb[:28]}"') # dacă diagrama are exact un triplet SL/intrare/TP, raportul distanțelor # în preț trebuie să fie cel din pixeli lv = levels(svg) if len(lv) == 3 and {r for _, _, r in lv} == {"sl", "entry", "tp"}: pts = sorted((p, y) for p, y, _ in lv) (plo, ylo), (pmid, ymid), (phi, yhi) = pts if pmid - plo > 0 and ylo - ymid > 0: rp = (phi - pmid) / (pmid - plo) rx = (ymid - yhi) / (ylo - ymid) if abs(rp - rx) > 0.05: problems.append(f"svg {i}: SCARĂ GREȘITĂ — raport în prețuri {rp:.2f}R, desenat {rx:.2f}R") return problems def check(path: Path) -> list[str]: md = path.read_text(encoding="utf-8") problems = [] for i, svg in enumerate(re.findall(r'', md, re.S)): problems += _audit_one(i, svg) return problems def selftest(): def svg(body): return f'{body}' # două panouri stivuite: fără translație etichetele ar părea suprapuse stacked = svg('DA — panoul de sus' 'NU — panoul de jos') assert not any("suprapuse" in p for p in _audit_one(0, stacked)), "translația nu a fost aplicată" # translație imbricată nested = svg('' 'jos' 'sus') assert not any("suprapuse" in p for p in _audit_one(0, nested)), "translația imbricată nu s-a acumulat" # suprapunerea reală din același panou rămâne semnalată real = svg('unu' 'doi') assert any("suprapuse" in p for p in _audit_one(0, real)), "suprapunerea reală a fost pierdută" # triplet SL/intrare/TP cu scară greșită rămâne semnalat bad = svg('SL 90.00' 'intrare 100.00' 'TP 110.00') assert any("SCARĂ" in p for p in _audit_one(0, bad)), "scara greșită nu a fost prinsă" # trei niveluri care NU sunt SL/intrare/TP nu mai declanșează SCARĂ notrip = svg('minimul 99.80' 'cel mai jos 99.90' 'close-ul 100.60') assert not any("SCARĂ" in p for p in _audit_one(0, notrip)), "niveluri care nu sunt triplet declanșează SCARĂ" print("selftest ok") def main(): if "--selftest" in sys.argv: selftest() return 0 files = [Path(a) for a in sys.argv[1:] if a != "--selftest"] or sorted(Path("summaries").glob("*/*.md")) total = 0 for f in files: probs = check(f) total += len(probs) print(f"\n=== {f} — {len(probs)} probleme" if probs else f"\n=== {f} — curat") for p in probs: print(" ", p) print(f"\nTOTAL: {total} probleme în {len(files)} fișiere") return 0 if __name__ == "__main__": sys.exit(main())