1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
name: CI
on:
push:
branches:
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
fmt:
name: cargo fmt --check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --check
clippy:
name: cargo clippy (${{ matrix.features == '' && 'default' || matrix.features }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Same reasoning as build-and-test's matrix below: each feature config is its own
# compilation unit (see Cargo.toml's [features]), so each needs its own clippy pass, not
# just a single default-feature run.
features:
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
key: clippy-${{ matrix.features }}
- run: cargo clippy --tests --features "${{ matrix.features }}" -- -D warnings
audit:
name: cargo audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: audit
- run: cargo install cargo-audit --locked
# No --ignore flags, and it should stay that way. RUSTSEC-2026-0195/-0194 (quick-xml, via
# syntect -> plist) were ignored here until 2026-09-15 on the stated grounds that syntect
# pinned a vulnerable plist. It does not - syntect 5.3.0 asks for `plist ^1.3`, which permits
# 1.10.1, and that depends on quick-xml ^0.42, past the >=0.41 fix. What actually held the
# resolver back was this crate's own `rust-version`: plist 1.10.1 needs Rust 1.88, so the
# MSRV-aware resolver would not take it while we claimed 1.85. Raising the MSRV cleared both.
- run: cargo audit
mapping-site-js:
name: JS tests (human_mapping site, web viewer)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
# The Makefile target, not the two `node` lines by hand: it is what `make test` and the
# pre-push hook run, and the three must not drift apart (see the target's own comment for
# what the tests do and do not cover).
- run: make test-mapping-site-js
# The browser viewer's model (assets/web/), same arrangement - see the target's comment.
- run: make test-web-js
# The analysis scripts under research/ compute the numbers that go into the papers, and until
# 2026-08-27 nothing checked them at all while the Rust half had clippy -D warnings across three
# feature configs. The ruff version is pinned for the same reason the rule set is pinned in the
# root ruff.toml: an unpinned linter turns CI red on a day nobody touched the code. The three
# directories are the same three `make lint-python` covers.
python-lint:
name: ruff + pytest (research/, scripts/, assets/)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/ruff-action@v3
with:
version: 0.16.4
args: check
src: research scripts assets
- uses: astral-sh/ruff-action@v3
with:
version: 0.16.4
args: format --check
src: research scripts assets
# The Gentoo ebuild names all ~293 dependency crates, generated from Cargo.lock. A lock
# change that does not reach the ebuild produces a package that fails to build for users
# while looking perfectly fine in review, so it is checked rather than remembered.
- name: Gentoo CRATES list is in sync with Cargo.lock
run: python3 scripts/generate_gentoo_crates.py --check
# The Python unit tests, in research/'s uv environment (pytest is a dev dependency there).
- uses: astral-sh/setup-uv@v6
- run: make test-python
# The release gate, run on every push rather than only at deploy time - a regression is far
# cheaper to find on the commit that caused it than in the middle of a release. Safe to gate in
# CI because it is algorithm-only: mismatch counts against the human mapping do not vary by
# machine (unlike the runtime figure, which `make check-quality` only warns on).
quality:
name: quality gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: quality
# `test-fixtures`, not the Makefile's default `stats`: the gate needs no git2/rusqlite, and
# keeping them out of this job's cache is worth the one override.
- run: make check-quality FEATURES=test-fixtures
# The painting counterpart of the quality gate, and gated here for the same reason: byte counts
# against the hand-painted ground truth are algorithm-only and do not vary by machine. It
# separates what a reader sees from the part of it no matcher improvement can remove - see the
# target's own comment in the Makefile for why this baseline, unlike the quality one, moves when
# the ground truth is improved.
painting:
name: painting gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: painting
- run: make check-painting-attribution
build-and-test:
name: build + test (${{ matrix.features == '' && 'default' || matrix.features }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# "" = default features (tui only). test-fixtures, stats and web are each their own
# compilation unit - see Cargo.toml's [features] - so each needs its own build+test pass,
# not just a single default-feature run.
features:
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.features }}
- uses: taiki-e/install-action@nextest
- name: Build
run: cargo build --release --features "${{ matrix.features }}"
# cargo-nextest, not `cargo test`: it runs each test in its own process instead of as a
# thread inside one long-lived binary, so this crate's test fixture caches (never-evicting,
# process-lifetime - see src/test/helper.rs) get reclaimed by the OS after every test rather
# than accumulating for the whole suite. Measured locally: same machine, same --release
# build, peak RSS 10.66GB under `cargo test --release` vs 5.37GB under `cargo nextest run
# --release`, at comparable wall-clock - the difference between a CI runner OOMing on this
# suite and not.
- name: Test
run: cargo nextest run --release --features "${{ matrix.features }}"