"""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()