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.
220 lines
8.2 KiB
Python
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"
|