File size: 4,444 Bytes
a3c590d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
from __future__ import annotations

from pathlib import Path

import pytest

from ragsolver.rag.parse_structure import pair_problems_and_solutions, parse_markdown

SAMPLE_MD = """\
<!-- page 1 of 3 -->
## Problemario

1. Enunciado del problema uno.

2. Enunciado del problema dos con una figura.

![Image block](doc:abc123/tier:standard/page:1/block:5)

<!-- page 2 of 3 -->
## Soluciones

1. Solución del problema uno.

2.

$$
x = 2
$$

<!-- page 3 of 3 -->
## Bibliografía

Referencia 1.
"""


def test_parse_markdown_splits_problems_and_solutions():
    parsed = parse_markdown(SAMPLE_MD)
    assert parsed["warnings"] == []
    assert [p.number for p in parsed["problems"]] == [1, 2]
    assert [s.number for s in parsed["solutions"]] == [1, 2]


def test_problem_page_and_image_locator_captured():
    parsed = parse_markdown(SAMPLE_MD)
    p2 = next(p for p in parsed["problems"] if p.number == 2)
    assert p2.page_start == 1
    assert p2.image_locators == ["doc:abc123/tier:standard/page:1/block:5"]


def test_solution_starting_as_bare_number_then_formula_is_captured():
    """Caso real encontrado en el PDF de ejemplo: 'N.' solo en su línea, la
    fórmula sigue después (docs/DECISIONS.md, solución 13)."""
    parsed = parse_markdown(SAMPLE_MD)
    s2 = next(s for s in parsed["solutions"] if s.number == 2)
    assert "x = 2" in s2.text_md


def test_pair_problems_and_solutions_matches_by_number():
    parsed = parse_markdown(SAMPLE_MD)
    cards, warnings = pair_problems_and_solutions(parsed)
    assert len(cards) == 2
    assert warnings == []
    assert cards[0]["problem"].number == 1
    assert cards[0]["solution"].number == 1


def test_embedded_subcase_numbering_does_not_hijack_an_earlier_item():
    """Regresión: una solución posterior puede traer sub-casos numerados en su
    propio texto ("1. Prolongar...", "2. Prolongar...") que un split por línea
    confundiría con el inicio de un ítem nuevo, pisando el ítem 1 real (pasó
    de verdad con la solución 1 del PDF de ejemplo -- ver docs/DECISIONS.md)."""
    md = """\
## Soluciones

Contenido de la solución 1 sin numeral propio.

2. Solución dos, con dos sub-casos:

1. Prolongar el segmento DC por el lado de C.

2. Prolongar el segmento DA por el lado de A.

3. Solución tres.
"""
    parsed = parse_markdown(md)
    numbers = [s.number for s in parsed["solutions"]]
    assert numbers == [1, 2, 3]
    sol1 = next(s for s in parsed["solutions"] if s.number == 1)
    assert sol1.text_md == "Contenido de la solución 1 sin numeral propio."  # sin numeral -> lo que precede al 2
    sol2 = next(s for s in parsed["solutions"] if s.number == 2)
    assert "Prolongar el segmento DC" in sol2.text_md
    assert "Prolongar el segmento DA" in sol2.text_md  # los sub-casos quedan DENTRO de la solución 2


def test_pair_reports_missing_solution_without_dropping_the_problem():
    parsed = parse_markdown(SAMPLE_MD.replace("2.\n\n$$\nx = 2\n$$\n\n", ""))
    cards, warnings = pair_problems_and_solutions(parsed)
    assert len(cards) == 2
    assert any("problema 2 sin solución" in w for w in warnings)
    assert cards[1]["problem"].number == 2
    assert cards[1]["solution"] is None


# --- Integración contra la salida real de MinerU sobre el PDF de ejemplo ---

REAL_MD_PATH = Path(__file__).resolve().parent.parent / "data/parsed/edd3436ca148/full.md"

pytestmark_real = pytest.mark.skipif(not REAL_MD_PATH.exists(), reason="requiere haber corrido mineru parse sobre el PDF de ejemplo")


@pytestmark_real
def test_real_document_pairs_at_least_90_percent_of_100_problems():
    md = REAL_MD_PATH.read_text(encoding="utf-8")
    parsed = parse_markdown(md)
    cards, warnings = pair_problems_and_solutions(parsed)

    numbers = {c["item_number"] for c in cards}
    assert numbers == set(range(1, 101)), "todos los problemas 1-100 deben aparecer como card, con o sin solución"

    paired = sum(1 for c in cards if c["problem"] is not None and c["solution"] is not None)
    # docs/DECISIONS.md: 92/100 con el parser determinista (sin LLM). Los 8 faltantes
    # son casos donde el número de ítem no aparece como texto plano al inicio de línea:
    # embebido dentro de una fórmula LaTeX (\tag{100.}, "2 7. A = B" dentro de un $$),
    # o -- la solución 1 -- directamente ausente en el documento original (la única
    # solución que no repite su número). No capturable sin una pasada con LLM.
    assert paired >= 90