arborist/tests/test_metacognition.py
russell@unturf.com f2bbe512db
qa(#000010): Phases 2-4 land — wired, governed, labeled, benched
Closes ticket #000010 (Meta-Cognition Preflight Guard). Mechanism
complete; defaults preserve the dry-run discipline pattern from
#000008.

Phase 2 — wire preflight into query() and runner.ask():
  - preflight_question() runs after policy resolution + quantifier
    classification, before retrieval.
  - QuestionState surfaces on miss path, cache-hit path, AND
    reject-broad early-return path of query() — schema column-
    aligned across all four returns.
  - runner.ask() carries the same fields for `aborist ask` parity.

Phase 3 — policy fields + governance hash + CLI flags:
  - 6 new policy fields, all default-on except
    metacognition_block_on_contradiction (default False — label-
    only by default; opt-in via --block-on-contradiction).
  - All 6 folded into _VERIFIER_POLICY_FIELDS so flipping any
    invalidates prior cache records on lookup.
  - 2 new CLI flags on `aborist query`:
      --no-preflight             Level 2 master kill
      --block-on-contradiction   strict mode (hard-block on
                                 lexical contradictions)

Phase 4 — audit-line labels + bench fields + tests:
  - _render_warrant_tail extended with 5 metacog tail tokens:
      · false premise
      · contradictory
      · stale risk
      · out of corpus
      · frame ambiguous
  - Bench rows in qa_sweep.py gain 7 new bounded-size projection
    fields (logical_statuses, question_shape, preflight_result,
    temporal_sensitivity, has_false_premise, has_contradiction,
    corpus_requirement). Full QuestionState stays on result dict
    for CLI render only.
  - tests/test_metacognition.py grew from 42 → 68 tests
    (16 new: 6 governance + 6 audit-line tail + 4 default-policy
    pinning).

Live verified end-to-end:

  $ make query-dry Q="Who is the current CEO of OpenAI?" BURN=1
    UNGROUNDED · via claim_lattice · stale risk
  $ make query-dry Q="When did Mr. Burns become Homer's biological
                      father?" BURN=1
    UNGROUNDED · via claim_lattice · false premise

978 tests passing; 36 skipped.

What's NOT shipped (deferred):
  - Run-DAG node binding for metacognition_preflight stage —
    joins ticket #000009 Phase 5 (same audit-replay gap; both
    nodes can land together).
  - Reference-frame plumbing — frame_detection runs post-retrieval,
    preflight here is pre-retrieval; deferred until two-pass
    or post-classification update lands.
  - SOFT_PREFLIGHT_HINT (model-assisted sidecar) — source doc §18
    reserves this label; hard rule preserved (no LLM in preflight
    hard path).
  - Bench A/B measuring preflight on vs off — quick to run once
    stack settles.

Ticket #000010 status: closed · landed 2026-05-03.
2026-05-03 18:22:18 -04:00

412 lines
15 KiB
Python

"""Meta-Cognition Preflight Guard — Ticket #000010 Phase 1 tests.
Pin the seven test cases from the source doc §14 plus per-detector
unit tests. All checks are deterministic; no LLM, no I/O.
Source: ``~/Downloads/meta-cognition_for_hermes(1).txt`` 2026-05-03.
"""
from __future__ import annotations
import pytest
from aborist.qa.metacognition import (
PREFLIGHT_VERSION,
QuestionState,
detect_contradiction,
detect_false_premise,
detect_out_of_corpus,
detect_temporal_sensitivity,
preflight_question,
)
# ---------------------------------------------------------------- temporal
@pytest.mark.parametrize("q", [
"Who is the current CEO of OpenAI?",
"What is the latest version of Python?",
"Who is the President of France today?",
"What is the current price of Bitcoin?",
"Who won the championship this year?",
"As of right now, who holds the world record?",
])
def test_temporal_high_sensitivity(q):
sens, matched = detect_temporal_sensitivity(q)
assert sens == "high", f"{q}{sens}"
assert len(matched) >= 1
@pytest.mark.parametrize("q", [
"what is the capital of france?",
"who painted the mona lisa?",
"what is the speed of light?",
"who wrote the play hamlet?",
])
def test_temporal_low_sensitivity_for_factoids(q):
sens, _ = detect_temporal_sensitivity(q)
assert sens == "low"
# ---------------------------------------------------------------- contradiction
def test_contradiction_unmarried_spouse():
pairs = detect_contradiction("Which unmarried spouse is Homer married to?")
# `unmarried` + `spouse` AND `unmarried` + `married` both fire.
pair_set = set(pairs)
assert ("unmarried", "spouse") in pair_set
assert ("unmarried", "married") in pair_set
def test_contradiction_alive_dead():
pairs = detect_contradiction("Which character is alive and dead simultaneously?")
assert ("alive", "dead") in set(pairs)
def test_contradiction_no_false_positives_on_singletons():
"""`unmarried` alone shouldn't fire — needs both halves of a pair."""
pairs = detect_contradiction("Is George Washington still unmarried?")
assert pairs == ()
def test_contradiction_clean_questions_have_no_pairs():
pairs = detect_contradiction("who painted the mona lisa?")
assert pairs == ()
# ---------------------------------------------------------------- false premise
def test_false_premise_when_did_x_stop_y():
hints = detect_false_premise("when did Mr. Burns stop being Homer's father?")
assert len(hints) >= 1
h = hints[0]
assert h["kind"] == "stopped_doing"
assert "Mr. Burns" in h["subject"]
def test_false_premise_why_did_x_cause_y():
hints = detect_false_premise("why did the moon cause the tides?")
assert len(hints) >= 1
assert hints[0]["kind"] == "caused"
def test_false_premise_how_did_x_become_y():
hints = detect_false_premise("how did Mr. Burns become Homer's biological father?")
assert len(hints) >= 1
assert hints[0]["kind"] == "became"
assert "Mr. Burns" in hints[0]["subject"]
def test_false_premise_no_hint_on_neutral_question():
hints = detect_false_premise("who is bilbo baggins?")
assert hints == ()
# ---------------------------------------------------------------- out of corpus
@pytest.mark.parametrize("q", [
"What does my uploaded contract say about clause 9?",
"What is in my private notes from yesterday?",
"What does the file I sent you say about X?",
"In my email inbox, what did Alice write?",
])
def test_out_of_corpus_detected(q):
assert detect_out_of_corpus(q) is True
@pytest.mark.parametrize("q", [
"What is the capital of France?",
"Who wrote Hamlet?",
"What is the speed of light?",
])
def test_out_of_corpus_not_detected_on_encyclopedic(q):
assert detect_out_of_corpus(q) is False
# ---------------------------------------------------------------- preflight (full integration)
def test_preflight_factoid_is_well_formed():
state = preflight_question("what is the capital of france?")
assert "well_formed" in state.logical_statuses
assert state.preflight_result == "PREFLIGHT_OK"
assert state.question_shape in ("single_fact", "negation")
def test_preflight_broad_quantifier_unbounded():
"""Source doc §5.3 + #000008 §10.1: `winners of all major sports?`
classifies as broad-quantifier-unbounded."""
state = preflight_question("Winners of all major sports?")
assert "broad_quantifier_unbounded" in state.logical_statuses
assert state.preflight_result == "PREFLIGHT_PARTIAL"
assert state.quantifier_intensity == "ALL"
assert state.scope_bound_hint == "unbounded"
def test_preflight_broad_quantifier_bounded_does_not_fire_unbounded():
"""`name all members of the beatles` is a bounded universal.
Should classify as `under_specified` (broad but bounded), NOT
`broad_quantifier_unbounded`."""
state = preflight_question("name all members of the beatles")
assert "broad_quantifier_unbounded" not in state.logical_statuses
assert state.scope_bound_hint == "bounded"
def test_preflight_contradictory_question():
"""Source doc §5.2: `Which unmarried spouse is Homer married to?`
classifies as contradictory."""
state = preflight_question("Which unmarried spouse is Homer married to?")
assert "contradictory_question" in state.logical_statuses
assert state.contradiction_pairs # at least one pair
def test_preflight_false_premise_suspected():
"""Source doc §5.1: `When did Mr. Burns become Homer's biological
father?` classifies as false_premise_suspected."""
state = preflight_question(
"When did Mr. Burns become Homer's biological father?"
)
assert "false_premise_suspected" in state.logical_statuses
assert state.false_premise_hints
def test_preflight_time_sensitive_stale_risk():
"""Source doc §5.4: `Who is the current CEO of X?` classifies
as stale_risk."""
state = preflight_question("Who is the current CEO of OpenAI?")
assert "stale_risk" in state.logical_statuses
assert state.temporal_sensitivity == "high"
assert state.answer_constraints.get("requires_current_source") is True
def test_preflight_out_of_corpus_blocked():
"""Source doc §5.5: out-of-corpus references should BLOCK."""
state = preflight_question("What does my uploaded contract say?")
assert "out_of_corpus_risk" in state.logical_statuses
assert state.preflight_result == "PREFLIGHT_BLOCKED"
def test_preflight_reference_frame_ambiguous():
"""Source doc §5.6: when caller passes multiple reference frames,
classifier marks reference_frame_ambiguous."""
state = preflight_question(
"Has Oceania always been at war with East Asia?",
reference_frames=("literal_geography", "orwell_1984"),
)
assert "reference_frame_ambiguous" in state.logical_statuses
# ---------------------------------------------------------------- gating
def test_master_kill_disables_preflight():
"""policy={'metacognition_enabled': False} short-circuits with
a stub QuestionState. Logical statuses tuple is empty;
preflight_result is PREFLIGHT_OK so downstream isn't blocked."""
state = preflight_question(
"Winners of all major sports?",
policy={"metacognition_enabled": False},
)
assert state.logical_statuses == ()
assert state.preflight_result == "PREFLIGHT_OK"
assert state.question_shape == "metacognition_disabled"
def test_per_detector_disable():
"""Each detector can be turned off independently. Verify the
contradiction check obeys its switch."""
state = preflight_question(
"Which unmarried spouse is Homer married to?",
policy={"metacognition_contradiction_check": False},
)
# Contradiction detector skipped → no contradictory_question status.
assert "contradictory_question" not in state.logical_statuses
assert state.contradiction_pairs == ()
def test_block_on_contradiction_opt_in():
"""By default contradictory questions surface as PARTIAL (label
only). `metacognition_block_on_contradiction=True` flips to
BLOCKED."""
state_default = preflight_question(
"Which unmarried spouse is Homer married to?",
)
state_strict = preflight_question(
"Which unmarried spouse is Homer married to?",
policy={"metacognition_block_on_contradiction": True},
)
assert state_default.preflight_result == "PREFLIGHT_PARTIAL"
assert state_strict.preflight_result == "PREFLIGHT_BLOCKED"
# ---------------------------------------------------------------- determinism
def test_question_hash_stable():
"""Same question (modulo case + whitespace) → same hash."""
a = preflight_question("Who painted the Mona Lisa?")
b = preflight_question(" who painted the mona lisa? ")
assert a.question_hash == b.question_hash
def test_preflight_policy_hash_changes_with_policy_flip():
"""Flipping a metacognition policy field bumps the policy hash —
enables governance binding via _VERIFIER_POLICY_FIELDS (Phase 3)."""
a = preflight_question(
"Winners of all major sports?",
policy={"metacognition_temporal_check": True},
)
b = preflight_question(
"Winners of all major sports?",
policy={"metacognition_temporal_check": False},
)
assert a.preflight_policy_hash != b.preflight_policy_hash
# ---------------------------------------------------------------- versioning
def test_preflight_version_pinned():
state = preflight_question("any question")
assert state.classifier_version == PREFLIGHT_VERSION
assert PREFLIGHT_VERSION == "metacognition-v0.1"
# ---------------------------------------------------------------- empty / edge
def test_empty_question_blocked():
state = preflight_question("")
assert state.preflight_result == "PREFLIGHT_BLOCKED"
assert state.question_shape == "empty"
def test_whitespace_only_question_blocked():
state = preflight_question(" \n\t ")
assert state.preflight_result == "PREFLIGHT_BLOCKED"
def test_question_state_is_serializable():
"""to_dict() must produce JSON-friendly output for run-DAG /
bench JSONL persistence."""
import json
state = preflight_question("Winners of all major sports?")
payload = state.to_dict()
# Round-trip through json.dumps; failure here means a non-
# serializable type leaked in.
json.dumps(payload, ensure_ascii=False)
# ---------------------------------------------------------------- governance binding (Phase 3)
@pytest.mark.parametrize("field", [
"metacognition_enabled",
"metacognition_temporal_check",
"metacognition_contradiction_check",
"metacognition_false_premise_check",
"metacognition_out_of_corpus_check",
"metacognition_block_on_contradiction",
])
def test_metacognition_field_is_in_verifier_policy_fields(field):
"""All six policy fields must be in _VERIFIER_POLICY_FIELDS so
flipping any of them invalidates prior cache records on lookup."""
from aborist.qa.keys import _VERIFIER_POLICY_FIELDS
assert field in _VERIFIER_POLICY_FIELDS
def test_governance_hash_changes_when_metacognition_enabled_flips():
from aborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash
base = dict.fromkeys(_VERIFIER_POLICY_FIELDS, "default")
base["metacognition_enabled"] = True
h_on = verifier_policy_hash(base)
base["metacognition_enabled"] = False
h_off = verifier_policy_hash(base)
assert h_on != h_off
def test_governance_hash_changes_when_block_on_contradiction_flips():
from aborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash
base = dict.fromkeys(_VERIFIER_POLICY_FIELDS, "default")
base["metacognition_block_on_contradiction"] = False
h_off = verifier_policy_hash(base)
base["metacognition_block_on_contradiction"] = True
h_on = verifier_policy_hash(base)
assert h_off != h_on
def test_default_policy_has_metacognition_enabled():
"""Master switch default-on per ticket #000010 §7.3 — detectors
are pure-on-question so the cost is negligible."""
from aborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY
from aborist.qa.query import DEFAULT_QUERY_POLICY
assert RUNNER_POLICY["metacognition_enabled"] is True
assert DEFAULT_QUERY_POLICY["metacognition_enabled"] is True
def test_default_policy_block_on_contradiction_off():
"""Default to label-only on contradictions. False-positive risk
not yet bench-validated; opt-in via --block-on-contradiction."""
from aborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY
from aborist.qa.query import DEFAULT_QUERY_POLICY
assert RUNNER_POLICY["metacognition_block_on_contradiction"] is False
assert DEFAULT_QUERY_POLICY["metacognition_block_on_contradiction"] is False
# ---------------------------------------------------------------- audit-line tails (Phase 4)
def _result_with_question_state(state: QuestionState) -> dict:
"""Synthetic query() result with the minimum fields needed by
`_render_warrant_tail`."""
return {
"violations": [],
"claim_cap_applied": None,
"question_state": state.to_dict(),
}
def test_tail_renders_false_premise():
from aborist.cli import _render_warrant_tail
state = preflight_question(
"When did Mr. Burns become Homer's biological father?"
)
tail = _render_warrant_tail(_result_with_question_state(state))
assert "false premise" in tail
def test_tail_renders_contradictory():
from aborist.cli import _render_warrant_tail
state = preflight_question("Which unmarried spouse is Homer married to?")
tail = _render_warrant_tail(_result_with_question_state(state))
assert "contradictory" in tail
def test_tail_renders_stale_risk():
from aborist.cli import _render_warrant_tail
state = preflight_question("Who is the current CEO of OpenAI?")
tail = _render_warrant_tail(_result_with_question_state(state))
assert "stale risk" in tail
def test_tail_renders_out_of_corpus():
from aborist.cli import _render_warrant_tail
state = preflight_question("What does my uploaded contract say?")
tail = _render_warrant_tail(_result_with_question_state(state))
assert "out of corpus" in tail
def test_tail_omits_metacog_when_well_formed():
from aborist.cli import _render_warrant_tail
state = preflight_question("what is the capital of france?")
tail = _render_warrant_tail(_result_with_question_state(state))
# well_formed should NOT produce any metacog tail tokens.
assert "false premise" not in tail
assert "contradictory" not in tail
assert "stale risk" not in tail
assert "out of corpus" not in tail
def test_tail_combines_metacog_with_existing_kinds():
"""Multiple tails compose: a TITLE_MISMATCH from the verifier
plus a stale_risk from the preflight should both surface."""
from aborist.cli import _render_warrant_tail
state = preflight_question("Who is the current CEO of OpenAI?")
result = _result_with_question_state(state)
result["violations"] = [{"kind": "TITLE_MISMATCH"}]
tail = _render_warrant_tail(result)
assert "title mismatch" in tail
assert "stale risk" in tail