Add post-dump plausibility gate returning status:degraded (#334)

Compare persisted SQLite node counts to in-memory dump counts after
index_repository completes so partial WAL/durability loss surfaces as
status:"degraded" instead of silent indexed.

Signed-off-by: Sam Li <yangsec888@gmail.com>
This commit is contained in:
Sam Li authored and soren committed 2026-06-23 06:38:45 +09:00
1 parent acb7eaaa05
commit 559993a853
10 files changed
+439 -17

No files matched your search

+6 -3
View File
@@ -111,7 +111,8 @@ FOUNDATION_SRCS = \
src/foundation/compat_regex.c \
src/foundation/mem.c \
src/foundation/diagnostics.c \
src/foundation/profile.c
src/foundation/profile.c \
src/foundation/dump_verify.c
# Existing extraction C code (compiled from current location)
EXTRACTION_SRCS = \
@@ -295,7 +296,8 @@ TEST_FOUNDATION_SRCS = \
tests/test_str_intern.c \
tests/test_log.c \
tests/test_str_util.c \
tests/test_platform.c
tests/test_platform.c \
tests/test_dump_verify.c
TEST_EXTRACTION_SRCS = \
tests/test_extraction.c \
@@ -313,7 +315,8 @@ TEST_STORE_SRCS = \
tests/test_store_arch.c \
tests/test_store_bulk.c \
tests/test_store_pragmas.c \
tests/test_store_checkpoint.c
tests/test_store_checkpoint.c \
tests/test_dump_verify_io.c
TEST_CYPHER_SRCS = \
tests/test_cypher.c
+1
View File
@@ -445,6 +445,7 @@ codebase-memory-mcp config reset auto_index # reset to default
| `CBM_DOWNLOAD_URL` | *(GitHub releases)* | Override the download URL for updates. Used for testing or self-hosted deployments. |
| `CBM_LOG_LEVEL` | `info` | Set the minimum log level. Accepted values (case-insensitive): `debug`, `info`, `warn`, `error`, `none` — or their numeric equivalents `0`–`4` matching the internal enum. Logs go to stderr; stdout is reserved for MCP JSON-RPC. |
| `CBM_WORKERS` | *(detected)* | Override the parallel-indexing worker count returned by `cbm_default_worker_count`. Useful inside containers where `sysconf(_SC_NPROCESSORS_ONLN)` reports host CPUs rather than the cgroup's effective quota. Range 1–256; invalid values are ignored with a warning. |
| `CBM_DUMP_VERIFY_MIN_RATIO` | `0.5` | After indexing, compare persisted SQLite node count to the in-memory dump count. When persisted nodes fall below this fraction of committed nodes (and committed > 50), `index_repository` returns `status:"degraded"` instead of silent `indexed`. Range 0–1; set `0` to disable. Invalid values are ignored with a warning. |
```bash
# Store indexes in a custom directory
+40
View File
@@ -0,0 +1,40 @@
/*
* dump_verify.c — Post-dump plausibility gate (#334).
*/
#include "foundation/dump_verify.h"
#include "foundation/constants.h"
#include "foundation/log.h"
#include "foundation/platform.h"
#include <stdlib.h>
#include <string.h>
bool cbm_dump_verify_is_degraded(int committed_nodes, int persisted_nodes, double ratio,
int min_floor) {
if (ratio <= 0.0) {
return false;
}
if (committed_nodes < 0) {
return false;
}
if (committed_nodes <= min_floor) {
return false;
}
if (persisted_nodes < 0) {
return true;
}
return (double)persisted_nodes < (double)committed_nodes * ratio;
}
double cbm_dump_verify_min_ratio(void) {
char buf[CBM_SZ_32];
if (cbm_safe_getenv("CBM_DUMP_VERIFY_MIN_RATIO", buf, sizeof(buf), NULL) != NULL) {
char *end = NULL;
double r = strtod(buf, &end);
if (end != buf && r >= 0.0 && r <= 1.0) {
return r;
}
cbm_log_warn("dump_verify.env.invalid", "value", buf, "fallback", "0.5");
}
return CBM_DUMP_VERIFY_DEFAULT_RATIO;
}
+32
View File
@@ -0,0 +1,32 @@
/*
* dump_verify.h — Post-dump plausibility gate (#334 design b).
*
* Compares committed in-memory node counts against persisted SQLite rows
* after index_repository completes. Nodes-only gate (edges shrink legitimately
* at dump when endpoints fail to resolve).
*/
#ifndef CBM_DUMP_VERIFY_H
#define CBM_DUMP_VERIFY_H
#include <stdbool.h>
/** Repos with at most this many committed nodes skip the ratio gate. */
enum { CBM_DUMP_VERIFY_MIN_FLOOR = 50 };
/** Default minimum persisted/committed ratio when env is unset. */
#define CBM_DUMP_VERIFY_DEFAULT_RATIO 0.5
/**
* True when persisted_nodes is implausibly below committed_nodes.
*
* Returns false when ratio <= 0 (gate disabled), committed_nodes < 0 (no dump),
* committed_nodes <= min_floor (sparse repo), or persisted >= committed * ratio.
* Returns true when persisted_nodes < 0 (count error).
*/
bool cbm_dump_verify_is_degraded(int committed_nodes, int persisted_nodes, double ratio,
int min_floor);
/** Read CBM_DUMP_VERIFY_MIN_RATIO (0..1); invalid/unset -> default 0.5. Set 0 to disable. */
double cbm_dump_verify_min_ratio(void);
#endif /* CBM_DUMP_VERIFY_H */
+70 -14
View File
@@ -54,6 +54,7 @@ enum {
#include "foundation/compat_thread.h"
#include "foundation/log.h"
#include "foundation/str_util.h"
#include "foundation/dump_verify.h"
#include "foundation/compat_regex.h"
#include "pipeline/artifact.h"
@@ -2687,28 +2688,82 @@ static void add_excluded_summary(yyjson_mut_doc *doc, yyjson_mut_val *root, char
yyjson_mut_obj_add_val(doc, root, "excluded", excluded);
}
/* Build the success portion of the index_repository response. */
static void build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *doc,
/* Build the success portion of the index_repository response.
* Returns true when status should be "degraded" (#334 plausibility gate). */
static bool build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *doc,
yyjson_mut_val *root, const char *project_name,
const char *repo_path, bool persistence,
const char *repo_path, bool persistence, cbm_pipeline_t *p,
char **excluded_dirs, int excluded_count) {
add_excluded_summary(doc, root, excluded_dirs, excluded_count);
int exp_nodes = -1;
int exp_edges = -1;
cbm_pipeline_get_committed_counts(p, &exp_nodes, &exp_edges);
const double ratio = cbm_dump_verify_min_ratio();
const int min_floor = CBM_DUMP_VERIFY_MIN_FLOOR;
cbm_store_t *store = resolve_store(srv, project_name);
int nodes = 0;
int edges = 0;
bool degraded = false;
if (!store) {
return;
degraded = true;
} else {
nodes = cbm_store_count_nodes(store, project_name);
edges = cbm_store_count_edges(store, project_name);
if (nodes < 0) {
degraded = true;
nodes = 0;
edges = edges >= 0 ? edges : 0;
} else if (cbm_dump_verify_is_degraded(exp_nodes, nodes, ratio, min_floor)) {
(void)cbm_store_checkpoint(store);
int nodes2 = cbm_store_count_nodes(store, project_name);
int edges2 = cbm_store_count_edges(store, project_name);
if (nodes2 >= 0) {
nodes = nodes2;
}
if (edges2 >= 0) {
edges = edges2;
}
degraded = cbm_dump_verify_is_degraded(exp_nodes, nodes, ratio, min_floor);
}
}
int nodes = cbm_store_count_nodes(store, project_name);
int edges = cbm_store_count_edges(store, project_name);
yyjson_mut_obj_add_int(doc, root, "nodes", nodes);
yyjson_mut_obj_add_int(doc, root, "edges", edges);
if (exp_nodes >= 0) {
yyjson_mut_obj_add_int(doc, root, "expected_nodes", exp_nodes);
yyjson_mut_obj_add_int(doc, root, "expected_edges", exp_edges);
}
if (degraded) {
if (!store) {
yyjson_mut_obj_add_str(doc, root, "hint",
"Index database failed integrity check and was removed. "
"Re-run index_repository(repo_path=...) to rebuild.");
cbm_log_warn("dump.verify", "reason", "store_missing", "expected_nodes",
exp_nodes >= 0 ? "set" : "unknown");
} else {
char exp_buf[MCP_FIELD_SIZE];
char got_buf[MCP_FIELD_SIZE];
snprintf(exp_buf, sizeof(exp_buf), "%d", exp_nodes);
snprintf(got_buf, sizeof(got_buf), "%d", nodes);
yyjson_mut_obj_add_str(
doc, root, "hint",
"Persisted far fewer nodes than indexed — likely durability loss from a "
"hard-killed sibling process. Re-run index_repository(repo_path=...) to rebuild.");
cbm_log_warn("dump.verify", "expected_nodes", exp_buf, "persisted_nodes", got_buf);
}
}
char adr_path[CBM_SZ_4K];
snprintf(adr_path, sizeof(adr_path), "%s/.codebase-memory/adr.md", repo_path);
struct stat adr_st;
bool adr_exists = (stat(adr_path, &adr_st) == 0);
yyjson_mut_obj_add_bool(doc, root, "adr_present", adr_exists);
if (!adr_exists) {
if (!adr_exists && !degraded) {
yyjson_mut_obj_add_str(
doc, root, "adr_hint",
"Project indexed. Consider creating an Architecture Decision Record: "
@@ -2723,6 +2778,8 @@ static void build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *
"Persistent artifact written to .codebase-memory/graph.db.zst. "
"Commit this file to share the index with teammates.");
}
return degraded;
}
static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
@@ -2803,19 +2860,18 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
yyjson_mut_doc_set_root(doc, root);
yyjson_mut_obj_add_str(doc, root, "project", project_name);
yyjson_mut_obj_add_str(doc, root, "status", rc == 0 ? "indexed" : "error");
if (rc != 0) {
if (rc == 0) {
bool degraded = build_index_success_response(srv, doc, root, project_name, repo_path,
persistence, p, excluded_dirs, excluded_count);
yyjson_mut_obj_add_str(doc, root, "status", degraded ? "degraded" : "indexed");
} else {
yyjson_mut_obj_add_str(doc, root, "status", "error");
yyjson_mut_obj_add_str(doc, root, "hint",
"Pipeline failed. Check repo_path exists and contains source files. "
"Try mode='fast' for a quicker diagnostic run.");
}
if (rc == 0) {
build_index_success_response(srv, doc, root, project_name, repo_path, persistence,
excluded_dirs, excluded_count);
}
char *json = yy_doc_to_str(doc);
yyjson_mut_doc_free(doc);
/* Free the pipeline only after the response doc copied the excluded list. */
+17
View File
@@ -93,6 +93,10 @@ struct cbm_pipeline {
/* User-defined extension overrides (loaded once per run) */
cbm_userconfig_t *userconfig;
/* Committed graph size at dump time (-1 = dump did not run). #334 gate axis. */
int committed_nodes;
int committed_edges;
};
/* ── Global pkgmap (one active pipeline at a time) ─────────────── */
@@ -154,6 +158,8 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path,
p->branch_qn = cbm_git_context_branch_qn(p->project_name, &p->git_ctx);
p->mode = mode;
p->persistence = false;
p->committed_nodes = -1;
p->committed_edges = -1;
atomic_init(&p->cancelled, 0);
return p;
@@ -218,6 +224,15 @@ void cbm_pipeline_get_excluded(const cbm_pipeline_t *p, char ***out, int *count)
}
}
void cbm_pipeline_get_committed_counts(const cbm_pipeline_t *p, int *nodes, int *edges) {
if (nodes) {
*nodes = p ? p->committed_nodes : -1;
}
if (edges) {
*edges = p ? p->committed_edges : -1;
}
}
/* Resolve the DB path for this pipeline. Caller must free(). */
static char *resolve_db_path(const cbm_pipeline_t *p) {
char *path = malloc(CBM_SZ_1K);
@@ -837,6 +852,8 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil
cbm_log_error("pipeline.err", "phase", "dump");
return rc;
}
p->committed_nodes = cbm_gbuf_node_count(p->gbuf);
p->committed_edges = cbm_gbuf_edge_count(p->gbuf);
cbm_log_info("pass.timing", "pass", "dump", "elapsed_ms", itoa_buf((int)elapsed_ms(*t)));
cbm_store_t *hash_store = cbm_store_open_path(db_path);
if (hash_store) {
+4
View File
@@ -72,6 +72,10 @@ int cbm_pipeline_get_mode(const cbm_pipeline_t *p);
* to NULL/0 when p is NULL or nothing was excluded. Do not free. */
void cbm_pipeline_get_excluded(const cbm_pipeline_t *p, char ***out, int *count);
/* Committed node/edge counts captured at dump time (-1 when dump did not run).
* Nodes are the #334 plausibility-gate axis; edges are informational only. */
void cbm_pipeline_get_committed_counts(const cbm_pipeline_t *p, int *nodes, int *edges);
/* ── Index lock (prevents concurrent pipeline runs on same DB) ──── */
/* Try to acquire the global index lock. Returns true if acquired,
+89
View File
@@ -0,0 +1,89 @@
/*
* test_dump_verify.c — Post-dump plausibility gate (#334).
*
* Pure-function matrix mirrors sast-ai-app checkSilentDegradation cases.
* I/O-level coverage that drives the gate against a real on-disk SQLite store
* lives in test_dump_verify_io.c (store-linked, excluded from test-foundation).
*/
#include "../src/foundation/compat.h"
#include "../src/foundation/dump_verify.h"
#include "test_framework.h"
#include <stdlib.h>
TEST(dump_verify_no_baseline) {
ASSERT_FALSE(cbm_dump_verify_is_degraded(-1, 500, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_sparse_at_floor) {
ASSERT_FALSE(cbm_dump_verify_is_degraded(50, 10, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
ASSERT_FALSE(cbm_dump_verify_is_degraded(12, 5, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_shortfall_below_ratio) {
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, 400, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_just_above_ratio) {
ASSERT_FALSE(cbm_dump_verify_is_degraded(1000, 500, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_just_below_ratio) {
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, 499, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_zero_persisted) {
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, 0, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_growth) {
ASSERT_FALSE(cbm_dump_verify_is_degraded(500, 750, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_count_error) {
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, -1, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_ratio_zero_disables) {
ASSERT_FALSE(cbm_dump_verify_is_degraded(1000, 10, 0.0, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_loosened_ratio) {
ASSERT_FALSE(cbm_dump_verify_is_degraded(1000, 600, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_tightened_ratio) {
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, 900, 0.95, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
TEST(dump_verify_edges_shrank_nodes_ok) {
/* Edges are not gated; this documents nodes-only semantics for integrators. */
ASSERT_FALSE(cbm_dump_verify_is_degraded(200, 200, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
PASS();
}
SUITE(dump_verify) {
RUN_TEST(dump_verify_no_baseline);
RUN_TEST(dump_verify_sparse_at_floor);
RUN_TEST(dump_verify_shortfall_below_ratio);
RUN_TEST(dump_verify_just_above_ratio);
RUN_TEST(dump_verify_just_below_ratio);
RUN_TEST(dump_verify_zero_persisted);
RUN_TEST(dump_verify_growth);
RUN_TEST(dump_verify_count_error);
RUN_TEST(dump_verify_ratio_zero_disables);
RUN_TEST(dump_verify_loosened_ratio);
RUN_TEST(dump_verify_tightened_ratio);
RUN_TEST(dump_verify_edges_shrank_nodes_ok);
}
+176
View File
@@ -0,0 +1,176 @@
/*
* test_dump_verify_io.c — I/O-level coverage for the #334 plausibility gate.
*
* Drives cbm_dump_verify_is_degraded against a real on-disk SQLite store and
* the actual cbm_store_count_nodes / cbm_store_open_path_query path used by
* resolve_store(). Includes a fork/crash port of bulk_crash_recovery proving
* uncommitted WAL frames are discarded on the next open and the gate trips when
* persisted rows fall short of the committed dump intent.
*
* Store-linked (excluded from the fast test-foundation target); runs under the
* full test-runner. The pure-function matrix lives in test_dump_verify.c.
*/
#include "../src/foundation/compat.h"
#include "../src/foundation/dump_verify.h"
#include "test_framework.h"
#include <stdio.h>
#include <stdlib.h>
#include <store/store.h>
#ifndef _WIN32
#include <sys/wait.h>
#include <unistd.h>
#endif
#define DV_PROJECT "io_dump_verify"
static void dv_temp_path(char *buf, size_t n) {
snprintf(buf, n, "%s/cmm_dump_verify_%d.db", cbm_tmpdir(), (int)getpid());
}
static void dv_cleanup(const char *path) {
remove(path);
char aux[640];
snprintf(aux, sizeof(aux), "%s-wal", path);
remove(aux);
snprintf(aux, sizeof(aux), "%s-shm", path);
remove(aux);
}
/* Commit `count` distinct nodes to the on-disk store at `path`. */
static int dv_write_nodes(const char *path, int count) {
cbm_store_t *s = cbm_store_open_path(path);
if (!s)
return -1;
if (cbm_store_upsert_project(s, DV_PROJECT, "/tmp/io_dump_verify") != CBM_STORE_OK) {
cbm_store_close(s);
return -1;
}
cbm_store_begin(s);
for (int i = 0; i < count; i++) {
char qn[64];
snprintf(qn, sizeof(qn), "io.node.%d", i);
cbm_node_t node = {.project = DV_PROJECT,
.label = "Function",
.name = "n",
.qualified_name = qn,
.file_path = "io.c",
.start_line = i,
.end_line = i};
if (cbm_store_upsert_node(s, &node) < 0) {
cbm_store_close(s);
return -1;
}
}
cbm_store_commit(s);
cbm_store_close(s);
return count;
}
/* Persisted node count via the same query path resolve_store() uses. */
static int dv_persisted_nodes(const char *path) {
cbm_store_t *s = cbm_store_open_path_query(path);
if (!s)
return CBM_STORE_ERR;
int n = cbm_store_count_nodes(s, DV_PROJECT);
cbm_store_close(s);
return n;
}
TEST(dump_verify_io_full_persist_ok) {
char path[640];
dv_temp_path(path, sizeof(path));
dv_cleanup(path);
ASSERT_EQ(dv_write_nodes(path, 200), 200);
int persisted = dv_persisted_nodes(path);
ASSERT_EQ(persisted, 200);
/* committed == persisted: a faithful dump is never degraded. */
ASSERT_FALSE(cbm_dump_verify_is_degraded(200, persisted, CBM_DUMP_VERIFY_DEFAULT_RATIO,
CBM_DUMP_VERIFY_MIN_FLOOR));
dv_cleanup(path);
PASS();
}
TEST(dump_verify_io_shortfall_after_delete) {
char path[640];
dv_temp_path(path, sizeof(path));
dv_cleanup(path);
ASSERT_EQ(dv_write_nodes(path, 1000), 1000);
ASSERT_FALSE(cbm_dump_verify_is_degraded(1000, dv_persisted_nodes(path),
CBM_DUMP_VERIFY_DEFAULT_RATIO,
CBM_DUMP_VERIFY_MIN_FLOOR));
/* Simulate persisted loss: most rows vanish while the committed intent
* (1000) is unchanged — the #334 silent-degradation signature. */
cbm_store_t *s = cbm_store_open_path(path);
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_exec(s, "DELETE FROM nodes WHERE start_line >= 100"), CBM_STORE_OK);
cbm_store_close(s);
int persisted = dv_persisted_nodes(path);
ASSERT_EQ(persisted, 100);
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, persisted, CBM_DUMP_VERIFY_DEFAULT_RATIO,
CBM_DUMP_VERIFY_MIN_FLOOR));
dv_cleanup(path);
PASS();
}
#ifndef _WIN32
TEST(dump_verify_io_fork_crash_uncommitted_discarded) {
char path[640];
dv_temp_path(path, sizeof(path));
dv_cleanup(path);
/* Baseline above the sparse floor is durably committed before the crash. */
ASSERT_EQ(dv_write_nodes(path, 60), 60);
pid_t pid = fork();
ASSERT_TRUE(pid >= 0);
if (pid == 0) {
/* Child: write a large uncommitted batch then crash without commit. */
cbm_store_t *s = cbm_store_open_path(path);
if (!s)
_exit(2);
cbm_store_begin(s);
for (int i = 1000; i < 6000; i++) {
char qn[64];
snprintf(qn, sizeof(qn), "io.node.%d", i);
cbm_node_t node = {.project = DV_PROJECT,
.label = "Function",
.name = "n",
.qualified_name = qn,
.file_path = "io.c",
.start_line = i,
.end_line = i};
cbm_store_upsert_node(s, &node);
}
/* No commit, no clean close: simulate a crash mid-dump. */
_exit(0);
}
int status = 0;
ASSERT_TRUE(waitpid(pid, &status, 0) == pid);
/* WAL recovery discards the child's uncommitted frames: only the baseline
* survives, so persisted (60) falls far short of the dump intent (5060). */
int persisted = dv_persisted_nodes(path);
ASSERT_EQ(persisted, 60);
ASSERT_TRUE(cbm_dump_verify_is_degraded(5060, persisted, CBM_DUMP_VERIFY_DEFAULT_RATIO,
CBM_DUMP_VERIFY_MIN_FLOOR));
dv_cleanup(path);
PASS();
}
#endif /* _WIN32 */
SUITE(dump_verify_io) {
RUN_TEST(dump_verify_io_full_persist_ok);
RUN_TEST(dump_verify_io_shortfall_after_delete);
#ifndef _WIN32
RUN_TEST(dump_verify_io_fork_crash_uncommitted_discarded);
#endif
}
+4
View File
@@ -97,6 +97,8 @@ extern void suite_grammar_probe_g(void);
extern void suite_incremental(void);
extern void suite_simhash(void);
extern void suite_stack_overflow(void);
extern void suite_dump_verify(void);
extern void suite_dump_verify_io(void);
/* Free the main thread's thread-local node-type bitset cache before exit so
* LeakSanitizer (Linux x64) doesn't report it. Worker threads free their own
@@ -114,6 +116,7 @@ int main(void) {
RUN_SUITE(log);
RUN_SUITE(str_util);
RUN_SUITE(platform);
RUN_SUITE(dump_verify);
/* Existing C code regression tests */
RUN_SUITE(ac);
@@ -131,6 +134,7 @@ int main(void) {
RUN_SUITE(store_bulk);
RUN_SUITE(store_pragmas);
RUN_SUITE(store_checkpoint);
RUN_SUITE(dump_verify_io);
/* Cypher (M6) */
RUN_SUITE(cypher);