arborist/tests/test_aliases.py
russell@unturf.com d3c40c95d2
ticket #000041 + #000042: alias mechanism + 4 more Hilbert chains landed
Implements both alias mechanisms (citation-aliases #000041,
term-aliases #000042) as one cohesive layer. Same audit
discipline; same opt-in via --use-aliases on warrant-resolve;
same distinct process_id "warrant-resolver-v1+alias" on alias-
resolved derivations rows.

What landed
===========
arborist/store.py — two new tables under SCHEMA_SQL:
  citation_aliases — substitute textbook for proprietary cite
  term_aliases     — bridge vocabulary mismatches (incidence ↔
                     connection in geometry, etc.)
Both with NOT NULL audit fields (decision_at + decision_by
+ optional decision_rationale); both with PK constraints
ensuring idempotent re-add.

arborist/qa/aliases.py — helper module:
  add_citation_alias / list / lookup / remove
  add_term_alias / list / lookup (bidirectional) / remove
  expand_query_with_term_aliases — OR-rewrites FTS5 tokens
                                   while preserving phrase syntax
  domain_for_pillar — Roman numeral → domain string

arborist/qa/warrant_resolver.py — two-pass cascade:
  Pass 1: unaliased queries (matches carry via_alias=False)
  Pass 2: alias-expanded queries (only when pass 1 missed;
          matches carry via_alias=True)
  ResolutionMatch grew a via_alias field; warrant_resolve
  uses it to pick the right process_id per derivation row.
  iter_claim_pack_records now yields a 6-tuple including the
  pillar (parsed from doc URI) so domain lookup works.

arborist/cli.py — alias subcommand group:
  arborist alias citation add ORIGINAL --substitute SUB --by FOX [...]
  arborist alias citation list / remove
  arborist alias term add TERM ALT --domain D --by FOX [...]
  arborist alias term list / remove
  warrant-resolve --use-aliases flag
  sweep --target warrants --use-aliases flag
All audit fields fail-closed at the API surface (refuses on
empty --by; ValueError raised at the helper level).

End-to-end smoke test
=====================
Registered the Hilbert smoke-test alias:
  arborist alias term add incidence connection \
      --domain geometry \
      --by "blackops 2026-05-09 (smoke test)" \
      --rationale "Hilbert 1902 Townsend uses 'connection' for
                   what modern texts call 'incidence'"

Re-ran warrant-resolve --use-aliases --write:

  records_total: 92
  records_resolved: 15  (was 11 without aliases)
  derivations_written: 15

Breakdown by process_id:
  warrant-resolver-v1:        11  (original-citation matches)
  warrant-resolver-v1+alias:   4  (alias-resolved Hilbert axioms)

The 4 alias-resolved records are exactly the Hilbert "Incidence"
axioms blocked by terminology mismatch in #000040 §6:
  Axiom of Line Incidence
  Axiom of Plane Incidence
  Axiom of Point-Line Incidence
  Axiom of Point-Plane Incidence

All four bound to chunk 56 in the Hilbert TeX surface — the
chapter discussing "axioms of connection" (Hilbert's original
1902 vocabulary). Audit trail correctly distinguishes
substituted chains from original ones.

Tests: 18 new in test_aliases.py covering add / list / lookup
/ remove / domain isolation / lowercase normalization /
bidirectional lookup / audit-discipline raises / query
expansion (basic + phrase-preserving + no-match passthrough +
unreachable-DB fallback). Full suite: 1623 passed / 28
skipped.

Tickets #000041 + #000042 closed. Operators can now add more
aliases via the CLI as fox makes decisions per #000038. The
alias mechanism is fail-closed by default — existing
warrant-resolve runs without --use-aliases continue to produce
the original 11/92 chains; --use-aliases opt-in adds the
substituted chains alongside without polluting the unsubsituted
audit trail.
2026-05-09 20:05:28 -04:00

257 lines
7.8 KiB
Python

"""Tests for the citation + term alias mechanisms (#000041 + #000042).
Pure unit + small-DB tests over the helpers in arborist/qa/aliases.py.
The end-to-end resolver-uses-alias path is exercised separately via
the `arborist warrant-resolve --use-aliases` CLI on real shards.
"""
from __future__ import annotations
import sqlite3
import pytest
from arborist.qa.aliases import (
add_citation_alias,
add_term_alias,
domain_for_pillar,
expand_query_with_term_aliases,
list_citation_aliases,
list_term_aliases,
lookup_citation_aliases,
lookup_term_aliases,
remove_citation_alias,
remove_term_alias,
)
from arborist.store import SCHEMA_SQL
@pytest.fixture
def db(tmp_path):
"""Fresh SQLite DB with the alias schema applied."""
p = tmp_path / "aliases.db"
conn = sqlite3.connect(str(p))
try:
conn.executescript(SCHEMA_SQL)
conn.commit()
finally:
conn.close()
return str(p)
# --- pillar domain mapping -------------------------------------------
@pytest.mark.parametrize(
"pillar,expected",
[
("I", "logic"),
("IV", "geometry"),
("VII", "combinatorics"),
("IX", "lambda-calculus"),
("UNKNOWN", ""),
("", ""),
],
)
def test_domain_for_pillar(pillar, expected):
assert domain_for_pillar(pillar) == expected
# --- citation aliases -----------------------------------------------
def test_citation_alias_add_lookup_remove(db):
inserted = add_citation_alias(
db,
original_ref="Introduction to Mathematical Logic by Elliott Mendelson",
substitute_ref="Principles of Mathematical Logic by Hilbert and Ackermann",
substitute_authors=["David Hilbert", "Wilhelm Ackermann"],
substitute_title="Principles of Mathematical Logic",
decision_by="fox 2026-05-09",
decision_rationale="Hilbert-Ackermann 1928 is PD; covers same propositional axioms",
)
assert inserted is True
aliases = lookup_citation_aliases(
db, "Introduction to Mathematical Logic by Elliott Mendelson"
)
assert len(aliases) == 1
a = aliases[0]
assert a.substitute_title == "Principles of Mathematical Logic"
assert "David Hilbert" in a.substitute_authors
# Idempotent — second add returns False.
inserted_again = add_citation_alias(
db,
original_ref="Introduction to Mathematical Logic by Elliott Mendelson",
substitute_ref="Principles of Mathematical Logic by Hilbert and Ackermann",
substitute_authors=["David Hilbert", "Wilhelm Ackermann"],
substitute_title="Principles of Mathematical Logic",
decision_by="fox 2026-05-09",
)
assert inserted_again is False
# Remove + verify gone.
removed = remove_citation_alias(
db,
"Introduction to Mathematical Logic by Elliott Mendelson",
"Principles of Mathematical Logic by Hilbert and Ackermann",
)
assert removed is True
assert lookup_citation_aliases(
db, "Introduction to Mathematical Logic by Elliott Mendelson"
) == []
def test_citation_alias_audit_discipline(db):
"""Empty `decision_by` raises — fail-closed audit."""
with pytest.raises(ValueError, match="decision_by must be non-empty"):
add_citation_alias(
db,
original_ref="A",
substitute_ref="B",
substitute_authors=[],
substitute_title="",
decision_by="",
)
def test_citation_alias_list_filter(db):
add_citation_alias(
db,
original_ref="Mendelson 1997",
substitute_ref="HA 1928",
substitute_authors=["Hilbert"],
substitute_title="Principles",
decision_by="fox",
)
add_citation_alias(
db,
original_ref="Stanley §1.2",
substitute_ref="Brualdi §3.5",
substitute_authors=["Brualdi"],
substitute_title="Combinatorics",
decision_by="fox",
)
all_a = list_citation_aliases(db)
assert len(all_a) == 2
filtered = list_citation_aliases(db, original_filter="Mendelson")
assert len(filtered) == 1
assert filtered[0].original_ref == "Mendelson 1997"
# --- term aliases ----------------------------------------------------
def test_term_alias_add_lookup(db):
inserted = add_term_alias(
db,
term="incidence",
alternate_term="connection",
domain="geometry",
decision_by="fox 2026-05-09",
decision_rationale="Hilbert 1902 Townsend uses 'connection'",
)
assert inserted is True
# Forward lookup
alts = lookup_term_aliases(db, "incidence", "geometry")
assert "connection" in alts
# Bidirectional — registered (incidence, connection) also returns
# "incidence" when looking up by "connection".
reverse = lookup_term_aliases(db, "connection", "geometry")
assert "incidence" in reverse
def test_term_alias_domain_isolation(db):
"""Same term in different domains stay distinct."""
add_term_alias(
db, term="set", alternate_term="class",
domain="set-theory", decision_by="fox",
)
add_term_alias(
db, term="set", alternate_term="ensemble",
domain="combinatorics", decision_by="fox",
)
assert lookup_term_aliases(db, "set", "set-theory") == ["class"]
assert lookup_term_aliases(db, "set", "combinatorics") == ["ensemble"]
def test_term_alias_lowercase_normalization(db):
"""Term + alternate + domain stored lowercased; lookup is
case-insensitive."""
add_term_alias(
db, term="Incidence", alternate_term="Connection",
domain="GEOMETRY", decision_by="fox",
)
assert lookup_term_aliases(db, "incidence", "geometry") == ["connection"]
assert lookup_term_aliases(db, "INCIDENCE", "Geometry") == ["connection"]
def test_term_alias_remove(db):
add_term_alias(
db, term="x", alternate_term="y",
domain="z", decision_by="fox",
)
removed = remove_term_alias(db, "x", "y", "z")
assert removed is True
assert lookup_term_aliases(db, "x", "z") == []
def test_term_alias_audit_discipline(db):
"""Empty fields raise — fail-closed."""
with pytest.raises(ValueError, match="decision_by must be non-empty"):
add_term_alias(
db, term="a", alternate_term="b",
domain="c", decision_by="",
)
with pytest.raises(ValueError, match="domain must be non-empty"):
add_term_alias(
db, term="a", alternate_term="b",
domain="", decision_by="fox",
)
# --- query expansion -------------------------------------------------
def test_expand_query_with_term_aliases_basic(db):
add_term_alias(
db, term="incidence", alternate_term="connection",
domain="geometry", decision_by="fox",
)
expanded = expand_query_with_term_aliases(
"incidence axiom", "geometry", db
)
assert "(incidence OR connection)" in expanded
# Other tokens pass through.
assert "axiom" in expanded
def test_expand_query_preserves_phrase_syntax(db):
add_term_alias(
db, term="line incidence", alternate_term="line connection",
domain="geometry", decision_by="fox",
)
expanded = expand_query_with_term_aliases(
'"line incidence"', "geometry", db
)
assert '"line incidence"' in expanded
assert '"line connection"' in expanded
assert " OR " in expanded
def test_expand_query_no_aliases_passes_through(db):
"""No registered aliases → query unchanged."""
q = "modus ponens"
expanded = expand_query_with_term_aliases(q, "logic", db)
assert expanded == q
def test_expand_query_unreachable_db_returns_original():
"""Bad DB path → query unchanged (fail-closed; aliases are opt-in)."""
expanded = expand_query_with_term_aliases(
"incidence", "geometry", "/nonexistent/path/aliases.db"
)
assert expanded == "incidence"