v0.0.80 — Auto-apply diagnose, LLM proposals, custom traits, catalog persistence
Released: 2026-05-19. Twelve commits.
Closes several long-running tracks. The Diagnose loop reaches slice 3 (opt-in silent auto-apply for the deterministic-safe finding code) and slice 2c (structured LLM fix proposals with a strict SELECT-only safety gate). Agent Reviews gains custom traits (capability #19). The Catalog graduates to Phase 3 persistence with a refreshable cache, and the four remaining adapter shells (Dataiku / Kafka / SSIS / Informatica) get file-inventory readers.
Diagnose — slice 3 silent auto-apply
POST /diagnose now accepts ?auto_apply=true. When set, any finding whose code is on card_diagnose.AUTO_APPLY_CODES is silently applied inline before the response goes back. Today the allowlist is exactly one entry — MOCK_STUCK_WITH_REAL_SQL — and the inline handler issues a direct UPDATE against dashboard_cards.card_config; no HTTP round-trip through the regular PATCH /cards path.
Hard safety floor:
LLM_PROPOSALis explicitly rejected byis_auto_applicable()so a future allowlist edit can't accidentally let LLM-authored SQL auto-mutate cards. The propose surface (slice 2c, below) keeps explicit operator confirmation in the loop forever.- Findings without a
fixpayload skip auto-apply even when their code is on the list. - Failed inline applications leave the finding in the response so the operator sees it + can retry manually; the response never silently swallows an error.
- Telemetry: every auto-applied fix writes a
card_autofix_patchrow withsource='diagnose_auto:<code>'so the conversion pivot can isolate auto vs manual fix paths.
Frontend (DashboardDetailPage): Auto-fix safe issues checkbox in the drawer header, sticky via localStorage. Off by default — explicit opt-in per operator. Green Auto-fixed N issues banner at the top of the body lists every code that was silently applied.
Diagnose — slice 2c structured LLM fix proposals
The Diagnose drawer now has three LLM surfaces (narrative / suggest / propose), each answering a different question. Slice 2c is the only path that returns a fix payload the operator can apply with one click — and it's gated by a strict SELECT-only validator so an LLM-hallucinated DDL/DML statement never makes it past the server.
services/card_propose.py:
validate_sql_only()— allow-list:SELECTorWITH … SELECTonly. Word-boundary regex rejectsDROP/DELETE/INSERT/UPDATE/TRUNCATE/ALTER/CREATE/GRANT/REVOKE/COPY/VACUUM/MERGEetc., but lets a column nameddelete_flagthrough.parse_proposals()— tolerates code-fenced / prose-wrapped JSON, caps at 3 entries, silently drops anything that fails the validator (operators never see a half-validated button).propose_fixes()— LLM call at temp 0.2 withresponse_format=json_object. Stamps the FE-ready fix envelope includingdestructive=True, so the Apply button always confirms.
POST /dashboards/{id}/cards/{cid}/diagnose/propose — same auth + telemetry shape as /diagnose/suggest, distinct TOOL_PROPOSE = "card_propose" enum so engineering can pivot proposal → apply conversion separately from rule-based fixes.
Frontend: sibling state for diagnoseProposals so proposals render through the same row component as rule-based findings, but in a green-themed panel below the purple "Look harder" suggestions block. applyDiagnoseFix accepts an optional codeOverride so proposals can be tagged as diagnose_drawer:LLM_PROPOSAL in autofix telemetry.
Agent reviews — custom traits (#19)
Adds a JSONB custom_traits column to agent_reviews carrying user-defined evaluation dimensions. Each entry shaped {"name": "<trait>", "prompt": "<rubric the LLM judge uses>"}.
agent_judge.judge_custom_traitwraps the user's rubric in the same JSON-shape envelope asreference/expectationsso the parse + verdict pipeline stays uniform.agent_test_runner._gradefans out one judge call per registered trait in parallel with the built-in pair. Reserved names (reference,expectations) are silently dropped server-side so a client that bypasses the UI can't shadow the built-ins; duplicates are deduped last-wins.PATCH /agent-reviews/{rid}acceptscustom_traits: CustomTrait[]and persists viaCAST(:custom_traits AS jsonb). Pre-migration tenants get a fallback retry without the column so the rest of the PATCH still lands.AgentReviewPageSettings tab: add / remove / edit traits inline; client-side validation mirrors the server.
Catalog — Phase 3 persistence + refresh
New honeyframe.catalog_assets table (PK source_instance + fqn) and two endpoints:
POST /api/catalog/sources/:source/refresh— crawls one adapter and upserts every asset, returning inserted / updated / total counts. Idempotent —discovered_atsurvives, onlyrefreshed_atmoves forward on re-runs.GET /api/catalog/assets?source=&asset_type=— reads from the cache./feedsstill hits the live adapter when you want fresh data;/assetsis the indexable, paginated path for the cockpit UI to scroll through 10K+ rows without re-crawling.
Migration 2026-05-19_catalog_assets.sql registered in migrate.py.
Catalog — file-inventory readers for 4 adapters
Phase 1 of each adapter (Dataiku / Kafka / SSIS / Informatica) now reads a JSON inventory the customer exports out-of-band, so the Catalog page renders real assets instead of an empty list for every non-dbt source. Phase 2 will replace list_assets with the live extractors each tool needs (Dataiku REST, Kafka Schema Registry + Connect, .dtsx XML, Informatica pmrep) — the public name / contract is unchanged so callers don't break.
Shared base in adapters/_inventory.py::FileInventoryAdapter:
- Inventory shape:
{namespace, default_owner, assets: [{name, schema, asset_type?, description?, owner?, tags?}]}. - FQN convention preserved:
<tool>.<namespace>.<schema>.<name>. - Cache invalidates on mtime so editing the inventory takes effect without restarting the service.
- Missing path →
health.reachable=False+ emptylist_assets, never raises (parity with the dbt adapter).
Thirty-six new parametrized unit tests cross-validate all 4 adapters against the same contract.
Webapp — filter-aware header chips + live SQL backing
GET /api/webapps/:key/pages/:page_key/header-signals runs optional header_meta_sql / header_alert_sql on a page and returns the resolved strings. Webapp.tsx fetches signals on page change and uses them when present, falling back to the static config strings. Lets a page's alert track real data (e.g. a "N branches critical" count derived from a per-tenant fact table) without redeploying when the underlying count changes.
The endpoint is also POST, accepting active filter params from FilterBar in the body. SQL referencing :branch_id_codes (comma-joined branch codes from BranchSelect's companion publish) gets the filter bound; null binds quietly so (NULLIF(:p,'') IS NULL OR branch_code = ANY(string_to_array(:p,','))) falls through to "show all" when nothing is selected. HeaderChips is extracted into its own component inside the WebappFilterProvider so it can subscribe to the debounced params and re-fetch when the user changes the branch dropdown.
Webapp builder — editable Page info aside
subtitle / footnote / header_meta / header_meta_sql / header_alert / header_alert_sql / header_alert_severity are now inline-editable in the right rail of /webapp/:key. EditField commits on blur so a stray keystroke can't spam the dirty-state.
Webapp — UX polish
- Skeleton loader — a 12-card page previously rendered 12 "Loading…" text labels while hydrating, then snapped to data. Replaced with a 3-bar
animate-pulseskeleton that approximates any card type (KPI / chart / table) and fills the sameflex-1box the chart will use. - Friendlier error state on
CardRef— muted alert icon + readable line break +red-500text instead of the previous one-line red string. - Sticky page header in the webapp canvas — Executive Overview / Branch Focus title stays pinned while the user scrolls.
- Pages list keyboard navigation — ↑/↓ walks pages from anywhere in the editor. Skipped when focus is in an
INPUT/TEXTAREA/ contenteditable or when a modifier key is held. - Drag-handle / delete affordance more discoverable — controls were
opacity-0 group-hover:opacity-100, completely invisible until hover. Nowopacity-40by default, pops toopacity-100on hover. - Friendlier empty state on cards — replaced bare "No data" with a muted icon + helper sentence. Card authors can override via
card_config.empty_state_message.
KPI hero — three polish fixes
- Target label alignment — default switched from
self-end(right) toself-center. Newcard_config.target_alignknob lets a card override toleftorrightexplicitly. - Target label overlapping the next card on narrow layouts — footer is now a 2-row column: row 1 holds the pct-of-target badge + delta (left-aligned); row 2 holds the Target label (right-aligned within the card). Card root gets
overflow-hidden+min-w-0so any future override that exceeds card width clips rather than bleeding into neighbours. - WebappBuilderPage horizontal overflow — the grid container measured
<main>.clientWidth(1700+ px on wide screens) but the visible canvas is wrapped inmax-w-[1400px] mx-auto. Right column of cards bled past the edge.containerWidthis now clamped tomin(main.clientWidth - 48px padding, 1400px). - Page Info aside field clipping — single-line fields used
truncate, hiding the tail of long Bahasa subtitles. New default isbreak-words; multiline keepswhitespace-pre-wrapfor embedded newlines. Container getsmin-w-0so flex ancestors don't force overflow.