Skip to content

Rewrite lintus in Rust, as a single binary - #3

Merged
virolea merged 9 commits into
mainfrom
claude/lintus-ruby-rust-port-e820a1
Sep 23, 2026
Merged

virolea merged 9 commits into
mainfrom
claude/lintus-ruby-rust-port-e820a1

Conversation

@virolea

@virolea virolea commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Lintus no longer needs Ruby: it is a single binary, installed from the GitHub releases, with install.sh, or with cargo install lintus. Config files, flags, output formats and exit statuses are unchanged.

What changes

  • lintus (repository root) is a port of the gem. Glob matching reproduces Ruby's File.fnmatch, rounding reproduces Ruby floats, and YAML is read with Psych's rules, so existing .lintus.yml files mean the same thing.
  • jev-api (crates/jev) is a typed Rust client for the Jev API: noul, choice and score questions. It is imported as jev; the jev name is taken on crates.io.
  • lintus auth login | status | logout saves the API key in ~/.config/lintus/credentials.json, readable by its owner only. A run takes the key from --api-key, then JEV_API_KEY, then the saved key.
  • Releases: a v* tag builds static Linux (x86_64, arm64), macOS (Intel, Apple silicon) and Windows binaries with checksums, attaches them to a GitHub release, and publishes both crates. Running the workflow by hand is a dry run.
  • The GitHub Action downloads the binary instead of setting up Ruby. The pre-commit hook builds lintus from source.
  • The Ruby implementation is removed. The gem stays at 0.2.0.

Two behaviours differ from the gem. --diff and --staged together are now an error. Skipped and failed files are listed in path order.

How it was verified

  • Conformance suite (tests/conformance, 53 tests): runs a lintus binary against temporary projects and a fake Jev API, and checks output, exit status and requests byte for byte. It passed against the Ruby gem before the port, and passes unchanged against the Rust binary.
  • Glob and rounding fixtures: about 14,000 cases generated from the Ruby implementation.
  • Real API: both implementations were run on this repository's 19 Ruby files. They checked the same files, asked the same rules and reported the same 8 offenses; probabilities differed only by model noise (at most 0.12).
  • Platforms: the tests pass on macOS and on Linux (musl). The static Linux binary reaches the API in busybox, debian:bookworm-slim and alpine images; the first two have no CA certificates, so the binary falls back to bundled roots.
  • Distribution: install.sh was tested against a local server, including a tampered archive. pre-commit try-repo builds and runs the hook on a machine without Rust.

Not verified yet

  • The Windows CI job, which this PR runs for the first time.
  • The release matrix: run Actions → Release → Run workflow before tagging v0.3.0.
  • The Action's download step, which needs a published release.
  • The first crates.io publish, which is done by hand before trusted publishing can be set up.

🤖 Generated with Claude Code

virolea and others added 9 commits September 23, 2026 21:03
A Cargo workspace with two crates, jev and lintus, still empty. The lintus
crate carries a conformance suite that runs a lintus binary against
temporary projects and a fake Jev API, then checks its output, exit status
and requests. LINTUS_BIN picks the binary, so the suite runs against the
Ruby implementation through test/support/lintus-ruby, where it passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Noul, choice and score questions, with the answers typed after the question
they answer. API error statuses map to the same kinds as the Ruby gem, and
rate limits are left to the caller to retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same command line, config file and output as the Ruby gem, as a single
binary. The conformance suite passes against both implementations.

Glob matching is a port of Ruby's File.fnmatch, and probabilities are
rounded and printed as Ruby floats; both are checked against fixtures
generated from Ruby. The config file is read with Psych's YAML 1.1 rules
(yes/no booleans, 100_000 integers, merge keys), so existing configs mean
the same thing.

New: `lintus auth login|status|logout` saves the API key in the user's
config directory, readable by its owner only. A run takes the key from
--api-key, then JEV_API_KEY, then the saved key. JEV_API_URL points the
client at another endpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Slim container images such as debian:bookworm-slim ship without CA
certificates, and every request failed there. The client still prefers the
operating system's store, so private CAs keep working.

The crate is published as jev-api, since jev is taken on crates.io; its
library is still named jev.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pre-commit installs a language: rust hook with `cargo install --path .` at
the repository root, which needs a package there rather than a bare
workspace. The hook now builds lintus from source, with a Rust toolchain of
its own if the machine has none; checked with `pre-commit try-repo`.

The conformance harness no longer relies on Unix-only paths, so the suite
can run on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Releases: a tag builds static Linux (x86_64, arm64), macOS (Intel, Apple
  silicon) and Windows binaries with checksums, attaches them to a GitHub
  release, and publishes jev-api and lintus to crates.io with trusted
  publishing. It replaces the gem release.
- install.sh installs the right binary for the machine and checks its
  checksum.
- The GitHub Action downloads the binary for the runner, the release
  matching its own tag by default, instead of setting up Ruby.
- CI runs the Rust tests on Linux, macOS and Windows, and the conformance
  suite against the Ruby implementation.
- Lintus lints its own Rust code with the Rust binary.
- README and CHANGELOG describe installing the binary and `lintus auth`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Running the Release workflow by hand builds and tests every target without
releasing, so the matrix can be checked before a tag exists.

The CHANGELOG notes the two places the Rust version behaves differently
from the gem. The pre-commit hook needs a C compiler, but no Rust: checked
in a container without one. RuboCop is clean on the new Ruby helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Rust binary passes the conformance suite that the gem passed. Against
the real API, both checked the same 19 files, asked the same rules, and
reported the same 8 offenses; the probabilities differed only by model noise
(at most 0.12) on byte-identical requests.

The gem stays published at 0.2.0. The glob fixture generator says which
commit to regenerate it from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests built the expected credentials path with a / inside, where the
binary rightly uses \ on Windows. CI now runs every test binary even when
one fails.

Lintus flagged crates/jev/src/client.rs in its own self-lint: an encoding
failure surfaced a bare serde message, and a response that broke off was
reported as the API being unreachable. Both now say what happened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@virolea
virolea merged commit ab509cb into main Sep 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant