RFD0007 - Riot Fix
- Feature Name:
riot_fix - Start Date:
2026-03-20 - Status:
implemented
Summary
Section titled “Summary”This RFD proposes turning riot-fix into the first syntax-aware linting and
auto-fix tool in the Riot stack, exposed primarily as riot fix. The system
should parse source files once with syn, run a configurable set of lint rules
over the resulting red tree, report structured diagnostics, and apply safe
edits by default. The immediate goal is not to solve formatting or
broad code modernization. The goal is to establish a reliable fix pipeline that
can power small, explicit, high-confidence rewrites across Riot codebases.
Motivation
Section titled “Motivation”Riot now has the parser foundation needed for a real fix tool:
synparses the Riot codebase successfully and has broad syntax coverage.synproduces a lossless CST.synhas a dedicated diagnostics suite and a large fixture corpus.ceiboprovides the underlying tree model needed for syntax-aware traversal.
That changes what is practical.
Until now, linting and mechanical cleanup in the repo have largely been one of three things:
- style and convention enforcement in human review
- ad hoc shell-based rewrites
- source-level grep checks without syntax awareness
Those approaches do not scale well once the codebase grows and the conventions become more intentional. Riot has strong conventions:
- prefer
open Std - avoid direct
Stdlib,Unix, andSysusage outside owned boundaries - keep package APIs abstract
- use the Riot stack rather than defaulting back to stock OCaml libraries
If Riot wants conventions over configuration and a value-oriented stack, then the stack should help enforce and repair those conventions.
There are several concrete use cases:
- A contributor runs
riot fix --checkand gets actionable diagnostics about directStdliborUnixusage in the wrong package. - A contributor runs
riot fixand the obvious safe rewrites are applied automatically. - Future migrations, such as renaming modules or updating API usage, are expressed as rules instead of one-off scripts.
- Editor or agent tooling can reuse the same diagnostics and fixes instead of inventing parallel logic.
This RFD is motivated by two beliefs:
- syntax-aware rewriting should be part of the Riot toolchain
- fixes should be explicit, safe, and explainable, not magical
Guide-level explanation
Section titled “Guide-level explanation”Contributors should think about riot fix as a parser-backed lint and rewrite
tool that applies safe fixes by default, with an explicit check-only mode.
In the default mode:
riot fixthe tool scans the workspace, parses each source file with syn, runs enabled
rules, applies safe edits, and reports what changed plus any remaining
findings.
In check-only mode:
riot fix --checkthe tool performs the same scan and analysis, but does not write files.
The key design point is that rules should produce both:
- a human-readable explanation of the problem
- an optional machine-applicable fix
The user model should look like this:
flowchart TD A[riot fix] --> B[scan workspace files] B --> C[parse each file with syn] C --> D[run enabled rules] D --> E[group diagnostics by file and rule] E --> F{--check?} F -->|yes| G[report findings] F -->|no| H[validate non-overlapping edits] H --> I[apply edits] I --> J[report applied fixes and remaining findings]Intended command surface
Section titled “Intended command surface”The core surface should be:
riot fixriot fix --checkriot fix <path>riot fix --rule no-stdlibriot fix --json
--check means “report but do not write; exit non-zero if issues were found”.
--json should expose structured output for editor integrations and agents.
What counts as a good fix
Section titled “What counts as a good fix”A good riot fix rule:
- matches a real Riot convention or migration need
- emits a specific diagnostic
- carries an edit only when the rewrite is unambiguous
- leaves code in a parseable state
- can explain what it changed
Examples of good early rules:
- replace
open Stdlibwithopen Std - replace forbidden stdlib module paths with Riot equivalents when the mapping is direct
- remove or rewrite obviously disallowed imports in packages that own stricter boundaries
Examples of bad early rules:
- global style normalization that is really formatter territory
- semantic refactors that require typechecking or cross-file reasoning
- “best guess” rewrites with multiple plausible outcomes
Relationship to riot fmt
Section titled “Relationship to riot fmt”riot fix is not riot fmt.
riot fix should perform targeted, rule-driven rewrites.
riot fmt would be a whole-program formatting tool with a much larger surface
area and a different correctness model.
The two can share parser infrastructure, but they should stay distinct in the user model and in implementation.
Reference-level explanation
Section titled “Reference-level explanation”1. Current package state
Section titled “1. Current package state”The repository already contains a packages/riot-fix package with the right
high-level pieces:
PipelineRuleDiagnosticFixTraversalCoordinator/Worker- a small set of built-in rules
Today, that package is better understood as a scaffold than a finished tool.
Important current properties:
Pipeline.runtokenizes and parses exactly once per file.- Parse diagnostics are converted into
riot-fixdiagnostics. - Rules run over a red tree, not raw text.
Fixalready models text edits and grouped fixes, but edit application is not implemented as the primary path yet.- The only meaningful built-in rule today is
no-stdlib.
That makes riot-fix the right place to evolve the feature rather than writing
riot fix from scratch somewhere else.
2. Architecture
Section titled “2. Architecture”The proposed steady-state architecture is:
flowchart TD A[CLI: riot fix] --> B[File_scanner] B --> C[Coordinator] C --> D[Worker per file] D --> E[Syn tokenize + parse] E --> F[Red tree] F --> G[Rule pipeline] G --> H[Diagnostics plus optional Fix values] H --> I[Fix planner] I --> J{mode} J -->|check| K[Reporter] J -->|default fix| L[edit validator] L --> M[file rewrite] M --> N[Reporter]The main subsystems are:
- scanner: decides which files enter the pipeline
- parser stage: produces syntax trees and parse diagnostics
- rule stage: emits lint diagnostics and optional fixes
- fix planner: groups, validates, and orders edits
- reporter: renders human and machine output
- CLI integration: exposes the feature through
riot
3. File model
Section titled “3. File model”The primary unit of work should be a single source file.
Each file run produces:
- the source path
- parse result
- parse diagnostics
- rule diagnostics
- zero or more proposed fixes
Cross-file edits are intentionally out of scope for the first version.
That keeps the system:
- safer
- easier to parallelize
- easier to reason about
- easier to recover from when a fix fails
4. Rule model
Section titled “4. Rule model”The current Rule.t type already has the right basic shape:
- metadata
- enabled flag
- function from context and red tree to diagnostics
The proposal is to extend the effective rule contract so rules can emit both diagnostics and fixes. There are two reasonable shapes:
- change
Diagnostic.tto optionally carry aFix.fix - change rules to return a richer issue type, for example:
type issue = { diagnostic : Diagnostic.t; fix : Fix.fix option;}This RFD prefers the second shape.
It keeps diagnostics as diagnostics, and makes fix attachment explicit rather than overloading every diagnostic value in the system.
The rule contract should become:
run : context -> red_tree -> issue listRule categories
Section titled “Rule categories”Rules should be grouped conceptually into:
- lint-only rules emit diagnostics but no fix
- safe default-fix rules emit diagnostics plus deterministic edits
- advisory migration rules may propose fixes later, but start in report-only mode
The first release of riot fix should only execute safe default-fix rules.
5. Fix model
Section titled “5. Fix model”Fix.text_edit and Fix.fix already exist and are close to the desired shape.
The fix planner should enforce:
- edits within one fix must not overlap
- edits across multiple applied fixes for a file must not overlap
- edits are applied in descending span order or another stable order that does not invalidate subsequent spans
- the resulting file should be reparsed after apply to guard against broken output
Proposed flow for one file:
sequenceDiagram participant Rule as Rule pipeline participant Plan as Fix planner participant File as Source file participant Syn as syn
Rule->>Plan: issue list Plan->>Plan: discard disabled or unsafe fixes Plan->>Plan: detect overlapping edits Plan->>File: apply edits in stable order File->>Syn: reparse rewritten source Syn-->>Plan: success or parse diagnostics Plan-->>Rule: applied or rejectedIf reparsing fails, the tool should:
- leave the original file untouched if possible, or
- fail that file atomically and report the rejected fix set
The system should strongly prefer atomic file rewrites.
6. Safety model
Section titled “6. Safety model”Safety is the core design constraint.
The first version of riot fix should only apply fixes that are:
- local to one file
- local to one syntactic construct
- unambiguous from syntax alone
- validated after rewrite by reparsing
Examples of safe:
- replacing one module token with another
- inserting a missing
open Stdin a known canonical location - rewriting a known forbidden path to a known allowed path
Examples of not yet safe:
- moving declarations across files
- changing names that may require broad symbol updates
- inference-dependent rewrites
- any rewrite that depends on types or module resolution outside the current file
7. CLI integration
Section titled “7. CLI integration”The primary entrypoint should live in riot-cli.
The command should:
- resolve the workspace root
- collect candidate files
- construct the default or requested rule set
- run
riot-fix - report results
- choose exit code based on mode
Proposed exit behavior:
0when no issues were found, or when all applicable fixes were applied successfully1when issues were found in--check1when default fix mode rejected a fix due to conflicts or invalid output1when default fix mode leaves remaining diagnostics that should still fail the command
If desired later, riot-fix can keep a standalone binary for development, but
riot fix should be the primary interface.
8. Output formats
Section titled “8. Output formats”The current text and JSON reporting split is good and should remain.
Text output should emphasize:
- file path
- span
- rule id
- message
- suggested fix title when present
- whether the fix was applied, skipped, or rejected
JSON output should be structured enough for:
- editors
- agents
- CI annotations
A single issue in JSON should include:
- file
- span
- severity
- rule id
- message
- optional suggestion
- optional fix metadata
- apply status in default fix mode
9. Initial built-in rules
Section titled “9. Initial built-in rules”The first meaningful built-in rules should stay small and Riot-specific.
Recommended initial set:
no-stdlibalready exists and should become the first full auto-fix rule where mappings are directno-unix-outside-owned-boundaryonly report at firstprefer-open-stdprobably report-only first, auto-fix laterno-sys-outside-owned-boundarysimilar tono-stdlib
This keeps the first version aligned with actual Riot conventions rather than trying to become a generic OCaml linter immediately.
10. Tests
Section titled “10. Tests”The test strategy should mirror the syn approach and stay explicit.
Three kinds of tests are needed:
- diagnostic tests specific source snippet to expected issues
- fix tests source snippet to rewritten output
- idempotence tests applying the same fix twice should not keep changing the file
For default-fix rules, each test should ideally check:
- diagnostics before apply
- rewritten output
- successful reparse after apply
Drawbacks
Section titled “Drawbacks”- A fix tool introduces a new layer of correctness requirements beyond parsing.
- Users may over-trust auto-fixes if the tool is not conservative enough.
- Rule authorship can become a maintenance burden if the rule surface grows too quickly.
- Some desired rewrites will remain out of scope until Riot has richer semantic analysis.
Rationale and alternatives
Section titled “Rationale and alternatives”This design is the best next step because it uses the parser work that now exists and aims at a smaller, more reliable surface than a formatter.
Alternatives considered:
-
Build
riot fmtfirst A formatter has much broader output-surface risk and a larger design space.riot fixcan deliver useful value sooner. -
Keep
riot-fixas a standalone side tool That makes adoption weaker and splits the user story away fromriot. -
Use regex or grep based rewriting That would be simpler short-term, but it would throw away the parser and tree infrastructure Riot just finished building.
-
Make fixes just another flavor of parser diagnostics That collapses two concepts together too early. The richer issue model keeps parse errors, lint issues, and auto-fixes easier to reason about.
If Riot does not build riot fix, then many of its conventions remain social
rules enforced mostly in review rather than in tooling.
Prior art
Section titled “Prior art”There is strong prior art for syntax-aware linting and safe code actions:
- Rust’s
clippyandcargo fix - ESLint with fixers
- Ruff’s lints and safe auto-fixes
- language-server quick fixes in several ecosystems
The common useful lesson is:
- diagnostics and fixes should stay paired
- automatic rewrites should begin with the safest local transformations
- broad formatting and broad refactoring should not be conflated with lint fixes
Riot should learn from those systems without copying their entire scope. The first version should be much smaller and more opinionated.
Unresolved questions
Section titled “Unresolved questions”- Should
riot-fixkeep its own binary in the long term, or become only ariotsubcommand? - Should rule configuration live in
riot.toml, or should the first version stay convention-driven with almost no configuration? - Should parse diagnostics and lint diagnostics be reported together in one stream, or clearly separated in output?
- What exact issue type should replace the current “diagnostic only” rule return value?
Future possibilities
Section titled “Future possibilities”Once riot fix exists as a stable syntax-aware rewrite tool, several follow-on
features become much easier:
riot fmt, built on the same parser stack but with a different output model- codemod-style migrations for Riot package APIs
- editor-integrated quick fixes powered by the same rule engine
- selective rule packs for package domains such as
std,suri, orriot - eventually, type-aware fixes once Riot has a stronger semantic layer to build on
The long-term opportunity is for Riot to own not just parsing and diagnostics, but the whole “find issue, explain issue, fix issue” workflow in one coherent toolchain.