Source code for recon_gen.apps.l2_flow_tracing.app

"""L2 Flow Tracing — exercise every L2 primitive on a runtime dashboard.

M.3.4 ships the skeleton: 4 sheets (Getting Started + Rails + Chains +
L2 Exceptions), description-driven prose on Getting Started, placeholder
prose on the other three. M.3.5+ populates each tab with its real
visuals + datasets.

The app is L2-instance-fed via the same M.2d.3 prefix pattern the L1
dashboard uses: ``cfg.db_table_prefix`` is set on the cfg yaml directly
(no auto-derivation needed), so dashboard ID, analysis ID, dataset IDs,
and tag-based cleanup all key off the per-deploy prefix.

Build pipeline::

    build_l2_flow_tracing_app(cfg, *, l2_instance=None) -> App

Default L2 instance is the persona-neutral ``spec_example.yaml``
(M.3.2 repointed away from sasquatch_ar so production library code
carries no implicit Sasquatch flavor); callers MAY override
(tests, alternative-persona deployments) via the kwarg.

Substep landmarks (each tab gets its own substep):

- M.3.4 — package skeleton + Analysis + Dashboard + 4 placeholder sheets (this commit)
- M.3.5 — Rails tab — per-Rail row table with declared + runtime columns
- M.3.6 — Chains tab — Sankey + parent-firing-count edges
- M.3.7 — L2 Exceptions tab — 6 KPI + drill sections
- M.3.8 — Auto metadata-driven filter dropdowns
"""

from __future__ import annotations

from typing import Literal

from recon_gen.apps.l2_flow_tracing.datasets import (
    DS_CHAIN_INSTANCES,
    DS_META_VALUES,
    DS_POSTINGS,
    DS_TT_INSTANCES,
    DS_TT_LEGS,
    DS_UNIFIED_L2_EXCEPTIONS,
    L2FT_ALL_SENTINEL,
    META_KEY_ALL_SENTINEL,
    META_VALUE_PLACEHOLDER_SENTINEL,
    P_L2FT_CHAINS_DATE_END as _P_L2FT_CHAINS_DATE_END,
    P_L2FT_CHAINS_DATE_START as _P_L2FT_CHAINS_DATE_START,
    P_L2FT_RAILS_DATE_END as _P_L2FT_RAILS_DATE_END,
    P_L2FT_RAILS_DATE_START as _P_L2FT_RAILS_DATE_START,
    P_L2FT_TT_DATE_END as _P_L2FT_TT_DATE_END,
    P_L2FT_TT_DATE_START as _P_L2FT_TT_DATE_START,
    build_all_l2_flow_tracing_datasets,
    bundle_status_values,
    chain_completion_status_values,
    declared_chain_parents,
    declared_metadata_keys,
    declared_rail_names,
    declared_template_names,
    transaction_status_values,
    tt_completion_status_values,
)
from recon_gen.common import rich_text as rt
from recon_gen.common.config import Config
from recon_gen.common.dataset_contract import ColumnShape
from recon_gen.common.handbook.l2ft_exceptions import (
    load_bundled_l2ft_exceptions,
    panel_markdown as l2ft_panel_markdown,
)
from recon_gen.common.ids import ParameterName, SheetId
from recon_gen.common.l2 import L2Instance
from recon_gen.common.models import DateTimeDefaultValues
from recon_gen.common.sheets.app_info import (
    APP_INFO_SHEET_DESCRIPTION,
    APP_INFO_SHEET_NAME,
    APP_INFO_SHEET_TITLE,
    app_info_liveness_id,
    app_info_matviews_id,
    populate_app_info_sheet,
)

# BO.5 — per-app App Info dataset identifiers; see l1_dashboard/app.py.
_DS_APP_INFO_LIVENESS = app_info_liveness_id("l2ft")
_DS_APP_INFO_MATVIEWS = app_info_matviews_id("l2ft")
from recon_gen.common.l2 import ThemePreset
from recon_gen.common.theme import resolve_l2_theme
from recon_gen.common.tree.actions import DrillWrite
from recon_gen.common.tree import (
    Analysis,
    App,
    Dataset,
    DateTimeParam,
    Drill,
    DrillParam,
    Sheet,
    StaticValues,
    StringParam,
    TextBox,
)


# Sheet IDs — inlined per the greenfield-app convention (no constants.py
# until / unless URL stability forces it).
SHEET_GETTING_STARTED = SheetId("l2ft-sheet-getting-started")
SHEET_RAILS = SheetId("l2ft-sheet-rails")
SHEET_CHAINS = SheetId("l2ft-sheet-chains")
SHEET_TRANSFER_TEMPLATES = SheetId("l2ft-sheet-transfer-templates")
SHEET_L2_EXCEPTIONS = SheetId("l2ft-sheet-l2-exceptions")
SHEET_APP_INFO = SheetId("l2ft-sheet-app-info")  # M.4.4.5


# M.3.10m + BS.3 follow-up (2026-05-30) — drill from the L2 Exceptions
# table to the per-rail or per-chain explorer. The drill writes the
# DESTINATION SHEET'S OWN PICKER PARAMETER (``pL2ftRail`` /
# ``pL2ftChainsChain``) directly, so the URL-pushed value flows through
# the existing dataset-param-pushdown SQL path that both QS + App2
# share. Pre-fix this used dedicated ``pL2ftRailDrill`` /
# ``pL2ftChainDrill`` parameters bridged into a CalcField +
# FilterGroup, which worked on QS but didn't narrow the SQL on App2
# (the drill param never reached a ``<<$pL2ftRailDrill>>`` placeholder).
# Writing the picker param directly = one mechanism, both renderers.
_DP_RAIL_DRILL = DrillParam(
    ParameterName("pL2ftRail"), ColumnShape.L2_DECLARED_NAME,
)
_DP_CHAIN_DRILL = DrillParam(
    ParameterName("pL2ftChainsChain"), ColumnShape.L2_DECLARED_NAME,
)


_GETTING_STARTED_NAME = "Getting Started"
_GETTING_STARTED_TITLE = "L2 Flow Tracing"
_GETTING_STARTED_DESCRIPTION = (
    "What this dashboard is. The L1 dashboard answers 'are my postings "
    "internally consistent?' One step up: the L2 Flow Tracing dashboard "
    "answers 'is my L2 declaration alive?' — every Rail, every Chain, "
    "every TransferTemplate, every LimitSchedule the L2 instance "
    "declares should produce activity in the runtime data. When it "
    "doesn't, that's an L2 hygiene problem, not an L1 ledger problem."
)


_RAILS_NAME = "Rails"
_RAILS_TITLE = "Rails — Transactions Explorer"
_RAILS_DESCRIPTION = (
    "Use this sheet to look up an individual transfer leg by ID or to "
    "inspect the journal rows on a specific rail / metadata slice. "
    "Filter the postings ledger by date range, rail, status, bundle "
    "status, and (cascading) metadata key + value. Pick a Metadata Key "
    "to populate the Value dropdown; pick one or more Values to narrow "
    "the table to legs carrying that metadata. The KPI row above the "
    "table orients you: how many legs are in the current window and "
    "how big the largest one is."
)
# Visual title for the legs table inside the Rails sheet. Exported so
# tests can import it instead of inlining the literal — see BE.2
# suppressions in test_l2ft_{metadata_cascade,additive_pickers,
# rails_dropdowns}.py before the 2026-05-27 sweep.
_RAILS_TRANSACTIONS_TITLE = "Transactions"


_CHAINS_NAME = "Chains"
_CHAINS_TITLE = "Chains — Per-Instance Explorer"
_CHAINS_DESCRIPTION = (
    "Filter declared chain firings by date range, chain (parent rail / "
    "template name), completion status, and (cascading) metadata key + "
    "value. One row per parent transfer firing; completion_status reads "
    "'Completed' when every Required child declared for the parent fired "
    "against this transfer_id, 'Incomplete' if any required child is "
    "missing, 'No Required Children' when only optional / XOR-group "
    "children are declared."
)


_TRANSFER_TEMPLATES_NAME = "Transfer Templates"
_TRANSFER_TEMPLATES_TITLE = "Transfer Templates — Multi-Leg Flow"
_TRANSFER_TEMPLATES_DESCRIPTION = (
    "Visualize the multi-leg flow of declared TransferTemplates: each "
    "shared Transfer's debit legs flow into the template (middle node), "
    "credit legs flow out to their destination accounts. Filter by date, "
    "template, net status (Balanced / Imbalanced — checks the "
    "ExpectedNet invariant), and (cascading) metadata key + value. The "
    "Sankey shows the flow shape; the Table below shows per-instance "
    "balance detail."
)


_L2_EXCEPTIONS_NAME = "L2 Exceptions"
_L2_EXCEPTIONS_TITLE = "L2 Hygiene Exceptions"
_L2_EXCEPTIONS_DESCRIPTION = (
    "All six L2 hygiene checks unified into one row-per-violation "
    "view. KPI = total open violations; bar chart breaks down by "
    "check_type so you see which check kind dominates today; the "
    "detail table sorts by count (descending) so the worst "
    "offenders surface first. Each check_type captures a "
    "'declaration vs runtime' mismatch the L1 dashboard doesn't "
    "catch — Chain Orphans, Unmatched Transfer Type, Dead Rails, "
    "Dead Bundles Activity, Dead Metadata Declarations, Dead Limit "
    "Schedules."
)


def _analysis_name(cfg: Config, l2_instance: L2Instance) -> str:
    """Title shown in QuickSight — matches L1's `Name (prefix)` shape so
    the two apps' QS asset names are visually consistent in the
    dashboard list."""
    return f"L2 Flow Tracing ({cfg.deployment_name})"


[docs] def build_l2_flow_tracing_app( cfg: Config, *, l2_instance: L2Instance | None = None, ) -> App: """Construct the L2 Flow Tracing App as a tree. M.3.4: registers Analysis + Dashboard + 4 placeholder sheets (Getting Started + Rails + Chains + L2 Exceptions). No datasets, no visuals beyond the description prose. M.3.5+ populates each placeholder one substep at a time. Dashboard ID convention: ``<deployment_name>-l2-flow-tracing`` — the per-deploy prefix the L1 dashboard also uses, so N apps can deploy against the same L2 instance AND the same app can deploy against N L2 instances without QS resource collisions. ``cfg.deployment_name`` is set on the cfg yaml directly, no auto-derivation from the L2 yaml. """ if l2_instance is None: from recon_gen.common.l2 import default_l2_instance l2_instance = default_l2_instance() # N.1.f / N.4.k — resolve theme once from the L2 instance, coerced # to the registry default for in-canvas accent colors when the # instance declares no inline ``theme:`` block. The CLI uses the # un-coerced ``resolve_l2_theme`` return to decide whether to # deploy a custom Theme resource (silent-fallback to AWS CLASSIC). from recon_gen.common.theme import DEFAULT_PRESET theme = resolve_l2_theme(l2_instance) or DEFAULT_PRESET app = App(name="l2-flow-tracing", cfg=cfg) analysis = app.set_analysis(Analysis( analysis_id_suffix="l2-flow-tracing-analysis", name=_analysis_name(cfg, l2_instance), )) # Tree Dataset refs keyed by visual_identifier — populators pull # by stable name. The CLI writes the AWS-shape DataSets separately # (this is the L1 dashboard's split-of-concern pattern). datasets = _l2ft_datasets(cfg, l2_instance) for ds in datasets.values(): app.add_dataset(ds) getting_started = analysis.add_sheet(Sheet( sheet_id=SHEET_GETTING_STARTED, name=_GETTING_STARTED_NAME, title=_GETTING_STARTED_TITLE, description=_GETTING_STARTED_DESCRIPTION, )) rails_sheet = analysis.add_sheet(Sheet( sheet_id=SHEET_RAILS, name=_RAILS_NAME, title=_RAILS_TITLE, description=_RAILS_DESCRIPTION, )) chains_sheet = analysis.add_sheet(Sheet( sheet_id=SHEET_CHAINS, name=_CHAINS_NAME, title=_CHAINS_TITLE, description=_CHAINS_DESCRIPTION, )) transfer_templates_sheet = analysis.add_sheet(Sheet( sheet_id=SHEET_TRANSFER_TEMPLATES, name=_TRANSFER_TEMPLATES_NAME, title=_TRANSFER_TEMPLATES_TITLE, description=_TRANSFER_TEMPLATES_DESCRIPTION, )) l2_exceptions_sheet = analysis.add_sheet(Sheet( sheet_id=SHEET_L2_EXCEPTIONS, name=_L2_EXCEPTIONS_NAME, title=_L2_EXCEPTIONS_TITLE, description=_L2_EXCEPTIONS_DESCRIPTION, )) _populate_getting_started( cfg, getting_started, l2_instance, theme=theme, ) _populate_rails_sheet( cfg, rails_sheet, analysis=analysis, datasets=datasets, l2_instance=l2_instance, ) _populate_chains_sheet( cfg, chains_sheet, analysis=analysis, datasets=datasets, l2_instance=l2_instance, ) _populate_transfer_templates_sheet( cfg, transfer_templates_sheet, analysis=analysis, datasets=datasets, l2_instance=l2_instance, theme=theme, ) # M.3.10m + BS.3 follow-up (2026-05-30) — L2 Exceptions drills now # write the destination sheet's own picker parameter directly # (pL2ftRail / pL2ftChainsChain). The pre-fix sentinel-pattern # CalcField + FilterGroup wiring (was _wire_l2ft_drill_filter_groups) # is gone — picker params are declared by _populate_rails_sheet / # _populate_chains_sheet, which already run above. _populate_l2_exceptions_sheet( cfg, l2_exceptions_sheet, datasets=datasets, rails_sheet=rails_sheet, chains_sheet=chains_sheet, theme=theme, ) # M.4.4.5 — App Info ("i") sheet, ALWAYS LAST. Diagnostic canary; # see common/sheets/app_info.py. Datasets registered via # `_l2ft_datasets` above (single source of truth across the # tree-ref + JSON-write flows). app_info_sheet = analysis.add_sheet(Sheet( sheet_id=SHEET_APP_INFO, name=APP_INFO_SHEET_NAME, title=APP_INFO_SHEET_TITLE, description=APP_INFO_SHEET_DESCRIPTION, )) populate_app_info_sheet( cfg, app_info_sheet, liveness_ds=datasets[_DS_APP_INFO_LIVENESS], matview_status_ds=datasets[_DS_APP_INFO_MATVIEWS], theme=theme, ) app.create_dashboard( dashboard_id_suffix="l2-flow-tracing", name=_analysis_name(cfg, l2_instance), ) return app
def _l2ft_datasets( cfg: Config, l2_instance: L2Instance, ) -> dict[str, Dataset]: """Build every L2 Flow Tracing dataset and return tree-ref Datasets keyed by visual_identifier. Each AWS DataSet's ``DataSetId`` becomes the tree Dataset's ARN path component; the visual identifier (the key passed to `build_dataset()`) becomes the tree Dataset's ``identifier`` field. The contract is registered as a side-effect of `build_dataset()`, so subsequent ``ds["col"]`` accesses validate. Mirrors `apps/l1_dashboard/app.py::_l1_datasets` pattern — the CLI writes the AWS shapes; this builds the typed tree refs for visual wiring on the App. """ aws_datasets = build_all_l2_flow_tracing_datasets(cfg, l2_instance) # Order matches `build_all_l2_flow_tracing_datasets`. M.3.10c # dropped DS_RAILS + the 28 per-key dropdowns; replaced with # DS_POSTINGS + DS_META_VALUES driving the cascade. M.3.10d # swapped DS_CHAINS (aggregated edges) for DS_CHAIN_INSTANCES. # M.3.10f added DS_TT_INSTANCES + DS_TT_LEGS for the Transfer # Templates sheet. M.3.10l replaced the 6 separate L2 exception # datasets with one DS_UNIFIED_L2_EXCEPTIONS (mirrors L1's # exceptions pattern). visual_ids = [ DS_POSTINGS, DS_META_VALUES, DS_CHAIN_INSTANCES, DS_TT_INSTANCES, DS_TT_LEGS, DS_UNIFIED_L2_EXCEPTIONS, _DS_APP_INFO_LIVENESS, _DS_APP_INFO_MATVIEWS, # M.4.4.5; BO.5 per-app ] return { vid: Dataset(identifier=vid, arn=cfg.dataset_arn(aws.DataSetId)) for vid, aws in zip(visual_ids, aws_datasets) } def _populate_getting_started( cfg: Config, sheet: Sheet, l2_instance: L2Instance, *, theme: ThemePreset, ) -> None: """Render the Getting Started sheet using the L2 instance's prose. Description-driven: welcome body comes from ``l2_instance.description`` (NOT a hardcoded persona string). Switching L2 instance switches the prose — same contract the L1 dashboard's Getting Started follows. """ accent = theme.accent # Q.1.c — collapse YAML literal-block whitespace (literal `|` block # scalars preserve hard newlines, which QuickSight's text-box # renderer drops without inserting word breaks → adjacent words # glom together). " ".join(text.split()) reflows the description # as a single paragraph; if multi-paragraph descriptions land # later, switch to a paragraph-aware reflow that preserves blank # lines as <br/><br/>. raw_body = ( l2_instance.description if l2_instance.description else "(L2 instance description missing — fill the top-level " "`description` field in the L2 YAML.)" ) welcome_body = " ".join(raw_body.split()) sheet.layout.row(height=8).add_text_box( TextBox( text_box_id="l2ft-gs-welcome", content=rt.text_box( rt.inline( _GETTING_STARTED_TITLE, font_size="36px", color=accent, ), rt.BR, rt.BR, rt.markdown(_GETTING_STARTED_DESCRIPTION), rt.BR, rt.BR, rt.subheading("L2 Instance", color=accent), rt.BR, rt.markdown(welcome_body), ), ), width=36, ) # Date-filter defaults are intentionally "all time" so a freshly-loaded # Rails tab renders all postings — the date pickers are for narrowing, # not for a default scope. The L1 dashboard's rolling-7-day default # DOESN'T fit here for two reasons: (1) the demo seed plants synthetic # postings dated 2029-11 to 2030-01-01 (deliberately decoupled from # wall-clock), so a "now-relative" default would exclude every demo row; # (2) Rails is an explorer tab — the analyst comes in not knowing what # range to look at, and an unconstrained default lets them see what's # there before narrowing. # # AR.4 — INTENTIONALLY NOT MIGRATED to `DateView`. The view primitive # models (anchor + span + empty-behavior); these static bounds model a # fourth shape — "no narrowing, this surface doesn't filter on date" — # which doesn't fit a frame-anchored view cleanly. The view-primitive # audit (§5 residual tension) distinguishes invariant-derived / # data/deadline-derived / subjective-view windows; this is a fourth # kind. Switching to a RollingDate-or-anchored view would require the # L2 instance to carry production data with current timestamps. # Phase BM — drop the trailing ``.NNNZ`` UTC-Z suffix that the pre-BM # analysis-only default carried. Post-BM the analysis-level value # propagates to dataset-SQL substitution via ``MappedDataSetParameters``; # the universal_date_range_clause helper expects the canonical # ``YYYY-MM-DDT00:00:00`` shape (Oracle SUBSTR(1, 10) handles either # but PG / SQLite are cleaner without the suffix). Both renderers # match-all from these literal bounds. _DATE_START_STATIC = "1900-01-01T00:00:00" _DATE_END_STATIC = "2099-12-31T00:00:00" def _populate_pushdown_dropdown( *, sheet: Sheet, analysis: Analysis, bridges: list[tuple[Dataset, str]], param_name: ParameterName, title: str, all_values: list[str], ) -> None: """Y.2.c + AA.A.3 — like ``_populate_param_filter_dropdown`` but the narrowing happens in the dataset SQL, not via a CategoryFilter. Wires two things: 1. A single-valued ``StringParam`` (default = ``L2FT_ALL_SENTINEL``, which the dataset SQL's ``_match_all_in_clause`` resolves to "match all rows") bridged to each ``(dataset, dataset_param)`` pair in ``bridges`` via ``MappedDataSetParameters``. Each dataset's CustomSQL substitutes ``<<$dataset_param>>`` into the ``('sentinel' = <<$p>> OR col = <<$p>>)`` predicate. (Usually one bridge; the Transfer Templates sheet uses two — its Template / Completion dropdowns narrow both the tt-instances Table and the tt-legs Sankey.) 2. A ``ParameterDropdown(SINGLE_SELECT, StaticValues)`` so the analyst picks one value with one click. No FilterGroup — that's the difference from the ``_populate_param_filter_dropdown`` shape. Clearing the dropdown reverts each dataset param to the sentinel default (= all rows match). AA.A.3 flipped this from MULTI to SINGLE per the drill-to-one default (audit at ``docs/audits/aa_a_dropdown_audit.md``). """ p = analysis.add_parameter(StringParam( name=param_name, multi_valued=False, default=[L2FT_ALL_SENTINEL], mapped_dataset_params=list(bridges), )) sheet.add_parameter_dropdown( parameter=p, title=title, selectable_values=StaticValues(values=list(all_values)), ) def _populate_rails_sheet( cfg: Config, sheet: Sheet, *, analysis: Analysis, datasets: dict[str, Dataset], l2_instance: L2Instance, ) -> None: """Rails sheet — interactive transactions explorer (M.3.10c rewrite). Six controls in the sheet's filter bar drive a transactions Table: 1. **Date From** + **Date To** — bind to ``pL2ftDateStart`` / ``pL2ftDateEnd``; ``TimeRangeFilter`` on ``posting``. 2. **Rail** — multi-select ``CategoryFilter`` on ``rail_name``. 3. **Status** — multi-select ``CategoryFilter`` on ``status``. 4. **Bundle** — multi-select ``CategoryFilter`` on the calc'd ``bundle_status`` ('Bundled' / 'Unbundled'). 5. **Metadata Key** — single-select ``ParameterDropdown`` with ``StaticValues`` (the L2's declared keys + ``__ALL__`` sentinel). Bound to ``pL2ftMetaKey``, mapped to ``pKey`` on the postings dataset (so its ``<<$pKey>>`` substitution narrows the table). 6. **Metadata Value** — multi-select ``ParameterDropdown`` with ``LinkedValues`` from the meta-values dataset. Bound to ``pL2ftMetaValue``, mapped to ``pValues`` on the postings dataset. ``CascadingControlConfiguration`` on this control points at the meta-values dataset's ``metadata_key`` column, so QS column-match-filters its rows by the Key dropdown's selection — which narrows the Value dropdown's options. Two distinct mechanisms working together: - Postings table filtering: dataset parameters (``<<$pKey>>`` / ``<<$pValues>>``) substituted into a JSONPath ``IN (...)`` predicate at query time. - Value dropdown options narrowing: column-match cascade against the long-form ``(metadata_key, metadata_value)`` meta-values dataset. (Earlier attempt to drive this via dataset parameters alone failed — QS's cascade is column-match, not parameter- driven re-query. See M.3.10c memory.) The declared-rails table that lived here pre-M.3.10c moved to a future Docs tab; the runtime postings explorer is the focus here. """ ds_postings = datasets[DS_POSTINGS] # 1+2. Date range — Phase BM pushdown via ``<<$pL2ftDate*>>`` on # ``posting``. ``mapped_dataset_params`` bridges the analysis-level # params into the postings dataset's matching DateTime dataset # params; the picker write goes through to the SQL substitution on # BOTH renderers. Pre-BM was a per-sheet ``TimeRangeFilter`` FG # narrowing only on QS — App2 saw the picker widget but the # dataset SQL had no bind for it (the UX lie BM dissolves). date_start = analysis.add_parameter(DateTimeParam( name=ParameterName(_P_L2FT_RAILS_DATE_START), time_granularity="DAY", default=DateTimeDefaultValues(StaticValues=[_DATE_START_STATIC]), mapped_dataset_params=[(ds_postings, _P_L2FT_RAILS_DATE_START)], )) date_end = analysis.add_parameter(DateTimeParam( name=ParameterName(_P_L2FT_RAILS_DATE_END), time_granularity="DAY", default=DateTimeDefaultValues(StaticValues=[_DATE_END_STATIC]), mapped_dataset_params=[(ds_postings, _P_L2FT_RAILS_DATE_END)], )) sheet.add_parameter_datetime_picker(parameter=date_start, title="Date From") sheet.add_parameter_datetime_picker(parameter=date_end, title="Date To") # 3-5. Three "default-all multi-select" dropdowns (rail / status / # bundle status). Y.2.c — the narrowing pushes into the postings # dataset SQL via multi-valued dataset parameters # (`<<$pL2ftRail>>` / `<<$pL2ftStatus>>` / `<<$pL2ftBundle>>`), no # analysis-level CategoryFilter / FilterGroup. Predecessor (X.1.g) # was `ParameterDropdown(StaticValues) + CategoryFilter.with_parameter` # — itself a replacement for `FilterDropdown(CategoryFilter(values=[], # FILTER_ALL_VALUES))`, which relied on QS's lazy `tenK-sample- # values-V2` fetch (the X.1.a cold-CI 404 source). StaticValues # options + a default spanning every value keeps "no narrowing" the # analyst's starting state without QS querying anything. _populate_pushdown_dropdown( sheet=sheet, analysis=analysis, bridges=[(ds_postings, "pL2ftRail")], param_name=ParameterName("pL2ftRail"), title="Rail", all_values=declared_rail_names(l2_instance), ) _populate_pushdown_dropdown( sheet=sheet, analysis=analysis, bridges=[(ds_postings, "pL2ftStatus")], param_name=ParameterName("pL2ftStatus"), title="Status", all_values=transaction_status_values(), ) _populate_pushdown_dropdown( sheet=sheet, analysis=analysis, bridges=[(ds_postings, "pL2ftBundle")], param_name=ParameterName("pL2ftBundle"), title="Bundle", all_values=bundle_status_values(), ) # 6. Metadata cascade — the M.3.10c novelty. # # Key: single-select StaticValues from the L2 walk + sentinel. # Bound to pL2ftMetaKey, which maps to `pKey` on BOTH the # postings dataset (controls the WHERE clause) and the # meta-values dataset (controls which key's values populate the # Value dropdown). p_meta_key = analysis.add_parameter(StringParam( name=ParameterName("pL2ftMetaKey"), default=[META_KEY_ALL_SENTINEL], multi_valued=False, # Bridge to the postings dataset only — meta-values now uses # QS's native column-match cascade (driven by the Value # dropdown's CascadingControlConfiguration, not by SQL # substitution on the meta-values dataset). mapped_dataset_params=[ (ds_postings, "pKey"), ], )) # Value: single-string text-field input bound to pL2ftMetaValue, # mapped to `pValues` on the postings dataset. # # Y.1.m: was multi_valued=True; the text-field control silently # reverts non-empty commits to default on multi-valued params # (whole cascade broke). Single-valued is the correct shape for # text-field input. Trade-off: analyst can only filter to one # metadata value at a time on this sheet — an acceptable cost since # the multi-value path was 100% broken in production. p_meta_value = analysis.add_parameter(StringParam( name=ParameterName("pL2ftMetaValue"), default=[META_VALUE_PLACEHOLDER_SENTINEL], multi_valued=False, mapped_dataset_params=[ (ds_postings, "pValues"), ], )) declared_keys = declared_metadata_keys(l2_instance) sheet.add_parameter_dropdown( parameter=p_meta_key, title="Metadata Key", type="SINGLE_SELECT", # Sentinel first so it's the visible default; declared keys # follow in sorted order. selectable_values=StaticValues( values=[META_KEY_ALL_SENTINEL] + declared_keys, ), ) # X.1.b — Free-text input (was LinkedValues dropdown). The # LinkedValues path triggered QS's lazy "sample values" fetch on # cold per-CI-run dashboards, throwing # ``[pageerror] Sample values not found`` and stranding the # Transactions table empty. Text input has no equivalent fetch — # the analyst types the literal value to filter on. sheet.add_parameter_text_field( parameter=p_meta_value, title="Metadata Value", ) # BO.12 — orientation KPI row above the table. The wide ledger dump # below was hostile as a cold-land target: an operator who hadn't # touched filters yet had no top-line signal for "how much is in # here?" Two KPIs answer the count + magnitude orientation # questions; the freshness signal the cold-read also asked for # (Latest Leg = MAX(posting)) can't render as a typed KPI Measure # because QS rejects NumericalMeasureField over a DATETIME column # ("can only refer to columns of types [INTEGER, DECIMAL]" at # analysis-create time — caught by the v11.24.0 CI deploy probe). # The Posting column on the Table below carries the same freshness # signal one row down. half = 36 // 2 kpi_row = sheet.layout.row(height=8) kpi_row.add_kpi( width=half, title="Legs in Window", subtitle=( "Count of postings rows matching all filters above. With " "no narrowing this is every leg in the visible date range " "across every status; pick a Status to scope to Posted-only. " # C9 (cold-read v11.26.1) — this count is windowed (current # date range only); the Executives KPIs + App Info matview # row counts are all-history. Cross-reference so the cross- # app delta reads as expected scope, not as disagreement. "**Scope note:** this is the **windowed** count — the " "Executives 'Total Transactions' KPI + App Info " "transactions matview show all-history rows. The gap " "between this number and either of those = legs outside " "the picker's date window." ), values=[ds_postings["id"].count()], ) kpi_row.add_kpi( width=half, title="Largest Leg", subtitle=( "Largest single-leg `amount_money` across the filtered " "rows. Sibling to Legs in Window — count + magnitude " "together tell you whether the visible slice is a long " "tail of small legs or a few outliers." ), values=[ds_postings["amount_money"].max(currency=True)], ) # Transactions table — the postings dataset's SQL handles the # metadata-cascade WHERE clause via dataset parameters; the four # category filters narrow further. sheet.layout.row(height=21).add_table( width=36, title=_RAILS_TRANSACTIONS_TITLE, subtitle=( "One row per leg matching all the filters above. With no " "Metadata Key picked, every leg in the date window appears; " "picking a Key + one or more Values narrows to legs whose " "metadata carries that key=value pair." ), columns=[ ds_postings["posting"].date(), ds_postings["rail_name"].dim(), ds_postings["transfer_id"].dim(), ds_postings["account_name"].dim(), ds_postings["amount_money"].numerical(currency=True), ds_postings["amount_direction"].dim(), ds_postings["status"].dim(), ds_postings["bundle_status"].dim(), ds_postings["transfer_parent_id"].dim(), ], ) def _populate_chains_sheet( cfg: Config, sheet: Sheet, *, analysis: Analysis, datasets: dict[str, Dataset], l2_instance: L2Instance, ) -> None: """Chains sheet — per-instance explorer (M.3.10d rewrite). Six controls in the sheet's filter bar drive a chain-instances Table: 1. **Date From** + **Date To** — bind to ``pL2ftChainsDateStart`` / ``pL2ftChainsDateEnd``; ``TimeRangeFilter`` on ``parent_posting``. 2. **Chain** — multi-select ``CategoryFilter`` on ``parent_chain_name``. 3. **Completion** — multi-select ``CategoryFilter`` on ``completion_status`` (Completed / Incomplete / No Required Children). 4. **Metadata Key** — single-select ``ParameterDropdown`` with ``StaticValues`` (the L2's declared keys + ``__ALL__`` sentinel). Mapped to ``pKey`` on the chain-instances dataset. 5. **Metadata Value** — multi-select ``ParameterDropdown`` with ``LinkedValues`` from the meta-values dataset (shared with Rails). Mapped to ``pValues`` on the chain-instances dataset. ``CascadingControlConfiguration`` on this control points at the meta-values dataset's ``metadata_key`` column for the column-match cascade (same mechanism Rails uses). Visualization choice: Chains is a *runtime causality* concept (parent transfer fires → child transfer should fire later), not a multi-leg flow graph — Sankey does not read naturally. Per-firing Table is the right shape for now; revisit if a better visual primitive emerges. Multi-leg flow visualization belongs on TransferTemplates (which have explicit leg topology), if/when an L2 Templates explorer surface is added. """ ds_chain_instances = datasets[DS_CHAIN_INSTANCES] # 1+2. Date range — Phase BM pushdown via ``<<$pL2ftChainsDate*>>`` # on ``parent_posting``. Separate from Rails' date params so the # analyst's chains-window selection doesn't perturb the rails view # (and vice versa). Pre-BM was a per-sheet TimeRangeFilter FG # narrowing only on QS — App2 saw the picker but the SQL didn't # bind it. date_start = analysis.add_parameter(DateTimeParam( name=ParameterName(_P_L2FT_CHAINS_DATE_START), time_granularity="DAY", default=DateTimeDefaultValues(StaticValues=[_DATE_START_STATIC]), mapped_dataset_params=[ (ds_chain_instances, _P_L2FT_CHAINS_DATE_START), ], )) date_end = analysis.add_parameter(DateTimeParam( name=ParameterName(_P_L2FT_CHAINS_DATE_END), time_granularity="DAY", default=DateTimeDefaultValues(StaticValues=[_DATE_END_STATIC]), mapped_dataset_params=[ (ds_chain_instances, _P_L2FT_CHAINS_DATE_END), ], )) sheet.add_parameter_datetime_picker(parameter=date_start, title="Date From") sheet.add_parameter_datetime_picker(parameter=date_end, title="Date To") # 3+4. Chain + Completion — Y.2.d — pushed into the chain-instances # dataset SQL (`<<$pL2ftChainsChain>>` / `<<$pL2ftChainsCompletion>>`), # no analysis-level CategoryFilter / FilterGroup. (X.1.g predecessor # was a parameter-bound CategoryFilter; before that an empty # FilterDropdown that forced QS's lazy dropdown-options fetch.) _populate_pushdown_dropdown( sheet=sheet, analysis=analysis, bridges=[(ds_chain_instances, "pL2ftChainsChain")], param_name=ParameterName("pL2ftChainsChain"), title="Chain", all_values=declared_chain_parents(l2_instance), ) _populate_pushdown_dropdown( sheet=sheet, analysis=analysis, bridges=[(ds_chain_instances, "pL2ftChainsCompletion")], param_name=ParameterName("pL2ftChainsCompletion"), title="Completion", all_values=chain_completion_status_values(), ) # 5+6. Metadata cascade — same mechanism as Rails (M.3.10c memory): # SQL substitution on the chain-instances dataset for the table's # WHERE clause + column-match CascadingControlConfiguration on the # Value dropdown for option-narrowing. Separate analysis params # from Rails so per-sheet selection doesn't bleed across tabs. p_meta_key = analysis.add_parameter(StringParam( name=ParameterName("pL2ftChainsMetaKey"), default=[META_KEY_ALL_SENTINEL], multi_valued=False, mapped_dataset_params=[ (ds_chain_instances, "pKey"), ], )) # Y.1.m: single-valued (was multi_valued=True, broke under text-field # control — see Rails sheet for the diagnostic). p_meta_value = analysis.add_parameter(StringParam( name=ParameterName("pL2ftChainsMetaValue"), default=[META_VALUE_PLACEHOLDER_SENTINEL], multi_valued=False, mapped_dataset_params=[ (ds_chain_instances, "pValues"), ], )) declared_keys = declared_metadata_keys(l2_instance) sheet.add_parameter_dropdown( parameter=p_meta_key, title="Metadata Key", type="SINGLE_SELECT", selectable_values=StaticValues( values=[META_KEY_ALL_SENTINEL] + declared_keys, ), ) # X.1.b — Free-text input (see Rails sheet for rationale). sheet.add_parameter_text_field( parameter=p_meta_value, title="Metadata Value", ) sheet.layout.row(height=21).add_table( width=36, title="Chain Instances", subtitle=( "One row per parent transfer firing. The two `Required " "Children` columns count CHILD CHAINS the L2 declares (not " "legs of a multi-leg transfer — a Debit+Credit pair is one " "transfer with two legs, not two children): `Required " "Children Declared` is the count of Required follow-on " "child chains the L2 declared for this parent; `Required " "Children Fired` is how many of those actually showed up " "for this transfer_id. A chain with only XOR-sibling " "children declares zero Required (exactly one fires by " "design), so a 0/0 row is normal for those. " "`completion_status` reads 'Completed' iff every Required " "child fired; 'Incomplete' if any is missing. With no " "Metadata Key picked, every firing in the date window " "appears." ), columns=[ ds_chain_instances["parent_posting"].date(), ds_chain_instances["parent_chain_name"].dim(), ds_chain_instances["parent_transfer_id"].dim(), ds_chain_instances["completion_status"].dim(), ds_chain_instances["required_fired"].numerical(), ds_chain_instances["required_total"].numerical(), ds_chain_instances["parent_amount_money"].numerical(currency=True), ds_chain_instances["parent_status"].dim(), ], ) def _populate_transfer_templates_sheet( cfg: Config, sheet: Sheet, *, analysis: Analysis, datasets: dict[str, Dataset], l2_instance: L2Instance, theme: ThemePreset, ) -> None: """Transfer Templates sheet — multi-leg flow Sankey + per-instance detail Table (M.3.10f). Two visuals stacked: Sankey (multi-leg flow through declared templates) and Table (per-shared-Transfer balance detail). Filter bar (six controls): 1. **Date From** + **Date To** — bind to ``pL2ftTtDateStart`` / ``pL2ftTtDateEnd``; ``TimeRangeFilter`` on ``posting``. ``cross_dataset='ALL_DATASETS'`` so the filter narrows BOTH tt-instances + tt-legs (which both carry the column). 2. **Template** — multi-select ``CategoryFilter`` on ``template_name``. Same ``ALL_DATASETS`` shape. 3. **Completion** — multi-select ``CategoryFilter`` on ``completion_status`` (Complete / Imbalanced / Orphaned) — tt-instances only (per-firing balance + chain-completion check). Filter narrows the table to bundles by their L1 + L2 outcome. 4. **Metadata Key** — single-select ``ParameterDropdown`` with ``StaticValues`` (the L2's declared keys + ``__ALL__`` sentinel). Mapped to ``pKey`` on BOTH tt-instances + tt-legs (so the cascade narrows both visuals via SQL substitution). 5. **Metadata Value** — multi-select ``ParameterDropdown`` with ``LinkedValues`` from the meta-values dataset (shared with Rails / Chains). Mapped to ``pValues`` on BOTH datasets. ``CascadingControlConfiguration`` on this control points at the meta-values dataset's ``metadata_key`` column for the column-match cascade. Sankey reads as: debit accounts → template → credit accounts. Each shared Transfer's debit legs flow into the template middle node, credit legs flow out. Picking a single Template collapses the Sankey to that one template's flow shape. """ ds_tt_instances = datasets[DS_TT_INSTANCES] ds_tt_legs = datasets[DS_TT_LEGS] # 1+2. Date range — Phase BM pushdown via ``<<$pL2ftTtDate*>>``. # Both tt-instances + tt-legs carry the same dataset params (the # pre-BM ``cross_dataset='ALL_DATASETS'`` TimeRangeFilter narrowed # both together; the BM bridge writes through to each per its own # ``<<$pL2ftTtDate*>>`` placeholder). tt_start_bridges = [ (ds_tt_instances, _P_L2FT_TT_DATE_START), (ds_tt_legs, _P_L2FT_TT_DATE_START), ] tt_end_bridges = [ (ds_tt_instances, _P_L2FT_TT_DATE_END), (ds_tt_legs, _P_L2FT_TT_DATE_END), ] date_start = analysis.add_parameter(DateTimeParam( name=ParameterName(_P_L2FT_TT_DATE_START), time_granularity="DAY", default=DateTimeDefaultValues(StaticValues=[_DATE_START_STATIC]), mapped_dataset_params=tt_start_bridges, )) date_end = analysis.add_parameter(DateTimeParam( name=ParameterName(_P_L2FT_TT_DATE_END), time_granularity="DAY", default=DateTimeDefaultValues(StaticValues=[_DATE_END_STATIC]), mapped_dataset_params=tt_end_bridges, )) sheet.add_parameter_datetime_picker(parameter=date_start, title="Date From") sheet.add_parameter_datetime_picker(parameter=date_end, title="Date To") # 3+4. Template + Completion — Y.2.e — pushed into BOTH the # tt-instances dataset SQL (the Table) AND the tt-legs dataset SQL # (the Sankey) via multi-valued dataset params; no FilterGroup. The # dual bridge replaces the pre-Y.2.e `cross_dataset='ALL_DATASETS'` # CategoryFilter — M.3.10k already denormalized `template_name` / # `completion_status` onto tt-legs so both the Table and the Sankey # narrow together, and that same denormalization makes the dual SQL # pushdown work. (X.1.g had a StaticValues-backed param CategoryFilter; # before that an empty FilterDropdown forcing QS's lazy options fetch.) _populate_pushdown_dropdown( sheet=sheet, analysis=analysis, bridges=[ (ds_tt_instances, "pL2ftTtTemplate"), (ds_tt_legs, "pL2ftTtTemplate"), ], param_name=ParameterName("pL2ftTtTemplate"), title="Template", all_values=declared_template_names(l2_instance), ) _populate_pushdown_dropdown( sheet=sheet, analysis=analysis, bridges=[ (ds_tt_instances, "pL2ftTtCompletion"), (ds_tt_legs, "pL2ftTtCompletion"), ], param_name=ParameterName("pL2ftTtCompletion"), title="Completion", all_values=tt_completion_status_values(), ) # 5+6. Metadata cascade — same mechanism as Rails / Chains. # mapped_dataset_params lists BOTH tt-instances + tt-legs so the # cascade narrows the Sankey + Table together. p_meta_key = analysis.add_parameter(StringParam( name=ParameterName("pL2ftTtMetaKey"), default=[META_KEY_ALL_SENTINEL], multi_valued=False, mapped_dataset_params=[ (ds_tt_instances, "pKey"), (ds_tt_legs, "pKey"), ], )) # Y.1.m: single-valued (was multi_valued=True, broke under text-field # control — see Rails sheet for the diagnostic). p_meta_value = analysis.add_parameter(StringParam( name=ParameterName("pL2ftTtMetaValue"), default=[META_VALUE_PLACEHOLDER_SENTINEL], multi_valued=False, mapped_dataset_params=[ (ds_tt_instances, "pValues"), (ds_tt_legs, "pValues"), ], )) declared_keys = declared_metadata_keys(l2_instance) sheet.add_parameter_dropdown( parameter=p_meta_key, title="Metadata Key", type="SINGLE_SELECT", selectable_values=StaticValues( values=[META_KEY_ALL_SENTINEL] + declared_keys, ), ) # X.1.b — Free-text input (see Rails sheet for rationale). sheet.add_parameter_text_field( parameter=p_meta_value, title="Metadata Value", ) # Edge legend. QuickSight Sankey doesn't support data-driven # ribbon colors (colors auto-assign per source/target node by the # theme), so the matched-vs-orphan distinction is encoded in the # NODE NAMES — orphan edges land on a "(orphan)" suffixed node. # This text box spells out the three edge kinds the analyst will # see in the Sankey below. accent = theme.accent sheet.layout.row(height=3).add_text_box( TextBox( text_box_id="l2ft-tt-sankey-legend", content=rt.text_box( rt.subheading("Edge legend", color=accent), rt.bullets_raw([ rt.inline("Account ↔ Template", color=accent) + ": the template's own legs (debit on source, " "credit on destination).", rt.inline("Template → <child rail>", color=accent) + ": declared chain child that fired (matched edge).", rt.inline( "Template → <child rail> (orphan)", color=accent, ) + ": declared chain child that didn't fire " "(orphan edge — the missing-link signal).", ]), ), ), width=36, ) # Sankey — multi-leg flow. flow_source / flow_target derive from # amount_direction (debit account → template → credit account). # Width = SUM(amount_abs). sheet.layout.row(height=12).add_sankey( width=36, title="Multi-Leg Flow — Account → Template → Account", subtitle=( "Width = total absolute amount through the edge in the " "filtered window. Pick a single Template to see just that " "template's flow shape. Ribbon colors are QuickSight's " "auto-assignment per source node — the matched-vs-orphan " "distinction is in the node names (see legend above)." ), source=ds_tt_legs["flow_source"].dim(), target=ds_tt_legs["flow_target"].dim(), weight=ds_tt_legs["amount_abs"].sum(currency=True), ) sheet.layout.row(height=12).add_table( width=36, title="Template Instances", subtitle=( "One row per shared Transfer. completion_status combines " "the L1 balance check (legs sum to expected_net within " "$0.01) with the L2 chain-completion check (every Required " "child fired AND every XOR group has exactly one fired): " "'Complete' / 'Imbalanced' (L1 break) / 'Orphaned' (L2 " "chain break)." ), columns=[ ds_tt_instances["posting"].date(), ds_tt_instances["template_name"].dim(), ds_tt_instances["transfer_id"].dim(), ds_tt_instances["completion_status"].dim(), ds_tt_instances["actual_net"].numerical(currency=True), ds_tt_instances["expected_net"].numerical(currency=True), ds_tt_instances["net_diff"].numerical(currency=True), ds_tt_instances["leg_count"].numerical(), ], ) def _l2ft_drill( *, target_sheet: Sheet, name: str, writes: list[DrillWrite], trigger: Literal["DATA_POINT_CLICK", "DATA_POINT_MENU"] = "DATA_POINT_MENU", ) -> Drill: """L2FT cross-sheet drill helper. BS.3 follow-up (2026-05-30): drill writes target the destination sheet's user-facing picker parameter directly (pL2ftRail / pL2ftChainsChain). The pre-fix machinery used dedicated pL2ftRailDrill / pL2ftChainDrill params that fed a CalcField + FilterGroup on the QS analysis layer — which left App2 unable to narrow (the SQL had no ``<<$pL2ftRailDrill>>`` placeholder). By writing the picker param directly, the existing ``_match_all_in_clause`` SQL pushdown narrows on both renderers. No reset-sentinel needed: the picker is the user-visible filter and any drill is a one-shot override (the picker becomes sticky until the user clears it, same as if they'd typed the value). """ return Drill( target_sheet=target_sheet, writes=writes, name=name, trigger=trigger, ) # AA.C.4 — height of the sheet-bottom hygiene-exceptions panel. # Mirrors L1's _PANEL_LAYOUT_HEIGHT (apps/l1_dashboard/app.py); kept # slightly taller because the L2FT panel is a 6-bullet roll-up # rather than per-kind, so the prose runs longer per row. _L2FT_PANEL_LAYOUT_HEIGHT = 8 def _populate_l2_exceptions_sheet( cfg: Config, sheet: Sheet, *, datasets: dict[str, Dataset], rails_sheet: Sheet, chains_sheet: Sheet, theme: ThemePreset, ) -> None: """L2 Exceptions sheet — unified violation view (M.3.10l rewrite of M.3.7). Mirrors L1's L1 Exceptions pattern: one KPI (total count), one bar chart (by check_type), one detail table (sorted by count DESC). All six L2 hygiene checks (Chain Orphans, Unmatched Rail Name, Dead Rails, Dead Bundles Activity, Dead Metadata, Dead Limit Schedules) UNION into one `unified-exceptions` dataset; the `check_type` discriminator column drives the bar chart breakout + the table's left-most grouping column. AA.C.4 appends a sheet-bottom panel sourced from ``src/recon_gen/docs/L2FT_Exceptions.md`` — one bullet per check kind, each carrying the parser-extracted ``**What to do:**`` paragraph. Mirrors the AA.C.3 L1 panels but in roll-up form (every L2FT kind lives on this one sheet) rather than per-kind stacked. Pre-M.3.10l this sheet had 6 vertically-stacked sections (header text-box + 2 KPIs + table per check) that totaled ~144 rows of vertical scroll; the unified view fits in one screen and matches the L1 dashboard's familiar shape. """ accent = theme.accent del accent # unused after the M.3.10l rewrite — kept the lookup # so a future legend / KPI tint can pick it up cheaply. ds = datasets[DS_UNIFIED_L2_EXCEPTIONS] # Row 1 — KPI (narrow, left) + bar chart (wide, right). KPI sits # next to the bar chart so the headline number reads alongside # the breakdown rather than dominating its own row. top_row = sheet.layout.row(height=10) top_row.add_kpi( width=12, title="Distinct Exception Types Open", subtitle=( "Count of distinct exception TYPES (one of the six L2 " "hygiene checks) currently with at least one open violation. " "**The detail table's `Violations per Type` column counts " "occurrences PER row** — these are two different units " "measuring different things, both correct." ), values=[ds["check_type"].distinct_count()], ) top_row.add_bar_chart( width=24, title="L2 Violations by Check Type", subtitle=( "Count per L2 hygiene check. A spike in one kind points " "at a recurring class of declaration-vs-runtime drift to " "investigate first." ), category=[ds["check_type"].dim()], values=[ds["check_type"].count()], category_label="Check Type", value_label="Open Violations", orientation="HORIZONTAL", ) # Row 2 — detail table. Right-click any row's entity_a to drill # into the source. Both menu items appear on every row regardless # of check_type; pick the one that matches the row's subject # (e.g., "View in Rails" for Dead Rails / Dead Metadata; "View # in Chains" for Chain Orphans). Rows whose entity_a isn't a rail # or chain parent (Unmatched Transfer Type, Dead Limit Schedules) # land an empty destination — clear "this drill doesn't apply" # signal. # AA.D.1: dropped currency=True — this is an INTEGER count # (orphan rows / dead-declaration rows), not a money amount. count_col = ds["count"].numerical() entity_a_col = ds["entity_a"].dim() sheet.layout.row(height=14).add_table( width=36, title="L2 Violation Detail", subtitle=( "Every row is one detected L2 violation. Sorted by " "occurrences (largest first). Right-click any row to drill " "into Rails (entity_a → Rail filter) or Chains (entity_a → " "Chain filter). Read entity_a / entity_b / detail in the " "context of the row's check_type — see the sheet " "description above for which fields each check populates." ), columns=[ ds["check_type"].dim(), entity_a_col, ds["entity_b"].dim(), ds["detail"].dim(), count_col, ], sort_by=(count_col, "DESC"), actions=[ _l2ft_drill( target_sheet=rails_sheet, name="View in Rails (filter rail_name to entity_a)", writes=[(_DP_RAIL_DRILL, entity_a_col)], trigger="DATA_POINT_MENU", ), _l2ft_drill( target_sheet=chains_sheet, name="View in Chains (filter parent_chain_name to entity_a)", writes=[(_DP_CHAIN_DRILL, entity_a_col)], trigger="DATA_POINT_MENU", ), ], ) # AA.C.4 — sheet-bottom panel sourced from L2FT_Exceptions.md. # All six L2FT hygiene checks roll up onto this one sheet (the # M.3.10l unified-exceptions view), so the panel is a roll-up # bullet list, not per-kind stacked. Mirrors L1's Today's # Exceptions intro panel shape (AA.C.3.e). sections = load_bundled_l2ft_exceptions() sheet.layout.row(height=_L2FT_PANEL_LAYOUT_HEIGHT).add_text_box( TextBox( text_box_id="l2ft-hygiene-panel", content=rt.text_box(rt.markdown(l2ft_panel_markdown(sections))), ), width=36, ) # --------------------------------------------------------------------------- # CLI / external-caller shims. Mirror the L1 dashboard signature so the CLI # can plumb through generically. # ---------------------------------------------------------------------------
[docs] def build_analysis( cfg: Config, *, l2_instance: L2Instance | None = None, ): """Build the complete L2 Flow Tracing Analysis resource via the tree.""" return build_l2_flow_tracing_app(cfg, l2_instance=l2_instance).emit_analysis()
[docs] def build_l2_flow_tracing_dashboard( cfg: Config, *, l2_instance: L2Instance | None = None, ): """Build the L2 Flow Tracing Dashboard resource via the tree.""" return build_l2_flow_tracing_app(cfg, l2_instance=l2_instance).emit_dashboard()