diff --git a/Makefile.cbm b/Makefile.cbm index 200ee61c7..83bd1feba 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -417,6 +417,19 @@ PROD_SRCS = $(FOUNDATION_SRCS) $(STORE_SRCS) $(CYPHER_SRCS) $(MCP_SRCS) $(DAEMON EXISTING_C_SRCS = $(EXTRACTION_SRCS) $(LSP_SRCS) $(TS_RUNTIME_SRC) \ $(GRAMMAR_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) +# Project headers, as prerequisites for the targets built in one compiler +# invocation from sources (test-runner, test-repro-runner, test-runner-tsan, +# codebase-memory-mcp, test-foundation). Those rules list only .c files, so +# editing a header changed nothing make could see and the target was NOT +# rebuilt — an incremental build then silently tested the OLD header value. +# Found while revert-checking a constants.h change: the check passed because +# nothing had recompiled. +# +# Vendored trees are deliberately excluded — they are pinned, they carry their +# own explicit dep lists (LSP_UNITY_DEPS, TS_RUNTIME_DEPS), and globbing tens of +# thousands of grammar headers would cost more than it protects. +PROJECT_HDRS = $(wildcard src/*.h src/*/*.h $(CBM_DIR)/*.h tests/*.h tests/repro/*.h) + # ── Test sources ───────────────────────────────────────────────── TEST_FOUNDATION_SRCS = \ @@ -645,7 +658,7 @@ $(BUILD_DIR): # ── Foundation-only test (fast, no extraction) ─────────────────── -$(BUILD_DIR)/test-foundation: $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) | $(BUILD_DIR) +$(BUILD_DIR)/test-foundation: $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) $(PROJECT_HDRS) | $(BUILD_DIR) $(CC) $(CFLAGS_TEST) -o $@ $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) $(LDFLAGS_TEST) test-foundation: $(BUILD_DIR)/test-foundation @@ -761,7 +774,7 @@ $(UNIXCODER_OBJ): $(UNIXCODER_BLOB_SRC) vendored/nomic/code_vectors.bin | $(BUIL OBJS_VENDORED_TEST = $(MIMALLOC_OBJ_TEST) $(SQLITE3_OBJ_TEST) $(TRE_OBJ_TEST) $(GRAMMAR_OBJS_TEST) $(TS_RUNTIME_OBJ_TEST) $(LSP_OBJ_TEST) $(PP_OBJ_TEST) $(LZ4_OBJ_TEST) $(ZSTD_OBJ_TEST) $(UNIXCODER_OBJ) OBJS_VENDORED_TSAN = $(MIMALLOC_OBJ_TSAN) $(SQLITE3_OBJ_TSAN) $(TRE_OBJ_TSAN) $(GRAMMAR_OBJS_TSAN) $(TS_RUNTIME_OBJ_TSAN) $(LSP_OBJ_TSAN) $(PP_OBJ_TSAN) $(LZ4_OBJ_TSAN) $(ZSTD_OBJ_TSAN) $(UNIXCODER_OBJ) -$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR) +$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR) $(CC) $(CFLAGS_TEST) -Itests -Itests/repro -o $@ \ $(ALL_TEST_SRCS) $(PROD_SRCS) \ $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \ @@ -789,7 +802,7 @@ test-focused: $(BUILD_DIR)/test-runner # Mirrors test-runner's link line but uses repro_main.c (own main + counters) # and TEST_REPRO_SRCS instead of ALL_TEST_SRCS. Exits non-zero while any bug is # still reproduced (the expected state); bug-repro.yml surfaces it as a board. -$(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR) +$(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR) $(CC) $(CFLAGS_TEST) -Itests -o $@ \ $(TEST_REPRO_SRCS) $(PROD_SRCS) \ $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \ @@ -831,7 +844,7 @@ TSAN_OPTIONS ?= halt_on_error=1 # Normal ASan/native/soak paths remain uncapped. High-worker TSan diagnostics # invoke test-runner-tsan directly so release gates cannot drift accidentally. -$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) | $(BUILD_DIR) +$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) $(PROJECT_HDRS) | $(BUILD_DIR) $(CC) $(CFLAGS_TSAN) -Itests -Itests/repro -o $@ \ $(ALL_TEST_SRCS) $(PROD_SRCS) \ $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \ @@ -904,7 +917,7 @@ $(BUILD_DIR)/.build-config: build-config-check | $(BUILD_DIR) else mv -f $@.tmp $@; rm -f $(BUILD_DIR)/codebase-memory-mcp; \ echo "=== build config changed -> full rebuild: $(BUILD_CONFIG_SIG) ==="; fi -$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) $(BUILD_DIR)/.build-config | $(BUILD_DIR) +$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) $(BUILD_DIR)/.build-config $(PROJECT_HDRS) | $(BUILD_DIR) $(CC) $(CFLAGS_PROD) -o $@ \ $(MAIN_SRC) $(PROD_SRCS) \ $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \ diff --git a/src/foundation/constants.h b/src/foundation/constants.h index dd673e15e..373ea1a51 100644 --- a/src/foundation/constants.h +++ b/src/foundation/constants.h @@ -94,4 +94,19 @@ enum { /* Common offset constants (used across many files). */ enum { SKIP_ONE = 1, PAIR_LEN = 2 }; +/* ── Label allowlists for SQL ──────────────────────────────────── + * SQL mirror of cbm_label_is_type_like() (internal/cbm/helpers.c). That + * function is documented as the single source of truth for type-like labels + * "instead of scattering `|| strcmp(label,\"Struct\")==0` across the tree" — + * but a SQL string literal cannot call it, so several queries hardcoded + * ('Function','Method','Class') and silently stopped matching the moment + * Struct/Interface/Enum/Type/Trait began being emitted. + * + * Use these instead of inlining a label list. tests/test_store_nodes.c pins + * them against cbm_label_is_type_like(), so adding a type-like label there + * without updating these fails CI rather than quietly shrinking query results. */ +#define CBM_SQL_TYPE_LIKE_LABELS "'Class','Struct','Interface','Enum','Type','Trait'" +#define CBM_SQL_CALLABLE_LABELS "'Function','Method'" +#define CBM_SQL_CALLABLE_OR_TYPE_LABELS CBM_SQL_CALLABLE_LABELS "," CBM_SQL_TYPE_LIKE_LABELS + #endif /* CBM_CONSTANTS_H */ diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 9cafa0836..640ce161a 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -2863,7 +2863,7 @@ static char *bm25_search(cbm_store_t *store, const char *project, const char *qu " (fts.base_rank " " - CASE WHEN n.label IN ('Function','Method') THEN 10.0 " " WHEN n.label = 'Route' THEN 8.0 " - " WHEN n.label IN ('Class','Interface','Type','Enum') THEN 5.0 " + " WHEN n.label IN (" CBM_SQL_TYPE_LIKE_LABELS ") THEN 5.0 " " ELSE 0.0 END) AS rank " "FROM (" " SELECT rowid, bm25(nodes_fts) AS base_rank" diff --git a/src/store/store.c b/src/store/store.c index 403d3b694..4a032970e 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -5276,7 +5276,7 @@ static int arch_boundaries(cbm_store_t *s, const char *project, const char *path char nsqlbuf[ST_SQL_BUF]; const char *nbase = "SELECT id, qualified_name, file_path FROM nodes WHERE project=?1 AND label IN " - "('Function','Method','Class')"; + "(" CBM_SQL_CALLABLE_OR_TYPE_LABELS ")"; if (scoped) { snprintf(nsqlbuf, sizeof(nsqlbuf), "%s%s ORDER BY id", nbase, arch_path_scope_sql()); } else { @@ -5419,7 +5419,7 @@ static int arch_packages_from_qn(cbm_store_t *s, const char *project, const char bool scoped = arch_path_prepare(path, norm, sizeof(norm), like, sizeof(like)); char qsqlbuf[ST_SQL_BUF]; const char *qbase = "SELECT qualified_name FROM nodes WHERE project=?1 AND label IN " - "('Function','Method','Class')"; + "(" CBM_SQL_CALLABLE_OR_TYPE_LABELS ")"; if (scoped) { snprintf(qsqlbuf, sizeof(qsqlbuf), "%s%s", qbase, arch_path_scope_sql()); } else { @@ -6686,7 +6686,7 @@ static int arch_clusters(cbm_store_t *s, const char *project, const char *path, bool scoped = arch_path_prepare(path, norm, sizeof(norm), like, sizeof(like)); char nsqlbuf[ST_SQL_BUF]; const char *nbase = "SELECT id, name, qualified_name, file_path FROM nodes " - "WHERE project=?1 AND label IN ('Function','Method','Class')"; + "WHERE project=?1 AND label IN (" CBM_SQL_CALLABLE_OR_TYPE_LABELS ")"; if (scoped) { snprintf(nsqlbuf, sizeof(nsqlbuf), "%s%s ORDER BY id LIMIT ?4", nbase, arch_path_scope_sql()); @@ -7760,7 +7760,7 @@ int cbm_store_vector_search(cbm_store_t *s, const char *project, const char **ke " FROM node_vectors v" " INNER JOIN nodes n ON n.id = v.node_id" " WHERE v.project = ?2" - " AND n.label IN ('Function','Method','Class')" + " AND n.label IN (" CBM_SQL_CALLABLE_OR_TYPE_LABELS ")" " ORDER BY score DESC" " LIMIT ?3"; diff --git a/tests/test_store_nodes.c b/tests/test_store_nodes.c index 250e94de1..cd37a47ce 100644 --- a/tests/test_store_nodes.c +++ b/tests/test_store_nodes.c @@ -6,11 +6,52 @@ */ #include "test_framework.h" #include +#include +#include #include #include #include #include +/* ── Label allowlist / SQL drift guard ──────────────────────────── */ + +/* CONTRACT PIN. `cbm_label_is_type_like()` is documented in cbm.h as the single + * source of truth for type-like labels, "instead of scattering + * `|| strcmp(label,\"Struct\")==0` across the tree". A SQL string literal cannot + * call it, so four queries in store.c and the BM25 ranking in mcp.c hardcoded + * their own label lists — and silently stopped matching once Struct (Rust, Go, + * Swift, D) began being emitted. get_architecture and vector search dropped + * every struct in the project; search_code under-ranked them. + * + * This pins the SQL mirrors to the C predicate in BOTH directions, so the next + * type-like label fails here instead of quietly shrinking query results. */ +TEST(sql_label_allowlists_match_cbm_label_is_type_like) { + /* Every label the C predicate accepts must appear in the SQL fragment. */ + static const char *const type_like[] = {"Class", "Struct", "Interface", + "Enum", "Type", "Trait"}; + for (size_t i = 0; i < sizeof(type_like) / sizeof(type_like[0]); i++) { + ASSERT_TRUE(cbm_label_is_type_like(type_like[i])); + char quoted[64]; + snprintf(quoted, sizeof(quoted), "'%s'", type_like[i]); + ASSERT_NOT_NULL(strstr(CBM_SQL_TYPE_LIKE_LABELS, quoted)); + ASSERT_NOT_NULL(strstr(CBM_SQL_CALLABLE_OR_TYPE_LABELS, quoted)); + } + /* And nothing the predicate rejects may be smuggled into the type-like + * fragment — otherwise the SQL would widen past the C contract. */ + static const char *const not_type_like[] = {"Function", "Method", "Module", + "File", "Folder", "Variable"}; + for (size_t i = 0; i < sizeof(not_type_like) / sizeof(not_type_like[0]); i++) { + ASSERT_FALSE(cbm_label_is_type_like(not_type_like[i])); + char quoted[64]; + snprintf(quoted, sizeof(quoted), "'%s'", not_type_like[i]); + ASSERT_NULL(strstr(CBM_SQL_TYPE_LIKE_LABELS, quoted)); + } + /* The callable fragment carries exactly Function and Method on top. */ + ASSERT_NOT_NULL(strstr(CBM_SQL_CALLABLE_OR_TYPE_LABELS, "'Function'")); + ASSERT_NOT_NULL(strstr(CBM_SQL_CALLABLE_OR_TYPE_LABELS, "'Method'")); + PASS(); +} + /* ── Schema / Open / Close ──────────────────────────────────────── */ TEST(store_open_memory) { @@ -1930,6 +1971,7 @@ SUITE(store_nodes) { RUN_TEST(store_coverage_meta_zero_row_truncation_and_delete); RUN_TEST(store_coverage_replace_rejects_invalid_row_arguments); RUN_TEST(store_coverage_replace_rolls_back_when_shadow_rebuild_fails); + RUN_TEST(sql_label_allowlists_match_cbm_label_is_type_like); RUN_TEST(store_open_memory); RUN_TEST(store_close_null); RUN_TEST(store_open_memory_twice);