Skip to content

feat(10-8): anti-cheat & frequency anti-repetition — basic item dedupe#66

Merged
Simplemart17 merged 5 commits into
mainfrom
feature/10-8-anti-cheat-frequency-anti-repetition
May 11, 2026
Merged

feat(10-8): anti-cheat & frequency anti-repetition — basic item dedupe#66
Simplemart17 merged 5 commits into
mainfrom
feature/10-8-anti-cheat-frequency-anti-repetition

Conversation

@Simplemart17

Copy link
Copy Markdown
Owner

Summary

  • Audit P3-9 + Epic 10.8 deliverable closed. Per-user MCQ + writing-prompt dedupe against the user's last 100 completed exercises at the same (skill, cefr_level). A B1 candidate doing 5 sequential reading drills no longer sees the same AI-generated question stems repeated across sessions.
  • Forward-only schema: new question_stem_hashes TEXT[] column on exercises via supabase/migrations/20260511000000_exercise_question_stem_hashes.sql. Pre-10-8 rows stay NULL; dedup helper treats NULL as "no hashes."
  • Shared djb2 hash extracted to src/lib/text-hash.ts so 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; private hashText deleted from realtime-transcript.ts + re-imported).
  • Retry-once-on-too-few-fresh contract for MCQ skills (runMcqDedupPipeline at temp 0.4 → filter → if < MIN_FRESH=4 retry 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.
  • Story 9-10 resilience: getSeenHashes returns empty set + breadcrumb on Supabase error — never blocks the user.
  • Speaking-task 3-day-bucket (Story 9-8) NOT touched — different anti-repetition contract; coexists.
  • Server-side item bank cache (Epic 13.x), mock-test section dedupe (Epic 17.x), cross-user item sharing, embedding-based semantic dedupe (Epic 11.6) all explicitly out of scope.

Story

Architecture (7 modules)

Layer Module Status
Shared hash src/lib/text-hash.ts NEW
Pure helpers + pipelines src/lib/exercise-dedup.ts NEW
Supabase I/O src/lib/exercise-dedup-db.ts NEW
Migration supabase/migrations/20260511000000_exercise_question_stem_hashes.sql NEW
Hook wiring src/hooks/use-exercise.ts MODIFIED (4 branches use pipelines; persistExercise gates on dedupExhausted flag)
Hash consumer src/lib/realtime-transcript.ts MODIFIED (private hashText deleted + re-imported)
Telemetry src/lib/sentry.ts MODIFIED (allowlist extended with 4 keys)

Review

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:

  • P1 (HIGH/ECH3): Writing-pipeline exhaustion was seed-poisoning the user's seen-set, causing permanent degradation for prolific writing users. Fixed via dedupExhausted flag on GeneratedExercise + gate in persistExercise.
  • P2 (HIGH/ECH1+ECH2): Added explicit exercise_type filter to getSeenHashes so cross-flow contamination (echo/dictation/translation) is structurally impossible.
  • P3 (MED/BH6+BH7+AA7): Strengthened mocked-Supabase test assertions — previously the completed=true filter + .limit(N) value were uninstrumented (tests passed for the wrong reason).
  • P4 (MED/AA1+BH19): Created the missing src/hooks/__tests__/use-exercise.test.ts integration test (AC feat(reading): update text styles for readability and consistency #7 had explicitly required it).
  • P11 (LOW/BH18+BH22): Dropped the unused GIN index from the migration (write amplification + non-CONCURRENTLY table lock concerns); deferred to Epic 13.x with CONCURRENTLY pattern.

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
  • CI Sentry DSN + Submit credentials leak guards ✓

Test plan

  • Apply migration: supabase db push (operator action; CLAUDE.md deploy substrate)
  • Smoke: complete 5 sequential listening exercises at the same CEFR level; verify question stems are visibly distinct (or that the retry path fires per Sentry breadcrumb)
  • Smoke: complete 5 sequential writing exercises at the same CEFR; verify prompts vary
  • Sentry: verify exercise-dedup-exhausted breadcrumbs appear when the AI's diversity ceiling is hit (expected for new users this will be rare)
  • Sentry: verify exercise-dedup-fetch breadcrumbs do NOT appear under normal use (only on Supabase outage)
  • Verify realtime-transcript.ts voice-conversation dedup (Story 9-5) is byte-identical post-hashText extraction (no behavior change in voice mode)
  • Verify Story 9-8 speaking task topic dedup (3-day-bucket) is unaffected (different module, different contract)

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).

…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
@Simplemart17 Simplemart17 merged commit ad3bf26 into main May 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant