fix(cefr): resolve authentic context sentence per term and repair legacy SRS cards

- Add resolve_context_sentence_for_term to replace placeholder blanks and lookup authentic sentences from cefr_questions or books
- Fix dual mistake ingestion in process_cefr_answer to assign distinct context to target and chosen distractor
- Provide authentic context_sentence in options_breakdown and pass it via data-context in cefr.js
- Add repair_mismatched_cefr_contexts in database.py to automatically repair cards with placeholder blanks
- Bump Service Worker cache to notes-v5.9
- Add test_cefr_context_resolution_and_repair covering resolution, dual ingestion, manual add, and DB repair
This commit is contained in:
Antigravity committed 2026-10-06 22:17:51 +03:00
1 parent 82b7a654ca
commit 3098d3016d
5 files changed
+230 -10

No files matched your search

+77 -7
View File
@@ -9,6 +9,7 @@ Features:
- Per-level progress tracking (mastered, learning, percentage).
"""
import re
import json
import sqlite3
import random
@@ -2039,6 +2040,64 @@ def get_cefr_quiz_batch(
return quiz_items
def resolve_context_sentence_for_term(
conn: sqlite3.Connection,
term: str,
candidate_sentence: str = "",
) -> str:
"""
Resolves the best authentic context sentence for a given term:
1. If candidate_sentence contains the term, use it (with ___ replaced).
2. Look up in cefr_questions specifically for this term.
3. Look up in books table for an authentic sentence containing this term.
4. Fallback to clean natural sentence.
"""
clean_term = term.strip()
if not clean_term:
return ""
# 1. If candidate_sentence already contains this term (case-insensitive word boundary)
if candidate_sentence:
cand_clean = candidate_sentence.strip()
if re.search(rf"\b{re.escape(clean_term)}\b", cand_clean, re.IGNORECASE):
return re.sub(r"\s+", " ", cand_clean.replace("___", clean_term)).strip()
# 2. Look up in cefr_questions specifically for this term
try:
q_row = conn.execute(
"SELECT sentence FROM cefr_questions WHERE lower(trim(term)) = lower(trim(?)) ORDER BY id ASC LIMIT 1",
(clean_term,),
).fetchone()
if q_row and q_row["sentence"]:
return re.sub(r"\s+", " ", q_row["sentence"].replace("___", clean_term)).strip()
except Exception:
pass
# 3. Look up in books table
try:
books = conn.execute("SELECT chapters_json FROM books ORDER BY id DESC LIMIT 10").fetchall()
for b in books:
try:
chapters = json.loads(b["chapters_json"])
except Exception:
continue
for ch in chapters:
content = ch.get("content", "")
if clean_term.lower() in content.lower():
for s in re.split(r"(?<=[.!?])\s+", content):
s_clean = s.strip()
if 20 <= len(s_clean) <= 180 and re.search(rf"\b{re.escape(clean_term)}\b", s_clean, re.IGNORECASE):
return re.sub(r"\s+", " ", s_clean).strip()
except Exception:
pass
# 4. If candidate_sentence had '___' and came from this term
if candidate_sentence and "___" in candidate_sentence:
return re.sub(r"\s+", " ", candidate_sentence.replace("___", clean_term)).strip()
return f"Using '{clean_term}' in meaningful sentences reinforces active recall and long-term fluency."
def process_cefr_answer(
conn: sqlite3.Connection,
user_id: int,
@@ -2105,9 +2164,10 @@ def process_cefr_answer(
if not is_correct:
# =====================================================================
# DUAL AUTOMATED GAP INGESTION (Crucial Pedagogical Requirement)
# 1. Correct target word -> goes 100% to vocabulary
# 2. Chosen wrong word -> also goes to vocabulary!
# 1. Correct target word -> goes 100% to vocabulary with its sentence
# 2. Chosen wrong word -> also goes to vocabulary with its OWN context!
# =====================================================================
target_ctx = resolve_context_sentence_for_term(conn, target_term, sentence_with_term)
target_card_info = _ingest_card_for_user(
conn,
user_id=user_id,
@@ -2115,7 +2175,7 @@ def process_cefr_answer(
translation=translation,
phonetic=phonetic,
pos=pos,
context_sentence=sentence_with_term,
context_sentence=target_ctx,
explanation=explanation,
level=level,
tag_suffix="test-mistake",
@@ -2130,6 +2190,7 @@ def process_cefr_answer(
sel_trans = sel_row["translation"] if sel_row else selected_clean
sel_phon = sel_row["phonetic"] if sel_row else ""
sel_pos = sel_row["pos"] if sel_row else ""
sel_ctx = resolve_context_sentence_for_term(conn, selected_clean)
selected_card_info = _ingest_card_for_user(
conn,
@@ -2138,7 +2199,7 @@ def process_cefr_answer(
translation=sel_trans,
phonetic=sel_phon,
pos=sel_pos,
context_sentence=sentence_with_term,
context_sentence=sel_ctx,
explanation=f"Выбрано по ошибке в вопросе про '{target_term}'",
level=level,
tag_suffix="test-mistake-chosen",
@@ -2193,11 +2254,14 @@ def process_cefr_answer(
if not is_correct and (is_target or was_selected):
is_auto_added = True
opt_ctx = resolve_context_sentence_for_term(conn, opt_clean, sentence_with_term if is_target else "")
options_breakdown.append({
"term": opt_clean,
"translation": opt_trans,
"phonetic": opt_phon,
"pos": opt_pos,
"context_sentence": opt_ctx,
"is_correct": is_target,
"was_selected": was_selected,
"in_library": bool(user_card) or is_auto_added,
@@ -2287,8 +2351,11 @@ def _ingest_card_for_user(
existing_tags.add(tag_suffix)
updated_tags = ", ".join(sorted(existing_tags))
updated_context = existing_card["context_sentence"] or ""
if context_sentence and context_sentence not in updated_context:
updated_context = (existing_card["context_sentence"] or "").strip()
# If existing context has "___" or lacks the term, overwrite with new authentic context!
if ("___" in updated_context or not re.search(rf"\b{re.escape(term_clean)}\b", updated_context, re.IGNORECASE)) and context_sentence:
updated_context = context_sentence
elif context_sentence and context_sentence not in updated_context:
updated_context = f"{updated_context}\n\n{context_sentence}".strip()
conn.execute(
@@ -2337,6 +2404,8 @@ def ingest_cefr_word(
clean_term = term.strip()
clean_level = level.upper().strip()
resolved_ctx = resolve_context_sentence_for_term(conn, clean_term, context_sentence)
row = conn.execute(
"SELECT term, translation, phonetic, pos FROM offline_dictionary WHERE term = ? COLLATE NOCASE",
(clean_term,),
@@ -2352,7 +2421,7 @@ def ingest_cefr_word(
translation=trans,
phonetic=phon,
pos=pos,
context_sentence=context_sentence,
context_sentence=resolved_ctx,
explanation=f"Добавлено вручную из вариантов вопроса теста CEFR ({clean_level})",
level=clean_level,
tag_suffix="manual-test-add",
@@ -2364,6 +2433,7 @@ def ingest_cefr_word(
"term": clean_term,
"translation": trans,
"phonetic": phon,
"context_sentence": resolved_ctx,
}