> **Plain-language companion:** [v0.30.0.md](v0.30.0.md)

## v0.30.0 — Pre-GA Correctness & Stability Sprint

**Status: ✅ Released (v0.30.0).** Derived from [plans/PLAN_OVERALL_ASSESSMENT_3.md](plans/PLAN_OVERALL_ASSESSMENT_3.md) §3, §4, §7, §8.
This release must land **before** v1.0.0 GA. Its purpose is to close every
P0 and P1 gap identified in the v0.27.0 assessment so that the stable
release inherits a clean correctness baseline.

> **Release Theme**
> v0.30.0 is the quality gate before the feature-rich v0.28–v0.29 arc and
> the v1.0 stable release. It fixes the remaining correctness defects that
> could produce silent wrong answers (EC-01 phantom drift, snapshot
> non-atomicity), eliminates the operational failure modes that could
> surprise production operators (unbounded IVM/template caches, text-based
> SPI error classification, snapshot partial-restore data loss), closes the
> documentation gaps that block self-service operation (upgrade notes, GUC
> reference, ERRORS guide), and hardens the test suite with the fuzz and E2E
> coverage that the new SNAP/PLAN/CLUS/METR functionality currently lacks.
> No new user-visible SQL API is added. The release is a prerequisite for
> `v1.0.0`; any item not landed here will block GA.

---

### Correctness

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| CORR-1 | Complete EC-01 phantom-row convergence | M | P0 |
| CORR-2 | Generalise SubLink detection to CASE/COALESCE/FuncCall | S | P1 |
| CORR-3 | Replace `SELECT * EXCEPT` with explicit column list in restore | S | P1 |

**CORR-1 — Complete EC-01 phantom-row convergence.**
The v0.24.0 hash fix is necessary but not sufficient: `is_deduplicated:
false` at `src/dvm/operators/join.rs:657–668` still forces the MERGE to
aggregate by row-id, and the conditional wiring that invokes
`src/refresh/phd1.rs` cross-cycle cleanup is incomplete. Option A:
wire every refresh cycle through unconditional PH-D1 cleanup with a small
batch size. Option B: re-derive the Part-2 row-id from the retained
base-table key snapshot so that `is_deduplicated: true` can be set for
INNER joins. Verify with the deterministic reproducer from
`test_tpch_q07_ec01b_combined_delete` and the IMMEDIATE-mode property
tests. **Schema change:** No. **Dependencies:** v0.24.0 PH-D1 skeleton.

**CORR-2 — SubLink detection for CASE/COALESCE/FuncCall.**
`node_tree_contains_sublink` only recurses into `T_BoolExpr` arguments;
SubLinks inside `T_CaseExpr`, `T_CoalesceExpr`, or function-argument lists
silently downgrade the query to FULL refresh without a user-visible
message. Generalise the walker or emit a `NOTICE` on forced downgrade.
**Schema change:** No. **Dependencies:** None.

**CORR-3 — Explicit column list for restore.**
`restore_from_snapshot_impl` unconditionally emits `SELECT * EXCEPT(…)`,
which is only available on PG 18 with specific minor-version patches.
Replace with a `pg_attribute` catalog walk that builds the explicit column
list, eliminating PG-minor sensitivity entirely. **Schema change:** No.
**Dependencies:** None.

---

### Stability

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| STAB-1 | Wrap snapshot/restore in SubTransaction RAII + exclusive lock | M | P0 |
| STAB-2 | Bound `IVM_DELTA_CACHE` via clock-style eviction | S | P1 |
| STAB-3 | Age-based purge for L2 template-cache catalog table | S | P1 |
| STAB-4 | Surface snapshot catalog INSERT failure as WARNING | XS | P0 |
| STAB-5 | Replace `wal_decoder.rs` `.expect()` with `c"test_decoding"` | XS | P1 |
| STAB-6 | Clear IVM thread-local cache via `XactCallback` on subxact abort | S | P1 |

**STAB-1 — Snapshot/restore atomicity.**
`snapshot_stream_table_impl` and `restore_from_snapshot_impl`
(`src/api/snapshot.rs:90–260`) run independent SPI statements with no
subxact bracket. A backend crash between the `CREATE TABLE AS` and the
catalog `INSERT` leaves an orphan table; a mid-restore statement failure
leaves the storage table truncated. Wrap the entire operation in the same
`SubTransaction` RAII helper used by `src/scheduler.rs:250–308`. For
restore, open with `LOCK TABLE … IN ACCESS EXCLUSIVE MODE` before the
`TRUNCATE`. Propagate snapshot-version-check failure as typed
`SnapshotSchemaVersionMismatch` instead of silently treating `None` as
compatible. **Schema change:** No. **Dependencies:** scheduler SubTransaction
helper (shipped v0.27.0).

**STAB-2 — Bound `IVM_DELTA_CACHE`.**
The thread-local `IVM_DELTA_CACHE` at `src/ivm.rs:107–143` has no eviction.
Implement a clock-style eviction that respects `pg_trickle.template_cache_max_entries`
— the same GUC already used by the L1 delta-template cache.
**Schema change:** No. **Dependencies:** None.

**STAB-3 — L2 template-cache age purge.**
`src/template_cache.rs` writes one row per stream table but never purges
stale entries from ALTER QUERY without DROP or source-OID renumbering. Add a
`cached_at TIMESTAMPTZ` column (migration) and a lightweight batched DELETE in
the scheduler's launcher tick, age-bounded by a new
`pg_trickle.template_cache_max_age_hours` GUC (default 168 h = 7 days).
**Schema change:** Yes — `ALTER TABLE pgtrickle.pgt_template_cache ADD COLUMN
cached_at TIMESTAMPTZ NOT NULL DEFAULT now()`.

**STAB-4 — Snapshot catalog INSERT failure warning.**
`snapshot_stream_table_impl` discards the `Result` of the catalog `INSERT INTO
pgtrickle.pgt_snapshots` with a `// best-effort` comment at
`src/api/snapshot.rs:152–167`. A silent failure produces a function return
value (the snapshot path) that `list_snapshots()` will never find. Promote the
failure to a pgrx `WARNING` message. **Schema change:** No.

**STAB-5 — `wal_decoder.rs` `.expect()` removal.**
`CString::new("test_decoding").expect(…)` at `src/wal_decoder.rs:307` is the
last production `.expect()` outside the sound `unreachable!()` post-`report()`
calls. Replace with `c"test_decoding"` (compile-time `CStr` literal; Rust 1.77+
stable) to eliminate the unreachable runtime path entirely. **Schema change:** No.

**STAB-6 — IVM cache `XactCallback`.**
If an IVM trigger function fails mid-statement, the
`__pgt_newtable_<oid>` / `__pgt_oldtable_<oid>` temp tables are cleaned up by
PG subxact abort, but the thread-local `IVM_DELTA_CACHE` is not cleared. A stale
entry can survive a failed apply and be reused in the next statement. Register a
`XactCallback` that calls `invalidate_ivm_delta_cache` on
`XACT_EVENT_ABORT_SUB`. **Schema change:** No.

---

### Performance

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| PERF-1 | Implement L0 shared-shmem dshash template cache | L | P1 |
| PERF-2 | Bound parser memory via `max_parse_nodes` GUC + `XactCallback` | M | P1 |
| PERF-3 | Add missing benchmark suite (SNAP, PLAN, CLUS, IVM apply, WAL decoder) | M | P1 |
| PERF-4 | Reduce catalog hot-path in `metrics_summary()` / `cluster_worker_summary()` | M | P2 |

**PERF-1 — L0 dshash shared-memory template cache.**
`src/shmem.rs:680–710` wires `L0_POPULATED_VERSION` as a signal that the L2
catalog cache was populated, but the actual `dshash` data structure that would
store delta SQL in shared memory is not implemented. Build the dshash store
so that any backend can satisfy a cache lookup from shared memory rather than
paying the ~1 ms L2 catalog SELECT on every cold start. Expected win: erase
the remaining cold-backend latency tail in high-backend-count deployments.
**Schema change:** No. **Dependencies:** `pg_dsm_create` and `dshash_create`
pgrx bindings (available in pgrx 0.18).

**PERF-2 — Parser memory bounds.**
`pg_trickle.max_parse_depth` (G13-SD) bounds recursion depth but not total
node count. A large `IN (1, …, 1_000_000)` list still allocates unboundedly in
`PARSE_ADVISORY_WARNINGS` and `cte_ctx.registry`. Add a
`pg_trickle.max_parse_nodes` GUC (default 100 000) and reject queries that
exceed it with `QueryTooComplex`. Clear thread-locals at end-of-statement via
`XactCallback`. **Schema change:** No (new GUC only).

**PERF-3 — Missing benchmarks.**
Seven performance-critical paths have no Criterion coverage: snapshot/restore
round-trip, predictive planner `recommend_schedule` at varying history lengths,
`cluster_worker_summary()` at scale, L2 template-cache hit/miss latency, IVM
apply path, WAL decoder poll loop, multi-database fairness. Add a benchmark
file or extend `benches/refresh_bench.rs` to cover all seven.
**Schema change:** No.

**PERF-4 — Reduce catalog hot-path in `metrics_summary()` and `cluster_worker_summary()`.**
Both new v0.27.0 functions re-query `pgtrickle.pgt_stream_tables` and
`pg_stat_activity` on every Prometheus scrape (default 5 s interval). At
200+ stream tables this adds a full catalog scan per database every 5 s, on
top of the scheduler's own reads. Introduce a short-lived shmem-backed
snapshot for these read paths that both surfaces share, refreshed once per
scheduler tick. Controlled by `pg_trickle.metrics_catalog_snapshot_ttl_ms`
GUC (default 4000 ms). **Schema change:** No.

---

### Scalability

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| SCAL-1 | Replace text-based SPI error classification with SQLSTATE codes | M | P0 |
| SCAL-2 | Replace `refresh::*` blanket re-exports with explicit `pub use` | XS | P1 |
| SCAL-3 | Remove dead `#[allow(unused_imports)]` shims in `refresh/orchestrator.rs` | XS | P2 |

**SCAL-1 — SQLSTATE-based error classification.**
`classify_spi_error_retryable` at `src/error.rs:213–260` matches English text
fragments. On a PostgreSQL build with non-English `lc_messages`, every pattern
silently breaks and every SPI error becomes retryable. Push SQLSTATE through
pgrx (`pg_sys::ErrorData.sqlerrcode`) and classify by 5-character code.
**Schema change:** No. **Dependencies:** pgrx 0.18 ErrorData access.

**SCAL-2 — Explicit `pub use` in `refresh/mod.rs`.**
`src/refresh/mod.rs` exposes `pub use codegen::*; pub use merge::*; pub use
orchestrator::*;`, promoting every new public symbol automatically. Convert to
explicit re-export lists to enforce module boundary discipline without breaking
callers. **Schema change:** No.

**SCAL-3 — Remove dead `#[allow(unused_imports)]` in `refresh/orchestrator.rs`.**
Every import at `src/refresh/orchestrator.rs:6–26` is annotated
`#[allow(unused_imports)]` — a maintenance hazard left over from the ARCH-1B
split. Removing an actually-unused import won't surface a warning, and the
imports become silently misleading. Convert each `use` to a concrete symbol and
remove the lint-suppression attribute. **Schema change:** No.

---

### Ease of Use

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| UX-1 | Document all v0.27.0 GUCs in `CONFIGURATION.md` | S | P0 |
| UX-2 | Document v0.15.0–v0.27.0 upgrade notes in `UPGRADING.md` | M | P0 |
| UX-3 | Document new error variants (HINT/DETAIL) in `ERRORS.md` | S | P1 |
| UX-4 | Complete `SQL_REFERENCE.md` for SNAP/PLAN/CLUS/METR functions | S | P1 |
| UX-5 | Add TUI parity for SNAP/PLAN/CLUS/METR functions | M | P1 |
| UX-6 | Ship first-party Grafana dashboard JSON in `monitoring/grafana/` | M | P1 |
| UX-7 | Document `pg_trickle_dump` in `BACKUP_AND_RESTORE.md` | XS | P1 |
| UX-8 | Add snapshot/PITR walkthrough to `GETTING_STARTED.md` | S | P1 |
| UX-9 | Document `change_buffer_durability` + `frontier_holdback_*` in `PRE_DEPLOYMENT.md` | XS | P1 |
| UX-10 | Add atomicity-gap warning + concurrent-restore note to `BACKUP_AND_RESTORE.md` | XS | P1 |
| UX-11 | Add FAQ entries: snapshot how-to, `schedule_recommendation_min_samples` tuning | XS | P2 |
| UX-12 | Add RELEASE.md checklist item for SNAP/PLAN/CLUS/METR migration script | XS | P2 |
| UX-13 | Add snapshot demo to `PLAYGROUND.md` | XS | P2 |
| UX-14 | Track CNPG 1.29 compatibility: update `cnpg/cluster-example.yaml` and CI | S | P1 |
| UX-15 | Open tracking issue for dbt-core 1.11 upgrade path | XS | P1 |
| UX-16 | Add noisy-neighbour example + alert to `docs/integrations/multi-tenant.md` | XS | P3 |
| UX-17 | Add `recommend_schedule` tuning recipe to `PERFORMANCE_COOKBOOK.md` | XS | P3 |
| UX-18 | Annotate `dbt-pgtrickle/README.md` with compatible extension version range | XS | P3 |

**UX-1** — `schedule_recommendation_min_samples` (default 20),
`schedule_alert_cooldown_seconds` (default 300), `metrics_request_timeout_ms`
(default 5000), `change_buffer_durability` (`unlogged|logged|sync`), and
several others from Appendix C of the assessment are missing from the
`CONFIGURATION.md` TOC. Add full Property/Default/Range/Context + Tuning
Guidance sections for each.

**UX-2** — `UPGRADING.md` stops at `0.13.0 → 0.14.0`. Add upgrade notes
(breaking changes, migration script highlights, GUC renames) for every
release from v0.15.0 through v0.27.0.

**UX-3** — New error variants shipped since v0.23.0 (`SnapshotAlreadyExists`,
`SnapshotSourceNotFound`, `SnapshotSchemaVersionMismatch`, `DiagnosticError`,
`PublicationAlreadyExists`, `PublicationNotFound`, `PublicationRebuildFailed`,
`SlaTooSmall`, `ChangedColsBitmaskFailed`) have no HINT/DETAIL entries in
`ERRORS.md`.

**UX-4** — Spot-check confirms `cluster_worker_summary()` and
`metrics_summary()` are referenced from `SCALING.md` but do not have full
sections in `SQL_REFERENCE.md`. Add complete parameter/return-type/example
entries for all eight SNAP/PLAN/CLUS/METR functions.

**UX-5** — `pgtrickle-tui` has no panels for `snapshot_stream_table`,
`restore_from_snapshot`, `list_snapshots`, `recommend_schedule`,
`schedule_recommendations`, `cluster_worker_summary`, `metrics_summary`.
Add SNAP/PLAN/CLUS/METR views to the TUI alongside the existing stream-table
management screens.

**UX-6** — The `monitoring/grafana/` directory has no first-party dashboard
JSON. The dashboard snippets in `docs/integrations/multi-tenant.md` are the
only artefact. Ship a baseline dashboard covering refresh latency, CDC buffer
growth, IVM lock-mode distribution, and per-DB cluster metrics.

**UX-7** — `src/bin/pg_trickle_dump.rs` (458 LOC) is the only
out-of-database backup tool. Document usage, flags, and restore procedure
in `BACKUP_AND_RESTORE.md`.

**UX-8** — Add a "Snapshot and Point-in-Time Recovery" section to
`GETTING_STARTED.md` with a worked example: take a snapshot, simulate data
loss, restore from snapshot, verify.

**UX-9** — `PRE_DEPLOYMENT.md` does not mention `change_buffer_durability`
(`unlogged|logged|sync`, default `unlogged`) or the `frontier_holdback_lsn_bytes`
gauge. These are operationally critical: operators setting `sync` for durability
need to know the write-amplification cost before deployment.
**Schema change:** No.

**UX-10** — `BACKUP_AND_RESTORE.md` does not warn about the atomicity gap
in `snapshot_stream_table` / `restore_from_snapshot` identified in §3.2 (no
subxact bracket; crash between `CREATE TABLE AS` and catalog INSERT leaves
an orphan table; mid-restore `TRUNCATE` + interrupted `INSERT` leaves
the storage table truncated). Add an explicit "Known Limitations" callout
until STAB-1 is fixed. Also document that `restore_from_snapshot` should not
be called while a concurrent `refresh_stream_table` is in flight.
**Schema change:** No.

**UX-11** — `FAQ.md` has no entry for "How do I take a snapshot?" or
"How do I tune `schedule_recommendation_min_samples`?". Add both with links
to `BACKUP_AND_RESTORE.md` and `CONFIGURATION.md`.
**Schema change:** No.

**UX-12** — `RELEASE.md` checklist should include a step to verify the
SNAP/PLAN/CLUS/METR migration script
(`sql/pg_trickle--0.26.0--0.27.0.sql`) passes
`scripts/check_upgrade_completeness.sh`. Add to the release runbook.
**Schema change:** No.

**UX-13** — `PLAYGROUND.md` has no snapshot demo. Add a `snapshot_stream_table`
→ data loss simulation → `restore_from_snapshot` example that works in the
`playground/docker-compose.yml` environment.
**Schema change:** No.

**UX-14** — `cnpg/cluster-example.yaml` targets CNPG 1.28+. CNPG 1.29 ships
in 2026-Q2. Refresh the manifest, add a CI job using the updated image, and
verify the ImageVolume mount still works under the new CNPG operator.
**Schema change:** No.

**UX-15** — `dbt-pgtrickle/AGENTS.md` pins `dbt-core ~=1.10` and notes
Python 3.13 only (mashumaro dep can't build on 3.14). dbt-core 1.11 is in
beta. Open a tracking GitHub issue: note the mashumaro blocker, watch the
1.11 release, and add a CI job testing against 1.11 once the dep resolves.
**Schema change:** No.

**UX-16** — `docs/integrations/multi-tenant.md` is excellent but has no worked
example of a noisy-neighbour scenario. Add one: show a high-frequency stream
table starving others, the Prometheus alert expression, and the remediation
step (`pg_trickle.per_db_refresh_quota_ms`). **Schema change:** No.

**UX-17** — `PERFORMANCE_COOKBOOK.md` should include a recipe:
"Use `recommend_schedule` to right-size a 100-table dbt project." Walk
through a cold-start, initial history collection, and the first
auto-recommendation. Link to `CONFIGURATION.md` for the relevant GUCs.
**Schema change:** No.

**UX-18** — `dbt-pgtrickle/README.md` does not state which extension version
it is tested against. Add a compatibility matrix row (e.g.
`dbt-pgtrickle 0.5.x` → `pg_trickle ≥0.25.0`) so users know what to pin.
**Schema change:** No.

---

### Test Coverage

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| TEST-1 | E2E: snapshot atomicity under crash (orphan table detection) | M | P0 |
| TEST-2 | E2E: snapshot version mismatch | S | P1 |
| TEST-3 | E2E: predictive planner with N < `min_samples` | S | P1 |
| TEST-4 | E2E: multi-DB worker fairness under contention | M | P1 |
| TEST-5 | Integration: `IVM_DELTA_CACHE` bounded over 1000 ALTER QUERY cycles | S | P1 |
| TEST-6 | Integration: `classify_spi_error_retryable` with `lc_messages=fr_FR` | S | P0 |
| TEST-7 | Fuzz: WAL decoder | M | P1 |
| TEST-8 | Fuzz: MERGE template generator | M | P1 |
| TEST-9 | Fuzz: snapshot SQL builder | S | P1 |
| TEST-10 | Fuzz: DAG SCC graph shapes | S | P2 |
| TEST-11 | EC-01 deterministic reproducer (replaces flaky `test_tpch_q07_*`) | M | P0 |
| TEST-12 | E2E: snapshot under concurrent `refresh_stream_table` | S | P1 |
| TEST-13 | E2E: snapshot → `pg_dump` → `pg_restore` round-trip | M | P1 |
| TEST-14 | Promote G17-MDB multi-database soak test from `stability-tests.yml` to `ci.yml` | S | P1 |
| TEST-15 | E2E: WAL decoder failure injection (slot missing / wrong plugin) | S | P2 |
| TEST-16 | E2E: statement-level CDC with mixed INSERT/UPDATE/DELETE ordering invariants | S | P2 |
| TEST-17 | E2E: `restore_from_snapshot` rollback on mid-INSERT constraint violation | S | P2 |
| TEST-18 | Property: `cluster_worker_summary` consistency under crash (proptest) | S | P2 |
| TEST-19 | Property: predictive planner monotone in history length (proptest) | S | P2 |

---

### Conflicts & Risks

- **STAB-3** requires a schema migration (adding `cached_at` column). Include
  in `sql/pg_trickle--0.29.0--0.30.0.sql`; verify via
  `scripts/check_upgrade_completeness.sh`.
- **CORR-1** touches the DVM join delta pipeline — the highest-risk module.
  Must be gated behind full TPC-H property test suite before merge.
- **PERF-1** requires `dshash` pgrx bindings that may not yet be fully
  stabilised in pgrx 0.18; de-risk with a spike before committing to the
  milestone.
- **SCAL-1** changes observable retry semantics. Roll out behind a
  `pg_trickle.use_sqlstate_classification` GUC (default `false`) initially,
  flip to `true` in v0.31.0 after one release of parallel validation.
- **UX-14** (CNPG 1.29) depends on CNPG 1.29 being available; if the release
  slips past the v0.30.0 window, defer to v1.0.0 but keep the tracking issue
  open.
- **UX-15** (dbt 1.11) is purely a tracking concern — no code changes are
  required in this release.

### Implementation Phases

| Phase | Description | Duration |
|-------|-------------|----------|
| Phase 1 | P0 correctness + stability: CORR-1, STAB-1, STAB-4, SCAL-1 | Days 1–6 |
| Phase 2 | P0 docs: UX-1, UX-2, TEST-6, TEST-11 | Days 6–9 |
| Phase 3 | P1 stability + safety: STAB-2, STAB-3, STAB-5, STAB-6, CORR-2, CORR-3 | Days 9–14 |
| Phase 4 | P1 architecture: SCAL-2, PERF-1, PERF-2 | Days 14–19 |
| Phase 5 | P1/P2 test coverage: TEST-1 through TEST-19, PERF-3 | Days 19–27 |
| Phase 6 | P1/P2/P3 docs & UX: UX-3 through UX-18, TUI parity | Days 27–34 |

> **v0.30.0 total: ~7–8 weeks** (correctness-critical path ~10 days;
> documentation and test coverage ~14 days; architecture improvements ~8 days;
> new test targets ~8 days; Low-priority doc polish ~4 days)

**Exit criteria:**
- [x] CORR-1: `test_tpch_q07_ec01b_combined_delete` passes reliably over 50 runs; IMMEDIATE-mode property tests show zero phantom drift
- [x] CORR-2: Parser emits `NOTICE` when SubLink inside CASE/COALESCE forces FULL downgrade
- [x] CORR-3: `restore_from_snapshot` uses explicit column list; works on PG 18.0 and 18.x
- [x] STAB-1: Snapshot under crash leaves no orphan tables (TEST-1); restore under concurrent refresh is safe
- [x] STAB-2: `IVM_DELTA_CACHE` size bounded by `template_cache_max_entries` (TEST-5)
- [x] STAB-3: L2 template-cache catalog table purged by scheduler tick; `cached_at` column present in migration
- [x] STAB-4: `snapshot_stream_table` emits `WARNING` when catalog INSERT fails
- [x] STAB-5: `wal_decoder.rs:307` uses `c"test_decoding"`; `just lint` clean
- [x] STAB-6: IVM cache cleared on subxact abort via `XactCallback`
- [x] SCAL-1: Retry classification by SQLSTATE; `lc_messages=fr_FR` test passes (TEST-6)
- [ ] PERF-3: All seven missing benchmarks present and passing Criterion baseline gate (deferred to v0.31.0)
- [x] UX-1: All Appendix C GUCs documented in `CONFIGURATION.md`
- [x] UX-2: `UPGRADING.md` covers v0.15.0 → v0.27.0
- [ ] UX-3: All new error variants have HINT/DETAIL in `ERRORS.md` (deferred to v0.31.0)
- [ ] UX-4: All eight SNAP/PLAN/CLUS/METR functions have full SQL_REFERENCE entries (deferred to v0.31.0)
- [ ] UX-5: TUI shows SNAP/PLAN/CLUS/METR panels (deferred to v0.31.0)
- [ ] UX-6: `monitoring/grafana/pg_trickle.json` ships and renders in Grafana 11+ (deferred to v0.31.0)
- [ ] UX-9: `PRE_DEPLOYMENT.md` documents `change_buffer_durability` and `frontier_holdback_lsn_bytes` (deferred to v0.31.0)
- [ ] UX-10: `BACKUP_AND_RESTORE.md` Known Limitations section added; concurrent-restore warning present (deferred to v0.31.0)
- [ ] UX-14: `cnpg/cluster-example.yaml` updated to CNPG 1.29; CI job green (deferred to v0.31.0)
- [ ] UX-15: dbt-core 1.11 tracking issue opened with mashumaro blocker documented (deferred to v0.31.0)
- [ ] TEST-7/8/9: New fuzz targets run in CI; no crashes after 24 h (deferred to v0.31.0)
- [ ] TEST-12: Snapshot under concurrent refresh test passes (deferred to v0.31.0)
- [ ] TEST-13: `pg_dump` → `pg_restore` round-trip test passes for snapshot tables (deferred to v0.31.0)
- [ ] TEST-14: G17-MDB multi-database soak test runs in `ci.yml` on every push to main (deferred to v0.31.0)
- [x] SCAL-3: Dead `#[allow(unused_imports)]` removed from `refresh/orchestrator.rs`; `just lint` clean
- [x] Extension upgrade path tested (`0.29.0 → 0.30.0`)
- [x] `just check-version-sync` passes

---

