Releases: chotchki/recon-gen
v14.5.1
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 beforedocker run(a stoppedci-shared-pg/-oracleno
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
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 withhtml.escape(quote=True)before the
markdown pass, which python-markdown then re-escaped inside the code
span. Fixed viaquote=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'sDrillStaticDateTime
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_plantplanted-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
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.py—JwtCookieMiddleware
decodes therecon_gen_sessionHMAC-SHA256 JWT on every request,
branches onHX-Requestto 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'sstarlette_client.OAuth. Logout follows
RP-Initiated Logout via the IdP'send_session_endpointdiscovery.make_app(cfg=...)— auth wiring fires only when
cfg.auth.oidcANDcfg.auth.sessionare both set. Absent
block ⇒ HTTP-local-dev passthrough (no behavior change for existing
deployments). Newitsdangerous>=2.2runtime 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 sharedrecon-gen-test-dexDocker container, mounts the
DC.3-managed LE cert for HTTPS, injects per-run scrambled
client_secret+ bcrypt-hashed user password via Dex's
secretEnv/hashFromEnvyaml 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 incmd_up_to. Narrower than TLS —qs_browseruses
auth-independent QS embed URLs. Hard-depends on DC.3; returns
EXIT_NEEDS_OPERATORwithtls-setup.mdhint when
cfg.auth.oidcis set butcfg.app2.tlsisNone.- 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.htmlform selectors.QsEmbedDriver
raisesNotImplementedErrorfor all three under a structured
triple (raise-site comment cross-linking
[[project_qs_embed_url_presigned_no_oidc]]+
docs/reference/quicksight-quirks.mdentry + 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.envcontinuation), IdP-side client registration
recipe, CI GitHub-secrets recipe, eight troubleshooting recipes,
rotation procedure..github/workflows/ci.yml—Generate random OIDC credentials
step + cfg-overwrite heredoc carryingauth.oidc:+auth.session:
blocks. Three new GitHub secrets surface (OIDC_CLIENT_SECRET/
JWT_SECRET/DEX_USER_PASSWORD) but the workflow'sopenssl 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 whencfg.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
viacfg.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 intoci.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.mdAA.A.l2ft-date-commit). QS's
ParameterDateTimePickerReactonChangewiring listens for
focus-loss, not Enter — same root cause as the σ-slider
commit-needs-Tab pattern already documented inline. Surfaced by
thetest_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 optionalcolumnsparameter so ETL integrators can
bulk-load schema columns the spine-author defaults exclude
(transfer_completion/bundle_idfor transactions;
supersedesfor balances)._coerce_to_cents_intnow accepts
str(CSV bulk loads —csv.DictReaderlands every column as a
string) and raisesTypeErroron unknown types instead of silent
passthrough that surfaced as opaque downstream BIGINT INSERT
failures.
v14.0.1
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
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 viaIntl.NumberFormatUSD. - QS BarChart count() axis label fix.
_axis_label_apply_to
now mirrors BL.1'scount() → _row_one_*field-well rewrite so
ApplyTo.Column.ColumnNamematches the well. QS was silently
dropping thevalue_labeloverride 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 sopage.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/shmsaturation (per-container 64MB
tmpfs default) under xdist-n 4concurrent 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_sessionfinishwith a clearly-marked summary block.
Raisessession.exitstatusto 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 viapg_matviews/pg_views/pg_indexes/
pg_tables(PG) or the equivalentuser_*catalogs (Oracle)
and drops withCASCADEin dependency-safe order. Idempotent- cleans cross-run debris in O(N) per kind. DuckDB no-op.
- DG.3 ci-shared-pg
--shm-size=2gon 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/shmpressure 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_pickableswitchesfilter_options→
typeahead_filter(virtualized MUI Autocomplete dropdowns
only mount ~12 alphabetical options on open; late-alphabet
accounts likeZBASubAccount/WireSettlementSuspense
silently fell out of the membership check).wait_for_dropdown_options_presentcatches innerTimeoutError- retries so the outer 15s budget actually does what it says
(was a single 2s inner failure propagating).
- retries so the outer 15s budget actually does what it says
_open_control_dropdowndoes 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. TreeValidatorsettle-retry loop. Recipient Fanout's 3
distinct_count()KPIs lag the Table's mount under CI load;
single-shot diff after per-titlewait_loadedmissed 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
renderTableignoredconditional_formattingentirely, 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_MENUdrills. Zero usage ofCellAccentMenuanywhere.
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
fromon.columnat 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_MENUdrill writes from the column → accent text- accent-tint background
- only
DATA_POINT_CLICKdrill(s) → accent text only
DroppedCellAccentText+CellAccentMenu+ theCellFormatunion
common/clickability.py+tests/unit/test_clickability.py—
pre-stable posture, no compat shim.
- any
- App2 renderer plumbing.
_VisualPlangrows
column_decoration: Mapping[str, str];_table_column_metawalks
visual.conditional_formattingand resolves the per-column kind via
the sameDrillable.visual_kind(drills)code path the QS-side
Drillable.emituses — App2 ≡ QS by construction.
_data_shape.shape_tableforwards ascolumn.decoration;bootstrap. js::renderTablepaintscell-accent/cell-accent-menuCSS
classes on each<td>. - CSS + cell-click affordance.
widgets-theme.cssadds
.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::wireRowDrillsbinds 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
CellAccentTextcallsites in
apps/l1_dashboard/app.pymigrated toDrillable. Class C wires:
Transactions Audittransfer_id→ Posting Ledger (uses the existing
_DP_TX_TRANSFERlanding pad +_wide_date_writes()pattern); Daily
Balances Auditdb_account_id→ Daily Statement; Posting Ledger
account_id→ Daily Statement (addedbusiness_daycolumn to
l1-transactions-dsviadate_trunc_day('posting', dialect)at
SELECT time, taggedColumnShape.DATETIME_DAYso the drill writes a
day-grain date). Class C strip: Posting Ledgertransfer_id
(self-drill). Class D adds: L2FT Violation Detailentity_a;
Investigation Account Network — Touching Edgescounterparty. - Type-system gate at
Table.__post_init__(per
[[feedback_invariants_in_types]]). Walks
conditional_formatting × actions; for eachDrillable.on.column,
asserts ≥1 drill writes from that column. RaisesValueErrorat
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
...
v13.15.0
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
renderTableignoredconditional_formattingentirely, 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_MENUdrills. Zero usage ofCellAccentMenuanywhere.
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
fromon.columnat 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_MENUdrill writes from the column → accent text- accent-tint background
- only
DATA_POINT_CLICKdrill(s) → accent text only
DroppedCellAccentText+CellAccentMenu+ theCellFormatunion
common/clickability.py+tests/unit/test_clickability.py—
pre-stable posture, no compat shim.
- any
- App2 renderer plumbing.
_VisualPlangrows
column_decoration: Mapping[str, str];_table_column_metawalks
visual.conditional_formattingand resolves the per-column kind via
the sameDrillable.visual_kind(drills)code path the QS-side
Drillable.emituses — App2 ≡ QS by construction.
_data_shape.shape_tableforwards ascolumn.decoration;bootstrap. js::renderTablepaintscell-accent/cell-accent-menuCSS
classes on each<td>. - CSS + cell-click affordance.
widgets-theme.cssadds
.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::wireRowDrillsbinds 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
CellAccentTextcallsites in
apps/l1_dashboard/app.pymigrated toDrillable. Class C wires:
Transactions Audittransfer_id→ Posting Ledger (uses the existing
_DP_TX_TRANSFERlanding pad +_wide_date_writes()pattern); Daily
Balances Auditdb_account_id→ Daily Statement; Posting Ledger
account_id→ Daily Statement (addedbusiness_daycolumn to
l1-transactions-dsviadate_trunc_day('posting', dialect)at
SELECT time, taggedColumnShape.DATETIME_DAYso the drill writes a
day-grain date). Class C strip: Posting Ledgertransfer_id
(self-drill). Class D adds: L2FT Violation Detailentity_a;
Investigation Account Network — Touching Edgescounterparty. - Type-system gate at
Table.__post_init__(per
[[feedback_invariants_in_types]]). Walks
conditional_formatting × actions; for eachDrillable.on.column,
asserts ≥1 drill writes from that column. RaisesValueErrorat
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 toparents[2]/docs/_handbook_per_sheet/
so it resolves identically in repo-checkout AND wheel-install. Mkdocs
configured toexclude_docs: _handbook_per_sheet/so the per-sheet
snippets stay out of the curated site build. - QS uppercase-hex fix.
_tint_hexemits{: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 fixedtop: 8pxwhich 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
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=tablesession toggle, grouped
by source role with single-leg rails grouping by theirleg_role
too; toggle anchors stripembed=1so 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 onloadso 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_transactionannotated.POSTED_STATUS: Final = "Posted"(half-open per operator lock —
"Posted" is canonical materialized state; other values stay
integrator-extensible; no CHECK added).Scopethreaded through 4 spine generators that were dropping to
barestr(chain_completion / failed_transaction / supersession /
inv_fanout).SupersedeReasonannotated on_txn_row{,_tuple}and supersession
writers (str | None→SupersedeReason | None).Origin—ORIGIN_INTERNAL_INITIATED,ORIGIN_EXTERNAL_FORCE_POSTED,
ORIGIN_EXTERNAL_AGGREGATEDFinalconstants (half-open like
status; the survey also surfaced + fixed a typo bug in
inv_fanout.py:222,241that 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 (theagree_fileOracle-DDL-race carve-out) collapses to one. - BK.6 / #35 —
LimitBreachandInboundCapBreachplants 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=8sat at the boundary of a 7-day window and broke
at the day rollover; clamped to5and filed as backlog for a
boundeddays_into_windowtype (seedate_range_model_audit.md §10).
- cust2 (was cust1-only) so the L1 inverse-picker test has ≥2
- QS typeahead_filter — non-empty-query branch landed on
QsEmbedDriverper the code comment that described the pattern;
unblocks 4 dispatch-level skips. The picker tests themselves were
already callingfilter_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 triggercommon/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.1 —
App2Driver.pick_filternow peekscurvs the resolved
target BEFORE running the action; when equal, skip the 30s
_wait_for_refetch(setValue is a no-op, nochangefires, no
refetch comes). Defensive against any picker test that hits the
already-at-target case. - CT.2 — Narrow
pytest.skipon 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/*/datarefetch 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_drilldownafter CR.6.a; now uses
drill_from_first_row_via_menuto match the production
DATA_POINT_MENUtrigger + 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_NAMEStyped 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_...
v13.14.3
Release v13.14.3
v13.14.2
Release v13.14.2
v13.14.1
Release v13.14.1