The rules, the config, the seal.
Every Stilyagi rule has an ID, a namespace, a severity, and a fix class — knowable at a glance. Configuration is a single TOML file. Suppressions live in the source. This page lays the whole operating surface out in one place.
The rules catalogue
Markdown 12 PyDoc 7 RsDoc 4 Punctuation 5 Grammar 3 Spell 3
| Rule | Title & description | Severity | Autofix |
|---|---|---|---|
| No rules match that filter. | |||
| MD MD201 |
Heading depth Headings must not skip levels (h2 → h4 is rejected). |
Warning | autofix safe |
| MD MD202 |
ATX heading style Prefer # over === / --- setext headings. |
Warning | autofix safe |
| MD MD203 |
Trailing heading punctuation No trailing punctuation on headings except question marks. |
Info | autofix safe |
| MD MD210 |
Unordered list marker Use - consistently across a document. |
Info | autofix safe |
| MD MD211 |
Ordered list prefix Order list items sequentially from 1, not all as 1. |
Warning | autofix safe |
| MD MD220 |
Hard line break No trailing double-space line breaks; use \ or a blank line. |
Info | autofix safe |
| MD MD230 |
Fenced code language Fenced code blocks must declare a language. |
Warning | autofix unsafe |
| MD MD231 |
Indented code Prefer fenced code over indented code. |
Info | autofix safe |
| MD MD240 |
Autolink bare URL Wrap bare URLs in <…> or a link. |
Warning | autofix safe |
| MD MD241 |
Link text empty Links must have non-empty text. |
Error | autofix — |
| MD MD250 |
Image alt text Images must carry alt text. |
Error | autofix — |
| MD MD260 |
Inline HTML Inline HTML in Markdown flagged; can be allowlisted. |
Info | autofix — |
| PUN PUN101 |
Double space after period Collapse double spaces after terminal punctuation. |
Info | autofix safe |
| PUN PUN201 |
Smart quotes Curly quotes preferred over ASCII quotes in prose regions. |
Warning | autofix safe |
| PUN PUN202 |
Em-dash spacing Em-dashes must not be surrounded by spaces (house style). |
Info | autofix safe |
| PUN PUN203 |
Oxford comma Final comma before the conjunction in a list of three or more. |
Warning | autofix safe |
| PUN PUN204 |
Ellipsis Use the ellipsis character, not three periods. |
Info | autofix safe |
| PYDOC PYDOC101 |
Missing docstring Public modules, classes, and functions require a docstring. |
Warning | autofix — |
| PYDOC PYDOC102 |
Summary line First docstring line must be a complete imperative sentence. |
Warning | autofix unsafe |
| PYDOC PYDOC103 |
Summary length Summary line must be 80 characters or fewer. |
Info | autofix — |
| PYDOC PYDOC110 |
Imperative mood Summary should read "Compute…" not "Computes…". |
Info | autofix unsafe |
| PYDOC PYDOC120 |
Args alignment Every declared parameter must be documented; no phantoms. |
Error | autofix — |
| PYDOC PYDOC121 |
Returns documented Functions with a return value require a Returns section. |
Warning | autofix — |
| PYDOC PYDOC130 |
Raises cited Documented exceptions must match actual raise statements. |
Warning | autofix — |
| RSDOC RSDOC101 |
Crate root docstring Every crate root must carry a //! documentation comment. |
Warning | autofix — |
| RSDOC RSDOC110 |
Public item doc Public items require /// documentation. |
Warning | autofix — |
| RSDOC RSDOC120 |
Code example Public API examples should compile under rustdoc. |
Info | autofix — |
| RSDOC RSDOC130 |
Intra-doc links Prefer intra-doc links over bare paths. |
Info | autofix safe |
| GRAM GRAM201 |
Passive voice Flag passive-voice sentences in prose regions. |
Info | autofix — |
| GRAM GRAM210 |
Sentence length Flag sentences beyond the configured word ceiling. |
Info | autofix — |
| GRAM GRAM220 |
Weasel words "Very", "quite", "rather" — flagged, not forbidden. |
Info | autofix — |
| SPELL SPELL101 |
Unknown word Words absent from dictionary and allowlist. |
Info | autofix — |
| SPELL SPELL110 |
Commonly misspelled High-confidence corrections for common misspellings. |
Warning | autofix safe |
| SPELL SPELL120 |
Terminology consistency Enforce canonical spellings for project terms. |
Warning | autofix safe |
Configuration · one file, no ceremony
pyproject.toml [tool.stilyagi]
The select / ignore model
Stilyagi borrows its configuration vocabulary from Ruff. You select rule codes
or namespaces to enable; you ignore specific codes to silence. Namespaces are
the uppercase prefix — MD, PUN, PYDOC — and match
by prefix, not by regex.
Per-rule options live under [rule.XXX]. A rule that accepts options (like
PUN201, smart quotes) documents its schema in its own description; invalid
keys fail the run rather than being silently ignored.
# stilyagi.toml line-length = 100 select = ["MD", "PUN", "PYDOC"] ignore = ["MD260", "PUN204"] # Per-rule configuration [rule.PUN201] style = "british" allow-straight = ["code", "inline_code"] [rule.GRAM210] max-words = 32 # Per-file overrides — glob-matched [per-file] "docs/reference/**" = { extend-select = ["GRAM"] } "**/CHANGELOG.md" = { ignore = ["MD201"] }
Suppressions · three scopes, no magic
<!-- stilyagi: disable-next-line PUN201 --> She said "no" — a single word, straight-quoted on purpose. This paragraph opts out of the sentence-length check for dramatic effect. <!-- stilyagi: disable-block GRAM210 --> It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of light, it was the season of darkness, it was the spring of hope, it was the winter of despair. <!-- stilyagi: enable-block GRAM210 --> Ordinary prose resumes here. <!-- stilyagi: disable-line PUN101 --> # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # disables only the current line
def compute_metrics(series, window=7):
"""
Compute rolling metrics across the given series.
The smoothing window is in trading days, not calendar days.
Args:
series: price series, normalized.
window: lookback in trading days (default: 7). # stilyagi: disable-line PYDOC103
_internal_debug_flag: unused; retained for historical
instrumentation and compatibility.
"""
...
Suppressions live in the source, not in the config. They travel with the prose they protect. A suppression must name its rule — wildcards are refused — so a reader can always answer the question: why is this line exempt?
The testing sandbox
hermetic by default
PlannedSpecified in RFC 0004; the current tree carries an internal golden-IR and fix round-trip scaffold while the public fixture is built.
Real runs, isolated state
Rule authoring is a first-class feature, so rule testing is one too. The sandbox is a
pytest fixture — stilyagi_path — that wraps tmp_path into a
temporary project root you write ordinary files into, then runs the real
stilyagi against them and hands back typed diagnostics.
Runs go through a subprocess by default, because that is what exercises the installed command surface, config discovery, entry-point loading, exit codes, JSON rendering, and the Rust extension import. An in-process fast path would test a different program from the one users run.
Hermetic is the default, not a flag: --isolated, --no-cache, a
working directory rooted in the temporary project, and UTF-8 forced. No user config
leaks in, no cache survives, and output paths are normalized to
./like/this.md so assertions read the same on every platform.
# tests/test_serial_comma.py from stilyagi.testing import assert_has_diagnostic def test_serial_comma(stilyagi_path): stilyagi_path.write_config( '[tool.stilyagi.lint]\n' 'select = ["PUN201"]\n' ) (stilyagi_path / "doc.md").write_text( "Apples, bananas and pears.\n" ) result = stilyagi_path.run_check("doc.md") diag = assert_has_diagnostic( result.diagnostics, code="PUN201" ) assert diag.fix.applicability == "safe"
Packs and extraction, both testable
Third-party rules arrive through entry points, so the sandbox has to be able to
manufacture something that looks installed.
install_rule_pack() writes a package tree and a .dist-info
directory with real entry_points.txt metadata, then puts it on the
subprocess path — the same discovery contract production uses, without a virtual
environment or a pip install per test.
run_dump_ir() returns parsed IR documents, so a test can assert on region
kinds, owner metadata, synthetic segments, and suppression state. That distinction
matters: a false positive is either a bad rule or a bad extractor, and the harness
should make it cheap to tell which.
The assertion helpers stay deliberately dumb —
assert_has_diagnostic, assert_no_diagnostics,
assert_only_codes, assert_has_fix,
assert_ir_region. They exist to delete repetitive test noise, not to become
a second assertion language. And the name is a promise about isolation, not about
safety: rule packs are trusted Python here exactly as they are everywhere else.
def test_custom_pack(stilyagi_path): stilyagi_path.install_rule_pack( name="demo-pack", modules={"demo_pack/rules.py": RULE_SOURCE}, rule_entry_points={ "demo": "demo_pack.rules" }, ) stilyagi_path.write_config( '[tool.stilyagi]\n' 'plugins = ["demo-pack"]\n' ) (stilyagi_path / "doc.md").write_text( "foo appears here\n" ) result = stilyagi_path.run_check("doc.md") assert_has_diagnostic( result.diagnostics, code="DEMO001" )
A colophon,
of sorts.
Stilyagi is, first, a working prose linter. It is also, deliberately, an agitprop object — a statement about what documentation tooling could be if we stopped treating prose as a second-class artefact of software.
Rust does the boring, bytes-level work: opening files, finding the regions that contain prose, emitting an intermediate representation with stable line indices and content hashes. Python does the interesting work: writing down the rules of a house style, one rule at a time, in a language the people who care about prose can read.
The capability planner is the hinge. It keeps the fast path fast and permits the slow path to exist. Without it, grammar and spellcheck could not join the catalogue; with it, they are merely entries in a toml file, loaded on demand, uninvolved when unwanted.
The name is deliberate. Stilyagi, like the people it takes its name from, believes that style is a political act — and that doing style well, in the grain of a rigorous system, is the point.