Source code for recon_gen.common.handbook.l2_triage_gaps

"""L2 Triage + L2 Coverage Gaps parser — BU.2a (Lock 10).

Reads ``src/recon_gen/docs/L2_Triage_Gaps.md`` (the single source of
truth for L2-to-runtime alignment gaps — both Triage and Coverage
families) and returns a typed mapping of section kind ->
:class:`L2TriageGapSection`.

Mirrors :mod:`common.handbook.l2ft_exceptions` (the L2FT hygiene
catalogue) and :mod:`common.handbook.invariants` (the L1 invariants
catalogue). Parser shape is identical to the L2FT flavor: ``### N.
<Title>`` headings, ``**Columns:** ...`` line, ``**What to do:** ...``
line — no SHOULD blockquote prefix (these are runtime checks against
the L2, not L1 SHOULD-constraints).

**Two families share this catalogue** per BU.0 round-4 Notes: L2
Triage gaps (the four :class:`common.l2.triage.GapKind` literals —
runtime has data the L2 doesn't account for) and L2 Coverage gaps
(``uncovered_rail`` / ``uncovered_template`` — L2 declares something
the runtime doesn't exercise). The family discriminator lives on
:class:`common.l2.plant_registry.PlantKindEntry.family`, not on the
section type. :data:`SECTION_TITLE_BY_KIND` pins the title-to-kind
mapping for both families — a doc reordering or heading rename
loud-fails at parser load rather than silently mis-attach.
"""

from __future__ import annotations

import re
from collections.abc import Mapping
from dataclasses import dataclass
from importlib import resources


# Pin: which section kind maps to which doc-section title. The doc
# is authored with operator-readable titles (``"Unmatched rail_name"``)
# while the typed kind keys are the GapKind literals
# (``"unmatched_rail"`` from :mod:`common.l2.triage`) plus the L2
# Coverage extension keys (``"uncovered_rail"`` / ``"uncovered_template"``).
# Keep these two vocabularies aligned here — a doc edit that renames a
# heading OR a new GapKind landing without a section will both surface
# as KeyError at parser load + at the anti-drift test in
# ``tests/unit/test_l2_triage_gaps_handbook.py``.
SECTION_TITLE_BY_KIND: Mapping[str, str] = {
    # -- L2 Triage (the four GapKind literals) ----
    "unmatched_rail": "Unmatched rail_name",
    "unmatched_template": "Unmatched template_name",
    "missing_limit_schedule": "Missing LimitSchedule",
    "missing_metadata_key": "Missing required metadata key",
    # -- L2 Coverage (declared but unexercised) ----
    "uncovered_rail": "Uncovered rail",
    "uncovered_template": "Uncovered template",
}


# Pin: per-kind editor CTA label rendered alongside the gap card.
# Moved off ``common.html._studio_routes._GAP_KIND_EDITOR_LABELS``
# into the typed catalogue so the editor copy + the section prose
# stay in one place. Triage card render reads this table; Coverage
# entries reuse the same editor surfaces (Rails / Templates).
EDITOR_LABEL_BY_KIND: Mapping[str, str] = {
    "unmatched_rail": "Open Rails editor",
    "unmatched_template": "Open Templates editor",
    "missing_limit_schedule": "Open Limits editor",
    "missing_metadata_key": "Open template editor",
    "uncovered_rail": "Open Rails editor",
    "uncovered_template": "Open Templates editor",
}


[docs] @dataclass(frozen=True) class L2TriageGapSection: """One parsed section from ``L2_Triage_Gaps.md``. Same shape as :class:`common.handbook.l2ft_exceptions.L2FTExceptionSection` plus a ``label`` field that's the canonical short operator-facing label (replaces the inline ``_GAP_KIND_LABELS`` dict in ``_studio_routes``). """ kind: str """Either one of the four :class:`common.l2.triage.GapKind` literals (L2 Triage family) — ``"unmatched_rail"`` / ``"unmatched_template"`` / ``"missing_limit_schedule"`` / ``"missing_metadata_key"`` — or one of the two L2 Coverage section kinds — ``"uncovered_rail"`` / ``"uncovered_template"``.""" title: str """Human heading — ``"Unmatched rail_name"`` / ``"Missing LimitSchedule"`` / etc. Matches the doc heading verbatim so a renderer can cross-link a gap card to its section.""" label: str """Short operator-facing label rendered in the triage accordion header. Replaces ``common.html._studio_routes._GAP_KIND_LABELS``. Same as ``title`` by default — split for future divergence.""" editor_label: str """CTA copy on the gap card's editor link (``"Open Rails editor"`` / etc.). Replaces ``_GAP_KIND_EDITOR_LABELS``.""" body: str """Prose paragraphs after the heading. The ``**What to do:** ...`` line is *extracted* into :attr:`what_to_do` and dropped from ``body`` (mirrors the L1 + L2FT parsers). The ``**Columns:** ...`` line stays inline.""" columns: tuple[str, ...] """Parsed column names from the ``**Columns:** ...`` line.""" what_to_do: str """Remediation paragraph parsed from the ``**What to do:** ...`` line. One-paragraph guidance — what does this gap mean for the integrator and what should they do about it."""
_HEADING = re.compile(r"^###\s+(?P<n>\d+)\.\s+(?P<title>.+?)\s*$") _HEADING_ANY = re.compile(r"^(#{2,3})\s+.+$") _COLUMNS_LINE = re.compile( r"^\*\*Columns:\*\*\s+(?P<rest>.+?)(?=^\s*$|\Z)", re.MULTILINE | re.DOTALL, ) _WHAT_TO_DO_LINE = re.compile( r"^\*\*What to do:\*\*\s+(?P<rest>.+?)(?=^\s*$|\Z)", re.MULTILINE | re.DOTALL, ) _COLUMN_TOKEN = re.compile(r"`([^`]+)`") def _title_to_kind(title: str) -> str: """Reverse-lookup the section kind for a doc heading title. Loud-fails when the doc title isn't in :data:`SECTION_TITLE_BY_KIND` — a doc edit that introduces a heading without a backing kind (or vice versa) is a bug the parser refuses to hide.""" for kind, kind_title in SECTION_TITLE_BY_KIND.items(): if kind_title == title: return kind raise KeyError( f"L2_Triage_Gaps.md heading {title!r} doesn't map to any " f"section kind in SECTION_TITLE_BY_KIND — either the doc renamed " f"a heading or a new GapKind / coverage kind was added without " f"updating common.handbook.l2_triage_gaps.SECTION_TITLE_BY_KIND." ) def _parse_columns(body: str) -> tuple[str, ...]: match = _COLUMNS_LINE.search(body) if not match: return () return tuple(_COLUMN_TOKEN.findall(match.group("rest"))) def _extract_what_to_do(body: str) -> tuple[str, str]: """Pull the ``**What to do:** ...`` paragraph out of ``body``. Returns ``(stripped_body, what_to_do)`` — same contract as the L1 + L2FT parsers. The doc wraps the paragraph across multiple physical lines; renderers want one continuous sentence so internal newlines collapse to single spaces.""" match = _WHAT_TO_DO_LINE.search(body) if not match: return body, "" raw = match.group("rest").strip() paragraph = " ".join(line.strip() for line in raw.splitlines() if line.strip()) span_start, span_end = match.span() stripped = (body[:span_start] + body[span_end:]).strip() while "\n\n\n" in stripped: stripped = stripped.replace("\n\n\n", "\n\n") return stripped, paragraph
[docs] def parse_l2_triage_gaps(md_text: str) -> dict[str, L2TriageGapSection]: """Walk the markdown source and yield one section per ``### N. <Title>`` heading. Returns ``{kind: L2TriageGapSection}`` keyed on the :class:`common.l2.triage.GapKind` literal.""" sections: dict[str, L2TriageGapSection] = {} lines = md_text.splitlines() i = 0 n = len(lines) while i < n: line = lines[i] match = _HEADING.match(line) if not match: i += 1 continue title = match.group("title").strip() kind = _title_to_kind(title) body_lines: list[str] = [] j = i + 1 while j < n: if _HEADING_ANY.match(lines[j]): break body_lines.append(lines[j]) j += 1 body = "\n".join(body_lines).strip("\n") columns = _parse_columns(body) body, what_to_do = _extract_what_to_do(body) sections[kind] = L2TriageGapSection( kind=kind, title=title, label=title, editor_label=EDITOR_LABEL_BY_KIND[kind], body=body, columns=columns, what_to_do=what_to_do, ) i = j return sections
[docs] def load_bundled_l2_triage_gaps() -> dict[str, L2TriageGapSection]: """Read the bundled ``L2_Triage_Gaps.md`` from ``recon_gen.docs`` and return parsed sections. Single call site for the triage page + the BU.2b registry adapter — neither needs to know where the doc lives.""" md_text = ( resources.files("recon_gen.docs") .joinpath("L2_Triage_Gaps.md") .read_text(encoding="utf-8") ) return parse_l2_triage_gaps(md_text)