Files
multica/apps
45027c1db6 MUL-5824 fix(search): demote cancelled issues and projects below live work (#6515)
* MUL-5824 fix(search): demote cancelled issues and projects below live work

Cancelled work was outranking live work in both search and the @mention
picker. Three separate causes:

- Issue search ordered by relevance first and status second, so statusRank
  was inert across tiers: a cancelled issue matching a title exactly (tier 1)
  beat an in_progress issue that merely contained the phrase (tier 3). With
  LIMIT 20 applied after ordering, a workspace with many cancelled issues
  could push live work off the page entirely.
- Project search had no status ranking at all, and the command palette
  renders projects above issues — so a cancelled project could be the first
  row of the whole result list.
- The mention picker merged its local issue cache ahead of the server
  results and only then truncated to MAX_ITEMS, so a cached cancelled row
  both outranked and crowded out server-ranked live matches.

Demote cancelled ahead of the relevance tiers in both query builders, and
apply the same partition in the picker before its truncation. 'done' is
untouched: finished work is still worth referencing, only cancelled work was
thrown away.

Exempt direct hits — an exact identifier or exact title means the searcher is
targeting that one record, so it stays on top. Curated mention groups
(current / recent / search) are exempt too: "Current" has to survive the
truncation even when the issue being viewed is itself cancelled.

Co-authored-by: multica-agent <[email protected]>

* MUL-5824 fix(search): demote cancelled across issue+project result lists

Follow-up to review on #6515. The per-type ranking landed server-side, but
every client that shows issues and projects together aggregates two
independently ranked responses — so a cancelled row from one still outranked a
live row from the other:

- The command palette and the mobile search screen render the whole Projects
  list before the whole Issues list, so one cancelled project was the first row
  of the result list next to a live issue.
- The context @mention picker concatenates issues then projects and tags both
  'search', while the client-side demotion only touched *ungrouped* cancelled
  issues. Cancelled search results were exempt and cancelled projects never
  participated at all.

Move the partition to the aggregation point and make it cross-type. New shared
helper @multica/core/search/cancelled-rank does one stable partition over both
types, preserving the server's relative order on each side and mirroring the
server's direct-hit exemption (exact identifier, bare number, exact title).

All three consumers now render live projects, then live issues, then one
trailing Cancelled section. A single trailing section is the only arrangement in
which no cancelled row of either type can precede a live row of the other, and
because it runs before truncation the cancelled tail is what gets dropped rather
than a live row. The mention picker keeps its Current / Recent exemptions: those
are explicit context, and "Current" has to survive truncation even when the
issue on screen is itself cancelled.

Mobile's row builder moved to apps/mobile/lib/search-rows.ts so the ordering is
testable without mounting the screen.

Also corrects the include_closed note in search_cancelled_rank_test.go: the
mobile mention picker sends include_closed=false, so it never sees cancelled
issues at all.

Tests: mixed issue/project regressions for the palette (3), the context mention
picker (4), the mobile row builder (6), and the shared helper (9). Each was
confirmed to fail with the partition neutralised.

Co-authored-by: multica-agent <[email protected]>

* MUL-5824 fix(mention): pin cancelled direct hits above the truncation

Second review round on #6515. The mention picker only exempted the curated
Current / Recent groups, so a cancelled issue or project the user had typed out
in full was still demoted — contradicting the direct-hit rule the backend and
the other two search surfaces already share.

The picker now applies that same query-aware rule via isIssueDirectHit /
isProjectDirectHit: exact identifier, bare number, or full title for issues, and
full title for projects. Both the partition and groupItems() take the query so
they cannot disagree about which rows are exempt.

Exemption alone was not enough. slice(0, MAX_ITEMS) runs on the merged list, so a
direct hit behind a full window of cached candidates was still cut — a stale
"row is still listed" claim in the old comment. The partition is now three
tiers, pinned → live → cancelled, where pinned holds curated context plus direct
hits. This mirrors the server, which already ranks direct hits first, and makes
"Current survives the truncation" structural rather than incidental.

isIssueDirectHit now takes all fields optional and derives the number from the
identifier when `number` is absent, which is what lets the picker share the rule:
its rows carry the identifier in `label` and the title in `description`, with no
number field. An explicit `number` still wins if both are present.

Tests: 5 mention regressions (exact identifier, bare number, project full title,
direct hit visible behind 20 live candidates, and a partial match that must still
be demoted) plus 3 helper cases for the identifier-derived number. Verified the
four ordering tests fail with the exemption reverted, and that three of them
still fail with exemption but no pinning — so the pinning tier is load-bearing,
not belt-and-braces.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Eve <[email protected]>
2026-08-07 12:33:47 +08:00
..