diff --git a/.github/workflows/release-benchmarks.yml b/.github/workflows/release-benchmarks.yml index 546c916..1d5e2d6 100644 --- a/.github/workflows/release-benchmarks.yml +++ b/.github/workflows/release-benchmarks.yml @@ -54,7 +54,7 @@ jobs: echo "::error::Dispatch with --ref $RELEASE_TAG and the matching tag input" exit 1 fi - target="$(uvx --python 3.15 --from research-repo-tools==0.1.9 research-repo-tools \ + target="$(uvx --python 3.15 --from research-repo-tools==0.2.0 research-repo-tools \ --root "$RUNNER_TEMP" performance release-draft "$GH_REPO" "$RELEASE_TAG" \ --commit "$GITHUB_SHA" --expected-title "$RELEASE_TAG" | jq -c .)" echo "target=$target" >> "$GITHUB_OUTPUT" @@ -194,7 +194,7 @@ jobs: [[ "$RELEASE_ASSET" == "la-stack-${RELEASE_TAG}-criterion-baseline.tar.gz" ]] || exit 1 printf '%s\n' "$RELEASE_TARGET" > "$RUNNER_TEMP/release-target.json" - uvx --python 3.15 --from research-repo-tools==0.1.9 research-repo-tools \ + uvx --python 3.15 --from research-repo-tools==0.2.0 research-repo-tools \ --root "$RUNNER_TEMP" performance release-upload "$GH_REPO" "$RELEASE_TAG" \ "$GITHUB_WORKSPACE/$RELEASE_ASSET" --target "$RUNNER_TEMP/release-target.json" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 782075b..6db1d8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ change files. Run bare `just` for the complete generated command list, including arguments and descriptions. The [command policy](docs/dev/docs.md#command-discovery-and-placement) owns recipe naming and documentation placement. -Setup, tool checks, updates, changelog, and opt-in CodeRabbit review use the published `research-repo-tools==0.1.9` package, +Setup, tool checks, updates, changelog, and opt-in CodeRabbit review use the published `research-repo-tools==0.2.0` package, locked in the `tooling` dependency group and included by `dev`. Normal setup and CI install it from PyPI through `uv sync --locked --group dev`; a sibling checkout is unnecessary. To upgrade it deliberately, review the exact diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md index 18ff895..15ed8e1 100644 --- a/docs/BENCHMARKING.md +++ b/docs/BENCHMARKING.md @@ -336,7 +336,10 @@ records the run identity and the actual current/baseline origins of each series. Historical CSV/JSON evidence remains frozen with its original provenance. The README table shows median times in nanoseconds, with lower and upper 95% confidence bounds below each timing. Its compact comparison columns show -speedup ratios; the confidence level is stated once in the table legend. +speedup ratios; the recorded confidence level is stated once in the table caption. +Shared `RunTableStyle` options produce this layout directly. Timing headings retain +measured versions, and each speedup names its peer-time numerator and la-stack-time +denominator. Ratios remain point estimates without ratio intervals or significance claims. Missing, stale, or inconsistent input aborts publication atomically. An existing local release tag must contain the exact selected asset bytes. A future release tag may be absent during release preparation. @@ -399,10 +402,10 @@ Serialization, validation, rendering, and promotion failures preserve existing published reports and selection. Review and commit the whole new run and index. Legacy snapshots under `docs/performance/` retain their original bytes, hashes and framing; see the [legacy index](performance/README.md). -Their custom writers and report renderer are retired. Verified comparison-only -conversion uses `tooling/performance-legacy.toml` with the shared `performance convert` -command, retaining the original hashes and opaque metadata. It cannot reconstruct -raw samples or create complete-run provenance. +Their custom writers, report renderer and CSV conversion declaration are retired. +Read their original CSV/JSON and archived reports directly. They cannot reconstruct +raw samples or supply complete-run provenance. Publication accepts the declared +Just phase format; replay of the retired Python phase driver is unsupported. The last published legacy report is preserved in the [Markdown archive](archive/performance/README.md). The checked-in current report keeps its historical format until a complete run is promoted; old summaries do diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 8411079..a20e53a 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -321,7 +321,7 @@ release ID and tag commit before attaching the archive. Do not edit the draft, move the tag, or publish manually while the workflow is running. -The privileged jobs install the exact published `research-repo-tools==0.1.9` +The privileged jobs install the exact published `research-repo-tools==0.2.0` package on Python 3.15 without repository checkout. Preflight emits versioned target JSON with the captured ID, commit and title assertion. Its trusted job output passes directly to the publisher, separately from the producer's inert diff --git a/docs/code_organization.md b/docs/code_organization.md index 7c0f4af..c539f3a 100644 --- a/docs/code_organization.md +++ b/docs/code_organization.md @@ -145,7 +145,8 @@ and review policy belong in [Security Checks](../SECURITY.md#security-checks). The shared package also owns managed tool installation, verification, and update implementation. `.python-version`, `rust-toolchain.toml`, and `pyproject.toml` own consumer declarations; `scripts/tests/test_toolchain_integration.py` checks -actual recipe sequencing and native managed execution. The +the exact published pin, registry lock, installed version and distribution hashes, +alongside actual recipe sequencing and native managed execution. The `.github/actions/setup-tools/action.yml` composite synchronizes the locked PyPI package and exports verified paths for CI. Release callers disable its caches. @@ -166,10 +167,12 @@ Performance consumers use shared Criterion parsing and estimate/comparison validation, digest verification, archive extraction, byte-preserving document sections, and multi-file transactions. Shared rendering produces complete-run reports, coordinate SVG, timing CSV and document tables. Historical artifact -bytes and fingerprint framing remain frozen; declarative shared conversion reads -the original CSV/metadata without a legacy writer. Published v0.1.9 +bytes and fingerprint framing remain frozen and readable directly. Published v0.2.0 owns common-harness orchestration, completeness, worktrees, complete run retention, validated latest selection, and transactional report publication. +Its `RunTableStyle` API owns compact README table formatting; the consumer selects +dimension headings, intervals below timings, a common confidence legend and +ratio-only speedups without rewriting Markdown cells or prose. Generic parsing, staging, and rollback tests belong upstream; local tests verify the scientific and retained-artifact integration boundaries. @@ -196,7 +199,7 @@ shared APIs for tracked-only measurements, metadata-free historical raw archives and la-stack README publication. `tooling/performance-plot.toml` declares dimensions, units, series and axes. `tooling/performance-report.toml` owns the canonical report path, title, and shared history path. -`tooling/performance-legacy.toml` declares shared historical comparison conversion. +The obsolete CSV conversion declaration and pre-Just Python phase replay are removed. The old comparison renderer, CSV/provenance writer, summary reader, plotter, release-inventory wrapper and Python packaging are removed. Only the two consumer adapters remain; `scripts/tests/test_performance_workflow.py` verifies diff --git a/docs/dev/performance-tooling-adoption.md b/docs/dev/performance-tooling-adoption.md index 7ba4012..a2f10d2 100644 --- a/docs/dev/performance-tooling-adoption.md +++ b/docs/dev/performance-tooling-adoption.md @@ -8,6 +8,29 @@ runtime code comes from the installed package. ## Capability disposition +Follow-up [#285](https://github.com/acgetchell/la-stack/issues/285) adopts the +published `research-repo-tools==0.2.0` wheel and sdist, including +[upstream #110](https://github.com/acgetchell/research-repo-tools/issues/110). +The exact PyPI pin and lock replace v0.1.9; the package also advances its required +Just dependency to v1.59.0. `RunTableStyle` now supplies the six-column README +layout directly, so `_readme_table()` and its Markdown string/column rewrites +are removed. The shared renderer retains recorded intervals, the selected +statistic, confidence level and explicit ratio direction. Scientific selection, +original reference phases, README markers, evidence freshness and transactional +asset pointers remain consumer responsibilities. + +Offline consumer checks exercise the actual `performance-readme` recipe with +mean and median estimates, original peer phases, retained history and the README +figure. No new measurements or regenerated immutable reports are required. +Offline rendering of the retained published run preserves its SVG and CSV bytes. +Current README republication still rejects the stale source digest and leaves +the figure and measured-asset pointers untouched. +The obsolete CSV conversion declaration and pre-Just Python phase replay are +removed. Retained complete runs use the declared Just driver; original legacy +CSV/JSON and Markdown records remain available without regeneration or conversion. +Closure of #285 requires native Linux/macOS/Windows CI on the adoption commit; +local validation alone does not establish those results. + | Upstream capability | Consumer adoption | Local responsibility | | --- | --- | --- | | #94 captured targets | Installed release-target CLI passes versioned JSON from trusted preflight to writer | Dispatch/ref equality, current attempt, title, privilege separation, asset filename and explicit publication | @@ -15,6 +38,7 @@ runtime code comes from the installed package. | #96 provenance reports | Shared renderer 2 and publication-relative evidence links | Scientific eligibility and frozen historical records | | #97 complete-run sections | Shared presentation and publication transaction | README markers, measured-asset pointers and current source/harness checks | | #98 Criterion verification | Declarative shared discovery, versioned inventory and saved-sample APIs | Full Cargo suites/features, minimum scientific coverage and correctness gates | +| #110 compact table styles | Installed `RunTableStyle` selects headings, intervals, confidence caption and ratio-only columns | Scientific selections, ratio pairs and README publication policy | The full-report standardization from #277 remains. New reports expose recorded provenance and evidence links. Shared mixed-renderer history preserves immutable @@ -31,7 +55,7 @@ The audit compared the dependency-only MCMC environment and declarative | `benchmark_contract.py` | Deleted; metric/group/peer/API declarations moved to `tooling/performance-policy.toml`; historical digests stay frozen | | `benchmark_summaries.py` | Deleted; frozen historical summaries are records rather than regeneration inputs | | `criterion_dim_plot.py` | Deleted; shared complete-run SVG/CSV/table presentation replaces raw-Criterion/gnuplot rendering | -| `performance_artifacts.py` | Deleted; shared evidence/publication and declarative `performance-legacy.toml` replace CSV/provenance writers and readers | +| `performance_artifacts.py` | Deleted; shared evidence/publication replace writers and readers; historical CSV conversion retired in #285 | | `release_baseline.py` | Deleted; native discovery uses the shared CLI and saved-copy verification uses the shared configured API | | Local wheel/sdist and console-entry-point packaging | Removed; `uv` manages a dependency-only environment | | Duplicate parser, renderer, legacy-writer and packaging tests | Removed; actual installed-package/configuration and scientific integration tests remain | @@ -40,11 +64,9 @@ Retirement conditions are satisfied by installed replacement checks, preservatio of original bytes/hashes and a documented supported workflow. The old report layout need not be regenerated indefinitely. New reports use the shared format; historical reports, CSV/JSON, indexes, plots and archives remain untouched. -Shared historical conversion verifies the original hash and retains opaque -metadata and original digest names. It produces comparison evidence, never -fabricated complete-run samples or newly captured host provenance. Retire its -conversion declaration only after verified companions exist for every original -and those original bytes remain independently available. +Historical conversion was verified during #276 and retired in #285. Original +summaries remain independently readable with their original hashes and opaque +metadata. They cannot provide complete-run samples or newly captured host provenance. Only two production modules remain: @@ -85,6 +107,17 @@ current published wheels support Python 3.15 and native gates pass. ## Size and validation evidence +The #285 consumer-test cleanup was checked against tests bundled in the published +v0.2.0 sdist. Shared implementation matrices are removed only where that release +already covers them; consumer policy and installed-package integration remain. + +| Coverage | Published upstream owner | Retained consumer check | +| --- | --- | --- | +| Changelog rendering and repeatability | `tests/changelog/test_cliff_template.py` and `test_workflow.py` | Actual la-stack formatter/preservation policy, one authored-note generation and historical notes | +| Measurement completeness and failed gates | `tests/performance/public_complete_consumer.py` | Actual sampling policy, required scientific inventory, native gate commands and original peer phases | +| Package identity | Consumer-owned | One test in `test_toolchain_integration.py` checks pin, registry, installed version and wheel/sdist hashes; recipe tests verify preservation and forwarding | +| Release event identity and check-status matrices | `tests/releases/test_publishing.py` | Every configured native check remains required; actual workflow approval, immutability, notes and upload barriers remain tested | + Counts use physical lines from `wc -l scripts/*.py scripts/tests/*.py` and `uv run --locked pytest --collect-only -q`. Baseline is `905aba7`; test counts describe consumer checks, not measurements. diff --git a/docs/performance/README.md b/docs/performance/README.md index 398f9bc..385da13 100644 --- a/docs/performance/README.md +++ b/docs/performance/README.md @@ -8,7 +8,7 @@ the [Benchmarking guide](../BENCHMARKING.md) owns the current workflow. ## Contents - [Saved runs](#saved-runs) -- [Regenerating reports](#regenerating-reports) +- [Reading retained reports](#reading-retained-reports) - [Comparing measurements](#comparing-measurements) - [Historical availability](#historical-availability) @@ -38,27 +38,19 @@ measurements; missing current measurements are never filled from an older run. `latest.json` identifies the last saved legacy snapshot. New complete-run publication maintains its own shared index and latest pointer instead. -## Regenerating reports +## Reading retained reports -While no shared complete run has been retained, these commands can use the -committed legacy snapshot when scratch report inputs are absent: +Read these original CSV/JSON records and the +[archived Markdown reports](../archive/performance/README.md) directly. +Their writers, custom renderer and conversion declaration are retired. +`performance-doc` and `performance-readme` require shared complete-run evidence; +they do not consume these summaries. Unsupported legacy scratch inputs block +fallback to older history, so archive or remove those scratch files first. -```bash -just performance-doc -just performance-readme -``` - -Neither command reruns benchmarks. Existing scratch inputs take precedence; -partial or corrupt inputs fail validation rather than silently selecting an -older snapshot. README publication still checks the measured source and -benchmark contract against the current checkout, along with the lockfile and -release version. Committing only the saved artifacts does not invalidate a -snapshot: its original measurement commit remains in provenance. Legacy inputs -without a benchmark-contract digest also require the original commit. - -Once a shared complete run is promoted, the commands select that history. Legacy summaries cannot be converted losslessly into complete-run evidence: they retain aggregate statistics and sample counts, but not the raw samples. +The [Benchmarking guide](../BENCHMARKING.md) owns current report and README +publication, including source freshness and measured-asset checks. ## Comparing measurements diff --git a/pyproject.toml b/pyproject.toml index cac782e..bd46a81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ required-version = "==0.13.1" # Semgrep has its own locked Python 3.14 environment under tooling/semgrep. [dependency-groups] -tooling = [ "research-repo-tools==0.1.9" ] +tooling = [ "research-repo-tools==0.2.0" ] dev = [ { include-group = "tooling" }, "actionlint-py==1.7.12.25", diff --git a/scripts/README.md b/scripts/README.md index 89dd4a3..eebdb01 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,7 +1,7 @@ # scripts/ Only two production Python adapters remain. Generic tools run from the installed -`research-repo-tools==0.1.9` base package; their declarations belong in +`research-repo-tools==0.2.0` base package; their declarations belong in `tooling/` and `pyproject.toml`. The project is a dependency-only environment, with no local Python distribution, build backend or console entry points. @@ -60,9 +60,11 @@ table renderers. The former preserves current source/harness membership and digests and advances the three existing measured-artifact pointers transactionally. README candidates pass the shared Markdown formatter with repository policy before any publication files are replaced. -The README adapter labels dimensions, keeps each confidence interval below its -timing, states the confidence level once, and selects the shared speedup columns. -Timing estimates and comparison arithmetic remain owned by the shared renderer. +README publication configures the shared `RunTableStyle` API to label dimensions, +keep both interval bounds below each timing, state the recorded confidence level +once, and select ratio-only speedup columns with explicit numerator/denominator +semantics. Series labels supply the timing headings and measured versions. +The shared renderer owns formatting, timing estimates and comparison arithmetic. ```bash just performance-release @@ -74,17 +76,19 @@ The raw-Criterion/gnuplot publisher, historical summary writer and custom report renderer are retired after installed-package replacement checks. Original CSV/JSON, reports, fingerprints and archives remain unchanged. They are read directly as frozen records; no regeneration requirement keeps -their writers alive. For verified historical conversion into shared comparison -evidence, use `research-repo-tools performance convert` with -`tooling/performance-legacy.toml`; this never reconstructs complete runs from -summaries. Do not promote converted historical summaries as new measurements. -The conversion declaration remains until every retained original has a verified -shared comparison companion and its original bytes remain independently available. +their writers alive. The obsolete CSV conversion declaration and pre-Just Python +phase format are retired. Report and README publication require complete-run +evidence with the declared Just driver. Historical summaries lack raw samples +and cannot supply that evidence; their original bytes remain available directly. Generic parser, renderer, transaction and provider failure matrices belong upstream. Consumer tests retain scientific selection/phase/gate checks, actual native recipe wiring, complete release archive round trips, captured-target handoff, offline publication and original artifact hashes. +`scripts/tests/test_toolchain_integration.py` owns the single published-package +contract: manifest pin, registry lock, installed version and wheel/sdist hashes. +Dependency-update and release-workflow tests derive that requirement from the +manifest and verify it survives their actual recipe execution. ## Changelog and release tooling diff --git a/scripts/archive_performance.py b/scripts/archive_performance.py index 8cf27d4..633a1b6 100644 --- a/scripts/archive_performance.py +++ b/scripts/archive_performance.py @@ -25,7 +25,16 @@ from research_repo_tools.release_assets import download_release_asset from research_repo_tools.release_discovery import normalize_tag, stable_published_releases from research_repo_tools.release_pairs import PairMode, ReleasePair, resolve_pair -from research_repo_tools.run_presentation import CoordinatePlot, RunLayout, RunRow, SeriesSelection, render_coordinate_svg, render_run_table, serialize_run_csv +from research_repo_tools.run_presentation import ( + CoordinatePlot, + RunLayout, + RunRow, + RunTableStyle, + SeriesSelection, + render_coordinate_svg, + render_run_table, + serialize_run_csv, +) from research_repo_tools.run_reports import load_latest_run, load_run_report_plan from research_repo_tools.worktrees import apply_snapshot, capture_snapshot, temporary_worktree @@ -217,6 +226,14 @@ def main(argv: list[str] | None = None) -> int: PLOT_CONFIG = "tooling/performance-plot.toml" +README_TABLE_STYLE = RunTableStyle( + row_heading="Dimension", + intervals="below", + timing_legend=True, + comparison_columns=("ratio",), + ratio_headings=("Speedup vs nalgebra", "Speedup vs faer"), + ratio_suffix="\N{MULTIPLICATION SIGN}", +) def _repo_root() -> Path: @@ -273,38 +290,6 @@ def _asset_links(document: bytes, *, metric: str, statistic: str, release: str) return pattern.sub(lambda match: match["prefix"] + release.encode() + match["asset"], document) -def _readme_table(evidence: Evidence, layout: RunLayout) -> str: - """Adapt shared timing/comparison cells to the compact README presentation.""" - rendered = render_run_table(evidence, layout).replace(" (0.95 confidence)", "") - lines = [] - for line in rendered.splitlines(): - output_line = line - if line.startswith("| "): - cells = line[2:-2].split(" | ") - if len(cells) != 8: - msg = "README comparison requires three timing and two comparison pairs" - raise ValueError(msg) - if cells[0] == "Case": - cells = ["Dimension", "la-stack", "nalgebra", "faer", "Speedup vs nalgebra", "Speedup vs faer"] - else: - if cells[0] != "---": - for index in (1, 2, 3): - cells[index] = cells[index].replace(" ns [", "
[", 1) - for index in (4, 6): - cells[index] += "\N{MULTIPLICATION SIGN}" - cells = [cells[index] for index in (0, 1, 2, 3, 4, 6)] - output_line = "| " + " | ".join(cells) + " |" - elif line.startswith("Intervals are recorded marginal timing intervals;"): - output_line = ( - f"{layout.statistic.capitalize()} time in ns; brackets show the lower and upper bounds of the **95% confidence interval**.\n" - "Speedups are ratios of point estimates; no ratio intervals or significance claims are inferred." - ) - elif line.startswith("Time reduction is 100"): - continue - lines.append(output_line) - return "\n".join(lines) + "\n" - - def publish_plot(root: Path, metric: str, statistic: Literal["mean", "median"], *, log_y: bool, update_readme: bool) -> None: """Retain original phases, enforce scientific eligibility, and publish once.""" # The report registry imports only the independent benchmark catalog. @@ -365,7 +350,10 @@ def publish_plot(root: Path, metric: str, statistic: Literal["mean", "median"], inputs["pyproject.toml"] = (root / "pyproject.toml").read_bytes() outputs["README.md"] = format_markdown( _asset_links( - replace_section(document, markers, _readme_table(evidence, layout)), metric=metric, statistic=statistic, release=context["release"] + replace_section(document, markers, render_run_table(evidence, layout, style=README_TABLE_STYLE)), + metric=metric, + statistic=statistic, + release=context["release"], ).decode("utf-8"), root / "README.md", root / "pyproject.toml", diff --git a/scripts/performance_runs.py b/scripts/performance_runs.py index c32e063..ff6013f 100644 --- a/scripts/performance_runs.py +++ b/scripts/performance_runs.py @@ -323,19 +323,7 @@ def declared_plan( # noqa: PLR0913 return CommonHarnessPlan(config, phases["baseline"], phases["current"], tuple(series)) -def _legacy_cargo_commands(suite: str, phase: str) -> str: - """Validate the frozen pre-Just evidence contract without running its driver.""" - suites = {"vs_linalg": ("-p", "la-stack-comparison", "--features", "bench"), "exact": ("--features", "bench,exact")} - return json.dumps( - [ - ("cargo", "bench", "--locked", *features, "--bench", name, "--", *(("la_stack",) if phase == "current" and name == "vs_linalg" else ()), "--noplot") - for name, features in suites.items() - if suite in {"all", name} - ] - ) - - -def validate_scientific_run(evidence: Evidence, *, release: bool = False, root: Path | None = None) -> CompleteRun: # noqa: C901 +def validate_scientific_run(evidence: Evidence, *, release: bool = False, root: Path | None = None) -> CompleteRun: """Require la-stack's sampling, selected coverage and original reference phases.""" run = validate_run_evidence(evidence) sources = dict(evidence.sources) @@ -349,10 +337,9 @@ def validate_scientific_run(evidence: Evidence, *, release: bool = False, root: msg = "same-version local comparisons require distinct source identities" raise ValueError(msg) samples = dict(run.phases) - for sample in samples.values(): - if sample.policy != CompletePolicy(sample.policy.expected): - msg = "la-stack requires 100 samples, mean and median, and 95% intervals in ns" - raise ValueError(msg) + if any(sample.policy != CompletePolicy(sample.policy.expected) for sample in samples.values()): + msg = "la-stack requires 100 samples, mean and median, and 95% intervals in ns" + raise ValueError(msg) expected = declared_plan( pair, context["suite"], @@ -365,29 +352,18 @@ def validate_scientific_run(evidence: Evidence, *, release: bool = False, root: if run.series != expected.series or context["baseline-api-compatibility"] != adapter: msg = "retained series or compatibility policy differs from la-stack declarations" raise ValueError(msg) - legacy = "phase-driver" not in context - compatible = set(expected.measurement.compatible) - ({"context.tool.just"} if legacy else set()) - if set(run.compatible) != compatible: + if set(run.compatible) != set(expected.measurement.compatible): msg = "retained run omits required host, toolchain, or dependency compatibility" raise ValueError(msg) required = dict(expected.measurement.context) - if legacy: - del required["phase-driver"] - required.update((f"{phase}-cargo-commands", _legacy_cargo_commands(context["suite"], phase)) for phase in ("baseline", "current")) for phase, source in evidence.sources: actual = dict(source.context) - if actual.get("phase-driver") != context.get("phase-driver") or any(actual.get(key) != value for key, value in required.items()): + if any(actual.get(key) != value for key, value in required.items()): msg = "retained phase differs from the declared consumer policy" raise ValueError(msg) for key, native_command in (("command", getattr(expected, phase).command), ("gate-command", getattr(expected, phase).gate)): - command = native_command recorded = tuple(json.loads(actual[key])) - if legacy: - # Interpreter paths varied across hosts. Old evidence remains - # readable without retaining or executing the Python driver. - command = ("scripts/performance_phase.py", "measure" if key == "command" else "gate", context["suite"], phase) - recorded = recorded[1:] - if recorded != command: + if recorded != native_command: raise ValueError(f"retained {phase} {key} differs from the declared Cargo phase") return run diff --git a/scripts/tests/test_changelog_integration.py b/scripts/tests/test_changelog_integration.py index af6838b..ed6c5f0 100644 --- a/scripts/tests/test_changelog_integration.py +++ b/scripts/tests/test_changelog_integration.py @@ -2,7 +2,6 @@ import shutil import tomllib -from importlib.metadata import version from pathlib import Path from typing import TYPE_CHECKING @@ -16,21 +15,9 @@ ROOT = Path(__file__).resolve().parents[2] -def test_published_package_and_shared_policy() -> None: - """The dependency-only environment and registry lock select the published package.""" +def test_consumer_changelog_policy_uses_the_shared_formatter() -> None: + """Retain la-stack's repository, formatter and authored-body policy.""" manifest = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) - assert manifest["project"]["dependencies"] == [] - assert manifest["tool"]["uv"]["package"] is False - assert "build-system" not in manifest - assert manifest["dependency-groups"]["tooling"] == ["research-repo-tools==0.1.9"] - assert {"include-group": "tooling"} in manifest["dependency-groups"]["dev"] - assert version("research-repo-tools") == "0.1.9" - lock = tomllib.loads((ROOT / "uv.lock").read_text(encoding="utf-8")) - package = next(item for item in lock["package"] if item["name"] == "research-repo-tools") - assert package["version"] == "0.1.9" - assert package["source"] == {"registry": "https://pypi.org/simple"} - assert package["wheels"][0]["hash"] == "sha256:c4805004e9a857c072e7b735b033dd13ad256fb283c6e1a4adca0aa59cb67540" - assert package["sdist"]["hash"] == "sha256:7bfb16d7a38e8b3cecd30ce526cf808b377b4480ebd512740198a0361c3d027b" assert manifest["tool"]["research-repo-tools"]["changelog"] == { "owner": "acgetchell", "repository": "la-stack", @@ -40,8 +27,8 @@ def test_published_package_and_shared_policy() -> None: assert not (ROOT / "cliff.toml").exists() -def test_authored_dependency_notes_survive_generation(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - """Exercise the consumer policy with Ruff, Ty and setuptools notes and code.""" +def test_consumer_generation_preserves_an_authored_dependency_note(tmp_path: Path) -> None: + """Exercise the installed CLI with la-stack's actual formatter and policy.""" for name in ("pyproject.toml", "changelog-rumdl.toml", ".python-version", "rust-toolchain.toml"): shutil.copyfile(ROOT / name, tmp_path / name) run_command("git", ["--no-pager", "init", "--quiet"], cwd=tmp_path) @@ -49,37 +36,14 @@ def test_authored_dependency_notes_survive_generation(tmp_path: Path, capsys: py run_command("git", ["--no-pager", "config", key, value], cwd=tmp_path) run_command("git", ["--no-pager", "commit", "--quiet", "--allow-empty", "-m", "feat: initial history"], cwd=tmp_path) run_command("git", ["--no-pager", "tag", "v0.4.5"], cwd=tmp_path) - notes = ( - ("ruff", "deps-dev", "Read [release notes](https://github.com/astral-sh/ruff/compare/0.16.1...0.16.2) before changing `Vector` checks."), - ("ty", "deps-dev", "Keep [changelog](https://github.com/astral-sh/ty/releases) context and `--error all`."), - ( - "setuptools", - "deps", - 'See [comparison](https://github.com/pypa/setuptools/compare/v83.0.0...v84.0.0).\n\n```toml\nrequires = ["setuptools>=84.0.0"]\n```', - ), - ) - for dependency, scope, body in notes: - run_command("git", ["--no-pager", "commit", "--quiet", "--allow-empty", "-m", f"chore({scope}): bump {dependency}\n\n{body}"], cwd=tmp_path) + note = "Read [release notes](https://github.com/astral-sh/ruff/compare/0.16.1...0.16.2) before changing `Vector` checks." + run_command("git", ["--no-pager", "commit", "--quiet", "--allow-empty", "-m", f"chore(deps-dev): bump ruff\n\n{note}"], cwd=tmp_path) (tmp_path / "CHANGELOG.md").write_text("# Changelog\n\n## [0.4.5] - 2000-01-02\n\n- Historical date.\n", encoding="utf-8") args = ["--root", str(tmp_path), "changelog", "generate", "--tag", "v0.4.6", "--date", "2026-10-09"] - assert main([*args, "--dry-run"]) == 0 - preview = capsys.readouterr().out - assert "### Dependencies" in preview - assert "## [0.4.5] - 2000-01-02" in preview - for expected in ( - "https://github.com/astral-sh/ruff/compare/0.16.1...0.16.2", - "https://github.com/astral-sh/ty/releases", - "https://github.com/pypa/setuptools/compare/v83.0.0...v84.0.0", - "before changing `Vector` checks.", - "context and `--error all`.", - 'requires = ["setuptools>=84.0.0"]', - ): - assert expected in preview assert main(args) == 0 - generated = (tmp_path / "CHANGELOG.md").read_bytes() - assert generated == preview.encode() - assert main(args) == 0 - assert (tmp_path / "CHANGELOG.md").read_bytes() == generated + generated = (tmp_path / "CHANGELOG.md").read_text(encoding="utf-8") + assert "### Dependencies" in generated + assert note in generated def test_consumer_recipes_and_retained_history(capsys: pytest.CaptureFixture[str]) -> None: diff --git a/scripts/tests/test_performance_workflow.py b/scripts/tests/test_performance_workflow.py index 32538c4..a374f51 100644 --- a/scripts/tests/test_performance_workflow.py +++ b/scripts/tests/test_performance_workflow.py @@ -18,9 +18,7 @@ from research_repo_tools import publication from research_repo_tools.common_measurement import CommonHarnessPlan, measure_prepared_pair from research_repo_tools.complete_runs import CompletePolicy, RunSeries, render_run, run_identity, serialize_run -from research_repo_tools.criterion import parse_comparison from research_repo_tools.just_inspect import dry_run -from research_repo_tools.legacy_evidence import convert_csv from research_repo_tools.postprocess_changelog import MarkdownFormatError from research_repo_tools.process import ExecutableNotFoundError, run_command from research_repo_tools.release_pairs import ReleasePair @@ -36,31 +34,26 @@ ROOT = Path(__file__).resolve().parents[2] PAIR = ReleasePair("v0.4.6", "v0.4.5") -DRIVER = """import json, os, sys +DRIVER = """import json, sys from pathlib import Path action, phase = sys.argv[1:] -fault = os.environ.get("FIXTURE_FAULT", "") -if action == "gate": - if fault == "gate": - raise SystemExit(1) - if fault == "stale": - Path("src/lib.rs").write_text("changed during gate") -else: - ids = INVENTORIES[phase] - for name in ids[:-1] if fault == "missing-row" else ids: +if action == "measure": + for name in INVENTORIES[phase]: path = Path("target/criterion") / name / "new" path.mkdir(parents=True) - value = 10.0 if phase == "baseline" else 8.0 + value = (16.0 if "/faer_" in name else 10.0) if phase == "baseline" else 8.0 estimate = {"point_estimate": value, "confidence_interval": {"lower_bound": value * .9, "upper_bound": value * 1.1, "confidence_level": .95}} - estimates = {"median": estimate} if fault == "missing-statistic" else {"mean": estimate, "median": estimate} - count = 99 if fault == "missing-sample" else 100 - sample = {"iters": [1] * count, "times": [value] * count} + mean = (32.0 if "/faer_" in name else 20.0) if phase == "baseline" else 12.0 + mean_interval = {"lower_bound": mean - mean / 10, "upper_bound": mean + mean / 10, "confidence_level": .95} + mean_estimate = {"point_estimate": mean, "confidence_interval": mean_interval} + estimates = {"mean": mean_estimate, "median": estimate} + sample = {"iters": [1] * 100, "times": [value] * 100} for filename, data in (("benchmark.json", {"full_id": name}), ("estimates.json", estimates), ("sample.json", sample)): (path / filename).write_text(json.dumps(data)) """ -def prepared(tmp_path: Path, fault: str = "") -> tuple[Path, Path, CommonHarnessPlan]: +def prepared(tmp_path: Path) -> tuple[Path, Path, CommonHarnessPlan]: """Use the real consumer inventory and policies with a bounded timing fixture.""" ids = tuple(sorted(required_report_ids())) inventories = { @@ -116,9 +109,7 @@ def prepared(tmp_path: Path, fault: str = "") -> tuple[Path, Path, CommonHarness cwd=root, ) baseline, current = tmp_path / "baseline", tmp_path / "current" - plan = policy.declared_plan( - PAIR, "all", "release-signal", inventories, baseline_env=(("FIXTURE_FAULT", fault),), current_env=(), config=policy.configuration(current) - ) + plan = policy.declared_plan(PAIR, "all", "release-signal", inventories, baseline_env=(), current_env=(), config=policy.configuration(current)) return baseline, current, plan @@ -155,7 +146,11 @@ def test_common_harness_and_reference_phase(tmp_path: Path) -> None: assert sources["baseline"].source_sha256 != sources["current"].source_sha256 assert (baseline / "benches/exact.rs").read_bytes() == (current / "benches/exact.rs").read_bytes() assert {series.phase for series in run.series if series.name in {"nalgebra", "faer"}} == {"baseline"} - assert all(sample.policy.statistics == ("mean", "median") for _, sample in run.phases) + for _, sample in run.phases: + assert sample.policy.sample_count == 100 + assert sample.policy.statistics == ("mean", "median") + assert sample.policy.confidence_level == 0.95 + assert sample.policy.unit == "ns" def test_measurement_worktrees_accept_an_aliased_system_temporary_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: @@ -209,14 +204,13 @@ def benchmark_ready(baseline: Path, current: Path, _plan: object, _pair: Release assert list(physical.iterdir()) == [] -@pytest.mark.parametrize("fault", ["gate", "stale", "missing-row", "missing-statistic", "missing-sample"]) -def test_declared_gates_and_completeness_fail_closed(tmp_path: Path, fault: str) -> None: - baseline, current, plan = prepared(tmp_path, fault) - with pytest.raises(ValueError, match=r"gate|changed|incomplete|sample|statistic|expected|missing|mean"): - measure_prepared_pair(baseline, current, plan, PAIR, working_tree=True) - assert not (current / "target/criterion").exists() - if fault in {"gate", "stale"}: - assert not (baseline / "target/criterion").exists() +def test_consumer_plan_rejects_an_inventory_missing_a_required_report_row() -> None: + missing = "d64/la_stack_lu_solve" + ids = tuple(sorted(required_report_ids())) + assert missing in ids + inventories = {"baseline": ids, "current": tuple(name for name in ids if name != missing)} + with pytest.raises(ValueError, match=rf"inventory omits selected report rows: \['{missing}'\]"): + policy.declared_plan(PAIR, "all", "release-signal", inventories, baseline_env=(), current_env=(), root=ROOT) @pytest.mark.parametrize("report_path", ["docs/performance.md", "docs/reports/current.md"]) @@ -287,30 +281,68 @@ def test_partial_scratch_and_stale_source_cannot_select_old_history(tmp_path: Pa policy.verify_current_source(root, evidence) -@pytest.mark.parametrize("archive", ["docs/performance-runs", "docs/performance-history"]) -def test_readme_uses_original_reference_phases_after_cleanup(complete_run: tuple[Path, Evidence], monkeypatch: pytest.MonkeyPatch, archive: str) -> None: +def prepare_readme_recipe(root: Path, statistic: str) -> None: + """Make the native README recipe runnable against the retained fixture.""" + original = ( + (ROOT / "README.md") + .read_text(encoding="utf-8") + .replace(":lu_solve:median:new:", f":lu_solve:{statistic}:new:") + .replace("vs_linalg_lu_solve_median", f"vs_linalg_lu_solve_{statistic}") + .replace("median time vs dimension", f"{statistic} time vs dimension") + ) + (root / "README.md").write_text(original, encoding="utf-8") + for name in ("justfile", "uv.lock", ".python-version"): + shutil.copyfile(ROOT / name, root / name) + (root / "scripts").mkdir() + for name in ("archive_performance.py", "performance_runs.py"): + shutil.copyfile(ROOT / "scripts" / name, root / "scripts" / name) + + +def assert_readme_presentation(readme: str, statistic: str) -> None: + """Check the consumer's compact style, comparison direction and figure.""" + table = readme.split(f"", 1)[1].split(f"", 1)[0] + assert "| Dimension | la-stack (v0.4.6) | nalgebra (" in table + assert "| faer (" in table + assert "| Speedup vs nalgebra | Speedup vs faer |" in table + assert "(0.95 confidence)" not in table + assert table.count("Confidence level: 95%.") == 1 + assert f"Statistic: {statistic}. Unit: ns." in table + current, peer, ratio = ("12
[10.8, 13.2]", "20
[18, 22]", "1.66667") if statistic == "mean" else ("8
[7.2, 8.8]", "10
[9, 11]", "1.25") + faer, faer_ratio = ("32
[28.8, 35.2]", "2.66667") if statistic == "mean" else ("16
[14.4, 17.6]", "2") + assert f"| 2 | {current} | {peer} | {faer} | {ratio}\N{MULTIPLICATION SIGN} | {faer_ratio}\N{MULTIPLICATION SIGN} |" in table + assert "ratios are point estimates, with no ratio intervals or significance claims" in table + for name in ("nalgebra", "faer"): + assert f"Speedup vs {name}: ratio; numerator = {name} (" in table + assert table.count("denominator = la-stack (v0.4.6)") == 2 + assert "time reduction" not in table + assert f"![LU solve (factor + solve): {statistic} time vs dimension][lu-solve-benchmark]" in readme + assert ( + f"[lu-solve-benchmark]: https://raw.githubusercontent.com/acgetchell/la-stack/{PAIR.current}/docs/assets/bench/vs_linalg_lu_solve_{statistic}.svg" + in readme + ) + + +@pytest.mark.parametrize(("archive", "statistic"), [("docs/performance-runs", "median"), ("docs/performance-history", "mean")]) +def test_readme_uses_original_reference_phases_after_cleanup( + complete_run: tuple[Path, Evidence], monkeypatch: pytest.MonkeyPatch, archive: str, statistic: str +) -> None: root, evidence = complete_run config = root / policy.REPORT_CONFIG config.write_text(config.read_text(encoding="utf-8").replace("docs/performance-runs", archive), encoding="utf-8") stem = root / "target/bench-reports/performance" workflow.save_scratch(root, stem, evidence) workflow.promote(root, stem) - shutil.copyfile(ROOT / "README.md", root / "README.md") + prepare_readme_recipe(root, statistic) + monkeypatch.setenv("UV_PROJECT_ENVIRONMENT", sys.prefix) + # Execute the actual recipe and installed renderer, without measurements. + result = run_command("just", ["--no-deps", "performance-readme", "lu_solve", statistic, "new", "true"], cwd=root, check=False) + assert result.returncode == 0, result.stdout + result.stderr monkeypatch.setattr(plot, "_repo_root", lambda: root) monkeypatch.chdir(root.parent) - arguments = ["--update-readme", "--log-y"] - assert plot.dimension_main(arguments) == 0 - readme = (root / "README.md").read_text(encoding="utf-8") - table = readme.split("", 1)[1].split("", 1)[0] - assert "| Dimension | la-stack | nalgebra | faer | Speedup vs nalgebra | Speedup vs faer |" in table - assert "(0.95 confidence)" not in table - assert table.count("95% confidence interval") == 1 - assert "Median time in ns; brackets show the lower and upper bounds" in table - assert "8
[7.2, 8.8]" in table - assert "1.25\N{MULTIPLICATION SIGN}" in table - assert "time reduction" not in table + arguments = ["--stat", statistic, "--update-readme", "--log-y"] + assert_readme_presentation((root / "README.md").read_text(encoding="utf-8"), statistic) assets = root / "docs/assets/bench" - csv = assets / "vs_linalg_lu_solve_median.csv" + csv = assets / f"vs_linalg_lu_solve_{statistic}.csv" import csv as csv_module # noqa: PLC0415 with csv.open(encoding="utf-8", newline="") as stream: @@ -321,13 +353,14 @@ def test_readme_uses_original_reference_phases_after_cleanup(complete_run: tuple assert {float(row["coordinate"]) for row in rows} == {2, 3, 4, 5, 8, 16, 32, 64} assert {row["unit"] for row in rows} == {"ns"} dimension_two = [row for row in rows if float(row["coordinate"]) == 2] - assert [(float(row["point"]), float(row["lower"]), float(row["upper"])) for row in dimension_two] == [(8.0, 7.2, 8.8), (10.0, 9.0, 11.0), (10.0, 9.0, 11.0)] + expected = [(12.0, 10.8, 13.2), (20.0, 18.0, 22.0), (32.0, 28.8, 35.2)] if statistic == "mean" else [(8.0, 7.2, 8.8), (10.0, 9.0, 11.0), (16.0, 14.4, 17.6)] + assert [(float(row["point"]), float(row["lower"]), float(row["upper"])) for row in dimension_two] == expected provenance = json.loads(csv.with_suffix(".provenance.json").read_bytes()) assert provenance["series"]["nalgebra"] == "baseline" assert provenance["run_id"] == run_identity(evidence) assert b" None: +def test_legacy_bytes_hashes_are_unchanged() -> None: directory = ROOT / "docs/performance/v0.4.6-vs-v0.4.5/39719a1eb599d294dbbc1e46dc6fb0de2028ad2e59aab5f94fb48c7bf3d7f841" hashes = { "performance.csv": "b263bb34bcfd43b8fc8109110db9f2e908516aef6c01ce16f70f5ac18dc7ef2d", @@ -563,19 +596,6 @@ def test_legacy_bytes_hashes_and_report_are_unchanged() -> None: } assert {name: hashlib.sha256((directory / name).read_bytes()).hexdigest() for name in hashes} == hashes - retained = convert_csv( - (directory / "performance.csv").read_bytes(), - (directory / "performance.provenance.json").read_bytes(), - (ROOT / "tooling/performance-legacy.toml").read_bytes(), - ) - comparison = parse_comparison(retained.payload) - assert len(comparison.comparisons) == 190 - assert len(comparison.missing_baseline) == 35 - assert not comparison.missing_current - assert "rational_input_d8/solve_big_rational_gaussian" in comparison.missing_baseline - assert dict(retained.sources)["baseline"].source_sha256 is None - assert dict(dict(retained.sources)["baseline"].context)["legacy.payload-sha256"] == hashes["performance.csv"] - def test_native_command_policy_and_compatibility_adapter() -> None: justfile = Path("tooling/performance.just") @@ -680,7 +700,7 @@ def test_native_inventory_and_phase_commands_use_the_measured_checkout(tmp_path: assert "test --locked --workspace --features bench,exact --test vs_linalg_inputs --test exact_bench_config" in lines[4] -def test_pre_just_run_remains_readable_without_its_python_driver(tmp_path: Path) -> None: +def test_retired_python_phase_format_is_rejected(tmp_path: Path) -> None: _, evidence = measured(tmp_path) run = policy.validate_scientific_run(evidence) cargo = { @@ -707,10 +727,8 @@ def test_pre_just_run_remains_readable_without_its_python_driver(tmp_path: Path) sources=tuple(sources), payload=serialize_run(replace(run, compatible=tuple(field for field in run.compatible if field != "context.tool.just"))), ) - policy.validate_scientific_run(legacy) - bad = replace(sources[0][1], context=tuple((key, "[]" if key == "baseline-cargo-commands" else value) for key, value in sources[0][1].context)) - with pytest.raises(ValueError, match="consumer policy"): - policy.validate_scientific_run(replace(legacy, sources=(("baseline", bad), sources[1]))) + with pytest.raises(ValueError, match="required host, toolchain, or dependency compatibility"): + policy.validate_scientific_run(legacy) def test_local_explicit_labels_and_release_eligibility(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: @@ -838,15 +856,6 @@ def test_cli_help_and_missing_checkout_diagnostics(tmp_path: Path, monkeypatch: assert "Traceback" not in output.err -def test_current_gate_failure_prevents_both_timing_phases(tmp_path: Path) -> None: - baseline, current, plan = prepared(tmp_path) - plan = replace(plan, current=replace(plan.current, environment=(("FIXTURE_FAULT", "gate"),))) - with pytest.raises(ValueError, match="current preflight gate failed"): - measure_prepared_pair(baseline, current, plan, PAIR, working_tree=True) - assert not (baseline / "target/criterion").exists() - assert not (current / "target/criterion").exists() - - def test_reference_phase_and_same_version_policy(tmp_path: Path) -> None: _, evidence = measured(tmp_path) run = policy.validate_scientific_run(evidence) diff --git a/scripts/tests/test_release_baseline.py b/scripts/tests/test_release_baseline.py index b549cd3..fd5d605 100644 --- a/scripts/tests/test_release_baseline.py +++ b/scripts/tests/test_release_baseline.py @@ -9,6 +9,7 @@ import sys import tarfile import textwrap +import tomllib from pathlib import Path import pytest @@ -18,6 +19,7 @@ from performance_runs import required_report_ids REPO_ROOT = Path(__file__).resolve().parents[2] +TOOLING_REQUIREMENT = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))["dependency-groups"]["tooling"][0] WORKFLOW = REPO_ROOT / ".github/workflows/release-benchmarks.yml" PREPARATION = REPO_ROOT / ".github/actions/prepare-release-benchmarks/action.yml" IDS = {"vs_linalg": ["d2/la_stack_dot", "diagnostic/peer"], "exact": ["exact_d2/det_exact"]} @@ -188,7 +190,7 @@ def test_workflow_requires_preflight_and_isolates_publication_permissions() -> N assert '--expected-title "$RELEASE_TAG"' in preflight assert '--target "$RUNNER_TEMP/release-target.json"' in publisher assert "--publish" not in publisher - assert (preflight + publisher).count("--from research-repo-tools==0.1.9") == 2 + assert (preflight + publisher).count(f"--from {TOOLING_REQUIREMENT}") == 2 assert "uses: ./" not in preflight + publisher assert "--clobber" not in workflow assert "needs: [validate-release, release-baseline]" in publisher @@ -337,7 +339,8 @@ def test_installed_cli_binds_trusted_target_and_leaves_draft_for_review(tmp_path # Substitute only uvx's environment acquisition; retain its exact package # arguments and execute the already locked package in a real child process. runner = ( - 'uvx() { [[ "$1 $2 $3 $4 $5" == "--python 3.15 --from research-repo-tools==0.1.9 research-repo-tools" ]] || return 90; ' + 'uvx() { [[ "$1 $2 $3 $4 $5" == "--python 3.15 --from ' + f'{TOOLING_REQUIREMENT} research-repo-tools" ]] || return 90; ' f'shift 5; {shlex.quote(sys.executable)} -m research_repo_tools "$@"; }}\n' ) result = run_shell(runner + shell_step("Validate draft release target"), fixture, env) diff --git a/scripts/tests/test_release_publishing.py b/scripts/tests/test_release_publishing.py index 26836ac..fdd4ee3 100644 --- a/scripts/tests/test_release_publishing.py +++ b/scripts/tests/test_release_publishing.py @@ -25,17 +25,11 @@ CHECKS = ("build (ubuntu-latest)", "build (macos-latest)", "build (windows-latest)", "Clippy SARIF Analysis", "Repository Rule SARIF Analysis") -def test_publishing_policy_uses_the_published_pin_and_native_scientific_gates() -> None: +def test_publishing_policy_requires_native_scientific_gates_and_the_criterion_asset() -> None: assert POLICY.publishing is not None assert (POLICY.publishing.registry, POLICY.publishing.package, POLICY.publishing.repository) == ("crates-io", "la-stack", "acgetchell/la-stack") assert tuple((check.name, check.app_id) for check in POLICY.publishing.required_checks) == tuple((name, 15368) for name in CHECKS) assert POLICY.publishing.required_assets == ("la-stack-v{version}-criterion-baseline.tar.gz",) - manifest = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) - assert manifest["dependency-groups"]["tooling"] == ["research-repo-tools==0.1.9"] - lock = tomllib.loads((REPO_ROOT / "uv.lock").read_text(encoding="utf-8")) - package = next(item for item in lock["package"] if item["name"] == "research-repo-tools") - assert package["version"] == "0.1.9" - assert package["source"] == {"registry": "https://pypi.org/simple"} @pytest.fixture @@ -84,7 +78,7 @@ def git(args: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: assets=((f"la-stack-{TAG}-criterion-baseline.tar.gz", 7, 100, "digest"),), ) monkeypatch.setattr(release_publishing, "lookup_release", lambda *_args: release) - return {"event": event, "path": event_file, "checks": checks} + return {"checks": checks} def test_installed_gate_accepts_actual_metadata_and_required_native_results(release_event: dict[str, Any]) -> None: @@ -92,35 +86,11 @@ def test_installed_gate_accepts_actual_metadata_and_required_native_results(rele @pytest.mark.parametrize("name", CHECKS) -@pytest.mark.parametrize("state", ["missing", "failure", "pending"]) -def test_every_native_gate_blocks_missing_or_unsuccessful_evidence(release_event: dict[str, Any], name: str, state: str) -> None: +def test_each_configured_native_check_is_required(release_event: dict[str, Any], capsys: pytest.CaptureFixture[str], name: str) -> None: check = next(item for item in release_event["checks"] if item["name"] == name) - if state == "missing": - check["name"] = "unrelated check" - else: - check.update(status="in_progress" if state == "pending" else "completed", conclusion=None if state == "pending" else "failure") + check["name"] = "unrelated check" assert main(["--root", str(REPO_ROOT), "release", "gate", TAG]) == 1 - - -@pytest.mark.parametrize(("field", "value"), [("draft", True), ("prerelease", True), ("tag_name", "v99.0.0")]) -def test_installed_gate_rejects_mismatched_release_events(release_event: dict[str, Any], field: str, value: object) -> None: - release_event["event"]["release"][field] = value - release_event["path"].write_text(json.dumps(release_event["event"]), encoding="utf-8") - assert main(["--root", str(REPO_ROOT), "release", "gate", TAG]) == 1 - - -@pytest.mark.parametrize("mismatch", ["repository", "commit", "version", "event"]) -def test_consumer_gate_rejects_wrong_identity(release_event: dict[str, Any], monkeypatch: pytest.MonkeyPatch, mismatch: str) -> None: - tag = TAG - if mismatch == "repository": - monkeypatch.setenv("GITHUB_REPOSITORY", "other/repository") - elif mismatch == "commit": - monkeypatch.setattr(release_publishing, "_remote_commit", lambda *_args: "b" * 40) - elif mismatch == "version": - tag = "v99.0.0" - else: - monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") - assert main(["--root", str(REPO_ROOT), "release", "gate", tag]) == 1 + assert f"required check {name!r} lacks one unambiguous successful result on {SHA}" in capsys.readouterr().err @pytest.mark.parametrize("immutable", [True, False, None]) diff --git a/scripts/tests/test_toolchain_integration.py b/scripts/tests/test_toolchain_integration.py index cb08f4a..50931d2 100644 --- a/scripts/tests/test_toolchain_integration.py +++ b/scripts/tests/test_toolchain_integration.py @@ -7,6 +7,7 @@ import sys import tomllib import zipfile +from importlib.metadata import version from pathlib import Path from typing import TYPE_CHECKING, cast @@ -44,6 +45,24 @@ } +def test_published_tooling_package_matches_manifest_lock_and_installation() -> None: + """Keep the exact published-package contract in one consumer test.""" + expected_version = "0.2.0" + manifest = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + assert manifest["project"]["dependencies"] == [] + assert manifest["tool"]["uv"]["package"] is False + assert "build-system" not in manifest + assert manifest["dependency-groups"]["tooling"] == [f"research-repo-tools=={expected_version}"] + assert {"include-group": "tooling"} in manifest["dependency-groups"]["dev"] + assert version("research-repo-tools") == expected_version + lock = tomllib.loads((REPO_ROOT / "uv.lock").read_text(encoding="utf-8")) + package = next(item for item in lock["package"] if item["name"] == "research-repo-tools") + assert package["version"] == expected_version + assert package["source"] == {"registry": "https://pypi.org/simple"} + assert package["wheels"][0]["hash"] == "sha256:73c4052993ca86fe4b1dec972b2b3bc9699493064c4acfdbe8969d89e453c602" + assert package["sdist"]["hash"] == "sha256:6a173267d09c8495ecac909ef4201a3e9c71ed453e3295a05d8f1a21585096a6" + + @pytest.fixture def consumer(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: """Use the actual recipes and declarations; record updater process boundaries.""" @@ -167,7 +186,8 @@ def test_python_update_preserves_shared_pin_and_tools_with_explicit_dev_sync(tmp run_command("uv", ["lock", "--upgrade"], cwd=root) lock = tomllib.loads((root / "uv.lock").read_text(encoding="utf-8")) packages = {package["name"]: package["version"] for package in lock["package"]} - assert packages["research-repo-tools"] == "0.1.9" + shared_name, shared_version = groups["tooling"][0].split("==") + assert packages[shared_name] == shared_version assert packages["ruff"] == "99.0.0" run_command("uv", ["sync", "--locked", "--group", "dev"], cwd=root) installed = run_command("uv", ["run", "--locked", "--no-sync", "python", "-c", "from importlib.metadata import version; print(version('ruff'))"], cwd=root) diff --git a/tooling/performance-legacy.toml b/tooling/performance-legacy.toml deleted file mode 100644 index 5768242..0000000 --- a/tooling/performance-legacy.toml +++ /dev/null @@ -1,80 +0,0 @@ -# Read the retained performance.csv format; never rewrite originals or complete runs. -schema = 1 -schema-field = "schema_version" -schemas = [ 1 ] -hash-field = "csv.sha256" -benchmark = "benchmark_id" -statistic = "median" -unit = "ns" -header = [ - "schema_version", - "suite", - "scope", - "benchmark_id", - "group", - "benchmark", - "baseline_benchmark", - "coverage_status", - "coverage_note", - "baseline_median_ns", - "baseline_ci_lower_ns", - "baseline_ci_upper_ns", - "current_median_ns", - "current_ci_lower_ns", - "current_ci_upper_ns", - "baseline_nalgebra_median_ns", - "baseline_nalgebra_ci_lower_ns", - "baseline_nalgebra_ci_upper_ns", - "baseline_faer_median_ns", - "baseline_faer_ci_lower_ns", - "baseline_faer_ci_upper_ns", -] -equal = [ - [ "release.baseline", "benchmark_provenance.baseline" ], - [ "release.current", "benchmark_provenance.current" ], - [ "benchmark_provenance.measurement.baseline_commit", "benchmark_provenance.validation.baseline_commit" ], - [ "benchmark_provenance.measurement.current_commit", "benchmark_provenance.validation.current_commit" ], - [ "benchmark_provenance.measurement.current_commit", "benchmark_provenance.publication.commit" ], - [ "benchmark_provenance.measurement.harness_sha256", "benchmark_provenance.publication.harness_sha256" ], - [ "benchmark_provenance.measurement.cargo_lock_sha256", "benchmark_provenance.publication.cargo_lock_sha256" ], -] - -[constants] -schema_version = "1" -scope = "release-signal" - -[assertions] -"benchmark_provenance.schema" = 2 -"benchmark_provenance.mode" = "shared-current-harness" -"benchmark_provenance.criterion.statistic" = "median" -"benchmark_provenance.criterion.scope" = "release-signal" -"benchmark_provenance.validation.baseline_revision" = "passed" -"benchmark_provenance.validation.current_revision" = "passed" -"benchmark_provenance.validation.harness" = "shared-current" -"benchmark_provenance.publication.correctness_gate" = "passed" - -[baseline] -point = "baseline_median_ns" -lower = "baseline_ci_lower_ns" -upper = "baseline_ci_upper_ns" - -[current] -point = "current_median_ns" -lower = "current_ci_lower_ns" -upper = "current_ci_upper_ns" - -[coverage] -column = "coverage_status" -common = "comparable" -added = "current-only" -missing = "baseline-only" - -[sources.baseline] -revision = "benchmark_provenance.measurement.baseline_commit" -release = "release.baseline" -record = "benchmark_provenance" - -[sources.current] -revision = "benchmark_provenance.measurement.current_commit" -release = "release.current" -record = "benchmark_provenance" diff --git a/uv.lock b/uv.lock index 8a13c5e..4525530 100644 --- a/uv.lock +++ b/uv.lock @@ -52,14 +52,14 @@ tooling = [ dev = [ { name = "actionlint-py", specifier = "==1.7.12.25" }, { name = "pytest", specifier = "==9.1.1" }, - { name = "research-repo-tools", specifier = "==0.1.9" }, + { name = "research-repo-tools", specifier = "==0.2.0" }, { name = "ruff", specifier = "==0.17.1" }, { name = "shellcheck-py", specifier = "==0.11.0.1" }, { name = "shfmt-py", specifier = "==4.2.0" }, { name = "ty", specifier = "==0.0.89" }, { name = "yamllint", specifier = "==1.38.0" }, ] -tooling = [{ name = "research-repo-tools", specifier = "==0.1.9" }] +tooling = [{ name = "research-repo-tools", specifier = "==0.2.0" }] [[package]] name = "packaging" @@ -121,16 +121,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd77 [[package]] name = "research-repo-tools" -version = "0.1.9" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pyyaml" }, { name = "rust-just" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/70/331d720e6840af08084a81bd22f18a05b79bdd8162703dafd5563a6eb9ac/research_repo_tools-0.1.9.tar.gz", hash = "sha256:7bfb16d7a38e8b3cecd30ce526cf808b377b4480ebd512740198a0361c3d027b", size = 837390, upload-time = "2026-10-14T06:20:36.39Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/b0/fdb4c8428e58a4528d34249be50221b240e598f24679e637dedbaeed0a77/research_repo_tools-0.2.0.tar.gz", hash = "sha256:6a173267d09c8495ecac909ef4201a3e9c71ed453e3295a05d8f1a21585096a6", size = 855482, upload-time = "2026-10-19T23:35:46.864Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/c6/af5e359be24f351f8b9603cc565807eebf27e9d06908877736e13eed7fb5/research_repo_tools-0.1.9-py3-none-any.whl", hash = "sha256:c4805004e9a857c072e7b735b033dd13ad256fb283c6e1a4adca0aa59cb67540", size = 369495, upload-time = "2026-10-14T06:20:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/59/24/46ac38772a7925460f3973162ca438093115a3e2750de8bdf4cebe15a35d/research_repo_tools-0.2.0-py3-none-any.whl", hash = "sha256:73c4052993ca86fe4b1dec972b2b3bc9699493064c4acfdbe8969d89e453c602", size = 372885, upload-time = "2026-10-19T23:35:45.405Z" }, ] [[package]] @@ -160,26 +160,26 @@ wheels = [ [[package]] name = "rust-just" -version = "1.58.0" +version = "1.59.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/d5/794e00d644caa238e931d4991d69951401492de55aa19334fdac72322ce2/rust_just-1.58.0.tar.gz", hash = "sha256:3226a7f201bf9a7766e676d32c5d8c5ae4fe0e3642628e0a9d4b62394dd0cefe", size = 2006103, upload-time = "2026-08-04T02:59:21.076Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/88/83783d99d727bd319a6ce369add39513b4f6382a64a49753555adfb41e78/rust_just-1.59.0.tar.gz", hash = "sha256:0859a87e37e249594ecb56ce740dd7e97d2c2d2ecea45ca0714555b7367512c2", size = 2010105, upload-time = "2026-10-16T03:59:56.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/12/66b02e33ada6fe66f77091f298de83ae8221ff0ac04472a16adea742688a/rust_just-1.58.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cf620ee15c9f12246ba628000d96f338f395d03c770dd55826ceed26881f9843", size = 2289171, upload-time = "2026-08-04T02:58:57.345Z" }, - { url = "https://files.pythonhosted.org/packages/46/2f/d223d8d3f6e686365d7e0377c7df276dbb0afa407ed5749c654920906152/rust_just-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:95029add8ceab772ce9e3542fad9543d13d142513c7898987fd7ad1317090101", size = 2129203, upload-time = "2026-08-04T02:58:59.075Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3c/4e548af5d2e23d4f5946e226f8ebf7daac1ee1803f9739d0a4193d63dfb0/rust_just-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1483226b767db4a0ea523eedd165667663115514c92a50577c9ab84aba76352", size = 2217447, upload-time = "2026-08-04T02:59:00.795Z" }, - { url = "https://files.pythonhosted.org/packages/4a/f3/8fbbdc706a52c875a46dd4c33d8d0220b76a712ed1aff68cc47590c1eabf/rust_just-1.58.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b7536e87b0fb4e9f0856488a296977dabaf2f3b6761bf084ec70ed6c8107255", size = 2188508, upload-time = "2026-08-04T02:59:02.27Z" }, - { url = "https://files.pythonhosted.org/packages/f8/df/218a4b42a5bc37d575cdf7aef2cdaea1d64679aa542361ca157ab15e3e65/rust_just-1.58.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77df7f183f54a3be94e331b98cbd27f299368ad2a8b208f3ac49af76ea0954b9", size = 2390783, upload-time = "2026-08-04T02:59:03.918Z" }, - { url = "https://files.pythonhosted.org/packages/86/05/001c035ba15b19a81039643506e62e5b82a80d4461617ec8dcac59c7dd7e/rust_just-1.58.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54521c790615033f92e2336b73f04017fdb3e8f7e57143011b2cb7b17ea74a36", size = 2504440, upload-time = "2026-08-04T02:59:05.175Z" }, - { url = "https://files.pythonhosted.org/packages/26/90/187046be529d840eec91ac51ecfd0fb8fc911f1771e0e51d85c3b03cd248/rust_just-1.58.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83dfc3d8b4802d7e21e8eefb102df049f9b171b998617735579d717bf01d098e", size = 2462963, upload-time = "2026-08-04T02:59:06.634Z" }, - { url = "https://files.pythonhosted.org/packages/31/f1/ddd312fe9e792bf3dde3bf7669e77a1ca069cebba864ee2a625b757207c3/rust_just-1.58.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4e2cc32ac8f3ec540129b09b818403cbbca44d84ebda4bb9e4b045dbd48a47", size = 2381777, upload-time = "2026-08-04T02:59:07.852Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7d/98fa3f4af78ea2339109d5fc70751bae918ad8438b3205fd9f7d532e8021/rust_just-1.58.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9085e86d98d15272e00721b95d125b63f28de0adc842127271ef3c1223bf150a", size = 2238418, upload-time = "2026-08-04T02:59:09.098Z" }, - { url = "https://files.pythonhosted.org/packages/0a/64/4059516b02862be59d37f389e64a77c6252effcfeabeb932a6793bc2ed81/rust_just-1.58.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2c6a9facc5aab021d1d54cc03fc406ab49ab7cbe802f233ba99db6251bfdb3b0", size = 2219864, upload-time = "2026-08-04T02:59:10.387Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8d/6c79364ce1973b405d90127f552fd21a372a1322a62d03af9f4fcc2199dc/rust_just-1.58.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:73f12c197573a945b6dc17e77d7c2bd3c1b297f9ddba9b2d64f19b79d9c5568c", size = 2210179, upload-time = "2026-08-04T02:59:12.123Z" }, - { url = "https://files.pythonhosted.org/packages/69/b4/287008f012ec8fd6022da97e08abc147e195577c23ca4e919ceac3609ad9/rust_just-1.58.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d4b5d2bf0f05b87d3bfdcf698583f4f64047c6b4f1f49bc002b57d21db3ed980", size = 2352630, upload-time = "2026-08-04T02:59:13.688Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1f/cb3c22d56649417792ed6c1e7b7d0835cc3f440a2f0a8ea332a2dbc9d26e/rust_just-1.58.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:4f95cccd7f85cb55da30d362d748747e89dfd24869973310168214bc7331cbac", size = 2413427, upload-time = "2026-08-04T02:59:15.204Z" }, - { url = "https://files.pythonhosted.org/packages/50/c8/1a3880ee39ee25dc2d8f1e5f965a2bed402859ada434d19b131d0cd86beb/rust_just-1.58.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6c0cdfe4db533d37a3ac0fd790b3fa21f8e3d913cc218fcde84c40bc02019667", size = 2461925, upload-time = "2026-08-04T02:59:16.683Z" }, - { url = "https://files.pythonhosted.org/packages/2f/56/3cc19726dec58831872bbedfac31f57e68db1c0bedbb4102714f0a45ef66/rust_just-1.58.0-py3-none-win32.whl", hash = "sha256:0e9baefc1464ab81a337f559e25a5c0c7f3d5c993d2fcd9ba1bc2585ab4bbac2", size = 2137004, upload-time = "2026-08-04T02:59:18.417Z" }, - { url = "https://files.pythonhosted.org/packages/fa/31/234039ddfd4fc10dc16679063d10848423fee6ab13aac3f5d8ce5d1d8b27/rust_just-1.58.0-py3-none-win_amd64.whl", hash = "sha256:145cea724b6d981422ad5357ac6fec53be62b8146ac67a3921c89a0db152887d", size = 2358021, upload-time = "2026-08-04T02:59:19.817Z" }, + { url = "https://files.pythonhosted.org/packages/8c/30/42f9d300a3a5368d8fc64f31d8b3a5f47c44830a1c493d4da7ce997c846e/rust_just-1.59.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:68795a4da5ffb65ed79a79ffaa8cdb06b7db15feba82a36fb0855a3eb964eb24", size = 2303024, upload-time = "2026-10-16T03:59:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/92/9f/7f9d7ef2d8b5b9cbfc9b0414fbb376d2a39a507012c57d13caa8b2a163b3/rust_just-1.59.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:105f7d21ff3a36ffbe1c2039bcac2c63b92941a11e3531ecdff3e4b6411f87d4", size = 2141287, upload-time = "2026-10-16T03:59:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f8/3dbe1c7d9c936f9e67db3def9a51c8df23eed1602f071b0a4ade2c6fc9c3/rust_just-1.59.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a305aa91fc425c44e3bcc7d5faae7b8c1503091d8e79aae1f6801dbdca478e20", size = 2249878, upload-time = "2026-10-16T03:59:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3e/0ee5d3426835b94e026d262ea4a603a891e2d22235e3b282981efd7e4adc/rust_just-1.59.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c9777e4c16cd27c474451787acaf8737a6eae4abb1eccd576654bef16d08b80", size = 2229179, upload-time = "2026-10-16T03:59:39.061Z" }, + { url = "https://files.pythonhosted.org/packages/17/51/ef2198d1a8008defe475fe1caee4ffee6d57b56bf74f349e49913a533f20/rust_just-1.59.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c3cf79d4fe8f30a59206ed6ac51464e7c3fa81d465f64e94a47592c12b4d9cd", size = 2434152, upload-time = "2026-10-16T03:59:40.682Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d6/68075c504b4032efc3a485de6707948e3cd9408918539e16a642ac66784a/rust_just-1.59.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc56bbfd4d7a28b3bd8562fa764c32d168d928c22d9f76e3e7448111a9d06882", size = 2526940, upload-time = "2026-10-16T03:59:41.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/69f81255329c9087b8a1ac4aa497b2fbe2a3435746aba69fe8604e296e08/rust_just-1.59.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7edb3daaed793bdc10234eeb5d1df1f72afc2b9f0c531f8f5835c6c852dc394", size = 2585018, upload-time = "2026-10-16T03:59:43.383Z" }, + { url = "https://files.pythonhosted.org/packages/44/44/88bb281b264fd843e66975637ec1befc442508fc60e509800a053e5fb9fa/rust_just-1.59.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ee47f26e3aff917107602d57a08ede20df447f78dc4f9ceb08e57a883c56eb9", size = 2428420, upload-time = "2026-10-16T03:59:44.812Z" }, + { url = "https://files.pythonhosted.org/packages/83/4c/e5cd2f2a2b0d7948e68ce8652a4cd71534de5e7dcdba695aeeca7e1e6f00/rust_just-1.59.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:af7fe75d0216eb442767fd3d72eec24b2d1482a2ab23b88eadf54682cad10f79", size = 2247743, upload-time = "2026-10-16T03:59:46.202Z" }, + { url = "https://files.pythonhosted.org/packages/00/d3/ab45c699d414fca4587d0aae27cd220e6f2885a6c4fe8da072062bed66bc/rust_just-1.59.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:58b64060b424f6a6ced877b4664122f9ac454d1302d97b73b170557b6c11568d", size = 2255848, upload-time = "2026-10-16T03:59:47.807Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/a632053b17a86077fd8900061291af35f1fc6ce5ba0b38c80ac529584e10/rust_just-1.59.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:11bff0fb4cff9cf728514af8802ad9d0d5163793226ea0196e38e7eeb1307edc", size = 2248429, upload-time = "2026-10-16T03:59:49.107Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/8dbd42136b95b03cdbcb93832a76fc07e1abbc5996d2ac38124148331325/rust_just-1.59.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b5a4633aa653afbecfb1fe17c857b8d3d5b7fed7ed058d353dd1c6d92c73ae56", size = 2405106, upload-time = "2026-10-16T03:59:50.318Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b6/550b1c12ed1149259722b79270e6549773ca06d6ab7148eaa4b345682642/rust_just-1.59.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:9e97d6f4b0afb02c4baf9ee4fe433c426709c86da11695cf2a6ef7f4faa508d4", size = 2421107, upload-time = "2026-10-16T03:59:51.465Z" }, + { url = "https://files.pythonhosted.org/packages/77/93/f7a02dfe75e6488b1bc71a174bb89d93b7754d7e74601894eac5b3fbe043/rust_just-1.59.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:55a22dbd38fb3491d221da1e45184d09460526fae73b845eb9cdfbefcf06822b", size = 2506671, upload-time = "2026-10-16T03:59:52.706Z" }, + { url = "https://files.pythonhosted.org/packages/1e/fb/2d09890ff5fc881f7cbcbf135c4bfa5a73014d718ab4b8611159abc9ff69/rust_just-1.59.0-py3-none-win32.whl", hash = "sha256:509f9e38f7080fb6c971e5ba4ca0572f9075e713b0e3d5105741f782d10fa17f", size = 2171808, upload-time = "2026-10-16T03:59:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/e0/29/81112f1180a156306bf36ab0ffd3e6ec35259ed40cefd8774c24560ce857/rust_just-1.59.0-py3-none-win_amd64.whl", hash = "sha256:75bde89d36bb0c7b35fb9cecefb303f154dc320d4b5b8c832c5ad0bf1904077b", size = 2413475, upload-time = "2026-10-16T03:59:55.345Z" }, ] [[package]]