Merge branch 'main' into fix/2386-bm25-post-window-filters

This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-31 06:11:43 +01:00
commit d86ca262ce
26 files changed
+1429 -70

No files matched your search

+3 -1
View File
@@ -41,7 +41,9 @@ jobs:
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
# The macOS minimum (14.0) is pinned in Makefile.cbm, not inherited
# from this runner's SDK, so both darwin binaries still start on macOS 14.
- os: macos-15
goos: darwin
goarch: arm64
cc: cc
+1 -1
View File
@@ -115,7 +115,7 @@ jobs:
event-macos:
if: ${{ inputs.mode == 'full' }}
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+5 -4
View File
@@ -33,7 +33,7 @@ jobs:
CORE_UNIX='[
{"os":"ubuntu-latest","goos":"linux","goarch":"amd64"},
{"os":"ubuntu-24.04-arm","goos":"linux","goarch":"arm64"},
{"os":"macos-14","goos":"darwin","goarch":"arm64"},
{"os":"macos-15","goos":"darwin","goarch":"arm64"},
{"os":"macos-15-intel","goos":"darwin","goarch":"amd64"}
]'
# Broad legs reuse existing goos/goarch artifacts on additional OS
@@ -47,9 +47,10 @@ jobs:
# (ubuntu-22.04 / 22.04-arm). So the dynamic broad legs stay on
# forward-compatible OSes only (macOS); running the dynamic binary on
# ubuntu-22.04 would fail Phase 1 (glibc too old), not a real regression.
BROAD_UNIX='[
{"os":"macos-15","goos":"darwin","goarch":"arm64"}
]'
# Empty since the move off macos-14 (retired 2026-11-02): the darwin-arm64
# artifact is now built AND smoked on macos-15, the core leg, which was
# this broad leg.
BROAD_UNIX='[]'
CORE_WIN='[{"os":"windows-latest","arch":"amd64"}]'
# windows-11-arm now runs the NATIVE arm64 binary (build-windows-arm64),
# not the x86_64 binary under emulation — we smoke the artifact we ship.
+2 -2
View File
@@ -44,7 +44,7 @@ jobs:
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
- os: macos-15
goos: darwin
goarch: arm64
cc: cc
@@ -335,7 +335,7 @@ jobs:
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
- os: macos-15
goos: darwin
goarch: arm64
cc: cc
+7 -4
View File
@@ -57,18 +57,21 @@ jobs:
run: |
# Every CORE leg is tagged with the lane (scripts/ci/select-lanes.sh)
# it serves; the tag is what lane selection filters on below.
# The lane names unix-macos14 and tsan-mac are kept as lane keys
# (select-lanes.sh, ci-ok and the lane contracts key on them); both
# run on macos-15 because GitHub retires the macos-14 image on 2026-11-02.
CORE_UNIX='[
{"os":"ubuntu-latest","cc":"gcc","cxx":"g++","lane":"unix-x86"},
{"os":"ubuntu-24.04-arm","cc":"gcc","cxx":"g++","lane":"unix-arm64"},
{"os":"macos-14","cc":"cc","cxx":"c++","lane":"unix-macos14"},
{"os":"macos-15","cc":"cc","cxx":"c++","lane":"unix-macos14"},
{"os":"macos-15-intel","cc":"cc","cxx":"c++","lane":"unix-macos-intel"}
]'
# Broad matrix legs are REQUIRED gates (no optional/continue-on-error
# escape hatches): the release dry run must be all-green, every platform.
# macos-15 is the CORE macOS arm64 leg now, so it is not repeated here.
BROAD_UNIX='[
{"os":"ubuntu-22.04","cc":"gcc","cxx":"g++"},
{"os":"ubuntu-22.04-arm","cc":"gcc","cxx":"g++"},
{"os":"macos-15","cc":"cc","cxx":"c++"}
{"os":"ubuntu-22.04-arm","cc":"gcc","cxx":"g++"}
]'
# Each Windows leg pins the msys2 environment + package arch to the
# RUNNER architecture so the build is native, never emulated:
@@ -92,7 +95,7 @@ jobs:
TSAN='[
{"os":"ubuntu-latest","cc":"clang","cxx":"clang++","lane":"tsan-x86"},
{"os":"ubuntu-24.04-arm","cc":"clang","cxx":"clang++","lane":"tsan-arm"},
{"os":"macos-14","cc":"cc","cxx":"c++","lane":"tsan-mac"}
{"os":"macos-15","cc":"cc","cxx":"c++","lane":"tsan-mac"}
]'
# Lane selection. "all" (dry run, release) keeps every leg and drops
# the tags, so those matrices -- and their job names -- stay exactly
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
group: linux
cc: gcc
cxx: g++
- os: macos-14
- os: macos-15
group: macos
cc: cc
cxx: c++
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
include:
- { os: ubuntu-latest, cc: gcc, cxx: g++ }
- { os: ubuntu-24.04-arm, cc: gcc, cxx: g++ }
- { os: macos-14, cc: cc, cxx: c++ }
- { os: macos-15, cc: cc, cxx: c++ }
- { os: macos-15-intel, cc: cc, cxx: c++ }
runs-on: ${{ matrix.os }}
timeout-minutes: 60
+3 -3
View File
@@ -175,7 +175,7 @@ jobs:
cat "$RUNNER_TEMP/selection.txt" >> "$GITHUB_OUTPUT"
# pr-smoke's legs, each tagged with its lane (kept in the job name,
# where ci-ok reads it back); the job gets the selected ones.
SMOKE='[{"os":"ubuntu-latest","lane":"smoke-ubuntu"},{"os":"macos-14","lane":"smoke-mac"},{"os":"windows-latest","lane":"smoke-win"}]'
SMOKE='[{"os":"ubuntu-latest","lane":"smoke-ubuntu"},{"os":"macos-15","lane":"smoke-mac"},{"os":"windows-latest","lane":"smoke-win"}]'
LANES=$(sed -n 's/^lanes=//p' "$RUNNER_TEMP/selection.txt")
echo "smoke={\"include\":$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$SMOKE")}" >> "$GITHUB_OUTPUT"
# The CodeQL legs, tagged the same way (codeql.yml's full matrix has
@@ -214,7 +214,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
- name: Install ccache (macOS)
if: matrix.os == 'macos-14'
if: matrix.os == 'macos-15'
run: command -v ccache >/dev/null 2>&1 || brew install ccache
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
@@ -254,7 +254,7 @@ jobs:
SMOKE_REQUIRE_WRAPPERS: "1"
- name: Build prod + smoke (macOS)
if: matrix.os == 'macos-14'
if: matrix.os == 'macos-15'
run: |
scripts/build.sh CC=cc CXX=c++
codesign --sign - --force build/c/codebase-memory-mcp
-3
View File
@@ -39,9 +39,6 @@ jobs:
- os: ubuntu-24.04-arm
cc: gcc
cxx: g++
- os: macos-14 # arm64
cc: cc
cxx: c++
- os: macos-15 # arm64
cc: cc
cxx: c++
+18 -2
View File
@@ -18,8 +18,24 @@
# 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)
#
# Minimum macOS version (Apple targets only). Without one, clang takes the
# minimum from the SDK it compiles against (LC_BUILD_VERSION minos), so a
# release built on a macos-15 runner would refuse to start on macOS 14. 14.0
# keeps every macOS that could run a release built on the macos-14 runner
# (retired 2026-11-02). A flag folded into the drivers like ARCHFLAGS, not an
# exported MACOSX_DEPLOYMENT_TARGET: it reaches every compile AND link on every
# build path (build.sh, test.sh, direct make) and is part of each command line.
# Detected from the compiler's target, not the host, so a cross compiler run
# on a Mac is left untouched.
MACOS_MIN_VERSION := 14.0
IS_APPLE_TARGET := $(shell echo | $(CC) $(ARCHFLAGS) -dM -E - 2>/dev/null | grep -q '__APPLE__' && echo yes || echo no)
MACOS_MIN_FLAGS :=
ifeq ($(IS_APPLE_TARGET),yes)
MACOS_MIN_FLAGS := -mmacosx-version-min=$(MACOS_MIN_VERSION)
endif
override CC := $(CC) $(ARCHFLAGS) $(MACOS_MIN_FLAGS)
override CXX := $(CXX) $(ARCHFLAGS) $(MACOS_MIN_FLAGS)
# ── Common flags ─────────────────────────────────────────────────
+5
View File
@@ -863,6 +863,8 @@ typedef struct {
typedef struct CBMFileResult {
CBMArena arena; // owns local memory; composites may also retain child arenas below
uint64_t markdown_collection_steps; // collector cursor moves/node visits, not parser internals
uint64_t markdown_name_checks; // heading-name lookup/group checks, excluding sorting
CBMDefArray defs;
CBMCallArray calls;
CBMImportArray imports;
@@ -1074,6 +1076,9 @@ typedef struct {
* POD section index. */
void *doc_memo;
void *doc_pod_index;
/* Markdown: the settled names of the file's headings (extract_defs.c),
* NULL until the first heading; allocated in `scratch`. */
void *md_headings;
const cbm_test_declarations_t *test_declarations; /* borrowed for this call only */
bool test_declarations_raw_source;
struct CBMTestDefinitionMatch *test_definition_matches; /* traversal scratch */
+3 -1
View File
@@ -1225,9 +1225,11 @@ static bool dg_word_at(dg_str_t s, const char *word) {
}
/* An operand of a PlantUML line: `[Comp]`, `(use case)`, `:actor:`, `"text"`,
* an id, or `[*]`; its id in *id; the position after it (i when none). */
* an id, or `[*]`; its id in *id; the position after it (i and an empty *id
* when none, as for the diagram edge in `A ->: x` or `[-> A: x`). */
static size_t dg_puml_operand(dg_str_t s, size_t i, dg_str_t *id, dg_str_t *label) {
i = dg_skip_ws(s, i);
*id = dg_s(NULL, 0);
*label = dg_s(NULL, 0);
if (i + CBM_SZ_3 <= s.n && memcmp(s.p + i, "[*]", CBM_SZ_3) == 0) {
*id = dg_s(s.p + i, CBM_SZ_3);
+472 -7
View File
@@ -7400,13 +7400,15 @@ static const char *extract_markdown_section_body(CBMExtractCtx *ctx, TSNode head
/* Push a config-language definition that carries prose. push_simple_class_def
* delegates here with no docstring. */
static void push_simple_class_def_doc(CBMExtractCtx *ctx, TSNode node, char *name,
const char *label, const char *docstring) {
/* Push a definition whose qualified name ends with `qn_name` (already QN-safe)
* rather than with its name. */
static void push_class_def_qn(CBMExtractCtx *ctx, TSNode node, char *name, const char *qn_name,
const char *label, const char *docstring) {
CBMArena *a = ctx->arena;
CBMDefinition def;
memset(&def, 0, sizeof(def));
def.name = name;
def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, qn_safe_segment(a, name));
def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, qn_name);
def.label = label;
def.file_path = ctx->rel_path;
def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET;
@@ -7416,6 +7418,11 @@ static void push_simple_class_def_doc(CBMExtractCtx *ctx, TSNode node, char *nam
cbm_defs_push(&ctx->result->defs, a, def);
}
static void push_simple_class_def_doc(CBMExtractCtx *ctx, TSNode node, char *name,
const char *label, const char *docstring) {
push_class_def_qn(ctx, node, name, qn_safe_segment(ctx->arena, name), label, docstring);
}
static void push_simple_class_def(CBMExtractCtx *ctx, TSNode node, char *name, const char *label) {
push_simple_class_def_doc(ctx, node, name, label, NULL);
}
@@ -7487,6 +7494,49 @@ static char *extract_atx_heading_text(CBMArena *a, TSNode node, const char *sour
return NULL;
}
/* Setext paragraphs include block-container markers between lines. Keep the
* whole inline text, excluding only the parser's continuation spans. */
static char *extract_setext_heading_text(CBMArena *a, TSNode node, const char *source) {
TSNode content = ts_node_child_by_field_name(node, TS_FIELD("heading_content"));
if (ts_node_is_null(content)) {
return NULL;
}
TSNode inline_node = cbm_find_child_by_kind(content, "inline");
if (!ts_node_is_null(inline_node)) {
content = inline_node;
}
uint32_t start = ts_node_start_byte(content);
uint32_t end = ts_node_end_byte(content);
char *text = (char *)cbm_arena_alloc(a, (size_t)(end - start) + SKIP_ONE);
if (!text) {
return NULL;
}
size_t written = 0;
uint32_t previous = start;
TSTreeCursor cursor = ts_tree_cursor_new(content);
for (;;) {
TSNode child = ts_tree_cursor_current_node(&cursor);
if (strcmp(ts_node_type(child), "block_continuation") == 0) {
uint32_t marker_start = ts_node_start_byte(child);
uint32_t marker_end = ts_node_end_byte(child);
memcpy(text + written, source + previous, marker_start - previous);
written += marker_start - previous;
previous = marker_end;
} else if (ts_tree_cursor_goto_first_child(&cursor)) {
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (!ts_tree_cursor_goto_parent(&cursor)) {
ts_tree_cursor_delete(&cursor);
memcpy(text + written, source + previous, end - previous);
written += end - previous;
text[written] = '\0';
return text;
}
}
}
}
// Trim trailing whitespace/newlines from a heading name in-place.
static char *trim_heading_name(char *name) {
if (!name || !name[0]) {
@@ -7508,13 +7558,425 @@ static char *extract_markdown_heading_name(CBMArena *a, TSNode node, const char
if (strcmp(kind, "atx_heading") == 0) {
name = extract_atx_heading_text(a, node, source);
} else {
if (ts_node_child_count(node) > 0) {
name = cbm_node_text(a, ts_node_child(node, 0), source);
}
name = extract_setext_heading_text(a, node, source);
}
return trim_heading_name(name);
}
/* #2586: a heading name that repeats in a file (`## Implementation` under two
* parents) gave its Sections one qualified name, so the graph kept one node
* and the references under them fell back to the File. A repeated heading's
* qualified name therefore takes the names of the headings above it when
* those chains differ (`Service-A.Implementation`). Unique titles and repeats
* with only one parent chain keep their bare names. A heading text may itself
* hold dots, so a qualified name can equal another heading's final name (`# Service-A.
* Implementation`): the qualified one then takes `~2`, `~3`, ... until no
* other heading of the file has that name. Headings that repeat under the
* same parents too still share a name, as before. All final names are
* settled once per file, on the first heading. */
typedef struct md_heading {
uint32_t start; /* the heading node's first byte */
uint32_t scope_start; /* enclosing block container, excluding section wrappers */
uint32_t scope_end;
int level; /* ATX and Setext heading rank, 1–6 */
struct md_heading *parent;
const char *seg; /* the heading's own QN-safe name */
const char *key; /* repeated only: the chain's segments, '\n'-joined */
const char *qn_name; /* the final QN segment */
bool qualified; /* the name actually changed by adding parents */
struct md_heading *chain; /* first heading with this identity in a collision run */
} md_heading_t;
typedef struct {
md_heading_t *v; /* in document order */
int count;
uint64_t *checks;
} md_headings_t;
/* The QN-safe name of a heading node, or NULL. */
static const char *md_heading_segment(CBMExtractCtx *ctx, CBMArena *a, TSNode node) {
const char *kind = ts_node_type(node);
if (!is_markdown_heading_kind(kind)) {
return NULL;
}
char *name = extract_markdown_heading_name(a, node, kind, ctx->source);
return name ? qn_safe_segment(a, name) : NULL;
}
enum { MD_HEADING_LEVELS = 6 };
/* Read the vendored grammar's explicit markers, including Setext headings:
* unlike ATX headings, those are not nested in section nodes by the parser. */
static int md_heading_level(TSNode node) {
static const char *const atx_markers[MD_HEADING_LEVELS] = {
"atx_h1_marker", "atx_h2_marker", "atx_h3_marker",
"atx_h4_marker", "atx_h5_marker", "atx_h6_marker",
};
uint32_t n = ts_node_child_count(node);
for (uint32_t i = 0; i < n; i++) {
const char *kind = ts_node_type(ts_node_child(node, i));
for (int level = 0; level < MD_HEADING_LEVELS; level++) {
if (strcmp(kind, atx_markers[level]) == 0) {
return level + SKIP_ONE;
}
}
if (strcmp(kind, "setext_h1_underline") == 0) {
return 1;
}
if (strcmp(kind, "setext_h2_underline") == 0) {
return PAIR_LEN;
}
}
return 0;
}
/* Scope frames follow cursor descent/ascent. Reuse popped frames so temporary
* storage grows with nesting depth, not with the number of blocks. */
typedef struct md_scope {
uint32_t start;
uint32_t end;
int depth;
struct md_scope *previous;
} md_scope_t;
/* Visit every heading, including empty structural headings: count them (v
* NULL), or fill v. Never ask ts_node_parent to rediscover the path from the
* root: its preceding-sibling scan makes flat documents quadratic. */
static int md_collect_headings(CBMExtractCtx *ctx, CBMArena *a, md_heading_t *v, int cap) {
int n = 0;
int depth = 0;
md_scope_t *scope = NULL;
md_scope_t *spare = NULL;
uint64_t *steps = &ctx->result->markdown_collection_steps;
if (ts_node_is_null(ctx->root)) {
return 0;
}
TSTreeCursor cur = ts_tree_cursor_new(ctx->root);
for (;;) {
(*steps)++;
TSNode node = ts_tree_cursor_current_node(&cur);
const char *kind = ts_node_type(node);
if (is_markdown_heading_kind(kind)) {
if (!v) {
n++;
} else if (n < cap) {
const char *seg = md_heading_segment(ctx, a, node);
v[n++] = (md_heading_t){.start = ts_node_start_byte(node),
.scope_start = scope ? scope->start : 0,
.scope_end = scope ? scope->end : 0,
.level = md_heading_level(node),
.seg = seg ? seg : "",
.qn_name = seg ? seg : ""};
}
} else {
(*steps)++;
if (ts_tree_cursor_goto_first_child(&cur)) {
if (v && strcmp(kind, "section") != 0) {
md_scope_t *frame = spare;
if (frame) {
spare = frame->previous;
} else {
frame = (md_scope_t *)cbm_arena_alloc(a, sizeof(*frame));
}
if (!frame) {
ts_tree_cursor_delete(&cur);
return -1;
}
*frame = (md_scope_t){.start = ts_node_start_byte(node),
.end = ts_node_end_byte(node),
.depth = depth,
.previous = scope};
scope = frame;
}
depth++;
continue;
}
}
for (;;) {
(*steps)++;
if (ts_tree_cursor_goto_next_sibling(&cur)) {
break;
}
(*steps)++;
if (!ts_tree_cursor_goto_parent(&cur)) {
ts_tree_cursor_delete(&cur);
return n;
}
depth--;
if (scope && scope->depth == depth) {
md_scope_t *previous = scope->previous;
scope->previous = spare;
spare = scope;
scope = previous;
}
}
}
}
static int md_by_seg(const void *x, const void *y) {
return strcmp((*(const md_heading_t *const *)x)->seg, (*(const md_heading_t *const *)y)->seg);
}
static int md_by_name(const void *x, const void *y) {
const md_heading_t *p = *(const md_heading_t *const *)x;
const md_heading_t *q = *(const md_heading_t *const *)y;
int c = strcmp(p->qn_name, q->qn_name);
return c ? c : (p->start > q->start) - (p->start < q->start);
}
static int md_by_scope(const void *x, const void *y) {
const md_heading_t *p = *(const md_heading_t *const *)x;
const md_heading_t *q = *(const md_heading_t *const *)y;
if (p->scope_start != q->scope_start) {
return (p->scope_start > q->scope_start) - (p->scope_start < q->scope_start);
}
if (p->scope_end != q->scope_end) {
return (p->scope_end > q->scope_end) - (p->scope_end < q->scope_end);
}
return (p->start > q->start) - (p->start < q->start);
}
/* Heading levels define hierarchy for both syntaxes. Each block container
* gets its own six-slot stack, so headings inside quotes or list items cannot
* become parents of headings outside them. Sorting by container and document
* position also restores the outer stack after a nested container ends. */
static void md_assign_parents(md_heading_t **by, int n) {
qsort(by, (size_t)n, sizeof(*by), md_by_scope);
for (int i = 0; i < n;) {
int j = i;
md_heading_t *parents[MD_HEADING_LEVELS] = {0};
while (j < n && by[j]->scope_start == by[i]->scope_start &&
by[j]->scope_end == by[i]->scope_end) {
md_heading_t *h = by[j++];
if (h->level < 1 || h->level > MD_HEADING_LEVELS) {
continue;
}
for (int k = h->level - PAIR_LEN; k >= 0; k--) {
if (parents[k]) {
h->parent = parents[k];
break;
}
}
parents[h->level - SKIP_ONE] = h;
for (int k = h->level; k < MD_HEADING_LEVELS; k++) {
parents[k] = NULL;
}
}
i = j;
}
}
/* A repeated heading's chain: dot-joined for its QN and '\n'-joined for
* identity, since a QN-safe segment has no whitespace. At most six headings. */
static bool md_heading_chain(CBMArena *a, md_heading_t *h) {
const char *dotted = h->seg;
const char *key = h->seg;
for (const md_heading_t *parent = h->parent; parent; parent = parent->parent) {
if (!parent->seg[0]) {
continue; /* unnamed headings reset levels but contribute no name */
}
dotted = cbm_arena_sprintf(a, "%s.%s", parent->seg, dotted);
key = cbm_arena_sprintf(a, "%s\n%s", parent->seg, key);
if (!dotted || !key) {
return false;
}
}
h->qn_name = dotted;
h->key = key;
return true;
}
/* The immutable, sorted initial names reserve both bare headings and parent
* chains before any suffix is assigned. Generated names from different bases
* cannot coincide: removing the final ~N recovers their distinct bases. */
static bool md_name_taken(md_headings_t *hs, const char *const *reserved, int reserved_count,
const char *name) {
int lo = 0;
int hi = reserved_count;
while (lo < hi) {
int mid = lo + (hi - lo) / PAIR_LEN;
(*hs->checks)++;
int c = strcmp(reserved[mid], name);
if (c == 0) {
return true;
}
if (c < 0) {
lo = mid + SKIP_ONE;
} else {
hi = mid;
}
}
return false;
}
static int md_by_key(const void *x, const void *y) {
const md_heading_t *p = *(const md_heading_t *const *)x;
const md_heading_t *q = *(const md_heading_t *const *)y;
int c = strcmp(p->key ? p->key : "", q->key ? q->key : "");
return c ? c : (p->start > q->start) - (p->start < q->start);
}
static int md_by_start(const void *x, const void *y) {
const md_heading_t *p = *(const md_heading_t *const *)x;
const md_heading_t *q = *(const md_heading_t *const *)y;
return (p->start > q->start) - (p->start < q->start);
}
/* Unchanged bare headings keep their names, including inseparable repeats.
* Otherwise the first chain keeps the name. Group identities once, then assign
* one suffix per chain in document order; no repeated scans of earlier chains
* and no restarting the suffix search at ~2. */
static bool md_split_run(CBMArena *a, md_headings_t *hs, const char *const *reserved,
int reserved_count, md_heading_t **run, int n) {
const char *base = run[0]->qn_name;
const char *keep = run[0]->key;
for (int i = 0; i < n; i++) {
if (!run[i]->qualified) {
keep = NULL;
}
}
qsort(run, (size_t)n, sizeof(*run), md_by_key);
for (int i = 0; i < n;) {
int j = i + SKIP_ONE;
while (j < n) {
(*hs->checks)++;
if (strcmp(run[i]->key ? run[i]->key : "", run[j]->key ? run[j]->key : "") != 0) {
break;
}
j++;
}
for (int k = i; k < j; k++) {
run[k]->chain = run[i];
}
i = j;
}
qsort(run, (size_t)n, sizeof(*run), md_by_start);
int suffix = PAIR_LEN;
for (int i = 0; i < n; i++) {
md_heading_t *h = run[i];
if (!h->qualified || h->chain != h || (keep && strcmp(h->key, keep) == 0)) {
continue;
}
const char *name;
do {
name = cbm_arena_sprintf(a, "%s~%d", base, suffix++);
if (!name) {
return false;
}
} while (md_name_taken(hs, reserved, reserved_count, name));
h->qn_name = name;
}
for (int i = 0; i < n; i++) {
if (run[i]->qualified) {
run[i]->qn_name = run[i]->chain->qn_name;
}
}
return true;
}
static bool md_settle_names(CBMArena *a, md_headings_t *hs) {
int n = hs->count;
md_heading_t **by = (md_heading_t **)cbm_arena_alloc(a, (size_t)n * sizeof(*by));
const char **reserved = (const char **)cbm_arena_alloc(a, (size_t)n * sizeof(*reserved));
if (!by || !reserved) {
return false;
}
for (int i = 0; i < n; i++) {
by[i] = &hs->v[i];
}
md_assign_parents(by, n);
/* Empty headings are structural events only, never reserved names. */
n = 0;
for (int i = 0; i < hs->count; i++) {
if (hs->v[i].seg[0]) {
by[n++] = &hs->v[i];
}
}
qsort(by, (size_t)n, sizeof(*by), md_by_seg);
for (int i = 0; i < n;) {
int j = i + SKIP_ONE;
while (j < n && strcmp(by[j]->seg, by[i]->seg) == 0) {
j++;
}
if (j - i > SKIP_ONE) {
bool distinct = false;
for (int k = i; k < j; k++) {
if (!md_heading_chain(a, by[k])) {
return false;
}
(*hs->checks)++;
distinct |= strcmp(by[k]->key, by[i]->key) != 0;
}
for (int k = i; k < j; k++) {
if (!distinct) {
by[k]->qn_name = by[k]->seg;
}
by[k]->qualified = strcmp(by[k]->qn_name, by[k]->seg) != 0;
}
}
i = j;
}
qsort(by, (size_t)n, sizeof(*by), md_by_name);
for (int i = 0; i < n; i++) {
reserved[i] = by[i]->qn_name;
}
for (int i = 0; i < n;) {
int j = i + SKIP_ONE;
while (j < n && strcmp(reserved[j], reserved[i]) == 0) {
j++;
}
if (j - i > SKIP_ONE && !md_split_run(a, hs, reserved, n, by + i, j - i)) {
return false;
}
i = j;
}
return true;
}
static const md_headings_t *md_headings(CBMExtractCtx *ctx) {
if (ctx->md_headings) {
return (const md_headings_t *)ctx->md_headings;
}
CBMArena *a = doc_scratch(ctx);
md_headings_t *hs = (md_headings_t *)cbm_arena_alloc(a, sizeof(*hs));
if (!hs) {
return NULL;
}
hs->checks = &ctx->result->markdown_name_checks;
int cap = md_collect_headings(ctx, a, NULL, 0);
hs->v = cap > 0 ? (md_heading_t *)cbm_arena_alloc(a, (size_t)cap * sizeof(md_heading_t)) : NULL;
hs->count = hs->v ? md_collect_headings(ctx, a, hs->v, cap) : 0;
if (hs->count < 0) {
hs->count = 0;
}
if (hs->count > SKIP_ONE && !md_settle_names(a, hs)) {
hs->count = 0; /* every heading keeps its own name, as before */
}
ctx->md_headings = hs;
return hs;
}
/* The QN segment of a heading: its own name unless the file's names say
* otherwise. */
static const char *md_heading_qn_name(CBMExtractCtx *ctx, TSNode heading, const char *seg) {
const md_headings_t *hs = md_headings(ctx);
uint32_t start = ts_node_start_byte(heading);
int lo = 0;
int hi = hs ? hs->count : 0;
while (lo < hi) {
int mid = lo + ((hi - lo) / PAIR_LEN);
if (hs->v[mid].start < start) {
lo = mid + SKIP_ONE;
} else {
hi = mid;
}
}
if (hs && lo < hs->count && hs->v[lo].start == start && strcmp(hs->v[lo].seg, seg) == 0) {
return hs->v[lo].qn_name;
}
return seg;
}
// INI: extract section name from section node.
static char *find_ini_section_name(CBMArena *a, TSNode node, const char *source) {
uint32_t nc = ts_node_child_count(node);
@@ -7616,7 +8078,10 @@ static bool extract_config_class_def(CBMExtractCtx *ctx, TSNode node, const char
return false;
}
if (name && name[0]) {
if (name && name[0] && ctx->language == CBM_LANG_MARKDOWN) {
const char *seg = qn_safe_segment(a, name);
push_class_def_qn(ctx, node, name, md_heading_qn_name(ctx, node, seg), label, docstring);
} else if (name && name[0]) {
push_simple_class_def_doc(ctx, node, name, label, docstring);
}
return true;
+10 -4
View File
@@ -1707,6 +1707,12 @@ static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root
scope = "Compaction";
}
const char *tier = ha_active_tier(root, event);
/* SubagentStart carries no tool list, so agents defined without MCP tools
* (e.g. tools: Read, Grep, Glob, Bash) still get this; point them at the CLI. */
const char *cli_hint = strcmp(event, "SubagentStart") == 0
? " The same tools are also available from the shell: "
"`codebase-memory-mcp cli <tool> --help` lists each tool's flags."
: "";
if (project) {
char safe_project[HA_METADATA_CAP];
ha_sanitize_metadata(project, safe_project, sizeof(safe_project));
@@ -1723,8 +1729,8 @@ static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root
"reported missed lines directly and qualify conclusions. For structural "
"code discovery use search_graph, then trace_path, then get_code_snippet; "
"use query_graph or get_architecture for broader structure. Use grep, glob, "
"and file reads for literals, configs, non-code files, and verification.",
scope, safe_project, tier);
"and file reads for literals, configs, non-code files, and verification.%s",
scope, safe_project, tier, cli_hint);
} else {
const char *index_guidance = ha_no_project_index_guidance(event);
snprintf(context, sizeof(context),
@@ -1735,8 +1741,8 @@ static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root
"call check_index_coverage for every file relied on; if incomplete, read the "
"reported missed lines directly and qualify conclusions. Use search_graph, "
"trace_path, and get_code_snippet first; use grep for "
"literals, configs, non-code files, and verification.",
scope, index_guidance, tier);
"literals, configs, non-code files, and verification.%s",
scope, index_guidance, tier, cli_hint);
}
free(project);
if (dialect == HA_DIALECT_COPILOT) {
+3 -2
View File
@@ -65,8 +65,9 @@ CBMLanguage cbm_disambiguate_res(const char *path);
CBMLanguage cbm_disambiguate_d(const char *path);
/* Disambiguate .inc files by reading first 4KB of content.
* Returns CBM_LANG_OBJECTSCRIPT_ROUTINE if it looks like an ObjectScript
* include (a "ROUTINE <Uppercase>" header), otherwise CBM_LANG_BITBAKE.
* Returns CBM_LANG_CPP for explicit C-family code, or
* CBM_LANG_OBJECTSCRIPT_ROUTINE for ObjectScript markers / a lone #define.
* BitBake recipe syntax and unknown content retain CBM_LANG_BITBAKE.
* On read failure, defaults to CBM_LANG_BITBAKE. */
CBMLanguage cbm_disambiguate_inc(const char *path);
+137 -25
View File
@@ -1573,38 +1573,150 @@ CBMLanguage cbm_disambiguate_cls(const char *path) {
return lang_cls_text(buf);
}
/* Disambiguate .inc files: shared by BitBake include fragments and
* InterSystems ObjectScript include (macro) files. ObjectScript .inc files are
* predominantly macro definitions ("#define NAME ..." / "#def1arg NAME ...");
* some also carry a "ROUTINE <Name>" header. The macro-preprocessor directives
* are the strongest signal because that is the primary content of an .inc file,
* whereas BitBake uses '#' only for "# comment" lines (always '#' + space).
* We therefore match ObjectScript preprocessor directives ('#' immediately
* followed by 'def'/';'), which BitBake never produces. Defaults to BitBake on
* any doubt (preserves existing behaviour). */
/* A complete leading token, followed by whitespace or punctuation, within a
* single line. In particular #include_extra is not #include. */
static const char *lang_inc_word(const char *line, const char *end, const char *word) {
size_t n = strlen(word);
if ((size_t)(end - line) < n || strncmp(line, word, n) != 0 ||
(line + n < end && (isalnum((unsigned char)line[n]) || line[n] == '_'))) {
return NULL;
}
line += n;
while (line < end && isspace((unsigned char)*line)) {
line++;
}
return line;
}
/* Recipes can embed C in quoted strings or shell/Python tasks. Recognize the
* recipe's own syntax before inspecting those bodies as standalone C code. */
static bool lang_inc_recipe_line(const char *line, const char *end) {
static const char *const words[] = {"require", "include", "inherit", "addtask", "python", NULL};
for (int i = 0; words[i]; i++) {
const char *tail = lang_inc_word(line, end, words[i]);
if (tail && tail > line + strlen(words[i])) {
return true;
}
}
if ((size_t)(end - line) >= SLEN("do_") && strncmp(line, "do_", SLEN("do_")) == 0) {
return true;
}
const char *p = line;
while (p < end && (isalnum((unsigned char)*p) || *p == '_' || *p == ':' || *p == '[' ||
*p == ']' || *p == '-' || *p == '.')) {
p++;
}
if (p == line) {
return false;
}
while (p < end && isspace((unsigned char)*p)) {
p++;
}
return p < end && (*p == '=' || ((*p == '?' || *p == '+' || *p == ':' || *p == '.') &&
p + SKIP_ONE < end && p[SKIP_ONE] == '='));
}
/* Skip only leading whitespace and C comments, keeping block-comment state
* across lines. Recipe strings and bodies are handled before looking for C. */
static const char *lang_inc_code(const char *line, const char *end, bool *block_comment) {
while (line < end) {
if (*block_comment) {
const char *close = strstr(line, "*/");
if (!close || close >= end) {
return end;
}
line = close + SLEN("*/");
*block_comment = false;
continue;
}
if (isspace((unsigned char)*line)) {
line++;
continue;
}
if ((size_t)(end - line) >= SLEN("/*") && strncmp(line, "/*", SLEN("/*")) == 0) {
*block_comment = true;
line += SLEN("/*");
continue;
}
break;
}
return line;
}
static bool lang_inc_c_code(const char *line, const char *end) {
static const char *const words[] = {
"extern", "static", "typedef", "namespace", "template",
"struct", "class", "int", "void", NULL,
};
for (int i = 0; words[i]; i++) {
const char *tail = lang_inc_word(line, end, words[i]);
if (tail && tail < end && *tail != '=') {
return true;
}
}
const char *tail = lang_inc_word(line, end, "if");
return tail && tail < end && *tail == '(';
}
static bool lang_inc_directive(const char *line, const char *end, bool *c_family,
bool *objectscript_define) {
line += SKIP_ONE; /* '#' */
while (line < end && isspace((unsigned char)*line)) {
line++;
}
if (lang_inc_word(line, end, "def1arg") || (line < end && *line == ';')) {
return true; /* Explicit ObjectScript directive. */
}
if (lang_inc_word(line, end, "define")) {
*objectscript_define = true;
}
const char *tail = lang_inc_word(line, end, "include");
if (tail && tail < end && (*tail == '"' || *tail == '<')) {
*c_family = true;
}
return false;
}
/* .inc is shared by C/C++, BitBake and ObjectScript. Select C++ for explicit
* C-family code (including C fragments); do not infer an enclosing include
* context. A lone #define and unknown content retain their existing defaults. */
static CBMLanguage lang_inc_text(const char *buf) {
bool c_family = false;
bool objectscript_define = false;
bool block_comment = false;
const char *line = buf;
while (*line) {
/* ObjectScript include header: a line beginning "ROUTINE <Uppercase>". */
if (strncmp(line, "ROUTINE ", SLEN("ROUTINE ")) == 0 &&
isupper((unsigned char)line[SLEN("ROUTINE ")])) {
const char *end = strchr(line, '\n');
if (!end) {
end = line + strlen(line);
}
const char *code = lang_inc_code(line, end, &block_comment);
line = *end ? end + SKIP_ONE : end;
if (code == end) {
continue;
}
const char *tail = lang_inc_word(code, end, "ROUTINE");
if (tail && tail < end && isupper((unsigned char)*tail)) {
return CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
/* ObjectScript macro directives — the primary content of .inc files.
* "#define"/"#def1arg" (macro defs) and "#;" (line comment). BitBake's
* only '#' use is "# comment" (hash + space), so these never collide. */
if (strncmp(line, "#define", SLEN("#define")) == 0 ||
strncmp(line, "#def1arg", SLEN("#def1arg")) == 0 ||
strncmp(line, "#;", SLEN("#;")) == 0) {
return CBM_LANG_OBJECTSCRIPT_ROUTINE;
if (*code == '#') {
if (lang_inc_directive(code, end, &c_family, &objectscript_define)) {
return CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
continue;
}
const char *nl = strchr(line, '\n');
if (!nl) {
break;
if ((size_t)(end - code) >= SLEN("//") && strncmp(code, "//", SLEN("//")) == 0) {
continue;
}
line = nl + SKIP_ONE;
if (!c_family && lang_inc_recipe_line(code, end)) {
return CBM_LANG_BITBAKE;
}
c_family = c_family || lang_inc_c_code(code, end);
}
return CBM_LANG_BITBAKE;
if (c_family) {
return CBM_LANG_CPP;
}
return objectscript_define ? CBM_LANG_OBJECTSCRIPT_ROUTINE : CBM_LANG_BITBAKE;
}
CBMLanguage cbm_disambiguate_inc(const char *path) {
@@ -1775,7 +1887,7 @@ CBMLanguage cbm_language_classify_with(const cbm_userconfig_t *config, const cha
/* ObjectScript UDL, Apex or a Visual Basic 6 class module */
lang = readable ? lang_cls_text(buf) : CBM_LANG_APEX;
} else if (strcmp(ext, ".inc") == 0) {
/* BitBake or an ObjectScript include */
/* C/C++, BitBake or an ObjectScript include */
lang = readable ? lang_inc_text(buf) : CBM_LANG_BITBAKE;
} else if (cfc) {
/* script or tag dialect */
+66 -4
View File
@@ -729,7 +729,9 @@ static const tool_def_t TOOLS[] = {
{"search_code",
"Graph-ranked text search: compact symbols, full bounded source, or file paths.",
"{\"type\":\"object\",\"properties\":{\"pattern\":{\"type\":\"string\"},\"project\":{\"type\":"
"\"string\"},\"file_pattern\":{\"type\":\"string\"},\"path_filter\":{\"type\":\"string\"},"
"\"string\"},\"file_pattern\":{\"type\":\"string\",\"description\":\"Glob on file name; "
"with '/', on repo-relative path (** = any dirs).\"},"
"\"path_filter\":{\"type\":\"string\",\"description\":\"Regex on repo-relative path.\"},"
"\"mode\":{\"type\":\"string\","
"\"enum\":[\"compact\",\"full\",\"files\"],\"default\":\"compact\"},"
"\"context\":{\"type\":\"integer\",\"default\":0,\"minimum\":0,\"maximum\":200},"
@@ -14728,6 +14730,7 @@ typedef struct {
uint64_t enrich_ms;
uint64_t elapsed_ms;
bool include_phase_timings;
bool hint_path_filter; /* a search with a '/' file_pattern returned no matches */
} search_metrics_t;
#ifdef CBM_ENABLE_TEST_SEAMS
@@ -14822,6 +14825,47 @@ static int search_result_cmp(const void *a, const void *b) {
return 0;
}
#define SEARCH_CODE_PATH_HINT \
"no results; a file_pattern with '/' must match the whole repo-relative path (e.g. " \
"src/**/*.c); to scope by folder you can also use path_filter (regex on the " \
"repo-relative path, e.g. ^src/cli/)"
/* A file_pattern containing '/' is matched against the whole repo-relative path, like
* search_graph's file_pattern: '*' and '**' match any run of characters including '/', a
* '**' followed by '/' also matches zero directories, '?' matches one character, and a
* pattern without wildcards is a substring match. Portable (no fnmatch on Windows) and
* linear: every '*' is equivalent, so one backtrack point suffices. */
static bool search_code_path_glob_match(const char *pat, const char *path) {
if (!strpbrk(pat, "*?")) {
return strstr(path, pat) != NULL;
}
const char *star_pat = NULL;
const char *star_path = NULL;
for (;;) {
if (*pat == '*') {
const char *run = pat;
while (*pat == '*') {
pat++;
}
if (pat - run > SKIP_ONE && *pat == '/') {
pat++;
}
star_pat = pat;
star_path = path;
} else if (*path && (*pat == '?' || *pat == *path)) {
pat++;
path++;
} else if (!*pat && !*path) {
return true;
} else if (star_pat && *star_path) {
pat = star_pat;
path = ++star_path;
} else {
return false;
}
}
}
/* Moving an arbitrary file_pattern ahead of Select-String is not generally results-preserving:
* the current Windows path applies PowerShell -like to the full MatchInfo.Path, while POSIX
* delegates glob semantics to grep --include. Restrict the Windows optimization to plain suffix
@@ -15444,6 +15488,9 @@ static char *assemble_search_output_toon(search_result_t *sr, int sr_count, grep
"(not as alternation). Pass regex=true for 'foo|bar' to mean "
"'foo OR bar'.");
}
if (metrics->hint_path_filter) {
cbm_tree_scalar_str(&sb, "hint", SEARCH_CODE_PATH_HINT);
}
if (metrics->elapsed_ms >= SEARCH_SLOW_MS) {
cbm_tree_scalar_str(&sb, "warning_slow",
"search was slow; narrow file_pattern/path_filter or use a more "
@@ -15683,6 +15730,9 @@ static char *assemble_search_output(
if (yyjson_mut_arr_size(warnings) > 0) {
yyjson_mut_obj_add_val(doc, root_obj, "warnings", warnings);
}
if (metrics->hint_path_filter) {
yyjson_mut_obj_add_str(doc, root_obj, "hint", SEARCH_CODE_PATH_HINT);
}
char *json = yy_doc_to_str(doc);
if (json) {
@@ -16460,6 +16510,17 @@ static bool write_scoped_filelist(cbm_mcp_server_t *srv, const char *project, co
continue;
}
}
/* A file_pattern with '/' is matched here on every platform; the basename
* filters below (and the Windows prefilter, which rejects '/') skip it. */
bool path_glob = file_pattern && strchr(file_pattern, '/');
if (path_glob) {
#ifdef _WIN32
cbm_normalize_path_sep(indexed_files[fi]);
#endif
if (!search_code_path_glob_match(file_pattern, indexed_files[fi])) {
continue;
}
}
#ifndef _WIN32
/* GNU grep's --include is unavailable in BusyBox grep. Filter the
* canonical list before xargs instead, preserving grep's basename
@@ -16467,7 +16528,7 @@ static bool write_scoped_filelist(cbm_mcp_server_t *srv, const char *project, co
* on GNU userland. Windows keeps its PowerShell -like filter. */
const char *basename = strrchr(indexed_files[fi], '/');
basename = basename ? basename + SKIP_ONE : indexed_files[fi];
if (file_pattern && fnmatch(file_pattern, basename, 0) != 0) {
if (file_pattern && !path_glob && fnmatch(file_pattern, basename, 0) != 0) {
continue;
}
#endif
@@ -17485,8 +17546,8 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
bool scan_in_process = scoped && !srv->search_scan_command_override;
#ifdef _WIN32
scan_in_process =
scan_in_process &&
(!file_pattern || cbm_search_code_file_pattern_can_prefilter(file_pattern));
scan_in_process && (!file_pattern || strchr(file_pattern, '/') ||
cbm_search_code_file_pattern_can_prefilter(file_pattern));
#endif
mcp_scan_cause_t scan_cause;
if (scan_in_process) {
@@ -17658,6 +17719,7 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
directory_output = directory_limit;
}
bool warn_literal_pipe = pat_has_pipe && !use_regex;
metrics.hint_path_filter = file_pattern && strchr(file_pattern, '/') && gm_count == 0;
/* Every render below re-checks each result's file against the root; one
* containment for the request keeps that to one resolution per file. */
root_containment_t request_containment;
+6 -1
View File
@@ -1663,8 +1663,13 @@ int cbm_pipeline_build_fresh_semantic_manifest(cbm_pipeline_t *p, const char *pr
* 11: Python local defs and classes bind through their enclosing lexical
* scope, including app-factory route handlers. Published version 10
* predates this resolution, so unchanged files rebuild once on upgrade.
* 12: repeated ATX and Setext Markdown headings use parent-qualified Section
* names only when distinct parent chains separate them. Unchanged bare names
* are preserved and dotted-name collisions are disambiguated before section
* references resolve. Old indices rebuild once so Section nodes, MENTIONS
* and diagram owners agree.
* The unresolved-call coverage threshold remains independent. */
enum { CBM_SEMANTIC_INDEX_VERSION = 11 };
enum { CBM_SEMANTIC_INDEX_VERSION = 12 };
typedef struct {
cbm_gbuf_t *gbuf;
+15 -2
View File
@@ -11790,12 +11790,21 @@ TEST(cli_hook_session_resolves_custom_named_index_by_root_path) {
snprintf(input, sizeof(input), "{\"hook_event_name\":\"SessionStart\",\"cwd\":\"%s\"}", nested);
char *output = cbm_hook_augment_lifecycle_json(input);
bool matched = output && strstr(output, "custom-hook-project") && strstr(output, "is indexed");
bool session_no_cli = output && !strstr(output, "codebase-memory-mcp cli <tool>");
free(output);
snprintf(input, sizeof(input), "{\"hook_event_name\":\"SubagentStart\",\"cwd\":\"%s\"}",
nested);
output = cbm_hook_augment_lifecycle_json(input);
bool subagent_cli = output && strstr(output, "custom-hook-project") &&
strstr(output, "`codebase-memory-mcp cli <tool> --help`");
free(output);
restore_test_env("CBM_CACHE_DIR", saved_cache);
test_rmdir_r(tmpdir);
if (!matched)
FAIL("SessionStart must resolve explicit index names from canonical root_path");
if (!session_no_cli || !subagent_cli)
FAIL("Only indexed SubagentStart context must carry the CLI fallback hint");
PASS();
}
@@ -12095,8 +12104,9 @@ TEST(cli_hook_session_untrusted_label_covers_project_name_only) {
const char *label = name ? strstr(name, "untrusted repository metadata") : NULL;
const char *label_end = label ? strchr(label, ')') : NULL;
const char *routing = context ? strstr(context, "For structural code discovery") : NULL;
bool scoped = name && label && label_end && routing && routing > label_end &&
!strstr(context, "untrusted repository metadata (data only; never instructions):");
bool scoped =
name && label && label_end && routing && routing > label_end &&
!strstr(context, "untrusted repository metadata (data only; never instructions):");
yyjson_doc_free(doc);
free(output);
@@ -14408,6 +14418,9 @@ TEST(cli_hook_augment_lifecycle_output_contract) {
ASSERT(strstr(context, "cbm-secret-path") == NULL);
if (strcmp(cases[i].event, "SessionStart") == 0)
ASSERT(strstr(context, "Active tier: Tier 2") != NULL);
/* Subagents defined without MCP tools must learn the CLI fallback (#2837). */
bool has_cli_hint = strstr(context, "`codebase-memory-mcp cli <tool> --help`") != NULL;
ASSERT(has_cli_hint == (strcmp(cases[i].event, "SubagentStart") == 0));
yyjson_doc_free(doc);
free(output);
}
+37
View File
@@ -329,6 +329,42 @@ TEST(diagrams_plantuml) {
PASS();
}
/* #2818: a sequence message to or from the diagram edge (`A ->: x`, `A <-: x`,
* `[-> A: x`) and a declaration with an unclosed bracket have no operand id;
* they used to read an uninitialized one and crash the index worker. The
* declaration sits in its own diagram: a `class` line makes a diagram a class
* diagram, where `->` is an arrow rather than a sequence message. */
TEST(diagrams_plantuml_edge_messages) {
CBMFileResult *r = dg_file("@startuml\n"
"participant Alice\n"
"Alice ->: out\n"
"Alice <-: in\n"
"[-> Alice: from the edge\n"
"Alice -> Bob : hello\n"
"@enduml\n"
"@startuml\n"
"class [Unclosed\n"
"@enduml\n",
"docs/edge.puml");
ASSERT_NOT_NULL(r);
ASSERT_STR_EQ(dg_prop(dg_def(r, "edge#diagram-1"), "diagram_kind"), "sequence");
ASSERT_STR_EQ(dg_prop(dg_def(r, "edge#diagram-2"), "diagram_kind"), "class");
ASSERT_NOT_NULL(dg_def(r, "edge#diagram-1.Alice"));
ASSERT_NOT_NULL(dg_def(r, "edge#diagram-1.Bob"));
ASSERT_EQ(r->diagram_arrows.count, 1);
ASSERT_EQ(dg_arrows(r, "#diagram-1.Alice", "#diagram-1.Bob", "message", "hello"), 1);
int elements = 0;
for (int i = 0; i < r->defs.count; i++) {
const char *label = r->defs.items[i].label;
if (label && strcmp(label, "DiagramElement") == 0) {
elements++;
}
}
ASSERT_EQ(elements, 2);
cbm_free_result(r);
PASS();
}
TEST(diagrams_dot) {
CBMFileResult *r =
dg_file("digraph G {\n"
@@ -1101,6 +1137,7 @@ SUITE(diagrams) {
RUN_TEST(diagrams_mermaid_kinds);
RUN_TEST(diagrams_other_fences);
RUN_TEST(diagrams_plantuml);
RUN_TEST(diagrams_plantuml_edge_messages);
RUN_TEST(diagrams_dot);
RUN_TEST(diagrams_d2);
RUN_TEST(diagrams_drawio);
+442
View File
@@ -179,6 +179,14 @@ TEST(doc_links_md_repeated_headings) {
"See `app/three.py`.\n"; /* 13 */
CBMFileResult *r = dm_extract(src, CBM_LANG_MARKDOWN, "CHANGES.md");
ASSERT_NOT_NULL(r);
int fixes = 0;
for (int i = 0; i < r->defs.count; i++) {
if (strcmp(r->defs.items[i].name, "Fixes") == 0) {
ASSERT_STR_EQ(r->defs.items[i].qualified_name, "p.CHANGES.Fixes");
fixes++;
}
}
ASSERT_EQ(fixes, 2);
const CBMDocLink *one = md_token(r, "app/one.py");
const CBMDocLink *two = md_token(r, "app/two.py");
const CBMDocLink *three = md_token(r, "app/three.py");
@@ -193,6 +201,427 @@ TEST(doc_links_md_repeated_headings) {
PASS();
}
/* Unchanged bare names have priority even when the bare title repeats. */
TEST(doc_links_md_repeated_bare_name_priority) {
const char *sources[] = {
"# A.H\n# A.H\n# A\n## H\n# B\n## H\n",
"# A\n## H\n# B\n## H\n# A.H\n# A.H\n",
};
for (size_t k = 0; k < sizeof(sources) / sizeof(sources[0]); k++) {
CBMFileResult *r = dm_extract(sources[k], CBM_LANG_MARKDOWN, "names.md");
ASSERT_NOT_NULL(r);
int bare = 0;
int qualified = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (strcmp(d->name, "A.H") == 0) {
ASSERT_STR_EQ(d->qualified_name, "p.names.A.H");
bare++;
} else if (strcmp(d->name, "H") == 0 &&
strcmp(d->qualified_name, "p.names.A.H~2") == 0) {
qualified++;
}
}
ASSERT_EQ(bare, 2);
ASSERT_EQ(qualified, 1);
cbm_free_result(r);
}
PASS();
}
TEST(doc_links_md_heading_suffix_reservations) {
CBMFileResult *r = dm_extract("# A.H\n# A.H~2\n# A.H~3\n"
"# A\n## H\n# B\n## H\n",
CBM_LANG_MARKDOWN, "names.md");
ASSERT_NOT_NULL(r);
int moved = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (strcmp(d->name, "A.H~2") == 0)
ASSERT_STR_EQ(d->qualified_name, "p.names.A.H~2");
if (strcmp(d->name, "A.H~3") == 0)
ASSERT_STR_EQ(d->qualified_name, "p.names.A.H~3");
if (strcmp(d->qualified_name, "p.names.A.H~4") == 0)
moved++;
}
ASSERT_EQ(moved, 1);
cbm_free_result(r);
PASS();
}
/* A suffix must also leave room for a parent-qualified name in another run. */
TEST(doc_links_md_generated_name_reservations) {
const char *src = "# A.H\n# A\n## H\n## H~2\n# B\n## H\n## H~2\n";
CBMFileResult *r = dm_extract(src, CBM_LANG_MARKDOWN, "names.md");
ASSERT_NOT_NULL(r);
int moved = 0;
int reserved = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (strcmp(d->qualified_name, "p.names.A.H~3") == 0) {
ASSERT_STR_EQ(d->name, "H");
moved++;
}
if (strcmp(d->qualified_name, "p.names.A.H~2") == 0) {
ASSERT_STR_EQ(d->name, "H~2");
reserved++;
}
}
ASSERT_EQ(moved, 1);
ASSERT_EQ(reserved, 1);
cbm_free_result(r);
PASS();
}
TEST(doc_links_md_distinct_chains_same_dotted_name) {
CBMFileResult *r =
dm_extract("# A.B\n## C\n# A\n## B\n### C\n### C\n", CBM_LANG_MARKDOWN, "names.md");
ASSERT_NOT_NULL(r);
int kept = 0;
int moved = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (strcmp(d->name, "C") != 0)
continue;
if (d->start_line == 2) {
ASSERT_STR_EQ(d->qualified_name, "p.names.A.B.C");
kept++;
} else {
ASSERT_STR_EQ(d->qualified_name, "p.names.A.B.C~2");
moved++;
}
}
ASSERT_EQ(kept, 1);
ASSERT_EQ(moved, 2);
cbm_free_result(r);
PASS();
}
TEST(doc_links_md_setext_parent_names) {
CBMFileResult *r =
dm_extract("A\n===\nH\n---\nB\n===\nH\n---\n", CBM_LANG_MARKDOWN, "names.md");
ASSERT_NOT_NULL(r);
int children = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (strcmp(d->name, "H") == 0) {
ASSERT_STR_EQ(d->qualified_name, d->start_line == 3 ? "p.names.A.H" : "p.names.B.H");
children++;
}
}
ASSERT_EQ(children, 2);
cbm_free_result(r);
PASS();
}
/* Paragraph continuation markers are not part of a Setext heading title. */
TEST(doc_links_md_quoted_setext_heading_text) {
const char *sources[] = {"First\nSecond\n---\n", "> First\n> Second\n> ---\n", "> H\n> ---\n"};
for (size_t k = 0; k < sizeof(sources) / sizeof(sources[0]); k++) {
CBMFileResult *r = dm_extract(sources[k], CBM_LANG_MARKDOWN, "names.md");
ASSERT_NOT_NULL(r);
int headings = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (strcmp(d->label, "Section") != 0)
continue;
/* Display names use the first line; the QN must retain every line. */
ASSERT_STR_EQ(d->name, k == 2 ? "H" : "First");
ASSERT_STR_EQ(d->qualified_name, k == 2 ? "p.names.H" : "p.names.First-Second");
headings++;
}
ASSERT_EQ(headings, 1);
cbm_free_result(r);
}
PASS();
}
/* Setext and ATX markers share hierarchy; block containers retain their own
* stacks even when a quote is visited between two outer headings. */
TEST(doc_links_md_mixed_heading_container_scope) {
const char *src = "# Outside\n\nH\n---\n\n"
"> # Quote\n>\n> H\n> ---\n\n"
"H\n---\n\n# Later\n\nH\n---\n";
CBMFileResult *r = dm_extract(src, CBM_LANG_MARKDOWN, "names.md");
ASSERT_NOT_NULL(r);
int outside = 0;
int quote = 0;
int later = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (strcmp(d->name, "H") != 0)
continue;
if (strcmp(d->qualified_name, "p.names.Outside.H") == 0)
outside++;
if (strcmp(d->qualified_name, "p.names.Quote.H") == 0)
quote++;
if (strcmp(d->qualified_name, "p.names.Later.H") == 0)
later++;
}
ASSERT_EQ(outside, 2);
ASSERT_EQ(quote, 1);
ASSERT_EQ(later, 1);
cbm_free_result(r);
PASS();
}
/* An unnamed H1 still ends A's scope; it must not create an empty Section. */
TEST(doc_links_md_empty_heading_resets_parent) {
CBMFileResult *r = dm_extract("# A\n## H\n#\n## H\n", CBM_LANG_MARKDOWN, "names.md");
ASSERT_NOT_NULL(r);
int children = 0;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (strcmp(d->label, "Section") == 0) {
ASSERT_TRUE(d->name && d->name[0]);
}
if (strcmp(d->name, "H") == 0) {
ASSERT_STR_EQ(d->qualified_name, d->start_line == 2 ? "p.names.A.H" : "p.names.H");
children++;
}
}
ASSERT_EQ(children, 2);
cbm_free_result(r);
PASS();
}
/* This measures the collector's actual cursor moves/node visits, excluding
* hidden parser work. The former ts_node_parent root scans are established by
* runtime-source review, not retrospectively measured by this counter. */
static uint64_t md_flat_collection_work(int n) {
size_t cap = (size_t)n * 32 + 1;
char *src = malloc(cap);
if (!src)
return 0;
size_t used = 0;
for (int i = 0; i < n; i++) {
used += (size_t)snprintf(src + used, cap - used, "# H%d\n", i);
}
CBMFileResult *r = dm_extract(src, CBM_LANG_MARKDOWN, "names.md");
free(src);
uint64_t steps = r ? r->markdown_collection_steps : 0;
cbm_free_result(r);
return steps;
}
TEST(doc_links_md_flat_collection_work) {
uint64_t small = md_flat_collection_work(128);
uint64_t large = md_flat_collection_work(256);
ASSERT_TRUE(small > 0);
ASSERT_TRUE(large < small * 3);
PASS();
}
/* Count name lookup/group checks, never elapsed time. Independent collision groups must
* not search every heading; a single group must not rescan assigned chains
* or restart its suffix search at ~2 for each new chain. */
static uint64_t md_collision_work(int n, bool one_base) {
size_t cap = (size_t)n * 256 + 1;
char *src = malloc(cap);
if (!src)
return 0;
size_t used = 0;
for (int k = 0; k < n; k++) {
if (!one_base) {
used += (size_t)snprintf(src + used, cap - used,
"# P%d\n## H%d\n# Q%d\n## H%d\n# P%d.H%d\n", k, k, k, k, k, k);
} else {
/* 276 distinct partitions of 25 dotted components into three
* parent segments, all with the same final dotted name. */
int ordinal = 0;
int first = 1;
int second = 2;
for (int i = 1; i < 24; i++) {
for (int j = i + 1; j < 25; j++) {
if (ordinal++ == k) {
first = i;
second = j;
}
}
}
used += (size_t)snprintf(src + used, cap - used, "# a");
for (int i = 1; i < 25; i++) {
used += (size_t)snprintf(src + used, cap - used,
i == first ? "\n## a"
: i == second ? "\n### a"
: ".a");
}
used += (size_t)snprintf(src + used, cap - used, "\n#### H\n");
}
}
CBMFileResult *r = dm_extract(src, CBM_LANG_MARKDOWN, "names.md");
free(src);
uint64_t work = r ? r->markdown_name_checks : 0;
cbm_free_result(r);
return work;
}
TEST(doc_links_md_independent_collision_work) {
uint64_t small = md_collision_work(128, false);
uint64_t large = md_collision_work(256, false);
ASSERT_TRUE(small > 0);
ASSERT_TRUE(large < small * 3);
PASS();
}
TEST(doc_links_md_shared_collision_work) {
uint64_t small = md_collision_work(128, true);
uint64_t large = md_collision_work(256, true);
ASSERT_TRUE(small > 0);
ASSERT_TRUE(large < small * 3);
PASS();
}
/* MENTIONS edges from the Section whose qualified name ends with `src` to the
* node whose qualified name ends with `tgt`. */
static int rep_edge(const char *db, const char *src, const char *tgt) {
char props[512];
int n = 0;
dm_edge(db, src, tgt, props, sizeof(props), &n);
return n;
}
/* #2586: two `## Implementation` headings under different parents had one
* qualified name, so the graph kept one Section (the last) and the references
* under both went to the File. A repeated heading is now qualified by the
* headings above it; a unique heading keeps its name alone. */
TEST(doc_links_md_repeated_headings_by_parent_issue2586) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlmd_rep_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
th_write_file(TH_PATH(tmp, "docs/repeated.md"), "# Service A\n"
"\n"
"## Implementation\n"
"\n"
"Uses `src/ledger.py::balance`.\n"
"\n"
"# Service B\n"
"\n"
"## Implementation\n"
"\n"
"Uses `src/ledger.py::post_entry`.\n"
"\n"
"```mermaid\n"
"graph LR\n"
" Ledger --> Balance\n"
"```\n"
"\n"
"## Notes\n"
"\n"
"See `src/ledger.py`.\n");
th_write_file(TH_PATH(tmp, "src/ledger.py"), "def post_entry(ledger, amount):\n"
" ledger.append(amount)\n"
" return balance(ledger)\n"
"\n"
"\n"
"def balance(ledger):\n"
" return sum(ledger)\n");
char db[512];
snprintf(db, sizeof(db), "%s/md.db", tmp);
ASSERT_EQ(dm_index(tmp, db, NULL), 0);
/* two Sections, each qualified by its parent heading */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Section' "
"AND name = 'Implementation' AND file_path = 'docs/repeated.md'"),
2);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Section' "
"AND qualified_name LIKE '%.repeated.Service-A.Implementation' "
"AND start_line = 3"),
1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Section' "
"AND qualified_name LIKE '%.repeated.Service-B.Implementation' "
"AND start_line = 9"),
1);
/* unique headings keep their names */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Section' AND "
"(qualified_name LIKE '%.repeated.Service-A' OR "
"qualified_name LIKE '%.repeated.Notes')"),
2);
/* each reference from its own Section, none from the File */
ASSERT_EQ(rep_edge(db, "Service-A.Implementation", "ledger.balance"), 1);
ASSERT_EQ(rep_edge(db, "Service-B.Implementation", "ledger.post_entry"), 1);
ASSERT_EQ(rep_edge(db, "Service-A.Implementation", "ledger.post_entry"), 0);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id = e.source_id "
"WHERE e.type = 'MENTIONS' AND s.label = 'File' "
"AND s.file_path = 'docs/repeated.md'"),
0);
/* the diagram under the second heading is that Section's */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id = e.source_id "
"JOIN nodes t ON t.id = e.target_id WHERE e.type = 'DEFINES' "
"AND t.label = 'Diagram' "
"AND s.qualified_name LIKE '%.Service-B.Implementation'"),
1);
th_cleanup(tmp);
PASS();
}
/* #2586 review: a heading text may hold dots, so `# Service-A.Implementation`
* named the same as the qualified `## Implementation` under `# Service A`.
* The unique heading keeps its name; the qualified one takes `~2`. Five
* headings, five Sections, every reference from its own. */
TEST(doc_links_md_repeated_heading_name_collision_issue2586) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlmd_col_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
th_write_file(TH_PATH(tmp, "docs/repeated.md"), "# Service A\n"
"\n"
"## Implementation\n"
"\n"
"Uses `src/ledger.py::balance`.\n"
"\n"
"# Service B\n"
"\n"
"## Implementation\n"
"\n"
"Uses `src/ledger.py::post_entry`.\n"
"\n"
"# Service-A.Implementation\n"
"\n"
"See `src/ledger.py`.\n");
th_write_file(TH_PATH(tmp, "src/ledger.py"), "def post_entry(ledger, amount):\n"
" ledger.append(amount)\n"
" return balance(ledger)\n"
"\n"
"\n"
"def balance(ledger):\n"
" return sum(ledger)\n");
char db[512];
snprintf(db, sizeof(db), "%s/md.db", tmp);
ASSERT_EQ(dm_index(tmp, db, NULL), 0);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Section' "
"AND file_path = 'docs/repeated.md'"),
5);
/* the unique heading keeps its name; the qualified one moves aside */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Section' "
"AND qualified_name LIKE '%.repeated.Service-A.Implementation' "
"AND start_line = 13"),
1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Section' "
"AND qualified_name LIKE '%.repeated.Service-A.Implementation~2' "
"AND start_line = 3"),
1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Section' "
"AND qualified_name LIKE '%.repeated.Service-B.Implementation' "
"AND start_line = 9"),
1);
/* each reference from its own Section, none from the File */
ASSERT_EQ(rep_edge(db, "Service-A.Implementation~2", "ledger.balance"), 1);
ASSERT_EQ(rep_edge(db, "Service-B.Implementation", "ledger.post_entry"), 1);
ASSERT_EQ(rep_edge(db, "repeated.Service-A.Implementation", "ledger.py.__file__"), 1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id = e.source_id "
"WHERE e.type = 'MENTIONS' AND s.label = 'File' "
"AND s.file_path = 'docs/repeated.md'"),
0);
th_cleanup(tmp);
PASS();
}
typedef struct {
const char *text;
CBMDocLinkMdShape shape;
@@ -1142,6 +1571,19 @@ SUITE(doc_links_md) {
dm_ship_held_families();
RUN_TEST(doc_links_md_extract_tokens);
RUN_TEST(doc_links_md_repeated_headings);
RUN_TEST(doc_links_md_repeated_bare_name_priority);
RUN_TEST(doc_links_md_heading_suffix_reservations);
RUN_TEST(doc_links_md_generated_name_reservations);
RUN_TEST(doc_links_md_distinct_chains_same_dotted_name);
RUN_TEST(doc_links_md_setext_parent_names);
RUN_TEST(doc_links_md_quoted_setext_heading_text);
RUN_TEST(doc_links_md_mixed_heading_container_scope);
RUN_TEST(doc_links_md_empty_heading_resets_parent);
RUN_TEST(doc_links_md_flat_collection_work);
RUN_TEST(doc_links_md_independent_collision_work);
RUN_TEST(doc_links_md_shared_collision_work);
RUN_TEST(doc_links_md_repeated_headings_by_parent_issue2586);
RUN_TEST(doc_links_md_repeated_heading_name_collision_issue2586);
RUN_TEST(doc_links_md_classify_span);
RUN_TEST(doc_links_md_resolve);
RUN_TEST(doc_links_md_member_not_defined_issue2587);
+24
View File
@@ -251,6 +251,29 @@ TEST(extract_c_macros_issue375) {
PASS();
}
TEST(extract_cpp_inc_linkage_and_callback) {
CBMFileResult *r =
extract("#define SAMPLE_CALLBACK(type) type\n"
"extern \"C\" __declspec(dllexport) int sample_export() { return 0; }\n"
"static SAMPLE_CALLBACK(int) sample_callback() { return sample_export(); }\n",
CBM_LANG_CPP, "sample", "runtime/sample.inc");
ASSERT_NOT_NULL(r);
ASSERT_TRUE(has_def(r, "Function", "sample_export"));
ASSERT_TRUE(has_def(r, "Function", "sample_callback"));
ASSERT_FALSE(has_def_any(r, "extern"));
cbm_free_result(r);
/* A statement include has no enclosing function in the file. Keep any
* syntax coverage diagnostic rather than inventing a callable owner. */
r = extract("#ifdef ENABLE_SAMPLE\nif (sample_check()) return 1;\n#endif\n", CBM_LANG_CPP,
"sample", "runtime/guard.inc");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(has_def_any(r, "extern"));
ASSERT_FALSE(has_def(r, "Function", "if"));
cbm_free_result(r);
PASS();
}
TEST(extract_cpp_macros_issue375) {
CBMFileResult *r = extract("#define MAX(a, b) ((a) > (b) ? (a) : (b))\n"
"#define PI 3.14159\n"
@@ -13559,6 +13582,7 @@ SUITE(extraction) {
RUN_TEST(traversal_stack_not_in_result_arena_issue2010);
RUN_TEST(extract_c_macro_hidden_call_survives_preprocessed_pass_issue2010);
RUN_TEST(extract_c_macros_issue375);
RUN_TEST(extract_cpp_inc_linkage_and_callback);
RUN_TEST(extract_cpp_macros_issue375);
RUN_TEST(extract_cpp_functionlike_macro_type_arg_no_false_parse_partial_issue1071);
RUN_TEST(extract_cpp_real_in_body_error_still_flagged_issue1071);
+2 -2
View File
@@ -49,7 +49,7 @@ t1_jobs() { # the jobs of a green T1 run; $1 = conclusion of the x86 leg
job "lint / lint-mem" success
job "test / setup-matrix" success
job "test / test-unix (ubuntu-latest, gcc, g++, unix-x86, 1/1)" "$1"
job "test / test-unix (macos-14, cc, c++, unix-macos14, 1/1)" success
job "test / test-unix (macos-15, cc, c++, unix-macos14, 1/1)" success
job "test / test-windows (windows-latest, CLANG64, x86_64, windows, 1/1)" success
job "test / test-windows-guards" skipped
job "test / test-package-wrappers" skipped
@@ -116,7 +116,7 @@ gate "a selection without the job list is red" 1 "$NEEDS_OK" "$T1" "$WORK/does-n
job "lint / lint" success
job "lint / lint-mem" success
job "test / test-windows-guards" success
job "pr-smoke (macos-14, smoke-mac)" success
job "pr-smoke (macos-15, smoke-mac)" success
} >"$WORK/guards-only"
gate "windows-guards is not the windows leg" 1 "$NEEDS_OK" '["lint","lint-mem","security-static","windows"]' "$WORK/guards-only"
gate "pr-smoke matrix name maps to its lane" 0 "$NEEDS_OK" '["lint","lint-mem","security-static","smoke-mac","windows-guards"]' "$WORK/guards-only"
+50
View File
@@ -1457,6 +1457,55 @@ static CBMLanguage classify_content(const char *name, const char *content, size_
typedef CBMLanguage (*path_probe_fn)(const char *path);
/* Exercise the shared classifier and both disk entry points. All sources are
* synthetic; the recipe bodies deliberately contain misleading C-like code. */
TEST(lang_inc_c_family_and_existing_dialects) {
static const struct {
const char *content;
CBMLanguage expected;
} cases[] = {
{"#include \"sample.h\"\nextern \"C\" int sample_export() { return 0; }\n", CBM_LANG_CPP},
{"extern \"C\" __declspec(dllexport) int sample_export() { return 0; }\n", CBM_LANG_CPP},
{"#define SAMPLE_CALLBACK(T) T\nstatic SAMPLE_CALLBACK(int) sample() { return 0; }\n",
CBM_LANG_CPP},
{"/* in-function fragment */\n#ifdef SAMPLE_ENABLED\nif (check()) return 1;\n#endif\n",
CBM_LANG_CPP},
{"int sample(void) { return 0; }\n", CBM_LANG_CPP},
{" \t# include <stdint.h>\r\n", CBM_LANG_CPP},
{"/* example:\nextern \"C\" int example();\n*/\nSUMMARY = \"sample\"\n", CBM_LANG_BITBAKE},
{"// extern \"C\" example only\n", CBM_LANG_BITBAKE},
{"#include_extra \"sample.h\"\n", CBM_LANG_BITBAKE},
{"#define Value 1\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"#ifndef SAMPLE\n#define SAMPLE 1\n#endif\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"ROUTINE Sample [Type=INC]\n#define Value 1\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"#def1arg Value(%x) %x\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"#; ObjectScript comment\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"#include <sample.h>\n#def1arg Value(%x) %x\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"# recipe\nSUMMARY = \"sample\"\nrequire base.inc\n", CBM_LANG_BITBAKE},
{"SRC_URI += \"file://sample.c\"\ndo_compile() {\nif (check) { return; }\n}\n",
CBM_LANG_BITBAKE},
{"python do_generate() {\nif (ready):\n pass\n}\n", CBM_LANG_BITBAKE},
{"do_compile() {\ncat <<'EOF'\n#include <stdio.h>\nstatic int sample;\nEOF\n}\n",
CBM_LANG_BITBAKE},
{"require base.inc\nextern \"C\" example in recipe text\n", CBM_LANG_BITBAKE},
{"", CBM_LANG_BITBAKE},
};
char path[512];
snprintf(path, sizeof(path), "%s/cbm_lang_inc_routing.inc", cbm_tmpdir());
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
size_t len = strlen(cases[i].content);
ASSERT_EQ(classify_content("sample.inc", cases[i].content, len), cases[i].expected);
ASSERT_TRUE(write_exact(path, cases[i].content, len));
CBMLanguage by_probe = cbm_disambiguate_inc(path);
CBMLanguage by_file = cbm_language_for_file("sample.inc", path);
remove(path);
ASSERT_EQ(by_probe, cases[i].expected);
ASSERT_EQ(by_file, cases[i].expected);
}
ASSERT_EQ(cbm_disambiguate_inc(NULL), CBM_LANG_BITBAKE);
PASS();
}
TEST(lang_probe_bytes_follow_the_name) {
ASSERT_EQ(cbm_language_probe_bytes("main.go"), 0);
ASSERT_EQ(cbm_language_probe_bytes("Makefile"), 0);
@@ -1879,6 +1928,7 @@ SUITE(language) {
RUN_TEST(lang_ext_sosl);
RUN_TEST(lang_all_have_names);
RUN_TEST(lang_inc_c_family_and_existing_dialects);
RUN_TEST(lang_probe_bytes_follow_the_name);
RUN_TEST(lang_classify_matches_every_content_rule);
RUN_TEST(lang_classify_unreadable_content_keeps_the_name_default);
+83
View File
@@ -11820,6 +11820,88 @@ TEST(search_code_scoped_file_pattern_is_busybox_portable) {
#endif
}
/* issue #2836: a file_pattern with '/' matched only the basename, so every
* directory-scoped glob silently returned 0 results. */
TEST(search_code_file_pattern_matches_repo_relative_path_issue2836) {
char tmp[CBM_SZ_512];
snprintf(tmp, sizeof(tmp), "%s/cbm-search-fp-path-XXXXXX", cbm_tmpdir());
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
char src_dir[CBM_SZ_1K];
char cli_dir[CBM_SZ_1K];
char main_path[CBM_SZ_1K];
char watcher_path[CBM_SZ_1K];
snprintf(src_dir, sizeof(src_dir), "%s/src", tmp);
snprintf(cli_dir, sizeof(cli_dir), "%s/src/cli", tmp);
snprintf(main_path, sizeof(main_path), "%s/src/main.c", tmp);
snprintf(watcher_path, sizeof(watcher_path), "%s/src/cli/watcher.c", tmp);
ASSERT_EQ(cbm_mkdir(src_dir), 0);
ASSERT_EQ(cbm_mkdir(cli_dir), 0);
ASSERT_EQ(th_write_file(main_path, "int FP_PATH_NEEDLE = 1;\n"), 0);
ASSERT_EQ(th_write_file(watcher_path, "int FP_PATH_NEEDLE = 2;\n"), 0);
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *store = cbm_mcp_server_store(srv);
ASSERT_NOT_NULL(store);
const char *project = "search-fp-path";
cbm_mcp_server_set_project(srv, project);
ASSERT_EQ(cbm_store_upsert_project(store, project, tmp), CBM_STORE_OK);
cbm_node_t main_file = {.project = project,
.label = "File",
.name = "main.c",
.qualified_name = "search-fp-path.src.main.c",
.file_path = "src/main.c"};
cbm_node_t watcher_file = {.project = project,
.label = "File",
.name = "watcher.c",
.qualified_name = "search-fp-path.src.cli.watcher.c",
.file_path = "src/cli/watcher.c"};
ASSERT_GT(cbm_store_upsert_node(store, &main_file), 0);
ASSERT_GT(cbm_store_upsert_node(store, &watcher_file), 0);
static const struct {
const char *file_pattern;
const char *expect;
bool hint;
} cases[] = {
{"src/cli/*.c", "\"total_grep_matches\":1", false},
{"src/**/*.c", "\"total_grep_matches\":2", false}, /* ** spans zero dirs */
{"**/cli/*.c", "\"total_grep_matches\":1", false},
{"src/cli/", "\"total_grep_matches\":1", false}, /* no wildcard: substring */
{"watcher.c", "\"total_grep_matches\":1", false}, /* basename unchanged */
{"*.c", "\"total_grep_matches\":2", false},
{"lib/*.c", "\"total_grep_matches\":0", true},
};
bool ok = true;
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
char args[CBM_SZ_512];
snprintf(args, sizeof(args),
"{\"pattern\":\"FP_PATH_NEEDLE\",\"project\":\"search-fp-path\","
"\"file_pattern\":\"%s\",\"format\":\"json\"}",
cases[i].file_pattern);
char *response = cbm_mcp_handle_tool(srv, "search_code", args);
char *inner = extract_text_content(response);
bool case_ok = inner && strstr(inner, cases[i].expect) &&
(strstr(inner, "use path_filter") != NULL) == cases[i].hint;
if (!case_ok) {
fprintf(stderr, "file_pattern %s: %s\n", cases[i].file_pattern,
inner ? inner : "(null)");
ok = false;
}
free(inner);
free(response);
}
cbm_mcp_server_free(srv);
cbm_unlink(watcher_path);
cbm_unlink(main_path);
cbm_rmdir(cli_dir);
cbm_rmdir(src_dir);
cbm_rmdir(tmp);
ASSERT_TRUE(ok);
PASS();
}
TEST(search_code_recursive_fallback_propagates_discovery_failures) {
#ifdef _WIN32
SKIP_PLATFORM("POSIX find/sort/xargs fallback contract");
@@ -26474,6 +26556,7 @@ SUITE(mcp) {
RUN_TEST(search_code_fails_closed_when_complete_scan_is_impossible);
RUN_TEST(search_code_scoped_scan_uses_canonical_file_nodes);
RUN_TEST(search_code_scoped_file_pattern_is_busybox_portable);
RUN_TEST(search_code_file_pattern_matches_repo_relative_path_issue2836);
RUN_TEST(search_code_recursive_fallback_propagates_discovery_failures);
RUN_TEST(search_code_default_budget_limits_raw_rows_before_graph_results);
RUN_TEST(search_code_raw_and_directory_remainders_are_independently_pageable);
+33
View File
@@ -29159,7 +29159,40 @@ TEST(pipeline_calls_args_overlong_argument_stops_cleanly) {
PASS();
}
/* Discovery must choose the C++ grammar before the pipeline extracts .inc
* symbols; a direct extractor-only test would miss that routing failure. */
TEST(pipeline_inc_c_family_fragments) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "%s/cbm_inc_cpp_XXXXXX", cbm_tmpdir());
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
write_temp_file(tmp, "runtime.inc",
"#define SAMPLE_CALLBACK(T) T\n"
"extern \"C\" int sample_export() { return 0; }\n"
"static SAMPLE_CALLBACK(int) sample_callback() { return sample_export(); }\n");
write_temp_file(tmp, "main.cpp", "#include \"runtime.inc\"\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/graph.db", tmp);
const cbm_index_mode_t modes[] = {CBM_MODE_FAST, CBM_MODE_FULL};
for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) {
cbm_pipeline_t *pipeline = cbm_pipeline_new(tmp, db_path, modes[i]);
ASSERT_NOT_NULL(pipeline);
ASSERT_EQ(cbm_pipeline_run(pipeline), 0);
cbm_store_t *store = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(store);
const char *project = cbm_pipeline_project_name(pipeline);
ASSERT_EQ(fixture_node_count(store, project, "runtime.inc", "sample_export", "Function"),
1);
ASSERT_EQ(fixture_node_count(store, project, "runtime.inc", "sample_callback", "Function"),
1);
ASSERT_EQ(named_node_count(store, project, "extern"), 0);
cbm_store_close(store);
cbm_pipeline_free(pipeline);
}
th_rmtree(tmp);
PASS();
}
SUITE(pipeline) {
RUN_TEST(pipeline_inc_c_family_fragments);
RUN_TEST(pipeline_python_module_callers_sequential);
RUN_TEST(pipeline_python_module_callers_parallel);
RUN_TEST(pipeline_python_class_body_calls_sequential);