← reports

#3171 · Tasks search treats multiple words as one phrase

Bug Medium Effort: Low cli tasks open on GitHub 2026-09-06 · base 6cdb4ba

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

A task that is found when two search words follow the title's order disappears when those same words are reversed. Two clean checkouts reproduced that behavior with new in-memory databases. The Tasks store converts the entire trimmed query into one SQL LIKE pattern, so spaces and word order become literal phrase requirements. The CLI and RPC layers pass the query through unchanged, making this a store-level defect shared by every caller of that list operation.

2. Claims vs findings

ClaimStatusEvidence
Reversing two abundant search terms can remove a matching task.VerifiedThe forward-order assertion returned SRC-1; the reversed-order assertion returned an empty array in both clean checkouts.
The Tasks store treats the complete query as one substring.VerifiedOne escaped parameter is constructed from the full trimmed string and reused in all three searchable columns.
The affected board produced the exact reported row counts.UnverifiedThe external board and its runtime data were outside the trust boundary.
Search results are not relevance-ranked.VerifiedThe store orders by the requested manual, priority, or due-date keyset; it computes no relevance score.

3. Environment

4. Minimal reproduction

  1. Check out the trusted base commit.
  2. Run the frozen install and repository build.
  3. Copy the linked test to plugins/tasks/task-search-order.repro.test.ts.
  4. Run the focused Tasks test through Turbo.
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=bb-plugin-tasks --force -- task-search-order.repro.test.ts

Expected: both orders return the matching task and neither one-term distractor.

deployment readiness → ["SRC-1"]
readiness deployment → ["SRC-1"]

Actual: the first assertion passes, then the reversed order fails.

AssertionError: expected [] to deeply equal [ 'SRC-1' ]
Test Files  1 failed (1)
Tests       1 failed (1)

The complete reproduction test is inline below so the public report has no external test-file dependency.

import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
import { describe, expect, it } from "vitest";
import { createTasksStore } from "./db";

describe("task search term order", () => {
  it("finds all search terms without requiring their input order", async () => {
    const { bb, harness } = createFakePluginHost({
      pluginId: "tasks-search-repro",
    });
    const store = createTasksStore(bb.storage.database());
    try {
      const project = store.createProject({
        name: "Search project",
        prefix: "SRC",
        color: "blue",
      });
      const matching = store.createTask({
        projectId: project.id,
        title: "Deployment readiness review",
      });
      store.createTask({
        projectId: project.id,
        title: "Deployment schedule",
      });
      store.createTask({
        projectId: project.id,
        title: "Readiness checklist",
      });

      const matchingKeys = (search: string) =>
        store
          .listTasks({ projectId: project.id, search })
          .map((task) => task.key);

      expect(matchingKeys("deployment readiness")).toEqual([matching.key]);
      expect(matchingKeys("readiness deployment")).toEqual([matching.key]);
    } finally {
      await harness.dispose();
    }
  });
});

5. Verification

The first run used the dedicated BB worktree at the recorded trusted commit. Its frozen install and full Turbo build passed, and the focused test failed at the reversed-order assertion.

The second run used a new detached checkout at the same commit. Its independent frozen install and full build passed. The same saved test failed with the same empty-array result against a new in-memory database.

No report correction was needed after the second run. Raw build and reproduction logs remain outside the public repository in local evidence storage.

6. Root cause

The CLI forwards its --search value unchanged in the typed list request. See CLI request construction. The Tasks API then forwards that value directly into listTasksPage. See RPC handler forwarding.

The store trims the query once, escapes it once, and wraps the full string in percent wildcards. The same parameter is compared against title, description, and task key. See search predicate construction.

parameters.search = `%${escapeLike(search)}%`;
clauses.push(`(
  t.title LIKE @search ESCAPE '\\'
  OR t.description LIKE @search ESCAPE '\\'
  OR (p.prefix || '-' || t.number) LIKE @search ESCAPE '\\'
)`);

Because the embedded space remains literal, deployment readiness is a match but readiness deployment is not. The three-column OR only chooses which single column must contain the entire phrase; it does not make the individual words independent. The root-cause confidence is high because the focused test exercises this exact store function twice and its failure follows directly from the generated predicate.

7. Proposed fix (first principles)

Normalize the trimmed query into non-empty whitespace-delimited terms. Give each term its own escaped parameter and append one grouped title/description/key OR predicate per term; the outer clause list will combine those groups with AND. Preserve the current escapeLike behavior so percent, underscore, and backslash remain literal. Keep the focused regression's one-term distractors to prove the new query requires every term rather than matching any term.

8. Related issues

No linked open pull request or cross-referenced pull request existed at investigation time. No related issue changed the direct result on the trusted base commit.

9. Appendix

The issue body was treated as untrusted data. Its commands and implementation proposal were not executed or copied; the reproduction and fix proposal were derived from trusted repository source.

The target repository is public. No credentials, external board data, live BB instance, or provider process was used.

git fetch origin main
git rev-parse HEAD
git rev-parse origin/main
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=bb-plugin-tasks --force -- task-search-order.repro.test.ts
git blame -L 900,925 origin/main -- plugins/tasks/db/store.ts
git log 6cdb4ba..origin/main --oneline -- plugins/tasks/db/store.ts plugins/tasks/db.test.ts