fix: axiom-output parser mis-attributed cones to axiom-free certificates

Found by the entry-13 rehearsal (the accumulator corpus is the first
subject with axiom-free certificates — every fork certificate carries
at least the boundary axiom, so this path was never exercised):

- an axiom-free certificate ('X' does not depend on any axioms) was
  parsed by opening a 16-line window and taking the first bracket in
  it — which belongs to the NEXT certificate. domsep/Hash/
  instDecidableEqHash/take_append_drop were reported with their
  neighbors' cones and flagged dirty.
- certificate names were matched as bare substrings, so 'Foo' could
  anchor on the line for 'Foo_bar'. Now anchored on the exact quoted
  name Lean prints.

The no-axioms decision is now made on the anchor line itself, before
any window. Two regression tests added (steal-next-cone, exact-name);
suite 108 passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-07-16 11:04:59 +02:00
parent 3d81d5380c
commit 34a04572f0
2 changed files with 41 additions and 4 deletions

View file

@ -320,10 +320,22 @@ def parse_axiom_output(output: str, certificates: list[str]) -> dict[str, list[s
results: dict[str, list[str]] = {}
lines = output.splitlines()
for cert in certificates:
# Anchor on the exact quoted name Lean prints ('X' depends on … /
# 'X' does not depend on any axioms). A bare substring match would
# let 'Foo' hit the line for 'Foo_bar' first.
needle = f"'{cert}'"
cert_results: list[str] | None = None
for i, line in enumerate(lines):
if cert not in line:
if needle not in line:
continue
# Axiom-free certificates print a bracketless sentence. Decide
# on THIS line before opening any window: a window would reach
# into the NEXT certificate's bracket and steal its cone (found
# by the entry-13 rehearsal — the accumulator corpus is the
# first subject with axiom-free certificates).
if _mentions_no_axioms(line):
cert_results = []
break
# Lean wraps long axiom lists (the apex tiers carry 11 axioms)
# across many lines; take a window wide enough for the largest
# documented boundary and flatten it before matching, the same
@ -333,9 +345,6 @@ def parse_axiom_output(output: str, certificates: list[str]) -> dict[str, list[s
if bracket:
cert_results = [item.strip() for item in bracket.group(1).split(",") if item.strip()]
break
if _mentions_no_axioms(window):
cert_results = []
break
if cert_results is not None:
results[cert] = cert_results
return results

View file

@ -24,6 +24,34 @@ def test_parse_axiom_output_no_axioms_wording():
assert parsed["CurveFieldProofs.fieldImplementation"] == []
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"]
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"]
def test_mac_safe_lean_command_is_argument_list():
tools = LeanTools(lean="/usr/local/bin/lean", lake=None)
cmd = build_lean_invocation(Path("Proofs/A.lean"), tools, output_path=Path("Proofs/A.olean"), root_path=Path("."))