Design notes.
Two working models — the IR as a contract, and the capability planner as a lazy loader — plus the architecture decisions that pin them in place. Hover below. Toggle. Watch the machine.
Every region remembers its bytes.
The IR is what the Rust extractor hands to the Python rule engine, and it is the only thing rules ever see. It is not a copy of the Markdown tree. It is a flat list of prose regions, each carrying its kind, its structural owner, and the exact byte range it occupies in the file on disk.
That byte range is the whole contract. Diagnostics and fixes are expressed in original source offsets, so an edit can only ever land on text that was really there. Where markup was elided — emphasis, link syntax, a soft line break — a segment map records which slice of the region's text came from which source bytes, and marks the remainder synthetic so no fix can touch it.
Two more fields are first-class rather than conveniences.
content_hash keys the extraction cache, so an unchanged file is never parsed
twice. line_index turns a byte offset into a line and column without reparsing
the source, which is what keeps diagnostic rendering cheap.
stilyagi dump-ir prints all of it as canonical JSON, and that is the debugging
surface the whole architecture leans on: when a rule misfires, the IR tells you whether the
extractor got the region wrong or the rule got the logic wrong.
☞ Hover either side of the panel below — the source span and the IR node that owns it light up together, and the footer reports the byte range.
[136,172]source-faithful · no rewrites
12content_hash sha256:7a3f…e041deterministic
Toggle rules. See which providers load.
Enabled ruleset
Planner verdict
From tokens to clauses
Part-of-speech tags are where a prose linter usually leaks its backend. Stilyagi normalizes
them into UPos — the seventeen coarse categories Universal Dependencies defines
once for every language it covers.
- ADJ
- ADP
- ADV
- AUX
- CCONJ
- DET
- INTJ
- NOUN
- NUM
- PART
- PRON
- PROPN
- PUNCT
- SCONJ
- SYM
- VERB
- X
Coarse is the point. UD keeps the inventory small and deliberately cross-linguistic, pushing
the finer detail into morphology — which Stilyagi carries separately as
MorphFeatures. The provider's own fine-grained tag stays reachable as
fine_pos for debugging, but it never becomes the cross-provider contract.
Four derived syntactic nodes — TokenNode, SentenceNode, NounPhraseNode, and CoordinationNode — are exposed to rule authors on top of that. The Embassy Rule: Python rules talk only to stable Stilyagi grammar nodes, not raw, unstable spaCy classes.
Nine decisions, on the record.
Python package, Rust extractor
AcceptedShip one wheel per platform. PyO3 bindings. Pre-built wheels for Linux, macOS, Windows; sdist only if requested.
No bundled spellchecker in base
AcceptedSpelling is an optional extra. Base install stays small and offline. Network-free by default is a hard contract.
Rules are trusted code
AcceptedPlugins run as ordinary Python. No sandbox. Documented loudly in the README, CLI help, and the plugin guide.
Region IR, not raw AST
AcceptedRules subscribe to typed region classes. Raw AST is an implementation detail of the extractor, not a public surface.
Capability planner
AcceptedProviders load only when at least one enabled rule declares them. Runtime cost is proportional to the enabled rule set.
Markdown first; MDX provisional
ProvisionalMDX support ships behind a feature flag. Tree-sitter grammar stability is the pacing risk.
SARIF as primary machine format
AcceptedJSON is the CLI-friendly fallback; SARIF is the CI integration format. One schema for both versions, stable.
Fix safety is explicit
Acceptedsafe / unsafe / manual are first-class. The --fix flag accepts a safety class; unsafe fixes never run without an opt-in.
Deterministic ordering
AcceptedNormalised path, byte offset, rule code, message hash. The same input yields the same output bytes, always.