gstack-learnings-search: a more specific query pushes the exact match off the list (token-OR + confidence-only ranking + silent truncation)
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
## Symptom
`gstack-learnings-search --query` gets **worse as the query gets more specific**. Adding a discriminating term can push the exact match off the end of the result list, and the command still returns ten confident, well-formed entries — so the caller reads them as the answer and concludes the thing they searched for is not in the store.
It does not error and does not return empty. It hands back a **false absence**.
## Reproduction
Against a store containing a learning keyed `verify-preflight-project-line-before-trusting-report` (26 entries total):
```
--query "preflight" -> 3 results, target PRESENT
--query "project" -> 4 results, target PRESENT
--query "preflight project" -> 7 results, target PRESENT
--query "preflight project line" -> 10 results, target ABSENT <-- the bug
--query "preflight project line" --limit 50 -> 14 results, target PRESENT
```
The key contains all three tokens, in that order.
Token counts in that store: `preflight` matches 3 entries, `line` matches 10 (substring hits inside *guideline*, *inline*, *pipeline*, and any insight text containing the word).
## Mechanism
`bin/gstack-learnings-search`, in the `bun -e` block:
```js
// Filter by query (token-OR: match if ANY whitespace-split token appears in ANY haystack)
if (queryTokens.length > 0) results = results.filter(e => {
...
return queryTokens.some(tok => haystacks.some(h => h.includes(tok)));
});
// Sort by effective confidence desc, then recency
results.sort((a, b) => {
if (b._effectiveConfidence !== a._effectiveConfidence) return b._effectiveConfidence - a._effectiveConfidence;
return new Date(b.ts).getTime() - new Date(a.ts).getTime();
});
results = results.slice(0, limit);
```
Three things compose into the failure:
1. **Token-OR with substring matching.** The union of the three tokens is 14 entries.
2. **Ranking ignores how many tokens matched.** Entries are ordered by confidence alone, so a confidence-10 entry matching only `line` outranks the 3-of-3 match at confidence 8.
3. **The limit truncates silently.** Default is 10. The exact answer is in the result *set* and off the end of the result *list*, with nothing said about it.
Worth noting the filter is **not** broken and does run — an earlier guess that multi-word queries "fall through to unfiltered" is wrong, and would send a fix to the wrong place.
## Why it matters
`skills/learn/SKILL.md` instructs the operator to run `--query "USER_QUERY"` with the user's own search terms, which are usually multi-word. So the documented primary search is at its worst on its normal input.
The failure direction is the dangerous one: a caller searching for an entry they have other evidence exists will get a plausible list back and conclude it is absent.
## Suggested fix
Rank by the number of distinct query tokens matched, then confidence, then recency, and disclose truncation. Locally verified as a three-line change:
```js
// in the filter
e._tokenHits = queryTokens.filter(tok => haystacks.some(h => h.includes(tok))).length;
return e._tokenHits > 0;
// in the sort, ahead of the confidence comparison
const ah = a._tokenHits || 0, bh = b._tokenHits || 0;
if (bh !== ah) return bh - ah;
// after slicing, on the summary line, only when a query was given
' -- ' + (totalMatched - results.length) + ' more matched, raise --limit to see them'
```
With a single-token query every hit scores 1, so the ordering degenerates to exactly the current behaviour and nothing that works today changes.
Two implementation notes from applying it:
- Gate the truncation notice on `queryTokens.length > 0`. Every skill preamble runs `--limit 3` with no query, and announcing truncation there adds a line to every skill invocation in every session.
- The notice has to go to **stdout**. The block ends `" 2>/dev/null || exit 0`, so anything on stderr is discarded.
Happy to open a PR if useful.
Optional follow-up, not required for this: word-boundary rather than substring matching, so `line` stops matching `guideline` and `pipeline`.
Contributor guide
Research direction
Start in bin/gstack-learnings-search, specifically the bun -e block, and reproduce the multi-token queries against the described store. Check that results prioritize entries matching more distinct tokens and that a queried, truncated result set reports additional matches on stdout; leave no-query skill preambles unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bun, javascript
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100