From 926bde7a862e45a926c99120c81098d9357597bb Mon Sep 17 00:00:00 2001 From: Asterios Raptis Date: Thu, 24 Sep 2026 14:04:20 +0200 Subject: [PATCH] chore(engine): pin 0.29.0 and leave the hint-length rule to the engine learn-content-engine 0.29.0 merged the two hint-length rules into one W-HINT-LENGTH (engine#186); the error-level copy in validate_content.py and its test go, in line with the template. The schema mirror is byte-identical between 0.28.0 and 0.29.0, so only the pin moves. ownership.json is the template's current copy. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/ownership.json | 3 - schema/engine-version.txt | 2 +- scripts/validate_content.py | 55 +------- tests/test_hint_length_rule.py | 235 --------------------------------- 4 files changed, 5 insertions(+), 290 deletions(-) delete mode 100644 tests/test_hint_length_rule.py diff --git a/.github/ownership.json b/.github/ownership.json index b0e1f75..eca8430 100644 --- a/.github/ownership.json +++ b/.github/ownership.json @@ -51,7 +51,6 @@ "tests/test_export_quiz_pdf.py": "template", "tests/test_from_cards_pairs_gate.py": "template", "tests/test_generate_exercises.py": "template", - "tests/test_hint_length_rule.py": "template", "tests/test_mc_only_exemption.py": "template", "tests/test_quality_state.py": "template", "tests/test_shape_parity.py": "template", @@ -96,8 +95,6 @@ "tests/test_export_set.py": "repo", "tests/test_hub_generated_sets.py": "repo", "tests/test_ki_einsteiger_ai_validated.py": "repo", - "tests/test_known_domain.py": "repo", - "tests/test_prompt_duplication_rule.py": "repo", "tests/test_readme_set_table.py": "repo", "schema/content-manifest.schema.json": "engine", "schema/lesson.schema.json": "engine", diff --git a/schema/engine-version.txt b/schema/engine-version.txt index 697f087..ae6dd4e 100644 --- a/schema/engine-version.txt +++ b/schema/engine-version.txt @@ -1 +1 @@ -0.28.0 +0.29.0 diff --git a/scripts/validate_content.py b/scripts/validate_content.py index 5b49e19..088464f 100644 --- a/scripts/validate_content.py +++ b/scripts/validate_content.py @@ -56,45 +56,10 @@ ISO_639_1 = re.compile(r"^[a-z]{2}$") -# Answer-length statements in exercise/blank hints (adaptive-learner-content#100). -# The app shows the answer's length automatically (the system hint), so an -# authored hint stating a letter/character count ("Vier Buchstaben.") is -# redundant at best and contradicts the system hint when it is wrong. Card -# hints are NOT covered: a character count there can be legitimate teaching -# content (e.g. explaining that ``s[0:3]`` yields 3 characters). Compounds -# like "Leerzeichen" do not match (no word boundary inside the compound), -# so indentation advice passes. -# -# The count words also cover the English number words (one to twelve; -# hyphenated adjectives like "five-letter" match via the ``[-\s]+`` joiner) -# and the single-character adjectives ("ein einzelnes Zeichen", "a single -# character" state answer length 1), adaptive-learner-content#102. Compounds -# still pass: "Ein einzelner Kleinbuchstabe" conveys CASE, which the system -# hint does not show, and "the letter she wrote" has no count word before the -# noun. -_HINT_COUNT_WORDS = ( - r"\d+|ein(?:e[nmrs]?)?|zwei|drei|vier|f(?:ü|ue)nf|sechs|sieben|acht|neun" - r"|zehn|elf|zw(?:ö|oe)lf" - r"|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve" - r"|einzeln\w*|single" -) -HINT_LENGTH_PATTERN = re.compile( - rf"\b(?:{_HINT_COUNT_WORDS})[-\s]+(?:buchstaben?|zeichen|letters?|characters?)\b" - r"|\w*buchstabig", - re.IGNORECASE, -) - - -def hint_states_answer_length(hint: object) -> bool: - """True when an authored hint states the answer's letter/character count. - - Matches a digit, a German or English number word, or a single-character - adjective ("einzeln..."/"single") followed by "Buchstabe(n)"/"Zeichen"/ - "letter(s)"/"character(s)" (plus the "-buchstabig" adjectives). Applied to - exercise-level and blank-level hints only - see the note on - ``HINT_LENGTH_PATTERN``. - """ - return isinstance(hint, str) and bool(HINT_LENGTH_PATTERN.search(hint)) +# Answer-length statements in hints are the engine's rule: W-HINT-LENGTH +# (learn-content-engine#186, since 0.29.0) checks exercise and blank hints with +# the forms this validator used to check, and reports them in the engine gate's +# warning step. Content rules belong to the engine, not to a second copy here. # Scripts we can distinguish from Latin (mirror the TS validator). SCRIPT_RANGES = { @@ -285,18 +250,6 @@ def validate_lesson_quality(lesson: dict, source: str, label: str, errors: list[ for ex in exercises: eid = ex.get("id", "?") - if hint_states_answer_length(ex.get("hint")): - errors.append( - f"{label}: exercise '{eid}' hint states a letter/character count " - "(redundant to the app's automatic length hint) - use a content hint" - ) - for blank_index, blank in enumerate(ex.get("blanks") or []): - if isinstance(blank, dict) and hint_states_answer_length(blank.get("hint")): - errors.append( - f"{label}: exercise '{eid}' blanks[{blank_index}] hint states a " - "letter/character count (redundant to the app's automatic " - "length hint) - use a content hint" - ) if ex.get("type") == "free_text": if len(ex.get("accept") or []) < MIN_FREE_TEXT_ACCEPTS: errors.append(f"{label}: free_text '{eid}' needs >= {MIN_FREE_TEXT_ACCEPTS} accepts") diff --git a/tests/test_hint_length_rule.py b/tests/test_hint_length_rule.py deleted file mode 100644 index d669965..0000000 --- a/tests/test_hint_length_rule.py +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env python3 -"""Quality rule: no answer-length statements in exercise/blank hints (#100). - -The app shows the answer's length automatically (the system hint), so an -authored hint stating a letter/character count ("Vier Buchstaben.") is -redundant at best - and drifts into visible contradiction when the content -changes (the DSGVO cloze said "Vier Buchstaben." for a five-letter answer). - -Scope: the rule covers ``exercise.hint`` and ``exercise.blanks[].hint`` - -the surfaces the app pairs with its automatic length hint. Card hints are -deliberately NOT covered: a character count there can be legitimate teaching -content (e.g. the python-basics slicing card explains that ``s[0:3]`` -yields 3 characters). -""" -from __future__ import annotations - -import sys -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[1] -SCRIPTS_DIR = REPO_ROOT / "scripts" - -# validate_content imports its sibling generate_search_index, so the scripts/ -# directory must be importable as a top-level package root. -if str(SCRIPTS_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPTS_DIR)) - -import validate_content as vc # noqa: E402 - - -def quality_errors(lesson: dict) -> list[str]: - errors: list[str] = [] - vc.validate_lesson_quality(lesson, "de", "", errors) - return [e for e in errors if "length" in e or "Länge" in e or "count" in e] - - -def lesson_with_exercise(exercise: dict, cards: list | None = None) -> dict: - return { - "id": "l1", - "title": "T", - "cards": cards or [], - "steps": [ - {"id": "s1", "type": "theory", "title": "Th", "body": "b"}, - {"id": "s2", "type": "exercise", "title": "Ex", "exercise": exercise}, - ], - } - - -def cloze(hint: str | None = None, blanks: list | None = None) -> dict: - return { - "id": "e1", - "type": "cloze", - "prompt": "p", - "card_ids": [], - "sentence": "a ___ b", - "blanks": blanks if blanks is not None else [{"accept": ["x"]}], - "cloze_mode": "type", - **({"hint": hint} if hint is not None else {}), - } - - -# --- 1. Reproduction (the DSGVO shape: a bare letter-count hint) ----------- - - -def test_letter_count_hint_is_flagged() -> None: - errors = quality_errors(lesson_with_exercise(cloze(hint="Vier Buchstaben."))) - assert errors, "a letter-count hint must be reported as a quality error" - - -def test_character_count_hint_is_flagged() -> None: - errors = quality_errors( - lesson_with_exercise(cloze(hint="Zwei Zeichen, das zweite klein.")) - ) - assert errors - - -# --- 2. Happy path: content hints pass -------------------------------------- - - -def test_content_hint_passes() -> None: - errors = quality_errors( - lesson_with_exercise( - cloze(hint="Abkürzung für die Datenschutz-Grundverordnung.") - ) - ) - assert errors == [] - - -def test_absent_hint_passes() -> None: - assert quality_errors(lesson_with_exercise(cloze())) == [] - - -# --- 3. Edge cases: rule scope ---------------------------------------------- - - -def test_blank_level_hint_is_flagged() -> None: - exercise = cloze( - blanks=[{"accept": ["x"], "hint": "Genau drei Buchstaben tippen."}] - ) - errors = quality_errors(lesson_with_exercise(exercise)) - assert errors - - -def test_card_hint_with_character_count_is_not_flagged() -> None: - """Card hints may talk about character counts (teaching content).""" - cards = [ - { - "id": "c1", - "front": "String-Slicing", - "back": "Teilstring", - "hint": "Der stop-Index ist ausschließend - s[0:3] liefert 3 Zeichen, nicht 4.", - } - ] - errors = quality_errors(lesson_with_exercise(cloze(), cards=cards)) - assert errors == [] - - -def test_free_text_hint_is_flagged_too() -> None: - """The rule covers every exercise type, not just cloze.""" - exercise = { - "id": "e1", - "type": "free_text", - "prompt": "p", - "card_ids": [], - "accept": ["Hola", "hola"], - "distractors": ["Adios"], - "hint": "Vier Buchstaben, das H ist stumm.", - } - errors = quality_errors(lesson_with_exercise(exercise)) - assert errors - - -# --- 4. Boundary: digit form, article form, and compound-word non-matches --- - - -def test_digit_count_is_flagged() -> None: - errors = quality_errors(lesson_with_exercise(cloze(hint="Nur 8 Zeichen lang."))) - assert errors - - -def test_article_count_form_is_flagged() -> None: - errors = quality_errors( - lesson_with_exercise(cloze(hint="Eine Kurzform mit einem Buchstaben.")) - ) - assert errors - - -def test_compound_leerzeichen_is_not_flagged() -> None: - """"vier Leerzeichen" is indentation advice, not an answer length.""" - errors = quality_errors( - lesson_with_exercise(cloze(hint="Einrückung: vier Leerzeichen pro Ebene.")) - ) - assert errors == [] - - -# --- 5. English count words + single-character adjectives (adaptive-learner-content#102) -# -# The #100 rule listed only German number words (+ digits), so English -# length hints ("Two letters.") and the German "ein einzelnes Zeichen" -# form (= answer length 1) slipped through the sweep. - - -def test_english_letter_count_is_flagged() -> None: - errors = quality_errors(lesson_with_exercise(cloze(hint="Two letters."))) - assert errors, "an English letter-count hint must be reported" - - -def test_english_count_inside_sentence_is_flagged() -> None: - errors = quality_errors( - lesson_with_exercise( - cloze(hint="Two short words; the first is two letters long.") - ) - ) - assert errors - - -def test_english_count_with_content_tail_is_flagged() -> None: - errors = quality_errors( - lesson_with_exercise(cloze(hint="Four letters, starts with C.")) - ) - assert errors - - -def test_english_hyphenated_count_adjective_is_flagged() -> None: - errors = quality_errors(lesson_with_exercise(cloze(hint="A five-letter word."))) - assert errors - - -def test_single_character_form_is_flagged() -> None: - errors = quality_errors( - lesson_with_exercise(cloze(hint="A single character is enough.")) - ) - assert errors - - -def test_einzelnes_zeichen_form_is_flagged() -> None: - errors = quality_errors( - lesson_with_exercise(cloze(hint="Ein einzelnes Zeichen, kein Doppelzeichen.")) - ) - assert errors - - -def test_english_letter_as_mail_is_not_flagged() -> None: - """"letter" in the postal sense must pass (no count word before it).""" - errors = quality_errors( - lesson_with_exercise(cloze(hint="Object pronoun: *The letter she wrote…*")) - ) - assert errors == [] - - -def test_compound_kleinbuchstabe_is_not_flagged() -> None: - """Compounds convey CASE, not length: the system hint shows no case.""" - errors = quality_errors( - lesson_with_exercise( - cloze(hint="Ein einzelner Kleinbuchstabe, für 'general'.") - ) - ) - assert errors == [] - - -def test_first_letter_content_hint_is_not_flagged() -> None: - """Naming the FIRST letter is content, not a length statement.""" - errors = quality_errors( - lesson_with_exercise(cloze(hint="Erster Buchstabe von 'write'.")) - ) - assert errors == [] - - -def test_capitalisation_advice_is_not_flagged() -> None: - errors = quality_errors( - lesson_with_exercise( - cloze(hint="Montag: im Englischen IMMER mit großem Anfangsbuchstaben.") - ) - ) - assert errors == []