import { isLearnedSemanticQuestion, learnedSemanticSideForPost } from '@/lib/gekanator' import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorQuestion } from '@/lib/gekanator' import type { Post } from '@/types' export type RecoveredCandidatePost = { postId: number answerCountAtRecovery: number scoreAtRecovery: number } export type RecoveredCandidateState = { answerCountAtRecovery: number scoreAtRecovery: number } const questionSupportsAnswerBasedHardFiltering = (question: GekanatorQuestion): boolean => !(isLearnedSemanticQuestion (question) || (question.kind === 'tag' && question.condition.type === 'tag' && !(question.condition.key.startsWith ('nico:')))) 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 recoveredCandidate = recoveredCandidatePosts.get (post.id) return answers.every ((answer, index) => { if (recoveredCandidate != null && index < recoveredCandidate.answerCountAtRecovery) return true if (softenedQuestionIds.has (answer.questionId)) return true const question = questionById.get (answer.questionId) if (!(question)) return true if (!(questionSupportsAnswerBasedHardFiltering (question))) return true switch (answer.answer) { case 'yes': case 'no': { const expected = learnedSemanticSideForPost (question, post) return expected === 'unknown' || (answer.answer === 'yes' && expected === 'positive') || (answer.answer === 'no' && expected === 'negative') } default: return true } }) }) } export const hardFilteredPostsForAnswer = ( { posts, question, answer }: { posts: Post[] question: GekanatorQuestion answer: GekanatorAnswerValue }, ): Post[] => { if (!(questionSupportsAnswerBasedHardFiltering (question))) return posts if (!(answer === 'yes' || answer === 'no')) return posts return posts.filter (post => { const side = learnedSemanticSideForPost (question, post) return side === 'unknown' || (answer === 'yes' && side === 'positive') || (answer === 'no' && side === 'negative') }) } 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 nextRecoveryTargetSize = (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 targetSize = nextRecoveryTargetSize (recoveryStepCount) const countedPostIds = new Set ([...eligiblePostIds, ...recovered.keys ()]) const addCount = targetSize - countedPostIds.size if (addCount <= 0) { return { recoveredCandidatePosts: recovered, recoveryStepCount: recoveryStepCount + 1 } } 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, addCount) if (candidates.length === 0) return null candidates.forEach (post => recovered.set (post.id, { answerCountAtRecovery, scoreAtRecovery: scores.get (post.id) ?? 0 })) return { recoveredCandidatePosts: recovered, recoveryStepCount: recoveryStepCount + 1 } }