Skip to content

fix(search): ignore empty-string label in search_graph name_pattern path#483

Open
halindrome wants to merge 2 commits into
DeusData:mainfrom
halindrome:fix/search-graph-empty-label
Open

fix(search): ignore empty-string label in search_graph name_pattern path#483
halindrome wants to merge 2 commits into
DeusData:mainfrom
halindrome:fix/search-graph-empty-label

Conversation

@halindrome

Copy link
Copy Markdown
Contributor

Closes #481

Problem

search_graph returns zero results when a request passes an empty-string label ("label": "") together with name_pattern (or qn_pattern). The reporter saw it as "name_pattern misses indexed Class nodes," but the trigger is the empty label, not the regex:

Call result
name_pattern=".*Foo.*" (no label key) matches ✓
name_pattern=".*Foo.*", label="" 0
name_pattern=".*Foo.*", label="Class" matches ✓
query="Foo", label="" (BM25) matches ✓

The BM25 query path treats an empty label as absent; the name_pattern path does not — so the same "label":"" payload works for query but returns nothing for name_pattern. Agents then fall back to grep even though the symbol is in the graph.

Root cause

search_where_basic (src/store/store.c) guarded the label clause on NULL only:

if (params->label) {                 // "" is non-NULL → still appended
    ... "n.label = ?" ...            // binds n.label = '' → matches no node
}

Fix

if (params->label && params->label[0]) {   // ignore empty-string label

An empty label is now a no-op, identical to an omitted label and to the BM25 path. Non-empty labels still filter exactly as before.

Test

store_search_empty_label_ignored (tests/test_store_search.c): name_pattern + label="" returns the same match as name_pattern alone, and a non-empty label still filters (.*Order.* + label="Class" → only the Class node).

Verification

  • scripts/build.sh clean; scripts/test.sh passes (the lone cli_hook_gate_script_no_predictable_tmp_issue384 failure is pre-existing on main, environment-dependent, untouched here).
  • Reproduced the original symptom on a real project and confirmed the fix: with the empty label ignored, name_pattern matches all expected Class nodes.

🤖 Generated with Claude Code

search_where_basic guarded the label filter on NULL only (`if (params->label)`),
so a search passing label="" appended a literal `n.label = ''` clause that
matches no node — silently returning zero results. The BM25 query path already
treats an empty label as absent, so name_pattern/qn_pattern searches and BM25
searches behaved inconsistently for the same `"label":""` payload (issue DeusData#481):
agents fell back to grep because structural class/service discovery returned
nothing.

Guard the clause on `params->label && params->label[0]` so an empty-string
label is a no-op, identical to an omitted label and to the BM25 path.

Adds store_search_empty_label_ignored: name_pattern + label="" returns the same
match as name_pattern alone, while a non-empty label still filters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
@halindrome

Copy link
Copy Markdown
Contributor Author

QA Round 1 — CLEAN

Reviewer model: claude-opus-4-8 · Contract source: issue #481 · Base: main

Contract Verification

Criterion Status Evidence
name_pattern + empty-string label must not return zero satisfied Guard if (params->label && params->label[0]) (store.c) skips the n.label = ? clause for label="". Exercised end-to-end: cbm_mcp_get_string_arg returns a non-NULL "" for JSON "label":"", so the real tool path hits the guard.
Empty label behaves like omitted label satisfied label=="" and label==NULL both skip the clause; other clauses unaffected. Test asserts name_pattern=".*Submit.*" + label="" → same single result as the pattern alone.
Consistent with BM25 query path satisfied bm25_search takes no label arg (filters only project + fixed noise-label set); there is no second place applying an empty label.
No regression to non-empty label satisfied Non-empty label still appends/binds n.label = ?. Test: .*Order.* (3 name matches) + label="Class" → only OrderService.
Covers both name_pattern and qn_pattern satisfied search_where_basic is the single shared WHERE builder; both pattern clauses sit after the label clause, one caller chain.

Findings

  • Contract: 0 · Regression: 0
  • Guard is subtractive for label=="", no-op otherwise; no node carries an empty label, so the prior n.label='' was strictly a bug. Memory-safe ([0] read only after non-NULL check).

Advisory (non-blocking)

  • F-01 (test gap): the test comment mentions qn_pattern but only name_pattern is exercised. Provably covered by the shared builder, but a future qn_pattern special-case could silently regress. → addressing with a qn_pattern + empty-label assertion.
  • F-02 (edge case, out of scope): a whitespace-only label (" ") is still treated as a real filter. Intentional — the [0] guard scopes precisely to the empty string reported in search_graph name_pattern misses indexed Class nodes found by BM25 query #481; whitespace is not part of the contract.

Verdict

CLEAN. One-line guard fully satisfies #481 for both pattern paths, genuinely matches BM25 behavior, no regression, memory-safe. Regression test fails on old code (label=""n.label='' → 0) and passes on new.

QA round 1 noted store_search_empty_label_ignored asserted only the
name_pattern path though the comment referenced qn_pattern too. Add a
qn_pattern + label="" assertion so a future change that special-cased the
qn_pattern branch can't silently reintroduce the empty-label bug. Both paths
share search_where_basic, so this locks in the shared guard.

Test-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
@halindrome

Copy link
Copy Markdown
Contributor Author

Addressed QA round 1 advisory F-01 in 19a0fc5 (test-only): store_search_empty_label_ignored now also asserts qn_pattern + label="" returns the match, locking in the shared-builder guard against a future qn_pattern special-case. F-02 (whitespace-only label) left as-is — intentionally out of scope for #481, which is specifically the empty string. Suite green (the lone unrelated cli_hook_gate failure is pre-existing).

@halindrome halindrome marked this pull request as ready for review June 17, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

search_graph name_pattern misses indexed Class nodes found by BM25 query

2 participants