mod miner

module miner

Ring-corpus miner helpers for the reproduction grind.

A mechanical miner that turns a merged upstream PR into a bump reproduction fixture pair has to reject two real shapes found while hand-mining ~10 merged easybuilders/easybuild-easyconfigs bumps: batch “toolchain refresh” PRs that mix a toolchain-generation definition with application recipes in one diff, and non-linear version history where the PR’s target version is not actually the newest at that toolchain generation (a backfill, not a clean next-version bump). Both checks operate on data the miner already has once it has parsed a candidate file and listed the merge-base tree, so neither needs new I/O.

Functions

fn compare_reproduction(emitted: &str, target: &str) -> ReproComparison

Score emitted against target and keep the raw diff for display.

fn is_backfill(candidate_version: &str, existing_versions: &[String]) -> bool

True when candidate_version is not the highest version already present in existing_versions for the same package and toolchain generation, at the PR’s merge-base commit.

A miner mining “PR bumps package X to version Y” candidates has to distinguish a clean next-version bump (Y is newer than anything already in the tree at that generation) from a backfill (an older release added alongside a newer one that already merged separately). Scoring a backfill as a bump-reproduction target compares the wrong two files. Version ordering uses cmp_version, not a string sort, so "4.2.1" correctly outranks "3.31.11" (a plain string compare would rank them the other way around, since ‘4’ > ‘3’ only holds for the leading character and a string sort does not parse dotted version segments numerically).

fn is_toolchain_meta_recipe(recipe: &ResolvedEasyconfig) -> bool

True when recipe defines a toolchain generation itself (for example gompi-2026.1.eb, easyblock = 'Toolchain', moduleclass = 'toolchain') rather than an application built with one.

Toolchain-definition recipes are not bump targets: bumping an application recipe assumes a toolchain generation to move it to already exists, and a toolchain-definition recipe is that generation, not a consumer of it. A miner that does not filter these out will try to “reproduce” a recipe that never had a previous-generation counterpart to bump from in the first place.

Checks easyblock first since it is the more direct signal (the literal easyblock class EasyBuild uses to build a toolchain definition); moduleclass is a secondary check for recipes where the parser did not resolve an easyblock value.

fn normalize_for_scoring(text: &str) -> String

Strip from text everything that cannot change what EasyBuild builds: comments, trailing whitespace, and runs of blank lines.

Two easyconfigs with the same normalization build the same thing, so this is the function that draws the ReproScore::Semantic boundary, and the reason scoring runs on it rather than on raw text: a raw diff reports a deleted commented-out line and a lost blank line as loudly as a dropped patch, which flattens the very distinction the ladder exists to make.

What it removes:

  • comment-only lines, dropped whole rather than left as blanks, so a deleted commented-out block does not resurface as whitespace noise;

  • the # ... tail of a code line, with the code before it kept;

  • trailing whitespace on any line not ending inside a string literal;

  • runs of blank lines, collapsed to one, and blank lines at either end.

What it keeps: everything inside a string literal, verbatim. A triple-quoted description carries its own blank lines, indentation and # characters into the built module, so normalizing them away would call two different module descriptions equivalent.

fn score_reproduction(emitted: &str, target: &str) -> ReproScore

Score emitted against the real merged target.

EXACT is raw equality, so it still means what a reader assumes it means; normalization only decides SEMANTIC against MATERIAL.

Enums

enum DiffTag

Which side of the comparison a diff line came from.

Removed

Present in the real merged target, absent from the emitted file.

Added

Present in the emitted file, absent from the target.

enum ReproScore

Where a reproduction attempt landed on the scoreboard ladder.

The two ladder rungs missing here, ERROR and excluded, are not outcomes of comparing two files: ERROR means no file was produced to compare, and excluded means the file was never a scoreable bump (a toolchain meta-recipe, a backfill, a package with no prior recipe). Both are decided before this comparison runs, and both are recorded with their reason rather than dropped.

Exact

Byte-identical to the merged file.

Semantic

Differs only in ways that cannot change the build: comments, trailing whitespace, blank-line runs.

Material

A real difference in what would be built.

Implementations

impl ReproScore

Functions

fn as_str(self) -> &'static str

The scoreboard spelling, for a | File | Score | Note | row.

Traits implemented

impl fmt::Display for ReproScore

Structs and Unions

struct DiffLine

One differing line of the raw (un-normalized) diff.

tag: DiffTag

Which file the line belongs to.

text: String

The line, exactly as it appears in that file.

struct ReproComparison

A scored reproduction: the ladder rung, plus the raw diff behind it.

score: ReproScore

The ladder rung, decided on normalized text.

raw_diff: Vec<DiffLine>

Every line that differs between the raw files, in file order.

Deliberately raw: a SEMANTIC verdict is only auditable if the reader can see the comment and whitespace differences the score forgave and judge that call for themselves.

Implementations

impl ReproComparison

Functions

fn render_raw_diff(&self) -> String

The raw diff as text, one -/+ line per difference.

- lines are the real merged target, + lines are what the bump emitted.