# Changelog All notable changes to SecantusDB are documented here. This file is the **system of record** for what shipped in each release — the per-release blog posts on [secantusdb.com](https://secantusdb.com/categories/releases.html) are generated from these entries via `tools/generate_blog_post.py`. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) with one extension: each release carries a one-to-three-paragraph **prose lede** between the date line and the structured `#### Added` / `#### Changed` / `#### Fixed` subsections. The prose lede is what the blog generator lifts verbatim as the marketing-post body, so it should read as a self-contained narrative — not as "v0.5.1bN ships X." This project adheres roughly to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), but while we're in beta the patch number `bN` rolls forward on every PyPI-visible push; the API surface itself is shaped by Semantic Versioning intent. ## [Unreleased] (No entries yet — the next release will be cut from work landing on `main` after v0.5.1b23.) ## [0.5.1b23] — 2026-05-19 ### Native TLS + mTLS + per-write `j:true` — production gaps closed Three slices land together against the production-readiness gaps called out in the `docs/production.md` page. `[tls] cert_file` + `[tls] key_file` (in `secantusdb.toml`) or `--tls-cert-file` / `--tls-key-file` (CLI) makes the daemon wrap every accepted socket in TLS before the wire protocol starts. Clients connect with `mongodb://host:port/?tls=true&tlsCAFile=` and SecantusDB negotiates the TLS handshake itself; the connection thread then sees an encrypted socket-like object and serves mongo wire frames over it unchanged. This closes one of the biggest production-deployment gaps the `docs/production.md` page called out — operators no longer need to terminate TLS at an nginx / HAProxy / stunnel reverse proxy that becomes part of the trust boundary. mTLS lands as a layer on top: set `[tls] ca_file` and the daemon asks connecting clients for their own X.509 cert during the TLS handshake, verifying it against the configured CA bundle. Set `[tls] require_client_cert = true` to reject clients that don't present a cert; the default (`false`, `CERT_OPTIONAL`) verifies a cert if presented and accepts clients without one — useful for staged rollouts. mTLS is a coarse-grained "you're someone we approved of" gate; SCRAM-SHA-256 still identifies the specific user on top. mongod's `MONGODB-X509` auth mechanism (cert-subject-DN as the username, no SCRAM step) is a separate follow-on slice. Python's `PROTOCOL_TLS_SERVER` (TLS 1.2+, no SSLv2/3 fallback, default cipher list) is the only protocol mode. The `SSLContext` is built once at startup and cached — hot cert rotation requires a daemon restart. `certbot renew --post-hook 'systemctl reload secantusdb'` is the standard pattern. Without the cert / key kwargs the daemon stays plaintext exactly as before — no regression risk for the 1300+ existing tests. The b20 `sync_on_commit` knob enabled per-commit fsync at the *connection* level — every write on the daemon shared the same durability mode. The third slice finishes the story: the per-write `writeConcern.j` flag now threads from the wire layer through `Storage.insert` / `update_matching` / `delete_matching` (and all four `findAndModify` paths) into `_batch_transaction(sync=True)`, which calls `session.commit_transaction("sync=on")`. A client can now mix `j: true` and `j: false` writes against one daemon: the j:true subset pays the per-commit fsync cost (closes the durability gap), the rest stays fast. #### Added - `[tls]` table in `secantusdb.toml` (`cert_file`, `key_file`, `ca_file`, `require_client_cert`). Half-configured TLS (only one of cert/key set) raises `ValueError` at startup so deployment mistakes can't silently fall back to plaintext. - `--tls-cert-file` / `--tls-key-file` / `--tls-ca-file` / `--tls-require-client-cert` CLI flags. Standard precedence: SecantusConfig defaults < TOML < explicit CLI. - `SecantusDBServer(tls_cert_file=..., tls_key_file=..., tls_ca_file=..., tls_require_client_cert=...)` kwargs. When cert/key are set an `ssl.SSLContext` is built in `__init__` and used to wrap accepted sockets in `_serve_forever`. When ca_file is also set, the context asks clients for an X.509 cert during the handshake and verifies it against that CA. - `tests/test_tls.py`: 12 tests via `trustme` for ephemeral CA + client cert fixtures. Covers TLS round-trip, non-TLS-client rejection, no-args plaintext path (no regression), half-configured raises, missing-cert startup error, active_conns leak guard, and the four mTLS modes (required + valid cert / required + no cert / required + foreign-CA cert / optional + both modes). - `journal: bool = False` kwarg on `Storage.insert` / `update_matching` / `delete_matching`. When True, the WT transaction commits with `session.commit_transaction("sync=on")` — forces a per-commit fsync of the log regardless of the connection's `transaction_sync` config. - `_batch_transaction(*, sync: bool = False)` context-manager kwarg. The per-commit-fsync escape hatch the new `journal` write kwargs route through. - `tests/test_write_concern_journal.py`: 10 tests covering the storage-layer kwarg threading (`_batch_transaction` is invoked with `sync=True/False` appropriately), wire-level happy paths on insert / update / delete / findAndModify, and the positive + negative routing assertions. #### Changed - TLS / mTLS handshake errors are logged + the socket closed + the active-connection slot released; the daemon keeps serving everyone else. - `writeConcern: {j: true}` is now honoured per-write: the wire layer extracts the flag and threads it through to `_batch_transaction(sync=True)`. Previously the flag was accepted on the wire but had no effect — only the daemon-wide `sync_on_commit` knob (b20) could enable per-commit fsync. - `docs/production.md` updated: "Native TLS" is no longer in the gaps list; the dedicated TLS section now shows the in-process config plus the mTLS opt-in instead of an nginx-stream-module example. - `docs/configuration.md` documents the full `[tls]` schema (cert / key / ca / require_client_cert), the hot-rotation caveat, and the cipher-suite "out of scope for v1" note. #### Dependencies - `trustme>=1.2` added to the `dev` extra for the test CA fixture (transitively pulls `cryptography`). ## [0.5.1b20] — 2026-05-19 ### `secantusdb.toml` config file, native checkpoint restore, j:true durability knob Two production-shaping slices land together. A new `secantusdb.toml` configuration file exposes every CLI flag plus the WT and oplog knobs that were previously hard-coded — including `cache_size` (so you can size the engine for your dataset instead of running with the 1 GB test default) and a `sync_on_commit` switch that closes the long-standing `writeConcern: {j: true}` durability gap by enabling WT's per-commit fsync. The loader auto-discovers `./secantusdb.toml`, `~/.secantus/secantusdb.toml`, and `/etc/secantus/secantusdb.toml`; an explicit `--config PATH` overrides the search. CLI flags still win over file values, so the file is a deployment baseline rather than a lock-in. A new `secantusAdmin.restoreArchive` wire command and matching `secantusdb-restore-archive` offline CLI close out the backup story started in b18 — extract a backup `.tar.gz` into a target directory the operator then points a fresh SecantusDB process at. The admin UI's per-row Restore button now adapts to backup type: mongodump directories still call `mongorestore`; native `.tar.gz` archives surface an inline target-dir field and an Extract action that hits the new endpoint. Restore intentionally doesn't try to swap the WT home under a running server (the connection-thread session-caching layer would need a wholesale rework first), and matches how real mongod restore tooling already trains operators. Drive-by fix: the admin UI's "Existing backups" list now also includes `.tar.gz` files. The native archives created by the b18 backup button were previously invisible because `list_backups` only enumerated directories. The new [Running in production](production.md) doc page ties the config-file, native-backup, and restore work together — honest comparison vs single-node Postgres (the more useful framing than "SecantusDB vs mongod"), the gaps you have to accept, and a concrete `systemd` / TLS / backup / monitoring deployment shape. #### Added - [Running in production](production.md) docs page — honest comparison vs single-node Postgres (the more useful framing than "SecantusDB vs mongod-for-prod"), the gaps you must accept (no native TLS, no PITR, no replication, beta maturity), and a concrete deployment shape: `systemd` unit, `secantusdb.toml` with `sync_on_commit = true`, SCRAM auth provisioning, nginx stream TLS termination, hourly native checkpoint backups with off-host sync, the restore drill, `serverStatus` scraping for Prometheus / Datadog, and capacity sizing notes for `cache_size`. - `secantusdb.toml` configuration file (see [Configuration](configuration.md) for the full schema). Auto- discovered from `./secantusdb.toml`, `~/.secantus/secantusdb.toml`, `/etc/secantus/secantusdb.toml`; `--config PATH` disables discovery and loads a specific file. Unknown keys / unknown top-level tables fail loudly at startup so typos can't silently leave the engine running on the hard-coded default. - `secantus.config.SecantusConfig` dataclass + `load_config()` / `apply_overrides()` helpers. CLI flags' argparse defaults are now `None` (the "user did not pass this" sentinel) so the precedence chain is `SecantusConfig defaults < secantusdb.toml < explicit CLI flag` — file is a per-deployment baseline, the CLI overrides for one-off runs. - New CLI flags exposing previously-hard-coded knobs: `--cache-size`, `--session-max`, `--sync-on-commit`, `--oplog-retention-seconds`, `--oplog-max-entries`. Each has a matching `[storage]` / `[oplog]` key in the config file. - `Storage.__init__` accepts `cache_size`, `session_max`, `sync_on_commit` kwargs. The WT engine config string is built from these instead of being a hard-coded literal. - `secantusAdmin.restoreArchive` wire command. Accepts `archivePath` (server-side path to `.tar.gz`), `targetDir` (extraction destination), and optional `allowExisting` (overlay into a non-empty dir). Returns `{targetDir, fileCount, archive, ok: 1}`. RBAC: `fsync` action, cluster scope. - `secantus.storage.extract_backup_archive(archive_path, target_dir, *, allow_existing=False)` — module-level helper shared by the wire command, the admin route, and the CLI. Validates that the archive contains a `WiredTiger` metadata file before unpacking, so a malformed tarball can't pollute the target. - `secantusdb-restore-archive` console script (new `[project.scripts]` entry). Same validation as the wire command, no server needed. - Admin UI per-row **Extract** action on `.tar.gz` rows, posting to `POST /backup/restore-archive` with editable target-dir form field; the existing `Restore` button still handles mongodump directories. #### Changed - `writeConcern: {j: true}` is now honourable end-to-end via `[storage] sync_on_commit = true` (or `--sync-on-commit`), which sets WT's `transaction_sync=(enabled=true,method=fsync)`. Closes the long-standing durability gap previously documented in the backlog. Off by default (matches mongod's default `{w:1, j:false}`) since the throughput cost is significant. - `secantus.admin.backup.list_backups()` now includes `*.tar.gz` files alongside directories. Native-archive backups produced by b18's backup button were previously invisible in the admin UI's "Existing backups" list. - `MongoFacade.restore_archive(archive_path, target_dir, *, allow_existing=False)` — new admin client facade method. #### Fixed - "Existing backups" table on `/backup` was silently dropping every `.tar.gz` produced by the native checkpoint backup path introduced in v0.5.1b18 (only dump *directories* were listed). Both kinds now render with the correct per-row restore action. ## [0.5.1b18] — 2026-05-18 ### Native WT-checkpoint backups, admin UI /oplog page, and change-stream fidelity wins The natural follow-on to v0.5.1b17's `local.oplog.rs` synthetic collection lands as the admin UI `/oplog` page: a paged entry browser with a window selector (last 50 / 500 / 5000), `op`-checkbox filter (`i` / `u` / `d` / `c` / `n`), `ns` substring filter, and a per-row expandable JSON body. Auto-refreshes every 5 s. The data source is just `client.local.oplog_rs.find()` — no new server-side surface needed, only the page chrome and an `_rows` partial that follows the same pattern as `/connections` + `/cursors`. `showExpandedEvents` on change streams now matches mongod: the flag defaults to `false`, and DDL "expanded" events (`createIndexes`, `dropIndexes`) are suppressed unless the user opts in via `coll.watch(show_expanded_events=True)`. Previously these surfaced unconditionally — more permissive than mongod, and broke the conformance contract for tests that assume the stable v1 event set. `killOp` lands as a real wire command that closes the target connection's socket via `shutdown(SHUT_RDWR)`. Any in-flight command finishes, the per-connection thread's next `recv` returns 0, the loop exits, and the connection unregisters cleanly. Real mongod uses a per-op interrupt flag, which would need cancellation infrastructure SecantusDB doesn't carry — but "close the socket" is the visible end-state users care about, and the kill-and-reap admin button on `/connections` is now functional. `$sample` becomes deterministic when `SECANTUS_SAMPLE_SEED=` is set in the environment. Builds a dedicated `random.Random(seed)` instance at module load instead of mutating the global `random` state, so other code sharing the process keeps its own entropy. Closes the long-standing test-flake source where `$sample` results varied run-to-run. #### Added - Admin UI `/oplog` page (`routers/oplog.py` + `templates/pages/oplog.html` + `templates/partials/oplog_rows.html`): window / op / ns filters, expandable per-row JSON, 5 s auto-refresh, sidebar entry between Profiler and Maintenance. - `killOp` wire command + `kill(conn_id)` on `ConnectionRegistry` (shuts down the socket via `shutdown(SHUT_RDWR)`). Per-connection sockets are now stashed on the registry at `_handle_client` time. - `A_KILLOP` privilege action in `secantus.rbac`; granted by `clusterAdmin` and `root`. - Admin UI `/connections` Kill button (was a placeholder), typed-confirm modal (`partials/connection_kill_modal.html`), facade `kill_connection(conn_id)` method. - `ChangeStreamSpec.show_expanded_events` parsed from `$changeStream.showExpandedEvents`; threaded into `changestreams.project`. - `SECANTUS_SAMPLE_SEED` env var (read at `aggregate` module import) — `$sample` uses a dedicated `random.Random(seed)` when set. - `secantusAdmin.backupArchive` wire command + `Storage.create_archive` + admin UI "Run native checkpoint backup" button: forces a WT checkpoint then tars the storage directory into a single `.tar.gz`. Faster + atomic vs `mongodump`; restore is "extract + start a new SecantusDB pointing at it". Rigorous round-trip test coverage in `tests/test_backup_restore.py` (doc identity at scale, every non-default index shape, oplog tail continuity, capped collection options + FIFO state, SCRAM users / roles, concurrent-writes consistency, archive portability, repeated- backup idempotency). - `$densify` month / quarter / year units via `dateutil.relativedelta`. `quarter` is canonically 3 months. Adds `python-dateutil>=2.8` to the runtime dependencies (pure Python, available almost everywhere as a transitive dep). #### Changed - `changestreams.project` suppresses `createIndexes` / `dropIndexes` events unless the caller passed `show_expanded_events=True` (mongod-faithful default-off). The three existing tests + cross-driver DDL smokes (mongosh / node / go / java) all set the opt-in. #### Fixed - Closes backlog entry `$sample uses random.sample without a fixed seed` — deterministic via env var. - Closes backlog entry `killOp / connection-close command` — admin UI Kill button is functional. - Closes backlog entry `showExpandedEvents — accepted, ignored`. - Closes backlog entry `Admin UI /oplog page`. - `updateDescription.truncatedArrays` now emits for any array shrink (not just strict head-prefix), with indexed ``updatedFields`` for kept-prefix changes — matches mongod's $v:2 in-place diff rather than wholesale-replacing on any reshape. Same-length-with- changes arrays also produce indexed ``arr.`` updates now (previously wholesale). Closes the §3.2 backlog entry. ## [0.5.1b17] — 2026-05-17 ### `local.oplog.rs` queryable from pymongo, `$merge` pipeline form + `$fill` stage + `$$var.path` resolution Real mongod exposes the oplog as a queryable collection at `local.oplog.rs` — pymongo clients can `db.oplog.rs.find()` against it the same way they would against any collection. Until this release, SecantusDB's oplog was internal only: `Storage.read_oplog` / `oplog_floor_seq` / `oplog_tail_seq` were Python methods but had no wire surface. Now `local.oplog.rs` is a synthetic read-only view — `list_collections("local")` surfaces it, `find` / `count` / `listCollections.options` route to a reader that walks the oplog WT table directly, and write attempts (`insert`, `update`, `delete`, `findAndModify`, `drop`, `create`, `createIndexes`) refuse with code 13 (Unauthorized) like mongod does. The deferred admin UI `/oplog` page is unblocked as a follow-up; for now, debugging an in-flight change-stream pipeline is as simple as `client.local.oplog_rs.find({"op": "u"}).sort("ts", -1).limit(20)`. The aggregation expression library picks up two of the three remaining stages on most "more stages" wishlists. `$merge` was partly implemented; this batch fills in the rest: `whenMatched: []` runs a sub-pipeline against the matched target doc with `$$new` bound to the source doc and any user `let` vars threaded through; `whenMatched: "delete"` (MongoDB 5.0+) removes the matched doc; a unique-index guard refuses non-`_id` `on` fields without a `unique: true` index covering them, matching mongod's rule against silent on-field collapse. `$fill` lands fresh — the 5.3+ stage for filling missing/null fields. Three modes per output field: `{value: }` replaces with an evaluated expression; `{method: "locf"}` carries the last observation forward within the partition's sortBy order; `{method: "linear"}` interpolates between bracketing non-null anchors along the sortBy field (works for numbers and datetimes — timedelta arithmetic divides cleanly to float and multiplies back to timedelta). Partitioning via `partitionByFields` or `partitionBy`; sortBy required when any output uses `method`. The `$merge` pipeline form was the first thing in the repo to exercise `$$var.path` (e.g. `$$new.delta`), and surfaced that the expression evaluator only did exact-name var lookup. Fixed in the same batch: `$$var.field.path` now walks the dotted path into the resolved value across `$$ROOT.f` / `$$CURRENT.f` / user-let vars. #### Added - `local.oplog.rs` synthetic collection: queryable via `find` / `count` / `listCollections`. Walks the existing oplog WT table via a private session for cross-thread visibility. `list_databases` surfaces `local` whenever the oplog is enabled. - `$merge whenMatched: []` with `$$new` binding + `let` clause for user-defined vars (`aggregate._stage_merge`). - `$merge whenMatched: "delete"` (MongoDB 5.0+). - `$merge` unique-index guard on non-`_id` `on` fields. - `$fill` stage with `value`, `locf`, and `linear` modes (`aggregate._stage_fill`). - `$$var.field.path` dotted-path resolution in `expressions._resolve_var`. - `docs/changelog.md` as the system of record (see the [changelog](changelog) itself and the `changelog/` Python package that generates blog posts from it). #### Changed - Writes to `local.oplog.rs` (insert / update / delete / findAndModify / drop / create / createIndexes) refuse with code 13 (Unauthorized). - `$merge` validates `whenMatched` / `whenNotMatched` against the allowed string sets — typos surface as `AggregateError` instead of silently falling through to the default merge. ## [0.5.1b16] — 2026-05-16 ### Sidebar grouping, auto-refreshing connections and cursors, Roles in the nav The `/connections` and `/cursors` admin pages have always been live-data views — they read `currentOp` and render the connection / cursor list each time the page is requested — but they didn't refresh. The dashboard polls 1 Hz over a WebSocket; these two felt stale next to it. v0.5.1b16 extracts each table's tbody into an HTMX partial and lets the tbody itself swap every 5 s via `hx-trigger="every 5s" hx-get="/connections/_rows"`. The page chrome and column headers stay fixed; only the rows refresh. `/connections` also gains a (disabled) Actions column with a tooltip explaining that connection-kill is deferred until SecantusDB grows `killOp` — purely a layout-symmetry fix so the page mirrors the shape of `/cursors`. The sidebar gets two structural fixes. A `Roles` entry now lives directly under `Users` with a sub-nav indent (it was reachable only via the breadcrumb on `/users`, and `roles.html` was setting `active="users"` so the wrong sidebar item highlighted while you were on the page). A second visual separator above `Change stream` marks the boundary between per-target data pages and operational-state pages, mirroring the existing separator below `Server`. A separate fix: 15 `*_via_mongosh` cross-driver smoke tests are now grouped into a single xdist worker. Mongosh launches a full Node-based shell, and under heavy parallel load the PBKDF2 work inside SCRAM-SHA-256 auth could blow past mongosh's connect timeout. Tagged with `@pytest.mark.xdist_group(name="mongosh_smokes")` so they serialize. #### Added - Sidebar `Roles` entry under `Users` with sub-nav indent + correct active highlight on `/roles`. - Sidebar visual separator above the operational-state group (`nav-ops-start` CSS class). - `/connections` + `/cursors` auto-refresh tbody (`hx-trigger="every 5s"`) with new `_rows` partial endpoints. - `/connections` disabled Actions column for layout symmetry with `/cursors`; tooltip explains `killOp` is deferred. #### Fixed - 15 `*_via_mongosh` cross-driver smoke tests serialized via `xdist_group="mongosh_smokes"` to dodge PBKDF2-handshake timeouts under parallel-test CPU contention. ## [0.5.1b15] — 2026-05-16 ### One scaffold for every confirmation modal — escape, focus-trap, restored focus The `secantus-admin` UI has nine confirmation / edit modals (drop-database, drop-collection, drop-index, drop-user, change-password, manage-roles, edit-document, delete-document, kill-cursor). They were assembled at slightly different times and drifted in five different ways — different destructive-button copy, different typed-confirm targets (the delete-document modal asked the user to type the collection name shared by every row; the kill-cursor modal asked for the giant int cursor id), no Escape-to-close, no focus restoration to the trigger element, no focus trap so Tab leaked back into the page behind, and `aria-label="Close"` only on two of nine close buttons. v0.5.1b15 consolidates all nine on a shared scaffold: a new `modal-shell.js` exposes `openModal(url)` / `closeModal()` / `setupModal(el)` plus a global htmx hook that captures the trigger element so `closeModal()` can restore focus. Each modal partial has the same overlay shape — `x-init="setupModal($el)"`, `@click.self="closeModal()"`, `@keydown.escape.window="closeModal()"`, `role="dialog"`, `aria-modal`, `aria-labelledby` — and Tab / Shift+Tab cycle within the modal's focusable children rather than escaping into the page behind. Three substantive fixes ride along with the scaffolding: destructive button copy now always restates action+noun (Kill cursor / Delete document / Drop index / Drop user / Drop database / Drop collection); the delete-document typed-confirm asks for the doc's `_id` value rather than the collection name; the kill-cursor typed-confirm asks for the collection `ns` rather than the unguessable cursor id. None of these change SecantusDB's wire-protocol behaviour. #### Added - `static/js/modal-shell.js`: `openModal(url)`, `closeModal()`, `setupModal(el)`, htmx hook for trigger-element capture. - `[x-cloak]` CSS helper to prevent Alpine flash on first paint. #### Changed - All 9 confirmation / edit modal partials use the shared overlay shape with `role="dialog"` / `aria-modal` / `aria-labelledby`. - Destructive button copy restates action+noun across the board. - `delete-document` typed-confirm uses the doc's `_id` value (was the collection name). - `kill-cursor` typed-confirm uses the collection `ns` (was the cursor id). #### Fixed - Escape now closes every modal. - Focus restored to the triggering element after modal close. - Tab focus-trap inside modals. - `aria-label="Close"` on all 9 close buttons (was on 2). ## [0.5.1b14] — 2026-05-15 ### Admin UI punch list — five silent-failure modes fixed The May 2026 end-to-end review of the `secantus-admin` web UI catalogued five P0s — bugs that didn't crash anything but presented wrong information to the user. v0.5.1b14 fixes all five. None require any database-level change; this is purely admin-UI plumbing, but each one was either lying to the user or hiding a real error behind cheerful copy. The biggest was the **profiler page swallowing every exception** while reading `system.profile`. A bare `except Exception:` rendered "no entries yet — run an operation to see one appear here" no matter what the underlying error was, including the target server being completely unreachable. The clause is now narrowed to `PyMongoError` and the friendly error message gets funnelled into the page's normal error banner. The same page also had a **`flash` keyword argument that the template never rendered** — every settings change returned `HX-Redirect` and the user saw zero confirmation that anything had happened. The POST handler now re-renders the page inline with a flash banner that names the new level / slowms / sampleRate values. The other three are dead-code cleanups: the **doc tour** in `docs/admin.md` walked the user through a `/console` page that was renamed to `/query` two refactors ago; the **Maintenance "Drop collection" form** had an `hx-get` pointing at a route that never existed; and the **dashboard router** still exposed a `GET /_partials/dashboard-tiles` endpoint from before the WebSocket dashboard landed. #### Fixed - Profiler page: narrowed bare `except Exception:` to `PyMongoError` so server-down errors surface (`routers/profiler.py`). - Profiler page: added flash banner block to template + POST handler re-renders inline instead of `HX-Redirect`. - Maintenance "Drop collection" form: dropped dead `hx-get="/maintenance/drop-collection-redirect"` attribute. - Dashboard router: deleted unused `GET /_partials/dashboard-tiles` endpoint, partial template, and the two tests that exercised them. - `docs/admin.md`: replaced stale `### Console` section with `### Query (/query)` + `### Insert (/insert)` + new `### Server (/server)` subsection. ## [0.5.1b13] — 2026-05-15 ### Zero actionable failures — every driver gauge classified, every gap explained Over the past few releases the cross-driver gauge pass rate has been climbing — 99.5% at v0.5.1b4, 99.9% by last week's refresh. The last 0.1% was a handful of failures that either could not be fixed in SecantusDB (a Java-driver SDAM cascade triggered by a server-side `APIStrictError`), reproduced only under heavy parallel load (two `mongo-go-driver` flakes), or assumed a multi-node replica-set deployment SecantusDB deliberately doesn't simulate (Ruby's `w: 2` write-concern test). Reporting them as plain "failures" overstated the gap — but silently dropping them would let real regressions hide in the same column. v0.5.1b13 introduces **`validation_summary/expected_failures.py`** — a small per-gauge registry of `(pattern, rationale)` entries. The cross-driver summary now separates "Failed" (unexpected, a real bug we need to fix) from "Expected" (a documented gap with a one-line reason that ships in the report). A new **Adjusted** column reports the rate excluding expected failures from the denominator — "how much of the conformable surface actually conforms." Current numbers: **7,186 tests, 6,254 passed, 0 unexpected failures, 5 expected failures, 927 skipped — 100.0% adjusted across every driver.** This release also bundles the gauge improvements that landed since v0.5.1b4: `mapReduce` returns a graceful empty result for non-canonical bodies, `$changeStream` against a standalone topology is rejected with code `40573`, Node CSOT explain-plus-`timeoutMS` tests pass via a new `block_connection` / `block_time_ms` failpoint pair, `getParameter` advertises `authenticationMechanisms: ["SCRAM-SHA-256"]`, and `createIndexes` / `create` reject unknown options up-front. #### Added - `validation_summary/expected_failures.py`: per-gauge registry of documented-known failures with rationales. - Cross-driver summary "Expected" + "Adjusted pass rate" columns. - `block_connection` / `block_time_ms` failpoint fields (`failpoints._FailCommand`). #### Changed - `mapReduce` returns a graceful empty result for non-canonical map/reduce bodies (wire-shape probes pass). - `$changeStream` on a standalone topology is rejected with code 40573. - `getParameter` advertises `authenticationMechanisms: ["SCRAM-SHA-256"]`. - `createIndexes` rejects unknown per-index options (`_INDEX_SPEC_KNOWN_OPTIONS` whitelist). - `create` rejects unknown collection options (`_CREATE_KNOWN_OPTIONS` whitelist). - `validate-all` serialized (`max_workers=1`) to dodge load-induced inter-gauge flakes. ## [0.5.1b4] — 2026-05-12 ### Cross-driver conformance summary — 99.5% across 7,186 tests on one page Until this release, comparing SecantusDB's conformance across the five driver gauges (pymongo / mongo-java-driver / mongo-go-driver / mongo-node-driver / mongo-ruby-driver) required opening five different reports and squinting at five different per-category breakdowns whose denominators came from incompatible units of count — JUnit `` versus Mocha test versus RSpec example versus `go test` event versus pytest item. v0.5.1b4 ships **`docs/validation-summary.md`** — a single table that normalises on test count, one row per gauge, the same five columns across the board: tests run, passed, failed, skipped, pass rate. A new `validation_summary` Python module reads each gauge's raw artifact under `.validation/` directly and renders the table; a new `invoke validate-summary` task refreshes it. Current numbers: **7,186 tests, 6,232 passed, 33 failed, 921 skipped — 99.5% pass rate** across all five drivers. Java is biggest by raw count (4,710 tests, 4,242 passed); Node smallest (364). This release also rolls up two driver-gauge fixes that landed since v0.5.1b1: a Java widening to 21 of 112 driver-sync functional classes (+34 passes), and a snapshot-read-concern rejection that turned three `SessionsTest` snapshot-error scenarios from "expected error, got success" into "expected error, got `SnapshotUnavailable` (code 246)". #### Added - `docs/validation-summary.md` cross-driver normalized table. - `validation_summary/` Python module (raw-artifact reader + renderer). - `invoke validate-summary` task. - `snapshot` readConcern rejected with code 246 (`SnapshotUnavailable`). - Java gauge: `ChangeStreamsTest`, `UnifiedWriteConcernTest`, `VersionedApiTest` unified-spec runners (21 of 112 driver-sync functional classes total). #### Fixed - RTD build for v0.5.1b3 failed on a missing toctree entry for the new summary file; b4 is the first release where the docs match what's on PyPI. ## [0.5.1b1] — 2026-05-12 ### Java gauge scope made honest — 18 of 112 driver-sync classes, five named follow-ups The Java gauge passing rate had been reported at "100%" — but only across the 13 driver-sync functional classes the gauge was running. v0.5.1b1 widens the include set to 18 of 112 and adds an explicit **Scope** section to `docs/validation-report-java.md` that surfaces the "X of 112 driver-sync functional classes" denominator so the headline number isn't misleading. The widened set surfaced five real failures, all named and tracked in `tasks/backlog.md` §5: Java apiStrict pool-clear cascade, mapReduce non-canonical bodies, snapshot reads on standalone, distinct apiStrict — none are SecantusDB bugs, but they're now documented expected-fail entries. #### Added - Java gauge include set widened to 18 of 112 driver-sync functional classes (`java_validation/include_modules.py` waves 1 + 2). - "Scope" section in Java validation report exposing the include-set denominator (`java_validation/generate_report.py`). ## [0.5.0b18] — 2026-05-12 ### Ruby gauge climbs to 99%, completing the cross-driver 99–100% band The Ruby gauge had been the weakest of the five at ~95% — a handful of real SecantusDB gaps the Ruby driver exercises but the others don't. v0.5.0b18 closes the high-value ones: `writeConcernError` is now attached on `w > 1` (CannotSatisfyWriteConcern code 100), invalid `wildcardProjection` is rejected on `createIndexes`, `commitQuorum` is validated at the top level, `listIndexes` rejects negative batchSize (code 51024), and `$collStats` surfaces capped-collection bounds (`storageStats.{capped, max, maxSize}`). Net: Ruby gauge from 94.6% → 99.7%, 13 net passes. All five driver gauges now sit in the 99–100% band. #### Added - `writeConcernError` attached on `w > 1` (`CannotSatisfyWriteConcern` code 100). - `createIndexes` validates `wildcardProjection` shape. - `commitQuorum` validated at top-level. - `$collStats` surfaces capped bounds (`storageStats.{capped, max, maxSize}`). #### Changed - `listIndexes` rejects negative `batchSize` with code 51024. ## Older releases Releases before v0.5.0b18 (the `v0.3.0aN` and `v0.4.0bN` lines, and v0.5.0b1 through v0.5.0b3) shipped before this changelog was the system of record. See the [GitHub Releases](https://github.com/jdrumgoole/SecantusDB/releases) page for the auto-generated commit-list notes from those tags. [Unreleased]: https://github.com/jdrumgoole/SecantusDB/compare/v0.5.1b18...HEAD [0.5.1b23]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b23 [0.5.1b20]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b20 [0.5.1b18]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b18 [0.5.1b17]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b17 [0.5.1b16]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b16 [0.5.1b15]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b15 [0.5.1b14]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b14 [0.5.1b13]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b13 [0.5.1b4]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b4 [0.5.1b1]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b1 [0.5.0b18]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.0b18