Upload 105 files
Browse files- app/pipeline/__pycache__/candidate_validator.cpython-311.pyc +0 -0
- app/pipeline/__pycache__/meaning_safety.cpython-311.pyc +0 -0
- app/pipeline/__pycache__/orchestrator.cpython-311.pyc +0 -0
- app/pipeline/candidate_validator.py +21 -0
- app/pipeline/meaning_safety.py +37 -0
- app/pipeline/orchestrator.py +75 -13
- scripts/test_phase_a_guards.py +62 -0
app/pipeline/__pycache__/candidate_validator.cpython-311.pyc
CHANGED
|
Binary files a/app/pipeline/__pycache__/candidate_validator.cpython-311.pyc and b/app/pipeline/__pycache__/candidate_validator.cpython-311.pyc differ
|
|
|
app/pipeline/__pycache__/meaning_safety.cpython-311.pyc
CHANGED
|
Binary files a/app/pipeline/__pycache__/meaning_safety.cpython-311.pyc and b/app/pipeline/__pycache__/meaning_safety.cpython-311.pyc differ
|
|
|
app/pipeline/__pycache__/orchestrator.cpython-311.pyc
CHANGED
|
Binary files a/app/pipeline/__pycache__/orchestrator.cpython-311.pyc and b/app/pipeline/__pycache__/orchestrator.cpython-311.pyc differ
|
|
|
app/pipeline/candidate_validator.py
CHANGED
|
@@ -367,6 +367,27 @@ def _invention_ok(original: str, candidate: str) -> bool:
|
|
| 367 |
if "program" in novel or "programme" in novel:
|
| 368 |
if novel & {"exercise", "fitness", "gym", "workout", "training"}:
|
| 369 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
return True
|
| 371 |
|
| 372 |
|
|
|
|
| 367 |
if "program" in novel or "programme" in novel:
|
| 368 |
if novel & {"exercise", "fitness", "gym", "workout", "training"}:
|
| 369 |
return False
|
| 370 |
+
# Invented "learn/learning to <verb>" when verb not in source (e.g. learning to read)
|
| 371 |
+
for m in re.finditer(
|
| 372 |
+
r"\b(?:learn(?:ing)?|study(?:ing)?)\s+to\s+([a-zA-Z]+)\b",
|
| 373 |
+
(candidate or ""),
|
| 374 |
+
flags=re.I,
|
| 375 |
+
):
|
| 376 |
+
verb = m.group(1).lower()
|
| 377 |
+
stem = verb
|
| 378 |
+
if stem.endswith("ing") and len(stem) > 5:
|
| 379 |
+
stem = stem[:-3]
|
| 380 |
+
elif stem.endswith("ed") and len(stem) > 4:
|
| 381 |
+
stem = stem[:-2]
|
| 382 |
+
elif stem.endswith("s") and len(stem) > 3:
|
| 383 |
+
stem = stem[:-1]
|
| 384 |
+
if stem not in o_toks and verb not in o_toks:
|
| 385 |
+
return False
|
| 386 |
+
# Invented "check with …" meeting frame not in source
|
| 387 |
+
if re.search(r"\bcheck with\b", candidate or "", flags=re.I) and not re.search(
|
| 388 |
+
r"\bcheck with\b", original or "", flags=re.I
|
| 389 |
+
):
|
| 390 |
+
return False
|
| 391 |
return True
|
| 392 |
|
| 393 |
|
app/pipeline/meaning_safety.py
CHANGED
|
@@ -164,6 +164,40 @@ def _lemma_still_negated(text: str, lemma: str) -> bool:
|
|
| 164 |
return False
|
| 165 |
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
def polarity_safe(original: str, candidate: str) -> bool:
|
| 168 |
"""Reject candidates that flip antonyms or strip/add negation on key verbs."""
|
| 169 |
if not original.strip() or not candidate.strip():
|
|
@@ -220,4 +254,7 @@ def polarity_safe(original: str, candidate: str) -> bool:
|
|
| 220 |
if not _NEG_EQUIV.search(c):
|
| 221 |
return False
|
| 222 |
|
|
|
|
|
|
|
|
|
|
| 223 |
return True
|
|
|
|
| 164 |
return False
|
| 165 |
|
| 166 |
|
| 167 |
+
def _comparison_sides(text: str) -> tuple[set[str], set[str]] | None:
|
| 168 |
+
"""Extract content stems on each side of a less/more…than comparison."""
|
| 169 |
+
m = re.search(
|
| 170 |
+
r"^(.{3,80}?)\b(?:less|more|fewer|greater|better|worse)\b"
|
| 171 |
+
r".{0,100}?\bthan\s+(.{3,80}?)(?:[.!?]|$)",
|
| 172 |
+
(text or "").strip(),
|
| 173 |
+
flags=re.I | re.S,
|
| 174 |
+
)
|
| 175 |
+
if not m:
|
| 176 |
+
return None
|
| 177 |
+
left = _stems_in_text(m.group(1))
|
| 178 |
+
right = _stems_in_text(m.group(2))
|
| 179 |
+
if not left or not right:
|
| 180 |
+
return None
|
| 181 |
+
return left, right
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _comparison_order_ok(original: str, candidate: str) -> bool:
|
| 185 |
+
"""Reject swapped comparison subjects (Electric…than petrol → Petrol…than electric)."""
|
| 186 |
+
sides_o = _comparison_sides(original)
|
| 187 |
+
sides_c = _comparison_sides(candidate)
|
| 188 |
+
if not sides_o or not sides_c:
|
| 189 |
+
return True
|
| 190 |
+
lo, ro = sides_o
|
| 191 |
+
lc, rc = sides_c
|
| 192 |
+
lo_rc = len(lo & rc) / max(1, len(lo))
|
| 193 |
+
lo_lc = len(lo & lc) / max(1, len(lo))
|
| 194 |
+
ro_lc = len(ro & lc) / max(1, len(ro))
|
| 195 |
+
# Original left aligns with candidate right, and original right with candidate left
|
| 196 |
+
if lo_rc >= 0.45 and ro_lc >= 0.45 and lo_lc < 0.45:
|
| 197 |
+
return False
|
| 198 |
+
return True
|
| 199 |
+
|
| 200 |
+
|
| 201 |
def polarity_safe(original: str, candidate: str) -> bool:
|
| 202 |
"""Reject candidates that flip antonyms or strip/add negation on key verbs."""
|
| 203 |
if not original.strip() or not candidate.strip():
|
|
|
|
| 254 |
if not _NEG_EQUIV.search(c):
|
| 255 |
return False
|
| 256 |
|
| 257 |
+
if not _comparison_order_ok(original, candidate):
|
| 258 |
+
return False
|
| 259 |
+
|
| 260 |
return True
|
app/pipeline/orchestrator.py
CHANGED
|
@@ -185,6 +185,24 @@ def _sentence_count_simple(text: str) -> int:
|
|
| 185 |
return len([p for p in parts if p.strip()])
|
| 186 |
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
def _classical_paragraph(paragraph: str, tone: str, strength: int, rng: random.Random) -> str:
|
| 189 |
"""Offline fallback: structure + curated lexicon (not the primary quality path)."""
|
| 190 |
styled = apply_tone_style(paragraph, tone, strength, rng)
|
|
@@ -357,6 +375,13 @@ def _accept_lm_unit(source_unit: str, candidate: str, *, max_sim: float = 0.90)
|
|
| 357 |
"""Strict gate for a single LM-rewritten sentence."""
|
| 358 |
if not candidate or not candidate.strip():
|
| 359 |
return False, ["empty"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
v = validate_candidate(
|
| 361 |
source_unit, candidate, min_meaning=0.80, max_surface=max_sim
|
| 362 |
)
|
|
@@ -463,9 +488,7 @@ def _hybrid_rewrite(
|
|
| 463 |
max_sim=0.90,
|
| 464 |
)
|
| 465 |
gen_clean = tidy(correct_text(gen_raw) if gen_raw else "")
|
| 466 |
-
|
| 467 |
-
if _sentence_count_simple(gen_clean) > 1 and _sentence_count_simple(src_u) == 1:
|
| 468 |
-
gen_clean = ""
|
| 469 |
ok, reasons = _accept_lm_unit(src_u, gen_clean, max_sim=0.90)
|
| 470 |
|
| 471 |
if ok:
|
|
@@ -482,11 +505,7 @@ def _hybrid_rewrite(
|
|
| 482 |
max_sim=0.88,
|
| 483 |
)
|
| 484 |
gen_clean2 = tidy(correct_text(gen_raw2) if gen_raw2 else "")
|
| 485 |
-
|
| 486 |
-
_sentence_count_simple(gen_clean2) > 1
|
| 487 |
-
and _sentence_count_simple(src_u) == 1
|
| 488 |
-
):
|
| 489 |
-
gen_clean2 = ""
|
| 490 |
ok2, reasons2 = _accept_lm_unit(src_u, gen_clean2, max_sim=0.88)
|
| 491 |
if ok2:
|
| 492 |
stats.gen_accepted += 1
|
|
@@ -512,9 +531,13 @@ def _hybrid_rewrite(
|
|
| 512 |
|
| 513 |
if not (chosen or "").strip():
|
| 514 |
chosen = src_u
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
outputs.append(_ensure_unit_terminal(chosen))
|
| 519 |
|
| 520 |
if len(outputs) != len(source_units):
|
|
@@ -527,8 +550,21 @@ def _hybrid_rewrite(
|
|
| 527 |
outputs.append(_ensure_unit_terminal(source_units[len(outputs)][1]))
|
| 528 |
outputs = outputs[: len(source_units)]
|
| 529 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
# Assemble ONLY from per-unit strings — no document-level grammar here
|
| 531 |
-
# (document LT was observed dropping sentences, e.g. Reading sample).
|
| 532 |
text = _assemble_hybrid_units(source_units, outputs)
|
| 533 |
scrubbed = tidy(_light_post_gen(text, tone))
|
| 534 |
if len(iter_source_units(scrubbed)) < len(source_units):
|
|
@@ -540,9 +576,15 @@ def _hybrid_rewrite(
|
|
| 540 |
text = _assemble_hybrid_units(source_units, outputs)
|
| 541 |
else:
|
| 542 |
text = scrubbed
|
| 543 |
-
# Final hard guarantee
|
| 544 |
if len(iter_source_units(text)) < len(source_units):
|
|
|
|
| 545 |
text = _assemble_hybrid_units(source_units, outputs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
return text, stats, used_gen
|
| 547 |
|
| 548 |
|
|
@@ -760,6 +802,26 @@ def rewrite_text(
|
|
| 760 |
else:
|
| 761 |
rewritten = tidy(cleaned_out)
|
| 762 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
ratio = _similarity_ratio(source, rewritten)
|
| 764 |
changed = rewritten.strip() != original.strip()
|
| 765 |
|
|
|
|
| 185 |
return len([p for p in parts if p.strip()])
|
| 186 |
|
| 187 |
|
| 188 |
+
def _clamp_to_source_shape(source_unit: str, candidate: str) -> str:
|
| 189 |
+
"""Keep LM output to the same sentence count as the source unit (1→1)."""
|
| 190 |
+
c = (candidate or "").strip()
|
| 191 |
+
if not c:
|
| 192 |
+
return ""
|
| 193 |
+
o_n = _sentence_count_simple(source_unit)
|
| 194 |
+
c_n = _sentence_count_simple(c)
|
| 195 |
+
if o_n == 1 and c_n != 1:
|
| 196 |
+
# Take first sentence only; if still multi or empty, reject
|
| 197 |
+
first = re.split(r"(?<=[.!?])\s+", c, maxsplit=1)[0].strip()
|
| 198 |
+
if _sentence_count_simple(first) == 1 and first:
|
| 199 |
+
return first
|
| 200 |
+
return ""
|
| 201 |
+
if o_n >= 1 and c_n < o_n:
|
| 202 |
+
return ""
|
| 203 |
+
return c
|
| 204 |
+
|
| 205 |
+
|
| 206 |
def _classical_paragraph(paragraph: str, tone: str, strength: int, rng: random.Random) -> str:
|
| 207 |
"""Offline fallback: structure + curated lexicon (not the primary quality path)."""
|
| 208 |
styled = apply_tone_style(paragraph, tone, strength, rng)
|
|
|
|
| 375 |
"""Strict gate for a single LM-rewritten sentence."""
|
| 376 |
if not candidate or not candidate.strip():
|
| 377 |
return False, ["empty"]
|
| 378 |
+
# Hard 1:1 sentence shape before other gates
|
| 379 |
+
o_n = _sentence_count_simple(source_unit)
|
| 380 |
+
c_n = _sentence_count_simple(candidate)
|
| 381 |
+
if o_n == 1 and c_n != 1:
|
| 382 |
+
return False, ["shape"]
|
| 383 |
+
if o_n >= 1 and c_n < o_n:
|
| 384 |
+
return False, ["shape"]
|
| 385 |
v = validate_candidate(
|
| 386 |
source_unit, candidate, min_meaning=0.80, max_surface=max_sim
|
| 387 |
)
|
|
|
|
| 488 |
max_sim=0.90,
|
| 489 |
)
|
| 490 |
gen_clean = tidy(correct_text(gen_raw) if gen_raw else "")
|
| 491 |
+
gen_clean = _clamp_to_source_shape(src_u, gen_clean)
|
|
|
|
|
|
|
| 492 |
ok, reasons = _accept_lm_unit(src_u, gen_clean, max_sim=0.90)
|
| 493 |
|
| 494 |
if ok:
|
|
|
|
| 505 |
max_sim=0.88,
|
| 506 |
)
|
| 507 |
gen_clean2 = tidy(correct_text(gen_raw2) if gen_raw2 else "")
|
| 508 |
+
gen_clean2 = _clamp_to_source_shape(src_u, gen_clean2)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
ok2, reasons2 = _accept_lm_unit(src_u, gen_clean2, max_sim=0.88)
|
| 510 |
if ok2:
|
| 511 |
stats.gen_accepted += 1
|
|
|
|
| 531 |
|
| 532 |
if not (chosen or "").strip():
|
| 533 |
chosen = src_u
|
| 534 |
+
chosen = _clamp_to_source_shape(src_u, chosen) or src_u
|
| 535 |
+
# Never let an accepted unit expand into multiple sentences
|
| 536 |
+
if _sentence_count_simple(chosen) != _sentence_count_simple(src_u):
|
| 537 |
+
if _sentence_count_simple(src_u) == 1:
|
| 538 |
+
chosen = _light_revert_polish(src_u, tone, rng, strength=1)
|
| 539 |
+
else:
|
| 540 |
+
chosen = src_u
|
| 541 |
outputs.append(_ensure_unit_terminal(chosen))
|
| 542 |
|
| 543 |
if len(outputs) != len(source_units):
|
|
|
|
| 550 |
outputs.append(_ensure_unit_terminal(source_units[len(outputs)][1]))
|
| 551 |
outputs = outputs[: len(source_units)]
|
| 552 |
|
| 553 |
+
# Per-slot sanity: empty/too-short slots → source unit
|
| 554 |
+
fixed_outs: list[str] = []
|
| 555 |
+
for (_pi, src_u), out in zip(source_units, outputs):
|
| 556 |
+
piece = (out or "").strip()
|
| 557 |
+
if (
|
| 558 |
+
not piece
|
| 559 |
+
or _sentence_count_simple(piece) < _sentence_count_simple(src_u)
|
| 560 |
+
or len(piece.split()) < max(3, int(len(src_u.split()) * 0.55))
|
| 561 |
+
):
|
| 562 |
+
piece = _ensure_unit_terminal(src_u)
|
| 563 |
+
stats.bump_reason("unit_restore")
|
| 564 |
+
fixed_outs.append(_ensure_unit_terminal(piece))
|
| 565 |
+
outputs = fixed_outs
|
| 566 |
+
|
| 567 |
# Assemble ONLY from per-unit strings — no document-level grammar here
|
|
|
|
| 568 |
text = _assemble_hybrid_units(source_units, outputs)
|
| 569 |
scrubbed = tidy(_light_post_gen(text, tone))
|
| 570 |
if len(iter_source_units(scrubbed)) < len(source_units):
|
|
|
|
| 576 |
text = _assemble_hybrid_units(source_units, outputs)
|
| 577 |
else:
|
| 578 |
text = scrubbed
|
| 579 |
+
# Final hard guarantee — never ship fewer units than source
|
| 580 |
if len(iter_source_units(text)) < len(source_units):
|
| 581 |
+
logger.warning("Final hybrid text lost units; restoring assembled outputs")
|
| 582 |
text = _assemble_hybrid_units(source_units, outputs)
|
| 583 |
+
if len(iter_source_units(text)) < len(source_units):
|
| 584 |
+
text = _assemble_hybrid_units(
|
| 585 |
+
source_units,
|
| 586 |
+
[_ensure_unit_terminal(u) for _i, u in source_units],
|
| 587 |
+
)
|
| 588 |
return text, stats, used_gen
|
| 589 |
|
| 590 |
|
|
|
|
| 802 |
else:
|
| 803 |
rewritten = tidy(cleaned_out)
|
| 804 |
|
| 805 |
+
# Nuclear hybrid safety: never return fewer sentence units than the source
|
| 806 |
+
if mode == "hybrid" and hybrid_stats and hybrid_stats.units:
|
| 807 |
+
n_src = hybrid_stats.units
|
| 808 |
+
if len(iter_source_units(rewritten)) < n_src:
|
| 809 |
+
logger.warning(
|
| 810 |
+
"Post-pipeline unit loss (%s→%s); restoring from grammar source units",
|
| 811 |
+
n_src,
|
| 812 |
+
len(iter_source_units(rewritten)),
|
| 813 |
+
)
|
| 814 |
+
src_units = iter_source_units(source)
|
| 815 |
+
rewritten = _assemble_hybrid_units(
|
| 816 |
+
src_units,
|
| 817 |
+
[
|
| 818 |
+
_ensure_unit_terminal(
|
| 819 |
+
_light_revert_polish(u, tone, rng, strength=1)
|
| 820 |
+
)
|
| 821 |
+
for _i, u in src_units
|
| 822 |
+
],
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
ratio = _similarity_ratio(source, rewritten)
|
| 826 |
changed = rewritten.strip() != original.strip()
|
| 827 |
|
scripts/test_phase_a_guards.py
CHANGED
|
@@ -270,6 +270,64 @@ def test_assemble_preserves_two_units() -> None:
|
|
| 270 |
print("assemble two units OK:", text)
|
| 271 |
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
if __name__ == "__main__":
|
| 274 |
test_rejects_multi_sentence_invention()
|
| 275 |
test_rejects_two_sentence_for_one()
|
|
@@ -288,4 +346,8 @@ if __name__ == "__main__":
|
|
| 288 |
test_rejects_garbage_dontstet()
|
| 289 |
test_rejects_recycle_negation_flip()
|
| 290 |
test_assemble_preserves_two_units()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
print("\nALL PHASE A TESTS PASSED")
|
|
|
|
| 270 |
print("assemble two units OK:", text)
|
| 271 |
|
| 272 |
|
| 273 |
+
def test_rejects_comparison_swap() -> None:
|
| 274 |
+
src = "Electric cars produce less air pollution than petrol cars."
|
| 275 |
+
bad = "Petrol cars make less air pollution than electric cars."
|
| 276 |
+
from app.pipeline.meaning_safety import polarity_safe
|
| 277 |
+
|
| 278 |
+
assert not polarity_safe(src, bad)
|
| 279 |
+
v = validate_candidate(src, bad, min_meaning=0.5)
|
| 280 |
+
assert not v.ok
|
| 281 |
+
assert "polarity" in v.reasons
|
| 282 |
+
ok, reasons = _accept_lm_unit(src, bad)
|
| 283 |
+
assert not ok
|
| 284 |
+
print("comparison swap reject OK:", v.reasons, reasons)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def test_rejects_learning_to_read_invention() -> None:
|
| 288 |
+
src = "Learning a second language opens more career opportunities."
|
| 289 |
+
bad = "Learning to read a second language opens many career opportunities."
|
| 290 |
+
v = validate_candidate(src, bad, min_meaning=0.5)
|
| 291 |
+
assert not v.ok
|
| 292 |
+
assert "invention" in v.reasons
|
| 293 |
+
print("learning-to-read invention reject OK:", v.reasons)
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def test_rejects_check_with_teachers_invention() -> None:
|
| 297 |
+
src = (
|
| 298 |
+
"Teachers should check progress regularly instead of grading only "
|
| 299 |
+
"the final report."
|
| 300 |
+
)
|
| 301 |
+
bad = (
|
| 302 |
+
"Teachers should check with teachers about progress instead of "
|
| 303 |
+
"grading the final report."
|
| 304 |
+
)
|
| 305 |
+
v = validate_candidate(src, bad, min_meaning=0.5)
|
| 306 |
+
assert not v.ok
|
| 307 |
+
assert "invention" in v.reasons
|
| 308 |
+
print("check-with-teachers invention reject OK:", v.reasons)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def test_clamp_rejects_multi_sentence() -> None:
|
| 312 |
+
from app.pipeline.orchestrator import (
|
| 313 |
+
_accept_lm_unit,
|
| 314 |
+
_clamp_to_source_shape,
|
| 315 |
+
_sentence_count_simple,
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
src = "Learning a second language opens more career opportunities."
|
| 319 |
+
bad = (
|
| 320 |
+
"Learning a second language opens more career opportunities. "
|
| 321 |
+
"Travel becomes easier too."
|
| 322 |
+
)
|
| 323 |
+
clamped = _clamp_to_source_shape(src, bad)
|
| 324 |
+
assert _sentence_count_simple(clamped) == 1
|
| 325 |
+
ok, reasons = _accept_lm_unit(src, bad)
|
| 326 |
+
assert not ok
|
| 327 |
+
assert "shape" in reasons
|
| 328 |
+
print("multi-sentence clamp/reject OK:", reasons)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
if __name__ == "__main__":
|
| 332 |
test_rejects_multi_sentence_invention()
|
| 333 |
test_rejects_two_sentence_for_one()
|
|
|
|
| 346 |
test_rejects_garbage_dontstet()
|
| 347 |
test_rejects_recycle_negation_flip()
|
| 348 |
test_assemble_preserves_two_units()
|
| 349 |
+
test_rejects_comparison_swap()
|
| 350 |
+
test_rejects_learning_to_read_invention()
|
| 351 |
+
test_rejects_check_with_teachers_invention()
|
| 352 |
+
test_clamp_rejects_multi_sentence()
|
| 353 |
print("\nALL PHASE A TESTS PASSED")
|