グカネータ / 質問パターン見直し (#41) (#365)

Reviewed-on: #365
Co-authored-by: miteruzo <miteruzo@naver.com>
Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #365 でマージされました.
このコミットが含まれているのは:
2026-06-12 01:35:31 +09:00
committed by みてるぞ
コミット def6870f06
14個のファイルの変更1077行の追加190行の削除
+151
ファイルの表示
@@ -0,0 +1,151 @@
import { describe, expect, it } from 'vitest'
import {
candidatePostsFor,
hardFilteredPostsForAnswer,
recoverCandidatePosts,
} from '@/lib/gekanatorCandidateRecovery'
import type {
GekanatorAnswerLog,
GekanatorAnswerValue,
GekanatorQuestion,
} from '@/lib/gekanator'
import type { Post } from '@/types'
const post = (id: number): Post => ({
id,
versionNo: 1,
url: `https://example.com/posts/${ id }`,
title: `post ${ id }`,
thumbnail: null,
thumbnailBase: null,
tags: [],
viewed: false,
related: [],
originalCreatedFrom: null,
originalCreatedBefore: null,
createdAt: '2026-06-10T00:00:00.000Z',
updatedAt: '2026-06-10T00:00:00.000Z',
uploadedUser: null,
})
const postSimilarityQuestion = (
id: string,
answers: Record<`${ number }`, GekanatorAnswerValue>,
): GekanatorQuestion => ({
id,
text: `${ id }?`,
kind: 'post_similarity',
condition: {
type: 'post-similarity',
postId: 9999,
answer: 'yes',
threshold: 0.65 },
source: 'user_suggested',
priorityWeight: 1,
exampleAnswers: answers,
test: candidate => answers[String (candidate.id) as `${ number }`] === 'yes',
})
const answer = (
question: GekanatorQuestion,
value: GekanatorAnswerValue,
): GekanatorAnswerLog => ({
questionId: question.id,
questionText: question.text,
questionCondition: question.condition,
answer: value,
originalAnswer: value,
})
describe('candidatePostsFor', () => {
it('lets recovered candidates ignore old answers but not later answers', () => {
const posts = [post (1), post (2), post (3)]
const oldQuestion = postSimilarityQuestion ('old', {
1: 'no',
2: 'yes',
3: 'yes',
})
const laterQuestion = postSimilarityQuestion ('later', {
1: 'no',
2: 'no',
3: 'yes',
})
const candidates = candidatePostsFor ({
posts,
questions: [oldQuestion, laterQuestion],
answers: [answer (oldQuestion, 'yes'), answer (laterQuestion, 'yes')],
softenedQuestionIds: new Set (),
rejectedPostIds: new Set (),
recoveredCandidatePosts: new Map ([
[1, 1],
[3, 1],
]) })
expect(candidates.map (candidate => candidate.id)).toEqual ([3])
})
it('does not let recovered candidates bypass explicit rejected posts', () => {
const posts = [post (1), post (2)]
const question = postSimilarityQuestion ('question', {
1: 'yes',
2: 'yes',
})
const candidates = candidatePostsFor ({
posts,
questions: [question],
answers: [answer (question, 'yes')],
softenedQuestionIds: new Set (),
rejectedPostIds: new Set ([1]),
recoveredCandidatePosts: new Map ([[1, 1]]) })
expect(candidates.map (candidate => candidate.id)).toEqual ([2])
})
})
describe('hardFilteredPostsForAnswer', () => {
it('returns zero candidates without falling back to the original pool', () => {
const posts = [post (1), post (2)]
const question = postSimilarityQuestion ('question', {
1: 'yes',
2: 'yes',
})
expect(hardFilteredPostsForAnswer ({
posts,
question,
answer: 'no',
})).toEqual ([])
})
})
describe('recoverCandidatePosts', () => {
it('recovers high-score non-rejected, non-eligible candidates in staged batches', () => {
const posts = Array.from ({ length: 10 }, (_value, index) => post (index + 1))
const scores = new Map (posts.map (candidate => [candidate.id, candidate.id]))
const recovered = recoverCandidatePosts ({
posts,
scores,
rejectedPostIds: new Set ([10]),
recoveredCandidatePosts: new Map ([[8, 1]]),
eligiblePostIds: new Set ([9]),
answerCountAtRecovery: 2,
recoveryStepCount: 0,
})
expect(recovered?.recoveryStepCount).toBe (1)
expect([...(recovered?.recoveredCandidatePosts.keys () ?? [])])
.toEqual ([8, 7, 6, 5, 4, 3, 2])
expect(recovered?.recoveredCandidatePosts.get (7)).toBe (2)
})
})