Skip to content
Closed
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
3 changes: 2 additions & 1 deletion Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ GRAPH_BUFFER_SRCS = src/graph_buffer/graph_buffer.c
# Pipeline module (new)
PIPELINE_SRCS = \
src/pipeline/fqn.c \
src/pipeline/project_resolve.c \
src/pipeline/path_alias.c \
src/pipeline/registry.c \
src/pipeline/pipeline.c \
Expand Down Expand Up @@ -370,7 +371,7 @@ TEST_DISCOVER_SRCS = \

TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c

TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_project_resolve.c

TEST_WATCHER_SRCS = tests/test_watcher.c

Expand Down
16 changes: 12 additions & 4 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ enum {
#include <sqlite3.h>
#include "cypher/cypher.h"
#include "pipeline/pipeline.h"
#include "pipeline/project_resolve.h"
#include "pipeline/pass_cross_repo.h"
#include "git/git_context.h"
#include "cli/cli.h"
Expand Down Expand Up @@ -5624,10 +5625,17 @@ static void detect_session(cbm_mcp_server_t *srv) {
* used by the pipeline, otherwise session queries look for a .db file
* that doesn't match the indexed project name. */
if (srv->session_root[0]) {
char *pname = cbm_project_name_from_path(srv->session_root);
if (pname) {
snprintf(srv->session_project, sizeof(srv->session_project), "%s", pname);
free(pname);
char *existing = cbm_find_existing_project_name(srv->session_root);
if (existing) {
snprintf(srv->session_project, sizeof(srv->session_project), "%s", existing);
cbm_log_info("session.project.reuse", "project", existing, "path", srv->session_root);
free(existing);
} else {
char *pname = cbm_project_name_from_path(srv->session_root);
if (pname) {
snprintf(srv->session_project, sizeof(srv->session_project), "%s", pname);
free(pname);
}
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6, PL_WAL_BUF = 1040 };
#define PL_NSEC_PER_SEC 1000000000LL
#include "pipeline/pipeline.h"
#include "pipeline/project_resolve.h"
#include "pipeline/artifact.h"
#include "pipeline/pipeline_internal.h"
#include "pipeline/pass_lsp_cross.h"
Expand Down Expand Up @@ -166,7 +167,8 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path,

p->repo_path = strdup(repo_path);
p->db_path = db_path ? strdup(db_path) : NULL;
p->project_name = cbm_project_name_from_path(repo_path);
char *existing = cbm_find_existing_project_name(repo_path);
p->project_name = existing ? existing : cbm_project_name_from_path(repo_path);
(void)cbm_git_context_resolve(repo_path, &p->git_ctx);
p->branch_qn = cbm_git_context_branch_qn(p->project_name, &p->git_ctx);
p->mode = mode;
Expand Down Expand Up @@ -1265,6 +1267,9 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
rc = try_incremental_or_delete_db(p, files, file_count);
if (rc >= 0) {
cbm_discover_free(files, file_count);
if (rc == 0) {
cbm_project_identity_cache_invalidate();
}
return rc;
}
cbm_log_info("pipeline.route", "path", "full");
Expand Down Expand Up @@ -1320,5 +1325,8 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
cbm_set_user_lang_config(NULL);
cbm_userconfig_free(p->userconfig);
p->userconfig = NULL;
if (rc == 0) {
cbm_project_identity_cache_invalidate();
}
return rc;
}
221 changes: 221 additions & 0 deletions src/pipeline/project_resolve.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* project_resolve.c — Canonical path identity and duplicate-index prevention.
*/
#include "pipeline/project_resolve.h"
#include "foundation/platform.h"
#include "foundation/compat_fs.h"
#include "foundation/str_util.h"
#include "git/git_context.h"
#include "store/store.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
char *identity_key;
char *project_name;
} proj_identity_entry_t;

typedef struct {
bool loaded;
char cache_dir[1024];
proj_identity_entry_t *entries;
size_t count;
} proj_identity_cache_t;

static proj_identity_cache_t g_identity_cache;

bool cbm_path_canonicalize(const char *path, char *out, size_t out_sz) {
if (!path || !out || out_sz == 0) {
return false;
}
out[0] = '\0';
#ifdef _WIN32
if (!_fullpath(out, path, out_sz)) {
return false;
}
cbm_normalize_path_sep(out);
#else
if (!realpath(path, real)) {
return false;
}
#endif
return out[0] != '\0';
}

bool cbm_project_identity_key(const char *repo_path, char *out, size_t out_sz) {
if (!repo_path || !out || out_sz == 0) {
return false;
}

cbm_git_context_t ctx = {0};
if (cbm_git_context_resolve(repo_path, &ctx) == 0 && ctx.is_git && ctx.worktree_root &&
ctx.worktree_root[0]) {
bool ok = cbm_path_canonicalize(ctx.worktree_root, out, out_sz);
cbm_git_context_free(&ctx);
return ok;
}
cbm_git_context_free(&ctx);
return cbm_path_canonicalize(repo_path, out, out_sz);
}

static bool identity_is_child(const char *child, const char *parent) {
if (!child[0] || !parent[0]) {
return false;
}
if (strcmp(child, parent) == 0) {
return true;
}
size_t plen = strlen(parent);
if (strncmp(child, parent, plen) != 0) {
return false;
}
return child[plen] == '/';
}

static bool is_project_db_file(const char *name, size_t len) {
if (len < 5 || strcmp(name + len - 3, ".db") != 0) {
return false;
}
if (name[0] == '_') {
return false;
}
return true;
}

static void identity_cache_free(void) {
for (size_t i = 0; i < g_identity_cache.count; i++) {
free(g_identity_cache.entries[i].identity_key);
free(g_identity_cache.entries[i].project_name);
}
free(g_identity_cache.entries);
memset(&g_identity_cache, 0, sizeof(g_identity_cache));
}

void cbm_project_identity_cache_invalidate(void) {
identity_cache_free();
}

static bool identity_cache_load(void) {
const char *cache_dir = cbm_resolve_cache_dir();
if (g_identity_cache.loaded &&
strcmp(g_identity_cache.cache_dir, cache_dir) == 0) {
return true;
}

identity_cache_free();

cbm_dir_t *d = cbm_opendir(cache_dir);
if (!d) {
snprintf(g_identity_cache.cache_dir, sizeof(g_identity_cache.cache_dir), "%s", cache_dir);
g_identity_cache.loaded = true;
return true;
}

cbm_dirent_t *entry;
while ((entry = cbm_readdir(d)) != NULL) {
const char *name = entry->name;
size_t len = strlen(name);
if (!is_project_db_file(name, len)) {
continue;
}

char db_path[2048];
snprintf(db_path, sizeof(db_path), "%s/%s", cache_dir, name);

cbm_store_t *store = cbm_store_open_path_query(db_path);
if (!store) {
continue;
}

char project_name[1024];
snprintf(project_name, sizeof(project_name), "%.*s", (int)(len - 3), name);

cbm_project_t proj = {0};
if (cbm_store_get_project(store, project_name, &proj) != CBM_STORE_OK || !proj.root_path) {
safe_str_free(&proj.name);
safe_str_free(&proj.indexed_at);
safe_str_free(&proj.root_path);
cbm_store_close(store);
continue;
}

char indexed_key[4096];
bool has_key = cbm_project_identity_key(proj.root_path, indexed_key, sizeof(indexed_key));

safe_str_free(&proj.name);
safe_str_free(&proj.indexed_at);
safe_str_free(&proj.root_path);
cbm_store_close(store);

if (!has_key) {
continue;
}

proj_identity_entry_t row = {
.identity_key = strdup(indexed_key),
.project_name = strdup(project_name),
};
if (!row.identity_key || !row.project_name) {
free(row.identity_key);
free(row.project_name);
continue;
}

proj_identity_entry_t *grown =
realloc(g_identity_cache.entries,
(g_identity_cache.count + 1) * sizeof(*g_identity_cache.entries));
if (!grown) {
free(row.identity_key);
free(row.project_name);
continue;
}
g_identity_cache.entries = grown;
g_identity_cache.entries[g_identity_cache.count++] = row;
}

cbm_closedir(d);
snprintf(g_identity_cache.cache_dir, sizeof(g_identity_cache.cache_dir), "%s", cache_dir);
g_identity_cache.loaded = true;
return true;
}

char *cbm_find_existing_project_name(const char *repo_path) {
if (!repo_path || !repo_path[0]) {
return NULL;
}

char query_key[4096];
if (!cbm_project_identity_key(repo_path, query_key, sizeof(query_key))) {
return NULL;
}

if (!identity_cache_load()) {
return NULL;
}

char *best_name = NULL;
size_t best_root_len = 0;

for (size_t i = 0; i < g_identity_cache.count; i++) {
const char *indexed_key = g_identity_cache.entries[i].identity_key;
const char *project_name = g_identity_cache.entries[i].project_name;
if (!indexed_key || !project_name) {
continue;
}

if (!identity_is_child(query_key, indexed_key)) {
continue;
}

size_t root_len = strlen(indexed_key);
if (!best_name || root_len > best_root_len) {
free(best_name);
best_name = strdup(project_name);
best_root_len = root_len;
}
}

return best_name;
}
22 changes: 22 additions & 0 deletions src/pipeline/project_resolve.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#ifndef CBM_PROJECT_RESOLVE_H
#define CBM_PROJECT_RESOLVE_H

#include <stdbool.h>
#include <stddef.h>

/* Canonicalize path (realpath / _fullpath). Returns false if path is invalid. */
bool cbm_path_canonicalize(const char *path, char *out, size_t out_sz);

/* Stable per-worktree identity: canonicalized git worktree root when available,
* else canonical filesystem path. Distinct linked worktrees stay distinct. */
bool cbm_project_identity_key(const char *repo_path, char *out, size_t out_sz);

/* Return heap-allocated existing project name when repo_path matches a cached
* index (exact worktree identity or a subdirectory of an indexed root).
* Caller frees; NULL if no match. Uses a read-only scan cached until invalidate. */
char *cbm_find_existing_project_name(const char *repo_path);

/* Drop the cached identity scan (call after indexing completes). */
void cbm_project_identity_cache_invalidate(void);

#endif
Loading
Loading