コミットを比較

..

4 コミット

作成者 SHA1 メッセージ 日付
みてるぞ 62d0830aec #371 2026-06-16 23:18:11 +09:00
みてるぞ cda90b76d2 #371 2026-06-16 22:31:03 +09:00
みてるぞ 673a5dbd23 #371 2026-06-16 21:52:10 +09:00
みてるぞ ffebce36b9 #371 2026-06-16 00:34:48 +09:00
17個のファイルの変更616行の追加1577行の削除
-58
ファイルの表示
@@ -125,64 +125,6 @@ npm run preview
- TypeScript and TSX use 4-space logical indentation.
- In TypeScript and TSX only, replace every leading run of 8 spaces with a tab.
- Tabs are only for leading indentation, never for spaces after non-space text.
- TypeScript and TSX imports may stay on one line if they remain within the
line limit; do not expand short type-only imports mechanically.
- In TypeScript and TSX, when a function takes one destructured object
argument plus an inline type, prefer this shape when it fits locally:
```ts
const helper = (
{ value, flag }: { value: string
flag: boolean },
): Result => {
// ...
}
```
- In TypeScript and TSX, put `switch` case block braces on their own lines
when a case needs a lexical block:
```ts
case 'yes':
case 'no':
{
const expected = valueFor (item)
return expected == null || expected === answer
}
```
- In TypeScript and TSX, use `value == null` and `value != null` as the
default nullish checks. Do not use `=== null`, `=== undefined`,
`!== null`, or `!== undefined`.
- If code appears to need a distinction between `null` and `undefined`, treat
that as a design smell and revise the logic to avoid the distinction.
External library APIs that explicitly require distinguishing the two are the
only exception.
- In TypeScript and TSX, keep short arrays on one line when they fit under the
line limit; break arrays only when readability or line length requires it.
- In TypeScript and TSX, when a ternary expression is split across multiple
lines, align `?` and `:` with the condition expression. Do not indent `?` and
`:` one extra level under the condition.
```ts
const value =
condition
? consequent
: alternate
```
- In TypeScript and TSX, keep short ternary expressions on one line when they
fit cleanly under the line limit.
- In TypeScript and TSX, prefer ternary expressions for simple conditional
value selection. Do not replace a clear ternary with `if` statements, and do
not introduce immediately invoked functions just to avoid or reformat a
ternary expression.
- In TypeScript and TSX, do not write `let` followed by later `if` assignments
when the value can be expressed as a single `const` initializer. Prefer
`const` because it prevents accidental later reassignment.
- When fixing formatting, change formatting only. Do not change expression
structure, control flow, or variable mutability unless the requested style
explicitly requires it.
- Do not add production dependencies without explicit approval.
- Do not create, modify, or run tests unless the user explicitly asks for
test work. When the user asks for tests, keep working and rerun them until
+1 -1
ファイルの表示
@@ -50,7 +50,7 @@ class GekanatorGamesController < ApplicationController
questions,
post_id: game.correct_post_id,
user: current_user,
limit: 6)
limit: 2)
render json: {
questions: selected.map { |question| extra_question_json(question) }
-4
ファイルの表示
@@ -1,10 +1,6 @@
require 'rails_helper'
RSpec.describe TagNameSanitisationRule, type: :model do
before do
described_class.unscoped.delete_all
end
describe '.sanitise' do
before do
described_class.create!(priority: 10, source_pattern: '_', replacement: '')
+7 -103
ファイルの表示
@@ -206,40 +206,6 @@ RSpec.describe 'Gekanator learning API', type: :request do
expect(example.gekanator_game_id).to eq(json['id'])
end
it 'learns accepted post_similarity answers from main game logs' do
sign_in_as admin
question = create_post_similarity_question!(text: '泣いてる?')
expect {
post '/gekanator/games', params: {
guessed_post_id: guessed_post.id,
correct_post_id: correct_post.id,
answers: [
{
question_id: "post-similarity:#{question.id}",
question_text: '泣いてる?',
answer: 'partial',
original_answer: 'partial'
}
]
}
}.to change { GekanatorQuestionExample.count }.by(1)
expect(response).to have_http_status(:created)
expect(json['learned_example_count']).to eq(1)
example = GekanatorQuestionExample.last
expect(example).to have_attributes(
gekanator_question_id: question.id,
post_id: correct_post.id,
user_id: admin.id,
answer: 'partial',
source: 'post_game_answer'
)
expect(example.gekanator_game_id).to eq(json['id'])
end
it 'does not learn fact questions or nico tag questions from main game logs' do
sign_in_as admin
@@ -509,59 +475,28 @@ RSpec.describe 'Gekanator learning API', type: :request do
end
describe 'GET /gekanator/games/:id/extra_questions' do
it 'returns at most six accepted user_suggested post_similarity questions without duplicates' do
it 'returns at most two accepted user_suggested post_similarity questions without duplicates' do
sign_in_as admin
lowest = create_post_similarity_question!(
text: 'lowest?',
priority_weight: 0.5
)
low = create_post_similarity_question!(
text: 'low?',
priority_weight: 1.0
)
middle = create_post_similarity_question!(
text: 'middle?',
priority_weight: 1.5
)
medium_high = create_post_similarity_question!(
text: 'medium high?',
priority_weight: 2.0
)
high = create_post_similarity_question!(
text: 'high?',
priority_weight: 2.5
)
higher = create_post_similarity_question!(
text: 'higher?',
priority_weight: 2.8
)
highest = create_post_similarity_question!(
text: 'highest?',
priority_weight: 3.0
)
overflow = create_post_similarity_question!(
text: 'overflow?',
priority_weight: 2.2
middle = create_post_similarity_question!(
text: 'middle?',
priority_weight: 2.0
)
get "/gekanator/games/#{game.id}/extra_questions"
expect(response).to have_http_status(:ok)
expect(json['questions'].length).to eq(6)
expect(json['questions'].map { _1['id'] }.uniq.length).to eq(6)
expect(json['questions'].map { _1['id'] }).to all(
be_in([
lowest.id,
low.id,
middle.id,
medium_high.id,
high.id,
higher.id,
highest.id,
overflow.id,
])
)
expect(json['questions'].length).to eq(2)
expect(json['questions'].map { _1['id'] }.uniq.length).to eq(2)
expect(json['questions'].map { _1['id'] }).to all(be_in([low.id, high.id, middle.id]))
end
it 'can return questions that already have an example for the correct post' do
@@ -584,37 +519,6 @@ RSpec.describe 'Gekanator learning API', type: :request do
expect(json['questions'].map { _1['id'] }).to include(existing.id)
end
it 'prioritizes questions the current user has not answered' do
sign_in_as admin
answered = create_post_similarity_question!(
text: 'already answered?',
priority_weight: 3.0
)
GekanatorQuestionExample.create!(
gekanator_question: answered,
post: other_post,
user: admin,
answer: 'yes',
source: 'post_game_extra'
)
unanswered =
6.times.map { |index|
create_post_similarity_question!(
text: "unanswered #{index}?",
priority_weight: 0.5
)
}
get "/gekanator/games/#{game.id}/extra_questions"
expect(response).to have_http_status(:ok)
expect(json['questions'].map { _1['id'] }).to match_array(
unanswered.map(&:id)
)
end
it 'can return questions already asked in the game using snake_case question_id' do
sign_in_as admin
バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 559 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 146 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 1.2 MiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 188 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 201 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 196 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 179 KiB

+1 -37
ファイルの表示
@@ -4,7 +4,6 @@ import { apiPost } from '@/lib/api'
import {
buildGekanatorQuestions,
expectedAnswerForQuestion,
learnedSemanticSideForPost,
questionIdForCondition,
restoreGekanatorQuestion,
saveGekanatorExtraQuestionAnswers,
@@ -189,33 +188,6 @@ describe('expectedAnswerForQuestion', () => {
})
})
describe('learnedSemanticSideForPost', () => {
it('classifies post_similarity examples as positive, negative, or unknown', () => {
const question: StoredGekanatorQuestion = {
id: 'post-similarity:10',
text: '喜多ちゃんが泣いてる?',
kind: 'post_similarity',
source: 'user_suggested',
priorityWeight: 1.2,
condition: {
type: 'post-similarity',
postId: 123,
answer: 'partial',
threshold: 0.65,
},
exampleAnswers: {
1: 'yes',
2: 'probably_no',
},
}
expect(learnedSemanticSideForPost(question, post({ id: 1 }))).toBe('positive')
expect(learnedSemanticSideForPost(question, post({ id: 2 }))).toBe('negative')
expect(learnedSemanticSideForPost(question, post({ id: 3 }))).toBe('unknown')
expect(learnedSemanticSideForPost(question, post({ id: 123 }))).toBe('positive')
})
})
describe('restoreGekanatorQuestion', () => {
it('uses default source and priority weight when omitted', () => {
const question = restoreGekanatorQuestion({
@@ -276,7 +248,7 @@ describe('restoreGekanatorQuestion', () => {
})
expect(question.test(post({ id: 1 }))).toBe(true)
expect(question.test(post({ id: 2 }))).toBe(true)
expect(question.test(post({ id: 2 }))).toBe(false)
})
it('normalizes legacy title-length-greater-than questions', () => {
@@ -400,10 +372,6 @@ describe('Gekanator API writers', () => {
type: 'tag',
key: 'character:喜多郁代',
},
questionMode: 'normal',
questionPurpose: 'effective_user_suggested',
effectiveQuestion: true,
learningQuestion: false,
answer: 'yes',
originalAnswer: 'partial',
},
@@ -428,10 +396,6 @@ describe('Gekanator API writers', () => {
type: 'tag',
key: 'character:喜多郁代',
},
question_mode: 'normal',
question_purpose: 'effective_user_suggested',
effective_question: true,
learning_question: false,
answer: 'yes',
original_answer: 'partial',
},
+7 -54
ファイルの表示
@@ -9,24 +9,11 @@ export type GekanatorAnswerValue =
| 'probably_no'
| 'unknown'
export type LearnedSemanticSide =
| 'positive'
| 'negative'
| 'unknown'
export type GekanatorQuestionPurpose =
| 'effective_user_suggested'
| 'learning_user_suggested'
| 'normal'
export type GekanatorAnswerLog = {
questionId: string
questionText: string
questionCondition?: GekanatorQuestionCondition
questionMode?: 'normal' | 'winning_run'
questionPurpose?: GekanatorQuestionPurpose
effectiveQuestion?: boolean
learningQuestion?: boolean
answer: GekanatorAnswerValue
originalAnswer: GekanatorAnswerValue }
@@ -176,26 +163,6 @@ const directExampleAnswerFor = (
return null
}
export const isLearnedSemanticQuestion = (
question: StoredGekanatorQuestion | GekanatorQuestion,
): boolean =>
question.kind === 'post_similarity'
&& question.source === 'user_suggested'
export const learnedSemanticSideForAnswer = (
answer: GekanatorAnswerValue | null,
): LearnedSemanticSide => {
if (answer === 'yes' || answer === 'partial')
return 'positive'
if (answer === 'no' || answer === 'probably_no')
return 'negative'
return 'unknown'
}
const countBy = <T extends string | number> (values: T[]): Map<T, number> => {
const counts = new Map<T, number> ()
values.forEach (value => counts.set (value, (counts.get (value) ?? 0) + 1))
@@ -318,8 +285,8 @@ const questionMatches = (
): boolean => {
const directAnswer = directExampleAnswerFor (question, post)
if (directAnswer)
return question.kind === 'post_similarity'
? learnedSemanticSideForAnswer (directAnswer) === 'positive'
return question.condition.type === 'post-similarity'
? directAnswer === question.condition.answer
: directAnswer === 'yes'
switch (question.condition.type)
@@ -361,11 +328,6 @@ export const expectedAnswerForQuestion = (
switch (question.condition.type)
{
case 'post-similarity':
if (question.condition.postId === post.id)
return question.condition.answer
return null
case 'tag':
case 'source':
case 'original-year':
@@ -376,17 +338,12 @@ export const expectedAnswerForQuestion = (
case 'title-has-ascii':
case 'title-contains':
return questionMatches (post, question) ? 'yes' : 'no'
case 'post-similarity':
return null
}
}
export const learnedSemanticSideForPost = (
question: StoredGekanatorQuestion | GekanatorQuestion | undefined,
post: Post | null,
): LearnedSemanticSide =>
learnedSemanticSideForAnswer (expectedAnswerForQuestion (question, post))
export const restoreGekanatorQuestion = (
question: StoredGekanatorQuestion,
): GekanatorQuestion => {
@@ -466,15 +423,15 @@ export const buildGekanatorQuestions = (
const originalYears = countBy (
posts
.map (originalYearOf)
.filter ((year): year is number => year != null))
.filter ((year): year is number => year !== null))
const originalMonths = countBy (
posts
.map (originalMonthOf)
.filter ((month): month is number => month != null))
.filter ((month): month is number => month !== null))
const originalMonthDays = countBy (
posts
.map (originalMonthDayOf)
.filter ((monthDay): monthDay is string => monthDay != null))
.filter ((monthDay): monthDay is string => monthDay !== null))
const titleLengthMedian = median (posts.map (post => post.title?.length ?? 0))
const titleWordCounts =
includeTitleContains
@@ -636,10 +593,6 @@ export const saveGekanatorGame = async ({
question_id: answer.questionId,
question_text: answer.questionText,
question_condition: answer.questionCondition ?? null,
question_mode: answer.questionMode,
question_purpose: answer.questionPurpose,
effective_question: answer.effectiveQuestion,
learning_question: answer.learningQuestion,
answer: answer.answer,
original_answer: answer.originalAnswer })) })
+10 -23
ファイルの表示
@@ -11,7 +11,6 @@ import type {
GekanatorAnswerValue,
GekanatorQuestion,
} from '@/lib/gekanator'
import type { RecoveredCandidateState } from '@/lib/gekanatorCandidateRecovery'
import type { Post } from '@/types'
@@ -79,15 +78,6 @@ const answer = (
})
const recoveredState = (
answerCountAtRecovery: number,
scoreAtRecovery = 0,
): RecoveredCandidateState => ({
answerCountAtRecovery,
scoreAtRecovery,
})
describe('candidatePostsFor', () => {
it('does not hard-filter semantic post_similarity answers', () => {
const posts = [post (1), post (2), post (3)]
@@ -109,8 +99,8 @@ describe('candidatePostsFor', () => {
softenedQuestionIds: new Set (),
rejectedPostIds: new Set (),
recoveredCandidatePosts: new Map ([
[1, recoveredState (1)],
[3, recoveredState (1)],
[1, 1],
[3, 1],
]) })
expect(candidates.map (candidate => candidate.id)).toEqual ([1, 2, 3])
@@ -132,8 +122,8 @@ describe('candidatePostsFor', () => {
softenedQuestionIds: new Set (),
rejectedPostIds: new Set (),
recoveredCandidatePosts: new Map ([
[1, recoveredState (1)],
[3, recoveredState (1)],
[1, 1],
[3, 1],
]) })
expect(candidates.map (candidate => candidate.id)).toEqual ([3])
@@ -152,7 +142,7 @@ describe('candidatePostsFor', () => {
answers: [answer (question, 'yes')],
softenedQuestionIds: new Set (),
rejectedPostIds: new Set ([1]),
recoveredCandidatePosts: new Map ([[1, recoveredState (1)]]) })
recoveredCandidatePosts: new Map ([[1, 1]]) })
expect(candidates.map (candidate => candidate.id)).toEqual ([2])
})
@@ -219,7 +209,7 @@ describe('recoverCandidatePosts', () => {
posts,
scores,
rejectedPostIds: new Set ([10]),
recoveredCandidatePosts: new Map ([[8, recoveredState (1, 8)]]),
recoveredCandidatePosts: new Map ([[8, 1]]),
eligiblePostIds: new Set ([9]),
answerCountAtRecovery: 2,
recoveryStepCount: 0,
@@ -228,10 +218,7 @@ describe('recoverCandidatePosts', () => {
expect(recovered?.recoveryStepCount).toBe (1)
expect([...(recovered?.recoveredCandidatePosts.keys () ?? [])])
.toEqual ([8, 7, 6, 5, 4])
expect(recovered?.recoveredCandidatePosts.get (7)).toEqual ({
answerCountAtRecovery: 2,
scoreAtRecovery: 7,
})
expect(recovered?.recoveredCandidatePosts.get (7)).toBe (2)
})
it('does not add posts when recovered and eligible candidates already hit the target', () => {
@@ -243,9 +230,9 @@ describe('recoverCandidatePosts', () => {
scores,
rejectedPostIds: new Set (),
recoveredCandidatePosts: new Map ([
[1, recoveredState (1, 1)],
[2, recoveredState (1, 2)],
[3, recoveredState (1, 3)],
[1, 1],
[2, 1],
[3, 1],
]),
eligiblePostIds: new Set ([4, 5, 6]),
answerCountAtRecovery: 2,
+91 -79
ファイルの表示
@@ -1,49 +1,53 @@
import { isLearnedSemanticQuestion,
learnedSemanticSideForPost } from '@/lib/gekanator'
import { expectedAnswerForQuestion } from '@/lib/gekanator'
import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorQuestion } 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 }
answerCountAtRecovery: number }
const questionSupportsAnswerBasedHardFiltering = (question: GekanatorQuestion): boolean =>
!(isLearnedSemanticQuestion (question)
|| (question.kind === 'tag'
const questionIsFactLikeForHardFiltering = (
question: GekanatorQuestion,
): boolean =>
!(question.kind === 'post_similarity'
|| (
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<string>
rejectedPostIds: Set<number>
recoveredCandidatePosts: Map<number, RecoveredCandidateState> },
): Post[] => {
export const candidatePostsFor = ({
posts,
questions,
answers,
softenedQuestionIds,
rejectedPostIds,
recoveredCandidatePosts,
}: {
posts: Post[]
questions: GekanatorQuestion[]
answers: GekanatorAnswerLog[]
softenedQuestionIds: Set<string>
rejectedPostIds: Set<number>
recoveredCandidatePosts: Map<number, number>
}): 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)
const answerCountAtRecovery = recoveredCandidatePosts.get (post.id)
return answers.every ((answer, index) => {
if (recoveredCandidate != null && index < recoveredCandidate.answerCountAtRecovery)
if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery)
return true
if (softenedQuestionIds.has (answer.questionId))
@@ -52,19 +56,16 @@ export const candidatePostsFor = (
const question = questionById.get (answer.questionId)
if (!(question))
return true
if (!(questionSupportsAnswerBasedHardFiltering (question)))
if (!(questionIsFactLikeForHardFiltering (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')
}
case 'no': {
const expected = expectedAnswerForQuestion (question, post)
return expected === null || expected === 'unknown' || expected === answer.answer
}
default:
return true
}
@@ -73,27 +74,33 @@ export const candidatePostsFor = (
}
export const hardFilteredPostsForAnswer = (
{ posts, question, answer }: { posts: Post[]
question: GekanatorQuestion
answer: GekanatorAnswerValue },
): Post[] => {
if (!(questionSupportsAnswerBasedHardFiltering (question)))
export const hardFilteredPostsForAnswer = ({
posts,
question,
answer,
}: {
posts: Post[]
question: GekanatorQuestion
answer: GekanatorAnswerValue
}): Post[] => {
if (!(questionIsFactLikeForHardFiltering (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 expected = expectedAnswerForQuestion (question, post)
return expected === null || expected === 'unknown' || expected === answer
})
}
const concreteAnswerOptions: GekanatorAnswerValue[] = ['yes', 'no', 'partial', 'probably_no']
const concreteAnswerOptions: GekanatorAnswerValue[] = [
'yes',
'no',
'partial',
'probably_no']
export const allConcreteAnswerOptionsExhausted = (
@@ -112,48 +119,53 @@ const nextRecoveryTargetSize = (recoveryStepCount: number): number =>
6 * (2 ** recoveryStepCount)
export const recoverCandidatePosts = (
{ posts,
scores,
rejectedPostIds,
recoveredCandidatePosts,
eligiblePostIds,
answerCountAtRecovery,
recoveryStepCount }: { posts: Post[]
scores: Map<number, number>
rejectedPostIds: Set<number>
recoveredCandidatePosts: Map<number, RecoveredCandidateState>
eligiblePostIds: Set<number>
answerCountAtRecovery: number
recoveryStepCount: number },
): { recoveredCandidatePosts: Map<number, RecoveredCandidateState>
recoveryStepCount: number } | null => {
export const recoverCandidatePosts = ({
posts,
scores,
rejectedPostIds,
recoveredCandidatePosts,
eligiblePostIds,
answerCountAtRecovery,
recoveryStepCount,
}: {
posts: Post[]
scores: Map<number, number>
rejectedPostIds: Set<number>
recoveredCandidatePosts: Map<number, number>
eligiblePostIds: Set<number>
answerCountAtRecovery: number
recoveryStepCount: number
}): {
recoveredCandidatePosts: Map<number, number>
recoveryStepCount: number
} | null => {
const recovered = new Map (recoveredCandidatePosts)
const targetSize = nextRecoveryTargetSize (recoveryStepCount)
const countedPostIds = new Set ([...eligiblePostIds, ...recovered.keys ()])
const countedPostIds = new Set ([
...eligiblePostIds,
...recovered.keys ()])
const addCount = targetSize - countedPostIds.size
if (addCount <= 0)
{
return { recoveredCandidatePosts: recovered,
recoveryStepCount: recoveryStepCount + 1 }
}
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)))
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 }))
candidates.forEach (post => recovered.set (post.id, answerCountAtRecovery))
return { recoveredCandidatePosts: recovered,
recoveryStepCount: recoveryStepCount + 1 }
return {
recoveredCandidatePosts: recovered,
recoveryStepCount: recoveryStepCount + 1 }
}
-32
ファイルの表示
@@ -60,14 +60,6 @@ const gekanatorBackdropSource = gekanatorPageSource.slice (
gekanatorPageSource.indexOf ('const GekanatorBackdrop'),
gekanatorPageSource.indexOf ('const expectedAnswerFor'))
const gekanatorChooseQuestionSource = gekanatorPageSource.slice (
gekanatorPageSource.indexOf ('const chooseQuestion'),
gekanatorPageSource.indexOf ('const winningRunPriorityFor'))
const gekanatorFallbackQuestionSource = gekanatorPageSource.slice (
gekanatorPageSource.indexOf ('const chooseFallbackQuestion'),
gekanatorPageSource.indexOf ('const shouldEnterGuessPhase'))
describe('GekanatorBackdrop regression structure', () => {
it('keeps displayedBackdropMode as the render-time source of truth', () => {
@@ -111,30 +103,6 @@ describe('GekanatorBackdrop regression structure', () => {
})
describe('Gekanator question selection regression structure', () => {
it('prefers normal questions after user_suggested quota has been met', () => {
const normalFallbackIndex = gekanatorChooseQuestionSource.indexOf (
'else if (normalPool.length > 0)')
const effectiveFallbackIndex = gekanatorChooseQuestionSource.indexOf (
'else if (effectiveUserSuggestedPool.length > 0)')
expect(normalFallbackIndex).toBeGreaterThan(0)
expect(effectiveFallbackIndex).toBeGreaterThan(0)
expect(normalFallbackIndex).toBeLessThan(effectiveFallbackIndex)
})
it('does not let fallback questions bypass user_suggested purpose tracking', () => {
expect(gekanatorFallbackQuestionSource).toContain (
"question.source !== 'user_suggested'")
})
it('does not show a fixed extra-question count in the extra learning UI', () => {
expect(gekanatorPageSource).not.toContain ('追加で 2 問まで答えてください。')
expect(gekanatorPageSource).toContain ('追加で質問に答えてください。')
})
})
describe('isQuestionHardFilteredAfterAnswers', () => {
it('blocks only contradictory or redundant month questions after a yes answer', () => {
const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 }
ファイル差分が大きすぎるため省略します 差分を読込み