Pipeline
Pipeline is the value prose format and prose check build and run. It carries the registered rules in their canonical order and reads them in a few ways. run splices the edits of each batch of consecutive rules the registry declares independent into a fresh buffer in one pass, reparses between batches so every later rule reads a settled AST, and returns the final Source beside its diagnostics and the set of rules that edited. diagnose collects every rule's findings against the source as written, for reporting. settle_report reads a buffer a run has already produced and names what still changes it, and unsettled returns the first part of that report alone.
Public Surface
Pipeline is fully public today, so a downstream Rust consumer builds one through the entry points below, runs it against a Source, and reads the returned text and diagnostics. Pipeline is Send + Sync, so one instance can be shared across rayon workers through Arc, and the same instance can drive many run calls in sequence, because run takes &self and consumes only the Source passed in.
Constructors
Pipeline::empty() -> Selfreturns a pipeline with no rules, for tests or a caller building its own rule set.Pipeline::with_defaults(config: &Config) -> Selfbuilds the canonical pipeline from every rule whoseenabledflag is set in[tool.prose]. Theprose serverformatting and diagnostics paths and the WebAssembly bindings call this, and the CLI reaches the same set throughwith_filterswith no flags.Pipeline::with_filters(config: &Config, select: &[RuleId], ignore: &[RuleId]) -> Selfapplies the CLI's--selectand--ignoresemantics. A non-emptyselectreplaces the configured-enabled set, an emptyselectfalls back to it, andignoresubtracts from that base, givingselect - ignore.Pipeline::for_rule(name: &str, config: &Config) -> Option<Self>builds a single-rule pipeline, for isolating one rule's diagnostics and for the exact-rule path ofprose check --select <rule>. ReturnsNonefor an unrecognized slug.Pipeline::sharing(self, sharing: Sharing) -> Selfsets which rules a run lets share one splice and one parse.Sharing::Declared, the default, follows the registry's shared-splice column.Sharing::Neverreparses after every editing rule, which is the run the subset probe measures a batched pair against.Sharing::Alwaysbatches every rule and reports a batch the reparse rejects asPipelineError::Batch, which is how the probe tests whether a pair's edits are independent.
Enumeration
Pipeline::known_ids() -> &'static [RuleId] returns the full registered-rule list in canonical order, the same list the CLI's --help reads. A consumer driving its own UI over the catalog reads from this.
Splitting
split(self) -> Vec<(RuleId, Self)> breaks a pipeline into one single-rule pipeline per rule it carries, in order, each keeping its rule exactly as the parent's selection built it. Several rules read a sibling's flag off the resolved selection, band-constants and alphabetize-siblings among them reading whether group-imports is enabled, so a rule built alone through for_rule is not the rule that runs beside its sibling. split is how a consumer runs the two one at a time without changing what either read.
fingerprint(&self) -> String renders every carried rule's settings, equal for two pipelines whose rules resolved alike, so a consumer with many single-rule pipelines can share the ones that would behave the same.
fingerprints(&self) -> Vec<String> renders one fingerprint per carried rule in registration order, each equal to what that rule's own single-rule pipeline renders, so a consumer comparing two selections position by position reads them without splitting either.
Execution
run(&self, source: Source) -> Result<(Source, Vec<Diagnostic>, BTreeSet<RuleId>), PipelineError> reads through the registered rules in their canonical order. Each batch of consecutive rules the registry declares independent computes its edits against one buffer, and the pipeline splices them in a single pass and rebuilds the tree through Source, which reparses only the statements those edits reached where it can and the whole file where it cannot. The new Source feeds the next batch, carrying every table of the previous buffer that all of the batch's edits left standing. A rule the registry does not declare independent of every rule already in the batch, or whose edits overlap one already batched, opens the next batch against the reparsed buffer. When a batch of several rules produces a splice the reparse rejects, the pipeline replays them one at a time, so the error names the rule whose own edits fail. The final text, every diagnostic, and the set of rules whose edits landed return to the caller, so a settle check running afterward reads that set rather than deriving it from the diagnostics.
Suppression is applied inside run, with every # fmt: off block, # fmt: skip marker, and # prose: ignore[<rule>] directive read at the point edits are emitted, so a suppressed fix group or lint diagnostic never reaches the returned vector. A fix group is dropped whole as soon as one of its edits falls under a directive, leaving a rule's co-dependent edits either all applied or all withheld.
format(&self, source: Source) -> Result<Source, PipelineError> makes the same pass and returns the settled text alone, skipping the diagnostics run collects and the lint pass it ends with, so a consumer that needs only the rewrite calls this. format_span(&self, source: Source, seats: Range<usize>) -> Result<Source, PipelineError> limits that pass to the rules at the positions in seats, so a caller that already has the text produced by a prefix of the pass resumes after it rather than re-deriving it, with the compile gate reading the segment's entry source. The corpus sweep runs its width-and-axis slices as a tree through these two, each budget-narrowed slice resuming after the positions it shares with an earlier slice.
diagnose(&self, source: &Source) -> Vec<Diagnostic> collects every enabled rule's findings against the unmodified source, applying no edits and never reparsing, so each range points into the source as written rather than into an intermediate rewrite. prose check, prose server, and a structured format report through diagnose, where a rendered diagnostic points at the file the author wrote, whereas run produces the rewritten text behind prose format's diff, on-disk rewrite, and would-reformat summary. Both read the same SuppressionMap and rule set, and differ only in that diagnose reads every rule against the original where run reads each against the buffer its batch opened on.
unsettled(&self, source: &Source) -> Vec<RuleId> names every rule this pipeline carries whose edits would still rewrite source, and returns empty for a buffer that has settled. It reads the subset the pipeline was built with rather than the default set, so a --select run answers for that selection alone, and a file carrying a file-level # prose: off answers empty because no rule reaches it. Every prose format run makes this pass over each file it rewrote, narrowed through unsettled_among to the rules that edited on the first pass, and raises the unstable-output notice where it names any rule. prose check --validate and the corpus sweeps keep the full pass, and the sweep over each rule alone and each ordered rule pair is where a rule that depends on a later rule to finish its work shows up.
unsettled_among(&self, source: &Source, fired: &BTreeSet<RuleId>) -> Vec<RuleId> makes that same pass over fired alone, the set run returns beside the rewritten text. A format run re-applies only the rules that edited on its first pass, so a rule that stayed silent is left to the full unsettled walk. The docs-site sandbox reads it through the wasm bindings to name what a second run would still change.
settle_report(&self, source: &Source) -> SettleReport makes the same pass once and returns three things unsettled collapses into one. editing is the rules whose edits still rewrite source, in registration order, unlanded is the rules with a fix group that splices back to the same text or does not apply, and witness is the first editing rule paired with the text its edits produce, which a report shows as the rewrite. unsettled makes the same pass without producing the witness and returns editing alone. The corpus sweep reads settle_report over every file a run produced, so a rule that is stable and incomplete at once shows up beside a rule that keeps editing.
pub struct SettleReport {
pub editing : Vec<RuleId>, // the rules whose edits still rewrite the buffer
pub unlanded : Vec<RuleId>, // the rules reporting a fix the weave never lands
pub witness : Option<(RuleId, String)>, // the first editing rule and the text it weaves
}Diagnostic carries the per-finding payload returned in the Vec:
pub struct Diagnostic {
pub fix : Option<Fix>, // the fix and its edits, or `None` for lint-only findings
pub message : String, // human-readable explanation of the finding
pub range : TextRange, // source span the finding points at
pub rule : RuleId, // slug of the rule that emitted the finding
pub severity : Severity, // `Format` for auto-fix, `Lint` for report-only
}Severity::Format carries a Some(fix) payload the pipeline applies, whereas Severity::Lint carries fix: None and reports a finding the user resolves by hand. A consumer building a structured output format (JSON, SARIF, GitHub annotations) routes by rule to tie each finding to the slug that emitted it.
PipelineError is pub and carries one variant per failure the pipeline can report:
pub enum PipelineError {
Batch { rules: Vec<RuleId> },
Cell { cell: OneIndexed, rule: RuleId, source: ParseError },
Compile { error: SemanticSyntaxError, rule: RuleId },
Reparse { rule: RuleId, source: ParseError },
}Every variant names the rule whose output failed:
- A
Batcherror names every rule of a batch whose one-pass splice the reparse rejected. It appears only underSharing::Always, whereas the default sharing replays the batch's rules one at a time and reports the responsible rule through one of the other three. - A
Cellerror means a notebook cell that parsed on its own before the rule ran no longer does, naming that cell by its position in the notebook. - A
Compileerror means the output parses yet fails the semantic-syntax check Python's owncompileapplies. - A
Reparseerror means a rule produced syntactically invalid Python.
The last three are rule-authoring bugs rather than conditions a consumer recovers from. The intermediate Source is dropped either way, so the caller gets no partial output to inspect.
Determinism
Rule order is fixed and the same on every run, so a given source and configuration always produce the same output. The registry pins the order in a single register_rules! macro invocation in crate/src/rules/registry.rs, and the pipeline runs rules in that order with no parallelism inside one Source. Parallelism across sources (two files at once) belongs to the path-mode CLI, which the walker drives above the pipeline rather than inside it.
Internal Surface
Pipeline::from_rules is pub(crate), so a downstream crate cannot register a hand-built rule list today. The Rule trait that concrete rules implement is also pub(crate), and each rule declares through its preserves_bindings method whether its edits leave every binding standing, which controls whether the next Source inherits the binding table across the reparse. Both open toward 1.0, when a consumer will be able to compose a custom rule set and implement project-specific rules against a stable trait.
Re-Using This Primitive
The canonical form for a downstream Rust consumer is:
use prose::config::Config;
use prose::pipeline::Pipeline;
use prose::source::Source;
let config = Config::default();
let pipeline = Pipeline::with_defaults(&config);
let source = Source::from_path("example.py")?;
let (formatted, diagnostics, fired) = pipeline.run(source)?;
println!("{}", formatted.text());For a single-rule isolation, Pipeline::for_rule("align-equals", &config) returns a pipeline that runs only align-equals against the source.
The Cargo dependency line (prose = { git = "...", tag = "<version>" }) lives on the Source page. The Python wheel exposes the CLI rather than the library, so a Python consumer reaches the same pipeline through the binary.
Related
Sourceis the value the pipeline reads and returns, reparsed between batches of independent rules so each later rule reads a settled AST, with the binding table carried into the new value where every member of the batch keeps every binding.RuleIdis the handle each rule registers under, which the pipeline's fixed ordering reads andknown_idsreturns.SuppressionMapfilters the pipeline's emitted edits and lint diagnostics, dropping suppressed entries before they reach the caller.BindingAnalysisbuilds on first read, carries across a reparse where every rule in the batch keeps every binding, and feeds the rules that read it.
For the rule catalog the pipeline runs, the Rules page lists every shipped rule by category, and the Pipeline Order reference renders the canonical run order with the rationale per rule.