Skip to content

v13.15.1

Choose a tag to compare

@github-actions github-actions released this 13 Jun 13:34
· 218 commits to main since this release

Two release-gate workflows behind this cut. Patch class — no API
changes, no schema deltas.

Phase DB.3 followups (operator-driven 2026-06-12)

  • App2 Sankey tooltip-only labels (operator: "the combining
    issue"). Drop the always-visible inline <text> labels that
    stacked into illegible overlap at 50+ nodes (L2FT Multi-Leg
    Flow). Mirror QS's hover-only behavior with SVG <title>
    children on every node + link. Native browser tooltips; no JS
    overlay. Currency formatting via Intl.NumberFormat USD.
  • QS BarChart count() axis label fix. _axis_label_apply_to
    now mirrors BL.1's count() → _row_one_* field-well rewrite so
    ApplyTo.Column.ColumnName matches the well. QS was silently
    dropping the value_label override on mismatch (Pending Aging
    and L2 Exceptions x-axis read _row_one_l1_*_ds (Sum) instead
    of the configured label).
  • QS embed tall-viewport for the cold-read parity capture
    pass (1600, 1000) → (1600, 4000). QS uses internal scroll
    containers so page.screenshot(full_page=True) only captured
    the viewport region; the tall viewport lets sheet content
    (KPI + chart + table + below-fold prose) render in one frame.
  • L2FT Transfer Templates sheet target for the parity-verify
    Sankey test — the multi-leg Sankey lives on Transfer Templates,
    not Chains. Test slug + audit-doc cross-references updated.

Phase DG — CI database hygiene + triage (full cycle)

Five-leaf phase that root-caused the multi-week CI red streak +
shipped the four supporting fixes.

  • DG.0 audit (docs/audits/dg_0_db_hygiene_audit.md). The
    red streak was POSIX /dev/shm saturation (per-container 64MB
    tmpfs default) under xdist -n 4 concurrent matview refreshes,
    plus accumulated per-test schema debris from
    tests/e2e/_isolation.py:158's silent-swallow best-effort
    teardown that fed on persistent CI containers across runs.
  • DG.1 fail-loud teardown. Drop the swallow; collect failures
    into _TEARDOWN_FAILURES + surface at session-end via
    pytest_sessionfinish with a clearly-marked summary block.
    Raises session.exitstatus to non-zero when non-empty so CI
    can't ignore future teardown failures.
  • DG.2 container-boot scorched-earth sweep. New runner step
    before the test layers fire — discovers every <base>_<6hex>_*
    object via pg_matviews / pg_views / pg_indexes /
    pg_tables (PG) or the equivalent user_* catalogs (Oracle)
    and drops with CASCADE in dependency-safe order. Idempotent
    • cleans cross-run debris in O(N) per kind. DuckDB no-op.
  • DG.3 ci-shared-pg --shm-size=2g on container creation +
    adoption-side check that recreates a reused container with the
    64MB default. The actual root cause of the DiskFull cascade —
    DG.2 cleans accumulated debris but doesn't reduce per-run
    /dev/shm pressure from concurrent activity.
  • DG.3 always-on hang diagnostic via stdlib faulthandler.
    Per [tool.pytest.ini_options] faulthandler_timeout = 180.
    Any test running >180s dumps thread tracebacks to stderr +
    continues (faulthandler reports, doesn't kill — full fixture
    teardown preserved). The runner's per-layer post-step greps
    stderr.log for trip count + emits
    [heartbeat-hit] layer=<X> — N trip(s) so CI logs surface
    it. Stack identifies the exact wedge location (Playwright's
    run_forever, psycopg socket blocks, matview refresh stalls).
    Always on; no opt-in flag.
  • DG.3 test-side picker fixes. The 12 v13.15.1-gate qs_browser
    failures bisected as 8× DiskFull cascade (cleared by shm-size)
    • 2× test-harness gaps + 2× pre-existing real bugs. Fix shapes:
    • _assert_pickable switches filter_options
      typeahead_filter (virtualized MUI Autocomplete dropdowns
      only mount ~12 alphabetical options on open; late-alphabet
      accounts like ZBASubAccount / WireSettlementSuspense
      silently fell out of the membership check).
    • wait_for_dropdown_options_present catches inner TimeoutError
      • retries so the outer 15s budget actually does what it says
        (was a single 2s inner failure propagating).
    • _open_control_dropdown does attached→scroll_into_view→
      visible instead of single state="visible" wait. Sheets with
      many control bar entries (Transactions has 5+) push later
      controls into a second row that's below the initial viewport.
    • 1s→5s search-input probe + skip the option-wait when
      search-variant was nudged. Typeahead pickers with empty seed
      don't surface options until something is typed; that's the
      caller's job.
    • Explicit Search-button click in
      narrow_dropdown_options_by_query + set_dropdown_value.
      QS's high-cardinality typeahead pickers (Transactions
      DS_L1_TX_IDS, 8k+ rows) render an explicit "Search" button
      instead of auto-narrowing on input change. Probe via
      button:has-text("Search") inside the popover containers +
      click if present; absent → MUI auto-narrowing variant fires
      on input.
    • TreeValidator settle-retry loop. Recipient Fanout's 3
      distinct_count() KPIs lag the Table's mount under CI load;
      single-shot diff after per-title wait_loaded missed them.
      Re-poll up to 3× / 1.5s after the initial diff.

Out of scope (release-time triage)

One residual qs_browser failure (test_inv_drilldown.py::test_account_network_table_walk_rerenders_table[qs])
xfailed strict=False — same Search-button-class typeahead picker
shape as the Transactions-Transfer case, this one with a
different option DOM that _OPTION_SELECTOR doesn't catch yet.
Investigation lives in PLAN backlog; will surface as XPASS when
fixed.

Three operator-dogfood bug-fix themes plus a substantive new feature
(Phase DA) collected into one release. Minor bump for the new App2
decoration feature (App2's renderer now paints drillable-column visuals
in parity with the QuickSight side).

1. Phase DA — App2/QS click-drill decoration parity + type-system gate.
Operator dogfood (2026-06-12, L1 Overdraft sheet) surfaced two coupled
defects in the click-drill decoration story:

  • App2's renderTable ignored conditional_formatting entirely, so
    every drillable column looked identical to non-drillable columns —
    operator couldn't tell which cells carried drills until they hovered.
  • Every L1 app site misused CellAccentText (the LEFT-click cue) for
    DATA_POINT_MENU drills. Zero usage of CellAccentMenu anywhere.
    Even on the QS side the column showed plain accent text while the
    actual drill was menu-only — analyst expectation vs reality drift.

The audit at docs/audits/da_0_clickability_audit.md found 15 mutations
across 12 Tables in 4 apps. Phase DA landed:

  • Collapsed two-type CellFormat design to one Drillable(on=Dim, color=str) marker. The visual cue (plain accent text vs accent
    text + tint background) auto-derives from the drill triggers writing
    from on.column at QS-emit time and at App2 plan-build time. Authors
    declare "this column carries a drill"; the type system + renderer
    pick the visual from the drill set:
    • any DATA_POINT_MENU drill writes from the column → accent text
      • accent-tint background
    • only DATA_POINT_CLICK drill(s) → accent text only
      Dropped CellAccentText + CellAccentMenu + the CellFormat union
    • common/clickability.py + tests/unit/test_clickability.py
      pre-stable posture, no compat shim.
  • App2 renderer plumbing. _VisualPlan grows
    column_decoration: Mapping[str, str]; _table_column_meta walks
    visual.conditional_formatting and resolves the per-column kind via
    the same Drillable.visual_kind(drills) code path the QS-side
    Drillable.emit uses — App2 ≡ QS by construction.
    _data_shape.shape_table forwards as column.decoration; bootstrap. js::renderTable paints cell-accent / cell-accent-menu CSS
    classes on each <td>.
  • CSS + cell-click affordance. widgets-theme.css adds
    .cell-accent { color: var(--color-accent); font-weight: 500 } and
    .cell-accent-menu { color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 10%, transparent); cursor: pointer }. bootstrap.js::wireRowDrills binds left-click on
    <td.cell-accent-menu> → opens the menu drill (same code path as the
    ⋯ button). stopPropagation() prevents the row-level CLICK drill
    from firing on Class B mix cells. Documents an operator-locked
    exception to "left clicks move LEFT" — App2 may break the rule when
    the explicit visual cue makes the affordance discoverable.
  • Apps sweep. All 13 existing CellAccentText callsites in
    apps/l1_dashboard/app.py migrated to Drillable. Class C wires:
    Transactions Audit transfer_id → Posting Ledger (uses the existing
    _DP_TX_TRANSFER landing pad + _wide_date_writes() pattern); Daily
    Balances Audit db_account_id → Daily Statement; Posting Ledger
    account_id → Daily Statement (added business_day column to
    l1-transactions-ds via date_trunc_day('posting', dialect) at
    SELECT time, tagged ColumnShape.DATETIME_DAY so the drill writes a
    day-grain date). Class C strip: Posting Ledger transfer_id
    (self-drill). Class D adds: L2FT Violation Detail entity_a;
    Investigation Account Network — Touching Edges counterparty.
  • Type-system gate at Table.__post_init__ (per
    [[feedback_invariants_in_types]]). Walks
    conditional_formatting × actions; for each Drillable.on.column,
    asserts ≥1 drill writes from that column. Raises ValueError at
    construction with diagnostic listing every Drill on the Table + the
    columns each one writes from, so the operator can spot off-by-one
    column-name typos at a glance. Permanently prevents the bug class
    from recurring.
  • Convention origin memory. "Left clicks move LEFT, right clicks
    move RIGHT" is a QuickSight-limitation workaround, not a deep design
    principle (operator clarification). App2 may break the rule when a
    better affordance exists — Phase DA's cell-click-opens-menu is one
    such authorized break.
  • Deployed-Studio handbook 404 fix. The per-sheet ? button's
    handbook source (docs/handbook/_shared/, l1/, executives/, etc.)
    moved into the wheel package at
    src/recon_gen/docs/_handbook_per_sheet/. The Starlette
    /handbook/<path> route updated to parents[2]/docs/_handbook_per_sheet/
    so it resolves identically in repo-checkout AND wheel-install. Mkdocs
    configured to exclude_docs: _handbook_per_sheet/ so the per-sheet
    snippets stay out of the curated site build.
  • QS uppercase-hex fix. _tint_hex emits {:02X} so the auto-derived
    background tint satisfies QS's ^#[A-F0-9]{6}$ validation pattern
    (CI 27439942692 caught the original lowercase-hex regression).

Full audit + locks + per-site resolution: docs/audits/da_0_clickability_audit.md.
App2-side parity screenshots: docs/audits/da_7_app2_snaps/.
Parity verify checklist: docs/audits/da_7_parity_verify.md.

2. Tom Select clear-button (× to clear a selection). Single-select
pickers previously required click-into + Delete-key to clear; the
operator flagged this as friction. Wired Tom Select's clear_button
plugin so a one-click ✕ at the right edge of the picker clears the
selection. Multi-select pickers also carry both remove_button
(per-chip ×) AND clear_button (clear-all ×) for symmetric affordance.

Visual polish iterated against operator dogfood feedback:

  • Override vendor cascade so the × pins to the right edge.
  • 1.5rem circular click target with danger-tint hover background.
  • Vertical-centered via top: 50% + translateY(-50%) (overrides
    vendor's fixed top: 8px which assumed default 20px-tall
    .ts-control).
  • Swapped the glyph from U+2A2F ⨯ (math operator — math-axis ink, not
    geometric center) to U+2715 ✕ (canonical UI close glyph, designed by
    font vendors to optically center) via .clear-button::before.
    Eliminates the "× floats above text baseline" perception.

Anti-regression test in test_html_filter_widgets.py pins the plugin
shape so future refactors can't silently drop the affordance.

3. Boot-id cache-bust for page-shell static assets. The shared
page shell in render.py hardcoded bare /static/output.css,
/static/widgets-theme.css, and seven vendor JS+CSS files. Iterating
on widgets-theme.css required the operator to Cmd+Shift+R after every
Studio restart to escape the browser cache.

Lifted Studio's existing _BOOT_ID + asset_url() helper (a
process-lifetime random hex token) from _studio_routes.py up to
render.py so the shared page shell's _VENDOR_CSS + _VENDOR_JS +
_PAGE_SHELL all route through it. Every <link> / <script> URL
the shell emits now carries ?cb=<boot>. Studio restart flips the
token, forcing every browser to refetch every asset on next page load
— no hard refresh needed.

Anti-regression test in test_vendor_assets.py
(test_page_shell_static_urls_cache_busted) regexes every
(href|src)="/static/..." out of a rendered shell and asserts ?cb=
appears in each one.

4. Row-drill MENU-only contract restored. Operator dogfood found
the L1 Overdraft sheet's table was making the entire row a left-click
target despite being declared as DATA_POINT_MENU-only. Root cause:
a clickDrill = drills[0] fallback in
bootstrap.js::wireRowDrills that promoted any first drill to a
row-wide left-click handler, overriding the documented contract.

Fallback removed. The existing
test_menu_drill_adds_ellipsis_button_per_row_and_header_cell test
was wrong (asserted n_drillable == 2 for MENU-only) — updated to
assert the correct shape: MENU-only Tables have n_drillable == 0,
no cursor: pointer on <tr>, and surface their drill ONLY via the
⋯ button + ctxmenu.