Skip to content
Open
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
20 changes: 20 additions & 0 deletions src/app/actions/recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,13 +309,16 @@
.from('profiles')
.select('level')
.eq('id', user.id)
.single();

.maybeSingle();

Check failure on line 314 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

Declaration or statement expected.
const userLevel = profile?.level ?? 0;

const replacement = await pickReplacement({
service,
userId: user.id,
preferDifficulty: data.difficulty as 'E' | 'M' | 'H',
level: userLevel,
userLevel,
});

Expand All @@ -327,17 +330,28 @@
service: NonNullable<ReturnType<typeof getServiceSupabase>>;
userId: string;
preferDifficulty: 'E' | 'M' | 'H';
level: number;
}): Promise<RecCard | null> {
const { service, userId, preferDifficulty, level } = args;
userLevel: number;
}): Promise<RecCard | null> {

Check failure on line 337 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

Declaration or statement expected.

Check failure on line 337 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

Declaration or statement expected.
const { service, userId, preferDifficulty, userLevel } = args;

Check failure on line 338 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

',' expected.

Check failure on line 338 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

':' expected.
const allowedDifficulties = getAllowedDifficulties(userLevel);

Check failure on line 339 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

',' expected.

Check failure on line 339 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

':' expected.

const { data: seen } = await service

Check failure on line 341 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

':' expected.
.from('recommendations')
.select('issue_id')
.eq('user_id', userId);

Check failure on line 344 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

',' expected.
const excludeIds = new Set((seen ?? []).map((r) => r.issue_id));

Check failure on line 345 in src/app/actions/recommendations.ts

View workflow job for this annotation

GitHub Actions / check

':' expected.

const allowedDifficulties = new Set<string>();
if (level >= 0) allowedDifficulties.add('E');
if (level >= 1) allowedDifficulties.add('M');
if (level >= 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 +362,12 @@
.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
17 changes: 17 additions & 0 deletions src/lib/pipeline/recommend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,23 @@ 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' })];
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