# Changelog All notable changes to Agnes AI Data Analyst. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html), pre-1.0 — public surface (CLI flags, REST endpoints, `instance.yaml` schema, `extract.duckdb` contract) may shift between minor versions; breaking changes called out under **Changed** or **Removed** with the **BREAKING** marker. CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every CI build; semver tags (`v0.X.Y`) are cut at release boundaries and reference the same commit as a `stable-*` tag from the same day. --- ## [Unreleased] ## [0.54.20] — 2026-05-15 ### Added - **Corporate Memory — BM25 relevance ranking on knowledge search.** Replaces the `title ILIKE '%q%' OR content ILIKE '%q%'` ranked-by-insertion-order query in `KnowledgeRepository.search` with DuckDB's `fts` extension (BM25). Czech queries match across diacritics (`cesky` → `česky`) via `strip_accents=1` + `lower=1`. Schema v47 builds the initial index over `knowledge_items(title, content)`; per-mutation rebuild fires only when `title` / `content` change (status flips skip). The lifespan in `app/main.py` rebuilds once at boot as a safety net for restarts on v47. Result rows now carry a `bm25_score` column (always present — `None` on the ILIKE fallback for shape uniformity). When the `fts` extension can't be loaded (offline / sandboxed install) **or** the index is missing (migration soft-fail, concurrent `overwrite=1` rebuild's drop-then- create window), `search` and `count_items` transparently fall through to the pre-#121 ILIKE query — same result-set membership, ordering regresses to `updated_at DESC`. Closes #121. - **Corporate Memory — bulk-edit batch bar on the All Items tab.** Symmetric to the Review-tab bar shipped in #126; row checkboxes, "Select all" header, and the five bulk-edit actions (Move to category / Move to domain / Add tag / Remove tag / Set audience) now appear on `/corporate-memory-admin` All Items as well. Approve / Reject stay scoped to Review per #129's scope decision (status flips belong with the per-row actions or the keyboard workflow). Closes #129. ### Internal - **Schema v47** — adds DuckDB `fts` BM25 index over `knowledge_items(title, content)`. Auto-migrates on first boot; soft-fails to ILIKE if the extension repo is unreachable. Index is a snapshot — see `src/fts.py` for the on-mutation / lifespan rebuild contract. ## [0.54.19] — 2026-05-15 ### Changed - `connectors/jira/scripts/consistency_check.py` — `AUTO_FIX_THRESHOLD` bumped from 10 to 20. Auto-backfill now covers typical SLA-poller hiccups before escalating to ERROR. `WARNING_THRESHOLD` unchanged. ### Fixed - **`connectors/jira` — transient Jira API failure no longer wipes existing `remote_links` parquet rows.** Pre-fix, all three `fetch_remote_links` sites (`service.py`, `scripts/backfill.py`, `scripts/backfill_remote_links.py`) silently returned `[]` on 401/403/429/5xx or `httpx.RequestError`. Callers overlaid that `[]` onto cached issue JSON, and `transform_remote_links` interpreted the empty list as "issue legitimately has no remote links — delete its existing rows", so a transient Jira auth blip (or a webhook burst hitting Jira's rate limiter) permanently wiped remote-link history. Now: every fetch site raises `JiraFetchError` on non-200/non-404 status and `httpx.RequestError` (including the "service not configured" path — a webhook arriving while API creds are missing no longer surfaces as a silent wipe), overlay sites skip the `_remote_links` key on raise (leaving it ABSENT, not present-but-empty), and `transform_remote_links` returns `None` for absent / `null` keys (preserve existing rows) vs `[]` (legitimate empty — wipe). Both consumers (batch `transform_all` and incremental `transform_single_issue`) honor the new contract. End-to-end tests lock both halves: `test_incremental_preserves_remote_links_when_overlay_absent` + `test_incremental_wipes_remote_links_when_overlay_present_but_empty`. The bulk-backfill scripts retain their existing `Retry-After` sleep+retry loop for 429 (appropriate for non-interactive batch contexts); only the webhook hot path raises on 429. ### Internal - `CLAUDE.md` — `connectors/jira/transform.py` removed from the "Files NOT to modify" list. The `_remote_links` hardening required modifying `transform_remote_links` and `transform_all` to honor the new "overlay absent → preserve existing rows" contract; the module remains sensitive (touch only with end-to-end understanding of the JSON-overlay / parquet-rewrite pipeline) but is no longer off-limits. ## [0.54.18] — 2026-05-15 ### Added - "Curated Memory" now sits in the primary navigation next to Data Packages, visible to every authenticated user. - **Per-user Dismiss** for Curated Memory items — analysts can opt-out of approved items from their AI-agent bundle and gray them out on `/corporate-memory`. Schema v46 adds `knowledge_item_user_dismissed`; new endpoints `POST /api/memory/{id}/dismiss` and `DELETE` (idempotent). **Mandatory items can never be dismissed** — the governance hard rule is enforced at two layers (API rejects with 400, SQL filter exempts mandatory rows even if a stale dismissal exists). `GET /api/memory` gains `hide_dismissed=false` (default off — dismissed items still visible with a badge + Undismiss button) and per-item `dismissed_by_me` flag. `GET /api/memory/bundle` always excludes dismissed items. - **"My Upvotes" filter** on `/corporate-memory` replaces the old dead "My Rules" sentinel. Backed by a new `?upvoted_by_me=true` filter on `/api/memory` that subquerying against `knowledge_votes`. - **Inline tag typeahead** in the admin edit modal — focus the tag input to browse all existing tags as a dropdown, type to filter (case-insensitive), ↑/↓ + Enter to add as pill, type a fresh value to surface "+ Add new tag: ". Tags now render as removable pills (× to remove); Backspace on empty input pops the last pill. - **Bulk-edit modal pickers** for `/admin/corporate-memory` — Category, Audience, and Add tag get `