Repository: smithy-lang/smithy
Pull request #2134 (2024-02-08 17:47:41)

Add JReleaser configuration for CLI releases
*Description of changes:*
- For speeding up CLI releases, we can use JReleaser to build, package, sign, and publish artifacts to GitHub release

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index ab40d21d7..fd0ef6457 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,6 +1,14 @@
-*Issue #, if available:*
 
-*Description of changes:*
+#### Background
+* What do these changes do? 
+* Why are they important?
 
+#### Testing
+* How did you test these changes?
 
+#### Links
+* Links to additional context, if necessary
+* Issue #, if applicable (see [here](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for a list of keywords to use for linking issues)
+
+---
 By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
diff --git a/.github/workflows/make-docs.yml b/.github/workflows/make-docs.yml
new file mode 100644
index 000000000..d79e73b0e
--- /dev/null
+++ b/.github/workflows/make-docs.yml
@@ -0,0 +1,86 @@
+name: make-docs
+on:
+  release:
+    types: [released]
+  workflow_dispatch: # on button click
+    inputs:
+      javadocs:
+        type: boolean
+        description: 'Build JavaDocs?'
+        required: true
+      release:
+        type: boolean
+        description: 'Release the docs?'
+        required: true
+
+env:
+  ARTIFACTS_DIR: /tmp/artifacts
+
+jobs:
+  build-docs:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout repo 
+      uses: actions/checkout@v4
+    - name: Setup
+      run: |
+        mkdir -p $ARTIFACTS_DIR
+
+    - name: Build JavaDocs
+      # run if 'javadocs' is not set to false 
+      if: ${{ !contains(inputs.javadocs, 'false') }}
+      run: |
+        VERSION=$(cat VERSION)
+        mkdir -p $ARTIFACTS_DIR/javadoc
+        ./gradlew javadoc
+        cp -R build/docs/javadoc/latest $ARTIFACTS_DIR/javadoc/$VERSION
+        mkdir $ARTIFACTS_DIR/javadoc/latest
+        cat << EOF > $ARTIFACTS_DIR/javadoc/latest/index.html
+        <!DOCTYPE html>
+        <meta charset="utf-8">
+        <title>Redirecting to latest javadocs</title>
+        <meta http-equiv="refresh" content="0; URL=../${VERSION}/">
+        EOF
+
+    - name: Build Smithy docs
+      run: |
+        cd docs
+        pip3 install -r requirements.txt
+        make clean
+        make html
+        cp -R build/html/* $ARTIFACTS_DIR
+        rm $ARTIFACTS_DIR/.buildinfo || true
+        rm $ARTIFACTS_DIR/objects.inv || true
+
+    - name: Upload artifacts
+      uses: actions/upload-artifact@v4
+      with:
+        name: smithy-docs
+        path: ${{ env.ARTIFACTS_DIR }}
+        retention-days: 7
+  
+
+  publish-docs:
+    runs-on: ubuntu-latest
+    # run if 'release' is not set to false 
+    if: ${{ !contains(inputs.release, 'false') }}
+    needs: build-docs
+    steps:
+    - name: Checkout GitHub pages branch
+      uses: actions/checkout@v4
+      with:
+        ref: 'gh-pages'
+    - name: Download artifacts
+      id: download
+      uses: actions/download-artifact@v4
+      with:
+        name: smithy-docs
+        path: /tmp/smithy-docs
+    - name: Copy artifacts
+      run: |
+        cp -R /tmp/smithy-docs/* .
+        git config --global user.name "smithy-automation"
+        git config --global user.email "github-smithy-automation@amazon.com"
+        git add -A
+        git commit -m "Update documentation"
+        git push
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79eb4ba8e..59ab6f48c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
 # Smithy Changelog
 
+## 1.45.0 (2024-02-14)
+
+### Features
+
+* Added new option to CLI to configure the format (`text` or `csv`) of validation output. ([#2133](https://github.com/smithy-lang/smithy/pull/2133))
+* Added options to CLI to hide or show validation events for specified validators. ([#2127](https://github.com/smithy-lang/smithy/pull/2127))
+* Added protocol tests for verifying serialization/deserialization behavior for maps with document values. ([#2125](https://github.com/smithy-lang/smithy/pull/2125))
+* Changed `UnreferencedShape` to be an opt-in linter instead of on-by-default to reduce friction when defining common shapes that are not connected to a service shape. Added a validator that allows you to configure what shape to check connectedness (defaults to service shape). ([#2119](https://github.com/smithy-lang/smithy/pull/2119))
+
+### Bug Fixes
+
+* Fixed headers when printing validation event output in csv format. ([#2136](https://github.com/smithy-lang/smithy/pull/2136))
+* Fixed `RestJsonZeroAndFalseQueryValues` protocol test to correctly include a params value for servers. ([#2132](https://github.com/smithy-lang/smithy/pull/2132))
+* Fixed regression in model validation that incorrectly allowed suppression of ERROR events. ([#2130](https://github.com/smithy-lang/smithy/pull/2130))
+
+### Documentation
+
+* Fixed typo in @pattern validation example. ([#2126](https://github.com/smithy-lang/smithy/pull/2126))
+
+
 ## 1.44.0 (2024-01-25)
 
 ### Features
diff --git a/README.md b/README.md
index 637d3ea99..de103e253 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,12 @@
 # <img alt="Smithy" src="docs/_static/favicon.png" width="28"> Smithy
 [![Build Status](https://github.com/smithy-lang/smithy/workflows/ci/badge.svg)](https://github.com/smithy-lang/smithy/actions/workflows/ci.yml)
 
+<strong> We want your feedback on Smithy.</strong>
+Please consider taking our brief <a href="https://amazonmr.au1.qualtrics.com/jfe/form/SV_9Rfqo2wizSpnZPM">survey</a>
+to share your feedback.
+
+---
+
 Smithy defines and generates clients, services, and documentation for
 any protocol.
 
@@ -42,26 +48,19 @@ guide][quickstart].
 
 
 # Building Smithy models
+> [!IMPORTANT]  
+> Before you proceed, make sure you have the [Smithy CLI installed](https://smithy.io/2.0/guides/smithy-cli/cli_installation.html#cli-installation).
 
-The [Smithy Gradle Plugin] is the best way to get started with building a
-Smithy model. First, create a [`smithy-build.json`] file:
+The [Smithy CLI] is the easiest way to get started with building Smithy models. First, create a [`smithy-build.json`] file:
 
 ```json
 {
-    "version": "1.0"
+    "version": "1.0",
+    "sources": ["model"]
 }
 ```
 
-Then, apply the Smithy Gradle Plugin in your `build.gradle.kts` file and run
-`gradle build`:
-
-```kotlin
-plugins {
-   id("software.amazon.smithy").version("0.6.0")
-}
-```
-
-Finally, create your first model `model/main.smithy`:
+Next, create your first model `model/main.smithy`:
 
 ```smithy
 $version: "2"
@@ -73,10 +72,11 @@ service ExampleService {
 }
 ```
 
+Finally, run `smithy build` to build the model with the [Smithy CLI].
+
 Find out more about building artifacts of your Smithy model in the [Building
 Smithy Models][building] guide. For more examples, see the
-[examples directory](https://github.com/awslabs/smithy-gradle-plugin/tree/main/examples)
-of the Smithy Gradle Plugin repository.
+[examples repository](https://github.com/smithy-lang/smithy-examples)
 
 # License
 
@@ -87,6 +87,7 @@ This library is licensed under the Apache 2.0 License.
 [javadocs]: https://smithy.io/javadoc/latest/
 [quickstart]: https://smithy.io/2.0/quickstart.html
 [Smithy Gradle Plugin]: https://github.com/awslabs/smithy-gradle-plugin/
+[Smithy CLI]: https://smithy.io/2.0/guides/smithy-cli/index.html
 [`smithy-build.json`]: https://smithy.io/2.0/guides/building-models/build-config.html#using-smithy-build-json
 [building]: https://smithy.io/2.0/guides/building-models/index.html
 [awesome-smithy]: https://github.com/smithy-lang/awesome-smithy
diff --git a/VERSION b/VERSION
index 372cf402c..50aceaa7b 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.44.0
+1.45.0
diff --git a/build.gradle b/build.gradle
index f246dd12b..1efd1a7aa 100644
--- a/build.gradle
+++ b/build.gradle
@@ -25,7 +25,7 @@ plugins {
     id "io.codearte.nexus-staging" version "0.30.0"
     id "me.champeau.jmh" version "0.7.1"
     id "com.github.johnrengelman.shadow" version "7.1.2"
-    id "org.jreleaser" version "1.9.0"
+    id "org.jreleaser" version "1.10.0"
 }
 
 ext {
@@ -326,8 +326,8 @@ if (useJreleaser) {
                         active = "ALWAYS"
                         url = "https://aws.oss.sonatype.org/service/local"
                         snapshotUrl = "https://aws.oss.sonatype.org/content/repositories/snapshots"
-                        closeRepository = false
-                        releaseRepository = false
+                        closeRepository = true
+                        releaseRepository = true
                         stagingRepository(stagingDirectory.get().toString())
                     }
                 }
diff --git a/docs/Makefile b/docs/Makefile
index b0a870ca9..4e191c35b 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -22,7 +22,7 @@ merge-versions:
 	mkdir -p build/html
 	cp -R "build/1.0/html" "build/html/1.0"
 	cp -R "build/2.0/html" "build/html/2.0"
-	cp -R "root/" "build/html"
+	cp -R root/* "build/html"
 
 openhtml:
 	open "build/html/index.html"
diff --git a/docs/source-2.0/conf.py b/docs/source-2.0/conf.py
index 9a2c11465..fc6fb0ec3 100644
--- a/docs/source-2.0/conf.py
+++ b/docs/source-2.0/conf.py
@@ -12,3 +12,7 @@ release = u'2.0'
 version = release
 
 html_theme_options['source_directory'] = "docs/source-2.0"
+html_theme_options['announcement'] = '''<strong> We want your feedback on Smithy.</strong>
+Please consider taking our brief <a href="https://amazonmr.au1.qualtrics.com/jfe/form/SV_9Rfqo2wizSpnZPM">survey</a>
+ to share your feedback.
+'''
diff --git a/docs/source-2.0/guides/gradle-plugin.rst b/docs/source-2.0/guides/gradle-plugin.rst
deleted file mode 100644
index 6a8fa7fa6..000000000
--- a/docs/source-2.0/guides/gradle-plugin.rst
+++ /dev/null
@@ -1,345 +0,0 @@
-.. _smithy-gradle-plugin:
-
-====================
-Smithy Gradle Plugin
-====================
-
-The `Smithy Gradle plugin`_ integrates Smithy with `Gradle`_. This plugin can
-build artifacts from Smithy models, generate JARs that contain Smithy models
-found in Java projects, and generate JARs that contain filtered :ref:`projections <projections>`
-of Smithy models.
-
-
-.. _plugin-install:
-
-Installation
-============
-
-The Smithy Gradle plugin is applied using the ``software.amazon.smithy`` plugin.
-The following example configures a project to use the Smithy Gradle plugin:
-
-.. code-block:: kotlin
-
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
-
-
-.. _smithy-model-sources:
-
-Smithy model sources
-====================
-
-When a JAR is generated for a project, any Smithy models found in the
-following directories will be added to the JAR:
-
-* ``model/``
-* ``src/main/smithy``
-* ``src/main/resources/META-INF/smithy``
-
-Models found in these directories are combined into a flattened directory
-structure and used to validate and build the Smithy model. A Smithy manifest
-file is automatically created for the detected models, and it along with the
-model files, are placed in the ``META-INF/smithy/`` resource of the created
-JAR. Any project that then depends on this created JAR will be able to find
-and use the Smithy models contained in the JAR when using *model discovery*.
-
-
-.. _building-smithy-models:
-
-Building Smithy models
-======================
-
-This plugin operates in two different modes:
-
-1. If no ``projection`` is specified for the ``SmithyExtension``, then the plugin
-   runs a "source" build using the "source" projection.
-2. If a ``projection`` is specified for the ``SmithyExtension``, then the plugin
-   runs in a "projection" mode.
-
-
-.. _smithy-extension-properties:
-
-Smithy extension properties
-===========================
-
-This plugin is configured using the ``software.amazon.smithy.gradle.SmithyExtension``
-extension:
-
-.. code-block:: kotlin
-
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        projection = "foo"
-    }
-
-
-This extension supports the following properties:
-
-.. list-table::
-    :header-rows: 1
-    :widths: 10 20 70
-
-    * - Property
-      - Type
-      - Description
-    * - projection
-      - ``String``
-      - Sets the :ref:`projection <projections>` name to build. There must
-        be a corresponding projection definition in the :ref:`smithy-build.json <smithy-build-json>`
-        file in the project.
-    * - projectionSourceTags
-      - ``Set<String>``
-      - Get the tags that are searched for in classpaths when determining
-        which models are projected into the created JAR. This plugin will look
-        through the JARs in the buildscript classpath to see if they contain a
-        META-INF/MANIFEST.MF attribute named "Smithy-Tags" that matches any of
-        the given projection source tags. The Smithy models found in each
-        matching JAR are copied into the JAR being projected. This allows a
-        projection JAR to aggregate models into a single JAR.
-    * - tags
-      - ``Set<String>``
-      - Set the tags that are added to the JAR. These tags are placed in the
-        META-INF/MANIFEST.MF attribute named "Smithy-Tags" as a comma
-        separated list. JARs with Smithy-Tags can be queried when building
-        projections so that the Smithy models found in each matching JAR are
-        placed into the projection JAR.
-    * - smithyBuildConfigs
-      - ``FileCollection``
-      - Sets a custom collection of smithy-build.json files to use when
-        building the model.
-    * - allowUnknownTraits
-      - ``boolean``
-      - Sets whether or not unknown traits in the model should be ignored. By
-        default, the build will fail if unknown traits are encountered.
-    * - outputDirectory
-      - ``File``
-      - Defines where Smithy build artifacts are written.
-
-
-.. _building-source-model:
-
-Building a source model
------------------------
-
-A "source" build is run when no ``projection`` is configured in
-``SmithyExtension``. Because no projection was specified, **smithy-build** is
-executed using the ``compileClasspath`` plus the ``buildscript`` classpath. To
-prevent accidentally relying on Smithy models that are only available to
-build scripts, Smithy models are discovered using only the
-``compileClasspath`` and ``runtimeClasspath``.
-
-The following example ``build.gradle.kts`` will build a Smithy model using a
-"source" build:
-
-.. code-block:: kotlin
-
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
-
-    // The SmithyExtension is used to customize the build. This example
-    // doesn't set any values and can be completely omitted.
-    configure<software.amazon.smithy.gradle.SmithyExtension> {}
-
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
-
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-
-        // These are just examples of dependencies. This model has a dependency on
-        // a "common" model package and uses the external AWS traits.
-        implementation("com.foo.baz:foo-model-internal-common:1.0.0")
-        implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
-    }
-
-
-.. _generating-projection:
-
-Generating a projection
------------------------
-
-A "projection" build is run when a :ref:`projection <projections>` is
-specified in the ``SmithyExtension``. You might want to create a projection of
-a model if you need to maintain an internal version of a model that contains
-more information and features than an external version of a model published to
-your customers.
-
-A "projection" build is executed using only the ``buildscript`` classpath, and
-Smithy models are discovered using only the ``buildscript`` classpath. This
-prevents models discovered in the original model from appearing in the
-projected version of the model.
-
-The following example ``build.gradle.kts`` file will run a "projection"
-build that uses the "external" projection.
-
-.. code-block:: kotlin
-
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
-
-    buildscript {
-        repositories {
-            mavenLocal()
-            mavenCentral()
-        }
-        dependencies {
-            classpath("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
-
-            // Take a dependency on the internal model package. This
-            // dependency *must* be a buildscript only dependency to ensure
-            // that is does not appear in the generated JAR.
-            classpath("com.foo.baz:foo-internal-model:1.0.0")
-        }
-    }
-
-    // Use the "external" projection. This projection must be found in the
-    // smithy-build.json file. This also ensures that models found in the
-    // foo-internal-package that weren't filtered out are copied into the
-    // projection created by this package.
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        projection = "external"
-        projectionSourceTags = setOf("com.foo.baz:foo-internal-model")
-    }
-
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
-
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-
-        // Any dependencies that the projected model needs must be (re)declared
-        // here. For example, let's assume that the smithy-aws-traits package is
-        // needed in the projected model too.
-        implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
-    }
-
-
-Because the ``projection`` of the ``SmithyExtension`` was set to ``external``, a
-``smithy-build.json`` file **must** be found that defines the ``external``
-projection. For example:
-
-.. code-block:: json
-
-    {
-        "version": "1.0",
-        "projections": {
-            "external": {
-                "transforms": [
-                    {
-                        "name": "excludeShapesByTag",
-                        "args": {
-                            "tags": ["internal"]
-                        }
-                    },
-                    {
-                        "name": "excludeShapesByTrait",
-                        "args": {
-                            "traits": ["internal"]
-                        }
-                    },
-                    {
-                        "name": "excludeMetadata",
-                        "args": {
-                            "keys": ["suppressions", "validators"]
-                        }
-                    },
-                    {
-                        "name": "removeUnusedShapes"
-                    }
-                ]
-            }
-        }
-    }
-
-
-.. _projection-tags:
-
-Projection tags
----------------
-
-Projections are meant to *project* and filter other models into another
-model. You need to specify which dependencies are being projected into your
-JAR by setting the ``projectionSourceTags`` property.
-
-.. code-block:: kotlin
-
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-            projection = "external"
-            projectionSourceTags = setOf("Foo", "com.baz:bar")
-    }
-
-
-Tags are used to logically group packages to make it easier to build
-projections. The ``tags`` property is used to add ``Smithy-Tags`` to a JAR.
-
-.. code-block:: kotlin
-
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        tags = setOf("X", "foobaz-model")
-    }
-
-
-For example, if your service is made up of 10 packages, you can add the
-"foobaz-model" Smithy tag to each package so that the only value that needs
-to be provided for ``tags`` to correctly project your model is "foobaz-model".
-
-When building a model package, this plugin will automatically add the group
-name of the package being built, the group name + ":" + name of the package,
-and group name + ":" + name + ":" version. This allows models to always
-be queried by group and artifact names in addition to custom tags.
-
-
-.. _artifacts-from-smithy-models:
-
-Building artifacts from Smithy models
--------------------------------------
-
-If a ``smithy-build.json`` file is found at the root of the project, then it
-will be used to generate :ref:`artifacts <projection-artifacts>` from the Smithy model.
-
-The following example generates an OpenAPI model from a Smithy model:
-
-.. code-block:: json
-
-    {
-        "version": "1.0",
-        "plugins": {
-            "openapi": {
-                "service": "foo.baz#MyService"
-            }
-        }
-    }
-
-
-The above Smithy plugin also requires a ``buildscript`` dependency in
-``build.gradle.kts``:
-
-.. code-block:: kotlin
-
-    buildscript {
-        // ...
-        dependencies {
-            // ...
-
-            // This dependency is required in order to apply the "openapi"
-            // plugin in smithy-build.json
-            classpath("software.amazon.smithy:smithy-openapi:__smithy_version__")
-        }
-    }
-
-Complete Examples
-=================
-
-For several complete examples, see the `examples directory`_ of the Smithy
-Gradle plugin repository, or check out the :doc:`Quick start guide </quickstart>` for a tutorial on
-creating a Smithy model and building it with the Smithy Gradle plugin.
-
-.. _examples directory: https://github.com/awslabs/smithy-gradle-plugin/tree/main/examples
-.. _Smithy Gradle plugin: https://github.com/awslabs/smithy-gradle-plugin/
-.. _Gradle: https://gradle.org/
diff --git a/docs/source-2.0/guides/gradle-plugin/gradle-migration-guide.rst b/docs/source-2.0/guides/gradle-plugin/gradle-migration-guide.rst
new file mode 100644
index 000000000..293d7c6f4
--- /dev/null
+++ b/docs/source-2.0/guides/gradle-plugin/gradle-migration-guide.rst
@@ -0,0 +1,186 @@
+.. _gradle_migration_guide:
+
+==========================================
+Migrating to Gradle plugin version 0.10.0+
+==========================================
+
+This guide describes how to migrate your Gradle build files
+to use the `Smithy Gradle plugins`_ v0.10.0+ without breaking
+your existing Gradle projects.
+
+.. warning::
+    Versions 0.10.0+ of the Smithy Gradle plugins will automatically apply
+    the Smithy formatter to your Smithy source files. This can result in a
+    large number of formatting changes the first time you apply these plugins
+    to your project. For instructions on how to disable automatic formatting
+    see :ref:`Disabling the Smithy formatter <disable-smithy-formatter>`.
+
+Update plugin artifact
+======================
+
+The Smithy Gradle plugin has been broken up into separate capability
+(``smithy-jar``) and convention (``smithy-base``) plugins. The
+``smithy-jar`` plugin operates similarly to the previous ``smithy`` plugin,
+but no longer applies the ``java`` plugin automatically. To migrate to the
+new plugin, change the artifact id in the ``plugins`` block and apply the
+``java`` (or ``java-library``) plugin if it is not already applied to
+your project.
+
+.. tab:: Kotlin
+
+    .. code-block:: diff
+        :caption: build.gradle.kts
+
+        plugins {
+        -     id("software.amazon.smithy").version("0.7.0")
+        +     `java`
+        +     id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: diff
+        :caption: build.gradle
+
+        plugins {
+        -      id 'software.amazon.smithy' version '0.7.0'
+        +      id 'java'
+        +      id 'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+        }
+
+Remove Buildscript Dependencies
+===============================
+
+Pre-0.10.0 versions of the Smithy Gradle plugins use the buildscript block
+for build-only dependencies such as Smithy build plugins. The 0.10.0+
+Gradle plugins create a new Gradle `Configuration`_ ``smithyBuild``
+for these build-only dependencies.
+
+.. tab:: Kotlin
+
+    .. code-block:: diff
+        :caption: build.gradle.kts
+
+        -buildscript {
+        -       repositories {
+        -           mavenLocal()
+        -           mavenCentral()
+        -       }
+        -
+        -       dependencies {
+        -           // Dependency repeated here and below because model discovery previously
+        -           // only ran on buildscript classpath for projections
+        -           classpath("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+        -           // Take a dependency on the internal model package. This
+        -           // dependency *must* be a buildscript only dependency to ensure
+        -           // that is does not appear in the generated JAR.
+        -           classpath("com.foo.baz:foo-internal-model:1.0.0")
+        -       }
+        -}
+
+        dependencies {
+            implementation("software.amazon.smithy:smithy-model:__smithy_version__")
+
+            implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+        +   smithyBuild("com.foo.baz:foo-internal-model:1.0.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: diff
+        :caption: build.gradle
+
+        -buildscript {
+        -       repositories {
+        -           mavenLocal()
+        -           mavenCentral()
+        -       }
+        -
+        -       dependencies {
+        -           // Dependency repeated here and below because model discovery previously
+        -           // only ran on buildscript classpath for projections
+        -           classpath 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+        -           // Take a dependency on the internal model package. This
+        -           // dependency *must* be a buildscript only dependency to ensure
+        -           // that is does not appear in the generated JAR.
+        -           classpath 'com.foo.baz:foo-internal-model:1.0.0'
+        -       }
+        -}
+
+        dependencies {
+            implementation 'software.amazon.smithy:smithy-model:__smithy_version__'
+
+            implementation 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+        +   smithyBuild 'com.foo.baz:foo-internal-model:1.0.0'
+        }
+
+Change ``projection`` property name
+===================================
+
+The property ``projection`` has also been updated to ``sourceProjection``.
+
+.. tab:: Kotlin
+
+        .. code-block:: diff
+            :caption: build.gradle.kts
+
+            -configure<software.amazon.smithy.gradle.SmithyExtension> {
+            +smithy {
+            -    projection = "foo"
+            +    sourceProjection.set("foo")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: diff
+            :caption: build.gradle
+
+            -configure<software.amazon.smithy.gradle.SmithyExtension> {
+            +smithy {
+            -    projection = "foo"
+            +    sourceProjection = "foo"
+            }
+
+Change ``smithyBuildJar`` task name
+===================================
+
+The ``smithyBuildJar`` task has been removed. Instead, the plugin now
+executes separate ``smithyBuild`` and ``smithyJarStaging`` tasks. If
+your project previously configured the ``smithyBuildJar``,
+configure the ``smithyBuild`` task instead. Tasks that depended on
+``smithyBuildJar`` should now depend on the ``jar`` task.
+
+.. tab:: Kotlin
+
+        .. code-block:: diff
+            :caption: build.gradle.kts
+
+            tasks {
+            -   smithyBuildJar {
+            +   smithyBuild {
+                    smithyBuildConfigs.set(files("smithy-build.json", other))
+                }
+                // ..
+            }
+
+            -tasks["smithyBuildJar"].dependsOn("otherTask")
+            +tasks["jar"].dependsOn("otherTask")
+
+.. tab:: Groovy
+
+        .. code-block:: diff
+            :caption: build.gradle
+
+            tasks {
+            -   smithyBuildJar {
+            +   smithyBuild {
+                    smithyBuildConfigs = files("smithy-build.json", other)
+                }
+                // ..
+            }
+
+            -tasks["smithyBuildJar"].dependsOn("otherTask")
+            +tasks["jar"].dependsOn("otherTask")
+
+.. _Smithy Gradle plugins: https://github.com/awslabs/smithy-gradle-plugin/
+.. _Configuration: https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.Configuration.html
diff --git a/docs/source-2.0/guides/gradle-plugin/index.rst b/docs/source-2.0/guides/gradle-plugin/index.rst
new file mode 100644
index 000000000..4c6db0a8a
--- /dev/null
+++ b/docs/source-2.0/guides/gradle-plugin/index.rst
@@ -0,0 +1,760 @@
+.. _smithy-gradle-plugin:
+
+=====================
+Smithy Gradle Plugins
+=====================
+
+The `Smithy Gradle plugins`_ integrate Smithy with the `Gradle`_ build system. These plugin can
+build artifacts from Smithy models, and generate JARs for Smithy models and model
+:ref:`projections <projections>`.
+
+.. toctree::
+    :maxdepth: 1
+    :caption: Migrate to version 0.8.0+
+
+    gradle-migration-guide
+
+Plugins
+=======
+
+Two official `Gradle`_ plugins are available:
+
+* `smithy-base`_:
+    This plugin configures the `SourceSet`_'s and `Configuration`_'s
+    for a Smithy project. It also creates the base ``smithyBuild`` task for the project that
+    builds the Smithy models and build artifacts for the project.
+* `smithy-jar`_:
+    Adds Smithy files to an existing JAR created by a ``jar`` task (usually created
+    by the `Java plugin`_ or `Kotlin JVM plugin`_). The ``smithy-jar`` plugin also adds build metadata
+    and tags to the JAR's MANIFEST. The ``smithy-jar`` plugin applies the ``smithy-base`` plugin when
+    applied.
+
+.. admonition:: Which plugin should I use?
+    :class: note
+
+    Most users should use the ``smithy-jar`` plugin. If you are building shared model packages,
+    custom traits, custom linters, or codegen integrations you should use the ``smithy-jar`` plugin.
+    If you are writing a code generator you can use the ``smithy-base`` plugin to set up basic
+    `Configuration`_'s and provide access to Smithy gradle tasks.
+
+.. _plugin-apply:
+
+Applying Plugins
+----------------
+
+The Smithy Gradle plugins are applied using the ``plugins`` block of a gradle build script.
+The following example configures a project to use the ``smithy-base`` Gradle plugin:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        plugins {
+            id("software.amazon.smithy.gradle.smithy-base").version("0.10.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        plugins {
+            id 'software.amazon.smithy.gradle.smithy-base' version '0.10.0'
+        }
+
+
+.. _smithy-model-sources:
+
+.. note::
+
+    The ``smithy-jar`` plugin requires that a `jar` task be registered
+    before it is applied. The `Java Plugin`_ and `Kotlin JVM plugin`_
+    are commonly used plugins that both register a `jar` task.
+
+Complete Examples
+=================
+
+For several complete examples, see the `examples directory`_ of the `Smithy
+Gradle plugins`_ repository, or check out the :doc:`Quick start guide </quickstart>` for a tutorial on
+creating a Smithy model and building it with the Smithy Gradle plugin.
+
+The examples in from the `examples directory`_  can be copied into your workspace using
+the :ref:`Smithy CLI <smithy-cli>` ``init`` command as follows:
+
+.. code-block:: text
+
+    smithy init -t <EXAMPLE_NAME> -o <OUTPUT_DIRECTORY> --url https://github.com/smithy-lang/smithy-gradle-plugin
+
+You can list all examples available in the `Smithy Gradle plugins`_ repository with the following command:
+
+.. code-block:: text
+
+    smithy init --list --url https://github.com/smithy-lang/smithy-gradle-plugin
+
+.. _building-smithy-models:
+
+Building Smithy models
+======================
+
+The ``smithyBuild`` task that builds smithy models operates in two different modes:
+
+1. If no ``projection`` is specified for the ``SmithyExtension``, then the task
+   runs a "source" build using the "source" projection.
+2. If a ``projection`` is specified for the ``SmithyExtension``, then the task
+   runs in "projection" mode.
+
+.. _building-source-model:
+
+Building a source model
+-----------------------
+
+A "source" build is run when no ``sourceProjection`` is configured in
+``SmithyExtension``. To prevent accidentally relying on Smithy models
+that are only available as build dependencies, Smithy models are discovered
+using only the :ref:`model sources <model-sources>` and ``runtimeClasspath``.
+
+The following example ``build.gradle.kts`` will build a Smithy model using a
+"source" build:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        plugins {
+            `java-library`
+            id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+        }
+
+        // The SmithyExtension is used to customize the build. This example
+        // doesn't set any values and can be completely omitted.
+        smithy {}
+
+        repositories {
+            mavenLocal()
+            mavenCentral()
+        }
+
+        dependencies {
+            implementation("software.amazon.smithy:smithy-model:__smithy_version__")
+
+            // These are just examples of dependencies. This model has a dependency on
+            // a "common" model package and uses the external AWS traits.
+            implementation("com.foo.baz:foo-model-internal-common:1.0.0")
+            implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        plugins {
+            id 'java-library'
+            'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+        }
+
+        // The SmithyExtension is used to customize the build. This example
+        // doesn't set any values and can be completely omitted.
+        smithy {}
+
+        repositories {
+            mavenLocal()
+            mavenCentral()
+        }
+
+        dependencies {
+            implementation 'software.amazon.smithy:smithy-model:__smithy_version__'
+
+            // These are just examples of dependencies. This model has a dependency on
+            // a "common" model package and uses the external AWS traits.
+            implementation 'com.foo.baz:foo-model-internal-common:1.0.0'
+            implementation 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+        }
+
+.. _generating-projection:
+
+Generating a projection
+-----------------------
+
+A "projection" build is run when a :ref:`projection <projections>` is
+specified in the ``SmithyExtension``. You might create a projection of
+a model if you need to maintain an internal version of a model that contains
+more information and features than an external version of a model published to
+your customers.
+
+Any projected models should be added to the ``smithyBuild`` configuration. This
+prevents packages with projected models from appearing as dependencies of the
+projected version of the model.
+
+The following example gradle build script will run a "projection"
+build using the "external" projection.
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            plugins {
+                `java-library`
+                id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+            dependencies {
+                implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+
+                // Take a dependency on the internal model package. This
+                // dependency *must* be a smithyBuild dependency to ensure
+                // that is does not appear in the generated JAR.
+                smithyBuild("com.foo.baz:foo-internal-model:1.0.0")
+            }
+
+            smithy {
+                // Use the "external" projection. This projection must be found in the
+                // smithy-build.json file. This also ensures that models found in the
+                // foo-internal-package that weren't filtered out are copied into the
+                // projection created by this package.
+                sourceProjection.set("external")
+                projectionSourceTags.addAll("com.foo.baz:foo-internal-model")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            plugins {
+                id 'java-library'
+                id 'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+            dependencies {
+                implementation 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+
+                // Take a dependency on the internal model package. This
+                // dependency *must* be a smithyBuild dependency to ensure
+                // that is does not appear in the generated JAR.
+                smithyBuild 'com.foo.baz:foo-internal-model:1.0.0'
+            }
+
+            smithy {
+                // Use the "external" projection. This projection must be found in the
+                // smithy-build.json file. This also ensures that models found in the
+                // foo-internal-package that weren't filtered out are copied into the
+                // projection created by this package.
+                sourceProjection = "external"
+                projectionSourceTags += ["com.foo.baz:foo-internal-model"]
+            }
+
+Because the ``sourceProjection`` of the ``SmithyExtension`` was set to ``external``, a
+``smithy-build.json`` file **must** be found that defines the ``external``
+projection. For example:
+
+.. code-block:: json
+    :caption: smithy-build.json
+
+    {
+        "version": "1.0",
+        "projections": {
+            "external": {
+                "transforms": [
+                    {
+                        "name": "excludeShapesByTag",
+                        "args": {
+                            "tags": ["internal"]
+                        }
+                    },
+                    {
+                        "name": "excludeShapesByTrait",
+                        "args": {
+                            "traits": ["internal"]
+                        }
+                    },
+                    {
+                        "name": "excludeMetadata",
+                        "args": {
+                            "keys": ["suppressions", "validators"]
+                        }
+                    },
+                    {
+                        "name": "removeUnusedShapes"
+                    }
+                ]
+            }
+        }
+    }
+
+.. _projection-tags:
+
+Projection tags
+^^^^^^^^^^^^^^^
+
+Projections can *project* and filter shapes from dependencies into the output model.
+"Projecting" a dependency into your output JAR will include that model in the output
+sources of your project. Downstream consumers of your JAR will then no longer need to
+include the "projected" dependency as a dependency of their project to resolve the
+"projected" shapes. You need to specify which dependencies are being projected into
+your JAR by setting the ``projectionSourceTags`` property.
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            smithy {
+                sourceProjection.set("external")
+                projectionSourceTags.addAll("Foo", "com.baz:bar")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            smithy {
+                projection = "external"
+                projectionSourceTags += setOf("Foo", "com.baz:bar")
+            }
+
+
+Tags are used to logically group packages to make it easier to build
+projections. The ``tags`` property is used to add ``Smithy-Tags`` attribute
+to the JAR MANIFEST.
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            smithy {
+                tags.addAll("X", "foobaz-model")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            smithy {
+                tags += ["X", "foobaz-model"]
+            }
+
+
+For example, if your service is made up of 10 packages, you can add the
+"foobaz-model" Smithy tag to each package so that the only value that needs
+to be provided for ``tags`` to correctly project your model is "foobaz-model".
+
+When building a model package, the Smithy Gradle plugin will automatically add
+the group name of the package being built, the group name + ":" + name of the package,
+and group name + ":" + name + ":" version. This allows models to always
+be queried by group and artifact names in addition to custom tags.
+
+.. _artifacts-from-smithy-models:
+
+Building artifacts from Smithy models
+-------------------------------------
+
+If a :ref:`smithy-build.json <smithy-build>` file is found at the root of the project,
+then it will be used to generate :ref:`artifacts <projection-artifacts>` from the Smithy
+model. Smithy uses :ref:`build plugins <plugins>` to generate projection artifacts. To
+use a smithy build plugin with Gradle, first add it as a ``smithyBuild`` dependency in
+the Gradle build script. The plugin will then be discoverable and can be configured
+in the ``smithy-build.json`` file.
+
+For example, to generate a Typescript client from a Smithy model, add the
+``smithy-typescript-codegen`` package as a ``smithyBuild`` dependency:
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            dependencies {
+                // ...
+
+                // This dependency is required in order to apply the "typescript-client-codegen"
+                // plugin in smithy-build.json
+                smithyBuild("software.amazon.smithy.typescript:smithy-typescript-codegen:0.19.0")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            dependencies {
+                // ...
+
+                // This dependency is required in order to apply the "typescript-client-codegen"
+                // plugin in smithy-build.json
+                smithyBuild 'software.amazon.smithy.typescript:smithy-typescript-codegen:0.19.0'
+            }
+
+The plugin can then be configured in the ``smithy-build.json`` to generate a typescript client
+from a Smithy model:
+
+.. code-block:: json
+    :caption: smithy-build.json
+
+    {
+        "version": "1.0",
+        "plugins": {
+            "typescript-client-codegen": {
+                "package": "@smithy/typescript-client-codegen-example",
+                "packageVersion": "0.0.1",
+                "packageJson": {
+                    "license": "MIT"
+                }
+            }
+        }
+    }
+
+.. _gradle-plugin-configuration:
+
+Configuration
+=============
+
+.. _model-sources:
+
+Smithy model sources
+--------------------
+
+Smithy gradle plugins assume Smithy model files (``*.smithy``) are organized in a similar way as Java source files,
+in sourceSets. The smithy-base plugin adds a new sources block named smithy to every sourceSet.
+By default, this source block will include Smithy models in:
+
+* ``model/``
+* ``src/$sourceSetName/smithy``
+* ``src/$sourceSetName/resources/META-INF/smithy``
+
+New source directories can be added to a smithy sources block as follows:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        sourceSets {
+            main {
+                smithy {
+                    srcDir("includes")
+                }
+            }
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        sourceSets {
+            main {
+                smithy {
+                    srcDir 'includes'
+                }
+            }
+        }
+
+Models found in these directories are combined into a flattened directory
+structure and used to validate and build the Smithy model. A Smithy manifest
+file is automatically created for the detected models, and it, along with the
+model files, are placed in the ``META-INF/smithy/`` resource of the created
+JAR. Any project that depends on this created JAR will be able to find
+and use the Smithy models contained in the JAR when using *model discovery*.
+
+Dependencies
+------------
+
+The Smithy build plugins use two different configurations to search for dependencies
+such as shared models or :ref:`smithy build plugins <plugins>`:
+
+* ``runtimeClasspath``:
+    Runtime dependencies that will be required by any output JARS or publications.
+    For example, a shared model package or Java trait definition.
+* ``smithyBuild``:
+    Build dependencies that are not required by output models.
+
+Runtime dependencies can be added directly to the ``runtimeClasspath`` configuration
+or to a configuration that extends ``runtimeClasspath``, such as the ``implementation``
+configuration added by the ``java-library`` plugin.
+
+.. _dependencies:
+
+Build Dependencies
+^^^^^^^^^^^^^^^^^^
+
+The ``smithy-base`` plugin adds a ``smithyBuild`` `Configuration`_ that can be used to specify
+dependencies used when calling ``smithy build``. These dependencies will not be included in any output JARs or publications. They are akin to `compileOnly`
+dependencies for Java projects. Smithy build plugins and projected dependencies should be
+included in the `smithyBuild` configuration. For example:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        dependencies {
+            smithyBuild("com.example.software:build-only:1.0.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        dependencies {
+            smithyBuild 'com.example.software:build-only:1.0.0'
+        }
+
+.. _smithy-extension-properties:
+
+Smithy extension properties
+---------------------------
+
+The Smithy Gradle plugins are configured using the `SmithyExtension`_ extension:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            projection.set("foo")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            projection = "foo"
+        }
+
+
+This extension supports the following properties:
+
+.. list-table::
+    :header-rows: 1
+    :widths: 10 20 70
+
+    * - Property
+      - Type
+      - Description
+    * - format
+      - ``boolean``
+      - Flag indicating whether to format Smithy source files. By default, the :ref:`Smithy CLI <smithy-cli>` ``format``
+        command is executed on all source directories. This opinionated formatter follows the best practices
+        recommended by the Smithy team. It is possible to disable the formatter by setting this flag to ``false``.
+    * - smithyBuildConfigs
+      - ``FileCollection``
+      - Sets a custom collection of smithy-build.json files to use when building the model.
+    * - sourceProjection
+      - ``String``
+      - Sets the :ref:`projection <projections>` name to use as the source (primary) projection.
+        The smithy sources for this projection will be packaged in output JARs by the ``smithy-jar``
+        plugin. There must be a corresponding projection definition in the :ref:`smithy-build.json <smithy-build-json>`
+        file in the project. Defaults to ``"source"``.
+    * - projectionSourceTags
+      - ``Set<String>``
+      - Get the tags that are searched for in classpaths when determining
+        which models are projected into the created JAR. This plugin will look
+        through the JARs in the buildscript classpath to see if they contain a
+        META-INF/MANIFEST.MF attribute named "Smithy-Tags" that matches any of
+        the given projection source tags. The Smithy models found in each
+        matching JAR are copied into the JAR being projected. This allows a
+        projection JAR to aggregate models into a single JAR.
+    * - tags
+      - ``Set<String>``
+      - Set the tags that are added to the JAR. These tags are placed in the
+        META-INF/MANIFEST.MF attribute named "Smithy-Tags" as a comma
+        separated list. JARs with Smithy-Tags can be queried when building
+        projections so that the Smithy models found in each matching JAR are
+        placed into the projection JAR.
+    * - allowUnknownTraits
+      - ``boolean``
+      - Sets whether or not unknown traits in the model should be ignored. By
+        default, the build will fail if unknown traits are encountered.
+    * - fork
+      - ``boolean``
+      - By default, the CLI is run in the same process as Gradle,
+        but inside a thread with a custom class loader. This should
+        work in most cases, but there is an option to run inside a
+        process if necessary.
+    * - outputDirectory
+      - ``Directory``
+      - Defines where Smithy build artifacts are written.
+
+
+Customize output directory
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, Smithy build artifacts will be placed in the project build
+directory in a ``smithyprojections/`` directory. There are two ways to override
+the output directory. The first method is to set the ``outputDirectory`` property
+in the ``smithy-build.json`` config for your Smithy project. For example:
+
+.. code-block:: json
+    :caption: smithy-build.json
+
+    {
+        "outputDirectory": "build/output"
+    }
+
+The second method is to configure the plugin using the ``smithy`` extension:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            outputDirectory.set(file("path/to/output"))
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            outputDirectory = file("path/to/output")
+        }
+
+.. note::
+
+    Setting the output directory on the plugin extension will
+    override any ``outputDirectory`` property set in the
+    smithy-build config.
+
+Set ``smithy-build`` configs to use
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, the plugin will look for a file called ``smithy-build.json``
+at the project's root and will use that as the :ref:`smithy-build config <smithy-build>`
+for your project. If no smithy-build.json file is found, then an empty build config
+is used to build the project.
+
+Alternatively, you can explicitly configure one or more smithy-build configs to use for
+your project as follows:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            smithyBuildConfigs.set(files("smithy-build-config.json"))
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            smithyBuildConfigs = files("smithy-build-config.json")
+        }
+
+Set Smithy Tags to add to a JAR
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When the smithy-jar plugin is applied to a project, it can add a number of Smithy
+tags to the MANIFEST of a generated JAR. These tags can be used by downstream
+consumers to filter which models to include in projections. Tags can be configured
+for the plugin as follows:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            tags.addAll("tag1", "anotherTag", "anotherTag2")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            tags += ["tag1", "anotherTag", "anotherTag2"]
+        }
+
+Fork a new process when executing Smithy CLI commands
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, :ref:`Smithy CLI <smithy-cli>` commands are run in the
+same process as Gradle, but inside a thread with a custom class loader.
+This should work in most cases, but there is an option to run inside a
+process if necessary. To run Smithy CLI commands in a process set the
+``fork`` configuration option to true:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            fork.set(true)
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            fork = true
+        }
+
+.. _disable-smithy-formatter:
+
+Disable Smithy Formatter
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, the :ref:`Smithy CLI <smithy-cli>` ``format`` command is
+executed on all source directories. This opinionated formatter follows
+the best practices recommended by the Smithy team. It is possible to disable
+the formatter by setting the format setting on the plugin extension to false:
+
+.. tab:: Kotlin
+
+    .. code-block::
+        :caption: build.gradle.kts
+
+        smithy {
+            format.set(false)
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            format = false
+        }
+
+.. _examples directory: https://github.com/awslabs/smithy-gradle-plugin/tree/main/examples
+.. _Smithy Gradle plugins: https://github.com/awslabs/smithy-gradle-plugin/
+.. _Gradle: https://gradle.org/
+.. _smithy-base: https://github.com/smithy-lang/smithy-gradle-plugin#smithy-base-plugin
+.. _smithy-jar: https://github.com/smithy-lang/smithy-gradle-plugin#smithy-jar-plugin
+.. _Configuration: https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.Configuration.html
+.. _SourceSet: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.SourceSet.html
+.. _Java plugin: https://docs.gradle.org/current/userguide/java_plugin.html
+.. _Kotlin JVM plugin: https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm
+.. _SmithyExtension: https://github.com/smithy-lang/smithy-gradle-plugin/blob/main/smithy-base/src/main/java/software/amazon/smithy/gradle/SmithyExtension.java
diff --git a/docs/source-2.0/guides/index.rst b/docs/source-2.0/guides/index.rst
index dee6887df..a45214311 100644
--- a/docs/source-2.0/guides/index.rst
+++ b/docs/source-2.0/guides/index.rst
@@ -7,7 +7,7 @@ Guides
 
     smithy-cli/index
     smithy-build-json
-    gradle-plugin
+    gradle-plugin/index
     using-code-generation/index
     model-linters
     model-validation-examples
diff --git a/docs/source-2.0/guides/model-linters.rst b/docs/source-2.0/guides/model-linters.rst
index 50d600e82..dbefc55d5 100644
--- a/docs/source-2.0/guides/model-linters.rst
+++ b/docs/source-2.0/guides/model-linters.rst
@@ -33,7 +33,67 @@ use in Smithy models.
 Linters in ``smithy-linters``
 -----------------------------
 
-The ``smithy-linters`` package defines the following linters.
+For Java developers using Smithy's reference implementation, the following
+linters (except for UnreferencedShape) are defined in the  ``software.amazon.smithy:smithy-linters``
+Maven package.
+
+
+.. _UnreferencedShape:
+
+UnreferencedShape
+=================
+
+Emits when a shape is not connected to the rest of the model. If no
+configuration is provided, the linter will check if a shape is connected to
+the closure of any service shape. A selector can be provided to define a
+custom set of "root" shapes to customize how the linter determines if a shape
+is unreferenced. Shapes that are connected through the :ref:`idref-trait`
+are considered connected.
+
+Rationale
+    Just like unused variables in code, removing unused shapes from a model
+    makes the model easier to maintain.
+
+Default severity
+    ``NOTE``
+
+Configuration
+    .. list-table::
+       :header-rows: 1
+       :widths: 20 20 60
+
+       * - Property
+         - Type
+         - Description
+       * - rootShapeSelector
+         - ``string``
+         - A :ref:`selector <selectors>` that specifies the root shape(s)
+           from which to detect if other shapes are connected. Defaults
+           to "service", meaning any shape connected to any service shape
+           in the model is considered referenced.
+
+Example:
+
+.. code-block:: smithy
+
+    $version: "2"
+
+    metadata validators = [
+        {
+            // Find shapes that aren't connected to service shapes or shapes
+            // marked with the trait, smithy.example#myCustomTrait.
+            name: "UnreferencedShape"
+            configuration: {
+                rootShapeSelector: ":is(service, [trait|smithy.example#myCustomTrait])"
+            }
+        }
+    ]
+
+.. note::
+
+    For backward compatibility reasons, the ``UnreferencedShape`` validator is available
+    in ``software.amazon.smithy:smithy-model`` Maven package and does not require
+    additional dependencies.
 
 
 .. _AbbreviationName:
diff --git a/docs/source-2.0/quickstart.rst b/docs/source-2.0/quickstart.rst
index c99297472..ed0acf26b 100644
--- a/docs/source-2.0/quickstart.rst
+++ b/docs/source-2.0/quickstart.rst
@@ -93,10 +93,31 @@ weather service.
 4. This service closure contains the following operations:
    ``ListCities``, ``GetCity``, ``GetForecast``, ``GetCurrentTime``.
 
-``Weather`` is a :ref:`service` shape that is defined inside of a
-:ref:`namespace <namespaces>`.
+First, create a directory called `smithy-quickstart` with a `model` directory
+and a weather model file such that your `smithy-quickstart` directory has the
+following file structure:
+
+.. code-block:: text
+
+    smithy-quickstart/
+    └── model/
+        └── weather.smithy
+
+.. tip::
+
+    Run the following command to create the quickstart directory
+
+    .. code-block:: text
+
+        mkdir -p smithy-quickstart/model \
+        && touch smithy-quickstart/model/weather.smithy \
+        && cd smithy-quickstart
+
+Next, we will start to model a ``Weather`` service in the `weather.smithy` file.
+``Weather`` is a :ref:`service` shape that is defined inside of a :ref:`namespace <namespaces>`.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     $version: "2"
     namespace example.weather
@@ -129,6 +150,7 @@ A resource is contained within a service or another resource. Resources have
 identifiers, operations, and any number of child resources.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     $version: "2"
     namespace example.weather
@@ -163,6 +185,7 @@ Each ``City`` has a single ``Forecast``. This can be defined by adding the
 ``Forecast`` to the ``resources`` property of the ``City``.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     resource City {
         identifiers: { cityId: CityId }
@@ -189,6 +212,7 @@ members of resource operations map to resource properties or identifiers to
 perform updates on or examine the state of a resource.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     resource City {
         identifiers: { cityId: CityId }
@@ -244,6 +268,7 @@ your API.
 Let's define the operation used to "read" a ``City``.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     @readonly
     operation GetCity {
@@ -291,6 +316,7 @@ Let's define the operation used to "read" a ``City``.
 And define the operation used to "read" a ``Forecast``.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     @readonly
     operation GetForecast {
@@ -332,6 +358,7 @@ bind the identifier of a ``City`` to its input structure; we are listing
 cities, so there's no way we could provide a ``City`` identifier.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     /// Provides weather forecasts.
     @paginated(inputToken: "nextToken", outputToken: "nextToken",
@@ -413,6 +440,7 @@ service.
 
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     /// Provides weather forecasts.
     @paginated(inputToken: "nextToken", outputToken: "nextToken",
@@ -442,51 +470,90 @@ service.
 Building the Model
 ==================
 
-Now that you have a model, you'll want to build it and generate things from it.
-Building the model creates projections of the model, applies plugins to
-generate artifacts, runs validation, and creates a JAR that contains the
-filtered projection. The `Smithy Gradle Plugin`_ is the best way to get started
-building a Smithy model. First, create a ``smithy-build.json`` file:
+Now that you have a model, you'll want to build it and generate additional
+artifacts from it. Building the model creates projections of the model, applies plugins to
+generate artifacts, and runs validation.
 
-.. code-block:: json
+.. tab:: Smithy CLI
 
-    {
-        "version": "1.0"
-    }
+    .. admonition:: Install required tools
+        :class: tip
 
-Then create a new ``build.gradle.kts`` file:
+        Before you proceed, make sure you have the :ref:`Smithy CLI installed <cli_installation>`.
 
-.. code-block:: kotlin
+    To build a Smithy model using the :ref:`the Smithy CLI <smithy-cli>`,
+    create a :ref:`smithy-build.json <smithy-build-json>` file in the ``smithy-quickstart`` directory:
 
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
+    .. code-block:: json
+        :caption: smithy-build.json
 
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
+        {
+            // Version of the smithy-build.json file specification
+            "version": "1.0",
+            // Location to search for Smithy model source files
+            "sources": ["model"]
+        }
 
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-    }
+    Next, run ``smithy build``. That's it! We just created a simple, read-only, ``Weather`` service.
 
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        // Uncomment this to use a custom projection when building the JAR.
-        // projection = "foo"
-    }
+.. tab:: Gradle
+
+    .. admonition:: Install required tools
+        :class: tip
+
+        Before you proceed, make sure you have `gradle installed`_.
+
+
+    To build a Smithy model using the :ref:`Smithy Gradle Plugin <smithy-gradle-plugin>`,
+    first, create a gradle build script file in the ``smithy-quickstart`` directory:
+
+    .. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            plugins {
+                `java-library`
+                id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+            }
 
-    // Uncomment to disable creating a JAR.
-    //tasks["jar"].enabled = false
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+    .. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            plugins {
+                id 'java-library'
+                id 'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+    Next, create a :ref:`smithy-build.json <smithy-build-json>` file in the
+    ``smithy-quickstart`` directory:
+
+    .. code-block:: json
+        :caption: smithy-build.json
+
+        {
+            // Version of the smithy-build.json file specification
+            "version": "1.0"
+        }
+
+    Finally, run ``gradle build``. That's it! We just created a simple, read-only, ``Weather`` service.
 
-Finally, copy the weather model to ``model/weather.smithy`` and
-then run ``gradle build`` (make sure you have `gradle installed`_).
 
 Next steps
 ==========
 
-That's it! We just created a simple, read-only, ``Weather`` service.
-
 1. Try adding a "create" lifecycle operation to ``City``.
 2. Try adding a "delete" lifecycle operation to ``City``.
 3. Try adding :ref:`HTTP binding traits <http-traits>` to the API.
@@ -511,49 +578,47 @@ There's plenty more to explore in Smithy.
 Complete example
 ================
 
-If you followed all the steps in this guide, you should have three files, laid
-out like so::
 
-    .
-    ├── build.gradle.kts
-    ├── model
-    │   └── weather.smithy
-    └── smithy-build.json
 
-The ``smithy-build.json`` should have the following contents:
+.. note::
 
-.. code-block:: json
+    You can clone a working version of this quickstart example using the
+    :ref:`Smithy CLI <smithy-cli>` ``init`` command.
 
-    {
-        "version": "1.0"
-    }
+    .. tab:: Quickstart with Smithy CLI
 
-The ``build.gradle.kts`` should have the following contents:
+        .. code-block::
 
-.. code-block:: kotlin
+            smithy init -o <output_directory>
 
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
+    .. tab:: Quickstart with Gradle
 
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
+        .. code-block::
 
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-    }
+            smithy init -t quickstart-gradle -o <output_directory>
 
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        // Uncomment this to use a custom projection when building the JAR.
-        // projection = "foo"
-    }
+If you followed all the steps in this guide, your working directory should be laid out like so:
+
+.. tab:: Smithy CLI
+
+    .. code-block:: text
+
+        .
+        ├── smithy-build.json
+        └── model
+            └── weather.smithy
+
+.. tab:: Gradle
+
+    .. code-block:: text
 
-    // Uncomment to disable creating a JAR.
-    //tasks["jar"].enabled = false
+        .
+        ├── build.gradle.kts
+        ├── smithy-build.json
+        └── model
+            └── weather.smithy
 
-Finally, the complete ``weather.smithy`` model should look like:
+The complete ``weather.smithy`` model should look like:
 
 .. code-block:: smithy
 
diff --git a/smithy-build/build.gradle b/smithy-build/build.gradle
index 7b47e4dcc..20a502b25 100644
--- a/smithy-build/build.gradle
+++ b/smithy-build/build.gradle
@@ -24,4 +24,7 @@ ext {
 dependencies {
     api project(":smithy-utils")
     api project(":smithy-model")
+
+    // Allows testing of annotation processor
+    testImplementation "com.google.testing.compile:compile-testing:0.21.0"
 }
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/processor/SmithyAnnotationProcessor.java b/smithy-build/src/main/java/software/amazon/smithy/build/processor/SmithyAnnotationProcessor.java
new file mode 100644
index 000000000..6eeb05f2c
--- /dev/null
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/processor/SmithyAnnotationProcessor.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.build.processor;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.lang.annotation.Annotation;
+import java.net.URL;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.tools.Diagnostic;
+import javax.tools.StandardLocation;
+import software.amazon.smithy.build.SmithyBuild;
+import software.amazon.smithy.build.SmithyBuildResult;
+import software.amazon.smithy.build.model.SmithyBuildConfig;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.loader.ModelAssembler;
+import software.amazon.smithy.model.loader.ModelDiscovery;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.utils.IoUtils;
+
+/**
+ * Base implementation of a Smithy annotation processor.
+ *
+ * <p>This implementation can be extended to create an Annotation processor for
+ * a Smithy build plugin that generates Java code. The processor will execute
+ * <strong>a single build plugin</strong> and write any generated Java code created
+ * by the plugin to the correct output path.
+ *
+ * <p>Smithy models on the classpath of extending annotation processors are automatically
+ * discovered and any java artifacts generated by this plugin are written to standard locations.
+ * Generated resource files are written to the class output while all other generated Java
+ * classes are written to the source output. Plugin artifacts that are not java files or
+ * resource files under {@code META-INF/} are ignored.
+ *
+ * <p>Extending classes must specify the name of the build plugin they will execute using
+ * {@link #getPluginName()}. This plugin must be discoverable on the annotation processor classpath.
+ * The plugin is then executed using the data from the annotation as the plugin configuration.
+ *
+ * <p>For example, if your plugin has the following configuration node in a `smithy-build.json` file:
+ * <pre>
+ *     "myPlugin" : {
+ *         "packageName" = "myPackage.namespace",
+ *         "listOfTags" = ["a", "b", "c"]
+ *     }
+ * </pre>
+ * Then you would define an annotation like the following:
+ * <pre>
+ * {@code
+ *  @Target(ElementType.PACKAGE)
+ *  public @interface MyPlugin {
+ *     String[] listOfTags();
+ *  }
+ * }
+ * </pre>
+ * The {@link #createPluginNode} method is used to map this annotation to the object node
+ * expected by the plugin. Once a mapping has been defined for the annotation, it can be
+ * added to the {@code package-info.java} as follows:
+ * <pre>
+ * {@code
+ * @MyPlugin(listOfTags = {"a", "b", "c"})
+ * package com.example.traitcodegen;
+ *
+ * import com.example.annotations.MyPlugin;
+ * }
+ * </pre>
+ * The base processor class will discover the namespace of the package the annotation
+ * is applied to and pass that as a parameter to the {@link #createPluginNode} method.
+ *
+ * @param <A> Annotation to execute the processor for. This annotation should
+ *            target the {@code PACKAGE} {@link java.lang.annotation.ElementType} and
+ *            should be applied in the {@code package-info.java} file of a package.
+ */
+public abstract class SmithyAnnotationProcessor<A extends Annotation> extends AbstractProcessor {
+    private static final String MANIFEST_PATH = "META-INF/smithy/manifest";
+    private Messager messager;
+    private Filer filer;
+
+    @Override
+    public synchronized void init(ProcessingEnvironment processingEnv) {
+        super.init(processingEnv);
+        messager = processingEnv.getMessager();
+        filer = processingEnv.getFiler();
+    }
+
+    @Override
+    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(getAnnotationClass());
+        if (elements.size() > 1) {
+            messager.printMessage(Diagnostic.Kind.ERROR,
+                    "Only one package can have the " + getAnnotationClass() + " annotation.");
+        } else if (elements.size() == 1) {
+            messager.printMessage(Diagnostic.Kind.NOTE,
+                    "Executing processor: " + this.getClass().getSimpleName() + "...");
+            String packageName = getPackageName(elements.iterator().next());
+            SmithyBuildConfig config = createBuildConfig(getAnnotation(elements), packageName);
+            executeSmithyBuild(config).allArtifacts()
+                    .filter(path -> path.toString().contains(getPluginName()) && path.toString().contains("source"))
+                    .forEach(this::writeArtifact);
+        }
+
+        // Always return false to ensure the annotation processor does not claim the annotation.
+        return false;
+    }
+
+    /**
+     * Name of the Smithy build plugin to execute with this annotation processor.
+     *
+     * @return name of plugin to run.
+     */
+    protected abstract String getPluginName();
+
+    /**
+     * Annotation class for the processor.
+     * <p>
+     * Each implementation of {@code SmithyProcessor} should have a specific package-scoped annotation
+     * used for configuration. {@link #createPluginNode(Annotation)} maps this annotation to the
+     * configuration node for the plugin specified by {@link #getPluginName()}.
+     *
+     * @return class of the annotation used by this processor
+     */
+    protected abstract Class<A> getAnnotationClass();
+
+    /**
+     * Maps annotation data to a plugin configuration node.
+     *
+     * @param annotation instance of generator annotation to use to create the build config.
+     * @return ObjectNode to use as plugin configuration node.
+     */
+    protected abstract ObjectNode createPluginNode(A annotation, String packageName);
+
+    private SmithyBuildConfig createBuildConfig(A annotation, String packageName) {
+        Map<String, ObjectNode> pluginMap = new HashMap<>();
+        pluginMap.put(getPluginName(), createPluginNode(annotation, packageName));
+        return SmithyBuildConfig.builder().version("1.0").plugins(pluginMap).build();
+    }
+
+    private SmithyBuildResult executeSmithyBuild(SmithyBuildConfig config) {
+        ModelAssembler assembler = Model.assembler();
+
+        // Discover any models on the annotation processor classpath
+        assembler.discoverModels(getClass().getClassLoader());
+
+        // Load any models found in the resources standard location
+        ModelDiscovery.findModels(getManifestUrl()).forEach(assembler::addImport);
+
+        SmithyBuild smithyBuild = SmithyBuild.create(getClass().getClassLoader());
+        smithyBuild.model(assembler.assemble().unwrap());
+        smithyBuild.config(config);
+
+        return smithyBuild.build();
+    }
+
+    private void writeArtifact(Path path) {
+        String pathStr = path.toString();
+        final String outputPath = pathStr.substring(pathStr.lastIndexOf(getPluginName())
+                + getPluginName().length() + 1);
+        try {
+            // Resources are written to the class output
+            if (outputPath.startsWith("META-INF")) {
+                try (Writer writer = filer
+                        .createResource(StandardLocation.CLASS_OUTPUT, "", convertOutputPath(outputPath))
+                        .openWriter()
+                ) {
+                    writer.write(IoUtils.readUtf8File(path));
+                }
+                // All other Java files are written to the source output
+            } else if (outputPath.endsWith(".java")) {
+                // The filer needs to use a namespace convention of `.` rather than the default separator for paths.
+                String javaPath = outputPath.replace(FileSystems.getDefault().getSeparator(), ".")
+                        .substring(0, outputPath.lastIndexOf(".java"));
+                try (Writer writer = filer.createSourceFile(javaPath).openWriter()) {
+                    writer.write(IoUtils.readUtf8File(path));
+                }
+            } else {
+                // Non-java files generated are ignored.
+                messager.printMessage(Diagnostic.Kind.NOTE, "Ignoring generated file: " + outputPath);
+            }
+        } catch (IOException exc) {
+            throw new UncheckedIOException(exc);
+        }
+    }
+
+    private A getAnnotation(Set<? extends Element> elements) {
+        return elements.stream()
+                .findFirst()
+                .map(element -> element.getAnnotation(getAnnotationClass()))
+                .orElseThrow(() -> new IllegalStateException("No annotation of type "
+                        + getAnnotationClass() + " found on element."));
+    }
+
+    private URL getManifestUrl() {
+        try {
+            return filer.getResource(StandardLocation.CLASS_PATH, "", MANIFEST_PATH).toUri().toURL();
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    // This is necessary to convert windows paths to a valid URI
+    private String convertOutputPath(String outputPath) {
+        return outputPath.replace(FileSystems.getDefault().getSeparator(), "/");
+    }
+
+    private String getPackageName(Element element) {
+        if (element instanceof PackageElement) {
+            PackageElement packageElement = (PackageElement) element;
+            return packageElement.getQualifiedName().toString();
+        } else {
+            throw new IllegalStateException("Expected Annotation to be applied to package element "
+                    + "but found : " + element);
+        }
+    }
+}
diff --git a/smithy-cli/build.gradle b/smithy-cli/build.gradle
index dcfb805ad..e3c64f55d 100644
--- a/smithy-cli/build.gradle
+++ b/smithy-cli/build.gradle
@@ -20,6 +20,7 @@ import java.nio.file.Paths
 plugins {
     id "application"
     id "org.beryx.runtime" version "1.12.7"
+    id "org.jreleaser" version "1.10.0"
 }
 
 description = "This module implements the Smithy command line interface."
@@ -163,7 +164,7 @@ runtime {
         // Using the Gradle wrapper script as-is results in a huge startup penalty, so I instead updated parts of the
         // script that didn't affect performance, and kept others that did. Namely, the set and eval code of the Gradle
         // startup script is significantly slower than what was used by the plugin.
-        unixScriptTemplate = file('config/unixStartScript.txt')
+        unixScriptTemplate = file('configuration/unixStartScript.txt')
         jvmArgs = [
             // Disable this when attempting to profile the CLI. In 99% of use cases this isn't not necessary.
             '-XX:-UsePerfData',
@@ -191,13 +192,16 @@ runtime {
     targetPlatform("windows-x64") {
         jdkHome = jdkDownload("${correttoRoot}-x64-windows-jdk.zip")
     }
+
+    // Because we're using target-platforms, it will use this property as a prefix for each target zip
+    imageZip = file("$buildDir/image/smithy-cli.zip")
 }
 
-// Add finishing touches to the distributables, such as an install script
+// Add finishing touches to the distributables, such as an install script, before it gets zipped
 tasks.runtime.doLast {
     targetPlatforms.each { targetPlatform ->
         copy {
-            from "config"
+            from "configuration"
             include targetPlatform.value.name.contains("windows") ? "install.bat" : "install"
             into Paths.get(
                     "${project.buildDir}", "image", "smithy-cli-${targetPlatform.value.name}").toString()
@@ -229,22 +233,101 @@ sourceSets {
     }
 }
 
-task integ(type: Test) {
-    useJUnitPlatform()
-    systemProperty "SMITHY_BINARY", "${smithyBinary}"
-    testClassesDirs = sourceSets["it"].output.classesDirs
-    classpath = sourceSets["it"].runtimeClasspath
+// ------ Setup Jreleaser -------
+tasks.assembleDist.doFirst {
+    // This is a workaround for a weird behavior.
+    // https://github.com/jreleaser/jreleaser/issues/1292
+    mkdir "$buildDir/jreleaser"
+}
+
+jreleaser {
+    gitRootSearch = true
 
-    // Configuration parameters to execute top-level classes in parallel but methods in same thread
-    systemProperties["junit.jupiter.execution.parallel.enabled"] = "true"
-    systemProperties["junit.jupiter.execution.parallel.mode.default"] = "same_thread"
-    systemProperties["junit.jupiter.execution.parallel.mode.classes.default"] = "concurrent"
 
-    testLogging {
-        events = ["passed", "skipped", "failed"]
-        exceptionFormat = "full"
+    project {
+        website = 'https://smithy.io'
+        authors = ['Smithy']
+        vendor = "Smithy"
+        license = 'Apache-2.0'
+        description = "Smithy CLI - A CLI for building, validating, querying, and iterating on Smithy models"
+        copyright = "2019"
+    }
+
+    checksum {
+        individual = true
+        files = false
+    }
+
+    release {
+        github {
+            overwrite = true
+            tagName = '{{projectVersion}}'
+            changelog {
+                // For now, we won't have a changelog added to the release. In the future, we could create a changelog-snippet
+                // from the real changelog as part of a command hook prior to the release step
+                enabled = false
+            }
+            commitAuthor {
+                name = "smithy-automation"
+                email = "github-smithy-automation@amazon.com"
+            }
+        }
+    }
+
+    files {
+        active = "ALWAYS"
+        artifact {
+            // We'll include the VERSION file in the release artifacts so that the version can be easily
+            // retrieving by hitting the GitHub `releases/latest` url 
+            path = "../VERSION"
+            extraProperties.put('skipSigning', true)
+        }
+    }
+
+    platform {
+        // These replacements are for the names of files that are released, *not* for names within this build config
+        replacements = [
+            'osx': 'darwin',
+            'aarch_64': 'aarch64',
+            'windows_x86_64': 'windows_x64'
+        ]
     }
-}
 
-// Runtime images need to be created before integration tests can run.
-tasks["integ"].dependsOn("runtime")
+    distributions {
+        smithy {
+            distributionType = 'JLINK'
+            stereotype = 'CLI'
+
+            artifact {
+                path = "build/image/smithy-cli-linux-x86_64.zip"
+                platform = "linux-x86_64"
+            }
+
+            artifact {
+                path = "build/image/smithy-cli-linux-aarch64.zip"
+                platform = "linux-aarch_64"
+            }
+
+            artifact {
+                path = "build/image/smithy-cli-darwin-x86_64.zip"
+                platform = "osx-x86_64"
+            }
+
+            artifact {
+                path = "build/image/smithy-cli-darwin-aarch64.zip"
+                platform = "osx-aarch_64"
+            }
+
+            artifact {
+                path = "build/image/smithy-cli-windows-x64.zip"
+                platform = "windows-x86_64"
+            }
+        }
+    }
+
+    signing {
+        active = "RELEASE"
+        armored = true
+        verify = true
+    }
+}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java
index e6567e1ae..9ff05b87f 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java
@@ -67,6 +67,7 @@ final class DiffCommand implements Command {
         arguments.addReceiver(new ConfigOptions());
         arguments.addReceiver(new ValidatorOptions());
         arguments.addReceiver(new BuildOptions());
+        arguments.addReceiver(new ValidationEventFormatOptions());
         arguments.addReceiver(new Options());
         arguments.getReceiver(BuildOptions.class).noPositionalArguments(true);
 
@@ -314,7 +315,7 @@ final class DiffCommand implements Command {
                     .config(config)
                     .arguments(arguments)
                     .env(env)
-                    .validationPrinter(env.stderr())
+                    .validationPrinter(env.stdout())
                     // Only report issues that fail the build.
                     .validationMode(Validator.Mode.QUIET_CORE_ONLY)
                     .defaultSeverity(Severity.DANGER);
@@ -326,6 +327,7 @@ final class DiffCommand implements Command {
                     .models(models)
                     .titleLabel("NEW", ColorTheme.DIFF_EVENT_TITLE)
                     .config(config)
+                    .disableOutputFormatFraming(true) // don't repeat things like CSV headers.
                     .build();
         }
 
@@ -337,6 +339,7 @@ final class DiffCommand implements Command {
                     .titleLabel("DIFF", ColorTheme.DIFF_TITLE)
                     .validatedResult(new ValidatedResult<>(newModel, events))
                     .defaultSeverity(null) // reset so it takes on standard option settings.
+                    .disableOutputFormatFraming(true) // don't repeat things like CSV headers.
                     .build();
         }
 
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java
index de67ab126..fc9ef35e7 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java
@@ -61,10 +61,31 @@ final class ModelBuilder {
     private ValidatedResult<Model> validatedResult;
     private String titleLabel;
     private Style[] titleLabelStyles;
+    private ValidationEventFormatOptions.Format validationOutputFormat;
+    private boolean disableOutputFormatFraming = false;
     private boolean disableConfigModels;
 
     public ModelBuilder arguments(Arguments arguments) {
         this.arguments = arguments;
+
+        // Determine how to format the output, whether it's text (the default) or CSV.
+        // Only some commands (like validate) actually let you customize the output format, so assume a default.
+        if (validationOutputFormat == null) {
+            validationOutputFormat(arguments.hasReceiver(ValidationEventFormatOptions.class)
+                    ? arguments.getReceiver(ValidationEventFormatOptions.class).format()
+                    : ValidationEventFormatOptions.Format.TEXT);
+        }
+
+        return this;
+    }
+
+    public ModelBuilder disableOutputFormatFraming(boolean disableOutputFormatFraming) {
+        this.disableOutputFormatFraming = disableOutputFormatFraming;
+        return this;
+    }
+
+    public ModelBuilder validationOutputFormat(ValidationEventFormatOptions.Format validationOutputFormat) {
+        this.validationOutputFormat = validationOutputFormat;
         return this;
     }
 
@@ -180,14 +201,22 @@ final class ModelBuilder {
                 .titleLabel(titleLabel, titleLabelStyles)
                 .build();
 
+        if (!disableOutputFormatFraming) {
+            validationOutputFormat.beginPrinting(validationPrinter);
+        }
+
         for (ValidationEvent event : sortedEvents) {
             // Only log events that are >= --severity. Note that setting --quiet inherently
             // configures events to need to be >= DANGER. Also filter using --show-validators and --hide-validators.
             if (validatorOptions.isVisible(event)) {
-                validationPrinter.println(formatter.format(event));
+                validationOutputFormat.print(validationPrinter, formatter, event);
             }
         }
 
+        if (!disableOutputFormatFraming) {
+            validationOutputFormat.endPrinting(validationPrinter);
+        }
+
         env.flush();
         // Note: disabling validation will still show a summary of failures if the model can't be loaded.
         Validator.validate(validationMode != Validator.Mode.ENABLE, colors, stderr, validatedResult);
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java
index 34c7ea5ea..aca6d6a49 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java
@@ -48,6 +48,7 @@ final class ValidateCommand implements Command {
         arguments.addReceiver(new DiscoveryOptions());
         arguments.addReceiver(new ValidatorOptions());
         arguments.addReceiver(new BuildOptions());
+        arguments.addReceiver(new ValidationEventFormatOptions());
 
         CommandAction action = HelpActionWrapper.fromCommand(
             this,
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidationEventFormatOptions.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidationEventFormatOptions.java
new file mode 100644
index 000000000..9eb3c6d94
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidationEventFormatOptions.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import java.util.function.Consumer;
+import software.amazon.smithy.cli.ArgumentReceiver;
+import software.amazon.smithy.cli.CliError;
+import software.amazon.smithy.cli.CliPrinter;
+import software.amazon.smithy.cli.HelpPrinter;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationEventFormatter;
+
+final class ValidationEventFormatOptions implements ArgumentReceiver {
+
+    private static final String VALIDATION_FORMAT = "--format";
+
+    enum Format {
+        TEXT {
+            @Override
+            void print(CliPrinter printer, ValidationEventFormatter formatter, ValidationEvent event) {
+                printer.println(formatter.format(event));
+            }
+        },
+
+        CSV {
+            @Override
+            void beginPrinting(CliPrinter printer) {
+                printer.println("severity,id,shape,file,line,column,message,hint,suppressionReason");
+            }
+
+            @Override
+            void print(CliPrinter printer, ValidationEventFormatter formatter, ValidationEvent event) {
+                printer.println(
+                        String.format("\"%s\",\"%s\",\"%s\",\"%s\",%d,%d,\"%s\",\"%s\",\"%s\"",
+                                      event.getSeverity().toString(),
+                                      formatCsv(event.getId()),
+                                      event.getShapeId().map(ShapeId::toString).orElse(""),
+                                      formatCsv(event.getSourceLocation().getFilename()),
+                                      event.getSourceLocation().getLine(),
+                                      event.getSourceLocation().getColumn(),
+                                      formatCsv(event.getMessage()),
+                                      formatCsv(event.getHint().orElse("")),
+                                      formatCsv(event.getSuppressionReason().orElse(""))));
+            }
+        };
+
+        void beginPrinting(CliPrinter printer) {}
+
+        abstract void print(CliPrinter printer, ValidationEventFormatter formatter, ValidationEvent event);
+
+        void endPrinting(CliPrinter printer) {}
+
+        private static String formatCsv(String value) {
+            // Replace DQUOTE with DQUOTEDQUOTE, escape newlines, and escape carriage returns.
+            return value.replace("\"", "\"\"").replace("\n", "\\n").replace("\r", "\\r");
+        }
+    }
+
+    private Format format = Format.TEXT;
+
+    @Override
+    public void registerHelp(HelpPrinter printer) {
+        printer.param(VALIDATION_FORMAT, null, "text|csv",
+                      "Specifies the format to write validation events (text or csv). Defaults to text.");
+    }
+
+    @Override
+    public Consumer<String> testParameter(String name) {
+        if (name.equals(VALIDATION_FORMAT)) {
+            return s -> {
+                switch (s) {
+                    case "csv":
+                        format(Format.CSV);
+                        break;
+                    case "text":
+                        format(Format.TEXT);
+                        break;
+                    default:
+                        throw new CliError("Unexpected " + VALIDATION_FORMAT + ": `" + s + "`");
+                }
+            };
+        }
+        return null;
+    }
+
+    void format(Format format) {
+        this.format = format;
+    }
+
+    Format format() {
+        return format;
+    }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyMixin.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyMixin.java
index fb6f6eaed..647f86b2b 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyMixin.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyMixin.java
@@ -99,22 +99,25 @@ final class ApplyMixin implements ShapeModifier {
                     }
                 }
             }
-            if (introducedMember == null && memberBuilders.containsKey(member.getMemberName())) {
-                MemberShape.Builder original = memberBuilders.get(member.getMemberName());
-                introducedMember = original.addMixin(member).build();
-                if (!introducedMember.getTarget().equals(member.getTarget())) {
-                    mixinMemberConflict(original, member);
+
+            if (introducedMember == null) {
+                if (memberBuilders.containsKey(member.getMemberName())) {
+                    MemberShape.Builder original = memberBuilders.get(member.getMemberName());
+                    introducedMember = original.addMixin(member).build();
+                    if (!introducedMember.getTarget().equals(member.getTarget())) {
+                        mixinMemberConflict(original, member);
+                    }
+                } else if (!introducedTraits.isEmpty()) {
+                    // Build local member copies before adding mixins if traits
+                    // were introduced to inherited mixin members.
+                    introducedMember = MemberShape.builder()
+                            .id(targetId)
+                            .target(member.getTarget())
+                            .source(member.getSourceLocation())
+                            .addTraits(introducedTraits.values())
+                            .addMixin(member)
+                            .build();
                 }
-            } else if (!introducedTraits.isEmpty()) {
-                // Build local member copies before adding mixins if traits
-                // were introduced to inherited mixin members.
-                introducedMember = MemberShape.builder()
-                        .id(targetId)
-                        .target(member.getTarget())
-                        .source(member.getSourceLocation())
-                        .addTraits(introducedTraits.values())
-                        .addMixin(member)
-                        .build();
             }
 
             if (introducedMember != null) {
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java
index d6128d045..bcd30b909 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java
@@ -237,22 +237,16 @@ final class IdlStringLexer {
     /**
      * Computes the number of leading whitespace characters in a string.
      *
-     * <p>This method returns -1 if the string is empty or if the string
-     * contains only whitespace. When determining the whitespace of the
-     * last line, the length of the line is returned if the entire
-     * line consists of only spaces. This ensures that the whitespace
-     * of the last line can be used to influence the indentation of the
-     * content as a whole (a significant trailing line policy).
+     * <p>This method returns -1 if the string contains only whitespace. When determining the whitespace of the
+     * last line, the length of the line is returned if the entire line consists of only spaces. This ensures that
+     * the whitespace of the last line can be used to influence the indentation of the content as a whole
+     * (a significant trailing line policy).
      *
      * @param line Line to search for whitespace.
      * @param isLastLine Whether or not this is the last line.
      * @return Returns the last whitespace index starting at 0 or -1.
      */
     private static int computeLeadingWhitespace(CharSequence line, boolean isLastLine) {
-        if (line.length() == 0) {
-            return -1;
-        }
-
         for (int offset = 0; offset < line.length(); offset++) {
             if (line.charAt(offset) != ' ') {
                 return offset;
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java
index e412f885a..701da61e5 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java
@@ -21,31 +21,49 @@ import java.util.function.Predicate;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.knowledge.NeighborProviderIndex;
 import software.amazon.smithy.model.loader.Prelude;
+import software.amazon.smithy.model.selector.Selector;
 import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.traits.TraitDefinition;
 import software.amazon.smithy.utils.FunctionalUtils;
 
 /**
- * Finds shapes that are not connected to a service shape, are not trait
- * definitions, are not referenced by trait definitions, and are not
- * referenced in trait values through
+ * Finds shapes that are not connected to a "root" shape, are not trait definitions, are not referenced by trait
+ * definitions, and are not referenced in trait values through
  * {@link software.amazon.smithy.model.traits.IdRefTrait}.
  *
+ * <p>The "root" shapes defaults to all service shapes in the model. You can customize this by providing a selector
+ * that considers every matching shape a root shape. For example, a model might consider all shapes marked with
+ * a trait called "root" to be a root shape.
+ *
  * <p>Prelude shapes are never considered unreferenced.
  */
 public final class UnreferencedShapes {
 
+    private static final Selector SERVICE_SHAPES = Selector.parse("service");
+
+    private final Selector rootShapeSelector;
     private final Predicate<Shape> keepFilter;
 
+    /**
+     * @param keepFilter        Predicate that if matched keeps a shape from being unreferenced.
+     * @param rootShapeSelector Selector that returns the root shapes to traverse from (defaults to all services).
+     */
+    public UnreferencedShapes(Predicate<Shape> keepFilter, Selector rootShapeSelector) {
+        this.keepFilter = keepFilter;
+        this.rootShapeSelector = rootShapeSelector;
+    }
+
     public UnreferencedShapes() {
         this(FunctionalUtils.alwaysTrue());
     }
 
-    /**
-     * @param keepFilter Predicate that if matched keeps a shape from being unreferenced.
-     */
+    public UnreferencedShapes(Selector selector) {
+        this(FunctionalUtils.alwaysTrue(), selector);
+    }
+
     public UnreferencedShapes(Predicate<Shape> keepFilter) {
-        this.keepFilter = keepFilter;
+        this(keepFilter, SERVICE_SHAPES);
     }
 
     /**
@@ -60,21 +78,26 @@ public final class UnreferencedShapes {
         Walker shapeWalker = new Walker(providerWithIdRefRelationships);
 
         // Find all shapes connected to any service shape.
-        Set<Shape> connected = new HashSet<>();
-        for (Shape service : model.getServiceShapes()) {
-            connected.addAll(shapeWalker.walkShapes(service));
+        Set<ShapeId> connected = new HashSet<>();
+
+        // Stop traversing into trees that are already traversed.
+        Predicate<Relationship> traversed = rel -> !connected.contains(rel.getNeighborShapeId());
+
+        Set<Shape> rootShapes = rootShapeSelector.select(model);
+        for (Shape root : rootShapes) {
+            shapeWalker.iterateShapes(root, traversed).forEachRemaining(shape -> connected.add(shape.getId()));
         }
 
         // Don't remove shapes that are traits or connected to traits.
         for (Shape trait : model.getShapesWithTrait(TraitDefinition.class)) {
-            connected.addAll(shapeWalker.walkShapes(trait));
+            shapeWalker.iterateShapes(trait, traversed).forEachRemaining(shape -> connected.add(shape.getId()));
         }
 
-        // Any shape that wasn't identified as connected to a service is considered unreferenced.
+        // Any shape that wasn't identified as connected to a root is considered unreferenced.
         Set<Shape> result = new HashSet<>();
         for (Shape shape : model.toSet()) {
             if (!shape.isMemberShape()
-                    && !connected.contains(shape)
+                    && !connected.contains(shape.getId())
                     && !Prelude.isPreludeShape(shape)
                     && keepFilter.test(shape)) {
                 result.add(shape);
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMemberUtils.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMemberUtils.java
index f9a775a46..3d2c5bfff 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMemberUtils.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMemberUtils.java
@@ -48,12 +48,12 @@ final class NamedMemberUtils {
                 String name = member.getMemberName();
                 if (members.get().containsKey(name)) {
                     MemberShape localMember = members.get().get(name);
-                    // Rebuild the member with the proper inherited mixin if needed.
+                    // Rebuild the member with the proper inherited mixins if needed.
                     // This catches errant cases where a member is added to a structure/union
                     // but omits the mixin members of parent shapes. Arguably, that's way too
                     // nuanced and error-prone to _not_ try to smooth over.
-                    if (localMember.getMixins().isEmpty() || !mixins.containsKey(member.getId())) {
-                        localMember = localMember.toBuilder().clearMixins().addMixin(member).build();
+                    if (localMember.getMixins().isEmpty() || !localMember.getMixins().contains(member.getId())) {
+                        localMember = rebuildMemberMixins(localMember, mixins.values());
                     }
                     computedMembers.put(name, localMember);
                 } else {
@@ -87,6 +87,14 @@ final class NamedMemberUtils {
         return Collections.unmodifiableMap(computedMembers);
     }
 
+    private static MemberShape rebuildMemberMixins(MemberShape member, Collection<Shape> mixins) {
+        MemberShape.Builder builder = member.toBuilder().clearMixins();
+        for (Shape mixin : mixins) {
+            mixin.getMember(member.getMemberName()).ifPresent(builder::addMixin);
+        }
+        return builder.build();
+    }
+
     static Set<MemberShape> flattenMixins(
             Map<String, MemberShape> members,
             Map<ShapeId, Shape> mixins,
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/linters/UnreferencedShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/linters/UnreferencedShapeValidator.java
new file mode 100644
index 000000000..fe0c42376
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/linters/UnreferencedShapeValidator.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.validation.linters;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.neighbor.UnreferencedShapes;
+import software.amazon.smithy.model.node.ExpectationNotMetException;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.selector.Selector;
+import software.amazon.smithy.model.selector.SelectorSyntaxException;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.validation.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidatorService;
+
+/**
+ * Finds shapes that aren't connected to any shapes considered "roots" (defaults to service shapes).
+ *
+ * <p>This linter is in smithy-model rather than smithy-linters so that it's easier for developers relying on the
+ * deprecated UnreferencedShape validator that was always on to migrate to this one instead without needing another
+ * dependency.
+ */
+public final class UnreferencedShapeValidator extends AbstractValidator {
+
+    public static final class Config {
+
+        private Selector rootShapeSelector = Selector.parse("service");
+
+        /**
+         * The Selector used to find root shapes, and any shape that is not connected to any of the returned root
+         * shapes is considered unreferenced.
+         *
+         * <p>Defaults to "service" if not set.
+         *
+         * @return Returns the root selector;
+         */
+        public Selector getRootShapeSelector() {
+            return rootShapeSelector;
+        }
+
+        public void setRootShapeSelector(Selector rootShapeSelector) {
+            this.rootShapeSelector = Objects.requireNonNull(rootShapeSelector);
+        }
+    }
+
+    public static final class Provider extends ValidatorService.Provider {
+        public Provider() {
+            super(UnreferencedShapeValidator.class, configuration -> {
+                Config config = new Config();
+                ObjectNode node = configuration.expectObjectNode()
+                        .expectNoAdditionalProperties(Collections.singleton("rootShapeSelector"));
+                node.getStringMember("rootShapeSelector").ifPresent(rootShapeNode -> {
+                    try {
+                        config.setRootShapeSelector(Selector.parse(rootShapeNode.getValue()));
+                    } catch (SelectorSyntaxException e) {
+                        throw new ExpectationNotMetException("Error parsing `rootShapeSelector`: " + e.getMessage(),
+                                                             rootShapeNode);
+                    }
+                });
+                return new UnreferencedShapeValidator(config);
+            });
+        }
+    }
+
+    private final Config config;
+
+    private UnreferencedShapeValidator(Config config) {
+        this.config = config;
+    }
+
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        List<ValidationEvent> events = new ArrayList<>();
+
+        for (Shape shape : new UnreferencedShapes(config.rootShapeSelector).compute(model)) {
+            events.add(note(shape, "This shape is unreferenced. It has no modeled connections to shapes "
+                                   + "that match the following selector: `" + config.rootShapeSelector + "`"));
+        }
+
+        return events;
+    }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnreferencedShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnreferencedShapeValidator.java
index 19bd195af..a400abfa3 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnreferencedShapeValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnreferencedShapeValidator.java
@@ -27,7 +27,11 @@ import software.amazon.smithy.model.validation.ValidationEvent;
 /**
  * Adds a validation note event for each shape in the model that is not
  * connected to a service shape.
+ *
+ * <p>This validator is deprecated and no longer applied by default.
+ * Use the UnreferencedShapeValidator from smithy-linters instead.
  */
+@Deprecated
 public final class UnreferencedShapeValidator extends AbstractValidator {
     @Override
     public List<ValidationEvent> validate(Model model) {
diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index 1a8691e08..42df78dca 100644
--- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -52,6 +52,5 @@ software.amazon.smithy.model.validation.validators.TraitTargetValidator
 software.amazon.smithy.model.validation.validators.TraitValueValidator
 software.amazon.smithy.model.validation.validators.UnionValidator
 software.amazon.smithy.model.validation.validators.UnitTypeValidator
-software.amazon.smithy.model.validation.validators.UnreferencedShapeValidator
 software.amazon.smithy.model.validation.validators.UnstableTraitValidator
 software.amazon.smithy.model.validation.validators.XmlNamespaceTraitValidator
diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.ValidatorService b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.ValidatorService
index c9fc2570d..05885ebaf 100644
--- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.ValidatorService
+++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.ValidatorService
@@ -1,2 +1,3 @@
 software.amazon.smithy.model.validation.linters.EmitEachSelectorValidator$Provider
 software.amazon.smithy.model.validation.linters.EmitNoneSelectorValidator$Provider
+software.amazon.smithy.model.validation.linters.UnreferencedShapeValidator$Provider
diff --git a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
index 2df51058e..a9b1a47f6 100644
--- a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
+++ b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
@@ -17,14 +17,13 @@ package software.amazon.smithy.syntax;
 
 import com.opencastsoftware.prettier4j.Doc;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.utils.StringUtils;
 
 final class FormatVisitor {
 
@@ -447,11 +446,49 @@ final class FormatVisitor {
             case TEXT_BLOCK: {
                 // Dispersing the lines of the text block preserves any indentation applied from formatting parent
                 // nodes.
-                List<Doc> lines = Arrays.stream(tree.concatTokens().split(System.lineSeparator()))
-                        .map(String::trim)
-                        .map(Doc::text)
-                        .collect(Collectors.toList());
-                return Doc.intersperse(Doc.line(), lines);
+
+                // We need to rebuild the text block to remove any incidental leading whitespace. The easiest way to
+                // do that is to use the already parsed and resolved value from the lexer.
+                String stringValue = cursor.getTree()
+                        .tokens()
+                        .findFirst()
+                        .orElseThrow(() -> new RuntimeException("TEXT_BLOCK cursor does not have an IDL token"))
+                        .getStringContents();
+
+                // If the last character is a newline, then the closing triple quote must be on the next line.
+                boolean endQuoteOnNextLine = stringValue.endsWith("\n") || stringValue.endsWith("\r");
+
+                List<Doc> resultLines = new ArrayList<>();
+                resultLines.add(Doc.text("\"\"\""));
+
+                String[] inputLines = stringValue.split("\\r?\\n", -1);
+                for (int i = 0; i < inputLines.length; i++) {
+                    boolean lastLine = i == inputLines.length - 1;
+
+                    // If this is the last line and the ending quote is on the next line, then skip the extra line.
+                    if (endQuoteOnNextLine && lastLine) {
+                        break;
+                    }
+
+                    String lineValue = inputLines[i];
+
+                    // Trim trailing whitespace.
+                    // TODO: This may need to be configurable.
+                    lineValue = StringUtils.stripEnd(lineValue, null);
+
+                    // Add the closing quote to this line if it needs to be on the last line.
+                    if (lastLine) {
+                        lineValue += "\"\"\"";
+                    }
+
+                    resultLines.add(Doc.text(lineValue));
+                }
+
+                if (endQuoteOnNextLine) {
+                    resultLines.add(Doc.text("\"\"\""));
+                }
+
+                return Doc.intersperse(Doc.line(), resultLines);
             }
 
             case TOKEN:
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java
index d5f5ce0d3..df1a00d68 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java
@@ -17,9 +17,12 @@ package software.amazon.smithy.utils;
 
 import java.lang.reflect.Method;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -574,6 +577,12 @@ import java.util.regex.Pattern;
  */
 public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
 
+    // Valid formatter characters that can be registered. Must be sorted for binary search to work.
+    static final char[] VALID_FORMATTER_CHARS = {
+            '!', '%', '&', '*', '+', ',', '-', '.', ';', '=', '@',
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', '`'};
+
     private static final Pattern LINES = Pattern.compile("\\r?\\n");
     private static final Map<Character, BiFunction<Object, String, String>> DEFAULT_FORMATTERS = MapUtils.of(
             'L', (s, i) -> formatLiteral(s),
@@ -603,22 +612,19 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
     /**
      * Copies settings from the given AbstractCodeWriter into this AbstractCodeWriter.
      *
-     * <p>The settings of the {@code other} AbstractCodeWriter will overwrite
-     * both global and state-based settings of this AbstractCodeWriter. Formatters of
-     * the {@code other} AbstractCodeWriter will be merged with the formatters of this
-     * AbstractCodeWriter, and in the case of conflicts, the formatters of the
-     * {@code other} will take precedence.
+     * <p>The settings of the {@code other} AbstractCodeWriter will overwrite both global and state-based settings
+     * of this AbstractCodeWriter.
      *
-     * <p>Stateful settings of the {@code other} AbstractCodeWriter are copied into
-     * the <em>current</em> state of this AbstractCodeWriter. Only the settings of
-     * the top-most state is copied. Other states, and the contents of the
-     * top-most state are not copied.
+     * <p>Stateful settings of the {@code other} AbstractCodeWriter like formatters, interceptors, and context are
+     * flattened and then copied into the <em>current</em> state of this AbstractCodeWriter. Any conflicts between
+     * formatters, interceptors, or context of the current writer are overwritten by the other writer. The stack of
+     * states and the contents written to {@code other} are not copied.
      *
      * <pre>{@code
-     * SimpleCodeWritera = new SimpleCodeWriter();
+     * SimpleCodeWriter a = new SimpleCodeWriter();
      * a.setExpressionStart('#');
      *
-     * SimpleCodeWriterb = new SimpleCodeWriter();
+     * SimpleCodeWriter b = new SimpleCodeWriter();
      * b.copySettingsFrom(a);
      *
      * assert(b.getExpressionStart() == '#');
@@ -634,6 +640,16 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
 
         // Copy the current state settings of other into the current state.
         currentState.copyStateFrom(other.currentState);
+
+        // Flatten containers into the current state. This is done in reverse order to ensure that more recent
+        // state changes supersede earlier changes.
+        Iterator<State> reverseOtherStates = other.states.descendingIterator();
+        while (reverseOtherStates.hasNext()) {
+            State otherState = reverseOtherStates.next();
+            currentState.interceptors.addAll(otherState.interceptors);
+            currentState.formatters.putAll(otherState.formatters);
+            currentState.context.putAll(otherState.context);
+        }
     }
 
     /**
@@ -686,7 +702,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public T putFormatter(char identifier, BiFunction<Object, String, String> formatFunction) {
-        this.currentState.formatters.get().putFormatter(identifier, formatFunction);
+        this.currentState.putFormatter(identifier, formatFunction);
         return (T) this;
     }
 
@@ -962,9 +978,15 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
 
             // Don't attempt to intercept anonymous sections.
             if (!(sectionValue instanceof AnonymousCodeSection)) {
-                for (CodeInterceptor<CodeSection, T> interceptor : popped.interceptors.peek().get(sectionValue)) {
-                    result = interceptSection(popped, interceptor, result);
+                // Ensure the remaining parent interceptors are applied in the order they were inserted.
+                // This is the reverse order used when normally iterating over the states deque.
+                Iterator<State> insertionOrderedStates = states.descendingIterator();
+                while (insertionOrderedStates.hasNext()) {
+                    State state = insertionOrderedStates.next();
+                    result = applyPoppedInterceptors(popped, state, sectionValue, result);
                 }
+                // Now ensure the popped state's interceptors are applied.
+                result = applyPoppedInterceptors(popped, popped, sectionValue, result);
             }
 
             if (popped.isInline) {
@@ -986,6 +1008,13 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         return (T) this;
     }
 
+    private String applyPoppedInterceptors(State popped, State state, CodeSection sectionValue, String result) {
+        for (CodeInterceptor<CodeSection, T> interceptor : state.getInterceptors(sectionValue)) {
+            result = interceptSection(popped, interceptor, result);
+        }
+        return result;
+    }
+
     // This method exists because inlining in popSection is impossible due to needing to mutate a result variable.
     @SuppressWarnings("unchecked")
     private String interceptSection(State popped, CodeInterceptor<CodeSection, T> interceptor, String previous) {
@@ -1070,7 +1099,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public T onSection(String sectionName, Consumer<Object> interceptor) {
-        currentState.interceptors.get().putInterceptor(CodeInterceptor.forName(sectionName, (w, p) -> {
+        currentState.putInterceptor(CodeInterceptor.forName(sectionName, (w, p) -> {
             String trimmedContent = removeTrailingNewline(p);
             interceptor.accept(trimmedContent);
         }));
@@ -1098,7 +1127,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public <S extends CodeSection> T onSection(CodeInterceptor<S, T> interceptor) {
-        currentState.interceptors.get().putInterceptor(interceptor);
+        currentState.putInterceptor(interceptor);
         return (T) this;
     }
 
@@ -1857,7 +1886,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public T putContext(String key, Object value) {
-        currentState.context.get().put(key, value);
+        currentState.context.put(key, value);
         return (T) this;
     }
 
@@ -1879,40 +1908,45 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
     /**
      * Removes a named key-value pair from the context of the current state.
      *
+     * <p>This method has no effect if the parent state defines the context key value pair.
+     *
      * @param key Key to add to remove from the current context.
      * @return Returns self.
      */
     @SuppressWarnings("unchecked")
     public T removeContext(String key) {
-        if (currentState.context.peek().containsKey(key)) {
-            currentState.context.get().remove(key);
+        if (currentState.context.containsKey(key)) {
+            currentState.context.remove(key);
+        } else {
+            // Parent states might have a value for this context key, so explicitly set it to null in this context.
+            currentState.context.put(key, null);
         }
         return (T) this;
     }
 
     /**
-     * Gets a named contextual key-value pair from the current state.
+     * Gets a named contextual key-value pair from the current state or any parent states.
      *
      * @param key Key to retrieve.
      * @return Returns the associated value or null if not present.
      */
     public Object getContext(String key) {
-        CodeSection section = currentState.sectionValue;
-        Map<String, Object> currentContext = currentState.context.peek();
-        if (currentContext.containsKey(key)) {
-            return currentContext.get(key);
-        } else if (section != null) {
-            Method method = findContextMethod(section, key);
-            if (method != null) {
-                try {
-                    return method.invoke(section);
-                } catch (ReflectiveOperationException e) {
-                    String message = String.format(
-                            "Unable to get context '%s' from a matching method of the current CodeSection: %s %s",
-                            key,
-                            e.getCause() != null ? e.getCause().getMessage() : e.getMessage(),
-                            getDebugInfo());
-                    throw new RuntimeException(message, e);
+        for (State state : states) {
+            if (state.context.containsKey(key)) {
+                return state.context.get(key);
+            } else if (state.sectionValue != null) {
+                Method method = findContextMethod(state.sectionValue, key);
+                if (method != null) {
+                    try {
+                        return method.invoke(state.sectionValue);
+                    } catch (ReflectiveOperationException e) {
+                        String message = String.format(
+                                "Unable to get context '%s' from a matching method of the current CodeSection: %s %s",
+                                key,
+                                e.getCause() != null ? e.getCause().getMessage() : e.getMessage(),
+                                getDebugInfo());
+                        throw new RuntimeException(message, e);
+                    }
                 }
             }
         }
@@ -1984,7 +2018,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
     // Used only by CodeFormatter to apply formatters.
     @SuppressWarnings("unchecked")
     String applyFormatter(char identifier, Object value) {
-        BiFunction<Object, String, String> f = currentState.formatters.peek().getFormatter(identifier);
+        BiFunction<Object, String, String> f = resolveFormatter(identifier);
         if (f != null) {
             return f.apply(value, getIndentText());
         } else if (identifier == 'C') {
@@ -2009,7 +2043,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
                 throw new ClassCastException(String.format(
                         "Expected value for 'C' formatter to be an instance of %s or %s, but found %s %s",
                         Runnable.class.getName(), Consumer.class.getName(),
-                        value.getClass().getName(), getDebugInfo()));
+                        value == null ? "null" : value.getClass().getName(), getDebugInfo()));
             }
         } else {
             // Return null if no formatter was found.
@@ -2017,6 +2051,16 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         }
     }
 
+    BiFunction<Object, String, String> resolveFormatter(char identifier) {
+        for (State state : states) {
+            BiFunction<Object, String, String> result = state.getFormatter(identifier);
+            if (result != null) {
+                return result;
+            }
+        }
+        return null;
+    }
+
     private final class State {
         private final boolean isRoot;
         private String indentText = "    ";
@@ -2030,9 +2074,9 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         private boolean needsIndentation;
 
         private CodeSection sectionValue;
-        private CopyOnWriteRef<Map<String, Object>> context;
-        private CopyOnWriteRef<CodeWriterFormatterContainer> formatters;
-        private CopyOnWriteRef<CodeInterceptorContainer<T>> interceptors;
+        private final Map<String, Object> context = new HashMap<>();
+        private final Map<Character, BiFunction<Object, String, String>> formatters = new HashMap<>();
+        private final List<CodeInterceptor<CodeSection, T>> interceptors = new ArrayList<>();
 
         private StringBuilder builder;
 
@@ -2046,11 +2090,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         State() {
             builder = new StringBuilder();
             isRoot = true;
-            CodeWriterFormatterContainer formatterContainer = new CodeWriterFormatterContainer();
-            DEFAULT_FORMATTERS.forEach(formatterContainer::putFormatter);
-            this.formatters = CopyOnWriteRef.fromOwned(formatterContainer);
-            this.context = CopyOnWriteRef.fromOwned(new HashMap<>());
-            this.interceptors = CopyOnWriteRef.fromOwned(new CodeInterceptorContainer<>());
+            DEFAULT_FORMATTERS.forEach(this::putFormatter);
         }
 
         @SuppressWarnings("CopyConstructorMissesField")
@@ -2060,10 +2100,11 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
             this.builder = copy.builder;
         }
 
+        // This does not copy context, interceptors, or formatters.
+        // State inheritance relies on stacks of States in an AbstractCodeWriter.
         private void copyStateFrom(State copy) {
             this.newline = copy.newline;
             this.expressionStart = copy.expressionStart;
-            this.context = copy.context;
             this.indentText = copy.indentText;
             this.leadingIndentString = copy.leadingIndentString;
             this.indentation = copy.indentation;
@@ -2071,9 +2112,6 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
             this.trimTrailingSpaces = copy.trimTrailingSpaces;
             this.disableNewline = copy.disableNewline;
             this.needsIndentation = copy.needsIndentation;
-            this.context = CopyOnWriteRef.fromBorrowed(copy.context.peek(), HashMap::new);
-            this.formatters = CopyOnWriteRef.fromBorrowed(copy.formatters.peek(), CodeWriterFormatterContainer::new);
-            this.interceptors = CopyOnWriteRef.fromBorrowed(copy.interceptors.peek(), CodeInterceptorContainer::new);
         }
 
         @Override
@@ -2200,6 +2238,48 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
                 return sectionValue.sectionName();
             }
         }
+
+        void putFormatter(Character identifier, BiFunction<Object, String, String> formatFunction) {
+            if (Arrays.binarySearch(VALID_FORMATTER_CHARS, identifier) < 0) {
+                throw new IllegalArgumentException("Invalid formatter identifier: " + identifier);
+            }
+            formatters.put(identifier, formatFunction);
+        }
+
+        BiFunction<Object, String, String> getFormatter(char identifier) {
+            return formatters.get(identifier);
+        }
+
+        @SuppressWarnings("unchecked")
+        void putInterceptor(CodeInterceptor<? extends CodeSection, T> interceptor) {
+            interceptors.add((CodeInterceptor<CodeSection, T>) interceptor);
+        }
+
+        /**
+         * Gets a list of interceptors that match the given type and for which the
+         * result of {@link CodeInterceptor#isIntercepted(CodeSection)} returns true
+         * when given {@code forSection}.
+         *
+         * @param forSection The section that is being intercepted.
+         * @param <S> The type of section being intercepted.
+         * @return Returns the list of matching interceptors.
+         */
+        <S extends CodeSection> List<CodeInterceptor<CodeSection, T>> getInterceptors(S forSection) {
+            // Add in parent interceptors.
+            List<CodeInterceptor<CodeSection, T>> result = new ArrayList<>();
+            // Merge in local interceptors.
+            for (CodeInterceptor<CodeSection, T> interceptor : interceptors) {
+                // Add the interceptor only if it's the right type.
+                if (interceptor.sectionType().isInstance(forSection)) {
+                    // Only add if the filter passes.
+                    if (interceptor.isIntercepted(forSection)) {
+                        result.add(interceptor);
+                    }
+                }
+            }
+
+            return result;
+        }
     }
 
     String removeTrailingNewline(String value) {
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java
index 8a7efaf99..04355e05a 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java
@@ -635,7 +635,7 @@ final class CodeFormatter {
             // Parse the formatter and apply it.
             int line = parser.line();
             int column = parser.column();
-            char identifier = parser.expect(CodeWriterFormatterContainer.VALID_FORMATTER_CHARS);
+            char identifier = parser.expect(AbstractCodeWriter.VALID_FORMATTER_CHARS);
 
             // The error message needs to be created here and given to the operation in way that it can
             // throw with an appropriate message.
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptorContainer.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptorContainer.java
deleted file mode 100644
index 2a0425ae0..000000000
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptorContainer.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.smithy.utils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A container for CodeSection.Interceptors registered with AbstractCodeWriter.
- */
-@SmithyInternalApi
-final class CodeInterceptorContainer<W extends AbstractCodeWriter<W>> {
-
-    private final List<CodeInterceptor<CodeSection, W>> interceptors = new ArrayList<>();
-    private final CodeInterceptorContainer<W> parent;
-
-    CodeInterceptorContainer() {
-        this(null);
-    }
-
-    CodeInterceptorContainer(CodeInterceptorContainer<W> parent) {
-        this.parent = parent;
-    }
-
-    @SuppressWarnings("unchecked")
-    void putInterceptor(CodeInterceptor<? extends CodeSection, W> interceptor) {
-        interceptors.add((CodeInterceptor<CodeSection, W>) interceptor);
-    }
-
-    /**
-     * Gets a list of interceptors that match the given type and for which the
-     * result of {@link CodeInterceptor#isIntercepted(CodeSection)} returns true
-     * when given {@code forSection}.
-     *
-     * @param forSection The section that is being intercepted.
-     * @param <S> The type of section being intercepted.
-     * @return Returns the list of matching interceptors.
-     */
-    <S extends CodeSection> List<CodeInterceptor<CodeSection, W>> get(S forSection) {
-        // Add in parent interceptors.
-        List<CodeInterceptor<CodeSection, W>> result = parent == null
-                ? new ArrayList<>()
-                : parent.get(forSection);
-        // Merge in local interceptors.
-        for (CodeInterceptor<CodeSection, W> interceptor : interceptors) {
-            // Add the interceptor only if it's the right type.
-            if (interceptor.sectionType().isInstance(forSection)) {
-                // Only add if the filter passes.
-                if (interceptor.isIntercepted(forSection)) {
-                    result.add(interceptor);
-                }
-            }
-        }
-
-        return result;
-    }
-}
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java
index e1a4ecbf3..7f25e9981 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java
@@ -42,7 +42,7 @@ public final class CodeWriterDebugInfo {
     /**
      * Get the CodeWriter state path from which the debug information was collected.
      *
-     * @return Returns the path as returned by {@link CodeWriter#getStateDebugPath()};
+     * @return Returns the path as returned by {@link AbstractCodeWriter#getDebugInfo()};
      */
     public String getStateDebugPath() {
         return getMetadata("path");
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterFormatterContainer.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterFormatterContainer.java
deleted file mode 100644
index e2dfffebb..000000000
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterFormatterContainer.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.smithy.utils;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.BiFunction;
-
-/**
- * A container for formatters registered with CodeWriter.
- */
-@SmithyInternalApi
-final class CodeWriterFormatterContainer {
-
-    // Must be sorted for binary search to work.
-    static final char[] VALID_FORMATTER_CHARS = {
-            '!', '%', '&', '*', '+', ',', '-', '.', ';', '=', '@',
-            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
-            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', '`'};
-
-    private final Map<Character, BiFunction<Object, String, String>> formatters = new HashMap<>();
-    private final CodeWriterFormatterContainer parent;
-
-    CodeWriterFormatterContainer() {
-        this(null);
-    }
-
-    CodeWriterFormatterContainer(CodeWriterFormatterContainer parent) {
-        this.parent = parent;
-    }
-
-    void putFormatter(Character identifier, BiFunction<Object, String, String> formatFunction) {
-        if (Arrays.binarySearch(VALID_FORMATTER_CHARS, identifier) < 0) {
-            throw new IllegalArgumentException("Invalid formatter identifier: " + identifier);
-        }
-        formatters.put(identifier, formatFunction);
-    }
-
-    BiFunction<Object, String, String> getFormatter(char identifier) {
-        BiFunction<Object, String, String> result = formatters.get(identifier);
-
-        if (result == null && parent != null) {
-            result = parent.getFormatter(identifier);
-        }
-
-        return result;
-    }
-}


Test patch:
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/processor/AnnotationProcessorTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/processor/AnnotationProcessorTest.java
new file mode 100644
index 000000000..7b85362c8
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/processor/AnnotationProcessorTest.java
@@ -0,0 +1,52 @@
+package software.amazon.smithy.build.processor;
+
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.CompilationSubject;
+import com.google.testing.compile.Compiler;
+import com.google.testing.compile.JavaFileObjects;
+import javax.tools.StandardLocation;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+
+/**
+ * Test the {@link SmithyAnnotationProcessor}
+ */
+public class AnnotationProcessorTest {
+    private static final Compiler compiler = Compiler.javac()
+            .withProcessors(new TestProcessorImplementation());
+
+    private static Compilation compilation;
+
+    @BeforeAll
+    static void compile() {
+        compilation = compiler.compile(JavaFileObjects.forResource(
+                "software/amazon/smithy/build/processor/package-info.java"));
+        CompilationSubject.assertThat(compilation).succeeded();
+    }
+
+    @Test
+    void generatesBasicJavaFiles() {
+        System.out.println(compilation.diagnostics());
+        CompilationSubject.assertThat(compilation)
+                .generatedFile(StandardLocation.SOURCE_OUTPUT, "com/example/testing/Empty.java")
+                .contentsAsUtf8String()
+                .isEqualTo(TestBuildPlugin.getEmptyClass("com.example.testing"));
+    }
+
+    @Test
+    void generatesMetaInfFiles() {
+        CompilationSubject.assertThat(compilation)
+                .generatedFile(StandardLocation.CLASS_OUTPUT, "META-INF/services/com.example.spi.impl.Example")
+                .contentsAsUtf8String()
+                .isEqualTo("com.example.testing.Empty");
+    }
+
+    @Test
+    void ignoresFile() {
+        CompilationSubject.assertThat(compilation).hadNoteContaining("Executing processor: TestProcessorImplementation...");
+        CompilationSubject.assertThat(compilation).hadNoteContaining("Ignoring generated file: ");
+        CompilationSubject.assertThat(compilation).hadNoteContaining("Ignored.ignored");
+    }
+}
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestBuildPlugin.java b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestBuildPlugin.java
new file mode 100644
index 000000000..30cbe2c5f
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestBuildPlugin.java
@@ -0,0 +1,65 @@
+package software.amazon.smithy.build.processor;
+
+import java.nio.file.Paths;
+import software.amazon.smithy.build.FileManifest;
+import software.amazon.smithy.build.PluginContext;
+import software.amazon.smithy.build.SmithyBuildPlugin;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.shapes.ShapeId;
+
+public class TestBuildPlugin implements SmithyBuildPlugin {
+    private static final String META_INF_LOCATION = "META-INF/services/";
+
+    @Override
+    public String getName() {
+        return "test-plugin";
+    }
+
+    @Override
+    public void execute(PluginContext context) {
+        Settings settings = Settings.from(context.getSettings());
+        FileManifest fileManifest = context.getFileManifest();
+
+        String pathifiedNamespace = settings.packageName.replace(".", "/");
+
+        // Write an empty java class
+        fileManifest.writeFile(pathifiedNamespace + "/Empty.java",
+                getEmptyClass(settings.packageName));
+
+        // Write a META-INF metadata file
+        fileManifest.writeFile(
+                Paths.get(META_INF_LOCATION + "com.example.spi.impl.Example").normalize(),
+                settings.packageName + "." + "Empty"
+        );
+
+        // Write a non-java file we expect to be ignored
+        fileManifest.writeFile(pathifiedNamespace + "/Ignored.ignored",
+                "ignore me. I dont compile!");
+
+        // Require that expected smithy shapes were correctly loaded
+        context.getModel().expectShape(ShapeId.from("com.testing.smithy#StructyMcStructFace"));
+    }
+
+    public static String getEmptyClass(String namespace) {
+        StringBuilder builder = new StringBuilder();
+        builder.append("package ").append(namespace).append(";").append(System.lineSeparator())
+                .append(System.lineSeparator())
+                .append("public final class Empty {").append(System.lineSeparator())
+                .append(System.lineSeparator())
+                .append("}").append(System.lineSeparator());
+        return builder.toString();
+    }
+
+
+    private static final class Settings {
+        private final String packageName;
+
+        private Settings(String packageName) {
+            this.packageName = packageName;
+        }
+
+        static Settings from(ObjectNode node) {
+            return new Settings(node.expectStringMember("packageName").getValue());
+        }
+    }
+}
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorAnnotation.java b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorAnnotation.java
new file mode 100644
index 000000000..c67c4690f
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorAnnotation.java
@@ -0,0 +1,9 @@
+package software.amazon.smithy.build.processor;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target(ElementType.PACKAGE)
+public @interface TestProcessorAnnotation {
+    String NAME = "software.amazon.smithy.build.processor.TestProcessorAnnotation";
+}
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorImplementation.java b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorImplementation.java
new file mode 100644
index 000000000..229cd8dc1
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorImplementation.java
@@ -0,0 +1,28 @@
+package software.amazon.smithy.build.processor;
+
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.ObjectNode;
+
+@SupportedAnnotationTypes(TestProcessorAnnotation.NAME)
+@SupportedSourceVersion(SourceVersion.RELEASE_8)
+public class TestProcessorImplementation extends SmithyAnnotationProcessor<TestProcessorAnnotation> {
+    @Override
+    protected String getPluginName() {
+        return "test-plugin";
+    }
+
+    @Override
+    protected Class<TestProcessorAnnotation> getAnnotationClass() {
+        return TestProcessorAnnotation.class;
+    }
+
+    @Override
+    protected ObjectNode createPluginNode(TestProcessorAnnotation annotation, String packageName) {
+        return Node.objectNodeBuilder()
+                .withMember("packageName", packageName)
+                .build();
+    }
+}
diff --git a/smithy-build/src/test/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin b/smithy-build/src/test/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin
new file mode 100644
index 000000000..e0fc632cc
--- /dev/null
+++ b/smithy-build/src/test/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin
@@ -0,0 +1,1 @@
+software.amazon.smithy.build.processor.TestBuildPlugin
diff --git a/smithy-build/src/test/resources/META-INF/smithy/manifest b/smithy-build/src/test/resources/META-INF/smithy/manifest
new file mode 100644
index 000000000..3f2bc2164
--- /dev/null
+++ b/smithy-build/src/test/resources/META-INF/smithy/manifest
@@ -0,0 +1,1 @@
+structs-n-stuff.smithy
diff --git a/smithy-build/src/test/resources/META-INF/smithy/structs-n-stuff.smithy b/smithy-build/src/test/resources/META-INF/smithy/structs-n-stuff.smithy
new file mode 100644
index 000000000..5066e4a0e
--- /dev/null
+++ b/smithy-build/src/test/resources/META-INF/smithy/structs-n-stuff.smithy
@@ -0,0 +1,7 @@
+$version: "2.0"
+
+namespace com.testing.smithy
+
+structure StructyMcStructFace {
+    fieldA: String
+}
diff --git a/smithy-build/src/test/resources/software/amazon/smithy/build/processor/package-info.java b/smithy-build/src/test/resources/software/amazon/smithy/build/processor/package-info.java
new file mode 100644
index 000000000..60bb108c7
--- /dev/null
+++ b/smithy-build/src/test/resources/software/amazon/smithy/build/processor/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Generates Test files
+ */
+@TestProcessorAnnotation
+package com.example.testing;
+
+import software.amazon.smithy.build.processor.TestProcessorAnnotation;
diff --git a/smithy-cli/config/README.md b/smithy-cli/configuration/README.md
similarity index 100%
rename from smithy-cli/config/README.md
rename to smithy-cli/configuration/README.md
diff --git a/smithy-cli/config/install b/smithy-cli/configuration/install
similarity index 100%
rename from smithy-cli/config/install
rename to smithy-cli/configuration/install
diff --git a/smithy-cli/config/install.bat b/smithy-cli/configuration/install.bat
similarity index 100%
rename from smithy-cli/config/install.bat
rename to smithy-cli/configuration/install.bat
diff --git a/smithy-cli/config/install.bats b/smithy-cli/configuration/install.bats
similarity index 100%
rename from smithy-cli/config/install.bats
rename to smithy-cli/configuration/install.bats
diff --git a/smithy-cli/config/unixStartScript.txt b/smithy-cli/configuration/unixStartScript.txt
similarity index 100%
rename from smithy-cli/config/unixStartScript.txt
rename to smithy-cli/configuration/unixStartScript.txt
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/DiffCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/DiffCommandTest.java
index fb2313380..05ea13753 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/DiffCommandTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/DiffCommandTest.java
@@ -71,6 +71,24 @@ public class DiffCommandTest {
         });
     }
 
+    @Test
+    public void canWriteCsvOutput() {
+        IntegUtils.withTempDir("diff", dir -> {
+            Path a = dir.resolve("a.smithy");
+            writeFile(a, "$version: \"2.0\"\nnamespace example\nstring A\n");
+
+            Path b = dir.resolve("b.smithy");
+            writeFile(b, "$version: \"2.0\"\nnamespace example\n@aaaaaa\nstring A\n");
+
+            RunResult result = IntegUtils.run(dir, ListUtils.of("diff", "--old", a.toString(),
+                                                                "--new", b.toString(),
+                                                                "--format", "csv"));
+            assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
+            assertThat(result.getOutput(), containsString("severity,id,"));
+            assertThat(result.getOutput(), containsString("ERROR"));
+        });
+    }
+
     @Test
     public void showsLabelForDiffEvents() {
         IntegUtils.withTempDir("diff", dir -> {
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/DiffCommandTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/DiffCommandTest.java
new file mode 100644
index 000000000..19a2150ca
--- /dev/null
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/DiffCommandTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.cli.CliUtils;
+
+public class DiffCommandTest {
+    @Test
+    public void canOutputCsv() throws Exception {
+        Path oldModel = Paths.get(getClass().getResource("diff/old.smithy").toURI());
+        Path newModel = Paths.get(getClass().getResource("diff/new.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("diff",
+                                                    "--old", oldModel.toString(),
+                                                    "--new", newModel.toString(),
+                                                    "--format", "csv");
+
+        assertThat(result.code(), not(0));
+
+        // Make sure FAILURE is sent to stderr.
+        assertThat(result.stderr(), containsString("FAILURE"));
+        assertThat(result.stdout(), not(containsString("FAILURE")));
+
+        String[] lines = result.stdout().split("(\\r\\n|\\r|\\n)");
+        assertThat(lines.length, is(2));
+        assertThat(lines[0], containsString("severity,id,shape,file,line,column,message,hint,suppressionReason"));
+        assertThat(lines[1], containsString("\"ERROR\",\"ChangedShapeType\",\"smithy.example#Hello\""));
+    }
+}
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java
index 942ead467..7461b109b 100644
--- a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java
@@ -216,4 +216,43 @@ public class ValidateCommandTest {
         assertThat(result.stdout(), not(containsString("EmitDangers")));
         assertThat(result.stdout(), containsString("HttpLabelTrait"));
     }
+
+    @Test
+    public void canOutputCsv() throws Exception {
+        Path validationEventsModel = Paths.get(getClass().getResource("validation-events.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--format", "csv",
+                                                    validationEventsModel.toString());
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stdout(), containsString("suppressionReason"));
+        assertThat(result.stdout(), containsString("EmitWarnings"));
+        assertThat(result.stdout(), containsString("EmitDangers"));
+        assertThat(result.stdout(), containsString("HttpLabelTrait"));
+        assertThat(result.stdout(), not(containsString("FAILURE"))); // stderr
+
+        String[] lines = result.stdout().split("(\\r\\n|\\r|\\n)");
+        assertThat(lines[0], containsString("severity,id,shape,file,line,column,message,hint,suppressionReason"));
+    }
+
+    @Test
+    public void canOutputText() throws Exception {
+        Path validationEventsModel = Paths.get(getClass().getResource("validation-events.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--format", "text",
+                                                    validationEventsModel.toString());
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stdout(), not(containsString("suppressionReason")));
+        assertThat(result.stdout(), containsString("EmitWarnings"));
+        assertThat(result.stdout(), containsString("EmitDangers"));
+        assertThat(result.stdout(), containsString("HttpLabelTrait"));
+        assertThat(result.stdout(), not(containsString("FAILURE"))); // stderr
+    }
+
+    @Test
+    public void outputFormatMustBeValid() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--format", "HELLO");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("Unexpected --format: `HELLO`"));
+    }
 }
diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/new.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/new.smithy
new file mode 100644
index 000000000..cdae50e01
--- /dev/null
+++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/new.smithy
@@ -0,0 +1,10 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@deprecated
+string Hello
+
+structure Foo {
+    hello: Hello
+}
diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/old.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/old.smithy
new file mode 100644
index 000000000..9bfda345b
--- /dev/null
+++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/old.smithy
@@ -0,0 +1,10 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@deprecated
+integer Hello
+
+structure Foo {
+    hello: Hello
+}
diff --git a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/camel-case-validator-defaults-test.errors b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/camel-case-validator-defaults-test.errors
index f68673b92..f98c5c12b 100644
--- a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/camel-case-validator-defaults-test.errors
+++ b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/camel-case-validator-defaults-test.errors
@@ -38,6 +38,3 @@
 [DANGER] ns.baz#lowerStructureTrait$UpperCamelCase: Member shape member name, `UpperCamelCase`, is not lower camel case; members in the ns.baz namespace must all use lower camel case. | DefaultCamelCase
 [DANGER] ns.foo#Structure$lowerCamelCase: Member shape member name, `lowerCamelCase`, is not upper camel case; members in the MyService must all use upper camel case. | DefaultCamelCase
 [DANGER] ns.foo#Structure$snake_case: Member shape member name, `snake_case`, is not upper camel case; members in the MyService must all use upper camel case. | DefaultCamelCase
-
-[NOTE] ns.foo#Enum: The enum shape is not connected to from any service shape. | UnreferencedShape
-[NOTE] ns.foo#IntEnum: The intEnum shape is not connected to from any service shape. | UnreferencedShape
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java
index dfce68688..a0ae485b7 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java
@@ -21,9 +21,13 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.Matchers.startsWith;
 
+import java.util.stream.Stream;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 public class IdlInternalTokenizerTest {
 
@@ -181,4 +185,81 @@ public class IdlInternalTokenizerTest {
         assertThat(lines, equalTo("Hi\nThere\n123\n456"));
         MatcherAssert.assertThat(tokenizer.removePendingDocCommentLines(), nullValue());
     }
+
+    public static Stream<Arguments> textBlockTests() {
+        return Stream.of(
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "    Hello\n"
+                        + "        - Indented\"\"\"\n",
+                        "Hello\n    - Indented"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "    Hello\n"
+                        + "        - Indented\n"
+                        + "    \"\"\"\n",
+                        "Hello\n    - Indented\n"
+                ),
+                Arguments.of(
+                    "\"\"\"\n"
+                    + "    Hello\n"
+                    + "        - Indented\n"
+                    + "\"\"\"\n",
+                    "    Hello\n        - Indented\n"
+                ),
+                Arguments.of(
+                    "\"\"\"\n"
+                    + "    Hello\"\"\"\n",
+                    "Hello"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "    Hello\n"
+                        + "\n"
+                        + "        - Indented\n"
+                        + "\"\"\"\n",
+                        "    Hello\n\n        - Indented\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "                        \n" // only WS doesn't influence line length calculations.
+                        + "          Hello\n"
+                        + "                        \n" // only WS doesn't influence line length calculations.
+                        + "          \"\"\"",
+                        "\nHello\n\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "\n" // empty lines are incidental whitespace.
+                        + "          Hello\n"
+                        + "                        \n" // only WS doesn't influence line length calculations.
+                        + "          \"\"\"",
+                        "\nHello\n\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "\n" // empty lines are incidental whitespace.
+                        + "Hello\n"
+                        + "\n"
+                        + "\n"
+                        + "\"\"\"",
+                        "\nHello\n\n\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "\"\"\"",
+                        ""
+                )
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("textBlockTests")
+    public void parsesTextBlocks(String model, String stringValue) {
+        IdlInternalTokenizer tokenizer = new IdlInternalTokenizer("a.smithy", model);
+        tokenizer.expect(IdlToken.TEXT_BLOCK);
+
+        assertThat(tokenizer.getCurrentTokenStringSlice().toString(), equalTo(stringValue));
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlModelLoaderTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlModelLoaderTest.java
index 9e8da5856..51767deef 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlModelLoaderTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlModelLoaderTest.java
@@ -491,4 +491,11 @@ public class IdlModelLoaderTest {
         assertThat(e.getMessage(),
                    startsWith("Syntax error at line 1, column 1: Parser exceeded maximum allowed depth of 64"));
     }
+
+    @Test
+    public void handlesMultipleMemberInheritance() {
+        ValidatedResult<Model> result = Model.assembler()
+                .addImport(getClass().getResource("error-recovery/to-dollar.smithy"))
+                .assemble();
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java
index 890f4e7f0..4067b7066 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java
@@ -18,6 +18,7 @@ package software.amazon.smithy.model.loader;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.containsInRelativeOrder;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
@@ -44,6 +45,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -77,6 +79,8 @@ import software.amazon.smithy.model.traits.DynamicTrait;
 import software.amazon.smithy.model.traits.InternalTrait;
 import software.amazon.smithy.model.traits.MediaTypeTrait;
 import software.amazon.smithy.model.traits.MixinTrait;
+import software.amazon.smithy.model.traits.PatternTrait;
+import software.amazon.smithy.model.traits.RequiredTrait;
 import software.amazon.smithy.model.traits.SensitiveTrait;
 import software.amazon.smithy.model.traits.SuppressTrait;
 import software.amazon.smithy.model.traits.TagsTrait;
@@ -1368,4 +1372,23 @@ public class ModelAssemblerTest {
                 .assemble()
                 .unwrap();
     }
+
+    @Test
+    public void handlesMultipleInheritanceForMixinMembers() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("mixins/multiple-inheritance-with-introduction.smithy"))
+                .assemble()
+                .unwrap();
+
+        MemberShape shape = model.expectShape(ShapeId.from("com.example#FinalStructure$member"), MemberShape.class);
+
+        assertThat(shape.getMixins(), contains(
+                ShapeId.from("com.example#MixinA$member"),
+                ShapeId.from("com.example#MixinB$member")
+        ));
+        assertThat(shape.getAllTraits().keySet(),
+                containsInAnyOrder(PatternTrait.ID, RequiredTrait.ID, InternalTrait.ID));
+        String actualPattern = shape.expectTrait(PatternTrait.class).getValue();
+        assertThat(actualPattern, equalTo("baz"));
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidationEventDecoratorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidationEventDecoratorTest.java
index c7e1279a6..8abf05328 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidationEventDecoratorTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidationEventDecoratorTest.java
@@ -17,20 +17,16 @@ package software.amazon.smithy.model.loader;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasSize;
-import static org.hamcrest.Matchers.notNullValue;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.validation.NodeValidationVisitorTest;
 import software.amazon.smithy.model.validation.ValidatedResult;
 import software.amazon.smithy.model.validation.ValidationEvent;
 import software.amazon.smithy.model.validation.ValidationEventDecorator;
@@ -40,23 +36,19 @@ import software.amazon.smithy.utils.SetUtils;
 
 public class ValidationEventDecoratorTest {
 
-    static final String HINT = "Consider connecting this structure to a service";
-    static final String UNREFERENCED_SHAPE_EVENT_ID = "UnreferencedShape";
-    static final Set<ShapeId> STRUCT_SHAPE_IDS = SetUtils.of(ShapeId.from("ns.foo#Structure"),
-                                                             ShapeId.from("ns.foo#Structure2"),
-                                                             ShapeId.from("ns.foo#Structure3"),
-                                                             ShapeId.from("ns.foo#Structure4"));
+    static final String HINT = "We had to deprecate this";
+    static final String SHAPE_EVENT_ID = "DeprecatedShape";
+    static final Set<ShapeId> MATCHING_SHAPE_IDS = SetUtils.of(ShapeId.from("smithy.example#Foo$a"));
 
     @Test
     public void canDecorateValidationEvents() {
         ValidatedResult<Model> result = Model.assembler()
-                                             .addImport(NodeValidationVisitorTest.class.getResource("node-validator"
-                                                                                                    + ".json"))
-                                             .validatorFactory(testFactory(new DummyHintValidationEventDecorator()))
-                                             .assemble();
+                .addImport(getClass().getResource("validation-event-decorator-test.smithy"))
+                .validatorFactory(testFactory(new TestValidationEventDecorator()))
+                .assemble();
         for (ValidationEvent event : result.getValidationEvents()) {
             ShapeId eventShapeId = event.getShapeId().orElse(null);
-            if (STRUCT_SHAPE_IDS.contains(eventShapeId)) {
+            if (MATCHING_SHAPE_IDS.contains(eventShapeId)) {
                 assertThat(event.getHint().isPresent(), equalTo(true));
                 assertThat(event.getHint().get(), equalTo(HINT));
             } else {
@@ -69,10 +61,9 @@ public class ValidationEventDecoratorTest {
     public void exceptionsAreNotCaughtWhenDecoratorsThrow() {
         assertThrows(RuntimeException.class, () -> {
             Model.assembler()
-                 .addImport(NodeValidationVisitorTest.class.getResource("node-validator"
-                                                                        + ".json"))
-                 .validatorFactory(testFactory(new ThrowingValidationEventDecorator()))
-                 .assemble();
+                    .addImport(getClass().getResource("validation-event-decorator-test.smithy"))
+                    .validatorFactory(testFactory(new ThrowingValidationEventDecorator()))
+                    .assemble();
         });
     }
 
@@ -96,11 +87,10 @@ public class ValidationEventDecoratorTest {
         };
     }
 
-    static class DummyHintValidationEventDecorator implements ValidationEventDecorator {
-
+    static class TestValidationEventDecorator implements ValidationEventDecorator {
         @Override
         public boolean canDecorate(ValidationEvent ev) {
-            return ev.containsId(UNREFERENCED_SHAPE_EVENT_ID) && ev.getMessage().contains("The structure ");
+            return ev.containsId(SHAPE_EVENT_ID);
         }
 
         @Override
@@ -110,15 +100,14 @@ public class ValidationEventDecoratorTest {
     }
 
     static class ThrowingValidationEventDecorator implements ValidationEventDecorator {
-
         @Override
         public boolean canDecorate(ValidationEvent ev) {
-            return ev.containsId(UNREFERENCED_SHAPE_EVENT_ID) && ev.getMessage().contains("The structure ");
+            return ev.containsId(SHAPE_EVENT_ID);
         }
 
         @Override
         public ValidationEvent decorate(ValidationEvent ev) {
-            throw new RuntimeException("ups");
+            throw new RuntimeException("oops!");
         }
     }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/StructureShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/StructureShapeTest.java
index a85d68fd7..e9bcdc7e5 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/StructureShapeTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/StructureShapeTest.java
@@ -310,4 +310,30 @@ public class StructureShapeTest {
 
         assertThat(concrete.getMemberNames(), contains("a", "b", "c", "d"));
     }
+
+    @Test
+    public void fixesMissingMemberMixins() {
+        StringShape string = StringShape.builder().id("smithy.example#String").build();
+        StructureShape mixin1 = StructureShape.builder()
+                .id("smithy.example#Mixin1")
+                .addTrait(MixinTrait.builder().build())
+                .addMember("a", string.getId())
+                .build();
+        StructureShape mixin2 = StructureShape.builder()
+                .id("smithy.example#Mixin2")
+                .addTrait(MixinTrait.builder().build())
+                .addMember("a", string.getId())
+                .build();
+        StructureShape concrete = StructureShape.builder()
+                .id("smithy.example#Concrete")
+                .addMember("a", string.getId())
+                .addMixin(mixin1)
+                .addMixin(mixin2)
+                .build();
+
+        assertThat(concrete.getMember("a").get().getMixins(), contains(
+                ShapeId.from("smithy.example#Mixin1$a"),
+                ShapeId.from("smithy.example#Mixin2$a")
+        ));
+    }
 }
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors
index 4b8456e12..ea6d0c6c1 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors
@@ -22,8 +22,6 @@
 [ERROR] ns.foo#NInput$a: This `a` structure member is marked with the `httpLabel` trait, but no corresponding `http` URI label could be found when used as the input of the `ns.foo#N` operation. | HttpLabelTrait
 [ERROR] ns.foo#PInput$a: The `a` structure member corresponds to a greedy label when used as the input of the `ns.foo#P` operation. This member targets (integer: `ns.foo#Integer`), but greedy labels must target string shapes. | HttpLabelTrait
 [ERROR] ns.foo#RInput$b: `httpHeader` binding of `x-foo` conflicts with the `httpPrefixHeaders` binding of `ns.foo#RInput$a` to ``. `httpHeader` bindings must not case-insensitively start with any `httpPrefixHeaders` bindings. | HttpPrefixHeadersTrait
-[NOTE] ns.foo#BadError: The structure shape is not connected to from any service shape. | UnreferencedShape
-[NOTE] ns.foo#BadErrorMultipleBindings: The structure shape is not connected to from any service shape. | UnreferencedShape
 [ERROR] ns.foo#GInput$b: Trait `httpHeader` cannot be applied to `ns.foo#GInput$b`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(boolean, number, string, timestamp, list > member > :test(boolean, number, string, timestamp))) | TraitTarget
 [ERROR] ns.foo#GOutput$b: Trait `httpHeader` cannot be applied to `ns.foo#GOutput$b`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(boolean, number, string, timestamp, list > member > :test(boolean, number, string, timestamp))) | TraitTarget
 [ERROR] ns.foo#HInput$a: Trait `httpHeader` cannot be applied to `ns.foo#HInput$a`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(boolean, number, string, timestamp, list > member > :test(boolean, number, string, timestamp))) | TraitTarget
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/linters/emit-each-selector-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/linters/emit-each-selector-validator.errors
index eca0fad78..66999efd6 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/linters/emit-each-selector-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/linters/emit-each-selector-validator.errors
@@ -12,16 +12,13 @@
 [DANGER] ns.foo#Long: Selector capture matched selector: long | long
 [DANGER] ns.foo#Long: Selector capture matched selector: number | number
 [DANGER] ns.foo#Long: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#Long: The long shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#Float: Selector capture matched selector: :is(long, float, boolean) | any
 [DANGER] ns.foo#Float: Selector capture matched selector: float | float
 [DANGER] ns.foo#Float: Selector capture matched selector: number | number
 [DANGER] ns.foo#Float: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#Float: The float shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#Boolean: Selector capture matched selector: :is(long, float, boolean) | any
 [DANGER] ns.foo#Boolean: Selector capture matched selector: boolean | boolean
 [DANGER] ns.foo#Boolean: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#Boolean: The boolean shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#Blob: Selector capture matched selector: [trait|mediaType$='plain'] | traitEndsWith
 [DANGER] ns.foo#Blob: Selector capture matched selector: [trait|mediaType*='PLAIN' i] | traitContainsCaseInsensitive
 [DANGER] ns.foo#Blob: Selector capture matched selector: [trait|mediaType*='plain'] | traitContains
@@ -33,10 +30,8 @@
 [DANGER] ns.foo#Blob: Selector capture matched selector: [trait|smithy.api#mediaType*=plain] | traitContains
 [DANGER] ns.foo#Blob: Selector capture matched selector: blob | blob
 [DANGER] ns.foo#Blob: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#Blob: The blob shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#List: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not
 [DANGER] ns.foo#List: Selector capture matched selector: list | list
-[NOTE] ns.foo#List: The list shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#List$member: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not
 [DANGER] ns.foo#List$member: Selector capture matched selector: :test(member > [id='ns.foo#String']) | memberTargetsString
 [DANGER] ns.foo#List$member: Selector capture matched selector: > | valid-neighbor-only
@@ -90,8 +85,6 @@
 [DANGER] ns.foo#OperationBInput$id: Selector capture matched selector: > | valid-neighbor-only
 [DANGER] ns.foo#OperationBInput$id: Selector capture matched selector: member | member
 [DANGER] ns.foo#UtcTimestamp: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#UtcTimestamp: The timestamp shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] other.ns#String: Selector capture matched selector: [id|name="String"] | shapeName
 [DANGER] other.ns#String: Selector capture matched selector: [id|name='String'] | shapeName
 [DANGER] other.ns#String: Selector capture matched selector: simpleType | simpleType
-[NOTE] other.ns#String: The string shape is not connected to from any service shape. | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.errors
deleted file mode 100644
index caa3aadb1..000000000
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.errors
+++ /dev/null
@@ -1,2 +0,0 @@
-[NOTE] ns.foo#UnreferencedString: The string shape is not connected to from any service shape. | UnreferencedShape
-[NOTE] ns.foo#UnreferencedStructure: The structure shape is not connected to from any service shape. | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.json
deleted file mode 100644
index a634b841e..000000000
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.json
+++ /dev/null
@@ -1,76 +0,0 @@
-{
-    "smithy": "2.0",
-    "shapes": {
-        "ns.foo#MyService": {
-            "type": "service",
-            "version": "2017-01-17",
-            "operations": [
-                {
-                    "target": "ns.foo#ReferencedOperation"
-                }
-            ]
-        },
-        "ns.foo#ReferencedOperation": {
-            "type": "operation",
-            "input": {
-                "target": "ns.foo#ReferencedOperationInput"
-            },
-            "output": {
-                "target": "ns.foo#ReferencedOperationOutput"
-            },
-            "errors": [
-                {
-                    "target": "ns.foo#ReferencedOperationError"
-                }
-            ],
-            "traits": {
-                "smithy.api#readonly": {}
-            }
-        },
-        "ns.foo#ReferencedOperationInput": {
-            "type": "structure",
-            "members": {
-                "int": {
-                    "target": "ns.foo#ReferencedInteger"
-                }
-            },
-            "traits": {
-                "smithy.api#input": {}
-            }
-        },
-        "ns.foo#ReferencedOperationOutput": {
-            "type": "structure",
-            "members": {
-                "float": {
-                    "target": "ns.foo#ReferencedFloat"
-                }
-            },
-            "traits": {
-                "smithy.api#output": {}
-            }
-        },
-        "ns.foo#ReferencedInteger": {
-            "type": "integer"
-        },
-        "ns.foo#ReferencedOperationError": {
-            "type": "structure",
-            "traits": {
-                "smithy.api#error": "client"
-            }
-        },
-        "ns.foo#ReferencedFloat": {
-            "type": "float"
-        },
-        "ns.foo#UnreferencedStructure": {
-            "type": "structure",
-            "members": {
-                "foo": {
-                    "target": "ns.foo#UnreferencedString"
-                }
-            }
-        },
-        "ns.foo#UnreferencedString": {
-            "type": "string"
-        }
-    }
-}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.errors
new file mode 100644
index 000000000..a36014120
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.errors
@@ -0,0 +1,2 @@
+[WARNING] smithy.example#Baz: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `[trait|smithy.example#root]` | UnreferencedShape
+[WARNING] smithy.example#Bam: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `[trait|smithy.example#root]` | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.smithy
new file mode 100644
index 000000000..960b44e40
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.smithy
@@ -0,0 +1,37 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+        // Elevate the severity so that NOTE events don't slip through.
+        severity: "WARNING"
+        configuration: {
+            rootShapeSelector: "[trait|smithy.example#root]"
+        }
+    }
+]
+
+namespace smithy.example
+
+// Considered referenced because it's a trait.
+@trait
+structure root {}
+
+// Considered referenced because of the root trait.
+@root
+structure MyAbc {
+    MyString: MyString
+}
+
+// Considered referenced because it's referenced by MyAbc$MyString
+string MyString
+
+// Considered referenced because of the root trait.
+@root
+string MyDef
+
+// Unreferenced.
+string Baz
+
+// Unreferenced.
+string Bam
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.errors
new file mode 100644
index 000000000..09f4ca5b1
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.errors
@@ -0,0 +1,1 @@
+[ERROR] -: Error creating `UnreferencedShape` validator: Error parsing | Model
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.smithy
new file mode 100644
index 000000000..84c0132c8
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.smithy
@@ -0,0 +1,10 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+        configuration: {
+            rootShapeSelector: "!!!!"
+        }
+    }
+]
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.errors
new file mode 100644
index 000000000..34611ace1
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.errors
@@ -0,0 +1,1 @@
+[NOTE] smithy.example#Baz: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.smithy
new file mode 100644
index 000000000..4110b262c
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.smithy
@@ -0,0 +1,29 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+    }
+]
+
+namespace smithy.example
+
+// A "root" shape, so does not emit.
+service MyService {
+    operations: [
+        MyOperation
+    ]
+}
+
+// Connected.
+operation MyOperation {
+    input := {
+        foo: MyString
+    }
+}
+
+// Connected.
+string MyString
+
+// Not connected.
+string Baz
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.errors
new file mode 100644
index 000000000..eabbbc6e9
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.errors
@@ -0,0 +1,2 @@
+[NOTE] smithy.example#Baz: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
+[NOTE] smithy.example#Bam: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.smithy
new file mode 100644
index 000000000..76bb6cf8a
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.smithy
@@ -0,0 +1,48 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+        configuration: {
+            rootShapeSelector: "service"
+        }
+    }
+]
+
+namespace smithy.example
+
+service MyService1 {
+    operations: [
+        MyOperation1
+    ]
+}
+
+service MyService2 {
+    operations: [
+        MyOperation2
+    ]
+}
+
+operation MyOperation1 {
+    input := {
+        foo: MyAbc
+    }
+}
+
+operation MyOperation2 {
+    output := {
+        bar: MyDef
+    }
+}
+
+structure MyAbc {
+    MyString: MyString
+}
+
+string MyString
+
+string MyDef
+
+string Baz
+
+string Bam
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.errors
new file mode 100644
index 000000000..eabbbc6e9
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.errors
@@ -0,0 +1,2 @@
+[NOTE] smithy.example#Baz: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
+[NOTE] smithy.example#Bam: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.smithy
new file mode 100644
index 000000000..8b6a3f5e1
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.smithy
@@ -0,0 +1,39 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+        configuration: {
+            rootShapeSelector: "service"
+        }
+    }
+]
+
+namespace smithy.example
+
+service MyService {
+    operations: [
+        MyOperation
+    ]
+}
+
+operation MyOperation {
+    input := {
+        foo: MyAbc
+    }
+    output := {
+        bar: MyDef
+    }
+}
+
+structure MyAbc {
+    MyString: MyString
+}
+
+string MyString
+
+string MyDef
+
+string Baz
+
+string Bam
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/multiple-inheritance-with-introduction.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/multiple-inheritance-with-introduction.smithy
new file mode 100644
index 000000000..df7f64952
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/multiple-inheritance-with-introduction.smithy
@@ -0,0 +1,22 @@
+$version: "2"
+
+namespace com.example
+
+@mixin
+structure MixinA {
+    @pattern("foo")
+    @required
+    member: String
+}
+
+@mixin
+structure MixinB {
+    @pattern("bar")
+    @internal
+    member: String
+}
+
+structure FinalStructure with [MixinA, MixinB] {
+    @pattern("baz")
+    member: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/validation-event-decorator-test.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/validation-event-decorator-test.smithy
new file mode 100644
index 000000000..9230df426
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/validation-event-decorator-test.smithy
@@ -0,0 +1,13 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@deprecated
+string MyString
+
+string OtherString
+
+structure Foo {
+    a: MyString
+    b: OtherString
+}
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
index 9a39712d8..0e4d4b241 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
@@ -9,7 +9,7 @@ metadata validators = [
         configuration: {
             selector: """
                 operation -[input]-> structure > member
-                :test(member > string:not([trait|enum]))
+                    :test(member > string:not([trait|enum]))
                 :test(member > string:not([trait|length]))
                 :test(member > string:not([trait|aws.api#arnReference]))
                 :test(member > string:not([trait|aws.api#providesPassRole]))
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
index 899acc3ee..242e7b96f 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
@@ -2,6 +2,7 @@ $version: "2.0"
 
 namespace smithy.example
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Foo.
@@ -9,6 +10,7 @@ namespace smithy.example
 )
 string Foo
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Bar.
@@ -17,6 +19,7 @@ string Foo
 )
 string Bar
 
+// The quotes and text will be indented inside the parens when formatted.
 @documentation(
     """
     This is the documentation for Baz.
@@ -25,10 +28,61 @@ string Bar
 )
 string Baz
 
+// The opening quotes will be indented and move to the next line, the contents of the block will remain indented
+// because indentation is based on the closing quote, the closing quote will be indented, and the closing paren on
+// the next line.
 @documentation(
     """
-    This is the documentation for Bux.
-    Lorem ipsum dolor.
+        This is the documentation for Bux.
+        Lorem ipsum dolor.
     """
 )
 string Bux
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on last line, and the closing paren will move to the next line.
+@documentation(
+    """
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }"""
+)
+string JsonDocsNoTrailingNewLine
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on its own line, and the closing paren will move to the next line.
+@documentation(
+    """
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }
+    """
+)
+string JsonDocsTrailingNewLine
+
+// Ensure extra newlines at the end are included.
+@documentation(
+    """
+    Hello with extra newlines.
+
+
+    """
+)
+string ExtraTrailingNewlines
+
+// Moves the quotes to the same indentation level and the closing parens to the next line.
+@documentation(
+    """
+    """
+)
+string EmptyTextBlock
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
index 834b3579b..2527d7609 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
@@ -2,6 +2,7 @@ $version: "2.0"
 
 namespace smithy.example
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Foo.
@@ -9,6 +10,7 @@ namespace smithy.example
 )
 string Foo
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Bar.
@@ -17,6 +19,7 @@ string Foo
 )
 string Bar
 
+// The quotes and text will be indented inside the parens when formatted.
 @documentation(
 """
 This is the documentation for Baz.
@@ -25,8 +28,51 @@ Lorem ipsum dolor.
 )
 string Baz
 
+// The opening quotes will be indented and move to the next line, the contents of the block will remain indented
+// because indentation is based on the closing quote, the closing quote will be indented, and the closing paren on
+// the next line.
 @documentation("""
     This is the documentation for Bux.
     Lorem ipsum dolor.
 """)
 string Bux
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on last line, and the closing paren will move to the next line.
+@documentation("""
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }""")
+string JsonDocsNoTrailingNewLine
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on its own line, and the closing paren will move to the next line.
+@documentation("""
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }
+    """)
+string JsonDocsTrailingNewLine
+
+// Ensure extra newlines at the end are included.
+@documentation("""
+    Hello with extra newlines.
+
+
+    """)
+string ExtraTrailingNewlines
+
+// Moves the quotes to the same indentation level and the closing parens to the next line.
+@documentation("""
+    """)
+string EmptyTextBlock
diff --git a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java
index af0faf970..c87738c88 100644
--- a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java
+++ b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java
@@ -23,8 +23,10 @@ import static org.hamcrest.Matchers.startsWith;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -982,6 +984,26 @@ public class CodeWriterTest {
         assertThat(a.toString(), equalTo("\n"));
     }
 
+    @Test
+    public void flattensStatesOfOtherWhenCopying() {
+        CodeWriter a = new CodeWriter();
+        a.pushState();
+        a.putFormatter('A', (value, indent) -> value.toString().toUpperCase(Locale.ENGLISH));
+        a.putContext("a", 1);
+        a.putContext("b", "B");
+        a.pushState();
+        a.putContext("b", 2); // override "b"
+        a.putFormatter('B', (value, indent) -> value.toString().toLowerCase(Locale.ENGLISH));
+
+        CodeWriter b = new CodeWriter();
+        b.putContext("c", 3);
+        b.copySettingsFrom(a);
+        b.writeInline("${1A} ${2B} ${a:L} ${b:L} ${c:L}", "Hello", "Goodbye");
+
+        assertThat(b.toString(), equalTo("HELLO goodbye 1 2 3\n"));
+        assertThat(a.toString(), equalTo("\n"));
+    }
+
     @Test
     public void canPassRunnableToFormatters() {
         CodeWriter writer = new CodeWriter();
@@ -1451,6 +1473,34 @@ public class CodeWriterTest {
         assertThat(writer.toString(), equalTo("foo: ok actually\n"));
     }
 
+    @Test
+    public void canAccessStateClassesOfInheritedStates() {
+        SimpleCodeWriter writer = new SimpleCodeWriter();
+        writer.putContext("a", "A");
+        writer.pushState();
+        writer.pushState(new CustomSection());
+        writer.putContext("b", "B");
+        writer.pushState();
+        writer.write("${a:L}, ${b:L}, ${hello:L}${partial:C|}");
+        writer.popState();
+
+        assertThat(writer.toString(), equalTo("A, B, Hello, 0T, 1T, 2T\n"));
+    }
+
+    @Test
+    public void canRemoveContextFromTheCurrentState() {
+        SimpleCodeWriter writer = new SimpleCodeWriter();
+        writer.putContext("a", "A");
+        writer.pushState();
+        writer.putContext("b", "B");
+        writer.removeContext("a");
+
+        writer.write("${a:L}, ${b:L}");
+        writer.popState();
+
+        assertThat(writer.toString(), equalTo(", B\n"));
+    }
+
     private static final class MySection implements CodeSection {
         public String getFoo() {
             return "foo";
@@ -1496,4 +1546,25 @@ public class CodeWriterTest {
             return false;
         }
     }
+
+    public static final class CustomSection implements CodeSection {
+        public String hello() {
+            return "Hello";
+        }
+
+        public List<String> values() {
+            return ListUtils.of("a", "b", "c");
+        }
+
+        public String test() {
+            return "T";
+        }
+
+        public Consumer<AbstractCodeWriter<?>> partial() {
+            // This partial ensures that nested contexts can reach up to parent contexts and sections to get values.
+            return writer -> {
+                writer.write("${#values}, ${key:L}${test:L}${/values}");
+            };
+        }
+    }
 }