diff --git a/CHANGELOG.md b/CHANGELOG.md index e614aed..bc010c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,34 @@ change measured timings are marked `bench:`. ## [Unreleased] +## [2.2.1] - 2026-09-10 + +### Fixed + +- **Every relative link in the README was a 404 on all eleven crates.io + pages.** All eleven crates set `readme = "../../README.md"`, so this file + ships as each one's readme — and crates.io rewrites a *relative* link + against the crate's directory in the repository rather than the repository + root. `docs/SAFETY.md` was served as + `…/blob/HEAD/crates/launchbound-space/docs/SAFETY.md`, which does not + exist, and the same eight links were dead on every crate: `docs/SAFETY.md`, + `docs/ARCHITECTURE.md`, `docs/LIMITATIONS.md` (twice, once with an + anchor), the Action directory, `rust-toolchain.toml` and both licences. + + Nothing here could see it. The same file renders correctly on GitHub, + where it really is at the root, so the only broken surface was the one + every reader arriving from crates.io meets first — and `docs/SAFETY.md`, + the document the README calls "the product", was among the dead. + + The links are absolute now. `scripts/check-readme-links.sh` (in `just ci`) + refuses a relative one, checks every target still exists in the checkout, + and checks `blob` versus `tree` — GitHub *redirects* a `blob` URL for a + directory rather than failing, so a checker that only reads status codes + calls it fine. + + Reaches future releases only: crates.io renders each version's readme as + published, so the 2.2.0 pages stay broken. + ## [2.2.0] - 2026-09-09 The cuda-oxide pin was 133 commits and one toolchain behind, and the watch diff --git a/Cargo.lock b/Cargo.lock index 926c944..13a0a10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" [[package]] name = "launchbound-bench" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "launchbound-space", @@ -1135,7 +1135,7 @@ dependencies = [ [[package]] name = "launchbound-build" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-space", "serde", @@ -1146,7 +1146,7 @@ dependencies = [ [[package]] name = "launchbound-cli" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "clap", @@ -1164,7 +1164,7 @@ dependencies = [ [[package]] name = "launchbound-metal" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-bench", "launchbound-space", @@ -1175,7 +1175,7 @@ dependencies = [ [[package]] name = "launchbound-model" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-space", "serde", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "launchbound-prune" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-build", "launchbound-space", @@ -1198,7 +1198,7 @@ dependencies = [ [[package]] name = "launchbound-report" -version = "2.2.0" +version = "2.2.1" dependencies = [ "insta", "jsonschema", @@ -1210,7 +1210,7 @@ dependencies = [ [[package]] name = "launchbound-runner" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-bench", "launchbound-search", @@ -1218,7 +1218,7 @@ dependencies = [ [[package]] name = "launchbound-search" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-bench", "proptest", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "launchbound-space" -version = "2.2.0" +version = "2.2.1" dependencies = [ "proptest", "serde", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "launchbound-tui" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index b42932e..ee3ffdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ exclude = ["corpus"] [workspace.package] -version = "2.2.0" +version = "2.2.1" edition = "2024" # MSRV for crates that do not require the pinned nightly (CONTRIBUTING.md); # set by ratatui 0.30. The analysis and compile paths require @@ -38,14 +38,14 @@ categories = ["development-tools", "development-tools::profiling"] # published crate is compatible with a release it was never built against -- # harmless for a path build, wrong as a record. They sat at 2.0.0 through the # whole 2.1.0 line; `just versions` (and a test) now refuse the drift. -launchbound-space = { path = "crates/launchbound-space", version = "2.2.0" } -launchbound-prune = { path = "crates/launchbound-prune", version = "2.2.0" } -launchbound-build = { path = "crates/launchbound-build", version = "2.2.0" } -launchbound-bench = { path = "crates/launchbound-bench", version = "2.2.0" } -launchbound-report = { path = "crates/launchbound-report", version = "2.2.0" } -launchbound-search = { path = "crates/launchbound-search", version = "2.2.0" } -launchbound-model = { path = "crates/launchbound-model", version = "2.2.0" } -launchbound-metal = { path = "crates/launchbound-metal", version = "2.2.0" } +launchbound-space = { path = "crates/launchbound-space", version = "2.2.1" } +launchbound-prune = { path = "crates/launchbound-prune", version = "2.2.1" } +launchbound-build = { path = "crates/launchbound-build", version = "2.2.1" } +launchbound-bench = { path = "crates/launchbound-bench", version = "2.2.1" } +launchbound-report = { path = "crates/launchbound-report", version = "2.2.1" } +launchbound-search = { path = "crates/launchbound-search", version = "2.2.1" } +launchbound-model = { path = "crates/launchbound-model", version = "2.2.1" } +launchbound-metal = { path = "crates/launchbound-metal", version = "2.2.1" } anyhow = "1" clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } diff --git a/README.md b/README.md index de43080..aff0c94 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # launchbound -[![license](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](LICENSE-MIT) +[![license](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](https://github.com/vyncint/launchbound/blob/main/LICENSE-MIT) [![crates.io](https://img.shields.io/crates/v/launchbound-cli?label=crates.io&color=orange)](https://crates.io/crates/launchbound-cli) -[![toolchain](https://img.shields.io/badge/toolchain-nightly--2026--08--28-orange)](rust-toolchain.toml) -[![GPU](https://img.shields.io/badge/GPU-not%20required-brightgreen)](docs/ARCHITECTURE.md) +[![toolchain](https://img.shields.io/badge/toolchain-nightly--2026--08--28-orange)](https://github.com/vyncint/launchbound/blob/main/rust-toolchain.toml) +[![GPU](https://img.shields.io/badge/GPU-not%20required-brightgreen)](https://github.com/vyncint/launchbound/blob/main/docs/ARCHITECTURE.md) [![CI](https://img.shields.io/github/actions/workflow/status/vyncint/launchbound/ci.yml?label=CI)](https://github.com/vyncint/launchbound/actions/workflows/ci.yml) A convergence-safe autotuner for Rust GPU kernels. @@ -15,7 +15,7 @@ NVIDIA it measures on real silicon. On Apple Silicon it measures on Metal. With no GPU at all it falls back to an analytical model — and says so. > Every design gate shipped with a measured result; the honest edges live -> in [docs/LIMITATIONS.md](docs/LIMITATIONS.md). Read that before trusting +> in [docs/LIMITATIONS.md](https://github.com/vyncint/launchbound/blob/main/docs/LIMITATIONS.md). Read that before trusting > a result. ## Why this tool exists @@ -80,7 +80,7 @@ either. The one `.maxntid` relationship the corpus does enforce (`block_x <= lb_max` in `stencil-1d`) holds because the kernel author wrote it as a constraint, not because launchbound knows what `.maxntid` means. A reader arriving from cuda-oxide's docs will assume otherwise; see -[docs/LIMITATIONS.md](docs/LIMITATIONS.md#launch_bounds-and-registers-are-not-validated). +[docs/LIMITATIONS.md](https://github.com/vyncint/launchbound/blob/main/docs/LIMITATIONS.md#launch_bounds-and-registers-are-not-validated). ## The pipeline @@ -113,7 +113,7 @@ For each candidate configuration at its own launch shape, `launchbound` runs A `reconverge` tool error (exit 2) is a hard stop for that candidate, never a pass by omission. The full rule, including why `--strict` is mandatory, lives -in [docs/SAFETY.md](docs/SAFETY.md) — that document is the product. A rejected +in [docs/SAFETY.md](https://github.com/vyncint/launchbound/blob/main/docs/SAFETY.md) — that document is the product. A rejected configuration is a feature, not a failure: the headline output includes every configuration that was *faster and refused*, with the rule ID and source span. @@ -166,7 +166,7 @@ shape make a barrier or a collective non-convergent, and does the static shared memory fit — and its verdict line says so. It has **no view of instruction availability**: whether the device code can be lowered for that part at all is `needs_cc` in `kernel.toml`, the author's claim, taken on -trust ([docs/LIMITATIONS.md](docs/LIMITATIONS.md)). +trust ([docs/LIMITATIONS.md](https://github.com/vyncint/launchbound/blob/main/docs/LIMITATIONS.md)). `apply` re-verifies what it emits through the gate before printing anything. `--no-verify` emits without it — for a machine that has the run directory @@ -189,7 +189,7 @@ missing reason is a usage error, not a warning. It is never the default, and `prune` needing no GPU is the reason it exists as its own verb: it is the only part of the pipeline a developer on a laptop can run, and it is the part that -finds the bugs. It also ships as a [GitHub Action](action/) — +finds the bugs. It also ships as a [GitHub Action](https://github.com/vyncint/launchbound/tree/main/action) — `uses: vyncint/launchbound/action@v2` puts the gate in your CI. ## Compared to the neighbours @@ -214,7 +214,7 @@ That report is re-runnable: `launchbound report --rejected`. ## Limitations -The honest list lives in [docs/LIMITATIONS.md](docs/LIMITATIONS.md) — read +The honest list lives in [docs/LIMITATIONS.md](https://github.com/vyncint/launchbound/blob/main/docs/LIMITATIONS.md) — read it before trusting a result. Highlights: a clean gate is **not a proof of correctness** (reconverge's documented limits are inherited wholesale, and the launch-shape classifier recognizes the measured `warp_id()` family); @@ -228,7 +228,7 @@ or not at all. ## License -Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at +Dual-licensed under [MIT](https://github.com/vyncint/launchbound/blob/main/LICENSE-MIT) or [Apache-2.0](https://github.com/vyncint/launchbound/blob/main/LICENSE-APACHE), at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional diff --git a/action/action.yml b/action/action.yml index e829e9f..9893291 100644 --- a/action/action.yml +++ b/action/action.yml @@ -40,7 +40,7 @@ inputs: default: tool-error version: description: >- - Which launchbound release runs the gate, e.g. "2.2.0". The default, + Which launchbound release runs the gate, e.g. "2.2.1". The default, `latest`, installs the newest release on crates.io each run. Pin a number if you want the gate to change only when you say so. required: false diff --git a/justfile b/justfile index 6230c61..f14ddbd 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,7 @@ default: ci # "all required jobs green" aggregator job in ci.yml — this recipe is the # aggregator, and the `ci` job runs it verbatim on both OSes — so a new gate # becomes required by being listed here. -ci: fmt-check clippy test docs deny schemas pins versions skill +ci: fmt-check clippy test docs deny schemas pins versions links skill # Cargo errors on a memberless virtual workspace, so the cargo recipes no-op # until the first crate lands in S1. `grep -c` prints 1 when packages is empty. @@ -63,6 +63,14 @@ pins: versions: ./scripts/check-versions.sh +# Every link in the README is absolute, names the right GitHub path kind, +# and points at something that exists. All eleven crates ship this file as +# their readme, and crates.io rewrites a relative link against the crate's +# directory -- so all eight were 404 on all eleven published 2.2.0 pages, +# and nothing here could see it. +links: + ./scripts/check-readme-links.sh + # Golden + JSON Schema validation of report documents (S4). schemas: cargo test -p launchbound-report --test schema_and_golden diff --git a/scripts/check-readme-links.sh b/scripts/check-readme-links.sh new file mode 100755 index 0000000..829c4ce --- /dev/null +++ b/scripts/check-readme-links.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Every link in the packaged README must be absolute, must name the right +# GitHub path kind, and must point at something that exists here. +# +# All eleven crates set `readme = "../../README.md"`, so this file is shipped +# as each one's readme and crates.io renders it there. crates.io rewrites a +# *relative* link against the crate's directory in the repository, not the +# repository root -- so `docs/SAFETY.md` became +# `…/blob/HEAD/crates/launchbound-space/docs/SAFETY.md`, and all eight +# relative links were 404 on all eleven published 2.2.0 pages. Nothing here +# could see it: the same file renders correctly on GitHub, where it *is* at +# the root. +# +# So: absolute links only, and the path each names must exist. The +# existence check is offline and deterministic, which is the point -- it +# catches a renamed or deleted target, and a link checker that only asks +# GitHub would not, because GitHub answers 200 for a redirect. +# +# `blob` vs `tree` is checked for the same reason: `blob/main/action/` is a +# directory and GitHub 301s it to `tree/main/action`. It resolves, so a +# status-code checker calls it fine; it is still the wrong URL to publish. +# +# Portability: macOS ships bash 3.2 and BSD grep/sed. +set -euo pipefail + +readme="${1:-README.md}" +root=$(cd "$(dirname "$0")/.." && pwd) +repo="https://github.com/vyncint/launchbound" +status=0 + +# `[text](target)` whose target is neither absolute nor a bare fragment. +relative=$(grep -oE '\]\([^)]+\)' "$readme" \ + | sed -E 's/^\]\(//; s/\)$//' \ + | grep -vE '^(https?:|#|mailto:)' || true) +if [ -n "$relative" ]; then + echo "$relative" | while IFS= read -r link; do + echo "README LINK: relative target \"$link\" — crates.io rewrites it against" >&2 + echo " crates//, where it does not exist. Use $repo/blob/main/$link" >&2 + done + status=1 +fi + +checked=0 +bad=0 +for url in $(grep -oE "$repo/(blob|tree)/main/[^)]+" "$readme" | sort -u); do + kind=$(printf '%s' "$url" | sed -E "s|^.*/(blob\|tree)/main/.*$|\1|") + path=$(printf '%s' "$url" | sed -E "s|^.*/(blob\|tree)/main/||; s|#.*$||; s|/$||") + checked=$((checked + 1)) + if [ ! -e "$root/$path" ]; then + echo "README LINK: \"$path\" is linked but not in the repository" >&2 + bad=$((bad + 1)) + continue + fi + # A directory is `tree`, a file is `blob`. GitHub redirects the wrong one + # rather than failing, so only this says so. + if [ -d "$root/$path" ] && [ "$kind" != tree ]; then + echo "README LINK: \"$path\" is a directory; use /tree/main/ not /blob/main/" >&2 + bad=$((bad + 1)) + elif [ -f "$root/$path" ] && [ "$kind" != blob ]; then + echo "README LINK: \"$path\" is a file; use /blob/main/ not /tree/main/" >&2 + bad=$((bad + 1)) + fi +done +if [ "$bad" -gt 0 ]; then + status=1 +fi + +if [ "$status" -eq 0 ]; then + echo "readme links: $checked absolute link(s), every target present and the right kind" +fi +exit "$status"