import { expectedAnswerForQuestion } from '@/lib/gekanator' import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorQuestion, } from '@/lib/gekanator' import type { Post } from '@/types' export type RecoveredCandidatePost = { postId: number answerCountAtRecovery: number } export const candidatePostsFor = ({ posts, questions, answers, softenedQuestionIds, rejectedPostIds, recoveredCandidatePosts, }: { posts: Post[] questions: GekanatorQuestion[] answers: GekanatorAnswerLog[] softenedQuestionIds: Set rejectedPostIds: Set recoveredCandidatePosts: Map }): Post[] => { const questionById = new Map (questions.map (question => [question.id, question])) return posts.filter (post => { if (rejectedPostIds.has (post.id)) return false const answerCountAtRecovery = recoveredCandidatePosts.get (post.id) return answers.every ((answer, index) => { if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery) return true if (softenedQuestionIds.has (answer.questionId)) return true const question = questionById.get (answer.questionId) if (!(question)) return true switch (answer.answer) { case 'yes': case 'no': { const expected = expectedAnswerForQuestion (question, post) return expected === null || expected === 'unknown' || expected === answer.answer } default: return true } }) }) } export const hardFilteredPostsForAnswer = ({ posts, question, answer, }: { posts: Post[] question: GekanatorQuestion answer: GekanatorAnswerValue }): Post[] => { if (answer === 'unknown') return posts return posts.filter (post => { const expected = expectedAnswerForQuestion (question, post) return expected === null || expected === 'unknown' || expected === answer }) } const concreteAnswerOptions: GekanatorAnswerValue[] = [ 'yes', 'no', 'partial', 'probably_no'] export const allConcreteAnswerOptionsExhausted = ( posts: Post[], question: GekanatorQuestion | null, ): boolean => { if (!(question)) return false return concreteAnswerOptions.every (answer => hardFilteredPostsForAnswer ({ posts, question, answer }).length === 0) } const nextRecoveryBatchSize = (recoveryStepCount: number): number => 6 * (2 ** recoveryStepCount) export const recoverCandidatePosts = ({ posts, scores, rejectedPostIds, recoveredCandidatePosts, eligiblePostIds, answerCountAtRecovery, recoveryStepCount, }: { posts: Post[] scores: Map rejectedPostIds: Set recoveredCandidatePosts: Map eligiblePostIds: Set answerCountAtRecovery: number recoveryStepCount: number }): { recoveredCandidatePosts: Map recoveryStepCount: number } | null => { const recovered = new Map (recoveredCandidatePosts) const candidates = posts .filter (post => !(rejectedPostIds.has (post.id)) && !(eligiblePostIds.has (post.id)) && !(recovered.has (post.id))) .sort ((a, b) => (scores.get (b.id) ?? Number.NEGATIVE_INFINITY) - (scores.get (a.id) ?? Number.NEGATIVE_INFINITY)) .slice (0, nextRecoveryBatchSize (recoveryStepCount)) if (candidates.length === 0) return null candidates.forEach (post => recovered.set (post.id, answerCountAtRecovery)) return { recoveredCandidatePosts: recovered, recoveryStepCount: recoveryStepCount + 1 } }