
    nsj}>                        d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddZ
ddZ G d d      Zdd	Zdd
Z	 	 	 	 	 	 ddZddZedk(  r e e             y)uq  Erzeugt aus einer realen QAF-Arbeitsmappe eine anonymisierte, strukturtreue Fixture.

Das Original wird ausschliesslich GELESEN. Die Ausgabe ist eine neue Datei.

── Warum openpyxl und nicht ExcelJS ────────────────────────────────────────────
ExcelJS kann die realen 8.8-Arbeitsmappen nicht einmal unveraendert
zurueckschreiben (Abbruch in cf-rule-xform bei einer bedingten Formatierungs-
regel ohne `formulae`). openpyxl schafft den Round-Trip auf Zellebene
verlustfrei; gemessen an 220705_QAF_HICE_Vergabe.xlsx bleiben 12 Blaetter,
6 veryHidden, 43 benannte Bereiche, 208 verbundene Zellen, 41 bedingte Formate,
26 Datenpruefungen, 1707 Formeln, 1509 Zahlen und 4833 Texte unveraendert.

Bekannte Einbusse, bewusst in Kauf genommen und im Manifest ausgewiesen: die
ZIP-Eintraege gehen von 71 auf 35 zurueck. Verloren gehen `customXml/*` sowie
die Erweiterungen zu bedingter Formatierung und Datenpruefung. Das sind
Dokument-Metadaten ohne fachliche Bedeutung — und sie koennen ihrerseits
Kundendaten tragen, weshalb ihr Wegfall fuer eine Fixture eher erwuenscht ist.

── Warum Werte ERSETZT und nicht skaliert werden ───────────────────────────────
Eine Skalierung aller Zahlen mit einem Faktor s bricht die Arithmetik: bei
Preis x Menge = Summe stuende s^2 im Produkt, aber s in einer hartcodierten
Summe. Eine Skalierung NUR der erkannten Geldzellen wiederum laesst alles
stehen, was der Parser nicht zuordnet — gemessen 759 von 1513 Zahlzellen.
Dort blieben echte Werte im Klartext.

Deshalb: jede Zahl in einer Zelle OHNE Formel wird deterministisch ersetzt.
Formelzellen bleiben unangetastet und rechnen sich aus den ersetzten Eingaben
konsistent neu. Die Groessenordnung bleibt erhalten (sonst waere die Fixture
fachlich unplausibel und Toleranzen/Reconciliation liefen ins Leere), die
Ziffern darunter nicht.

Damit gilt: KEIN Originalwert wandert in die Fixture. Die Leck-Pruefung am Ende
verifiziert das gegen die vollstaendige Menge der Originalwerte, nicht nur gegen
die zugeordneten.
    )annotationsN)Pathc                    t        j                  | dz   dj                  |      z   j                  d            j	                         }t
        j                  |d d d      S )N|utf-8   big)hashlibsha256joinencodedigestint
from_bytes)seedpartshs      F/home/aria/work/kadi-v2/scripts/qaf-v2/anonymise/anonymise_workbook.py_digestr   =   sL    s
SXXe_4<<WEFMMOA>>!BQ%''    c                
   | dk(  ryt        |||      }| dk  rdnd}t        |       }|dk\  r t        t        t	        |                  dz
  nd}d|z  }d|dz   z  }	|	|z
  }
||t        t	        |
dz        d      z  dz  z   }t        |       j                         rnt        dt	        |            }|t	        |      k(  rFt	        |      |t	        |      z
  dz   t        t	        |
      d      z  z   }|t	        |      k(  r|dz  }||z  S dt        |       v r&t        t        |       j                  d      d         nd}t        |t        |d	            }||k(  r't        |dt        |d	       z  z   t        |d	            }||z  S )
u  Ersetzt eine Zahl deterministisch unter Erhalt von Vorzeichen, Groessenordnung
    und Nachkommastellen-Charakter.

    Null bleibt Null: eine 0 traegt keine Information ueber die Kalkulation, und
    sie ist fachlich bedeutsam (Nullkosten-Zugang ist ein eigener Pflichtfall).
    Ganze Zahlen bleiben ganz — sonst wuerde aus einer Stueckzahl eine Kommazahl
    und der Parser saehe eine Struktur, die es im Original nicht gibt.
    r      
     g     @@.      )r   abslenstrr   maxfloat
is_integersplitroundmin)valuer   sheetcoordrndsign	magnitudeexplohispannew	candidatedecimalsresults                  r   replace_numberr6   B   s    z
$u
%C2DE
I +4q.#c#i.!
"Q
&bC	sB	aB7D
c#dTk*A..&8
8CU| 3s8$	 I&BIB$7!$;s3t9a?P#PQIC	N*Q	i14E
1Bs3u:##C(,-H3Ha()Fvs8Q'7&7 77Xq9IJ&=r   c                  R    e Zd ZdZd
dZedd       ZddZddZddZ	ddZ
ddZy	)TextAnonymiseru  Pseudonymisiert Texte strukturerhaltend.

    Formular-Beschriftungen bleiben stehen: sie sind Vokabular der BMW-Vorlage,
    kein Lieferantengeheimnis — und ohne sie erkennt der Parser die Mappe nicht
    mehr, womit die Fixture nichts mehr pruefen wuerde.

    Gleiche Texte bekommen dasselbe Pseudonym (sonst waeren Gleichteile keine
    mehr), und Klammer-Varianten werden getrennt behandelt (sonst faellt die
    Umbenennungs-Erkennung weg). Pseudonyme teilen sich bewusst KEINEN Wortstamm:
    "Pos01"/"Pos02" statt "Teil 01"/"Teil 02", sonst gilt jeder Sammelname fuer
    jedes Teil als Treffer und die Zuordnungsregeln greifen anders als im Original.
    c                    || _         |D ch c]  }| j                  |       c}| _        || _        i | _        i | _        y c c}w N)labels_normlabels_normalisedr   _map	_variants)selfr;   r   xs       r   __init__zTextAnonymiser.__init__{   s?    9?!@A$**Q-!@	$&	)+ "As   Ac                h    t        j                  dd| j                         j                               S )N\s+ )resubstriplower)ss    r   r<   zTextAnonymiser._norm   s"    vvfc1779??#455r   c                <    | j                  |      | j                  v S r:   )r<   r=   )r@   texts     r   is_labelzTextAnonymiser.is_label   s    zz$4#9#999r   c                   |j                         }|dk(  r|S | j                  |      r|S t        j                  d|      r%t	        t        j
                  dd|            dk  r|S t        j                  d|      }|rD| j                  |j                  d             d| j                  |j                  d             d	S | j                  |      S )
N [\d\s.,:/-]+z\Dr   (.*?)\s*\(([^)]*)\)\s*r    (r   ))	rH   rM   rF   	fullmatchr    rG   _tokengroup_variantr@   rL   rawms       r   	pseudonymzTextAnonymiser.pseudonym   s    jjl"9K==K <<-#bffUB6L2MQR2RKLL2C8kk!''!*-.bqwwqz1J0K1MM{{3r   c                
   |j                         }|dk(  r|S t        j                  d|      }|rD| j                  |j	                  d             d| j                  |j	                  d             dS | j                  |      S )u  Wie pseudonym(), aber ohne die Beschriftungs-Ausnahme.

        Fuer Stellen, an denen ein Wort keine Beschriftung sein KANN, auch wenn
        es zufaellig wie eine aussieht — Dokument-Metadaten etwa. Siehe
        anonymise_workbook_xml.py, ersetze_weiteren_eintrag().
        rO   rQ   r   rR   r   rS   )rH   rF   rT   rU   rV   rW   rX   s       r   pseudonym_erzwungenz"TextAnonymiser.pseudonym_erzwungen   su     jjl"9KLL2C8kk!''!*-.bqwwqz1J0K1MM{{3r   c                    | j                  |      }|dk(  r|S || j                  vr)dt        | j                        dz   d| j                  |<   | j                  |   S )NrO   Posr   03d)r<   r>   r    r@   rJ   keys      r   rU   zTextAnonymiser._token   sY    jjm"9Hdii"3tyy>A#5c":;DIIcNyy~r   c                    | j                  |      }|| j                  vr(dt        | j                        dz    | j                  |<   | j                  |   S )Nvarr   )r<   r?   r    ra   s      r   rW   zTextAnonymiser._variant   sN    jjmdnn$$'DNN(;a(?'@"ADNN3~~c""r   N)r;   set[str]r   r!   )rJ   r!   returnr!   )rL   r!   rf   bool)rL   r!   rf   r!   )__name__
__module____qualname____doc__rB   staticmethodr<   rM   r[   r]   rU   rW    r   r   r8   r8   m   s9    , 6 6: 0 #r   r8   c           	        ddl m} ddlm}  || dd      }t	               }t	               }dddddd}	|j
                  D ]  }
|
j                         D ]k  }|D ]b  }|j                  }|t        |t              r|j                  d      r|	d	xx   d
z  cc<   B ||j                         |j                   }t        |t              rst        |t        t        f      ra|j!                  t        |             |dk(  r|	dxx   d
z  cc<   t#        t        |      ||
j$                  |      |_        |	dxx   d
z  cc<   t        |t              s|j!                  |       t&        j)                  |      dk(  r&t*        j-                  |      }||k(  r|	dxx   d
z  cc<   O||_        |	dxx   d
z  cc<   e n  |j.                  j1                  dd       |j3                  |       |j5                          t7        || |||      }|	|t9        j:                  |j=                               j?                         dS )Nr   load_workbookget_column_letterFT	data_only
keep_links)numbers_replacednumbers_kept_zerotexts_pseudonymisedtexts_kept_labelformulas_untouched=rz   r   rw   rv   rO   ry   rx   )parentsexist_ok)statsleaksr   ) openpyxlrp   openpyxl.utilsrr   set
worksheets	iter_rowsr(   
isinstancer!   
startswithcolumnrowrg   r   r#   addr6   titler8   r<   ANONr[   parentmkdirsavecloseverify_no_leakr
   r   
read_bytes	hexdigest)srcdstr;   r   rp   rr   wboriginal_numbersoriginal_textsr~   wsr   cellvr*   anonr   s                    r   	anonymiser      s   &0	se	=B#&5"uN!"ST!"!=E mm !:<<>  	:C :JJ9 a%!,,s*;./14/,T[[9:488*Ea&a#u.$((q2Av12a72 !/a$%!PDJ,-2-3'"&&q)%++A."4 >>!,Dqy01Q61%)
3494?: 	:!:F JJTD1GGCLHHJ3%5~vNEUnnS^^%56@@BD Dr   c                    | dk(  ryt        |       dk\  ryt        t        |             }d|v xr/ t        |j	                  d      d   j                  d            dk\  S )	u  Traegt dieser Zahlenwert genug Information, um ein Geheimnis zu sein?

    Bei 1507 ersetzten Zahlen in einem kleinen Wertebereich kollidieren triviale
    Werte zwangslaeufig: eine 12 im Original und eine neu erzeugte 12 an ganz
    anderer Stelle sind kein Leck, sondern Arithmetik. Eine Pruefung, die darauf
    anschlaegt, meldet 39 Fehlalarme und wird deshalb irgendwann abgeschaltet —
    das waere der eigentliche Schaden.

    Charakteristisch ist ein Wert, wenn er sich nicht zufaellig wiederholt:
    ab drei Nachkommastellen oder ab Betraegen >= 1000. Genau dort sitzen die
    Kalkulationsgroessen (397.9058, 12.9631), nicht bei Stueckzahlen und Indizes.
    r   Fr   Tr   r   0   )r   reprr#   r    r%   rstrip)r(   rJ   s     r   is_characteristicr      s\     z
5zTU5\A!8>AGGCL,33C89Q>>r   c                   |D ch c]5  }t        j                  dd|j                         j                               7 c}g }dfd}ddlm} ddlm}	  ||dd	      }
 || dd	      }|
j                  D ]0  }||j                     }|j                         D ]
  }|D ]  }|j                  }|t        |t              r#t        |t              r|j                  d      rE |	|j                          |j"                   }||   j                  }t        |t$        t&        f      r1|dk7  s||k(  s|j)                  d|j                   d| d|        t        |t              s ||      r||k(  s|j)                  d|j                   d| d|d
d           3 t+               }t+               }|j                  D ]  }|j                         D ]  }|D ]  }|j                  }t        |t              s|"t        |t$        t&        f      r|j-                  t'        |             St        |t              sd|j                  d      rv|j-                  |          |
j/                          |j/                          ||z  D ]"  }t1        |      s|j)                  d|        $ ||z  D ]"  } ||      r|j)                  d|d
d         $ |S c c}w )u  Zwei Pruefungen, beide notwendig.

    1. ZELLGENAU: Jede Zelle, die im Original einen Wert trug, muss in der
       Ausgabe einen anderen tragen (ausser Null, Formeln und Beschriftungen).
       Das ist der harte Nachweis, dass tatsaechlich ersetzt wurde — und er
       faellt nicht auf Kollisionen herein.
    2. GLOBAL fuer charakteristische Werte: Ein Betrag mit vielen Nachkomma-
       stellen oder >= 1000 darf NIRGENDWO in der Ausgabe auftauchen, auch
       nicht an anderer Stelle. Sonst koennte ein verschobener Wert das
       Geheimnis weitertragen.

    Asymmetrisch mit Absicht: ersetzt wird nach Regel, geprueft wird gegen alle
    Originalwerte. Andersherum bliebe jede Zelle unbemerkt, die die Regel nicht
    erfasst.
    rD   rE   c                    t        j                  dd| j                         j                               }|dk(  xs3 |v xs- t	        t        j
                  d| j                                     S )NrD   rE   rO   rP   )rF   rG   rH   rI   rg   rT   )tnormlabels_norms     r   is_trivial_textz'verify_no_leak.<locals>.is_trivial_text!  sU    vvfc1779??#45rzbT[0bDoWXW^W^W`9a4bbr   r   ro   rq   FTrs   Nr{   zunveraendert !z:     z,charakteristische Zahl weiterhin vorhanden: z"Originaltext weiterhin vorhanden: (   )r   r!   rf   rg   )rF   rG   rH   rI   r   rp   r   rr   r   r   r   r(   r   rg   r!   r   r   r   r   r#   appendr   r   r   r   )r   r   r   r   r;   rA   r   r   rp   rr   wb_srcwb_outws_srcws_outr   r   r   r*   r2   out_numbers	out_textsr   nr   r   s                           @r   r   r     s   " DJJa266&#qwwy'89JKEc '03%DAF3%DAF ## S%##% 	SC SJJ9
1d 3a%!,,s*;,T[[9:488*EUm))a#u.Av#(}V\\N!E7"QC%PQ3'0BsaxLL=awb3B!QRS	SS$ "eK%I 	%<<> 	%C %JJa&!)a#u.OOE!H-3'S0AMM!$%	%	% LLN
LLN+ MQLLGsKLM i' Hq!LL=afXFGH Lm Ks   :K(c                    t        j                  t              } | j                  d       | j                  d       | j                  ddd       | j                  dd	
       | j	                         }t        |j                        t        |j                        }}|j                         st        d| t        j                         yt        t        j                  t        |j                        j!                  d            d         }t#        ||j$                        at)        ||||j$                        }t        t        j*                  |d   dd             |d   r{t        dt-        |d          dt        j                         t/        t        |d               d d D ]   }t        d|z   t        j                         " |j1                  d       yt        d|        t        d|d           t        d       y)N)descriptionsourcedestz--labelsTz JSON mit Formular-Beschriftungen)requiredhelpz--seedzqaf-golden-2026)defaultzQuelle fehlt: )filer   r   r;   r~   F)indentensure_asciir   u   
ABBRUCH — z  Originalwert(e) in der Ausgabe:   z  )
missing_okr   z

Fixture: z	SHA-256: r   zHLeck-Pruefung bestanden (kein Originalwert, keine Originalzeichenkette).r   )argparseArgumentParserrk   add_argument
parse_argsr   r   r   existsprintsysstderrr   jsonloadsr;   	read_textr8   r   r   r   dumpsr    sortedunlink)apargsr   r   r;   r5   leaks          r   mainr   W  s   		 	 W	5BOOHOOFOOJ4VOWOOH&7O8==?DDKK $tyy/C::<se$3::6D-77@A(KLF &$)),DsC3F	$**VG_QU
CDgs6'?344TU\_\f\fg3vg/0"5 	0D$+CJJ/	0

d
#	Ku
	IfX&'
()	
TUr   __main__)r   r!   r   r!   rf   r   )
r(   r#   r   r!   r)   r!   r*   r!   rf   r#   )
r   r   r   r   r;   re   r   r!   rf   dict)r(   r#   rf   rg   )r   r   r   r   r   z
set[float]r   re   r;   re   rf   z	list[str])rf   r   )rk   
__future__r   r   r
   r   rF   r   pathlibr   r   r6   r8   r   r   r   r   rh   
SystemExitrm   r   r   <module>r      s   "H #    	 
 "(
(VO# O#h4Dn?*G#+G5=GBKGTD z
TV
 r   