2026-07-03 08:51:03 +00:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from pacta.lean import LeanTools, build_lean_env, build_lean_invocation, lean_check_files, parse_axiom_output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_axiom_output_multiline():
|
|
|
|
|
output = """
|
|
|
|
|
'CurveFieldProofs.fieldImplementation' depends on axioms:
|
|
|
|
|
[propext, Classical.choice, Quot.sound]
|
|
|
|
|
'CurveFieldProofs.edwardsImplementation' depends on axioms:
|
|
|
|
|
[propext, Classical.choice, Quot.sound]
|
|
|
|
|
"""
|
|
|
|
|
parsed = parse_axiom_output(
|
|
|
|
|
output,
|
|
|
|
|
["CurveFieldProofs.fieldImplementation", "CurveFieldProofs.edwardsImplementation"],
|
|
|
|
|
)
|
|
|
|
|
assert parsed["CurveFieldProofs.fieldImplementation"] == ["propext", "Classical.choice", "Quot.sound"]
|
|
|
|
|
assert parsed["CurveFieldProofs.edwardsImplementation"] == ["propext", "Classical.choice", "Quot.sound"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_axiom_output_no_axioms_wording():
|
|
|
|
|
output = "'CurveFieldProofs.fieldImplementation' does not depend on any axioms\n"
|
|
|
|
|
parsed = parse_axiom_output(output, ["CurveFieldProofs.fieldImplementation"])
|
|
|
|
|
assert parsed["CurveFieldProofs.fieldImplementation"] == []
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 09:04:59 +00:00
|
|
|
def test_parse_axiom_output_axiom_free_cert_does_not_steal_next_cone():
|
|
|
|
|
# Regression (found by the entry-13 rehearsal, 2026-07-16): an
|
|
|
|
|
# axiom-free certificate is followed by a cone-carrying one. The old
|
|
|
|
|
# windowed search reached past the bracketless "does not depend"
|
|
|
|
|
# sentence and attributed the NEXT certificate's cone. The
|
|
|
|
|
# accumulator corpus is the first subject with axiom-free
|
|
|
|
|
# certificates (domsep, Hash, instDecidableEqHash, take_append_drop),
|
|
|
|
|
# so no fork attestation ever exercised this path.
|
|
|
|
|
output = (
|
|
|
|
|
"'LTLAcc.domsep' does not depend on any axioms\n"
|
|
|
|
|
"'LTLAcc.eq_dropLast_append_of_getLast?' depends on axioms: [propext]\n"
|
|
|
|
|
)
|
|
|
|
|
parsed = parse_axiom_output(output, ["LTLAcc.domsep", "LTLAcc.eq_dropLast_append_of_getLast?"])
|
|
|
|
|
assert parsed["LTLAcc.domsep"] == []
|
|
|
|
|
assert parsed["LTLAcc.eq_dropLast_append_of_getLast?"] == ["propext"]
|
|
|
|
|
|
|
|
|
|
|
fix: fail-closed certificate classification + record-scoped axiom parsing (review round 6)
R6-B1 (Claude, executed end-to-end): provenness was decided by a
WHOLE-OUTPUT 'no axioms' sentence, so an axiom-free certificate whose
line was entirely absent still scored proven+clean ([]==[]). The
reviewer drove a doctored 60-line output through the real gate and got
61/61 with domsep never audited. Classification is now extracted into
classify_certificates(): proven iff the certificate's OWN anchor was
parsed (axiom-free anchors populate []); absent certs are
unknown/failed + not_checked — never clean. This also fail-closes the
typo'd-future-cert case (R6-C2).
GPT §6: parse_axiom_output is now RECORD-scoped — anchors delimit
records, a cone bracket is accepted only inside its own record, missing
or truncated brackets yield MISSING (fail closed) instead of borrowing
the next certificate's bracket, and cones may wrap arbitrarily (the
old fixed 16-line window was a latent overflow for the 11-axiom
ed25519 apex cones on this estate). Anchor names are captured between
the exact quotes Lean prints.
Six new regression tests (absent-axiom-free-not-clean, missing-bracket
no-steal, truncated cone, >16-line wrap, duplicate anchor, interleaved
diagnostics). Suite: 114 passed / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:15:58 +00:00
|
|
|
def test_classify_absent_axiom_free_cert_is_not_clean():
|
|
|
|
|
# Regression (round-6 Claude R6-B1, executed end-to-end by the
|
|
|
|
|
# reviewer): domsep's line deleted from otherwise-pristine output
|
|
|
|
|
# still yielded 61/61 because a whole-output "no axioms" sentence +
|
|
|
|
|
# ([] == []) scored the ABSENT cert proven+clean. Provenness must be
|
|
|
|
|
# the cert's own anchor, i.e. membership in the parsed dict.
|
|
|
|
|
from pacta.lean import classify_certificates
|
|
|
|
|
|
|
|
|
|
output = (
|
|
|
|
|
"'LTLAcc.Hash' does not depend on any axioms\n"
|
|
|
|
|
"'LTLAcc.MTH' depends on axioms: [propext, LTLAcc.sha256, Quot.sound]\n"
|
|
|
|
|
)
|
|
|
|
|
certs = ["LTLAcc.Hash", "LTLAcc.domsep", "LTLAcc.MTH"]
|
|
|
|
|
parsed = parse_axiom_output(output, certs)
|
|
|
|
|
results = classify_certificates(parsed, certs, 0, lambda c: [])
|
|
|
|
|
by_name = {r.name: r for r in results}
|
|
|
|
|
assert by_name["LTLAcc.Hash"].status == "proven"
|
|
|
|
|
assert by_name["LTLAcc.domsep"].status == "unknown"
|
|
|
|
|
assert by_name["LTLAcc.domsep"].axiom_status == "not_checked"
|
|
|
|
|
assert not all(r.axiom_status == "clean" for r in results)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_missing_bracket_does_not_steal_next_record():
|
|
|
|
|
# GPT round-6 §6: a cone-bearing anchor with a MISSING bracket must
|
|
|
|
|
# not consume the next certificate's bracket.
|
|
|
|
|
output = (
|
|
|
|
|
"'A.a' depends on axioms:\n"
|
|
|
|
|
"'B.b' depends on axioms: [propext]\n"
|
|
|
|
|
)
|
|
|
|
|
parsed = parse_axiom_output(output, ["A.a", "B.b"])
|
|
|
|
|
assert "A.a" not in parsed
|
|
|
|
|
assert parsed["B.b"] == ["propext"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_truncated_cone_is_missing():
|
|
|
|
|
output = "'A.a' depends on axioms: [propext, Classical.choice\n"
|
|
|
|
|
parsed = parse_axiom_output(output, ["A.a"])
|
|
|
|
|
assert "A.a" not in parsed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_long_wrapped_cone_beyond_old_window():
|
|
|
|
|
# The ed25519 apex tiers carry 11 axioms; the old fixed 16-line
|
|
|
|
|
# window was a latent overflow. Records now extend to the next
|
|
|
|
|
# anchor regardless of length.
|
|
|
|
|
items = [f"Ax{i}" for i in range(11)]
|
|
|
|
|
wrapped = "[\n" + ",\n".join(items) + "\n" * 10 + "]"
|
|
|
|
|
output = f"'A.a' depends on axioms: {wrapped}\n'B.b' does not depend on any axioms\n"
|
|
|
|
|
parsed = parse_axiom_output(output, ["A.a", "B.b"])
|
|
|
|
|
assert parsed["A.a"] == items
|
|
|
|
|
assert parsed["B.b"] == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_duplicate_anchor_first_wins():
|
|
|
|
|
output = (
|
|
|
|
|
"'A.a' depends on axioms: [propext]\n"
|
|
|
|
|
"'A.a' depends on axioms: [Quot.sound]\n"
|
|
|
|
|
)
|
|
|
|
|
parsed = parse_axiom_output(output, ["A.a"])
|
|
|
|
|
assert parsed["A.a"] == ["propext"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_interleaved_diagnostics_inside_record():
|
|
|
|
|
output = (
|
|
|
|
|
"'A.a' depends on axioms:\n"
|
|
|
|
|
"warning: something unrelated\n"
|
|
|
|
|
"[propext, Quot.sound]\n"
|
|
|
|
|
"'B.b' does not depend on any axioms\n"
|
|
|
|
|
)
|
|
|
|
|
parsed = parse_axiom_output(output, ["A.a", "B.b"])
|
|
|
|
|
assert parsed["A.a"] == ["propext", "Quot.sound"]
|
|
|
|
|
assert parsed["B.b"] == []
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 09:04:59 +00:00
|
|
|
def test_parse_axiom_output_exact_name_not_prefix():
|
|
|
|
|
# 'LTLAcc.MTH' must not match the line for 'LTLAcc.MTH_single' even
|
|
|
|
|
# when the latter comes first in the output.
|
|
|
|
|
output = (
|
|
|
|
|
"'LTLAcc.MTH_single' depends on axioms: [propext, LTLAcc.sha256, Quot.sound]\n"
|
|
|
|
|
"'LTLAcc.MTH' depends on axioms: [propext, LTLAcc.sha256, Quot.sound]\n"
|
|
|
|
|
)
|
|
|
|
|
parsed = parse_axiom_output(output, ["LTLAcc.MTH"])
|
|
|
|
|
assert parsed["LTLAcc.MTH"] == ["propext", "LTLAcc.sha256", "Quot.sound"]
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 08:51:03 +00:00
|
|
|
def test_mac_safe_lean_command_is_argument_list():
|
|
|
|
|
tools = LeanTools(lean="/usr/local/bin/lean", lake=None)
|
2026-07-03 11:03:58 +00:00
|
|
|
cmd = build_lean_invocation(Path("Proofs/A.lean"), tools, output_path=Path("Proofs/A.olean"), root_path=Path("."))
|
|
|
|
|
assert cmd == ["/usr/local/bin/lean", "--root=.", "-o", "Proofs/A.olean", "Proofs/A.lean"]
|
2026-07-03 08:51:03 +00:00
|
|
|
assert "timeout" not in cmd
|
|
|
|
|
assert "taskset" not in cmd
|
|
|
|
|
assert "free" not in cmd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_lean_path_uses_python_pathsep(tmp_path):
|
|
|
|
|
verification = tmp_path / "verification"
|
|
|
|
|
(verification / "gen").mkdir(parents=True)
|
|
|
|
|
env = build_lean_env(verification, base_env={"LEAN_PATH": "old"})
|
|
|
|
|
assert str(verification / "gen") in env["LEAN_PATH"]
|
|
|
|
|
assert str(verification) in env["LEAN_PATH"]
|
|
|
|
|
assert "old" in env["LEAN_PATH"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_lean_check_requires_discovered_files(tmp_path):
|
|
|
|
|
result = lean_check_files([], tmp_path / "verification")
|
|
|
|
|
assert not result.attempted
|
|
|
|
|
assert "No Lean files" in result.diagnostics[0]
|