Skip to content

Releases: chotchki/recon-gen

v14.5.1

17 Jun 19:31

Choose a tag to compare

Patch. v14.5.0 was tagged but never published: a runner-side CI outage
stranded it. The self-hosted runner's daily
docker image prune -af --filter "until=168h" cron removed the un-pullable
recon-gen/oracle-19c:local image (a stable base, always older than the 7-day
window), and a later run's teardown left the shared PG/Oracle containers
stopped — so the "Ensure shared PG + Oracle are up" CI step failed and the
release gate correctly blocked the publish. v14.5.1 ships the same Daily
Statement features as v14.5.0 below
(no product change) plus the CI fix.

Fixed — CI / runner resilience

  • The "Ensure shared PG + Oracle are up" step now docker rm -f's any stopped
    same-name leftover before docker run (a stopped ci-shared-pg/-oracle no
    longer exits 125 on a create name-conflict), and fails loudly with a rebuild
    pointer if the Oracle image is missing instead of a cryptic
    pull-access-denied. (Runner-side, the Oracle image is now
    recon-gen.keep-labeled and excluded from the prune cron so it can't be
    swept again.)

v14.4.1

16 Jun 19:58

Choose a tag to compare

Patch. v14.4.0 was tagged while the qs_browser test layer was red — the
Phase DK/DL overnight refactors had fallout that an exit-247 watchdog
hang was masking. This cut fixes the user-facing App2 bugs that surfaced
and stabilizes the CI suite. No new features.

Fixed — App2 (self-hosted dashboards)

  • Literal ' in descriptions. Sheet/visual descriptions with a
    quoted code span (e.g. L1 Pending Aging's `status='Pending'`)
    rendered a literal ' instead of '. _render_inline_markdown
    over-escaped the apostrophe with html.escape(quote=True) before the
    markdown pass, which python-markdown then re-escaped inside the code
    span. Fixed via quote=False (prose is text content, never an HTML
    attribute). The DK.11 literal-entity scanner now also pierces
    <code>/<pre> for the quote family so a recurrence fails a test.
  • Cross-sheet drills widen the destination date window again. Drilling
    "View Transactions for this transfer" (Pending / Unbundled Aging,
    Supersession Audit → Transactions) onto an OLD transfer landed on an
    empty table — App2 was dropping the drill's DrillStaticDateTime
    date-widen writes (the assumption "App2's date filter defaults to all
    rows" went false at the DK data-anchor refactor). App2 now applies
    static-value drill writes, restoring App2↔QS parity.

Test / CI stabilization (Phase DP)

  • Picker anchors + drill guardrails now exercise VALID rows instead of
    _spine_plant planted-error markers (which surfaced as picker anchors
    and drill rows after DL.3's marker sweep), eliminating the exit-247
    hang that had painted main red since the v14.4.0 cut.
  • The QS embed driver's empty-state read converts from a fixed 800ms
    one-shot to a bounded paint-poll, fixing the flaky
    dropdown_pickers_inverse_excludes_anchor[qs-*] restore step under
    concurrent-worker load.

(The original DL.3 leaf-drift → Daily-Statement picker-shape fix — drill
writing account_id vs the picker's account_display — shipped in
v14.4.0; v14.4.1 fixes the regressions the same overnight sweep layered
on top, now guarded by the parametrized cross-sheet-drill suite.)

v14.2.0

15 Jun 07:33

Choose a tag to compare

Minor bump. Phase DD lays down the OIDC code-flow login wiring on top
of DC's HTTPS surface. New capability — recon-gen studio /
recon-gen dashboards can gate every request through any
OIDC-compliant provider (Okta / Auth0 / AzureAD / Keycloak / Dex)
with zero per-tenant code, and the runner spins a real Dex container
for test/CI parity with the production posture.

What's new

  • src/recon_gen/common/html/auth.pyJwtCookieMiddleware
    decodes the recon_gen_session HMAC-SHA256 JWT on every request,
    branches on HX-Request to return 401 JSON vs 302 → /auth/login.
    Public-prefix bypass ("/auth/", "/static/", "/docs/", "/health").
    oauth_routes(...) builds the three /auth/{login,callback,logout}
    routes via authlib's starlette_client.OAuth. Logout follows
    RP-Initiated Logout via the IdP's end_session_endpoint discovery.
  • make_app(cfg=...) — auth wiring fires only when
    cfg.auth.oidc AND cfg.auth.session are both set. Absent
    block ⇒ HTTP-local-dev passthrough (no behavior change for existing
    deployments). New itsdangerous>=2.2 runtime dep (SessionMiddleware
    for authlib's PKCE/state round-trip storage).
  • src/recon_gen/_dev/oidc/ — 5-module Dex coordinator
    (secrets / container / config_writer / ensure / init). Spins or
    adopts the shared recon-gen-test-dex Docker container, mounts the
    DC.3-managed LE cert for HTTPS, injects per-run scrambled
    client_secret + bcrypt-hashed user password via Dex's
    secretEnv/hashFromEnv yaml fields. Issuer URLs locked at
    https://localdev.recon-gen.hotchkiss.io:5557/dex (DEV) /
    https://localci.recon-gen.hotchkiss.io:5556/dex (CI). Excluded
    from the published wheel; the Dex container + bcrypt deps live
    under the [dev] extra only.
  • OIDC_TOUCHING_LAYERS = ("app2",) + _ensure_oidc_if_configured
    pre-flight in cmd_up_to. Narrower than TLS — qs_browser uses
    auth-independent QS embed URLs. Hard-depends on DC.3; returns
    EXIT_NEEDS_OPERATOR with tls-setup.md hint when
    cfg.auth.oidc is set but cfg.app2.tls is None.
  • DashboardDriver OIDC verbs
    sign_in_via_oidc(*, email, password) /
    sign_out_via_oidc() /
    inspect_jwt_cookie(). App2 drives Dex v2.40.0's
    password.html + approval.html form selectors. QsEmbedDriver
    raises NotImplementedError for all three under a structured
    triple (raise-site comment cross-linking
    [[project_qs_embed_url_presigned_no_oidc]] +
    docs/reference/quicksight-quirks.md entry + memory file) — QS
    embed URLs are pre-signed at mint time so OIDC verbs have no QS
    analog.
  • docs/operations/oidc-setup.md — full operator runbook:
    three postures (HTTP local-dev / production OIDC / managed Dex),
    cfg block + field semantics, secrets convention
    (run/secrets.env continuation), IdP-side client registration
    recipe, CI GitHub-secrets recipe, eight troubleshooting recipes,
    rotation procedure.
  • .github/workflows/ci.ymlGenerate random OIDC credentials
    step + cfg-overwrite heredoc carrying auth.oidc: + auth.session:
    blocks. Three new GitHub secrets surface (OIDC_CLIENT_SECRET /
    JWT_SECRET / DEX_USER_PASSWORD) but the workflow's openssl rand
    step generates fresh per-run values today; the secrets are
    configured for future pinning.
  • tests/e2e/app2/test_oauth_login_flow.py — 7-test browser e2e
    pinning the JwtCookieMiddleware contract through Playwright
    (unauth 302 / HX-Request 401 / valid JWT renders / cookie
    inspection / sign-in idempotency / sign-out clears cookie /
    tampered JWT 401). Skips when cfg.auth.oidc / cfg.auth.session
    absent (cfg-shape skip — POLICY 1 consistent across CI / local).

Env-var surface

Four new RECON_GEN_* env vars register with the typed registry:

  • RECON_GEN_OIDC_CLIENT_SECRET — OIDC client secret (named via
    cfg.auth.oidc.client_secret_env).
  • RECON_GEN_JWT_SECRET — local session JWT HMAC signing key (named
    via cfg.auth.session.jwt_secret_env).
  • RECON_GEN_DEX_USER_PASSWORD — plaintext password for the static
    Dex test user (testuser@example.com).
  • RECON_GEN_DEX_URL — short-circuit for the pre-spun shared-Dex
    pattern (registered, not yet wired into ci.yml; reserved for the
    future shared-CI-Dex step).

No breaking changes

Existing deployments with no auth: block in their cfg yaml see
zero behavior change — the middleware short-circuits to passthrough
and the new env-var registrations are inert. To opt in: add the two
blocks per docs/operations/oidc-setup.md Step 2.

Carry-along fixes

  • Date-picker value-commit needs Tab, not Enter
    (docs/reference/quicksight-quirks.md AA.A.l2ft-date-commit). QS's
    ParameterDateTimePicker React onChange wiring listens for
    focus-loss, not Enter — same root cause as the σ-slider
    commit-needs-Tab pattern already documented inline. Surfaced by
    the test_l2ft_additive_pickers_keep_anchor_row[qs-Rails] triage
    (5,551 rows post-filter vs the expected ≤9). Backlog #83 carries
    the typed-wrapper follow-up.
  • Bulk-insert helpers — bulk_insert_tx / bulk_insert_balance
    gain an optional columns parameter so ETL integrators can
    bulk-load schema columns the spine-author defaults exclude
    (transfer_completion / bundle_id for transactions;
    supersedes for balances). _coerce_to_cents_int now accepts
    str (CSV bulk loads — csv.DictReader lands every column as a
    string) and raises TypeError on unknown types instead of silent
    passthrough that surfaced as opaque downstream BIGINT INSERT
    failures.

v14.0.1

14 Jun 22:54

Choose a tag to compare

Patch release. v14.0.0 was the intended Phase DE cut but release CI
caught a hardcoded test assertion (test_run_postgres_config_smoke
asserted the operator's local deployment_name="qsgen-postgres"; the
CI workflow rewrites run/config.postgres.yaml with a per-run
qs-ci-<run-id>-pg value before tests fire — POLICY 1 divergence).
The over-specified assertion was dropped; the dialect check (which
survives CI's overwrite) remains. No runtime behavior change vs
v14.0.0; pip-install v14.0.1 to skip the broken-release-attempt tag.

Paired into this cut: README v14 cfg-shape callout + chain layer
string + triage verb; docs/specs/SPEC_studio.md field names; Phase
DE swept to PLAN_ARCHIVE.md.

v13.15.1

13 Jun 13:34

Choose a tag to compare

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
    ...
Read more

v13.15.0

12 Jun 21:06

Choose a tag to compare

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.

v13.14.4

12 Jun 14:31

Choose a tag to compare

Forty-eight commits since v13.14.3. Three load-bearing themes plus
infrastructure cleanup.

1. Phase BX close — editor polish + role-reframe row. The L2
editor's open cells from the 2026-05 cold-read land:

  • BX.3 Rail list table view (?view=table session toggle, grouped
    by source role with single-leg rails grouping by their leg_role
    too; toggle anchors strip embed=1 so a click from a home-section
    embed lands on the chromed standalone page, not the bare fragment).
  • BX.4 Read-card sections mirror the edit form's grouping (Identity
    / Classification / Topology / etc.) instead of one flat <dl>.
  • BX.9 Theme editor reorder — essentials (accent / secondary / logo)
    at top with a live-preview card and 300ms-debounced auto-save on
    blur; everything else collapses into <details>Advanced</details>,
    default-closed at DEFAULT_PRESET, default-open when an L2 has
    non-default values.
  • BX.14 Domain-flavor banking phrasing for every L2 validator error
    • [?] glossary triggers in the rejected-save banner, sourced from
      8 new GLOSSARY entries keyed by error code family.
  • BX.15 [?] chip tooltips next to Coverage + Trainer toggles on
    the diagram sidebar.
  • BX.16 Inline chain shape-preview below the children chip-list on
    chain edit pages (reuses the BX.8 mini-diagram wasm-graphviz
    renderer; fires on load so the initial paint pre-populates from
    the form's current parent value).
  • BX.17 Polish cluster — duration picker quick-select chips
    (Instant / 1h / EOD / Next-day + free-text), reference-panel
    default-open behavior on empty list pages only, completion-DSL
    autocomplete.

The persona audit confirmed BXa absorbed all live references; two
dead-code refs cleaned up (_VALID_KINDS retightened to
frozenset[EntityKind]; unreachable ("theme", "persona") ternary
removed).

2. Untyped-enum sweep — typed primitives for the five most-touched
enum surfaces.
Per a fresh audit
(docs/audits/untyped_enum_audit_2026_06_11.md), L2 primitives carried
five enums as bare str despite stable schema CHECKs (or no CHECK at
all). Sweep landed in staged commits for bisect-ability:

  • AmountDirection = Literal["Debit", "Credit"] (closed); ~12 spine
    writers + etl.write_transaction annotated.
  • POSTED_STATUS: Final = "Posted" (half-open per operator lock —
    "Posted" is canonical materialized state; other values stay
    integrator-extensible; no CHECK added).
  • Scope threaded through 4 spine generators that were dropping to
    bare str (chain_completion / failed_transaction / supersession /
    inv_fanout).
  • SupersedeReason annotated on _txn_row{,_tuple} and supersession
    writers (str | NoneSupersedeReason | None).
  • OriginORIGIN_INTERNAL_INITIATED, ORIGIN_EXTERNAL_FORCE_POSTED,
    ORIGIN_EXTERNAL_AGGREGATED Final constants (half-open like
    status; the survey also surfaced + fixed a typo bug in
    inv_fanout.py:222,241 that used "ExternalInitiated" — not in
    the canonical set per fuzzer + ETL walkthrough; corrected to
    ORIGIN_EXTERNAL_AGGREGATED. SPEC docs synced).

A new no-raw-enum-equality AST lint sits at the test boundary so
raw-string comparisons against the canonical values can't regress
(extended through the constructor-input sweep — 187 raw kind=/scope=/
origin= etc. kwargs across 25 test files now carry typed constants).

3. BX.new.list-cascade-reload HTMX-inheritance silent swap bug.
The biggest single fix this cycle. The standalone list page's
cascade-reload wrapper carried hx-select="#list-page-body", which
HTMX 1.9 inherits to all descendants by default. Every Delete / Edit /
save / toggle button inside the wrapper picked up the inherited
hx-select. When the Delete button fired its hx-get to /delete-confirm,
HTMX applied the inherited select against the countdown HTML response
— there is no #list-page-body in that response, so the swap silently
emptied the target. afterSwap fires successfully; the wrapper just
gets cleared with nothing in. ~3h of MutationObserver tracing to find.

Fix: hx-disinherit="*" on the cascade-reload wrapper. Documented as a
generic quirks-log entry in docs/reference/quicksight-quirks.md
this is exactly the silent-success failure mode the docs file exists to
catch.

Test infrastructure + audits. Five smaller landings:

  • CB.5 deletion + runner collapse — the two deprecated agreement
    test files (test_inv_dashboard_agreement.py + test_audit_dashboard_agreement.py)
    carrying "CB.5 follow-up deletes the file entirely" comments since
    2026-05 finally get deleted (1,835 lines), with a 13-file orphan
    reference sweep. The runner's qs_browser layer's two-invocation
    split (the agree_file Oracle-DDL-race carve-out) collapses to one.
  • BK.6 / #35LimitBreach and InboundCapBreach plants on cust1
    • cust2 (was cust1-only) so the L1 inverse-picker test has ≥2
      distinct account_id values. Calendar-drift footgun caught the next
      day: days_ago=8 sat at the boundary of a 7-day window and broke
      at the day rollover; clamped to 5 and filed as backlog for a
      bounded days_into_window type (see date_range_model_audit.md §10).
  • QS typeahead_filter — non-empty-query branch landed on
    QsEmbedDriver per the code comment that described the pattern;
    unblocks 4 dispatch-level skips. The picker tests themselves were
    already calling filter_options / pick_filter, so the actual
    skip reclaim is narrower than the triage claimed.
  • Three audit docs landed — untyped enum surface, no-raw-str-args
    lint blast radius (1,364 hits across the corpus; recommended
    Option A scoped to 39 enum-shaped names + treat as its own future
    phase), QS Browser skip triage (117 skips → 40 recoverable across
    8 buckets).
  • PG snapshotter pgcrypto FileLock refactor — already shipped in
    v13.14.2, now fully exercised; one corner-case stale-tab race
    caught + fixed.

Operator-facing UX rotation worth flagging. Delete behavior went
through three iterations this cycle in response to live dogfood:
top-of-page confirm banner → in-place button swap with countdown +
Cancel + reason tooltip → terser "In use" button text + dropped
Cancel + smaller tooltip → text-on-button only ("Delete" / "In Use"),
no countdown tooltip. The cumulative result is a Delete UX that
indicates state via button text + position rather than a separate
hover/popup affordance. Browser e2e tests cover all three render
paths.

Two clusters land together:

Phase CT — Oracle 19c plant flow fix (user-reported #197 /
CS.10.followup #328).
Two date-literal footguns where bare ISO-8601
strings were used as TIMESTAMP comparands. Oracle 19c rejects with
ORA-01843 ("not a valid month") — its NLS_TIMESTAMP_FORMAT default
doesn't auto-coerce that shape. PG + DuckDB happened to accept it via
implicit coercion, so the bug was Oracle-only and silently swallowed
inside the trainer Apply flow (the DELETE error'd out before reaching
the matview, leaving the plant a no-op).

Fix: use date_literal(iso, dialect) from common/sql/dialect.py
DATE 'YYYY-MM-DD' (portable across PG / DuckDB / Oracle).

Affected callsites:

  • common/l2/plant_registry.py::_invoke_balance_cadence_gap_plant
    — the user-reported trigger
  • common/l2/deploy_pipeline.py::_build_generator_sql (X.4.h trainer
    cutoff / scrub-head feature) — sibling bug, same shape

Verification: Oracle 19c trainer plant matrix went from 7/16 passed
to 8/16 passed (the formerly Oracle-only skip on
balance_cadence_gap is gone). Other 8 remain universally skipped
under BV.3.3.c.bug4-followup (chain-coherence dashboard rendering,
not Oracle-specific). Two new unit gates pin the typed DATE 'YYYY-MM-DD' shape across all three dialects so the regression
can't reappear.

Two CI-stabilization companions landed alongside CT.0:

  • CT.1App2Driver.pick_filter now peeks cur vs the resolved
    target BEFORE running the action; when equal, skip the 30s
    _wait_for_refetch (setValue is a no-op, no change fires, no
    refetch comes). Defensive against any picker test that hits the
    already-at-target case.
  • CT.2 — Narrow pytest.skip on the [app2] parametrize of
    test_inv_drilldown.py::test_account_network_table_walk_rerenders_table
    pending backlog #331 (App2 Anchor parameter pick fires change but
    no /visuals/*/data refetch on CI; [qs] variant continues to
    gate the same K.4.8 invariant on the production renderer).

Phase CS — 14 backlog items (CR follow-throughs + operator-visible
polish + Oracle/Studio bug bash). Highlights:

  • CS.1 — Removed dead SQLite plumbing surfaced by no-sqlite-prose
    lint (CB.8 cleanup). DB connection-leak gate renamed to
    RECON_GEN_DB_CONN_LEAK_GATE.
  • CS.2 — Re-lit test_inv_drilldown after CR.6.a; now uses
    drill_from_first_row_via_menu to match the production
    DATA_POINT_MENU trigger + row-content assertion (catches the
    K.4.8f-3 no-op shape that a count-based assertion would miss).
  • CS.4 — Sankey node-cap operator visibility: subtitles name the
    cap on Investigation + L2FT Sankeys; caps aligned at 50.
    Dynamic banner deferred to CS.4.followup #326.
  • CS.6 — Reordered L1 + L2 dashboards: Exceptions sheet promoted
    to position 2 (right after Getting Started) so operator's daily
    triage flow lives above lower-volume secondary surfaces. SheetIds
    unchanged so deep-link URLs still resolve.
  • CS.7 — L1 Exceptions copy: prose said 10 invariants; matview
    has 12 (post-CL.6 + AB-era chain-coherence). New
    L1_EXCEPTIONS_BRANCH_NAMES typed tuple is now the single source
    of truth; unit gate parses the matview SQL and asserts the
    bidirectional match so the count can't drift again.
  • CS.8 — Themed 503 page for PoolReleasedDuringRefresh (the
    CO.x DuckDB writer-lock fix's transient-state surface). Operator
    sees a calm "Data refresh in progress" with auto-reload meta
    refresh instead of an untemplated 500.
  • CS.9 — `RECON_...
Read more

v13.14.3

11 Jun 22:58

Choose a tag to compare

Release v13.14.3

v13.14.2

11 Jun 18:24

Choose a tag to compare

Release v13.14.2

v13.14.1

11 Jun 06:21

Choose a tag to compare

Release v13.14.1