How Stilyagi works.
Five stages from filesystem to diagnostic. Rust extracts structure. Python analyses it. A capability planner decides which linguistic providers to load. The formatter renders verdicts. CI reads them.
Five stages, one contract.
Discover
Config loading, path walking, owner attribution. A normalised set of FileInputs arrives at the extractor.
PythonExtract
Rust reads source bytes and emits a region-oriented Document IR with segments, a line_index, and a content_hash.
RustPlan
The capability planner inspects enabled rules and materialises only the annotators that are actually required.
BridgeAnalyse
Rules run against typed region wrappers. Diagnostics, suppressions, and fix payloads are accumulated deterministically.
PythonReport
The formatter renders plain text, JSON, or SARIF. Exit codes are disciplined. CI is happy.
PythonExtraction only.
- Parses Markdown, MDX, reStructuredText, Python, and Rust source.
- Emits a region-oriented Document IR keyed by byte offsets.
- Computes the
line_indexandcontent_hash. - Owns no rules. Owns no diagnostics. Owns no policy.
- Narrow FFI boundary: one data structure crosses the line.
Analysis, fixes, and output.
- Owns the rule engine, capability planner, and diagnostic model.
- Provides typed wrappers over regions: Heading, Paragraph, Code, Docstring.
- Plans spaCy and spellchecker providers on demand.
- Applies safe fixes; flags unsafe fixes; refuses to touch synthetic spans.
- Emits text, JSON, and SARIF with stable ordering.
A rule is a Python class.
# rules/heading_depth.py
from stilyagi.rule import Rule, Level
from stilyagi.regions import Heading
class HeadingDepthRule(Rule):
"""Reject Markdown headings deeper than level 3."""
code = "MD201"
name = "heading-depth"
level = Level.WARNING
pack = "default"
capabilities = () # no spaCy, no spellchecker
def check_heading(self, h: Heading) -> None:
if h.depth > 3:
self.report(
span=h.marker_span,
message=f"Heading depth {h.depth} exceeds maximum of 3.",
suggestion="Promote to a new section, or use bold text.",
)
The IR is the contract.
Rules never see raw source bytes directly, and never drive a parser. They receive a typed
region — a Heading, a Paragraph, a Docstring — backed
by a single shared Document IR. The region knows its span, its depth, its content, its
markers. It knows whether it was synthesised from structural inference or extracted verbatim.
This is what separates Stilyagi from a regex harness. A regex doesn't know whether it is inside a code block, a docstring, a comment, or a heading. A Stilyagi rule subscribes to region classes and is only ever called for the ones it asked for — which is also what lets the capability planner skip spaCy loading when no enabled rule needs it.
Determinism is designed, not incidental.
Every diagnostic carries a canonical sort key. Files are ordered by normalised path; diagnostics within a file by byte offset, rule code, and stable message hash. Two rules cannot emit fixes to overlapping, non-identical ranges without one losing explicitly. Stilyagi wants CI to be bored, and agents to have something they can trust.
Six verbs. No surprises.

--format text|json|sarif--fix=safe|unsafe|none--select CODE,CODE--ignore PATTERN--stdin-filename PATH--config PATH--safety safe|unsafe--dry-run--diff--preview--file PATH--format json|pretty--include annotations--mask-hashesThe capability planner.
Linguistic providers are expensive. A small lint run that only checks heading depth should not pay for spaCy model loading, a symspell dictionary, or a terminology index. The planner exists to prevent this tax.
Each rule declares a capabilities tuple. At startup, the planner intersects the
enabled ruleset with the capability graph and materialises only the providers required. Rules
never import providers directly — they ask the region for an annotation, and the planner
guarantees it will be there or the rule will be skipped with a single loud diagnostic.
The same mechanism lets Stilyagi stay offline: grammar and spelling providers ship as optional extras, stilyagi[grammar] and stilyagi[spell], and the planner refuses to enable a rule whose capability is absent. The base install does nothing clever, reaches no network, and stays useful.