docs: private static List<Fragment> planForConjunction(ConjunctionQuery query, TransactionOLTP tx) {
          // a query plan is an ordered list of fragments
          final List<Fragment> plan = new ArrayList<>();
  
          // flatten all the possible fragments from the conjunction query (these become edges in the query graph)
          final Set<Fragment> allFragments = query.getEquivalentFragmentSets().stream()
                  .flatMap(EquivalentFragmentSet::stream).collect(Collectors.toSet());
  
          // if role players' types are known, we can infer the types of the relation, adding label & isa fragments
          Set<Fragment> inferredFragments = inferRelationTypes(tx, allFragments);
          allFragments.addAll(inferredFragments);
  
          // convert fragments into nodes - some fragments create virtual middle nodes to ensure the Janus edge is traversed
          ImmutableMap<NodeId, Node> queryGraphNodes = buildNodesWithDependencies(allFragments);
  
          // it's possible that some (or all) fragments are disconnected, e.g. $x isa person; $y isa dog;
          Collection<Set<Fragment>> connectedFragmentSets = getConnectedFragmentSets(allFragments);
  
          // build a query plan for each query subgraph separately
          for (Set<Fragment> connectedFragments : connectedFragmentSets) {
              // one of two cases - either we have a connected graph > 1 node, which is used to compute a MST, OR exactly 1 node
              Arborescence<Node> subgraphArborescence = computeArborescence(connectedFragments, queryGraphNodes, tx);
              if (subgraphArborescence != null) {
                  // collect the mapping from directed edge back to fragments -- inverse operation of creating virtual middle nodes
                  Map<Node, Map<Node, Fragment>> middleNodeFragmentMapping = virtualMiddleNodeToFragmentMapping(connectedFragments, queryGraphNodes);
                  List<Fragment> subplan = GreedyTreeTraversal.greedyTraversal(subgraphArborescence, queryGraphNodes, middleNodeFragmentMapping);
                  plan.addAll(subplan);
              } else {
                  // find and include all the nodes not touched in the MST in the plan
                 Set<Node> unhandledNodes = connectedFragments.stream()
                          .flatMap(fragment -> fragment.getNodes().stream())
                          .map(node -> queryGraphNodes.get(node.getNodeId()))
                          .collect(Collectors.toSet());
                 if (unhandledNodes.size() != 1) {
                     throw GraknServerException.create("Query planner exception - expected one unhandled node, found " + unhandledNodes.size());
                 }
                 plan.addAll(nodeToPlanFragments(Iterators.getOnlyElement(unhandledNodes.iterator()), queryGraphNodes, false));
              }
          }
  
          // this shouldn't be necessary, but we keep it just in case of an edge case that we haven't thought of
          List<Fragment> remainingFragments = fragmentsForUnvisitedNodes(queryGraphNodes, queryGraphNodes.values());
          if (remainingFragments.size() > 0) {
              LOG.warn("Expected all fragments to be handled, but found these: " + remainingFragments);
              plan.addAll(remainingFragments);
          }
  
          LOG.trace("Greedy Plan = {}", plan);
          return plan;
      }