Skip to content
Merged
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
23 changes: 18 additions & 5 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -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 = \
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) \
Expand Down Expand Up @@ -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) \
Expand Down Expand Up @@ -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) \
Expand Down Expand Up @@ -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) \
Expand Down
15 changes: 15 additions & 0 deletions src/foundation/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
2 changes: 1 addition & 1 deletion src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions src/store/store.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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";

Expand Down
42 changes: 42 additions & 0 deletions tests/test_store_nodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,52 @@
*/
#include "test_framework.h"
#include <store/store.h>
#include <foundation/constants.h>
#include <cbm.h>
#include <sqlite3.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

/* ── 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) {
Expand Down Expand Up @@ -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);
Expand Down
Loading