"""Phase A guards: shape, invention, no dropped units.""" from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) from app.pipeline.alignment import iter_source_units from app.pipeline.candidate_validator import validate_candidate from app.pipeline.grammar_fix import correct_text from app.pipeline.orchestrator import _accept_lm_unit, _looks_needs_lm, rewrite_text def test_rejects_multi_sentence_invention() -> None: src = ( "Eating fast foods is becoming very common and people don't realize " "how much it affects their health." ) bad = ( "Fast food is becoming increasingly prevalent, and many individuals aren't " "aware of its detrimental impact on their health due to a lack of awareness " "and education." ) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "invention" in v.reasons or "shape" in v.reasons or "length" in v.reasons ok, reasons = _accept_lm_unit(src, bad) assert not ok print("multi-sentence/invention reject OK:", v.reasons, reasons) def test_rejects_two_sentence_for_one() -> None: src = "Exercise is one of the most important activities that keeps our body fit." bad = ( "In addition to our exercise routines, keeping our bodies fit is also crucial. " "Exercise remains essential." ) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "shape" in v.reasons print("shape reject OK") def test_skip_short_and_light() -> None: assert not _looks_needs_lm("Yes it is.", 1) # <4 words assert not _looks_needs_lm( "Nowadays many people are living an unhealthy life because they don't have enough time.", 0, ) assert _looks_needs_lm( "Nowadays many people are living an unhealthy life because they don't have enough time.", 1, ) # Short claims must still go to LM at Normal (was ≥8 — grammar-only bug) assert _looks_needs_lm("Another important thing is sleeping.", 1) assert _looks_needs_lm("Without enough sleep, the brain works poorly.", 1) print("skip heuristics OK") def test_sleep_style_must_visibly_rewrite() -> None: """User expectation: not grammar-only; wording must clearly change.""" import random from unittest.mock import patch from app.pipeline.orchestrator import ( _is_true_rewrite, _require_visible_rewrite, _similarity_ratio, _stem_jaccard, ) rng = random.Random(7) src = "Another important thing is sleeping." out = _require_visible_rewrite(src, src, "Neutral", rng, target_sim=0.85) assert _is_true_rewrite(src, out), out assert _similarity_ratio(src, out) < 0.90 assert _stem_jaccard(src, out) < 0.92 assert "sleep" in out.lower() print("sleep opener rewrite OK:", out) src2 = "Without enough sleep, the brain." out2 = _require_visible_rewrite(src2, src2, "Neutral", rng, target_sim=0.85) assert _is_true_rewrite(src2, out2), out2 assert "sleep" in out2.lower() print("without-enough rewrite OK:", out2) raw = "Another important thing are sleeping. Without enough sleep, the brain" g = correct_text(raw) with patch( "app.pipeline.orchestrator.rewrite_unit", side_effect=lambda u, **_k: u, ): from app.pipeline.orchestrator import _hybrid_rewrite out, _stats, _ = _hybrid_rewrite( g, "Neutral", 1, rng, can_generate=True ) assert _similarity_ratio(g, out) < 0.90 assert "sleep" in out.lower() # Must not be grammar-only near-copy of the classic paste assert "another important thing is sleeping" not in out.lower() print("sleep document hybrid rewrite OK:", out) def test_exercise_tv_must_not_be_reorder_only() -> None: """Exercise/TV sample should change via reorder and/or wording; keep claims.""" import random from unittest.mock import patch from app.pipeline.orchestrator import ( _hybrid_rewrite, _is_true_rewrite, _similarity_ratio, ) raw = ( "Exercise is one of the most important activity that keeps our body fit. " "If a person exercise regularly, they will have less chance to gets diseases. " "Unfortunately, many peoples prefers watching television instead of doing " "physical activities." ) g = correct_text(raw) rng = random.Random(11) with patch( "app.pipeline.orchestrator.rewrite_unit", side_effect=lambda u, **_k: u, ): out, stats, _ = _hybrid_rewrite(g, "Neutral", 1, rng, can_generate=True) low = out.lower() assert out.strip() != g.strip() assert "tv" in low or "television" in low assert "fit" in low or "exercise" in low or "work out" in low or "workout" in low from app.pipeline.alignment import iter_source_units src_units = [u for _i, u in iter_source_units(g)] out_units = [u for _i, u in iter_source_units(out)] assert len(out_units) == len(src_units) true_n = sum(1 for s, o in zip(src_units, out_units) if _is_true_rewrite(s, o)) assert true_n >= max(1, len(src_units) - 1), (true_n, out) print("exercise/TV rewrite OK:", out) print(" stats", stats) _ = _similarity_ratio # kept for debugging imports def test_complete_wording_rewrite_not_shuffle() -> None: """User expects structural reorder, e.g. time/manner fronting.""" import random from app.pipeline.orchestrator import ( _is_structural_reorder, _is_true_rewrite, _require_visible_rewrite, _sentence_reorder_variants, _try_restructure_unit, ) from app.pipeline.restructure import restructure_sentence src = "Ram went to school yesterday happily." expected_style = "Yesterday, Ram happily went to school." assert _is_true_rewrite(src, expected_style), "reorder must count as rewrite" assert _is_structural_reorder(src, expected_style) rr = restructure_sentence(src) assert rr is not None, "restructure_sentence must handle Ram" assert _is_structural_reorder(src, rr.text), rr.text assert "yesterday" in rr.text.lower() assert "happily" in rr.text.lower() assert "headed" not in rr.text.lower() assert "cheerfully" not in rr.text.lower() assert "day before" not in rr.text.lower() try_out = _try_restructure_unit(src) assert try_out is not None assert _is_structural_reorder(src, try_out), try_out variants = _sentence_reorder_variants(src) assert any("yesterday, ram happily went to school" in v.lower() for v in variants), variants out = _require_visible_rewrite( src, src, "Neutral", random.Random(5), target_sim=0.95 ) assert _is_true_rewrite(src, out), out assert _is_structural_reorder(src, out), out assert "ram" in out.lower() and "school" in out.lower() assert "yesterday" in out.lower() assert "happily" in out.lower() assert "went" in out.lower() # Must not synonym-thrash the Ram sentence assert "headed" not in out.lower() assert "cheerfully" not in out.lower() assert "day before" not in out.lower() print("Ram sentence reorder OK:", out) def test_restructure_time_only_and_manner() -> None: """Additional reorder templates: time front; manner before verb.""" from app.pipeline.orchestrator import ( _is_structural_reorder, _try_restructure_unit, ) from app.pipeline.restructure import restructure_sentence # Time-only → front time src_time = "She finished the report yesterday." rr_time = restructure_sentence(src_time) assert rr_time is not None, "time-only restructure" assert rr_time.text.lower().startswith("yesterday"), rr_time.text assert _is_structural_reorder(src_time, rr_time.text) or "yesterday" in rr_time.text.lower() assert "finished" in rr_time.text.lower() and "report" in rr_time.text.lower() print("time-only reorder OK:", rr_time.text) # Manner + place → manner before verb src_manner = "They walked to the park slowly." out_m = _try_restructure_unit(src_manner) or ( restructure_sentence(src_manner).text if restructure_sentence(src_manner) else None ) assert out_m is not None, "manner reorder" low = out_m.lower() assert "slowly" in low and "park" in low and "walked" in low # Manner should appear before the verb when template applies if "slowly walked" in low or low.index("slowly") < low.index("walked"): pass else: # Accept time/place template variants as long as words preserved assert _is_structural_reorder(src_manner, out_m) or set( w for w in low.replace(".", "").split() if len(w) > 2 ) >= {"they", "walked", "park", "slowly"} print("manner reorder OK:", out_m) # Subject manner verb place style src_happy = "Maya drove to work quickly." rr_h = restructure_sentence(src_happy) assert rr_h is not None assert "maya" in rr_h.text.lower() and "quickly" in rr_h.text.lower() assert "work" in rr_h.text.lower() print("manner+place reorder OK:", rr_h.text) def test_require_visible_keeps_structural_reorder() -> None: """Existing structural reorder must not be synonym-polished away.""" import random from app.pipeline.orchestrator import ( _is_structural_reorder, _require_visible_rewrite, ) src = "Ram went to school yesterday happily." cand = "Yesterday, Ram happily went to school." out = _require_visible_rewrite( src, cand, "Neutral", random.Random(1), target_sim=0.5 ) assert _is_structural_reorder(src, out), out assert "headed" not in out.lower() assert "cheerfully" not in out.lower() print("keep structural reorder OK:", out) def test_rejects_degree_adv_junk_reorder() -> None: """Do not ship 'more makes' style degree-adverb reorders.""" from app.pipeline.orchestrator import _try_restructure_unit from app.pipeline.restructure import restructure_sentence src = "Online learning makes education more accessible for students in remote areas." rr = restructure_sentence(src) if rr is not None: assert "more makes" not in rr.text.lower(), rr.text try_out = _try_restructure_unit(src) if try_out is not None: assert "more makes" not in try_out.lower(), try_out print("degree-adv junk reorder rejected OK:", rr, try_out) def test_because_front_preserves_clauses() -> None: from app.pipeline.restructure import restructure_sentence src = ( "Nowadays many people are living an unhealthy life " "because they don't have enough time." ) rr = restructure_sentence(src) assert rr is not None, "because_front should apply" assert rr.template_id == "because_front" low = rr.text.lower() assert low.startswith("because") assert "unhealthy" in low and "enough time" in low assert "nowadays" in low print("because_front OK:", rr.text) def test_esl_wording_avoids_synonym_thrash() -> None: import random from app.pipeline.orchestrator import _require_visible_rewrite sleep = "Sleeping is still an important thing." out_s = _require_visible_rewrite( sleep, sleep, "Neutral", random.Random(3), target_sim=0.95 ) low_s = out_s.lower() assert "endeavor" not in low_s and "nevertheless" not in low_s assert "sleep" in low_s or "sleeping" in low_s print("sleep wording OK:", out_s) climate = "Climate change is causing more extreme weather events around the world." out_c = _require_visible_rewrite( climate, climate, "Neutral", random.Random(7), target_sim=0.95 ) low_c = out_c.lower() assert "uttermost" not in low_c and "upwind" not in low_c assert "climate" in low_c and "weather" in low_c print("climate wording OK:", out_c) def test_rejects_hollow_ons_bills() -> None: src = ( "People should save money for emergencies instead of spending every paycheck." ) bad = ( "People should save money for emergencies instead of spending it ONS bills or bills." ) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "broken" in v.reasons or "key_content" in v.reasons print("hollow ONS bills reject OK:", v.reasons) def test_assembly_preserves_unit_count() -> None: raw = ( "Nowadays many peoples are living unhealthy life because they don't have enough times. " "Eating fast foods are becoming very common and peoples don't realizes how much it affect their health.\n\n" "Exercise is one of the most important activity that keeps our body fit. " "If a person exercise regularly, they will have less chance to gets diseases. " "Unfortunately, many peoples prefers watching television instead of doing physical activities" ) source = correct_text(raw) n_in = len(iter_source_units(source)) r = rewrite_text(raw, tone="Neutral", strength=1, ml_polish=False) # Classical path still grammar-fixes; count sentences in output vs source units n_out = len(iter_source_units(r.text)) assert n_out >= n_in - 1 # allow minor splitter variance after grammar assert "television" in r.text.lower() or "physical" in r.text.lower() print("unit preservation OK in=", n_in, "out=", n_out) def test_live_bug_patterns_rejected() -> None: src = ( "Nowadays many people are living an unhealthy life because they don't have enough time." ) invented = ( "Many people these days live unfulfilling unhealthy lives due to lack of " "awareness and education." ) v = validate_candidate(src, invented, min_meaning=0.5) assert not v.ok assert "invention" in v.reasons print("live-bug invention OK") def test_rejects_fast_food_collocation_drop() -> None: src = ( "Eating fast foods is becoming very common and people don't realize " "how much it affects their health." ) # FLAN slip: drops "fast", keeps bare "Food" bad = ( "Food is becoming very common, and people don't realize how much it " "affects their health." ) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "collocation" in v.reasons ok, reasons = _accept_lm_unit(src, bad) assert not ok print("fast-food collocation reject OK:", v.reasons, reasons) def test_rejects_workout_program_invention() -> None: src = ( "Unfortunately, many people prefer watching television instead of " "doing physical activities." ) bad = ( "Unfortunately, many people prefer watching television instead of " "following a workout program." ) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "invention" in v.reasons ok, reasons = _accept_lm_unit(src, bad) assert not ok print("workout-program invention reject OK:", v.reasons, reasons) def test_keeps_fast_food_when_preserved() -> None: src = ( "Eating fast foods is becoming very common and people don't realize " "how much it affects their health." ) good = ( "Eating fast food is getting very common, and people don't realize " "how much it harms their health." ) from app.pipeline.candidate_validator import _protected_collocations_ok assert _protected_collocations_ok(src, good) print("fast-food keep OK") def test_rejects_live_mangled_online_learning() -> None: src = ( "Many students prefer to study from home because it saves time and money." ) bad = ( "It takes less time to study from home than it would at home." ) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert any(r in v.reasons for r in ("key_content", "broken", "coverage", "drift")) ok, reasons = _accept_lm_unit(src, bad) assert not ok print("online-learning mangle reject OK:", v.reasons, reasons) def test_rejects_dropped_online_modifier() -> None: src = "Online learning has become more popular during recent years." bad = "Learning is becoming more popular in recent years." v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "key_content" in v.reasons print("online modifier reject OK:", v.reasons) def test_rejects_mangled_climate_claim() -> None: src = ( "Climate change is a serious problem that affects every country." ) bad = "The impact of climate change has every country." v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert any(r in v.reasons for r in ("key_content", "coverage", "drift")) print("climate mangle reject OK:", v.reasons) def test_accepts_faithful_paraphrase() -> None: src = ( "Many students prefer to study from home because it saves time and money." ) good = ( "A lot of students prefer studying at home since it saves them time and money." ) from app.pipeline.candidate_validator import _key_content_ok, _broken_rewrite_ok assert _key_content_ok(src, good) assert _broken_rewrite_ok(src, good) print("faithful paraphrase key-content OK") def test_rejects_dropped_books_and_cities() -> None: src = "Reading books regularly improves vocabulary and critical thinking." bad = "Reading helps improve vocabulary and critical thinking." v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "key_content" in v.reasons print("books drop reject OK:", v.reasons) src2 = "Public transport is cheaper than owning a car in big cities." bad2 = "Public transport is cheaper than owning a car." v2 = validate_candidate(src2, bad2, min_meaning=0.5) assert not v2.ok assert "key_content" in v2.reasons or "length" in v2.reasons print("cities drop reject OK:", v2.reasons) def test_rejects_save_spend_flip() -> None: src = "Many students prefer to study from home because it saves time and money." bad = ( "Many students prefer to study from home because it allows them to " "spend their time and money." ) from app.pipeline.meaning_safety import polarity_safe assert not polarity_safe(src, bad) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "polarity" in v.reasons print("save/spend polarity reject OK:", v.reasons) def test_rejects_garbage_dontstet() -> None: src = ( "If companies don't reduce packaging and people don't recycle properly, " "the problem will become worse every year." ) bad = ( "If companies don't reduce packaging, people don'tstet recycle properly, " "the problem will grow." ) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "broken" in v.reasons or "key_content" in v.reasons print("dontstet garbage reject OK:", v.reasons) def test_rejects_recycle_negation_flip() -> None: src = ( "If companies don't reduce packaging and people don't recycle properly, " "the problem will become worse every year." ) bad = ( "If people recycle and companies don't reduce their packaging and products, " "the problem will become worse every year." ) from app.pipeline.meaning_safety import polarity_safe assert not polarity_safe(src, bad) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "polarity" in v.reasons print("recycle negation flip reject OK:", v.reasons) def test_assemble_preserves_two_units() -> None: from app.pipeline.orchestrator import _assemble_hybrid_units units = [ (0, "Reading books regularly improves vocabulary and critical thinking."), (0, "Children who don't read enough often struggle with writing essays later in school."), ] outs = [ "Reading books regularly improves vocabulary and critical thinking", "Children who don't read enough often struggle with writing essays later in school", ] text = _assemble_hybrid_units(units, outs) assert "books" in text.lower() assert "children" in text.lower() assert text.count(".") >= 2 print("assemble two units OK:", text) def test_rejects_comparison_swap() -> None: src = "Electric cars produce less air pollution than petrol cars." bad = "Petrol cars make less air pollution than electric cars." from app.pipeline.meaning_safety import polarity_safe assert not polarity_safe(src, bad) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "polarity" in v.reasons ok, reasons = _accept_lm_unit(src, bad) assert not ok print("comparison swap reject OK:", v.reasons, reasons) def test_rejects_learning_to_read_invention() -> None: src = "Learning a second language opens more career opportunities." bad = "Learning to read a second language opens many career opportunities." v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "invention" in v.reasons print("learning-to-read invention reject OK:", v.reasons) def test_rejects_check_with_teachers_invention() -> None: src = ( "Teachers should check progress regularly instead of grading only " "the final report." ) bad = ( "Teachers should check with teachers about progress instead of " "grading the final report." ) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "invention" in v.reasons print("check-with-teachers invention reject OK:", v.reasons) def test_rejects_libraries_to_schools_swap() -> None: src = ( "If cities don't fund them properly, many young people lose access " "to learning resources." ) bad = ( "If cities don't fund their schools correctly, many young people lose " "access to learning resources." ) v = validate_candidate(src, bad, min_meaning=0.5) assert not v.ok assert "entity_inject" in v.reasons print("libraries/them->schools inject reject OK:", v.reasons) src2 = "Public libraries offer free books and quiet spaces for study." bad2 = "Public schools offer free books and quiet spaces for study." v2 = validate_candidate(src2, bad2, min_meaning=0.5) assert not v2.ok assert "topic_anchor" in v2.reasons or "key_content" in v2.reasons print("libraries->schools topic reject OK:", v2.reasons) def test_clamp_rejects_multi_sentence() -> None: from app.pipeline.orchestrator import ( _accept_lm_unit, _clamp_to_source_shape, _sentence_count_simple, ) src = "Learning a second language opens more career opportunities." bad = ( "Learning a second language opens more career opportunities. " "Travel becomes easier too." ) clamped = _clamp_to_source_shape(src, bad) assert _sentence_count_simple(clamped) == 1 ok, reasons = _accept_lm_unit(src, bad) assert not ok assert "shape" in reasons print("multi-sentence clamp/reject OK:", reasons) if __name__ == "__main__": test_rejects_multi_sentence_invention() test_rejects_two_sentence_for_one() test_skip_short_and_light() test_sleep_style_must_visibly_rewrite() test_exercise_tv_must_not_be_reorder_only() test_complete_wording_rewrite_not_shuffle() test_restructure_time_only_and_manner() test_require_visible_keeps_structural_reorder() test_rejects_degree_adv_junk_reorder() test_because_front_preserves_clauses() test_esl_wording_avoids_synonym_thrash() test_rejects_hollow_ons_bills() test_assembly_preserves_unit_count() test_live_bug_patterns_rejected() test_rejects_fast_food_collocation_drop() test_rejects_workout_program_invention() test_keeps_fast_food_when_preserved() test_rejects_live_mangled_online_learning() test_rejects_dropped_online_modifier() test_rejects_mangled_climate_claim() test_accepts_faithful_paraphrase() test_rejects_dropped_books_and_cities() test_rejects_save_spend_flip() test_rejects_garbage_dontstet() test_rejects_recycle_negation_flip() test_assemble_preserves_two_units() test_rejects_comparison_swap() test_rejects_learning_to_read_invention() test_rejects_check_with_teachers_invention() test_rejects_libraries_to_schools_swap() test_clamp_rejects_multi_sentence() print("\nALL PHASE A TESTS PASSED")