arborist/tests/test_crosslang_guard.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

220 lines
8.2 KiB
Python

"""Ticket #000001 §7 Phase 0 — deterministic cross-language guard.
Two contracts under test:
1. **English byte-identity.** `guard()` returns None for any
pure-ASCII query without inverted punctuation — including the
#000053/#000054 acronym shapes (`CPU`/`GPU`/`AI`/`ML`/`DNA`).
A None decision means `query()` does nothing, so the English
retrieval path is unchanged by construction.
2. **Cross-language behaviour.** The non-English signal fires on
`¿`/`¡`/accented letters; a query with a surviving content token
strips es function words from retrieval only; a pure-function-word
query fails closed to UNGROUNDED *before* retrieval/LLM via the
Merkle-auditable reject-DAG path (same shape as the
`quantifier_should_reject` path).
No network. The integration test uses a stub corpus + a chat client
that raises if the LLM is reached (proving the pre-LLM short-circuit).
"""
from __future__ import annotations
from collections.abc import Iterator
import pytest
from arborist.qa.crosslang import (
CrossLanguageDecision,
guard,
strip_for_retrieval,
)
# --- Contract 1: English / acronym byte-identity (the #000053/#000054 guard)
@pytest.mark.parametrize(
"q",
[
"What is anarcho-capitalism?",
"what is a CPU?",
"GPU vs CPU performance",
"AI and ML differences",
"what is DNA?",
"Who is Russell Ballestrini?",
"tell me all there is to know about FBI history",
"",
],
)
def test_english_and_acronyms_are_byte_identical_noop(q):
# None → query() does nothing → English path unchanged.
assert guard(q) is None
# --- Contract 2: signal + fail-closed semantics
def test_signal_fires_with_content_token_not_fail_closed():
d = guard("¿Qué es el anarcocapitalismo?")
assert isinstance(d, CrossLanguageDecision)
assert d.fail_closed is False
assert d.content_tokens == ("anarcocapitalismo",)
# ASCII regex truncates "¿Qué" → "Qu"; es stoppack catches it.
assert set(d.dropped) == {"Qu", "es", "el"}
@pytest.mark.parametrize("q", ["¿Qué es el?", "¿qué es?", "¡Es la de los!"])
def test_pure_function_words_fail_closed(q):
d = guard(q)
assert d is not None and d.fail_closed is True
assert d.content_tokens == ()
def test_bare_ascii_function_words_do_not_fire_english_safe():
# No ¿/¡, no accent → no signal → None. We never guess language
# from bare ASCII (that would risk English false-positives).
assert guard("es la de los") is None
def test_accented_letter_alone_triggers_signal():
# No inverted punctuation, but a non-ASCII letter still signals.
d = guard("relatividad de Einstein según teoría")
assert d is not None
assert "Einstein" in d.content_tokens # proper noun survives
def test_strip_for_retrieval_drops_es_keeps_content_in_order():
d = guard("¿Qué es el anarcocapitalismo?")
assert strip_for_retrieval("¿Qué es el anarcocapitalismo?", d) == "anarcocapitalismo"
# Mixed: English content tokens are retained (downstream applies
# its own English stopword filter).
d2 = guard("¿qué es la teoría anarcocapitalismo theory?")
out = strip_for_retrieval("¿qué es la teoría anarcocapitalismo theory?", d2)
assert "anarcocapitalismo" in out and "theory" in out
assert "es" not in out.split() and "la" not in out.split()
def test_reason_strings_distinguish_the_two_paths():
fc = guard("¿Qué es el?")
ok = guard("¿Qué es el anarcocapitalismo?")
assert "fail closed" in fc.reason.lower()
assert "stripped" in ok.reason.lower()
# --- Contract 2 (integration): query() short-circuits pre-LLM, auditable
class _NoLLM:
"""Chat client that proves the LLM was never reached."""
def chat_completion(self, messages, **kwargs) -> str: # noqa: D401
raise AssertionError("LLM must not be called on the fail-closed path")
def _ingest_one(tmp_path, text, title):
from arborist.document import Document
from arborist.ingest import ingest_source
from arborist.source import Source
from arborist.store import connect
class FakeSource(Source):
source_type = "test"
def __init__(self, docs):
self.docs = docs
def iter_documents(self) -> Iterator[Document]:
yield from self.docs
shard = tmp_path / "shard.db"
conn = connect(shard)
try:
ingest_source(
conn,
FakeSource([Document(uri="t://d", content=text, source_type="test", title=title)]),
)
finally:
conn.close()
return shard
def test_query_fail_closed_is_pre_llm_and_merkle_auditable(tmp_path):
from arborist.qa.dag import verify_run_dag
from arborist.qa.query import DEFAULT_QUERY_POLICY, query
shard = _ingest_one(tmp_path, "Anarcho-capitalism is a political philosophy. " * 40,
"Anarcho-capitalism")
result = query(
question="¿Qué es el?", # pure function words → fail closed
qa_db=tmp_path / "qa.db",
chat_client=_NoLLM(), # raises if the LLM is reached
model_id="stub",
single_db=shard,
policy=dict(DEFAULT_QUERY_POLICY, answer_mode="claim_lattice",
crosslang_guard_enabled=True),
)
assert result["status"] == "cross_language_unsupported"
assert result["audit_mode"] == "UNGROUNDED"
assert result["cache_key"] is None
assert result["lookup_path"] == "preflight"
assert result["violations"][0]["kind"] == "CROSS_LANGUAGE_UNSUPPORTED"
# 3-stage reject DAG recomputes (same auditability as broad-reject).
assert verify_run_dag(result["run_dag_blob"]) is True
def test_query_english_does_not_trip_the_guard(tmp_path):
from arborist.qa.client import StubClient
from arborist.qa.query import DEFAULT_QUERY_POLICY, query
shard = _ingest_one(tmp_path, "Anarcho-capitalism is a political philosophy. " * 40,
"Anarcho-capitalism")
result = query(
question="What is anarcho-capitalism?",
qa_db=tmp_path / "qa.db",
chat_client=StubClient(answer="Anarcho-capitalism is a philosophy. [E1]\n"),
model_id="stub",
single_db=shard,
policy=dict(DEFAULT_QUERY_POLICY, answer_mode="claim_lattice",
crosslang_guard_enabled=True), # guard ON, English still safe
)
# Even with the guard enabled, the English path never reaches the
# cross-language status (guard() returns None for pure ASCII).
assert result["status"] != "cross_language_unsupported"
def test_query_field_case_proceeds_past_guard(tmp_path):
"""`¿Qué es el anarcocapitalismo?` is NOT fail-closed (a content
token survives) → it must proceed past the guard (no pre-retrieval
short-circuit), exercising the stripped-retrieval path."""
from arborist.qa.client import StubClient
from arborist.qa.query import DEFAULT_QUERY_POLICY, query
shard = _ingest_one(tmp_path, "Unrelated content about gardening. " * 40, "Gardening")
result = query(
question="¿Qué es el anarcocapitalismo?",
qa_db=tmp_path / "qa.db",
chat_client=StubClient(answer="x [E1]\n"),
model_id="stub",
single_db=shard,
policy=dict(DEFAULT_QUERY_POLICY, answer_mode="claim_lattice",
crosslang_guard_enabled=True),
)
assert result["status"] != "cross_language_unsupported"
def test_flag_off_reverts_to_legacy_behaviour(tmp_path):
"""Default policy (flag OFF) → the guard is fully bypassed: even a
pure-function-word Spanish query does NOT short-circuit, proving
the A/B baseline for experimentation is byte-for-byte legacy."""
from arborist.qa.client import StubClient
from arborist.qa.query import DEFAULT_QUERY_POLICY, query
assert DEFAULT_QUERY_POLICY["crosslang_guard_enabled"] is False
shard = _ingest_one(tmp_path, "Unrelated content about gardening. " * 40, "Gardening")
result = query(
question="¿Qué es el?", # would fail-closed IF the flag were on
qa_db=tmp_path / "qa.db",
chat_client=StubClient(answer="x [E1]\n"),
model_id="stub",
single_db=shard,
policy=dict(DEFAULT_QUERY_POLICY, answer_mode="claim_lattice"), # flag OFF
)
assert result["status"] != "cross_language_unsupported"