| author | Alan Dipert
<alan@dipert.org> 2025-12-04 16:02:46 UTC |
| committer | Alan Dipert
<alan@dipert.org> 2025-12-04 16:02:46 UTC |
| parent | 423981e67394badd8bc6f4312b6c4244dad62452 |
| 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)