arborist/tests/test_entity_mask.py
russell@unturf.com 2c98fc964e
feat: cross-language Q&A (Operation Sandwich) + Windows quickstart — all default-OFF
Three workstreams, full suite 2482 passed, experimental paths default-OFF.

#000055 — Windows quickstart without make
  tasks.py (pure-stdlib runner) + make.bat shim + .gitattributes;
  README Windows section rewritten. Quickstart needs only Python
  3.10+ (no make/bzip2/curl/bash). Mirrors the Makefile quickstart
  subset; drift-pinned by tests/test_tasks_runner.py.

#000001 §7 Phase 0 — deterministic cross-language guard
  arborist/qa/crosslang.py: non-English signal (¿/¡/non-ASCII) + an
  es function-word stoppack. Fail-closed to UNGROUNDED before
  retrieval/LLM (mirrors the quantifier reject-DAG) when no content
  token survives, else strips es stopwords from the retrieval query
  only. English path byte-identical by construction. Default OFF
  (crosslang_guard_enabled). Measured: the anarcocapitalismo field
  case 10.4s -> 1.6s.

#000056 — Operation Sandwich (cross-language grounding)
  arborist/qa/mt/: opus-mt es/fr/ru<->en, lazy per-pair memoised
  singleton (fixes the 88%-engine-error concurrency defect),
  manifest-pinned, [mt] extra; entity_mask wrapper. Sandwich =
  translate query in (retrieval + LLM prompt) -> English answer ->
  UNTOUCHED verifier grounds English-vs-English -> translate the
  verified answer out as display-only (banner-labelled, zero
  grounding). question_hash + verifier_policy_hash invariant; MT
  engine identity binds into RetrievalPlan, not governance. CLI
  --crosslang-translate / make XLANG_MT=1. Default OFF; entity_mask
  default OFF (measured net-negative at bench scale). Fan-out bench
  (bench/*.py): Spanish ~0% -> 71% grounded vs the real no-support
  baseline; the round-trip predictor was tried and refuted; the
  entity-mask lever failed at scale (corpus-title anchoring untried).

CLAUDE.md: cross-language bright-line convention + module map.
Pre-existing modified diagram files are intentionally excluded.
2026-05-18 12:12:23 -04:00

59 lines
2.4 KiB
Python

"""Entity-preserving MT — #000056 §9 (the validated lever).
Deterministic detection + mask/restore round-trip + the
MaskedTranslator protocol wrapper. No model, no network.
"""
from __future__ import annotations
from arborist.qa.mt import StubTranslator
from arborist.qa.mt.entity_mask import MaskedTranslator, mask, protect_spans, restore
def _spans(text):
return [text[s:e] for s, e in protect_spans(text)]
def test_detects_capitalised_runs_and_quotes_not_function_words():
assert _spans("who is Paul of Tarsus?") == ["Paul of Tarsus"]
assert _spans("when was the Eiffel Tower built?") == ["Eiffel Tower"]
assert _spans("a bridge between New London & Groton?") == ["New London & Groton"]
# quoted span keeps the quote chars (restored verbatim — harmless)
assert _spans('what does "winter is coming" mean?') == ['"winter is coming"']
# wh / function lead word is NOT an entity; lowercase noun isn't either.
assert _spans("what is anarchocapitalism?") == []
assert _spans("how does intel compare to amd?") == []
def test_mask_restore_round_trips_exactly():
q = "who wrote the play Hamlet?"
m, mp = mask(q)
assert "Hamlet" not in m and "ZQX0XQZ" in m
assert restore(m, mp) == q
# Restore tolerates MT mangling the sentinel's case/spacing.
assert restore("Quien escribio la obra zqx 0 xqz?", mp) == \
"Quien escribio la obra Hamlet?"
def test_masked_translator_preserves_entity_through_a_mangling_engine():
# Engine that lowercases everything it actually sees — would destroy
# "Hamlet" if it reached MT. Masking keeps it verbatim.
mangle = StubTranslator(fn=lambda t, s, g: t.lower())
mt = MaskedTranslator(mangle)
out = mt.translate("who wrote the play Hamlet?", "en", "es")
assert "Hamlet" in out # entity survived the mangler
assert out.startswith("who") # non-entity text still translated
def test_graceful_degrade_propagates():
dead = StubTranslator(fn=lambda *a: "x", available=False)
mt = MaskedTranslator(dead)
assert mt.available is False
# Inner unavailable → original text back (sandwich → Phase-0).
assert mt.translate("who is Paul of Tarsus?", "es", "en") == \
"who is Paul of Tarsus?"
def test_engine_identity_carries_mask_policy():
mt = MaskedTranslator(StubTranslator(engine_id="opus-mt-v1"))
assert "entmask-v1" in mt.engine_id
assert mt.manifest_hash.endswith(":entmask-v1")