| author | Alan Dipert
<alan@dipert.org> 2025-12-04 04:13:02 UTC |
| committer | Alan Dipert
<alan@dipert.org> 2025-12-04 04:13:02 UTC |
| parent | 7a21fc57efc6289e2dbbb0f076013dc2698364b8 |
| test_generator.py | +19 | -11 |
diff --git a/test_generator.py b/test_generator.py index a11dae6..1e94f3c 100644 --- a/test_generator.py +++ b/test_generator.py @@ -439,8 +439,12 @@ def _planned_features( difficulty: str, remaining: Dict[str, int], idx: int, -) -> SentenceFeatures: - """Greedy planner to satisfy coverage quotas deterministically with overlap.""" +) -> tuple[SentenceFeatures, Dict[str, int]]: + """Greedy planner to satisfy coverage quotas deterministically with overlap. + + Returns the planned SentenceFeatures and a delta dict for counters to apply only + if the resulting item is accepted. + """ subj_pool = [ np_features("man", AGENT, plural=False, adjectives=["tall"]), np_features("woman", AGENT, plural=False, adjectives=["tall"]), @@ -464,45 +468,46 @@ def _planned_features( verb_id = "see" tense = "PRES" use_irregular_verb = True + delta: Dict[str, int] = {"irregular_verb": 0, "irregular_noun": 0, "ditransitive": 0, "fem_plural": 0, "plural": 0, "adjective": 0} # 1) Irregular verb coverage (monotransitive chase past) if remaining.get("irregular_verb", 0) > 0: verb_id = "chase" tense = "PAST" use_irregular_verb = True - remaining["irregular_verb"] -= 1 + delta["irregular_verb"] = 1 # 2) Ditransitive coverage (can overlap with irregular noun) if verb_id != "chase" and remaining.get("ditransitive", 0) > 0: verb_id = "give" obj2 = theme_pool[idx % len(theme_pool)] - remaining["ditransitive"] -= 1 + delta["ditransitive"] = 1 # 3) Irregular noun coverage (can overlap with ditransitive) if remaining.get("irregular_noun", 0) > 0: obj1 = np_features("boy", RECIPIENT, plural=True, adjectives=["red"], use_irregular=True) - remaining["irregular_noun"] -= 1 + delta["irregular_noun"] = 1 # 4) fem plural receiver (if applicable) if remaining.get("fem_plural", 0) > 0 and obj1.noun_id in {"woman", "girl"}: obj1 = np_features(obj1.noun_id, RECIPIENT, feminine=True, plural=True, adjectives=obj1.adjectives) - remaining["fem_plural"] -= 1 + delta["fem_plural"] = 1 # 5) plural coverage (if still needed) if remaining.get("plural", 0) > 0 and not (obj1.plural or subj.plural or (obj2 and obj2.plural)): obj1 = np_features(obj1.noun_id, obj1.role, feminine=obj1.feminine, plural=True, adjectives=obj1.adjectives) - remaining["plural"] -= 1 + delta["plural"] = 1 # 6) adjective coverage if remaining.get("adjective", 0) > 0: if not obj1.adjectives: obj1 = np_features(obj1.noun_id, obj1.role, feminine=obj1.feminine, plural=obj1.plural, adjectives=["red"]) - remaining["adjective"] -= 1 + delta["adjective"] = 1 elif not subj.adjectives: subj = np_features(subj.noun_id, subj.role, feminine=subj.feminine, plural=subj.plural, adjectives=["tall"]) - remaining["adjective"] -= 1 + delta["adjective"] = 1 - return sentence_features(verb_id, tense, subj, obj1, obj2, use_irregular_verb=use_irregular_verb) + return sentence_features(verb_id, tense, subj, obj1, obj2, use_irregular_verb=use_irregular_verb), delta def _difficulty_score(sf: SentenceFeatures, irregular: bool) -> float: @@ -620,13 +625,16 @@ def generate_test( current_number = question_counter + len(questions) difficulty_tag = "early" if current_number <= 8 else "mid" if current_number <= 24 else "late" - sf_override = _planned_features(spec, rng, difficulty_tag, remaining, current_number) + sf_override, delta = _planned_features(spec, rng, difficulty_tag, remaining.copy(), current_number) q = generate_item( spec, section.focus_concepts, section.id, item_type, rng, difficulty=difficulty_tag, sf_override=sf_override ) # enforce invariants: one correct, 4 unique options if not question_valid(q, spec): continue + # apply deltas only when item accepted + for key, dec in delta.items(): + remaining[key] = max(0, remaining.get(key, 0) - dec) questions.append(q) item_map.append( {