Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ JSON arguments can also be piped on stdin, for tools that take arguments. A tool
|------|-------------|
| `index_repository` | Index a repository into the graph. Auto-sync keeps it fresh after that. |
| `list_projects` | List all indexed projects with node/edge counts. |
| `delete_project` | Remove a project and all its graph data. |
| `delete_project` | Remove a project and all its graph data, including its ADR sidecar (`<db>.adr.db`). |
| `index_status` | Check indexing status of a project. |
| `check_index_coverage` | Check whether exact paths or a scope are indexed and fresh. A clean result means no recorded gap, not proof of completeness. |

Expand Down
4 changes: 2 additions & 2 deletions scripts/memory-core-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ src/pipeline/pass_semantic.c 3
src/pipeline/pass_tests.c 3
src/pipeline/pass_usages.c 4
src/pipeline/path_alias.c 42
src/pipeline/pipeline.c 134
src/pipeline/pipeline.c 131
src/pipeline/pipeline_delta.c 19
src/pipeline/pipeline_incremental.c 144
src/pipeline/pipeline_incremental.c 134
src/pipeline/registry.c 17
src/pipeline/worker_pool.c 2
src/store/store.c 358
Expand Down
13 changes: 11 additions & 2 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -7193,6 +7193,13 @@ static const char *get_cache_dir(const char *home_dir) {
return cbm_resolve_cache_dir();
}

/* A per-project ADR sidecar ("<db>.adr.db") sits in the cache beside its index
* but is not itself an index — exclude it from listing/counting. */
static bool cli_is_adr_sidecar(const char *name, size_t len) {
const size_t ext = sizeof(".adr.db") - 1;
return len >= ext && strcmp(name + len - ext, ".adr.db") == 0;
}

int cbm_list_indexes(const char *home_dir) {
const char *cache_dir = get_cache_dir(home_dir);
if (!cache_dir) {
Expand All @@ -7208,7 +7215,8 @@ int cbm_list_indexes(const char *home_dir) {
cbm_dirent_t *ent;
while ((ent = cbm_readdir(d)) != NULL) {
size_t len = strlen(ent->name);
if (len > DB_EXT_LEN && strcmp(ent->name + len - DB_EXT_LEN, ".db") == 0) {
if (len > DB_EXT_LEN && strcmp(ent->name + len - DB_EXT_LEN, ".db") == 0 &&
!cli_is_adr_sidecar(ent->name, len)) {
printf(" %s/%s\n", cache_dir, ent->name);
count++;
}
Expand Down Expand Up @@ -10437,7 +10445,8 @@ static int count_db_indexes(const char *home) {
cbm_dirent_t *ent;
while ((ent = cbm_readdir(d)) != NULL) {
size_t len = strlen(ent->name);
if (len > DB_EXT_LEN && strcmp(ent->name + len - DB_EXT_LEN, ".db") == 0) {
if (len > DB_EXT_LEN && strcmp(ent->name + len - DB_EXT_LEN, ".db") == 0 &&
!cli_is_adr_sidecar(ent->name, len)) {
count++;
}
}
Expand Down
37 changes: 32 additions & 5 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ static const tool_def_t TOOLS[] = {
"\"offset\":{\"type\":\"integer\",\"default\":0,\"minimum\":0},"
"\"metadata_only\":{\"type\":\"boolean\",\"default\":false,"
"\"description\":\"Compatibility: omit counts, size, and branch.\"}}}"},
{"delete_project", "Delete a project from the index",
{"delete_project", "Delete a project from the index, including its ADR store",
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"}},\"required\":["
"\"project\"]}"},

Expand Down Expand Up @@ -2623,6 +2623,11 @@ static bool is_project_db_file(const char *name, size_t len) {
if (len < MCP_MIN_DB_NAME || strcmp(name + len - MCP_DB_EXT, ".db") != 0) {
return false;
}
/* A per-project ADR sidecar ("<db>.adr.db") ends in ".db" but is not itself
* an index — skip it in every cache/cross-repo scan (matches the CLI). */
if (len >= SLEN(".adr.db") && strcmp(name + len - SLEN(".adr.db"), ".adr.db") == 0) {
return false;
}
if (strncmp(name, "_", SLEN("_")) == 0 || strncmp(name, ":memory:", SLEN(":memory:")) == 0) {
return false;
}
Expand Down Expand Up @@ -6809,7 +6814,9 @@ static char *handle_index_status(cbm_mcp_server_t *srv, const char *args) {
return result;
}

/* delete_project: just erase the .db file (and WAL/SHM). */
/* delete_project: erase the .db file (and WAL/SHM) plus the ADR sidecar
* "<db>.adr.db" (and its WAL/SHM/journal), so deleting a project removes its
* ADR store with it. */
static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
char *name = get_project_arg(args);
if (!name) {
Expand Down Expand Up @@ -6848,18 +6855,37 @@ static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
const char *error_detail = NULL;
bool is_error = false;

/* ADR sidecar lives beside the graph DB as "<db>.adr.db" — remove it (and
* its own WAL/SHM/journal) whenever the project is deleted. Sized as the
* graph path plus the ".adr.db" suffix. */
char adr_path[CBM_SZ_1K + sizeof(".adr.db")];
int adr_n = snprintf(adr_path, sizeof(adr_path), "%s.adr.db", path);
bool have_adr_path = adr_n > 0 && (size_t)adr_n < sizeof(adr_path);

if (exists) {
int rc = cbm_unlink(path);
(void)cbm_unlink(wal);
(void)cbm_unlink(shm);
if (rc == 0) {
status = "deleted";
/* Only drop the ADR store once the graph DB is actually gone, so a
* failed graph delete leaves the project (and its ADR) intact. */
if (have_adr_path) {
(void)cbm_unlink(adr_path);
(void)cbm_remove_db_sidecars(adr_path);
}
} else {
status = "delete_failed";
error_detail = strerror(errno);
is_error = true;
}
} else {
/* No graph DB, but a sidecar could still linger (e.g. ADR written then
* the index deleted out of band) — best-effort clean it up. */
if (have_adr_path) {
(void)cbm_unlink(adr_path);
(void)cbm_remove_db_sidecars(adr_path);
}
is_error = true;
}

Expand Down Expand Up @@ -17173,9 +17199,10 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
}
}

/* ADRs are stored in the SQLite store (project_summaries), the SAME
* backend the UI /api/adr endpoints use — so writes via the MCP tool and
* the UI are visible to each other (#256). */
/* ADRs are stored in the per-project ADR sidecar DB ("<db>.adr.db"), the
* SAME backend the UI /api/adr endpoints use (via cbm_store_adr_*) — so
* writes via the MCP tool and the UI are visible to each other (#256), and
* a reindex of the graph DB never disturbs them. */
store_recovery_status_t recovery_status = STORE_RECOVERY_NONE;
cbm_store_t *resolved =
resolve_store_internal(srv, project, mutation_held, !write_request, &recovery_status, true);
Expand Down
85 changes: 77 additions & 8 deletions src/pipeline/artifact.c
Original file line number Diff line number Diff line change
Expand Up @@ -888,13 +888,54 @@ static void artifact_snapshot_tmp_close(artifact_snapshot_tmp_t *tmp) {
tmp->dir[0] = '\0';
}

/* Make the snapshot's project_summaries carry exactly the current ADR — and
* nothing stale. ADRs now live in the "<db>.adr.db" sidecar, but a legacy
* pre-sidecar row can still linger in the graph DB and gets copied into the
* snapshot by VACUUM INTO; incremental/closure-repair clones keep carrying it.
* So ALWAYS clear project_summaries first, then insert only when an ADR is
* present (`adr` is NULL when the sidecar has none). Otherwise a no-ADR export
* would ship the stale row and a teammate's import would restore a decision
* record that was deleted. Best-effort: on any failure the artifact simply
* omits the ADR, as before the sidecar existed. */
static void snapshot_inject_adr(const char *snapshot_path, const cbm_adr_t *adr) {
sqlite3 *db = NULL;
if (sqlite3_open_v2(snapshot_path, &db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK) {
sqlite3_close(db);
return;
}
sqlite3_exec(db,
"CREATE TABLE IF NOT EXISTS project_summaries ("
" project TEXT PRIMARY KEY, summary TEXT NOT NULL, source_hash TEXT NOT NULL,"
" created_at TEXT NOT NULL, updated_at TEXT NOT NULL);",
NULL, NULL, NULL);
/* Drop any stale row (a deleted ADR must not resurrect through the export). */
sqlite3_exec(db, "DELETE FROM project_summaries;", NULL, NULL, NULL);
if (adr && adr->project && adr->content) {
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db,
"INSERT INTO project_summaries "
"(project, summary, source_hash, created_at, updated_at) "
"VALUES (?1, ?2, '', ?3, ?4);",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, adr->project, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 2, adr->content, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 3, adr->created_at ? adr->created_at : "", -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 4, adr->updated_at ? adr->updated_at : "", -1, SQLITE_TRANSIENT);
sqlite3_step(st);
sqlite3_finalize(st);
}
}
sqlite3_close(db);
}

/* Prepare a stripped DB copy for best-quality export.
* VACUUM INTO → (optionally) drop indexes → VACUUM. Returns malloc'd buffer
* or NULL. VACUUM INTO runs on BOTH quality levels: it is the consistent
* snapshot — the store runs in WAL mode, so raw main-file bytes miss
* committed transactions still in the -wal and can be mid-checkpoint torn
* (#895). Only the index-stripping is BEST-only. */
static char *prepare_snapshot_db(const char *db_path, size_t *out_size, bool strip_indexes) {
* VACUUM INTO → (optionally) drop indexes → VACUUM → (optionally) inject the
* ADR. Returns malloc'd buffer or NULL. VACUUM INTO runs on BOTH quality
* levels: it is the consistent snapshot — the store runs in WAL mode, so raw
* main-file bytes miss committed transactions still in the -wal and can be
* mid-checkpoint torn (#895). Only the index-stripping is BEST-only. */
static char *prepare_snapshot_db(const char *db_path, size_t *out_size, bool strip_indexes,
const cbm_adr_t *adr) {
artifact_snapshot_tmp_t tmp;
if (!artifact_snapshot_tmp_open(&tmp)) {
artifact_export_fail("prepare_snapshot_dir", cbm_tmpdir(), "private_tmpdir_failed", errno);
Expand Down Expand Up @@ -939,6 +980,11 @@ static char *prepare_snapshot_db(const char *db_path, size_t *out_size, bool str
}
}

/* Carry the ADR (from the sidecar) into the snapshot so the exported
* artifact ships the team's decision record. After the strip/VACUUM, so the
* row is not dropped. */
snapshot_inject_adr(tmp_path, adr);

/* Reopened by path rather than held on a descriptor across VACUUM INTO,
* because sqlite owns the create. The private directory is what makes that
* safe: an attacker who cannot enter it cannot swap the file underneath. */
Expand Down Expand Up @@ -984,15 +1030,28 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char *
char *db_data = NULL;
int compression_level = ART_ZSTD_FAST;

/* Fetch the ADR from the sidecar (read-only; does not create it) so the
* snapshot can carry it — the team-shared artifact used to include the ADR
* via project_summaries, and the sidecar must not silently break that. */
cbm_adr_t adr = {0};
bool have_adr = false;
cbm_store_t *adr_src = cbm_store_open_path_query(db_path);
if (adr_src) {
have_adr = cbm_store_adr_get(adr_src, project_name, &adr) == CBM_STORE_OK;
cbm_store_close(adr_src);
}
const cbm_adr_t *adr_arg = have_adr ? &adr : NULL;

if (quality == CBM_ARTIFACT_BEST) {
compression_level = ART_ZSTD_BEST;
db_data = prepare_snapshot_db(db_path, &db_size, true);
db_data = prepare_snapshot_db(db_path, &db_size, true, adr_arg);
} else {
/* FAST keeps zstd-3 and its indexes, but still snapshots via
* VACUUM INTO: the raw main-file bytes of a live WAL store are a
* torn copy (#895). */
db_data = prepare_snapshot_db(db_path, &db_size, false);
db_data = prepare_snapshot_db(db_path, &db_size, false, adr_arg);
}
cbm_store_adr_free(&adr);

if (!db_data || db_size == 0) {
free(db_data);
Expand Down Expand Up @@ -1202,6 +1261,16 @@ int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
cbm_unlink(wal);
cbm_unlink(shm);

/* Restore the imported ADR (carried in the graph DB's project_summaries)
* into the per-project sidecar, so a teammate's first index starts with the
* team's decisions. migrate-once uses INSERT OR IGNORE, so it never clobbers
* an ADR already present locally. Best-effort. */
cbm_store_t *restore = cbm_store_open_path_existing(cache_db_path);
if (restore) {
(void)cbm_store_adr_migrate_once(restore);
cbm_store_close(restore);
}

cbm_log_info("artifact.import", "db", cache_db_path, "size_mb",
itoa_buf((int)((size_t)dlen / ART_BYTES_PER_MB)));

Expand Down
Loading
Loading