ó
    À‘´jåo  ã                  ó„  • S r SSKJr  SSKrSSKJr  SSKJrJrJ	r	J
r
JrJrJr  SSKJr  SSKJrJr  SSKJr  \R*                  " S	\R,                  5      rS
rSrSrSrSr " S S\5      r " S S\5      rS*S jrS+S jr S,S jr!S-S jr"      S.S jr#              S/S jr$      S0S jr%        S1S jr&          S2S jr'S3S jr(S4S jr)          S5S jr*\4     S6S jjr+    S7S  jr, S8           S9S! jjr-\ " S" S#\
5      5       r. " S$ S%5      r/ " S& S'5      r0\S(   r1S:S;S) jjr2g)<uu  
CompletenessAuditor â€” was the whole table captured, or did part of it get
cut off at an edge? The deterministic text-layer audit behind step 3 of the
Camelot correspondence flow (`quber.core.extractors.camelot.correspondence`).

Camelot reads the PDF *text layer*, not pixels, so the cell values it emits
are the document's real characters. This audit looks for one failure:
truncation at an edge, where the chunk began below the table's top or ended
above its bottom. Camelot can also drop an interior row's values during
cell assignment. This audit ignores missing figures deep inside the
captured span, so it does not catch that.

The audit reads the same text layer Camelot does (via PyMuPDF). For the
table's region it compares the *value-like* numeric figures present in the
text layer against those present in the assembled markdown. A formatted
figure in the extracted columns that never made it into the extraction
counts as a drop only when it sits within EDGE_BAND_PTS of the extracted
span's top or bottom edge. The table was then truncated at that edge.
Judging truncation by eye over-flags by misreading digits off a rasterized
image. This audit does not have that false-positive source, because it
compares real text-layer tokens against real output tokens rather than
re-reading numbers.

"Value-like" is deliberately narrow: numbers carrying a thousands
separator, a decimal, a percent, or five-plus digits. Parentheses or a
currency sign alone do not qualify, so "(84)" is not value-like. Bare
small integers (years like 2024, days like 31, footnote markers) are
excluded so a caption or footnote inside the rough region does not read
as a dropped row. The audit itself never edits the table; it only flags.

When the audit reports a gap, the correspondence orchestrator first extends
the table with the next leftover chunk and re-audits, until no leftover
chunk remains. It then runs the text-layer fill in this module
(`round_off_grid`), which writes the missing figures into the grid, and
re-audits once more. The table is marked incomplete and the gap reported
only if figures are still missing after the fill (the report-don't-drop
rule).
é    )ÚannotationsN)ÚPath)ÚListÚLiteralÚOptionalÚProtocolÚSequenceÚTupleÚruntime_checkable)Úlogger)Ú	BaseModelÚField)Úget_settingsaË  
    \(?\$?                                   # optional open paren / currency
    (?:
        \d{1,3}(?:,\d{3})+(?:\.\d+)?         # thousands-grouped: 1,234 or 1,234.56
      | \d+\.\d+                             # decimal: 12.3
      | \d+(?:\.\d+)?%                       # percentage: 5% or 12.3%
      | \d{5,}                               # long integer: 12345+
    )
    \)?%?                                    # optional close paren / percent
    g      D@g       @g      (@g      @g      2@c                  ó¢   • \ rS rSr% Sr\" SS9rS\S'   \" SS9rS\S	'   \" S
S9r	S\S'   \" SS9r
S\S'   \" SSS9rS\S'   \" SSS9rS\S'   Srg)ÚMissingFigureé`   zÊA value-like figure the completeness check found in the text layer
within the table's edge band but absent from the extraction. Carries
enough geometry for the fill step to place it back into the grid.
z<Display form as it appears in the text layer, e.g. '240,083'©ÚdescriptionÚstrÚvaluezToken center x in PDF pointsÚfloatÚxz-Token center y in PDF points (top-left frame)Úyz5Which edge of the captured span this figure sits pastzLiteral['top', 'bottom']Úedgeéÿÿÿÿz?Column index assigned by round_off (0 is label); -1 if unplaced©Údefaultr   ÚintÚcolumnÚ z>Label of the row this figure was placed into; set by round_offÚ	row_label© N)Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__r   r   Ú__annotations__r   r   r   r   r!   Ú__static_attributes__r"   ó    Ú7/home/mande/repo/quber/src/quber/agents/completeness.pyr   r   `   s€   ‡ ññ
 Ð#aÑb€Eˆ3ÓbÙÐ!?Ñ@€A€uÓ@ÙÐ!PÑQ€A€uÓQÙ%*ØKñ&€DÐ
"ó ñ ØÐ añ€FˆCó ñ ØÐ `ñ€Iˆsö r*   r   c                  ót   • \ rS rSr% \" SS9rS\S'   \" SSS9rS	\S
'   \" SSS9rS	\S'   \" \	SS9r
S\S'   Srg)ÚCompletenessVerdictét   uý   True if every value-like figure adjacent to the extracted data (in its column span) is present in the extraction. False if a formatted figure sits just past the top or bottom edge but is absent from the extraction â€” the table was cut off at that edge.r   ÚboolÚcompleter    z·When complete is False, which edge was truncated and an example of the missing figure(s) (e.g. 'the table continues below the last extracted row (e.g. 240,083)'). Empty when complete.r   r   Úgapz:One short sentence summarizing the value-token comparison.ÚreasonzfFigures found past an edge but absent from the extraction; the fill step's input. Empty when complete.)Údefault_factoryr   zList[MissingFigure]Úmissing_figuresr"   N)r#   r$   r%   r&   r   r0   r(   r1   r2   Úlistr4   r)   r"   r*   r+   r-   r-   t   sf   ‡ Ùð.ñ€Hˆdó ñ Øð5ñ€Cˆó ñ ØØPñ€FˆCó ñ ,1ØØ|ñ,€OÐ(ö r*   r-   c                ó2   • [         R                  " SSU 5      $ )zÑCollapse a value token to a comparable key: drop currency, commas,
parentheses and surrounding whitespace; keep digits, decimal point and
percent. So '$240,083', '240,083' and '(240,083)' all key to '240083'.
z[\s,$()]r    )ÚreÚsub)Útokens    r+   Únormalize_valuer:   �   s   € ô
 �6Š6�+˜r 5Ó)Ð)r*   c                óˆ   • [         R                  U 5       Vs/ s H  n[        UR                  S5      5      PM     sn$ s  snf )z=Every value-like figure in `text`, normalized for comparison.r   )ÚVALUE_TOKEN_REÚfinditerr:   Úgroup)ÚtextÚms     r+   Úvalue_tokensrA   —   s4   € ä1?×1HÑ1HÈÔ1NÓOÒ1N¨AŒO˜AŸG™G A›JÖ'Ñ1NÑOÐOùÒOs   ˜$?c                ó^  • SSK nUR                  [        U 5      5      n X1S-
     nUR                  SSS9 Vs/ s HB  n[        US   5      R	                  5       (       d  M&  [        US   5      R	                  5       PMD     snUR                  5         $ s  snf ! UR                  5         f = f)a  The PDF text layer of one 1-indexed page as its text blocks in reading
order, via PyMuPDF. A block is what the layout keeps together: a paragraph,
a multi-line title, one panel of a slide. Where the page prints columns,
a line-by-line reading interleaves them; blocks do not.r   Né   ÚblocksT)Úsorté   )ÚfitzÚopenr   Úget_textÚstripÚclose)ÚsourceÚpagerG   ÚdocÚpgÚbs         r+   Úpage_blocksrQ   œ   sŠ   € ó
 à
�)‰)”C˜“KÓ
 €CðØ˜‘‰]ˆØ+-¯;©;°xÀd¨;Ñ+KÓaÒ+K aÌsÐSTÐUVÑSWËyÏÉ×O`Ó!”�A�a‘D“	—‘Ö!Ñ+KÑaà�	‰	�ùò bøà�	‰	�ús"    B ¹%BÁ" BÂB ÂB ÂB,c                óÔ  • SSK nUR                  [        U 5      5      n X1S-
     nUR                  nUR	                  S5       Vs/ s HF  n[        US   5      [        US   5      [        US   5      [        US   5      [        US   5      4PMH     nn[        UR                  5      [        UR                  5      U4UR                  5         $ s  snf ! UR                  5         f = f)a  Read the PDF text layer for one 1-indexed page via PyMuPDF.

Returns the page width/height in points and the word boxes as
(x0, y0, x1, y1, text) in PyMuPDF's top-left-origin point frame. This
is the same text layer Camelot parses, so the figures match exactly.
r   NrC   Úwordsé   é   rF   )	rG   rH   r   ÚrectrI   r   ÚwidthÚheightrK   )rL   rM   rG   rN   rO   rV   ÚwrS   s           r+   Ú
page_wordsrZ   «   sÈ   € ó à
�)‰)”C˜“KÓ
 €CðØ˜‘‰]ˆØ�w‰wˆàUW×U`ÑU`ÐahÔUió?
ÚUiÐPQŒU�1�Q‘4‹[œ%  !¡›+¤u¨Q¨q©T£{´E¸!¸A¹$³KÄÀQÀqÁTÃÓKÑUið 	ð ?
ô �T—Z‘ZÓ ¤%¨¯©Ó"4°eÐ;à�	‰	�ùò?
øð
 	�	‰	�ús    &C ÁACÂ,C ÃC ÃC'c                óx  • / n/ n/ n/ nU  H{  nUc  M  Uu  pxpšUR                  [        Xy5      5        UR                  [        Xy5      5        UR                  U[        XŠ5      -
  5        UR                  U[        XŠ5      -
  5        M}     U(       d  g[        U5      [        U5      [        U5      [        U5      4$ )z÷Union of the matched Camelot chunk bboxes, converted to PyMuPDF's
top-left point frame: (x0, top, x1, bottom). Camelot bboxes are PDF
points with a bottom-left origin, so y flips against the page height.
Returns None if no chunk carried geometry.
N)ÚappendÚminÚmax)Úextracted_bboxesÚpage_hÚxs0Úxs1ÚtopsÚbottomsÚbbÚx1Úy1Úx2Úy2s              r+   Úextracted_span_ptsrj   À   s¤   € ð €CØ€CØ€DØ€GÛˆØ‰:ÙØ‰ˆ�Ø�
‰
”3�r“;ÔØ�
‰
”3�r“;ÔØ�‰�FœS ›[Ñ(Ô)Ø�‰�v¤ B£Ñ+Ö,ñ ö ØÜ�‹H”c˜$“i¤ S£¬3¨w«<Ð8Ð8r*   c                ó8  • [        [        U5      5      nUbU  [        US   US   5      U -  n[        US   US   5      U -  n[        US   US   5      U-  n	[        US   US   5      U-  n
O	SU SU4u  pxpš[	        XA5      nUb"  US   [
        -
  nUS   [
        -   nUS   US   pþOXxpÜS=pï/ n/ n/ n/ nU GH  u  nnnnnUU-   S-  nUU-   S-  nUUs=::  a  U::  a  O  OU	Us=::  a  U
::  d  O  M;  UUs=::  a  U::  d  O  ML  [        R                  U5       H°  nUR                  S5      n[        U5      nUU;   a  M'  Ub  Uc  UR                  U5        M@  [        UU-
  5      n[        UU-
  5      nU[        :”  a  U[        :”  a  Mr  UU::  a  SOS	n U S:X  a  UOUR                  U5        UR                  [        UUUU S
95        M²     GM     U(       d  U(       d  U(       d
  [        SSS9$ SS jn!/ n"U(       a  U"R                  SU!" U5       S35        U(       a  U"R                  SU!" U5       S35        U(       a  U"R                  SU!" U5       S35        [        U5      [        U5      -   [        U5      -   n#[        SSR!                  U"5      U# S3US9$ )a  Deterministic edge-truncation check by text-layer value comparison.

`region_bbox` is the detector's neighbor-bounded box (normalized 0..1,
top-left). `extracted_bboxes` are the matched Camelot chunk bboxes (PDF
points, bottom-left). All geometry is reduced to PyMuPDF's top-left
point frame. A value-like figure is considered when it sits inside the
region and within the extracted chunks' x-span plus COLUMN_PAD_PTS, and
is absent from `assembled_markdown`. It is reported only if it sits
within EDGE_BAND_PTS of the extracted span's top or bottom edge, and is
classified top or bottom by the nearer edge. Figures deeper inside the
span or farther past an edge are ignored. When no chunk carries a bbox,
every missing figure in the region's own column span is reported with
the edge unclear.
Nr   rT   rC   rU   ç        ç       @ÚtopÚbottom)r   r   r   r   TzQall value-like figures in the table's column region are present in the extraction©r0   r2   c                óJ   • SR                  [        R                  U 5      5      $ )Nz, )ÚjoinÚdictÚfromkeys)Úvalss    r+   ÚsampleÚ$completeness_verdict.<locals>.sample*  s   € Ø�y‰yœŸ™ tÓ,Ó-Ð-r*   z5rows above the first extracted row are missing (e.g. Ú)z7the table continues below the last extracted row (e.g. zhvalue-like figures in the region are absent from the extraction, edge unclear (no chunk geometry) (e.g. Fz; zH value-like figure(s) in the table region are absent from the extraction)r0   r1   r2   r4   )ru   ú	List[str]Úreturnr   )ÚsetrA   r]   r^   rj   ÚCOLUMN_PAD_PTSr<   r=   r>   r:   r\   ÚabsÚEDGE_BAND_PTSr   r-   Úlenrr   )$Úpage_wr`   rS   Úregion_bboxr_   Úassembled_markdownÚ	md_valuesÚrx0Úrx1Úr_topÚr_botÚspanÚcol_x0Úcol_x1Úext_topÚext_botÚtop_missingÚbottom_missingÚspan_unknown_missingÚfiguresÚx0Úy0rf   rg   r?   ÚcxÚcyr@   ÚorigÚvÚd_topÚd_botr   rv   ÚpartsÚtotals$                                       r+   Úcompleteness_verdictr›   Ú   sæ  € ô, ”LÐ!3Ó4Ó5€Ið ÑÜ�+˜a‘. +¨a¡.Ó1°FÑ:ˆÜ�+˜a‘. +¨a¡.Ó1°FÑ:ˆÜ�K ‘N K°¡NÓ3°fÑ<ˆÜ�K ‘N K°¡NÓ3°fÑ<‰à!$ f¨c°6Ð!9Ñˆ�%äÐ.Ó7€DØÑØ�a‘œ>Ñ)ˆØ�a‘œ>Ñ)ˆØ ™7 D¨¡G‘ð �Ø Ð ˆà€KØ "€NØ&(ÐØ#%€Gä %ÑˆˆB��B˜Ø�2‰g˜‰_ˆØ�2‰g˜‰_ˆØ�rÕ ˜SÖ  U¨bÕ%9°EÕ%9ÙØ˜"Õ& Õ&ÙÜ×(Ñ(¨Ö.ˆAØ—7‘7˜1“:ˆDÜ Ó%ˆAØ�I‹~ÙØ‰ '¡/Ø$×+Ñ+¨AÔ.ÙÜ˜˜W™Ó%ˆEÜ˜˜W™Ó%ˆEØ”}Ó$¨´Ó)>ñ Ø6;¸u³n©UÈ(ˆDØ  E›M‰[¨~×EÑEÀaÔHØ�N‰Nœ=¨t°r¸RÀdÑKÖLô! /ñ !&ö2 ž>Ö-AÜ"ØØfñ
ð 	
ô
.ð €EÞØ�‰ÐLÉVÐT_ÓM`ÐLaÐabÐcÔdÞØ�‰ÐNÉvÐVdÓOeÐNfÐfgÐhÔiÞØ�‰ð)Ù)/Ð0DÓ)EÐ(FÀaðIô	
ô �Óœs >Ó2Ñ2´SÐ9MÓ5NÑN€EÜØØ�I‰I�eÓØ�Ð`ÐaØñ	ð r*   c                ó4  ^• [        U4S jU  5       5      nU(       d  / $ US   //nUSS  H=  nXCS   S   -
  [        :”  a  UR                  U/5        M)  US   R                  U5        M?     U Vs/ s H  n[        U5      [	        U5      -  PM     sn$ s  snf )zËNumeric-column center x's (PDF points) inferred from the value-like
tokens inside the captured span. The fill step slots a restored row's
figures into the column whose center is nearest each figure's x.
c              3  ó®   >#   • U  HJ  u  pp4nTS    X$-   S-  s=::  a  TS   ::  d  M!  O  M%  [         R                  U5      (       d  MA  X-   S-  v •  ML     g7f)rC   rm   rU   N©r<   Úsearch)Ú.0r‘   r’   rf   rg   r?   rˆ   s         €r+   Ú	<genexpr>Ú infer_columns.<locals>.<genexpr>H  s]   øé € ð â&+Ñ"ˆR�R˜TØ�‰7�r‘w #‘oÕ0¨¨a©Ñ0ó 	á0ñ 	ä5C×5JÑ5JÈ4×5Pó 	ˆ‰�CŽÚ&+ùs   ƒ A§A«AÁAr   rC   Nr   )ÚsortedÚCOLUMN_GAP_PTSr\   Úsumr   )rS   rˆ   ÚxsÚclustersr   Úcs    `    r+   Úinfer_columnsr©   @  s�   ø€ ô 
ô á&+óó 
€Bö
 Øˆ	Ø$& q¡E 7 )€HØ��‹VˆØ˜‰|˜BÑÑ¤.Ó0Ø�O‰O˜Q˜CÖ à�R‰L×Ñ Ö"ñ	 ñ
 &.Ó.¢X ŒC�‹F”S˜“VŒO¡XÑ.Ð.ùÒ.s   Á1!Bc                ó8  • / nU  Hr  u  pEpgnXW-   S-  n	[        X‘-
  5      [        :”  d  XF-   S-  U:¼  a  M0  [        R                  U5      (       d  UR	                  5       S;   a  M`  UR                  XH45        Mt     SR                  S [        U5       5       5      $ )zÇLabel for a restored row: the non-figure words at row `y` that sit
left of the first numeric column, in reading order. Lone currency and
paren glyphs are dropped so the label is just the row's text.
rm   >   r    Ú$Ú(rx   Ú c              3  ó*   #   • U  H	  u  pUv •  M     g 7f©Nr"   )r    Ú_Úts      r+   r¡   Úrow_label_at.<locals>.<genexpr>i  s   é € Ð0¢-™$˜!•A¢-ùs   ‚)r}   ÚROW_TOL_PTSr<   rŸ   rJ   r\   rr   r£   )
rS   r   Úfirst_col_xr™   r‘   r’   rf   rg   r?   r”   s
             r+   Úrow_label_atrµ   X  sŽ   € ð &(€EÛ %Ñˆ�˜Ø‰g˜‰_ˆÜˆr‰v‹;œÓ$¨©°C©¸;Ó(FÙÜ× Ñ  ×&Ñ&¨$¯*©*«,Ð:MÓ*MÙØ�‰�b�ZÖ ñ !&ð �8‰8Ñ0¤&¨¤-Ó0Ó0Ð0r*   c           	     ó°  ^^• [        X5      mT(       a  U(       d  U / 4$ / n[        US S9 Hr  mU(       a?  [        US   S   TR                  -
  5      [        ::  a  US   S   R                  T5        MI  UR                  TR                  TR                  T/45        Mt     / n/ n/ nU Hº  u  p‰n
[        XTS   [        -
  5      nS/[        T5      -  nU
 HT  m[        [        [        T5      5      UU4S jS9nTR                  XÍ'   US-   Tl        UTl        UR                  T5        MV     S	S
R                  U/U-   5      -   S-   nU	S:X  a  UOUR                  XŽ45        M¼     U R!                  5       n[        U5       H  u  nnUR                  U5        M     [        USS9 H  u  nnUR#                  SU5        M     SR                  U5      U4$ )a  Rebuild truncated edge rows from the text layer and merge them into the
markdown. Returns (new_markdown, placed_figures).

Deterministic and value-preserving: the inserted figures are the literal
text-layer tokens (the same source Camelot reads), never invented. Each
placed figure has its column index and row label set so the caller can
record the amendment. A bottom-edge row is appended; a top-edge row is
inserted at the top. Returns the markdown unchanged with an empty list if
no columns can be inferred or there is nothing to place.
c                ó   • U R                   $ r¯   ©r   ©r@   s    r+   Ú<lambda>Úround_off.<locals>.<lambda>�  ó   € °A·C²Cr*   ©Úkeyr   r   rT   r    c                ó:   >• [        TU    TR                  -
  5      $ r¯   )r}   r   )ÚiÚcolsÚmfs    €€r+   rº   r»   Ž  s   ø€ ´S¸¸a¹À2Ç4Á4¹Ô5Hr*   rC   z| z | z |rn   T)ÚreverseÚ
)r©   r£   r}   r   r³   r\   r   rµ   r¤   r   r]   Úranger   r   r!   rr   Ú
splitlinesÚinsert)r‚   rS   rˆ   r4   ÚrowsÚplacedÚ	top_linesÚbottom_linesr   r   ÚfigsÚlabelÚcellsÚciÚrow_mdÚlinesr°   rÁ   rÂ   s                    @@r+   Ú	round_offrÒ   l  s³  ù€ ô  ˜Ó%€DÞ–Ø! 2Ð%Ð%à9;€DÜ�_©-Ô8ˆÞ”C˜˜R™ ™ b§d¡dÑ*Ó+¬{Ó:Ø�‰H�Q‰K×Ñ˜rÖ"à�K‰K˜Ÿ™˜rŸw™w¨¨Ð-Ö.ñ	 9ð #%€FØ)+€IØ,.€LÛ‰ˆ�Ü˜U t¨A¡w´Ñ'?Ó@ˆØ�”s˜4“yÑ ˆÛˆBÜ”Uœ3˜t›9Ó%Õ+HÑIˆBØŸ™ˆE‰IØ˜Q™ˆBŒIØ ˆBŒLØ�M‰M˜"Öñ ð ˜Ÿ
™
 E 7¨U¡?Ó3Ñ3°dÑ:ˆØ˜e“m‰¨×=Ñ=¸q¸kÖJñ ð ×)Ñ)Ó+€EÜ˜LÖ)‰	ˆˆ6Ø�‰�VÖñ *ä˜I¨tÔ4‰	ˆˆ6Ø�‰�Q˜Öñ 5à�9‰9�UÓ˜VÐ#Ð#r*   c                óÆ   ^• U (       d  / $ [        S U  5       5      n/ n[        U5       H3  m[        U4S jU SS  5       5      (       d  M"  UR                  T5        M5     U$ )z¼Grid column indices that hold value-like figures in the data rows
(row 0 is treated as the header). Ordered left to right; these are the
columns a restored row's figures get slotted into.
c              3  ó8   #   • U  H  n[        U5      v •  M     g 7fr¯   ©r   ©r    Úrs     r+   r¡   Ú'numeric_grid_columns.<locals>.<genexpr>¥  s   é € Ð&¢˜1”�A—�¢ùó   ‚c              3  ó~   >#   • U  H2  nT[        U5      :  =(       a    [        R                  UT   5      v •  M4     g 7fr¯   )r   r<   rŸ   )r    ÚrowÚjs     €r+   r¡   rØ   ¨  s0   øé € ÐSÊÀ#ˆq”3�s“8‰|×=¤× 5Ñ 5°c¸!±fÓ =Ô=Êùs   ƒ:=rC   N)r^   rÅ   Úanyr\   )rÎ   rW   ÚoutrÜ   s      @r+   Únumeric_grid_columnsrß   ž  sY   ø€ ö
 Øˆ	ÜÑ&¡Ó&Ó&€EØ€CÜ�5Ž\ˆÜÔSÈÈqÈrÉÓS×SÓSØ�J‰J�qŽMñ ð €Jr*   c                ój   • [         R                  " SSU 5      R                  5       R                  5       $ )zdCollapse whitespace and case for matching a restored row to an
existing grid row by its label cell.
z\s+r­   )r7   r8   rJ   Úlower)r?   s    r+   Únormalize_labelrâ   ­  s)   € ô �6Š6�&˜#˜tÓ$×*Ñ*Ó,×2Ñ2Ó4Ð4r*   c           	     ó"  ^^^^• [        X5      m[        U 5      mT(       a  T(       a  U(       d  U  Vs/ s H  n[        U5      PM     sn/ 4$ U  Vs/ s H  n[        U5      PM     nn[        S U 5       SS9nU H$  mTR	                  S/U[        T5      -
  -  5        M&     SUU4S jjn/ n[        US S9 Hr  n	U(       a?  [        US   S   U	R                  -
  5      [        ::  a  US   S	   R                  U	5        MI  UR                  U	R                  U	R                  U	/45        Mt     / n
U GHÁ  u  p¼n[        XS   R                  TS   [        -
  5      nU V	s0 s H#  n	[        U" U	R                  5      US
-
  5      U	_M%     nn	SnU(       a[  [!        U5       HL  u  nm[#        U4S jT 5       5      nT(       d  M$  [%        TS   5      [%        U5      :X  d  MA  U(       d  MJ  Un  O   UcM  S/[        US
5      -  nUUS'   US:X  a  [        U5      O[        S
[        U5      5      nUR'                  UU5        UR)                  5        HŸ  u  mn	UU   T   R+                  5       (       aN  T Vs/ s H*  nUU:  d  M  UU   U   R+                  5       (       a  M(  UPM,     nnU(       d  Ma  [        UU4S jS9mU	R,                  UU   T'   TU	l        Xél        U
R                  U	5        M¡     GMÄ     XZ4$ s  snf s  snf s  sn	f s  snf )a…  Round off a truncated table by inserting the missing edge figures
into a structured cell grid (not a markdown string). Returns
(new_cells, placed_figures).

Clean composition: a figure is placed into the grid column whose
inferred x matches the figure's x (text-layer numeric columns map
left-to-right onto the grid's numeric columns). A restored row is filled
in place into the first grid row whose label cell matches and which has
at least one empty numeric column, so the column count is preserved.
Otherwise a new row is inserted, at the bottom for a bottom-edge
truncation and after the header for a top-edge one. A figure whose
mapped cell is already full goes to the nearest empty numeric column of
that row. If none is empty the figure is left unplaced and the re-audit
still reports it. Values are the literal text-layer tokens, never
invented; a non-empty Camelot cell is never overwritten.
c              3  ó8   #   • U  H  n[        U5      v •  M     g 7fr¯   rÕ   rÖ   s     r+   r¡   Ú!round_off_grid.<locals>.<genexpr>Ð  s   é € Ð+¢˜A”�Q—�¢ùrÙ   r   )r   r    c                óz   >^ • [        [        [        T5      5      UU 4S jS9nU[        T5      :  a  TU   $ TS   $ )Nc                ó&   >• [        TU    T-
  5      $ r¯   ©r}   )rÀ   Úcol_xr   s    €€r+   rº   Ú8round_off_grid.<locals>.grid_col_for_x.<locals>.<lambda>Õ  s   ø€ ´°U¸1±XÀ±\Ô1Br*   r½   r   )r]   rÅ   r   )r   Úkré   Únum_colss   ` €€r+   Úgrid_col_for_xÚ&round_off_grid.<locals>.grid_col_for_xÔ  s7   ù€ Ü””c˜%“jÓ!Õ'BÑCˆØ¤# h£-Ó/ˆx˜‰{ÐA°X¸b±\ÐAr*   c                ó   • U R                   $ r¯   r¸   r¹   s    r+   rº   Ú round_off_grid.<locals>.<lambda>Ú  r¼   r*   r½   r   rT   rC   Nc              3  ó|   >#   • U  H1  o[        T5      :  =(       a    TU   R                  5       (       + v •  M3     g 7fr¯   )r   rJ   )r    r¨   r×   s     €r+   r¡   rå   ð  s,   øé € ÐQÊÀ1¤3 q£6™z×>°!°A±$·*±*³,Ô.>Ô>Êùs   ƒ9<ro   c                ó    >• [        U T-
  5      $ r¯   rè   )rÜ   Úgcs    €r+   rº   rð     s   ø€ ´°A¸±F´r*   )r   r   rz   r   )r©   rß   r5   r^   Úextendr   r£   r}   r   r³   r\   r   rµ   r¤   r]   r   Ú	enumeraterÝ   râ   rÇ   ÚitemsrJ   r   r   r!   )rÎ   rS   rˆ   r4   r×   Ú	new_cellsrW   rí   rÈ   rÂ   rÉ   r°   r   rÌ   rÍ   ÚwantÚtargetÚriÚhas_roomrÛ   rÜ   Úemptiesré   ró   rì   s       `                 @@@r+   Úround_off_gridrý   ´  sÉ  û€ ô, ˜%Ó&€EÜ# EÓ*€HÞž®Ù!&Ó'¢˜A”�Q–¡Ñ'¨Ð+Ð+á38Ó!9²5¨a¤$ q¦'±5€IÐ!9ÜÑ+¡Ó+°QÑ7€EÛˆØ	�‰�"�˜¤ Q£™Ñ(Ö)ñ ÷Bð Bð
 :<€DÜ�_©-Ô8ˆÞ”C˜˜R™ ™ b§d¡dÑ*Ó+¬{Ó:Ø�‰H�Q‰K×Ñ˜rÖ"à�K‰K˜Ÿ™˜rŸw™w¨¨Ð-Ö.ñ	 9ð #%€FÜ‰ˆ�Ü˜U¨¡G§I¡I¨u°Q©x¼.Ñ/HÓIˆñ DHÓHÂ4¸R”‘N 2§4¡4Ó(¨%°!©)Ó4°bÒ8Á4ˆÐHð ˆÞÜ" 9Ö-‘��AÜÔQÉÓQÓQ�ß�1œ¨¨1©Ó.´/À%Ó2HÕHÏXÈXØ�FÙñ	 .ð
 ‰>Ø�$œ˜U A›Ñ&ˆCØˆC�‰FØ'+¨xÓ'7”S˜”^¼SÀÄCÈ	ÃNÓ=SˆFØ×Ñ˜V SÔ)à—j‘j–l‰FˆB�Ø˜Ñ  Ñ$×*Ñ*×,Ñ,ñ '/Óa¢h °!°e±)›1ÀIÈfÑDUÐVWÑDX×D^ÑD^×D`Ÿ1¡h�ÐaÞÙÜ˜Ô&;Ñ<�Ø$&§H¡HˆI�fÑ˜bÑ!ØˆBŒIØ ŒLØ�M‰M˜"Öô #ñ3 ðL ÐÐùòu (ùâ!9ùò, Iùò2 bs#   ´K=ÁLÅ"*LÉ7
LÊLÊ"Lc                ó¬   • [        S U  5       5      n/ nSnU H7  u  pVUb  XT-
  U:”  a  UR                  / 5        UnUS   R                  U5        M9     U$ )zEGroup words into visual rows by y-center (words within `tol` points).c              3  ó@   #   • U  H  oS    US   -   S-  U4v •  M     g7f)rC   rU   rm   Nr"   ©r    rY   s     r+   r¡   Útext_rows.<locals>.<genexpr>  s$   é € Ð;²U°�q‘T˜A˜a™D‘[ CÑ'¨Õ+²Uùs   ‚Nr   )r£   r\   )rS   Útolrö   rÈ   Úrow_cyr”   rY   s          r+   Ú	text_rowsr  
  s_   € ô
 Ñ;±UÓ;Ó;€EØ?A€DØ"€FÛ‰ˆØ‰>˜R™[¨3Ó.Ø�K‰K˜ŒOØˆFØˆR‰�‰˜Öñ	 ð
 €Kr*   c           	     ó:  • / n[        U 5       Hª  n[        S U 5       5      S:  a  M  U Vs/ s H  o3S   PM	     snU Vs/ s H  o3S   PM	     sn-   nU Vs/ s H  o3S   PM	     snU Vs/ s H  o3S   PM	     sn-   nUR                  [        U5      [	        U5      [        U5      [	        U5      45        M¬     U(       d  / $ UR                  5         [        US   5      /nUSS  Hu  u  pxpšXvS   S   -
  [        ::  aJ  [	        US   S   U5      US   S'   [        US   S   U	5      US   S'   [	        US   S   U
5      US   S'   Mb  UR                  XxXš/5        Mw     U Vs/ s H  o»S   US   US   US   4PM     sn$ s  snf s  snf s  snf s  snf s  snf )aŸ  Vertical bands of contiguous *tabular* rows, each (top, bottom, x_left,
x_right) in points. A tabular row carries two-or-more value-like figures
(data rows of a real table); prose lines and lone labels are excluded.
Contiguous tabular rows (vertical gap <= BAND_GAP_PTS) merge into one
band. Used to relocate a detector box onto the text that is actually
there, instead of trusting a box that landed in whitespace.
c              3  ód   #   • U  H&  n[         R                  US    5      (       d  M"  Sv •  M(     g7f)rF   rC   Nrž   r   s     r+   r¡   Ú tabular_bands.<locals>.<genexpr>&  s#   é € Ð=š#�Q¤×!6Ñ!6°q¸±t×!<�q‰qš#ùs   ‚!0§	0rT   rC   rU   r   Nr   )r  r¥   r\   r]   r^   rE   r5   ÚBAND_GAP_PTS)rS   ÚspansrÛ   rY   Úysr¦   Úbandsrn   ÚbotÚxlÚxrrP   s               r+   Útabular_bandsr    sœ  € ð 68€EÜ˜ÖˆÜÑ=™#Ó=Ó=ÀÓAÙÙÓ šC�q�Œd™CÑ ±#Ó#6²#¨Q a¤D±#Ñ#6Ñ6ˆÙÓ šC�q�Œd™CÑ ±#Ó#6²#¨Q a¤D±#Ñ#6Ñ6ˆØ�‰”c˜"“gœs 2›w¬¨B«´°R³Ð9Ö:ñ  ö Øˆ	Ø	‡J�J„LÜ $ U¨1¡X£Ð/€EØ! ! "›IÑˆ�"Ø�r‘˜1‘Ñ¤Ó-Ü˜u R™y¨™|¨SÓ1ˆE�"‰I�a‰LÜ˜u R™y¨™|¨RÓ0ˆE�"‰I�a‰LÜ˜u R™y¨™|¨RÓ0ˆE�"‰I�a‹Là�L‰L˜# BÐ+Ö,ñ &ñ /4Ó4ªe¨ˆq‰T�1�Q‘4˜˜1™˜q ™tÓ$©eÑ4Ð4ùò !ùÒ#6ùÚ ùÒ#6ùò 5s   ­FÁF	ÁFÁ,FÅ&Fc           
     óÜ  • [        U S   U S   5      U-  n[        U S   U S   5      U-  nSnSnU H3  n	[        S[        XiS   5      [        XYS   5      -
  5      n
X¨:”  d  M1  X©pxM5     Ucb  XC-  nU V	s/ s H  n	[        U	S   U-
  XYS   -
  S5      U	4PM     nn	U VV	s/ s H  u  pÙXÛ::  d  M  XÙ4PM     nnn	U(       d  g[        US S9S   nUu  pïnnUU-  Xã-  UU-  Xó-  4$ s  sn	f s  sn	nf )a]  Snap a detector box (normalized 0..1, top-left) onto the tabular text
it actually covers. Returns the repaired normalized box, or None when no
tabular band overlaps the box or sits within `near_pad_frac` of the page
height of it. On None the caller keeps the detector's box unchanged, so
the detection is never dropped here.

Picks the band with the most vertical overlap; if the box overlaps none
(it landed just off the table), takes the nearest band within the pad.
The repaired box spans the band's real text extent, which fixes both a
too-narrow box (clipped row labels) and a vertically-offset box.
rC   rU   Nrl   r   c                ó   • U S   $ )Nr   r"   )Úgs    r+   rº   Úrepair_box.<locals>.<lambda>Y  s   €  q¨¢tr*   r½   )r]   r^   )Úbox_normr  r€   r`   Únear_pad_fracÚbtre   ÚbestÚbest_ovÚbandÚovÚpadÚnearr1   rn   r  r  r  s                     r+   Ú
repair_boxr  9  s  € ô$ 
ˆX�a‰[˜( 1™+Ó	&¨Ñ	/€BÜ	ˆX�a‰[˜( 1™+Ó	&¨Ñ	/€BØ8<€DØ€GÛˆÜ�”c˜" 1™gÓ&¬¨R°a±Ó)9Ñ9Ó:ˆØ�<Ø’Tñ ð �|ØÑ$ˆÙINÓOÊÀ”�T˜!‘W˜r‘\ 2¨Q©¡<°Ó5°tÓ<ÉˆÐOÙ-1Ô@ªT¡	 °S±Z“�“©TˆÑ@ÞØÜ�4™^Ñ,¨QÑ/ˆØÑ€Cˆb�"Ø�‰K˜™ r¨F¡{°C±LÐAÐAùò PùÛ@s   Á3$C#ÂC(Â-C(c                  ó6   • \ rS rSr            SS jrSrg)ÚCompletenessAuditori^  c             ƒ  ó   #   • g 7fr¯   r"   )ÚselfrL   rM   r�   r_   r‚   s         r+   ÚauditÚCompletenessAuditor.audit`  s
   é € ð "ùs   ‚r"   N©rL   r   rM   r   r�   ú+Optional[Tuple[float, float, float, float]]r_   ú5Sequence[Optional[Tuple[float, float, float, float]]]r‚   r   rz   r-   )r#   r$   r%   r&   r"  r)   r"   r*   r+   r  r  ^  sE   † ð"ð ð"ð ð	"ð
 Að"ð Pð"ð  ð"ð 
÷"r*   r  c                  ó:   • \ rS rSrSr            SS jrSrg)ÚTextLayerCompletenessik  a  Deterministic CompletenessAuditor backed by the PDF text layer.

Reads the page's words via PyMuPDF in a worker thread (blocking IO),
then compares value-like figures in the table's column region against
the assembled markdown. No model, no network, no image.
c          	   ƒ  ó$  #   • SS K n UR                  [        [        U5      U5      I S h  v•N u  pxn	[        UUU	UUUS9$  N! [         a@  n
[
        R                  " SU[        U5      R                  U
5        [        SSS9s S n
A
$ S n
A
ff = f7f)Nr   z]completeness: could not read text layer; defaulting to complete=true page={} source={} exc={}Tz'default complete: text layer unreadablerp   )r€   r`   rS   r�   r_   r‚   )
ÚasyncioÚ	to_threadrZ   r   Ú	Exceptionr   ÚerrorÚnamer-   r›   )r!  rL   rM   r�   r_   r‚   r*  r€   r`   rS   Úexcs              r+   r"  ÚTextLayerCompleteness.audits  s›   é € ó 	ð		hØ*1×*;Ñ*;¼JÌÈVËÐVZÓ*[×$[Ñ!ˆF˜Eô $ØØØØ#Ø-Ø1ñ
ð 	
ñ %\øÜó 	hÜ�LŠLØoØÜ�V“×!Ñ!Øô	ô '°Ð=fÑgÕgûð	hüsC   ‚Bˆ#A «A¬A ³BÁA Á
BÁ5BÂBÂBÂBÂBr"   Nr$  )r#   r$   r%   r&   r'   r"  r)   r"   r*   r+   r(  r(  k  sJ   † ñð
ð ð
ð ð	
ð
 Að
ð Pð
ð  ð
ð 
÷
r*   r(  c                  óH   • \ rS rSrSrSSS jjr            S	S jrSrg)
ÚMockCompletenessi’  z$Returns a canned verdict. For tests.Nc                ó4   • U=(       d
    [        SSS9U l        g )NTÚmockrp   )r-   Úverdict)r!  r5  s     r+   Ú__init__ÚMockCompleteness.__init__•  s   € Ø×SÔ"5¸tÈFÑ"Sˆ�r*   c             ƒ  ó,   #   • XX4U4nU R                   $ 7fr¯   ©r5  )r!  rL   rM   r�   r_   r‚   r°   s          r+   r"  ÚMockCompleteness.audit˜  s   é € ð ˜;Ð:LÐMˆØ�|‰|Ðùs   ‚r9  r¯   )r5  zOptional[CompletenessVerdict]rz   ÚNoner$  )r#   r$   r%   r&   r'   r6  r"  r)   r"   r*   r+   r2  r2  ’  sN   † Ù.öTð
ð ð
ð ð	
ð
 Að
ð Pð
ð  ð
ð 
÷
r*   r2  )r?   r4  c                ó°   • U =(       d    [        5       R                  R                  nUS:X  a
  [        5       $ US:X  a
  [	        5       $ [        SU< S35      e)Nr?   r4  z$Unknown QUBER_COMPLETENESS_BACKEND: z. Expected text|mock.)r   ÚllmÚcompleteness_backendr(  r2  Ú
ValueError)ÚbackendÚselecteds     r+   Úget_completenessrB  ¨  sR   € Ø×Aœ,›.×,Ñ,×AÑA€HØ�6ÓÜ$Ó&Ð&Ø�6ÓÜÓ!Ð!Ü
Ð;¸H¹<ÐG\Ð]Ó
^Ð^r*   )r9   r   rz   r   )r?   r   rz   ry   )rL   r   rM   r   rz   ry   )rL   r   rM   r   rz   zATuple[float, float, List[Tuple[float, float, float, float, str]]])r_   r&  r`   r   rz   r%  )r€   r   r`   r   rS   ú0Sequence[Tuple[float, float, float, float, str]]r�   r%  r_   r&  r‚   r   rz   r-   )rS   rC  rˆ   ú!Tuple[float, float, float, float]rz   zList[float])rS   rC  r   r   r´   r   rz   r   )
r‚   r   rS   rC  rˆ   rD  r4   úSequence[MissingFigure]rz   zTuple[str, List[MissingFigure]])rÎ   úSequence[Sequence[str]]rz   z	List[int])r?   r   rz   r   )
rÎ   rF  rS   rC  rˆ   rD  r4   rE  rz   z+Tuple[List[List[str]], List[MissingFigure]])rS   rC  r  r   rz   z2List[List[Tuple[float, float, float, float, str]]])rS   rC  rz   z'List[Tuple[float, float, float, float]])gš™™™™™©?)r  rD  r  z+Sequence[Tuple[float, float, float, float]]r€   r   r`   r   r  r   rz   r%  r¯   )r@  zOptional[CompletenessBackend]rz   r  )3r'   Ú
__future__r   r7   Úpathlibr   Útypingr   r   r   r   r	   r
   r   Úlogurur   Úpydanticr   r   Úquber.settingsr   ÚcompileÚVERBOSEr<   r~   r|   r¤   r³   r  r   r-   r:   rA   rQ   rZ   rj   r›   r©   rµ   rÒ   rß   râ   rý   r  r  r  r  r(  r2  ÚCompletenessBackendrB  r"   r*   r+   Ú<module>rP     s‹  ðñ%õN #ã 	Ý ß X× XÑ Xå ß %å 'ð —’ð	ð ‡J�Jó€ð. €ð €ð
 €Ø€ð €ô�Iô ô(˜)ô ô6*ôPô
ôð*9ØKð9àð9ð 1ô9ð4cØðcàðcð <ðcð =ð	cð
 Lðcð ðcð ôcðL/Ø;ð/à
+ð/ð ô/ð01Ø;ð1àð1ð ð1ð 	ô	1ð(/$Øð/$à;ð/$ð ,ð/$ð -ð	/$ð
 %ô/$ôdô5ðSØ"ðSà;ðSð ,ðSð -ð	Sð
 1ôSðp ðØ;ðà	ðð 8õð 5Ø;ð5à,ô5ðH  ð"BØ/ð"Bà6ð"Bð ð"Bð ð	"Bð
 ð"Bð 1õ"BðJ ô	"˜(ó 	"ó ð	"÷$
ñ $
÷Nñ ð& ˜nÑ-Ð ÷_r*   