Repository navigation
feat(export): derive a claim-drawn mutant battery from the recorded run - #16
Merged
utsab345 merged 12 commits intoOct 17, 2026
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the first half of #14: the claim-drawn mutant is now derived rather than hand-built, and
stepfork scorereports whether an exported expectation still rejects it.The derivation is mechanical, and it uses two values the export already has: the expectation the generated test asserts, and the recorded run's output (
RunEnd.outputin the bundle). The claim-drawn row is that second value verbatim — the run's own output is the most plausible output of a variant implementation, because an implementation did produce it. A claim-pinned expectation rejects it (the run it came from is the bug, so the expectation must fail against it); a run-pinned expectation accepts it, and that acceptance is the finding: the generated test would pass against the code that produced the recording. The rest of the battery is controls — perturbations of the expectation's own leaves (an integer moved by one, a string suffixed, a value replaced bynull) — which a run-pinned expectation rejects too, and which therefore make the contrast legible without carrying the verdict.Two guards are deliberate, and both are tested:
run_pinned, the one reading this must not produce;stepfork scorealso reports pin coverage: which leaves carry a value the run did not produce, and which merely reproduce the run. A leaf that reproduces the run is not a defect — a fix does not change every field — but it is not evidence of anchoring either.Changes
src/stepfork/export/mutants.py(new) —derive_battery,pin_coverage,recorded_output; the reasoning is in the module docstring.src/stepfork/export/testfile.py(new) — reads the generated test's literals (TRACE_PATH,EXPECTATION,HAS_EXPECTATION,TRAJECTORY_EXPECTATION) withast, plus a whitelist evaluator for the two path-expression shapes the generator writes. The file is parsed, never imported or executed; an expression outside those shapes is refused with a message that points at--bundlerather than guessed at.src/stepfork/cli/score.py(new) —stepfork score TEST [--expect-output PATH] [--bundle PATH] [--json]. Exits0claim-pinned,1run-pinned,2cannot answer (no expectation, no recorded run, unreadable file).src/stepfork/cli/main.py— registers the command.src/stepfork/export/__init__.py— re-exports the derivation.src/stepfork/export/scoring.py—ExpectationScore.summary()now leads with its verdict, so a caller can print one labelled line instead of prefixing a label to a sentence that already contains it. The existing assertions (run-pinned,cannot separate,no expectation to score) still hold.src/stepfork/cli/export.py—_recorded_outputmoves tomutants.recorded_output: "the recorded run" had one definition and now has two callers, so it gets one definition (net −8 lines).docs/cli.md+docs/pytest-export.md.One departure from my own #14 wording, flagged for you. That comment said "ship it with the exported test". I did not embed the battery in the generated file, because the documented workflow is to edit the expectation by hand: copy the recorded result into a separate
expected.json, edit the fields that define the fix, review it. An embedded battery is a copy of a value the user is told to edit, so it goes stale silently; derived at score time it always matches the expectation and the bundle in front of it. This also leaves the generated template unchanged, so no existing exported test needs regenerating and no new trace format is involved. If you would rather have it embedded, say so and I will move it.Testing
Built a venv with
uv venv+uv pip install typer pytest pydantic rich hypothesis(the dependency groups do not resolve here:uv synctimes out and the dev group pulls in langgraph), then:PYTHONPATH=src python -m pytest tests/ --ignore=tests/unit/test_langgraph_integration.py→ 549 passed, 4 failed. All four failures are the same missing optional extra —ModuleNotFoundError: No module named 'langchain_core'— intest_docs_langgraph_example,test_langgraph_agent(×2) andtest_model_decision_demo, and none of them touch this change. The 39 tests added here are inside that 549.ruff check src tests→ clean ·ruff format --check src tests→ 117 files already formatted (I ranruff formaton the files I touched this time, so there should be no formatting follow-up).mypy→ 14 errors in 2 files, all pre-existing in this environment:src/stepfork/integrations/langgraph.pyandtests/unit/test_langgraph_integration.pycannot resolvelangchain_core/langgraph. I ran the same command on a pristine checkout (git stash) and it reports the same 14, so this change adds none.--expect-output examples/booking_agent/expected.json, then scored it →claim-pinned: rejected 7/7 mutants,pins: 1 live (selected_flight); 2 leaf(s) reproduce the run (destination, plan). The live pin is the field the generated test's own message names (behavior mismatch at 'selected_flight').Checklist
Still open from #14, untouched here: the tri-state verdict (
COULD_NOT_EXECUTE) and the runner-owned receipt (exit status + bundle digest + test hash), plus thestepfork score/stepfork run --receiptCLI half you listed. This PR only covers the derived battery and the scoring command. Happy to send the verdict/receipt piece as the next PR whenever you want it.