fix(skill): rank recalled skills by relevance rather than by slug - #533
Merged
Conversation
Which skills an objective was offered depended on the alphabet. A store answered a term with its matches ordered by slug and cut to the limit, and recall asked for exactly the five it could offer, so for a word six or more skills carried, everything sorted later was never a candidate. Rank then scored a candidate on how many of the objective's terms it contained, counting a word most of the library carries the same as one only this skill has. Together those made a library that degrades as it grows: adding a skill displaces an unrelated one, and the skill that loses is not the skill that was added. Assembling the pack with every authored skill present failed five rows of the retrieval table, among them deletion losing "clean up the leftovers from the last change" and domain-language losing "what should I call this thing". Three changes. Both stores now order a text search by how well the row matches, the sqlite one by bm25 with the description weighted highest and the in-memory one by a field-weighted count, so a cap selects the best answers instead of the first alphabetically. Recall gathers a pool wider than the offer, since ranking happens afterwards. Rank weights each term by how few of the candidates carry it, and decides relevance on its own before evidence breaks a tie, so a verified skill still cannot outrank a better match. Two descriptions gained a word their own objectives are phrased with, which the old ranking had hidden: deletion never said "clean up" or "leftovers", and domain-language never said "call". Both rows had been passing because no other skill matched either, not because these two did. The pack now passes with all thirteen authored skills present, including the four not yet landed, with no further description tuning. Signed-off-by: Ion Alpha <contact@ionalpha.io>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Skill recall stops selecting alphabetically. A text search in either store is now ordered by how well the row matches (
bm25in sqlite with the description weighted highest, a field-weighted count in the in-memory store), recall gathers a candidate pool wider than the offer it can make, andRankweights each term by how few of the candidates carry it, deciding relevance before evidence breaks a tie.Two descriptions in the bundled pack also gained a word their own objectives are phrased with:
deletionnever said "clean up" or "leftovers", anddomain-languagenever said "call".Why
Which skills an objective was offered depended on the alphabet. A store answered a term with its matches ordered by slug and cut to the limit, and
Gatherasked for exactly the five skills recall can offer, so for any word six or more skills carried, everything sorted later was never a candidate and nothing reported it.Rankthen scored on a raw count of matching terms, so a word most of the library carries counted the same as one only this skill has.The result is a library that gets worse as it grows: adding a skill displaces an unrelated one, and the skill that loses is not the one being added. Assembled with every authored skill present, the pack failed five rows of its own retrieval table,
deletionlosing "clean up the leftovers from the last change" anddomain-languagelosing "what should I call this thing" among them. The pack is at nine skills, the dev pack alone is planned for twenty-four, and a user's library is larger still.The two description edits are the other half of the same finding. Both of those rows had been passing because nothing else matched either, not because those skills matched: the words a person types were never in the text the ranker reads.
How to verify
go test ./skill/... ./storage/sqlite/is green.Four tests carry the change, and each fails on the old behaviour:
TestRecallOffersTheBestMatchWhateverItsSlugSortsAs: seven decoys mention the word in passing and the skill about it sorts last. Before, it was cut before ranking and the offer was the five decoys.TestRankWeighsARareTermAboveACommonOne: the candidate carrying the one discriminating term is ranked first, ahead of four that share a common one.TestSearchCapsToTheBestMatchesandTestSkillSearchRanksTheDescriptionAboveThePassingMention: one per store, a capped search answers with the skill whose description is about the query rather than the one whose slug sorts first.TestPackIsRetrievablepasses with all thirteen authored skills present, including the four still in worktrees, with no further description tuning.TestRankBreaksTiesOnEvidenceAndNeverOverRelevancestill holds.Notes for reviewers
The document frequency behind the term weights is counted over the gathered candidates rather than the whole library. Those are the skills being chosen between and it costs no second pass over the store, but it does mean a weight is relative to the pool, not absolute.
candidatePoolis 4, so recall issues the same number of queries as before, each asking for twenty rows instead of five.Three tests in
cmd/flynnfail on this machine (TestNewExternalAgent,TestExternalAdapterIsBuiltForEveryBackend,TestAttestedDeclarationMatchesRecordedEvents). They fail the same way on a cleanmain: a local codex install with no vendored binary for this platform.