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.
257 lines
7.8 KiB
Python
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"
|