feat(10-8): anti-cheat & frequency anti-repetition — basic item dedupe#66
Merged
Simplemart17 merged 5 commits intoMay 11, 2026
Merged
Conversation
…module - text-hash.ts: NEW shared djb2 helper extracted from realtime-transcript.ts (Story 10-2 delete-don't-alias); single source of truth for Story 9-5 voice transcript dedup + Story 10-8 exercise dedup - exercise-dedup.ts: NEW pure helpers (normalizeQuestionStem with NFC + zero-width strip + locale-fr lowercase + whitespace collapse; hashQuestionStem; extractExerciseHashes for 4 skill shapes; filterUnseenQuestions; isWritingPromptSeen; runMcqDedupPipeline + runWritingDedupPipeline orchestrators); MIN_FRESH_QUESTIONS_PER_SKILL narrowed to MCQ-only Record (review P8) - exercise-dedup-db.ts: NEW Supabase I/O layer getSeenHashes with Story 9-10 resilience (empty set + Sentry breadcrumb on error); explicit exercise_type filter prevents cross-flow contamination (review P2) - realtime-transcript.ts: private hashText deleted, replaced with import from shared module - sentry.ts: SENTRY_EXTRAS_ALLOWLIST extended with generatedCount/filteredCount/seenCount/retries
…chema migration - use-exercise.ts: generateExercise fetches seenHashes once, then all 4 skill branches invoke runMcqDedupPipeline (listening/reading/grammar) or runWritingDedupPipeline (writing); on exhaustion fire captureError(_, "exercise-dedup-exhausted"); persistExercise insert payload includes question_stem_hashes (with P1/ECH3 anti-poisoning gate: writing-exhausted persists [] to avoid re-seeding the same dup) - migration 20260511000000_exercise_question_stem_hashes.sql: forward-only ADD COLUMN IF NOT EXISTS question_stem_hashes TEXT[]; GIN index intentionally omitted (review P11) — unnecessary for the dedup query path + would cause table lock during non-CONCURRENTLY migration; Epic 13.x will add it then via CREATE INDEX CONCURRENTLY
- text-hash.test.ts (NEW, 7 cases): empty-string seed, ASCII determinism, Unicode codepoint, surrogate-pair/emoji, idempotence, sentinel pin hashText("hello world")="eslcxt", collision-resistance smoke
- exercise-dedup.test.ts (NEW, ~43 cases): pure-helper coverage of normalizeQuestionStem (incl. zero-width strip P15), hashQuestionStem, extractExerciseHashes (4-skill defensive shape handling), filterUnseenQuestions, isWritingPromptSeen, MIN_FRESH_QUESTIONS_PER_SKILL MCQ-only Record (P8) + full runMcqDedupPipeline (5 cases) + runWritingDedupPipeline (3 cases) orchestration
- exercise-dedup-db.test.ts (NEW, ~14 cases): mocked-Supabase coverage with strengthened P3 assertions — happy-path 15-hash union + cross-row dedup + empty/null data + Supabase-error resilience + throw resilience + NULL column + non-array defensive guards + per-column .eq() filter pinning + .limit(N) value pinning + .order/select pinning + writing→free_write exercise_type mapping
- use-exercise.test.ts (NEW, 11 cases, review P4): persistExercise integration contract — listening/reading/grammar/writing insert payload shape + dedupExhausted-gate behavior (P1/ECH3 anti-poisoning verified)
- CLAUDE.md: add "TCF exercise anti-repetition" architecture line after Story 10-7 — documents shared text-hash module, exercise-dedup pure helpers + pipelines, getSeenHashes Supabase layer with Story 9-10 resilience, forward-only migration, persistExercise wiring with P1 dedupExhausted gate, Sentry telemetry allowlist, Story 9-x / 10-x invariants preserved; review-patch P7 polish: 2026-05-10 date stamp + 7-cases pin + use-exercise.test.ts reference clause - shippable-roadmap.md: P3-9 row + Epic 10.8 deliverable line marked DONE - 10-8 story file: status review; all checkboxes [x]; Dev Agent Record filled with implementation notes + Senior Developer Review section (9 patches applied: 2 HIGH P1/P2 + 4 MED P3/P4/P6/P8 + 3 LOW P7/P11/P15; 3 deferred; 15 rejected; 894 tests passing) - sprint-status.yaml: 10-8 review state
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
(skill, cefr_level). A B1 candidate doing 5 sequential reading drills no longer sees the same AI-generated question stems repeated across sessions.question_stem_hashes TEXT[]column onexercisesviasupabase/migrations/20260511000000_exercise_question_stem_hashes.sql. Pre-10-8 rows stay NULL; dedup helper treats NULL as "no hashes."src/lib/text-hash.tsso Story 9-5 voice transcript dedup (fallback_<hash>keys) + Story 10-8 exercise dedup (question_stem_hashes) share one source of truth (Story 10-2 delete-don't-alias pattern; privatehashTextdeleted fromrealtime-transcript.ts+ re-imported).runMcqDedupPipelineat temp 0.4 → filter → if< MIN_FRESH=4retry at temp 0.6 → fallback-on-exhaustion). Single-prompt regenerate-once for writing (runWritingDedupPipeline). On exhaustion:captureError(_, "exercise-dedup-exhausted", { skill, cefrLevel, generatedCount, filteredCount, seenCount, retries })so operator can see AI diversity ceiling in prod.getSeenHashesreturns empty set + breadcrumb on Supabase error — never blocks the user.Story
Architecture (7 modules)
src/lib/text-hash.tssrc/lib/exercise-dedup.tssrc/lib/exercise-dedup-db.tssupabase/migrations/20260511000000_exercise_question_stem_hashes.sqlsrc/hooks/use-exercise.tsdedupExhaustedflag)src/lib/realtime-transcript.tshashTextdeleted + re-imported)src/lib/sentry.tsReview
3-layer adversarial review (Blind Hunter / Edge Case Hunter / Acceptance Auditor) raised 33 findings (deduplicated to 27). Outcome: 9 review patches applied (2 HIGH + 4 MED + 3 LOW), 3 deferred, 15 rejected as noise / false positives / pre-existing. See
## Senior Developer Review (AI)section in the story file for full triage.Notable patches:
dedupExhaustedflag onGeneratedExercise+ gate inpersistExercise.exercise_typefilter togetSeenHashesso cross-flow contamination (echo/dictation/translation) is structurally impossible.completed=truefilter +.limit(N)value were uninstrumented (tests passed for the wrong reason).src/hooks/__tests__/use-exercise.test.tsintegration test (AC feat(reading): update text styles for readability and consistency #7 had explicitly required it).Quality gates
npm run type-check✓ (0 errors)npm run lint✓ (0 errors, 0 warnings)npm run format:check✓npm test✓ (894 passing, was 822 pre-story — +72 net)npm run check:colors✓Test plan
supabase db push(operator action; CLAUDE.md deploy substrate)exercise-dedup-exhaustedbreadcrumbs appear when the AI's diversity ceiling is hit (expected for new users this will be rare)exercise-dedup-fetchbreadcrumbs do NOT appear under normal use (only on Supabase outage)realtime-transcript.tsvoice-conversation dedup (Story 9-5) is byte-identical post-hashTextextraction (no behavior change in voice mode)Epic 10 closure
This is the last open story in Epic 10. After merge, Epic 10 (TCF Pedagogy Realignment) is fully implemented (10-1 through 10-8 all done). The path-to-shippable roadmap shifts to Epic 11 (AI Robustness & Cost Discipline).