Skip to main content
Version: v0.1.7

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_PROPOSAL is explicitly rejected by is_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 fix payload 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_patch row with source='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: SELECT or WITH … SELECT only. Word-boundary regex rejects DROP / DELETE / INSERT / UPDATE / TRUNCATE / ALTER / CREATE / GRANT / REVOKE / COPY / VACUUM / MERGE etc., but lets a column named delete_flag through.
  • 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 with response_format=json_object. Stamps the FE-ready fix envelope including destructive=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_trait wraps the user's rubric in the same JSON-shape envelope as reference / expectations so the parse + verdict pipeline stays uniform.
  • agent_test_runner._grade fans 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} accepts custom_traits: CustomTrait[] and persists via CAST(:custom_traits AS jsonb). Pre-migration tenants get a fallback retry without the column so the rest of the PATCH still lands.
  • AgentReviewPage Settings 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_at survives, only refreshed_at moves forward on re-runs.
  • GET /api/catalog/assets?source=&asset_type= — reads from the cache. /feeds still hits the live adapter when you want fresh data; /assets is 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 + empty list_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-pulse skeleton that approximates any card type (KPI / chart / table) and fills the same flex-1 box the chart will use.
  • Friendlier error state on CardRef — muted alert icon + readable line break + red-500 text 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. Now opacity-40 by default, pops to opacity-100 on 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

  1. Target label alignment — default switched from self-end (right) to self-center. New card_config.target_align knob lets a card override to left or right explicitly.
  2. 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-0 so any future override that exceeds card width clips rather than bleeding into neighbours.
  3. WebappBuilderPage horizontal overflow — the grid container measured <main>.clientWidth (1700+ px on wide screens) but the visible canvas is wrapped in max-w-[1400px] mx-auto. Right column of cards bled past the edge. containerWidth is now clamped to min(main.clientWidth - 48px padding, 1400px).
  4. Page Info aside field clipping — single-line fields used truncate, hiding the tail of long Bahasa subtitles. New default is break-words; multiline keeps whitespace-pre-wrap for embedded newlines. Container gets min-w-0 so flex ancestors don't force overflow.