Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/app/actions/recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,14 @@

// Insert a replacement pick. Same difficulty if possible. Excludes
// anything the user has already seen (any status).
const { data: profile, error: profileErr } = await service
.from('profiles')
.select('level')
.eq('id', user.id)
.single();

if (profileErr) return err('persist_failed', profileErr.message);

const { data: profile } = await service
.from('profiles')
.select('level')
Expand Down Expand Up @@ -338,6 +346,14 @@
.eq('user_id', userId);
const excludeIds = new Set((seen ?? []).map((r) => r.issue_id));

const allowedDifficulties = new Set<string>();
if (userLevel >= 0) allowedDifficulties.add('E');
if (userLevel >= 1) allowedDifficulties.add('M');
if (userLevel >= 2) allowedDifficulties.add('H');

// Try same tier first, then any allowed tier
for (const fallback of [false, true]) {
let q = service
// Try same tier first, then any tier. Health >= 40 filter mirrors filterAndRank.
for (const where of [{ difficulty: preferDifficulty }, null]) {
const q = service
Expand All @@ -348,6 +364,11 @@
.in('difficulty', where ? [where.difficulty] : allowedDifficulties)
.order('scored_at', { ascending: false })
.limit(50);
if (!fallback) {
q = q.eq('difficulty', preferDifficulty);
} else {
q = q.in('difficulty', Array.from(allowedDifficulties));
}
const { data: pool } = await q;
const pick = (pool ?? []).find((i) => !excludeIds.has(i.id));
if (!pick) continue;
Expand Down Expand Up @@ -448,3 +469,3 @@
await cacheDel(`recs:${user.id}`);
return ok({ id: data.id });
}
15 changes: 15 additions & 0 deletions src/lib/pipeline/recommend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ describe('filterAndRank', () => {
expect(result).toEqual([]);
});

it('falls back to other allowed tiers but respects max difficulty cap', () => {
const issues = [
issue({ id: 1, difficulty: 'H' }),
issue({ id: 2, difficulty: 'M' }),
issue({ id: 3, difficulty: 'E' }),
];
const result = filterAndRank(issues, {
level: 1,
excludeIssueIds: new Set(),
allowFallback: true,
});
expect(result).toHaveLength(2);
expect(result.map((r) => r.difficulty).sort()).toEqual(['E', 'M']);
});

it('does not fallback to higher difficulty tiers', () => {
const issues = [issue({ id: 1, difficulty: 'M' }), issue({ id: 2, difficulty: 'M' })];

Expand Down
8 changes: 8 additions & 0 deletions src/lib/pipeline/recommend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ export function filterAndRank(pool: readonly ScoredIssue[], opts: RecommendOptio
// Fallback: if any tier came up empty, optionally borrow from adjacent (only easier).
if (opts.allowFallback && result.length < totalDesired(mix)) {
const seen = new Set(result.map((r) => r.id));
const allowedDifficulties = new Set<string>();
if (opts.level >= 0) allowedDifficulties.add('E');
if (opts.level >= 1) allowedDifficulties.add('M');
if (opts.level >= 2) allowedDifficulties.add('H');

const extras = eligible
.filter((i) => !seen.has(i.id) && allowedDifficulties.has(i.difficulty))
.sort((a, b) => rankScore(b) - rankScore(a));
const allowedDifficulties = getAllowedDifficulties(opts.level);

const extras = eligible
Expand Down
Loading