git » alan.git » commit 621d861

Ensure global unique correct answers and tighten validation

author Alan Dipert
2025-12-04 16:02:46 UTC
committer Alan Dipert
2025-12-04 16:02:46 UTC
parent 423981e67394badd8bc6f4312b6c4244dad62452

Ensure global unique correct answers and tighten validation

property_tests.py +36 -1
test_generator.py +7 -0

diff --git a/property_tests.py b/property_tests.py
index 96747f5..5f6de13 100644
--- a/property_tests.py
+++ b/property_tests.py
@@ -156,7 +156,38 @@ def meanings_unique_in_options(q: Dict, spec) -> bool:
         m = to_meaning(sf)
         if any(meanings_equal(m, existing) for existing in meanings):
             return False
-        meanings.append(m)
+            meanings.append(m)
+    return True
+
+
+def unique_correct_answers(data: Dict, spec) -> bool:
+    """Ensure no correct answer surface or meaning repeats across the test."""
+    seen_meanings = []
+    seen_surfaces = set()
+    for sec in data.get("sections", []):
+        for q in sec.get("questions", []):
+            correct = next((o for o in q.get("options", []) if o.get("is_correct")), None)
+            if not correct:
+                return False
+            feat = correct.get("features")
+            if not feat:
+                return False
+            sf = SentenceFeatures(
+                subject=_np_from_dict(feat["subject"]),
+                obj1=_np_from_dict(feat["obj1"]),
+                obj2=_np_from_dict(feat["obj2"]) if feat.get("obj2") else None,
+                verb_id=feat["verb_id"],
+                tense=feat["tense"],
+                use_irregular_verb=feat.get("use_irregular_verb", True),
+            )
+            meaning = to_meaning(sf)
+            surface = realize_sentence(spec, sf)
+            if surface in seen_surfaces:
+                return False
+            if any(meanings_equal(meaning, m) for m in seen_meanings):
+                return False
+            seen_surfaces.add(surface)
+            seen_meanings.append(meaning)
     return True
 
 
@@ -444,6 +475,10 @@ def validate_data(data: Dict, spec=None, overrides: Dict[str, int] | None = None
                 ok = False
                 if not quiet:
                     print(f"FAIL prefix/scope for Q{q.get('number')} option {opt['label']}")
+    if not unique_correct_answers(data, spec):
+        ok = False
+        if not quiet:
+            print("FAIL unique correct answers across test")
     if not check_irregulars(
         data,
         spec,
diff --git a/test_generator.py b/test_generator.py
index 3b2ed6f..6b920f6 100644
--- a/test_generator.py
+++ b/test_generator.py
@@ -893,6 +893,8 @@ def generate_test(
     }
 
     unlocked: set[str] = set()
+    seen_correct_meanings = set()
+    seen_correct_surfaces = set()
     for section in blueprint.sections:
         unlocked |= set(section.introduce_concepts)
         constraints = section_constraints(unlocked)
@@ -928,9 +930,14 @@ def generate_test(
             correct_opt = next((o for o in q.options if o.is_correct), None)
             if correct_opt:
                 meaning = to_meaning(correct_opt.features)
+                surface = correct_opt.text
                 if meaning in section_meanings:
                     continue
+                if meaning in seen_correct_meanings or surface in seen_correct_surfaces:
+                    continue
                 section_meanings.add(meaning)
+                seen_correct_meanings.add(meaning)
+                seen_correct_surfaces.add(surface)
             # apply deltas only when item accepted
             for key, dec in delta.items():
                 remaining[key] = max(0, remaining.get(key, 0) - dec)