Changelog
All notable changes to alint are documented here. The format is based on Keep a Changelog; the project adheres to Semantic Versioning.
Post-v0.9.20 polish accumulating toward the next tag. No user-facing behavior change; CLI output, docs, schema completeness, dependency bumps, and CI hygiene only. The bulk of this entry is automated drift-prevention scaffolding so the same class of “live site silently goes stale” gap can’t reopen post-launch.
Changed
Section titled “Changed”- CLI color + AutoStream parity for
list,explain,facts,suggest— matchescheck/fixcolor discipline. - Em dashes dropped from
README.md, bundled rule messages, and the synceddocs/site/tree. - Install snippets lead with
curl | bashinREADME.mdanddocs/site/getting-started/installation.md. --formatstyle normalized to space form (--format json, not--format=json) across README + cookbook; clap accepts both, this is style consistency only.- Structured query is now its own family-level H2 in
docs/rules.md(was nested under Content). 13 families per the README claim now matches the doc structure + alint.org sidebar. - Docker GitHub Actions bumped via Dependabot —
setup-qemu,setup-buildx,loginto v4;build-pushto v7.release.ymlandbench-docker.ymlnow on the same major versions. tomlcrate bumped 0.9 → 1.1 (Dependabot major). API stable onfrom_str; all tests pass.
GOVERNANCE.md+.github/FUNDING.yml(pre-launch repo polish).ci/scripts/preflight.shwrapper + gitpre-pushhook enforcing fmt / clippy / doc / pin-sync.ci/scripts/check-version-pins.sh+.alint.ymldogfood ruleinstall-snippets-match-workspace-versionfor automated version-string drift detection.- Usage examples for 7 under-documented rule kinds + an
xtask docs-coverageaudit to enforce per-kind doc presence. schemas/v1/agent-report.json— published JSON Schema for the--format agentoutput, closing the gap behind theschema_version: 1stability promise.- Benchmarks trajectory pipeline —
render-history.py --json-outemitsbenchmarks-trajectory.jsonfrom per-versionresults.jsonfiles + CHANGELOG headlines;xtask docs-exportbundles it; alint.org’s/benchmarks/page reads it at build time. Replaces a hand-edited HTML table that had drifted six releases behind.coverage_audit_benchmarks_trajectorytest guards the data flow. bench-docker.ymlconcurrency group so two rapid pushes don’t race the:edgeimage tag.
- Broken alint.org links to
docs/development/rule-authoring.md(changelog + roadmap had uppercaseRULE-AUTHORING.mdreferences; source file renamed to match the synced lowercase slug). alint initis now suggested from the “no .alint.yml found” error paths (first-time-user dead-end resolved).walker.rs::descendants_ofdoc comment now correctly describes the cycle-defense rationale (was inaccurate about symlink exclusion; the actual guarantee comes from theignorecrate’s ancestor-loop detection).render-history.pyforces UTF-8 stdout so Windows CI doesn’t trip on em dashes in the markdown render.xtask docs-exporttreatspython3as a soft dependency: emits a warning + skips trajectory generation on hosts without it (the self-hosted CI runner case).docs-bundle.ymlruns on ubuntu-latest which always has python3, so production bundles are unaffected.fix(dogfood): excludealint-e2e/fixtures/**from self-lint (test fixtures legitimately violate hygiene rules).chore(privacy): replace personal email with alint.org aliases across CONTRIBUTING / SECURITY / README contact lines.
RELEASING.mdstep 1 now points atbump-version.shand documents the two deliberate exclusions ([workspace.dependencies]intra-workspace API floor +npm/package.jsonwhich the release workflow rewrites at publish time).
- v0.9.18 / v0.9.19 / v0.9.20 macro-bench + criterion results
published under
docs/benchmarks/.
[0.9.20] — 2026-05-10 (cross-command output polish)
Section titled “[0.9.20] — 2026-05-10 (cross-command output polish)”Extends v0.9.19’s width-aware-output / --no-docs / message-wrap
treatment from alint check to every other human-renderer command
in the CLI. Surfaced while polishing the alint.org landing-page
demo: alint fix’s output had no width awareness, so the demo
cast had to bump its frame to 110 cols + accept one wrap to fit
the bundled-rule messages.
Engine
Section titled “Engine”wrap_messagepromoted to public API underalint_output::wrap_message. Renderers outside the alint-output crate (e.g.alint suggest) can now apply the same word-aware wrap with hanging-indent semantics thatalint checkuses.alint fixhonours--width. The fix renderer (write_fix_humaninhuman.rs) wraps each violation’s message with a 4-col continuation indent under the·/✓glyph. Status-suffix prose ((no fixer),(skipped: <reason>)) stays attached to the message text so it never lands on a line by itself looking orphaned. Samewrap_messagehelper as check.
alint suggesthonours--widthfor--format humanoutput. Per-proposalsummarytext wraps at 4-col indent (under the proposal glyph); per-proposalevidencetext wraps at 9-col indent (under the└─tree marker). Newwidth: Option<usize>field onsuggest::RunOptions(library API).alint explainhonours--no-docsby suppressing thepolicy_url:line. URLs remain in machine-readable formats regardless.alint explaindrops thedebug: {rule:?}dump. That line rendered each rule’s internalDebugrepr — useful for alint developers, noise for end users (24+ KB of regex automaton state for some rule kinds). Use--format jsononalint checkfor the wire-shape, or read the YAML rule definition directly.
Tooling
Section titled “Tooling”ci/scripts/audit-bundled-message-lengths.sh— informational audit that walks everycrates/alint-dsl/rulesets/v1/**/*.yml, extracts each rule’smessage:text, and reports any whose effective single-line render exceeds the budget (default 66 chars = 80-col terminal − 14-col MSG_INDENT). Walked 94 rules on the v0.9.19 corpus; 66 nominally over budget but most are intentionally multi-paragraph andwrap_messagehandles them gracefully. Pass--fail-over <N>to make the script a CI gate for messages exceeding a hard limit.
- 1 trycmd
explain.tomlsnapshot regenerated for the droppeddebug:line.
Demo refresh (alint.org)
Section titled “Demo refresh (alint.org)”- Re-record the landing-page cast with v0.9.20: shrinks the cast
frame back to 92 cols (vs 110 in v0.9.19’s 4-command cast)
because
alint fixnow honours--width=88cleanly. Same 4-command sequence (check → cat .alint.yml → fix → check), same reading beats. Cleaner visual.
[0.9.19] — 2026-05-09 (output polish)
Section titled “[0.9.19] — 2026-05-09 (output polish)”Quality-of-life patch focused on the human formatter’s behaviour
in narrow terminals, screen recordings, and CI logs — surfaced
while polishing the alint.org landing-page CLI demo (long
docs: URLs and 200+ char rule messages were wrapping
unpredictably inside the asciinema-player frame).
Engine
Section titled “Engine”- Width-aware message wrapping in the grouped human formatter.
Long violation messages now wrap at
effective_width()cols with continuation lines re-indented toMSG_INDENT(14 cols). Word-aware on whitespace; long unbreakable tokens (URLs, hashed identifiers) get their own line and are allowed to overflow rather than being broken mid-token. Embedded\nin message text honoured as paragraph breaks. Width detection unchanged (TTY → kernel-reported cols, non-TTY → DEFAULT_WIDTH = 80, both clamped [40, 120]).
--width <COLS>(global) — explicit override of the detected width. Required for reproducible captures (asciinema, screencasts) and useful for piping into fixed-width log viewers / narrow CI dashboards.--no-docs(global) — suppress per-violationdocs:URL lines in human output. URLs remain in JSON / SARIF / GitHub / markdown formats. Designed for narrow terminals + screen recordings where long URLs disrupt visual alignment.HumanOptions::Default::show_docs = true(library API). Manual Default impl preserved current behaviour for library callers. The CLI’s--no-docsflag flips it tofalse.
Bundled rulesets — verbose-message tightening
Section titled “Bundled rulesets — verbose-message tightening”Three of the longest-message bundled rules tightened to fit within ~80 cols on a single wrapped line at 14-col indent. No behavioural change; just tighter copy.
oss-baseline@v1::node-engine-or-nvmrc: 196 → 95 chars. (“Pin the Node.js version with.nvmrc,.node-version, or.tool-versionsso local and CI installs match.”)node@v1::node-no-tracked-dist: 137 → 84 chars. (“Build-output directories shouldn’t be tracked. Setlevel: offif this one is intentionally shipped.”)agent-context@v1::agent-context-recommended: 112 → 78 chars. (“Add AGENTS.md / CLAUDE.md / .cursorrules so coding agents share versioned instructions.”)
Quality bar going forward for new bundled-rule messages: aim for ≤ 80 chars on a single wrapped line. Verbose-but-useful detail goes in the policy URL or the rule’s docs page.
- 6 new
wrap_messageunit tests covering: short text, word- boundary wrap, long unbreakable tokens, embedded newlines, empty input, tiny-width clamp. - 9 trycmd
help-*.stdoutsnapshots regenerated for the 2 new global flags. - 1
cli_flag_inventorysnapshot regenerated.
[0.9.18] — 2026-05-08 (pre-launch fixes)
Section titled “[0.9.18] — 2026-05-08 (pre-launch fixes)”Findings from the v0.9.17 deep-analysis pass (30 case studies — see
docs/development/case-study-deep-analysis-log.md)
surfaced 6 bundled-ruleset bugs + 3 case-study config issues. v0.9.18
closes them before P4 launch. The only engine change is one small
extension: dir_absent rules now honour scope_filter: (required by
A1 below), allowing dir-iterating rules to use the same ancestor-
manifest gate that scopes per-file rules.
Live-tree revalidation against the cloned case-study trees confirmed the FP-class elimination end-to-end (see deep-analysis log v0.9.18 revalidation evidence section for the full per-repo table). Headline numbers:
airflow apache-2-source-has-license-header: 8,228 → 79 (-99%) rust-lang/rust rust-sources-snake-case: 1,091 → 10 (-99%) ruff python-sources-final-newline: 176 → 0 (-100%) ruff python-sources-no-trailing-whitespace: 59 → 0 (-100%) tensorflow tensorflow-bazel-files-…: 700 → 241 (-66%) bazel hygiene-no-js-build-outputs: 30 → 0 (-100%) dotnet/runtime hygiene-no-js-build-outputs: 21 → 1 (-95%) ruleset/per-repo total FP elimination: ~10,000+ violations
Engine
Section titled “Engine”dir_absenthonoursscope_filter:. Previously rejected at build time with “scope_filter is supported on per-file rules only”. Required by A1 below —hygiene-no-js-build-outputsneedsscope_filter: { has_ancestor: package.json }to scope per-JS-package. The fix removed thereject_scope_filter_on_cross_filecall fromdir_absent::buildand switched its scope construction toScope::from_spec(spec)(the canonical from-spec constructor).Scope::matchesalready worked for dir paths (Path::parent()is path-agnostic), so no other engine change beyond the build-path swap. JSON Schema description forscope_filterupdated to documentdir_absentsupport. Symmetricdir_exists/file_absent/file_existsextensions deferred — none of the v0.9.18 fix-list items need them.
Fixed (bundled rulesets — A1–A6)
Section titled “Fixed (bundled rulesets — A1–A6)”- A1 —
hygiene-no-js-build-outputs(andnode-no-tracked-dist) gated onscope_filter: { has_ancestor: package.json }. Without this gate, the rule fired on every**/dist,**/build, etc. directory regardless of context — false positives in 8 polyglot monorepos (k8s, golang/go, dotnet, bazel, vscode, nixpkgs, deno, angular). - A2 —
apache-2-source-has-license-headerpattern broadened to catch BOTH short SPDX form AND long ASF preamble. Pre-fix pattern only matched the short SPDX form, missing the longer “Licensed to the Apache Software Foundation (ASF)…” preamble every Apache TLP uses. airflow surfaced 8,228 false positives; arrow + spark each carried per-repo overrides duplicating the alternation pattern. The bundled rule now uses'Licensed (to the Apache Software Foundation|under the Apache License,?\s*Version 2)'— superseding the per-repo overrides. - A3 —
python@v1cosmetic rules default-exclude test-fixture paths.python-sources-final-newlineandpython-sources-no-trailing-whitespacenow excludetests/fixtures/**,**/testdata/**,Lib/test/**(cpython), andcrates/**/resources/**(Rust-Python projects like ruff). ruff alone surfaced 235 FPs across its 1,597 deliberately-malformed Python test fixtures. - A4 —
monorepo/cargo-workspace@v1documents non-canonical layout override. The bundledselect: "crates/*"is the canonical layout; non-canonical workspaces (deno’sext/*+libs/*; clap’sclap_*) override the rule via per-config (rule-id reuse). Bundled YAML now ships a SCOPE NOTE enumerating the override recipe with a worked example. The principled fix — atoml_path_array_iterrule kind that derives the iteration set from$.workspace.members[*]— is a v0.10 design candidate. - A5 —
oss-license-existsacceptsLICENSE.TXT(uppercase),LICENSE.rst,COPYING.{md,txt}, and a few less-common variants. dotnet/runtime canonical filename isLICENSE.TXT; filesystem globs are case-sensitive on most platforms. Broadened via brace expansion. Mirror change tooss-license-non-empty(paths must stay symmetric). - **A6 —
rust@v1’srust-sources-snake-caseexcludes test-fixture- doc subtrees with deliberately non-snake-case filenames.**
rust-lang/rust’s actual FP class is hyphenated names in
src/doc/**, Miri/clippy/rustfmt test fixtures, and**/*.miri.rsdot-suffix files (NOTcompiler/**— that subtree is already snake-case-correct, contrary to the deep-analysis log description). rust-lang/rust violations dropped 1,091 → 10 (99% reduction).
- doc subtrees with deliberately non-snake-case filenames.**
rust-lang/rust’s actual FP class is hyphenated names in
Fixed (case-study configs — B1–B3)
Section titled “Fixed (case-study configs — B1–B3)”- B1 —
examples/microsoft-typescript/.alint.yml: pitfall #22 block-scalar fix onts-copyright-header-{src,scripts}(|→|-);ts-copyright-header-srclevel loweredwarning→info(the convention isn’t actually applied to source files — TypeScript bundles its CopyrightNotice block via Hereby’sgenerateLibs). - B2 —
examples/denoland-deno/.alint.yml: defensive pitfall #22 swap ondeno-copyright-js-ts. Verified NOT firing today (regex luck), but|-removes the dependency on regex luck. - B3 —
examples/tensorflow-tensorflow/.alint.yml:tensorflow-bazel-files-have-apache-headerpremise repair (700 FPs pre-fix). TF declares licensing per-Bazel-package vialicenses(["notice"])+default_applicable_licenses, NOT per-file headers. Pattern replaced with'(licenses\(.*notice|default_applicable_licenses.*license)'- scope narrowed to
BUILD/BUILD.bazelonly.
- scope narrowed to
Validation
Section titled “Validation”- Cross-cutting revalidation pass. Re-ran alint v0.9.18 against
the cloned case-study trees at
/tmp/<repo>/. Per-repo FP elimination tabulated indocs/development/case-study-deep-analysis-log.mdv0.9.18 revalidation evidence section. The revalidation surfaced two scope-mismatches in the original A3 + A6 commits that didn’t catch the actual FP source — corrected mid-flight in commitdc7c3ed8(A3:crates/**/resources/test/fixtures/**→crates/**/resources/**; A6:compiler/**→src/doc/**+src/tools/{miri,clippy,rustfmt}/tests/**+**/*.miri.rs). coverage_audit_smoke_fixturesextended. Three new fixtures exercise the A1, A3, A6 bundled-rule defaults viatests/coverage_audit_smoke_fixtures.rs. Each fixture’sexpected.tomldocuments the count drift a refactor would produce so the audit failure points at the regression class. Total smoke fixtures: 5 → 8.- Per-README cosmetic count refresh deferred — those tables need re-running per-repo and become noise in the launch sprint. The deep-analysis log evidence table is the v0.9.18 ship-state authority.
[0.9.17] — 2026-05-06
Section titled “[0.9.17] — 2026-05-06”Corrective release that re-publishes v0.9.16’s content + the
build.rs/main.rs lint fixes that prevented v0.9.16’s Release
workflow from publishing artifacts. Same scope as v0.9.16 (Config
DX hardening + 21-pitfall catalogue + 30 OSS case studies + P2b
Wave 2 evidence + per-rule respect_gitignore: false knob +
operator-polish), plus the small set of corrections enumerated
below. v0.9.16 the tag exists; v0.9.16 the release does not —
crates.io / Homebrew / Docker / npm never received a v0.9.16
artifact (the Release workflow’s preflight failed at clippy
because the workspace’s [lints.clippy] enables pedantic = warn
-D warnings, which caught 7 cast lints incrates/alint/build.rsand 2 lints incrates/alint/src/main.rs— both new files added in v0.9.16. Localcargo clippy --workspace --all-targets -- -D warningsruns against a warm cache hadn’t surfaced them; runningci/scripts/clippy.shfrom a clean state would have).
crates/alint/build.rspedantic-clippy compliance. Casts in the days-to-ymd astronomical algorithm get a function-level#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss, clippy::cast_possible_truncation)]with a comment explaining that the algorithm’s invariants (doe∈[0, 146_097),yoe∈[0, 400),m∈[1, 12],d∈[1, 31],y∈[1970, ~25_000]) make the lints fire on shapes that never occur. Plusunwrap_or→map_orand three doc-comment backtick fixes.crates/alint/src/main.rspanic-hook lints.map(...) .unwrap_or_else(...)→map_or_else(...)oninfo.location();out.push_str(&format!(...))→write!(out, ...)on theurl_encodepercent-encoder.examples/flutter-flutter/.alint.ymltwo real false-positives.- BSD-header rule excludes
packages/flutter_tools/templates/**(those are user-app templates created byflutter create, not flutter source — they don’t carry the Flutter Authors header by design). resolution: workspacerule excludespackages/flutter_tools/pubspec.yaml(flutter_tools historically stands outside the root pub workspace, alongside flutter_goldens which was already excluded — the rule was producing a FP on a legitimate exception).
- BSD-header rule excludes
docs/launch-prep.mdcross_language_implementation_complete source count corrected from 4 to 5. flutter is the 5th source AND a genuinely distinct topology — platform-driven polyglot variant (engine ABI surfaces ↔ per-OS native implementations underengine/src/flutter/shell/platform/{android,darwin/ios, darwin/macos,linux,windows,fuchsia}/) versus the data-format- driven variant (arrow + tensorflow + protobuf) and the within- language source↔golden variant (angular). Generalises to every cross-platform UI framework (React Native, MAUI, Qt, Tauri).docs/development/CONFIG-AUTHORING.mdpitfall #18 demand-signal count. flutter shipspubspec.locktracked-AND-gitignored (same shape as bazel’s.bazelversion) — second independent demand signal for the per-rulerespect_gitignore: falseknob that v0.9.16 shipped. The note added at the bottom of the pitfall #18 source attribution.
- Headline launch-evidence catch — Trojan-Source CVE-2021-42574.
alint catches 5 real CVE-2021-42574 (Trojan Source) bidirectional-
control-character errors in flutter/flutter’s
docs/releases/archive/, via the bundledoss-baselineruleset’sno_bidi_control_charactersrule. This is the strongest single piece of “alint catches things other tools miss” evidence in the case-study corpus — flutter’s existing tooling (analyzer, dart format, the per-package pre-commits) doesn’t catch this class. Worked into the launch- evidence drafts underdocs/marketing/drafts/.
Note on v0.9.16
Section titled “Note on v0.9.16”The v0.9.16 git tag (bc507b17 annotated tag → commit 764c817f)
remains on the remote pointing at the broken commit. It is not
moved: option A1 (“force-recreate the tag at the corrected
commit”) was considered and rejected in favour of A3 (“cut v0.9.17
as the corrective release”) for orthodox-semver reasons. Adopters
who pulled the v0.9.16 tag will see no published artifacts at any
of the usual channels (crates.io / Homebrew / Docker / npm); v0.9.17
is the first publishable tag in the 0.9.16 content lineage.
[0.9.16] — 2026-05-06 (tag-only — never published, see v0.9.17)
Section titled “[0.9.16] — 2026-05-06 (tag-only — never published, see v0.9.17)”Config DX hardening release. Closes the launch-prep validation pass
with seven-phase coverage of the 17 schema + runtime pitfalls
surfaced during the P2a 20-repo case-study sweep, plus the
deny_unknown_fields uniformity audit that makes that coverage
hold uniformly across all 60 rule kinds. A config that hits any of
the catalogued pitfalls is now caught somewhere in the toolchain
(schema at edit time, parse error at load time, runtime audit at
PR time) — for every rule kind, not just the half that already
had deny_unknown_fields declared.
(Note: there is no v0.9.15 tag. The seven-phase work landed locally
under that label on 2026-05-06 but was rolled into v0.9.16 alongside
the uniformity audit before the public release. The version sequence
is v0.9.14 → v0.9.16 by design — the git log entries between
them are the seven phases plus the uniformity work.)
The headline numbers:
- 21 distinct pitfalls catalogued in
docs/development/CONFIG-AUTHORING.mdwith canonical-correct YAML for each (17 from P2a; 2 from P2b Wave 1 —.gitignoremasks tracked-file presence checks;root_only: truesilently no-matches multi-component literals; 2 from P2b Wave 2 — cross-file value-equality across structurally-different files needing per-file value extraction;yaml_path_*rules erroring on multi-document YAML files). Pitfalls #18 and #19 are now fixed in the engine (#18 ships the per-rulerespect_gitignore: falseknob; #19 ships the literal_is_nested runtime guard so misconfigurations no longer fire on unrelated files); pitfalls #20 and #21 stay documented with a workaround until the v0.10 design candidates ship. - 7 silently-broken structured-path rules in committed pilot + Wave 1 configs (6 bool-match + 1 array-semantics) caught by the validation pass and fixed in-flight.
- 30 production OSS repos with working
.alint.ymlconfigs + case-study writeups underexamples/<owner>-<repo>/— 20 from P2a (single-language + diverse-ecosystem) + 5 from P2b Wave 1 (curated pre-launch polyglot subset: NixOS/nixpkgs, bazelbuild/bazel, tensorflow/tensorflow, apache/spark, microsoft/vscode) + 5 from P2b Wave 2 (platform-driven polyglot subset: angular/angular, istio/istio, dotnet/runtime, protocolbuffers/protobuf, flutter/flutter — total 322 polyglot rules across the Wave 2 set, zero new rule-kind candidates beyond the v0.10/v0.11 backlogs, validating the saturation analysis). Five positioning narratives crystallised across the corpus. - Scale validation: the 5-repo P2b Wave 1 wasn’t expected to
surface new pitfalls (saturation analysis from P2a’s Wave 3 had
predicted reconfirmation, not new candidates) but was commissioned
to test unknown-unknowns: scale stress, build-system shape, per-
language API parity, polyglot-discipline convergence, and flagship-
visibility apples-to-apples comparison. Headline results:
- NixOS/nixpkgs at 39,101 files + 20,678
pkgs/by-name/*/*/package directories: alint’s full 79-rule pass — including the headlinefor_each_dirover the by-name tree — completes in 273 ms wall-clock. “Any size repo” is now empirically defensible. - microsoft/vscode
build/hygiene.tsapples-to-apples: alint covers ~75 % of the 8 distinct hygiene checks (6 of 8) declaratively in one config. “alint is whatbuild/hygiene.tswould look like as a tool, not a per-repo script” — verified against the live tree (222 violations, zero false positives). - 3 Apache TLPs converge: arrow + spark + airflow share 9 of
12 governance artefacts;
apache/governance@v1bundled ruleset promotes from “v0.10+ idea” to “v0.10 ship-target.” cross_language_implementation_completev0.11+ candidate now demand-validated AND saturated: 4 sources after Wave 2 (arrow- tensorflow + protobuf + angular). protobuf is the densest with ~45 cross-language assertions one rule would express; angular contributes the within-language source↔golden variant. Promotes from “v0.11+ flagship” to “v0.11+ ship-target.”
- New v0.10 rule-kind candidate:
xml_path_matches/xml_path_equals(spark — completes the structured-query family; generalises to Maven, Ant, Gradle XML, .nuspec, .csproj). Promoted to v0.10 ship-target by Wave 2 — dotnet/runtime stress-tests at ~2,300 distinct XML manifests (1,091 .csproj + 234 solution files + 257 Directory.Build.* + 520 .props/.targets), one order of magnitude bigger than spark. - New bundled ruleset added to v0.10 ship-list:
dotnet@v1— surfaced by dotnet/runtime; 12 of 14 dotnet-specific rules in the case study consolidate into oneextends:line. Adopter surface is large (every dotnet/* + every Azure SDK + every microsoft/* .NET project).
- NixOS/nixpkgs at 39,101 files + 20,678
- Every example config ships with
# yaml-language-server: $schema=…so opening it in any LSP-aware editor (vscode-yaml / JetBrains / coc-yaml) gets autocomplete + validation for free. alint validate-config <path>is the new entry point for editor LSP / pre-commit / fail-fast CI integrations.#[serde(deny_unknown_fields)]is now uniform across all 60 rule kinds — 13 rule Options structs that previously silently accepted unknown fields now reject them, surfacing the Phase 3 did-you-mean enrichment for those kinds. 13 unit-style integration tests (one per kind) continuously verify uniformity at PR time.- Per-rule
respect_gitignore: false(pitfall #18 fix). Closes the bazel-style “tracked AND gitignored” pattern surfaced by Wave 1.file_existsrules can now bypass the workspace-level gitignore setting at single-rule granularity; the literal-path fast path also checks the filesystem directly so a.bazelversion-style file is found even when the walker pre-filters it out. Two regression scenarios (positive + negative) lock the behaviour incrates/alint-e2e/scenarios/check/git/. - Operator polish —
alint --versionincludes commit SHA + build date, baked at compile time via a newcrates/alint/build.rs. Bug reports can paste the full version string and a maintainer can pinpoint the exact commit. Falls back gracefully to “unknown” when built from a published tarball without a git tree. - Operator polish — pre-filled GitHub-issue URL on panic. A
custom panic hook prints a
https://github.com/asamarts/alint/issues/new?title=…&body=…link with version + OS + location + payload pre-populated. Skipped whenRUST_BACKTRACEis set so the standard backtrace path stays unchanged for developers. SECURITY.mdadds an explicit “Telemetry-free” guarantee —alint sends nothing over the network at runtime, with a verifiablestrace -e trace=networkcommand. Closes the trust-on-first-run gap for security-team adopters.
No engine changes; all bench-relevant code paths unchanged. The release exists to make alint configs obviously-correct at write time, parse time, and run time — the P2a pass proved that without this layer of DX, real adopters silently drift into broken-but- parseable rules (we found seven such drifts in our own evidence configs before they shipped to launch readers).
docs/development/CONFIG-AUTHORING.md— 17-pitfall catalogue (Phase 1). Every pitfall the P2a validation pass surfaced, with the canonical-correct YAML alongside. Authors writing a new.alint.ymlare pointed here as the cheat sheet, with explicit notes for AI agents drafting configs against rule-source memory rather than the doc.crates/alint-e2e/tests/coverage_audit_examples_parse.rs— examples-parse audit (Phase 2). Every shippedexamples/*/.alint.ymlMUST load + build cleanly viaalint_dsl::load+RuleRegistry::build. Schema drift surfaces at PR time. Catches the 12 schema-rename-class pitfalls transitively. New companion check (every_example_carries_the_yaml_language_server_directive) enforces the LSP-magic-comment line on every example.crates/alint-e2e/tests/coverage_audit_rules_md_drift.rs— per-ruleset table drift audit. Walks every YAML undercrates/alint-dsl/rulesets/v1/, parses every###alint://bundled/…@v1“ section indocs/rules.md, asserts identical rule-id sets per ruleset. Caught the same drift class on 3 more rulesets the day it landed (ci/github-actions, agent-hygiene, agent-context); all fixed in the same commit. Closes the audit gap that let the oss-baseline 8/9/11/15 drift slip through during P2a.- Did-you-mean parse errors (Phase 3). New
crates/alint-core/src/did_you_mean.rsmodule with two-tier resolution: curated overrides for the highest-drift renames (argv→command,secondary→partner,style→target,pattern→prefix|suffix,matches↔equalsfor the structured-path family) plus a Levenshtein fallback (distance ≤ 2) for the long tail. Hooked atRuleRegistry::build— no per-rule edits. 18 unit tests + 9 integration tests covering 6 pitfalls end-to-end. Side-effect: added#[serde(deny_unknown_fields)]toEqualsOptionsandMatchesOptionsinstructured_path.rsso pitfall #16 (matches:↔equals:rename) surfaces as unknown-field rather than missing-required. - Domain-specific error messages (Phase 4). Five pitfalls now
produce hint-bearing errors instead of generic parser output:
- #10 — JSONPath dashed-key access. New
crates/alint-core/src/jsonpath_diagnostics.rsinspects path sources for.<dashed-ident>patterns (top-level OR inside filter expressions) and suggests bracket notation. - #11 —
scope_filter.has_ancestorwith path separator. The error now points atpaths:glob as the alternative for directory scoping. - #12a —
&&/||/!inwhen:. Parse-error wrapper detects symbolic operators and suggests theand/or/notkeyword. - #12b —
iter.foo.bar(...)method-call shape. Global regex catches the double-dot chain and points at thematchesoperator. - #15 —
file_starts_with.prefix: "". Build-time error now points atfile_min_lines: 1/file_min_size: <bytes>as the right tool for non-emptiness checks.
- #10 — JSONPath dashed-key access. New
- JSON Schema editor-LSP wiring (Phase 5). New audit at
crates/alint-e2e/tests/coverage_audit_schema_drift.rswalks therule_kind_dispatchoneOf, extracts every kind name (handles both single-constand multi-nameenumdiscriminators for legacy aliases), and verifies registry ↔ schema parity. Five spot-check tests exercise pitfalls #1/#4/#9/#15/#16 against the live schema via thejsonschemacrate, asserting they’re rejected at edit-time. Plus the# yaml-language-server: $schema=…magic comment was added to all 20 example configs and a companion audit keeps it there. alint validate-config <path>subcommand (Phase 6). Parse-only command (no tree walk) that runs the full load + build + when-parse path and reports pass/fail. Accepts a file or directory. Two output formats:human(one-liner stdout + error chain on stderr) andjson(stable{valid, rule_count, config_path, error}envelope). Exit codes: 0 valid / 1 invalid / 2 invocation error. Did-you-mean hints from Phases 3 + 4 flow through transparently — covered by a trycmd test that exhibits anargvtypo and asserts the canonical-correct hint appears.- Smoke-test fixture audit (Phase 7). New audit at
crates/alint-e2e/tests/coverage_audit_smoke_fixtures.rscloses the runtime-semantic gap left by Phases 1-6. Walkscrates/alint-e2e/fixtures/smoke/<scenario>/directories; each scenario is a self-contained config + file tree +expected.tomlwith canonical violation counts. The audit runs the engine over each tree and asserts actuals match — a refactor that silently re-introduces pitfalls #13 (regex anchoring), #14 (YAML\nin regex), #16 (*_path_matches- bool), or #17 (
*_path_equals+[*]) changes the count and fails. Initial coverage: 4 fixtures targeting the four classes. Sanity-verified by deliberately removing(?m)from a fixture rule (audit failed) and restoring it (audit green).
- bool), or #17 (
- 20 P2a case studies under
examples/<owner>-<repo>/: kubernetes/kubernetes, rust-lang/rust, golang/go, python/cpython, nodejs/node, apache/airflow, denoland/deno, tokio-rs/tokio, astral-sh/uv, astral-sh/ruff, clap-rs/clap, microsoft/typescript, facebook/react, prettier/prettier, pnpm/pnpm, helm/helm, pytorch/pytorch, vercel/turbo, plus two polyglot wins (vercel/next.js, apache/arrow). Each is a working.alint.yml+ a markdown writeup of what alint catches that the repo’s existing tooling doesn’t. Theexamples/README.mdgallery organises them by the five positioning narratives surfaced.
- 6 bool-match rules in committed
examples/*/.alint.ymlconfigs that were silently broken by pitfall #16 (*_path_matchesagainst bool fields produces a runtime “not a string” error on every match). Switched to*_path_equalswith native YAML bool literals: typescript ×2 (compilerOptions.strict,compilerOptions.checkJs), vercel/turbo (replaced withfile_content_matchesfor the either-of-many-bools case), tokio (package.publish), ruff (package.publish), pnpm (engineStrict). - deno’s
deno-dlint-includes-camelcaserule was silently broken by pitfall #17 (*_path_equalsagainst[*]flips intent from “any” to “every”). Switched tofile_content_matchesagainst the JSON text. oss-baseline@v1ruleset count drift. The bundled YAML has 15 rules;docs/rules.mdclaimed “Nine rules” but listed 11; the alint.org compare-page draft said 8. All three reconciled to 15.- JSONPath outer-parens filter wasn’t a real pitfall. A
reported “pitfall #18” (the apache/arrow case study originally
claimed
?(...)was rejected) was investigated againstserde_json_path0.7.x and proven valid — the original failure was a dashed-key inside the filter (an instance of pitfall #10 in filter context), not the parens. Catalogue dropped 18 → 17; the arrow case study now points at #10.
Changed
Section titled “Changed”#[serde(deny_unknown_fields)]is now uniform across the rule catalogue. Added toEqualsOptions+MatchesOptionsinstructured_path.rs(Phase 3 dependency for pitfall #16’smatches:↔equals:rename to surface as unknown-field rather than missing-required), and as a v0.9.16 sweep to 13 more rule Options structs that previously silently accepted unknown fields:file_content_matches,file_content_forbidden,file_header,file_footer,file_max_lines,file_max_size,file_min_lines,file_min_size,file_shebang,filename_case,filename_regex,commented_out_code,markdown_paths_resolve. This is the behaviour change the original v0.9.15 plan flagged for “v0.9.16+” — the launch-prep deliberation chose to roll it into the same release rather than ship a half-cooked uniformity story. Adopters whose configs had stray fields on these rule kinds will now see clean unknown-field errors with did-you-mean hints instead of silent acceptance.crates/alint-e2e/fixtures/smoke/content_matches_yaml_newline_canonical/— additional smoke fixture covering pitfall #14 (YAML\nin regex patterns). Phase 7 shipped 4 fixtures; v0.9.16 adds this 5th to bring per-pitfall regression coverage uniform.
Marketing drafts (not yet published)
Section titled “Marketing drafts (not yet published)”docs/marketing/STATE.md is the new single source of truth for
what’s live, stale, and drafting across the README + alint.org
surfaces. Seven drafts under docs/marketing/drafts/ are queued
for publish coordinated with this release’s docs roll:
readme-hero.md— incremental polish on the README hero adding the case-study social proof + 5-narrative section.alint-org-hero.md— content brief for the alint.org landing with version badge, speed claim in the hero bullets, Repolinter-archived-2026 framing, and the 20-case-study link.alint-org-examples-gallery.md— content brief for the new/examples/route + auto-synced per-case-study sub-pages.alint-org-compare.md— direct comparison table vs Repolinter, ls-lint, Megalinter, EditorConfig, and custom shell scripts.migrate-from-repolinter.md,migrate-from-ls-lint.md,migrate-from-custom-bash.md— three step-by-step migration guides. Repolinter is the highest-intent SEO target.
The drafts are not part of the v0.9.15 binary release; they ship when the alint.org site repo is updated next.
[0.9.14] — 2026-05-05
Section titled “[0.9.14] — 2026-05-05”CI automation release. The bench-record.yml workflow that
captures publish-grade bench data on every release tag is now
fully end-to-end automated for the first time. Pre-v0.9.14 a
maintainer had to (a) manually gh pr create because the
workflow’s gh CLI step lacked auth, (b) manually toggle a repo
policy because the GITHUB_TOKEN couldn’t open PRs, (c)
manually re-render HISTORY.md after every bench-record
merge, and (d) manually edit xtask/scripts/render-history.py
to add each new version to its hardcoded list. v0.9.14 closes
all four gaps. Future tag-pushes produce a one-click-merge PR
with bench data + criterion + HISTORY refresh, no human
in the loop until the review.
No alint binary changes. CLI users see nothing different — the release exists to seal the bench-record automation as proven end-to-end (validated against the v0.9.13 tag in PR #14 before this release was cut).
xtask/scripts/render-history.pyauto-discovery. Versions read from the filesystem (docs/benchmarks/macro/results/<arch>/v*/) instead of a hardcodedKNOWN_VERSIONSlist. Per-release date + headline blurb extracted from CHANGELOG.md’s## [X.Y.Z] — YYYY-MM-DDheader + first sentence of the paragraph below. Manual fallback dict retained for v0.5.6/v0.5.7 (predate the CHANGELOG-tracked corpus). Maintainer effort to land a new release row in HISTORY drops from “edit two places in the script + commit” to “make sure the CHANGELOG entry has a punchy first sentence”.bench-record.ymlHISTORY auto-refresh step. The workflow now runsrender-history.pyafter the bench capture and includes the refreshed HISTORY.md in the same bench-record commit. The PR is one-merge-completes-everything for both the bench data and the HISTORY row.bench-record.ymlstandalone python3 install. The self-hosted runner has nopython3preinstalled and nosudoavailable; v0.9.14 installs a prebuilt standalone Python from indygreg/python-build-standalone into$HOME/.local/python/. Same pattern the gh CLI tarball install (added in v0.9.12) uses.bench-record.ymlrebase-onto-main before push. Whenworkflow_dispatch -f ref=<old-tag>benches an older ref, the working tree’s workflow YAML diverges from main’s; the resulting push triggers GitHub’s “GITHUB_TOKEN cannot create or update workflow files” protection. v0.9.14 stages bench artefacts to/tmpand rebases ontoorigin/mainbefore committing — the resulting push contains only bench data, no workflow-file diff. Side benefit: tag-triggered runs are also more robust (the bench commit always lands on top of current main, eliminating a class of subtle merge conflicts).bench-record.ymlworking-tree reset before checkout.git reset --hard HEAD+git clean -fd docs/benchmarks/clears the modified HISTORY.md (from the previous render step) and untracked bench dirs (which conflict with main’s tracked versions of the same paths after a prior bench-record PR merged). Without it, the rebase checkout aborts.
bench-record.ymlbench-recordrepo label now exists (created during the v0.9.13 manual workaround). The workflow’sgh pr create --label bench-recordinvocation no longer fails on label resolution.- Repo policy “Allow GitHub Actions to create and approve pull requests” enabled. The workflow’s GITHUB_TOKEN can now open PRs without manual fallback (previously failed with “GitHub Actions is not permitted to create or approve pull requests”).
Internal
Section titled “Internal”- 4 iterations to validate the pipeline end-to-end on the
v0.9.13 tag (failures: missing python3, workflow-file push
protection, working-tree conflicts on rebase, then success).
Each iteration produced a real fix; the pipeline is now
fully proven from
gh workflow runto merged PR. - v0.9.13 bench data refreshed via PR #14 (the validation run). Slightly tighter numbers than the original capture; max CV 0.041 across all 80 cells.
Held for future
Section titled “Held for future”v0.9.14-tag-push validation — this release is the first one whose bench-record run will fire from a tag-push (not workflow_dispatch). Expected outcome: PR auto-opens, bench data + HISTORY.md ready for one-click merge.- SHA-rotate the docker/ actions in
release.yml* — still pinned at the same SHAs as v0.9.12; separate “rotate + verify” cycle (note from v0.9.13).
[0.9.13] — 2026-05-04
Section titled “[0.9.13] — 2026-05-04”Dependency-refresh release. Closes the 10 open Dependabot PRs plus opportunistic bumps the version specs already accepted. Net workspace perf neutral-to-positive across S1-S10 — most scenarios are 4-13 % faster at 100k from upstream improvements (jsonschema 0.46’s API rework + lockfile patches).
Cargo workspace deps bumped
Section titled “Cargo workspace deps bumped”anstream0.6 → 1.0 — stability commit, no API changes.trycmd0.15 → 1.x — stability commit.rand0.9 → 0.10 +rand_chacha0.9 → 0.10 (paired ecosystem bump). One import incrates/alint-bench/src/tree.rsswitched fromuse rand::Rngtouse rand::{Rng, RngExt, ...}to pick up the newrandom_range/randommethods (the rand 0.10 rename ofgen_range/gen).jsonschema0.29 → 0.46. The 0.36 release privatisedValidationError::instance_pathand exposed it via an accessor method; 4 call sites updated (alint-rules/src/json_schema_passes.rs,alint-output/tests/{cross_formatter,fix_report_schema}.rs,alint-dsl/tests/schema.rs).sha20.10 → 0.11 — workspace consumers (alint-dsl/sri,alint-output/gitlab,alint-rules/file_hash) all use the stableDigest/Sha256::new()/update/finalizeAPI which is unchanged across the major. No code changes needed.- Lockfile refresh:
rustls0.23.40,libc0.2.186,wasm-bindgen0.2.120,winnow1.0.2, etc. — all in-spec patch-level pickups.
GitHub Actions bumped
Section titled “GitHub Actions bumped”| Action | From | To |
|---|---|---|
actions/checkout | v4 | v6 |
actions/setup-node | v4 | v6 |
actions/cache | v4 | v5 |
actions/upload-artifact | v4 | v7 |
actions/download-artifact | v4 | v8 (paired w/ upload) |
codecov/codecov-action | v4 | v6 |
webfactory/ssh-agent | v0.9.0 | v0.10.0 |
docker/build-push-action | v6 | v7 (unpinned only) |
docker/login-action | v3 | v4 (unpinned only) |
docker/setup-buildx-action | v3 | v4 (unpinned only) |
docker/setup-qemu-action | v3 | v4 (unpinned only) |
The SHA-pinned variants of the docker/* actions in release.yml
are intentionally left at their pinned SHAs — supply-chain
hardening for the publishing path requires a separate “rotate
SHA + verify” cycle, not a blanket version bump.
Performance
Section titled “Performance”100k/full deltas vs v0.9.12 baseline:
| Scenario | v0.9.12 | v0.9.13 | Δ |
|---|---|---|---|
| S1 | 163ms | 151ms | -7 % |
| S2 | 258ms | 254ms | -2 % |
| S3 | 1301ms | 1130ms | -13 % |
| S4 | 156ms | 160ms | +3 % |
| S5 | 885ms | 847ms | -4 % |
| S6 | 1121ms | 1011ms | -10 % |
| S7 | 329ms | 316ms | -4 % |
| S8 | 1104ms | 1032ms | -7 % |
| S9 | 694ms | 666ms | -4 % |
| S10 | 324ms | 328ms | +1 % |
S3 / S6 / S8 wins are likely jsonschema 0.46’s Validator
performance work and lockfile patch updates compounding.
1m/full all within ±5 % (most slightly faster). Full numbers in
docs/benchmarks/macro/results/linux-x86_64/v0.9.13/
once the bench-record workflow lands the canonical capture.
Held for v0.9.14+
Section titled “Held for v0.9.14+”toml0.9 → 1.1 — config-format-stability review needed (parser/writer rewrite,FromStrsemantics changed,serde/stdnow opt-in default features).tower-lspdormant dep — the crate appears stalled; re-evaluate at v0.10 LSP design time vs maintained alternatives (tower-lsp-serverfork,lsp-server,async-lsp).- SHA-pinned
docker/*actions inrelease.yml— separate rotate + verify cycle.
Internal
Section titled “Internal”- All 1141 workspace tests pass.
cargo clippy --workspace --all-targets --all-features -- -D warningsclean.cargo doc --no-deps --workspacewithRUSTDOCFLAGS=-D warningsclean.
[0.9.12] — 2026-05-03
Section titled “[0.9.12] — 2026-05-03”Backlog cleanup release closing the explicitly-held v0.9 items that didn’t fit cleanly into earlier cuts. Three structural audits + one engine refactor + the bench-record CI fix.
Removed — alint-core API
Section titled “Removed — alint-core API”Rule::wants_git_tracked()— deprecated in v0.9.11 with a v0.9.12 removal date; now gone. Override [Rule::git_tracked_mode] instead. The engine consultsgit_tracked_mode()(added in v0.9.11) to pick the right pre-filteredFileIndexfor each opted-in rule.
Added — alint-core API
Section titled “Added — alint-core API”alint_core::CompiledNestedSpec— wraps a [NestedRuleSpec] with itswhen:source pre-compiled to a [crate::when::WhenExpr] at rule-build time. Mirrors the v0.9.5-erawhen_iter:pattern (parse once at build, evaluate per iteration with fresh iter context). The 3 cross-file iteration rules (for_each_dir,for_each_file,every_matching_has) holdVec<CompiledNestedSpec>and consume it via the sharedevaluate_for_eachhelper.
- Nested
when:no longer re-parses per iteration. Pre-v0.9.12 the nestedwhen:source string was re-parsed insideevaluate_for_each’s loop on every (entry, nested-rule) pair — at scale (e.g. workspace bundles with 5000+ packages × 3+ nested rules), thousands of redundant parses per cross-file rule eval. The newCompiledNestedSpecparses each source string exactly once at build time. Misconfigured nested-when:clauses now surface at config- load time instead of mid-evaluation. bench-record.ymlworkflow now completes end-to-end. Pre-v0.9.12 the workflow’sgh pr createstep failed withgh: command not foundon the self-hosted runner — bench data was captured + pushed to abench-record/<tag>branch but the PR-opening step crashed, leaving stale branches (bench-record/v0.9.{9,10,11}exist on remote with captured but unsurfaced data). v0.9.12 adds aninstall gh CLIstep before the PR-opening step. Same release adds S10 to the workflow’s scenarios list (was S1-S9 only, missing the v0.9.9 addition).
Internal
Section titled “Internal”coverage_audit_when_wiring.rs(new). Asserts every cross-file iteration rule (for_each_dir,for_each_file,every_matching_has) wires itswhen_iter:field through the sharedparse_when_iterhelper at build time AND consults the resultingOption<WhenExpr>in its dispatch path. Catches the silent-no-op recurrence-risk shape on thewhen_iter:axis the same way v0.9.10’s audit catches it onscope_filter:.coverage_audit_engine_when_dispatch.rs(new). Asserts every dispatch site inengine.rsthat callsrule.evaluate(...)orrun_entry(...)is preceded by anentry.whenconsultation in the surrounding 60 lines, OR delegates torun_entry(which does the consultation centrally). Separate sub-test assertsrun_per_file’s inlineentry.whencheck stays in place. Catches a future engine extension (e.g., a hypothetical fix-only path or LSP single-file re-evaluation path) that adds a new dispatch site and forgets the gate.compile_nested_requirehelper (alint-rules’sfor_each_dirmodule) — single point all 3 cross-file iteration rules call to compile theirVec<NestedRuleSpec>intoVec<CompiledNestedSpec>at build time. New iteration rules thread their require list through this.
Held for v0.9.13+ / indefinitely
Section titled “Held for v0.9.13+ / indefinitely”when:ownership remains explicitly out of scope.when:data (facts/vars/iter) varies at evaluate-time so it can’t be pre-computed into a struct field; the engine already owns the top-level dispatch and the v0.9.12 audits- pre-compilation close the remaining gaps.
- Stale
bench-record/v0.9.{9,10,11}branches on remote contain captured-but-unsurfaced bench data from before the workflow fix. Future bench-record runs (starting v0.9.12) produce a fresh PR with S1-S10 + criterion + everything; the stale branches can be deleted at the maintainer’s leisure or kept as historical artefacts of the broken state.
[0.9.11] — 2026-05-03
Section titled “[0.9.11] — 2026-05-03”Structural fix for the git_tracked_only: silent-no-op
recurrence-risk shape. v0.9.10 closed the analogous
scope_filter: bug class via Scope ownership; v0.9.11
closes the git_tracked_only shape via engine-side
pre-filtered FileIndexes. Rules opted into
git_tracked_only: no longer carry a runtime
if self.git_tracked_only && !ctx.is_git_tracked(...)
check — the engine hands them a Context whose index
already iterates only the tracked subset.
Added — alint-core API
Section titled “Added — alint-core API”alint_core::GitTrackedMode { Off, FileOnly, DirAware }enum. Returned byRule::git_tracked_mode()so the engine knows which pre-filteredFileIndexto hand the rule. File-mode rules (file_exists,file_absent) returnFileOnly; dir-mode rules (dir_exists,dir_absent) returnDirAware.Rule::git_tracked_mode(&self) -> GitTrackedModetrait method (defaultOff). Replaces the boolean consultation thatwants_git_tracked()provided.
Deprecated
Section titled “Deprecated”Rule::wants_git_tracked()— kept for one minor version as a default that delegates togit_tracked_mode() != Off. Out-of-tree rule plugins that override this method continue to work; the engine no longer consults it. Removed in v0.9.12. Overridegit_tracked_modeinstead.
git_tracked_onlysilent-no-op bug class closed structurally. A new rule that shipsgit_tracked_only: boolon its spec but forgets to overridegit_tracked_mode()defaults toOff— same defensive default as today. But once it overridesgit_tracked_mode(), the engine’s pre-filteredFileIndexautomatically narrows the rule’s iteration — no per-evaluateis_git_tracked(...)check to forget. The auditcoverage_audit_git_tracked_only.rswas updated to flag both the missing-mode-override AND the re-introduction of any per-rule runtime check.file_existsliteral-path fast path now applies even withgit_tracked_only: true. Pre-v0.9.11, the rule forced the slow path (entry iteration) whengit_tracked_onlywas set, because the per-entryis_git_trackedcheck ran inside the slow loop. The engine’s pre-filtered index makes the literal-pathcontains_filelookup correct without that check — 10-30 % S8 speedup at small/medium sizes (see Performance).
Internal
Section titled “Internal”- Engine
build_git_tracked_indexes: builds at most two pre-filteredFileIndexes per run (one perGitTrackedModein use). Build cost amortises across however many rules opt into each mode. pick_ctxextended to route opted-in rules to the right pre-filteredContext. Existence rules already declaredrequires_full_index = true, so the substitution is safe — we’re swapping the full-indexContextfor a tracked-narrowed one.- Outside-git-repo silent-no-op preserved via the
empty-index fallback: when
git ls-filesreturns nothing (no repo / non-zero exit), the engine builds empty pre-filtered indexes for opted-in modes so rules iterate zero entries and fire zero violations. - 4 existence rules (
file_exists,file_absent,dir_exists,dir_absent) cleaned up: per-evaluateis_git_tracked(...)/dir_has_tracked_files(...)checks deleted;git_tracked_mode()overrides added.
Performance
Section titled “Performance”S8 (git overlay) at all sizes is faster:
| Size/mode | v0.9.10 | v0.9.11 | Δ |
|---|---|---|---|
| 1k/full | 24.2ms | 21.9ms | -10 % |
| 1k/changed | 29.9ms | 20.4ms | -32 % |
| 10k/full | 139.7ms | 118.0ms | -15 % |
| 10k/changed | 87.3ms | 74.7ms | -14 % |
| 100k/full | 1068.7ms | 1064.0ms | -0 % |
| 100k/changed | 623.3ms | 579.0ms | -7 % |
The 1k/changed -32 % is mostly the file_exists
literal-path fast path applying for the first time with
git_tracked_only. Larger sizes show a smaller win
because the slow-path iteration cost is dominant.
Held for v0.9.12+
Section titled “Held for v0.9.12+”Rule::wants_git_tracked()removal. Deprecated this cycle; remove next.when:ownership remains explicitly out of scope — different semantics, no shared silent-no-op shape.
[0.9.10] — 2026-05-03
Section titled “[0.9.10] — 2026-05-03”Structural fix for the scope_filter: silent-no-op bug class.
v0.9.6 / v0.9.7 / v0.9.9 each shipped a different group of rules
that silently dropped scope_filter: because rules held a
standalone Option<ScopeFilter> field separate from the Scope
that owned the path-glob match — the compiler had no way to
catch a rule that forgot to consult both. v0.9.10 collapses the
two so a single Scope::matches(path, &FileIndex) covers both
predicates. The bug class can no longer recur.
Breaking — alint-core API
Section titled “Breaking — alint-core API”alint_core::Scope::matchessignature changed from(&Path)to(&Path, &FileIndex). Required so theScopeFilterancestor walk can consult the index. CLI users- bundled rulesets unaffected;
alint-core-as-a-library consumers (out-of-tree plugin authors) will see a compile error at every call site and must thread their&FileIndexthrough.
- bundled rulesets unaffected;
alint_core::Rule::scope_filter()trait method removed. Any rule that overrode it should drop the override; the rule’sScope(built viaScope::from_spec(spec)?) now carries the filter and the engine consults it via the per-file dispatch’spath_scope().matches(path, index)call.- New
alint_core::Scope::from_spec(spec)constructor bundlespaths:+scope_filter:parsing into one call. Replaces the per-ruleScope::from_paths_spec(paths)?+scope_filter: spec.parse_scope_filter()?two-line pattern. - New
alint_core::Scope::scope_filter()accessor exposes the optional filter for dispatch sites that need to consult it without going throughmatches(e.g. for custom narrowing logic). Most callers should not need it.
Internal
Section titled “Internal”- 41 rules cleaned up to drop their now-redundant
scope_filter: Option<ScopeFilter>field, theif let Some(filter) = ...runtime check insideevaluate(), and theimpl Rule { fn scope_filter() }override. -502 LOC across the rule files. - Engine consultation removed —
crates/alint-core/src/engine.rs::run_per_fileno longer separately checksentry.rule.scope_filter()after thepath_scope().matches()call; the latter covers it. for_each_dirliteral-path bypass simplified — the v0.9.9nested_rule.scope_filter()guard is now redundant (subsumed bypf.path_scope().matches(literal, ctx.index)).- New audit test
coverage_audit_scope_owns_filter.rsfails CI if any rule re-introduces a standalonescope_filter: Option<ScopeFilter>field or re-declares the deletedRule::scope_filtertrait method. Catches recurrence at PR time, not at runtime.
Performance
Section titled “Performance”- Within ±5 % of v0.9.9 across S1-S10 at all sizes — the per-
rule iteration shape is unchanged (one
if !scope.matches(p, idx) { continue; }per file), only the field layout is flatter. Full numbers indocs/benchmarks/macro/results/linux-x86_64/v0.9.10/.
Held for v0.9.11+
Section titled “Held for v0.9.11+”git_tracked_onlyownership parity withscope_filterwas scoped out — different field semantics, separate work. The same recurrence risk applies (a rule could ship withgit_tracked_only:silently dropped) but the field is rarer in practice and currently 100 % wired up. Tracked.
[0.9.9] — 2026-05-03
Section titled “[0.9.9] — 2026-05-03”Patch release for two scope_filter: silent-no-op gaps surfaced
by the post-v0.9.8 audit. v0.9.7 wired the filter through the
25 per-file content rules; v0.9.9 closes the residual coverage on
the 17 rules that bypass the engine’s per-file dispatch path AND
the for_each_dir literal-path bypass introduced in v0.9.8.
- 17 rules now honour
scope_filter:. The rules whoseRule::evaluateiteratesctx.index.files()directly (rather than opting into the engine’s per-file dispatch) had noscope_filterplumbing at all — the gate silently dropped on the way throughbuild(), identical shape to the v0.9.6 → v0.9.7 bug for per-file content rules. Affects:file_max_size,file_min_size,no_empty_files,executable_bit,executable_has_shebang,shebang_has_executable,no_symlinks,filename_case,filename_regex,no_illegal_windows_names,max_files_per_directory,max_directory_depth,json_schema_passes,command,git_blame_age,no_case_conflicts. Each rule now storesscope_filter: Option<ScopeFilter>, exposes it viaRule::scope_filter(), and short-circuits itsctx.index.files()loop on out-of-scope files (preserving the per-file iteration shape — only the work done per file changes). Per-rule unit tests assert narrowing behaviour. no_submodulesrejectsscope_filter:at build time. The rule is hardcoded to inspect.gitmodulesat the repo root — it does not iterate the file index, so ascope_filteron it is meaningless. Build-time rejection via the newreject_scope_filter_with_reason(spec, kind, reason)helper inalint-core::scope_filter.for_each_dirliteral-path bypass now consultsscope_filter. v0.9.8’s fast path (dispatching a nested per-file rule viaPerFileRule::evaluate_fileagainst a single literal, bypassing the rule’s ownevaluate) executed regardless of whether the literal’s ancestor chain satisfied the nested rule’sscope_filter:. Divergent from the rule-major fallback, which already honoured the filter (since v0.9.7). The bypass guard atcrates/alint-rules/src/for_each_dir.rs::evaluate_for_eachnow consultsnested_rule.scope_filter()before taking the fast path. E2e regression atcrates/alint-e2e/scenarios/check/scope_filter/scope_filter_nested_under_for_each_dir.yml.
alint_core::reject_scope_filter_with_reason(spec, kind, reason)— symmetric toreject_scope_filter_on_cross_file, but for rules whose dispatch shape is hardcoded to a single path. Thereasonfield surfaces why the rule cannot honour the filter (improves the error message authors get).- Macro bench scenario S10 at
xtask/src/bench/scenarios/s10_scope_filter_outside_per_file.yml— 5 rules from outside the PerFileRule dispatch path (file_max_size,no_empty_files,no_symlinks,filename_case,filename_regex) each withscope_filter: { has_ancestor: <manifest> }over the polyglot tree. Catches the same bug class S9 catches for per-file rules, but on the rule set v0.9.8 silently dropped. - 17 unit tests (
scope_filter_narrows) — one per rule — asserting in-scope / out-of-scope narrowing. - 1 e2e regression for bug #2 covering the for_each_dir literal-path bypass shape.
Changed
Section titled “Changed”docs/design/v0.9/scope-filter.mdgains a “Post-v0.9.6 follow-ups” section documenting the v0.9.7, v0.9.9, and v0.9.10 trajectory — captures why this kept happening (separateScope+Option<ScopeFilter>fields without compiler-enforced wiring) and the v0.9.10 structural fix that prevents recurrence.
Performance
Section titled “Performance”- 1k/10k/100k S10 numbers within ±5 % of v0.9.8 baseline (the
per-file iteration shape is unchanged; only the per-file
cost of an in-scope vs out-of-scope check shifts). The
point of S10 is regression detection, not a speedup
headline. Full numbers in
docs/benchmarks/macro/results/linux-x86_64/v0.9.9/.
Internal
Section titled “Internal”- All 393 alint-rules tests + 227 e2e scenarios pass.
cargo clippy --workspace --all-targets --all-features -- -D warningsclean.
[0.9.8] — 2026-05-02
Section titled “[0.9.8] — 2026-05-02”Cross-file dispatch fast paths, round 2. v0.9.5 closed the
for_each_dir × file_exists cliff via the lazy path_set index;
v0.9.8 closes the residual O(D × N) shapes that v0.9.5 didn’t
cover, identified by the comprehensive v0.9.5/.6/.7 macro-bench
backfill (1M S7 was stuck at ~614 s across all three releases).
Performance
Section titled “Performance”- 1M S7 full: 614 s → 15.3 s (40× speedup).
- 1M S7 changed: 618 s → 17.9 s (34× speedup).
- 1M S6 / S8 / S9 within ±5 % of v0.9.7 (no regression on the per-file dispatch fast paths).
- See
docs/benchmarks/HISTORY.mdper-scenario tables for the full v0.9.5 → v0.9.8 trajectory and the bench captures underdocs/benchmarks/macro/results/linux-x86_64/v0.9.8/.
FileIndex::children_of(dir) -> &[usize]— direct children ofdir, by index intoentries. LazyOnceLockbuild mirrors the v0.9.5path_setshape; O(N) build, O(1) per-dir lookup.FileIndex::file_basenames_of(dir) -> impl Iterator<Item = &str>— direct file children’s basenames, borrowed from the underlyingArc<Path>. Used bydir_containsand equivalent.FileIndex::descendants_of(dir) -> impl Iterator<Item = &FileEntry>— recursive, depth-first, lazy. Stack-of-iterators state; does NOT materialise the full subtree.- Debug-only tracing for index builds — emits
phase=index_build kind=<name> elapsed_us=N entries=Mevents behind#[cfg(debug_assertions)]. Release builds compile away both theInstant::now()timer and the event emission entirely (zero overhead for users running release binaries; the events are forxtask bench-scaleprofile runs and contributor debugging). coverage_audit_cross_file_dispatch.rs— soft-fail audit that scans cross-file rule sources for the O(D × N)entries.iter()pattern v0.9.8 closed. Surfaces gaps for the next refactor; doesn’t block unrelated work.
Changed
Section titled “Changed”dir_only_containsevaluates viachildren_ofinstead of the per-dirfor file in ctx.index.files()+is_direct_childfilter. At 1M files / 5K matched dirs, drops the inner loop from 1M iterations to ~200 (the dir’s actual children).dir_containsevaluates viachildren_of+ inline basename extraction. Per-(dir, matcher) drops from O(N) to O(children).evaluate_for_eachinfor_each_dir.rs(shared byfor_each_dir,for_each_file,every_matching_has) gained a literal-path bypass: when a nested rule’spaths:template resolves to a single literal AND the rule opts intoas_per_file(), dispatch viaevaluate_fileagainst the in-index entry directly instead of running the rule’s fullevaluate(ctx)(which would iteratectx.index.files()per call, multiplying the 1M scan by the iteration count). Closes the residual cliff Phase E iter 1 surfaced (S7’severy-lib-has-contentwas 484 s post-Phase-C; this drops it to milliseconds × N iterations).
Internal
Section titled “Internal”- New helpers
nested_spec_single_literalandevaluate_one_per_file_ruleincrates/alint-rules/src/for_each_dir.rs. is_direct_childfree function deleted fromdir_only_contains.rs(no longer needed oncechildren_ofis the dispatch shape).- Memory cost of the new
parent_to_childrenindex: ~500 KB HashMap + ~8 MB usize indices at 1M files / 5K dirs. Negligible vs the existing 1 GBentriesvec. - All 376 alint-rules tests + 226 e2e scenarios + 183 alint-core tests + 11 new walker tests pass.
[0.9.7] — 2026-05-02
Section titled “[0.9.7] — 2026-05-02”Patch release for the v0.9.6 scope_filter: runtime no-op (v0.9.6
shipped the field, the type, and the engine gate but never wired the
parsed filter onto the built rule). Same release also lands the
follow-up audit cleanup (rule-count corrections, design-doc status
flips, alint.org sidebar gaps, release.yml preflight gate) and the
v0.10 LSP design pass with tower-lsp added to workspace deps as a
dormant dependency.
scope_filter:is now honoured at runtime by every per-file rule. v0.9.6 shipped the field onRuleSpec, theScopeFilter/ScopeFilterSpecruntime types, and the engine’s per-file dispatch gate (crates/alint-core/src/engine.rs:run_per_file), but no per-file rule builder threaded the parsed filter onto the built rule —Rule::scope_filter()always returned the trait defaultNone, so the gate was a silent no-op for every rule (including the bundled ecosystem rulesets that motivated the primitive). Each of the 25 per-file rule types now stores a parsedOption<ScopeFilter>, exposes it viaRule::scope_filter(), and gates its rule-major fallback path on it as well (soalint fixrespects the filter the same wayalint checkdoes). NewRuleSpec::parse_scope_filter()helper inalint-core::configkeeps the per-rule build-site change to one line. Integration regression test:crates/alint-rules/tests/scope_filter_integration.rs.
- 5 e2e scenarios under
crates/alint-e2e/scenarios/check/scope_filter/covering positive/negative cases, two-name lists,extends:inheritance, combined per-file + tree-level gates, and a bundledrust@v1dogfood against a polyglot tree. - 10 cross-file
reject_scope_filter_on_cross_fileunit tests, one per cross-file rule that calls the rejection helper (onlypair.rshad one before). docs/design/v0.10/— three-doc design pass for the v0.10 LSP server (lsp_server.md,single_file_reevaluation.md,vscode_extension.md) mirroring the per-cut design-pass shape used for v0.7 and v0.9.tower-lsp = "0.20"added to[workspace.dependencies]ahead of thecrates/alint-lsp/crate landing in v0.10. Dormant — no current member depends on it, socargo builddoesn’t pull it.development/rule-authoring.mdnow reaches alint.org viaxtask docs-export(previously invisible to the sync script).
Changed
Section titled “Changed”release.ymlpreflight gate: new top-level job runs fmt + clippy + test +cargo doc -D warningson the tagged commit; every publishing job (build matrix, release, docker, npm, homebrew, publish-crates)needs:it. Closes the gap where release.yml could fire while ci.yml was failing on the same SHA.README.mdrule-count + family-count corrections (54 → 60 across thirteen families; was missingGit hygieneandPlugin (tier 1)), ruleset count reconciled (line 28 said 19, line 589 said 17 — both now 19), version anchor “v0.7 ships” → “v0.9.6 ships”.docs/design/v0.7/{commented_out_code,markdown_paths_resolve}.md,v0.9/{parallel_walker,scope-filter}.md— status headers flipped from “Design draft” to “Implemented in v0.7.x / v0.9.x” with crate pointers.docs/site/**integration pages — 12 instances ofv0.4.7updated to v0.9.6;concepts/index.md“Four formats” → “Eight formats”;quickstart.mdruleset count “eleven” → “nineteen”.action.ymlformat input description lists all 8 formats.npm/package.jsonversion 0.5.10 → 0.9.7 (was four releases stale in the checked-in source; release.yml’s publish step rewrites this from the tag at publish time, but a stale value misled readers).action-selftest.ymlpinned-version test 0.3.1 → v0.9.6 (six minors stale)..alint.ymldogfood factis_rust→has_rustto match the v0.9.6 bundled-fact rename.
[0.9.6] — 2026-05-02
Section titled “[0.9.6] — 2026-05-02”Closes the v0.9 cut with the scope_filter: primitive — closest-
ancestor manifest scoping for per-file rules, designed to make the
bundled ecosystem rulesets (rust@v1, node@v1, python@v1,
go@v1, java@v1) correctly handle nested packages in polyglot
monorepos.
scope_filter: { has_ancestor: <list> }rule-schema field (per-file rules only). The engine walksPath::parent()upward from the file and consults the v0.9.5 path-index at each directory; first-match-wins on the upward walk gates the rule per-file. The file’s own directory counts as an ancestor. Cross-file rules (pair,for_each_dir,file_exists, etc.) rejectscope_filter:at build time. Design:docs/design/v0.9/scope-filter.md.alint_core::ScopeFilter+ScopeFilterSpecruntime types,Rule::scope_filter()trait method (defaultNone). Net additive — rules that don’t override the trait method see no behaviour change. Newalint_core::reject_scope_filter_on_cross_filehelper + 11 cross-file rule build-time guards.- Bench-scale S9 — nested polyglot monorepo scenario.
extends: rust + node + pythonovercrates/+packages/+apps/ecosystem subtrees; newalint_bench::tree::generate_nested_polyglot_monorepohelper. 100k S9 = 688 ms ± 13 ms on the published baseline machine. - Rule-authoring docs:
scope_filter:section inrule-authoring.mdwith the bundled-ruleset migration recipe.
Changed
Section titled “Changed”- Bundled ecosystem rulesets renamed
is_*→has_*for the ecosystem facts. Five rulesets:is_rust→has_rust,is_node→has_node,is_python→has_python,is_go→has_go,is_java→has_java. The new name reads better with the broadened heuristic (has_<ecosystem>is true if any ecosystem manifest exists anywhere in the tree, not just at the root). No backwards-compat alias period — repos that override these facts in their ownfacts:blocks need to update the identifier; tree-level gates that referenced the fact name (when: facts.is_rustetc.) need the same update. - Bundled ecosystem facts broadened with
**/<manifest>patterns —[Cargo.toml]→[Cargo.toml, "**/Cargo.toml"]and analogously for the other four. Polyglot monorepos with no root manifest now correctly fire the ruleset. - Per-file content rules in the five ecosystem rulesets get
scope_filter:added (e.g.scope_filter: { has_ancestor: Cargo.toml }onrust-sources-final-newline). Filename-based rules (filename_case, etc.) keep their existingpaths:globs;scope_filter:is supported onPerFileRule-trait rules only.
Performance
Section titled “Performance”scope_filter: adds ≤ 150 ns per (file × rule) ancestor-walk
step on the v0.9.5 path-index — sub-millisecond at K100 across
five rules. Same-machine pre/post measurement on the engine
commit (7b080a0) shows the null-default Rule::scope_filter()
trait method is unmeasurable on rules that don’t override it.
See docs/benchmarks/investigations/2026-05-scope-filter-baseline-drift/
for the dispositive A/B and the lesson on machine-state drift in
cross-version bench comparisons.
[0.9.5] — 2026-05-01
Section titled “[0.9.5] — 2026-05-01”Reopens v0.9 with cross-file dispatch fast paths, the test/coverage floor that prevents the same class of regression from slipping by again, and a benchmark-docs reorganisation that makes the perf story discoverable from a single entry point. Six sub-phases, all in this release:
| Sub-phase | What it ships |
|---|---|
| .5 | Cross-file dispatch fast paths (lazy path-index on FileIndex + literal-path fast paths in file_exists / structured_path / iter.has_file) |
| .6 | Coverage audits (pass/fail symmetry, bundled-ruleset coverage, git-mode symmetry) |
| .7 | 16 new coverage scenarios filling the audit punch list |
| .8 | Bench-scale S6 / S7 / S8 + generate_git_monorepo helper |
| .9 | Rule-authoring workflow doc; alint already self-lints via action-selftest.yml |
| Reorg | Benchmark documentation reorganisation: docs/benchmarks/{micro,macro,investigations,archive}/ layout, per-version results, top-level README.md / HISTORY.md / RUNNING.md, new xtask publish-benches subcommand |
No new rule kinds, formatters, or schema changes; output bytes byte-identical to v0.9.4 across all 8 formatters.
Full design:
docs/design/v0.9/coverage-and-dogfood.md.
Workflow: docs/development/rule-authoring.md.
Bench layout: docs/benchmarks/README.md.
Performance (.5)
Section titled “Performance (.5)”xtask gen-monorepo --size 1m, S3 = oss-baseline + rust + monorepo + cargo-workspace, hyperfine --warmup 1 --runs 3:
| Cell | v0.9.4 | v0.9.5 | Speedup |
|---|---|---|---|
1m S3 full | 731.856 s | 11.194 s ± 0.154 | 65.4× |
1m S3 changed | 724.362 s | 6.728 s ± 0.059 | 107.7× |
100k S3 engine total | 10.7 s | 186 ms | 57× |
10k S3 engine total | 226 ms | 23 ms | 9.8× |
Also ~50–80× faster than the published v0.5.6 baseline (the
fastest 1M S3 numbers ever shipped). Full numbers under
docs/benchmarks/macro/results/linux-x86_64/v0.9.5/.
Engine (.5)
Section titled “Engine (.5)”alint-core::walker::FileIndexgains a lazyOnceLock<HashSet<Arc<Path>>>keyed on every file (non- dir) entry. Built on first call tocontains_fileorfile_path_set; concurrent first-call safe via OnceLock.- New
FileIndex::contains_file(&Path) -> bool— the canonical O(1) “does this exact relative path exist?” query.find_file(&Path)keeps its signature but does the O(1) check first; the linear&FileEntryscan only runs on a hit. NewFileIndex::from_entries(Vec<FileEntry>)constructor for tests/benches. Engine::runaddstracing::info!per-phase + per-cross- file-rule wall-time emission with stable structured fields (phase,elapsed_us, optionalrules/files). Drive withALINT_LOG=alint_core=info. Production runs pay nothing — events fire only when info is enabled for this target.- Per-file dispatch loop in
Engine::run_per_fileswitches fromindex.files().par_bridge()toindex.entries.par_iter().filter(|e| !e.is_dir)— the native RayonParallelIteratorover the underlyingVecuses work-stealing slabs instead of par_bridge’s Mutex- guarded channel. The applicable-rule inner loop carriesentry_idxdirectly instead of re-resolving via O(L)positionlookup per applicable rule per file.
Rules (.5)
Section titled “Rules (.5)”file_exists— when everypaths:entry is a literal relative path AND the rule is notgit_tracked_only, the build path pre-extracts the literals intoVec<PathBuf>; evaluate usescontains_fileper literal. Glob/exclude/git-tracked patterns keep the existing O(N) scope-match scan.structured_path(the shared impl behind*_path_{equals,matches}for json/yaml/toml) — same literal-paths fast path. Joinsctx.root + literaldirectly for thestd::fs::readrather than callingfind_file.iter_has_file(theiter.has_file("…")when_iter:builtin) — when the pattern is a literal filename, computesiter.path.join(pattern)and consultscontains_file. Glob patterns (**/*.bzl) keep the scope-match scan.pair::evaluateswapsfind_file(&p).is_some()for the cheapercontains_file(&p).
Coverage audits (.6)
Section titled “Coverage audits (.6)”Three new tests under crates/alint-e2e/tests/:
coverage_audit_pass_fail.rs— every canonical rule kind has at least one scenario where it fires AND at least one where it stays silent. Ships with a smallNATIVE_FIRES_ALLOWLISTfor kinds whose firing case can’t be expressed in YAML today (executable_bit,executable_has_shebang,no_symlinks,git_blame_age,git_commit_message); each entry points at the native Rust integration test that DOES cover the firing path.coverage_audit_bundled_rulesets.rs— everycrates/alint-dsl/rulesets/v1/**/*.ymlis referenced by at least one well-formed scenario AND at least one ill-formed scenario.coverage_audit_git_modes.rs— the three pure-git rule kinds (git_blame_age,git_commit_message,git_no_denied_paths) need both an in-repo and an outside-git scenario.coverage_audit_bench_listing.rs(soft, always passes) — emits aneprintln!listing of rule kinds absent from any bench scenario.
Coverage scenarios (.7)
Section titled “Coverage scenarios (.7)”16 new scenarios filling the v0.9.6 audit punch list:
structured/{json,toml,yaml}_path_{equals,matches}_silent_when_satisfied.yml(6)bundled/{agent_context,agent_hygiene,ci_github_actions,docs_adr,hygiene_lockfiles,hygiene_no_tracked_artifacts,tooling_editorconfig}_well_formed_passes.yml(7)bundled/{agent_context_stub,agent_hygiene_scratch_doc}_flagged.yml(2)git/git_no_denied_paths_silent_outside_git.yml(1)
E2e scenario count: 205 → 221. All four coverage_audit_*
tests green by default.
Bench-scale extension (.8)
Section titled “Bench-scale extension (.8)”Three new perf-shape scenarios; S1–S5 unchanged:
- S6 — Per-file content fan-out: 13 content rules over
**/*.rs. - S7 — Cross-file relational:
pair,unique_by,for_each_dir,for_each_file,dir_only_contains,every_matching_has. - S8 — Git-tracked overlay: S3 reshape with
.git/+git_no_denied_paths+git_tracked_only.
New alint_bench::tree::generate_git_monorepo helper for
S8 — runs git init && git add -A && git commit on a
materialised monorepo tree. xtask enum extends to
Scenario::S1..S8; default --scenarios stays S1,S2,S3.
Workflow doc + dogfood scope (.9)
Section titled “Workflow doc + dogfood scope (.9)”- New
docs/development/rule-authoring.md— the four-step process every new rule / bundled ruleset / alias goes through; documents the two-layer enforcement (alint check . + Rust audits), family conventions, scenario shape, the native-test allowlist for testkit gaps, and the concrete audit failures contributors will hit. - The aspirational declarative-coverage rules in
.alint.yml(e.g. “every rule-source file has ≥1 e2e scenario”) need a rule kind alint doesn’t yet have: aggregate “does any file in this scope contain pattern X?” semantics. Today’sfile_content_matchesis per-file; the right primitive is deferred to v0.10+. - alint still self-lints in CI today via the existing
.alint.ymlandaction-selftest.yml; v0.9.9 just formalises the workflow contributors should follow.
Benchmark documentation reorganisation
Section titled “Benchmark documentation reorganisation”The docs/benchmarks/ tree was shaped accidentally over four
release cycles and had four different per-version layouts.
This release lands a clean micro / macro / investigations /
archive split:
- Top-level —
README.md(entry point with current numbers),HISTORY.md(per-release perf changelog),RUNNING.md(how-to-run),METHODOLOGY.md(focused rewrite of the why-behind-the-split). micro/— criterion micro-benches with a per-bench catalogue and per-versionresults/<arch>/<version>/snapshots.macro/— hyperfine bench-scale with the S1–S8 catalogue, tool matrix, and per-versionresults/<arch>/<version>/snapshots.investigations/— ad-hoc deep-dives (wasdocs/perf/); the v0.9.5 cliff investigation lives at2026-05-cross-file-rules/.archive/— superseded snapshots (v0.1 single-file era, v0.9 development-cycle baselines + per-phase outputs). Read-only by contract.
xtask bench-scale --out default fixes the long-standing
footgun where every run silently overwrote
docs/benchmarks/v0.5/scale/<arch>/; new default is
docs/benchmarks/macro/results/<arch>/v<workspace-version>/,
read from the workspace Cargo.toml so it tracks the
version as it bumps. New xtask publish-benches snapshots
target/criterion/ into the per-version published
location with an optional --trim flag dropping criterion’s
HTML reports.
Tooling
Section titled “Tooling”- New
xtask gen-monorepo --size {1k|10k|100k|1m} --out PATHmaterializes a persistent monorepo tree at a fixed path. Used by the perf-investigation flow to skip 5+ minutes of tree-gen between profile runs. Size labels matchbench-scale’s internal monorepo shape, so trees are byte-identical to the published bench corpus.
0.9.4 — 2026-04-30
Section titled “0.9.4 — 2026-04-30”Mechanical follow-up to v0.9.3: migrates 16 of the remaining ~22 per-file content rules to opt into the file-major dispatch path. After this cut, every per-file rule that reads file content benefits from the v0.9.3 read coalescing whenever multiple content rules share a scope. No new rule kinds, formatters, or subcommands; every v0.9.3 config runs unchanged. Output bytes from all 8 formatters are byte-identical to v0.9.3.
Performance
Section titled “Performance”Each migrated rule grows a PerFileRule impl alongside
its existing Rule impl; the existing Rule::evaluate
body becomes a thin wrapper that walks the index, reads
each file, and delegates to evaluate_file (used by
alint fix and test harnesses that bypass the engine).
Migrated (16 rules):
Content-pattern (5): file_content_matches,
file_content_forbidden, file_header, file_footer,
file_shebang.
File-property (5): file_max_lines, file_min_lines,
file_hash, file_is_ascii, file_is_text (declares
max_bytes_needed: TEXT_INSPECT_LEN).
Unicode-safety (4): no_bom (declares
max_bytes_needed: 4; rule-major path uses the bounded
read_prefix_n helper), no_bidi_controls,
no_zero_width_chars, no_merge_conflict_markers.
Complex-content (2): markdown_paths_resolve (uses
ctx.index in evaluate_file to look up backticked
paths), structured_path (which backs json_path_*,
yaml_path_*, toml_path_*).
Deliberately not migrated
Section titled “Deliberately not migrated”file_max_size/file_min_size— metadata-only checks, nofs::readto coalesce.json_schema_passes— emits one repository-level violation when the schema fails to compile; the per- file dispatch path would multiply that 1 schema-error into N file-errors.
Build / test
Section titled “Build / test”- Workspace test suite green (1000+ tests across 7 crates).
- Pre-v0.9.4 baseline frozen at
docs/benchmarks/archive/v0.9-development-baselines/baseline-v0.9.3/criterion/. v0.9.4 numbers atdocs/benchmarks/micro/results/linux-x86_64/v0.9.4/criterion/.
0.9.3 — 2026-04-30
Section titled “0.9.3 — 2026-04-30”Third phase of the v0.9 engine-optimization cut: the per-file dispatch flip plus the per-rule scanning conversions deferred from v0.9.2. No new user-visible rule kinds, formatters, or subcommands; every v0.9.2 config runs unchanged. Output bytes from all 8 formatters are byte-identical to v0.9.2.
Performance
Section titled “Performance”- Per-file dispatch flip. New
PerFileRuletrait next toRule; rules opt in viaRule::as_per_file(&self) -> Option<&dyn PerFileRule> { None }. The engine partitions entries: per-file rules run under a file-major loop that reads each matched file once and dispatches to every applicable rule against the same byte buffer. Cross-file rules (pair,for_each_dir,every_matching_has,unique_by,dir_*,file_*existence rules,git_no_denied_paths,git_commit_message) keep the rule-majorpar_iterpath. Coalesces N reads of one file across N rules sharing it. - Line-oriented rules migrated to
PerFileRule+ byte- slice scanning (deferred from v0.9.2):no_trailing_whitespace(skips the redundant UTF-8 validation pass viabytes.split(|&b| b == b'\n')+last()byte check),final_newline(boundedlast()byte check;max_bytes_needed: Some(1)),line_endings(existing byte-level walker preserved throughevaluate_file),max_consecutive_blank_lines,indent_style,line_max_width(the latter three keep per-line UTF-8 validation where character counts / whitespace classifiers need&str). - Bounded prefix/suffix reads on byte-pattern rules
(deferred from v0.9.2):
file_starts_withreads only the firstprefix.len()bytes;file_ends_withreads only the lastsuffix.len()bytes via a newread_suffix_nhelper that seeks from EOF. Both opt intoPerFileRuleand declaremax_bytes_needed.executable_has_shebangandshebang_has_executableswitch their rule-major paths to bounded 2-byte reads (#!); they deliberately skip the dispatch-flip path because they needmetadata().permissions()to short-circuit on non-+xfiles before any read happens.
Internal
Section titled “Internal”- New
crates/alint-rules/src/io.rshelpers:read_prefix_n(path, n)andread_suffix_n(path, n). Rule::evaluatefor migrated rules becomes a thin wrapper that walks the index, reads each file, and delegates toevaluate_file— used byalint fix(sequential filesystem mutation rules out coalesced reads there) and by test harnesses that bypass the engine.Engine::runaggregates per-file violations back into per-ruleRuleResults preserving each rule’s metadata (level / policy_url / is_fixable). Final assembly walksself.entriesin order so output remains deterministic.when:evaluates once per per-file rule before the file loop (not per file) — verdict is independent of the file sinceiterisNoneat the engine level.
- Four new engine tests exercising the dispatch flip:
dispatch_flip_routes_per_file_rule_through_file_major_loop,dispatch_flip_aggregates_multiple_per_file_rules(verifies aggregation buckets violations per rule across N rules sharing one scope),dispatch_flip_passes_when_no_violations(passing rules omitted),dispatch_flip_preserves_cross_file_rules_unchanged.
Migration scope
Section titled “Migration scope”8 of ~30 per-file content rules migrated to PerFileRule
in v0.9.3 (the line-oriented + bounded-read families
deferred from v0.9.2). The remaining ~22 content rules
(file_content_matches, file_content_forbidden,
file_header, file_footer, file_max_lines,
file_min_lines, file_max_size, file_min_size,
file_hash, file_is_ascii, file_is_text,
file_shebang, json_path_* / yaml_path_* /
toml_path_*, json_schema_passes, no_bom,
no_bidi_controls, no_zero_width_chars,
no_merge_conflict_markers, markdown_paths_resolve,
commented_out_code) keep the rule-major path. They’ll
migrate incrementally in v0.9.4 as a mechanical follow-up;
v0.9.3 ships the engine restructure + 8-rule reference
implementation that proves the shape works.
Out of scope (deferred)
Section titled “Out of scope (deferred)”- Engine-side honouring of the
max_bytes_needed()hint — v0.9.3 reads whole files; a future pass could bound the read when every applicable rule’s hint sums to less than the file size. Hint is captured on the trait so rules can declare it now.
Build / test
Section titled “Build / test”- Workspace test suite green (1000+ tests across 7 crates).
- Pre-v0.9.3 baseline frozen at
docs/benchmarks/archive/v0.9-development-baselines/baseline-v0.9.2/criterion/. v0.9.3 numbers atdocs/benchmarks/archive/v0.9-development-phases/v0.9.3-dispatch-flip/criterion/.
0.9.2 — 2026-04-30
Section titled “0.9.2 — 2026-04-30”Second phase of the v0.9 engine-optimization cut: the type-level memory pass. No new user-visible rule kinds, formatters, or subcommands; every v0.9.1 config runs unchanged. Output bytes from all 8 formatters are byte-identical to v0.9.1.
Performance
Section titled “Performance”Arc<Path>onFileEntry::pathandViolation::path. The walker builds oneArc::fromper file; everyViolationreferencing that file now shares the same allocation viaArc::clone(atomic refcount bump) instead of copying the path bytes. At 100k violations that’s ~100k saved path allocations. Canonical caller pattern is now.with_path(entry.path.clone()).Arc<str>onRuleResult::rule_idandpolicy_url. Set once per rule by the engine, shared across N violations of that rule (one alloc per rule instead of per violation).Cow<'static, str>onViolation::message. Per- match templated messages (format!("line {n}:…")) live asCow::Owned(String)(no change in cost); fixed messages can live asCow::Borrowed("…")for a future audit pass. Public API preserves&v.message: &strviaCow::as_ref.
Internal
Section titled “Internal”Violation::with_path(impl Into<Arc<Path>>)acceptsPathBuf,&Path,Box<Path>, andArc<Path>. Tests using a string literal migrate towith_path(Path::new("…")).Violation::new(impl Into<Cow<'static, str>>)accepts&'static str,String, andCow<'static, str>. Borrowed&strfrom non-static lifetimes pass via.to_string().- ~70 rule call sites and 8 output formatter structs
migrated. Output formatters that previously held
Option<PathBuf>/Stringinternally now borrowOption<&'a Path>/&'a strfrom the source where possible (json, agent, markdown), or convert at the serialise boundary via.to_string()/.as_deref()where the format requires owned. humanformatter:BTreeMapkeyed onOption<Arc<Path>>instead ofOption<PathBuf>— sort order unchanged (Arcsorts via Path’s Ord impl). markdownformatter: bucket map switches toBTreeMap<&'a Path, …>— eliminates the per-violation path clone the bucketing did before.unique_byandno_case_conflictsgroup onArc<Path>internally instead ofPathBuf.
Scope deferred to v0.9.3
Section titled “Scope deferred to v0.9.3”The original v0.9.2 design scope included per-rule
byte-slice scanning conversions (the 6 line-oriented rules:
no_trailing_whitespace, final_newline, line_endings,
max_consecutive_blank_lines, indent_style,
line_max_width) and bounded prefix/suffix reads (the 4
first-/last-bytes-only rules: file_starts_with,
file_ends_with, executable_has_shebang,
shebang_has_executable). Both moved to v0.9.3 because
the per-file dispatch flip hands each rule a pre-loaded
&[u8] slice — bundling the conversions there avoids
touching the same rule bodies twice. See
docs/design/v0.9/memory_pass.md and
docs/design/v0.9/dispatch_flip.md for the rationale.
Build / test
Section titled “Build / test”- Workspace test suite green (1000+ tests across 7 crates).
target-baseline*/added to.gitignorefor the baseline-bench scratch dirs the v0.9.x flow uses.- Pre-v0.9.2 baseline frozen at
docs/benchmarks/archive/v0.9-development-baselines/baseline-v0.9.1/criterion/for same-day per-phase delta comparisons. v0.9.2 numbers atdocs/benchmarks/archive/v0.9-development-phases/v0.9.2-memory-pass/criterion/.
0.9.1 — 2026-04-30
Section titled “0.9.1 — 2026-04-30”First phase of the v0.9 engine-optimization cut. Parallel walker. No user-visible rule kinds, formatters, or subcommands change; every v0.8 config runs unchanged.
Performance
Section titled “Performance”-
Walker is now parallel.
crates/alint-core/src/walker.rsswitches fromWalkBuilder::build()(single-threaded iterator) toWalkBuilder::build_parallel()driving a per-threadParallelVisitorthat accumulatesFileEntrys in a thread-localVecand merges viaDrop. A deterministic post-sort by relative path (entries.sort_unstable_by(|a, b| a.path.cmp(&b.path))) restores the byte-identical output that snapshot tests + formatters depend on. Walker output is the same as v0.8.2 for any input tree; only the intermediate execution is parallel.Numbers vs the captured pre-v0.9 baseline at
docs/benchmarks/archive/v0.9-development-baselines/baseline-pre/criterion:bench before after delta walker/1000052.25 ms 18.67 ms -64.26% walker/10008.85 ms 5.25 ms -40.62% walker/1001.62 ms 2.62 ms +61.18% The walker/100 regression is the small-N thread-spawn- overhead trade the design doc anticipated — 1ms of overhead at the 100-file size, dwarfed by the 33.6ms saved at 10k files. v0.7.0 gate stays green; max delta -4.77% on
rule_engine/1000.
Added — design + benchmark snapshots
Section titled “Added — design + benchmark snapshots”docs/design/v0.9/— design pass for the v0.9 cut. README + per-sub-theme drafts for the parallel walker (this release), memory-footprint pass (v0.9.2), and per-file-rule dispatch flip (v0.9.3). Same shape asdocs/design/v0.7/(Problem → Surface → Semantics → False-positive surface → Implementation → Tests → Open questions).docs/benchmarks/archive/v0.9-development-baselines/baseline-pre/— frozen criterion snapshot captured onbec0cf4(the v0.9 starting point) so subsequent v0.9 phases have a same-day, same- hardware “before” reference to measure deltas against.docs/benchmarks/archive/v0.9-development-phases/v0.9.1-parallel-walker/— post-merge bench snapshot for this release, with a README documenting the deltas vs both the v0.7.0 floor (the gate) and the pre-v0.9 baseline (the per-phase delta).
- Three new walker unit tests:
walk_output_is_deterministic_across_runs(two runs over the same tree produce equalFileIndexs — guard against a forgotten post-sort),walk_output_is_alphabetically_sorted(catches a sort that silently breaks), andwalk_handles_thousand_files(concurrency stress: exactly 1k entries, sorted, no duplicates / drops).
Internal
Section titled “Internal”walk()body factored intobuild_walk_builderandresult_to_entryprivate helpers so the visitor closure stays small. No public API change —walk(root, opts) -> Result<FileIndex>is unchanged.
Out of scope (v0.9.2 / v0.9.3 sub-themes)
Section titled “Out of scope (v0.9.2 / v0.9.3 sub-themes)”- Memory-footprint pass (Cow audit on
Violation/RuleResult/Report; line-slice scanning on the line- oriented rules; dhat workflow). Designed indocs/design/v0.9/memory_pass.md; ships as v0.9.2. - Per-file-rule dispatch flip (new
PerFileRulesub-trait; file-major engine loop; coalesce reads when N rules share one file). Designed indocs/design/v0.9/dispatch_flip.md; ships as v0.9.3.
0.8.2 — 2026-04-29
Section titled “0.8.2 — 2026-04-29”Second hotfix on v0.8.0; supersedes v0.8.1. Same code as v0.8.0 / v0.8.1; manifest + script changes only.
v0.8.1 reverted publish = false on the three internal
crates (the v0.8.0 audit-residue bug) but the next publish
attempt failed at alint-dsl@0.8.1 with “no matching
package named alint-rules found”. Cargo publish validates
dev-dependencies too, and alint-dsl carries
alint-rules as a [dev-dependencies] entry (added by
v0.8.5’s fixture-completeness test). With the original
publish order (alint-core → alint-dsl → alint-rules → alint-output → alint), alint-dsl packages before
alint-rules is on crates.io.
v0.8.1 thus shipped to GitHub Releases, npm, Homebrew,
Docker, and alint-core@0.8.1 on crates.io — but
alint-dsl@0.8.1 and downstream (alint-rules, alint-output,
alint) all failed.
ci/scripts/publish-crates.sh— re-ordered theCRATESlist soalint-rulesandalint-outputpublish beforealint-dsl:alint-core → alint-rules → alint-output → alint-dsl → alint. The new order satisfies every dep direction including dev-deps.cargo install alintagainst v0.8.2 resolves cleanly. v0.8.0 and v0.8.1 remain partially published on crates.io; consumers should pin to v0.8.2 orlatest.
0.8.1 — 2026-04-29 (partially published — superseded by v0.8.2)
Section titled “0.8.1 — 2026-04-29 (partially published — superseded by v0.8.2)”Reverted publish = false on alint-dsl/rules/output to fix
v0.8.0’s crates.io publish failure. Pipeline progressed
further (alint-core@0.8.1 published) but failed on
alint-dsl@0.8.1 because of an unrelated dep-order issue —
see v0.8.2 above for the resolution. Use v0.8.2 instead.
Changed (carried into v0.8.2)
Section titled “Changed (carried into v0.8.2)”alint-dsl/alint-rules/alint-output— revertedpublish = false. Their manifests now publish to crates.io alongsidealint-coreandalint. The crates’ descriptions still read “Internal: Not a stable public API” — the published status is a load-bearing accident of cargo’s resolver, not an invitation to use them. The comment block on each manifest documents the constraint so a future audit doesn’t repeat the trap.
0.8.0 — 2026-04-29
Section titled “0.8.0 — 2026-04-29”Note: this tag shipped to GitHub Releases, npm, Homebrew, and Docker, plus
alint-coreon crates.io — but thealintbinary’s crates.io publish failed (see v0.8.1 above for the root cause + fix). Forcargo install alintuse v0.8.1+; all other distribution channels for v0.8.0 are usable.
The v0.8 cut — five sub-phases (v0.8.1 → v0.8.5) building the comprehensive test/bench/rot-prevention foundation that v0.9’s engine optimization needs to land safely. No new user-facing rule kinds, formatters, or subcommands; entirely internal. Schema-compatible: every v0.7 config runs unchanged.
Added — test infrastructure
Section titled “Added — test infrastructure”- Cross-platform CI —
.github/workflows/cross-platform.ymlrunscargo test --workspace --lockedon macOS-arm64 and Windows-x86_64 GitHub-hosted runners on every PR/push. Caught two real production bugs the Linux lane missed (glob mixed-separator + bench-compare key separator, both fixed before merge). - Coverage workflow —
.github/workflows/coverage.ymlrunscargo llvm-covover the workspace (xtask excluded as dev tooling). Enforces 85% line-coverage floor; workspace currently at 90.57%. LCOV + HTML uploaded as artifacts; opt-in Codecov upload viaCODECOV_TOKEN. - Mutants nightly —
.github/workflows/mutants.ymlrotates one workspace crate per night through cargo-mutants. Surfaces unkilled mutants as test-coverage gaps via uploaded artifacts. - Comprehensive bench suite —
single_file_rules.rs,cross_file_rules.rs,structured_query.rs,output_formats.rs,fix_throughput.rs,blame_cache.rs,dsl_extends.rsundercrates/alint-bench/benches/. Plus hyperfine scenarios S4 (agent-hygiene) + S5 (fix-pass) and a walker parallelism baseline captured for v0.9.
Added — rot prevention
Section titled “Added — rot prevention”xtask bench-compare— diffs twotarget/criterion/trees and gates on regressions past--threshold(default 10%). PR-time perf-regression gate.- JSON report schemas —
schemas/v1/check-report.jsonandschemas/v1/fix-report.jsonlock the public contract foralint check --format jsonandalint fix --format json. Cross-formatter and fix-report tests validate output against the schemas. - Fixture-completeness test in alint-dsl asserts the
canonical
all_kinds.yamlfixture exercises every registered rule kind (now 70, up from 18). - Scenario-coverage audit in alint-e2e asserts every registered rule kind has at least one e2e scenario.
- Default-option snapshot in alint-dsl captures every
rule’s resolved Debug output into a checked-in snapshot.
Catches silent shifts to
#[serde(default = ...)]values (e.g.commented_out_code::min_lines). - CLI flag inventory snapshot captures the full
per-subcommand flag list separately from
--helptext to catch flag-name drift independently of help-text edits. - Cross-formatter snapshot test —
crates/alint-output/tests/cross_formatter.rs. Same fixed Report rendered through all 8 output formatters with 13 invariant tests (rule_id presence, JSON parses, schema_version key, SARIF driver completeness, etc.). - Pty integration test —
crates/alint/tests/pty_color.rs(Unix-only) covers the--color=autoresolution branch trycmd’s pipe-only spawn can’t reach. .gitattributespins LF for byte-stable test artifacts (snapshots, trycmd.stdout/.stderr/.toml, YAML fixtures) so Windows checkouts don’t introduce CRLF drift.
Added — coverage breadth
Section titled “Added — coverage breadth”- ~155 new rule-kind unit tests across 34 previously under-covered rule kinds (build / options / evaluate fires / evaluate silent / edge cases — the standard quintet).
- Fail-variant e2e scenarios for the 3 pass-only
unix-metadata rules (
no_symlinks,executable_bit,executable_has_shebang). - E2E scenarios for the 4 zero-e2e rules
(
json_schema_passes,git_no_denied_paths,git_commit_message,command). - Integration tests under
crates/alint-rules/tests/for the shell-out rules:git_no_denied_paths,git_commit_message,command(mirrors v0.7.3’sgit_blame_ageintegration-test pattern). - alint-core internal tests —
engine.rs,walker.rs,registry.rs,report.rs,error.rs,level.rs,scope.rs,config.rs,rule.rsall gained unit tests (had 0 pre-v0.8.2). - alint-dsl edges — extends-chain cycle detection,
diamond inheritance,
extends:filter validation, nested-config path-prefix rewriting,.alint.d/merge order determinism, HTTPS timeout / size-cap enforcement, SRI algorithm-mismatch errors, template-instantiation edge cases.
Added — CLI surface
Section titled “Added — CLI surface”- trycmd 33 → 56 cases — stderr snapshots for every
error path, per-subcommand
--helpsnapshots,--color × NO_COLOR × CLICOLOR_FORCEmatrix,--progress × TTY/non-TTYmatrix.
Fixed — production bugs surfaced by the new lanes
Section titled “Fixed — production bugs surfaced by the new lanes”- DSL nested-config glob mixed separators — on
Windows,
discover_nestedjoinedrel_dir.to_string_lossy()(native separators) with the user’s pattern (/), producing globs likepackages\foo/README.mdthat globset couldn’t match. Nested-config rules silently no-op’d on Windows. Now the prefix is normalised to/before joining. - xtask bench-compare key separators — comparison keys
carried native separators, breaking cross-OS tree
comparison (
target/criterion-mainfrom a Linux runner vs.target/criterionfrom a Windows checkout). Now normalised to/. - CLICOLOR_FORCE on
--color=auto— theautoresolution path didn’t honorCLICOLOR_FORCEbefore passing the choice to anstream. Fixed via aColorChoice::resolve()pre-pass.
Internal — release safety
Section titled “Internal — release safety”alint-dsl,alint-rules,alint-outputnow carrypublish = false. Their descriptions have always read “Internal: Not a stable public API”; the manifest now matches. Historical v0.5.x → v0.7.0 versions remain on crates.io for compatibility; new versions are gated off.ci/scripts/publish-crates.shreduced to publishing onlyalint-core(public library) andalint(binary). The other three are internal implementation detail.
0.7.0 — 2026-04-28
Section titled “0.7.0 — 2026-04-28”Closes the v0.7 cut. Three new rule kinds and two new
subcommands targeting agent-driven development workflows
specifically. Where v0.6 was config-only — bundled rulesets
composed from existing primitives — v0.7 extends the engine
itself: markdown_paths_resolve / commented_out_code /
git_blame_age are new rule kinds with their own heuristic
surfaces, and alint suggest / alint export-agents-md
add the first new top-level subcommands since alint init
in v0.5.4.
Schema-compatible: every v0.6 config runs unchanged. The new
rule kinds parse new YAML shapes that older configs simply
don’t use; version: 1 continues to cover them.
The two subcommands close the cold-start adoption gap for
agent-heavy repos. alint suggest scans for known
antipatterns and proposes rules to catch them — its
stale-TODO suggester eats git_blame_age’s own dogfood.
alint export-agents-md makes alint the single source of
truth for AGENTS.md / CLAUDE.md / .cursorrules
directive blocks: the agent reads what alint enforces, no
duplicate config to maintain.
-
alint export-agents-mdsubcommand — generate (or maintain a section of)AGENTS.mdfrom the active rule set, so the agent’s pre-prompt directives stay in sync with the lint config. Closes the “67% of teams maintain duplicate configs between AGENTS.md and CI lint” gap by making alint the single source of truth.Two output formats:
markdown(default) — section-per-severity bullet list shaped to drop into anAGENTS.md/CLAUDE.md/.cursorrulesdirective block.json— stable shape behindschema_version: 1, parallel tosuggest’s envelope, suitable for agent consumption.
Three output destinations:
- stdout (default) — pipe / paste by hand.
--output PATH— overwrite-create the named file.--inline --output PATH— splice the generated section between<!-- alint:start -->and<!-- alint:end -->markers in the target file. The canonical workflow: humans own the prose outside the markers; alint owns the directive block between them. Re-runs are idempotent — when the existing between-markers content already matches what we’d generate, the file isn’t rewritten.
Inline mode auto-initialises markers when the target file lacks them: the section is appended to the end with a stderr warning, and subsequent runs splice in place. Multiple-pair / orphan-marker shapes hard-error rather than silently overwrite — splicing is destructive and ambiguity surfaces explicitly.
Severity grouping:
Errors (commit will fail),Warnings (review before merge), optionalInfo (informational nudges)(gated by--include-info— info-level rules are nudges, not directives, and clutter the agent’s context window unless you really want them). Rules without an explicitmessage:fall back to a synthesised ”rule” line so no directive is silently dropped. Stable byte-for-byte output across runs: line endings always
\n, sort by severity desc + rule_id asc within each section. Re-running--inlineproduces identical bytes; round-trip identity short-circuits the write.Terminal window alint export-agents-md # stdoutalint export-agents-md --output AGENTS.md # write a filealint export-agents-md --inline --output AGENTS.md # splice in placealint export-agents-md --format=json # stable JSON for agentsalint export-agents-md --include-info # include info-level rulesalint export-agents-md --section-title "Lint policy" # custom headingDesign doc:
docs/design/v0.7/alint_export_agents_md.md. -
alint suggestsubcommand — scan the repo for known antipatterns and propose rules that would catch them. Acts as a smartalint initfor retrofitting alint onto a long- running, agent-heavy codebase. Three output formats:--format=human(default): colourised proposal table with optional--explainevidence block.--format=yaml: paste-ready config snippet (extends:rules:).
--format=json: stable shape behindschema_version: 1for agent consumption.
Three suggester families ship in v0.7.4:
- Bundled-ruleset — high-confidence proposals for
oss-baseline@v1(always) plus per-language (rust@v1/node@v1/python@v1/go@v1/java@v1) and workspace-flavour overlays based on the same ecosystem detectionalint inituses. - Antipattern — medium-confidence proposal of
extends: agent-hygiene@v1when the repo contains backup-suffix files, scratch / planning docs at root (PLAN.md,NOTES.md, …), orconsole.log-style debug residue in non-test JS / TS source.tests/,__tests__/,fixtures/, andsnapshots/paths are skipped automatically. - Stale-TODO — medium-confidence
git_blame_agerule proposal when ≥ 3TODO/FIXME/XXX/HACKmarkers are older than 180 days. Eats our own v0.7.3 dogfood.
--include-bundledoverrides the already-covered filter (which would otherwise skip arust@v1proposal when the user’s existing.alint.ymlalready extends it).--confidence={low,medium,high}raises the floor on what surfaces. The command always exits 0 unless the scan itself fails —suggestis exploration, not a CI gate. -
--progress={auto,always,never}+-q/--quietglobal flags — controls stderr-side progress for slow commands. Strict stream split: structured stdout (--format=json/yaml/human) is byte-for-byte clean regardless of progress activity; spinners and status lines live exclusively on stderr.auto(default): animated bars when stderr is a TTY; one-line milestones to plain stderr when captured (CI logs).always: same asautoplus a stderr summary line. Bars still require a TTY — non-TTYalwaysfalls back to one-line milestones.never: zero stderr noise.--quietis the alias.
indicatifpowers the animated bars; thecrates/alint/src/progress.rsmodule wraps it behind a null-handle pattern so suggester code passes&Progresswithout branching on visibility. -
git_blame_agerule kind — fire on lines matching a regex whosegit blameauthor-time is older thanmax_age_days. Closes the gap betweenlevel: warningon every TODO (too noisy) andlevel: off(accepts unbounded debt accumulation). Same regex match shape asfile_content_forbidden, plus a per-line age gate. New{{ctx.match}}message placeholder substitutes capture group 1 (or the full match when no capture is present), so messages can be specific about which marker was caught. Outside a git repo, on untracked files, or when blame fails for any other reason, the rule silently no-ops per file — same advisory posture asgit_no_denied_paths/git_commit_message. Check-only.- id: stale-todoskind: git_blame_agepaths: "**/*.{rs,ts,tsx,js,jsx,py,go,java}"pattern: '\b(TODO|FIXME|XXX|HACK)\b'max_age_days: 180level: warningmessage: "`{{ctx.match}}` is >180 days old — resolve or remove."Engine plumbing: a new shared
BlameCacheis built once per run when any rule reportswants_git_blame(), so multiple blame-aware rules over overlappingpaths:re-use the parsed output. Cache memoises both successes and failures so a large rule fan-out doesn’t re-shell-out to git per file. Heuristic notes (formatting passes reset blame age unless.git-blame-ignore-revsis honoured; vendored / imported code carries the import commit’s timestamp; squash-merged PRs collapse to a single date) are documented indocs/rules.mdanddocs/design/v0.7/git_blame_age.md.Pairs naturally with
alint check --changedso blame only runs over modified files in CI.Design doc:
docs/design/v0.7/git_blame_age.md. -
commented_out_coderule kind — heuristic detector for blocks of commented-out source code (as opposed to prose comments, license headers, doc comments, or ASCII banners). Counts the fraction of non-whitespace characters that are structural punctuation strongly biased toward code (( ) { } [ ] ; = < > & | ^); scores ≥threshold(default 0.5 after normalisation; midpoint between obvious-prose 0.0 and obvious-code 1.0) mark the block as code-shaped. Supports rust / typescript / javascript / python / go / java / c / cpp / ruby / shell. Doc-comment blocks (///,//!,/** */) and the file’s firstskip_leading_lineslines (default 30 — license headers) are excluded by construction. Runs of 5+ identical characters (============,----,####) are dropped before scoring so ASCII-art separators don’t flag as code. Severity floor iswarning, nevererrorby default — heuristics have non-zero FP rate. Check-only — auto-removing commented-out code is destructive. Field-tested against 5 repos (alint, alint.org, Aider, Cline, OpenHands) before commit: zero false positives across 16 hits.- id: no-commented-codekind: commented_out_codepaths: "src/**/*.{ts,tsx,js,jsx,rs,py}"min_lines: 3threshold: 0.5level: warningDesign doc:
docs/design/v0.7/commented_out_code.md. -
markdown_paths_resolverule kind — validates that backticked workspace paths in markdown files resolve to real files or directories. Targets the AGENTS.md / CLAUDE.md /.cursorrulesstaleness problem more precisely than v0.6’s regex-heuristicagent-context-no-stale-pathsrule. Requiredprefixesfield declares which path-shapes to validate (eliminating the “is this a path or a word” question by construction). Skips fenced and 4-space-indented code blocks. Strips trailing punctuation, trailing slashes,:line/#L<n>location suffixes before lookup. Glob characters in the path resolve via the file index. Check-only. Design doc:docs/design/v0.7/markdown_paths_resolve.md.- id: agents-md-paths-resolvekind: markdown_paths_resolvepaths: ["AGENTS.md", "CLAUDE.md", ".cursorrules"]prefixes: ["src/", "crates/", "docs/"]level: warning
0.6.0 — 2026-04-27
Section titled “0.6.0 — 2026-04-27”Two bundled rulesets and a new output format aimed at the agent-driven-development era. Schema-compatible: every v0.5.12 config runs unchanged, and the new bundled rulesets compose from rule kinds that have shipped since v0.1.
The framing: alint’s structural / repo-shape niche (filesystem shape and contents of a repository, not the semantics of code inside it) fits agent-driven development naturally. Coding agents leave characteristic structural debris — backup-suffix files, scratch / planning docs, debug-print residue, stale model-attributed TODOs — that existing rule kinds catch cleanly when packaged into the right bundled ruleset. v0.6 does that packaging, plus a new output format optimised for agents consuming alint inside their own self-correction loops. No new rule kinds, no engine changes, no architectural shift.
-
alint://bundled/agent-hygiene@v1— six-rule bundled ruleset targeting the leftover patterns that show up disproportionately in commits authored or co-authored by Claude Code, Cursor, Copilot agent, Aider, Codex, and similar tools. Composes with the existinghygiene/*rulesets — extend all three on agent-heavy projects without overlap:extends:- alint://bundled/hygiene/no-tracked-artifacts@v1- alint://bundled/hygiene/lockfiles@v1- alint://bundled/agent-hygiene@v1Rules:
agent-no-versioned-duplicates— bans filenames matching*_old.*/*_new.*/*_final.*/*_FINAL.*/*_copy.*/*_backup.*/*.copy.*(warning). The*_v[0-9]*/*-v[0-9]*patterns were considered and deliberately omitted — too many real codebases use those for legitimate API versioning, schema migrations, release notes, and versioned tests (gitlab_v1_*.py,076_add_v1_tables.py,release-notes-v1.md,test_v1_api.py).agent-no-scratch-docs-at-root— bansPLAN.md/NOTES.md/ANALYSIS.md/SUMMARY.md/FIX.md/DECISION.md/TODO.md/SCRATCH.md/DEBUG.md/TEMP.md/WIP.mdat the repo root (warning,root_only: true).agent-no-affirmation-prose— flags AI-style stock phrases in source / markdown ("You're absolutely right","Excellent question","Happy to help", etc.) (info).agent-no-console-log— bansconsole.log/.debug/.tracein non-test JS / TS source (warning). Excludes test directories (**/*test*/**— broader thantest*/**, catchescross-sdk-tests/,e2e-tests/, etc.), build / dev tooling configs,**/scripts/**,**/website/**/**/public/**/**/demo/**,**/vendor/**and**/.claude/**(agent-worktree scratch space).agent-no-debugger-statements— bansdebugger;/breakpoint()in non-test source (error). The regex requires;immediately afterdebuggerso the rule doesn’t trip on the WORD “debugger” appearing in prose comments. Same exclusion list as the console-log rule.agent-no-model-todos— bansTODO(claude:)/FIXME(cursor:)/XXX(gpt:)and similar model-attributed markers (warning). Excludes CHANGELOG / ROADMAP / cookbook / test directories — projects that document these patterns trip on their own examples otherwise.
-
alint://bundled/agent-context@v1— four-rule bundled ruleset for the agent-instruction files coding agents read on every session:AGENTS.md(the cross-tool standard backed by agents.md / OpenAI Codex),CLAUDE.md,.cursorrules,GEMINI.md, and.github/copilot-instructions.md. Gated byfacts.has_agent_contextso it’s a safe no-op in repos without any of these files; extend it unconditionally even from polyglot configs.Rules:
agent-context-recommended—file_existsinfo-level nudge.agent-context-non-stub—file_min_lines: 10(warning).agent-context-not-bloated—file_max_lines: 300(info). Threshold from Augment Code’s 2026-03 research on AGENTS.md effectiveness.agent-context-no-stale-paths— regex-heuristic info-level reminder that backticked workspace paths drift. The precise check ships in v0.7 as themarkdown_paths_resolverule kind.
-
--format=agentJSON output — eighth output format, alongsidehuman/json/sarif/github/markdown/junit/gitlab. Shaped for AI coding agents that consume alint inside their own self-correction loops. Differences vs.--format=json: violations are a flat list (no per-rule nesting); each violation carries anagent_instructionfield with templated remediation phrasing (severity + human message + location + fix availability + policy URL);severityis the lowercase string ("error"/"warning"/"info"). Aliases:--format=agent/--format=agentic/--format=ai. Stable behindschema_version: 1. The fix-report falls back to the human formatter (an agent confirming a fix landed re-runsalint check --format=agent).
Internal
Section titled “Internal”- Two new tests in
crates/alint-dsl/src/bundled.rscontinue to enforce that every shipped ruleset declares its canonical# alint://bundled/<name>@v<rev>URI tag and parses as a valid config; the new rulesets are exercised by the existing test loop. - Five unit tests for the agent formatter cover empty
reports, path-bound violations, fixable violations
(suggesting
alint fix --only <id>inagent_instruction), cross-file violations (repository-level phrasing), and severity-count aggregation. - Help-text snapshot (
crates/alint/tests/cli/help-top-level.stdout) refreshed to mentionagentin the--colorflag’s documented list of plain-bytes formats.
[0.5.12] — 2026-04-27
Section titled “[0.5.12] — 2026-04-27”Maintenance release. Verifies the npm auto-publish CI wiring
end-to-end after v0.5.11’s publish-npm job failed (the
NPM_TOKEN secret hadn’t been provisioned yet, and a detour
through Trusted Publishing was blocked by a broken 2FA
configuration UI on npmjs.com).
No code changes. Every v0.5.11 config runs unchanged.
Changed
Section titled “Changed”- The npm scope is now
@asamarts/alint(matches theasamarts/alintGitHub repo,asamarts/homebrew-alinttap, andghcr.io/asamarts/alintDocker image). The v0.5.11 entry referenced@alint/alint, then@a-lint/alintduring the org-name dance; both were placeholders. The install snippet now matches what’s actually published.
npm install --save-dev @asamarts/alintnpx alint check[0.5.11] — 2026-04-27
Section titled “[0.5.11] — 2026-04-27”npm install channel. Closes the v0.5 milestone — every deferred item from the v0.5 roadmap is now shipped.
-
@asamarts/alintnpm package — fifth install channel alongsidecargo install alint, the Homebrew tap, the Docker image, andinstall.sh. The npm package is a thin shim that downloads the matching pre-built binary at install time, verifies its SHA-256 against the same.sha256companions the other paths consume, and stages it underbin-platform/for the npm-exposedbin/alint.jsshim to spawn at runtime.Terminal window # project-localnpm install --save-dev @asamarts/alintnpx alint check# globalnpm install -g @asamarts/alintalint check- The package itself ships zero JS runtime behaviour.
- Single runtime dep (
tarfor archive extraction). - Skip the postinstall network hop with
ALINT_SKIP_INSTALL=1(for CI systems that snapshotnode_modules). - Supported platforms: linux x64/arm64 (musl), macOS x64/arm64, Windows x64.
- Auto-published from
release.ymlon tag push, alongside crates.io / Docker / Homebrew. The publish job stampspackage.json’s version to match the tag immediately beforenpm publish --access public.
Internal
Section titled “Internal”- New
npm/directory at the repo root holdspackage.json,install.js(postinstall),bin/alint.js(runtime shim),README.md, and.npmignore. release.ymlgains apublish-npmjob:needs: release(the GH Release must be live before any user’s postinstall can fetch the binary tarballs); readsNPM_TOKENsecret from repo settings.
[0.5.10] — 2026-04-27
Section titled “[0.5.10] — 2026-04-27”DSL ergonomics: three composition primitives that close common monorepo / ops pain points. Schema-compatible: every v0.5.9 config runs unchanged.
-
content_from: <path>on fix ops —file_create/file_prepend/file_appendaccept a path-relative-to-lint-root as an alternative to inlinecontent:. The two are mutually exclusive (XOR enforced at config-load time). Read at fix-apply time via the newContentSourceSpecenum on the fixer struct; missing source produces aSkippedoutcome with a clear message rather than aborting the run. Use case: LICENSE / NOTICE / CONTRIBUTING / SPDX-header boilerplate that’s awkward to inline lives in.alint/templates/and gets referenced by short relative path. -
.alint.d/*.ymldrop-ins — auto-discovered next to the top-level.alint.ymland merged in alphabetical order. The last drop-in wins on field-level conflict, mirroring the/etc/*.d/convention. Stage00-base.ymlfor ops defaults,50-team.ymlfor team policies,99-local.ymlfor developer-local tweaks. Trust- equivalent to the main config (same workspace) — drop- ins CAN declarecustom:facts andkind: commandrules without the trust-gate that protects HTTPS / bundled extends. Non-yaml files in the dir are silently skipped. Sub-extended configs don’t get their own.alint.d/; only the top-level config does. -
Rule templates / parameterized rules — top-level
templates:block defines reusable rule bodies; rules instantiate them viaextends_template: <id>and avars:map for the{{vars.<name>}}substitution. Recursive substitution walks lists and nested mappings, sopaths:/fix.file_create.content/ etc all get vars-expanded. Unknown placeholders preserve literally so typos surface. Leaf-only (a template can’t itselfextends_template:another, mirroring the bundled rulesets restriction). Templates merge throughextends:chains by id.templates:- id: dir-has-readmekind: file_existspaths: ["{{vars.dir}}/README.md"]level: warningmessage: "{{vars.dir}} is missing a README"rules:- extends_template: dir-has-readmeid: packages-have-readmevars: { dir: packages }- extends_template: dir-has-readmeid: services-have-readmevars: { dir: services }
Internal
Section titled “Internal”- New
ContentSourceSpec(Inline(String)/File(PathBuf)) andresolve_content_sourcehelper inalint-core::config, exported through the crate root.From<String>/From<&str>impls keep inline-string construction terse for tests. RawConfiggainstemplates: Vec<Mapping>;merge()merges templates by id;finalize()runs theexpand_templatepass before each rule deserializes intoRuleSpec.- New
expand_template+substitute_template_vars/_valuehelpers inalint-dslreuse the existingalint_core::template::render_messageengine for the{{namespace.key}}substitution layer. - JSON Schema (
schemas/v1/config.json+ the in-crate copy) defines top-leveltemplates: [], the newrule_template_instanceshape (oneOf-branch with the kind-driven shape), and theoneOfbetweencontent/content_fromon each affected fix op. Drift test passes. - 12 new unit tests across the three features (3 fixer tests for content_from, 3 lib tests for drop-in collection + merge, 6 lib tests for template expansion).
[0.5.9] — 2026-04-27
Section titled “[0.5.9] — 2026-04-27”json_schema_passes (last unshipped structured-query
primitive), two new git-aware rule kinds, and four
OpenSSF Scorecard-overlap additions to oss-baseline@v1.
Schema-compatible: every v0.5.8 config runs unchanged.
-
json_schema_passes— validate JSON / YAML / TOML files against a JSON Schema. Targets coerce through serde into the sameserde_json::Valuetree the schema sees, so YAML configs (Kubernetes manifests, GitHub Actions workflows, Helmvalues.schema.json) and TOML manifests (Cargo, pyproject) all work against a JSON schema document. Schema is loaded + compiled lazily on the firstevaluate()call and cached on the rule viaOnceLock. Each schema-validation error becomes one violation with the failing instance path; a target that fails to parse produces a single parse-error violation, not a flood. Format is detected from extension; passformat:to override. -
git_no_denied_paths— fire when any tracked file matches a configured glob denylist. The absence-axis companion ofgit_tracked_only(v0.4.8). Catches secrets (*.env,id_rsa,*.pem), bulky generated artefacts (dist/**,*.log), and “do not commit” sentinels in one rule rather than onefile_absentper pattern. Reports every matching denylist entry per offending path. Outside a git repo, silently no-ops. -
git_commit_message— validate HEAD’s commit message shape via regex (pattern:), max subject length (subject_max_length:), and body-required (requires_body:). At least one of the three must be set. Subject length counts characters, not bytes. Outside a git repo, with no commits, or whengitisn’t on PATH, silently no-ops. Pairs naturally withalint check --changedfor per-PR enforcement. -
alint-core::git::head_commit_message(root)— new helper alongsidecollect_tracked_paths/collect_changed_paths, with the same advisoryOption<String>return shape. -
Four Scorecard-overlap rules in
oss-baseline@v1(info-level, no new rule kinds — composes from existingfile_exists+file_min_size):oss-security-policy-non-empty— 200B floor on SECURITY.md (catches the empty stub that satisfies Scorecard’s existence check while providing no reporting guidance).oss-dependency-update-tool—file_existsagainst every blessed Dependabot / Renovate config location.oss-codeowners-exists— CODEOWNERS at root,.github/, ordocs/.oss-codeowners-non-empty— 10B floor on CODEOWNERS.
Internal
Section titled “Internal”alint-rulesgainsjsonschema = "0.29"as a regular dep (already a workspace dep used byalint-dsl’s drift tests).- JSON Schema (
schemas/v1/config.json+ the in-crate copy) definesrule_json_schema_passes,rule_git_no_denied_paths, andrule_git_commit_message. Drift test passes. - 21 new unit tests across the three rule files (9 for
json_schema_passes, 5 forgit_no_denied_paths, 7 forgit_commit_message); 1 new e2e fixture (oss_baseline_complete_repo_passupdated for the four new rules); two existing override scenarios updated to account for the new bundled rules.
[0.5.8] — 2026-04-26
Section titled “[0.5.8] — 2026-04-26”Three new output formats. Brings the count to seven and closes the v0.5 output-format roadmap item. Schema-compatible: every v0.5.7 config runs unchanged.
-
--format markdown(aliasmd) — GitHub-Flavored Markdown suited to PR comments, mkdocs report pages, and Slack via webhook bridges. H1 banner + one-line summary (**N violations across M files** (E errors, W warnings))- one H2 section per file with bulleted violations + a
trailing “Cross-file” section for path-less / cross-file
violations. Output is byte-deterministic so PR-comment
workflows can diff alint output across runs without
spurious churn.
alint fix --format markdowngets a dedicated renderer too — lists each rule’s items withapplied/skipped/unfixablestatus.
- one H2 section per file with bulleted violations + a
trailing “Cross-file” section for path-less / cross-file
violations. Output is byte-deterministic so PR-comment
workflows can diff alint output across runs without
spurious churn.
-
--format junit(aliasjunit-xml) — the de-facto- standard CI test-report XML consumed by Jenkins, Azure DevOps, GitHub’sdorny/test-reporter, and GitLab CI’s JUnit integration. Common-denominator schema: a single<testsuites>wrapping a single<testsuite name="alint">, with one<testcase>per (rule, file/path-less-bucket). Passing rules contribute self-closed testcases; each violation becomes a testcase with a<failure>whosetypeattribute carries the alint level (error/warning/info) so consumers can filter level-specifically. XML 1.0-illegal control characters are stripped on the way out. -
--format gitlab(aliasesgitlab-codequality,code-quality) — GitLab CI’s native Code Quality JSON, which is the upstream Code Climate “Issue” specification. One issue object per violation:{ description, check_name, fingerprint, severity, location: { path, lines: { begin } } }. Severity mapping:Error → major,Warning → minor,Info → info. Fingerprint is the SHA-256 hex ofrule_id|path|message— the line number is intentionally omitted so a violation that drifts up or down by a few lines stays the same issue across runs. Path-less / cross-file violations emitlocation.path = "."(repository root) so the report still validates against the GitLab schema.
Internal
Section titled “Internal”alint-outputgains asha2dep for the GitLab fingerprint (already a workspace dep used byalint-rules+alint-dsl).- 38 new unit tests across the three formatters cover empty reports, level-mapping, cross-file edge cases, determinism, special-character escaping, and (for GitLab) fingerprint stability across line-drift + sensitivity to message changes.
[0.5.7] — 2026-04-26
Section titled “[0.5.7] — 2026-04-26”Competitive bench publication. The v0.5.6 harness becomes a
multi-tool driver: alint, ls-lint, Repolinter, and find +
ripgrep pipelines all run against the same synthetic
trees on the same hardware, producing wall-time numbers
that are directly comparable. Reproducibility via a new
ghcr.io/asamarts/alint-bench Docker image (every
competitor pinned by version) and an xtask bench-scale --docker flag that re-execs the bench inside the image.
Schema-compatible; every v0.5.6 config runs unchanged.
-
xtask bench-scale --tools <list>— the bench harness now runs an arbitrary set of tools across the same(size × scenario × mode)matrix v0.5.6 introduced. Defaultalint(preserves the v0.5.6 publication shape),allexpands to every known tool, comma lists pick a subset (alint,grep,alint,ls-lint). Tools missing onPATHare log-and-dropped at resolve time so a partial installation still produces alint-only rows without aborting.- alint — full matrix (every scenario × mode).
- ls-lint — gated to (S1, full); ls-lint is
extension/case-class only and has no
--changed-equivalent. - Repolinter — gated to (S2, full); pinned to
0.11.2 (last pre-archive release, repo archived
2026-02-06). The size-bound check (no files >10 MiB)
is dropped: Repolinter has no built-in primitive
and emulating via
scriptrules would distort timings beyond recognition. - find + ripgrep pipeline (
grep) — gated to (S1, full) and (S2, full); shell-pipeline baseline representing the small-team “we just chainfindandrg” status quo. S3’s cross-file rules have no sane shell expression and are out of scope.
-
bench/Dockerfile+ghcr.io/asamarts/alint-benchimage — the canonical competitive-bench environment. Built and pushed by.github/workflows/bench-docker.ymlon tag pushes (:<ver>+:latest), main pushes (:edge), and manual dispatch. Pinned versions of hyperfine, ripgrep, repolinter, ls-lint, Node 20, and the Rust toolchain so a given image tag IS the bench environment for that release. -
xtask bench-scale --docker— re-execs the bench inside the published image. Bind-mounts the workspace at/work, uses a named volume for the cargo target dir so target/ artefacts persist across runs without shadowing the host. Image override viaALINT_BENCH_IMAGE=.... -
First competitive numbers under
docs/benchmarks/macro/results/linux-x86_64/v0.5.7/. Same fingerprint as v0.5.6’s alint-only publication; rows for ls-lint / repolinter / grep added at the scenarios + sizes each tool supports. Headline ratios (linux-x86_64, 100k files):- S1 (filename hygiene): alint vs ls-lint vs
find | greppipelines. - S2 (existence + content): alint vs Repolinter
vs
find+rgpipelines. - S3 (workspace bundle): alint only — no competitor models cross-file rules.
Per-row markdown plus a
results.jsonwith the full matrix; the newToolcolumn makes pivoting trivial. - S1 (filename hygiene): alint vs ls-lint vs
-
Published 1M-file numbers under
docs/benchmarks/macro/results/linux-x86_64/v0.5.6/1m/results.md. Six rows (1m × {S1,S2,S3} × {full,changed}) on the same hardware as v0.5.6’s 1k/10k/100k publication. Headlines: 1m / S1 / full ≈ 3.5s, 1m / S2 / full ≈ 10s, 1m / S3 / full ≈ 9.5min — the cross-file rules in the workspace bundle scale superlinearly with N, exactly as the methodology predicted.--changedsaves ~34% on S2’s content rules at 1m but barely helps S3 (cross-file rules can’t be filtered). -
Auto-reduced sampling at 1m. The harness caps
1m-row warmup at 1 and measured runs at 3 regardless of--warmup/--runs. A single1m / S3invocation runs for several minutes; thirteen of them per row would push the matrix to many hours. The trade-off is wider stddev at 1m —methodology.mdis updated to flag this so readers don’t compare 1m’s stddev to the smaller-size rows like-for-like. Stddev is reported as0.0when hyperfine emitsnull(single-run rows) instead of failing the whole bench.
-
--include-1mnow actually adds the 1m size to the matrix when--sizesis at its default (1k,10k,100k). Previously the flag only filtered 1m out unless you also retyped the size list — the opposite of what the help text implied. -
Tool-version fingerprint stays one line. Multi-line
--versionbanners (notably ripgrep’s) were being stored verbatim in the fingerprint’stool_versionsmap, blowing up the rendered “Tools: …” line in committed reports. Capture now keeps just the first line of each tool’s--versionoutput.
0.5.6 — 2026-04-26
Section titled “0.5.6 — 2026-04-26”Scale-ceiling bench publication + a latent walker bug fix
that the bench surfaced. New xtask bench-scale subcommand
runs alint across a (size × scenario × mode) matrix with
hardware-fingerprint capture and JSON + Markdown
publication; v0.5.7 layers competitive comparisons
(ls-lint, Repolinter, find/grep) on top of the same
infrastructure. Schema-compatible; every v0.5.5 config
runs unchanged.
-
xtask bench-scale— scale-ceiling benchmark driver. Runs alint across:- Sizes:
1k/10k/100k(default), opt into1mvia--include-1m. Synthetic monorepo trees generated deterministically from the seed (default0xa11e47). - Scenarios:
S1(filename hygiene, 8 rules) /S2(existence + content, 8 rules) /S3(workspace bundle:oss-baseline+rust+monorepo+monorepo/cargo-workspace). - Modes:
full(every file evaluated) andchanged(alint check --changedagainst a deterministic 10% diff — measures the v0.5.0 incremental path).
Per-row hyperfine measurement (3 warmup + 10 measured runs by default); JSON + Markdown output under
docs/benchmarks/macro/results/<os>-<arch>/<version>/. Hardware fingerprint (CPU model + cores, RAM, FS type, kernel, rustc, alint version + git SHA, hyperfine version) embedded in every report so cross-machine comparisons stay honest. - Sizes:
-
alint_bench::tree::generate_monorepo— new Cargo-workspace-shaped synthetic-tree generator with real workspace[workspace]+ per-package[package].nameCargo.toml content (so themonorepo/cargo-workspace@v1ruleset’s structured-query rules see well-formed manifests). Full determinism for byte-identical trees across platforms. -
alint_bench::tree::select_subset— deterministic Fisher-Yates partial shuffle for picking a fraction of files to “touch” in--changed-mode benches. -
First published numbers:
docs/benchmarks/macro/results/linux-x86_64/v0.5.7/with 18 rows (3 sizes × 3 scenarios × 2 modes) on AMD Ryzen 9 3900X / 62 GB / ext4 / Linux 6.1. Companiondocs/benchmarks/{README.md,RUNNING.md,macro/README.md} (post-reorg layout)documents the harness + scenario definitions + how to reproduce. -
CLI flags:
--sizes,--scenarios,--modes,--warmup,--runs,--seed,--diff-pct,--out,--include-1m,--quick,--json-only. The default (cargo xtask bench-scalewith no args) produces the full publication-grade matrix.
- Walker no longer descends into
.git/.alint checkagainst a tree containing a.git/directory used to walk into git’s internal storage — wasted work for every alint rule (none of them target.git/objects/*) and a TOCTOU hazard during git’s auto-gc / packfile rewrites. The walker now adds.gitto its exclusion overrides unconditionally. No user-visible behaviour change for repos whose.gitignorealready covers.git/-shaped paths; benchmark and large-monorepo runs become both faster and reliable.
Internal
Section titled “Internal”- New
xtask/src/bench/module:mod.rs(orchestration- types),
fingerprint.rs(hardware capture per OS),scenarios/*.yml(S1/S2/S3 alint configs embedded viainclude_str!).
- types),
xtaskgainsserde(withderive) andserde_jsondev-deps for hyperfine--export-jsonparsing and the results.json schema.- 11 new unit tests on
alint_bench::treecovering the monorepo shape, file-count exactness, deterministic output for the same seed, andselect_subset’s fraction / clamping / determinism semantics.
Compatibility
Section titled “Compatibility”- Schema version remains
1. No rule-config changes. - Public API additions are non-breaking. Walker
.git/-exclusion is a behaviour fix, not a config change.
0.5.5 — 2026-04-26
Section titled “0.5.5 — 2026-04-26”Two license-compliance bundled rulesets — the v0.5 cycle’s expansion beyond the workspace-tier monorepo audience to OSS maintainers and corporate-policy teams. Schema-compatible; every v0.5.4 config runs unchanged.
-
alint://bundled/compliance/reuse@v1— FSFE REUSE Specification compliance. Three rules covering the spec’s two load-bearing requirements:reuse-licenses-dir-exists— top-levelLICENSES/directory present (per § License files).reuse-source-has-spdx-identifier— every source file carries anSPDX-License-Identifier:header in its first ~10 lines.reuse-source-has-copyright-text— every source file carries anSPDX-FileCopyrightText:header.
Source-file rules cover the common code extensions (
*.{rs,py,js,jsx,ts,tsx,go,java,kt,c,cc,cpp,h,hpp, hh,sh,rb,swift}) and exclude vendored / build / dist directories. Projects that license files via.licensecompanions orREUSE.tomlmappings can narrowpaths:on the source rules.extends:- alint://bundled/compliance/reuse@v1 -
alint://bundled/compliance/apache-2@v1— compliance for projects distributed under the Apache License, Version 2.0. Three rules verifying the artefacts the license text itself requires of redistributors:apache-2-license-text-present— LICENSE (or LICENSE.md / LICENSE.txt / COPYING) contains the canonical “Apache License, Version 2.0” text.apache-2-notice-file-exists— root NOTICE file present (per Apache-2.0 §4(d)).apache-2-source-has-license-header— every source file carries the canonical “Licensed under the Apache License, Version 2.0” header in its first ~25 lines.
Substring-matches the canonical license title rather than doing full bit-for-bit comparison, so SPDX templates, apache.org’s template, and GitHub’s auto-init all parse as compliant. Dual-licensed projects (e.g. Apache-2.0 OR MIT) can extend this ruleset and use
level: offon rules they don’t want firing strictly.Bundled catalog: 15 → 17.
Internal
Section titled “Internal”- New ruleset directory
crates/alint-dsl/rulesets/v1/compliance/with two.ymlfiles; registered inalint_dsl::bundled::REGISTRY. Neither ruleset uses a fact gate — adopting a compliance ruleset is the user’s signal that the project intends to be compliant with the named scheme. - 6 new e2e scenarios under
crates/alint-e2e/scenarios/check/bundled-compliance/: per ruleset, happy-path + missing-core-artefact + missing-source-header.
Compatibility
Section titled “Compatibility”- Schema version remains
1. Pure config — no new rule kinds, no new core APIs. - JSON / SARIF / GitHub outputs byte-equivalent for configs that don’t extend the new rulesets.
0.5.4 — 2026-04-26
Section titled “0.5.4 — 2026-04-26”alint init — the missing one-line adoption story.
Detects the repo’s ecosystem (Rust / Node / Python / Go /
Java) and optionally its workspace shape (Cargo / pnpm /
Yarn-or-npm), then writes a .alint.yml extending the
right bundled rulesets. Closes the v0.5 monorepo theme on
the adoption side: every primitive shipped in v0.5.0–v0.5.3
now has a one-line on-ramp. Schema-compatible; every v0.5.3
config runs unchanged.
-
alint init [PATH]— new subcommand. Detects the repo’s ecosystem from root manifests and writes a.alint.ymlwithextends:lines foross-baseline@v1plus each detected language ruleset. Detection is deliberately a presence check (file exists, no parsing) so it stays fast and predictable:- Rust:
Cargo.toml - Node:
package.json - Python:
pyproject.toml/setup.py/setup.cfg - Go:
go.mod - Java:
pom.xml/build.gradle/build.gradle.kts
Refuses to overwrite an existing config (any of
.alint.yml/.alint.yaml/alint.yml/alint.yaml) — exits non-zero with a clear message pointing the user at deletion. - Rust:
-
alint init --monorepo— adds workspace detection on top of the language scan. Recognises:- Cargo workspaces — root
Cargo.tomlcontains a[workspace]table (line-prefix check, no TOML parsing). - pnpm workspaces — root
pnpm-workspace.yaml/.ymlexists. - Yarn / npm workspaces — root
package.jsoncontains"workspaces".
When a workspace is detected, the generated config also extends
monorepo@v1and the matchingmonorepo/<flavor>-workspace@v1overlay, plus setsnested_configs: trueso each subdirectory can layer its own.alint.ymlon top.# `alint init --monorepo` in a Cargo workspace produces:version: 1nested_configs: trueextends:- alint://bundled/oss-baseline@v1- alint://bundled/rust@v1- alint://bundled/monorepo@v1- alint://bundled/monorepo/cargo-workspace@v1Bazel / Lerna / Nx / Turbo detection deferred — the three flavours that have bundled overlays cover the workspace-tier sweet spot.
- Cargo workspaces — root
Internal
Section titled “Internal”- New
crates/alint/src/init.rsmodule withDetection/Language/WorkspaceFlavortypes, a puredetect()function and a deterministicrender()emitter. Output is hand-formatted YAML (not serialized via serde) so the generated file can carry header comments documenting what was detected and how to use it. - 17 new unit tests covering the detector + emitter (per-language detection, polyglot repos, workspace precedence, header summary).
- 3 new trycmd CLI tests under
tests/cli/init-*.toml: empty-repo init, monorepo-cargo init, refuses-overwrite. The trycmdfs.sandbox = truemode lets us assert on the post-run sandbox state (the generated.alint.yml) alongside stdout / stderr / exit. help-top-levelsnapshot regenerated to include theinitsubcommand line.
Compatibility
Section titled “Compatibility”- Schema version remains
1. Pure additive — no rule, config, or output changes. - Public API unchanged (the
initmodule is private to the binary crate). - New
tempfiledev-dependency onalint(already a workspace dep elsewhere; the binary needs it for the init unit tests).
0.5.3 — 2026-04-26
Section titled “0.5.3 — 2026-04-26”Three workspace-aware bundled rulesets layered on top of
monorepo@v1. Each is gated by a workspace-flavor fact and
uses the v0.5.2 when_iter: filter to scope per-member
checks to actual package directories — crates/notes/
(no Cargo.toml) or packages/drafts/ (no package.json)
are filtered out without firing false positives.
Schema-compatible; every v0.5.2 config runs unchanged.
-
alint://bundled/monorepo/cargo-workspace@v1— Cargo workspaces. Gated byfacts.is_cargo_workspace(rootCargo.tomldeclares[workspace]). Three rules:members = [...]declared at the workspace root (toml_path_matches); everycrates/*directory with its ownCargo.tomlhas a README; every member’sCargo.tomldeclares[package].name.extends:- alint://bundled/monorepo@v1- alint://bundled/rust@v1- alint://bundled/monorepo/cargo-workspace@v1 -
alint://bundled/monorepo/pnpm-workspace@v1— pnpm workspaces. Gated byfacts.is_pnpm_workspace(rootpnpm-workspace.yaml/.ymlexists). Three rules:packages: [...]declared inpnpm-workspace.yaml(yaml_path_matches); everypackages/*with apackage.jsonhas a README; every member’spackage.jsondeclaresname. -
alint://bundled/monorepo/yarn-workspace@v1— Yarn / npm workspaces (the workspace declaration lives in the rootpackage.jsonfor both). Gated byfacts.is_yarn_workspace(rootpackage.jsoncontains"workspaces"). Three rules:workspaces: [...]is non-empty (json_path_matchesagainst$.workspaces[*]); every{packages,apps}/*with apackage.jsonhas a README; every member’spackage.jsondeclaresname. Validates the array form; the rarer object form ("workspaces": {"packages": [...]}) is gated by the fact but not field-validated here.Bundled catalog: 12 → 15 rulesets.
Internal
Section titled “Internal”- New ruleset directory
crates/alint-dsl/rulesets/v1/monorepo/with three.ymlfiles; registered inalint_dsl::bundled::REGISTRY. Each ruleset declares its ownis_*_workspacefact inline rather than promoting them to the corefacts:catalogue — the facts are workspace-specific and not meant to be referenced from user configs. - 7 new e2e scenarios under
crates/alint-e2e/scenarios/check/bundled-monorepo/: per-flavor “filters non-member dirs” + “silent outside workspace” pair, pluscargo-workspace’s “fires on missing members” case.
Compatibility
Section titled “Compatibility”- Schema version remains
1. All three rulesets are pure config — no new rule kinds, no new core APIs. - JSON / SARIF / GitHub outputs byte-equivalent for configs that don’t extend the new rulesets.
0.5.2 — 2026-04-26
Section titled “0.5.2 — 2026-04-26”Per-iteration when: filter on iterating rules — closes the
second monorepo-scale gap from the v0.5 roadmap. Combined
with --changed (v0.5.0) and command plugin (v0.5.1),
this is the third leg of the v0.5 monorepo theme.
Schema-compatible; every v0.5.1 config runs unchanged.
-
when_iter:field onfor_each_dir,for_each_file, andevery_matching_has. Optional expression evaluated against each iterated entry’sitercontext; iterations whose verdict is false are skipped before any nested rule is built. Closes the Bazel/Cargo/pnpm-workspace gap where users previously had to widenselect:and rely on inner rules to short-circuit.- id: workspace-member-has-readmekind: for_each_dirselect: "crates/*"when_iter: 'iter.has_file("Cargo.toml")'require:- kind: file_existspaths: "{path}/README.md"level: errorWithout
when_iter:,crates/notes/(noCargo.toml) would have fired the missing-README rule. With it, only workspace members are evaluated. -
iter.*namespace inwhen:expressions — exposes the iterated entry’s metadata to the existingwhen:grammar. Same expression compiles inwhen_iter:(outer iteration filter) and in any nested rule’swhen:(per-iteration nested gate). Outside an iteration context,iter.Xresolves tonullanditer.has_file(_)tofalse, matching the “missing fact is falsy” convention.Reference Type Notes iter.pathstring Relative path of the iterated entry. iter.basenamestring Basename. iter.parent_namestring Parent dir name. iter.stemstring Basename minus final extension. iter.extstring Final extension without the dot. iter.is_dirbool trueforfor_each_dir,falseforfor_each_file.iter.has_file(pattern)bool Glob match relative to the iterated directory. Always falseon file iteration. -
Function-call syntax in the
when:grammar. Limited to a fixed allow-list of methods oniter(currently justhas_file); typos in user configs surface as “unknown iter method” parse errors instead of silently coercing tofalse. Calls on non-iter namespaces are a parse error.
Internal
Section titled “Internal”- New public types
IterEnvandWhenEnv::with_iter()inalint-core::when. NewWhenExpr::CallAST variant.WhenEnv::new()constructor for callers without iteration context. - Shared parser helper
for_each_dir::parse_when_iterreused byfor_each_fileandevery_matching_has. - 9 new unit tests in
alint-core::whencovering the iter namespace + function-call grammar + outside-iter fallback. 4 new e2e scenarios undercrates/alint-e2e/scenarios/check/when_iter/: marker-file filter, basename predicate, recursive-glob predicate, composition withfacts.*.
Compatibility
Section titled “Compatibility”- Schema version remains
1.when_iter:is opt-in; rules that don’t use it behave identically to v0.5.1. - Public API additions are non-breaking.
WhenEnvgains aniter: Option<IterEnv>field; the newWhenEnv::new()constructor and existing struct-literal syntax both work. Out-of-tree code constructingWhenEnv { facts, vars }(without explicititer) needs to additer: None(or switch toWhenEnv::new(facts, vars)). evaluate_for_each(analint-rulescrate-private helper) gained awhen_iterparameter — only matters if you’ve forked the crate.
0.5.1 — 2026-04-26
Section titled “0.5.1 — 2026-04-26”Plugin tier 1: command rule kind. Wraps any CLI on PATH
into alint’s report. Continues the v0.5 monorepo-scale theme
— pairs naturally with --changed so per-file external
checks (actionlint, shellcheck, kubeconform, …) become
incremental in CI. Schema-compatible; every v0.5.0 config
runs unchanged.
-
kind: command— per-file rule that spawns argv with path-template substitution ({path},{dir},{stem},{ext},{basename},{parent_name}). Exit0is a pass; non-zero produces a violation whose message is the truncated stdout+stderr. Working dir is the repo root; stdin is closed (/dev/null). Output is capped at 16 KiB per stream to keep reports legible.- id: workflows-cleankind: commandpaths: ".github/workflows/*.{yml,yaml}"command: ["actionlint", "{path}"]level: errorEnvironment threaded into each invocation:
ALINT_PATH(relative to root),ALINT_ROOT(absolute),ALINT_RULE_ID,ALINT_LEVEL, plusALINT_VAR_<NAME>per top-levelvars:entry andALINT_FACT_<NAME>per resolved fact. -
timeout: <seconds>option oncommandrules. Default 30s. Past the limit, the child process is killed and a violation reports the timeout. Bounds runaway tools so a hung child never stalls the whole run. -
--changedinteraction.commandis per-file (norequires_full_indexoverride), so it inherits the v0.5 filtered-index iteration:alint check --changedspawns the wrapped tool only for files in the diff. Ashellcheckrule on a 200-script repo invokesshellcheckzero times when the diff doesn’t touch any.sh. Largest practical multiplier on CI cost for external-linter wrappers.
Security
Section titled “Security”- Trust gate.
commandrules are only permitted in the user’s own top-level.alint.yml. Akind: commandrule introduced viaextends:— local file, HTTPS URL, oralint://bundled/<name>@<rev>— is rejected at load time with a clear error pointing at the offending source. Adopting a published ruleset must never gain it arbitrary process execution. New public functionalint_dsl::reject_command_rules_inmirrors the existingalint_core::facts::reject_custom_facts_ingate.
Internal
Section titled “Internal”-
New
alint_rules::commandmodule (~330 LOC including 9 unit tests). Polling-based wait loop with 10ms granularity for the timeout path; output capping viaRead::take(OUTPUT_CAP_BYTES). JSON Schema gains arule_commandbranch; root + in-crate copies kept byte-identical by the existing drift-guard test. -
3 new e2e integration tests under
crates/alint-e2e/tests/command_plugin.rs(#[cfg(unix)]— relies on/bin/sh): full-engine pass case, full-engine fail case (one violation per failing file), and the--changedinteraction (only invoked for files in the diff). 2 new unit tests inalint-dslcovering the trust gate (rejected fromextends:, allowed in top-level).
Compatibility
Section titled “Compatibility”- Schema version remains
1. JSON / SARIF / GitHub outputs byte-equivalent for configs that don’t usecommand. - Public API additions are non-breaking.
Ruletrait unchanged; the newreject_command_rules_inis a new public function inalint-dsl.
0.5.0 — 2026-04-26
Section titled “0.5.0 — 2026-04-26”First v0.5 cut. Headline: incremental alint check --changed
mode for pre-commit and PR-check paths. Schema-compatible;
every v0.4.10 config runs unchanged. JSON / SARIF / GitHub
outputs byte-equivalent for full-tree runs.
-
alint check --changed [--base=<ref>]and the same flags onalint fix. With--base, the changed-set is derived from `git diff —name-only —relative...HEAD` (three-dot — merge-base diff, the right shape for PR checks). Without `--base`, it's `git ls-files --modified --others --exclude-standard` (working-tree diff, the right shape for pre-commit). The engine evaluates per-file rules against a [`FileIndex`] filtered to the changed-set, so a Java license-header rule scoped to `**/*.java` skips entirely when no `.java` file is in the diff. Cross-file rules (`pair`, `for_each_dir`, `every_matching_has`, `unique_by`, `dir_contains`, `dir_only_contains`) and existence rules (`file_exists`, `file_absent`, `dir_exists`, `dir_absent`) keep full-tree semantics for iteration; existence rules additionally skip when their `paths:` scope doesn't intersect the diff so an unchanged-but-missing LICENSE doesn't fire on every PR. Empty diffs short-circuit to an empty report (the no-op-commit case in pre-commit). Outside a git repo or when `git` isn't on PATH, `--changed` exits non-zero with a clear message rather than silently fall back to a full check. Terminal window # Pre-commit: lint the working-tree diff.alint check --changed# PR check: lint everything that diverged from main.alint check --changed --base=main --format=sarif -
Rule::requires_full_index() -> boolandRule::path_scope() -> Option<&Scope>on the publicalint-core::Ruletrait. Both default to “no opt-in”, so out-of-tree rule implementations compile unchanged. Internal rules override on the eleven cases that need full-tree semantics: the six cross-file kinds plus the four existence kinds. Per-file rules need no override — the engine hands them the filtered index and their existingScope::matchesloops do the right thing. -
alint_core::git::collect_changed_paths(root, base)helper, parallel to the existingcollect_tracked_paths. Returns the changed-set as aHashSet<PathBuf>of paths relative toroot, orNoneoutside a git repo / whengitexits non-zero. -
Engine::with_changed_paths(set)builder method. Threads the changed-set throughEngine::runandEngine::fix. Every call costs one walk over the index entries to build a filtered subset; absent the builder call, the engine behaves exactly as before. -
Step::CheckChangedinalint-testkit’s scenario harness. Five new e2e scenarios undercrates/alint-e2e/scenarios/check/changed/cover: per-file rule skipped when scope misses the diff, per-file rule fires only on changed files, cross-filepairkeeps full-tree semantics, existence rule skips when scope doesn’t intersect, and the empty-diff short-circuit.
Compatibility
Section titled “Compatibility”- Schema version remains
1. Every v0.4 config runs unchanged. - Public API additions are non-breaking:
Ruletrait methods have defaults,Engine::with_changed_pathsis additive. Embedders that hand-construct anEnginekeep compiling. alint-testkit::Stepgained a variant (Step::CheckChanged); embedders that exhaustively matched onStepneed to add an arm for it.
0.4.10 — 2026-04-25
Section titled “0.4.10 — 2026-04-25”Three new content-family rule kinds rounding out the family. Schema-compatible; every v0.4.9 config runs unchanged. JSON / SARIF / GitHub outputs byte-equivalent.
-
file_max_lines(aliasmax_lines). Mirror offile_min_lines: files in scope must have AT MOSTmax_lineslines. Samewc -laccounting. Catches the everything-module anti-pattern. -
file_footer(aliasfooter). Mirror offile_headeranchored at the END of the file: the lastlines:lines must match a regex. Use cases: license footers, signed-off-by trailers, generated-file sentinels. Fix op:file_append. -
file_shebang(aliasshebang). First line of each file must match a regex. Pairs withexecutable_has_shebang(which checks shebang presence) —file_shebangchecks shebang shape, e.g.^#!/usr/bin/env bash$to enforce a specific interpreter. Defaults to^#!(presence only).Brings the rule catalogue to ~55 kinds.
-
6 new e2e scenarios covering pass/fail paths for the three new kinds.
0.4.9 — 2026-04-25
Section titled “0.4.9 — 2026-04-25”Java bundled ruleset. Schema-compatible; every v0.4.8 config runs unchanged. JSON / SARIF / GitHub outputs byte-equivalent.
-
alint://bundled/java@v1(10 rules). Maven + Gradle hygiene, gatedwhen: facts.is_java:java-manifest-exists—pom.xml,build.gradle, orbuild.gradle.ktsat the root (error).java-build-wrapper-committed—mvnw/gradlewchecked in for reproducible builds (info).java-no-tracked-target/java-no-tracked-build— Maven’starget/and Gradle’sbuild/not committed. Both usegit_tracked_only: true(the v0.4.8 primitive) so a developer’s locally-built directories stay silent; only directories whose contents made it into git’s index fire (error).java-no-class-files—*.classfiles not committed (git_tracked_only: true, error).java-sources-pascal-case— PascalCase filenames for*.java, withpackage-info.java/module-info.javaexcluded (warning).java-sources-final-newline/java-sources-no-trailing-whitespace— text hygiene, auto-fixable (info).java-sources-no-bidi/java-sources-no-zero-width— Trojan Source defenses (error).
Brings the bundled catalogue to 12 rulesets. The
git_tracked_onlyrules in this ruleset are the first bundled use of v0.4.8’s git-aware primitive — thesilent_on_locally_built_targete2e scenario proves the wiring end-to-end.
First git-aware primitive lands. Schema-compatible; every v0.4.7 config runs unchanged. JSON / SARIF / GitHub outputs gain no new keys.
-
git_tracked_only: booloption onRuleSpec, currently honoured byfile_exists,file_absent,dir_exists, anddir_absent. Whentrue, the rule’spaths-matched entries are intersected withgit ls-files’s output so only files / directories actually in git’s index participate. Closes the approximation gap documented on the walker-and-gitignore concept page: adir_absentrule on**/targetwithgit_tracked_only: truefires only whentarget/was actually committed, never on a developer’s locally-builttarget/(gitignored or not). Outside a git repo, or whengitisn’t on PATH, the tracked-set is empty and rules with the flag set become silent no-ops — the right default for “don’t let X be committed” semantics.- id: target-not-trackedkind: dir_absentpaths: "**/target"git_tracked_only: truelevel: errorOther rule kinds currently ignore the field; we’ll extend coverage as concrete use cases come up. The roadmap’d
git_no_denied_pathsandgit_commit_messageprimitives are still pending.
Changed
Section titled “Changed”alint-core::Contextgains agit_tracked: Option<&HashSet<PathBuf>>field, plusis_git_tracked/dir_has_tracked_fileshelpers. External embedders constructing aContextby hand need to addgit_tracked: None. The engine collects the set at most once perrun/fix, only when at least one rule’swants_git_tracked()is true — zero cost when no rule opts in.alint-testkit’sGivenblock accepts an optionalgit: { init, add, commit }block so e2e scenarios can stand up a real git repo in their tempdir before alint runs.
Distribution breadth. Schema-compatible; every v0.4.6 config runs unchanged. JSON/SARIF/GitHub outputs byte-equivalent. No Rust code changes — this release ships new install paths only.
Docker image
Section titled “Docker image”-
ghcr.io/asamarts/alint— distroless multi-arch (linux/amd64,linux/arm64) image based ongcr.io/distroless/static-debian12:nonroot. Built by the release workflow from the same statically-linked musl binaries shipped in the GitHub Release tarballs, so the in-image binary matches the tarballed one byte-for-byte. Tags published per release: the exact git tag (:v0.4.7), the bare semver (:0.4.7), the<major>.<minor>channel (:0.4), and:latest.Terminal window docker run --rm -v "$PWD:/repo" ghcr.io/asamarts/alint:latestRuns as the distroless
nonrootuser (UID 65532). Foralint fixworkflows that need to write with host ownership, pass-u $(id -u):$(id -g).
Homebrew tap
Section titled “Homebrew tap”-
asamarts/alint— dedicated Homebrew tap at asamarts/homebrew-alint shipping aFormula/alint.rbthat resolves the right pre-built tarball for each platform (macOS arm64 + x86_64, Linuxbrew arm64 + x86_64) and verifies its SHA-256.Terminal window brew tap asamarts/alintbrew install alintThe formula is regenerated on every tagged release by a new
homebrewjob in.github/workflows/release.ymldrivingci/scripts/update-homebrew-formula.sh. The script takes SHAs directly from the release’sSHA256SUMSartifact — no re-download, no re-build — and pushes via a per-repo ed25519 deploy key scoped to the tap.
Infrastructure
Section titled “Infrastructure”- New release-workflow jobs:
docker(builds + pushes the multi-arch image to ghcr.io) andhomebrew(regenerates the formula and pushes to the tap). Both run after the existingbuild/releasejobs; failures there skip the distribution jobs cleanly. - New script
ci/scripts/update-homebrew-formula.shemits a completeFormula/alint.rbgivenVERSION+ aSHA256SUMSpath. Handles bothsha256sum-style and Windows-mode (*file) sum lines, errors clearly on missing platforms, validates required env up front. - New test harness
ci/scripts/test-update-homebrew-formula.sh(14 assertions — happy path, asterisk-prefix handling, missing-platform + missing-env error paths, version / license / test-block shape). Wired intoci/scripts/test.shso it runs on every CI pass.
Ecosystem coverage + debugging ergonomics. Schema-compatible; every v0.4.5 config runs unchanged. JSON output unchanged for existing commands; SARIF and GitHub outputs byte-equivalent.
Two new bundled rulesets
Section titled “Two new bundled rulesets”-
alint://bundled/python@v1(7 rules). Canonical Python-project hygiene:python-manifest-exists— pyproject.toml / setup.py / setup.cfg at the root (error).python-has-lockfile— uv.lock / poetry.lock / Pipfile.lock / pdm.lock (warning).python-pyproject-declares-name— PEP 621$.project.nameviatoml_path_matches(warning).python-pyproject-declares-requires-python— arequires-pythonfloor viatoml_path_matcheson$.project['requires-python'](info).python-module-snake-case— PEP 8 snake_case filenames for top-level andsrc/**/*.py(info).python-sources-final-newline+python-sources-no-trailing-whitespace(info, auto-fixable).python-sources-no-bidi— Trojan Source defense (error).
Every rule gated with
when: facts.is_python, so the ruleset silently no-ops on non-Python repos. -
alint://bundled/go@v1(7 rules). Go-module hygiene:go-mod-exists— go.mod at the root (error).go-sum-exists— go.sum at the root (warning).go-mod-declares-module-path—module <path>directive (error,file_content_matches).go-mod-declares-go-version—go <major>.<minor>directive (warning,file_content_matches).go-sources-no-bidi/go-sources-no-zero-width— Trojan Source defenses (error).go-sources-final-newline(info, auto-fixable).
Every rule gated with
when: facts.is_go.Brings the bundled catalog to eleven rulesets.
alint facts subcommand
Section titled “alint facts subcommand”- New top-level subcommand that evaluates every
facts:entry in the effective config and prints the resolved value. Debugging aid forwhen:clauses — quickly answers “did myfacts.is_pythonactually match?” without running the full check pass. Supports--format human(columnar) and--format json({facts: [{id, kind, value}, ...]}).
Changed
Section titled “Changed”alint-core::FactKind::name()added — returns the YAML discriminator string (any_file_exists,count_files, etc.). Used by thefactssubcommand’s renderers; available to external embedders.
Supply-chain hardening ruleset + composition ergonomics. Schema-compatible; every v0.4.4 config runs unchanged. JSON output gains no new keys; SARIF and GitHub outputs are byte-equivalent.
New bundled ruleset
Section titled “New bundled ruleset”-
alint://bundled/ci/github-actions@v1(3 rules). GitHub Actions hardening guided by the two OpenSSF Scorecard checks with the strongest supply-chain signal:gha-workflow-contents-read— every workflow declarespermissions.contents: readat the workflow level (yaml_path_equals, warning).gha-pin-actions-to-sha— everyuses:across every job is pinned to a 40-char commit SHA, not a mutable tag (yaml_path_matcheswithif_present: true, warning).gha-workflow-has-name— every workflow declares aname:so the Actions UI shows something friendlier than the filename (yaml_path_matches, info).
Scoped to
.github/workflows/*.y{,a}ml, so it no-ops in repos that don’t use GitHub Actions. Brings the bundled catalogue to nine rulesets.
Structured-query if_present
Section titled “Structured-query if_present”if_present: trueoption on every structured-query rule kind ({json,yaml,toml}_path_{equals,matches}). When enabled, a JSONPath query that returns zero matches is silently OK — only actual matches that fail the op produce violations. Preserves the default “missing = violation” semantics (if_present: false, the existing behaviour). Required for conditional predicates like “everyuses:is SHA-pinned” where a workflow with onlyrun:steps shouldn’t be flagged.
Selective bundled adoption
Section titled “Selective bundled adoption”-
only:/except:onextends:entries. An entry can now be a mapping that filters the inherited rule set by id before merging:extends:- url: alint://bundled/oss-baseline@v1except: [oss-code-of-conduct-exists] # drop one rule- url: alint://bundled/ci/github-actions@v1only: [gha-pin-actions-to-sha] # keep one ruleFilters resolve against the fully-resolved rule set of the entry (i.e. anything it transitively extends).
only:andexcept:are mutually exclusive on a single entry. Listing an unknown id is a load-time error so typos don’t silently drop anything.Closes the “all-or-nothing” limitation on bundled-ruleset adoption that forced users to extend + then restate overrides with
level: offfor every rule they wanted to skip.
Changed
Section titled “Changed”Config.extendstype changed fromVec<String>toVec<ExtendsEntry>.ExtendsEntryis an untagged enum that accepts either a bare string (classic form) or a mapping{url, only?, except?}. YAML ergonomics unchanged for the string form; existing configs continue to parse as before.
Rule-catalogue expansion + README rewrite. Schema-compatible; every v0.4.3 config runs unchanged. JSON output gains no new keys; SARIF and GitHub outputs are byte-equivalent.
Content-family additions
Section titled “Content-family additions”file_min_size— files in scope must be at leastmin_bytesbytes. Complementsfile_max_size. Picks up the “zero-byte LICENSE” case that passesfile_existsbut carries no information.file_min_lines— files in scope must have at leastmin_lineslines (wc -lsemantics: every\nterminates a line, plus one more when the file has trailing unterminated content). Catches the classic “README is a title plusTODO” stub. Both kinds register short aliases (min_size,min_lines) alongside the prefixed names.
Structured-query family (six new rule kinds)
Section titled “Structured-query family (six new rule kinds)”JSONPath (RFC 9535) queries over JSON / YAML / TOML documents,
powered by serde_json_path. YAML and TOML files are
deserialized through serde into a serde_json::Value so the
same path-expression engine applies to all three. Missing
JSONPath matches are treated as violations (conservative — scope
narrowly or relax the path for optional keys); when a query
returns multiple matches, every match must satisfy the rule.
Unparseable files surface a single per-file violation rather
than being silently skipped.
json_path_equals/json_path_matches—equalscompares by value (string / number / bool / null);matchesruns a regex against the string form of the matched value. Canonical use: enforce apackage.jsonlicense, require a semverversion, lock aprivate: trueflag.yaml_path_equals/yaml_path_matches— same engine over YAML. Canonical use: lock GitHub Actions workflows topermissions.contents: read, require everyuses:across every job to be pinned to a 40-char commit SHA.toml_path_equals/toml_path_matches— same engine over TOML. Canonical use: requireedition = "2024"across everyCargo.tomlin a workspace, enforce$.project.versionsemver inpyproject.toml.
oss-baseline ruleset extensions
Section titled “oss-baseline ruleset extensions”oss-license-non-empty—file_min_sizeat 200 bytes on the LICENSE, catching zero-byte placeholders.oss-readme-non-stub—file_min_linesat 3 on the README, gentle enough to pass for early-stage repos.
Changed
Section titled “Changed”- README rewrite. Replaced the single monolithic
.alint.ymlexample with a 12-pattern cookbook covering the real-world use cases v0.4 now spans: bundled-ruleset adoption, composition overrides, structured queries againstpackage.json/ GitHub workflows /Cargo.toml, monorepo per-package rules viafor_each_dir, nested-config subtree scoping, auto-fix hygiene, fact-gated conditionals, cross-filepair/unique_by, and the security-family bans. Bumped the family count from ten to eleven and the rule-kind count from ~42 to ~50.
Composition ergonomics + monorepo support + four new bundled rulesets. Schema-compatible; every v0.4.2 config runs unchanged. JSON output gains no new keys; SARIF and GitHub outputs are byte-equivalent.
Composition
Section titled “Composition”-
Field-level rule override. Children in the
extends:chain can specify only the fields that change. A common override shrinks from four lines to two:# before — had to restate kind + paths to tweak levelrules:- id: no-bakkind: file_absentpaths: "**/*.bak"level: warning# after — id + changed fields are enough; rest inheritsrules:- id: no-baklevel: warningThe loader keeps rules as raw
serde_yaml_ng::Mappings through theextends:chain and field-merges by id. After all extends resolve, each merged mapping is deserialized once into aRuleSpec— a rule that never receives akindanywhere in its chain surfaces as a clean error referencing the offending id. Facts still replace wholesale by id (their kind is a discriminated union). -
Nested
.alint.ymldiscovery for monorepos. Opt in withnested_configs: trueon the root config. The loader walks the tree (respecting.gitignore+ignore:), picks up every nested.alint.yml/.alint.yaml, and prefixes each nested rule’s path-like fields (paths,select,primary) with the nested config’s relative directory. A rule declared inpackages/frontend/.alint.ymlwithpaths: "**/*.ts"evaluates as if it readpaths: "packages/frontend/**/*.ts"at the root.MVP guardrails: nested configs can only declare
version:andrules:; every nested rule must have at least one scope field; absolute paths and..-prefixed globs are rejected; rule-id collisions across configs error with a clear message (per-subtree overrides are a follow-up).
Bundled rulesets
Section titled “Bundled rulesets”Four new rulesets pulled from a research pass across Turborepo/Nx/Bazel/Cargo/pnpm docs, OpenSSF Scorecard, and Repolinter’s archived corpus. Buildable on the existing primitive set — no new rule kinds required.
-
alint://bundled/hygiene/no-tracked-artifacts@v1— 11 rules.dir_absentonnode_modules,target,dist,build,out,.next,.nuxt,.svelte-kit,.turbo,coverage,__pycache__,.venv,.mypy_cache,.pytest_cache,.ruff_cache,.bundle,vendor/bundle,.go-build.file_absenton.DS_Store,._*,Thumbs.db,desktop.ini,*~,*.swp,*.swo,*.bak,*.orig,.env,.env.local,.env.*.local,.env.development/production/staging(.env.exampleis exempt). 10 MiB size gate. Several rules auto-fixable viafile_remove. -
alint://bundled/hygiene/lockfiles@v1— 7 rules, one per package manager (npm / pnpm / yarn / bun / Cargo / Poetry / uv). Each uses aninclude/excludepath pair so the root lockfile is exempted while nested copies are flagged as a workspace-misconfiguration smell. -
alint://bundled/tooling/editorconfig@v1— 3 info-level rules: root.editorconfig+.gitattributesexist, and.gitattributescontains atext=normalization directive. -
alint://bundled/docs/adr@v1— 4 rules. Files underdocs/adr/matchNNNN-kebab-case-title.md; each ADR has## Status,## Context,## Decisionsections. Gap-free ADR numbering deferred to a futurenumeric_sequenceprimitive.
Bundled catalog now: 8 rulesets (4 ecosystem + 4 namespaced).
Slash-namespaced names (hygiene/*, tooling/*, docs/*)
route through the existing alint://bundled/<name>@<rev> URI
scheme — the @ separator parses cleanly around slashes in
the name.
Config
Section titled “Config”nested_configs: truefield on the rootConfigto opt in to nested-config discovery.
Changed
Section titled “Changed”extends:schema description refreshed to cover SRI syntax,alint://bundled/URLs, merge semantics, and thelevel: offdisable idiom. Old description claimed HTTPS was “reserved for a future version” (shipped in v0.2.1).
Workspace: 422 → 437 tests (+15). Includes 6 new unit tests on nested-discovery, 3 e2e on field-level override, 2 e2e on nested discovery, 4 e2e on Phase A bundled rulesets.
0.4.2 — 2026-04-22
Section titled “0.4.2 — 2026-04-22”Pretty-output overhaul of the human formatter. Schema-compatible;
every v0.4.1 config still runs, every JSON/SARIF/GitHub output is
byte-equivalent. Only the human-mode rendering and three new global
CLI flags (--color, --ascii, --compact) are new.
- Grouped-by-file layout — violations now render under a
dim section header per file (
─── src/foo.rs ─────…), with a leading “Repository-level” bucket for path-less findings. Each violation shows<sigil> <level> <rule-id> [fixable]on one line and the message (optionally prefixed withline:col) on the next. Policy URLs render asdocs: <url>immediately under the relevant violation. - Per-severity summary —
Summary (N violations): ✗ 2 errors ⚠ 1 warning ℹ 5 info+X passing · Y failing · Z auto-fixable+ a call-to-action line→ runalint fixto resolve N fixable violation(s).when anything’s auto-fixable. All-passed gets a concise green✓ All N rule(s) passed.. --color <auto|always|never>global flag. Defaults toauto— honorsNO_COLOR,CLICOLOR_FORCE, and TTY status viaanstream::AutoStream. JSON / SARIF / GitHub formats are unaffected.--asciiglobal flag forces ASCII glyphs (e.g.x/!/iinstead of✗/⚠/ℹ,---instead of───). Auto-enabled whenTERM=dumb.--compactglobal flag switches to one-line-per-violation output suitable for editors,grep,wc -l. Format:path:line:col: level: rule-id: message [fixable], with<repo>as the pseudo-path for path-less findings.- OSC 8 hyperlinks on policy URLs when the terminal supports
them (detected via
supports-hyperlinks). Modern terminals (iTerm2, Kitty, WezTerm, Alacritty, VSCode, GNOME Terminal, Windows Terminal) make thedocs:URL clickable without any visible change. Older terminals see the plain URL they always saw. RuleResult.is_fixableexposed on the JSON output asfixable: bool, letting tooling decide whether to prompt users towardalint fixwithout cross-referencing rule metadata.
Changed
Section titled “Changed”- Terminal-width-aware section separators. Auto-detected via
terminal_sizeon TTY, clamped to[40, 120]cols so wide terminals don’t produce unreadably long rules and narrow ones still get some visual fill. Falls back to 80 cols off-TTY. - Tightened vertical density — no blank lines between violations within a bucket. Visual separation still reads clearly because the colored sigil anchors at column 2 while continuation lines indent to column 14. A typical 8-violation run drops from ~36 to ~27 lines of output.
Internal
Section titled “Internal”- New
alint-output::stylemodule centralizing role-basedanstyle::Styleconstants,GlyphSet(Unicode / ASCII), andHumanOptions(plumbing for glyphs / hyperlinks / width / compact). Swapping the palette is a one-file edit. Format::write_with_options/Format::write_fix_with_optionsadded; existingwrite/write_fixremain as default-opts shims so external embedders compile unchanged.
Dependencies
Section titled “Dependencies”anstyle+anstream(ANSI styling with built-inNO_COLOR/ TTY handling).supports-hyperlinks(OSC 8 detection).terminal_size(column-width detection).
0.4.1 — 2026-04-21
Section titled “0.4.1 — 2026-04-21”Packaging fix. v0.4.0 is functionally identical but failed to
publish beyond alint-core on crates.io — the bundled-rulesets
include_str! paths crossed the crate boundary, so
cargo publish for alint-dsl couldn’t find
rulesets/v1/*.yml when packaging the tarball.
- Move
rulesets/→crates/alint-dsl/rulesets/. The rulesets now live inside the crate that embeds them, socargo publishpicks them up automatically. Compile-timeinclude_str!paths inbundled.rschange from"../../../rulesets/…"to"../rulesets/…". No user-visible behaviour change — thealint://bundled/<name>@<rev>URI scheme and all four rulesets work identically.
Known leftover
Section titled “Known leftover”alint-core@0.4.0is live on crates.io (it published successfully before the packaging error stopped the chain). It’s functionally identical toalint-core@0.4.1and nothing transitively depends on it — safe to ignore or yank later.
0.4.0 — 2026-04-21
Section titled “0.4.0 — 2026-04-21”Headline: bundled rulesets. The single biggest adoption
lever identified during pre-launch review — reduces onboarding
from “write a ruleset” to “add one extends: line.” Also lands
pre-commit framework integration so any pre-commit user can
adopt alint with 4 lines of YAML.
Bundled rulesets
Section titled “Bundled rulesets”alint://bundled/<name>@<rev>URI scheme for offline resolution of built-in rulesets. Rulesets live underrulesets/<rev>/<name>.ymland are embedded in the binary viainclude_str!at compile time. Cycle-safe, leaf-only — a bundled ruleset cannot itself declareextends:and cannot introducecustom:facts, inheriting the same safety guards as HTTPS extends.alint://bundled/oss-baseline@v1— 9 rules. Community docs (README, LICENSE, SECURITY.md, CODE_OF_CONDUCT.md, .gitignore) + merge-marker + bidi-control bans + trailing-whitespace / final-newline hygiene (auto-fixable).alint://bundled/rust@v1— 10 rules. Cargo.toml / Cargo.lock / rust-toolchain existence, no trackedtarget/, snake_case source filenames, Trojan-Source defenses. Every rule gatedwhen: facts.is_rustso extending it from a polyglot repo is a safe no-op outside Rust trees.alint://bundled/node@v1— 8 rules. package.json + lockfile (npm / pnpm / yarn / bun), no trackednode_modules/or common build outputs, Node version pinned via.nvmrc/.node-version/.tool-versions, JS/TS source hygiene. Gatedwhen: facts.is_node.alint://bundled/monorepo@v1— 4 rules. Every directory under{packages,crates,apps,services}/*has a README + ecosystem manifest; unique basenames. Pair with rust@v1 / node@v1 for per-package ecosystem checks.
Distribution
Section titled “Distribution”-
.pre-commit-hooks.yamlat the repo root exposes two hooks for pre-commit users:alint— runsalint check; non-mutating.alint-fix— runsalint fix;stages: [manual]by default so it only runs when explicitly invoked viapre-commit run alint-fix.
Both use
language: rust, so pre-commit builds alint on first run — zero install step.
Changed
Section titled “Changed”- README quickstart gains a “Bundled rulesets (one-line baseline)” section and a “pre-commit” subsection under “Use in CI”.
docs/rules.mdgains a Bundled rulesets section with a per-ruleset table (rule id / kind / default level / fix op) for every shipped ruleset, plus the override pattern.- ROADMAP: the original v0.4 scope (structured-query
primitives, git-aware primitives, Homebrew / Docker / npm,
markdown/junit/gitlab outputs,
commandplugin, nested config discovery) rolls forward to v0.5. Bundled rulesets moved from v0.5 → v0.4 because they’re the largest single-step adoption lever.
Compatibility
Section titled “Compatibility”- Schema version remains
1. Every v0.3 config runs unchanged. - No changes to the
Rule,Fixer, orEngineAPIs.alint-dslgains a new publicbundledmodule; the existingload/load_withentry points are unchanged.
0.3.2 — 2026-04-21
Section titled “0.3.2 — 2026-04-21”Patch release fixing a broken action.yml that affects every
asamarts/alint@v0.2.1 / v0.3.0 / v0.3.1 consumer. No code
changes to the CLI; no schema changes.
action.yml— theoutputs.sarif-file.descriptionvalue was an unquoted YAML scalar containing`format: sarif`. GitHub’s Actions YAML parser recently tightened and now rejects the embedded:as an ambiguous nested mapping, causinguses: asamarts/alint@<any-released-tag>to fail at job-setup time withMapping values are not allowed in this context.. The description is now double-quoted.- docs build — rustdoc’s
redundant_explicit_linksbecame warn-by-default in a recent stable; combined with the workspace’sRUSTDOCFLAGS="-D warnings"it was breaking thecargo docCI job. Fixed a redundant link target inalint-testkit::runner. Affects contributors / anyone building from source with the current stable; no effect on the release binary.
Changed
Section titled “Changed”- Action self-test workflow now pins
uses:refs to@mainso it exercises the current action code instead of an immutable (and, as of this week, unparsable)@v0.2.1. The explicit-version test still passes a stable release tag as theversion:input — bumped fromv0.2.1tov0.3.1so the downloaded CLI understands the dogfood config’s v0.3 rule kinds.
Upgrade note
Section titled “Upgrade note”Consumers pinning asamarts/alint@v0.2.1 / v0.3.0 / v0.3.1
should bump to @v0.3.2. There are no API / CLI / config
changes — configs that worked under @v0.3.1 continue to work
verbatim.
0.3.1 — 2026-04-21
Section titled “0.3.1 — 2026-04-21”Documentation-only patch release following v0.3.0. No code changes; no schema changes; v0.3.0 configs run unchanged.
docs/rules.md— per-rule user reference organised by the ten families (Existence, Content, Naming, Text hygiene, Security / Unicode, Encoding, Structure, Portable metadata, Unix metadata, Git hygiene, Cross-file). Each entry has a purpose, a small YAML example, and a pointer to its fix op if one exists.
Changed
Section titled “Changed”docs/design/ARCHITECTURE.md— rule-catalogue section expanded with new family tables (Text hygiene, Security / Unicode sanity, Structure, Portable metadata, Unix metadata, Git hygiene) and a new Fix operations subsection listing every op with its rule-kind cross-reference.README.md— status line bumped from “v0.2 / 18 rules / 4 families” to “v0.3 / ~42 rules / 10 families / 12 fix ops”; GitHub Action references updated fromasamarts/alint@v0.2.1to@v0.3.0..alint.yml(dogfood) — expanded from 17 to 32 rules to exercise the v0.3 catalogue against alint’s own tree. All rules pass.
0.3.0 — 2026-04-21
Section titled “0.3.0 — 2026-04-21”Rule-catalogue expansion. Adds ~25 new rule kinds across seven
phase commits plus one new fix op, covering categories other
repo-linters don’t reach: Windows-name reserved words, bidi /
zero-width Unicode scanners, Unix-metadata checks, and
byte-level prefix/suffix. Also introduces the fix_size_limit
config knob and short-name aliases for the rules that don’t
have a dir_* sibling.
Rule kinds (text hygiene — Phase 1)
Section titled “Rule kinds (text hygiene — Phase 1)”no_trailing_whitespace— flag trailing space/tab on any line. Fixable viafile_trim_trailing_whitespace(preserves LF vs CRLF endings).final_newline— file must end with\n. Fixable viafile_append_final_newline.line_endings—target: lf | crlf; every line must use the configured ending. Fixable viafile_normalize_line_endings.line_max_width— cap line length in characters (not bytes); optionaltab_widthfor tab expansion.
Rule kinds (security / Unicode — Phase 2)
Section titled “Rule kinds (security / Unicode — Phase 2)”no_merge_conflict_markers— flag<<<<<<<,=======,>>>>>>>markers at the start of a line.no_bidi_controls— flag Trojan-Source bidi overrides (U+202A–202E, U+2066–2069). Fixable viafile_strip_bidi.no_zero_width_chars— flag body-internal zero-width characters (U+200B/C/D plus non-leading U+FEFF). Leading BOM isno_bom’s concern. Fixable viafile_strip_zero_width.
Rule kinds (encoding + content fingerprint — Phase 3)
Section titled “Rule kinds (encoding + content fingerprint — Phase 3)”file_is_ascii— every byte must be < 0x80.no_bom— flag UTF-8 / UTF-16 LE/BE / UTF-32 LE/BE byte-order marks. Fixable viafile_strip_bom.file_hash— assert a SHA-256 digest for specific files (rules-as-tripwire for generated artefacts).
Rule kinds (structure — Phase 4)
Section titled “Rule kinds (structure — Phase 4)”max_directory_depth— cap how deep the tree may go.max_files_per_directory— cap per-directory fanout.no_empty_files— flag zero-byte files. Fixable viafile_remove.
Rule kinds (portable metadata — Phase 5)
Section titled “Rule kinds (portable metadata — Phase 5)”no_case_conflicts— flag paths that collide under a case-insensitive filesystem (macOS HFS+/APFS, Windows NTFS defaults).no_illegal_windows_names— reject CON/PRN/AUX/NUL, COM1-9, LPT1-9 (case-insensitive, regardless of extension), trailing dots/spaces, and the reserved chars<>:"|?*.
Rule kinds (Unix metadata + git — Phase 6)
Section titled “Rule kinds (Unix metadata + git — Phase 6)”no_symlinks— flag tracked paths that are symbolic links. Fixable viafile_remove.executable_bit—require: true|false; enforce or forbid the+xbit. Unix-only; no-op on Windows.executable_has_shebang—+xfiles must begin with#!. Unix-only.shebang_has_executable— files starting with#!must have+xset. Unix-only.no_submodules— flag.gitmodulesat the repo root. Always targets.gitmodules(nopathsoverride). Fixable viafile_remove.
Rule kinds (hygiene + fingerprint — Phase 7)
Section titled “Rule kinds (hygiene + fingerprint — Phase 7)”indent_style—style: tabs|spaces, optionalwidthfor spaces; every non-blank line must indent with the configured style.max_consecutive_blank_lines—max: N; cap runs of blank lines. Fixable via new opfile_collapse_blank_lines.file_starts_with— byte-level prefix check. Works on binary files, unlikefile_headerwhich is UTF-8 text.file_ends_with— byte-level suffix check.
Fix ops
Section titled “Fix ops”file_trim_trailing_whitespace— strip trailing space/tab on every line (preserves line endings).file_append_final_newline— add\nwhen missing.file_normalize_line_endings— rewrite to the parent rule’slf/crlftarget.file_strip_bidi— remove U+202A–202E, U+2066–2069.file_strip_zero_width— remove U+200B/C/D and body-internal U+FEFF.file_strip_bom— strip a leading UTF-8/16/32 BOM.file_collapse_blank_lines— collapse blank-line runs to the parent rule’smax.
Config + ergonomics
Section titled “Config + ergonomics”fix_size_limit(top-level config field) — maximum bytes a content-editing fix will touch. Default 1 MiB; explicitnulldisables the cap; path-only fixes (file_create,file_remove,file_rename) ignore it. Over-limit files reportSkippedwith a stderr warning.- Short-name rule aliases — rules without a
dir_*sibling also resolve under their unprefixed name:content_matches,content_forbidden,header,max_size,is_text.file_exists/file_absentkeep the prefix because they mirrordir_exists/dir_absent.
Changed
Section titled “Changed”- JSON Schema (
schemas/v1/config.json) gains every new rule kind, fix op, and thefix_size_limitfield. Root and in-crate copies stay byte-identical via the drift-guard test.
Compatibility
Section titled “Compatibility”- Schema version remains
1. Every v0.2 config runs unchanged under v0.3. TheRuleandFixertraits gained no new required methods; out-of-tree implementations compile unmodified.Engine::with_fix_size_limitis additive.
0.2.1 — 2026-04-20
Section titled “0.2.1 — 2026-04-20”Patch release. Finishes the v0.2 roadmap item that didn’t make it
into v0.2.0: extends: composition.
extends:for local files. A config can inherit rules, facts, vars, and ignore globs from another YAML file on disk (relative or absolute path). Resolution is recursive with cycle detection; merge is id-based with child-overrides-parent semantics for rules + facts, dict-merge for vars, and concatenation for ignore globs.extends:for HTTPS URLs with SHA-256 SRI. Remote entries take the formhttps://.../foo.yml#sha256-<64 hex chars>. The SRI is non-negotiable: URLs without it are rejected. Responses are verified against the declared hash before use and cached atomically on disk at<user-cache-dir>/alint/rulesets/<sri>.yml.ureqis the underlying HTTPS client (rustls for TLS — no OS-native crypto linking).alint_dsl::load_with(path, &LoadOptions)for embedders and tests that need to pin the cache path or override the fetcher.- Action self-test workflow (
.github/workflows/action-selftest.yml) that dogfoodsasamarts/alint@<tag>acrossubuntu-lateston four configurations (default,format: sarif+ JSON-parse assertion,format: json, explicitversion:input). Catches regressions in the release-tarball → install.sh → binary distribution chain that in-process tests don’t exercise.
Security
Section titled “Security”- HTTPS
extends:requires SRI on every entry; no trust-on-first-use.http://schemes are rejected outright. Cache entries are re-verified against SRI on read, so a tampered on-disk cache fails loudly rather than serving bad content. Body size is capped at 16 MiB to bound memory against a hostile server.
Known limitations
Section titled “Known limitations”- Remote configs cannot themselves contain
extends:(nested remote extends deferred — a relative path inside a fetched config has no principled base for resolution). - The config-wide
respect_gitignorefield cannot distinguish “unset” from thetruedefault during merge; the child’s value wins unconditionally.
Added dependencies
Section titled “Added dependencies”ureq(rustls TLS),sha2,directories. Release-binary size impact: ~+1.5–2 MiB, mostly from rustls’ embedded root certs.
0.2.0 — 2026-04-19
Section titled “0.2.0 — 2026-04-19”Second release. The theme is composition and remediation: cross-file rules, conditional gating, auto-fix, and two new output formats. Every v0.1 config continues to validate and run under v0.2 without changes.
Rule kinds
Section titled “Rule kinds”- Cross-file primitives (7 kinds) —
pair,for_each_dir,for_each_file,dir_contains,dir_only_contains,unique_by,every_matching_has. These support path-template substitution ({dir},{stem},{ext},{basename},{path},{parent_name}) and nested rule instantiation for per-iteration semantics.
Facts + conditional rules
Section titled “Facts + conditional rules”- Facts system — repository properties evaluated once per run. Three kinds
shipped:
any_file_exists,all_files_exist,count_files. Referenced fromwhen:clauses and rule messages. when:expression language — bounded recursive-descent parser with boolean logic (and,or,not), comparison operators (==,!=,<,<=,>,>=),in(list or substring),matches(regex), literal types (bool/int/string/list/null), andfacts.*/vars.*identifiers. Parsed at rule-build time; gates both top-level rules and nested rules insidefor_each_*.
fix subcommand
Section titled “fix subcommand”alint fix [path]— applies mechanical corrections for violations whose rule declares a fix strategy. Five ops:file_create(paired withfile_exists) — writes declared content.file_remove(paired withfile_absent) — deletes the violating file.file_prepend(paired withfile_header) — injects content at the top, preserving UTF-8 BOM.file_append(paired withfile_content_matches) — appends content.file_rename(paired withfilename_case) — converts the stem to the rule’s target case, preserving extension and parent directory.
--dry-runpreviews the outcome without touching disk.- Safety —
file_createrefuses to overwrite existing files;file_renamerefuses to overwrite a collision; all ops skip cleanly with a diagnostic when preconditions aren’t met.
Output formats
Section titled “Output formats”sarif— SARIF 2.1.0 JSON, targeting GitHub Code Scanning’s upload action. Each violation becomes aresultwith aphysicalLocationanchored on the violating path.github— GitHub Actions workflow-command annotations (::error title=...::). Renders inline on PR file-changed view.
Distribution
Section titled “Distribution”- Official GitHub Action at
asamarts/alint@v0.2.0— composite action wrappinginstall.sh. Inputs:version,path,config(multi-line),format(defaultgithub),fail-on-warning,args,working-directory. Output:sarif-filefor feeding the Code Scanning uploader.
Testing infrastructure
Section titled “Testing infrastructure”alint-testkit+alint-e2e(internal crates) — scenario-driven end-to-end tests. 51 YAML scenarios auto-generated into#[test]s viadir-test, 20 CLI snapshot tests viatrycmd, and 4 property-based invariants viaproptest. See the ARCHITECTURE / testing sections for the rationale.
Changed
Section titled “Changed”- Internal workspace crates published —
alint-dsl,alint-rules, andalint-outputare now published on crates.io. They carrydescription: "Internal: ... Not a stable public API."; onlyalintandalint-coreare semver-stable. This change is required so thatcargo install alintresolves its transitive path-or-crates-io dependencies. - JSON Schema (
schemas/v1/config.json) gains thefix:block, every new rule kind, and thefacts:+when:fields. Root + in-crate copies are kept byte-identical by a drift-guard test.
install.shno longer aborts on SIGPIPE when resolving the latest release tag. Previously,curl | awk '{...; exit}'underset -o pipefailcaused curl to error out with exit 23 after awk’s early exit; the fetch is now decoupled from the parse.
Compatibility
Section titled “Compatibility”- Config schema version remains
1. Existing v0.1 configs run unchanged. - The public API of
alint-corehas not been broken. TheRuletrait gained afixer()method with aNonedefault, so out-of-tree rule implementations compile without modification.
0.1.0 — 2026-04-19
Section titled “0.1.0 — 2026-04-19”Initial release. MVP.
- 11 rule primitives —
file_exists,file_absent,dir_exists,dir_absent,file_content_matches,file_content_forbidden,file_header,filename_case,filename_regex,file_max_size,file_is_text. - Walker that honors
.gitignore; globset-based scopes with Git-style**semantics (separator-aware). - CLI subcommands:
check,list,explain. - Output formats:
human,json. - JSON Schema at
schemas/v1/config.jsonfor editor autocomplete. - Benchmarks (criterion micros + hyperfine macros).
- Static binaries on GitHub Releases for Linux (x86_64/aarch64 musl), macOS (x86_64/aarch64), and Windows (x86_64).
- Install script (
install.sh) with platform detection + SHA-256 verification. - Dogfood
.alint.ymlexercising the tool against its own repo.