core/evals/capability_index/index.py
Shay 514c6c52ca feat(evals): AGI-roadmap Phase 1 — cross-domain capability index (the MEASURE yardstick)
The instrument that gates every later "more capable" claim and makes "general,
not narrow" a number. evals/capability_index/ composes the self-loading
independent-gold reasoning lanes (deductive_logic, dimensional, relational_metric)
into one report with honest, un-gameable axes:

- accuracy (of committed answers; wrong stays 0 in assert mode),
- coverage (attempted-not-refused),
- coverage_geomean — the headline: geometric mean of per-domain coverage, which is
  0 if ANY domain has zero coverage, so a narrow per-domain win cannot move it; it
  rises only when breadth rises,
- capability_score = coverage_geomean × accuracy, HARD-GATED to 0 if any domain
  committed a wrong answer (assert-mode invariant),
- a deterministic digest (the replayable baseline the autonomous loop must climb).

Baseline (today): score 0.9196, accuracy 1.0, breadth 3, wrong_total 0 — high
because all three composed lanes are formal/structured; when comprehension-gated
NL domains join, the geomean will honestly drop to expose the breadth gap (the
instrument working). Adapters surface any lane that fails to run as not_covered —
no silent drop (proven: it caught a deductive-report shape mismatch mid-build).

Pure aggregation + the geomean anti-gaming property + the wrong=0 hard gate are
unit-tested; a real-composition integration test asserts wrong=0 + breadth=3.
10 tests + 52 architectural invariants pass. Additive (new evals/ package).
Part of docs/analysis/AGI-candidacy-autonomous-improvement-roadmap-2026-06-05.md (Phase 1).
2026-06-05 15:17:46 -07:00

153 lines
5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""The capability-index schema + pure aggregation (Phase 1 core).
Pure functions over per-domain counts — no lane execution here (that is
``adapters.py``), so the math is trivially testable and the anti-gaming property
is provable in isolation.
"""
from __future__ import annotations
import hashlib
import json
import math
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class DomainResult:
"""One domain's outcome counts on its independent-gold lane."""
domain: str
correct: int
wrong: int
refused: int
@property
def total(self) -> int:
return self.correct + self.wrong + self.refused
@property
def attempted(self) -> int:
"""Committed an answer (not refused)."""
return self.correct + self.wrong
@property
def coverage(self) -> float:
"""Fraction it was willing to answer."""
return self.attempted / self.total if self.total else 0.0
@property
def accuracy(self) -> float:
"""Accuracy OF COMMITTED answers (1.0 when it commits nothing wrong)."""
return self.correct / self.attempted if self.attempted else 1.0
@dataclass(frozen=True, slots=True)
class CapabilityIndex:
domains: tuple[DomainResult, ...]
@property
def wrong_total(self) -> int:
return sum(d.wrong for d in self.domains)
@property
def assert_mode_valid(self) -> bool:
"""Assert-mode invariant: zero wrong commits across all domains."""
return self.wrong_total == 0
@property
def _attempted(self) -> int:
return sum(d.attempted for d in self.domains)
@property
def _total(self) -> int:
return sum(d.total for d in self.domains)
@property
def coverage(self) -> float:
"""Micro coverage across all cases."""
return self._attempted / self._total if self._total else 0.0
@property
def accuracy(self) -> float:
"""Micro accuracy of committed answers."""
correct = sum(d.correct for d in self.domains)
return correct / self._attempted if self._attempted else 1.0
@property
def coverage_geomean(self) -> float:
"""Geometric mean of per-domain coverage — the anti-gaming headline.
Zero if ANY domain has zero coverage, so a narrow per-domain win cannot
move it; it rises only when breadth rises. This is "general, not narrow"
as a number.
"""
if not self.domains:
return 0.0
# geomean = exp(mean(log(coverage))); any 0 -> 0.
if any(d.coverage <= 0.0 for d in self.domains):
return 0.0
log_sum = sum(math.log(d.coverage) for d in self.domains)
return math.exp(log_sum / len(self.domains))
@property
def breadth(self) -> int:
"""How many domains the engine covers at all."""
return sum(1 for d in self.domains if d.coverage > 0.0)
@property
def min_domain_coverage(self) -> float:
return min((d.coverage for d in self.domains), default=0.0)
@property
def capability_score(self) -> float:
"""The single number: breadth-aware coverage × accuracy, hard-gated on
the assert-mode invariant (any wrong commit zeroes it)."""
if not self.assert_mode_valid:
return 0.0
return self.coverage_geomean * self.accuracy
def aggregate(results: list[DomainResult]) -> CapabilityIndex:
"""Aggregate per-domain results into the cross-domain index."""
return CapabilityIndex(domains=tuple(results))
def deterministic_digest(index: CapabilityIndex) -> str:
"""SHA-256 over the per-domain counts + verdict axes (reproducible)."""
payload = {
"domains": [
{"domain": d.domain, "correct": d.correct, "wrong": d.wrong, "refused": d.refused}
for d in sorted(index.domains, key=lambda d: d.domain)
],
"wrong_total": index.wrong_total,
"assert_mode_valid": index.assert_mode_valid,
}
serialized = json.dumps(payload, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
def index_to_dict(index: CapabilityIndex) -> dict:
"""JSON-safe report view of the index."""
return {
"capability_score": round(index.capability_score, 6),
"coverage_geomean": round(index.coverage_geomean, 6),
"coverage_micro": round(index.coverage, 6),
"accuracy_micro": round(index.accuracy, 6),
"breadth": index.breadth,
"min_domain_coverage": round(index.min_domain_coverage, 6),
"wrong_total": index.wrong_total,
"assert_mode_valid": index.assert_mode_valid,
"deterministic_digest": deterministic_digest(index),
"domains": [
{
"domain": d.domain,
"correct": d.correct,
"wrong": d.wrong,
"refused": d.refused,
"coverage": round(d.coverage, 6),
"accuracy": round(d.accuracy, 6),
}
for d in sorted(index.domains, key=lambda d: d.domain)
],
}