///|
fn Model::operation_events(self : Model) -> Array[ValidationEvent] {
  let events : Array[ValidationEvent] = []
  let edges = selector_edges(self)
  for descriptor in ["input", "output"] {
    let opposite = if descriptor == "input" { "output" } else { "input" }
    for _, shape in self.shapes {
      if shape.trait_value("smithy.api#" + descriptor) is None {
        continue
      }
      let operations : Set[ShapeId] = Set([])
      for edge in edges {
        if edge.to != shape.id {
          continue
        }
        guard self.shapes.get(edge.from) is Some(owner) else { continue }
        if edge.label == descriptor {
          operations.add(owner.id)
          if !shape.id.name.has_prefix(owner.id.name) {
            events.push(
              validation_event(
                "OperationInputOutputName." + descriptor,
                "WARNING",
                Some(owner),
                "The " +
                descriptor +
                " of this operation should target a shape that starts with the operation's name, '" +
                owner.id.name +
                "', but the targeted shape is `" +
                shape.id.to_string() +
                "`",
              ),
            )
          }
        } else if edge.label == opposite {
          events.push(
            validation_event(
              "OperationInputOutputMisuse",
              "ERROR",
              Some(owner),
              "Operation `" +
              opposite +
              "` cannot target structures marked with the `@" +
              descriptor +
              "` trait: `" +
              shape.id.to_string() +
              "`",
            ),
          )
        } else if edge.label == "" && owner.body is Member(..) {
          events.push(
            validation_event(
              "OperationInputOutputMisuse",
              "ERROR",
              Some(owner),
              "Members cannot target structures marked with the @" +
              descriptor +
              " trait: " +
              shape.id.to_string(),
            ),
          )
        }
      }
      if operations.length() > 1 {
        let names = operations
          .iter()
          .map(id => "`" + id.to_string() + "`")
          .to_array()
        names.sort()
        events.push(
          validation_event(
            "OperationInputOutputMisuse",
            "ERROR",
            Some(shape),
            "Shapes marked with the @" +
            descriptor +
            " trait cannot be used as " +
            descriptor +
            " by multiple operations: " +
            names.join(", "),
          ),
        )
      }
    }
  }
  for _, operation in self.shapes {
    guard operation.body is Operation(input~, output~, ..) else { continue }
    for
      (descriptor, target, suffixes) in [
        ("input", input, ["Input", "Request"]),
        ("output", output, ["Output", "Response"]),
      ] {
      for suffix in suffixes {
        let id = operation.id.namespace + "#" + operation.id.name + suffix
        if id == target.to_string() {
          continue
        }
        if self.get(id) is Some(ambiguous) {
          events.push(
            validation_event(
              "OperationNameAmbiguity",
              "WARNING",
              Some(ambiguous),
              "The name of this shape implies that it is the " +
              descriptor +
              " of " +
              operation.id.to_string() +
              ", but that operation uses " +
              target.to_string() +
              " for " +
              descriptor +
              ". This kind of ambiguity can confuse developers calling this operation " +
              "and can cause issues in code generators that use similar naming conventions to generate " +
              descriptor +
              " types.",
            ),
          )
        }
      }
    }
  }
  events
}