* feat(determine): add transitive strict-order relational inference Add sound transitive closure for declared strict-order relational predicates — less_than, greater_than, before_event, after_event, and ONLY these. A query p(a, c) determines True when a same-predicate chain p(a, b), p(b, c), ... over the predicate's OWN realized edges is found (BFS reachability) and the proof_chain ROBDD verifies the transitive entailment (search-then-verify, through the UNCHANGED evaluate_entailment via a new lower_transitive_chain). Mirrors _determine_subsumption. Scope / firewall: - TRANSITIVE_PREDICATES is closed and default-off; sibling_of, parent_of, left_of/right_of, inside_of, during_event, overlaps_event are EXCLUDED. - Same-predicate edges only — no transitive-through-inverse, no cross-predicate (triple firewall: recall filter + lowering re-reject + ROBDD re-verify). - Open-world: asserts only answer=True via the shared _relational_determined surface (rule="transitive"), so INV-30 stays at exactly 3 Determined sites and no answer=False is added. One-hop inverse/symmetric (#775) is unchanged. FrameVerdict / closed-world (ADR-0222) is untouched. wrong=0 bite (unit + lane + independent oracle): non-transitive-predicate chains, non-admitted spatial chains, mixed-predicate chains, disjoint chains, reflexive cycles, and inverse+transitive composition all REFUSE. Measurement: new evals/relational_transitive lane with an INDEPENDENT BFS transitive-closure oracle (imports no engine module; INV-25/27), cross-checked both directions (positives oracle-True, confusers oracle-False). Capability-index breadth 10 -> 11, wrong_total 0, deterministic digest re-frozen. Cross-PR reconcile: migrated the one-hop confuser rinf-ref-001 (a less_than chain, which now correctly determines transitively) to a B2 positive; repurposed its unit test to a non-transitive parent_of chain; corrected a pre-existing stale breadth==9 assertion (#775 added a 10th domain but never updated test_capability_index). Verified in the worktree: determine + transitive/one-hop lanes + capability baseline/index + ProofWriter-OWA floor + INV-30/29/21/02 + full smoke — all green. A 3-skeptic read-only adversarial review found no wrong=0 leak. * test(determine): B2 hardening — TRANSITIVE_PREDICATES pin + lower_transitive defensive + budget Add the required B2 hardening tests before merge: - test_transitive_predicates_closed_and_excludes: the table is EXACTLY {less_than, greater_than, before_event, after_event}, every member is in RELATIONAL_PREDICATES, and every deliberately-excluded predicate (sibling_of, spouse_of, parent_of, child_of, left_of, right_of, inside_of, during_event, overlaps_event) is explicitly absent. (Closes the gap where relational.py's comment claimed this test existed but it did not.) - tests/test_composition_lower_transitive.py: direct defensive tests on lower_transitive_chain — empty path, mislabeled (cross-predicate) edge, wrong arity, non-contiguous path, and path-not-reaching-target all lower to None (refuse); plus exact 2-hop and 3-hop theory pins. - test_edge_budget_exhaustion_refuses: above _TRANSITIVE_EDGE_BUDGET the transitive search declines (a safe coverage refusal), never proves.
126 lines
5.1 KiB
Python
126 lines
5.1 KiB
Python
"""Cross-domain capability index — AGI-roadmap Phase 1 (MEASURE).
|
||
|
||
The yardstick that gates every later "more capable" claim. Two honest axes —
|
||
**accuracy** (of committed answers; wrong stays 0 in assert mode) and **coverage**
|
||
(attempted-not-refused) — aggregated across domains so it CANNOT be gamed by a
|
||
narrow per-domain win: the headline coverage is the GEOMETRIC MEAN across domains,
|
||
which only rises if *every* domain rises. A hack that maxes one lane and leaves
|
||
the rest at zero leaves the geomean ~0.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from evals.capability_index.index import (
|
||
DomainResult,
|
||
aggregate,
|
||
deterministic_digest,
|
||
)
|
||
|
||
|
||
def _d(domain: str, correct: int, wrong: int, refused: int) -> DomainResult:
|
||
return DomainResult(domain=domain, correct=correct, wrong=wrong, refused=refused)
|
||
|
||
|
||
def test_domain_result_axes() -> None:
|
||
r = _d("logic", correct=8, wrong=0, refused=2)
|
||
assert r.total == 10
|
||
assert r.attempted == 8
|
||
assert r.coverage == 0.8
|
||
assert r.accuracy == 1.0 # of committed answers
|
||
|
||
|
||
def test_aggregate_axes_micro() -> None:
|
||
idx = aggregate([_d("a", 6, 0, 4), _d("b", 2, 0, 8)])
|
||
assert idx.wrong_total == 0
|
||
assert idx.coverage == 0.4 # (6+2)/(10+10) micro
|
||
assert idx.accuracy == 1.0 # no wrong
|
||
assert idx.breadth == 2 # both domains have some coverage
|
||
|
||
|
||
def test_geomean_coverage_resists_narrow_gaming() -> None:
|
||
# A NARROW hack: one domain maxed, the rest at zero coverage.
|
||
narrow = aggregate(
|
||
[_d("gamed", 10, 0, 0), _d("x", 0, 0, 10), _d("y", 0, 0, 10)]
|
||
)
|
||
# A BALANCED engine: every domain partially covered.
|
||
balanced = aggregate(
|
||
[_d("gamed", 4, 0, 6), _d("x", 4, 0, 6), _d("y", 4, 0, 6)]
|
||
)
|
||
# Micro-coverage is similar (~0.33 vs 0.40), but the geomean exposes the hack:
|
||
assert narrow.coverage_geomean == 0.0 # any zero-coverage domain -> geomean 0
|
||
assert balanced.coverage_geomean > 0.39
|
||
# The capability score (geomean × accuracy) refuses to reward the narrow hack.
|
||
assert narrow.capability_score == 0.0
|
||
assert balanced.capability_score > 0.39
|
||
|
||
|
||
def test_balanced_progress_moves_the_score_monotonically() -> None:
|
||
low = aggregate([_d("a", 2, 0, 8), _d("b", 2, 0, 8)])
|
||
high = aggregate([_d("a", 6, 0, 4), _d("b", 6, 0, 4)])
|
||
assert high.coverage_geomean > low.coverage_geomean
|
||
assert high.capability_score > low.capability_score
|
||
|
||
|
||
def test_wrong_is_a_hard_gate() -> None:
|
||
# In assert mode wrong MUST be 0; any wrong invalidates the index (score 0)
|
||
# and is surfaced — never averaged away.
|
||
idx = aggregate([_d("a", 8, 1, 1), _d("b", 5, 0, 5)])
|
||
assert idx.wrong_total == 1
|
||
assert idx.assert_mode_valid is False
|
||
assert idx.capability_score == 0.0 # wrong=0 is non-negotiable in assert mode
|
||
|
||
|
||
def test_digest_is_deterministic_and_bites() -> None:
|
||
a = aggregate([_d("a", 6, 0, 4), _d("b", 2, 0, 8)])
|
||
b = aggregate([_d("a", 6, 0, 4), _d("b", 2, 0, 8)])
|
||
assert deterministic_digest(a) == deterministic_digest(b)
|
||
moved = aggregate([_d("a", 7, 0, 3), _d("b", 2, 0, 8)])
|
||
assert deterministic_digest(moved) != deterministic_digest(a)
|
||
|
||
|
||
def test_empty_index_is_well_defined() -> None:
|
||
idx = aggregate([])
|
||
assert idx.coverage == 0.0
|
||
assert idx.coverage_geomean == 0.0
|
||
assert idx.breadth == 0
|
||
assert idx.capability_score == 0.0
|
||
|
||
|
||
def test_real_lanes_compose_into_the_index_with_wrong_zero() -> None:
|
||
# The baseline: three structured-input reasoning lanes PLUS the comprehension lanes
|
||
# (prose -> MeaningGraph -> projection -> independent oracle / pack-named predicate)
|
||
# compose into the cross-domain index with zero wrong commits. The one-hop
|
||
# relational_inference lane (#775) is the 10th domain; the transitive
|
||
# relational_transitive lane (B2) is the 11th. (This assertion was stale at 9 from
|
||
# the #596 era — #775's 10th domain was never added here; corrected with B2.)
|
||
from evals.capability_index.adapters import collect_domain_results
|
||
|
||
collection = collect_domain_results()
|
||
assert collection.not_covered == () # every adapter ran (no silent drop)
|
||
idx = aggregate(list(collection.results))
|
||
assert idx.wrong_total == 0
|
||
assert idx.assert_mode_valid
|
||
assert idx.breadth == 11
|
||
assert {d.domain for d in idx.domains} == {
|
||
"deductive_logic",
|
||
"dimensional",
|
||
"relational_metric",
|
||
"comprehension_set_membership",
|
||
"comprehension_syllogism",
|
||
"comprehension_total_ordering",
|
||
"comprehension_propositional",
|
||
"comprehension_relational_metric",
|
||
"comprehension_relational_predicate",
|
||
"comprehension_relational_inference",
|
||
"comprehension_relational_transitive",
|
||
}
|
||
assert idx.capability_score > 0.5 # real, non-trivial cross-domain capability
|
||
|
||
|
||
def test_index_report_is_deterministic_across_runs() -> None:
|
||
# The capability number is reproducible — improvement is a replayable curve.
|
||
from evals.capability_index.adapters import collect_domain_results
|
||
|
||
a = deterministic_digest(aggregate(list(collect_domain_results().results)))
|
||
b = deterministic_digest(aggregate(list(collect_domain_results().results)))
|
||
assert a == b
|