Files
codebase-memory-mcp/Makefile.cbm
T
Andrew Hundt 32bc95314e feat(release): embed UI/templates and retain dependency indexing
Previous behavior: destination 9953c328 shipped external UI and integration sidecars plus release variants. Upstream d58afe56 recorded that this layout did not reduce the roughly three Microsoft !ml findings across dry runs and broke the unsuffixed archive names consumed by package manifests.

What changed:
- Makefile.cbm:427 and Makefile.cbm:1507-1509 retain depindex and PageRank sources while linking generated embedded_assets.c into the release executable.
- src/main.c:1129 and src/ui/config.c:44 reject non-exact --ui and --port values before daemon admission with actionable errors.
- src/pipeline/pipeline.c:3278-3295 reports main and sidecar rollback failures and returns persistence failure when cancellation rollback cannot restore the prior generation.
- src/store/store.c:1291-2175 keeps upstream Windows file-API normalization on SQLite open, prepare, backup, seal, and dump paths.
- src/cli/client_adapter.c:33-41 retains zero-length and SIZE_MAX growth guards from api-consolidation.
- scripts/test.sh:239 and scripts/test.sh:320 keep vendored integrity and generation-bound embedded UI readiness in the canonical harness.
- release scripts and npm, PyPI, and Go wrappers publish one unsuffixed four-member archive and one cached executable while retaining lease and concurrent-winner handling.

Security rationale: upstream 93696aeb measured that externalization moved rather than removed Microsoft heuristic findings; Makefile.cbm:231 and scripts/ci/check-binary-composition.sh retain separate executable/read-only ELF segments. The VirusTotal gate accepts only one disclosed Microsoft label ending in !ml and rejects every other malicious, suspicious, short, or malformed result.

Verification:
- final staged product build and ad-hoc signature passed
- UI suite: 20 passed
- pipeline rollback: 2 passed; destination-prepare cancellation: 1 passed
- CLI installer-handoff test: 1 passed
- embedded daemon readiness: 3 scenarios passed
- Go: 42 passed; npm: 11 passed; PyPI unittest: 28 passed
- vendored integrity and 8-archive/32-association extraction contracts passed
- invalid --ui=maybe and --port=0 exited 2 with exact errors
- staged diff whitespace and secret-pattern checks passed

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-08-10 01:26:53 -04:00

1716 lines
81 KiB
Makefile

# Makefile.cbm — Build system for pure C rewrite
#
# Usage:
# make -f Makefile.cbm test # Build + run all tests (ASan + UBSan)
# CBM_ONLY_SUITE=pipeline make -f Makefile.cbm test
# # Run one suite after building
# CBM_ONLY_SUITE=pipeline CBM_ONLY_TEST=exact build/c/test-runner
# # Run matching tests inside one suite
# make -f Makefile.cbm test-foundation # Foundation tests only (fast)
# make -f Makefile.cbm test-tsan # Thread sanitizer build
# make -f Makefile.cbm test-syntax TEST_SYNTAX_SRCS="tests/test_mcp.c"
# # syntax-check selected C units with real test flags
# make -f Makefile.cbm cbm # Production binary (auto-signed on macOS)
# make -f Makefile.cbm native-extraction-benchmark
# # O2 production extraction benchmark runner
# make -f Makefile.cbm install # Build + install to INSTALL_DIR (default ~/.local/bin)
# make -f Makefile.cbm clean-c # Remove build artifacts
#
# macOS signing note:
# macOS 25+ enforces ad-hoc code signatures on binaries. Copying a binary
# without re-signing causes immediate SIGKILL at runtime. This Makefile
# runs `codesign --force --sign -` automatically after every build and
# install step on macOS. On Linux and other platforms the step is a no-op.
# Compiler selection — override via: make CC=gcc CXX=g++
# macOS: cc (Apple Clang) — universal binary with ASan support
# Linux: gcc/g++ — system default with full sanitizer support
# CI scripts pass CC/CXX explicitly; don't rely on defaults here
# Target architecture (macOS): build.sh/test.sh export ARCHFLAGS="-arch <arch>"
# (see scripts/env.sh). Fold it into the compiler drivers with `override` so it
# reaches EVERY compile and link recipe — including the vendored objects below
# that use their own *_CFLAGS — and so it survives a command-line `CC=` override.
# ARCHFLAGS is empty on Linux/Windows and for native direct `make` invocations,
# leaving CC/CXX unchanged there.
override CC := $(CC) $(ARCHFLAGS)
override CXX := $(CXX) $(ARCHFLAGS)
# ── Common flags ─────────────────────────────────────────────────
# Include paths for:
# src/ — new foundation headers
# vendored/ — yyjson, xxhash, sqlite3
# internal/cbm — existing extraction headers (cbm.h, helpers.h, etc.)
# internal/cbm/vendored/ts_runtime/include — tree-sitter API
CBM_DIR = internal/cbm
TS_INCLUDE = $(CBM_DIR)/vendored/ts_runtime/include
# Vendored tree-sitter src/ contains unicode/ headers (umachine.h, utf.h, utf8.h).
# This ensures we use our vendored copies instead of requiring system libicu-dev.
TS_SRC = $(CBM_DIR)/vendored/ts_runtime/src
# ── Optional libgit2 (faster git history parsing) ────────────────
# Auto-detected via pkg-config. Falls back to popen("git log ...") if absent.
LIBGIT2_CFLAGS := $(shell pkg-config --cflags libgit2 2>/dev/null)
LIBGIT2_LIBS := $(shell pkg-config --libs libgit2 2>/dev/null)
ifneq ($(LIBGIT2_LIBS),)
LIBGIT2_FLAGS = -DHAVE_LIBGIT2 $(LIBGIT2_CFLAGS)
else
LIBGIT2_FLAGS =
LIBGIT2_LIBS =
endif
# ── Platform detection & code signing ───────────────────────────
# macOS 25+ kills unsigned or invalidly-signed binaries with SIGKILL.
# codesign --force --sign - applies an ad-hoc signature (no Apple Developer
# account required). On Linux/other platforms this entire block is a no-op.
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
CODESIGN_BIN := $(shell command -v codesign 2>/dev/null)
ifneq ($(CODESIGN_BIN),)
# codesign is available — sign and report
define codesign_binary
@$(CODESIGN_BIN) --force --sign - $(1) 2>&1 && \
echo " ✓ signed $(1) (ad-hoc, macOS 25+ compatible)" || \
{ echo " ✗ WARNING: codesign failed for $(1) — binary may crash on macOS 25+"; true; }
endef
else
# codesign not found — warn but don't fail the build
define codesign_binary
@echo " ✗ WARNING: codesign not found — $(1) may crash on macOS 25+ (install Xcode CLT)"
endef
endif
else
# Non-macOS: signing is a documented no-op
define codesign_binary
@echo " (signing skipped — not macOS)"
endef
endif
# GCC-only warning suppressions (Clang rejects unknown -Wno-* with -Werror).
# Detect GCC by checking for __GNUC__ without __clang__ — handles all versions.
IS_GCC := $(shell echo | $(CC) -dM -E - 2>/dev/null | grep -q '__GNUC__' && ! echo | $(CC) -dM -E - 2>/dev/null | grep -q '__clang__' && echo yes || echo no)
GCC_ONLY_FLAGS :=
ifeq ($(IS_GCC),yes)
GCC_ONLY_FLAGS := -Wno-format-truncation -Wno-unused-result \
-Wno-stringop-truncation -Wno-alloc-size-larger-than
endif
# -Wdate-time: with -Werror, any use of __DATE__/__TIME__/__TIMESTAMP__ fails the
# build. Build-time entropy is how one identical source tree produced a new hash
# on every compile (see the local patch in vendored/mimalloc/src/options.c) —
# once removed, it must not creep back through a new call site.
CFLAGS_COMMON = -std=c11 -D_DEFAULT_SOURCE -D_GNU_SOURCE -Wall -Wextra -Werror \
-Wno-unused-parameter -Wno-sign-compare -Wdate-time \
$(GCC_ONLY_FLAGS) \
$(LIBGIT2_FLAGS) \
-Isrc -Ivendored -Ivendored/sqlite3 \
-Ivendored/mimalloc/include \
-I$(CBM_DIR) -I$(TS_INCLUDE) -I$(BUILD_DIR)/generated
CXXFLAGS_COMMON = -std=c++14 -Wall -Wextra -Werror \
-Wno-unused-parameter \
-I$(CBM_DIR) -I$(TS_INCLUDE)
# Test seams are opt-in for production binaries. The test runner always enables
# them through EDITOR_TEST_DEFINES, while selected production watchdog tests set
# TEST_SEAMS=1 explicitly. A forgotten flag therefore produces a clean release
# binary instead of silently shipping test-only process and lease controls.
TEST_SEAM_DEFINE :=
ifeq ($(TEST_SEAMS),1)
TEST_SEAM_DEFINE := -DCBM_ENABLE_TEST_SEAMS=1
endif
# Production flags. CFLAGS_EXTRA carries the generated version define;
# EXTRA_{C,CXX,LD}FLAGS are explicit developer/CI overrides appended last.
# CBM_BIND_TS_ALLOCATOR=1: bind the tree-sitter runtime to mimalloc (#424). Only
# the prod build uses mimalloc (MI_OVERRIDE=1); the test build is CRT+ASan, where
# binding would create an alloc/free mismatch, so the guard is prod-only.
CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(TEST_SEAM_DEFINE) \
$(GLOBAL_OVERRIDE_DEFINE) $(CFLAGS_EXTRA) $(EXTRA_CFLAGS)
CXXFLAGS_PROD = $(CXXFLAGS_COMMON) -O2 $(EXTRA_CXXFLAGS)
# Test flags: debug + sanitizers (override SANITIZE= to disable on Windows)
SANITIZE = -fsanitize=address,undefined -fno-omit-frame-pointer
EDITOR_TEST_DEFINES = -DCBM_JSON_LIKE_ENABLE_TEST_API=1 \
-DCBM_TOML_EDIT_ENABLE_TEST_API=1 -DCBM_YAML_ENABLE_TEST_API=1 \
-DCBM_TEXT_EDIT_ENABLE_TEST_API=1 -DCBM_CLI_ENABLE_TEST_API=1 \
-DCBM_DIAGNOSTICS_ENABLE_TEST_API=1 -DCBM_PIPELINE_ENABLE_TEST_API=1 \
-DCBM_WATCHER_ENABLE_TEST_API=1 -DCBM_ENABLE_TEST_SEAMS=1
TEST_INCLUDE_FLAGS = -Itests -Itests/repro
# The build system is the single source of truth for "is this binary
# instrumented": compiler-specific probes (__SANITIZE_ADDRESS__) miss
# clang's feature-check spelling and every non-ASan sanitizer, so the
# trap-UBSan leg once ran NATIVE timing budgets on an instrumented
# binary. Any non-empty SANITIZE ⇒ sanitized budgets everywhere.
ifneq ($(strip $(SANITIZE)),)
SANITIZED_DEFINE = -DCBM_SANITIZED_BUILD=1
else
SANITIZED_DEFINE =
endif
KOTLIN_DEDUP_TEST_DEFINE = -DCBM_KOTLIN_DEDUP_TEST_API=1
CALL_REFERENCE_LOOKUP_TEST_DEFINE = -DCBM_CALL_REFERENCE_LOOKUP_TEST_API=1
INCREMENTAL_TEST_DEFINE = -DCBM_INCREMENTAL_TEST_API=1
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(SANITIZED_DEFINE) \
$(KOTLIN_DEDUP_TEST_DEFINE) $(CALL_REFERENCE_LOOKUP_TEST_DEFINE) \
$(INCREMENTAL_TEST_DEFINE) -g -O1 $(SANITIZE)
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE) $(CXX_STDLIB_FLAGS)
# TSan (can't combine with ASan)
TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
-g -O1 $(TSAN_SANITIZE)
CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -g -O1 \
$(TSAN_SANITIZE)
# Windows needs ws2_32 (Winsock), psapi (GetProcessMemoryInfo), shell32
# (CommandLineToArgvW — the UTF-8 argv path in main.c, #423/#20), and
# advapi32 (owner-only activation-candidate ACLs), plus
# --allow-multiple-definition (MinGW CRT symbol clashes).
# Auto-detected via compiler; no manual override needed.
IS_MINGW := $(shell echo | $(CC) -dM -E - 2>/dev/null | grep -q 'define _WIN32 ' && echo yes || echo no)
WIN32_LIBS :=
# Route Windows allocations through mimalloc (#581). mimalloc's own static
# override is gated on _MSC_VER, which clang/MinGW never defines, so it
# compiled out and the CRT won the link: ordinary malloc went to the CRT heap,
# which keeps freed pages committed, leaving every allocator tuning in
# cbm_mem_init POSIX-only. --wrap redirects the same population of allocations
# that link-order override captures on POSIX, so the purge configuration
# finally applies here too. src/foundation/mem_override_win.c supplies the
# wrappers; its deallocating paths check mi_is_in_heap_region so a CRT-owned
# pointer can never reach mi_free (#424).
MIMALLOC_WRAP_SYMS := malloc calloc realloc free strdup strndup _msize \
_aligned_malloc _aligned_free
MIMALLOC_WRAP_FLAGS :=
ifeq ($(IS_MINGW),yes)
MIMALLOC_WRAP_FLAGS := $(foreach sym,$(MIMALLOC_WRAP_SYMS),-Wl,--wrap=$(sym))
endif
# Linux wraps a smaller set for MEASUREMENT only (see mem_override_posix.c):
# allocations already reach mimalloc here, but the profiler needs the same
# observation point as Windows or the platform comparison is not like-for-like.
# macOS ld has no --wrap, so it stays census-only.
IS_LINUX := $(shell uname -s 2>/dev/null | grep -q Linux && echo yes || echo no)
MIMALLOC_WRAP_SYMS_POSIX := malloc calloc realloc free strdup
MIMALLOC_WRAP_FLAGS_POSIX :=
ifeq ($(IS_LINUX),yes)
MIMALLOC_WRAP_FLAGS_POSIX := $(foreach sym,$(MIMALLOC_WRAP_SYMS_POSIX),-Wl,--wrap=$(sym))
endif
ifeq ($(IS_MINGW),yes)
# --no-insert-timestamp: the PE header otherwise carries the link wall clock, so
# two Windows builds of identical source are never byte-identical and every
# release is a brand-new file to a reputation system. Same motivation as the
# mimalloc __DATE__ patch; ELF and Mach-O have no equivalent field to clear.
WIN32_LIBS := -lws2_32 -lpsapi -lshell32 -ladvapi32 -lbcrypt -Wl,--allow-multiple-definition -Wl,--stack,8388608 -Wl,--no-insert-timestamp -static
endif
# STATIC=1 produces a fully static binary (for Alpine/musl portable builds)
ifeq ($(STATIC),1)
STATIC_FLAGS := -static
endif
# W^X: demand a non-executable stack on ELF. This is belt to the braces of the
# .note.GNU-stack annotation in vendored/nomic/code_vectors_blob.S — that note
# fixes the CAUSE (an unannotated object makes ld assume the worst for the whole
# link), this flag fixes the OUTCOME, and the composition gate proves it on the
# shipped artifact. ELF-only: Apple's ld rejects -z noexecstack outright and it
# is meaningless for PE, so it is gated rather than made "common".
#
# -z separate-code additionally keeps read-only DATA out of the executable
# segment. GNU ld enables it by default on x86-64 but NOT on aarch64, so the
# arm64 binaries emitted ONE R E PT_LOAD spanning the whole image: .rodata is
# correctly marked A (not AX), but the kernel applies SEGMENT permissions, so
# ~259 MB of tree-sitter parse tables was mapped executable at runtime while the
# amd64 build of the same source mapped them R only. That is a large ROP gadget
# surface for data that is never executed, and it is invisible to section flags.
ELF_HARDENING_FLAGS :=
ifeq ($(IS_LINUX),yes)
# A Linux container may cross-compile a PE binary. IS_LINUX identifies the
# build host, so the compiler-derived target guard is also required.
ifneq ($(IS_MINGW),yes)
ELF_HARDENING_FLAGS := -Wl,-z,noexecstack -Wl,-z,separate-code
endif
endif
# The POSIX wrap shim exists only so the profiler can observe allocations. It
# must never reach a sanitized build: the Linux/macOS test builds are CRT+ASan,
# and redirecting malloc into mimalloc underneath ASan's own interception mixes
# two allocators on the same pointers. Windows keeps its wrap flags everywhere,
# because there the shim is what makes mimalloc own the allocations at all.
# C++ stdlib selection: MSan builds must link an MSan-instrumented libc++
# instead of the system libstdc++ (scripts/msan.sh overrides both vars).
CXX_STDLIB ?= -lstdc++
CXX_STDLIB_FLAGS ?=
LDFLAGS = -lm $(CXX_STDLIB) -lpthread -lz $(LIBGIT2_LIBS) $(WIN32_LIBS) \
$(STATIC_FLAGS) $(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS) $(EXTRA_LDFLAGS)
LDFLAGS_TEST = -lm $(CXX_STDLIB) -lpthread -lz $(SANITIZE) $(LIBGIT2_LIBS) \
$(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS)
LDFLAGS_TSAN = -lm $(CXX_STDLIB) -lpthread -lz $(TSAN_SANITIZE) $(LIBGIT2_LIBS) \
$(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS)
# nosan: no ASan/UBSan — required for macOS 'leaks' tool (incompatible with ASan malloc replacement).
# Keep every test API identical to the sanitizer/TSan runners; changing the
# allocator instrumentation must not silently compile a different test suite.
CFLAGS_NOSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) \
$(KOTLIN_DEDUP_TEST_DEFINE) $(CALL_REFERENCE_LOOKUP_TEST_DEFINE) \
$(INCREMENTAL_TEST_DEFINE) -g -O1
LDFLAGS_NOSAN = -lm $(CXX_STDLIB) -lpthread -lz $(LIBGIT2_LIBS) $(WIN32_LIBS) \
$(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS)
# ── Source files ─────────────────────────────────────────────────
FOUNDATION_SRCS = \
src/foundation/mem_override_win.c \
src/foundation/arena.c \
src/foundation/hash_table.c \
src/foundation/str_intern.c \
src/foundation/log.c \
src/foundation/str_util.c \
src/foundation/workspace.c \
src/foundation/platform.c \
src/foundation/system_info.c \
src/foundation/slab_alloc.c \
src/foundation/yaml.c \
src/foundation/compat.c \
src/foundation/compat_thread.c \
src/foundation/compat_fs.c \
src/foundation/compat_regex.c \
src/foundation/mem.c \
src/foundation/diagnostics.c \
src/foundation/profile.c \
src/foundation/dump_verify.c \
src/foundation/limits.c \
src/foundation/subprocess.c \
src/foundation/sha256.c \
src/foundation/secure_random.c \
src/foundation/macos_acl.c \
src/foundation/private_file_lock.c \
src/foundation/lock_registry.c
# Existing extraction C code (compiled from current location)
EXTRACTION_SRCS = \
$(CBM_DIR)/cbm.c \
$(CBM_DIR)/extract_defs.c \
$(CBM_DIR)/extract_calls.c \
$(CBM_DIR)/extract_imports.c \
$(CBM_DIR)/extract_usages.c \
$(CBM_DIR)/extract_unified.c \
$(CBM_DIR)/extract_semantic.c \
$(CBM_DIR)/extract_type_refs.c \
$(CBM_DIR)/extract_type_assigns.c \
$(CBM_DIR)/extract_env_accesses.c \
$(CBM_DIR)/extract_channels.c \
$(CBM_DIR)/extract_k8s.c \
$(CBM_DIR)/helpers.c \
$(CBM_DIR)/lang_specs.c \
$(CBM_DIR)/macro_table.c \
$(CBM_DIR)/iris_export_xml.c \
$(CBM_DIR)/service_patterns.c
# LSP resolvers (compiled as one unit via lsp_all.c)
LSP_SRCS = $(CBM_DIR)/lsp_all.c
# Header/source dependencies of the lsp_all unity object. lsp_all.c #includes
# every lsp/*.c resolver, which in turn include cbm.h — and CBMCall is copied
# BY VALUE across the lsp -> pipeline boundary (cbm_calls_push). This object is
# compiled standalone and linked in, NOT recompiled with the rest on every link,
# so it must list the headers/sources it pulls in. Without this, a changed struct
# layout (e.g. a new CBMCall field) leaves a stale lsp_all.o with the old layout —
# an ODR mismatch that under-reads the by-value struct copy. Explicit because this
# Makefile does not use compiler auto-dependency (-MMD) generation.
LSP_UNITY_DEPS = $(CBM_DIR)/lsp_all.c \
$(wildcard $(CBM_DIR)/lsp/*.c $(CBM_DIR)/lsp/*.h \
$(CBM_DIR)/lsp/generated/*.c $(CBM_DIR)/*.h)
# Tree-sitter runtime
TS_RUNTIME_SRC = $(CBM_DIR)/ts_runtime.c
TS_RUNTIME_DEPS = \
$(TS_RUNTIME_SRC) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/include/tree_sitter/*.h) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/src/*.c) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/src/*.h) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/src/portable/*.h) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/src/unicode/*.h)
# All 159 grammar shim files
GRAMMAR_SRCS = $(wildcard $(CBM_DIR)/grammar_*.c)
# LZ4 + Aho-Corasick
AC_LZ4_SRCS = $(CBM_DIR)/ac.c $(CBM_DIR)/lz4_store.c
# Zstd compression (for persistent artifacts)
ZSTD_SRCS = $(CBM_DIR)/zstd_store.c
# Preprocessor (C++)
PREPROCESSOR_SRC = $(CBM_DIR)/preprocessor.cpp
# SQLite writer
SQLITE_WRITER_SRC = $(CBM_DIR)/sqlite_writer.c
# Store module (new)
STORE_SRCS = src/store/store.c
# Cypher module (new)
CYPHER_SRCS = src/cypher/cypher.c
# MCP server module (new)
MCP_SRCS = src/mcp/mcp.c src/mcp/index_supervisor.c src/mcp/compact_out.c
# Shared MCP daemon coordination and local transport
DAEMON_SRCS = \
src/daemon/daemon.c \
src/daemon/project_lock.c \
src/daemon/version_cohort.c \
src/daemon/service.c \
src/daemon/runtime.c \
src/daemon/application.c \
src/daemon/frontend.c \
src/daemon/host.c \
src/daemon/bootstrap.c \
src/daemon/ipc.c
# Discover module (new)
DISCOVER_SRCS = \
src/discover/language.c \
src/discover/userconfig.c \
src/discover/gitignore.c \
src/discover/discover.c
# Graph buffer module (new)
GRAPH_BUFFER_SRCS = src/graph_buffer/graph_buffer.c
# Pipeline module (new)
PIPELINE_SRCS = \
src/pipeline/fqn.c \
src/pipeline/lsp_surface.c \
src/pipeline/pipeline_delta.c \
src/pipeline/path_alias.c \
src/pipeline/registry.c \
src/pipeline/pipeline.c \
src/pipeline/pipeline_incremental.c \
src/pipeline/worker_pool.c \
src/pipeline/pass_parallel.c \
src/pipeline/pass_definitions.c \
src/pipeline/pass_calls.c \
src/pipeline/pass_lsp_cross.c \
src/pipeline/pass_usages.c \
src/pipeline/pass_semantic.c \
src/pipeline/pass_tests.c \
src/pipeline/pass_githistory.c \
src/pipeline/pass_gitdiff.c \
src/pipeline/pass_configures.c \
src/pipeline/pass_configlink.c \
src/pipeline/pass_normalize.c \
src/pipeline/httplink.c \
src/pipeline/pass_httplinks.c \
src/pipeline/pass_route_nodes.c \
src/pipeline/pass_enrichment.c \
src/pipeline/pass_envscan.c \
src/pipeline/pass_compile_commands.c \
src/pipeline/pass_infrascan.c \
src/pipeline/pass_k8s.c \
src/pipeline/pass_similarity.c \
src/pipeline/pass_semantic_edges.c \
src/pipeline/pass_complexity.c \
src/pipeline/pass_cross_repo.c \
src/pipeline/artifact.c \
src/pipeline/pass_pkgmap.c
# SimHash / MinHash module
SIMHASH_SRCS = src/simhash/minhash.c
# Semantic embedding module
SEMANTIC_SRCS = src/semantic/semantic.c src/semantic/ast_profile.c src/semantic/rotsq.c
# nomic-embed-code pretrained vectors (assembler blob)
UNIXCODER_BLOB_SRC = vendored/nomic/code_vectors_blob.S
# Depindex module (dependency/reference API indexing)
DEPINDEX_SRCS = src/depindex/depindex.c
# PageRank module (node + edge ranking)
PAGERANK_SRCS = src/pagerank/pagerank.c
# Traces module (new)
TRACES_SRCS = src/traces/traces.c
# Watcher module (new)
WATCHER_SRCS = src/watcher/watcher.c
# Git context module (new)
GIT_SRCS = src/git/git_command.c \
src/git/git_context.c \
src/git/git_snapshot.c
# CLI module (new)
CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/client_adapter.c \
src/cli/agent_clients.c src/cli/agent_profiles.c \
src/cli/config_json_like.c src/cli/config_toml_edit.c src/cli/config_yaml_edit.c \
src/cli/config_text_edit.c src/cli/activation_transaction.c
# UI module (graph visualization)
UI_SRCS = \
src/ui/config.c \
src/ui/http_server.c \
src/ui/layout3d.c \
src/ui/httpd.c \
src/ui/embedded_stub.c
# mimalloc (vendored, global allocator override)
#
# Override strategy is platform-specific:
# * Unix (macOS/Linux): rely on static-link-order override — the prod mimalloc
# object is linked first so its strong malloc/free symbols win. We do NOT
# define MI_MALLOC_OVERRIDE here: that would compile alloc-override.c's
# forwarding definitions (malloc/free/posix_memalign/...) which, on macOS's
# two-level namespace, makes the binary's free == mi_free while system
# libraries keep allocating via the system allocator — pointers crossing that
# boundary hit "mimalloc: error: mi_free: invalid pointer". (Repro:
# `codebase-memory-mcp index <dir>` aborted on every run.)
# * Windows (MinGW, static CRT): the link-order trick fails (#424's allocator
# mismatch), so we define MI_MALLOC_OVERRIDE=1 to compile the static-CRT
# override added in mimalloc 3.3.0 (the _MSC_VER / _ACRTIMP /
# _CRT_HYBRIDPATCHABLE entry points) which take precedence over the static
# CRT's malloc/free.
# Note: mimalloc's source gates the override body on MI_MALLOC_OVERRIDE (CMake
# derives it from the MI_OVERRIDE option); MI_OVERRIDE alone is not read by the
# source — it is kept here only as this project's prod/test marker.
MIMALLOC_SRC = vendored/mimalloc/src/static.c
# Linux belongs on this list too (#1360). The "Unix relies on static-link-order
# override" strategy described above never actually worked: mimalloc defines
# malloc/free ONLY when MI_MALLOC_OVERRIDE is set (it gates alloc-override.c),
# so with the define off there were no strong symbols for the link order to
# prefer. Measured on the shipped v0.9.1-rc.1 artifacts — linux-arm64 glibc AND
# musl-static both report 0/6 allocator-owned size classes, i.e. ordinary malloc
# was going to libc and every purge/reclaim option set in cbm_mem_init applied
# only to the bound sqlite/tree-sitter populations.
#
# The macOS objection does not transfer. That abort is specific to the two-level
# namespace, where this binary's free becomes mi_free while system libraries keep
# allocating from the system allocator, so a pointer crossing the boundary hits
# "mi_free: invalid pointer". ELF is a flat namespace: interposition is
# process-wide and all-or-nothing, which is the configuration mimalloc is
# normally deployed in. macOS therefore stays off, deliberately and permanently.
#
# TWO defines, switched together in ONE place so they cannot drift:
# MI_MALLOC_OVERRIDE reaches the mimalloc translation unit and turns the
# override on; CBM_MEM_GLOBAL_OVERRIDE reaches OUR sources and tells the startup
# audit what to expect. CBM_MEM_GLOBAL_OVERRIDE goes only into CFLAGS_PROD,
# because MIMALLOC_CFLAGS_TEST compiles mimalloc with -DMI_OVERRIDE=0 and no
# override define at all — a test binary has no global override BY CONSTRUCTION
# and must never be told to expect one, or it warns on every run.
MIMALLOC_OVERRIDE_DEFINE :=
GLOBAL_OVERRIDE_DEFINE :=
ifneq ($(filter yes,$(IS_MINGW) $(IS_LINUX)),)
MIMALLOC_OVERRIDE_DEFINE := -DMI_MALLOC_OVERRIDE=1
GLOBAL_OVERRIDE_DEFINE := -DCBM_MEM_GLOBAL_OVERRIDE=1
endif
MIMALLOC_CFLAGS = -std=c11 -O2 -w \
-Ivendored/mimalloc/include \
-Ivendored/mimalloc/src \
-DMI_OVERRIDE=1 \
$(MIMALLOC_OVERRIDE_DEFINE)
MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \
-Ivendored/mimalloc/include \
-Ivendored/mimalloc/src \
-DMI_OVERRIDE=0
# sqlite3 (vendored amalgamation — compiled ourselves for ASan instrumentation)
# SQLITE_ENABLE_FTS5: enables the FTS5 full-text search extension used by the
# BM25 search path in search_graph (see nodes_fts virtual table in store.c).
#
# SQLITE_OMIT_LOAD_EXTENSION: no cbm code calls sqlite3_load_extension or
# sqlite3_enable_load_extension, and we never want a graph DB to be able to pull
# a shared library into the process. Loading is off by default at runtime, but
# "disabled by default" is a setting while "not compiled in" is a property —
# this compiles the machinery out, removing the API surface and part of the
# dlopen/dlsym import surface with it. The composition gate asserts the symbols
# are absent from every release artifact.
SQLITE3_OMIT_FLAGS = -DSQLITE_OMIT_LOAD_EXTENSION
SQLITE3_SRC = vendored/sqlite3/sqlite3.c
SQLITE3_CFLAGS = -std=c11 -O2 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5 \
$(SQLITE3_OMIT_FLAGS)
SQLITE3_CFLAGS_TEST = -std=c11 -g -O1 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5 \
$(SQLITE3_OMIT_FLAGS)
# TRE regex (vendored, Windows only — POSIX uses system <regex.h>)
TRE_SRC = vendored/tre/tre_all.c
TRE_CFLAGS = -std=c11 -g -O1 -w -Ivendored/tre
# yyjson (vendored)
YYJSON_SRC = vendored/yyjson/yyjson.c
# All production sources
PROD_SRCS = $(FOUNDATION_SRCS) $(STORE_SRCS) $(CYPHER_SRCS) $(MCP_SRCS) $(DAEMON_SRCS) $(DISCOVER_SRCS) $(GRAPH_BUFFER_SRCS) $(PIPELINE_SRCS) $(DEPINDEX_SRCS) $(PAGERANK_SRCS) $(SIMHASH_SRCS) $(SEMANTIC_SRCS) $(TRACES_SRCS) $(WATCHER_SRCS) $(GIT_SRCS) $(CLI_SRCS) $(UI_SRCS) $(YYJSON_SRC)
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 $(CBM_DIR)/lsp/*.h tests/*.h \
tests/repro/*.h)
# ── Test sources ─────────────────────────────────────────────────
TEST_FOUNDATION_SRCS = \
tests/test_foundation_main.c \
$(TEST_FOUNDATION_CASE_SRCS)
TEST_FOUNDATION_CASE_SRCS = \
tests/test_arena.c \
tests/test_hash_table.c \
tests/test_dyn_array.c \
tests/test_str_intern.c \
tests/test_log.c \
tests/test_str_util.c \
tests/test_workspace.c \
tests/test_platform.c \
tests/test_diagnostics.c \
tests/test_dump_verify.c \
tests/test_subprocess.c \
tests/test_private_file_lock.c \
tests/test_lock_registry.c
TEST_MAIN_SRC = tests/test_main.c
TEST_EXTRACTION_SRCS = \
tests/test_extraction.c \
tests/test_extraction_inheritance.c \
tests/test_extraction_imports.c \
tests/test_parse_coverage.c \
tests/test_grammar_regression.c \
tests/test_grammar_labels.c \
tests/test_grammar_imports.c \
tests/test_ac.c
TEST_STORE_SRCS = \
tests/test_store_nodes.c \
tests/test_store_edges.c \
tests/test_store_search.c \
tests/test_store_arch.c \
tests/test_store_bulk.c \
tests/test_store_pragmas.c \
tests/test_store_checkpoint.c \
tests/test_dump_verify_io.c \
tests/test_schema_declared_property_keys.c
TEST_CYPHER_SRCS = \
tests/test_cypher.c
TEST_MCP_SRCS = \
tests/test_mcp.c \
tests/test_index_supervisor.c
TEST_DAEMON_SRCS = \
tests/test_daemon.c \
tests/test_project_lock.c \
tests/test_version_cohort.c \
tests/test_daemon_version.c \
tests/test_daemon_runtime.c \
tests/test_daemon_application.c \
tests/test_daemon_frontend.c \
tests/test_daemon_bootstrap.c \
tests/test_daemon_ipc.c
TEST_DISCOVER_SRCS = \
tests/test_language.c \
tests/test_userconfig.c \
tests/test_gitignore.c \
tests/test_git_context.c \
tests/test_discover.c
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
TEST_PIPELINE_SRCS = \
tests/test_registry.c \
tests/test_pipeline.c \
tests/test_cross_repo.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_call_reference_contract.c \
tests/repro/repro_call_scope_usages.c \
tests/repro/repro_call_argument_usages.c \
tests/repro/repro_reference_precision.c \
tests/repro/repro_lexical_binding_precision.c \
tests/repro/repro_call_argument_matrix_a.c \
tests/repro/repro_call_argument_matrix_b.c \
tests/repro/repro_call_node_behaviors.c \
tests/repro/repro_language_registry.c \
tests/repro/repro_call_node_manifest.c \
tests/repro/repro_lsp_ordered_signatures.c \
tests/repro/repro_lsp_ordered_local.c \
tests/repro/repro_ts_overload_return_chains.c \
tests/repro/repro_harness_cleanup.c \
tests/repro/repro_runner_filter.c
TEST_WATCHER_SRCS = tests/test_watcher.c
TEST_LZ4_SRCS = tests/test_lz4.c
TEST_ZSTD_SRCS = tests/test_zstd.c
TEST_ARTIFACT_SRCS = tests/test_artifact.c
TEST_SQLITE_WRITER_SRCS = tests/test_sqlite_writer.c
TEST_GO_LSP_SRCS = tests/test_go_lsp.c
TEST_C_LSP_SRCS = tests/test_c_lsp.c
TEST_PHP_LSP_SRCS = tests/test_php_lsp.c
TEST_CS_LSP_SRCS = tests/test_cs_lsp.c
TEST_CS_LSP_BENCH_SRCS = tests/test_cs_lsp_bench.c
TEST_PERL_LSP_SRCS = tests/test_perl_lsp.c
TEST_SCOPE_SRCS = tests/test_scope.c
TEST_TYPE_REP_SRCS = tests/test_type_rep.c
TEST_PY_LSP_SRCS = tests/test_py_lsp.c
TEST_PY_LSP_BENCH_SRCS = tests/test_py_lsp_bench.c
TEST_PY_LSP_STRESS_SRCS = tests/test_py_lsp_stress.c
TEST_PY_LSP_SCALE_SRCS = tests/test_py_lsp_scale.c
TEST_TS_LSP_SRCS = tests/test_ts_lsp.c
TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c
TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c tests/test_grammar_probe_d.c tests/test_grammar_probe_e.c tests/test_grammar_probe_f.c tests/test_grammar_probe_g.c
TEST_TRACES_SRCS = tests/test_traces.c
TEST_CLI_SRCS = tests/test_cli.c tests/test_agent_clients.c tests/test_agent_profiles.c \
tests/test_config_json_like.c \
tests/test_config_toml_edit.c tests/test_config_yaml_edit.c tests/test_config_text_edit.c \
tests/test_activation_transaction.c
TEST_MEM_SRCS = tests/test_mem.c
TEST_UI_SRCS = tests/test_ui.c
TEST_HTTPD_SRCS = tests/test_httpd.c
TEST_DEPINDEX_SRCS = tests/test_depindex.c
TEST_PAGERANK_SRCS = tests/test_pagerank.c
TEST_TOKEN_REDUCTION_SRCS = tests/test_token_reduction.c
TEST_TOOL_CONSOLIDATION_SRCS = tests/test_tool_consolidation.c
TEST_INPUT_VALIDATION_SRCS = tests/test_input_validation.c
TEST_SECURITY_SRCS = tests/test_security.c
TEST_YAML_SRCS = tests/test_yaml.c
TEST_HTTPLINK_SRCS = tests/test_httplink.c
TEST_SEMANTIC_SRCS = tests/test_semantic.c
TEST_AST_PROFILE_SRCS = tests/test_ast_profile.c
TEST_SLAB_ALLOC_SRCS = tests/test_slab_alloc.c
TEST_SIMHASH_SRCS = tests/test_simhash.c
TEST_STACK_OVERFLOW_SRCS = tests/test_stack_overflow.c
# Cumulative BUG-REPRODUCTION suite (separate runner, NOT in ALL_TEST_SRCS).
# Contains RED reproductions plus GREEN controls — see tests/repro/repro_main.c.
# Kept out of the gating `make test` so `ci-ok` stays green; run via `make test-repro`.
TEST_REPRO_SRCS = \
tests/repro/repro_main.c \
tests/repro/repro_parallel_determinism.c \
tests/repro/repro_extraction.c \
tests/repro/repro_runner_filter.c \
tests/repro/repro_harness_cleanup.c \
tests/repro/repro_language_registry.c \
tests/repro/repro_call_node_manifest.c \
tests/repro/repro_call_scope_usages.c \
tests/repro/repro_call_argument_usages.c \
tests/repro/repro_lsp_ordered_signatures.c \
tests/repro/repro_lsp_ordered_local.c \
tests/repro/repro_ts_overload_return_chains.c \
tests/repro/repro_reference_precision.c \
tests/repro/repro_lexical_binding_precision.c \
tests/repro/repro_call_argument_matrix_a.c \
tests/repro/repro_call_argument_matrix_b.c \
tests/repro/repro_call_node_behaviors.c \
tests/repro/repro_issue495.c \
tests/repro/repro_issue521.c \
tests/repro/repro_issue382.c \
tests/repro/repro_issue408.c \
tests/repro/repro_issue56.c \
tests/repro/repro_issue480.c \
tests/repro/repro_issue571.c \
tests/repro/repro_issue523.c \
tests/repro/repro_issue546.c \
tests/repro/repro_issue627.c \
tests/repro/repro_issue514.c \
tests/repro/repro_issue510.c \
tests/repro/repro_issue557.c \
tests/repro/repro_issue520.c \
tests/repro/repro_issue333.c \
tests/repro/repro_issue570.c \
tests/repro/repro_issue409.c \
tests/repro/repro_issue431.c \
tests/repro/repro_issue607.c \
tests/repro/repro_issue403.c \
tests/repro/repro_issue434.c \
tests/repro/repro_issue471.c \
tests/repro/repro_issue221.c \
tests/repro/repro_issue548.c \
tests/repro/repro_new_ts_class_field_arrow.c \
tests/repro/repro_new_py_tuple_unpack.c \
tests/repro/repro_new_cypher_limit_zero.c \
tests/repro/repro_issue363.c \
tests/repro/repro_issue581.c \
tests/repro/repro_issue787.c \
tests/repro/repro_issue842.c \
tests/repro/repro_issue964.c \
tests/repro/repro_invariant_calls.c \
tests/repro/repro_invariant_graph.c \
tests/repro/repro_invariant_breadth.c \
tests/repro/repro_invariant_enclosing_parity.c \
tests/repro/repro_invariant_lsp_rescue.c \
tests/repro/repro_invariant_discovery_fqn.c \
tests/repro/repro_grammar_core.c \
tests/repro/repro_grammar_scripting.c \
tests/repro/repro_grammar_functional.c \
tests/repro/repro_grammar_systems.c \
tests/repro/repro_grammar_web.c \
tests/repro/repro_grammar_config.c \
tests/repro/repro_grammar_build.c \
tests/repro/repro_grammar_shells.c \
tests/repro/repro_grammar_scientific.c \
tests/repro/repro_grammar_markup.c \
tests/repro/repro_grammar_misc.c \
tests/repro/repro_lsp_c_cpp.c \
tests/repro/repro_lsp_go_py.c \
tests/repro/repro_lsp_ts.c \
tests/repro/repro_ts_inherited_method.c \
tests/repro/repro_lsp_java_cs.c \
tests/repro/repro_lsp_kt_php_rust.c
ALL_TEST_SRCS = \
$(TEST_MAIN_SRC) \
$(TEST_FOUNDATION_CASE_SRCS) \
$(TEST_EXTRACTION_SRCS) \
$(TEST_STORE_SRCS) \
$(TEST_CYPHER_SRCS) \
$(TEST_MCP_SRCS) \
$(TEST_DAEMON_SRCS) \
$(TEST_DISCOVER_SRCS) \
$(TEST_GRAPH_BUFFER_SRCS) \
$(TEST_PIPELINE_SRCS) \
$(TEST_WATCHER_SRCS) \
$(TEST_LZ4_SRCS) \
$(TEST_ZSTD_SRCS) \
$(TEST_ARTIFACT_SRCS) \
$(TEST_SQLITE_WRITER_SRCS) \
$(TEST_GO_LSP_SRCS) \
$(TEST_C_LSP_SRCS) \
$(TEST_PHP_LSP_SRCS) \
$(TEST_CS_LSP_SRCS) \
$(TEST_CS_LSP_BENCH_SRCS) \
$(TEST_PERL_LSP_SRCS) \
$(TEST_SCOPE_SRCS) \
$(TEST_TYPE_REP_SRCS) \
$(TEST_PY_LSP_SRCS) \
$(TEST_PY_LSP_BENCH_SRCS) \
$(TEST_PY_LSP_STRESS_SRCS) \
$(TEST_PY_LSP_SCALE_SRCS) \
$(TEST_TS_LSP_SRCS) \
$(TEST_JAVA_LSP_SRCS) \
$(TEST_KOTLIN_LSP_SRCS) \
$(TEST_RUST_LSP_SRCS) \
$(TEST_TRACES_SRCS) \
$(TEST_HTTPLINK_SRCS) \
$(TEST_CLI_SRCS) \
$(TEST_MEM_SRCS) \
$(TEST_UI_SRCS) \
$(TEST_HTTPD_SRCS) \
$(TEST_DEPINDEX_SRCS) \
$(TEST_PAGERANK_SRCS) \
$(TEST_TOKEN_REDUCTION_SRCS) \
$(TEST_TOOL_CONSOLIDATION_SRCS) \
$(TEST_INPUT_VALIDATION_SRCS) \
$(TEST_SECURITY_SRCS) \
$(TEST_YAML_SRCS) \
$(TEST_SEMANTIC_SRCS) \
$(TEST_AST_PROFILE_SRCS) \
$(TEST_SLAB_ALLOC_SRCS) \
$(TEST_SIMHASH_SRCS) \
$(TEST_STACK_OVERFLOW_SRCS) \
$(TEST_INTEGRATION_SRCS)
# ── Build directories ────────────────────────────────────────────
BUILD_DIR = build/c
# ── Object file compilation (grammars need relaxed warnings) ─────
# Grammar + tree-sitter runtime: compiled without -Werror (upstream code has warnings)
GRAMMAR_CFLAGS = -std=c11 -D_DEFAULT_SOURCE -O2 -w -Isrc -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC)
GRAMMAR_CFLAGS_TEST = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -Isrc -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \
$(SANITIZE)
GRAMMAR_CFLAGS_TSAN = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -Isrc -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \
$(TSAN_SANITIZE)
# Object files for grammars + ts_runtime + lsp_all + preprocessor
GRAMMAR_OBJS_TEST = $(patsubst $(CBM_DIR)/%.c,$(BUILD_DIR)/%.o,$(GRAMMAR_SRCS))
TS_RUNTIME_OBJ_TEST = $(BUILD_DIR)/ts_runtime.o
LSP_OBJ_TEST = $(BUILD_DIR)/lsp_all.o
PP_OBJ_TEST = $(BUILD_DIR)/preprocessor.o
GRAMMAR_OBJS_TSAN = $(patsubst $(CBM_DIR)/%.c,$(BUILD_DIR)/tsan_%.o,$(GRAMMAR_SRCS))
TS_RUNTIME_OBJ_TSAN = $(BUILD_DIR)/tsan_ts_runtime.o
LSP_OBJ_TSAN = $(BUILD_DIR)/tsan_lsp_all.o
PP_OBJ_TSAN = $(BUILD_DIR)/tsan_preprocessor.o
# Grammar wrappers include generated parser/scanner C files. Compiler-generated
# dependency files keep incremental builds correct after grammar refreshes and
# branch/worktree switches. The v1 stamp forces pre-depfile objects to rebuild
# once; subsequent builds invalidate only the grammar whose included files moved.
GRAMMAR_DEP_STAMP = $(BUILD_DIR)/.grammar-deps-v1
GRAMMAR_DEPFILES = $(addsuffix .d,$(GRAMMAR_OBJS_TEST) $(GRAMMAR_OBJS_TSAN))
# ── Targets ──────────────────────────────────────────────────────
.PHONY: test test-par test-focused test-compositional test-repro test-foundation test-tsan \
test-syntax test-daemon-smoke \
test-leak test-analyze test-memory test-gmalloc cbm \
cbm-with-ui frontend embed clean-c lint lint-tidy lint-cppcheck \
lint-format lint-source-safety install test-runner-nosan security \
native-extraction-benchmark
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# ── Foundation-only test (fast, no extraction) ───────────────────
$(BUILD_DIR)/test-foundation: $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) \
$(MIMALLOC_OBJ_TEST) $(TS_RUNTIME_OBJ_TEST) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -o $@ $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) \
$(MIMALLOC_OBJ_TEST) $(TS_RUNTIME_OBJ_TEST) $(LDFLAGS_TEST)
test-foundation: $(BUILD_DIR)/test-foundation
cd $(CURDIR) && $(BUILD_DIR)/test-foundation
# Fast C syntax check for selected test or production translation units. Reuse
# the nosan test flags so gated *_for_testing declarations and both test include
# roots cannot drift from the real test builds. Example:
# make -f Makefile.cbm test-syntax TEST_SYNTAX_SRCS="tests/test_mcp.c src/mcp/mcp.c"
TEST_SYNTAX_SRCS ?=
test-syntax:
@test -n "$(strip $(TEST_SYNTAX_SRCS))" || \
(echo "TEST_SYNTAX_SRCS is required for test-syntax"; exit 2)
$(CC) $(CFLAGS_NOSAN) $(TEST_INCLUDE_FLAGS) -fsyntax-only $(TEST_SYNTAX_SRCS)
# ── Grammar/TS/LSP object files (compiled with relaxed warnings) ─
$(GRAMMAR_DEP_STAMP): | $(BUILD_DIR)
@touch $@
$(GRAMMAR_OBJS_TEST): $(BUILD_DIR)/%.o: $(CBM_DIR)/%.c $(GRAMMAR_DEP_STAMP) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TEST) -MMD -MP -MF $@.d -c -o $@ $<
$(BUILD_DIR)/ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TEST) -c -o $@ $<
$(BUILD_DIR)/lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TEST) $(SANITIZE) -c -o $@ $<
$(BUILD_DIR)/preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
$(CXX) $(CXXFLAGS_TEST) -w -I$(CBM_DIR)/vendored -c -o $@ $<
$(GRAMMAR_OBJS_TSAN): $(BUILD_DIR)/tsan_%.o: $(CBM_DIR)/%.c $(GRAMMAR_DEP_STAMP) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TSAN) -MMD -MP -MF $@.d -c -o $@ $<
$(BUILD_DIR)/tsan_ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
$(BUILD_DIR)/tsan_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
$(BUILD_DIR)/tsan_preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
$(CXX) $(CXXFLAGS_TSAN) -w -I$(CBM_DIR)/vendored -c -o $@ $<
# ── Full test binary ─────────────────────────────────────────────
# mimalloc object files
MIMALLOC_OBJ_TEST = $(BUILD_DIR)/mimalloc.o
MIMALLOC_OBJ_TSAN = $(BUILD_DIR)/tsan_mimalloc.o
MIMALLOC_OBJ_PROD = $(BUILD_DIR)/prod_mimalloc.o
$(BUILD_DIR)/mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
$(CC) $(MIMALLOC_CFLAGS_TEST) -c -o $@ $<
$(BUILD_DIR)/tsan_mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
$(CC) $(MIMALLOC_CFLAGS_TEST) $(TSAN_SANITIZE) -c -o $@ $<
# static.c is an AMALGAMATION: it #includes options.c and friends, none of which
# make can see through the single named prerequisite, and the compile flags live
# in this Makefile. Both gaps bit for real — the __DATE__ removal in options.c and
# SQLITE_OMIT_LOAD_EXTENSION below each silently did nothing on an incremental
# build because the object was considered up to date. Depend on the included
# sources AND on this Makefile so a flag change rebuilds too.
$(BUILD_DIR)/prod_mimalloc.o: $(MIMALLOC_SRC) $(wildcard vendored/mimalloc/src/*.c) \
$(wildcard vendored/mimalloc/include/*.h) Makefile.cbm | $(BUILD_DIR)
$(CC) $(MIMALLOC_CFLAGS) -c -o $@ $<
# sqlite3 object files (vendored amalgamation)
SQLITE3_OBJ_TEST = $(BUILD_DIR)/sqlite3.o
SQLITE3_OBJ_TSAN = $(BUILD_DIR)/tsan_sqlite3.o
SQLITE3_OBJ_PROD = $(BUILD_DIR)/prod_sqlite3.o
$(BUILD_DIR)/sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
$(CC) $(SQLITE3_CFLAGS_TEST) $(SANITIZE) -c -o $@ $<
$(BUILD_DIR)/tsan_sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
$(CC) $(SQLITE3_CFLAGS_TEST) $(TSAN_SANITIZE) -c -o $@ $<
$(BUILD_DIR)/prod_sqlite3.o: $(SQLITE3_SRC) Makefile.cbm | $(BUILD_DIR)
$(CC) $(SQLITE3_CFLAGS) -c -o $@ $<
# TRE regex (only compiled on Windows — POSIX uses system <regex.h>)
TRE_OBJ_TEST :=
TRE_OBJ_TSAN :=
TRE_OBJ_PROD :=
ifeq ($(IS_MINGW),yes)
TRE_OBJ_TEST = $(BUILD_DIR)/tre.o
TRE_OBJ_TSAN = $(BUILD_DIR)/tsan_tre.o
TRE_OBJ_PROD = $(BUILD_DIR)/prod_tre.o
# tre.o keeps UBSan's alignment check OFF, and that is not a coverage
# preference -- it is a portability fact. Arming it here (attempted, then
# reverted after this bisect) makes the vendored TRE regex engine trap on
# Windows/ARM64, where the CLANGARM64 leg builds with -fsanitize-trap and a
# trap is an illegal instruction with no diagnostic: every fixture-indexing
# suite died with STATUS_ILLEGAL_INSTRUCTION and no message. macOS and Linux
# stay clean under the same check because Windows is LLP64 -- 32-bit long --
# so TRE's struct layouts and access widths differ there and nowhere else.
#
# TRE is third-party code we vendor and do not modify, so the alignment
# behaviour is upstream's to change; suppressing the check for this one
# object is the honest scope. Everything else keeps alignment armed.
$(BUILD_DIR)/tre.o: $(TRE_SRC) | $(BUILD_DIR)
$(CC) $(TRE_CFLAGS) $(SANITIZE) -fno-sanitize=alignment -c -o $@ $<
$(BUILD_DIR)/tsan_tre.o: $(TRE_SRC) | $(BUILD_DIR)
$(CC) $(TRE_CFLAGS) $(TSAN_SANITIZE) -c -o $@ $<
$(BUILD_DIR)/prod_tre.o: $(TRE_SRC) | $(BUILD_DIR)
$(CC) $(TRE_CFLAGS) -O2 -c -o $@ $<
endif
# ── Nosan build: ASan-free test runner for macOS heap leak detection ─────────
#
# WHY THIS EXISTS:
# 'make test-leak' uses Apple's 'leaks --atExit' tool to find heap leaks.
# But leaks cannot inspect a process that uses a custom malloc (such as ASan).
# The regular test-runner is built with -fsanitize=address,undefined, which
# replaces malloc → leaks aborts with "unable to inspect heap ranges".
#
# HOW IT WORKS:
# We rebuild all ASan-instrumented vendored objects without -fsanitize flags
# into $(NOSAN_DIR), then link test-runner-nosan against them.
# The resulting binary runs the full test suite under Apple's heap profiler.
# Full leak report is written to $(LEAK_LOG) = build/c/leak-report.txt.
#
# HOW TO USE:
# make test-leak # runs full suite + heap check, saves report to LEAK_LOG
# cat build/c/leak-report.txt # review complete leak report after run
#
# WHICH OBJECTS NEED NOSAN VARIANTS (use SANITIZE in their *_TEST flags):
# sqlite3, lsp_all, preprocessor, grammar/*.c, ts_runtime
# WHICH ARE REUSED AS-IS (never use SANITIZE):
# mimalloc (MIMALLOC_CFLAGS_TEST has no -fsanitize)
# tre (only on Windows; TRE_CFLAGS has no -fsanitize)
#
NOSAN_DIR = $(BUILD_DIR)/nosan
GRAMMAR_CFLAGS_NOSAN = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -Isrc -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC)
GRAMMAR_OBJS_NOSAN = $(patsubst $(CBM_DIR)/%.c,$(NOSAN_DIR)/%.o,$(GRAMMAR_SRCS))
GRAMMAR_DEPFILES += $(addsuffix .d,$(GRAMMAR_OBJS_NOSAN))
$(NOSAN_DIR):
mkdir -p $(NOSAN_DIR)
# Grammar C files (tree-sitter parsers) — recompiled without ASan/UBSan
$(GRAMMAR_OBJS_NOSAN): $(NOSAN_DIR)/%.o: $(CBM_DIR)/%.c $(GRAMMAR_DEP_STAMP) | $(NOSAN_DIR)
$(CC) $(GRAMMAR_CFLAGS_NOSAN) -MMD -MP -MF $@.d -c -o $@ $<
$(NOSAN_DIR)/ts_runtime.o: $(CBM_DIR)/ts_runtime.c | $(NOSAN_DIR)
$(CC) $(GRAMMAR_CFLAGS_NOSAN) -c -o $@ $<
$(NOSAN_DIR)/lsp_all.o: $(LSP_UNITY_DEPS) | $(NOSAN_DIR)
$(CC) $(GRAMMAR_CFLAGS_NOSAN) -c -o $@ $<
$(NOSAN_DIR)/preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(NOSAN_DIR)
$(CXX) $(CXXFLAGS_COMMON) -g -O1 -w -I$(CBM_DIR)/vendored -c -o $@ $<
$(NOSAN_DIR)/sqlite3.o: $(SQLITE3_SRC) | $(NOSAN_DIR)
$(CC) $(SQLITE3_CFLAGS_TEST) -c -o $@ $<
OBJS_VENDORED_NOSAN = $(MIMALLOC_OBJ_TEST) $(NOSAN_DIR)/sqlite3.o $(TRE_OBJ_TEST) \
$(GRAMMAR_OBJS_NOSAN) $(NOSAN_DIR)/ts_runtime.o \
$(NOSAN_DIR)/lsp_all.o $(NOSAN_DIR)/preprocessor.o \
$(NOSAN_LZ4_OBJ) $(NOSAN_ZSTD_OBJ) $(UNIXCODER_OBJ)
# Vendored LZ4 (test build)
LZ4_OBJ_TEST = $(BUILD_DIR)/test_lz4.o $(BUILD_DIR)/test_lz4hc.o
LZ4_OBJ_TSAN = $(BUILD_DIR)/tsan_lz4.o $(BUILD_DIR)/tsan_lz4hc.o
$(BUILD_DIR)/test_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(SANITIZE) -w -I$(CBM_DIR) -c -o $@ $<
$(BUILD_DIR)/test_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(SANITIZE) -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
$(BUILD_DIR)/tsan_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(TSAN_SANITIZE) -w -I$(CBM_DIR) -c -o $@ $<
$(BUILD_DIR)/tsan_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(TSAN_SANITIZE) -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
# Vendored zstd (test build)
# ZSTD_EXTRA_CFLAGS: hook for sanitizer lanes. The MSan lane needs
# `-include stdint.h` for THIS object only: zstd's MEMORY_SANITIZER-guarded
# block declares __msan_test_shadow returning intptr_t, but the amalgamator
# collapsed the `#define ZSTD_DEPS_NEED_STDINT` re-include that should supply
# it. The flag cannot go on the global SANITIZE line: force-including a libc
# header ahead of a source file freezes glibc's feature-test macros before
# files like sqlite3.c set _GNU_SOURCE themselves, which strips
# MREMAP_MAYMOVE/nanosleep out of their view of libc.
ZSTD_OBJ_TEST = $(BUILD_DIR)/test_zstd.o
ZSTD_OBJ_TSAN = $(BUILD_DIR)/tsan_zstd.o
$(BUILD_DIR)/test_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(SANITIZE) $(ZSTD_EXTRA_CFLAGS) -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
$(BUILD_DIR)/tsan_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(TSAN_SANITIZE) -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
# Vendored LZ4/zstd (NOSAN build — sanitizer-free so `leaks --atExit` can walk the
# heap on macOS; ASan replaces malloc and is incompatible with the leaks tool).
NOSAN_LZ4_OBJ = $(NOSAN_DIR)/lz4.o $(NOSAN_DIR)/lz4hc.o
NOSAN_ZSTD_OBJ = $(NOSAN_DIR)/zstd.o
$(NOSAN_DIR)/lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(NOSAN_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -c -o $@ $<
$(NOSAN_DIR)/lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(NOSAN_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
$(NOSAN_DIR)/zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(NOSAN_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
# nomic-embed-code pretrained vector blob
UNIXCODER_OBJ = $(BUILD_DIR)/unixcoder_blob.o
$(UNIXCODER_OBJ): $(UNIXCODER_BLOB_SRC) vendored/nomic/code_vectors.bin | $(BUILD_DIR)
$(CC) -c -o $@ $<
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) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) $(TEST_INCLUDE_FLAGS) -o $@ \
$(ALL_TEST_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TEST) \
$(LDFLAGS_TEST)
$(BUILD_DIR)/test-runner-nosan: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_NOSAN) | $(BUILD_DIR) $(NOSAN_DIR)
$(CC) $(CFLAGS_NOSAN) $(TEST_INCLUDE_FLAGS) -o $@ \
$(ALL_TEST_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_NOSAN) \
$(LDFLAGS_NOSAN)
test: $(BUILD_DIR)/test-runner
cd $(CURDIR) && $(BUILD_DIR)/test-runner
# Parallel variant: every suite as its own process; identical gate quality
# (see the ZERO-LOSS CONTRACT in scripts/run-tests-parallel.sh — union guard
# against --list-suites + aggregated pass/fail/skip totals).
test-par: $(BUILD_DIR)/test-runner
cd $(CURDIR) && bash scripts/run-tests-parallel.sh $(BUILD_DIR)/test-runner
# Focused native development is intentionally a separate, explicit target so
# an inherited TEST_SUITES value cannot silently narrow the gating test target.
TEST_SUITES ?=
test-focused: $(BUILD_DIR)/test-runner
@test -n "$(strip $(TEST_SUITES))" || \
(echo "TEST_SUITES is required for test-focused"; exit 2)
cd $(CURDIR) && $(BUILD_DIR)/test-runner $(TEST_SUITES)
# Cross-parent interaction canaries. Keep this target small: each row pairs a
# classifier or policy from one side of the merge with the publication action
# that consumes it, while the full suite remains the exhaustive gate.
test-compositional: $(BUILD_DIR)/test-runner
cd $(CURDIR) && CBM_ONLY_SUITE=pipeline \
CBM_ONLY_TEST=incremental_frontier_full_fallback_failure_preserves_dirty_ledger \
$(BUILD_DIR)/test-runner
cd $(CURDIR) && CBM_ONLY_SUITE=input_validation \
CBM_ONLY_TEST=path_project_autoindex_honors_dep_limit_and_refreshes_rank \
$(BUILD_DIR)/test-runner
cd $(CURDIR) && CBM_ONLY_SUITE=daemon_application \
CBM_ONLY_TEST=daemon_application_auto_index_honors_tracked_file_limit \
$(BUILD_DIR)/test-runner
cd $(CURDIR) && CBM_ONLY_SUITE=pagerank \
CBM_ONLY_TEST=pagerank_refresh_defer_exact_delta_reindexes_does_not_defer_containment \
$(BUILD_DIR)/test-runner
cd $(CURDIR) && CBM_ONLY_SUITE=pagerank \
CBM_ONLY_TEST=pagerank_refresh_defer_all_incremental_reindexes_defers_full_fallback \
$(BUILD_DIR)/test-runner
cd $(CURDIR) && CBM_ONLY_SUITE=mcp \
CBM_ONLY_TEST=tool_query_graph_uses_active_relationship_query_with_ready_overlay \
$(BUILD_DIR)/test-runner
cd $(CURDIR) && CBM_ONLY_SUITE=mcp \
CBM_ONLY_TEST=watcher_publication_reopens_cached_store_generation \
$(BUILD_DIR)/test-runner
# These two established stdio-notification oracles use POSIX pipe/alarm
# fixtures and are not registered by tests/test_mcp.c on native Windows.
ifneq ($(OS),Windows_NT)
cd $(CURDIR) && CBM_ONLY_SUITE=mcp \
CBM_ONLY_TEST=mcp_index_repository_inprocess_sends_list_changed \
$(BUILD_DIR)/test-runner
cd $(CURDIR) && CBM_ONLY_SUITE=mcp \
CBM_ONLY_TEST=mcp_autoindex_thread_sends_list_changed \
$(BUILD_DIR)/test-runner
endif
cd $(CURDIR) && CBM_ONLY_SUITE=daemon_runtime \
CBM_ONLY_TEST=daemon_runtime_request_cancel_is_exact_and_session_remains_usable \
$(BUILD_DIR)/test-runner
cd $(CURDIR) && CBM_ONLY_SUITE=mcp \
CBM_ONLY_TEST=tool_index_repository_lock_wait_honors_request_cancel \
$(BUILD_DIR)/test-runner
# ── Cumulative bug-reproduction runner (RED board by design, non-gating) ──
# 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) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -Itests -o $@ \
$(TEST_REPRO_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TEST) \
$(LDFLAGS_TEST)
test-repro: $(BUILD_DIR)/test-repro-runner
cd $(CURDIR) && bash tests/repro/repro_script_summary.sh
cd $(CURDIR) && bash tests/repro/repro_script_zero_selection.sh
cd $(CURDIR) && bash tests/repro/repro_script_missing_summary.sh
cd $(CURDIR) && bash tests/repro/repro_script_skipped_summary.sh
cd $(CURDIR) && bash tests/repro/repro_runner_zero_selection.sh
cd $(CURDIR) && bash tests/repro/repro_runner_passing_semantics.sh
cd $(CURDIR) && bash tests/repro/repro_script_no_workspace_artifact.sh
cd $(CURDIR) && bash tests/repro/repro_script_stderr_summary.sh
cd $(CURDIR) && bash tests/repro/repro_make_tracks_headers.sh
cd $(CURDIR) && $(BUILD_DIR)/test-repro-runner
# ── TSan full test ───────────────────────────────────────────────
#
# ThreadSanitizer build for data-race detection in the parallel pipeline.
# Mirrors test-runner but links vendored objects compiled with
# -fsanitize=thread and links the test mimalloc object (MI_OVERRIDE=0 —
# does NOT override malloc, so TSan intercepts the real malloc/free) to
# satisfy the unconditional mi_* calls in src/foundation/mem.c.
#
# Cannot be combined with ASan/UBSan. Uses the system allocator.
# Every threaded production surface that runs clean AND stable under TSan:
# allocator concurrency (mem, slab_alloc), the parallel extraction worker pool
# (parallel, worker_pool, pipeline), the filesystem watcher (watcher), the
# embedded HTTP server (httpd), diagnostics sampling (diagnostics), the MCP
# server + mutation guard (mcp, mcp_mutation_guard), subprocess supervision
# (subprocess), and the runnable daemon-coordination paths (daemon,
# daemon_application). Was `mem slab_alloc parallel` — a keyhole that ran TSan
# over no real threaded production code.
#
# NO EXCLUSIONS. A sanitizer covers the complete surface or it is asserting
# coverage it does not have. The three suites once excluded here
# (daemon_runtime, daemon_ipc, daemon_frontend) are back:
# daemon_ipc, daemon_frontend — the documented test-harness race and thread
# leaks no longer reproduce; both run clean under TSan.
# daemon_runtime — did NOT deadlock as documented. It reported a REAL
# production data race on the log sink (plain global function pointer,
# written by the configuring thread, read AND CALLED by connection
# workers), now fixed with atomics in log.c. Excluding the suite is what
# had hidden it.
TEST_TSAN_SUITES ?= mem slab_alloc parallel worker_pool watcher httpd pipeline \
diagnostics mcp mcp_mutation_guard subprocess daemon daemon_application \
daemon_runtime daemon_ipc daemon_frontend
# Keep the project suppression separate from the caller's ordinary TSan knobs.
# Advanced runs can point this at a combined suppression file without replacing
# halt/report/history settings supplied through TSAN_OPTIONS.
TSAN_PROJECT_OPTIONS ?= suppressions=$(CURDIR)/tests/tsan.supp
TSAN_WORKERS ?= 4
# halt_on_error: a race is a bug, stop at the first one.
#
# report_thread_leaks=0: this disables TSan's thread-HYGIENE check only —
# RACE detection, the reason the lane exists, is completely unaffected. Some
# daemon fixtures fork after the process has gone multi-threaded, and in the
# forked child TSan sees the parent's finished threads as never-joined even
# though the fixture joins them (test_daemon_frontend.c:726 joins the thread
# reported at :698). It fires on macOS and not Linux, i.e. it tracks fork
# semantics rather than anything about our code. The alternative was dropping
# whole suites from the lane, which costs real race coverage; this costs none.
TSAN_OPTIONS ?= halt_on_error=1:report_thread_leaks=0
# A fixed concurrent envelope keeps TSan deterministic across developer hosts
# and GitHub runners. Four workers still exercise real races without turning
# sanitizer-instrumented allocator bookkeeping into a high-core lock convoy.
# 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) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TSAN) $(TEST_INCLUDE_FLAGS) -o $@ \
$(ALL_TEST_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TSAN) \
$(LDFLAGS_TSAN)
test-tsan: $(BUILD_DIR)/test-runner-tsan
@echo "Running ThreadSanitizer with $(TSAN_WORKERS) workers. Reports go to stderr."
cd $(CURDIR) && CBM_WORKERS=$(TSAN_WORKERS) \
TSAN_OPTIONS="$(TSAN_OPTIONS):$(TSAN_PROJECT_OPTIONS)" \
$(BUILD_DIR)/test-runner-tsan $(TEST_TSAN_SUITES)
# ── Leak detection ───────────────────────────────────────────────
# macOS: uses `leaks --atExit` (Apple Clang LSan not available on all versions)
# Linux: ASAN_OPTIONS=detect_leaks=1 (GCC/Clang ASan always includes LSan)
# Note: if false positives appear from system libraries on Linux, create lsan.supp
# and set LSAN_OPTIONS=suppressions=lsan.supp
LEAK_LOG = $(BUILD_DIR)/leak-report.txt
# Stream a long-running gate through tee while preserving both failure sources.
# Make recipes use /bin/sh by default, so Bash-only PIPESTATUS is not portable
# (notably, Debian/Ubuntu /bin/sh is dash). The pipeline's left-hand subshell
# records its status in a per-process sidecar; the parent shell then returns the
# command failure first, or tee's failure when the command itself succeeded.
define run_logged_command
@status_file="$(1).status.$$$$"; \
trap 'rm -f "$$status_file"' 0 1 2 3 15; \
{ $(2); printf '%s\n' "$$?" > "$$status_file"; } 2>&1 | tee "$(1)"; \
tee_status=$$?; \
if test ! -s "$$status_file"; then \
echo "ERROR: logged command ended without recording its status" >&2; \
exit 1; \
fi; \
command_status=$$(sed -n '1p' "$$status_file"); \
if test "$$command_status" -ne 0; then exit "$$command_status"; fi; \
exit "$$tee_status"
endef
# Apple's leaks debugger stops descendant test binaries, so crash, socket-
# inheritance, and UI child-process probes cannot run under `leaks --atExit`
# without deadlocking or failing their parent. Cover the allocation-owning
# stores, pipelines, rankers, and parallel workers explicitly instead. The test runner
# also always includes httplink, token_reduction, depindex, pagerank,
# tool_consolidation, and input_validation when suite arguments are present.
TEST_LEAK_SUITES ?= arena hash_table dyn_array str_intern store_nodes store_edges \
store_search store_bulk store_pragmas store_checkpoint dump_verify_io \
graph_buffer registry pipeline worker_pool parallel slab_alloc mem \
integration incremental
# Guard Malloc is inherited across exec on macOS. Keep its default gate on
# allocation-owning, in-process surfaces: otherwise external helpers such as
# git are instrumented too, and watcher tests observe libgmalloc/tool behavior
# instead of the server's memory safety. CBM_ONLY_SUITE/CBM_ONLY_TEST still
# allow an explicit narrower probe.
TEST_GMALLOC_SUITES ?= $(TEST_LEAK_SUITES) lz4 zstd artifact sqlite_writer
ifeq ($(UNAME_S),Darwin)
# macOS: 'leaks' cannot inspect ASan-instrumented processes (ASan replaces malloc).
# Use test-runner-nosan (no ASan/UBSan) so leaks can walk the heap.
test-leak: $(BUILD_DIR)/test-runner-nosan
@echo "Running heap leak detection via 'leaks --atExit' on nosan build (macOS). May take 2-5 minutes."
@echo "Full report saved to $(LEAK_LOG). Exit 0 = no leaks."
$(call run_logged_command,$(LEAK_LOG),leaks --atExit -- $(BUILD_DIR)/test-runner-nosan $(TEST_LEAK_SUITES))
else
test-leak: $(BUILD_DIR)/test-runner
@echo "Running heap leak detection via ASan/LSan (Linux). Full report saved to $(LEAK_LOG). Exit 0 = no leaks."
$(call run_logged_command,$(LEAK_LOG),ASAN_OPTIONS=detect_leaks=1 $(BUILD_DIR)/test-runner)
endif
# ── Memory-corruption debug (macOS) ───────────────────────────────
# Catches uninit reads, use-after-free, and overruns that ASan/TSan can miss
# (used to investigate the B1 custom-writer non-deterministic corruption).
# All run the NOSAN binary: ASan replaces malloc, which would defeat these
# libmalloc knobs. Set CBM_ONLY_SUITE=<suite> to target a slow suite, and
# optionally CBM_ONLY_TEST=<substring> to run matching tests after suite setup.
MEM_LOG = $(BUILD_DIR)/mem-report.txt
ifeq ($(UNAME_S),Darwin)
# MallocScribble=1 → freed memory filled with 0x55 (catches use-after-free).
# MallocPreScribble=1 → freshly-allocated memory filled with 0xAA (catches
# uninitialized reads — the macOS MSan equivalent; makes uninit deterministic).
test-memory: $(BUILD_DIR)/test-runner-nosan
@echo "Running under MallocScribble+MallocPreScribble (macOS nosan): uninit reads -> 0xAA, use-after-free -> 0x55."
@echo "Full report saved to $(MEM_LOG)."
$(call run_logged_command,$(MEM_LOG),MallocScribble=1 MallocPreScribble=1 $(BUILD_DIR)/test-runner-nosan)
# Guard Malloc (libgmalloc): a guard page around EVERY allocation → crashes at
# the exact overrun / use-after-free, with a stack trace. Stricter than scribble
# (which only paints bytes); slower and noisier. The decisive memory tool.
test-gmalloc: $(BUILD_DIR)/test-runner-nosan
@echo "Running allocation-owning suites under Guard Malloc (libgmalloc). Crashes at the exact overrun/UAF. Slow."
@echo "Full report saved to $(MEM_LOG)."
$(call run_logged_command,$(MEM_LOG),DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib $(BUILD_DIR)/test-runner-nosan $(TEST_GMALLOC_SUITES))
else
test-memory test-gmalloc: $(BUILD_DIR)/test-runner
@echo "These targets are macOS-only (MallocScribble/libgmalloc). On Linux use 'make test-leak' (ASan/LSan), or build with -fsanitize=memory (MSan) for uninit detection."
endif
# ── Static analysis (Clang analyzer only — GCC has no --analyze flag) ──────
ifeq ($(IS_GCC),no)
# Every test seam define is required, not optional: the test sources call the
# *_for_testing APIs that those defines gate. Analyzing without one makes those
# calls implicit declarations returning int, which reports false compile errors
# and poisons the analyzer's type reasoning for the whole translation unit.
ANALYZE_LOG = $(BUILD_DIR)/analyze-report.txt
test-analyze: $(ALL_TEST_SRCS) $(PROD_SRCS) | $(BUILD_DIR)
@echo "Running Clang static analyzer..."
@$(CC) --analyze $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) \
$(KOTLIN_DEDUP_TEST_DEFINE) $(CALL_REFERENCE_LOOKUP_TEST_DEFINE) \
$(INCREMENTAL_TEST_DEFINE) $(TEST_INCLUDE_FLAGS) \
$(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) >"$(ANALYZE_LOG)" 2>&1; \
analyze_status=$$?; \
if grep -E "warning:|error:|note:" "$(ANALYZE_LOG)"; then \
:; \
elif [ $$analyze_status -eq 0 ]; then \
echo "No issues found."; \
else \
cat "$(ANALYZE_LOG)"; \
fi; \
exit $$analyze_status
else
test-analyze:
@echo "Static analysis skipped: requires Clang (not GCC). Install clang and re-run."
endif
# Real-binary POSIX lifecycle smoke. The endpoint is deliberately account-wide;
# an explicit Make invocation requires a clean rendezvous and fails rather than
# silently skipping an occupied one. Deterministic C tests cover Windows IPC.
test-daemon-smoke: $(BUILD_DIR)/codebase-memory-mcp
cd $(CURDIR) && CBM_DAEMON_SMOKE_REQUIRE_RUN=1 python3 tests/test_daemon_smoke.py $(BUILD_DIR)/codebase-memory-mcp
# ── Production binary ────────────────────────────────────────────
# Grammar/TS/LSP objects for production (compiled with relaxed warnings, -O2)
GRAMMAR_OBJS_PROD = $(patsubst $(CBM_DIR)/%.c,$(BUILD_DIR)/prod_%.o,$(GRAMMAR_SRCS))
TS_RUNTIME_OBJ_PROD = $(BUILD_DIR)/prod_ts_runtime.o
LSP_OBJ_PROD = $(BUILD_DIR)/prod_lsp_all.o
PP_OBJ_PROD = $(BUILD_DIR)/prod_preprocessor.o
GRAMMAR_DEPFILES += $(addsuffix .d,$(GRAMMAR_OBJS_PROD))
-include $(GRAMMAR_DEPFILES)
$(GRAMMAR_OBJS_PROD): $(BUILD_DIR)/prod_%.o: $(CBM_DIR)/%.c $(GRAMMAR_DEP_STAMP) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS) -MMD -MP -MF $@.d -c -o $@ $<
$(BUILD_DIR)/prod_ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
$(BUILD_DIR)/prod_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
$(BUILD_DIR)/prod_preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
$(CXX) $(CXXFLAGS_PROD) -w -I$(CBM_DIR)/vendored -c -o $@ $<
# Vendored LZ4 (compiled separately, not unity-built via lz4_store.c)
LZ4_OBJ_PROD = $(BUILD_DIR)/prod_lz4.o $(BUILD_DIR)/prod_lz4hc.o
$(BUILD_DIR)/prod_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) -c -o $@ $<
$(BUILD_DIR)/prod_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
# Vendored zstd (compiled separately, not unity-built via zstd_store.c)
ZSTD_OBJ_PROD = $(BUILD_DIR)/prod_zstd.o
$(BUILD_DIR)/prod_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
OBJS_VENDORED_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(GRAMMAR_OBJS_PROD) $(TS_RUNTIME_OBJ_PROD) $(LSP_OBJ_PROD) $(PP_OBJ_PROD) $(LZ4_OBJ_PROD) $(ZSTD_OBJ_PROD) $(UNIXCODER_OBJ)
MAIN_SRC = src/main.c
# Rebuild when the build CONFIGURATION changes, not only when sources do. The
# product binary is compiled in ONE shot from sources, so after a TEST_SEAMS or
# version flip make finds the binary newer than every source and skips the recipe
# entirely, handing back a binary built with the PREVIOUS configuration. The
# dangerous direction is a release build silently keeping test seams, which is a
# property no reviewer can see by reading the diff. The stamp's CONTENT is the
# configuration and it is rewritten only when that content changes, so ordinary
# incremental builds are unaffected. (Single-quoted: the signature may contain
# the escaped quotes of -DCBM_VERSION, which are literal inside '', but must not
# contain a literal single quote.)
# SANITIZE belongs here: it changes the instrumentation compiled into every
# object, so a build that only changes it MUST rebuild. Its absence made
# `scripts/test.sh SANITIZE= ...` -- the documented way to get a plain build
# for trap debugging -- silently re-run the previously instrumented binary,
# which during this bisect produced a "crashes without sanitizers too" result
# that was pure artifact and sent the investigation down the wrong path.
BUILD_CONFIG_SIG := TEST_SEAMS=$(TEST_SEAMS)|CFLAGS_EXTRA=$(CFLAGS_EXTRA)|SANITIZE=$(SANITIZE)
.PHONY: build-config-check
build-config-check:
$(BUILD_DIR)/.build-config: build-config-check | $(BUILD_DIR)
@printf '%s\n' '$(BUILD_CONFIG_SIG)' > $@.tmp
@if cmp -s $@.tmp $@; then rm -f $@.tmp; \
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 $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_PROD) -o $@ \
$(MAIN_SRC) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_PROD) \
$(LDFLAGS)
# Extraction latency/scale probe linked under the release configuration. It
# deliberately excludes test API defines and sanitizer flags, unlike
# test-runner-nosan, so measurements represent the shipped hot path while
# remaining independent of the account-wide daemon cohort.
NATIVE_EXTRACTION_BENCH_SRCS = \
benchmarks/native_extraction_benchmark_main.c \
$(TEST_CS_LSP_BENCH_SRCS) \
$(TEST_PY_LSP_BENCH_SRCS) \
$(TEST_PY_LSP_SCALE_SRCS)
$(BUILD_DIR)/native-extraction-benchmark: $(NATIVE_EXTRACTION_BENCH_SRCS) $(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) $(TEST_INCLUDE_FLAGS) -o $@ \
$(NATIVE_EXTRACTION_BENCH_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_PROD) \
$(LDFLAGS)
$(call codesign_binary,$@)
native-extraction-benchmark: $(BUILD_DIR)/native-extraction-benchmark
@echo "Built: $(BUILD_DIR)/native-extraction-benchmark"
cbm: $(BUILD_DIR)/codebase-memory-mcp
@echo "Built: $(BUILD_DIR)/codebase-memory-mcp"
$(call codesign_binary,$(BUILD_DIR)/codebase-memory-mcp)
# ── Install to INSTALL_DIR (default ~/.local/bin) ────────────────
# Re-signs after copy — required on macOS 25+ where cp invalidates the
# existing ad-hoc signature and an unsigned binary gets SIGKILL at startup.
INSTALL_DIR ?= $(HOME)/.local/bin
install: cbm
@echo "Installing to $(INSTALL_DIR)/codebase-memory-mcp ..."
install -d "$(INSTALL_DIR)"
install -m 755 "$(BUILD_DIR)/codebase-memory-mcp" "$(INSTALL_DIR)/codebase-memory-mcp"
$(call codesign_binary,$(INSTALL_DIR)/codebase-memory-mcp)
@echo "Done. Run: $(INSTALL_DIR)/codebase-memory-mcp"
# ── Build with embedded UI (requires Node.js) ───────────────────
# Swap embedded_stub.c for the generated embedded_assets.c
UI_SRCS_WITH_ASSETS = $(subst src/ui/embedded_stub.c,src/ui/embedded_assets.c,$(UI_SRCS))
PROD_SRCS_WITH_ASSETS = $(subst src/ui/embedded_stub.c,src/ui/embedded_assets.c,$(PROD_SRCS))
# Embedded asset object files (generated by embed script)
EMBED_OBJS = $(wildcard $(BUILD_DIR)/embedded/embed_*.o)
frontend:
cd graph-ui && npm ci && npm run build
embed: frontend
scripts/embed-frontend.sh graph-ui/dist $(BUILD_DIR)/embedded
cbm-with-ui: embed $(OBJS_VENDORED_PROD)
$(CC) $(CFLAGS_PROD) -o $(BUILD_DIR)/codebase-memory-mcp \
$(MAIN_SRC) $(PROD_SRCS_WITH_ASSETS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_PROD) \
$(wildcard $(BUILD_DIR)/embedded/embed_*.o) \
$(LDFLAGS)
@echo "Built with UI: $(BUILD_DIR)/codebase-memory-mcp"
clean-c: export BUILD_DIR := $(BUILD_DIR)
clean-c:
@bash -c 'source scripts/path-safety.sh && cbm_remove_build_dir "$$PWD" "$$BUILD_DIR"'
# ── Linting ─────────────────────────────────────────────────────
# clang-tidy and clang-format: use Homebrew LLVM on macOS, overridable via make args
# e.g. make lint-format CLANG_FORMAT=clang-format-18
LLVM_BIN := $(shell brew --prefix llvm 2>/dev/null)/bin
CLANG_TIDY ?= $(shell [ -x "$(LLVM_BIN)/clang-tidy" ] && echo "$(LLVM_BIN)/clang-tidy" || echo clang-tidy)
CLANG_FORMAT ?= $(shell [ -x "$(LLVM_BIN)/clang-format" ] && echo "$(LLVM_BIN)/clang-format" || echo clang-format)
CPPCHECK ?= cppcheck
# macOS SDK sysroot (needed for Homebrew LLVM to find system headers)
SYSROOT = $(shell xcrun --show-sdk-path 2>/dev/null)
SYSROOT_FLAG = $(if $(SYSROOT),-isysroot $(SYSROOT),)
# Our source files (excluding vendored, grammars, tree-sitter runtime)
LINT_SRCS = $(FOUNDATION_SRCS) $(STORE_SRCS) $(CYPHER_SRCS) $(MCP_SRCS) $(DAEMON_SRCS) \
$(DISCOVER_SRCS) $(GRAPH_BUFFER_SRCS) $(PIPELINE_SRCS) $(SIMHASH_SRCS) $(SEMANTIC_SRCS) \
$(TRACES_SRCS) $(WATCHER_SRCS) $(CLI_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) \
$(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(MAIN_SRC)
LINT_HDRS = $(wildcard src/**/*.h src/*.h $(CBM_DIR)/*.h)
LINT_TEST_SRCS = $(ALL_TEST_SRCS)
# clang-tidy: deep static analysis (config in .clang-tidy)
lint-tidy:
@echo "=== clang-tidy ==="
@$(CLANG_TIDY) --quiet $(LINT_SRCS) -- $(CFLAGS_COMMON) $(SYSROOT_FLAG)
# cppcheck: complementary analysis (config in .cppcheck). Analyze the
# failpoint-enabled superset so test-only allocation and cleanup paths remain
# checked instead of collapsing to production constant-false stubs.
lint-cppcheck:
@echo "=== cppcheck ==="
@$(CPPCHECK) --enable=warning,style,performance,portability \
--std=c11 --language=c \
-DCBM_ENABLE_TEST_SEAMS \
--suppressions-list=.cppcheck \
--error-exitcode=1 \
--inline-suppr \
--quiet \
--suppress=varFuncNullUB \
--suppress=intToPointerCast \
--suppress=unusedStructMember \
--suppress=nullPointerOutOfMemory \
--suppress=nullPointerArithmeticOutOfMemory \
--suppress=ctunullpointerOutOfMemory \
--suppress='nullPointer:internal/cbm/sqlite_writer.c' \
-Isrc -Ivendored -Ivendored/sqlite3 \
-I$(CBM_DIR) -I$(TS_INCLUDE) -I$(BUILD_DIR)/generated \
$(LINT_SRCS)
# clang-format: formatting check (config in .clang-format, dry-run = no changes)
lint-format:
@echo "=== clang-format ==="
@$(CLANG_FORMAT) --dry-run --Werror $(LINT_SRCS) $(LINT_HDRS)
# Ban ALL NOLINT variants EXCEPT whitelisted NOLINT(misc-no-recursion).
# The whitelist is in src/foundation/recursion_whitelist.h with documented reasoning.
# Rule: NOLINT(misc-no-recursion) is allowed ONLY on function definitions that are
# listed in recursion_whitelist.h. All other NOLINT forms are banned.
lint-no-suppress:
@echo "=== NOLINT check ==="
@if grep -rn 'NOLINT' src/ internal/cbm/*.c internal/cbm/*.h 2>/dev/null \
| grep -v vendored \
| grep -v 'NOLINT(misc-no-recursion)' \
| grep -v 'recursion_whitelist.h'; then \
echo "ERROR: Banned NOLINT comment found in source code."; \
echo "Only NOLINT(misc-no-recursion) is allowed, and only for whitelisted functions."; \
echo "See src/foundation/recursion_whitelist.h for the whitelist."; \
exit 1; \
fi
@echo " Checking NOLINT(misc-no-recursion) against whitelist..."
@scripts/check-nolint-whitelist.sh
@bash scripts/check-source-safety.sh
lint-source-safety:
@bash scripts/check-source-safety.sh
@bash scripts/test-source-safety.sh
# All linters (run with make -j3 lint for parallel execution)
lint: lint-tidy lint-cppcheck lint-format lint-no-suppress
@echo "=== All linters passed ==="
# CI linters (no clang-tidy — platform-dependent, enforced locally via pre-commit)
lint-ci: lint-cppcheck lint-format lint-no-suppress
@echo "=== CI linters passed ==="
# ── Local memory-diagnostic lanes (not PR-CI gates by decision: the diag
# build doubles a runner's bill for a marginal delta, and a gating analyzer
# lane needs a suppression story the NOLINT ban deliberately forbids.
# Run these on the local ladder; both found real bugs on first use.) ──
# Newest pinned toolchain + ASan/UBSan + straighter stacks. Separate BUILD_DIR
# so the shipping-toolchain build stays untouched.
#
# This lane also turns on the ASan checks that are OFF by default everywhere
# else, which is the difference between running ASan and running all of it:
# detect_stack_use_after_return a returned-to stack frame reused later
# detect_stack_use_after_scope a pointer outliving its enclosing block
# strict_string_checks str* reads validated over the whole buffer
#
# detect_invalid_pointer_pairs is deliberately NOT here. It fires during
# static initialisation inside vendored simplecpp -- a std::string global at
# simplecpp.cpp:101 -- reporting a pair whose second "pointer" is the
# sentinel 0xfffffffffffffff3, i.e. libstdc++ string internals rather than
# anything this codebase wrote. The option is a process-wide runtime flag
# with no per-file scoping, so it cannot be aimed away from vendored code the
# way the analyzer's path filter can. Enabling it would mean a permanently
# red lane reporting a non-defect, which teaches people to ignore the lane.
# The instrumentation it requires (-fsanitize=pointer-compare,pointer-
# subtract) comes out with it, since it is dead weight without the check.
# They stay on the diagnostic lane rather than the gating ones until they have
# a clean history here; promoting them is a separate, deliberate step.
DIAG_LLVM_BIN ?= /opt/homebrew/opt/llvm/bin
diag:
@echo "=== diagnostic lane: $(shell $(DIAG_LLVM_BIN)/clang --version | head -1) ==="
$(MAKE) -f Makefile.cbm build/diag/test-runner \
CC=$(DIAG_LLVM_BIN)/clang CXX=$(DIAG_LLVM_BIN)/clang++ BUILD_DIR=build/diag \
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-optimize-sibling-calls"
ASAN_OPTIONS="detect_stack_use_after_return=1:strict_string_checks=1:detect_stack_use_after_scope=1" \
./build/diag/test-runner
# ── macOS leak detection ─────────────────────────────────────────
# LeakSanitizer is ON by default under ASan on Linux, so the Linux test leg
# has had leak coverage all along and the Linux-only leaks this branch fixed
# were found there. On macOS it is OFF by default and Apple's clang refuses
# detect_leaks=1 outright ("Exiting: detect_leaks is not supported on this
# platform"), which is why this repo had NO macOS leak coverage — a whole
# platform's worth of the sanitizer matrix missing.
#
# Apple's refusal is not a darwin limitation. Upstream clang supports LSan on
# darwin/arm64: this lane runs the full suite clean and was verified to still
# catch a deliberately leaked allocation. So the lane is simply "the normal
# ASan test build, compiled by Homebrew LLVM instead of Apple clang, run with
# leak detection turned on".
LSAN_LLVM_BIN ?= $(shell brew --prefix llvm 2>/dev/null)/bin
LSAN_SUITES ?=
test-lsan:
@echo "=== macOS leak lane: $(shell $(LSAN_LLVM_BIN)/clang --version | head -1) ==="
$(MAKE) -f Makefile.cbm build/lsan/test-runner \
CC=$(LSAN_LLVM_BIN)/clang CXX=$(LSAN_LLVM_BIN)/clang++ BUILD_DIR=build/lsan
cd $(CURDIR) && ASAN_OPTIONS="detect_leaks=1:halt_on_error=1" \
./build/lsan/test-runner $(LSAN_SUITES)
# Path-sensitive memory analysis: leak paths, null derefs, uninitialized reads.
# Non-gating: triage findings against the code (this lane's first run produced
# 9 real fixes and 6 recorded false positives — see the analyzer batch commit).
LINT_MEM_CHECKS = -*,clang-analyzer-unix.Malloc,clang-analyzer-unix.MallocSizeof,clang-analyzer-core.NullDereference,clang-analyzer-core.CallAndMessage,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.NewDelete,clang-analyzer-cplusplus.NewDeleteLeaks
lint-mem:
@echo "=== clang-analyzer memory lane (non-gating; triage, don't suppress) ==="
@printf '%s\n' $(LINT_SRCS) | xargs -P 10 -I{} $(CLANG_TIDY) --quiet \
--checks='$(LINT_MEM_CHECKS)' {} -- $(CFLAGS_COMMON) $(SYSROOT_FLAG) 2>/dev/null; true
# Gating variant: identical run, but vendored-tree diagnostics are excluded
# (analyzer diagnostics bypass --header-filter by design; this mirrors the
# .cppcheck vendored suppression) and any finding the gate cannot account for
# fails the target. A finding here is a leak path, null deref, or uninitialized
# read the analyzer can prove — triage it against the code.
#
# A finding is accounted for by being FIXED, or — only where the analyzer is
# genuinely wrong — by an argued entry in scripts/lint-mem-whitelist.txt. Each
# entry is pinned to the sha256 of the function it argues about, so editing
# that function invalidates it and the finding must be argued again against
# the new code. A suppression is never allowed to outlive its reasoning, and
# NOLINT is never honoured here.
lint-mem-ci:
@echo "=== clang-analyzer memory gate ==="
@printf '%s\n' $(LINT_SRCS) | xargs -P 4 -I{} $(CLANG_TIDY) --quiet \
--checks='$(LINT_MEM_CHECKS)' {} -- $(CFLAGS_COMMON) $(SYSROOT_FLAG) 2>/dev/null \
| python3 scripts/lint-mem-gate.py
# ── Security audit (6 layers) ────────────────────────────────────
# Run all security checks: static audit, binary strings, UI, install, network
# Requires: production binary already built (make cbm)
security: cbm
@echo "=== Running security audit suite ==="
scripts/security-audit.sh
scripts/security-strings.sh $(BUILD_DIR)/codebase-memory-mcp
scripts/security-ui.sh
scripts/security-install.sh $(BUILD_DIR)/codebase-memory-mcp
scripts/security-network.sh $(BUILD_DIR)/codebase-memory-mcp
scripts/security-fuzz.sh $(BUILD_DIR)/codebase-memory-mcp
scripts/security-vendored.sh
@echo "=== All security checks passed ==="