Summary
normalizeToken() in packages/core/src/signals.ts strips only one character for the -ed and -es suffixes, so a task word and the identifier it refers to stem to different tokens unless the base form happens to end in e.
if (token.length > 5 && token.endsWith("ies")) return `${token.slice(0, -3)}y`;
if (token.length > 5 && token.endsWith("ing")) return token.slice(0, -3);
if (token.length > 4 && token.endsWith("ed")) return token.slice(0, -1); // strips "d" only
if (token.length > 4 && token.endsWith("es")) return token.slice(0, -1); // strips "s" only
if (token.length > 3 && token.endsWith("s")) return token.slice(0, -1);
Reproduction
node -e "const {tokenizeText}=require('./packages/core/dist/signals.js'); \
for (const [a,b] of [['failed','fail'],['boxes','box'],['routing','route'],['parsing','parse']]) \
console.log(a, '->', [...tokenizeText(a)], '|', b, '->', [...tokenizeText(b)]);"
failed -> [ 'faile' ] | fail -> [ 'fail' ] mismatch
boxes -> [ 'boxe' ] | box -> [ 'box' ] mismatch
routing -> [ 'rout' ] | route -> [ 'route' ] mismatch
parsing -> [ 'pars' ] | parse -> [ 'parse' ] mismatch
created -> [ 'create'] | create-> [ 'create' ] match (base ends in "e")
Impact
This is the matching function behind path overlap, content overlap, and findRelatedTests() — the heart of the ranker. A very common phrasing like "password reset failed" does not match a fail/failure identifier or a retry-failed.ts path. Two separate rules interact badly:
-ed / -es under-strip (failed -> faile).
-ing over-strips relative to an -e base (routing -> rout, but route -> route).
So the two halves of the stemmer disagree about whether a trailing e survives, and neither direction reliably converges on the base form.
Suggested fix
Make the suffix rules converge on one normal form. A minimal version that fixes all four cases above: after stripping -ed / -ing / -es, also strip a trailing e (or equivalently, normalize both route and routing to rout). Anything that makes stem(x) === stem(inflect(x)) hold is fine — the important part is that the rules agree.
Because this changes ranking, it should be landed together with npm run evaluate results and, ideally, a couple of new benchmarks/cases.json entries using past-tense task phrasing.
Note: apps/web/app/demo.tsx contains a hand-copied version of the same rules and has the same defect — see the separate issue on demo/core tokenizer drift.
Summary
normalizeToken()inpackages/core/src/signals.tsstrips only one character for the-edand-essuffixes, so a task word and the identifier it refers to stem to different tokens unless the base form happens to end ine.Reproduction
Impact
This is the matching function behind path overlap, content overlap, and
findRelatedTests()— the heart of the ranker. A very common phrasing like "password reset failed" does not match afail/failureidentifier or aretry-failed.tspath. Two separate rules interact badly:-ed/-esunder-strip (failed->faile).-ingover-strips relative to an-ebase (routing->rout, butroute->route).So the two halves of the stemmer disagree about whether a trailing
esurvives, and neither direction reliably converges on the base form.Suggested fix
Make the suffix rules converge on one normal form. A minimal version that fixes all four cases above: after stripping
-ed/-ing/-es, also strip a trailinge(or equivalently, normalize bothrouteandroutingtorout). Anything that makesstem(x) === stem(inflect(x))hold is fine — the important part is that the rules agree.Because this changes ranking, it should be landed together with
npm run evaluateresults and, ideally, a couple of newbenchmarks/cases.jsonentries using past-tense task phrasing.Note:
apps/web/app/demo.tsxcontains a hand-copied version of the same rules and has the same defect — see the separate issue on demo/core tokenizer drift.