Files
turboquant_plus/refract/tests/test_report_json_layout.py
TheTomandClaude Opus 4.7 a4c59a0b05 REFRACT v0.3.2: alpha release — 4 axes, 4 subcommands, 3 backends, 9 docs
Compressed history of v0.1.4 → v0.3.2 in one alpha-ship commit so the
framework is testable end-to-end by friends. Detailed per-version
discussion lives in refract/CHANGELOG.md.

What ships
----------

Four scoring axes vs the model's own fp16-KV reference:
  - Axis A (Trajectory, v0.1.4) — decode-time token IDs via patched
    llama-completion, no detokenize round-trip. Replaces the buggy
    v0.1.x GTM as the default.
  - Axis B (KLD@D) — corpus KL divergence, bit-exact zero on Metal
    when ref==cand. Native MLX implementation in addition to the
    llama-perplexity subprocess path.
  - Axis C (R-NIAH, v0.2.0) — long-context retrieval at multiple
    (length, position) cells. Confidence guard for low base_acc.
  - Axis D (PLAD, v0.2.0) — perturbation-locality drift across
    typo/case/punct/paraphrase. Skipped-perturbation handling.

Composite = harmonic mean. Bands EXCELLENT/PASS/DEGRADED/FAIL.

Four CLI subcommands:
  - score          — run REFRACT on a candidate KV config
  - selftest       — 30s preflight: binaries, flags, model probe
  - compare        — multi-report side-by-side
  - repeatability  — run N times, report stdev/range health verdict

Three backends:
  - llamacpp (production)        subprocess wrappers around llama.cpp
  - mlx (production)             native via mlx-lm; verified end-to-end
                                 on Qwen2.5-3B-Instruct-4bit
  - vllm (skeleton)              interface defined; plug-in pointers
                                 in module docstring

Reporting
---------

  - Text (ANSI colour, terminal)
  - JSON schema refract.report.v0.3.1 with framework_version +
    environment metadata (backend, llama_cpp_commit, mlx_lm_version)
  - HTML (self-contained single file) with hardware info, model
    params, repro command, R-NIAH heatmap, PLAD per-perturbation bars

Discoveries from the matrix runs (all logged in CHANGELOG):
  - "secret password" needle wording triggers safety refusals on
    instruct models. v0.2.1 switched to neutral "rare paint color"
  - Thinking-mode models burn n_predict before answering. v0.3.0 added
    -rea off + auto-detect at startup
  - Symmetric turbo4/turbo4 is the canonical FAIL config; framework
    correctly identifies (composite ~19) on gemma-4-26B-A4B-Q8

Documentation
-------------

  - README.md         what + alpha banner + "where do I go" navigator
  - QUICKSTART.md     0 to first PASS in 30 minutes
  - INTERPRETATION.md per-axis "what to do if low" guide
  - LEADERBOARD.md    cross-model rankings + "ranks the quant, not the
                      model" disclaimer
  - PITFALLS.md       known traps from today's matrix runs
  - LIMITATIONS.md    what v0.3 explicitly does not do
  - CHANGELOG.md      version history with discovery stories
  - MATRIX-RESULTS.md the 7-model matrix log
  - examples/        4 sample JSONs + HTML reports + README

Tests
-----

80 → 82 unit tests, all green:
  - 17 trajectory tests (incl. R-NIAH/PLAD impls + 4-axis composite)
  - 5 strip-noise pinning tests
  - 14 unit tests (math + KVConfig)
  - 8 JSON schema tests (incl. v0.3.1 framework_version field)
  - 6 KLD regex tests
  - 5 corpus-identity tests
  - 5 tokenize-to-ids tests
  - 4 command-construction tests

Verified
--------

  - 7-model v0.3.0 matrix end-to-end (~2h on M5 Max 128GB)
  - Llama-4 Scout extras + symmetric-turbo negative control
  - MLX backend on Qwen2.5-3B-Instruct-4bit
  - selftest on llamacpp (no model) and mlx (with model)
  - 2-run repeatability on qwen2.5-1.5b: composite stdev = 0.00 on
    Trajectory + KLD (bit-exact reproducibility on cheap axes)

Open follow-ups (v0.4)
----------------------

  - vLLM backend implementation (skeleton today)
  - T-Call axis (tool-call fidelity) building on v0.3 chat-template
  - Bundled corpus + prompts distribution
  - `refract leaderboard --in dir/ --out LEADERBOARD.md` auto-gen

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 15:13:04 -05:00

137 lines
4.6 KiB
Python

"""v0.1.3 regression tests for report.json_report() schema layout.
Pins the v0.1.1 schema fix (composite is a scalar, not a nested dict at
top level) and the v0.1.3 schema bump.
"""
from __future__ import annotations
from refract.axes.gtm import GTMResult
from refract.axes.kld import KLDResult
from refract.report import json_report
from refract.score import composite_score
def _build_minimal_report() -> dict:
gtm = GTMResult(
score=92.0,
full_match_rate=0.5,
median_first_divergence=20,
mean_prefix_agreement_length=44.0,
mean_cand_length=48.0,
mean_ref_length=48.0,
n_prompts=2,
n_tokens_each=48,
per_prompt=[
{"id": "p1", "ref": "x", "cand": "x"},
{"id": "p2", "ref": "y", "cand": "z"},
],
)
kld = KLDResult(
score=99.5, mean_kld=0.005,
ppl=6.0, rms_dp_pct=1.1, same_topp_pct=98.4,
base_path="/tmp/base.bin", chunks=32, ctx=512,
is_self_reference=False,
corpus={"path": "/tmp/wiki.raw", "size_bytes": 1234,
"sha256_head": "deadbeef", "sha256_head_bytes": 1234},
)
composite = composite_score(gtm.score, kld.score)
return json_report(
model="test.gguf",
reference_label="ctk=f16,ctv=f16",
candidate_label="ctk=q8_0,ctv=turbo4",
composite=composite, gtm=gtm, kld=kld,
)
def test_composite_is_scalar_at_top_level():
"""v0.1.1 regression: d['composite'] must be a number, not a nested dict.
v0.1 had this nested as a dict; aggregator scripts printed '{compos…' as
if it were a number."""
rep = _build_minimal_report()
assert isinstance(rep["composite"], float)
# Sanity: it's the actual harmonic mean of 92 and 99.5
assert 90 < rep["composite"] < 100
def test_band_is_string_at_top_level():
rep = _build_minimal_report()
assert isinstance(rep["band"], str)
assert rep["band"] in {"EXCELLENT", "PASS", "DEGRADED", "FAIL"}
def test_composite_detail_is_dict():
rep = _build_minimal_report()
assert isinstance(rep["composite_detail"], dict)
# gtm_score / kld_score live inside composite_detail
assert "gtm_score" in rep["composite_detail"]
assert "kld_score" in rep["composite_detail"]
def test_axes_block_present():
rep = _build_minimal_report()
assert "axes" in rep
assert "gtm" in rep["axes"]
assert "kld" in rep["axes"]
# v0.1.3: GTM block exposes the new mean_cand/mean_ref fields
assert rep["axes"]["gtm"]["mean_cand_length"] == 48.0
assert rep["axes"]["gtm"]["mean_ref_length"] == 48.0
def test_schema_is_v0_3_1():
rep = _build_minimal_report()
assert rep["schema"] == "refract.report.v0.3.1"
def test_framework_version_present():
rep = _build_minimal_report()
assert "framework_version" in rep
assert isinstance(rep["framework_version"], str)
def test_environment_block_present():
rep = _build_minimal_report()
assert "environment" in rep
assert isinstance(rep["environment"], dict)
# v0.1.4 additions: layman summary + per-axis bands/descriptions.
def test_summary_is_layman_string():
"""v0.1.4: top-level 'summary' field carries a one-line plain-English
interpretation of the score band so non-techie consumers can read the
report without grepping the paper."""
rep = _build_minimal_report()
assert isinstance(rep.get("summary"), str)
assert rep["summary"] # non-empty
# The summary's content reflects the band; pin a substring of each band
# so the prose can be edited but the contract holds.
band_to_substr = {
"EXCELLENT": "Indistinguishable",
"PASS": "Minor drift",
"DEGRADED": "Audit",
"FAIL": "broken",
}
expected = band_to_substr[rep["band"]]
assert expected.lower() in rep["summary"].lower(), (
f"summary {rep['summary']!r} does not match band {rep['band']!r}"
)
def test_axes_carry_per_axis_band():
"""v0.1.4: each axis block carries its own band, not just the
composite. Lets the layman see which axis caused a DEGRADED."""
rep = _build_minimal_report()
assert rep["axes"]["gtm"]["band"] in {"EXCELLENT", "PASS", "DEGRADED", "FAIL"}
assert rep["axes"]["kld"]["band"] in {"EXCELLENT", "PASS", "DEGRADED", "FAIL"}
def test_axes_carry_description():
"""v0.1.4: each axis block has a one-line 'description' so a layman
knows what 'KLD: DEGRADED' actually means without the paper."""
rep = _build_minimal_report()
for ax in ("gtm", "kld"):
d = rep["axes"][ax].get("description")
assert isinstance(d, str) and d, f"missing description on axis {ax}"