
    ig3              	          d Z ddlZddlmc mZ ddlZddlmZ ddl	Z	ddl
mZmZmZ ddlmZmZmZmZ 	 	 d"dededed	efd
Z	 	 	 d#dededed	efdZded	efdZd Zd Zd Zd Zd Zd Zd Zd Zd Z d Z!d Z"d Z#d Z$d Z%d Z&d  Z'd! Z(y)$u   
Story 3.05 — Chunker Integration Tests
=======================================
17 tests covering Stories 3.01–3.04.

Run:
    cd /mnt/e/genesis-system
    python3 -m pytest tests/kb/test_m3_chunker_integration.py -v
    N)datetime)ChunkExtractedContentPlatformConfig)chunk_batch
chunk_textchunk_with_headings
tag_chunkstexturltitlereturnc           	      &    t        ||| g g g i       S )N)r   r   r   headingscode_blockstablesmetadata)r   )r   r   r   s      =/mnt/e/genesis-system/tests/kb/test_m3_chunker_integration.py_make_contentr      s&    
      
chunk_sizechunk_overlapnamec                 "    t        |dd| |      S )NHubSpotzhttps://knowledge.hubspot.com)r   display_namedocs_base_urlr   r   )r   )r   r   r   s      r   _make_configr   .   s     
 5# r   n_charsc                 8    d}| t        |      z  dz   }||z  d|  S )uG   Return prose with natural sentence endings of total length ≥ n_chars.z-This is a sample sentence with enough words.    N)len)r   sentencerepetitionss      r   _long_proser%   <   s,    >Hc(m+q0K{"HW--r   c                     d} | dd } t        | dd      }t        |      }d}||k(  }|st        j                  d|fd	||f      d
t	        j
                         v st        j                  t              rt        j                  t              nd
dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}|d   }|j                  } |       }d}||k7  }	|	st        j                  d|	fd||f      t        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }dd|iz  }
t        t        j                  |
            dx}x}x}x}	}y)z.A 100-character text produces exactly 1 chunk.zVHello world. This is a short piece of text.Hello world. This is a short piece of text.Nd        r   overlap   ==)z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)sr"   chunkspy0py1py3py6assert %(py8)spy8r    )!=)zD%(py5)s
{%(py5)s = %(py3)s
{%(py3)s = %(py1)s.strip
}()
} != %(py8)s)r2   r3   py5r6   zassert %(py10)spy10)r   r"   
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanationstrip)r   r/   @py_assert2@py_assert5@py_assert4@py_format7@py_format9@py_assert0@py_assert7@py_assert6@py_format11s              r   test_chunk_short_textrM   G   s'   <D:Ds;Fv;!;!;!33vv;!!9"9??"?"""""""""9"""?""""""""""""""r   c                     t        d      } t        | dd      }t        |      }d}||kD  }|st        j                  d|fd||f      dt        j                         v st        j                  t              rt        j                  t              ndd	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      d
z  }dd|iz  }t        t        j                  |            dx}x}}|D ]  }|j                  } |       }	|	st        j                  d      dz   dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |	      dz  }
t        t        j                  |
            dx}}	 y)z@A 5 000-char text with chunk_size=1500 produces multiple chunks.i  r(   r)   r*   r,   >z/%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} > %(py6)sr"   r/   r0   r5   r6   NzEmpty chunk foundzB
>assert %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.strip
}()
}cr1   py2py4)r%   r   r"   r;   r<   r=   r>   r?   r@   rA   rB   rC   _format_assertmsg)r   r/   rD   rE   rF   rG   rH   rR   @py_assert1@py_assert3@py_format5s              r   test_chunk_long_textrZ   P   s   tDs;Fv;;?;33vv; .ww-wy-y---------q---q---w---y------.r   c            	         t        d      D  cg c]  } d|  d
 }} dj                  |      }t        |dd      }|D ]  }|j                         }|D ]  }||v }|st	        j
                  d|fd||f      d	t        j                         v st	        j                  |      rt	        j                  |      nd	d
t        j                         v st	        j                  |      rt	        j                  |      nd
dz  }t	        j                  d| d      dz   d|iz  }	t        t	        j                  |	            d}  yc c} w )z0Chunks end at sentence boundaries, not mid-word.2   zSentence number z ends right here. i,  r*   in)z%(py0)s in %(py2)swordr   r1   rT   zWord 'z' not in original text
>assert %(py4)srU   N)rangejoinr   splitr;   r<   r=   r>   r?   r@   rV   rA   rB   )
i	sentencesr   r/   chunkchunk_wordsr`   rW   @py_format3rY   s
             r   test_chunk_sentence_boundaryrk   Z   s     CH)LQ#A3&78LIL88IDb9F G kkm 	GD4<FFF44FFFFFF4FFF4FFFFFF4FFF4FFFF6$/E!FFFFFFF	G	G Ms   Ec                     t        d      } t        | dd      }t        |      }d}||k\  }|st        j                  d|fd||f      dt        j                         v st        j                  t              rt        j                  t              ndd	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      d
z  }dd|iz  }t        t        j                  |            dx}x}}d}t        t        |      dz
        D ]F  }t        ||   j                               }	t        ||dz      j                               }
|	|
z  sDd} n |s{t        j                  d      dz   ddt        j                         v st        j                  |      rt        j                  |      ndiz  }t        t        j                  |            y)z*Consecutive chunks share overlapping text.    r)   r*   r!   >=)z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} >= %(py6)sr"   r/   r0   r5   r6   NFr,   Tz5No overlapping words found between consecutive chunks
>assert %(py0)sr1   found_overlap)r%   r   r"   r;   r<   r=   r>   r?   r@   rA   rB   rc   setre   rV   )r   r/   rD   rE   rF   rG   rH   rr   rf   words_awords_b@py_format1s               r   test_overlap_contentrw   h   sE   tDs;Fv;!;!;!33vv;!M3v;?# fQioo'(fQUm))+,W M QQQQQQQQQ=QQQ=QQQQQ=r   c            	      (   t        d      } d}t        | |d      }t        |      D ]h  \  }}t        |      }||k  }|sKt	        j
                  d|fd||f      dt        j                         v st	        j                  t              rt	        j                  t              nddt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      d	t        j                         v st	        j                  |      rt	        j                  |      nd	d
z  }t	        j                  d| dt        |       d|       dz   d|iz  }t        t	        j                  |            dx}}k y)z9No chunk (from chunk_text) exceeds chunk_size characters.ip     r'   r*   )<=)z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} <= %(py5)sr"   rR   r   )r1   r2   r3   r9   Chunk z is z chars, exceeds limit 
>assert %(py7)spy7N)r%   r   	enumerater"   r;   r<   r=   r>   r?   r@   rV   rA   rB   )	r   r   r/   idxrR   rD   rF   @py_format6@py_format8s	            r   test_chunk_size_respectedr   x   s_   tDJSAFF# 
Q1v 	
v# 	
 	
 	
v 	
 	
	6	
 	
   	
 	
 		  	
 	
	6	
 	
   	
 	
 		  	
 	
 		  	
 	
	6	
 	
  $ 	
 	
 		 $ 	
 	
  SEc!fX%;J<H	
 	
 	
 	
 	

r   c                  t   dt        d      z   dz   t        d      z   } t        |       }t        |d      }|D cg c]  }|j                   }}d |D        }t	        |      }|st        j                  d      d	z   d
t        j                         v st        j                  t              rt        j                  t              nd
t        j                  |      t        j                  |      dz  }t        t        j                  |            dx}}|D cg c]  }d|j                  v s| }}|s{t        j                  d      dz   ddt        j                         v st        j                  |      rt        j                  |      ndiz  }	t        t        j                  |	            yc c}w c c}w )z6Chunks under a heading have heading_context populated.z# Getting Started

  z

## Installation

rn   hubspotplatformc              3       K   | ]  }|  y wN ).0ctxs     r   	<genexpr>z1test_heading_context_preserved.<locals>.<genexpr>   s     'ss's   z%No heading_context found in any chunkz.
>assert %(py4)s
{%(py4)s = %(py0)s(%(py2)s)
}anyrS   NInstallationz6No chunks found with 'Installation' in heading_contextrq   r1   install_chunks)r%   r   r	   heading_contextr   r;   rV   r=   r>   r?   r@   rA   rB   )
r   contentr/   rR   contextsrW   rX   rY   r   rv   s
             r   test_heading_context_preservedr      s3    	 
d
	
#	$ d
	 	 D!G 9=F+12a!!2H2'h'P3''P'PP)PPPPPPP3PPP3PPP'PPP'PPPPPP!'OA>Q=N=N+NaONOSSSSSSSSS>SSS>SSSSS>	 3 Ps   F0F5,F5c            
         d} d| z   dz   }t        |      }t        |dd      }|D cg c]"  }d|j                  v sd|j                  v s!|$ }}|s{t        j                  d	      d
z   ddt        j                         v st        j                  |      rt        j                  |      ndiz  }t        t        j                  |            |D ]#  }|j                  }|j                  }	d}
 |	|
      }d}||k\  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |	      t        j                  |
      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}	x}
x}x}}& yc c}w )zCA code block smaller than chunk_size is kept together in one chunk.z-```python
def hello():
    return 'world'
```zSome prose before. z Some prose after.r   r(   )r   r   z	```pythonz```z(Code block not found intact in any chunkrq   r1   matchingr!   ro   )zf%(py8)s
{%(py8)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.text
}.count
}(%(py6)s)
} >= %(py11)sm)r1   rT   rU   r4   r6   py11zassert %(py13)spy13N)r   r	   r   r;   rV   r=   r>   r?   r@   rA   rB   countr<   )coder   r   r/   rR   r   rv   r   rW   rX   rE   rJ   @py_assert10@py_assert9@py_format12@py_format14s                   r   test_code_block_intactr      sR   =D 4'*>>DD!G 9NF!Oa[AFF%:uOHO?????????8???8????? (vv'v||'E'|E"'a'"a''''"a''''''q'''q'''v'''|'''E'''"'''a''''''''( Ps   G8G8G8c                     t        d      dz   } t        |       }t        |d      }t        |d      }|D cg c]  }|j                   }}|D cg c]  }|j                   }}||k(  }|st	        j
                  d|fd||f      dt        j                         v st	        j                  |      rt	        j                  |      nddt        j                         v st	        j                  |      rt	        j                  |      ndd	z  }t	        j                  d
      dz   d|iz  }	t        t	        j                  |	            d}yc c}w c c}w )z0Same content always produces the same chunk IDs.r   z

More content here.r   r   r-   )z%(py0)s == %(py2)sids_aids_bra   zchunk_ids are not deterministicrb   rU   N)r%   r   r	   chunk_idr;   r<   r=   r>   r?   r@   rV   rA   rB   )
r   r   chunks_achunks_brR   r   r   rW   rj   rY   s
             r   test_deterministic_idsr      s    t77DD!G"7Y?H"7Y?H!)*AQZZ*E*!)*AQZZ*E*E><<<5E<<<<<<5<<<5<<<<<<E<<<E<<<<<<<<<<< +*s   EEc                  J   t        d      } t        |       }t        |d      }t        |d      }|D ]  }|j                  }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }t        j                  d	|j                   d
      dz   d|iz  }	t        t        j                  |	            dx}x}} y)z<All chunks returned by tag_chunks have the correct platform.r   rawr   ghlr-   )z0%(py2)s
{%(py2)s = %(py0)s.platform
} == %(py5)srR   r1   rT   r9   zExpected platform='ghl', got ''r|   r}   N)r%   r   r	   r
   r   r;   r<   r=   r>   r?   r@   rV   rA   rB   
r   r   r/   taggedrR   rW   rF   rX   r   r   s
             r   test_platform_tagr      s    tDD!G 59F/F SzzRURzU"RRRzURRRRRRqRRRqRRRzRRRURRR&DQZZLPQ$RRRRRRRRSr   c                  Z   t        d      } t        |       }t        |d      }d}t        |d|      }|D ]  }|j                  }||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      d	t        j                         v st        j                  |      rt        j                  |      nd	d
z  }dd|iz  }	t        t        j                  |	            dx}} y)z/customer_id is set on all chunks when provided.  r   r   zcustomer-abc-123r   customer_idr-   )z3%(py2)s
{%(py2)s = %(py0)s.customer_id
} == %(py4)srR   cidrS   zassert %(py6)sr4   Nr%   r   r	   r
   r   r;   r<   r=   r>   r?   r@   rA   rB   )
r   r   r/   r   r   rR   rW   rX   rY   rG   s
             r   test_customer_id_isolationr      s    tDD!G 9=F
CDF $}}#}####}######q###q###}################$r   c                     t        d      } t        |       }t        |d      }t        |dd      }|D ]  }|j                  }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      d	z  }d
d|iz  }	t        t        j                  |	            dx}x}} y)z9customer_id=None represents shared/global knowledge base.r   r   r   Nr   )is)z3%(py2)s
{%(py2)s = %(py0)s.customer_id
} is %(py5)srR   r   zassert %(py7)sr}   r   r   s
             r   test_global_kbr      s    tDD!G 9=FEF %}}$$}$$$$}$$$$$$q$$$q$$$}$$$$$$$$$$%r   c                  z   t        d      } t        |       }t        |d      }t        |d      }|D ]  }d}|j                  }||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }t        j                  d	      d
z   d|iz  }	t        t        j                  |	            dx}x}}|j                  d   }
t        j                  |
d        y)zEingested_at is present in metadata and is a valid ISO 8601 timestamp.r(   r   r   ingested_atr^   )z0%(py1)s in %(py5)s
{%(py5)s = %(py3)s.metadata
}rR   )r2   r3   r9   z!ingested_at missing from metadatar|   r}   Nz%Y-%m-%dT%H:%M:%SZ)r%   r   r	   r
   r   r;   r<   r@   r=   r>   r?   rV   rA   rB   r   strptime)r   r   r/   r   rR   rI   rF   rD   r   r   tss              r   test_metadata_timestampr      s    tDD!G 9=F3F 4O

O}
*OOO}
OOO}OOOOOOOOOOOO
OOO,OOOOOOOOZZ&"23	4r   c                     t        d      D  cg c]  } t        t        d      d|         }} t               }t	        ||      }t        |      }d}||kD  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndd	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      d
z  }t        j                  d      dz   d|iz  }t        t        j                  |            dx}x}}yc c} w )zEchunk_batch with 3 non-empty contents returns N total chunks (N > 0).   r   https://example.com/pager   r   rO   rQ   r"   r/   r0   z+Expected at least one chunk from 3 contents
>assert %(py8)sr6   N)rc   r   r%   r   r   r"   r;   r<   r=   r>   r?   r@   rV   rA   rB   )	rf   contentsconfigr/   rD   rE   rF   rG   rH   s	            r   test_batch_3_contentsr      s     q 	k$'/Gs-KLH  ^F6*Fv;II;?III;IIIIII3III3IIIIIIvIIIvIII;IIIIIIIIIIIIIIs    E.c                     t        t        d      d      } t        dd      }t        t        d      d      }t               }t        | ||g|      }|D ch c]  }|j                   }}d}||v}|st        j                  d|fd	||f      t        j                  |      d
t        j                         v st        j                  |      rt        j                  |      nd
dz  }	t        j                  d      dz   d|	iz  }
t        t        j                  |
            dx}}d}||v }|st        j                  d|fd||f      t        j                  |      d
t        j                         v st        j                  |      rt        j                  |      nd
dz  }	dd|	iz  }
t        t        j                  |
            dx}}d}||v }|st        j                  d|fd||f      t        j                  |      d
t        j                         v st        j                  |      rt        j                  |      nd
dz  }	dd|	iz  }
t        t        j                  |
            dx}}yc c}w )zEchunk_batch with 2 good + 1 empty content returns chunks from 2 only.r   zhttps://example.com/g1r   r7   zhttps://example.com/emptyr(   zhttps://example.com/g2)not in)z%(py1)s not in %(py3)ssource_urls)r2   r3   z+Empty content should not produce any chunksz
>assert %(py5)sr9   Nr^   )z%(py1)s in %(py3)szassert %(py5)s)r   r%   r   r   
source_urlr;   r<   r@   r=   r>   r?   rV   rA   rB   )good1emptygood2r   r/   rR   r   rI   rD   @py_format4r   s              r   test_batch_with_emptyr      s   +d+1IJE""=>E+d+1IJE^F%.7F)/0A1<<0K0& &k9  &k    '      /:    /:    	6     $2#{2222#{222#222222{222{2222222#2#{2222#{222#222222{222{2222222 1s   I2c                  \   dt        d      z   dz   t        d      z   } t        |       }t        |ddd      }t        |      D ]  \  }}|j                  }|j
                  } |       }|st        j                  d	| d
      dz   dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      dz  }t        t        j                  |            dx}x}} y)z(Every chunk produced has non-empty text.z# Section One

rm   z

## Section Two

r   r   ry   r'   r   r   r+   r{   z has empty textz\
>assert %(py6)s
{%(py6)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.text
}.strip
}()
}rR   r1   rT   rU   r4   N)r%   r   r	   r~   r   rC   r;   rV   r=   r>   r?   r@   rA   rB   )	r   r   r/   r   rR   rW   rX   rE   rG   s	            r   test_no_empty_chunksr     s     	
d
	
"	# d
	 	 D!G 9VYZFF# =Qvv<v||<|~<~<<uO<<<<<<<q<<<q<<<v<<<|<<<~<<<<<<=r   c                     t        d      } t        |       }t        |ddd      }dj                  d |D              }t	        | j                               }t	        |j                               }||z
  }t        |      }t        |      }d}	t        ||	      }
||
z  }d	}||k  }|s't        j                  d
|fd||f      dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |	      t        j                  |
      t        j                  |      d
z  }t        j                  t        |       d      dz   d|iz  }t        t        j                   |            dx}x}x}	x}
x}x}}y)u  
    Reassembled text from chunks approximates the original.

    Because of overlap, the original text is contained *within* the
    concatenation of all chunk texts — we verify that all non-overlapping
    words from the original appear in at least one chunk.
    r   r   ry   r'   r   r]   c              3   4   K   | ]  }|j                     y wr   )r   )r   rR   s     r   r   z(test_chunk_reassembly.<locals>.<genexpr>  s     /1/s   r,   g?)<)z(%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} / %(py12)s
{%(py12)s = %(py4)s(%(py8)s
{%(py8)s = %(py5)s(%(py6)s)
}, %(py10)s)
}) < %(py16)sr"   missingmaxoriginal_words)
r1   r2   r3   rU   r9   r4   r6   r:   py12py16z2 words from original not found in reassembled textz
>assert %(py18)spy18N)r%   r   r	   rd   rs   re   r"   r   r;   r<   r=   r>   r?   r@   rV   rA   rB   )originalr   r/   combinedr   combined_wordsr   rD   rJ   r   @py_assert11@py_assert13@py_assert15@py_assert14@py_format17@py_format19s                   r   test_chunk_reassemblyr     st    4 HH%G 9VYZFxx///H)*N)*N~-Gw< c.1 1 #115 <55  5<   5                                        "    "      #1    #1    2    45    6    9=    w<.JK      r   c            	      	   dt        d      z   dz   t        d      z   } t        |       }t        |d      }t        |d      }t        |      }t        |      }||k(  }|st	        j
                  d|fd||f      d	t        j                         v st	        j                  t              rt	        j                  t              nd	d
t        j                         v st	        j                  |      rt	        j                  |      nd
t	        j                  |      d	t        j                         v st	        j                  t              rt	        j                  t              nd	dt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      dz  }t	        j                  d      dz   d|iz  }t        t	        j                  |            dx}x}}t        ||      D ]t  \  }	}
|	j                  }|
j                  }||k(  }|st	        j
                  d|fd||f      dt        j                         v st	        j                  |	      rt	        j                  |	      ndt	        j                  |      dt        j                         v st	        j                  |
      rt	        j                  |
      ndt	        j                  |      dz  }t	        j                  d|	j                   d|
j                         dz   d|iz  }t        t	        j                  |            dx}x}}|	j                  }|
j                  }||k(  }|st	        j
                  d|fd||f      dt        j                         v st	        j                  |	      rt	        j                  |	      ndt	        j                  |      dt        j                         v st	        j                  |
      rt	        j                  |
      ndt	        j                  |      dz  }t	        j                  d      dz   d|iz  }t        t	        j                  |            dx}x}}w y)z>Same input always produces the same chunks with identical IDs.z# Heading One

r   z

## Heading Two

r(   r   r   r-   )zN%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py8)s
{%(py8)s = %(py5)s(%(py6)s)
}r"   run1run2)r1   r2   r3   r9   r4   r6   z&Different number of chunks across runsz
>assert %(py10)sr:   N)zN%(py2)s
{%(py2)s = %(py0)s.chunk_id
} == %(py6)s
{%(py6)s = %(py4)s.chunk_id
}abr   zchunk_id mismatch: z != r   r6   )zF%(py2)s
{%(py2)s = %(py0)s.text
} == %(py6)s
{%(py6)s = %(py4)s.text
}zchunk text differs across runs)r%   r   r	   r"   r;   r<   r=   r>   r?   r@   rV   rA   rB   zipr   r   )r   r   r   r   rD   rJ   rF   rH   rL   r   r   rW   rE   rX   rG   s                  r   test_idempotent_chunkingr   (  s   T!225MMP[\`PaaDD!Gw;Dw;Dt9KD	K9	!KKK9	KKKKKK3KKK3KKKKKKtKKKtKKK9KKKKKKKKKKKKKKKDKKKDKKK	KKK#KKKKKKKKD$ B1zz[QZZ[zZ'[[[zZ[[[[[[q[[[q[[[z[[[[[[Q[[[Q[[[Z[[[+>qzzl$qzzl)[[[[[[[[vvAAvAAAvAAAAAAqAAAqAAAvAAAAAAAAAAAAAAA!AAAAAAAABr   )r   z	Test Page)r(   r)   r   ))__doc__builtinsr=   _pytest.assertion.rewrite	assertionrewriter;   rer   pytestcore.kb.contractsr   r   r   core.kb.chunkerr   r   r	   r
   strr   intr   r%   rM   rZ   rk   rw   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   <module>r      s     	   E E  *
	  	"   	. . .#.GR 
T$(=S$%
4"J3&=,Br   