diff --git a/crates/flake8_to_ruff/src/converter.rs b/crates/flake8_to_ruff/src/converter.rs
index 0caafa0b54df59..4b6693d9c4d963 100644
--- a/crates/flake8_to_ruff/src/converter.rs
+++ b/crates/flake8_to_ruff/src/converter.rs
@@ -16,8 +16,8 @@ use ruff_linter::settings::types::PythonVersion;
 use ruff_linter::warn_user;
 use ruff_workspace::options::{
     Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions,
-    Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, McCabeOptions,
-    Options, Pep8NamingOptions, PydocstyleOptions,
+    Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintOptions,
+    McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions,
 };
 use ruff_workspace::pyproject::Pyproject;
 
@@ -103,6 +103,7 @@ pub(crate) fn convert(
 
     // Parse each supported option.
     let mut options = Options::default();
+    let mut lint_options = LintOptions::default();
     let mut flake8_annotations = Flake8AnnotationsOptions::default();
     let mut flake8_bugbear = Flake8BugbearOptions::default();
     let mut flake8_builtins = Flake8BuiltinsOptions::default();
@@ -150,7 +151,7 @@ pub(crate) fn convert(
                 "per-file-ignores" | "per_file_ignores" => {
                     match parser::parse_files_to_codes_mapping(value.as_ref()) {
                         Ok(per_file_ignores) => {
-                            options.per_file_ignores =
+                            lint_options.per_file_ignores =
                                 Some(parser::collect_per_file_ignores(per_file_ignores));
                         }
                         Err(e) => {
@@ -358,47 +359,47 @@ pub(crate) fn convert(
     }
 
     // Deduplicate and sort.
-    options.select = Some(
+    lint_options.select = Some(
         select
             .into_iter()
             .sorted_by_key(RuleSelector::prefix_and_code)
             .collect(),
     );
-    options.ignore = Some(
+    lint_options.ignore = Some(
         ignore
             .into_iter()
             .sorted_by_key(RuleSelector::prefix_and_code)
             .collect(),
     );
     if flake8_annotations != Flake8AnnotationsOptions::default() {
-        options.flake8_annotations = Some(flake8_annotations);
+        lint_options.flake8_annotations = Some(flake8_annotations);
     }
     if flake8_bugbear != Flake8BugbearOptions::default() {
-        options.flake8_bugbear = Some(flake8_bugbear);
+        lint_options.flake8_bugbear = Some(flake8_bugbear);
     }
     if flake8_builtins != Flake8BuiltinsOptions::default() {
-        options.flake8_builtins = Some(flake8_builtins);
+        lint_options.flake8_builtins = Some(flake8_builtins);
     }
     if flake8_errmsg != Flake8ErrMsgOptions::default() {
-        options.flake8_errmsg = Some(flake8_errmsg);
+        lint_options.flake8_errmsg = Some(flake8_errmsg);
     }
     if flake8_pytest_style != Flake8PytestStyleOptions::default() {
-        options.flake8_pytest_style = Some(flake8_pytest_style);
+        lint_options.flake8_pytest_style = Some(flake8_pytest_style);
     }
     if flake8_quotes != Flake8QuotesOptions::default() {
-        options.flake8_quotes = Some(flake8_quotes);
+        lint_options.flake8_quotes = Some(flake8_quotes);
     }
     if flake8_tidy_imports != Flake8TidyImportsOptions::default() {
-        options.flake8_tidy_imports = Some(flake8_tidy_imports);
+        lint_options.flake8_tidy_imports = Some(flake8_tidy_imports);
     }
     if mccabe != McCabeOptions::default() {
-        options.mccabe = Some(mccabe);
+        lint_options.mccabe = Some(mccabe);
     }
     if pep8_naming != Pep8NamingOptions::default() {
-        options.pep8_naming = Some(pep8_naming);
+        lint_options.pep8_naming = Some(pep8_naming);
     }
     if pydocstyle != PydocstyleOptions::default() {
-        options.pydocstyle = Some(pydocstyle);
+        lint_options.pydocstyle = Some(pydocstyle);
     }
 
     // Extract any settings from the existing `pyproject.toml`.
@@ -436,6 +437,10 @@ pub(crate) fn convert(
         }
     }
 
+    if lint_options != LintOptions::default() {
+        options.lint = Some(lint_options);
+    }
+
     // Create the pyproject.toml.
     Pyproject::new(options)
 }
@@ -464,7 +469,7 @@ mod tests {
     use ruff_linter::rules::flake8_quotes;
     use ruff_linter::rules::pydocstyle::settings::Convention;
     use ruff_linter::settings::types::PythonVersion;
-    use ruff_workspace::options::{Flake8QuotesOptions, Options, PydocstyleOptions};
+    use ruff_workspace::options::{Flake8QuotesOptions, LintOptions, Options, PydocstyleOptions};
     use ruff_workspace::pyproject::Pyproject;
 
     use crate::converter::DEFAULT_SELECTORS;
@@ -474,8 +479,8 @@ mod tests {
     use super::super::plugin::Plugin;
     use super::convert;
 
-    fn default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> Options {
-        Options {
+    fn lint_default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> LintOptions {
+        LintOptions {
             ignore: Some(vec![]),
             select: Some(
                 DEFAULT_SELECTORS
@@ -485,7 +490,7 @@ mod tests {
                     .sorted_by_key(RuleSelector::prefix_and_code)
                     .collect(),
             ),
-            ..Options::default()
+            ..LintOptions::default()
         }
     }
 
@@ -496,7 +501,10 @@ mod tests {
             &ExternalConfig::default(),
             None,
         );
-        let expected = Pyproject::new(default_options([]));
+        let expected = Pyproject::new(Options {
+            lint: Some(lint_default_options([])),
+            ..Options::default()
+        });
         assert_eq!(actual, expected);
     }
 
@@ -512,7 +520,8 @@ mod tests {
         );
         let expected = Pyproject::new(Options {
             line_length: Some(LineLength::try_from(100).unwrap()),
-            ..default_options([])
+            lint: Some(lint_default_options([])),
+            ..Options::default()
         });
         assert_eq!(actual, expected);
     }
@@ -529,7 +538,8 @@ mod tests {
         );
         let expected = Pyproject::new(Options {
             line_length: Some(LineLength::try_from(100).unwrap()),
-            ..default_options([])
+            lint: Some(lint_default_options([])),
+            ..Options::default()
         });
         assert_eq!(actual, expected);
     }
@@ -544,7 +554,10 @@ mod tests {
             &ExternalConfig::default(),
             Some(vec![]),
         );
-        let expected = Pyproject::new(default_options([]));
+        let expected = Pyproject::new(Options {
+            lint: Some(lint_default_options([])),
+            ..Options::default()
+        });
         assert_eq!(actual, expected);
     }
 
@@ -559,13 +572,16 @@ mod tests {
             Some(vec![]),
         );
         let expected = Pyproject::new(Options {
-            flake8_quotes: Some(Flake8QuotesOptions {
-                inline_quotes: Some(flake8_quotes::settings::Quote::Single),
-                multiline_quotes: None,
-                docstring_quotes: None,
-                avoid_escape: None,
+            lint: Some(LintOptions {
+                flake8_quotes: Some(Flake8QuotesOptions {
+                    inline_quotes: Some(flake8_quotes::settings::Quote::Single),
+                    multiline_quotes: None,
+                    docstring_quotes: None,
+                    avoid_escape: None,
+                }),
+                ..lint_default_options([])
             }),
-            ..default_options([])
+            ..Options::default()
         });
         assert_eq!(actual, expected);
     }
@@ -584,12 +600,15 @@ mod tests {
             Some(vec![Plugin::Flake8Docstrings]),
         );
         let expected = Pyproject::new(Options {
-            pydocstyle: Some(PydocstyleOptions {
-                convention: Some(Convention::Numpy),
-                ignore_decorators: None,
-                property_decorators: None,
+            lint: Some(LintOptions {
+                pydocstyle: Some(PydocstyleOptions {
+                    convention: Some(Convention::Numpy),
+                    ignore_decorators: None,
+                    property_decorators: None,
+                }),
+                ..lint_default_options([Linter::Pydocstyle.into()])
             }),
-            ..default_options([Linter::Pydocstyle.into()])
+            ..Options::default()
         });
         assert_eq!(actual, expected);
     }
@@ -605,13 +624,16 @@ mod tests {
             None,
         );
         let expected = Pyproject::new(Options {
-            flake8_quotes: Some(Flake8QuotesOptions {
-                inline_quotes: Some(flake8_quotes::settings::Quote::Single),
-                multiline_quotes: None,
-                docstring_quotes: None,
-                avoid_escape: None,
+            lint: Some(LintOptions {
+                flake8_quotes: Some(Flake8QuotesOptions {
+                    inline_quotes: Some(flake8_quotes::settings::Quote::Single),
+                    multiline_quotes: None,
+                    docstring_quotes: None,
+                    avoid_escape: None,
+                }),
+                ..lint_default_options([Linter::Flake8Quotes.into()])
             }),
-            ..default_options([Linter::Flake8Quotes.into()])
+            ..Options::default()
         });
         assert_eq!(actual, expected);
     }
@@ -630,7 +652,8 @@ mod tests {
         );
         let expected = Pyproject::new(Options {
             target_version: Some(PythonVersion::Py38),
-            ..default_options([])
+            lint: Some(lint_default_options([])),
+            ..Options::default()
         });
         assert_eq!(actual, expected);
 
diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs
index b3159f4eb6ab56..06eadb86b3e447 100644
--- a/crates/ruff_cli/src/args.rs
+++ b/crates/ruff_cli/src/args.rs
@@ -610,7 +610,7 @@ impl ConfigurationTransformer for CliOverrides {
             config.cache_dir = Some(cache_dir.clone());
         }
         if let Some(dummy_variable_rgx) = &self.dummy_variable_rgx {
-            config.dummy_variable_rgx = Some(dummy_variable_rgx.clone());
+            config.lint.dummy_variable_rgx = Some(dummy_variable_rgx.clone());
         }
         if let Some(exclude) = &self.exclude {
             config.exclude = Some(exclude.clone());
@@ -624,7 +624,7 @@ impl ConfigurationTransformer for CliOverrides {
         if let Some(fix_only) = &self.fix_only {
             config.fix_only = Some(*fix_only);
         }
-        config.rule_selections.push(RuleSelection {
+        config.lint.rule_selections.push(RuleSelection {
             select: self.select.clone(),
             ignore: self
                 .ignore
@@ -657,7 +657,7 @@ impl ConfigurationTransformer for CliOverrides {
             config.preview = Some(*preview);
         }
         if let Some(per_file_ignores) = &self.per_file_ignores {
-            config.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));
+            config.lint.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));
         }
         if let Some(respect_gitignore) = &self.respect_gitignore {
             config.respect_gitignore = Some(*respect_gitignore);
diff --git a/crates/ruff_cli/tests/lint.rs b/crates/ruff_cli/tests/lint.rs
new file mode 100644
index 00000000000000..2b0f2194365ee8
--- /dev/null
+++ b/crates/ruff_cli/tests/lint.rs
@@ -0,0 +1,153 @@
+//! Tests the interaction of the `lint` configuration section
+
+#![cfg(not(target_family = "wasm"))]
+
+use std::fs;
+use std::process::Command;
+use std::str;
+
+use anyhow::Result;
+use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
+use tempfile::TempDir;
+
+const BIN_NAME: &str = "ruff";
+const STDIN_BASE_OPTIONS: &[&str] = &["--no-cache", "--output-format", "text"];
+
+#[test]
+fn top_level_options() -> Result<()> {
+    let tempdir = TempDir::new()?;
+    let ruff_toml = tempdir.path().join("ruff.toml");
+    fs::write(
+        &ruff_toml,
+        r#"
+extend-select = ["B", "Q"]
+
+[flake8-quotes]
+inline-quotes = "single"
+"#,
+    )?;
+
+    assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+        .args(STDIN_BASE_OPTIONS)
+        .arg("--config")
+        .arg(&ruff_toml)
+        .arg("-")
+        .pass_stdin(r#""abcba".strip("aba")"#), @r###"
+    success: false
+    exit_code: 1
+    ----- stdout -----
+    -:1:1: B005 Using `.strip()` with multi-character strings is misleading the reader
+    -:1:15: Q000 [*] Double quotes found but single quotes preferred
+    Found 2 errors.
+    [*] 1 potentially fixable with the --fix option.
+
+    ----- stderr -----
+    "###);
+    Ok(())
+}
+
+#[test]
+fn lint_options() -> Result<()> {
+    let tempdir = TempDir::new()?;
+    let ruff_toml = tempdir.path().join("ruff.toml");
+    fs::write(
+        &ruff_toml,
+        r#"
+[lint]
+extend-select = ["B", "Q"]
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+    )?;
+
+    assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+        .args(STDIN_BASE_OPTIONS)
+        .arg("--config")
+        .arg(&ruff_toml)
+        .arg("-")
+        .pass_stdin(r#""abcba".strip("aba")"#), @r###"
+    success: false
+    exit_code: 1
+    ----- stdout -----
+    -:1:1: B005 Using `.strip()` with multi-character strings is misleading the reader
+    -:1:15: Q000 [*] Double quotes found but single quotes preferred
+    Found 2 errors.
+    [*] 1 potentially fixable with the --fix option.
+
+    ----- stderr -----
+    "###);
+    Ok(())
+}
+
+/// Tests that configurations from the top-level and `lint` section are merged together.
+#[test]
+fn mixed_levels() -> Result<()> {
+    let tempdir = TempDir::new()?;
+    let ruff_toml = tempdir.path().join("ruff.toml");
+    fs::write(
+        &ruff_toml,
+        r#"
+extend-select = ["B", "Q"]
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+    )?;
+
+    assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+        .args(STDIN_BASE_OPTIONS)
+        .arg("--config")
+        .arg(&ruff_toml)
+        .arg("-")
+        .pass_stdin(r#""abcba".strip("aba")"#), @r###"
+    success: false
+    exit_code: 1
+    ----- stdout -----
+    -:1:1: B005 Using `.strip()` with multi-character strings is misleading the reader
+    -:1:15: Q000 [*] Double quotes found but single quotes preferred
+    Found 2 errors.
+    [*] 1 potentially fixable with the --fix option.
+
+    ----- stderr -----
+    "###);
+    Ok(())
+}
+
+/// Tests that options in the `lint` section have higher precedence than top-level options (because they are more specific).
+#[test]
+fn precedence() -> Result<()> {
+    let tempdir = TempDir::new()?;
+    let ruff_toml = tempdir.path().join("ruff.toml");
+    fs::write(
+        &ruff_toml,
+        r#"
+[lint]
+extend-select = ["B", "Q"]
+
+[flake8-quotes]
+inline-quotes = "double"
+
+[lint.flake8-quotes]
+inline-quotes = "single"
+"#,
+    )?;
+
+    assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
+        .args(STDIN_BASE_OPTIONS)
+        .arg("--config")
+        .arg(&ruff_toml)
+        .arg("-")
+        .pass_stdin(r#""abcba".strip("aba")"#), @r###"
+    success: false
+    exit_code: 1
+    ----- stdout -----
+    -:1:1: B005 Using `.strip()` with multi-character strings is misleading the reader
+    -:1:15: Q000 [*] Double quotes found but single quotes preferred
+    Found 2 errors.
+    [*] 1 potentially fixable with the --fix option.
+
+    ----- stderr -----
+    "###);
+    Ok(())
+}
diff --git a/crates/ruff_dev/src/generate_docs.rs b/crates/ruff_dev/src/generate_docs.rs
index b6b70c7f4e3249..7ff895515ca56f 100644
--- a/crates/ruff_dev/src/generate_docs.rs
+++ b/crates/ruff_dev/src/generate_docs.rs
@@ -125,13 +125,13 @@ mod tests {
         let mut output = String::new();
         process_documentation(
             "
-See also [`mccabe.max-complexity`] and [`task-tags`].
+See also [`lint.mccabe.max-complexity`] and [`lint.task-tags`].
 Something [`else`][other].
 
 ## Options
 
-- `task-tags`
-- `mccabe.max-complexity`
+- `lint.task-tags`
+- `lint.mccabe.max-complexity`
 
 [other]: http://example.com.",
             &mut output,
@@ -139,18 +139,18 @@ Something [`else`][other].
         assert_eq!(
             output,
             "
-See also [`mccabe.max-complexity`][mccabe.max-complexity] and [`task-tags`][task-tags].
+See also [`lint.mccabe.max-complexity`][lint.mccabe.max-complexity] and [`lint.task-tags`][lint.task-tags].
 Something [`else`][other].
 
 ## Options
 
-- [`task-tags`][task-tags]
-- [`mccabe.max-complexity`][mccabe.max-complexity]
+- [`lint.task-tags`][lint.task-tags]
+- [`lint.mccabe.max-complexity`][lint.mccabe.max-complexity]
 
 [other]: http://example.com.
 
-[task-tags]: ../settings.md#task-tags
-[mccabe.max-complexity]: ../settings.md#mccabe-max-complexity
+[lint.task-tags]: ../settings.md#lint-task-tags
+[lint.mccabe.max-complexity]: ../settings.md#lint-mccabe-max-complexity
 "
         );
     }
diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs
index ea135d5ba44de8..3e73b74f430100 100644
--- a/crates/ruff_dev/src/generate_options.rs
+++ b/crates/ruff_dev/src/generate_options.rs
@@ -14,7 +14,11 @@ pub(crate) fn generate() -> String {
 }
 
 fn generate_set(output: &mut String, set: &Set) {
-    writeln!(output, "### {title}\n", title = set.title()).unwrap();
+    if set.level() < 2 {
+        writeln!(output, "### {title}\n", title = set.title()).unwrap();
+    } else {
+        writeln!(output, "#### {title}\n", title = set.title()).unwrap();
+    }
 
     if let Some(documentation) = set.metadata().documentation() {
         output.push_str(documentation);
@@ -32,56 +36,69 @@ fn generate_set(output: &mut String, set: &Set) {
 
     // Generate the fields.
     for (name, field) in &fields {
-        emit_field(output, name, field, set.name());
+        emit_field(output, name, field, set);
         output.push_str("---\n\n");
     }
 
     // Generate all the sub-sets.
     for (set_name, sub_set) in &sets {
-        generate_set(output, &Set::Named(set_name, *sub_set));
+        generate_set(output, &Set::Named(set_name, *sub_set, set.level() + 1));
     }
 }
 
 enum Set<'a> {
     Toplevel(OptionSet),
-    Named(&'a str, OptionSet),
+    Named(&'a str, OptionSet, u32),
 }
 
 impl<'a> Set<'a> {
     fn name(&self) -> Option<&'a str> {
         match self {
             Set::Toplevel(_) => None,
-            Set::Named(name, _) => Some(name),
+            Set::Named(name, _, _) => Some(name),
         }
     }
 
     fn title(&self) -> &'a str {
         match self {
             Set::Toplevel(_) => "Top-level",
-            Set::Named(name, _) => name,
+            Set::Named(name, _, _) => name,
         }
     }
 
     fn metadata(&self) -> &OptionSet {
         match self {
             Set::Toplevel(set) => set,
-            Set::Named(_, set) => set,
+            Set::Named(_, set, _) => set,
+        }
+    }
+
+    fn level(&self) -> u32 {
+        match self {
+            Set::Toplevel(_) => 0,
+            Set::Named(_, _, level) => *level,
         }
     }
 }
 
-fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: Option<&str>) {
-    // if there's a group name, we need to add it to the anchor
-    if let Some(group_name) = group_name {
+fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set: &Set) {
+    let header_level = if parent_set.level() < 2 {
+        "####"
+    } else {
+        "#####"
+    };
+
+    // if there's a set name, we need to add it to the anchor
+    if let Some(set_name) = parent_set.name() {
         // the anchor used to just be the name, but now it's the group name
         // for backwards compatibility, we need to keep the old anchor
         output.push_str(&format!("<span id=\"{name}\"></span>\n"));
 
         output.push_str(&format!(
-            "#### [`{name}`](#{group_name}-{name}) {{: #{group_name}-{name} }}\n"
+            "{header_level} [`{name}`](#{set_name}-{name}) {{: #{set_name}-{name} }}\n"
         ));
     } else {
-        output.push_str(&format!("#### [`{name}`](#{name})\n"));
+        output.push_str(&format!("{header_level} [`{name}`](#{name})\n"));
     }
     output.push('\n');
     output.push_str(field.doc);
@@ -92,8 +109,8 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name:
     output.push('\n');
     output.push_str(&format!(
         "**Example usage**:\n\n```toml\n[tool.ruff{}]\n{}\n```\n",
-        if group_name.is_some() {
-            format!(".{}", group_name.unwrap())
+        if let Some(set_name) = parent_set.name() {
+            format!(".{set_name}")
         } else {
             String::new()
         },
diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs
index 5e48b2f5bfd62a..8865fd384f6868 100644
--- a/crates/ruff_macros/src/config.rs
+++ b/crates/ruff_macros/src/config.rs
@@ -1,14 +1,15 @@
-use ruff_python_trivia::textwrap::dedent;
-
+use proc_macro2::TokenTree;
 use quote::{quote, quote_spanned};
 use syn::parse::{Parse, ParseStream};
 use syn::spanned::Spanned;
 use syn::token::Comma;
 use syn::{
     AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, ExprLit, Field,
-    Fields, Lit, LitStr, Path, PathArguments, PathSegment, Token, Type, TypePath,
+    Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Token, Type, TypePath,
 };
 
+use ruff_python_trivia::textwrap::dedent;
+
 pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
     let DeriveInput {
         ident,
@@ -44,15 +45,33 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
                     .find(|attr| attr.path().is_ident("option"))
                 {
                     output.push(handle_option(field, attr, docs)?);
-                };
-
-                if field
+                } else if field
                     .attrs
                     .iter()
                     .any(|attr| attr.path().is_ident("option_group"))
                 {
                     output.push(handle_option_group(field)?);
-                };
+                } else if let Some(serde) = field
+                    .attrs
+                    .iter()
+                    .find(|attr| attr.path().is_ident("serde"))
+                {
+                    // If a field has the `serde(flatten)` attribute, flatten the options into the parent
+                    // by calling `Type::record` instead of `visitor.visit_set`
+                    if let (Type::Path(ty), Meta::List(list)) = (&field.ty, &serde.meta) {
+                        for token in list.tokens.clone() {
+                            if let TokenTree::Ident(ident) = token {
+                                if ident == "flatten" {
+                                    let ty_name = ty.path.require_ident()?;
+                                    output.push(quote_spanned!(
+                                        ident.span() => (#ty_name::record(visit))
+                                    ));
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
             }
 
             let docs: Vec<&Attribute> = struct_attributes
diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs
index e3a6974e6b28cd..789d233ee11d60 100644
--- a/crates/ruff_wasm/src/lib.rs
+++ b/crates/ruff_wasm/src/lib.rs
@@ -6,11 +6,9 @@ use wasm_bindgen::prelude::*;
 
 use ruff_formatter::{FormatResult, Formatted};
 use ruff_linter::directives;
-use ruff_linter::line_width::{LineLength, TabSize};
 use ruff_linter::linter::{check_path, LinterResult};
 use ruff_linter::registry::AsRule;
-use ruff_linter::settings::types::PythonVersion;
-use ruff_linter::settings::{flags, DUMMY_VARIABLE_RGX, PREFIXES};
+use ruff_linter::settings::flags;
 use ruff_linter::source_kind::SourceKind;
 use ruff_python_ast::{Mod, PySourceType};
 use ruff_python_codegen::Stylist;
@@ -22,7 +20,7 @@ use ruff_python_trivia::CommentRanges;
 use ruff_source_file::{Locator, SourceLocation};
 use ruff_text_size::Ranged;
 use ruff_workspace::configuration::Configuration;
-use ruff_workspace::options::Options;
+use ruff_workspace::options::{LintOptions, Options};
 use ruff_workspace::Settings;
 
 #[wasm_bindgen(typescript_custom_section)]
@@ -119,46 +117,34 @@ impl Workspace {
     #[wasm_bindgen(js_name = defaultSettings)]
     pub fn default_settings() -> Result<JsValue, Error> {
         serde_wasm_bindgen::to_value(&Options {
-            // Propagate defaults.
-            allowed_confusables: Some(Vec::default()),
-            builtins: Some(Vec::default()),
-            dummy_variable_rgx: Some(DUMMY_VARIABLE_RGX.as_str().to_string()),
-            extend_fixable: Some(Vec::default()),
-            extend_ignore: Some(Vec::default()),
-            extend_select: Some(Vec::default()),
-            extend_unfixable: Some(Vec::default()),
-            external: Some(Vec::default()),
-            ignore: Some(Vec::default()),
-            line_length: Some(LineLength::default()),
-            preview: Some(false),
-            select: Some(PREFIXES.to_vec()),
-            tab_size: Some(TabSize::default()),
-            target_version: Some(PythonVersion::default()),
             // Ignore a bunch of options that don't make sense in a single-file editor.
             cache_dir: None,
             exclude: None,
             extend: None,
             extend_exclude: None,
             extend_include: None,
-            extend_per_file_ignores: None,
             fix: None,
             fix_only: None,
-            fixable: None,
+            lint: Some(LintOptions {
+                extend_per_file_ignores: None,
+                fixable: None,
+                logger_objects: None,
+                per_file_ignores: None,
+                task_tags: None,
+                unfixable: None,
+                ignore_init_module_imports: None,
+                ..LintOptions::default()
+            }),
             force_exclude: None,
             output_format: None,
-            ignore_init_module_imports: None,
             include: None,
-            logger_objects: None,
             namespace_packages: None,
-            per_file_ignores: None,
             required_version: None,
             respect_gitignore: None,
             show_fixes: None,
             show_source: None,
             src: None,
-            task_tags: None,
             typing_modules: None,
-            unfixable: None,
             ..Options::default()
         })
         .map_err(into_error)
diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs
index 3ea2bf9e1cfd59..3752c0aec72dc1 100644
--- a/crates/ruff_workspace/src/configuration.rs
+++ b/crates/ruff_workspace/src/configuration.rs
@@ -39,9 +39,9 @@ use crate::options::{
     Flake8ComprehensionsOptions, Flake8CopyrightOptions, Flake8ErrMsgOptions, Flake8GetTextOptions,
     Flake8ImplicitStrConcatOptions, Flake8ImportConventionsOptions, Flake8PytestStyleOptions,
     Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions,
-    Flake8UnusedArgumentsOptions, FormatOptions, FormatOrOutputFormat, IsortOptions, McCabeOptions,
-    Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions, PydocstyleOptions,
-    PyflakesOptions, PylintOptions,
+    Flake8UnusedArgumentsOptions, FormatOptions, FormatOrOutputFormat, IsortOptions, LintOptions,
+    McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions,
+    PydocstyleOptions, PyflakesOptions, PylintOptions,
 };
 use crate::settings::{
     FileResolverSettings, FormatterSettings, LineEnding, Settings, EXCLUDE, INCLUDE,
@@ -59,64 +59,37 @@ pub struct RuleSelection {
 
 #[derive(Debug, Default)]
 pub struct Configuration {
-    pub rule_selections: Vec<RuleSelection>,
-    pub per_file_ignores: Option<Vec<PerFileIgnore>>,
-
-    pub allowed_confusables: Option<Vec<char>>,
-    pub builtins: Option<Vec<String>>,
+    // Global options
     pub cache_dir: Option<PathBuf>,
-    pub dummy_variable_rgx: Option<Regex>,
-    pub exclude: Option<Vec<FilePattern>>,
+    pub output_format: Option<SerializationFormat>,
+    pub fix: Option<bool>,
+    pub fix_only: Option<bool>,
+    pub show_fixes: Option<bool>,
+    pub show_source: Option<bool>,
+    pub required_version: Option<Version>,
+    pub preview: Option<PreviewMode>,
     pub extend: Option<PathBuf>,
+
+    // File resolver options
+    pub exclude: Option<Vec<FilePattern>>,
     pub extend_exclude: Vec<FilePattern>,
     pub extend_include: Vec<FilePattern>,
-    pub extend_per_file_ignores: Vec<PerFileIgnore>,
-    pub external: Option<Vec<String>>,
-    pub fix: Option<bool>,
-    pub fix_only: Option<bool>,
     pub force_exclude: Option<bool>,
-    pub output_format: Option<SerializationFormat>,
-    pub ignore_init_module_imports: Option<bool>,
     pub include: Option<Vec<FilePattern>>,
-    pub line_length: Option<LineLength>,
-    pub logger_objects: Option<Vec<String>>,
-    pub namespace_packages: Option<Vec<PathBuf>>,
-    pub preview: Option<PreviewMode>,
-    pub required_version: Option<Version>,
     pub respect_gitignore: Option<bool>,
-    pub show_fixes: Option<bool>,
-    pub show_source: Option<bool>,
-    pub src: Option<Vec<PathBuf>>,
-    pub tab_size: Option<TabSize>,
+
+    // Generic python options settings
+    pub builtins: Option<Vec<String>>,
+    pub namespace_packages: Option<Vec<PathBuf>>,
     pub target_version: Option<PythonVersion>,
-    pub task_tags: Option<Vec<String>>,
+    pub src: Option<Vec<PathBuf>>,
     pub typing_modules: Option<Vec<String>>,
-    // Plugins
-    pub flake8_annotations: Option<Flake8AnnotationsOptions>,
-    pub flake8_bandit: Option<Flake8BanditOptions>,
-    pub flake8_bugbear: Option<Flake8BugbearOptions>,
-    pub flake8_builtins: Option<Flake8BuiltinsOptions>,
-    pub flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
-    pub flake8_copyright: Option<Flake8CopyrightOptions>,
-    pub flake8_errmsg: Option<Flake8ErrMsgOptions>,
-    pub flake8_gettext: Option<Flake8GetTextOptions>,
-    pub flake8_implicit_str_concat: Option<Flake8ImplicitStrConcatOptions>,
-    pub flake8_import_conventions: Option<Flake8ImportConventionsOptions>,
-    pub flake8_pytest_style: Option<Flake8PytestStyleOptions>,
-    pub flake8_quotes: Option<Flake8QuotesOptions>,
-    pub flake8_self: Option<Flake8SelfOptions>,
-    pub flake8_tidy_imports: Option<Flake8TidyImportsOptions>,
-    pub flake8_type_checking: Option<Flake8TypeCheckingOptions>,
-    pub flake8_unused_arguments: Option<Flake8UnusedArgumentsOptions>,
-    pub isort: Option<IsortOptions>,
-    pub mccabe: Option<McCabeOptions>,
-    pub pep8_naming: Option<Pep8NamingOptions>,
-    pub pycodestyle: Option<PycodestyleOptions>,
-    pub pydocstyle: Option<PydocstyleOptions>,
-    pub pyflakes: Option<PyflakesOptions>,
-    pub pylint: Option<PylintOptions>,
-    pub pyupgrade: Option<PyUpgradeOptions>,
 
+    // Global formatting options
+    pub line_length: Option<LineLength>,
+    pub tab_size: Option<TabSize>,
+
+    pub lint: LintConfiguration,
     pub format: Option<FormatConfiguration>,
 }
 
@@ -133,11 +106,10 @@ impl Configuration {
         }
 
         let target_version = self.target_version.unwrap_or_default();
-        let rules = self.as_rule_table();
         let preview = self.preview.unwrap_or_default();
 
         let formatter = if let Some(format) = self.format {
-            let default = FormatterSettings::default();
+            let formatter_defaults = FormatterSettings::default();
 
             // TODO(micha): Support changing the tab-width but disallow changing the number of spaces
             FormatterSettings {
@@ -146,20 +118,26 @@ impl Configuration {
                     PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
                     PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
                 },
-                line_width: self.line_length.map_or(default.line_width, |length| {
-                    LineWidth::from(NonZeroU16::from(length))
-                }),
-                line_ending: format.line_ending.unwrap_or(default.line_ending),
-                indent_style: format.indent_style.unwrap_or(default.indent_style),
-                quote_style: format.quote_style.unwrap_or(default.quote_style),
+                line_width: self
+                    .line_length
+                    .map_or(formatter_defaults.line_width, |length| {
+                        LineWidth::from(NonZeroU16::from(length))
+                    }),
+                line_ending: format.line_ending.unwrap_or(formatter_defaults.line_ending),
+                indent_style: format
+                    .indent_style
+                    .unwrap_or(formatter_defaults.indent_style),
+                quote_style: format.quote_style.unwrap_or(formatter_defaults.quote_style),
                 magic_trailing_comma: format
                     .magic_trailing_comma
-                    .unwrap_or(default.magic_trailing_comma),
+                    .unwrap_or(formatter_defaults.magic_trailing_comma),
             }
         } else {
             FormatterSettings::default()
         };
 
+        let lint = self.lint;
+
         Ok(Settings {
             cache_dir: self
                 .cache_dir
@@ -186,135 +164,135 @@ impl Configuration {
             },
 
             linter: LinterSettings {
+                rules: lint.as_rule_table(preview),
                 target_version,
                 project_root: project_root.to_path_buf(),
-                rules,
-                allowed_confusables: self
+                allowed_confusables: lint
                     .allowed_confusables
                     .map(FxHashSet::from_iter)
                     .unwrap_or_default(),
                 builtins: self.builtins.unwrap_or_default(),
-                dummy_variable_rgx: self
+                dummy_variable_rgx: lint
                     .dummy_variable_rgx
                     .unwrap_or_else(|| DUMMY_VARIABLE_RGX.clone()),
-                external: FxHashSet::from_iter(self.external.unwrap_or_default()),
-                ignore_init_module_imports: self.ignore_init_module_imports.unwrap_or_default(),
+                external: FxHashSet::from_iter(lint.external.unwrap_or_default()),
+                ignore_init_module_imports: lint.ignore_init_module_imports.unwrap_or_default(),
                 line_length: self.line_length.unwrap_or_default(),
                 tab_size: self.tab_size.unwrap_or_default(),
                 namespace_packages: self.namespace_packages.unwrap_or_default(),
                 per_file_ignores: resolve_per_file_ignores(
-                    self.per_file_ignores
+                    lint.per_file_ignores
                         .unwrap_or_default()
                         .into_iter()
-                        .chain(self.extend_per_file_ignores)
+                        .chain(lint.extend_per_file_ignores)
                         .collect(),
                 )?,
                 src: self.src.unwrap_or_else(|| vec![project_root.to_path_buf()]),
 
-                task_tags: self
+                task_tags: lint
                     .task_tags
                     .unwrap_or_else(|| TASK_TAGS.iter().map(ToString::to_string).collect()),
-                logger_objects: self.logger_objects.unwrap_or_default(),
+                logger_objects: lint.logger_objects.unwrap_or_default(),
                 preview,
                 typing_modules: self.typing_modules.unwrap_or_default(),
                 // Plugins
-                flake8_annotations: self
+                flake8_annotations: lint
                     .flake8_annotations
                     .map(Flake8AnnotationsOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_bandit: self
+                flake8_bandit: lint
                     .flake8_bandit
                     .map(Flake8BanditOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_bugbear: self
+                flake8_bugbear: lint
                     .flake8_bugbear
                     .map(Flake8BugbearOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_builtins: self
+                flake8_builtins: lint
                     .flake8_builtins
                     .map(Flake8BuiltinsOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_comprehensions: self
+                flake8_comprehensions: lint
                     .flake8_comprehensions
                     .map(Flake8ComprehensionsOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_copyright: self
+                flake8_copyright: lint
                     .flake8_copyright
                     .map(Flake8CopyrightOptions::try_into_settings)
                     .transpose()?
                     .unwrap_or_default(),
-                flake8_errmsg: self
+                flake8_errmsg: lint
                     .flake8_errmsg
                     .map(Flake8ErrMsgOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_implicit_str_concat: self
+                flake8_implicit_str_concat: lint
                     .flake8_implicit_str_concat
                     .map(Flake8ImplicitStrConcatOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_import_conventions: self
+                flake8_import_conventions: lint
                     .flake8_import_conventions
                     .map(Flake8ImportConventionsOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_pytest_style: self
+                flake8_pytest_style: lint
                     .flake8_pytest_style
                     .map(Flake8PytestStyleOptions::try_into_settings)
                     .transpose()?
                     .unwrap_or_default(),
-                flake8_quotes: self
+                flake8_quotes: lint
                     .flake8_quotes
                     .map(Flake8QuotesOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_self: self
+                flake8_self: lint
                     .flake8_self
                     .map(Flake8SelfOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_tidy_imports: self
+                flake8_tidy_imports: lint
                     .flake8_tidy_imports
                     .map(Flake8TidyImportsOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_type_checking: self
+                flake8_type_checking: lint
                     .flake8_type_checking
                     .map(Flake8TypeCheckingOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_unused_arguments: self
+                flake8_unused_arguments: lint
                     .flake8_unused_arguments
                     .map(Flake8UnusedArgumentsOptions::into_settings)
                     .unwrap_or_default(),
-                flake8_gettext: self
+                flake8_gettext: lint
                     .flake8_gettext
                     .map(Flake8GetTextOptions::into_settings)
                     .unwrap_or_default(),
-                isort: self
+                isort: lint
                     .isort
                     .map(IsortOptions::try_into_settings)
                     .transpose()?
                     .unwrap_or_default(),
-                mccabe: self
+                mccabe: lint
                     .mccabe
                     .map(McCabeOptions::into_settings)
                     .unwrap_or_default(),
-                pep8_naming: self
+                pep8_naming: lint
                     .pep8_naming
                     .map(Pep8NamingOptions::try_into_settings)
                     .transpose()?
                     .unwrap_or_default(),
-                pycodestyle: self
+                pycodestyle: lint
                     .pycodestyle
                     .map(PycodestyleOptions::into_settings)
                     .unwrap_or_default(),
-                pydocstyle: self
+                pydocstyle: lint
                     .pydocstyle
                     .map(PydocstyleOptions::into_settings)
                     .unwrap_or_default(),
-                pyflakes: self
+                pyflakes: lint
                     .pyflakes
                     .map(PyflakesOptions::into_settings)
                     .unwrap_or_default(),
-                pylint: self
+                pylint: lint
                     .pylint
                     .map(PylintOptions::into_settings)
                     .unwrap_or_default(),
-                pyupgrade: self
+                pyupgrade: lint
                     .pyupgrade
                     .map(PyUpgradeOptions::into_settings)
                     .unwrap_or_default(),
@@ -325,26 +303,14 @@ impl Configuration {
     }
 
     pub fn from_options(options: Options, project_root: &Path) -> Result<Self> {
+        // TODO warn about legacy options
+        let lint = if let Some(lint) = options.lint {
+            lint.combine(options.lint_top_level)
+        } else {
+            options.lint_top_level
+        };
+
         Ok(Self {
-            rule_selections: vec![RuleSelection {
-                select: options.select,
-                ignore: options
-                    .ignore
-                    .into_iter()
-                    .flatten()
-                    .chain(options.extend_ignore.into_iter().flatten())
-                    .collect(),
-                extend_select: options.extend_select.unwrap_or_default(),
-                fixable: options.fixable,
-                unfixable: options
-                    .unfixable
-                    .into_iter()
-                    .flatten()
-                    .chain(options.extend_unfixable.into_iter().flatten())
-                    .collect(),
-                extend_fixable: options.extend_fixable.unwrap_or_default(),
-            }],
-            allowed_confusables: options.allowed_confusables,
             builtins: options.builtins,
             cache_dir: options
                 .cache_dir
@@ -354,11 +320,7 @@ impl Configuration {
                 })
                 .transpose()
                 .map_err(|e| anyhow!("Invalid `cache-dir` value: {e}"))?,
-            dummy_variable_rgx: options
-                .dummy_variable_rgx
-                .map(|pattern| Regex::new(&pattern))
-                .transpose()
-                .map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
+
             exclude: options.exclude.map(|paths| {
                 paths
                     .into_iter()
@@ -400,28 +362,6 @@ impl Configuration {
                         .collect()
                 })
                 .unwrap_or_default(),
-            extend_per_file_ignores: options
-                .extend_per_file_ignores
-                .map(|per_file_ignores| {
-                    per_file_ignores
-                        .into_iter()
-                        .map(|(pattern, prefixes)| {
-                            PerFileIgnore::new(pattern, &prefixes, Some(project_root))
-                        })
-                        .collect()
-                })
-                .unwrap_or_default(),
-            external: options.external,
-            fix: options.fix,
-            fix_only: options.fix_only,
-            output_format: options.output_format.or_else(|| {
-                options
-                    .format
-                    .as_ref()
-                    .and_then(FormatOrOutputFormat::as_output_format)
-            }),
-            force_exclude: options.force_exclude,
-            ignore_init_module_imports: options.ignore_init_module_imports,
             include: options.include.map(|paths| {
                 paths
                     .into_iter()
@@ -431,6 +371,15 @@ impl Configuration {
                     })
                     .collect()
             }),
+            fix: options.fix,
+            fix_only: options.fix_only,
+            output_format: options.output_format.or_else(|| {
+                options
+                    .format
+                    .as_ref()
+                    .and_then(FormatOrOutputFormat::as_output_format)
+            }),
+            force_exclude: options.force_exclude,
             line_length: options.line_length,
             tab_size: options.tab_size,
             namespace_packages: options
@@ -438,14 +387,6 @@ impl Configuration {
                 .map(|namespace_package| resolve_src(&namespace_package, project_root))
                 .transpose()?,
             preview: options.preview.map(PreviewMode::from),
-            per_file_ignores: options.per_file_ignores.map(|per_file_ignores| {
-                per_file_ignores
-                    .into_iter()
-                    .map(|(pattern, prefixes)| {
-                        PerFileIgnore::new(pattern, &prefixes, Some(project_root))
-                    })
-                    .collect()
-            }),
             required_version: options.required_version,
             respect_gitignore: options.respect_gitignore,
             show_source: options.show_source,
@@ -455,9 +396,156 @@ impl Configuration {
                 .map(|src| resolve_src(&src, project_root))
                 .transpose()?,
             target_version: options.target_version,
+            typing_modules: options.typing_modules,
+
+            lint: LintConfiguration::from_options(lint, project_root)?,
+
+            format: if let Some(FormatOrOutputFormat::Format(format)) = options.format {
+                Some(FormatConfiguration::from_options(format, project_root)?)
+            } else {
+                None
+            },
+        })
+    }
+
+    #[must_use]
+    pub fn combine(self, config: Self) -> Self {
+        Self {
+            builtins: self.builtins.or(config.builtins),
+            cache_dir: self.cache_dir.or(config.cache_dir),
+            exclude: self.exclude.or(config.exclude),
+            extend: self.extend.or(config.extend),
+            extend_exclude: config
+                .extend_exclude
+                .into_iter()
+                .chain(self.extend_exclude)
+                .collect(),
+            extend_include: config
+                .extend_include
+                .into_iter()
+                .chain(self.extend_include)
+                .collect(),
+            include: self.include.or(config.include),
+            fix: self.fix.or(config.fix),
+            fix_only: self.fix_only.or(config.fix_only),
+            output_format: self.output_format.or(config.output_format),
+            force_exclude: self.force_exclude.or(config.force_exclude),
+            line_length: self.line_length.or(config.line_length),
+            tab_size: self.tab_size.or(config.tab_size),
+            namespace_packages: self.namespace_packages.or(config.namespace_packages),
+            required_version: self.required_version.or(config.required_version),
+            respect_gitignore: self.respect_gitignore.or(config.respect_gitignore),
+            show_source: self.show_source.or(config.show_source),
+            show_fixes: self.show_fixes.or(config.show_fixes),
+            src: self.src.or(config.src),
+            target_version: self.target_version.or(config.target_version),
+            preview: self.preview.or(config.preview),
+            typing_modules: self.typing_modules.or(config.typing_modules),
+
+            lint: self.lint.combine(config.lint),
+
+            format: match (self.format, config.format) {
+                (Some(format), Some(other_format)) => Some(format.combine(other_format)),
+                (Some(format), None) => Some(format),
+                (None, Some(format)) => Some(format),
+                (None, None) => None,
+            },
+        }
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct LintConfiguration {
+    // Rule selection
+    pub rule_selections: Vec<RuleSelection>,
+    pub per_file_ignores: Option<Vec<PerFileIgnore>>,
+    pub extend_per_file_ignores: Vec<PerFileIgnore>,
+
+    // Global lint settings
+    pub dummy_variable_rgx: Option<Regex>,
+    pub allowed_confusables: Option<Vec<char>>,
+    pub external: Option<Vec<String>>,
+    pub ignore_init_module_imports: Option<bool>,
+    pub logger_objects: Option<Vec<String>>,
+    pub task_tags: Option<Vec<String>>,
+
+    // Plugins
+    pub flake8_annotations: Option<Flake8AnnotationsOptions>,
+    pub flake8_bandit: Option<Flake8BanditOptions>,
+    pub flake8_bugbear: Option<Flake8BugbearOptions>,
+    pub flake8_builtins: Option<Flake8BuiltinsOptions>,
+    pub flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
+    pub flake8_copyright: Option<Flake8CopyrightOptions>,
+    pub flake8_errmsg: Option<Flake8ErrMsgOptions>,
+    pub flake8_gettext: Option<Flake8GetTextOptions>,
+    pub flake8_implicit_str_concat: Option<Flake8ImplicitStrConcatOptions>,
+    pub flake8_import_conventions: Option<Flake8ImportConventionsOptions>,
+    pub flake8_pytest_style: Option<Flake8PytestStyleOptions>,
+    pub flake8_quotes: Option<Flake8QuotesOptions>,
+    pub flake8_self: Option<Flake8SelfOptions>,
+    pub flake8_tidy_imports: Option<Flake8TidyImportsOptions>,
+    pub flake8_type_checking: Option<Flake8TypeCheckingOptions>,
+    pub flake8_unused_arguments: Option<Flake8UnusedArgumentsOptions>,
+    pub isort: Option<IsortOptions>,
+    pub mccabe: Option<McCabeOptions>,
+    pub pep8_naming: Option<Pep8NamingOptions>,
+    pub pycodestyle: Option<PycodestyleOptions>,
+    pub pydocstyle: Option<PydocstyleOptions>,
+    pub pyflakes: Option<PyflakesOptions>,
+    pub pylint: Option<PylintOptions>,
+    pub pyupgrade: Option<PyUpgradeOptions>,
+}
+
+impl LintConfiguration {
+    fn from_options(options: LintOptions, project_root: &Path) -> Result<Self> {
+        Ok(LintConfiguration {
+            rule_selections: vec![RuleSelection {
+                select: options.select,
+                ignore: options
+                    .ignore
+                    .into_iter()
+                    .flatten()
+                    .chain(options.extend_ignore.into_iter().flatten())
+                    .collect(),
+                extend_select: options.extend_select.unwrap_or_default(),
+                fixable: options.fixable,
+                unfixable: options
+                    .unfixable
+                    .into_iter()
+                    .flatten()
+                    .chain(options.extend_unfixable.into_iter().flatten())
+                    .collect(),
+                extend_fixable: options.extend_fixable.unwrap_or_default(),
+            }],
+            allowed_confusables: options.allowed_confusables,
+            dummy_variable_rgx: options
+                .dummy_variable_rgx
+                .map(|pattern| Regex::new(&pattern))
+                .transpose()
+                .map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
+            extend_per_file_ignores: options
+                .extend_per_file_ignores
+                .map(|per_file_ignores| {
+                    per_file_ignores
+                        .into_iter()
+                        .map(|(pattern, prefixes)| {
+                            PerFileIgnore::new(pattern, &prefixes, Some(project_root))
+                        })
+                        .collect()
+                })
+                .unwrap_or_default(),
+            external: options.external,
+            ignore_init_module_imports: options.ignore_init_module_imports,
+            per_file_ignores: options.per_file_ignores.map(|per_file_ignores| {
+                per_file_ignores
+                    .into_iter()
+                    .map(|(pattern, prefixes)| {
+                        PerFileIgnore::new(pattern, &prefixes, Some(project_root))
+                    })
+                    .collect()
+            }),
             task_tags: options.task_tags,
             logger_objects: options.logger_objects,
-            typing_modules: options.typing_modules,
             // Plugins
             flake8_annotations: options.flake8_annotations,
             flake8_bandit: options.flake8_bandit,
@@ -483,18 +571,10 @@ impl Configuration {
             pyflakes: options.pyflakes,
             pylint: options.pylint,
             pyupgrade: options.pyupgrade,
-
-            format: if let Some(FormatOrOutputFormat::Format(format)) = options.format {
-                Some(FormatConfiguration::from_options(format, project_root)?)
-            } else {
-                None
-            },
         })
     }
 
-    pub fn as_rule_table(&self) -> RuleTable {
-        let preview = self.preview.unwrap_or_default();
-
+    fn as_rule_table(&self, preview: PreviewMode) -> RuleTable {
         // The select_set keeps track of which rules have been selected.
         let mut select_set: RuleSet = PREFIXES
             .iter()
@@ -734,49 +814,19 @@ impl Configuration {
                 .chain(self.rule_selections)
                 .collect(),
             allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
-            builtins: self.builtins.or(config.builtins),
-            cache_dir: self.cache_dir.or(config.cache_dir),
             dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
-            exclude: self.exclude.or(config.exclude),
-            extend: self.extend.or(config.extend),
-            extend_exclude: config
-                .extend_exclude
-                .into_iter()
-                .chain(self.extend_exclude)
-                .collect(),
-            extend_include: config
-                .extend_include
-                .into_iter()
-                .chain(self.extend_include)
-                .collect(),
             extend_per_file_ignores: config
                 .extend_per_file_ignores
                 .into_iter()
                 .chain(self.extend_per_file_ignores)
                 .collect(),
             external: self.external.or(config.external),
-            fix: self.fix.or(config.fix),
-            fix_only: self.fix_only.or(config.fix_only),
-            output_format: self.output_format.or(config.output_format),
-            force_exclude: self.force_exclude.or(config.force_exclude),
-            include: self.include.or(config.include),
             ignore_init_module_imports: self
                 .ignore_init_module_imports
                 .or(config.ignore_init_module_imports),
-            line_length: self.line_length.or(config.line_length),
             logger_objects: self.logger_objects.or(config.logger_objects),
-            tab_size: self.tab_size.or(config.tab_size),
-            namespace_packages: self.namespace_packages.or(config.namespace_packages),
             per_file_ignores: self.per_file_ignores.or(config.per_file_ignores),
-            required_version: self.required_version.or(config.required_version),
-            respect_gitignore: self.respect_gitignore.or(config.respect_gitignore),
-            show_source: self.show_source.or(config.show_source),
-            show_fixes: self.show_fixes.or(config.show_fixes),
-            src: self.src.or(config.src),
-            target_version: self.target_version.or(config.target_version),
-            preview: self.preview.or(config.preview),
             task_tags: self.task_tags.or(config.task_tags),
-            typing_modules: self.typing_modules.or(config.typing_modules),
             // Plugins
             flake8_annotations: self.flake8_annotations.combine(config.flake8_annotations),
             flake8_bandit: self.flake8_bandit.combine(config.flake8_bandit),
@@ -812,13 +862,6 @@ impl Configuration {
             pyflakes: self.pyflakes.combine(config.pyflakes),
             pylint: self.pylint.combine(config.pylint),
             pyupgrade: self.pyupgrade.combine(config.pyupgrade),
-
-            format: match (self.format, config.format) {
-                (Some(format), Some(other_format)) => Some(format.combine(other_format)),
-                (Some(format), None) => Some(format),
-                (None, Some(format)) => Some(format),
-                (None, None) => None,
-            },
         }
     }
 }
@@ -876,7 +919,6 @@ impl FormatConfiguration {
         }
     }
 }
-
 pub(crate) trait CombinePluginOptions {
     #[must_use]
     fn combine(self, other: Self) -> Self;
@@ -920,7 +962,7 @@ mod tests {
     use ruff_linter::settings::types::PreviewMode;
     use ruff_linter::RuleSelector;
 
-    use crate::configuration::{Configuration, RuleSelection};
+    use crate::configuration::{LintConfiguration, RuleSelection};
 
     const NURSERY_RULES: &[Rule] = &[
         Rule::MissingCopyrightNotice,
@@ -984,12 +1026,11 @@ mod tests {
         selections: impl IntoIterator<Item = RuleSelection>,
         preview: Option<PreviewMode>,
     ) -> RuleSet {
-        Configuration {
+        LintConfiguration {
             rule_selections: selections.into_iter().collect(),
-            preview,
-            ..Configuration::default()
+            ..LintConfiguration::default()
         }
-        .as_rule_table()
+        .as_rule_table(preview.unwrap_or_default())
         .iter_enabled()
         // Filter out rule gated behind `#[cfg(feature = "unreachable-code")]`, which is off-by-default
         .filter(|rule| rule.noqa_code() != "RUF014")
diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs
index 8de33d824e85d8..d231e1001ba7df 100644
--- a/crates/ruff_workspace/src/options.rs
+++ b/crates/ruff_workspace/src/options.rs
@@ -36,30 +36,6 @@ use crate::settings::LineEnding;
 #[serde(deny_unknown_fields, rename_all = "kebab-case")]
 #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub struct Options {
-    /// A list of allowed "confusable" Unicode characters to ignore when
-    /// enforcing `RUF001`, `RUF002`, and `RUF003`.
-    #[option(
-        default = r#"[]"#,
-        value_type = "list[str]",
-        example = r#"
-            # Allow minus-sign (U+2212), greek-small-letter-rho (U+03C1), and the asterisk-operator (U+2217),
-            # which could be confused for "-", "p", and "*", respectively.
-            allowed-confusables = ["−", "ρ", "∗"]
-        "#
-    )]
-    pub allowed_confusables: Option<Vec<char>>,
-
-    /// A list of builtins to treat as defined references, in addition to the
-    /// system builtins.
-    #[option(
-        default = r#"[]"#,
-        value_type = "list[str]",
-        example = r#"
-            builtins = ["_"]
-        "#
-    )]
-    pub builtins: Option<Vec<String>>,
-
     /// A path to the cache directory.
     ///
     /// By default, Ruff stores cache results in a `.ruff_cache` directory in
@@ -77,19 +53,98 @@ pub struct Options {
     )]
     pub cache_dir: Option<String>,
 
-    /// A regular expression used to identify "dummy" variables, or those which
-    /// should be ignored when enforcing (e.g.) unused-variable rules. The
-    /// default expression matches `_`, `__`, and `_var`, but not `_var_`.
+    /// A path to a local `pyproject.toml` file to merge into this
+    /// configuration. User home directory and environment variables will be
+    /// expanded.
+    ///
+    /// To resolve the current `pyproject.toml` file, Ruff will first resolve
+    /// this base configuration file, then merge in any properties defined
+    /// in the current configuration file.
     #[option(
-        default = r#""^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$""#,
-        value_type = "re.Pattern",
+        default = r#"None"#,
+        value_type = "str",
         example = r#"
-            # Only ignore variables named "_".
-            dummy-variable-rgx = "^_$"
+            # Extend the `pyproject.toml` file in the parent directory.
+            extend = "../pyproject.toml"
+            # But use a different line length.
+            line-length = 100
         "#
     )]
-    pub dummy_variable_rgx: Option<String>,
+    pub extend: Option<String>,
+
+    /// The style in which violation messages should be formatted: `"text"`
+    /// (default), `"grouped"` (group messages by file), `"json"`
+    /// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub
+    /// Actions annotations), `"gitlab"` (GitLab CI code quality report),
+    /// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands).
+    #[option(
+        default = r#""text""#,
+        value_type = r#""text" | "json" | "junit" | "github" | "gitlab" | "pylint" | "azure""#,
+        example = r#"
+            # Group violations by containing file.
+            output-format = "grouped"
+        "#
+    )]
+    pub output_format: Option<SerializationFormat>,
+
+    /// Enable autofix behavior by-default when running `ruff` (overridden
+    /// by the `--fix` and `--no-fix` command-line flags).
+    #[option(default = "false", value_type = "bool", example = "fix = true")]
+    pub fix: Option<bool>,
+
+    /// Like `fix`, but disables reporting on leftover violation. Implies `fix`.
+    #[option(default = "false", value_type = "bool", example = "fix-only = true")]
+    pub fix_only: Option<bool>,
+
+    /// Whether to show source code snippets when reporting lint violations
+    /// (overridden by the `--show-source` command-line flag).
+    #[option(
+        default = "false",
+        value_type = "bool",
+        example = r#"
+            # By default, always show source code snippets.
+            show-source = true
+        "#
+    )]
+    pub show_source: Option<bool>,
 
+    /// Whether to show an enumeration of all autofixed lint violations
+    /// (overridden by the `--show-fixes` command-line flag).
+    #[option(
+        default = "false",
+        value_type = "bool",
+        example = r#"
+            # Enumerate all fixed violations.
+            show-fixes = true
+        "#
+    )]
+    pub show_fixes: Option<bool>,
+
+    /// Require a specific version of Ruff to be running (useful for unifying
+    /// results across many environments, e.g., with a `pyproject.toml`
+    /// file).
+    #[option(
+        default = "None",
+        value_type = "str",
+        example = r#"
+            required-version = "0.0.193"
+        "#
+    )]
+    pub required_version: Option<Version>,
+
+    /// Whether to enable preview mode. When preview mode is enabled, Ruff will
+    /// use unstable rules and fixes.
+    #[option(
+        default = "false",
+        value_type = "bool",
+        example = r#"
+            # Enable preview features
+            preview = true
+        "#
+    )]
+    pub preview: Option<bool>,
+
+    // File resolver options
     /// A list of file patterns to exclude from linting.
     ///
     /// Exclusions are based on globs, and can be either:
@@ -115,25 +170,6 @@ pub struct Options {
     )]
     pub exclude: Option<Vec<String>>,
 
-    /// A path to a local `pyproject.toml` file to merge into this
-    /// configuration. User home directory and environment variables will be
-    /// expanded.
-    ///
-    /// To resolve the current `pyproject.toml` file, Ruff will first resolve
-    /// this base configuration file, then merge in any properties defined
-    /// in the current configuration file.
-    #[option(
-        default = r#"None"#,
-        value_type = "str",
-        example = r#"
-            # Extend the `pyproject.toml` file in the parent directory.
-            extend = "../pyproject.toml"
-            # But use a different line length.
-            line-length = 100
-        "#
-    )]
-    pub extend: Option<String>,
-
     /// A list of file patterns to omit from linting, in addition to those
     /// specified by `exclude`.
     ///
@@ -173,7 +209,236 @@ pub struct Options {
             extend-include = ["*.pyw"]
         "#
     )]
-    pub extend_include: Option<Vec<String>>,
+    pub extend_include: Option<Vec<String>>,
+
+    /// Whether to enforce `exclude` and `extend-exclude` patterns, even for
+    /// paths that are passed to Ruff explicitly. Typically, Ruff will lint
+    /// any paths passed in directly, even if they would typically be
+    /// excluded. Setting `force-exclude = true` will cause Ruff to
+    /// respect these exclusions unequivocally.
+    ///
+    /// This is useful for [`pre-commit`](https://pre-commit.com/), which explicitly passes all
+    /// changed files to the [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit)
+    /// plugin, regardless of whether they're marked as excluded by Ruff's own
+    /// settings.
+    #[option(
+        default = r#"false"#,
+        value_type = "bool",
+        example = r#"
+            force-exclude = true
+        "#
+    )]
+    pub force_exclude: Option<bool>,
+
+    /// A list of file patterns to include when linting.
+    ///
+    /// Inclusion are based on globs, and should be single-path patterns, like
+    /// `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is
+    /// included here not for configuration but because we lint whether e.g. the
+    /// `[project]` matches the schema.
+    ///
+    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
+    #[option(
+        default = r#"["*.py", "*.pyi", "**/pyproject.toml"]"#,
+        value_type = "list[str]",
+        example = r#"
+            include = ["*.py"]
+        "#
+    )]
+    pub include: Option<Vec<String>>,
+
+    /// Whether to automatically exclude files that are ignored by `.ignore`,
+    /// `.gitignore`, `.git/info/exclude`, and global `gitignore` files.
+    /// Enabled by default.
+    #[option(
+        default = "true",
+        value_type = "bool",
+        example = r#"
+            respect-gitignore = false
+        "#
+    )]
+    pub respect_gitignore: Option<bool>,
+
+    // Generic python options
+    /// A list of builtins to treat as defined references, in addition to the
+    /// system builtins.
+    #[option(
+        default = r#"[]"#,
+        value_type = "list[str]",
+        example = r#"
+            builtins = ["_"]
+        "#
+    )]
+    pub builtins: Option<Vec<String>>,
+
+    /// Mark the specified directories as namespace packages. For the purpose of
+    /// module resolution, Ruff will treat those directories as if they
+    /// contained an `__init__.py` file.
+    #[option(
+        default = r#"[]"#,
+        value_type = "list[str]",
+        example = r#"
+            namespace-packages = ["airflow/providers"]
+        "#
+    )]
+    pub namespace_packages: Option<Vec<String>>,
+
+    /// The minimum Python version to target, e.g., when considering automatic
+    /// code upgrades, like rewriting type annotations. Ruff will not propose
+    /// changes using features that are not available in the given version.
+    ///
+    /// For example, to represent supporting Python >=3.10 or ==3.10
+    /// specify `target-version = "py310"`.
+    ///
+    /// If omitted, and Ruff is configured via a `pyproject.toml` file, the
+    /// target version will be inferred from its `project.requires-python`
+    /// field (e.g., `requires-python = ">=3.8"`). If Ruff is configured via
+    /// `ruff.toml` or `.ruff.toml`, no such inference will be performed.
+    #[option(
+        default = r#""py38""#,
+        value_type = r#""py37" | "py38" | "py39" | "py310" | "py311" | "py312""#,
+        example = r#"
+            # Always generate Python 3.7-compatible code.
+            target-version = "py37"
+        "#
+    )]
+    pub target_version: Option<PythonVersion>,
+
+    /// The directories to consider when resolving first- vs. third-party
+    /// imports.
+    ///
+    /// As an example: given a Python package structure like:
+    ///
+    /// ```text
+    /// my_project
+    /// ├── pyproject.toml
+    /// └── src
+    ///     └── my_package
+    ///         ├── __init__.py
+    ///         ├── foo.py
+    ///         └── bar.py
+    /// ```
+    ///
+    /// The `./src` directory should be included in the `src` option
+    /// (e.g., `src = ["src"]`), such that when resolving imports,
+    /// `my_package.foo` is considered a first-party import.
+    ///
+    /// When omitted, the `src` directory will typically default to the
+    /// directory containing the nearest `pyproject.toml`, `ruff.toml`, or
+    /// `.ruff.toml` file (the "project root"), unless a configuration file
+    /// is explicitly provided (e.g., via the `--config` command-line flag).
+    ///
+    /// This field supports globs. For example, if you have a series of Python
+    /// packages in a `python_modules` directory, `src = ["python_modules/*"]`
+    /// would expand to incorporate all of the packages in that directory. User
+    /// home directory and environment variables will also be expanded.
+    #[option(
+        default = r#"["."]"#,
+        value_type = "list[str]",
+        example = r#"
+            # Allow imports relative to the "src" and "test" directories.
+            src = ["src", "test"]
+        "#
+    )]
+    pub src: Option<Vec<String>>,
+
+    /// A list of modules whose exports should be treated equivalently to
+    /// members of the `typing` module.
+    ///
+    /// This is useful for ensuring proper type annotation inference for
+    /// projects that re-export `typing` and `typing_extensions` members
+    /// from a compatibility module. If omitted, any members imported from
+    /// modules apart from `typing` and `typing_extensions` will be treated
+    /// as ordinary Python objects.
+    #[option(
+        default = r#"[]"#,
+        value_type = "list[str]",
+        example = r#"typing-modules = ["airflow.typing_compat"]"#
+    )]
+    pub typing_modules: Option<Vec<String>>,
+
+    // Global Formatting options
+    /// The line length to use when enforcing long-lines violations (like
+    /// `E501`). Must be greater than `0` and less than or equal to `320`.
+    #[option(
+        default = "88",
+        value_type = "int",
+        example = r#"
+        # Allow lines to be as long as 120 characters.
+        line-length = 120
+        "#
+    )]
+    #[cfg_attr(feature = "schemars", schemars(range(min = 1, max = 320)))]
+    pub line_length: Option<LineLength>,
+
+    /// The tabulation size to calculate line length.
+    #[option(
+        default = "4",
+        value_type = "int",
+        example = r#"
+            tab-size = 8
+        "#
+    )]
+    pub tab_size: Option<TabSize>,
+
+    /// The lint sections specified at the top level.
+    #[serde(flatten)]
+    pub lint_top_level: LintOptions,
+
+    /// Options to configure the code formatting.
+    ///
+    /// Previously:
+    /// The style in which violation messages should be formatted: `"text"`
+    /// (default), `"grouped"` (group messages by file), `"json"`
+    /// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub
+    /// Actions annotations), `"gitlab"` (GitLab CI code quality report),
+    /// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands).
+    ///
+    /// This option has been **deprecated** in favor of `output-format`
+    /// to avoid ambiguity with Ruff's upcoming formatter.
+    #[option_group]
+    #[cfg_attr(feature = "schemars", schemars(skip))]
+    pub format: Option<FormatOrOutputFormat>,
+
+    /// Experimental section to configure Ruff's linting. This new section will eventually
+    /// replace the top-level linting options.
+    ///
+    /// Options specified in the `lint` section take precedence over the top-level settings.
+    #[option_group]
+    pub lint: Option<LintOptions>,
+}
+
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+#[derive(
+    Debug, PartialEq, Eq, Default, ConfigurationOptions, CombineOptions, Serialize, Deserialize,
+)]
+#[serde(deny_unknown_fields, rename_all = "kebab-case")]
+pub struct LintOptions {
+    /// A list of allowed "confusable" Unicode characters to ignore when
+    /// enforcing `RUF001`, `RUF002`, and `RUF003`.
+    #[option(
+        default = r#"[]"#,
+        value_type = "list[str]",
+        example = r#"
+            # Allow minus-sign (U+2212), greek-small-letter-rho (U+03C1), and the asterisk-operator (U+2217),
+            # which could be confused for "-", "p", and "*", respectively.
+            allowed-confusables = ["−", "ρ", "∗"]
+        "#
+    )]
+    pub allowed_confusables: Option<Vec<char>>,
+
+    /// A regular expression used to identify "dummy" variables, or those which
+    /// should be ignored when enforcing (e.g.) unused-variable rules. The
+    /// default expression matches `_`, `__`, and `_var`, but not `_var_`.
+    #[option(
+        default = r#""^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$""#,
+        value_type = "re.Pattern",
+        example = r#"
+            # Only ignore variables named "_".
+            dummy-variable-rgx = "^_$"
+        "#
+    )]
+    pub dummy_variable_rgx: Option<String>,
 
     /// A list of rule codes or prefixes to ignore, in addition to those
     /// specified by `ignore`.
@@ -238,15 +503,6 @@ pub struct Options {
     )]
     pub external: Option<Vec<String>>,
 
-    /// Enable autofix behavior by-default when running `ruff` (overridden
-    /// by the `--fix` and `--no-fix` command-line flags).
-    #[option(default = "false", value_type = "bool", example = "fix = true")]
-    pub fix: Option<bool>,
-
-    /// Like `fix`, but disables reporting on leftover violation. Implies `fix`.
-    #[option(default = "false", value_type = "bool", example = "fix-only = true")]
-    pub fix_only: Option<bool>,
-
     /// A list of rule codes or prefixes to consider autofixable. By default,
     /// all rules are considered autofixable.
     #[option(
@@ -259,47 +515,6 @@ pub struct Options {
     )]
     pub fixable: Option<Vec<RuleSelector>>,
 
-    /// The style in which violation messages should be formatted: `"text"`
-    /// (default), `"grouped"` (group messages by file), `"json"`
-    /// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub
-    /// Actions annotations), `"gitlab"` (GitLab CI code quality report),
-    /// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands).
-    #[option(
-        default = r#""text""#,
-        value_type = r#""text" | "json" | "junit" | "github" | "gitlab" | "pylint" | "azure""#,
-        example = r#"
-            # Group violations by containing file.
-            output-format = "grouped"
-        "#
-    )]
-    pub output_format: Option<SerializationFormat>,
-
-    #[option(
-        default = r#"false"#,
-        value_type = "bool",
-        example = r#"
-            force-exclude = true
-        "#
-    )]
-    /// Whether to enforce `exclude` and `extend-exclude` patterns, even for
-    /// paths that are passed to Ruff explicitly. Typically, Ruff will lint
-    /// any paths passed in directly, even if they would typically be
-    /// excluded. Setting `force-exclude = true` will cause Ruff to
-    /// respect these exclusions unequivocally.
-    ///
-    /// This is useful for [`pre-commit`](https://pre-commit.com/), which explicitly passes all
-    /// changed files to the [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit)
-    /// plugin, regardless of whether they're marked as excluded by Ruff's own
-    /// settings.
-    #[option(
-        default = r#"false"#,
-        value_type = "bool",
-        example = r#"
-            force-exclude = true
-        "#
-    )]
-    pub force_exclude: Option<bool>,
-
     /// A list of rule codes or prefixes to ignore. Prefixes can specify exact
     /// rules (like `F841`), entire categories (like `F`), or anything in
     /// between.
@@ -330,46 +545,6 @@ pub struct Options {
     )]
     pub ignore_init_module_imports: Option<bool>,
 
-    /// A list of file patterns to include when linting.
-    ///
-    /// Inclusion are based on globs, and should be single-path patterns, like
-    /// `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is
-    /// included here not for configuration but because we lint whether e.g. the
-    /// `[project]` matches the schema.
-    ///
-    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
-    #[option(
-        default = r#"["*.py", "*.pyi", "**/pyproject.toml"]"#,
-        value_type = "list[str]",
-        example = r#"
-            include = ["*.py"]
-        "#
-    )]
-    pub include: Option<Vec<String>>,
-
-    /// The line length to use when enforcing long-lines violations (like
-    /// `E501`). Must be greater than `0` and less than or equal to `320`.
-    #[option(
-        default = "88",
-        value_type = "int",
-        example = r#"
-        # Allow lines to be as long as 120 characters.
-        line-length = 120
-        "#
-    )]
-    #[cfg_attr(feature = "schemars", schemars(range(min = 1, max = 320)))]
-    pub line_length: Option<LineLength>,
-
-    /// The tabulation size to calculate line length.
-    #[option(
-        default = "4",
-        value_type = "int",
-        example = r#"
-            tab-size = 8
-        "#
-    )]
-    pub tab_size: Option<TabSize>,
-
     /// A list of objects that should be treated equivalently to a
     /// `logging.Logger` object.
     ///
@@ -395,30 +570,6 @@ pub struct Options {
     )]
     pub logger_objects: Option<Vec<String>>,
 
-    /// Require a specific version of Ruff to be running (useful for unifying
-    /// results across many environments, e.g., with a `pyproject.toml`
-    /// file).
-    #[option(
-        default = "None",
-        value_type = "str",
-        example = r#"
-            required-version = "0.0.193"
-        "#
-    )]
-    pub required_version: Option<Version>,
-
-    /// Whether to automatically exclude files that are ignored by `.ignore`,
-    /// `.gitignore`, `.git/info/exclude`, and global `gitignore` files.
-    /// Enabled by default.
-    #[option(
-        default = "true",
-        value_type = "bool",
-        example = r#"
-            respect-gitignore = false
-        "#
-    )]
-    pub respect_gitignore: Option<bool>,
-
     /// A list of rule codes or prefixes to enable. Prefixes can specify exact
     /// rules (like `F841`), entire categories (like `F`), or anything in
     /// between.
@@ -436,113 +587,6 @@ pub struct Options {
     )]
     pub select: Option<Vec<RuleSelector>>,
 
-    /// Whether to show source code snippets when reporting lint violations
-    /// (overridden by the `--show-source` command-line flag).
-    #[option(
-        default = "false",
-        value_type = "bool",
-        example = r#"
-            # By default, always show source code snippets.
-            show-source = true
-        "#
-    )]
-    pub show_source: Option<bool>,
-
-    /// Whether to show an enumeration of all autofixed lint violations
-    /// (overridden by the `--show-fixes` command-line flag).
-    #[option(
-        default = "false",
-        value_type = "bool",
-        example = r#"
-            # Enumerate all fixed violations.
-            show-fixes = true
-        "#
-    )]
-    pub show_fixes: Option<bool>,
-
-    /// The directories to consider when resolving first- vs. third-party
-    /// imports.
-    ///
-    /// As an example: given a Python package structure like:
-    ///
-    /// ```text
-    /// my_project
-    /// ├── pyproject.toml
-    /// └── src
-    ///     └── my_package
-    ///         ├── __init__.py
-    ///         ├── foo.py
-    ///         └── bar.py
-    /// ```
-    ///
-    /// The `./src` directory should be included in the `src` option
-    /// (e.g., `src = ["src"]`), such that when resolving imports,
-    /// `my_package.foo` is considered a first-party import.
-    ///
-    /// When omitted, the `src` directory will typically default to the
-    /// directory containing the nearest `pyproject.toml`, `ruff.toml`, or
-    /// `.ruff.toml` file (the "project root"), unless a configuration file
-    /// is explicitly provided (e.g., via the `--config` command-line flag).
-    ///
-    /// This field supports globs. For example, if you have a series of Python
-    /// packages in a `python_modules` directory, `src = ["python_modules/*"]`
-    /// would expand to incorporate all of the packages in that directory. User
-    /// home directory and environment variables will also be expanded.
-    #[option(
-        default = r#"["."]"#,
-        value_type = "list[str]",
-        example = r#"
-            # Allow imports relative to the "src" and "test" directories.
-            src = ["src", "test"]
-        "#
-    )]
-    pub src: Option<Vec<String>>,
-
-    /// Mark the specified directories as namespace packages. For the purpose of
-    /// module resolution, Ruff will treat those directories as if they
-    /// contained an `__init__.py` file.
-    #[option(
-        default = r#"[]"#,
-        value_type = "list[str]",
-        example = r#"
-            namespace-packages = ["airflow/providers"]
-        "#
-    )]
-    pub namespace_packages: Option<Vec<String>>,
-
-    /// The minimum Python version to target, e.g., when considering automatic
-    /// code upgrades, like rewriting type annotations. Ruff will not propose
-    /// changes using features that are not available in the given version.
-    ///
-    /// For example, to represent supporting Python >=3.10 or ==3.10
-    /// specify `target-version = "py310"`.
-    ///
-    /// If omitted, and Ruff is configured via a `pyproject.toml` file, the
-    /// target version will be inferred from its `project.requires-python`
-    /// field (e.g., `requires-python = ">=3.8"`). If Ruff is configured via
-    /// `ruff.toml` or `.ruff.toml`, no such inference will be performed.
-    #[option(
-        default = r#""py38""#,
-        value_type = r#""py37" | "py38" | "py39" | "py310" | "py311" | "py312""#,
-        example = r#"
-            # Always generate Python 3.7-compatible code.
-            target-version = "py37"
-        "#
-    )]
-    pub target_version: Option<PythonVersion>,
-
-    /// Whether to enable preview mode. When preview mode is enabled, Ruff will
-    /// use unstable rules and fixes.
-    #[option(
-        default = "false",
-        value_type = "bool",
-        example = r#"
-            # Enable preview features
-            preview = true
-        "#
-    )]
-    pub preview: Option<bool>,
-
     /// A list of task tags to recognize (e.g., "TODO", "FIXME", "XXX").
     ///
     /// Comments starting with these tags will be ignored by commented-out code
@@ -551,25 +595,12 @@ pub struct Options {
     #[option(
         default = r#"["TODO", "FIXME", "XXX"]"#,
         value_type = "list[str]",
-        example = r#"task-tags = ["HACK"]"#
+        example = r#"
+            task-tags = ["HACK"]
+        "#
     )]
     pub task_tags: Option<Vec<String>>,
 
-    /// A list of modules whose exports should be treated equivalently to
-    /// members of the `typing` module.
-    ///
-    /// This is useful for ensuring proper type annotation inference for
-    /// projects that re-export `typing` and `typing_extensions` members
-    /// from a compatibility module. If omitted, any members imported from
-    /// modules apart from `typing` and `typing_extensions` will be treated
-    /// as ordinary Python objects.
-    #[option(
-        default = r#"[]"#,
-        value_type = "list[str]",
-        example = r#"typing-modules = ["airflow.typing_compat"]"#
-    )]
-    pub typing_modules: Option<Vec<String>>,
-
     /// A list of rule codes or prefixes to consider non-autofix-able.
     #[option(
         default = "[]",
@@ -677,20 +708,6 @@ pub struct Options {
     #[option_group]
     pub pyupgrade: Option<PyUpgradeOptions>,
 
-    /// Options to configure the code formatting.
-    ///
-    /// Previously:
-    /// The style in which violation messages should be formatted: `"text"`
-    /// (default), `"grouped"` (group messages by file), `"json"`
-    /// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub
-    /// Actions annotations), `"gitlab"` (GitLab CI code quality report),
-    /// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands).
-    ///
-    /// This option has been **deprecated** in favor of `output-format`
-    /// to avoid ambiguity with Ruff's upcoming formatter.
-    #[option_group]
-    pub format: Option<FormatOrOutputFormat>,
-
     // Tables are required to go last.
     /// A list of mappings from file pattern to rule codes or prefixes to
     /// exclude, when considering any matching files.
diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs
index cf6fb30893b2e4..64884596972265 100644
--- a/crates/ruff_workspace/src/pyproject.rs
+++ b/crates/ruff_workspace/src/pyproject.rs
@@ -161,7 +161,7 @@ mod tests {
     use ruff_linter::line_width::LineLength;
     use ruff_linter::settings::types::PatternPrefixPair;
 
-    use crate::options::Options;
+    use crate::options::{LintOptions, Options};
     use crate::pyproject::{find_settings_toml, parse_pyproject_toml, Pyproject, Tools};
     use crate::tests::test_resource_path;
 
@@ -236,7 +236,10 @@ select = ["E501"]
             pyproject.tool,
             Some(Tools {
                 ruff: Some(Options {
-                    select: Some(vec![codes::Pycodestyle::E501.into()]),
+                    lint_top_level: LintOptions {
+                        select: Some(vec![codes::Pycodestyle::E501.into()]),
+                        ..LintOptions::default()
+                    },
                     ..Options::default()
                 })
             })
@@ -254,8 +257,11 @@ ignore = ["E501"]
             pyproject.tool,
             Some(Tools {
                 ruff: Some(Options {
-                    extend_select: Some(vec![codes::Ruff::_100.into()]),
-                    ignore: Some(vec![codes::Pycodestyle::E501.into()]),
+                    lint_top_level: LintOptions {
+                        extend_select: Some(vec![codes::Ruff::_100.into()]),
+                        ignore: Some(vec![codes::Pycodestyle::E501.into()]),
+                        ..LintOptions::default()
+                    },
                     ..Options::default()
                 })
             })
@@ -308,10 +314,14 @@ other-attribute = 1
                     "migrations".to_string(),
                     "with_excluded_file/other_excluded_file.py".to_string(),
                 ]),
-                per_file_ignores: Some(FxHashMap::from_iter([(
-                    "__init__.py".to_string(),
-                    vec![codes::Pyflakes::_401.into()]
-                )])),
+
+                lint_top_level: LintOptions {
+                    per_file_ignores: Some(FxHashMap::from_iter([(
+                        "__init__.py".to_string(),
+                        vec![codes::Pyflakes::_401.into()]
+                    )])),
+                    ..LintOptions::default()
+                },
                 ..Options::default()
             }
         );
diff --git a/ruff.schema.json b/ruff.schema.json
index cfe3bdc0bcca7b..124d6efcdc5ebd 100644
--- a/ruff.schema.json
+++ b/ruff.schema.json
@@ -326,17 +326,6 @@
         "null"
       ]
     },
-    "format": {
-      "description": "Options to configure the code formatting.\n\nPreviously: The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), `\"github\"` (GitHub Actions annotations), `\"gitlab\"` (GitLab CI code quality report), `\"pylint\"` (Pylint text format) or `\"azure\"` (Azure Pipeline logging commands).\n\nThis option has been **deprecated** in favor of `output-format` to avoid ambiguity with Ruff's upcoming formatter.",
-      "anyOf": [
-        {
-          "$ref": "#/definitions/FormatOrOutputFormat"
-        },
-        {
-          "type": "null"
-        }
-      ]
-    },
     "ignore": {
       "description": "A list of rule codes or prefixes to ignore. Prefixes can specify exact rules (like `F841`), entire categories (like `F`), or anything in between.\n\nWhen breaking ties between enabled and disabled rules (via `select` and `ignore`, respectively), more specific prefixes override less specific prefixes.",
       "type": [
@@ -388,6 +377,17 @@
       "maximum": 320.0,
       "minimum": 1.0
     },
+    "lint": {
+      "description": "Experimental section to configure Ruff's linting. This new section will eventually replace the top-level linting options.\n\nOptions specified in the `lint` section take precedence over the top-level settings.",
+      "anyOf": [
+        {
+          "$ref": "#/definitions/LintOptions"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
     "logger-objects": {
       "description": "A list of objects that should be treated equivalently to a `logging.Logger` object.\n\nThis is useful for ensuring proper diagnostics (e.g., to identify `logging` deprecations and other best-practices) for projects that re-export a `logging.Logger` object from a common module.\n\nFor example, if you have a module `logging_setup.py` with the following contents: ```python import logging\n\nlogger = logging.getLogger(__name__) ```\n\nAdding `\"logging_setup.logger\"` to `logger-objects` will ensure that `logging_setup.logger` is treated as a `logging.Logger` object when imported from other modules (e.g., `from logging_setup import logger`).",
       "type": [
@@ -1162,79 +1162,6 @@
       },
       "additionalProperties": false
     },
-    "FormatOptions": {
-      "type": "object",
-      "properties": {
-        "exclude": {
-          "description": "A list of file patterns to exclude from formatting.\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).\n\nNote that you don't need to list files that are excluded by [`exclude`](#exclude).",
-          "type": [
-            "array",
-            "null"
-          ],
-          "items": {
-            "type": "string"
-          }
-        },
-        "indent-style": {
-          "description": "Whether to use 4 spaces or hard tabs for indenting code.\n\nDefaults to 4 spaces. We only recommend changing this option, if you need to for accessibility reasons.",
-          "anyOf": [
-            {
-              "$ref": "#/definitions/IndentStyle"
-            },
-            {
-              "type": "null"
-            }
-          ]
-        },
-        "line-ending": {
-          "description": "The character Ruff uses at the end of a line.",
-          "anyOf": [
-            {
-              "$ref": "#/definitions/LineEnding"
-            },
-            {
-              "type": "null"
-            }
-          ]
-        },
-        "preview": {
-          "description": "Whether to enable the unstable preview style formatting.",
-          "type": [
-            "boolean",
-            "null"
-          ]
-        },
-        "quote-style": {
-          "description": "Whether to prefer single `'` or double `\"` quotes for strings and docstrings.\n\nRuff may deviate from this option if using the configured quotes would require more escaped quotes:\n\n```python a = \"It's monday morning\" b = \"a string without any quotes\" ```\n\nRuff leaves `a` unchanged when using `quote-style = \"single\"` because it is otherwise necessary to escape the `'` which leads to less readable code: `'It\\'s monday morning'`. Ruff changes the quotes of `b` to use single quotes.",
-          "anyOf": [
-            {
-              "$ref": "#/definitions/QuoteStyle"
-            },
-            {
-              "type": "null"
-            }
-          ]
-        },
-        "skip-magic-trailing-comma": {
-          "description": "Ruff uses existing trailing commas as an indication that short lines should be left separate. If this option is set to `true`, the magic trailing comma is ignored.",
-          "type": [
-            "boolean",
-            "null"
-          ]
-        }
-      },
-      "additionalProperties": false
-    },
-    "FormatOrOutputFormat": {
-      "anyOf": [
-        {
-          "$ref": "#/definitions/FormatOptions"
-        },
-        {
-          "$ref": "#/definitions/SerializationFormat"
-        }
-      ]
-    },
     "ImportSection": {
       "anyOf": [
         {
@@ -1255,24 +1182,6 @@
         "local-folder"
       ]
     },
-    "IndentStyle": {
-      "oneOf": [
-        {
-          "description": "Use tabs to indent code.",
-          "type": "string",
-          "enum": [
-            "tab"
-          ]
-        },
-        {
-          "description": "Use [`IndentWidth`] spaces to indent code.",
-          "type": "string",
-          "enum": [
-            "space"
-          ]
-        }
-      ]
-    },
     "IsortOptions": {
       "type": "object",
       "properties": {
@@ -1506,43 +1415,423 @@
       },
       "additionalProperties": false
     },
-    "LineEnding": {
-      "oneOf": [
-        {
-          "description": "Line endings will be converted to `\\n` as is common on Unix.",
-          "type": "string",
-          "enum": [
-            "lf"
+    "LineLength": {
+      "description": "The length of a line of text that is considered too long.\n\nThe allowed range of values is 1..=320",
+      "type": "integer",
+      "format": "uint16",
+      "minimum": 1.0
+    },
+    "LintOptions": {
+      "type": "object",
+      "properties": {
+        "allowed-confusables": {
+          "description": "A list of allowed \"confusable\" Unicode characters to ignore when enforcing `RUF001`, `RUF002`, and `RUF003`.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "type": "string",
+            "maxLength": 1,
+            "minLength": 1
+          }
+        },
+        "dummy-variable-rgx": {
+          "description": "A regular expression used to identify \"dummy\" variables, or those which should be ignored when enforcing (e.g.) unused-variable rules. The default expression matches `_`, `__`, and `_var`, but not `_var_`.",
+          "type": [
+            "string",
+            "null"
           ]
         },
-        {
-          "description": "Line endings will be converted to `\\r\\n` as is common on Windows.",
-          "type": "string",
-          "enum": [
-            "cr-lf"
+        "extend-fixable": {
+          "description": "A list of rule codes or prefixes to consider autofixable, in addition to those specified by `fixable`.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "$ref": "#/definitions/RuleSelector"
+          }
+        },
+        "extend-per-file-ignores": {
+          "description": "A list of mappings from file pattern to rule codes or prefixes to exclude, in addition to any rules excluded by `per-file-ignores`.",
+          "type": [
+            "object",
+            "null"
+          ],
+          "additionalProperties": {
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/RuleSelector"
+            }
+          }
+        },
+        "extend-select": {
+          "description": "A list of rule codes or prefixes to enable, in addition to those specified by `select`.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "$ref": "#/definitions/RuleSelector"
+          }
+        },
+        "external": {
+          "description": "A list of rule codes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "type": "string"
+          }
+        },
+        "fixable": {
+          "description": "A list of rule codes or prefixes to consider autofixable. By default, all rules are considered autofixable.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "$ref": "#/definitions/RuleSelector"
+          }
+        },
+        "flake8-annotations": {
+          "description": "Options for the `flake8-annotations` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8AnnotationsOptions"
+            },
+            {
+              "type": "null"
+            }
           ]
         },
-        {
-          "description": "The newline style is detected automatically on a file per file basis. Files with mixed line endings will be converted to the first detected line ending. Defaults to [`LineEnding::Lf`] for a files that contain no line endings.",
-          "type": "string",
-          "enum": [
-            "auto"
+        "flake8-bandit": {
+          "description": "Options for the `flake8-bandit` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8BanditOptions"
+            },
+            {
+              "type": "null"
+            }
           ]
         },
-        {
-          "description": "Line endings will be converted to `\\n` on Unix and `\\r\\n` on Windows.",
-          "type": "string",
-          "enum": [
-            "native"
+        "flake8-bugbear": {
+          "description": "Options for the `flake8-bugbear` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8BugbearOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-builtins": {
+          "description": "Options for the `flake8-builtins` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8BuiltinsOptions"
+            },
+            {
+              "type": "null"
+            }
           ]
+        },
+        "flake8-comprehensions": {
+          "description": "Options for the `flake8-comprehensions` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8ComprehensionsOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-copyright": {
+          "description": "Options for the `flake8-copyright` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8CopyrightOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-errmsg": {
+          "description": "Options for the `flake8-errmsg` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8ErrMsgOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-gettext": {
+          "description": "Options for the `flake8-gettext` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8GetTextOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-implicit-str-concat": {
+          "description": "Options for the `flake8-implicit-str-concat` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8ImplicitStrConcatOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-import-conventions": {
+          "description": "Options for the `flake8-import-conventions` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8ImportConventionsOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-pytest-style": {
+          "description": "Options for the `flake8-pytest-style` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8PytestStyleOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-quotes": {
+          "description": "Options for the `flake8-quotes` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8QuotesOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-self": {
+          "description": "Options for the `flake8_self` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8SelfOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-tidy-imports": {
+          "description": "Options for the `flake8-tidy-imports` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8TidyImportsOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-type-checking": {
+          "description": "Options for the `flake8-type-checking` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8TypeCheckingOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "flake8-unused-arguments": {
+          "description": "Options for the `flake8-unused-arguments` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Flake8UnusedArgumentsOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "ignore": {
+          "description": "A list of rule codes or prefixes to ignore. Prefixes can specify exact rules (like `F841`), entire categories (like `F`), or anything in between.\n\nWhen breaking ties between enabled and disabled rules (via `select` and `ignore`, respectively), more specific prefixes override less specific prefixes.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "$ref": "#/definitions/RuleSelector"
+          }
+        },
+        "ignore-init-module-imports": {
+          "description": "Avoid automatically removing unused imports in `__init__.py` files. Such imports will still be flagged, but with a dedicated message suggesting that the import is either added to the module's `__all__` symbol, or re-exported with a redundant alias (e.g., `import os as os`).",
+          "type": [
+            "boolean",
+            "null"
+          ]
+        },
+        "isort": {
+          "description": "Options for the `isort` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/IsortOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "logger-objects": {
+          "description": "A list of objects that should be treated equivalently to a `logging.Logger` object.\n\nThis is useful for ensuring proper diagnostics (e.g., to identify `logging` deprecations and other best-practices) for projects that re-export a `logging.Logger` object from a common module.\n\nFor example, if you have a module `logging_setup.py` with the following contents: ```python import logging\n\nlogger = logging.getLogger(__name__) ```\n\nAdding `\"logging_setup.logger\"` to `logger-objects` will ensure that `logging_setup.logger` is treated as a `logging.Logger` object when imported from other modules (e.g., `from logging_setup import logger`).",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "type": "string"
+          }
+        },
+        "mccabe": {
+          "description": "Options for the `mccabe` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/McCabeOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pep8-naming": {
+          "description": "Options for the `pep8-naming` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Pep8NamingOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "per-file-ignores": {
+          "description": "A list of mappings from file pattern to rule codes or prefixes to exclude, when considering any matching files.",
+          "type": [
+            "object",
+            "null"
+          ],
+          "additionalProperties": {
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/RuleSelector"
+            }
+          }
+        },
+        "pycodestyle": {
+          "description": "Options for the `pycodestyle` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/PycodestyleOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pydocstyle": {
+          "description": "Options for the `pydocstyle` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/PydocstyleOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pyflakes": {
+          "description": "Options for the `pyflakes` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/PyflakesOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pylint": {
+          "description": "Options for the `pylint` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/PylintOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pyupgrade": {
+          "description": "Options for the `pyupgrade` plugin.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/PyUpgradeOptions"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "select": {
+          "description": "A list of rule codes or prefixes to enable. Prefixes can specify exact rules (like `F841`), entire categories (like `F`), or anything in between.\n\nWhen breaking ties between enabled and disabled rules (via `select` and `ignore`, respectively), more specific prefixes override less specific prefixes.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "$ref": "#/definitions/RuleSelector"
+          }
+        },
+        "task-tags": {
+          "description": "A list of task tags to recognize (e.g., \"TODO\", \"FIXME\", \"XXX\").\n\nComments starting with these tags will be ignored by commented-out code detection (`ERA`), and skipped by line-length rules (`E501`) if `ignore-overlong-task-comments` is set to `true`.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "type": "string"
+          }
+        },
+        "unfixable": {
+          "description": "A list of rule codes or prefixes to consider non-autofix-able.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "$ref": "#/definitions/RuleSelector"
+          }
         }
-      ]
-    },
-    "LineLength": {
-      "description": "The length of a line of text that is considered too long.\n\nThe allowed range of values is 1..=320",
-      "type": "integer",
-      "format": "uint16",
-      "minimum": 1.0
+      },
+      "additionalProperties": false
     },
     "McCabeOptions": {
       "type": "object",
@@ -1807,13 +2096,6 @@
         }
       ]
     },
-    "QuoteStyle": {
-      "type": "string",
-      "enum": [
-        "single",
-        "double"
-      ]
-    },
     "RelativeImportsOrder": {
       "oneOf": [
         {