Files
Fredrik Dahlgren 7b9bd5f950 Add writing-lean-proofs plugin (#226)
* Add writing-lean-proofs plugin

Structured Lean 4 proof writing and library design following Mathlib
conventions: sorry-skeleton workflows, API-first definitions, lemma
extraction, and an anti-pattern catalog mapped to the linters that
enforce each rule.

Includes a review-flow eval suite (evals/): five Lean fixtures derived
from a real formal-verification project with known planted flaws and
known non-flaws, natural review prompts, per-case rubrics, and a runner
comparing a baseline arm against a skill arm with an LLM judge plus a
deterministic no-rewrite check. The grader ships a self-test that
asserts a known-bad review fails every criterion. Smoke-tested: the
baseline arm reproduces the folk-advice mistake the skill corrects
(calling redundant `show` lines noise); the skill arm passes 5/5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address Claude review findings

- Validate the grader's "id" field before writing grades.json, so a
  malformed grader response fails with a clear error instead of a
  KeyError in the summary.
- Restore title-case "When to Use"/"When NOT to Use" headings: the
  validator's required-section check matches them case-sensitively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address github-actions review findings

Eval harness:
- checksum_tree fails when it finds zero .lean files, so the no-rewrite
  check can no longer pass vacuously
- both arms run with --setting-sources project, so a user-level install
  of this skill cannot contaminate the baseline arm
- a failed case is recorded and the run continues; the runner exits
  non-zero after printing the summary
- critical steps in run_case return explicitly (errexit is suppressed
  when the function runs in an if-context)
- self-test prints its artifact dir; the on-failure leak is intentional
- case 04: dropped the trailing omega so the squeezed simp only in
  carry_le_one is genuinely terminal (the direction test was inverted)
- case 05: overall-verdict retyped from must-flag to overall, with the
  grader prompt told to judge such criteria solely by pass-when text
- fixture overlap removed: Felt.lean's unscored unfold replaced by a
  bound-free lemma; Bounds.lean's unscored Fact instance replaced by
  the NeZero instance ZMod.val_lt actually needs

Skill content (claims verified against Mathlib docs and the ImProver
paper):
- lake build alone does not catch sorries (they are warnings); step 4
  now includes an explicit grep gate
- style linters are enabled in Mathlib's own build but off by default
  downstream; the opt-in is now spelled out. The show, nameCheck, and
  setOption linters do exist, so those attributions stand.
- ImProver's 100% is on the paper's accuracy metric and holds by
  construction (fallback to unchanged input); now stated as such
- isCompact_union does not exist in Mathlib; example is isCompact_iUnion
- library-design.md now distinguishes rfl-proved API lemmas (correct)
  from downstream proofs needing rfl (the smell)
- plugin README Contents paths fixed to be relative to the skill dir

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address second-round github-actions review findings

Eval harness:
- case 05 gains an engagement criterion: the review must name specific
  declarations from the file, so an empty "looks fine" can no longer
  score 5/5 by omission
- the grader self-test is now two-sided: the canned bad review must
  fail every criterion (catches a permissive judge) and a new canned
  good review must pass every criterion (catches an over-strict judge)
- case 01 scores the native_decide in the Fact instance; it was the
  most severe flaw in the fixture and previously unscored
- checksum_tree excludes .claude/ so both arms fingerprint the same
  file set
- runtime check on the isolation guard: a baseline transcript that
  mentions writing-lean-proofs fails the case as contaminated
- results stamp includes the PID so same-second runs cannot collide
- case 04: mod_add_carry_mul uses non-terminal simp [carry] (a bare
  simp risked "no progress"/goal-closing errors that would invalidate
  the criterion); rubric wording updated

Skill content:
- the sorry check had inverted exit status for CI use (grep exits 1 on
  no match); now ! grep, with the comment/docstring false-positive
  caveat and #print axioms alternative spelled out
- anti-patterns.md documents native_decide (trust-base widening,
  caught by #print axioms, kernel/certificate alternatives)
- llm-techniques.md sibling links demoted to plain-text mentions:
  AGENTS.md prohibits reference chains (file1 -> file2), which the
  earlier link conversion had introduced

Verified: two-sided self-test passes (bad 0/7, good 7/7); live case 05
run scores baseline 2/6 vs skill 6/6 with no-rewrite passing in both
arms and no contamination false-positive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add linting, performance, and tactics references to Lean skill

* Make the eval harness's own checks able to fail

Five review findings, all cases of a check that reports rather than gates.

- no-rewrite gates the exit status. It was written, printed, and otherwise
  ignored: CASE_FAILURES only counted reviewer/grader *errors*, so a reviewer
  that started applying its own fixes under --permission-mode acceptEdits
  would rewrite every fixture, print no-rewrite:fail on every line, and still
  exit 0 — CI would read the run as green.

- Add the skill arm's mirror of the baseline contamination backstop. cp -R
  succeeding proves the skill is on disk, not that the CLI discovered it. If
  project-skill discovery under --setting-sources project ever changes, the
  skill arm would run bare and both arms would score alike — which is
  indistinguishable from the true negative "the skill provides no uplift",
  the one conclusion this suite exists to measure. One canary call per run
  asks the CLI what it can see.

- Guarantee the scratch tree is removed. run_case's body moved into
  run_case_body so the single rm -rf covers every early return; six paths
  (fixture copy, skill copy, both checksums, the reviewer call, the
  contamination check) previously leaked a fixture copy — plus a full skill
  copy on the skill arm — per case.

- Run the offline half of --self-test before the CLI preflight, so the
  schema, isolation-fixture and rubric checks work with no `claude` on PATH
  and no authentication — which is where you would want them, e.g. a CI
  shell-test suite.

- Count rubric criteria with the same pattern the Python capture uses. The
  shell count accepted `- id: foo bar`, which the capture drops, so a
  malformed line surfaced later as a confusing id-mismatch diff.

Also anchor linting.md's warnings-as-errors CI snippet against vacuity: Lake
caches per-module artifacts and linter warnings are emitted only on
recompile, so on a restored cache build.log is empty, the grep matches
nothing, and the gate reports clean over live warnings and sorries — the
exact failure the same file preaches against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: kz-tob <kara.zaffarano@trailofbits.com>
Co-authored-by: Marc Ilunga <marc.ilunga@trailofbits.com>
2026-08-07 16:23:15 -04:00

7.7 KiB
Raw Permalink Blame History

Evals: writing-lean-proofs (review flow)

This suite tests one flow of the writing-lean-proofs skill: reviewing an existing Lean file ("review this Lean file", "how could these proofs be improved"). It does not test proof writing, library design from scratch, or refactoring.

What a case is

Each case under cases/<name>/ is:

  • input/*.lean — a small, self-contained fixture with known planted flaws and known non-flaws (correct code that folk advice wrongly flags). Fixtures are close derivatives of a real Trail of Bits formal-verification project; the flaws are real patterns observed there, not inventions.
  • prompt.md — the user prompt, phrased the way a user actually asks ("Please review the Lean file X.lean").
  • rubric.md — grading criteria. must-flag: the review names the specific declaration, explains why it's a problem, and proposes a fix. must-not-flag: the review does not assert a known non-issue as a problem.

Every case also gets a deterministic no-rewrite check: the reviewer was asked for a review, so the fixture files must be byte-identical afterwards. A rewritten fixture fails the run — the check gates the exit status, it is not only reported in the summary.

The cases

Case Planted flaws Non-flaws (must not flag)
01-definitions-review global Fact instance; native_decide in library code; Prop/Bool dual spelling of IsU32; copy-pasted doc comment scoped set_option ... in; junk-value toU32
02-missing-api downstream unfold GOLDILOCKS_PRIME while u32_lt_prime sits unused above @[simp] ... := rfl projection lemmas (they ARE the API)
03-normal-form statements in > form; gt_iff_lt tax visible in a proof unsqueezed terminal simp
04-structural unscoped set_options; dishonest show; unfocused goals; squeezed terminal simp and bare non-terminal simp (direction test)
05-clean-restraint none — the file is deliberately good (an engagement criterion requires the review to name specific declarations, so an empty "looks fine" cannot score by omission) terminal simp, redundant show lines, := rfl def lemma; no invented rules

Cases 03/05 and the non-flaw columns carry the uplift signal: the skill contradicts popular folk advice there (squeeze everything, delete redundant shows, split proofs over N lines), so a baseline run tends to fail them while a skill run should not.

Running

Requires the claude CLI and python3. Fixtures are not compiled — no Lean toolchain is needed; the flaws are stylistic/structural and reviewable from source.

./run.sh                        # all cases, baseline + skill arms
./run.sh --arm skill            # skill arm only
./run.sh --arm baseline 03-normal-form 05-clean-restraint
EVAL_MODEL=<model-id-or-alias> ./run.sh # pin a model accepted by your CLI
  • The skill arm copies skills/writing-lean-proofs into the work dir's .claude/skills/, so the reviewer discovers it the way a plugin user would. The baseline arm runs bare. Comparing arms measures uplift.
  • Both arms run with --setting-sources project. Claude Code's skill-location documentation puts personal skills in the user source and project skills in the project source, so the flag excludes personal skills while retaining the copied skill in the skill arm. Before any model call, the runner also rejects a personal or enabled-plugin copy of writing-lean-proofs; this converts a future isolation regression into a preflight failure. A baseline transcript that names the skill is a final runtime backstop.
  • The skill arm has the mirror of that backstop: one canary call per run asks the CLI to list the skills it can see under --setting-sources project and fails the run if the copied skill is not among them. Copying the skill in does not prove the CLI discovered it; without this, a change to project- skill discovery would silently make the skill arm run bare, and the suite would report the two arms as tied — indistinguishable from a true "no uplift" result.
  • Each reviewer runs headless (claude -p) in a throwaway copy of the fixture with --permission-mode acceptEdits: edits are possible, so the no-rewrite check is meaningful.
  • Grading is an LLM judge (claude -p) in an empty working directory with project-only settings, applying rubric.md to the review transcript via grade-prompt.md. The runner requires exactly one JSON array whose ids are unique and exactly match the rubric in order, and rejects missing evidence, extra keys, an empty transcript, a zero-criterion rubric, or a fixture with zero Lean files.
  • Results: results/<timestamp>/<arm>/<case>/{transcript.md,grades.json,no-rewrite.txt,...} plus a printed summary. Results are gitignored.

Grader self-test

./run.sh --self-test

Two-sided, both against case 01's rubric:

  • Before calling the judge, deterministic malformed-output fixtures prove that the validator rejects duplicate, reordered, and unknown criterion ids, and an enabled-plugin fixture proves that the isolation preflight detects a colliding copy of the skill. This half makes no model call and runs before the CLI preflight, so it works on a machine with no claude on PATH and no authentication — which is where you would want to run it, e.g. from a CI shell-test suite.

  • selftest/bad-review.md — misses every planted flaw and commits every forbidden move (turns one tactic per line into an exceptionless rule, invents a 20-line threshold, flags the scoped set_option, demands squeezing terminal simp, and wants Option instead of junk values) — must fail every criterion. This catches a grader that has become too permissive.

  • selftest/good-review.md — flags all four planted flaws with location, rationale, and fix, and commits none of the forbidden moves — must pass every criterion. This catches a grader that has become too strict (which would otherwise read as "the skill provides no uplift").

Run this after editing rubric 01, grade-prompt.md, or when changing EVAL_MODEL. Model aliases change over time, so prefer the CLI's stable family alias or a full model id reported by the installed claude version instead of copying an id from this README. Rubrics 0205 have no canned coverage; after editing those, spot-check a live run's grades.json evidence fields instead.

Interpreting results

  • A must-flag fail on the skill arm means the skill didn't surface its own rule — look at whether the SKILL.md wording is reachable from a plain "review this file" prompt.
  • A must-not-flag fail on the skill arm is worse: the skill's anti-folk-advice guidance lost to the model's prior. Those rules may need to be more prominent (they live in the "Rationalizations to reject" and anti-patterns tables).
  • Baseline-arm failures are expected and are the point: cases the baseline already aces provide no signal about the skill.

Provenance and caveats

  • Fixtures derive from an internal Trail of Bits Lean project with identifiers and structure simplified; used with permission.
  • Fixtures were written to be plausible Lean 4 + Mathlib but are not compiled in CI. If a fixture contains an accidental error a reviewer fixates on, treat that as fixture debt and fix the fixture, not the rubric.
  • The LLM judge makes borderline calls on hedged reviews ("you might consider squeezing..."). grade-prompt.md instructs it to fail only actual recommendations; spot-check grades.json evidence fields when a number looks surprising.