ReproMin reduces a failing repository while continuously checking that the original failure still occurs. Its output is intended to be a small, standalone reproduction that can be attached to an issue or turned into a regression test.
Status: pre-alpha feasibility build. The default host backend executes the supplied shell command directly and is not a sandbox. The optional Docker backend reduces access but is not a complete security boundary.
Source reducers such as C-Reduce and Perses minimize programs or individual inputs. Environment tools such as ReproZip and containers capture what is needed to rerun a command. ReproMin targets the layer between them: project files, modules, build manifests, dependencies, and source symbols.
- CI and application teams get a small failure artifact instead of a full checkout when a regression needs to be reported or reviewed.
- Library and build-tool maintainers can isolate the exact dependency, manifest entry, source symbol, or module that keeps a failure reproducible.
- Test and benchmark authors can turn a reduced tree into a deterministic
regression fixture with an auditable
report.json. - AI-assisted debugging workflows can use the optional semantic reducer to propose edits while the ordinary oracle remains the acceptance gate.
The result is evidence for one configured reproduction in one recorded environment. It is not a proof of code correctness, production reliability, or a security sandbox. For automatic CI artifacts, see the GitHub Action guide.
ReproMin requires Python 3.9 or newer and has no runtime dependencies.
For a copy-paste, network-free workflow, start with the examples guide. It creates a tiny failing project, reduces it, and explains where the payload and evidence report are written.
Before a long reduction, run the read-only doctor preflight to detect supported reducers and verify that an optional failure command passes its baseline checks in fresh copies:
repomin doctor . \
--command 'python -m pytest -q' \
--match 'FAILED tests/test_regression.py' \
--output /tmp/project-reproAfter a reduction, use the replay command to check the recorded failure contract against fresh copies of the exported payload:
repomin report replay /tmp/project-repro.repomin/report.json \
--payload /tmp/project-repro \
--yes中文用户可以先阅读中文快速开始,其中包含一个 可直接运行的最小缩减示例、安全边界和报告说明。
The current pre-alpha release is distributed from GitHub Releases; it is not
published to PyPI yet. Use an isolated virtual environment so the repomin
command does not modify your system Python:
python3 -m venv .venv
. .venv/bin/activate # macOS/Linux
python -m pip install --upgrade pip
REPOMIN_VERSION=0.1.0.dev4
python -m pip install \
"https://github.com/fly1d/repomin/releases/download/v${REPOMIN_VERSION}/repomin-${REPOMIN_VERSION}-py3-none-any.whl"
python -m repomin --versionWindows PowerShell users can create and activate the same kind of environment
with py -3 -m venv .venv and .venv\Scripts\Activate.ps1, then install the
wheel with PowerShell's environment-variable syntax:
py -3 -m venv .venv
.venv\Scripts\Activate.ps1
$env:REPOMIN_VERSION = "0.1.0.dev4"
python -m pip install "https://github.com/fly1d/repomin/releases/download/v${env:REPOMIN_VERSION}/repomin-${env:REPOMIN_VERSION}-py3-none-any.whl"
python -m repomin --versionIf PowerShell blocks Activate.ps1 because of its execution policy, leave the
environment unactivated and replace python above with
.venv\Scripts\python.exe.
The release page includes SHA-256 checksums for the wheel and source archive; verify the downloaded asset there when supply-chain verification is required. The wheel is preferred for a quick install because it needs no build step.
To install the source archive instead, keep the same REPOMIN_VERSION value:
REPOMIN_VERSION=0.1.0.dev4
python -m pip install \
"https://github.com/fly1d/repomin/releases/download/v${REPOMIN_VERSION}/repomin-${REPOMIN_VERSION}.tar.gz"Pip will create an isolated build environment for the declared build tools.
If your shell cannot find repomin, activate .venv again or invoke
python -m repomin with the same interpreter used for installation. Confirm
the selected environment with python -m pip show repomin; remove it with
python -m pip uninstall repomin when needed.
For development, clone the repository and install the optional tooling extras in editable mode:
git clone https://github.com/fly1d/repomin.git
cd repomin
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[dev]"
python -m repomin --versionThe plain python -m pip install -e . form is also sufficient when you only
need the package and do not plan to run the lint, coverage, build, or release
checks. A version-matched source archive is available on the same release
page for users who
need to inspect or build from source.
ReproMin can print completion definitions for Bash, Zsh, Fish, and PowerShell. Evaluate the script for the current shell, or install it using that shell's normal completion directory:
# Bash
eval "$(repomin completion bash)"
# Zsh
eval "$(repomin completion zsh)"
# Fish
repomin completion fish | source
# PowerShell
Invoke-Expression (repomin completion powershell | Out-String)The completion includes the supported adapter, source reducer, semantic backend, Docker policy, and other enum values. Path-bearing options fall back to the shell's file completion.
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e .
repomin /path/to/project \
--command './mvnw test -Dtest=PaymentServiceTest' \
--match 'NoSuchMethodError' \
--java-exception \
--jobs 4 \
--output /path/to/payment-reproThe source repository is copied before the baseline command runs. Accepted mutations are made only in temporary copies, and an existing output directory is never overwritten. Export freezes the accepted tree's complete fingerprint, copies into a unique staging path under the output's parent, verifies the staged fingerprint, and then publishes with the platform's atomic no-replace rename primitive. Copy, verification, and publication failures remove staging without exposing a partial output. A platform without a provable no-replace directory rename fails the export instead of using a racy fallback.
The output directory contains only the reduced repository payload. ReproMin
writes report.json and REPOMIN.md to the sibling metadata directory
OUTPUT.repomin, so its own evidence cannot change the tree that passed the
oracle. REPOMIN.md also records the execution backend and, for Docker runs,
the image reference, immutable image ID, and network policy. Environment
variable values are never written; only their names are listed when configured.
Neither an existing output nor an existing metadata directory is overwritten.
Use repeatable --ignore NAME options to exclude an exact file or directory
basename before the first baseline run. The name applies recursively, in
addition to ReproMin's built-in generated/dependency directories:
repomin /path/to/project \
--command './run-repro.sh' \
--match 'ORIGINAL_FAILURE' \
--ignore '.env' \
--ignore 'fixtures-large' \
--output /tmp/reproIgnored entries are never copied into candidate workspaces or exports. The
effective sorted basename set is recorded in report.json and persistent
session identity, so changing it on --resume is rejected. --ignore accepts
one ordinary basename at a time; it is not a shell glob or a .gitignore
parser.
For a monorepo where only one same-named subtree should be excluded, use an exact repository-relative path:
repomin /path/to/project \
--command './run-repro.sh' \
--match 'ORIGINAL_FAILURE' \
--ignore-path 'services/api/private' \
--output /tmp/repro--ignore-path removes that path and all descendants wherever they occur in
the reduction workspace. It rejects absolute paths, .. segments, and glob
syntax; it is intentionally not a .gitignore parser.
To reuse an existing rule file without converting every line to an exact flag,
apply the repository .gitignore explicitly:
repomin /path/to/project \
--command './run-repro.sh' \
--match 'ORIGINAL_FAILURE' \
--gitignore \
--output /tmp/repro--gitignore adds the repository root .gitignore. Add other rule files with
repeatable --gitignore-file PATH; relative paths are resolved against the
repository. These rules are evaluated after the built-in and exact exclusions,
so a negated ! rule can restore a path excluded by an earlier rule file entry
but can never restore a built-in or exact exclusion. The supported subset
handles comments, blank lines, negation, trailing-slash directory rules,
leading-slash anchoring, *, **, ?, and [...] character classes. Escaping
and per-directory precedence are not a full git implementation; ambiguous files
should use exact --ignore-path entries instead. The rule-file paths and a
SHA-256 digest of their contents are recorded in the report and persistent
session identity, so a changed rule file is rejected on --resume.
For repositories with per-directory rule files, use --gitignore-recursive.
It applies the root .gitignore plus every nested .gitignore, each relative
to its own directory, in top-down order:
repomin /path/to/project \
--command './run-repro.sh' \
--match 'ORIGINAL_FAILURE' \
--gitignore-recursive \
--output /tmp/reproDirectories excluded by the built-in, exact ignore, or already-applied
gitignore rules are not descended into, so their nested rule files are not
collected. The recorded gitignore file list and digest include every rule file
actually used, and gitignore_recursive is part of the report and session
identity.
To keep a file or directory that the oracle does not otherwise require, use
repeatable --keep RELATIVE_PATH. The file reducer will not delete the exact
path or any descendant of a kept directory:
repomin /path/to/project \
--command './run-repro.sh' \
--match 'ORIGINAL_FAILURE' \
--keep 'LICENSE' \
--keep 'fixtures/golden' \
--output /tmp/repro--keep uses the same exact relative-path grammar as --ignore-path and does
not accept glob syntax. It protects only file/directory deletion; manifest,
source, and other reducers may still edit files inside a kept directory. The
sorted keep paths are recorded in the report and session identity, so a changed
set is rejected on --resume.
To shrink the contents of a specific UTF-8 text file rather than deleting the
whole file, pass repeatable --text-file RELATIVE_PATH. The reducer applies
hierarchical line-range deletion and still requires every accepted edit to
preserve the configured failure:
repomin /path/to/project \
--command './run-failing-command' \
--match 'ORIGINAL_FAILURE' \
--text-file 'fixtures/input.txt' \
--text-file 'config/plain.ini' \
--output /tmp/repro--text-file uses the same exact relative-path grammar as --keep. It edits
only the explicitly listed files; binary files and files not decodable as UTF-8
are skipped. The selected paths are part of the session identity, so a changed
set is rejected on --resume.
Use repeatable --env NAME=VALUE options when the reproduction needs explicit
environment flags. The same values are passed to host and Docker runners, while
reports and checkpoints retain only sorted variable names and a SHA-256 digest
of the name/value mapping:
repomin /path/to/project \
--command './run-repro.sh' \
--match 'ORIGINAL_FAILURE' \
--env CI=1 \
--env FEATURE_GATE=disabled \
--output /tmp/reproValues are intentionally omitted from report.json, REPOMIN.md, and
state.json; review the command and the reproduction environment separately
before sharing a metadata sidecar. REPOMIN is reserved for ReproMin's
internal marker and cannot be overridden.
For a native process crash that may not print stable text, learn its termination instead of supplying an output regular expression:
repomin /path/to/project \
--command 'exec ./reproduce-crash' \
--process-failure \
--output /path/to/crash-repro--process-failure learns the baseline's exact process-level signature and
requires every accepted candidate to retain it. A directly observed POSIX
termination is stored as its signal number and name, and a Windows status is
normalized to its unsigned 32-bit value; common exception statuses are named in
the report. Every other non-zero result remains an exact exit-code signature.
The mode does not need --match; when --match is also supplied, both the
termination and output must match. It cannot be combined with --exit-code,
which is the manual alternative when the expected code is already known.
--exit-code can also be the sole failure criterion, without a --match
regular expression:
repomin /path/to/project \
--command './run-failing-command' \
--exit-code 7 \
--output /path/to/exit-code-reproIn this mode only the exact exit status is preserved; captured stdout/stderr may change freely.
Use exec in a host shell command when the direct POSIX signal matters. Shells
and container runtimes often expose a child signal as the ordinary exit code
128 + signal; ReproMin preserves that exact code but deliberately does not
guess that it was a signal. Timeouts and resource exhaustion never become
process-failure signatures.
--jobs N evaluates up to N candidates concurrently and consumes results in
candidate order, independent of completion order. It normally commits the
lowest-index successful candidate. A reducer may instead propose the compatible
union of multiple successful candidates, but that union is committed only after
its own oracle run passes. The default is 1 because separate working
directories do not isolate ports, databases, services, or other external state
used by the command.
Use --max-attempts N to bound a long reduction. ReproMin stops preparing new
candidate attempts after N logical attempts, exports the latest accepted
tree, and sets budget_exhausted in the report:
repomin /path/to/project \
--command './mvnw test -Dtest=PaymentServiceTest' \
--match 'NoSuchMethodError' \
--max-attempts 500 \
--output /tmp/payment-repro-boundedThe budget applies only to reduction candidates, not baseline, final
validation, or optional holdout samples. A bounded run is still a valid,
reproducible tree; it is simply not necessarily a global fixed point. The
budget is part of the session identity, so changing it on --resume is
rejected.
Use --max-duration SECONDS to bound the reduction by wall-clock time instead.
The same final-validity and identity rules apply:
repomin /path/to/project \
--command './mvnw test -Dtest=PaymentServiceTest' \
--match 'NoSuchMethodError' \
--max-duration 120 \
--output /tmp/payment-repro-timedOn timeout, resource exhaustion, Ctrl-C, or an exception in a parallel window, ReproMin cancels every active candidate command before discarding its trial directories. Host commands run in a managed process tree: POSIX uses a gated process group and Windows uses a suspended process assigned to a Job Object before it starts. A command that returns while leaving ordinary background children is cleaned up before its result is consumed. Combined stdout and stderr capture is limited to 64 MiB; exceeding the limit is a resource failure and can never satisfy the oracle.
Candidate results are cached by repository content for the duration of one
ReproMin process. Use --no-cache when the failure depends on time, network
responses, or other state not represented by files in the candidate.
An opt-in semantic reducer seam can propose source edits from a provider-agnostic OpenAI-compatible endpoint. It is disabled by default, has no third-party runtime dependency, and never binds a default provider:
export REPOMIN_SEMANTIC_ENDPOINT=http://localhost:8000/v1/chat/completions
export REPOMIN_SEMANTIC_MODEL=your-local-model
export REPOMIN_SEMANTIC_TOKEN=optional-bearer-token
repomin /path/to/project \
--command './run-failing-command' \
--match 'ORIGINAL_FAILURE' \
--semantic-reducer http \
--output /path/to/repro--semantic-endpoint, --semantic-model, and --semantic-timeout can also be
passed directly; the bearer token is read only from REPOMIN_SEMANTIC_TOKEN so
it never appears in argv or reports. The backend responds with a JSON edits
array of either {"path": "...", "replace": "..."} or
{"path": "...", "delete": true}; markdown code fences around that JSON are
tolerated. Every returned candidate still goes through the complete failure
oracle and is discarded if it stops reproducing the configured failure. Reports
record semantic_reducer,
semantic_model, semantic_endpoint, semantic_calls, and
semantic_accepted; see docs/LLM_REDUCTION.md.
Structured reducers start with deterministic multi-target batches and split a rejected batch into smaller batches. Batching changes how much is proposed, not the acceptance rule: every batch is materialized in its own repository copy and must pass the complete oracle before it is promoted. When multiple compatible Java candidates pass in one parallel window, ReproMin may also test their union; the union is promoted only after an additional oracle run.
For intermittent failures, --baseline-runs N --min-baseline-passes K allows
up to N-K non-passing baseline samples while still requiring a reproducible
failure often enough to start. --candidate-runs N --min-candidate-passes K
runs every candidate in fresh copies and accepts it only when at least K
samples satisfy the complete oracle. Timeouts and resource-exhausted runs are
never counted as passes and reject that candidate's sample set. Candidate
content caching is disabled automatically when --candidate-runs is greater
than one, because replaying one old sample would invalidate the statistical
check. Defaults remain strict: all baseline runs must pass and each candidate
is run once.
Repeated candidate sampling can stop in either direction. It rejects early when
the remaining planned samples cannot possibly satisfy the count/rate criteria,
or as soon as an observed timeout or resource exhaustion makes the sample set
invalid. Without a rate gate, a candidate accepts as soon as it has K passing
samples because later ordinary failures cannot undo that count.
With a candidate rate gate, early acceptance requires all three of these
conditions: the count minimum is met, a Jeffreys beta-binomial mixture
confidence-sequence lower bound for the observed prefix is at least R, and the
planned-N exact one-sided binomial gate would still pass if every unexecuted
sample were a failure. The mixture uses alpha = 1 - confidence, so inspecting
it after every sample remains valid under optional stopping. The worst-case
exact check ensures that early acceptance does not enlarge the fixed-N
pass/fail acceptance set. Observed timeout and resource failures are checked
before acceptance; samples skipped after a stopping decision never run, so a
resource failure they might have produced cannot be observed.
For a less brittle statistical gate, add --min-baseline-rate R and/or
--min-candidate-rate R. Full sample sets use an exact one-sided binomial test,
equivalent to requiring a Clopper-Pearson lower confidence bound of at least
R; the default confidence level is 95% and can be changed with --confidence.
Rates must satisfy 0 < R < 1; use an all-runs count threshold for strict 100%
behavior. When a rate is specified without its corresponding
--min-*-passes, the count minimum is one and the rate is the meaningful gate.
If both are specified, both constraints must pass.
When a baseline rate gate is combined with --java-exception,
--python-exception, or --process-failure, the first basic-passing sample
with an extractable signature is a discovery sample. It fixes the signature but
is not reused as rate evidence; only the remaining planned slots test that
fixed signature.
The count minimum still applies to matching samples across the complete N
runs. The CLI therefore checks rate attainability with at most N-1
post-discovery samples. Without a baseline rate gate, signature selection keeps
the existing stable-mode count semantics.
For example:
repomin /path/to/project \
--command 'pytest -q tests/test_regression.py' \
--match 'checkout failed' \
--baseline-runs 10 --min-baseline-rate 0.6 \
--candidate-runs 10 --min-candidate-rate 0.6 \
--run-confidence 0.95Baselines and final validation always run all configured samples, and a
candidate that reaches N uses the ordinary fixed-size count and exact gates.
The anytime bound is an additional early-acceptance gate, not a replacement for
that terminal rule. Without --run-confidence, its coverage is per candidate.
--run-confidence C adds run-wide control for the adaptively selected candidate
families and requires --min-candidate-rate. Candidate family j receives
alpha_j = (1-C)/(j(j+1)), capped by the ordinary 1-confidence alpha. The
terminal exact gate and anytime bound both use that candidate's allocated
confidence. Since the harmonic allocation sums to at most 1-C, a union bound
controls the probability of accepting any candidate whose true oracle pass rate
is at most R, conditional on each candidate's fresh samples satisfying the
documented iid model given prior reduction history. Cross-candidate independence
is not required.
Only candidates that actually change the tree consume a family index. Parallel
windows allocate indices in deterministic candidate order; combinations and
changed cache/duplicate candidates consume separate indices. Persistent sessions
checkpoint allocated indices before sampling, so an interrupted allocation is
never reused. As alpha shrinks, a fixed --candidate-runs eventually cannot pass
even with all samples successful; ReproMin then fails closed and asks for more
runs or a lower rate/run confidence instead of reporting a fixed point.
The report records configured rates, confidence, observed rates, and Wilson
lower bounds. For a baseline rate gate, baseline_rate_evidence_runs and
baseline_rate_evidence_passes identify the exact-test sample set, while
baseline_exact_lower_bound, baseline_exact_p_value, and
baseline_exact_rate_gate_passed expose its one-sided Clopper-Pearson result.
These fields are null when no baseline rate gate is configured. The ordinary
baseline_runs, baseline_passes, baseline_rate, and
baseline_lower_bound fields continue to describe all baseline samples, so a
signature discovery split does not silently change their meaning. A third
party can reproduce the baseline gate from the evidence counts,
min_baseline_rate, and confidence.
On resume, an older non-signature checkpoint that predates these fields is
upgraded from its full-sample baseline counts. An older signature checkpoint
without the post-discovery counts is rejected because its discovery position
cannot be reconstructed safely.
candidate_sampling_policy identifies the stopping-rule version.
When run-wide control is enabled, the report also records run_confidence,
candidate_family_control_policy, the number of allocated families, the
cumulative nominal alpha upper bound, and each accepted event's family index,
binary confidence, and actual alpha.
It also records candidate_early_acceptances,
candidate_early_rejections, and candidate_samples_saved; the saved count
includes both stopping directions. Accepted events expose
oracle_anytime_lower_bound and oracle_early_acceptance so early decisions can
be distinguished from full-size decisions.
Candidate, baseline, and ordinary final-validation samples participate in selection or consistency checking. They are not independent evidence about the one final artifact. An optional holdout runs a new, fixed-size sample only after the reducer reaches its global fixed point and the ordinary final validation passes:
repomin /path/to/project \
--command 'pytest -q tests/test_regression.py' \
--match 'checkout failed' \
--holdout-runs 29 \
--min-holdout-rate 0.9 \
--holdout-confidence 0.95 \
--session /tmp/checkout-repro-session \
--output /tmp/checkout-repro--holdout-runs and --min-holdout-rate must be supplied together; holdout
confidence is separate from --confidence and defaults to 0.95. ReproMin
rejects an unattainable plan before reduction. At 95% one-sided confidence, for
example, certifying a minimum rate of 0.90 requires at least 29 runs even when
every run passes.
The cleaned payload, oracle signature, runner configuration, sample count,
target rate, and confidence are frozen before the first holdout command. Every
sample starts from a fresh copy of that same payload. ReproMin does not use its
candidate cache, does not stop early, and does not include any earlier sample in
the holdout count. Certification uses a fixed-N, one-sided exact
Clopper-Pearson lower bound; the gate itself is an exact binomial upper-tail
comparison. A timeout or resource-exhausted sample is a failure and vetoes
certification even if the remaining pass count would clear the rate gate.
The certificate says that, assuming the recorded holdout runs are independent
and identically distributed, the oracle pass probability in that runner
environment has the reported lower confidence bound. It does not say that the
code is correct, that the match identifies the intended bug, or that the bound
is a production failure rate. Fresh repository copies do not isolate ports,
services, host caches, time, or network state. Docker with a pinned local image
and --docker-network none improves environmental control, but ReproMin cannot
verify the iid assumption.
Persistent sessions write the holdout plan before each command and record each
result before advancing. An interrupted in-flight slot is permanently counted
as a non-pass on resume instead of being selectively retried. A completed
success or failure is terminal for that session; --resume never samples it
again. A failed holdout exits 3 and does not create the output directory.
Starting new sessions until one passes is multiple testing and is outside the
single-attempt confidence guarantee.
The top-level holdout_certification block in OUTPUT.repomin/report.json
records the versioned policy,
attempt ID, payload and oracle digests, exact bound and p-value, required and
observed pass counts, veto counters, resume state, and per-sample summaries.
When no holdout is requested its status is explicitly not_requested, and the
default command count is unchanged.
Reports can be checked without rerunning the reproduction command. The command
validates the schema and phase/holdout accounting; --payload additionally
checks the exported tree fingerprint and payload size. If an artifact store
rewrites filesystem metadata such as modification times, validation can report
a content-only fingerprint match and explicitly mark metadata drift:
repomin report validate /tmp/checkout-repro.repomin/report.json \
--payload /tmp/checkout-reproUse --json for a compact result suitable for CI checks.
To execute the report's failure command, first review the unsigned report and payload, then opt in explicitly. Replay validates the payload before execution and runs every sample in a separate temporary copy:
repomin report replay /tmp/checkout-repro.repomin/report.json \
--payload /tmp/checkout-repro \
--runs 2 \
--yesReplay exit code 0 means only that every current-environment run matched the
recorded oracle. It is not holdout certification or a correctness/root-cause
proof. See docs/REPLAY.md for legacy reports, explicit
environment values, Docker behavior, privacy, and exit codes.
Long reductions can persist their accepted tree and progress in a directory outside the source repository:
repomin /path/to/project \
--command './mvnw test -Dtest=PaymentServiceTest' \
--match 'NoSuchMethodError' \
--session /tmp/payment-repro-session \
--output /tmp/payment-reproIf the process is interrupted, the latest accepted mutation is already stored
in the session. Run the same command with --resume to continue:
repomin /path/to/project \
--command './mvnw test -Dtest=PaymentServiceTest' \
--match 'NoSuchMethodError' \
--session /tmp/payment-repro-session \
--resume \
--output /tmp/payment-reproThe version-3 checkpoint contains the current tree, learned exception signature, oracle baseline, reduction statistics, events, completed phases, and any write-ahead holdout state. ReproMin checks the source tree fingerprint and all failure, runner, resource, and reducer options before resuming; changed inputs are rejected instead of silently continuing from an incompatible state. The session directory is a working state store, not part of the exported reproduction, and can be removed after the output has been verified.
Tree fingerprints use the domain-separated tree-sha256-v2 policy. Its
canonical encoding length-prefixes each path, permission mode, modification
time, entry type, regular-file or symlink payload, filesystem flags exposed by
the runtime's stat_result.st_flags, and enumerable extended attributes. It
also includes the payload root's own metadata. Access times are deliberately
excluded and normalized after every repository copy and command-tree
fingerprint so fresh commands see the same value. A representable repository
tree contains only directories, regular
files, and relative symbolic links whose resolved targets remain inside the
repository. Regular files with a link count greater than one are rejected,
including files with a hardlink alias outside the repository, because copying
cannot preserve that topology safely. FIFOs, sockets, and block or character
devices are also rejected before copying, fingerprinting file contents, or
running a command. BSD st_flags values that make an entry immutable,
append-only, or non-removable (UF_IMMUTABLE, UF_APPEND, SF_IMMUTABLE,
and SF_APPEND, plus UF_NOUNLINK or SF_NOUNLINK on systems that enforce
them) are rejected because the copied tree could not be normalized, reduced,
and removed reliably. Other exposed and copied flags remain supported and part
of the fingerprint. Linux inode flags managed by chattr are not exposed by
Python's stat_result and are outside this guarantee. On Windows, non-symlink
reparse points such as junctions are rejected before directory traversal.
Commands run only in private copies. During cleanup, ReproMin clears copied
flags only on directories and entries with one hardlink; it will report a
cleanup failure rather than alter a multiply-linked inode whose other name may
be outside the private tree.
Checkpoints from schema 1 or 2 used the ambiguous v1 encoding and are
intentionally rejected; start a new session rather than relabeling their saved
fingerprints.
If a process stops after a certified payload was exported, --resume reuses
the existing output only when its complete tree fingerprint matches the frozen
certificate. It can then recreate a wholly missing OUTPUT.repomin sidecar or
accept a complete sidecar whose contents still match the certificate. A partial
or changed payload/sidecar is rejected without running another holdout sample.
Existing output remains an error for sessions without a successful holdout,
because those checkpoints do not contain a certified export fingerprint.
The versioned reduction strategy is part of that session identity. A checkpoint created without the current strategy version, or by a different strategy, is rejected because its completed-phase claims are not interchangeable with the current fixed-point contract.
The effective ignored-basename set is also part of session identity. This keeps the source fingerprint and the copied workspace semantics aligned when a session is resumed.
The sorted exact ignored-path set is part of the same identity and is recorded
in the report. Changing either exclusion option on --resume is rejected
before another oracle command runs.
Explicit environment overrides are part of session identity through their sorted names and digest. Resuming with a changed value or name is rejected before another oracle command runs, without exposing the value in the checkpoint.
For the host backend, every baseline, candidate, final-validation, and holdout
command runs in a fresh directory whose leaf name is exactly OUTPUT.name.
The versioned host-output-basename-v1 policy and that basename are part of the
session identity and report, so changing the output basename on resume is
rejected. Unique internal parent directories still vary between samples. A
command that depends on an absolute path, parent-directory name, inode, or
device is outside this host guarantee; use the Docker backend, whose working
directory is always /workspace, when a fixed complete path is required.
--match searches the complete captured stdout and stderr. Build tools and
tracebacks may echo source lines or command arguments, so a short token can
remain present after the intended failure has changed. Prefer a pattern that
includes stable failure context and an end-of-line boundary; for exception
failures, also enable the corresponding structural signature option below.
Holdout certification measures the configured oracle, so it cannot repair an
underspecified match expression.
--java-exception learns the root Java exception from the repeated baseline
runs and requires every accepted candidate to preserve its class, normalized
message, and first three method frames. Source line numbers and Java module
prefixes are ignored. ReproMin reads both console stack traces and Maven
Surefire TEST-*.xml failures, so quiet Maven output can still be identified.
Leave this option off when exception messages intentionally contain unstable
request IDs, timestamps, or other per-run values.
--python-exception provides the equivalent guard for Python tracebacks. It
preserves the exception class, whitespace-normalized message, and three
innermost frames while ignoring absolute working directories and source line
numbers. Standard tracebacks, chained exceptions, Python 3.11 exception groups,
and pytest-rendered failures are supported. When one run reports multiple
failures, ReproMin prefers the exception whose class, message, or frames match
--match. The Java and Python signature options are mutually exclusive.
The Java source reducer can use pre-resolved dependencies during compiler
attribution. Pass one classpath entry per --java-classpath PATH occurrence:
repomin /path/to/project \
--command './mvnw test' \
--match 'NoSuchMethodError' \
--java-classpath /opt/repro-deps/api.jar \
--java-classpath /opt/repro-deps/classesEach PATH is one atomic entry. ReproMin does not split an occurrence on :
or ;, so a Unix filename containing a colon remains representable. Entries
are kept in argument order because classpath precedence is significant; two
entries that identify the same physical file or directory, including symlink
or hardlink aliases, are rejected rather than deduplicated. A relative entry is
resolved once against the original SOURCE
directory, never against the shell working directory or a temporary candidate
copy. Every entry must resolve to a readable existing regular file or
directory. A top-level symlink is fixed to its canonical target; a classpath
directory containing a nested symlink or special file is rejected. Regular
files are passed to javac without an extension or archive-format policy. Pass
dependency archives separately; a directory entry contributes compiled classes
but does not recursively add archives stored below it.
--java-classpath affects only the compiler API used by the host-side Java
structure analyzer. It does not modify --command, the oracle's runtime
classpath, or the contents and mounts of a Docker container. This remains true
with --backend docker: the entries must be readable on the host, and a path
that exists only in the image is invalid. ReproMin does not invoke Maven or
Gradle to discover dependencies. Resolve or stage the exact entries before
starting reduction.
For Maven test sources, dependencies can be staged outside SOURCE with a
pinned plugin goal and then supplied one archive at a time:
REPOMIN_CP_DIR="$(mktemp -d)"
./mvnw -q -f app/pom.xml \
org.apache.maven.plugins:maven-dependency-plugin:3.8.1:copy-dependencies \
-DincludeScope=test \
-DoutputDirectory="$REPOMIN_CP_DIR"
set --
for repomin_jar in "$REPOMIN_CP_DIR"/*.jar; do
set -- "$@" --java-classpath "$repomin_jar"
done
repomin . \
--command './mvnw test' \
--match 'NoSuchMethodError' \
"$@"For Gradle, use a trusted external task or init script to copy
testCompileClasspath (or compileClasspath for main-only source) into an
external directory, then build the same repeated argument list. ReproMin does
not parse build-tool console output as a classpath.
The ordered canonical paths and a content fingerprint of every file or
directory entry are part of persistent-session identity and are revalidated
before each Java analysis pass. A change detected before a Java analysis pass
aborts analysis; --resume rejects a changed path, order, or entry content.
Classpath entries are not specially copied into the checkpoint. Entries
outside SOURCE must remain host-readable; use a durable staging directory
instead of mktemp for a session that may be resumed. A directory fingerprint
recursively covers relative paths, entry types, permission bits, and
regular-file contents.
ReproMin can run every baseline and candidate in a fresh container. Images must already exist locally; it never pulls one automatically.
repomin /path/to/project \
--command './mvnw test -Dtest=PaymentServiceTest' \
--match 'NoSuchMethodError' \
--backend docker \
--docker-image eclipse-temurin:17-jdk \
--output /path/to/payment-reproDocker runs with networking disabled by default, a read-only container root,
all Linux capabilities dropped, no privilege escalation, a 512-process limit,
and a bounded temporary filesystem. Only the disposable candidate repository
is mounted writable. Use --docker-network bridge or host only when the
reproduction genuinely requires external services.
Each run receives a deterministic unique container name before the Docker CLI
starts. Timeout, resource, and interruption cleanup can therefore issue
docker rm -f even if Docker has not written its cidfile yet; after stopping
the client, ReproMin retries removal for a bounded settling period.
Before the first reproduction command, ReproMin resolves the supplied image
reference to Docker's immutable sha256:... image ID. Every container in that
process runs the ID rather than the possibly mutable tag. Reports record both
image and image_id; persistent sessions bind both values, so --resume
rejects a tag that now resolves to different image contents before sampling.
Resource limits can also be set per container:
repomin /path/to/project \
--command 'python -m pytest' \
--match 'RuntimeError: checkout failed' \
--backend docker \
--docker-image project-tests:local \
--docker-cpus 2 \
--docker-memory 1GiB \
--docker-workspace-limit 2GiB \
--docker-tmpfs-size 512MiBCPU and memory use Docker's native limits; swap is capped at the same value as
memory. --docker-workspace-limit monitors the total logical size of the
writable repository and destroys a container that crosses it. The monitor is
sampled, so it is a practical runaway-write guard rather than a filesystem
quota. PID and /tmp limits default to 512 and 1 GiB and can be changed with
--docker-pids-limit and --docker-tmpfs-size. Resource-exhausted runs are
never accepted by the failure oracle, even if they printed matching output.
When no persistent --session is supplied, Docker Desktop sessions are
created beside the source repository so the path is inside the same shared
host directory. That disposable session is removed when the process exits
normally. The source parent, or the explicit --session path when one is
used, must be available to the selected Docker daemon as a bind-mount source.
Docker Desktop and Colima can have different shared-path settings; in
particular, do not assume that host /tmp is shared. A rejected bind mount is
an environment configuration error and no oracle sample is accepted from it.
- Run the baseline more than once and require the configured minimum number of samples to have a non-zero exit plus the output regular expression and, optionally, a stable Java exception, Python exception, or process termination signature. Process mode can omit the output expression.
- For Maven projects, try removing modules, dependencies, plugins, and properties using an XML parser.
- For Gradle Groovy and Kotlin DSL builds, lex balanced syntax and try removing included modules, dependencies, plugins, repositories, configurations, and properties.
- For Python projects, structurally reduce PEP 621, Poetry, PDM, dependency
group, and uv declarations in
pyproject.toml, plus logical lines and local include chains inrequirements*.txt. - For Node.js projects, structurally reduce dependency, script, workspace,
bundle/file,
resolutions, andoverridesentries in each validpackage.json. Lockfiles and runtime/exports metadata remain untouched. - For PHP Composer projects, structurally reduce package requirement,
replacement/conflict/provide, script, and repository entries in each valid
composer.json. Autoload, arbitrary extra metadata, andcomposer.lockremain untouched. - For .NET projects, structurally reduce selected MSBuild item entries in
.csproj,.fsproj, and.vbprojfiles plus sharedDirectory.Build.propsfiles, including package, project, and framework references plus content items. Property groups, imports, and arbitrary build metadata remain untouched. - For Ruby/Bundler projects, structurally reduce complete single-line
gemdeclarations inGemfile,gems.rb, andGemfile.*manifests. Multiline Ruby calls, arbitrary Ruby code, andGemfile.lockremain untouched. - For Rust projects, structurally reduce dependency tables and workspace
members/excludearrays in eachCargo.toml.Cargo.lock, features, and arbitrary metadata remain untouched. - For Go projects, structurally reduce
require,replace,exclude, andretractentries in eachgo.mod, plususeand workspace-levelreplaceentries ingo.work.go.sum, module declarations, and Go version/toolchain directives remain untouched. - Apply hierarchical delta debugging to directories and files.
- When JDK 11 or newer is available, compile the analysis helper with
javac --release 11and use the JDK syntax tree API to remove Java imports, type members, statements, annotations, parameters, and call arguments, then simplify binary, conditional, cast, unary, and literal expressions. When attribution is safe, compiler symbols let ReproMin remove an unused constructor parameter or an unused parameter on an eligible non-native method together with every resolved source call argument as one atomic candidate. Eligible methods includestaticandprivatemethods plus closed-dispatch instance methods declared in an ordinary top-level or member class when either the method or its class isfinal. - For Python projects, use the standard-library AST to remove imports, definitions, and statements with UTF-8 range validation.
- Requeue components dirtied by other accepted changes and run the ordinary final consistency validation.
- If configured, freeze the payload and run the one-time fixed-size holdout, then export the exact certified tree and emit the sibling metadata directory only after certification succeeds.
An opt-in semantic reducer seam (--semantic-reducer http) lets a
provider-agnostic backend propose source edits through the same oracle
pipeline. It is disabled by default, has no third-party runtime dependency,
and never binds a default provider. See docs/LLM_REDUCTION.md.
Every baseline, candidate, and final command runs in a disposable copy. Files created or changed by the command are never promoted into the reduced output.
Each reducer runs to a local fixed point. Maven, Gradle, Python manifest, Node manifest, Cargo manifest, Go manifest, and Python source reducers repeatedly discover targets after accepted hierarchical batches. The file reducer alternates directory and file minimization until neither can unlock the other. Java uses epochs: a stable target rejected in one epoch is deferred while other Java edits are accepted, then reconsidered in the next epoch. It stops only after a complete epoch accepts nothing.
The global scheduler is a dirty worklist. When one locally stable reducer accepts a mutation, every other reducer is queued again because that change may unlock its previously rejected targets. Termination means every enabled reducer has reached local stability after the last change made by another reducer.
The JSON report identifies this algorithm as
hierarchical-fixed-point-v2 and contains additive phase_statistics. Per
phase, attempts counts logical candidates, including no-ops and combination
candidates. oracle_sample_uses counts logical sample uses, while
oracle_samples counts samples that actually executed the command;
cache_hits is the difference. Therefore:
attempts = no_op + rejected + accepted + superseded + aborted
oracle_sample_uses = oracle_samples + cache_hits
superseded means a candidate passed the oracle but was not promoted because a
lower-index passing candidate or a separately validated combination was chosen.
samples_saved counts planned repeated samples skipped by early stopping.
oracle_seconds sums command durations and can exceed phase wall time under
parallel execution. Byte counts are net regular-file changes per reducer pass.
phase_statistics.coverage is complete only when the checkpoint contains the
whole phase-statistics history and no phase was interrupted. Compatible older
checkpoints without these counters, and sessions resumed from an active phase,
report partial; ReproMin does not invent missing historical measurements. A
hard-interrupted active pass is classified as aborted when it is restored.
- Host execution remains the default for compatibility.
- Additional input exclusions are exact recursively applied basenames supplied
with repeatable
--ignore NAME, exact repository-relative subtrees supplied with repeatable--ignore-path RELATIVE_PATH, or explicit gitignore-style rule files supplied with--gitignore,--gitignore-file PATH, or--gitignore-recursive. The rule file reader implements the documented subset above, not git's full escaping semantics. Repeatable--keep RELATIVE_PATHprotects an exact path from file deletion without disabling content-level reducers. - Explicit reproduction environment overrides use repeatable
--env NAME=VALUE; values are injected into host/Docker commands but are represented only by names and a SHA-256 digest in reports and checkpoints. - Docker images must contain
/bin/shand all required build dependencies. - Structured manifest adapters currently support Maven, Gradle, Python and
Pipenv,
npm-compatible
package.jsonfiles, Composercomposer.jsonfiles, MSBuild project files and sharedDirectory.Build.propsfiles, Ruby/Bundler Gemfiles, Cargo manifests, and Go module/workspace manifests. Node lockfiles,composer.lock,Gemfile.lock,Cargo.lock,Pipfile.lock, andgo.sumare intentionally not rewritten by their adapters. Composer autoload and arbitraryextrametadata remain unchanged. MSBuild property groups, imports, and arbitrary metadata remain unchanged. Ruby multiline calls and arbitrary Ruby code remain unchanged. Go workspace manifests are limited touseand workspace-levelreplaceentries; othergo.workmetadata remains unchanged. - Failure matching is based on exit status and, unless
--exit-codeor--process-failureis enabled, a required regular expression. - Learned failure signatures support Java exceptions, Python tracebacks, direct POSIX signals, Windows status codes, and exact non-zero exit codes.
- Sampling uses maximum run counts and fixed-size exact one-sided rate gates;
Wilson lower bounds remain descriptive report metrics. Candidate prefixes can
also use a Jeffreys beta-binomial mixture
confidence sequence for conservative, anytime-valid early acceptance without
enlarging the fixed-size pass/fail acceptance set. Coverage is per candidate
by default;
--run-confidenceadds harmonic alpha spending across the candidate families in one reduction session. This guarantee is conditional on the documented fresh-sample model and is not a correctness guarantee. - Native source reducers support Java declarations and expressions (requires
JDK 11+) and Python statements (standard library AST). The Java analysis
helper itself is compiled with
javac --release 11; that setting does not select the source release of the project being reduced. - Coordinated Java parameter removal supports constructors, non-native
staticandprivatemethods, and closed-dispatch instance methods. An instance method is eligible only when it is declared by an ordinary top-level or member class and either the method or that class isfinal; a source-local override family may instead be coordinated when it has one package-visible root and afinalleaf method or owner. Current overrides and interface implementations against external declarations, open virtual methods, methods whose reduced signature would newly override or implement an inherited contract, and generic-erasure or bridge clashes are excluded. Enum, record, local, and anonymous-class instance methods are also excluded. The parameter must remain unreferenced in the executable body, the executable must not be used by a direct method or constructor reference, and the group must contain at least one source declaration and at least one resolved direct call-site argument edit. Closed source override families include every declaration in the atomic change set. Anonymous-class construction blocks all constructor groups for its source base type, and record constructors are not supported. - Java compiler attribution uses the remaining source files and any explicit
--java-classpathentries; build classpaths are never discovered automatically. External binary types can improve resolution, but an external executable never forms a source mutation group. If attribution throws, produces an unrecoverable compiler diagnostic, or finds anERRORtype in a source type hierarchy or anywhere on a method-invocation, constructor-expression, or member-reference path, coordinated candidates are disabled globally for that analysis pass; syntax-only Java candidates remain available. Otherwise unresolved links are never guessed. Reflection, string- orMethodType-basedMethodHandles.Lookupcalls, generated callers, JNI and framework entry points, precompiled external callers that retain the old descriptor, and other unobserved call sites are left to the reproduction oracle rather than rewritten speculatively. The oracle preserves only the configured command, exit behavior, output match, and optional failure signature; it does not prove source or ABI compatibility, successful compilation, or behavior outside the exercised path. Use a command that compiles and tests the affected code, and enable--java-exceptionwhen the exception identity is part of the reproduction. - Parallel jobs isolate repository files, not external process side effects.
- On POSIX, a command can deliberately call
setsid()or otherwise leave its assigned process group. Output capture remains bounded and cannot hang the reducer, but that escaped process may survive; use Docker or a disposable VM for commands that daemonize or are not trusted. - Persistent sessions do not checkpoint external services, databases, network responses, or processes started by the reproduction command.
- Holdout confidence is a single-attempt guarantee conditional on fresh iid samples. Repeating failed attempts in new sessions, retaining only successful CI jobs, or changing the artifact after seeing holdout results invalidates that interpretation.
These limits keep mutations auditable while the benchmark and failure-oracle coverage expands.
python3 scripts/check_contribution.py
python3 scripts/check_contribution.py --with-benchmarksSee CONTRIBUTING.md for extension points and project rules, docs/ARCHITECTURE.md for reducer invariants, and SECURITY.md before running commands from an untrusted project.
- docs/ARCHITECTURE.md - core invariants, reducer behavior, and report/checkpoint schema.
- docs/REPORT_SCHEMA.md - versioned
report.jsonfields, accounting identities, holdout evidence, and consumer guidance. - docs/REPLAY.md - fresh-copy replay, environment checks, security boundaries, and machine-readable evidence.
- docs/LLM_REDUCTION.md - optional semantic reducer seam and its provider-agnostic contract.
- docs/DOCTOR.md - read-only toolchain and baseline preflight.
- docs/GITHUB_ACTION.md - use ReproMin in CI to upload a minimized failure reproduction and its report.
- docs/ROADMAP.md - current priorities, future directions, and explicit non-goals.
- benchmarks/README.md - real fixtures and acceptance gates.
- CONTRIBUTING.md - project rules and extension points.
- CONTRIBUTORS.md - community contributions and the complete GitHub contributors graph.
- SUPPORT.md - where to ask questions and how to prepare useful issue reports.
- docs/REAL_FAILURE_PILOT.md - how to share a sanitized real CI or dependency failure.
- CHANGELOG.md - notable changes by release.
- docs/RELEASING.md - GitHub Release checklist and artifact verification.
ReproMin accepts bug reports, feature requests, reducer adapters, and documentation improvements. Start with CONTRIBUTING.md and the repository issue templates. Participation is governed by the Code of Conduct. Security reports should follow SECURITY.md.
For general usage questions and design conversations, use GitHub Discussions. New contributors can pick a scoped task from Good first issues, and support details are collected in SUPPORT.md. Users with a real CI or dependency failure can follow the pilot guide before sharing a sanitized workflow in issue #11.
Apache-2.0.