File size: 5,359 Bytes
526950e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""Parse `## References` markdown tables on syntopicon entry files into
structured reference dicts, resolved against a (session_date, row_index)
lookup map produced by the lecture chunker.
"""
from __future__ import annotations
import logging
import re
from pathlib import Path


logger = logging.getLogger(__name__)

_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
_PROBLEM_ID_RE = re.compile(r"^P\d+$")


def parse_row_spec(rows_raw: str) -> list[int]:
    """Parse a Row(s) cell. Supports '159', '159, 232', '159-162'.

    Returns sorted unique list. Whitespace tolerated. Returns empty list on
    unparseable input.
    """
    s = rows_raw.strip()
    if not s:
        return []
    if "-" in s and "," not in s:
        # Range form
        try:
            lo, hi = s.split("-", 1)
            return list(range(int(lo.strip()), int(hi.strip()) + 1))
        except ValueError:
            return []
    if "," in s:
        try:
            return sorted({int(p.strip()) for p in s.split(",") if p.strip()})
        except ValueError:
            return []
    try:
        return [int(s)]
    except ValueError:
        return []


def parse_references(
    markdown_path: Path,
    row_lookup: dict[tuple[str, int], tuple[str, str]],
) -> list[dict]:
    """Parse the ## References table in a syntopicon markdown file.

    Returns a list of reference dicts. Each dict has session_date, speaker,
    row_index, how_used, notes, problem_id (if notes match /^P\\d+$/),
    lecture_turn_doc_id, and lecture_problem_doc_id.

    Unresolvable rows are logged at WARNING and skipped. Malformed rows and
    bad dates are also skipped, with logging. Files without ## References
    return [] silently.
    """
    text = markdown_path.read_text(encoding="utf-8")
    lines = text.splitlines()

    # Find ## References heading
    in_section = False
    in_table = False
    skipped_separator = False
    refs: list[dict] = []
    for line in lines:
        stripped = line.strip()
        if stripped.lower().startswith("## references"):
            in_section = True
            continue
        if not in_section:
            continue
        if stripped.startswith("## "):
            # Different section; stop
            break
        if not stripped:
            if in_table:
                # Blank line ends table
                break
            continue
        if not in_table:
            # Skip until table header
            if stripped.startswith("|"):
                in_table = True
            continue
        if not skipped_separator:
            # The line right after the header is the separator |---|---|
            if "---" in stripped:
                skipped_separator = True
                continue
            # No separator? still skip if it looks like one; otherwise treat as data
            if stripped.startswith("|"):
                skipped_separator = True
                # fall through to parse this row as data? No — skip header again.
                continue
        # Parse a data row
        cells = [c.strip() for c in stripped.strip("|").split("|")]
        if len(cells) != 5:
            logger.warning(
                "syntopicon_reference_parser: malformed row in %s (got %d cells, want 5): %r",
                markdown_path, len(cells), stripped,
            )
            continue
        session_date, speaker, rows_raw, how_used, notes = cells
        if not _DATE_RE.match(session_date):
            logger.warning(
                "syntopicon_reference_parser: bad date format %r in %s",
                session_date, markdown_path,
            )
            continue
        row_indices = parse_row_spec(rows_raw)
        problem_id = notes if _PROBLEM_ID_RE.match(notes.strip()) else None
        for row_index in row_indices:
            key = (session_date, row_index)
            if key not in row_lookup:
                logger.warning(
                    "syntopicon_reference_parser: unresolvable row %s in %s (row=%d)",
                    session_date, markdown_path, row_index,
                )
                continue
            turn_id, problem_did = row_lookup[key]
            refs.append({
                "session_date": session_date,
                "speaker": speaker,
                "row_index": row_index,
                "how_used": how_used,
                "notes": notes,
                "problem_id": problem_id,
                "lecture_turn_doc_id": turn_id,
                "lecture_problem_doc_id": problem_did,
            })
    if in_section and not refs:
        logger.warning(
            "syntopicon_reference_parser: %s has '## References' heading but no resolvable rows",
            markdown_path,
        )
    return refs


def parse_all_references(
    syntopicon_dir: Path,
    row_lookup: dict[tuple[str, int], tuple[str, str]],
) -> dict[str, list[dict]]:
    """Walk the syntopicon directory; for each *.md file (excluding _registry.json
    and other non-term files), parse references. Returns {term_slug: refs}.

    The term_slug is the markdown filename stem (e.g., 'demand' for 'demand.md').
    """
    out: dict[str, list[dict]] = {}
    for md_path in sorted(syntopicon_dir.glob("*.md")):
        if md_path.stem.startswith("_"):
            continue
        out[md_path.stem] = parse_references(md_path, row_lookup)
    return out