Repository: smithy-lang/smithy
Pull request #2020 (2023-10-25 22:05:48)

Correct issues and bugs with neighbor traversal
This commit fixes a few fundamental issues with how neighbors and model graph traversal was implemented. These changes are bug fixes and correctness fixes, though they could potentially impact existing selectors of a very small set of users (e.g., there is no known impact to Amazon's very large corpus of Smithy models).

The theme of this change is more accurately exposing the Smithy model as a graph, making graph traversal unambiguous. There are a few major issues addressed in this CR.

1. The bound relationship is no longer emitted or documented. It can still be used if referred to explicitly in a directed neighbor selector.
2. instanceOperation relationship is now "synthetic", deprecated, and not documented. It is no longer emitted from a NeighborVisitor but can be used if referred to explicitly in a directed neighbor selector.
3. The operation relationship now only includes operations bound to resources and service via the operations property.
4. The collectionOperation relationship now only includes operations bound to a resource vai the collectionOperations property.
5. Recursive neighbor traversal from a member no longer includes the containing shape.

Further background and justification can be found below.

**Bound relationships are confusing and buggy**

Smithy today documents a “bound” relationship as:

> resource → bound: The service or resource to which the resource is bound.
> operation → bound: The service or resource to which the operation is bound.

However, the implementation of bound is inconsistent and buggy, and worse, the idea itself is confusing: there should be no relationship from a shape to shapes that refer to it. The only reason the “bound” relationship exists is that we added it before reverse neighbor selectors were added to Smithy.

Consider the following model that is commented with each neighbor we previously emitted:

```
$version: "2.0"

namespace smithy.example

// [Relationship shape="smithy.example#Foo" type="RESOURCE" neighbor="smithy.example#Bar"]
// [Relationship shape="smithy.example#Bar" type="BOUND" neighbor="smithy.example#Foo"]
service Foo {
    resources: [Bar]
}

// [Relationship shape="smithy.example#Bar" type="OPERATION" neighbor="smithy.example#GetBar"]
// [Relationship shape="smithy.example#Bar" type="READ" neighbor="smithy.example#GetBar"]
// [Relationship shape="smithy.example#Bar" type="INSTANCE_OPERATION" neighbor="smithy.example#GetBar"]
// [Relationship shape="smithy.example#GetBar" type="BOUND" neighbor="smithy.example#Bar"]
// [Relationship shape="smithy.example#Foo" type="RESOURCE" neighbor="smithy.example#Bar"]
// [Relationship shape="smithy.example#Bar" type="BOUND" neighbor="smithy.example#Foo"]
resource Bar {
    read: GetBar
}

// [Relationship shape="smithy.example#GetBar" type="INPUT" neighbor="smithy.example#GetBarInput"]
@readonly
operation GetBar {
    // [Relationship shape="smithy.example#GetBarInput" type="STRUCTURE_MEMBER" neighbor="smithy.example#GetBarInput$greeting"]
    input := {
        // [Relationship shape="smithy.example#GetBarInput$greeting" type="MEMBER_CONTAINER" neighbor="smithy.example#GetBarInput"]
        // [Relationship shape="smithy.example#GetBarInput$greeting" type="MEMBER_TARGET" neighbor="smithy.example#Greeting"]
        greeting: Greeting
    }
}

// No relationships
string Greeting
```

Let’s walk through each shape and document what the relationships currently are, what they should be according to documentation, and what they are after removing BOUND.

Previously, we emitted:

- Foo -[resource]→ Bar
  - This is correct.
- Bar -[bound]→ Foo
  - This is wrong in both docs and the ideal. Emitting the bound relationship here is wrong since the documentation states it’s between the resource and a service, so looking at relationships from a service should not emit BOUND.

This shape now only emits:

- Foo -[resource]→ Bar

Previously, we emitted:

- Bar -[read]→ GetBar
  - This is correct and expected.
- Bar -[instanceOperation]→ GetBar
  - This is correct, though it should be a “synthetic” relationship that selectors know about but not actually emitted from `NeighborVisitor`.
- Bar -[operation]→ GetBar
  - This is correct according to the current documentation, but is confusing because we don’t know if the operation was bound using read or the “operations” property. (see next topic).
- Foo -[resource]→ Bar
  - This is completely wrong and not supported by documentation. It’s unquestionably a bug.
- GetBar -[bound]→ Bar
  - This is wrong since according to the documentation, this relationship should come from GetBar not Bar.
- Bar -[bound]→ Foo
  - This is correct according to the documentation, but shouldn’t be emitted at all ideally since this shape has no defined edges from it to Foo and these kinds of inverted edges are confusing.

This shape now only emits:

- Bar -[read]→ GetBar

Today, we emit (and continue to emit after this change):

- GetBar -[input] → GetBarInput

According to documentation, we should emit:

- GetBar -[input]→ GetBarInput
- GetBar -[bound] → Bar

However, given bound relationships are buggy and counter-intuitive, we will continue to not emit a bound relationship here.

Because these neighbors are implemented so inconsistently, using them with selectors is confusing and buggy to the point of the feature being unusable.

This selector works as expected and sees that `GetBar` is connected to `Bar` using a `READ` relationship.

```
operation <
```

Returns:

- smithy.example#Bar

The documentation says that operations have a BOUND relationship to any resource or service they are bound to, but that doesn't work today:

```
operation -[bound]->
```

Returns: []

```
operation <-[bound]-
```

Returns []

If we check if a resource has a `bound` relationship from the operation, it surprisingly works. This is very wrong considering there's no edge found from the operation to the resource, but the resource can find the edge (this is due to not implementing the reverse lookups in neighbor provider).

```
resource <-[bound]-
```

Returns:

- smithy.example#GetBar

Strangely, if we check the bound relationships from resources, we get the resource itself back:

```
resource -[bound]->
```

Returns:

- smithy.example#Bar
  - No resource is bound to another resource in this model, so that's clearly wrong. What's happening here is that due to a bug, the resource emits a bound relationship back to itself.
- smithy.example#Foo

Just getting all edges using ">" and "<" returns the same results:

```
operation >
```

Incorrectly (according to docs previously) returns:

- smithy.example#GetBarInput

```
operation <
```

Correctly (according to the docs previously) returns:

- smithy.example#Bar

However, getting all directed edges of a resource is strange:

```
resource >
```

Returns:

- smithy.example#Bar
  - Bad, and due to the BOUND edge from the service to the resource
- smithy.example#Foo
  - Strange, given the resource doesn't have a direct edge to the service other than BOUND.
- smithy.example#GetBar

Getting even weirder due to bugs in the current implementation is getting reverse edges of a resource:

```
resource <
```

Returns:

- smithy.example#Foo (good, the service refers to it)
- smithy.example#GetBar (bad, because there is no corresponding edge when you get directed neighbors from GetBar)

Making this all more confusing, using a Walker will include service shapes a resource is bound to:

```
walker.walkShapeIds(model.expectShape(ShapeId.from("smithy.example#Bar")))
```

Or, using a selector:

```
resource ~>
```

Returns:

- smithy.example#Foo (bad since it traversed up)
- smithy.example#Bar ✅ (walker always includes the given shape)
- smithy.example#GetBar ✅
- smithy.example#GetBarInput ✅
- smithy.example#GetBarInput$greeting ✅
- smithy.example#Greeting ✅

However, if we check `GetBar`, we don't crawl up to the resource! (this is actually better behavior but different from the above).

```
operation ~>
```

Returns:

- smithy.example#GetBar
- smithy.example#GetBarInput
- smithy.example#GetBarInput$greeting
- smithy.example#Greeting

**Operation relationships were ambiguous**

When it comes to traversing the graph of shapes to operations, the current implementation leaves things ambiguous. We emit the following kinds of relationships today (omitting BOUND relationships since they’re covered above):

- Service “operations”
  - Service → OPERATION → Operation shape
- Resource “operations”
  - Resource → OPERATION → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
- Resource “collectionOperations”
  - Resource → OPERATION → Operation shape
  - Resource → COLLECTION_OPERATION → Operation shape
- Resource “create”
  - Resource → CREATE → Operation shape
  - Resource → COLLECTION_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “list”
  - Resource → LIST → Operation shape
  - Resource → COLLECTION_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “put”
  - Resource → PUT → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “read”
  - Resource → READ → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “update”
  - Resource → UPDATE → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “delete”
  - Resource → DELETE → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape

These relationships are problematic for several reasons:

- Emitting an OPERATION relationship for every operation makes it impossible to determine how an operation was bound to a resource. Was it bound through a lifecycle? Through “operations”? Through “collectionOperations”?
- Emitting an INSTANCE_OPERATION relationship is unnecessary and could become a synthetic kind of relationship (that is, rather than actually emit it, just implement special handling for `resource -[instanceOperation]->`.
- Emitting COLLECTION_OPERATION for `list` and `create` is problematic because you can’t tell if everything returned from `resource -[collectionOperation]->` was bound through `collectionOperations` or not without convoluted `:not` clauses.

We now only emit the following events from resources and services for operation bindings:

- Service “operations”
  - Service → OPERATION → Operation shape
- Resource “operations”
  - Resource → OPERATION → Operation shape
- Resource “collectionOperations”
  - Resource → COLLECTION_OPERATION → Operation shape
- Resource “create”
  - Resource → CREATE → Operation shape
- Resource “list”
  - Resource → LIST → Operation shape
- Resource “put”
  - Resource → PUT → Operation shape
- Resource “read”
  - Resource → READ → Operation shape
- Resource “update”
  - Resource → UPDATE → Operation shape
- Resource “delete”
  - Resource → DELETE → Operation shape

The "instanceOperation" relationship is now deprecated, no longer emitted, and no longer documented. Because "collectionOperation" is now fixed to only include operations actually bound through "collectionOperations" it would be confusing to have "instanceOperation" act as a synthetic grouping of relationships while "collectionOperation" does not, despite being the other kind of resource binding than instance operations.

You can still refer to it using `-[instanceOperation]->` but a relationship isn't emitted via NeighborVisitor.

**Recursive member traversal crawled up to its container**

When you use a recursive neighbor selector on a member shape, it crawls up to the containing shape. This is wrong because it makes getting the closure of a single member is impossible. For example:

```
structure Foo {
    bar: String
    baz: Integer
}
```

Today, using `member [id|member = 'bar'] ~>` would return:

- Foo
- Bar
- Baz
- String
- Integer

The intended result was just `String`. The previous result was wrong because the edge between the member and its container wasn't filtered out of recursive neighbor traversal, causing all shapes in the containing shape to be returned in the closure.

*Issue #, if available:*

*Description of changes:*


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/docs/_templates/base.html b/docs/_templates/base.html
new file mode 100644
index 000000000..da343cb73
--- /dev/null
+++ b/docs/_templates/base.html
@@ -0,0 +1,6 @@
+{% extends "!base.html" %}
+
+{%- block extrahead -%}
+<script src="https://a0.awsstatic.com/s_code/js/3.0/awshome_s_code.js"></script>
+<script src="https://d2c.aws.amazon.com/client/loader/v1/d2c-load.js"></script>
+{%- endblock -%}
diff --git a/docs/source-1.0/spec/core/selectors.rst b/docs/source-1.0/spec/core/selectors.rst
index ab5fd940c..aa52d00bf 100644
--- a/docs/source-1.0/spec/core/selectors.rst
+++ b/docs/source-1.0/spec/core/selectors.rst
@@ -1151,61 +1151,50 @@ The table below lists the labeled directed relationships from each shape.
       - Description
     * - service
       - operation
-      - Each operation that is bound to a service.
+      - Each operation bound to a service.
     * - service
       - resource
-      - Each resource that is bound to a service.
+      - Each resource bound to a service.
     * - service
       - error
-      - Each error structure referenced by the service (if present).
+      - Each error structure referenced by the service.
     * - resource
       - identifier
-      - The identifier referenced by the resource (if specified).
+      - Each identifier shape of a resource.
     * - resource
-      - operation
-      - Each operation that is bound to a resource through the
-        "operations", "create", "put", "read", "update", "delete", and "list"
-        properties.
+      - property
+      - Each property shape of a resource.
     * - resource
-      - instanceOperation
-      - Each operation that is bound to a resource through the
-        "operations", "put", "read", "update", and "delete" properties.
+      - resource
+      - Each resource bound to a resource.
     * - resource
-      - collectionOperation
-      - Each operation that is bound to a resource through the
-        "collectionOperations", "create", and "list" properties.
+      - operation
+      - Each operation bound to a resource through the "operations" property.
     * - resource
-      - resource
-      - Each resource that is bound to a resource.
+      - collectionOperation
+      - Each operation bound to a resource through the "collectionOperations"
+        property.
     * - resource
       - create
-      - The operation referenced by the :ref:`create-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`create-lifecycle` of a resource.
     * - resource
       - read
-      - The operation referenced by the :ref:`read-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`read-lifecycle` of a resource.
     * - resource
       - update
-      - The operation referenced by the :ref:`update-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`update-lifecycle` of a resource.
     * - resource
       - delete
-      - The operation referenced by the :ref:`delete-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`delete-lifecycle` of a resource.
     * - resource
       - list
-      - The operation referenced by the :ref:`list-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`list-lifecycle` of a resource.
     * - resource
-      - bound
-      - The service or resource to which the resource is bound.
-    * - operation
-      - bound
-      - The service or resource to which the operation is bound.
+      - put
+      - The operation defined as the :ref:`put-lifecycle` of a resource.
     * - operation
       - input
-      - The input structure of the operation (if present).
+      - The input structure of an operation.
 
         .. note::
 
@@ -1214,7 +1203,7 @@ The table below lists the labeled directed relationships from each shape.
 
     * - operation
       - output
-      - The output structure of the operation (if present).
+      - The output structure of an operation.
 
         .. note::
 
@@ -1223,23 +1212,25 @@ The table below lists the labeled directed relationships from each shape.
 
     * - operation
       - error
-      - Each error structure referenced by the operation (if present).
+      - Each error structure of an operation.
     * - list
       - member
-      - The :ref:`member` of the list. Note that this is not the shape targeted
-        by the member.
+      - The :ref:`member <member>` of a list.
     * - map
       - member
-      - The key and value members of the map. Note that these are not the
-        shapes targeted by the member.
+      - The key and value members of a map.
     * - structure
       - member
-      - Each structure member. Note that these are not the shapes targeted by
-        the members.
+      - Each structure member.
     * - union
       - member
-      - Each union member. Note that these are not the shapes targeted by
-        the members.
+      - Each union member.
+    * - enum
+      - member
+      - Each enum member.
+    * - intEnum
+      - member
+      - Each intEnum member.
     * - member
       -
       - The shape targeted by the member. Note that member targets have no
@@ -1249,13 +1240,13 @@ The table below lists the labeled directed relationships from each shape.
       - Each trait applied to a shape. The neighbor shape is the shape that
         defines the trait. This kind of relationship is only traversed if the
         ``trait`` relationship is explicitly stated as a desired directed
-        neighbor relationship type.
+        neighbor relationship type (for example, ``-[trait]->``).
 
-.. important::
+.. note::
 
-    Implementations MUST tolerate parsing unknown relationship types. When
-    evaluated, the directed traversal of unknown relationship types yields
-    no shapes.
+    Implementations MAY tolerate parsing unknown relationship types. When
+    evaluated, the traversal of unknown relationship types SHOULD yield
+    nothing.
 
 
 Functions
@@ -1300,7 +1291,7 @@ no documentation:
 
 .. code-block:: none
 
-    :test(-[bound, resource]->)
+    :test(resource >)
     :not([trait|documentation])
 
 
diff --git a/docs/source-2.0/aws/aws-iam.rst b/docs/source-2.0/aws/aws-iam.rst
index 06b70d2f1..5ba48d570 100644
--- a/docs/source-2.0/aws/aws-iam.rst
+++ b/docs/source-2.0/aws/aws-iam.rst
@@ -221,10 +221,47 @@ Condition keys in IAM policies can be evaluated with `condition operators`_.
 Summary
     Declares that the condition keys of a resource should not be inferred.
 Trait selector
-    ``resource``
+    ``:test(service, resource)``
 Value type
     Annotation trait
 
+When a service is marked with the ``aws.iam#disableConditionKeyInference``
+trait, all the resources bound to the service will not have condition
+keys automatically inferred from its identifiers and the identifiers
+of its ancestors.
+
+The following example shows resources ``MyResource1`` and ``MyResource2``
+have had condition key inference disabled because they are bound to a
+service marked with ``aws.iam#disableConditionKeyInference`` trait.
+
+.. code-block:: smithy
+
+    $version: "2"
+
+    namespace smithy.example
+
+    use aws.api#service
+    use aws.iam#disableConditionKeyInference
+
+    @service(sdkId: "My Value", arnNamespace: "myservice")
+    @disableConditionKeyInference
+    service MyService {
+        version: "2017-02-11"
+        resources: [MyResource1, MyResource2]
+    }
+
+    resource MyResource1 {
+        identifiers: {
+            foo: String
+        }
+    }
+
+    resource MyResource2 {
+        identifiers: {
+            foo: String
+        }
+    }
+
 A resource marked with the ``aws.iam#disableConditionKeyInference`` trait will
 not have its condition keys automatically inferred from its identifiers and
 the identifiers of its ancestors (if present.)
diff --git a/docs/source-2.0/spec/selectors.rst b/docs/source-2.0/spec/selectors.rst
index 4282c5bb1..1fa9f99a0 100644
--- a/docs/source-2.0/spec/selectors.rst
+++ b/docs/source-2.0/spec/selectors.rst
@@ -999,9 +999,8 @@ Forward undirected neighbor
 ----------------------------
 
 A :token:`forward undirected neighbor <selectors:SelectorForwardUndirectedNeighbor>`
-(``>``) yields every shape that is connected to the current shape. For
-example, the following selector matches the key and value members of
-every map:
+(``>``) yields every shape referred to by the current shape. For example, the
+following selector matches the key and value members of every map:
 
 .. code-block:: none
 
@@ -1020,7 +1019,7 @@ Forward directed neighbors
 
 The forward undirected neighbor selector (``>``) is an *undirected* edge
 traversal. Sometimes, a directed edge traversal is necessary. For example,
-the following selector matches the "bound", "input", "output", and "error"
+the following selector matches the "input", "output", "error", and "mixin"
 relationships of each operation:
 
 .. code-block:: none
@@ -1159,64 +1158,50 @@ The table below lists the labeled directed relationships from each shape.
       - Description
     * - service
       - operation
-      - Each operation that is bound to a service.
+      - Each operation bound to a service.
     * - service
       - resource
-      - Each resource that is bound to a service.
+      - Each resource bound to a service.
     * - service
       - error
-      - Each error structure referenced by the service (if present).
+      - Each error structure referenced by the service.
     * - resource
       - identifier
-      - An identifier referenced by the resource (if specified).
+      - Each identifier shape of a resource.
     * - resource
       - property
-      - A property referenced by the resource.
+      - Each property shape of a resource.
     * - resource
-      - operation
-      - Each operation that is bound to a resource through the
-        "operations", "create", "put", "read", "update", "delete", and "list"
-        properties.
+      - resource
+      - Each resource bound to a resource.
     * - resource
-      - instanceOperation
-      - Each operation that is bound to a resource through the
-        "operations", "put", "read", "update", and "delete" properties.
+      - operation
+      - Each operation bound to a resource through the "operations" property.
     * - resource
       - collectionOperation
-      - Each operation that is bound to a resource through the
-        "collectionOperations", "create", and "list" properties.
-    * - resource
-      - resource
-      - Each resource that is bound to a resource.
+      - Each operation bound to a resource through the "collectionOperations"
+        property.
     * - resource
       - create
-      - The operation referenced by the :ref:`create-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`create-lifecycle` of a resource.
     * - resource
       - read
-      - The operation referenced by the :ref:`read-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`read-lifecycle` of a resource.
     * - resource
       - update
-      - The operation referenced by the :ref:`update-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`update-lifecycle` of a resource.
     * - resource
       - delete
-      - The operation referenced by the :ref:`delete-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`delete-lifecycle` of a resource.
     * - resource
       - list
-      - The operation referenced by the :ref:`list-lifecycle` property of
-        a resource (if present).
+      - The operation defined as the :ref:`list-lifecycle` of a resource.
     * - resource
-      - bound
-      - The service or resource to which the resource is bound.
-    * - operation
-      - bound
-      - The service or resource to which the operation is bound.
+      - put
+      - The operation defined as the :ref:`put-lifecycle` of a resource.
     * - operation
       - input
-      - The input structure of the operation (if present).
+      - The input structure of an operation.
 
         .. note::
 
@@ -1225,7 +1210,7 @@ The table below lists the labeled directed relationships from each shape.
 
     * - operation
       - output
-      - The output structure of the operation (if present).
+      - The output structure of an operation.
 
         .. note::
 
@@ -1234,23 +1219,25 @@ The table below lists the labeled directed relationships from each shape.
 
     * - operation
       - error
-      - Each error structure referenced by the operation (if present).
+      - Each error structure of an operation.
     * - list
       - member
-      - The :ref:`member` of the list, including if it was inherited from a
-        mixin. Note that this is not the shape targeted by the member.
+      - The :ref:`member <member>` of a list.
     * - map
       - member
-      - The key and value members of the map, including those inherited from
-        mixins. Note that these are not the shapes targeted by the member.
+      - The key and value members of a map.
     * - structure
       - member
-      - Each structure member, including members inherited from mixins. Note
-        that these are not the shapes targeted by the members.
+      - Each structure member.
     * - union
       - member
-      - Each union member, including members inherited from mixins. Note that
-        these are not the shapes targeted by the members.
+      - Each union member.
+    * - enum
+      - member
+      - Each enum member.
+    * - intEnum
+      - member
+      - Each intEnum member.
     * - member
       -
       - The shape targeted by the member. Note that member targets have no
@@ -1260,7 +1247,7 @@ The table below lists the labeled directed relationships from each shape.
       - Each trait applied to a shape. The neighbor shape is the shape that
         defines the trait. This kind of relationship is only traversed if the
         ``trait`` relationship is explicitly stated as a desired directed
-        neighbor relationship type.
+        neighbor relationship type (for example, ``-[trait]->``).
     * - ``*``
       - mixin
       - Every mixin applied to the shape.
@@ -1268,13 +1255,13 @@ The table below lists the labeled directed relationships from each shape.
         .. note::
 
             A normal ``member`` relationship exists from a given shape to all
-            its inherited mixin members.
+            its mixed in members.
 
-.. important::
+.. note::
 
-    Implementations MUST tolerate parsing unknown relationship types. When
-    evaluated, the directed traversal of unknown relationship types yields
-    no shapes.
+    Implementations MAY tolerate parsing unknown relationship types. When
+    evaluated, the traversal of unknown relationship types SHOULD yield
+    nothing.
 
 
 Functions
@@ -1319,8 +1306,7 @@ no documentation:
 
 .. code-block:: none
 
-    :test(-[bound, resource]->)
-    :not([trait|documentation])
+    :test(< resource) :not([trait|documentation])
 
 
 ``:is``
diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndex.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndex.java
index 5648ba664..af5c1ed4b 100644
--- a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndex.java
+++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndex.java
@@ -64,14 +64,14 @@ public final class ConditionKeysIndex implements KnowledgeIndex {
                 service.getResources().stream()
                         .flatMap(id -> OptionalUtils.stream(model.getShape(id)))
                         .forEach(resource -> {
-                            compute(model, service.getId(), arnRoot, resource, null);
+                            compute(model, service, arnRoot, resource, null);
                         });
 
                 // Compute the keys of operations of the service.
                 service.getOperations().stream()
                         .flatMap(id -> OptionalUtils.stream(model.getShape(id)))
                         .forEach(operation -> {
-                            compute(model, service.getId(), arnRoot, operation, null);
+                            compute(model, service, arnRoot, operation, null);
                         });
             });
         });
@@ -153,7 +153,7 @@ public final class ConditionKeysIndex implements KnowledgeIndex {
 
     private void compute(
             Model model,
-            ShapeId service,
+            ServiceShape service,
             String arnRoot,
             Shape subject,
             ResourceShape parent
@@ -163,21 +163,24 @@ public final class ConditionKeysIndex implements KnowledgeIndex {
 
     private void compute(
             Model model,
-            ShapeId service,
+            ServiceShape service,
             String arnRoot,
             Shape subject,
             ResourceShape parent,
             Set<String> parentDefinitions
     ) {
         Set<String> definitions = new HashSet<>(parentDefinitions);
-        resourceConditionKeys.get(service).put(subject.getId(), definitions);
+        resourceConditionKeys.get(service.getId()).put(subject.getId(), definitions);
         subject.getTrait(ConditionKeysTrait.class).ifPresent(trait -> definitions.addAll(trait.getValues()));
 
         // Continue recursing into resources and computing keys.
         subject.asResourceShape().ifPresent(resource -> {
+            boolean disableConditionKeyInference = resource.hasTrait(DisableConditionKeyInferenceTrait.class)
+                        || service.hasTrait(DisableConditionKeyInferenceTrait.class);
+
             // Add any inferred resource identifiers to the resource and to the service-wide definitions.
-            Map<String, String> childIdentifiers = !resource.hasTrait(DisableConditionKeyInferenceTrait.class)
-                    ? inferChildResourceIdentifiers(model, service, arnRoot, resource, parent)
+            Map<String, String> childIdentifiers = !disableConditionKeyInference
+                    ? inferChildResourceIdentifiers(model, service.getId(), arnRoot, resource, parent)
                     : MapUtils.of();
 
             // Compute the keys of each child operation, passing no keys.
diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json
deleted file mode 100644
index 67e616c2b..000000000
--- a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json
+++ /dev/null
@@ -1,299 +0,0 @@
-{
-    "smithy": "2.0",
-    "shapes": {
-        "aws.iam#actionPermissionDescription": {
-            "type": "string",
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": "operation"
-                },
-                "smithy.api#documentation": "A brief description of what granting the user permission to invoke an operation would entail. This description should begin with something similar to 'Enables the user to...' or 'Grants permission to...'"
-            }
-        },
-        "aws.iam#defineConditionKeys": {
-            "type": "map",
-            "key": {
-                "target": "aws.iam#IamIdentifier"
-            },
-            "value": {
-                "target": "aws.iam#ConditionKeyDefinition"
-            },
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": "service"
-                },
-                "smithy.api#documentation": "Defines the set of condition keys that appear within a service in addition to inferred and global condition keys."
-            }
-        },
-        "aws.iam#conditionKeys": {
-            "type": "list",
-            "member": {
-                "target": "aws.iam#IamIdentifier"
-            },
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": ":test(resource, operation)"
-                },
-                "smithy.api#documentation": "Applies condition keys by name to a resource or operation."
-            }
-        },
-        "aws.iam#disableConditionKeyInference": {
-            "type": "structure",
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": "resource"
-                },
-                "smithy.api#documentation": "Disables the automatic inference of condition keys of a resource."
-            }
-        },
-        "aws.iam#requiredActions": {
-            "type": "list",
-            "member": {
-                "target": "aws.iam#IamIdentifier"
-            },
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": "operation"
-                },
-                "smithy.api#documentation": "Other actions that the invoker must be authorized to perform when executing the targeted operation."
-            }
-        },
-        "aws.iam#supportedPrincipalTypes": {
-            "type": "list",
-            "member": {
-                "target": "aws.iam#PrincipalType"
-            },
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": ":test(service, operation)"
-                },
-                "smithy.api#documentation": "The principal types that can use the service or operation."
-            }
-        },
-        "aws.iam#iamResource": {
-            "type": "structure",
-            "members": {
-                "name": {
-                    "target": "smithy.api#String",
-                    "traits": {
-                        "smithy.api#documentation": "The name of the resource in AWS IAM."
-                    }
-                }
-            },
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": "resource"
-                },
-                "smithy.api#documentation": "Indicates properties of a Smithy resource in AWS IAM."
-            }
-        },
-        "aws.iam#IamIdentifier": {
-            "type": "string",
-            "traits": {
-                "smithy.api#private": {},
-                "smithy.api#pattern": "^([A-Za-z0-9][A-Za-z0-9-\\.]{0,62}:[^:]+)$"
-            }
-        },
-        "aws.iam#ConditionKeyDefinition": {
-            "type": "structure",
-            "members": {
-                "documentation": {
-                    "target": "smithy.api#String",
-                    "traits": {
-                        "smithy.api#documentation": "Defines documentation about the condition key."
-                    }
-                },
-                "externalDocumentation": {
-                    "target": "smithy.api#String",
-                    "traits": {
-                        "smithy.api#documentation": "A valid URL that defines more information about the condition key."
-                    }
-                },
-                "type": {
-                    "target": "aws.iam#ConditionKeyType",
-                    "traits": {
-                        "smithy.api#required": {}
-                    }
-                }
-            },
-            "traits": {
-                "smithy.api#private": {},
-                "smithy.api#documentation": "A defined condition key to appear within a service in addition to inferred and global condition keys."
-            }
-        },
-        "aws.iam#ConditionKeyType": {
-            "type": "enum",
-            "members": {
-                "ARN": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "ARN",
-                        "smithy.api#documentation": "A String type that contains an Amazon Resource Name (ARN)."
-                    }
-                },
-                "ARRAY_OF_ARN": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "ArrayOfARN",
-                        "smithy.api#documentation": "An unordered list of ARN types."
-                    }
-                },
-                "BINARY": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "Binary",
-                        "smithy.api#documentation": "A String type that contains base-64 encoded binary data."
-                    }
-                },
-                "ARRAY_OF_BINARY": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "ArrayOfBinary",
-                        "smithy.api#documentation": "An unordered list of Binary types."
-                    }
-                },
-                "STRING": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "String",
-                        "smithy.api#documentation": "A general string type."
-                    }
-                },
-                "ARRAY_OF_STRING": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "ArrayOfString",
-                        "smithy.api#documentation": "An unordered list of String types."
-                    }
-                },
-                "NUMERIC": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "Numeric",
-                        "smithy.api#documentation": "A general type for integers and floats."
-                    }
-                },
-                "ARRAY_OF_NUMERIC": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "ArrayOfNumeric",
-                        "smithy.api#documentation": "An unordered list of Numeric types."
-                    }
-                },
-                "DATE": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "Date",
-                        "smithy.api#documentation": "A String type that conforms to the datetime profile of ISO 8601."
-                    }
-                },
-                "ARRAY_OF_DATE": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "ArrayOfDate",
-                        "smithy.api#documentation": "An unordered list of Date types."
-                    }
-                },
-                "BOOL": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "Bool",
-                        "smithy.api#documentation": "A general boolean type."
-                    }
-                },
-                "ARRAY_OF_BOOL": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "ArrayOfBool",
-                        "smithy.api#documentation": "An unordered list of Bool types."
-                    }
-                },
-                "IP_ADDRESS": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "IPAddress",
-                        "smithy.api#documentation": "A String type that conforms to RFC 4632."
-                    }
-                },
-                "ARRAY_OF_IP_ADDRESS": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "ArrayOfIPAddress",
-                        "smithy.api#documentation": "An unordered list of IPAddress types."
-                    }
-                }
-            },
-            "traits": {
-                "smithy.api#private": {},
-                "smithy.api#documentation": "The IAM policy type of the value that will supplied for this context key"
-            }
-        },
-        "aws.iam#PrincipalType": {
-            "type": "enum",
-            "members": {
-                "ROOT": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "Root",
-                        "smithy.api#documentation": "An AWS account."
-                    }
-                },
-                "IAM_USER": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "IAMUser",
-                        "smithy.api#documentation": "An AWS IAM user."
-                    }
-                },
-                "IAM_ROLE": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "IAMRole",
-                        "smithy.api#documentation": "An AWS IAM role."
-                    }
-                },
-                "FEDERATED_USER": {
-                    "target": "smithy.api#Unit",
-                    "traits": {
-                        "smithy.api#enumValue": "FederatedUser",
-                        "smithy.api#documentation": "A federated user session."
-                    }
-                }
-            },
-            "traits": {
-                "smithy.api#private": {},
-                "smithy.api#documentation": "An IAM policy principal type."
-            }
-        },
-        "aws.iam#actionName": {
-            "type": "string",
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": "operation"
-                },
-                "smithy.api#documentation": "Provides a custom IAM action name. By default, the action name is the same as the operation name."
-            }
-        },
-        "aws.iam#serviceResolvedConditionKeys": {
-            "type": "list",
-            "member": {
-                "target": "aws.iam#IamIdentifier"
-            },
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": "service"
-                },
-                "smithy.api#documentation": "Specifies the list of IAM condition keys which must be resolved by the service, as opposed to being pulled from the request."
-            }
-        },
-        "aws.iam#conditionKeyValue": {
-            "type": "string",
-            "traits": {
-                "smithy.api#trait": {
-                    "selector": "member"
-                },
-                "smithy.api#documentation": "Uses the associated member’s value as this condition key’s value. Needed when the member name doesn't match the condition key name."
-            }
-        }
-    }
-}
diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy
new file mode 100644
index 000000000..3b5f585d8
--- /dev/null
+++ b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy
@@ -0,0 +1,140 @@
+$version: "2.0"
+
+namespace aws.iam
+
+/// Provides a custom IAM action name. By default, the action name is the same as the operation name.
+@trait(selector: "operation")
+string actionName
+
+/// A brief description of what granting the user permission to invoke an operation would entail.
+/// This description should begin with something similar to 'Enables the user to...' or 'Grants permission to...'
+@trait(selector: "operation")
+string actionPermissionDescription
+
+/// Applies condition keys by name to a resource or operation.
+@trait(selector: ":test(resource, operation)")
+list conditionKeys {
+    member: IamIdentifier
+}
+
+/// Uses the associated member’s value as this condition key’s value.
+/// Needed when the member name doesn't match the condition key name.
+@trait(selector: "member")
+string conditionKeyValue
+
+/// Defines the set of condition keys that appear within a service in addition to
+///  inferred and global condition keys.
+@trait(selector: "service")
+map defineConditionKeys {
+    key: IamIdentifier
+    value: ConditionKeyDefinition
+}
+
+/// Disables the automatic inference of condition keys of service's resources or a specific resource.
+@trait(selector: ":test(service, resource)")
+structure disableConditionKeyInference {}
+
+/// Indicates properties of a Smithy resource in AWS IAM.
+@trait(selector: "resource")
+structure iamResource {
+    /// The name of the resource in AWS IAM.
+    name: String
+}
+
+/// Other actions that the invoker must be authorized to perform when executing the targeted operation.
+@trait(selector: "operation")
+list requiredActions {
+    member: IamIdentifier
+}
+
+/// Specifies the list of IAM condition keys which must be resolved by the service,
+/// as opposed to being pulled from the request.
+@trait(selector: "service")
+list serviceResolvedConditionKeys {
+    member: IamIdentifier
+}
+
+/// The principal types that can use the service or operation.
+@trait(selector: ":test(service, operation)")
+list supportedPrincipalTypes {
+    member: PrincipalType
+}
+
+/// A defined condition key to appear within a service in addition to inferred and global condition keys.
+@private
+structure ConditionKeyDefinition {
+    /// Defines documentation about the condition key.
+    documentation: String
+
+    /// A valid URL that defines more information about the condition key.
+    externalDocumentation: String
+
+    @required
+    type: ConditionKeyType
+}
+
+/// The IAM policy type of the value that will supplied for this context key
+@private
+enum ConditionKeyType {
+    /// A String type that contains an Amazon Resource Name (ARN).
+    ARN
+
+    /// An unordered list of ARN types.
+    ARRAY_OF_ARN = "ArrayOfARN"
+
+    /// A String type that contains base-64 encoded binary data.
+    BINARY = "Binary"
+
+    /// An unordered list of Binary types.
+    ARRAY_OF_BINARY = "ArrayOfBinary"
+
+    /// A general string type.
+    STRING = "String"
+
+    /// An unordered list of String types.
+    ARRAY_OF_STRING = "ArrayOfString"
+
+    /// A general type for integers and floats.
+    NUMERIC = "Numeric"
+
+    /// An unordered list of Numeric types.
+    ARRAY_OF_NUMERIC = "ArrayOfNumeric"
+
+    /// A String type that conforms to the datetime profile of ISO 8601.
+    DATE = "Date"
+
+    /// An unordered list of Date types.
+    ARRAY_OF_DATE = "ArrayOfDate"
+
+    /// A general boolean type.
+    BOOL = "Bool"
+
+    /// An unordered list of Bool types.
+    ARRAY_OF_BOOL = "ArrayOfBool"
+
+    /// A String type that conforms to RFC 4632.
+    IP_ADDRESS = "IPAddress"
+
+    /// An unordered list of IPAddress types.
+    ARRAY_OF_IP_ADDRESS = "ArrayOfIPAddress"
+}
+
+@pattern("^([A-Za-z0-9][A-Za-z0-9-\\.]{0,62}:[^:]+)$")
+@private
+string IamIdentifier
+
+/// An IAM policy principal type.
+@private
+enum PrincipalType {
+    /// An AWS account.
+    ROOT = "Root"
+
+    /// An AWS IAM user.
+    IAM_USER = "IAMUser"
+
+    /// An AWS IAM role.
+    IAM_ROLE = "IAMRole"
+
+    /// A federated user session.
+    FEDERATED_USER = "FederatedUser"
+}
diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/manifest b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/manifest
index dac078e3e..b28a484eb 100644
--- a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/manifest
+++ b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/manifest
@@ -1,1 +1,1 @@
-aws.iam.json
+aws.iam.smithy
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/TopDownIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/TopDownIndex.java
index 218bdd1df..b44919708 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/TopDownIndex.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/TopDownIndex.java
@@ -25,6 +25,7 @@ import java.util.function.Predicate;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.neighbor.NeighborProvider;
 import software.amazon.smithy.model.neighbor.Relationship;
+import software.amazon.smithy.model.neighbor.RelationshipType;
 import software.amazon.smithy.model.neighbor.Walker;
 import software.amazon.smithy.model.shapes.OperationShape;
 import software.amazon.smithy.model.shapes.ResourceShape;
@@ -48,18 +49,8 @@ public final class TopDownIndex implements KnowledgeIndex {
 
         // Only traverse resource and operation bindings.
         Predicate<Relationship> filter = rel -> {
-            switch (rel.getRelationshipType()) {
-                case RESOURCE:
-                case OPERATION:
-                case CREATE:
-                case READ:
-                case UPDATE:
-                case DELETE:
-                case LIST:
-                    return true;
-                default:
-                    return false;
-            }
+            RelationshipType type = rel.getRelationshipType();
+            return type == RelationshipType.RESOURCE || type.isOperationBinding();
         };
 
         for (ResourceShape resource : model.getResourceShapes()) {
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlNodeParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlNodeParser.java
index 2aee9178a..fa35f6556 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlNodeParser.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlNodeParser.java
@@ -74,7 +74,7 @@ final class IdlNodeParser {
                 return result;
             case IDENTIFIER:
                 String shapeId = loader.internString(IdlShapeIdParser.expectAndSkipShapeId(tokenizer));
-                return parseIdentifier(loader, shapeId, location);
+                return createIdentifier(loader, shapeId, location);
             case NUMBER:
                 Number number = tokenizer.getCurrentTokenNumberValue();
                 tokenizer.next();
@@ -95,10 +95,10 @@ final class IdlNodeParser {
      * @param location   Source location to assign to the identifier.
      * @return Returns the parsed identifier.
      */
-    static Node parseIdentifier(IdlModelLoader loader, String identifier, SourceLocation location) {
+    static Node createIdentifier(IdlModelLoader loader, String identifier, SourceLocation location) {
         Keyword keyword = Keyword.from(identifier);
         return keyword == null
-               ? parseSyntacticShapeId(loader, identifier, location)
+               ? createSyntacticShapeId(loader, identifier, location)
                : keyword.createNode(location);
     }
 
@@ -138,7 +138,7 @@ final class IdlNodeParser {
         }
     }
 
-    private static Node parseSyntacticShapeId(
+    private static Node createSyntacticShapeId(
             IdlModelLoader loader,
             String identifier,
             SourceLocation location
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlTraitParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlTraitParser.java
index 0d8006e2b..82b52de57 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlTraitParser.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlTraitParser.java
@@ -193,19 +193,30 @@ final class IdlTraitParser {
                 }
             case IDENTIFIER:
             default:
-                String identifier = loader.internString(tokenizer.getCurrentTokenLexeme());
-                tokenizer.next();
+                // Handle: `foo`, `foo$bar`, `foo.bar#baz`, `foo.bar#baz$bam`, `foo: bam`
+                String identifier = loader.internString(IdlShapeIdParser.expectAndSkipShapeId(tokenizer));
                 tokenizer.skipWsAndDocs();
-                if (tokenizer.getCurrentToken() == IdlToken.COLON) {
+                if (tokenizer.getCurrentToken() == IdlToken.RPAREN || isItDefinitelyShapeId(identifier)) {
+                    return IdlNodeParser.createIdentifier(loader, identifier, location);
+                } else {
+                    tokenizer.expect(IdlToken.COLON);
                     tokenizer.next();
                     tokenizer.skipWsAndDocs();
                     return parseStructuredTrait(loader, new StringNode(identifier, location));
-                } else {
-                    return IdlNodeParser.parseIdentifier(loader, identifier, location);
                 }
         }
     }
 
+    private static boolean isItDefinitelyShapeId(String identifier) {
+        for (int i = 0; i < identifier.length(); i++) {
+            char c = identifier.charAt(i);
+            if (c == '.' || c == '$' || c == '#') {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static ObjectNode parseStructuredTrait(IdlModelLoader loader, StringNode firstKey) {
         IdlInternalTokenizer tokenizer = loader.getTokenizer();
         loader.increaseNestingLevel();
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java
index afad3188c..0267aaf8a 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java
@@ -19,7 +19,6 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import software.amazon.smithy.model.Model;
-import software.amazon.smithy.model.shapes.EntityShape;
 import software.amazon.smithy.model.shapes.EnumShape;
 import software.amazon.smithy.model.shapes.IntEnumShape;
 import software.amazon.smithy.model.shapes.ListShape;
@@ -72,7 +71,7 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
             knownMemberCount += shape.getMixins().size();
             List<Relationship> result = new ArrayList<>(knownMemberCount);
             for (ShapeId mixin : shape.getMixins()) {
-                result.add(relationship(shape, RelationshipType.MIXIN, mixin));
+                push(result, shape, RelationshipType.MIXIN, mixin);
             }
             return result;
         }
@@ -80,105 +79,51 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
 
     @Override
     public List<Relationship> serviceShape(ServiceShape shape) {
-        int operationAndResourceRelationships = 2 * (shape.getAllOperations().size() + shape.getResources().size());
-        int errorSize = shape.getErrors().size();
-        int neededSize = operationAndResourceRelationships + errorSize;
+        int neededSize = shape.getOperations().size() + shape.getResources().size() + shape.getErrors().size();
         List<Relationship> result = initializeRelationships(shape, neededSize);
 
-        // Add OPERATION from service -> operation. Add BINDING from operation -> service.
         for (ShapeId operation : shape.getOperations()) {
-            addBinding(result, shape, operation, RelationshipType.OPERATION);
+            push(result, shape, RelationshipType.OPERATION, operation);
         }
-        // Add RESOURCE from service -> resource. Add BINDING from resource -> service.
+
         for (ShapeId resource : shape.getResources()) {
-            addBinding(result, shape, resource, RelationshipType.RESOURCE);
+            push(result, shape, RelationshipType.RESOURCE, resource);
         }
-        // Add ERROR relationships from service -> errors.
+
         for (ShapeId errorId : shape.getErrors()) {
-            result.add(relationship(shape, RelationshipType.ERROR, errorId));
+            push(result, shape, RelationshipType.ERROR, errorId);
         }
+
         return result;
     }
 
-    private void addBinding(List<Relationship> result, Shape container, ShapeId bindingTarget, RelationshipType type) {
+    private void push(List<Relationship> result, Shape container, RelationshipType type, ShapeId bindingTarget) {
         result.add(relationship(container, type, bindingTarget));
-        addBound(result, container, bindingTarget);
     }
 
-    private void addBound(List<Relationship> result, Shape container, ShapeId bindingTarget) {
-        model.getShape(bindingTarget)
-                .ifPresent(op -> result.add(relationship(op, RelationshipType.BOUND, container.getId())));
+    private void push(List<Relationship> result, Shape container, RelationshipType type, MemberShape bindingTarget) {
+        result.add(relationship(container, type, bindingTarget));
     }
 
     @Override
     public List<Relationship> resourceShape(ResourceShape shape) {
-        // This is a rough estimate for the number of needed relationships.
-        int operationAndResourceRelationships = 2 * (shape.getAllOperations().size() + shape.getResources().size());
-        int identifierSize = shape.getIdentifiers().size();
-        int neededSize = (operationAndResourceRelationships * 2) + identifierSize;
+        int neededSize = shape.getAllOperations().size() + shape.getResources().size()
+                         + shape.getIdentifiers().size() + shape.getProperties().size();
         List<Relationship> result = initializeRelationships(shape, neededSize);
-
-        // Add IDENTIFIER relationships.
-        shape.getIdentifiers().forEach((k, v) -> result.add(relationship(shape, RelationshipType.IDENTIFIER, v)));
-        // Add PROPERTY relationships.
-        shape.getProperties().forEach((k, v) -> result.add(relationship(shape, RelationshipType.PROPERTY, v)));
-        // Add RESOURCE from resourceA -> resourceB and BOUND from resourceB -> resourceA
-        shape.getResources().forEach(id -> addBinding(result, shape, id, RelationshipType.RESOURCE));
-        // Add all operation BINDING relationships (resource -> operation) and BOUND relations (operation -> resource).
-        shape.getAllOperations().forEach(id -> addBinding(result, shape, id, RelationshipType.OPERATION));
-        // Do the same, but for all of the lifecycle operations. Note: does not yet another BOUND relationships.
-        // READ, UPDATE, DELETE, and PUT are all instance operations by definition
-        shape.getRead().ifPresent(id -> {
-            result.add(relationship(shape, RelationshipType.READ, id));
-            result.add(relationship(shape, RelationshipType.INSTANCE_OPERATION, id));
-        });
-        shape.getUpdate().ifPresent(id -> {
-            result.add(relationship(shape, RelationshipType.UPDATE, id));
-            result.add(relationship(shape, RelationshipType.INSTANCE_OPERATION, id));
-        });
-        shape.getDelete().ifPresent(id -> {
-            result.add(relationship(shape, RelationshipType.DELETE, id));
-            result.add(relationship(shape, RelationshipType.INSTANCE_OPERATION, id));
-        });
-        shape.getPut().ifPresent(id -> {
-            result.add(relationship(shape, RelationshipType.PUT, id));
-            result.add(relationship(shape, RelationshipType.INSTANCE_OPERATION, id));
-        });
-        // LIST and CREATE are, by definition, collection operations
-        shape.getCreate().ifPresent(id -> {
-            result.add(relationship(shape, RelationshipType.CREATE, id));
-            result.add(relationship(shape, RelationshipType.COLLECTION_OPERATION, id));
-        });
-        shape.getList().ifPresent(id -> {
-            result.add(relationship(shape, RelationshipType.LIST, id));
-            result.add(relationship(shape, RelationshipType.COLLECTION_OPERATION, id));
-        });
-        // Add in all the other collection operations
-        shape.getCollectionOperations().forEach(id -> result.add(
-                relationship(shape, RelationshipType.COLLECTION_OPERATION, id)));
-        // Add in all the other instance operations
-        shape.getOperations().forEach(id -> result.add(
-                relationship(shape, RelationshipType.INSTANCE_OPERATION, id)));
-
-        // Find resource shapes that bind this resource to it.
-        for (ResourceShape resource : model.getResourceShapes()) {
-            addServiceAndResourceBindings(result, shape, resource);
-        }
-
-        // Find service shapes that bind this resource to it.
-        for (ServiceShape service : model.getServiceShapes()) {
-            addServiceAndResourceBindings(result, shape, service);
-        }
-
+        shape.getIdentifiers().forEach((k, v) -> push(result, shape, RelationshipType.IDENTIFIER, v));
+        shape.getProperties().forEach((k, v) -> push(result, shape, RelationshipType.PROPERTY, v));
+        shape.getResources().forEach(id -> push(result, shape, RelationshipType.RESOURCE, id));
+        shape.getList().ifPresent(id -> push(result, shape, RelationshipType.LIST, id));
+        shape.getCreate().ifPresent(id -> push(result, shape, RelationshipType.CREATE, id));
+        shape.getPut().ifPresent(id -> push(result, shape, RelationshipType.PUT, id));
+        shape.getRead().ifPresent(id -> push(result, shape, RelationshipType.READ, id));
+        shape.getUpdate().ifPresent(id -> push(result, shape, RelationshipType.UPDATE, id));
+        shape.getDelete().ifPresent(id -> push(result, shape, RelationshipType.DELETE, id));
+        shape.getOperations().forEach(id -> result.add(relationship(shape, RelationshipType.OPERATION, id)));
+        shape.getCollectionOperations().forEach(id -> push(result, shape, RelationshipType.COLLECTION_OPERATION, id));
         return result;
     }
 
-    private void addServiceAndResourceBindings(List<Relationship> result, ResourceShape resource, EntityShape entity) {
-        if (entity.getResources().contains(resource.getId())) {
-            addBinding(result, entity, resource.getId(), RelationshipType.RESOURCE);
-        }
-    }
-
     @Override
     public List<Relationship> operationShape(OperationShape shape) {
         ShapeId input = shape.getInput().orElse(null);
@@ -191,16 +136,17 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
         List<Relationship> result = initializeRelationships(shape, assumedRelationshipCount);
 
         if (input != null) {
-            result.add(relationship(shape, RelationshipType.INPUT, input));
+            push(result, shape, RelationshipType.INPUT, input);
         }
 
         if (output != null) {
-            result.add(relationship(shape, RelationshipType.OUTPUT, output));
+            push(result, shape, RelationshipType.OUTPUT, output);
         }
 
         for (ShapeId errorId : shape.getErrors()) {
-            result.add(relationship(shape, RelationshipType.ERROR, errorId));
+            push(result, shape, RelationshipType.ERROR, errorId);
         }
+
         return result;
     }
 
@@ -211,10 +157,10 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
         // Emit a relationship from a member back to the enum, but not from an enum member to Unit.
         boolean isEnumShape = container instanceof EnumShape || container instanceof IntEnumShape;
         List<Relationship> result = initializeRelationships(shape, 1 + (isEnumShape ? 0 : 1));
-        result.add(relationship(shape, RelationshipType.MEMBER_CONTAINER, shape.getContainer()));
+        push(result, shape, RelationshipType.MEMBER_CONTAINER, shape.getContainer());
 
         if (!isEnumShape) {
-            result.add(relationship(shape, RelationshipType.MEMBER_TARGET, shape.getTarget()));
+            push(result, shape, RelationshipType.MEMBER_TARGET, shape.getTarget());
         }
 
         return result;
@@ -224,7 +170,7 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
     public List<Relationship> enumShape(EnumShape shape) {
         List<Relationship> result = initializeRelationships(shape, shape.getAllMembers().size());
         for (MemberShape member : shape.getAllMembers().values()) {
-            result.add(relationship(shape, RelationshipType.ENUM_MEMBER, member));
+            push(result, shape, RelationshipType.ENUM_MEMBER, member);
         }
         return result;
     }
@@ -233,7 +179,7 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
     public List<Relationship> intEnumShape(IntEnumShape shape) {
         List<Relationship> result = initializeRelationships(shape, shape.getAllMembers().size());
         for (MemberShape member : shape.getAllMembers().values()) {
-            result.add(relationship(shape, RelationshipType.INT_ENUM_MEMBER, member));
+            push(result, shape, RelationshipType.INT_ENUM_MEMBER, member);
         }
         return result;
     }
@@ -241,22 +187,22 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
     @Override
     public List<Relationship> listShape(ListShape shape) {
         List<Relationship> result = initializeRelationships(shape, 1);
-        result.add(relationship(shape, RelationshipType.LIST_MEMBER, shape.getMember()));
+        push(result, shape, RelationshipType.LIST_MEMBER, shape.getMember());
         return result;
     }
 
     @Override
     public List<Relationship> setShape(SetShape shape) {
         List<Relationship> result = initializeRelationships(shape, 1);
-        result.add(relationship(shape, RelationshipType.SET_MEMBER, shape.getMember()));
+        push(result, shape, RelationshipType.SET_MEMBER, shape.getMember());
         return result;
     }
 
     @Override
     public List<Relationship> mapShape(MapShape shape) {
         List<Relationship> result = initializeRelationships(shape, 2);
-        result.add(relationship(shape, RelationshipType.MAP_KEY, shape.getKey()));
-        result.add(relationship(shape, RelationshipType.MAP_VALUE, shape.getValue()));
+        push(result, shape, RelationshipType.MAP_KEY, shape.getKey());
+        push(result, shape, RelationshipType.MAP_VALUE, shape.getValue());
         return result;
     }
 
@@ -264,7 +210,7 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
     public List<Relationship> structureShape(StructureShape shape) {
         List<Relationship> result = initializeRelationships(shape, shape.getAllMembers().size());
         for (MemberShape member : shape.getAllMembers().values()) {
-            result.add(Relationship.create(shape, RelationshipType.STRUCTURE_MEMBER, member));
+            push(result, shape, RelationshipType.STRUCTURE_MEMBER, member);
         }
         return result;
     }
@@ -273,7 +219,7 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
     public List<Relationship> unionShape(UnionShape shape) {
         List<Relationship> result = initializeRelationships(shape, shape.getAllMembers().size());
         for (MemberShape member : shape.getAllMembers().values()) {
-            result.add(Relationship.create(shape, RelationshipType.UNION_MEMBER, member));
+            push(result, shape, RelationshipType.UNION_MEMBER, member);
         }
         return result;
     }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java
index 7f0ada641..97aaceb5b 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java
@@ -29,6 +29,7 @@ import software.amazon.smithy.model.shapes.SetShape;
 import software.amazon.smithy.model.shapes.StructureShape;
 import software.amazon.smithy.model.shapes.UnionShape;
 import software.amazon.smithy.model.traits.TraitDefinition;
+import software.amazon.smithy.utils.SmithyInternalApi;
 
 /**
  * Defines the relationship types between neighboring shapes.
@@ -50,31 +51,16 @@ public enum RelationshipType {
 
     /**
      * A collection operation relationship exists between a resource and the
-     * operations bound to the resource in the "collectionOperations", "create",
-     * and "list" properties.
+     * operations bound to the resource in the "collectionOperations" property.
      */
     COLLECTION_OPERATION("collectionOperation", RelationshipDirection.DIRECTED),
 
-    /**
-     * An instance operation relationship exists between a resource and the
-     * operations bound to the resource in the "Operations", "put", "read",
-     * "update", and "delete" properties.
-     */
+    @Deprecated
+    @SmithyInternalApi
     INSTANCE_OPERATION("instanceOperation", RelationshipDirection.DIRECTED),
 
-    /**
-     * A BINDING relationship exists between the following shapes:
-     *
-     * <ul>
-     *     <li>Between an operation and the service or resource that the
-     *     operation is bound to (through operations or a lifecycle).</li>
-     *     <li>Between a resource and the resource or service that the
-     *     resource is bound to in the "resources" property.</li>
-     * </ul>
-     *
-     * The subject of the relationship is that shape that was bound, and the
-     * target is the shape that declared the binding.
-     */
+    @Deprecated
+    @SmithyInternalApi
     BOUND("bound", RelationshipDirection.INVERTED),
 
     /**
@@ -266,4 +252,68 @@ public enum RelationshipType {
     public RelationshipDirection getDirection() {
         return direction;
     }
+
+    /**
+     * Checks if the given relationship connects a container shape to a member.
+     *
+     * @return Returns true if a member.
+     */
+    public boolean isMemberBinding() {
+        switch (this) {
+            case STRUCTURE_MEMBER:
+            case UNION_MEMBER:
+            case LIST_MEMBER:
+            case SET_MEMBER:
+            case MAP_KEY:
+            case MAP_VALUE:
+            case INT_ENUM_MEMBER:
+            case ENUM_MEMBER:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Checks if the binding is to an operation (create|read|update|delete|put|list|operation|collectionOperation).
+     *
+     * @return Returns true if the binding is for any kind of operation binding.
+     */
+    public boolean isOperationBinding() {
+        return isInstanceOperationBinding() || isCollectionOperationBinding();
+    }
+
+    /**
+     * Returns true if relationship connects a resource to an instance operation.
+     *
+     * @return True if an instance operation.
+     */
+    public boolean isInstanceOperationBinding() {
+        switch (this) {
+            case OPERATION:
+            case READ:
+            case UPDATE:
+            case DELETE:
+            case PUT:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Returns true if relationship connects a resource to a collection operation.
+     *
+     * @return True if a collection operation.
+     */
+    public boolean isCollectionOperationBinding() {
+        switch (this) {
+            case COLLECTION_OPERATION:
+            case CREATE:
+            case LIST:
+                return true;
+            default:
+                return false;
+        }
+    }
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/NeighborSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/NeighborSelector.java
index 12f1e2113..116e98ff8 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/NeighborSelector.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/NeighborSelector.java
@@ -15,6 +15,7 @@
 
 package software.amazon.smithy.model.selector;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -22,12 +23,14 @@ import java.util.function.Function;
 import software.amazon.smithy.model.neighbor.NeighborProvider;
 import software.amazon.smithy.model.neighbor.Relationship;
 import software.amazon.smithy.model.neighbor.RelationshipType;
+import software.amazon.smithy.model.shapes.ResourceShape;
+import software.amazon.smithy.model.shapes.ServiceShape;
 import software.amazon.smithy.model.shapes.Shape;
 
 final class NeighborSelector implements InternalSelector {
 
-    private static final NeighborSelector FORWARD = new NeighborSelector(Collections.emptyList(), Direction.FORWARD);
-    private static final NeighborSelector REVERSE = new NeighborSelector(Collections.emptyList(), Direction.REVERSE);
+    static final NeighborSelector FORWARD = new NeighborSelector(Collections.emptyList(), Direction.FORWARD);
+    static final NeighborSelector REVERSE = new NeighborSelector(Collections.emptyList(), Direction.REVERSE);
 
     private final List<String> relTypes;
     private final Direction direction;
@@ -43,12 +46,12 @@ final class NeighborSelector implements InternalSelector {
     private enum Direction {
         FORWARD {
             @Override
-            Response emit(Context context, Relationship rel, Receiver next) {
-                return next.apply(context, rel.getNeighborShape().get());
+            protected Response emit(Context context, Relationship rel, Receiver next) {
+                return next.apply(context, rel.expectNeighborShape());
             }
 
             @Override
-            Function<Context, NeighborProvider> neighborFactory(boolean includeTraits) {
+            protected Function<Context, NeighborProvider> neighborFactory(boolean includeTraits) {
                 return includeTraits
                        ? context -> context.neighborIndex.getProviderWithTraitRelationships()
                        : context -> context.neighborIndex.getProvider();
@@ -56,29 +59,60 @@ final class NeighborSelector implements InternalSelector {
         },
         REVERSE {
             @Override
-            Response emit(Context context, Relationship rel, Receiver next) {
+            protected Response emit(Context context, Relationship rel, Receiver next) {
                 return next.apply(context, rel.getShape());
             }
 
             @Override
-            Function<Context, NeighborProvider> neighborFactory(boolean includeTraits) {
+            protected Function<Context, NeighborProvider> neighborFactory(boolean includeTraits) {
                 return includeTraits
                        ? context -> context.neighborIndex.getReverseProviderWithTraitRelationships()
                        : context -> context.neighborIndex.getReverseProvider();
             }
         };
 
-        abstract Response emit(Context context, Relationship rel, Receiver next);
+        protected abstract Response emit(Context context, Relationship rel, Receiver next);
 
-        abstract Function<Context, NeighborProvider> neighborFactory(boolean includeTraits);
+        protected abstract Function<Context, NeighborProvider> neighborFactory(boolean includeTraits);
     }
 
-    static NeighborSelector forward(List<String> relTypes) {
-        return relTypes.isEmpty() ? FORWARD : new NeighborSelector(relTypes, Direction.FORWARD);
+    static InternalSelector forward(List<String> relationships) {
+        return fromStrings(relationships, Direction.FORWARD);
     }
 
-    static NeighborSelector reverse(List<String> relTypes) {
-        return relTypes.isEmpty() ? REVERSE : new NeighborSelector(relTypes, Direction.REVERSE);
+    static InternalSelector reverse(List<String> relationships) {
+        return fromStrings(relationships, Direction.REVERSE);
+    }
+
+    private static InternalSelector fromStrings(List<String> relationships, Direction direction) {
+        // Handle the deprecated synthetic "bound" relationship.
+        boolean bound = relationships.removeIf(r -> r.equals("bound"));
+
+        // Handle the deprecated synthetic "instanceOperation" relationship.
+        boolean instanceOperation = relationships.removeIf(r -> r.equals("instanceOperation"));
+
+        // The common case of not using the deprecated selectors should not need to wrap in additional selectors.
+        if (!bound && !instanceOperation) {
+            return new NeighborSelector(relationships, direction);
+        }
+
+        // Try to exact-size the array, though it could be oversized if there are only deprecated rels (rare).
+        List<InternalSelector> predicates = new ArrayList<>(1 + (bound ? 1 : 0) + (instanceOperation ? 1 : 0));
+
+        if (!relationships.isEmpty()) {
+            predicates.add(new NeighborSelector(relationships, direction));
+        }
+
+        if (bound) {
+            predicates.add(new BoundRelationship());
+        }
+
+        if (instanceOperation) {
+            boolean traits = relationships.contains("trait");
+            predicates.add(new InstanceOperationRelationship(direction, direction.neighborFactory(traits)));
+        }
+
+        return IsSelector.of(predicates);
     }
 
     @Override
@@ -110,4 +144,56 @@ final class NeighborSelector implements InternalSelector {
             return relTypes.contains(relType);
         }
     }
+
+    @Deprecated
+    private static final class BoundRelationship implements InternalSelector {
+        @Override
+        public Response push(Context ctx, Shape shape, Receiver next) {
+            // Emulating the previously buggy behavior of only emitting bound rels from services and not operations.
+            if (shape.isResourceShape()) {
+                for (ResourceShape resource : ctx.getModel().getResourceShapes()) {
+                    if (resource.getResources().contains(shape.getId())) {
+                        if (next.apply(ctx, resource) == Response.STOP) {
+                            return Response.STOP;
+                        }
+                    }
+                }
+                for (ServiceShape service : ctx.getModel().getServiceShapes()) {
+                    if (service.getResources().contains(shape.getId())) {
+                        if (next.apply(ctx, service) == Response.STOP) {
+                            return Response.STOP;
+                        }
+                    }
+                }
+            }
+            return InternalSelector.Response.CONTINUE;
+        }
+    }
+
+    @Deprecated
+    private static final class InstanceOperationRelationship implements InternalSelector {
+
+        private final Direction direction;
+        private final Function<Context, NeighborProvider> neighborFactory;
+
+        InstanceOperationRelationship(Direction direction, Function<Context, NeighborProvider> neighborFactory) {
+            this.direction = direction;
+            this.neighborFactory = neighborFactory;
+        }
+
+        @Override
+        public Response push(Context ctx, Shape shape, Receiver next) {
+            if (shape.isResourceShape()) {
+                NeighborProvider resolvedProvider = neighborFactory.apply(ctx);
+                for (Relationship rel : resolvedProvider.getNeighbors(shape)) {
+                    if (rel.getRelationshipType().isInstanceOperationBinding()) {
+                        if (direction.emit(ctx, rel, next) == Response.STOP) {
+                            return Response.STOP;
+                        }
+                    }
+                }
+            }
+            return Response.CONTINUE;
+        }
+    }
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/RecursiveNeighborSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/RecursiveNeighborSelector.java
index 49b005f90..653de39ba 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/RecursiveNeighborSelector.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/RecursiveNeighborSelector.java
@@ -16,18 +16,26 @@
 package software.amazon.smithy.model.selector;
 
 import java.util.Iterator;
+import java.util.function.Predicate;
+import software.amazon.smithy.model.neighbor.Relationship;
+import software.amazon.smithy.model.neighbor.RelationshipDirection;
 import software.amazon.smithy.model.neighbor.Walker;
 import software.amazon.smithy.model.shapes.Shape;
 
 /**
- * Uses a {@link Walker} to find all shapes connected to the set of
- * given shapes.
+ * Find all shapes recursively connected to a shape using directed edges.
  */
 final class RecursiveNeighborSelector implements InternalSelector {
+
+    private static final Predicate<Relationship> ONLY_DIRECTED = r -> {
+        // Don't crawl up from members to their containers.
+        return r.getRelationshipType().getDirection() == RelationshipDirection.DIRECTED;
+    };
+
     @Override
     public Response push(Context context, Shape shape, Receiver next) {
         Walker walker = new Walker(context.neighborIndex.getProvider());
-        Iterator<Shape> shapeIterator = walker.iterateShapes(shape);
+        Iterator<Shape> shapeIterator = walker.iterateShapes(shape, ONLY_DIRECTED);
 
         while (shapeIterator.hasNext()) {
             Shape nextShape = shapeIterator.next();
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorParser.java
index 4fa749e2d..789f97a73 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorParser.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorParser.java
@@ -108,7 +108,7 @@ final class SelectorParser extends SimpleParser {
                 }
             case '>': // forward undirected neighbor
                 skip();
-                return NeighborSelector.forward(Collections.emptyList());
+                return NeighborSelector.FORWARD;
             case '<': // reverse [un]directed neighbor
                 skip();
                 if (peek() == '-') { // reverse directed neighbor (<-[X, Y, Z]-)
@@ -116,7 +116,7 @@ final class SelectorParser extends SimpleParser {
                     expect('[');
                     return parseSelectorDirectedReverseNeighbor();
                 } else { // reverse undirected neighbor (<)
-                    return NeighborSelector.reverse(Collections.emptyList());
+                    return NeighborSelector.REVERSE;
                 }
             case '~': // ~>
                 skip();
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/ExamplesTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/ExamplesTrait.java
index f3ac1a863..5dbac8017 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/ExamplesTrait.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/ExamplesTrait.java
@@ -193,6 +193,25 @@ public final class ExamplesTrait extends AbstractTrait implements ToSmithyBuilde
             return builder.build();
         }
 
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Example example = (Example) o;
+            return allowConstraintErrors == example.allowConstraintErrors && Objects.equals(title, example.title)
+                    && Objects.equals(documentation, example.documentation) && Objects.equals(input, example.input)
+                    && Objects.equals(output, example.output) && Objects.equals(error, example.error);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(title, documentation, input, output, error, allowConstraintErrors);
+        }
+
         @Override
         public Builder toBuilder() {
             return new Builder().documentation(documentation).title(title).input(input).output(output).error(error)
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/MarkAndSweep.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/MarkAndSweep.java
index 1791c6330..551ddd9ed 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/MarkAndSweep.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/MarkAndSweep.java
@@ -25,6 +25,7 @@ import java.util.stream.Stream;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.neighbor.NeighborProvider;
 import software.amazon.smithy.model.neighbor.Relationship;
+import software.amazon.smithy.model.neighbor.RelationshipDirection;
 import software.amazon.smithy.model.neighbor.RelationshipType;
 import software.amazon.smithy.model.shapes.Shape;
 
@@ -134,19 +135,8 @@ final class MarkAndSweep {
                     // other shapes, not references to this shape that the shape
                     // contains (like members).
                     .filter(rel -> {
-                        switch (rel.getRelationshipType()) {
-                            case MEMBER_CONTAINER:
-                            case LIST_MEMBER:
-                            case STRUCTURE_MEMBER:
-                            case SET_MEMBER:
-                            case UNION_MEMBER:
-                            case MAP_KEY:
-                            case MAP_VALUE:
-                            case BOUND:
-                                return false;
-                            default:
-                                return true;
-                        }
+                        RelationshipType type = rel.getRelationshipType();
+                        return type.getDirection() == RelationshipDirection.DIRECTED && !type.isMemberBinding();
                     })
                     // Don't allow recursive member references to exclude themselves.
                     // This check ensures that recursive member references don't exclude
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PrivateAccessValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PrivateAccessValidator.java
index 3ca96db18..b99bdd686 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PrivateAccessValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PrivateAccessValidator.java
@@ -24,6 +24,7 @@ import software.amazon.smithy.model.neighbor.NeighborProvider;
 import software.amazon.smithy.model.neighbor.Relationship;
 import software.amazon.smithy.model.neighbor.RelationshipType;
 import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.traits.PrivateTrait;
 import software.amazon.smithy.model.validation.AbstractValidator;
 import software.amazon.smithy.model.validation.ValidationEvent;
@@ -50,17 +51,18 @@ public final class PrivateAccessValidator extends AbstractValidator {
         String namespace = shape.getId().getNamespace();
         for (Relationship rel : relationships) {
             if (!rel.getShape().getId().getNamespace().equals(namespace)) {
-                ValidationEvent privateAccessValidationEvent = getPrivateAccessValidationEvent(rel);
-                events.add(privateAccessValidationEvent);
+                events.add(getPrivateAccessValidationEvent(rel));
             }
         }
     }
 
     private ValidationEvent getPrivateAccessValidationEvent(Relationship relationship) {
+        ShapeId neighborId = relationship.expectNeighborShape().getId();
         String message = String.format(
                 "This shape has an invalid %s relationship that targets a private shape, `%s`, in another namespace.",
                 relationship.getRelationshipType().toString().toLowerCase(Locale.US),
-                relationship.getNeighborShape().get().getId());
+                neighborId
+        );
 
         // For now, emit a warning for trait relationships instead of an error. This is because private access on trait
         // relationships was not being validated in the past, so emitting a warning maintains backward compatibility.


Test patch:
diff --git a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
index a2047f153..9b1d6cfb6 100644
--- a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
+++ b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
@@ -22,16 +22,11 @@ import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Optional;
-import java.util.stream.Collectors;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.validation.Severity;
-import software.amazon.smithy.model.validation.ValidatedResult;
-import software.amazon.smithy.model.validation.ValidationEvent;
 
 public class ConditionKeysIndexTest {
     @Test
@@ -65,4 +60,67 @@ public class ConditionKeysIndexTest {
         assertThat(index.getDefinedConditionKeys(service, ShapeId.from("smithy.example#GetResource2")).keySet(),
                 is(empty()));
     }
+
+    @Test
+    public void disableConditionKeyInferenceForResources() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("disable-condition-key-inference-for-resources.smithy"))
+                .discoverModels(getClass().getClassLoader())
+                .assemble()
+                .unwrap();
+
+        ShapeId service = ShapeId.from("smithy.example#MyService");
+
+        ConditionKeysIndex index = ConditionKeysIndex.of(model);
+
+        assertThat(index.getConditionKeyNames(service),
+                containsInAnyOrder("my:service", "aws:operation1", "resource:1", "myservice:Resource1Id1"));
+
+        // Verify inference key myservice:Resource2Id2 does not exist
+        assertThat(index.getConditionKeyNames(service), not(contains("myservice:Resource2Id2")));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
+                containsInAnyOrder("resource:1", "myservice:Resource1Id1"));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
+                not(contains("myservice:Resource2Id2")));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
+                containsInAnyOrder("resource:1", "myservice:Resource1Id1"));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
+                not(contains("myservice:Resource2Id2")));
+    }
+
+    @Test
+    public void disableConditionKeyInferenceForService() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("disable-condition-key-inference-for-service.smithy"))
+                .discoverModels(getClass().getClassLoader())
+                .assemble()
+                .unwrap();
+
+        ShapeId service = ShapeId.from("smithy.example#MyService");
+
+        ConditionKeysIndex index = ConditionKeysIndex.of(model);
+
+        assertThat(index.getConditionKeyNames(service),
+                containsInAnyOrder("my:service", "aws:operation1", "resource:1"));
+
+        // Verify inference key myservice:Resource1Id1 AND myservice:Resource2Id2 do not exist
+        assertThat(index.getConditionKeyNames(service),
+                not(contains("myservice:Resource1Id1", "myservice:Resource2Id2")));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
+                not(contains("myservice:Resource1Id1", "myservice:Resource2Id2")));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
+                contains("resource:1"));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
+                contains("resource:1"));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
+                not(contains("myservice:Resource1Id1", "myservice:Resource2Id2")));
+    }
 }
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-resources.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-resources.smithy
new file mode 100644
index 000000000..d6d8ee926
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-resources.smithy
@@ -0,0 +1,63 @@
+$version: "1.0"
+namespace smithy.example
+
+@aws.api#service(sdkId: "My")
+@aws.iam#defineConditionKeys("my:service": {type: "String", documentation: "Foo baz"})
+service MyService {
+  version: "2019-02-20",
+  operations: [Operation1],
+  resources: [Resource1]
+}
+
+@aws.iam#conditionKeys(["aws:operation1", "my:service"])
+operation Operation1 {}
+
+@aws.iam#conditionKeys(["resource:1"])
+resource Resource1 {
+  identifiers: {
+    id1: ArnString,
+  },
+  resources: [Resource2]
+}
+
+@aws.iam#disableConditionKeyInference
+resource Resource2 {
+  identifiers: {
+    id1: ArnString,
+    id2: FooString,
+  },
+  read: GetResource2,
+  list: ListResource2,
+}
+
+@readonly
+operation GetResource2 {
+    input: GetResource2Input
+}
+
+structure GetResource2Input {
+  @required
+  id1: ArnString,
+
+  @required
+  id2: FooString
+}
+
+@documentation("This is Foo")
+string FooString
+
+@readonly
+operation ListResource2 {
+    input: ListResource2Input,
+    output: ListResource2Output
+}
+
+structure ListResource2Input {
+  @required
+  id1: ArnString,
+}
+
+structure ListResource2Output {}
+
+@aws.api#arnReference(type: "ec2:Instance")
+string ArnString
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-service.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-service.smithy
new file mode 100644
index 000000000..2c7396243
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-service.smithy
@@ -0,0 +1,63 @@
+$version: "1.0"
+namespace smithy.example
+
+@aws.api#service(sdkId: "My")
+@aws.iam#defineConditionKeys("my:service": {type: "String", documentation: "Foo baz"})
+@aws.iam#disableConditionKeyInference
+service MyService {
+  version: "2019-02-20",
+  operations: [Operation1],
+  resources: [Resource1]
+}
+
+@aws.iam#conditionKeys(["aws:operation1", "my:service"])
+operation Operation1 {}
+
+@aws.iam#conditionKeys(["resource:1"])
+resource Resource1 {
+  identifiers: {
+    id1: ArnString,
+  },
+  resources: [Resource2]
+}
+
+resource Resource2 {
+  identifiers: {
+    id1: ArnString,
+    id2: FooString,
+  },
+  read: GetResource2,
+  list: ListResource2,
+}
+
+@readonly
+operation GetResource2 {
+  input: GetResource2Input
+}
+
+structure GetResource2Input {
+  @required
+  id1: ArnString,
+
+  @required
+  id2: FooString
+}
+
+@documentation("This is Foo")
+string FooString
+
+@readonly
+operation ListResource2 {
+  input: ListResource2Input,
+  output: ListResource2Output
+}
+
+structure ListResource2Input {
+  @required
+  id1: ArnString,
+}
+
+structure ListResource2Output {}
+
+@aws.api#arnReference(type: "ec2:Instance")
+string ArnString
diff --git a/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-union.smithy b/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-union.smithy
index f5ac1bb44..cd0ca8cd4 100644
--- a/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-union.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-union.smithy
@@ -15,14 +15,14 @@ apply MalformedUnion @httpMalformedRequestTests([
     {
         id: "RestJsonMalformedUnionMultipleFieldsSet",
         documentation: """
-        When the union has multiple fields set, the response should be a 400
-        SerializationException.""",
+            When the union has multiple fields set, the response should be a 400
+            SerializationException.""",
         protocol: restJson1,
         request: {
             method: "POST",
             uri: "/MalformedUnion",
             body: """
-            { "union" : { "int": 2, "string": "three" } }""",
+                { "union" : { "int": 2, "string": "three" } }""",
             headers: {
                 "content-type": "application/json"
             }
@@ -37,8 +37,8 @@ apply MalformedUnion @httpMalformedRequestTests([
     {
         id: "RestJsonMalformedUnionKnownAndUnknownFieldsSet",
         documentation: """
-        When the union has multiple fields set, even when only one is modeled,
-        the response should be a 400 SerializationException.""",
+            When the union has multiple fields set, even when only one is modeled,
+            the response should be a 400 SerializationException.""",
         protocol: restJson1,
         request: {
             method: "POST",
@@ -59,14 +59,14 @@ apply MalformedUnion @httpMalformedRequestTests([
     {
         id: "RestJsonMalformedUnionNoFieldsSet",
         documentation: """
-        When the union has no fields set, the response should be a 400
-        SerializationException.""",
+            When the union has no fields set, the response should be a 400
+            SerializationException.""",
         protocol: restJson1,
         request: {
             method: "POST",
             uri: "/MalformedUnion",
             body: """
-            { "union" : { "int": null } }""",
+                { "union" : { "int": null } }""",
             headers: {
                 "content-type": "application/json"
             }
@@ -81,14 +81,14 @@ apply MalformedUnion @httpMalformedRequestTests([
     {
         id: "RestJsonMalformedUnionValueIsArray",
         documentation: """
-        When the union value is actually an array, the response should be a 400
-        SerializationException.""",
+            When the union value is actually an array, the response should be a 400
+            SerializationException.""",
         protocol: restJson1,
         request: {
             method: "POST",
             uri: "/MalformedUnion",
             body: """
-            { "union" : ["int"] }""",
+                { "union" : ["int"] }""",
             headers: {
                 "content-type": "application/json"
             }
@@ -99,7 +99,33 @@ apply MalformedUnion @httpMalformedRequestTests([
                 "x-amzn-errortype": "SerializationException"
             }
         }
-    },
+    }
+    {
+        id: "RestJsonMalformedUnionUnknownMember",
+        documentation: """
+            When an unknown union member is received, the response should be a 400
+            SerializationException."""
+        protocol: restJson1
+        request: {
+            method: "POST"
+            uri: "/MalformedUnion"
+            body: """
+                {
+                    "union": {
+                        "unknown": "hello"
+                    }
+                }""",
+            headers: {
+                "content-type": "application/json"
+            }
+        }
+        response: {
+            code: 400
+            headers: {
+                "x-amzn-errortype": "SerializationException"
+            }
+        }
+    }
 ])
 
 structure MalformedUnionInput {
@@ -111,4 +137,3 @@ union SimpleUnion {
 
     string: String
 }
-
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/ModelTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/ModelTest.java
index 0a780d5fd..da572e985 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/ModelTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/ModelTest.java
@@ -30,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -38,13 +39,10 @@ import software.amazon.smithy.model.knowledge.KnowledgeIndex;
 import software.amazon.smithy.model.knowledge.TopDownIndex;
 import software.amazon.smithy.model.node.ExpectationNotMetException;
 import software.amazon.smithy.model.node.Node;
-import software.amazon.smithy.model.shapes.EnumShape;
-import software.amazon.smithy.model.shapes.IntegerShape;
-import software.amazon.smithy.model.shapes.ListShape;
-import software.amazon.smithy.model.shapes.Shape;
-import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.shapes.StringShape;
-import software.amazon.smithy.model.shapes.TimestampShape;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.node.StringNode;
+import software.amazon.smithy.model.shapes.*;
+import software.amazon.smithy.model.traits.ExamplesTrait;
 import software.amazon.smithy.model.traits.TraitDefinition;
 import software.amazon.smithy.model.traits.synthetic.OriginalShapeIdTrait;
 
@@ -79,6 +77,26 @@ public class ModelTest {
         assertThat(modelA, not(equalTo(null)));
     }
 
+    @Test
+    public void modelEqualityExamples() {
+        StructureShape opInput = StructureShape.builder().id(ShapeId.fromParts("foo", "FooInput")).addMember("test", ShapeId.from("smithy.api#String")).build();
+        Supplier<OperationShape> op = () -> {
+            ExamplesTrait.Example example = ExamplesTrait.Example.builder().title("anything").input(ObjectNode.builder().withMember("test", StringNode.from("something")).build()).build();
+            ExamplesTrait examples = ExamplesTrait.builder().addExample(example).build();
+            return OperationShape.builder().id(ShapeId.fromParts("foo", "Foo")).input(opInput).addTrait(examples).build();
+        };
+
+        Model modelA = Model.builder()
+                .addShape(op.get())
+                .build();
+        Model modelB = Model.builder()
+                .addShape(op.get())
+                .build();
+
+        assertThat(modelA, equalTo(modelA));
+        assertThat(modelA, equalTo(modelB));
+    }
+
     @Test
     public void successfullyExpectsShapesOfType() {
         StringShape shape = StringShape.builder().id("ns.foo#A").build();
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/BottomUpNeighborVisitorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/BottomUpNeighborVisitorTest.java
index 9d155c476..2caa46860 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/BottomUpNeighborVisitorTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/BottomUpNeighborVisitorTest.java
@@ -180,10 +180,7 @@ public class BottomUpNeighborVisitorTest {
                 .build();
         NeighborProvider neighborVisitor = NeighborProvider.reverse(model);
 
-        assertThat(neighborVisitor.getNeighbors(service), containsInAnyOrder(
-                Relationship.create(resourceShape, RelationshipType.BOUND, service),
-                Relationship.create(operationShape, RelationshipType.BOUND, service)
-        ));
+        assertThat(neighborVisitor.getNeighbors(service), empty());
     }
 
     @Test
@@ -215,11 +212,9 @@ public class BottomUpNeighborVisitorTest {
                 Relationship.create(child1, RelationshipType.RESOURCE, child2)));
         assertThat(neighborVisitor.getNeighbors(child1), containsInAnyOrder(
                 Relationship.create(resource, RelationshipType.RESOURCE, child1),
-                Relationship.create(otherService, RelationshipType.RESOURCE, child1),
-                Relationship.create(child2, RelationshipType.BOUND, child1)));
+                Relationship.create(otherService, RelationshipType.RESOURCE, child1)));
         assertThat(neighborVisitor.getNeighbors(resource), containsInAnyOrder(
-                Relationship.create(parent, RelationshipType.RESOURCE, resource),
-                Relationship.create(child1, RelationshipType.BOUND, resource)));
+                Relationship.create(parent, RelationshipType.RESOURCE, resource)));
         assertThat(neighborVisitor.getNeighbors(child2), containsInAnyOrder(
                 Relationship.create(child1, RelationshipType.RESOURCE, child2)));
     }
@@ -280,34 +275,20 @@ public class BottomUpNeighborVisitorTest {
 
         assertThat(neighborVisitor.getNeighbors(namedOperation), containsInAnyOrder(
                 Relationship.create(resource, RelationshipType.OPERATION, namedOperation),
-                Relationship.create(otherService, RelationshipType.OPERATION, namedOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, namedOperation)));
+                Relationship.create(otherService, RelationshipType.OPERATION, namedOperation)));
         assertThat(neighborVisitor.getNeighbors(createOperation), containsInAnyOrder(
-                Relationship.create(resource, RelationshipType.CREATE, createOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, createOperation),
-                Relationship.create(resource, RelationshipType.COLLECTION_OPERATION, createOperation)));
+                Relationship.create(resource, RelationshipType.CREATE, createOperation)));
         assertThat(neighborVisitor.getNeighbors(getOperation), containsInAnyOrder(
-                Relationship.create(resource, RelationshipType.READ, getOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, getOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, getOperation)));
+                Relationship.create(resource, RelationshipType.READ, getOperation)));
         assertThat(neighborVisitor.getNeighbors(updateOperation), containsInAnyOrder(
-                Relationship.create(resource, RelationshipType.UPDATE, updateOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, updateOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, updateOperation)));
+                Relationship.create(resource, RelationshipType.UPDATE, updateOperation)));
         assertThat(neighborVisitor.getNeighbors(deleteOperation), containsInAnyOrder(
-                Relationship.create(resource, RelationshipType.DELETE, deleteOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, deleteOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, deleteOperation)));
+                Relationship.create(resource, RelationshipType.DELETE, deleteOperation)));
         assertThat(neighborVisitor.getNeighbors(listOperation), containsInAnyOrder(
-                Relationship.create(resource, RelationshipType.LIST, listOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, listOperation),
-                Relationship.create(resource, RelationshipType.COLLECTION_OPERATION, listOperation)));
+                Relationship.create(resource, RelationshipType.LIST, listOperation)));
         assertThat(neighborVisitor.getNeighbors(putOperation), containsInAnyOrder(
-                Relationship.create(resource, RelationshipType.PUT, putOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, putOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, putOperation)));
+                Relationship.create(resource, RelationshipType.PUT, putOperation)));
         assertThat(neighborVisitor.getNeighbors(collectionOperation), containsInAnyOrder(
-                Relationship.create(resource, RelationshipType.OPERATION, collectionOperation),
                 Relationship.create(resource, RelationshipType.COLLECTION_OPERATION, collectionOperation)));
     }
 
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java
index e125770ed..5208a8fb2 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java
@@ -300,9 +300,7 @@ public class NeighborVisitorTest {
 
         assertThat(relationships, containsInAnyOrder(
                 Relationship.create(service, RelationshipType.RESOURCE, resourceShape),
-                Relationship.create(service, RelationshipType.OPERATION, operationShape),
-                Relationship.create(resourceShape, RelationshipType.BOUND, service),
-                Relationship.create(operationShape, RelationshipType.BOUND, service)));
+                Relationship.create(service, RelationshipType.OPERATION, operationShape)));
     }
 
     @Test
@@ -381,8 +379,6 @@ public class NeighborVisitorTest {
         List<Relationship> relationships = resource.accept(neighborVisitor);
 
         assertThat(relationships, containsInAnyOrder(
-                Relationship.create(parent, RelationshipType.RESOURCE, resource),
-                Relationship.create(resource, RelationshipType.BOUND, parent),
                 Relationship.create(resource, RelationshipType.IDENTIFIER, identifier),
                 Relationship.create(resource, RelationshipType.PROPERTY, property),
                 Relationship.create(resource, RelationshipType.CREATE, createOperation),
@@ -391,34 +387,9 @@ public class NeighborVisitorTest {
                 Relationship.create(resource, RelationshipType.DELETE, deleteOperation),
                 Relationship.create(resource, RelationshipType.LIST, listOperation),
                 Relationship.create(resource, RelationshipType.PUT, putOperation),
-                Relationship.create(resource, RelationshipType.COLLECTION_OPERATION, createOperation),
-                Relationship.create(resource, RelationshipType.COLLECTION_OPERATION, listOperation),
                 Relationship.create(resource, RelationshipType.COLLECTION_OPERATION, collectionOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, getOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, updateOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, deleteOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, namedOperation),
-                Relationship.create(resource, RelationshipType.INSTANCE_OPERATION, putOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, createOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, getOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, updateOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, deleteOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, listOperation),
                 Relationship.create(resource, RelationshipType.OPERATION, namedOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, putOperation),
-                Relationship.create(resource, RelationshipType.OPERATION, collectionOperation),
-                Relationship.create(resource, RelationshipType.RESOURCE, child1),
-
-                Relationship.create(namedOperation, RelationshipType.BOUND, resource),
-                Relationship.create(createOperation, RelationshipType.BOUND, resource),
-                Relationship.create(getOperation, RelationshipType.BOUND, resource),
-                Relationship.create(updateOperation, RelationshipType.BOUND, resource),
-                Relationship.create(deleteOperation, RelationshipType.BOUND, resource),
-                Relationship.create(listOperation, RelationshipType.BOUND, resource),
-                Relationship.create(putOperation, RelationshipType.BOUND, resource),
-                Relationship.create(collectionOperation, RelationshipType.BOUND, resource),
-
-                Relationship.create(child1, RelationshipType.BOUND, resource)
+                Relationship.create(resource, RelationshipType.RESOURCE, child1)
         ));
     }
 
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/selector/NeighborSelectorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/selector/NeighborSelectorTest.java
new file mode 100644
index 000000000..181bd2b75
--- /dev/null
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/selector/NeighborSelectorTest.java
@@ -0,0 +1,52 @@
+package software.amazon.smithy.model.selector;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.empty;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.ShapeId;
+
+public class NeighborSelectorTest {
+
+    private static final Model MODEL = Model.assembler()
+                .addImport(NeighborSelectorTest.class.getResource("neighbor-test.smithy"))
+                .assemble()
+                .unwrap();
+
+    @Test
+    public void specialCasesDeprecatedBoundSelectorFromResource() {
+        Selector selector = Selector.parse("resource -[bound]->");
+
+        assertThat(selector.select(MODEL), contains(MODEL.expectShape(ShapeId.from("smithy.example#MyService2"))));
+    }
+
+    @Test
+    public void specialCasesDeprecatedBoundSelectorWithBugCompatibility() {
+        Selector selector = Selector.parse("operation -[bound]->");
+
+        assertThat(selector.select(MODEL), empty());
+    }
+
+    @Test
+    public void specialCasesDeprecatedInstanceOperation() {
+        Selector selector = Selector.parse("-[instanceOperation]->");
+
+        assertThat(selector.select(MODEL), containsInAnyOrder(
+                MODEL.expectShape(ShapeId.from("smithy.example#DeleteMyResource")),
+                MODEL.expectShape(ShapeId.from("smithy.example#GetMyResource"))));
+    }
+
+    @Test
+    public void canUseDeprecatedRelsWithRealRels() {
+        Selector selector = Selector.parse("-[bound, instanceOperation, resource]->");
+
+        assertThat(selector.select(MODEL), containsInAnyOrder(
+                MODEL.expectShape(ShapeId.from("smithy.example#MyService2")),
+                MODEL.expectShape(ShapeId.from("smithy.example#MyResource")),
+                MODEL.expectShape(ShapeId.from("smithy.example#DeleteMyResource")),
+                MODEL.expectShape(ShapeId.from("smithy.example#GetMyResource"))));
+    }
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/selector/RecursiveNeighborSelectorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/selector/RecursiveNeighborSelectorTest.java
index 8b18748b0..87c95c374 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/selector/RecursiveNeighborSelectorTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/selector/RecursiveNeighborSelectorTest.java
@@ -44,6 +44,9 @@ public class RecursiveNeighborSelectorTest {
         Set<String> result = selectIds("service[id=smithy.example#MyService2] ~> *");
 
         assertThat(result, containsInAnyOrder(
+                "smithy.example#MyResource",
+                "smithy.example#GetMyResource",
+                "smithy.example#DeleteMyResource",
                 "smithy.example#Input",
                 "smithy.example#Output$foo",
                 "smithy.example#Error$foo",
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 dfe9e1609..eca0fad78 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
@@ -53,7 +53,6 @@
 [DANGER] ns.foo#Map$value: Selector capture matched selector: > | valid-neighbor-only
 [DANGER] ns.foo#Map$value: Selector capture matched selector: member | member
 [DANGER] ns.foo#MyService: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not
-[DANGER] ns.foo#MyService: Selector capture matched selector: > | valid-neighbor-only
 [DANGER] ns.foo#MyService: Selector capture matched selector: [service|version^=2017] | serviceVersion
 [DANGER] ns.foo#MyResource: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not
 [DANGER] ns.foo#MyResource: Selector capture matched selector: > | valid-neighbor-only
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/target-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/target-validator.errors
index 71aaedc4e..994dec353 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/target-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/target-validator.errors
@@ -20,5 +20,4 @@
 [ERROR] ns.foo#InvalidResourceLifecycle: Resource delete lifecycle operation must target an operation, but found (integer: `ns.foo#Integer`) | Target
 [ERROR] ns.foo#InvalidResourceLifecycle: Resource read lifecycle operation must target an operation, but found (integer: `ns.foo#Integer`) | Target
 [ERROR] ns.foo#InvalidResourceLifecycle: Resource update lifecycle operation must target an operation, but found (integer: `ns.foo#Integer`) | Target
-[ERROR] ns.foo#InvalidResourceLifecycle: resource shape `operation` relationships must target a operation shape, but found (integer: `ns.foo#Integer`) | Target
 [ERROR] ns.foo#InvalidTraitReference$member: Found a MEMBER_TARGET reference to trait definition `ns.foo#fooTrait`. Trait definitions cannot be targeted by members or referenced by shapes in any other context other than applying them as traits. | Target
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/traits/annotation-unclosed-object1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/traits/annotation-unclosed-object1.smithy
index 87564f759..74952506c 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/traits/annotation-unclosed-object1.smithy
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/traits/annotation-unclosed-object1.smithy
@@ -1,4 +1,4 @@
-// Syntax error at line 4, column 8: Expected RPAREN(')') but found IDENTIFIER('MyString') | Model
+// Syntax error at line 4, column 8: Expected COLON(':') but found IDENTIFIER('MyString') | Model
 namespace com.foo
 @foo(
 string MyString
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/traits/invalid-trait-shape-id-key.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/traits/invalid-trait-shape-id-key.smithy
index 6d3ac8643..83e4c9e55 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/traits/invalid-trait-shape-id-key.smithy
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/traits/invalid-trait-shape-id-key.smithy
@@ -1,4 +1,4 @@
-// Syntax error at line 4, column 30: Expected RPAREN(')') but found DOT('.') | Model
+// Syntax error at line 4, column 42: Expected RPAREN(')') but found COLON(':') | Model
 namespace smithy.example
 
 @externalDocumentation(smithy.example#foo: "bar")
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/traits/reference-shape-from-trait.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/traits/reference-shape-from-trait.json
new file mode 100644
index 000000000..e49630c64
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/traits/reference-shape-from-trait.json
@@ -0,0 +1,53 @@
+{
+    "smithy": "2.0",
+    "shapes": {
+        "smithy.example#StructA": {
+            "type": "structure",
+            "members": {
+                "id": {
+                    "target": "smithy.api#String"
+                }
+            }
+        },
+        "smithy.example#StructB": {
+            "type": "structure",
+            "members": {
+                "structAId": {
+                    "target": "smithy.api#String",
+                    "traits": {
+                        "smithy.example#link": "smithy.example#StructA$id"
+                    }
+                }
+            }
+        },
+        "smithy.example#StructC": {
+            "type": "structure",
+            "members": {
+                "structAId": {
+                    "target": "smithy.api#String",
+                    "traits": {
+                        "smithy.example#link": "smithy.example#StructA$id"
+                    }
+                }
+            }
+        },
+        "smithy.example#StructD": {
+            "type": "structure",
+            "members": {
+                "structAId": {
+                    "target": "smithy.api#String",
+                    "traits": {
+                        "smithy.example#link": "smithy.example#StructA"
+                    }
+                }
+            }
+        },
+        "smithy.example#link": {
+            "type": "string",
+            "traits": {
+                "smithy.api#idRef": {},
+                "smithy.api#trait": {}
+            }
+        }
+    }
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/traits/reference-shape-from-trait.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/traits/reference-shape-from-trait.smithy
new file mode 100644
index 000000000..a23e03dcf
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/traits/reference-shape-from-trait.smithy
@@ -0,0 +1,26 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@trait
+@idRef
+string link
+
+structure StructA {
+    id: String
+}
+
+structure StructB {
+    @link(StructA$id)
+    structAId: String
+}
+
+structure StructC {
+    @link(smithy.example#StructA$id)
+    structAId: String
+}
+
+structure StructD {
+    @link(smithy.example#StructA)
+    structAId: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/neighbors.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/neighbors.smithy
index fce837184..a82486d86 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/neighbors.smithy
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/neighbors.smithy
@@ -6,12 +6,28 @@ metadata selectorTests = [
         matches: [
             smithy.example#OperationA
             smithy.example#OperationB
+            smithy.example#CreateBar
+            smithy.example#GetBar
+            smithy.example#UpdateBar
+            smithy.example#DeleteBar
+            smithy.example#ListBar
+            smithy.example#PutBar
+            smithy.example#NamedOperation
+            smithy.example#NamedCollectionOperation
         ]
     }
     {
         selector: "service[trait|title] ~> operation:not([trait|http])"
         matches: [
             smithy.example#OperationB
+            smithy.example#CreateBar
+            smithy.example#GetBar
+            smithy.example#UpdateBar
+            smithy.example#DeleteBar
+            smithy.example#ListBar
+            smithy.example#PutBar
+            smithy.example#NamedOperation
+            smithy.example#NamedCollectionOperation
         ]
     }
     {
@@ -19,12 +35,14 @@ metadata selectorTests = [
         skipPreludeShapes: true
         matches: [
             smithy.example#String
+            smithy.example#BarId
         ]
     }
     {
         selector: ":not([trait|trait]) :not(< *)"
         skipPreludeShapes: true
         matches: [
+            smithy.example#Service
             smithy.example#List
             smithy.example#Structure
         ]
@@ -42,6 +60,121 @@ metadata selectorTests = [
             smithy.example#Regex
         ]
     }
+    {
+        selector: "resource >"
+        matches: [
+            smithy.example#BarId
+            smithy.example#Color
+            smithy.example#CreateBar
+            smithy.example#GetBar
+            smithy.example#UpdateBar
+            smithy.example#DeleteBar
+            smithy.example#ListBar
+            smithy.example#PutBar
+            smithy.example#NamedOperation
+            smithy.example#NamedCollectionOperation
+        ]
+    }
+    {
+        selector: "resource <"
+        matches: [
+            smithy.example#Service
+        ]
+    }
+    {
+        selector: "resource <-[resource]-"
+        matches: [
+            smithy.example#Service
+        ]
+    }
+    {
+        selector: "resource -[property]->"
+        matches: [
+            smithy.example#Color
+        ]
+    }
+    {
+        selector: "resource -[identifier]->"
+        matches: [
+            smithy.example#BarId
+        ]
+    }
+    {
+        selector: "resource -[operation]->"
+        matches: [
+            smithy.example#NamedOperation
+        ]
+    }
+    {
+        selector: "resource -[collectionOperation]->"
+        matches: [
+            smithy.example#NamedCollectionOperation
+        ]
+    }
+    {
+        selector: "resource -[create]->"
+        matches: [
+            smithy.example#CreateBar
+        ]
+    }
+    {
+        selector: "resource -[read]->"
+        matches: [
+            smithy.example#GetBar
+        ]
+    }
+    {
+        selector: "resource -[update]->"
+        matches: [
+            smithy.example#UpdateBar
+        ]
+    }
+    {
+        selector: "resource -[delete]->"
+        matches: [
+            smithy.example#DeleteBar
+        ]
+    }
+    {
+        selector: "resource -[list]->"
+        matches: [
+            smithy.example#ListBar
+        ]
+    }
+    {
+        selector: "resource -[put]->"
+        matches: [
+            smithy.example#PutBar
+        ]
+    }
+    {
+        selector: "[id|name = CreateBar] ~>"
+        matches: [
+            smithy.example#CreateBarInput
+            smithy.example#BarInstanceInput
+            smithy.example#BarInstanceInput$barId
+            smithy.example#BarId
+            smithy.example#CreateBarInput$color
+            smithy.example#Color
+            smithy.example#Color$BLUE
+            smithy.example#Color$GREEN
+            smithy.example#Color$RED
+            smithy.example#CreateBarOutput
+            smithy.example#CreateBarOutput$barId
+        ]
+    }
+    {
+        selector: "[id|member = barId] >"
+        matches: [
+            smithy.example#BarId
+        ]
+    }
+    {
+        selector: "[id|member = barId] ~>"
+        matches: [
+            smithy.example#BarId
+        ]
+    }
 ]
 
 namespace smithy.example
@@ -50,6 +183,7 @@ namespace smithy.example
 service Service {
     version: "2019-06-17",
     operations: [OperationA, OperationB]
+    resources: [Bar]
 }
 
 // Inherits the authorizer of ServiceA
@@ -84,3 +218,90 @@ blob StreamFile
 
 @trait
 string Regex
+
+resource Bar {
+    properties: {
+        color: Color
+    }
+    identifiers: {
+        barId: BarId
+    }
+    create: CreateBar
+    read: GetBar
+    update: UpdateBar
+    delete: DeleteBar
+    list: ListBar
+    put: PutBar
+    operations: [NamedOperation]
+    collectionOperations: [NamedCollectionOperation]
+}
+
+string BarId
+
+enum Color {
+    RED
+    GREEN
+    BLUE
+}
+
+@mixin
+structure BarInstanceInput for Bar {
+    @required
+    $barId
+}
+
+operation CreateBar {
+    input := for Bar {
+        $color
+    }
+    output := with [BarInstanceInput] {}
+}
+
+@readonly
+operation GetBar {
+    input := with [BarInstanceInput] {}
+    output := for Bar {
+        @required
+        $barId
+
+        $color
+    }
+}
+
+operation UpdateBar {
+    input := for Bar with [BarInstanceInput] {
+        $color
+    }
+}
+
+@idempotent
+operation DeleteBar {
+    input := with [BarInstanceInput] {}
+}
+
+@readonly
+operation ListBar {
+    input := for Bar {
+        $color
+    }
+    output := {
+        bars: BarList
+    }
+}
+
+list BarList {
+    member: BarId
+}
+
+@idempotent
+operation PutBar {
+    input := with [BarInstanceInput] {
+        color: Color
+    }
+}
+
+operation NamedOperation {
+    input := with [BarInstanceInput] {}
+}
+
+operation NamedCollectionOperation {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/neighbor-test.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/neighbor-test.smithy
index 90f16c65c..06ffe6233 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/neighbor-test.smithy
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/neighbor-test.smithy
@@ -21,6 +21,7 @@ service MyService1 {
 service MyService2 {
     version: "2020-01-01",
     operations: [Operation],
+    resources: [MyResource]
 }
 
 operation Operation {
@@ -41,3 +42,14 @@ structure Output {
 structure Error {
   foo: smithy.api#String,
 }
+
+resource MyResource {
+    read: GetMyResource
+    delete: DeleteMyResource
+}
+
+@readonly
+operation GetMyResource {}
+
+@idempotent
+operation DeleteMyResource {}