Source code for recon_gen.common.handbook.l2ft_exceptions

"""L2FT Hygiene Exceptions parser — AA.C.4.

Reads ``src/recon_gen/docs/L2FT_Exceptions.md`` (the single
source of truth for the six L2FT runtime checks) and returns a typed
mapping of exception kind -> :class:`L2FTExceptionSection`.

Mirrors :mod:`common.handbook.invariants` (which serves the L1
invariants doc + L1 dashboard panels). Two parser deltas from the L1
flavor:

- L2FT exception headings are plain ``### N. <Title>`` — no
  ``{{ l2_instance_name }}_<kind>`` Jinja prefix, because the kinds
  are check_type labels (``"Chain Orphans"`` / ``"Unmatched Rail
  Name"`` / etc.) shipped in the L2FT dashboard's unified-exceptions
  ``check_type`` column. Kind keys are slug-cased titles
  (``"chain_orphans"`` / ``"unmatched_rail_name"`` / …).
- No SHOULD-blockquote convention. L2FT exceptions are runtime
  checks against the L2 declaration, not L1 SHOULD-constraints —
  the leading prose paragraph is the "what this surfaces"
  description, no blockquote indirection.

The doc carries ``**Columns:** ...`` + ``**What to do:** ...`` lines
in the same shape as L1_Invariants.md; the parser extracts them
identically.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from importlib import resources


# Pin: which parsed exception kind maps to which L2FT dashboard sheet.
# All six L2FT hygiene checks roll up onto the unified ``L2 Hygiene
# Exceptions`` sheet (M.3.10l) — there's no per-kind sheet split (the
# row count per kind would be too sparse). The mapping is kept here
# for symmetry with :data:`common.handbook.invariants.INVARIANT_KIND_TO_SHEET`
# and to let AA.C.6 browser tests assert sheet-side wiring without
# hard-coding the sheet name in two places.
L2FT_EXCEPTION_KIND_TO_SHEET: dict[str, str] = {
    "chain_orphans": "L2 Hygiene Exceptions",
    "unmatched_rail_name": "L2 Hygiene Exceptions",
    "dead_rails": "L2 Hygiene Exceptions",
    "dead_bundles_activity": "L2 Hygiene Exceptions",
    # NB: slug-from-title yields ``dead_metadata_declarations`` even
    # though the build_exc_dead_metadata function name is shorter.
    # The title (and the unified dataset's ``check_type`` literal) is
    # ``"Dead Metadata Declarations"`` per the dashboard wording — the
    # kind slug tracks the title since that's what the parser derives.
    "dead_metadata_declarations": "L2 Hygiene Exceptions",
    "dead_limit_schedules": "L2 Hygiene Exceptions",
}


[docs] @dataclass(frozen=True) class L2FTExceptionSection: """One parsed section from ``L2FT_Exceptions.md``. Same shape as :class:`common.handbook.invariants.InvariantSection` minus ``short_statement`` — L2FT exceptions don't use blockquote SHOULD-statements (they're runtime checks against L2, not L1 invariants). """ kind: str """Slug-cased section title — ``"chain_orphans"`` / ``"unmatched_rail_name"`` / etc. Joins to :data:`L2FT_EXCEPTION_KIND_TO_SHEET` and to the L2FT unified dataset's ``check_type`` literals (title-cased there: ``"Chain Orphans"``).""" title: str """Human heading — ``"Chain Orphans"`` / ``"Unmatched Rail Name"``. Matches the unified L2 Exceptions dataset's ``check_type`` literal exactly so a future renderer can cross-link a row to its panel description.""" body: str """Prose paragraphs after the heading. The ``**What to do:** ...`` line is *extracted* into :attr:`what_to_do` and dropped from ``body`` so the dashboard panel can render the remediation in its own styled block. The ``**Columns:** ...`` line stays inline.""" columns: tuple[str, ...] """Parsed column names from the ``**Columns:** ...`` line. Empty tuple when the section doesn't declare columns.""" what_to_do: str """Remediation paragraph parsed from the ``**What to do:** ...`` line. One-paragraph guidance: what does a row in this check mean for the integrator, and what should they do about it. Empty string when the section omits the line."""
_HEADING = re.compile(r"^###\s+(?P<n>\d+)\.\s+(?P<title>.+?)\s*$") """Matches ``### 1. Chain Orphans``. No Jinja prefix (the L1 flavor has ``\\`{{ l2_instance_name }}_<kind>\\``` — L2FT doesn't).""" _HEADING_ANY = re.compile(r"^(#{2,3})\s+.+$") """Any ``##`` / ``###`` heading — used as a section terminator.""" _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: """``"Chain Orphans"`` -> ``"chain_orphans"``. The kind is the snake-cased title, stable across L2 instances (the dashboard's ``check_type`` literal IS the title, so the slug derivation is one-step and round-trippable).""" out: list[str] = [] for ch in title: if ch.isalnum(): out.append(ch.lower()) elif out and out[-1] != "_": out.append("_") return "".join(out).strip("_") def _parse_columns(body: str) -> tuple[str, ...]: """Extract every backticked token from a ``**Columns:** ...`` line.""" 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 :func:`common.handbook.invariants._extract_what_to_do`. The doc wraps the paragraph across multiple physical lines; the dashboard panel wants 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_l2ft_exceptions(md_text: str) -> dict[str, L2FTExceptionSection]: """Walk the markdown source and yield one section per ``### N. <Title>`` heading. Returns ``{kind: L2FTExceptionSection}``.""" sections: dict[str, L2FTExceptionSection] = {} 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) # Consume body until the next heading (## or ###). 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] = L2FTExceptionSection( kind=kind, title=title, body=body, columns=columns, what_to_do=what_to_do, ) i = j return sections
[docs] def load_bundled_l2ft_exceptions() -> dict[str, L2FTExceptionSection]: """Read the bundled ``L2FT_Exceptions.md`` from ``recon_gen.docs`` and return parsed sections. Single call site for the dashboard-side consumer (AA.C.4 panel wiring) — it doesn't need to know where the doc lives. """ md_text = ( resources.files("recon_gen.docs") .joinpath("L2FT_Exceptions.md") .read_text(encoding="utf-8") ) return parse_l2ft_exceptions(md_text)
[docs] def panel_markdown(sections: dict[str, L2FTExceptionSection]) -> str: """Compose the L2 Hygiene Exceptions sheet's bottom panel. L2FT's six checks all roll up onto one sheet (the unified Hygiene Exceptions sheet), so unlike L1 — where each invariant kind has its own sheet + panel — L2FT gets one panel that lists all six kinds with their remediation guidance inline. Shape mirrors L1's L1 Exceptions intro panel (AA.C.3.e). Returns a markdown string suitable for ``rich_text.markdown(...)``. """ parts = [ "**L2 Hygiene Exceptions — what each check surfaces**", ( "Each row in the table above is a piece of L2 declaration " "that doesn't match the live runtime. None of these break " "the ledger; they break the L2-to-runtime correspondence " "the integrator's ETL is supposed to maintain. The six " "check kinds:" ), ] for section in sections.values(): line = f"- **{section.title}.** " if section.what_to_do: line += section.what_to_do else: # Soft contract: every L2FT exception section SHOULD carry # a what_to_do line. Tests pin this — the fallback exists # so a partial-edit doc doesn't blow up the dashboard. line += "(remediation guidance not yet authored)" parts.append(line) return "\n\n".join(parts)