Repository: 7mind/izumi
Pull request #746 (2019-11-26 00:17:53)

Correct TriSplit, optimizations, correct AbstractPlan#definition

codecov-io: # [Codecov](https://codecov.io/gh/7mind/izumi/pull/746?src=pr&el=h1) Report
> Merging [#746](https://codecov.io/gh/7mind/izumi/pull/746?src=pr&el=desc) into [develop](https://codecov.io/gh/7mind/izumi/commit/01e4d2ab78fae8b509fd5540fbf4d4a8ec10fc7a?src=pr&el=desc) will **decrease** coverage by `0.57%`.
> The diff coverage is `74.78%`.

[![Impacted file tree graph](https://codecov.io/gh/7mind/izumi/pull/746/graphs/tree.svg?width=650&token=77pa3UU1Je&height=150&src=pr)](https://codecov.io/gh/7mind/izumi/pull/746?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##           develop     #746      +/-   ##
===========================================
- Coverage    70.34%   69.76%   -0.58%     
===========================================
  Files          556      558       +2     
  Lines        14590    14634      +44     
  Branches       725      734       +9     
===========================================
- Hits         10263    10210      -53     
- Misses        4327     4424      +97
```


| [Impacted Files](https://codecov.io/gh/7mind/izumi/pull/746?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [.../src/main/scala/org/scalatest/ScalatestSuite.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLXRlc3RraXQvc3JjL21haW4vc2NhbGEvb3JnL3NjYWxhdGVzdC9TY2FsYXRlc3RTdWl0ZS5zY2FsYQ==) | `0% <ø> (ø)` | :arrow_up: |
| [.../izumi/distage/roles/services/RoleAppPlanner.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLXJvbGVzL3NyYy9tYWluL3NjYWxhL2l6dW1pL2Rpc3RhZ2Uvcm9sZXMvc2VydmljZXMvUm9sZUFwcFBsYW5uZXIuc2NhbGE=) | `100% <ø> (ø)` | :arrow_up: |
| [...tkit/integration/docker/ContainerHealthCheck.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLXRlc3RraXQvc3JjL21haW4vc2NhbGEvaXp1bWkvZGlzdGFnZS90ZXN0a2l0L2ludGVncmF0aW9uL2RvY2tlci9Db250YWluZXJIZWFsdGhDaGVjay5zY2FsYQ==) | `0% <ø> (ø)` | :arrow_up: |
| [...main/scala/izumi/distage/roles/internal/Help.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLXJvbGVzL3NyYy9tYWluL3NjYWxhL2l6dW1pL2Rpc3RhZ2Uvcm9sZXMvaW50ZXJuYWwvSGVscC5zY2FsYQ==) | `0% <ø> (ø)` | :arrow_up: |
| [...izumi/distage/roles/services/RoleAppExecutor.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLXJvbGVzL3NyYy9tYWluL3NjYWxhL2l6dW1pL2Rpc3RhZ2Uvcm9sZXMvc2VydmljZXMvUm9sZUFwcEV4ZWN1dG9yLnNjYWxh) | `82.85% <ø> (ø)` | :arrow_up: |
| [...mi/distage/roles/services/IntegrationChecker.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLXJvbGVzL3NyYy9tYWluL3NjYWxhL2l6dW1pL2Rpc3RhZ2Uvcm9sZXMvc2VydmljZXMvSW50ZWdyYXRpb25DaGVja2VyLnNjYWxh) | `84.21% <ø> (ø)` | :arrow_up: |
| [...cala/izumi/distage/provisioning/FactoryTools.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLW1vZGVsL3NyYy9tYWluL3NjYWxhL2l6dW1pL2Rpc3RhZ2UvcHJvdmlzaW9uaW5nL0ZhY3RvcnlUb29scy5zY2FsYQ==) | `50% <ø> (ø)` | :arrow_up: |
| [...la/izumi/distage/model/definition/LocatorDef.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLW1vZGVsL3NyYy9tYWluL3NjYWxhL2l6dW1pL2Rpc3RhZ2UvbW9kZWwvZGVmaW5pdGlvbi9Mb2NhdG9yRGVmLnNjYWxh) | `73.68% <ø> (ø)` | :arrow_up: |
| [...r/plugins/macrodefs/StaticPluginCheckerMacro.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLXJvbGVzL3NyYy9tYWluL3NjYWxhL2l6dW1pL2Rpc3RhZ2Uvc3RhdGljaW5qZWN0b3IvcGx1Z2lucy9tYWNyb2RlZnMvU3RhdGljUGx1Z2luQ2hlY2tlck1hY3JvLnNjYWxh) | `71.42% <ø> (ø)` | :arrow_up: |
| [...n/scala/izumi/distage/model/plan/DepTreeNode.scala](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree#diff-ZGlzdGFnZS9kaXN0YWdlLW1vZGVsL3NyYy9tYWluL3NjYWxhL2l6dW1pL2Rpc3RhZ2UvbW9kZWwvcGxhbi9EZXBUcmVlTm9kZS5zY2FsYQ==) | `16.12% <0%> (-1.73%)` | :arrow_down: |
| ... and [34 more](https://codecov.io/gh/7mind/izumi/pull/746/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/7mind/izumi/pull/746?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/7mind/izumi/pull/746?src=pr&el=footer). Last update [01e4d2a...9a6d755](https://codecov.io/gh/7mind/izumi/pull/746?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/distage/distage-config/src/main/scala/izumi/distage/config/ConfigProvider.scala b/distage/distage-config/src/main/scala/izumi/distage/config/ConfigProvider.scala
index b46445aba..edb9ffb02 100644
--- a/distage/distage-config/src/main/scala/izumi/distage/config/ConfigProvider.scala
+++ b/distage/distage-config/src/main/scala/izumi/distage/config/ConfigProvider.scala
@@ -11,6 +11,7 @@ import izumi.fundamentals.platform.strings.IzString._
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 import izumi.fundamentals.typesafe.config.RuntimeConfigReader
 import com.typesafe.config.{ConfigException, ConfigObject, ConfigValue}
+import izumi.distage.model.plan.operations.OperationOrigin
 
 import scala.util.Try
 import scala.util.control.NonFatal
@@ -54,20 +55,20 @@ class ConfigProvider
       case TranslationResult.Success(op, _) => op
     }
 
-    val newPlan = SemiPlan(plan.definition, ops :+ usedConfigOp, plan.gcMode)
+    val newPlan = SemiPlan(ops :+ usedConfigOp, plan.gcMode)
     newPlan
   }
 
   private def resolvedConfigOp(config: AppConfig, updatedSteps: Vector[TranslationResult]): ExecutableOp.WiringOp.ReferenceInstance = {
     val paths = updatedSteps.collect {
       case TranslationResult.Success(_, path) => path
-
     }
     val resolvedConfig = ResolvedConfig(config, paths.toSet)
     val target = DIKey.get[ResolvedConfig]
     ExecutableOp.WiringOp.ReferenceInstance(
-      target
-      , Wiring.SingletonWiring.Instance(target.tpe, resolvedConfig), None
+      target,
+      Wiring.SingletonWiring.Instance(target.tpe, resolvedConfig),
+      OperationOrigin.Unknown,
     )
   }
 
diff --git a/distage/distage-config/src/main/scala/izumi/distage/config/TranslationResult.scala b/distage/distage-config/src/main/scala/izumi/distage/config/TranslationResult.scala
index d8d3a908d..66d205f98 100644
--- a/distage/distage-config/src/main/scala/izumi/distage/config/TranslationResult.scala
+++ b/distage/distage-config/src/main/scala/izumi/distage/config/TranslationResult.scala
@@ -4,6 +4,8 @@ import izumi.distage.model.plan.ExecutableOp
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.SafeType
 import com.typesafe.config.{ConfigOrigin, ConfigValue}
+import izumi.distage.model.plan.ExecutableOp.SemiplanOp
+import izumi.distage.model.plan.operations.OperationOrigin
 import izumi.fundamentals.platform.exceptions.IzThrowable._
 
 sealed trait TranslationResult
@@ -17,19 +19,21 @@ object TranslationResult {
 
     protected def origin: String = {
       op.origin match {
-        case Some(v) =>
-          s"${v.origin.toString} ($target)"
-        case None =>
+        case d: OperationOrigin.Defined =>
+          s"${d.binding.origin.toString} ($target)"
+
+        case OperationOrigin.Unknown =>
           target.toString
       }
+
     }
   }
 
-  final case class Success(op: ExecutableOp, path: ConfigPath) extends TranslationResult
+  final case class Success(op: SemiplanOp, path: ConfigPath) extends TranslationResult
 
-  final case class Passthrough(op: ExecutableOp) extends TranslationResult
+  final case class Passthrough(op: SemiplanOp) extends TranslationResult
 
-  final case class MissingConfigValue(op: ExecutableOp, paths: Seq[(ConfigPath, Throwable)], configOrigin: ConfigOrigin) extends TranslationFailure {
+  final case class MissingConfigValue(op: SemiplanOp, paths: Seq[(ConfigPath, Throwable)], configOrigin: ConfigOrigin) extends TranslationFailure {
     override def toString: String = {
       import izumi.fundamentals.platform.strings.IzString._
       val tried = paths.map {
@@ -44,11 +48,11 @@ object TranslationResult {
     }
   }
 
-  final case class ExtractionFailure(op: ExecutableOp, tpe: SafeType, path: String, config: ConfigValue, configOrigin: ConfigOrigin, f: Throwable) extends TranslationFailure {
+  final case class ExtractionFailure(op: SemiplanOp, tpe: SafeType, path: String, config: ConfigValue, configOrigin: ConfigOrigin, f: Throwable) extends TranslationFailure {
     override def toString: String = s"$origin: cannot read $tpe out of $path ==> $config: ${f.stackTrace}\nWhen reading from ConfigOrigin: $configOrigin"
   }
 
-  final case class Failure(op: ExecutableOp, configOrigin: ConfigOrigin, f: Throwable) extends TranslationFailure {
+  final case class Failure(op: SemiplanOp, configOrigin: ConfigOrigin, f: Throwable) extends TranslationFailure {
     override def toString: String = s"$origin: unexpected exception: ${f.stackTrace}\nWhen reading from ConfigOrigin: $configOrigin"
   }
 
diff --git a/distage/distage-core/src/main/scala/distage/Distage.scala b/distage/distage-core/src/main/scala/distage/Distage.scala
index b502b6e95..0b94dd531 100644
--- a/distage/distage-core/src/main/scala/distage/Distage.scala
+++ b/distage/distage-core/src/main/scala/distage/Distage.scala
@@ -1,6 +1,7 @@
 package distage
 
 import izumi.distage.model.definition.Axis.AxisValue
+import izumi.distage.model.plan.ExecutableOp
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 import izumi.distage.{model, planning}
 
@@ -42,7 +43,7 @@ trait Distage {
   val OrderedPlan: model.plan.OrderedPlan.type = model.plan.OrderedPlan
   type SemiPlan = model.plan.SemiPlan
   val SemiPlan: model.plan.SemiPlan.type = model.plan.SemiPlan
-  type AbstractPlan = model.plan.AbstractPlan
+  type AbstractPlan[OpType <: ExecutableOp] = model.plan.AbstractPlan[OpType]
 
   type Id = model.definition.Id
   type With[T] = model.definition.With[T]
@@ -62,7 +63,7 @@ trait Distage {
   type BootstrapModule = model.definition.BootstrapModule
   val BootstrapModule: model.definition.BootstrapModule.type = model.definition.BootstrapModule
 
-  val CompactPlanFormatter: model.plan.CompactPlanFormatter.type = model.plan.CompactPlanFormatter
+  val CompactPlanFormatter: model.plan.repr.CompactPlanFormatter.type = model.plan.repr.CompactPlanFormatter
 
   type BootstrapModuleDef = model.definition.BootstrapModuleDef
 
diff --git a/distage/distage-core/src/main/scala/distage/package.scala b/distage/distage-core/src/main/scala/distage/package.scala
index ce43807b5..81121aa2a 100644
--- a/distage/distage-core/src/main/scala/distage/package.scala
+++ b/distage/distage-core/src/main/scala/distage/package.scala
@@ -1,4 +1,5 @@
 import izumi.distage.model.definition.Axis.AxisValue
+import izumi.distage.model.plan.ExecutableOp
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 import izumi.distage.{model, planning}
 
@@ -40,7 +41,7 @@ package object distage extends Distage {
   override val OrderedPlan: model.plan.OrderedPlan.type = model.plan.OrderedPlan
   override type SemiPlan = model.plan.SemiPlan
   override val SemiPlan: model.plan.SemiPlan.type = model.plan.SemiPlan
-  override type AbstractPlan = model.plan.AbstractPlan
+  override type AbstractPlan[OpType <: ExecutableOp] = model.plan.AbstractPlan[OpType]
 
   override type Id = model.definition.Id
   override type With[T] = model.definition.With[T]
@@ -60,7 +61,7 @@ package object distage extends Distage {
   override type BootstrapModule = model.definition.BootstrapModule
   override val BootstrapModule: model.definition.BootstrapModule.type = model.definition.BootstrapModule
 
-  override val CompactPlanFormatter: model.plan.CompactPlanFormatter.type = model.plan.CompactPlanFormatter
+  override val CompactPlanFormatter: model.plan.repr.CompactPlanFormatter.type = model.plan.repr.CompactPlanFormatter
 
   override type BootstrapModuleDef = model.definition.BootstrapModuleDef
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala
index 52c9170eb..55135b730 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala
@@ -4,7 +4,8 @@ import izumi.distage.model._
 import izumi.distage.model.definition.DIResource.DIResourceBase
 import izumi.distage.model.definition.ModuleBase
 import izumi.distage.model.monadic.DIEffect
-import izumi.distage.model.plan.{AbstractPlan, DodgyPlan, OrderedPlan, SemiPlan}
+import izumi.distage.model.plan.initial.PrePlan
+import izumi.distage.model.plan.{AbstractPlan, ExecutableOp, OrderedPlan, SemiPlan}
 import izumi.distage.model.provisioning.PlanInterpreter
 import izumi.distage.model.provisioning.PlanInterpreter.{FailedProvision, FinalizersFilter}
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.TagK
@@ -14,7 +15,7 @@ class InjectorDefaultImpl(parentContext: Locator) extends Injector {
   private val planner: Planner = parentContext.get[Planner]
   private val interpreter = parentContext.get[PlanInterpreter]
 
-  override def freeze(plan: DodgyPlan): SemiPlan = {
+  override def freeze(plan: PrePlan): SemiPlan = {
     planner.freeze(plan)
   }
 
@@ -26,7 +27,7 @@ class InjectorDefaultImpl(parentContext: Locator) extends Injector {
     planner.rewrite(module)
   }
 
-  override def prepare(input: PlannerInput): DodgyPlan = {
+  override def prepare(input: PlannerInput): PrePlan = {
     planner.prepare(input)
   }
 
@@ -38,7 +39,7 @@ class InjectorDefaultImpl(parentContext: Locator) extends Injector {
     planner.finish(semiPlan)
   }
 
-  override def merge(a: AbstractPlan, b: AbstractPlan): OrderedPlan = {
+  override def merge[OpType <: ExecutableOp](a: AbstractPlan[OpType], b: AbstractPlan[OpType]): OrderedPlan = {
     planner.merge(a, b)
   }
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapPlanningObserver.scala b/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapPlanningObserver.scala
index 41f894556..f093fd24f 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapPlanningObserver.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapPlanningObserver.scala
@@ -1,15 +1,16 @@
 package izumi.distage.bootstrap
 
-import izumi.distage.model.plan.{DodgyPlan, SemiPlan, OrderedPlan}
+import izumi.distage.model.plan.initial.PrePlan
+import izumi.distage.model.plan.{OrderedPlan, SemiPlan}
 import izumi.distage.model.planning.PlanningObserver
 import izumi.fundamentals.platform.console.TrivialLogger
 
 class BootstrapPlanningObserver(logger: TrivialLogger) extends PlanningObserver {
-  override def onSuccessfulStep(next: DodgyPlan): Unit = {
+  override def onSuccessfulStep(next: PrePlan): Unit = {
     doLog("Next Plan", next.toString)
   }
 
-  override def onPhase00PlanCompleted(plan: DodgyPlan): Unit = {
+  override def onPhase00PlanCompleted(plan: PrePlan): Unit = {
 
   }
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetHook.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetHook.scala
index c0fa98753..d7f6ffd11 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetHook.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetHook.scala
@@ -1,6 +1,7 @@
 package izumi.distage.planning
 
 import izumi.distage.model.definition.ImplDef
+import izumi.distage.model.plan.operations.OperationOrigin
 import izumi.distage.model.plan.{ExecutableOp, OrderedPlan, SemiPlan}
 import izumi.distage.model.planning.PlanningHook
 import izumi.distage.model.providers.ProviderMagnet
@@ -79,7 +80,7 @@ class AutoSetHook[INSTANCE: Tag, BINDING: Tag](private val wrap: INSTANCE => BIN
 
     val newSteps = plan.steps.flatMap {
       // do not process top-level references to avoid duplicates (the target of reference will be included anyway)
-      case op @ (_: ExecutableOp.WiringOp.ReferenceKey | _: ExecutableOp.ProxyOp.InitProxy) =>
+      case op: ExecutableOp.WiringOp.ReferenceKey =>
         Seq(op)
 
       case op =>
@@ -97,7 +98,7 @@ class AutoSetHook[INSTANCE: Tag, BINDING: Tag](private val wrap: INSTANCE => BIN
           case s: DIKey.SetElementKey if s.set == setKey =>
             Seq(op)
 
-          case _ if ExecutableOp.instanceType(op) <:< instanceType =>
+          case _ if op.instanceType <:< instanceType =>
             if (instanceType == setElementType) {
               newMembers += op.target
               Seq(op)
@@ -115,7 +116,7 @@ class AutoSetHook[INSTANCE: Tag, BINDING: Tag](private val wrap: INSTANCE => BIN
     }
 
     val newSetKeys = ListSet.newBuilder.++=(newMembers).result()
-    val newSetOp = ExecutableOp.CreateSet(setKey, setKey.tpe, newSetKeys, None)
+    val newSetOp = ExecutableOp.CreateSet(setKey, setKey.tpe, newSetKeys, OperationOrigin.Unknown)
 
     plan.copy(steps = newSteps :+ newSetOp)
   }
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/BindingTranslatorImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/BindingTranslatorImpl.scala
index 1684f8733..97d0c438c 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/BindingTranslatorImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/BindingTranslatorImpl.scala
@@ -3,7 +3,8 @@ package izumi.distage.planning
 import izumi.distage.model.definition.Binding.{EmptySetBinding, SetElementBinding, SingletonBinding}
 import izumi.distage.model.definition.{Binding, ImplDef}
 import izumi.distage.model.plan.ExecutableOp.{CreateSet, InstantiationOp, MonadicOp, WiringOp}
-import izumi.distage.model.plan._
+import izumi.distage.model.plan.initial.{NextOps, PrePlan}
+import izumi.distage.model.plan.operations.OperationOrigin
 import izumi.distage.model.planning._
 import izumi.distage.model.reflection.ReflectionProvider
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring.SingletonWiring._
@@ -11,7 +12,7 @@ import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring._
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.{DIKey, Wiring}
 
 trait BindingTranslator {
-  def computeProvisioning(currentPlan: DodgyPlan, binding: Binding): NextOps
+  def computeProvisioning(currentPlan: PrePlan, binding: Binding): NextOps
 }
 
 class BindingTranslatorImpl(
@@ -19,7 +20,7 @@ class BindingTranslatorImpl(
                              protected val hook: PlanningHook,
 
                            ) extends BindingTranslator {
-  def computeProvisioning(currentPlan: DodgyPlan, binding: Binding): NextOps = {
+  def computeProvisioning(currentPlan: PrePlan, binding: Binding): NextOps = {
     binding match {
       case singleton: SingletonBinding[_] =>
         val newOp = provisionSingleton(singleton)
@@ -35,7 +36,7 @@ class BindingTranslatorImpl(
         val setKey = set.key.set
 
         val next = computeProvisioning(currentPlan, SingletonBinding(elementKey, set.implementation, set.tags, set.origin))
-        val oldSet = next.sets.getOrElse(target, CreateSet(setKey, target.tpe, Set.empty, Some(binding)))
+        val oldSet = next.sets.getOrElse(target, CreateSet(setKey, target.tpe, Set.empty, OperationOrigin.UserBinding(binding)))
         val newSet = oldSet.copy(members = oldSet.members + elementKey)
 
         NextOps(
@@ -44,7 +45,7 @@ class BindingTranslatorImpl(
         )
 
       case set: EmptySetBinding[_] =>
-        val newSet = CreateSet(set.key, set.key.tpe, Set.empty, Some(binding))
+        val newSet = CreateSet(set.key, set.key.tpe, Set.empty, OperationOrigin.UserBinding(binding))
 
         NextOps(
           sets = Map(set.key -> newSet)
@@ -67,35 +68,36 @@ class BindingTranslatorImpl(
         pureWiringToWiringOp(target, binding, w)
 
       case w: MonadicWiring.Effect =>
-        MonadicOp.ExecuteEffect(target, pureWiringToWiringOp(w.effectDIKey, binding, w.effectWiring), w, Some(binding))
+        MonadicOp.ExecuteEffect(target, pureWiringToWiringOp(w.effectDIKey, binding, w.effectWiring), w, OperationOrigin.UserBinding(binding))
 
       case w: MonadicWiring.Resource =>
-        MonadicOp.AllocateResource(target, pureWiringToWiringOp(w.effectDIKey, binding, w.effectWiring), w, Some(binding))
+        MonadicOp.AllocateResource(target, pureWiringToWiringOp(w.effectDIKey, binding, w.effectWiring), w, OperationOrigin.UserBinding(binding))
     }
   }
 
   private[this] def pureWiringToWiringOp(target: DIKey, binding: Binding, wiring: PureWiring): WiringOp = {
+    val userBinding = OperationOrigin.UserBinding(binding)
     wiring match {
       case w: Constructor =>
-        WiringOp.InstantiateClass(target, w, Some(binding))
+        WiringOp.InstantiateClass(target, w, userBinding)
 
       case w: AbstractSymbol =>
-        WiringOp.InstantiateTrait(target, w, Some(binding))
+        WiringOp.InstantiateTrait(target, w, userBinding)
 
       case w: Factory =>
-        WiringOp.InstantiateFactory(target, w, Some(binding))
+        WiringOp.InstantiateFactory(target, w, userBinding)
 
       case w: FactoryFunction =>
-        WiringOp.CallFactoryProvider(target, w, Some(binding))
+        WiringOp.CallFactoryProvider(target, w, userBinding)
 
       case w: Function =>
-        WiringOp.CallProvider(target, w, Some(binding))
+        WiringOp.CallProvider(target, w, userBinding)
 
       case w: Instance =>
-        WiringOp.ReferenceInstance(target, w, Some(binding))
+        WiringOp.ReferenceInstance(target, w, userBinding)
 
       case w: Reference =>
-        WiringOp.ReferenceKey(target, w, Some(binding))
+        WiringOp.ReferenceKey(target, w, userBinding)
     }
   }
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/ForwardingRefResolverDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/ForwardingRefResolverDefaultImpl.scala
index 0ee51995a..b70fbb3ae 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/ForwardingRefResolverDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/ForwardingRefResolverDefaultImpl.scala
@@ -53,7 +53,7 @@ class ForwardingRefResolverDefaultImpl
     }
 
     val imports = plan.steps.collect({ case i: ImportDependency => i })
-    OrderedPlan(plan.definition, imports ++ proxyOps, plan.gcMode, plan.topology)
+    OrderedPlan(imports ++ proxyOps, plan.gcMode, plan.topology)
   }
 
   protected def initProxiesAtTheEnd(proxies: List[ProxyOp.MakeProxy], resolvedSteps: Seq[ExecutableOp]): Seq[ExecutableOp] = {
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/PlanAnalyzerDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/PlanAnalyzerDefaultImpl.scala
index 2a38f9bee..7666b646f 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/PlanAnalyzerDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/PlanAnalyzerDefaultImpl.scala
@@ -3,6 +3,9 @@ package izumi.distage.planning
 import izumi.distage.model.plan.ExecutableOp.ProxyOp.{InitProxy, MakeProxy}
 import izumi.distage.model.plan.ExecutableOp.{CreateSet, ImportDependency, InstantiationOp, MonadicOp, WiringOp}
 import izumi.distage.model.plan._
+import izumi.distage.model.plan.topology.DependencyGraph.DependencyKind
+import izumi.distage.model.plan.topology.PlanTopology.PlanTopologyImmutable
+import izumi.distage.model.plan.topology.{DependencyGraph, PlanTopology}
 import izumi.distage.model.planning.PlanAnalyzer
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/PlanMergingPolicyDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/PlanMergingPolicyDefaultImpl.scala
index bcfd505e3..eac087214 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/PlanMergingPolicyDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/PlanMergingPolicyDefaultImpl.scala
@@ -1,19 +1,21 @@
 package izumi.distage.planning
 
+import distage.DIKey
 import izumi.distage.model.exceptions.ConflictingDIKeyBindingsException
+import izumi.distage.model.plan.ExecutableOp.SemiplanOp
 import izumi.distage.model.plan._
+import izumi.distage.model.plan.initial.PrePlan
 import izumi.distage.model.planning.PlanMergingPolicy
 import izumi.distage.model.planning.PlanMergingPolicy.{DIKeyConflictResolution, WithResolve}
 import izumi.fundamentals.platform.language.Quirks
 import izumi.fundamentals.platform.strings.IzString._
-import distage.DIKey
 
 import scala.collection.mutable
 
 class PlanMergingPolicyDefaultImpl extends PlanMergingPolicy with WithResolve {
 
-  override final def freeze(plan: DodgyPlan): SemiPlan = {
-    val resolved = mutable.HashMap[DIKey, Set[ExecutableOp]]()
+  override final def freeze(plan: PrePlan): SemiPlan = {
+    val resolved = mutable.HashMap[DIKey, Set[SemiplanOp]]()
     val issues = mutable.HashMap[DIKey, DIKeyConflictResolution.Failed]()
 
     plan.freeze.foreach {
@@ -29,12 +31,12 @@ class PlanMergingPolicyDefaultImpl extends PlanMergingPolicy with WithResolve {
     if (issues.nonEmpty) {
       handleIssues(plan, resolved.toMap, issues.toMap)
     } else {
-      SemiPlan(plan.definition, resolved.values.flatten.toVector, plan.gcMode)
+      SemiPlan(resolved.values.flatten.toVector, plan.gcMode)
     }
   }
 
 
-  protected def handleIssues(plan: DodgyPlan, resolved: Map[DIKey, Set[ExecutableOp]], issues: Map[DIKey, DIKeyConflictResolution.Failed]): SemiPlan = {
+  protected def handleIssues(plan: PrePlan, resolved: Map[DIKey, Set[SemiplanOp]], issues: Map[DIKey, DIKeyConflictResolution.Failed]): SemiPlan = {
     Quirks.discard(plan, resolved)
     throwOnIssues(issues)
   }
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
index 41bd3b1e8..b4a04a9c1 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
@@ -1,16 +1,18 @@
 package izumi.distage.planning
 
+import distage.DIKey
+import izumi.distage.model.definition.ModuleBase
 import izumi.distage.model.exceptions.{SanityCheckFailedException, UnsupportedOpException}
 import izumi.distage.model.plan.ExecutableOp.WiringOp.ReferenceKey
-import izumi.distage.model.plan.ExecutableOp.{ImportDependency, InstantiationOp, ProxyOp}
+import izumi.distage.model.plan.ExecutableOp.{ImportDependency, InstantiationOp}
 import izumi.distage.model.plan._
+import izumi.distage.model.plan.initial.PrePlan
+import izumi.distage.model.plan.operations.OperationOrigin
 import izumi.distage.model.planning._
 import izumi.distage.model.reflection.SymbolIntrospector
 import izumi.distage.model.{Planner, PlannerInput}
 import izumi.functional.Value
 import izumi.fundamentals.graphs.Toposort
-import distage.DIKey
-import izumi.distage.model.definition.ModuleBase
 
 final class PlannerDefaultImpl
 (
@@ -45,7 +47,7 @@ final class PlannerDefaultImpl
     hook.hookDefinition(module)
   }
 
-  override def freeze(plan: DodgyPlan): SemiPlan = {
+  override def freeze(plan: PrePlan): SemiPlan = {
     Value(plan)
       .map(hook.phase00PostCompletion)
       .eff(planningObserver.onPhase00PlanCompleted)
@@ -54,12 +56,12 @@ final class PlannerDefaultImpl
   }
 
   // TODO: add tests
-  override def merge(a: AbstractPlan, b: AbstractPlan): OrderedPlan = {
-    order(SemiPlan(a.definition ++ b.definition, (a.steps ++ b.steps).toVector, a.gcMode ++ b.gcMode))
+  override def merge[OpType <: ExecutableOp](a: AbstractPlan[OpType], b: AbstractPlan[OpType]): OrderedPlan = {
+    order(SemiPlan((a.toSemi.steps ++ b.toSemi.steps).toVector, a.gcMode ++ b.gcMode))
   }
 
-  override def prepare(input: PlannerInput): DodgyPlan = {
-      input.bindings.bindings.foldLeft(DodgyPlan.empty(input.bindings, input.mode)) {
+  override def prepare(input: PlannerInput): PrePlan = {
+      input.bindings.bindings.foldLeft(PrePlan.empty(input.bindings, input.mode)) {
       case (currentPlan, binding) =>
         Value(bindingTranslator.computeProvisioning(currentPlan, binding))
           .eff(sanityChecker.assertProvisionsSane)
@@ -103,19 +105,19 @@ final class PlannerDefaultImpl
       .filterKeys(k => !plan.index.contains(k))
       .map {
         case (missing, refs) =>
-          val maybeFirstOrigin = refs.headOption.flatMap(key => plan.index.get(key)).flatMap(_.origin)
-          missing -> ImportDependency(missing, refs.toSet, maybeFirstOrigin)
+          val maybeFirstOrigin = refs.headOption.flatMap(key => plan.index.get(key)).map(_.origin.toSynthetic)
+          missing -> ImportDependency(missing, refs.toSet, maybeFirstOrigin.getOrElse(OperationOrigin.Unknown))
       }
       .toMap
 
     val allOps = (imports.values ++ plan.steps).toVector
     val roots = plan.gcMode.toSet
-    val missingRoots: Vector[ExecutableOp] = roots.diff(allOps.map(_.target).toSet).map {
+    val missingRoots = roots.diff(allOps.map(_.target).toSet).map {
       root =>
-        ImportDependency(root, Set.empty, None)
+        ImportDependency(root, Set.empty, OperationOrigin.Unknown)
     }.toVector
 
-    SemiPlan(plan.definition, missingRoots ++ allOps, plan.gcMode)
+    SemiPlan(missingRoots ++ allOps, plan.gcMode)
   }
 
   private[this] def reorderOperations(completedPlan: SemiPlan): OrderedPlan = {
@@ -161,8 +163,6 @@ final class PlannerDefaultImpl
           throw new UnsupportedOpException(s"Failed to break circular dependencies, best candidate $best is reference O_o: $keys", op)
         case op: ImportDependency =>
           throw new UnsupportedOpException(s"Failed to break circular dependencies, best candidate $best is import O_o: $keys", op)
-        case op: ProxyOp =>
-          throw new UnsupportedOpException(s"Failed to break circular dependencies, best candidate $best is proxy O_o: $keys", op)
         case op: InstantiationOp if !symbolIntrospector.canBeProxied(op.target.tpe) =>
           throw new UnsupportedOpException(s"Failed to break circular dependencies, best candidate $best is not proxyable (final?): $keys", op)
         case _: InstantiationOp =>
@@ -184,11 +184,11 @@ final class PlannerDefaultImpl
 
     val sortedOps = sortedKeys.flatMap(k => index.get(k).toSeq)
 
-    OrderedPlan(completedPlan.definition, sortedOps.toVector, completedPlan.gcMode, topology)
+    OrderedPlan(sortedOps.toVector, completedPlan.gcMode, topology)
   }
 
   private[this] def hasByNameParameter(fsto: ExecutableOp): Boolean = {
-    val fstoTpe = ExecutableOp.instanceType(fsto)
+    val fstoTpe = fsto.instanceType
     val ctorSymbol = symbolIntrospector.selectConstructorMethod(fstoTpe)
     val hasByName = ctorSymbol.exists(symbolIntrospector.hasByNameParameter)
     hasByName
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/PlanningHookAggregate.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/PlanningHookAggregate.scala
index 18bece9be..95db39b46 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/PlanningHookAggregate.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/PlanningHookAggregate.scala
@@ -1,7 +1,8 @@
 package izumi.distage.planning
 
 import izumi.distage.model.definition.{Binding, ModuleBase}
-import izumi.distage.model.plan.{DodgyPlan, SemiPlan, OrderedPlan}
+import izumi.distage.model.plan.initial.PrePlan
+import izumi.distage.model.plan.{OrderedPlan, SemiPlan}
 import izumi.distage.model.planning.PlanningHook
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 
@@ -24,7 +25,7 @@ final class PlanningHookAggregate
     }
   }
 
-  override def phase00PostCompletion(plan: DodgyPlan): DodgyPlan = {
+  override def phase00PostCompletion(plan: PrePlan): PrePlan = {
     hooks.foldLeft(plan) {
       case (acc, hook) =>
         hook.phase00PostCompletion(acc)
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/PlanningObserverAggregate.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/PlanningObserverAggregate.scala
index 76a2c783d..af44ab7c8 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/PlanningObserverAggregate.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/PlanningObserverAggregate.scala
@@ -1,14 +1,15 @@
 package izumi.distage.planning
 
-import izumi.distage.model.plan.{DodgyPlan, OrderedPlan, SemiPlan}
+import izumi.distage.model.plan.initial.PrePlan
+import izumi.distage.model.plan.{OrderedPlan, SemiPlan}
 import izumi.distage.model.planning.PlanningObserver
 
 final class PlanningObserverAggregate(planningObservers: Set[PlanningObserver]) extends PlanningObserver {
-  override def onSuccessfulStep(next: DodgyPlan): Unit = {
+  override def onSuccessfulStep(next: PrePlan): Unit = {
     planningObservers.foreach(_.onSuccessfulStep(next))
   }
 
-  override def onPhase00PlanCompleted(plan: DodgyPlan): Unit = {
+  override def onPhase00PlanCompleted(plan: PrePlan): Unit = {
     planningObservers.foreach(_.onPhase00PlanCompleted(plan))
   }
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/extensions/GraphDumpObserver.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/extensions/GraphDumpObserver.scala
index 4deff4e0d..d4afaade8 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/extensions/GraphDumpObserver.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/extensions/GraphDumpObserver.scala
@@ -13,6 +13,8 @@ import izumi.distage.planning.extensions.GraphDumpObserver.RenderedDot
 import izumi.fundamentals.graphs.dotml.Digraph
 import izumi.fundamentals.platform.language.Quirks._
 import distage._
+import izumi.distage.model.plan.initial.PrePlan
+import izumi.distage.model.plan.repr.KeyMinimizer
 
 import scala.collection.mutable
 
@@ -22,9 +24,9 @@ final class GraphDumpObserver
 ) extends PlanningObserver {
   private[this] val beforeFinalization = new AtomicReference[SemiPlan](null)
 
-  override def onSuccessfulStep(next: DodgyPlan): Unit = {}
+  override def onSuccessfulStep(next: PrePlan): Unit = {}
 
-  override def onPhase00PlanCompleted(plan: DodgyPlan): Unit = synchronized {
+  override def onPhase00PlanCompleted(plan: PrePlan): Unit = synchronized {
     beforeFinalization.set(null)
   }
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/gc/TracingDIGC.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/gc/TracingDIGC.scala
index 60d6e56f0..7e232feba 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/gc/TracingDIGC.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/gc/TracingDIGC.scala
@@ -1,9 +1,7 @@
 package izumi.distage.planning.gc
 
 import izumi.distage.model.GCMode
-import izumi.distage.model.definition.Module
-import izumi.distage.model.exceptions.UnsupportedOpException
-import izumi.distage.model.plan.ExecutableOp.{CreateSet, ImportDependency, MonadicOp, ProxyOp, WiringOp}
+import izumi.distage.model.plan.ExecutableOp._
 import izumi.distage.model.plan.{ExecutableOp, SemiPlan}
 import izumi.distage.model.planning.DIGarbageCollector
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
@@ -11,15 +9,15 @@ import izumi.fundamentals.graphs.AbstractGCTracer
 
 import scala.collection.mutable
 
-class TracingDIGC
+class TracingDIGC[OpType <: ExecutableOp]
 (
   roots: Set[DIKey],
-  fullIndex: Map[DIKey, ExecutableOp],
+  fullIndex: Map[DIKey, SemiplanOp],
   override val ignoreMissingDeps: Boolean,
-) extends AbstractGCTracer[DIKey, ExecutableOp] {
+) extends AbstractGCTracer[DIKey, SemiplanOp] {
 
   @inline
-  override protected def extractDependencies(index: Map[DIKey, ExecutableOp], node: ExecutableOp): Set[DIKey] = {
+  override protected def extractDependencies(index: Map[DIKey, SemiplanOp], node: SemiplanOp): Set[DIKey] = {
     node match {
       case op: ExecutableOp.InstantiationOp =>
         op match {
@@ -47,8 +45,6 @@ class TracingDIGC
         }
       case _: ImportDependency =>
         Set.empty
-      case p: ProxyOp =>
-        throw new UnsupportedOpException(s"Garbage collector didn't expect a proxy operation; proxies can't exist at this stage", p)
     }
   }
 
@@ -100,7 +96,7 @@ class TracingDIGC
 
   override protected def isRoot(node: DIKey): Boolean = roots.contains(node)
 
-  override protected def id(node: ExecutableOp): DIKey = node.target
+  override protected def id(node: SemiplanOp): DIKey = node.target
 }
 
 object TracingDIGC extends DIGarbageCollector {
@@ -108,15 +104,9 @@ object TracingDIGC extends DIGarbageCollector {
     plan.gcMode match {
       case GCMode.GCRoots(roots) =>
         assert(roots.nonEmpty)
-        val collected = new TracingDIGC(roots, plan.index, ignoreMissingDeps = false).gc(plan.steps)
-
-        val updatedDefn = {
-          val oldDefn = plan.definition.bindings
-          val reachable = collected.reachable
-          Module.make(oldDefn.filter(reachable contains _.key))
-        }
 
-        SemiPlan(updatedDefn, collected.nodes, plan.gcMode)
+        val collected = new TracingDIGC(roots, plan.index, ignoreMissingDeps = false).gc(plan.steps)
+        SemiPlan(collected.nodes, plan.gcMode)
 
       case GCMode.NoGC =>
         plan
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/Planner.scala b/distage/distage-model/src/main/scala/izumi/distage/model/Planner.scala
index 6211530eb..d87306d05 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/Planner.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/Planner.scala
@@ -2,6 +2,7 @@ package izumi.distage.model
 
 import izumi.distage.model.definition.ModuleBase
 import izumi.distage.model.plan._
+import izumi.distage.model.plan.initial.PrePlan
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.DIKey
 
@@ -73,21 +74,35 @@ case class TriSplittedPlan(
                             shared: Subplan,
                           )
 
+object TriSplittedPlan {
+  implicit class TriPlanEx(split: TriSplittedPlan) {
+    def render(): String = {
+      import izumi.fundamentals.platform.strings.IzString._
+      Seq(
+        split.shared.plan.render().listing("Shared Plan"),
+        split.side.plan.render().listing("Side Plan"),
+        split.primary.plan.render().listing("Primary Plan"),
+      ).mkString("\n")
+    }
+  }
+
+}
+
 
 /** Transforms [[ModuleBase]] into [[OrderedPlan]] */
 trait Planner {
   def plan(input: PlannerInput): OrderedPlan
 
-  def merge(a: AbstractPlan, b: AbstractPlan): OrderedPlan
+  def merge[OpType <: ExecutableOp](a: AbstractPlan[OpType], b: AbstractPlan[OpType]): OrderedPlan
 
   // plan lifecycle
   def planNoRewrite(input: PlannerInput): OrderedPlan
 
   def rewrite(module: ModuleBase): ModuleBase
 
-  def prepare(input: PlannerInput): DodgyPlan
+  def prepare(input: PlannerInput): PrePlan
 
-  def freeze(plan: DodgyPlan): SemiPlan
+  def freeze(plan: PrePlan): SemiPlan
 
   def finish(semiPlan: SemiPlan): OrderedPlan
 
@@ -95,7 +110,7 @@ trait Planner {
 
   final def triSplitPlan(appModule: ModuleBase, primaryRoots: Set[DIKey])(extractSubRoots: OrderedPlan => Set[DIKey]): TriSplittedPlan = {
     val rewritten = rewrite(appModule)
-    val primaryPlan = toSubplanNoRewrite(appModule, primaryRoots)
+    val primaryPlan = toSubplanNoRewrite(rewritten, primaryRoots)
 
     // here we extract integration checks out of our shared components plan and build it
     val subplanRoots = extractSubRoots(primaryPlan)
@@ -115,12 +130,10 @@ trait Planner {
     val sharedPlan = toSubplanNoRewrite(appModule, sharedKeys)
 
     val noSharedComponentsModule = appModule.drop(sharedKeys)
-    val primplan = toSubplanNoRewrite(noSharedComponentsModule, primaryRoots)
-    val subModule = noSharedComponentsModule.drop(primplan.index.keySet)
-    val subplan = toSubplanNoRewrite(subModule, subplanRoots)
+    val primplan = extractedPrimaryPlan.replaceWithImports(sharedKeys)
 
-    //    val conflicts = primplan.index.keySet.intersect(subplan.index.keySet).filterNot(k => primplan.index(k).isInstanceOf[ExecutableOp.ImportDependency])
-    //    assert(conflicts.isEmpty, s"conflicts: ${conflicts}")
+    val subModule = noSharedComponentsModule.drop(primplan.index.keySet)
+    val subplan = extractedSubplan.replaceWithImports(sharedKeys)
 
     val sharedModule = appModule.preserveOnly(sharedPlan.index.keySet)
     val primModule = noSharedComponentsModule.drop(subplan.index.keySet)
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/definition/LocatorDef.scala b/distage/distage-model/src/main/scala/izumi/distage/model/definition/LocatorDef.scala
index 964e190ee..04cb83e47 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/definition/LocatorDef.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/definition/LocatorDef.scala
@@ -11,6 +11,10 @@ import izumi.distage.model.definition.dsl.AbstractBindingDefDSL._
 import izumi.distage.model.exceptions.LocatorDefUninstantiatedBindingException
 import izumi.distage.model.plan.ExecutableOp.WiringOp.ReferenceInstance
 import izumi.distage.model.plan._
+import izumi.distage.model.plan.operations.OperationOrigin
+import izumi.distage.model.plan.topology.DependencyGraph
+import izumi.distage.model.plan.topology.DependencyGraph.DependencyKind
+import izumi.distage.model.plan.topology.PlanTopology.PlanTopologyImmutable
 import izumi.distage.model.provisioning.PlanInterpreter
 import izumi.distage.model.references.IdentifiedRef
 import izumi.distage.model.reflection.universe
@@ -51,17 +55,11 @@ trait LocatorDef
 
     val ops = frozenInstances.map {
       case IdentifiedRef(key, value) =>
-        ReferenceInstance(key, Instance(key.tpe, value), None)
+        val origin = OperationOrigin.SyntheticBinding(Binding.SingletonBinding[DIKey](key, ImplDef.InstanceImpl(key.tpe, value), Set.empty, SourceFilePosition.unknown))
+        ReferenceInstance(key, Instance(key.tpe, value), origin)
     }.toVector
 
-    val moduleDef = Module.make(
-      frozenInstances.map {
-        case IdentifiedRef(key, value) =>
-          Binding.SingletonBinding[DIKey](key, ImplDef.InstanceImpl(key.tpe, value), Set.empty, SourceFilePosition.unknown)
-      }.toSet
-    )
-
-    OrderedPlan(moduleDef, ops, GCMode.NoGC, topology)
+    OrderedPlan(ops, GCMode.NoGC, topology)
   }
 
   override def parent: Option[Locator] = None
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/exceptions/ForwardRefException.scala b/distage/distage-model/src/main/scala/izumi/distage/model/exceptions/ForwardRefException.scala
index a6d23d0d2..7907d5037 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/exceptions/ForwardRefException.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/exceptions/ForwardRefException.scala
@@ -1,6 +1,6 @@
 package izumi.distage.model.exceptions
 
-import izumi.distage.model.plan.PlanTopology
+import izumi.distage.model.plan.topology.PlanTopology
 
 class ForwardRefException(message: String, val topology: PlanTopology) extends DIException(message, null)
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/exceptions/MissingRefException.scala b/distage/distage-model/src/main/scala/izumi/distage/model/exceptions/MissingRefException.scala
index ae7ceb254..d0da11849 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/exceptions/MissingRefException.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/exceptions/MissingRefException.scala
@@ -1,6 +1,6 @@
 package izumi.distage.model.exceptions
 
-import izumi.distage.model.plan.PlanTopology
+import izumi.distage.model.plan.topology.PlanTopology
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 
 class MissingRefException(message: String, val missing: Set[RuntimeDIUniverse.DIKey], val topology: Option[PlanTopology]) extends DIException(message, null)
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala b/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala
index 0f87e81ca..5f75f94c2 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala
@@ -21,8 +21,8 @@ object DIEffectAsync extends LowPriorityDIEffectAsyncInstances {
   implicit val diEffectParIdentity: DIEffectAsync[Identity] = {
     new DIEffectAsync[Identity] {
       override def parTraverse_[A](l: Iterable[A])(f: A => Unit): Unit = {
-
         parTraverse(l)(f)
+        ()
       }
       override def sleep(duration: FiniteDuration): Identity[Unit] = {
         Thread.sleep(duration.toMillis)
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlan.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlan.scala
index 273ab5354..d28bca7aa 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlan.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlan.scala
@@ -1,306 +1,57 @@
 package izumi.distage.model.plan
 
-import cats.Applicative
-import cats.kernel.Monoid
+import izumi.distage.model.GCMode
 import izumi.distage.model.definition.ModuleBase
-import izumi.distage.model.plan.ExecutableOp.ImportDependency
-import izumi.distage.model.plan.ExecutableOp.ProxyOp.{InitProxy, MakeProxy}
-import izumi.distage.model.plan.ExecutableOp.WiringOp.{CallProvider, ReferenceInstance}
-import izumi.distage.model.plan.SemiPlanOrderedPlanInstances.{CatsMonoid, resolveImportsImpl}
-import izumi.distage.model.providers.ProviderMagnet
-import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring.SingletonWiring
-import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring.SingletonWiring.Instance
-import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
-import izumi.distage.model.{GCMode, Locator}
-import izumi.functional.Renderable
+import izumi.distage.model.plan.ExecutableOp.SemiplanOp
+import izumi.distage.model.plan.impl.{AbstractPlanOps, OrderedPlanExtensions, OrderedPlanOps, PlanLazyOps, SemiPlanExtensions, SemiPlanOps}
+import izumi.distage.model.plan.topology.PlanTopology
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 
-// TODO: we need to parameterize plans with op types to avoid possibility of having proxy ops in semiplan
-sealed trait AbstractPlan {
-  def definition: ModuleBase
 
-  def steps: Seq[ExecutableOp]
+sealed trait AbstractPlan[OpType <: ExecutableOp] extends AbstractPlanExtendedAPI[OpType] {
+  def definition: ModuleBase
 
   def gcMode: GCMode
 
-  def resolveImports(f: PartialFunction[ImportDependency, Any]): AbstractPlan
-
-  def resolveImport[T: Tag](instance: T): AbstractPlan
-
-  def resolveImport[T: Tag](id: String)(instance: T): AbstractPlan
-
-  def locateImports(locator: Locator): AbstractPlan
-
-  final lazy val index: Map[DIKey, ExecutableOp] = {
-    steps.map(s => s.target -> s).toMap
-  }
-
-  /** Get all imports (unresolved dependencies).
-   *
-   * Note, presence of imports does not automatically mean that a plan is invalid,
-   * Imports may be fulfilled by a `Locator`, by BootstrapContext, or they may be materialized by a custom
-   * [[izumi.distage.model.provisioning.strategies.ImportStrategy]]
-   * */
-  final def getImports: Seq[ImportDependency] =
-    steps.collect { case i: ImportDependency => i }
-
-  final def resolveImportsOp(f: PartialFunction[ImportDependency, Seq[ExecutableOp]]): SemiPlan = {
-    SemiPlan(definition, steps = AbstractPlan.resolveImports(f, steps.toVector), gcMode)
-  }
-
-  final def providerImport[T](function: ProviderMagnet[T]): SemiPlan = {
-    resolveImportsOp {
-      case i if i.target.tpe == function.get.ret =>
-        Seq(CallProvider(i.target, SingletonWiring.Function(function.get, function.get.associations), i.origin))
-    }
-  }
-
-  final def providerImport[T](id: String)(function: ProviderMagnet[T]): SemiPlan = {
-    resolveImportsOp {
-      case i if i.target == DIKey.IdKey(function.get.ret, id) =>
-        Seq(CallProvider(i.target, SingletonWiring.Function(function.get, function.get.associations), i.origin))
-    }
-  }
-
-  final def keys: Set[DIKey] = {
-    steps.map(_.target).toSet
-  }
-
-  final def filter[T: Tag]: Seq[ExecutableOp] = {
-    steps.filter(_.target == DIKey.get[T])
-  }
-
-  final def map(f: ExecutableOp => ExecutableOp): SemiPlan = {
-    val SemiPlan(definition, steps, gcMode) = toSemi
-    SemiPlan(definition, steps.map(f), gcMode)
-  }
-
-  final def flatMap(f: ExecutableOp => Seq[ExecutableOp]): SemiPlan = {
-    val SemiPlan(definition, steps, gcMode) = toSemi
-    SemiPlan(definition, steps.flatMap(f), gcMode)
-  }
-
-  final def collect(f: PartialFunction[ExecutableOp, ExecutableOp]): SemiPlan = {
-    val SemiPlan(definition, steps, gcMode) = toSemi
-    SemiPlan(definition, steps.collect(f), gcMode)
-  }
+  def steps: Seq[OpType]
 
-  final def ++(that: AbstractPlan): SemiPlan = {
-    val SemiPlan(definition, steps, gcMode) = toSemi
-    val that0 = that.toSemi
-    SemiPlan(definition ++ that0.definition, steps ++ that0.steps, gcMode)
-  }
-
-  final def collectChildren[T: Tag]: Seq[ExecutableOp] = {
-    val parent = SafeType.get[T]
-    steps.filter {
-      op =>
-        val maybeChild = ExecutableOp.instanceType(op)
-        maybeChild <:< parent
-    }
-  }
-
-  final def foldLeft[T](z: T, f: (T, ExecutableOp) => T): T = {
-    steps.foldLeft(z)(f)
-  }
+  def index: Map[RuntimeDIUniverse.DIKey, OpType]
 
   override def toString: String = {
     steps.map(_.toString).mkString("\n")
   }
-
-  def toSemi: SemiPlan = {
-    val safeSteps = steps.flatMap {
-      case _: InitProxy =>
-        Seq.empty
-      case i: MakeProxy =>
-        Seq(i.op)
-      case o => Seq(o)
-    }
-    SemiPlan(definition, safeSteps.toVector, gcMode)
-  }
-
-}
-
-object AbstractPlan {
-  private[plan] def resolveImports(f: PartialFunction[ImportDependency, Seq[ExecutableOp]], steps: Vector[ExecutableOp]): Vector[ExecutableOp] =
-    steps.flatMap {
-      case i: ImportDependency =>
-        f.lift(i) getOrElse Seq(i)
-      case op =>
-        Seq(op)
-    }
-
-  private[plan] def importToInstances(f: PartialFunction[ImportDependency, Any]): PartialFunction[ImportDependency, Seq[ExecutableOp]] =
-    Function.unlift(i => f.lift(i).map(instance => Seq(ReferenceInstance(i.target, Instance(i.target.tpe, instance), i.origin))))
-}
-
-/**
- * An unordered plan.
- *
- * You can turn into an [[OrderedPlan]] via [[izumi.distage.model.Planner.finish]]
- */
-final case class SemiPlan(definition: ModuleBase, steps: Vector[ExecutableOp], gcMode: GCMode) extends AbstractPlan {
-
-  override def toSemi: SemiPlan = this
-
-  override def resolveImport[T: Tag](instance: T): SemiPlan =
-    resolveImports {
-      case i if i.target == DIKey.get[T] =>
-        instance
-    }
-
-  override def resolveImport[T: Tag](id: String)(instance: T): SemiPlan = {
-    resolveImports {
-      case i if i.target == DIKey.get[T].named(id) =>
-        instance
-    }
-  }
-
-  override def resolveImports(f: PartialFunction[ImportDependency, Any]): SemiPlan = {
-    copy(steps = AbstractPlan.resolveImports(AbstractPlan.importToInstances(f), steps))
-  }
-
-  override def locateImports(locator: Locator): SemiPlan = {
-    resolveImports(Function.unlift(i => locator.lookupLocal[Any](i.target)))
-  }
-
-}
-
-object SemiPlan {
-
-  /**
-    * This instance uses 'no more orphans' trick to provide an Optional instance
-    * only IFF you have cats-effect as a dependency without REQUIRING a cats-effect dependency.
-    *
-    * Optional instance via https://blog.7mind.io/no-more-orphans.html
-    */
-  implicit def optionalCatsMonoidForSemiplan[K[_] : CatsMonoid]: K[SemiPlan] =
-    new Monoid[SemiPlan] {
-      override def empty: SemiPlan = SemiPlan(ModuleBase.empty, Vector.empty, GCMode.NoGC)
-
-      override def combine(x: SemiPlan, y: SemiPlan): SemiPlan = x ++ y
-    }.asInstanceOf[K[SemiPlan]]
-
-  import cats.instances.vector._
-  import cats.syntax.functor._
-  import cats.syntax.traverse._
-
-  implicit final class SemiPlanExts(private val plan: SemiPlan) extends AnyVal {
-    def traverse[F[_] : Applicative](f: ExecutableOp => F[ExecutableOp]): F[SemiPlan] =
-      plan.steps.traverse(f).map(s => plan.copy(steps = s))
-
-    def flatMapF[F[_] : Applicative](f: ExecutableOp => F[Seq[ExecutableOp]]): F[SemiPlan] =
-      plan.steps.traverse(f).map(s => plan.copy(steps = s.flatten))
-
-    def resolveImportF[T]: ResolveImportFSemiPlanPartiallyApplied[T] = new ResolveImportFSemiPlanPartiallyApplied(plan)
-
-    def resolveImportF[F[_] : Applicative, T: Tag](f: F[T]): F[SemiPlan] = resolveImportF[T](f)
-
-    def resolveImportsF[F[_] : Applicative](f: PartialFunction[ImportDependency, F[Any]]): F[SemiPlan] =
-      resolveImportsImpl(f, plan.steps).map(s => plan.copy(steps = s))
-  }
-
-}
-
-final case class OrderedPlan(definition: ModuleBase, steps: Vector[ExecutableOp], gcMode: GCMode, topology: PlanTopology) extends AbstractPlan {
-  override def resolveImports(f: PartialFunction[ImportDependency, Any]): OrderedPlan = {
-    copy(steps = AbstractPlan.resolveImports(AbstractPlan.importToInstances(f), steps))
-  }
-
-  override def resolveImport[T: Tag](instance: T): OrderedPlan =
-    resolveImports {
-      case i if i.target == DIKey.get[T] =>
-        instance
-    }
-
-  override def resolveImport[T: Tag](id: String)(instance: T): OrderedPlan = {
-    resolveImports {
-      case i if i.target == DIKey.get[T].named(id) =>
-        instance
-    }
-  }
-
-  override def locateImports(locator: Locator): OrderedPlan = {
-    resolveImports(Function.unlift(i => locator.lookupLocal[Any](i.target)))
-  }
 }
 
-object OrderedPlan {
-  implicit val defaultFormatter: Renderable[OrderedPlan] = CompactPlanFormatter.OrderedPlanFormatter
-
-  def empty: OrderedPlan = OrderedPlan(ModuleBase.empty, Vector.empty, GCMode.NoGC, PlanTopologyImmutable(DependencyGraph(Map.empty, DependencyKind.Depends), DependencyGraph(Map.empty, DependencyKind.Required)))
-
-  implicit final class PlanSyntax(private val plan: OrderedPlan) extends AnyVal {
-    def render()(implicit ev: Renderable[OrderedPlan]): String = ev.render(plan)
-
-    def renderDeps(node: DepNode): String = new DepTreeRenderer(node, plan).render()
-
-    def renderAllDeps(): String = {
-      val effectiveRoots = plan.keys.filter(k => plan.topology.dependees.direct(k).isEmpty)
-      effectiveRoots.map(root => plan.topology.dependencies.tree(root)).map(renderDeps).mkString("\n")
-    }
-  }
-
-  implicit final class OrderedPlanExts(private val plan: OrderedPlan) extends AnyVal {
-
-    import cats.instances.vector._
-    import cats.syntax.functor._
-    import cats.syntax.traverse._
 
-    def traverse[F[_] : Applicative](f: ExecutableOp => F[ExecutableOp]): F[SemiPlan] =
-      plan.steps.traverse(f).map(SemiPlan(plan.definition, _, plan.gcMode))
+object AbstractPlan extends AbstractPlanOps
 
-    def flatMapF[F[_] : Applicative](f: ExecutableOp => F[Seq[ExecutableOp]]): F[SemiPlan] =
-      plan.steps.traverse(f).map(s => SemiPlan(plan.definition, s.flatten, plan.gcMode))
+sealed trait ExtendedPlan[OpType <: ExecutableOp] extends AbstractPlan[OpType] with PlanLazyOps[OpType]
 
-    def resolveImportF[T]: ResolveImportFOrderedPlanPartiallyApplied[T] = new ResolveImportFOrderedPlanPartiallyApplied(plan)
-
-    def resolveImportF[F[_] : Applicative, T: Tag](f: F[T]): F[OrderedPlan] = resolveImportF[T](f)
-
-    def resolveImportsF[F[_] : Applicative](f: PartialFunction[ImportDependency, F[Any]]): F[OrderedPlan] =
-      resolveImportsImpl(f, plan.steps).map(s => plan.copy(steps = s))
-  }
+/**
+  * An unordered plan.
+  *
+  * You can turn into an [[OrderedPlan]] via [[izumi.distage.model.Planner.finish]]
+  */
+final case class SemiPlan(
+                           steps: Vector[SemiplanOp],
+                           gcMode: GCMode
+                         ) extends ExtendedPlan[SemiplanOp] with SemiPlanOps
 
-}
+object SemiPlan extends SemiPlanExtensions
 
-private[plan] final class ResolveImportFSemiPlanPartiallyApplied[T](private val plan: SemiPlan) extends AnyVal {
-  def apply[F[_] : Applicative](f: F[T])(implicit ev: Tag[T]): F[SemiPlan] =
-    plan.resolveImportsF[F] {
-      case i if i.target == DIKey.get[T] => f.asInstanceOf[F[Any]]
-    }
-}
+/**
+  * Linearized graph which is ready to be consumed by linear executors
+  *
+  * May contain cyclic dependencies resolved with proxies
+  */
+final case class OrderedPlan(
+                              steps: Vector[ExecutableOp],
+                              gcMode: GCMode,
+                              topology: PlanTopology
+                            ) extends ExtendedPlan[ExecutableOp] with OrderedPlanOps
 
-private[plan] final class ResolveImportFOrderedPlanPartiallyApplied[T](private val plan: OrderedPlan) extends AnyVal {
-  def apply[F[_] : Applicative](f: F[T])(implicit ev: Tag[T]): F[OrderedPlan] =
-    plan.resolveImportsF[F] {
-      case i if i.target == DIKey.get[T] => f.asInstanceOf[F[Any]]
-    }
-}
 
-private object SemiPlanOrderedPlanInstances {
+object OrderedPlan extends OrderedPlanExtensions
 
-  /**
-    * This instance uses 'no more orphans' trick to provide an Optional instance
-    * only IFF you have cats-effect as a dependency without REQUIRING a cats-effect dependency.
-    *
-    * Optional instance via https://blog.7mind.io/no-more-orphans.html
-    */
-  sealed abstract class CatsMonoid[K[_]]
-  object CatsMonoid {
-    @inline implicit final def get: CatsMonoid[Monoid] = null
-  }
 
-  import cats.instances.vector._
-  import cats.syntax.functor._
-  import cats.syntax.traverse._
 
-  @inline
-  final def resolveImportsImpl[F[_] : Applicative](f: PartialFunction[ImportDependency, F[Any]], steps: Vector[ExecutableOp]): F[Vector[ExecutableOp]] =
-    steps.traverse {
-      case i: ImportDependency =>
-        f.lift(i).map {
-          _.map[ExecutableOp](instance => ReferenceInstance(i.target, Instance(i.target.tpe, instance), i.origin))
-        } getOrElse Applicative[F].pure(i)
-      case op =>
-        Applicative[F].pure(op)
-    }
-}
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlanExtendedAPI.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlanExtendedAPI.scala
new file mode 100644
index 000000000..47402b9f6
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlanExtendedAPI.scala
@@ -0,0 +1,83 @@
+package izumi.distage.model.plan
+
+import izumi.distage.model.Locator
+import izumi.distage.model.plan.ExecutableOp.ProxyOp.{InitProxy, MakeProxy}
+import izumi.distage.model.plan.ExecutableOp.{ImportDependency, ProxyOp, SemiplanOp}
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
+
+trait AbstractPlanExtendedAPI[OpType <: ExecutableOp] {
+  this: AbstractPlan[OpType] =>
+  def resolveImports(f: PartialFunction[ImportDependency, Any]): AbstractPlan[OpType]
+
+  def resolveImport[T: Tag](instance: T): AbstractPlan[OpType]
+
+  def resolveImport[T: Tag](id: String)(instance: T): AbstractPlan[OpType]
+
+  def locateImports(locator: Locator): AbstractPlan[OpType]
+
+  /** Get all imports (unresolved dependencies).
+    *
+    * Note, presence of imports does not automatically mean that a plan is invalid,
+    * Imports may be fulfilled by a `Locator`, by BootstrapContext, or they may be materialized by a custom
+    * [[izumi.distage.model.provisioning.strategies.ImportStrategy]]
+    * */
+  final def getImports: Seq[ImportDependency] =
+    steps.collect { case i: ImportDependency => i }
+
+  final def keys: Set[DIKey] = {
+    steps.map(_.target).toSet
+  }
+
+  final def filter[T: Tag]: Seq[ExecutableOp] = {
+    steps.filter(_.target == DIKey.get[T])
+  }
+
+  final def map(f: SemiplanOp => SemiplanOp): SemiPlan = {
+    val SemiPlan(steps, gcMode) = toSemi
+    SemiPlan(steps.map(f), gcMode)
+  }
+
+  final def flatMap(f: SemiplanOp => Seq[SemiplanOp]): SemiPlan = {
+    val SemiPlan(steps, gcMode) = toSemi
+    SemiPlan(steps.flatMap(f), gcMode)
+  }
+
+  final def collect(f: PartialFunction[SemiplanOp, SemiplanOp]): SemiPlan = {
+    val SemiPlan(steps, gcMode) = toSemi
+    SemiPlan(steps.collect(f), gcMode)
+  }
+
+  final def ++(that: AbstractPlan[OpType]): SemiPlan = {
+    val SemiPlan(steps, gcMode) = toSemi
+    val that0 = that.toSemi
+    SemiPlan(steps ++ that0.steps, gcMode)
+  }
+
+  final def collectChildren[T: Tag]: Seq[ExecutableOp] = {
+    val parent = SafeType.get[T]
+    steps.filter {
+      op =>
+        val maybeChild = op.instanceType
+        maybeChild <:< parent
+    }
+  }
+
+  final def foldLeft[T](z: T, f: (T, ExecutableOp) => T): T = {
+    steps.foldLeft(z)(f)
+  }
+
+  def toSemi: SemiPlan = {
+    val safeSteps: Seq[SemiplanOp] = steps.flatMap{
+      case s: SemiplanOp =>
+        Seq(s)
+      case s: ProxyOp =>
+        s match {
+          case m: MakeProxy =>
+            Seq(m.op)
+          case _: InitProxy =>
+            Seq.empty
+        }
+    }
+    SemiPlan(safeSteps.toVector, gcMode)
+  }
+}
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/DependencyKind.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/DependencyKind.scala
deleted file mode 100644
index 513f74f42..000000000
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/DependencyKind.scala
+++ /dev/null
@@ -1,11 +0,0 @@
-package izumi.distage.model.plan
-
-sealed trait DependencyKind
-
-object DependencyKind {
-
-  final case object Depends extends DependencyKind
-
-  final case object Required extends DependencyKind
-
-}
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/ExecutableOp.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/ExecutableOp.scala
index 1a27a6521..78680f084 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/ExecutableOp.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/ExecutableOp.scala
@@ -1,64 +1,79 @@
 package izumi.distage.model.plan
 
-import izumi.distage.model.definition.Binding
 import izumi.distage.model.plan.ExecutableOp.ProxyOp.MakeProxy
+import izumi.distage.model.plan.operations.OperationOrigin
+import izumi.distage.model.plan.repr.{KeyFormatter, OpFormatter, TypeFormatter}
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring._
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 
 import scala.annotation.tailrec
 
+
+
+
 sealed trait ExecutableOp {
   def target: DIKey
-  def origin: Option[Binding]
+  def origin: OperationOrigin
 
   override final def toString: String = new OpFormatter.Impl(KeyFormatter.Full, TypeFormatter.Full).format(this)
 }
 
 object ExecutableOp {
-  final case class ImportDependency(target: DIKey, references: Set[DIKey], origin: Option[Binding]) extends ExecutableOp
 
-  sealed trait InstantiationOp extends ExecutableOp
-  final case class CreateSet(target: DIKey, element: SafeType, members: Set[DIKey], origin: Option[Binding]) extends InstantiationOp
+  sealed trait SemiplanOp extends ExecutableOp
+  final case class ImportDependency(target: DIKey, references: Set[DIKey], origin: OperationOrigin.Synthetic) extends SemiplanOp
+
+  sealed trait InstantiationOp extends SemiplanOp
+
+  final case class CreateSet(target: DIKey, element: SafeType, members: Set[DIKey], origin: OperationOrigin) extends InstantiationOp
 
   sealed trait WiringOp extends InstantiationOp {
     def target: DIKey
     def wiring: PureWiring
-    def origin: Option[Binding]
+    def origin: OperationOrigin
   }
+
   object WiringOp {
-    final case class InstantiateClass(target: DIKey, wiring: SingletonWiring.Constructor, origin: Option[Binding]) extends WiringOp
-    final case class InstantiateTrait(target: DIKey, wiring: SingletonWiring.AbstractSymbol, origin: Option[Binding]) extends WiringOp
-    final case class InstantiateFactory(target: DIKey, wiring: Factory, origin: Option[Binding]) extends WiringOp
-    final case class CallProvider(target: DIKey, wiring: SingletonWiring.Function, origin: Option[Binding]) extends WiringOp
-    final case class CallFactoryProvider(target: DIKey, wiring: FactoryFunction, origin: Option[Binding]) extends WiringOp
-    final case class ReferenceInstance(target: DIKey, wiring: SingletonWiring.Instance, origin: Option[Binding]) extends WiringOp
-    final case class ReferenceKey(target: DIKey, wiring: SingletonWiring.Reference, origin: Option[Binding]) extends WiringOp
+    final case class InstantiateClass(target: DIKey, wiring: SingletonWiring.Constructor, origin: OperationOrigin) extends WiringOp
+    final case class InstantiateTrait(target: DIKey, wiring: SingletonWiring.AbstractSymbol, origin: OperationOrigin) extends WiringOp
+    final case class InstantiateFactory(target: DIKey, wiring: Factory, origin: OperationOrigin) extends WiringOp
+    final case class CallProvider(target: DIKey, wiring: SingletonWiring.Function, origin: OperationOrigin) extends WiringOp
+    final case class CallFactoryProvider(target: DIKey, wiring: FactoryFunction, origin: OperationOrigin) extends WiringOp
+    final case class ReferenceInstance(target: DIKey, wiring: SingletonWiring.Instance, origin: OperationOrigin) extends WiringOp
+    final case class ReferenceKey(target: DIKey, wiring: SingletonWiring.Reference, origin: OperationOrigin) extends WiringOp
   }
 
   sealed trait MonadicOp extends InstantiationOp {
     def effectWiring: PureWiring
     def wiring: Wiring.MonadicWiring
   }
+
   object MonadicOp {
-    final case class ExecuteEffect(target: DIKey, effectOp: WiringOp, wiring: Wiring.MonadicWiring.Effect, origin: Option[Binding]) extends MonadicOp {
+    final case class ExecuteEffect(target: DIKey, effectOp: WiringOp, wiring: Wiring.MonadicWiring.Effect, origin: OperationOrigin) extends MonadicOp {
       override def effectWiring: RuntimeDIUniverse.Wiring.PureWiring = wiring.effectWiring
     }
-    final case class AllocateResource(target: DIKey, effectOp: WiringOp, wiring: Wiring.MonadicWiring.Resource, origin: Option[Binding]) extends MonadicOp {
+    final case class AllocateResource(target: DIKey, effectOp: WiringOp, wiring: Wiring.MonadicWiring.Resource, origin: OperationOrigin) extends MonadicOp {
       override def effectWiring: RuntimeDIUniverse.Wiring.PureWiring = wiring.effectWiring
     }
   }
 
   sealed trait ProxyOp extends ExecutableOp
+
   object ProxyOp {
-    final case class MakeProxy(op: InstantiationOp, forwardRefs: Set[DIKey], origin: Option[Binding], byNameAllowed: Boolean) extends ProxyOp {
+    final case class MakeProxy(op: InstantiationOp, forwardRefs: Set[DIKey], origin: OperationOrigin, byNameAllowed: Boolean) extends ProxyOp {
       override def target: DIKey = op.target
     }
-    final case class InitProxy(target: DIKey, dependencies: Set[DIKey], proxy: MakeProxy, origin: Option[Binding]) extends ProxyOp
+    final case class InitProxy(target: DIKey, dependencies: Set[DIKey], proxy: MakeProxy, origin: OperationOrigin) extends ProxyOp
+  }
+
+  implicit class ExecutableOpExt(val op: ExecutableOp) extends AnyVal {
+    def instanceType: SafeType = opInstanceType(op)
+
   }
 
   @tailrec
-  def instanceType(op: ExecutableOp): SafeType = {
+  private[this] def opInstanceType(op: ExecutableOp): SafeType = {
     op match {
       case w: WiringOp =>
         w.wiring match {
@@ -68,11 +83,46 @@ object ExecutableOp {
             w.target.tpe
         }
       case p: MakeProxy =>
-        instanceType(p.op)
+        opInstanceType(p.op)
       case o =>
         o.target.tpe
     }
   }
+
+//  @tailrec
+//  def underlyingInstanceType(op: ExecutableOp): SafeType = {
+//    op match {
+//      case op: ImportDependency =>
+//        op.target.tpe
+//      case op: InstantiationOp =>
+//        op match {
+//          case op: CreateSet =>
+//            op.target.tpe
+//          case op: WiringOp =>
+//            op.wiring match {
+//              case u: Wiring.SingletonWiring =>
+//                u.instanceType
+//              case _: Wiring.Factory | _: Wiring.FactoryFunction =>
+//                op.target.tpe
+//            }
+//          case op: MonadicOp =>
+//            op match {
+//              case eff: MonadicOp.ExecuteEffect =>
+//                underlyingInstanceType(eff.effectOp)
+//              case res: MonadicOp.AllocateResource =>
+//                underlyingInstanceType(res.effectOp)
+//            }
+//        }
+//      case op: ProxyOp =>
+//        op match {
+//          case p: MakeProxy =>
+//            underlyingInstanceType(p.op)
+//          case p: ProxyOp.InitProxy =>
+//            underlyingInstanceType(p.proxy)
+//        }
+//    }
+//  }
+
 }
 
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/AbstractPlanOps.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/AbstractPlanOps.scala
new file mode 100644
index 000000000..bf29299b9
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/AbstractPlanOps.scala
@@ -0,0 +1,29 @@
+package izumi.distage.model.plan.impl
+
+import izumi.distage.model.plan.ExecutableOp
+import izumi.distage.model.plan.ExecutableOp.WiringOp.ReferenceInstance
+import izumi.distage.model.plan.ExecutableOp.{ImportDependency, SemiplanOp}
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring.SingletonWiring.Instance
+
+trait AbstractPlanOps {
+  private[plan] def resolveImports[OpType <: ExecutableOp](f: PartialFunction[ImportDependency, Seq[OpType]], steps: Vector[OpType]): Vector[ExecutableOp] =
+    steps.flatMap {
+      case i: ImportDependency =>
+        f.lift(i) getOrElse Seq(i)
+      case op =>
+        Seq(op)
+    }
+
+  private[plan] def resolveImports1(f: PartialFunction[ImportDependency, Seq[SemiplanOp]], steps: Vector[SemiplanOp]): Vector[SemiplanOp] =
+    steps.flatMap {
+      case i: ImportDependency =>
+        f.lift(i) getOrElse Seq(i)
+      case op =>
+        Seq(op)
+    }
+
+
+  private[plan] def importToInstances(f: PartialFunction[ImportDependency, Any]): PartialFunction[ImportDependency, Seq[ExecutableOp.WiringOp]] =
+    Function.unlift(i => f.lift(i).map(instance => Seq(ReferenceInstance(i.target, Instance(i.target.tpe, instance), i.origin))))
+
+}
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanExtensions.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanExtensions.scala
new file mode 100644
index 000000000..8dfcb0bd9
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanExtensions.scala
@@ -0,0 +1,60 @@
+package izumi.distage.model.plan.impl
+
+import cats.Applicative
+import izumi.distage.model.GCMode
+import izumi.distage.model.plan.ExecutableOp.{ImportDependency, SemiplanOp}
+import izumi.distage.model.plan.{ExecutableOp, OrderedPlan, SemiPlan}
+import izumi.distage.model.plan.impl.OrderedPlanExtensions.{OrderedPlanExts, OrderedPlanSyntax}
+import SemiPlanOrderedPlanInstances.resolveImportsImpl
+import izumi.distage.model.plan.repr.{CompactPlanFormatter, DepTreeRenderer}
+import izumi.distage.model.plan.topology.DepTreeNode.DepNode
+import izumi.distage.model.plan.topology.DependencyGraph
+import izumi.distage.model.plan.topology.DependencyGraph.DependencyKind
+import izumi.distage.model.plan.topology.PlanTopology.PlanTopologyImmutable
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
+import izumi.functional.Renderable
+
+import scala.language.implicitConversions
+
+trait OrderedPlanExtensions {
+  implicit val defaultFormatter: Renderable[OrderedPlan] = CompactPlanFormatter.OrderedPlanFormatter
+
+  def empty: OrderedPlan = OrderedPlan(Vector.empty, GCMode.NoGC, PlanTopologyImmutable(DependencyGraph(Map.empty, DependencyKind.Depends), DependencyGraph(Map.empty, DependencyKind.Required)))
+
+  @inline implicit final def toPlanSyntax(plan: OrderedPlan): OrderedPlanSyntax = new OrderedPlanSyntax(plan)
+  @inline implicit final def toOrderedPlanExts(plan: OrderedPlan): OrderedPlanExts = new OrderedPlanExts(plan)
+}
+
+private[plan] object OrderedPlanExtensions {
+  final class OrderedPlanSyntax(private val plan: OrderedPlan) extends AnyVal {
+    def render()(implicit ev: Renderable[OrderedPlan]): String = ev.render(plan)
+
+    def renderDeps(node: DepNode): String = new DepTreeRenderer(node, plan).render()
+
+    def renderAllDeps(): String = {
+      val effectiveRoots = plan.keys.filter(k => plan.topology.dependees.direct(k).isEmpty)
+      effectiveRoots.map(root => plan.topology.dependencies.tree(root)).map(renderDeps).mkString("\n")
+    }
+  }
+
+  final class OrderedPlanExts(private val plan: OrderedPlan) extends AnyVal {
+
+    import cats.instances.vector._
+    import cats.syntax.functor._
+    import cats.syntax.traverse._
+
+    def traverse[F[_] : Applicative](f: ExecutableOp => F[SemiplanOp]): F[SemiPlan] =
+      plan.steps.traverse(f).map(SemiPlan(_, plan.gcMode))
+
+    def flatMapF[F[_] : Applicative](f: ExecutableOp => F[Seq[SemiplanOp]]): F[SemiPlan] =
+      plan.steps.traverse(f).map(s => SemiPlan(s.flatten, plan.gcMode))
+
+    def resolveImportF[T]: ResolveImportFOrderedPlanPartiallyApplied[T] = new ResolveImportFOrderedPlanPartiallyApplied(plan)
+
+    def resolveImportF[F[_] : Applicative, T: Tag](f: F[T]): F[OrderedPlan] = resolveImportF[T](f)
+
+    def resolveImportsF[F[_] : Applicative](f: PartialFunction[ImportDependency, F[Any]]): F[OrderedPlan] =
+      resolveImportsImpl(f, plan.steps).map(s => plan.copy(steps = s))
+  }
+
+}
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanOps.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanOps.scala
new file mode 100644
index 000000000..1f580df77
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanOps.scala
@@ -0,0 +1,57 @@
+package izumi.distage.model.plan.impl
+
+import izumi.distage.model.Locator
+import izumi.distage.model.plan.ExecutableOp.ImportDependency
+import izumi.distage.model.plan.{AbstractPlan, OrderedPlan}
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
+
+trait OrderedPlanOps {
+  this: OrderedPlan =>
+  /**
+    * Be careful, don't use this method blindly, it can disrupt graph connectivity when used improperly.
+    *
+    * Proper usage assume that `keys` contains complete subgraph reachable from graph roots.
+    */
+  def replaceWithImports(keys: Set[DIKey]): OrderedPlan = {
+    val roots = gcMode.toSet
+    val newSteps = steps.flatMap {
+      case s if keys.contains(s.target) =>
+        val dependees = topology.dependees.direct(s.target)
+        if (dependees.diff(keys).nonEmpty || roots.contains(s.target)) {
+          val dependees = topology.dependees.transitive(s.target).diff(keys)
+          Seq(ImportDependency(s.target, dependees, s.origin.toSynthetic))
+        } else {
+          Seq.empty
+        }
+      case s =>
+        Seq(s)
+    }
+
+    OrderedPlan(
+      newSteps,
+      gcMode,
+      topology.removeKeys(keys),
+    )
+  }
+
+  override def resolveImports(f: PartialFunction[ImportDependency, Any]): OrderedPlan = {
+    copy(steps = AbstractPlan.resolveImports(AbstractPlan.importToInstances(f), steps))
+  }
+
+  override def resolveImport[T: Tag](instance: T): OrderedPlan =
+    resolveImports {
+      case i if i.target == DIKey.get[T] =>
+        instance
+    }
+
+  override def resolveImport[T: Tag](id: String)(instance: T): OrderedPlan = {
+    resolveImports {
+      case i if i.target == DIKey.get[T].named(id) =>
+        instance
+    }
+  }
+
+  override def locateImports(locator: Locator): OrderedPlan = {
+    resolveImports(Function.unlift(i => locator.lookupLocal[Any](i.target)))
+  }
+}
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/PlanLazyOps.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/PlanLazyOps.scala
new file mode 100644
index 000000000..b8d5fc93b
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/PlanLazyOps.scala
@@ -0,0 +1,31 @@
+package izumi.distage.model.plan.impl
+
+import izumi.distage.model.definition.ModuleBase
+import izumi.distage.model.plan.operations.OperationOrigin
+import izumi.distage.model.plan.{AbstractPlan, ExecutableOp}
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
+
+trait PlanLazyOps[OpType <: ExecutableOp] {
+  this: AbstractPlan[OpType] =>
+  def index: Map[DIKey, OpType] = lazyIndex
+  def definition: ModuleBase = lazyDefn
+
+  private[this] final lazy val lazyDefn = {
+    val userBindings = steps.flatMap {
+      op =>
+        op.origin match {
+          case OperationOrigin.UserBinding(binding) =>
+            Seq(binding)
+          case _ =>
+            Seq.empty
+        }
+
+    }.toSet
+    ModuleBase.make(userBindings)
+  }
+
+  private[this] final lazy val lazyIndex : Map[DIKey, OpType] = {
+    steps.map(s => s.target -> s).toMap
+  }
+
+}
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/PlanSyntaxCatsBoilerplate.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/PlanSyntaxCatsBoilerplate.scala
new file mode 100644
index 000000000..d244ae090
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/PlanSyntaxCatsBoilerplate.scala
@@ -0,0 +1,65 @@
+package izumi.distage.model.plan.impl
+
+import cats.Applicative
+import cats.kernel.Monoid
+import izumi.distage.model.plan.ExecutableOp.WiringOp.ReferenceInstance
+import izumi.distage.model.plan.ExecutableOp.{ImportDependency, SemiplanOp}
+import izumi.distage.model.plan.{ExecutableOp, OrderedPlan, SemiPlan}
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring.SingletonWiring.Instance
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
+
+
+private[plan] final class ResolveImportFSemiPlanPartiallyApplied[T](private val plan: SemiPlan) extends AnyVal {
+  def apply[F[_] : Applicative](f: F[T])(implicit ev: Tag[T]): F[SemiPlan] =
+    plan.resolveImportsF[F] {
+      case i if i.target == DIKey.get[T] => f.asInstanceOf[F[Any]]
+    }
+}
+
+private[plan] final class ResolveImportFOrderedPlanPartiallyApplied[T](private val plan: OrderedPlan) extends AnyVal {
+  def apply[F[_] : Applicative](f: F[T])(implicit ev: Tag[T]): F[OrderedPlan] =
+    plan.resolveImportsF[F] {
+      case i if i.target == DIKey.get[T] => f.asInstanceOf[F[Any]]
+    }
+}
+
+private[plan] object SemiPlanOrderedPlanInstances {
+
+  /**
+    * This instance uses 'no more orphans' trick to provide an Optional instance
+    * only IFF you have cats-effect as a dependency without REQUIRING a cats-effect dependency.
+    *
+    * Optional instance via https://blog.7mind.io/no-more-orphans.html
+    */
+  sealed abstract class CatsMonoid[K[_]]
+
+  object CatsMonoid {
+    @inline implicit final def get: CatsMonoid[Monoid] = null
+  }
+
+  import cats.instances.vector._
+  import cats.syntax.functor._
+  import cats.syntax.traverse._
+
+  @inline
+  final def resolveImportsImpl[F[_] : Applicative](f: PartialFunction[ImportDependency, F[Any]], steps: Vector[ExecutableOp]): F[Vector[ExecutableOp]] =
+    steps.traverse {
+      case i: ImportDependency =>
+        f.lift(i).map {
+          _.map[ExecutableOp](instance => ReferenceInstance(i.target, Instance(i.target.tpe, instance), i.origin))
+        } getOrElse Applicative[F].pure(i)
+      case op =>
+        Applicative[F].pure(op)
+    }
+
+  @inline
+  final def resolveImportsImpl1[F[_] : Applicative](f: PartialFunction[ImportDependency, F[Any]], steps: Vector[SemiplanOp]): F[Vector[SemiplanOp]] =
+    steps.traverse {
+      case i: ImportDependency =>
+        f.lift(i).map {
+          _.map[SemiplanOp](instance => ReferenceInstance(i.target, Instance(i.target.tpe, instance), i.origin))
+        } getOrElse Applicative[F].pure(i)
+      case op =>
+        Applicative[F].pure(op)
+    }
+}
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/SemiPlanExtensions.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/SemiPlanExtensions.scala
new file mode 100644
index 000000000..bfddf924d
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/SemiPlanExtensions.scala
@@ -0,0 +1,51 @@
+package izumi.distage.model.plan.impl
+
+import cats.Applicative
+import cats.kernel.Monoid
+import izumi.distage.model.GCMode
+import izumi.distage.model.plan.ExecutableOp.{ImportDependency, SemiplanOp}
+import izumi.distage.model.plan.SemiPlan
+import izumi.distage.model.plan.impl.SemiPlanExtensions.SemiPlanExts
+import izumi.distage.model.plan.impl.SemiPlanOrderedPlanInstances.{CatsMonoid, resolveImportsImpl1}
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Tag
+
+import scala.language.implicitConversions
+
+trait SemiPlanExtensions {
+  /**
+    * This instance uses 'no more orphans' trick to provide an Optional instance
+    * only IFF you have cats-effect as a dependency without REQUIRING a cats-effect dependency.
+    *
+    * Optional instance via https://blog.7mind.io/no-more-orphans.html
+    */
+  implicit def optionalCatsMonoidForSemiplan[K[_] : CatsMonoid]: K[SemiPlan] =
+    new Monoid[SemiPlan] {
+      override def empty: SemiPlan = SemiPlan(Vector.empty, GCMode.NoGC)
+
+      override def combine(x: SemiPlan, y: SemiPlan): SemiPlan = x ++ y
+    }.asInstanceOf[K[SemiPlan]]
+
+  @inline implicit final def toSemiPlanExts(plan: SemiPlan): SemiPlanExts = new SemiPlanExts(plan)
+
+}
+
+private[plan] object SemiPlanExtensions {
+  import cats.instances.vector._
+  import cats.syntax.functor._
+  import cats.syntax.traverse._
+
+  final class SemiPlanExts(private val plan: SemiPlan) extends AnyVal {
+    def traverse[F[_] : Applicative](f: SemiplanOp => F[SemiplanOp]): F[SemiPlan] =
+      plan.steps.traverse(f).map(s => plan.copy(steps = s))
+
+    def flatMapF[F[_] : Applicative](f: SemiplanOp => F[Seq[SemiplanOp]]): F[SemiPlan] =
+      plan.steps.traverse(f).map(s => plan.copy(steps = s.flatten))
+
+    def resolveImportF[T]: ResolveImportFSemiPlanPartiallyApplied[T] = new ResolveImportFSemiPlanPartiallyApplied(plan)
+
+    def resolveImportF[F[_] : Applicative, T: Tag](f: F[T]): F[SemiPlan] = resolveImportF[T](f)
+
+    def resolveImportsF[F[_] : Applicative](f: PartialFunction[ImportDependency, F[Any]]): F[SemiPlan] =
+      resolveImportsImpl1(f, plan.steps).map(s => plan.copy(steps = s))
+  }
+}
\ No newline at end of file
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/SemiPlanOps.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/SemiPlanOps.scala
new file mode 100644
index 000000000..97c7cef16
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/impl/SemiPlanOps.scala
@@ -0,0 +1,54 @@
+package izumi.distage.model.plan.impl
+
+import izumi.distage.model.Locator
+import izumi.distage.model.plan.ExecutableOp.WiringOp.CallProvider
+import izumi.distage.model.plan.ExecutableOp.{ImportDependency, SemiplanOp}
+import izumi.distage.model.plan.{AbstractPlan, SemiPlan}
+import izumi.distage.model.providers.ProviderMagnet
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring.SingletonWiring
+
+trait SemiPlanOps {
+  this: SemiPlan =>
+
+  override def toSemi: SemiPlan = this
+
+  override def resolveImport[T: Tag](instance: T): SemiPlan =
+    resolveImports {
+      case i if i.target == DIKey.get[T] =>
+        instance
+    }
+
+  override def resolveImport[T: Tag](id: String)(instance: T): SemiPlan = {
+    resolveImports {
+      case i if i.target == DIKey.get[T].named(id) =>
+        instance
+    }
+  }
+
+  override def resolveImports(f: PartialFunction[ImportDependency, Any]): SemiPlan = {
+    copy(steps = AbstractPlan.resolveImports1(AbstractPlan.importToInstances(f), steps))
+  }
+
+  override def locateImports(locator: Locator): SemiPlan = {
+    resolveImports(Function.unlift(i => locator.lookupLocal[Any](i.target)))
+  }
+
+  final def resolveImportsOp(f: PartialFunction[ImportDependency, Seq[SemiplanOp]]): SemiPlan = {
+    SemiPlan(steps = AbstractPlan.resolveImports1(f, steps.toVector), gcMode)
+  }
+
+  final def providerImport[T](function: ProviderMagnet[T]): SemiPlan = {
+    resolveImportsOp {
+      case i if i.target.tpe == function.get.ret =>
+        Seq(CallProvider(i.target, SingletonWiring.Function(function.get, function.get.associations), i.origin))
+    }
+  }
+
+  final def providerImport[T](id: String)(function: ProviderMagnet[T]): SemiPlan = {
+    resolveImportsOp {
+      case i if i.target == DIKey.IdKey(function.get.ret, id) =>
+        Seq(CallProvider(i.target, SingletonWiring.Function(function.get, function.get.associations), i.origin))
+    }
+  }
+}
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/NextOps.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/initial/NextOps.scala
similarity index 70%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/NextOps.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/initial/NextOps.scala
index e11e0e698..792416717 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/NextOps.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/initial/NextOps.scala
@@ -1,9 +1,9 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.initial
 
 import izumi.distage.model.plan.ExecutableOp.{CreateSet, InstantiationOp}
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 
 final case class NextOps(
-                          sets: Map[RuntimeDIUniverse.DIKey, CreateSet]
-                          , provisions: Seq[InstantiationOp]
+                          sets: Map[RuntimeDIUniverse.DIKey, CreateSet],
+                          provisions: Seq[InstantiationOp],
                         )
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/DodgyPlan.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/initial/PrePlan.scala
similarity index 81%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/DodgyPlan.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/initial/PrePlan.scala
index 31775089c..f69417439 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/DodgyPlan.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/initial/PrePlan.scala
@@ -1,18 +1,18 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.initial
 
 import izumi.distage.model.GCMode
 import izumi.distage.model.definition.{Binding, ModuleBase}
-import izumi.distage.model.plan.DodgyPlan._
 import izumi.distage.model.plan.ExecutableOp.{CreateSet, InstantiationOp}
+import izumi.distage.model.plan.initial.PrePlan.{JustOp, SetOp, TraceableOp}
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.DIKey
 import izumi.fundamentals.collections.ImmutableMultiMap
-import izumi.fundamentals.collections.IzCollections._
 import izumi.fundamentals.platform.language.Quirks
 import izumi.fundamentals.platform.language.Quirks._
+import izumi.fundamentals.collections.IzCollections._
 
 import scala.collection.mutable
 
-final class DodgyPlan(
+final class PrePlan(
                        val definition: ModuleBase,
                        val gcMode: GCMode,
                      ) {
@@ -27,7 +27,7 @@ final class DodgyPlan(
     ops.map(_.key).toSet.size
   }
 
-  def append(b: Binding, op: NextOps): DodgyPlan = {
+  def append(b: Binding, op: NextOps): PrePlan = {
     Quirks.discard(b)
 
     op.provisions.foreach {
@@ -42,7 +42,7 @@ final class DodgyPlan(
   }
 }
 
-object DodgyPlan {
+object PrePlan {
   sealed trait TraceableOp {
     def key: DIKey
     def op: InstantiationOp
@@ -51,8 +51,8 @@ object DodgyPlan {
   final case class JustOp(key: DIKey, op: InstantiationOp, binding: Binding) extends TraceableOp
   final case class SetOp(key: DIKey, op: CreateSet, binding: Binding) extends TraceableOp
 
-  def empty(definition: ModuleBase, gcMode: GCMode): DodgyPlan = {
-    new DodgyPlan(definition, gcMode)
+  def empty(definition: ModuleBase, gcMode: GCMode): PrePlan = {
+    new PrePlan(definition, gcMode)
   }
 }
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala
new file mode 100644
index 000000000..1d5a0a1d5
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala
@@ -0,0 +1,27 @@
+package izumi.distage.model.plan.operations
+
+import izumi.distage.model.definition.Binding
+
+sealed trait OperationOrigin {
+  def toSynthetic: OperationOrigin.Synthetic
+}
+
+object OperationOrigin {
+  sealed trait Defined extends OperationOrigin {
+    def binding: Binding
+  }
+
+  case class UserBinding(binding: Binding) extends Defined {
+    override def toSynthetic: Synthetic = SyntheticBinding(binding)
+  }
+
+  sealed trait Synthetic extends OperationOrigin
+
+  case class SyntheticBinding(binding: Binding) extends Synthetic with Defined {
+    override def toSynthetic: Synthetic = this
+  }
+
+  case object Unknown extends Synthetic {
+    override def toSynthetic: Synthetic = this
+  }
+}
\ No newline at end of file
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIAssociation.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/WithDIAssociation.scala
similarity index 85%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIAssociation.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/WithDIAssociation.scala
index dd20fd91b..dd9dd2ea9 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIAssociation.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/WithDIAssociation.scala
@@ -1,7 +1,7 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.operations
 
 import izumi.distage.model.references.WithDIKey
-import izumi.distage.model.reflection.universe._
+import izumi.distage.model.reflection.universe.{DIUniverseBase, WithDICallable, WithDISafeType, WithDISymbolInfo}
 
 trait WithDIAssociation {
   this:  DIUniverseBase
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIDependencyContext.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/WithDIDependencyContext.scala
similarity index 85%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIDependencyContext.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/WithDIDependencyContext.scala
index ca824db96..09003cdd5 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIDependencyContext.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/WithDIDependencyContext.scala
@@ -1,6 +1,6 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.operations
 
-import izumi.distage.model.reflection.universe._
+import izumi.distage.model.reflection.universe.{DIUniverseBase, WithDICallable, WithDISafeType, WithDISymbolInfo}
 
 trait WithDIDependencyContext {
   this: DIUniverseBase
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIWiring.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/WithDIWiring.scala
similarity index 97%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIWiring.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/WithDIWiring.scala
index de0b88f14..27097d80f 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIWiring.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/operations/WithDIWiring.scala
@@ -1,7 +1,7 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.operations
 
 import izumi.distage.model.references.WithDIKey
-import izumi.distage.model.reflection.universe._
+import izumi.distage.model.reflection.universe.{DIUniverseBase, WithDICallable, WithDISafeType, WithDISymbolInfo}
 import izumi.fundamentals.platform.language.Quirks._
 
 trait WithDIWiring {
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/CompactPlanFormatter.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/CompactPlanFormatter.scala
similarity index 88%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/CompactPlanFormatter.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/CompactPlanFormatter.scala
index 7eebd0992..eb90c31fb 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/CompactPlanFormatter.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/CompactPlanFormatter.scala
@@ -1,5 +1,6 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.repr
 
+import izumi.distage.model.plan.OrderedPlan
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.{DIKey, SafeType}
 import izumi.functional.Renderable
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/DepTreeNode.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/DepTreeRenderer.scala
similarity index 63%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/DepTreeNode.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/DepTreeRenderer.scala
index 1272bc900..b97b754fc 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/DepTreeNode.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/DepTreeRenderer.scala
@@ -1,41 +1,20 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.repr
 
+import izumi.distage.model.plan.topology.DepTreeNode
+import izumi.distage.model.plan.topology.DepTreeNode._
+import izumi.distage.model.plan.topology.DependencyGraph.DependencyKind
+import izumi.distage.model.plan.OrderedPlan
+import izumi.distage.model.plan.operations.OperationOrigin
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 
 import scala.collection.mutable
 
-sealed trait DepTreeNode {
-  def level: Int
-
-}
-
-final case class Truncated(level: Int) extends DepTreeNode
-
-final case class CircularReference(key: DIKey, level: Int) extends DepTreeNode
-
-final case class DepNode(key: DIKey, graph: DependencyGraph, level: Int, limit: Option[Int], exclusions: Set[DIKey]) extends DepTreeNode {
-  def children: Set[DepTreeNode] = {
-    val children = graph.direct(key)
-
-    if (limit.exists(_ <= level)) {
-      Set(Truncated(level))
-    } else {
-      children.diff(exclusions).map {
-        child =>
-          DepNode(child, graph, level + 1, limit, exclusions + key)
-      } ++ children.intersect(exclusions).map {
-        child =>
-          CircularReference(child, level + 1)
-      }
-    }
-  }
-}
-
 
 class DepTreeRenderer(node: DepNode, plan: OrderedPlan) {
   val minimizer = new KeyMinimizer(collectKeys(node))
 
   def render(): String = render(node)
+
   /**
     * May be unsafe to call on big graphs
     */
@@ -73,9 +52,20 @@ class DepTreeRenderer(node: DepNode, plan: OrderedPlan) {
     }
   }
 
+  private def renderOrigin(origin: OperationOrigin): String = {
+    origin match {
+      case OperationOrigin.UserBinding(binding) =>
+        binding.origin.toString
+      case OperationOrigin.SyntheticBinding(binding) =>
+        binding.origin.toString
+      case OperationOrigin.Unknown =>
+        ""
+    }
+  }
+
 
   private def renderKey(key: DIKey): String = {
-    s"${minimizer.render(key)} ${plan.index.get(key).flatMap(_.origin).map(_.origin).getOrElse("")}"
+    s"${minimizer.render(key)} ${plan.index.get(key).map(op => renderOrigin(op.origin)).getOrElse("")}"
   }
 
   private def collectKeys(node: DepTreeNode): Set[DIKey] = {
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/KeyFormatter.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/KeyFormatter.scala
similarity index 92%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/KeyFormatter.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/KeyFormatter.scala
index 0eac84822..5d4aa41c3 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/KeyFormatter.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/KeyFormatter.scala
@@ -1,4 +1,4 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.repr
 
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/KeyMinimizer.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/KeyMinimizer.scala
similarity index 98%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/KeyMinimizer.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/KeyMinimizer.scala
index 825785302..8f003dba3 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/KeyMinimizer.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/KeyMinimizer.scala
@@ -1,4 +1,4 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.repr
 
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 import izumi.fundamentals.collections.IzCollections._
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/OpFormatter.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/OpFormatter.scala
similarity index 90%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/OpFormatter.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/OpFormatter.scala
index 1728ef5ac..038a7c024 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/OpFormatter.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/repr/OpFormatter.scala
@@ -1,10 +1,11 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.repr
 
-import izumi.distage.model.definition.Binding
-import izumi.distage.model.plan.ExecutableOp.ProxyOp._
 import izumi.distage.model.plan.ExecutableOp.MonadicOp._
+import izumi.distage.model.plan.ExecutableOp.ProxyOp._
 import izumi.distage.model.plan.ExecutableOp.WiringOp._
 import izumi.distage.model.plan.ExecutableOp.{CreateSet, ImportDependency, InstantiationOp, WiringOp, _}
+import izumi.distage.model.plan.ExecutableOp
+import izumi.distage.model.plan.operations.OperationOrigin
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring.MonadicWiring._
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring.SingletonWiring._
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring._
@@ -16,8 +17,15 @@ trait OpFormatter {
 }
 
 object OpFormatter {
-  def formatBindingPosition(origin: Option[Binding]): String = {
-    origin.fold("(<unknown>)")(_.origin.toString)
+  def formatBindingPosition(origin: OperationOrigin): String = {
+    origin match {
+      case OperationOrigin.UserBinding(binding) =>
+        binding.origin.toString
+      case OperationOrigin.SyntheticBinding(binding) =>
+        binding.origin.toString
+      case OperationOrigin.Unknown =>
+        "(<unknown>)"
+    }
   }
 
   class Impl
@@ -26,8 +34,8 @@ object OpFormatter {
   , typeFormatter: TypeFormatter
   ) extends OpFormatter {
 
-    import typeFormatter.formatType
     import keyFormatter.formatKey
+    import typeFormatter.formatType
 
     override def format(op: ExecutableOp): String = {
       op match {
@@ -75,7 +83,12 @@ object OpFormatter {
 
         case ImportDependency(target, references, origin) =>
           val pos = formatBindingPosition(origin)
-          s"${formatKey(target)} $pos := import $target // required for ${references.map(formatKey).mkString(" and ")}"
+          val hint = if (references.nonEmpty) {
+            s"// required for ${references.map(formatKey).mkString(" and ")}"
+          } else {
+            " // no dependees"
+          }
+          s"${formatKey(target)} $pos := import $target $hint".trim
 
         case p: ProxyOp =>
           p match {
@@ -100,7 +113,7 @@ object OpFormatter {
       }
     }
 
-    private def formatOp(target: DIKey, deps: Wiring, origin: Option[Binding]): String = {
+    private def formatOp(target: DIKey, deps: Wiring, origin: OperationOrigin): String = {
       val op = formatWiring(deps)
       val pos = formatBindingPosition(origin)
       s"${formatKey(target)} $pos := $op"
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/topology/DepTreeNode.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/topology/DepTreeNode.scala
new file mode 100644
index 000000000..37796ad45
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/topology/DepTreeNode.scala
@@ -0,0 +1,32 @@
+package izumi.distage.model.plan.topology
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
+
+sealed trait DepTreeNode {
+  def level: Int
+}
+
+object DepTreeNode {
+
+  final case class Truncated(level: Int) extends DepTreeNode
+
+  final case class CircularReference(key: DIKey, level: Int) extends DepTreeNode
+
+  final case class DepNode(key: DIKey, graph: DependencyGraph, level: Int, limit: Option[Int], exclusions: Set[DIKey]) extends DepTreeNode {
+    def children: Set[DepTreeNode] = {
+      val children = graph.direct(key)
+
+      if (limit.exists(_ <= level)) {
+        Set(Truncated(level))
+      } else {
+        children.diff(exclusions).map {
+          child =>
+            DepNode(child, graph, level + 1, limit, exclusions + key)
+        } ++ children.intersect(exclusions).map {
+          child =>
+            CircularReference(child, level + 1)
+        }
+      }
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/PlanTopology.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/topology/DependencyGraph.scala
similarity index 51%
rename from distage/distage-model/src/main/scala/izumi/distage/model/plan/PlanTopology.scala
rename to distage/distage-model/src/main/scala/izumi/distage/model/plan/topology/DependencyGraph.scala
index 8f5fe7530..4ff471fdf 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/PlanTopology.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/topology/DependencyGraph.scala
@@ -1,10 +1,35 @@
-package izumi.distage.model.plan
+package izumi.distage.model.plan.topology
 
+import izumi.distage.model.plan.topology.DepTreeNode.DepNode
+import izumi.distage.model.plan.topology.DependencyGraph.DependencyKind
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 
 import scala.collection.mutable
 
+
 case class DependencyGraph(graph: Map[DIKey, Set[DIKey]], kind: DependencyKind) {
+  def dropDepsOf(keys: Set[DIKey]): DependencyGraph = {
+    kind match {
+      case DependencyKind.Depends =>
+        val filtered = graph.view
+          .flatMap {
+            case (k, _) if keys.contains(k) =>
+              Seq(k -> Set.empty[DIKey])
+            case o => Seq(o)
+          }
+          .toMap
+        DependencyGraph(filtered, kind)
+      case DependencyKind.Required =>
+        val filtered = graph.view
+          .flatMap {
+            case (k, v)  =>
+              Seq(k -> v.diff(keys))
+          }
+          .toMap
+        DependencyGraph(filtered, kind)
+    }
+  }
+
   /**
     * This method is relatively expensive
     *
@@ -37,30 +62,14 @@ case class DependencyGraph(graph: Map[DIKey, Set[DIKey]], kind: DependencyKind)
   }
 }
 
-/**
-  * This class represents direct node dependencies and allows to retrive full transitive dependencies for a node
-  */
-sealed trait PlanTopology {
-  def dependees: DependencyGraph
+object DependencyGraph {
+  sealed trait DependencyKind
 
-  def dependencies: DependencyGraph
+  object DependencyKind {
 
-  /**
-    * This method is relatively expensive
-    *
-    * @return full set of all the keys transitively depending on key
-    */
-  def transitiveDependees(key: DIKey): Set[DIKey] = dependees.transitive(key)
+    final case object Depends extends DependencyKind
 
-  /**
-    * This method is relatively expensive
-    *
-    * @return full set of all the keys transitively depending on key
-    */
-  def transitiveDependencies(key: DIKey): Set[DIKey] = dependencies.transitive(key)
-}
+    final case object Required extends DependencyKind
 
-final case class PlanTopologyImmutable(
-                                        dependees: DependencyGraph
-                                        , dependencies: DependencyGraph
-                                      ) extends PlanTopology
+  }
+}
\ No newline at end of file
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/topology/PlanTopology.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/topology/PlanTopology.scala
new file mode 100644
index 000000000..ec1ac4724
--- /dev/null
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/topology/PlanTopology.scala
@@ -0,0 +1,45 @@
+package izumi.distage.model.plan.topology
+
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
+
+
+/**
+  * This class represents direct node dependencies and allows to retrive full transitive dependencies for a node
+  */
+sealed trait PlanTopology {
+  def removeKeys(keys: Set[DIKey]): PlanTopology
+
+  def dependees: DependencyGraph
+
+  def dependencies: DependencyGraph
+
+  /**
+    * This method is relatively expensive
+    *
+    * @return full set of all the keys transitively depending on key
+    */
+  def transitiveDependees(key: DIKey): Set[DIKey] = dependees.transitive(key)
+
+  /**
+    * This method is relatively expensive
+    *
+    * @return full set of all the keys transitively depending on key
+    */
+  def transitiveDependencies(key: DIKey): Set[DIKey] = dependencies.transitive(key)
+}
+
+object PlanTopology {
+
+  final case class PlanTopologyImmutable(
+                                          dependees: DependencyGraph
+                                          , dependencies: DependencyGraph
+                                        ) extends PlanTopology {
+    override def removeKeys(keys: Set[DIKey]): PlanTopology = {
+      PlanTopologyImmutable(
+        dependees.dropDepsOf(keys),
+        dependencies.dropDepsOf(keys),
+      )
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanAnalyzer.scala b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanAnalyzer.scala
index 273eb7b25..dee95ab5b 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanAnalyzer.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanAnalyzer.scala
@@ -1,6 +1,7 @@
 package izumi.distage.model.planning
 
-import izumi.distage.model.plan.{ExecutableOp, PlanTopology}
+import izumi.distage.model.plan.ExecutableOp
+import izumi.distage.model.plan.topology.PlanTopology
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 
 import scala.collection.mutable
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanMergingPolicy.scala b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanMergingPolicy.scala
index 40bb2264c..f7796668b 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanMergingPolicy.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanMergingPolicy.scala
@@ -1,25 +1,27 @@
 package izumi.distage.model.planning
 
-import izumi.distage.model.plan.DodgyPlan.{JustOp, SetOp, TraceableOp}
-import izumi.distage.model.plan.{DodgyPlan, ExecutableOp, SemiPlan}
+import izumi.distage.model.plan.initial.PrePlan.{JustOp, SetOp, TraceableOp}
+import izumi.distage.model.plan.ExecutableOp.SemiplanOp
+import izumi.distage.model.plan.SemiPlan
+import izumi.distage.model.plan.initial.PrePlan
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 import izumi.fundamentals.platform.language.Quirks
 
 trait PlanMergingPolicy {
-  def freeze(plan: DodgyPlan): SemiPlan
+  def freeze(plan: PrePlan): SemiPlan
 }
 
 object PlanMergingPolicy {
 
   sealed trait DIKeyConflictResolution
   object DIKeyConflictResolution {
-    final case class Successful(op: Set[ExecutableOp]) extends DIKeyConflictResolution
-    final case class Failed(candidates: Set[ExecutableOp], explanation: String) extends DIKeyConflictResolution
+    final case class Successful(op: Set[SemiplanOp]) extends DIKeyConflictResolution
+    final case class Failed(candidates: Set[SemiplanOp], explanation: String) extends DIKeyConflictResolution
   }
 
   trait WithResolve {
     this: PlanMergingPolicy =>
-    final protected def resolve(plan: DodgyPlan, key: DIKey, operations: Set[TraceableOp]): DIKeyConflictResolution = {
+    final protected def resolve(plan: PrePlan, key: DIKey, operations: Set[TraceableOp]): DIKeyConflictResolution = {
       operations match {
         case s if s.size == 1 =>
           DIKeyConflictResolution.Successful(Set(s.head.op))
@@ -39,7 +41,7 @@ object PlanMergingPolicy {
       }
     }
 
-    protected def resolveConflict(plan: DodgyPlan, key: DIKey, operations: Set[JustOp]): DIKeyConflictResolution = {
+    protected def resolveConflict(plan: PrePlan, key: DIKey, operations: Set[JustOp]): DIKeyConflictResolution = {
       Quirks.discard(key, plan)
       DIKeyConflictResolution.Failed(operations.map(_.op), "Default policy cannot handle multiple bindings")
     }
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanningHook.scala b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanningHook.scala
index 8d47552f6..0ee0fa169 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanningHook.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanningHook.scala
@@ -1,7 +1,8 @@
 package izumi.distage.model.planning
 
 import izumi.distage.model.definition.{Binding, ModuleBase}
-import izumi.distage.model.plan.{DodgyPlan, OrderedPlan, SemiPlan}
+import izumi.distage.model.plan.initial.PrePlan
+import izumi.distage.model.plan.{OrderedPlan, SemiPlan}
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Wiring
 import izumi.fundamentals.platform.language.Quirks._
 
@@ -9,7 +10,7 @@ trait PlanningHook {
   def hookDefinition(defn: ModuleBase): ModuleBase = defn
   def hookWiring(binding: Binding.ImplBinding, wiring: Wiring): Wiring = { binding.discard(); wiring }
 
-  def phase00PostCompletion(plan: DodgyPlan): DodgyPlan = plan
+  def phase00PostCompletion(plan: PrePlan): PrePlan = plan
   def phase10PostGC(plan: SemiPlan): SemiPlan = plan
   def phase20Customization(plan: SemiPlan): SemiPlan = plan
   def phase45PreForwardingCleanup(plan: SemiPlan): SemiPlan = plan
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanningObserver.scala b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanningObserver.scala
index 9fe2c997e..7a3be3f24 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanningObserver.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanningObserver.scala
@@ -1,6 +1,7 @@
 package izumi.distage.model.planning
 
-import izumi.distage.model.plan.{DodgyPlan, SemiPlan, OrderedPlan}
+import izumi.distage.model.plan.initial.PrePlan
+import izumi.distage.model.plan.{OrderedPlan, SemiPlan}
 import izumi.fundamentals.platform.language.Quirks._
 
 // TODO: Just one method onPhase(Id, plan) ?
@@ -11,9 +12,9 @@ import izumi.fundamentals.platform.language.Quirks._
   * @see GraphDumpObserver
   */
 trait PlanningObserver {
-  def onSuccessfulStep(next: DodgyPlan): Unit = next.discard()
+  def onSuccessfulStep(next: PrePlan): Unit = next.discard()
 
-  def onPhase00PlanCompleted(plan: DodgyPlan): Unit = plan.discard()
+  def onPhase00PlanCompleted(plan: PrePlan): Unit = plan.discard()
   def onPhase05PreGC(plan: SemiPlan): Unit = plan.discard()
   def onPhase10PostGC(plan: SemiPlan): Unit = plan.discard()
   def onPhase20Customization(plan: SemiPlan): Unit = plan.discard()
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/planning/SanityChecker.scala b/distage/distage-model/src/main/scala/izumi/distage/model/planning/SanityChecker.scala
index 66a03be75..0f2b49e0e 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/planning/SanityChecker.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/planning/SanityChecker.scala
@@ -1,6 +1,7 @@
 package izumi.distage.model.planning
 
 import izumi.distage.model.plan._
+import izumi.distage.model.plan.initial.NextOps
 
 trait SanityChecker {
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala b/distage/distage-model/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala
index cce784c7b..7c1247ce7 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala
@@ -4,7 +4,8 @@ import izumi.distage.model.Locator
 import izumi.distage.model.definition.DIResource.DIResourceBase
 import izumi.distage.model.exceptions.{DIException, ProvisioningException}
 import izumi.distage.model.monadic.DIEffect
-import izumi.distage.model.plan.{OpFormatter, OrderedPlan}
+import izumi.distage.model.plan.OrderedPlan
+import izumi.distage.model.plan.repr.OpFormatter
 import izumi.distage.model.provisioning.PlanInterpreter.{FailedProvision, FinalizersFilter}
 import izumi.distage.model.provisioning.Provision.ProvisionImmutable
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/DIUniverse.scala b/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/DIUniverse.scala
index 551667618..6c8c53513 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/DIUniverse.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/DIUniverse.scala
@@ -1,7 +1,8 @@
 package izumi.distage.model.reflection.universe
 
 import izumi.distage.model.definition.WithDIAnnotation
-import izumi.distage.model.plan.{WithDIAssociation, WithDIDependencyContext, WithDIWiring}
+import izumi.distage.model.plan.operations.{WithDIAssociation, WithDIDependencyContext, WithDIWiring}
+import izumi.distage.model.plan.operations.{WithDIDependencyContext, WithDIWiring}
 import izumi.distage.model.references._
 
 trait DIUniverse
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/WithDICallable.scala b/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/WithDICallable.scala
index 250e148bf..1163d104c 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/WithDICallable.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/WithDICallable.scala
@@ -1,7 +1,7 @@
 package izumi.distage.model.reflection.universe
 
 import izumi.distage.model.exceptions.DIException
-import izumi.distage.model.plan.{WithDIAssociation, WithDIWiring}
+import izumi.distage.model.plan.operations.{WithDIAssociation, WithDIWiring}
 import izumi.distage.model.references.{WithDIKey, WithDITypedRef}
 
 trait WithDICallable {
diff --git a/distage/distage-model/src/main/scala/izumi/distage/provisioning/FactoryTools.scala b/distage/distage-model/src/main/scala/izumi/distage/provisioning/FactoryTools.scala
index 94fe9fc53..215d90874 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/provisioning/FactoryTools.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/provisioning/FactoryTools.scala
@@ -1,8 +1,8 @@
 package izumi.distage.provisioning
 
-import izumi.distage.model.definition.Binding
 import izumi.distage.model.exceptions.UnexpectedProvisionResultException
 import izumi.distage.model.plan.ExecutableOp.WiringOp
+import izumi.distage.model.plan.operations.OperationOrigin
 import izumi.distage.model.provisioning.NewObjectOp
 import izumi.distage.model.provisioning.NewObjectOp.{NewImport, NewInstance}
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
@@ -28,7 +28,7 @@ object FactoryTools {
     }
   }
 
-  def mkExecutableOp(key: RuntimeDIUniverse.DIKey, wiring: RuntimeDIUniverse.Wiring.SingletonWiring, binding: Option[Binding]): WiringOp =
+  def mkExecutableOp(key: RuntimeDIUniverse.DIKey, wiring: RuntimeDIUniverse.Wiring.SingletonWiring, binding: OperationOrigin): WiringOp =
     wiring match {
       case w: SingletonWiring.Constructor =>
         val target = RuntimeDIUniverse.DIKey.ProxyElementKey(key, w.instanceType)
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppLauncher.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppLauncher.scala
index 51d5642e9..3659da2f6 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppLauncher.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppLauncher.scala
@@ -94,7 +94,7 @@ abstract class RoleAppLauncherImpl[F[_]: TagK: DIEffect] extends RoleAppLauncher
     lateLogger.info(s"Planning finished. ${appPlan.app.primary.plan.keys.size -> "main ops"}, ${appPlan.app.side.plan.keys.size -> "integration ops"}, ${appPlan.app.shared.plan.keys.size -> "shared ops"}, ${appPlan.runtime.keys.size -> "runtime ops"}")
 
     val injector = appPlan.injector
-    val r = makeExecutor(parameters, roles, lateLogger, injector, makeStartupExecutor(lateLogger, injector))
+    val r = makeExecutor(parameters, roles, lateLogger, makeStartupExecutor(lateLogger, injector))
     r.runPlan(appPlan)
   }
 
@@ -129,8 +129,8 @@ abstract class RoleAppLauncherImpl[F[_]: TagK: DIEffect] extends RoleAppLauncher
     new RoleAppPlanner.Impl[F](options, bsModule, activation, lateLogger)
   }
 
-  protected def makeExecutor(parameters: RawAppArgs, roles: RolesInfo, lateLogger: IzLogger, injector: Injector, startupPlanExecutor: StartupPlanExecutor[F]): RoleAppExecutor[F] = {
-    new RoleAppExecutor.Impl[F](shutdownStrategy, roles, injector, lateLogger, parameters, startupPlanExecutor)
+  protected def makeExecutor(parameters: RawAppArgs, roles: RolesInfo, lateLogger: IzLogger, startupPlanExecutor: StartupPlanExecutor[F]): RoleAppExecutor[F] = {
+    new RoleAppExecutor.Impl[F](shutdownStrategy, roles, lateLogger, parameters, startupPlanExecutor)
   }
 
   protected def makeStartupExecutor(lateLogger: IzLogger, injector: Injector): StartupPlanExecutor[F] = {
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/ConfigWriter.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/ConfigWriter.scala
index d2876143d..0799e2cfb 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/ConfigWriter.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/ConfigWriter.scala
@@ -14,7 +14,6 @@ import izumi.distage.roles.config.ContextOptions
 import izumi.distage.roles.internal.ConfigWriter.{ConfigurableComponent, WriteReference}
 import izumi.distage.roles.model.meta.{RoleBinding, RolesInfo}
 import izumi.distage.roles.model.{RoleDescriptor, RoleTask}
-import izumi.distage.roles.config.ContextOptions
 import izumi.distage.roles.services.RoleAppPlanner
 import izumi.fundamentals.platform.cli.model.raw.RawEntrypointParams
 import izumi.fundamentals.platform.cli.model.schema.{ParserDef, RoleParserSchema}
@@ -72,7 +71,6 @@ class ConfigWriter[F[_] : DIEffect]
 
       try {
         writeConfig(options, versionedComponent, None, refConfig)
-
         minimizedConfig(refConfig, role)
           .foreach {
             cfg =>
@@ -139,7 +137,11 @@ class ConfigWriter[F[_] : DIEffect]
     }
 
     if (plans.app.primary.plan.steps.exists(_.target == roleDIKey)) {
-      getConfig(plans.app.primary.plan)
+      val cfg = getConfig(plans.app.primary.plan)
+        .orElse(getConfig(plans.app.shared.plan))
+        .orElse(getConfig(plans.app.side.plan))
+
+      cfg
         .map(_.withFallback(getConfigOrEmpty(plans.app.side.plan)))
         .map(_.withFallback(getConfigOrEmpty(plans.app.shared.plan)))
         .map(_.withFallback(getConfigOrEmpty(plans.runtime)))
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/Help.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/Help.scala
index d0d570a47..dbd76724f 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/Help.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/Help.scala
@@ -1,7 +1,6 @@
 package izumi.distage.roles.internal
 
 import izumi.distage.model.monadic.DIEffect
-import izumi.distage.roles.RoleAppLauncher
 import izumi.distage.roles.RoleAppLauncher.Options
 import izumi.distage.roles.model.meta.RolesInfo
 import izumi.distage.roles.model.{AppActivation, RoleDescriptor, RoleTask}
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/IntegrationChecker.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/IntegrationChecker.scala
index eba5ba84c..58e3244c0 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/IntegrationChecker.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/IntegrationChecker.scala
@@ -71,7 +71,7 @@ object IntegrationChecker {
       }
     }
 
-    private def runCheck(resource: IntegrationCheck)(implicit F: DIEffect[F], P: DIEffectAsync[F]): F[Either[ResourceCheck.Failure, Unit]] = {
+    private def runCheck(resource: IntegrationCheck)(implicit F: DIEffect[F]): F[Either[ResourceCheck.Failure, Unit]] = {
       F.maybeSuspend {
         logger.debug(s"Checking $resource")
         try {
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PruningPlanMergingPolicy.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PruningPlanMergingPolicy.scala
index eb8b0e4d1..aa0c06ed0 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PruningPlanMergingPolicy.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PruningPlanMergingPolicy.scala
@@ -2,7 +2,9 @@ package izumi.distage.roles.services
 
 import distage.DIKey
 import izumi.distage.model.definition.BindingTag
-import izumi.distage.model.plan.{DodgyPlan, ExecutableOp, SemiPlan}
+import izumi.distage.model.plan.ExecutableOp.SemiplanOp
+import izumi.distage.model.plan.SemiPlan
+import izumi.distage.model.plan.initial.PrePlan
 import izumi.distage.model.planning.PlanMergingPolicy.DIKeyConflictResolution
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 import izumi.distage.planning.PlanMergingPolicyDefaultImpl
@@ -17,7 +19,7 @@ class PruningPlanMergingPolicy(
                               ) extends PlanMergingPolicyDefaultImpl {
   private val activeTags = activation.active.values.toSet
 
-  override protected def resolveConflict(plan: DodgyPlan, key: RuntimeDIUniverse.DIKey, operations: Set[DodgyPlan.JustOp]): DIKeyConflictResolution = {
+  override protected def resolveConflict(plan: PrePlan, key: RuntimeDIUniverse.DIKey, operations: Set[PrePlan.JustOp]): DIKeyConflictResolution = {
     assert(operations.size > 1)
     val filtered = operations.filter {
       _.binding.tags
@@ -32,9 +34,9 @@ class PruningPlanMergingPolicy(
         logger.debug(s"Untagged conflicts were filtered out in $key: ${noTags.niceList() -> "filtered conflicts"}")
         //logger.warn(s"Untagged alternatives for conflict in $key were filtered out, continuing...")
       }
-      DIKeyConflictResolution.Successful(explicitlyEnabled.map(_.op: ExecutableOp))
+      DIKeyConflictResolution.Successful(explicitlyEnabled.map(_.op: SemiplanOp))
     } else if (noTags.size == 1) {
-      DIKeyConflictResolution.Successful(noTags.map(_.op: ExecutableOp))
+      DIKeyConflictResolution.Successful(noTags.map(_.op: SemiplanOp))
     } else if (filtered.nonEmpty) {
       val hints = makeHints(filtered)
       DIKeyConflictResolution.Failed(operations.map(_.op), s"${filtered.size} options left, possible disambiguations: ${hints.niceList()}")
@@ -44,7 +46,7 @@ class PruningPlanMergingPolicy(
     }
   }
 
-  override protected def handleIssues(plan: DodgyPlan, resolved: Map[DIKey, Set[ExecutableOp]], issues: Map[DIKey, DIKeyConflictResolution.Failed]): SemiPlan = {
+  override protected def handleIssues(plan: PrePlan, resolved: Map[DIKey, Set[SemiplanOp]], issues: Map[DIKey, DIKeyConflictResolution.Failed]): SemiPlan = {
     logger.debug(s"Not enough data to solve conflicts, will try to prune: ${formatIssues(issues) -> "issues"}")
 
     val ops = resolved.values.flatten.toVector
@@ -81,14 +83,14 @@ class PruningPlanMergingPolicy(
         logger.debug(s"Pruning strategy successfully resolved ${issues.size -> "conlicts"}, ${erased.size -> "erased"}, ${erased.keys.niceList() -> "erased conflicts"}")
         //logger.warn(s"Pruning strategy successfully resolved ${issues.size -> "conlicts"}, ${erased.size -> "erased"}, continuing...")
         val allResolved = (resolved.values.flatten ++ good.values.flatten).toVector
-        SemiPlan(plan.definition, allResolved, plan.gcMode)
+        SemiPlan(allResolved, plan.gcMode)
       }
     } else {
       throwOnIssues(issues)
     }
   }
 
-  private def makeHints(ops: Set[DodgyPlan.JustOp]): Seq[String] = {
+  private def makeHints(ops: Set[PrePlan.JustOp]): Seq[String] = {
     ops
       .toSeq
       .map {
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppExecutor.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppExecutor.scala
index 1fd4bbe9d..b6612b300 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppExecutor.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppExecutor.scala
@@ -1,6 +1,6 @@
 package izumi.distage.roles.services
 
-import distage.{Injector, TagK}
+import distage.TagK
 import izumi.distage.model.Locator
 import izumi.distage.model.monadic.DIEffect
 import izumi.distage.model.monadic.DIEffect.syntax._
@@ -21,7 +21,6 @@ object RoleAppExecutor {
   class Impl[F[_]: TagK](
                           hook: AppShutdownStrategy[F],
                           roles: RolesInfo,
-                          injector: Injector,
                           lateLogger: IzLogger,
                           parameters: RawAppArgs,
                           startupPlanExecutor: StartupPlanExecutor[F],
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppPlanner.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppPlanner.scala
index a6cdb6941..f5b0c8203 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppPlanner.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppPlanner.scala
@@ -62,11 +62,11 @@ object RoleAppPlanner {
       check.verify(appPlan.primary.plan)
 
 //      import izumi.fundamentals.platform.strings.IzString._
-//      logger.info(s"${fullAppModule.bindings.niceList() -> "app module"}")
-//      logger.info(s"${runtimePlan.render() -> "runtime plan"}")
-//      logger.info(s"${appPlan.shared.plan.render() -> "shared plan"}")
-//      logger.info(s"${appPlan.side.plan.render() -> "integration plan"}")
-//      logger.info(s"${appPlan.primary.plan.render() -> "primary plan"}")
+//      logger.error(s"${fullAppModule.bindings.niceList() -> "app module"}")
+//      logger.error(s"${runtimePlan.render() -> "runtime plan"}")
+//      logger.error(s"${appPlan.shared.plan.render() -> "shared plan"}")
+//      logger.error(s"${appPlan.side.plan.render() -> "integration plan"}")
+//      logger.error(s"${appPlan.primary.plan.render() -> "primary plan"}")
 
       AppStartupPlans(
         runtimePlan,
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/staticinjector/plugins/macrodefs/StaticPluginCheckerMacro.scala b/distage/distage-roles/src/main/scala/izumi/distage/staticinjector/plugins/macrodefs/StaticPluginCheckerMacro.scala
index 09f3070e2..93f399476 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/staticinjector/plugins/macrodefs/StaticPluginCheckerMacro.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/staticinjector/plugins/macrodefs/StaticPluginCheckerMacro.scala
@@ -17,7 +17,6 @@ import izumi.distage.plugins.PluginBase
 import izumi.distage.plugins.load.PluginLoader.PluginConfig
 import izumi.distage.plugins.load.PluginLoaderDefaultImpl
 import izumi.distage.plugins.merge.SimplePluginMergeStrategy
-import izumi.distage.roles.RoleAppLauncher
 import izumi.distage.roles.RoleAppLauncher.Options
 import izumi.distage.roles.services.{ActivationParser, PruningPlanMergingPolicy}
 import izumi.distage.staticinjector.plugins.ModuleRequirements
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/ClientDispatcher.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/ClientDispatcher.scala
index 55958b858..b21802eda 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/ClientDispatcher.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/ClientDispatcher.scala
@@ -29,7 +29,7 @@ class ClientDispatcher[C <: Http4sContext]
     codec.encode(request)
       .flatMap {
         encoded =>
-          val outBytes: Array[Byte] = printer.pretty(encoded).getBytes
+          val outBytes: Array[Byte] = printer.print(encoded).getBytes
           val req = buildRequest(baseUri, request, outBytes)
 
           logger.debug(s"${request.method -> "method"}: Prepared request $encoded")
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala
index abc334e73..40a717b68 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala
@@ -163,7 +163,7 @@ class HttpServer[C <: Http4sContext]
   protected def handleResult(context: WebsocketClientContextImpl[C], result: BIOExit[Throwable, Option[RpcPacket]]): Option[String] = {
     result match {
       case Success(v) =>
-        v.map(_.asJson).map(printer.pretty)
+        v.map(_.asJson).map(printer.print)
 
       case Error(error, _) =>
         Some(handleWsError(context, List(error), None, "failure"))
@@ -231,11 +231,11 @@ class HttpServer[C <: Http4sContext]
     causes.headOption match {
       case Some(cause) =>
         logger.error(s"${context -> null}: WS Execution failed, $kind, $data, $cause")
-        printer.pretty(rpc.RpcPacket.rpcCritical(data.getOrElse(cause.getMessage), kind).asJson)
+        printer.print(rpc.RpcPacket.rpcCritical(data.getOrElse(cause.getMessage), kind).asJson)
 
       case None =>
         logger.error(s"${context -> null}: WS Execution failed, $kind, $data")
-        printer.pretty(rpc.RpcPacket.rpcCritical("?", kind).asJson)
+        printer.print(rpc.RpcPacket.rpcCritical("?", kind).asJson)
     }
   }
 
@@ -257,7 +257,7 @@ class HttpServer[C <: Http4sContext]
       case Success(v) =>
         v match {
           case Some(value) =>
-            dsl.Ok(printer.pretty(value))
+            dsl.Ok(printer.print(value))
           case None =>
             logger.warn(s"${context -> null}: No service handler for $method")
             dsl.NotFound()
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithHttp4sWsClient.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithHttp4sWsClient.scala
index a5c354003..cde6e9b2d 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithHttp4sWsClient.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithHttp4sWsClient.scala
@@ -131,7 +131,7 @@ class ClientWsDispatcher[C <: Http4sContext]
               logger.error(s"${packetInfo -> null}: WS processing failed, $exception $trace")
               F.pure(Some(rpc.RpcPacket.buzzerFail(Some(id), exception.getMessage)))
           }
-          maybeEncoded <- F(maybePacket.map(r => printer.pretty(r.asJson)))
+          maybeEncoded <- F(maybePacket.map(r => printer.print(r.asJson)))
           _ <- F {
             maybeEncoded match {
               case Some(response) =>
@@ -183,7 +183,7 @@ class ClientWsDispatcher[C <: Http4sContext]
               val pid = w.id.get // guaranteed to be present
 
               F {
-                val out = printer.pretty(transformRequest(w).asJson)
+                val out = printer.print(transformRequest(w).asJson)
                 logger.debug(s"${request.method -> "method"}, ${pid -> "id"}: Prepared request $encoded")
                 requestState.request(pid, request.method)
                 send(out)
diff --git a/logstage/logstage-di/src/main/scala/izumi/logstage/distage/PlanningObserverLoggingImpl.scala b/logstage/logstage-di/src/main/scala/izumi/logstage/distage/PlanningObserverLoggingImpl.scala
index b2e77a473..2f59aab14 100644
--- a/logstage/logstage-di/src/main/scala/izumi/logstage/distage/PlanningObserverLoggingImpl.scala
+++ b/logstage/logstage-di/src/main/scala/izumi/logstage/distage/PlanningObserverLoggingImpl.scala
@@ -1,15 +1,16 @@
 package izumi.logstage.distage
 
-import izumi.distage.model.plan.{DodgyPlan, SemiPlan, OrderedPlan}
+import izumi.distage.model.plan.initial.PrePlan
+import izumi.distage.model.plan.{OrderedPlan, SemiPlan}
 import izumi.distage.model.planning.PlanningObserver
 import izumi.logstage.api.IzLogger
 
 class PlanningObserverLoggingImpl(log: IzLogger) extends PlanningObserver {
-  override def onSuccessfulStep(next: DodgyPlan): Unit = {
+  override def onSuccessfulStep(next: PrePlan): Unit = {
     log.trace(s"DIStage performed planning step:\n$next")
   }
 
-  override def onPhase00PlanCompleted(plan: DodgyPlan): Unit = {
+  override def onPhase00PlanCompleted(plan: PrePlan): Unit = {
     log.debug(s"[onPhase00PlanCompleted]:\n$plan")
   }
 


Test patch:
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/BasicTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/BasicTest.scala
index f6ced455e..e598e4dcf 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/injector/BasicTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/BasicTest.scala
@@ -242,7 +242,7 @@ class BasicTest extends WordSpec with MkInjector {
     val injector = mkInjector()
 
     val plan1 = injector.plan(definition)
-    val plan2 = injector.finish(plan1.providerImport {
+    val plan2 = injector.finish(plan1.toSemi.providerImport {
       verse: String@Id("verse") =>
         TestInstanceBinding(verse)
     })
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/CompactPlanFormatterTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/CompactPlanFormatterTest.scala
index a4bf8f55f..c872d0e1c 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/injector/CompactPlanFormatterTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/CompactPlanFormatterTest.scala
@@ -3,7 +3,7 @@ package izumi.distage.injector
 import izumi.distage.fixtures.BasicCases.BasicCase1.{Impl1, JustTrait}
 import izumi.distage.fixtures.HigherKindCases.HigherKindsCase1.OptionT
 import izumi.distage.model.PlannerInput
-import izumi.distage.model.plan.CompactPlanFormatter._
+import izumi.distage.model.plan.repr.CompactPlanFormatter._
 import izumi.functional.Renderable._
 import distage.ModuleDef
 import org.scalatest.WordSpec
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/PlanOperationsTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/PlanOperationsTest.scala
index 04872f846..fa749cf9e 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/injector/PlanOperationsTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/PlanOperationsTest.scala
@@ -2,6 +2,7 @@ package izumi.distage.injector
 
 import distage._
 import izumi.distage.model.PlannerInput
+import izumi.distage.model.plan.ExecutableOp.ImportDependency
 import org.scalatest.WordSpec
 
 
@@ -9,31 +10,98 @@ class PlanOperationsTest extends WordSpec with MkInjector {
 
   import PlanOperationsTest._
 
-  "maintain correct operation order" in {
-    val primary = Set[DIKey](DIKey.get[PrimaryComponent])
-    val sub = Set[DIKey](DIKey.get[IntegrationComponent])
+  private val icKey: DIKey = DIKey.get[IntegrationComponent]
+  private val pcKey: DIKey = DIKey.get[PrimaryComponent]
+
+  private val sc0: DIKey = DIKey.get[SharedComponent0]
+  private val sc1: DIKey = DIKey.get[SharedComponent1]
+  private val sc2: DIKey = DIKey.get[SharedComponent2]
+
+
+  private val injector = mkInjector()
+
+  "support plan trisplit" in {
+    val primary = Set(pcKey)
+    val sub = Set(icKey)
 
     val definition = PlannerInput(new ModuleDef {
       make[PrimaryComponent]
       make[IntegrationComponent]
       make[SharedComponent0]
       make[SharedComponent1]
+      make[SharedComponent2]
     }, primary ++ sub)
 
-    val injector = mkInjector()
-
     val split = injector.triSplitPlan(definition.bindings, primary)(_ => sub)
 
-    assert(Set[DIKey](DIKey.get[SharedComponent0], DIKey.get[SharedComponent1]).diff(split.shared.plan.index.keySet).isEmpty)
+    assert(Set(sc0, sc1, sc2).diff(split.shared.plan.index.keySet).isEmpty)
+
     assert((primary ++ sub).intersect(split.shared.plan.index.keySet).isEmpty)
     assert(primary.intersect(split.side.plan.index.keySet).isEmpty)
     assert(sub.intersect(split.primary.plan.index.keySet).isEmpty)
 
-    assert(split.primary.plan.index.keySet.intersect(split.side.plan.index.keySet) == Set[DIKey](DIKey.get[SharedComponent1]))
-    assert(split.primary.plan.index.keySet.intersect(split.shared.plan.index.keySet) == Set[DIKey](DIKey.get[SharedComponent1]))
-    assert(split.side.plan.index.keySet.intersect(split.shared.plan.index.keySet) == Set[DIKey](DIKey.get[SharedComponent1]))
+    assert(split.primary.plan.index.keySet.intersect(split.side.plan.index.keySet) == Set(sc2))
+    assert(split.primary.plan.index.keySet.intersect(split.shared.plan.index.keySet) == Set(sc2))
+    assert(split.side.plan.index.keySet.intersect(split.shared.plan.index.keySet) == Set(sc2))
   }
 
+
+  "support ghost components in trisplit" in {
+    val primary = Set(pcKey, icKey)
+    val sub = Set(icKey)
+
+    val definition = PlannerInput(new ModuleDef {
+      make[PrimaryComponent]
+      make[IntegrationComponent]
+      make[SharedComponent0]
+      make[SharedComponent1]
+      make[SharedComponent2]
+    }, primary ++ sub)
+
+    val split = injector.triSplitPlan(definition.bindings, primary)(_ => sub)
+
+    val sideIndex = split.side.plan.index
+    val primaryIndex = split.primary.plan.index
+    val sharedIndex = split.shared.plan.index
+
+    assert(primaryIndex.keySet.intersect(sideIndex.keySet).intersect(sharedIndex.keySet) == Set(icKey))
+
+    assert(sharedIndex.keySet == Set(sc0, sc1, sc2, icKey))
+
+    assert(sideIndex.keySet == Set(icKey))
+    assert(sideIndex.get(icKey).exists(_.isInstanceOf[ImportDependency]))
+
+    assert(primaryIndex.keySet == Set(icKey, pcKey, sc2))
+    assert(primaryIndex.get(icKey).exists(_.isInstanceOf[ImportDependency]))
+    assert(primaryIndex.get(sc2).exists(_.isInstanceOf[ImportDependency]))
+  }
+
+  "support plan separation" in {
+    val primary = Set(pcKey)
+    val sub = Set(icKey)
+
+    val definition = PlannerInput(new ModuleDef {
+      make[PrimaryComponent]
+      make[IntegrationComponent]
+      make[SharedComponent0]
+      make[SharedComponent1]
+      make[SharedComponent2]
+    }, primary ++ sub)
+
+    val srcPlan = injector.plan(definition)
+
+    def verifySingleImport(key: DIKey): Unit = {
+      val plan = srcPlan.replaceWithImports(Set(key))
+      assert(plan.index.get(key).exists(_.isInstanceOf[ImportDependency]))
+      assert(plan.index.values.collect({ case i: ImportDependency => i }).size == 1)
+      assert(!plan.definition.keys.contains(key))
+      ()
+    }
+
+    verifySingleImport(icKey)
+    verifySingleImport(sc0)
+    verifySingleImport(sc1)
+  }
 }
 
 object PlanOperationsTest {
@@ -42,8 +110,10 @@ object PlanOperationsTest {
 
   case class SharedComponent1(com0: SharedComponent0)
 
-  case class IntegrationComponent(sharedComponent1: SharedComponent1)
+  case class SharedComponent2(com1: SharedComponent1)
+
+  case class IntegrationComponent(sharedComponent1: SharedComponent2)
 
-  case class PrimaryComponent(sharedComponent1: SharedComponent1)
+  case class PrimaryComponent(sharedComponent1: SharedComponent2)
 
 }
\ No newline at end of file
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
index 866966f3e..dacbdfd28 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
@@ -7,13 +7,13 @@ import java.util.UUID
 import distage.plugins.{PluginBase, PluginDef}
 import distage.{DIKey, Injector, Locator}
 import izumi.distage.model.Locator.LocatorRef
-import izumi.distage.model.definition.{BootstrapModule, ModuleDef}
+import izumi.distage.model.definition.BootstrapModule
 import izumi.distage.monadic.modules.IdentityDIEffectModule
 import izumi.distage.roles.RoleAppMain
 import izumi.distage.roles.config.ContextOptions
 import izumi.distage.roles.model.AppActivation
 import izumi.distage.roles.services.{IntegrationChecker, PluginSource, RoleAppPlanner}
-import izumi.distage.roles.test.fixtures.Fixture.{InitCounter, Resource0, Resource1, Resource2}
+import izumi.distage.roles.test.fixtures.Fixture.{Resource0, Resource1, Resource2, XXX_ResourceEffectsRecorder}
 import izumi.distage.roles.test.fixtures._
 import izumi.fundamentals.platform.functional.Identity
 import izumi.fundamentals.platform.resources.ArtifactVersion
@@ -32,22 +32,27 @@ class RoleAppTest extends WordSpec
 
   object TestEntrypoint extends RoleAppMain.Silent(new TestLauncher)
 
+  class XXX_TestWhiteboxProbe extends PluginDef {
+    val resources = new XXX_ResourceEffectsRecorder
+    private var locator0: LocatorRef = null
+    lazy val locator: Locator = locator0.get
+
+    make[XXX_ResourceEffectsRecorder].from(resources)
+    make[XXX_LocatorLeak].from {
+      locatorRef: LocatorRef =>
+        locator0 = locatorRef
+        XXX_LocatorLeak(locator0)
+    }
+  }
+
   "Role Launcher" should {
     "be able to start roles" in {
-      val initCounter = new InitCounter
-      var locator0: LocatorRef = null
-      lazy val locator: Locator = locator0.get
+      val probe = new XXX_TestWhiteboxProbe()
 
       new RoleAppMain.Silent({
         new TestLauncher {
           override protected def pluginSource: PluginSource = super.pluginSource.map { l =>
-            l.copy(app = Seq(l.app.merge overridenBy new ModuleDef {
-              make[InitCounter].from {
-                locatorRef: LocatorRef =>
-                  locator0 = locatorRef
-                  initCounter
-              }
-            }))
+            l.copy(app = Seq(l.app.merge overridenBy probe))
           }
         }
       }).main(Array(
@@ -56,18 +61,13 @@ class RoleAppTest extends WordSpec
         ":" + TestRole00.id,
       ))
 
-//      println(initCounter.startedCloseables)
-//      println(initCounter.closedCloseables)
-//      println(initCounter.checkedResources)
-
-      assert(initCounter.startedCloseables == initCounter.closedCloseables.reverse)
-      assert(initCounter.checkedResources.toSet == Set(locator.get[Resource1], locator.get[Resource2]))
+      assert(probe.resources.startedCloseables == probe.resources.closedCloseables.reverse)
+      assert(probe.resources.checkedResources.toSet == Set(probe.locator.get[Resource1], probe.locator.get[Resource2]))
     }
 
     "start roles regression test" in {
-      val initCounter = new InitCounter
-      var locator0: LocatorRef = null
-      lazy val locator: Locator = locator0.get
+      val probe = new XXX_TestWhiteboxProbe()
+
 
       new RoleAppMain.Silent({
         new TestLauncher {
@@ -80,12 +80,9 @@ class RoleAppTest extends WordSpec
               new PluginDef {
                 make[Resource0].from[Resource1]
                 many[Resource0].ref[Resource0]
-                make[InitCounter].from {
-                  locatorRef: LocatorRef =>
-                    locator0 = locatorRef
-                    initCounter
-                }
-              }))
+              },
+              probe
+            ))
           }
         }
       }).main(Array(
@@ -95,22 +92,18 @@ class RoleAppTest extends WordSpec
       ))
 
 
-
-//      println(initCounter.startedCloseables)
-//      println(initCounter.closedCloseables)
-//      println(initCounter.checkedResources)
-
-      assert(initCounter.startedCloseables == initCounter.closedCloseables.reverse)
-      assert(initCounter.checkedResources.toSet == Set(locator.get[Resource0], locator.get[Resource2]))
+      assert(probe.resources.startedCloseables == probe.resources.closedCloseables.reverse)
+      assert(probe.resources.checkedResources.toSet.size == 2)
+      assert(probe.resources.checkedResources.toSet == Set(probe.locator.get[Resource0], probe.locator.get[Resource2]))
     }
 
     "integration checks are discovered and ran from a class binding when key is not an IntegrationCheck" in {
       val logger = IzLogger()
-      val initCounter = new InitCounter
+      val initCounter = new XXX_ResourceEffectsRecorder
       val definition = (new ResourcesPluginBase {
         make[Resource0].from[Resource1]
         many[Resource0].ref[Resource0]
-        make[InitCounter].fromValue(initCounter)
+        make[XXX_ResourceEffectsRecorder].fromValue(initCounter)
       } ++ IdentityDIEffectModule)
       val roleAppPlanner = new RoleAppPlanner.Impl[Identity](
         ContextOptions(),
@@ -137,7 +130,7 @@ class RoleAppTest extends WordSpec
 
     "integration checks are discovered and ran, ignoring duplicating reference bindings" in {
       val logger = IzLogger()
-      val initCounter = new InitCounter
+      val initCounter = new XXX_ResourceEffectsRecorder
       val definition = new ResourcesPluginBase {
         make[Resource1]
         make[Resource0].using[Resource1]
@@ -145,7 +138,7 @@ class RoleAppTest extends WordSpec
         many[Resource0]
           .ref[Resource0]
           .ref[Resource0 with AutoCloseable]
-        make[InitCounter].fromValue(initCounter)
+        make[XXX_ResourceEffectsRecorder].fromValue(initCounter)
       } ++ IdentityDIEffectModule
       val roleAppPlanner = new RoleAppPlanner.Impl[Identity](
         ContextOptions(),
@@ -174,20 +167,24 @@ class RoleAppTest extends WordSpec
       val version = ArtifactVersion(s"0.0.0-${UUID.randomUUID().toString}")
       withProperties(overrides ++ Map(TestPlugin.versionProperty -> version.version)) {
         TestEntrypoint.main(Array(
-          "-ll", "critical",
+          "-ll", "info",
           ":configwriter", "-t", prefix
         ))
       }
 
       val cfg1 = cfg("configwriter", version)
-      val cfg2 = cfg("configwriter-minimized", version)
-      val cfg3 = cfg("testrole00-minimized", version)
+      val cfg11 = cfg("configwriter-minimized", version)
+
+      val cfg2 = cfg("testrole00", version)
+      val cfg21 = cfg("testrole00-minimized", version)
 
       assert(cfg1.exists())
+      assert(cfg11.exists())
       assert(cfg2.exists())
-      assert(cfg3.exists())
-      assert(cfg1.length() > cfg2.length())
-      assert(new String(Files.readAllBytes(cfg3.toPath), UTF_8).contains("integrationOnlyCfg"))
+      assert(cfg21.exists())
+
+      assert(cfg1.length() > cfg11.length())
+      assert(new String(Files.readAllBytes(cfg21.toPath), UTF_8).contains("integrationOnlyCfg"))
     }
   }
 
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
index 9add98faa..a30772541 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
@@ -16,7 +16,7 @@ object Fixture {
                               systemPropList: List[Int],
                             )
 
-  class InitCounter {
+  class XXX_ResourceEffectsRecorder {
     val startedCloseables: mutable.ArrayBuffer[AutoCloseable] = mutable.ArrayBuffer()
     val closedCloseables: mutable.ArrayBuffer[AutoCloseable] = mutable.ArrayBuffer()
     val checkedResources: mutable.ArrayBuffer[IntegrationCheck] = mutable.ArrayBuffer()
@@ -24,7 +24,7 @@ object Fixture {
 
   trait Resource0
 
-  class Resource1(val closeable: Resource2, counter: InitCounter) extends Resource0 with AutoCloseable with IntegrationCheck {
+  class Resource1(val closeable: Resource2, counter: XXX_ResourceEffectsRecorder) extends Resource0 with AutoCloseable with IntegrationCheck {
     counter.startedCloseables += this
 
     override def close(): Unit = counter.closedCloseables += this
@@ -35,7 +35,7 @@ object Fixture {
     }
   }
 
-  class Resource2(val roleComponent: Resource3, counter: InitCounter) extends Resource0 with AutoCloseable with IntegrationCheck {
+  class Resource2(val roleComponent: Resource3, counter: XXX_ResourceEffectsRecorder) extends Resource0 with AutoCloseable with IntegrationCheck {
     counter.startedCloseables += this
 
     override def close(): Unit = counter.closedCloseables += this
@@ -46,16 +46,16 @@ object Fixture {
     }
   }
 
-  case class Resource3(roleComponent: Resource4, counter: InitCounter) extends Resource0
+  case class Resource3(roleComponent: Resource4, counter: XXX_ResourceEffectsRecorder) extends Resource0
 
-  case class Resource4(closeable: Resource5, counter: InitCounter) extends Resource0
+  case class Resource4(closeable: Resource5, counter: XXX_ResourceEffectsRecorder) extends Resource0
 
-  case class Resource5(roleComponent: Resource6, counter: InitCounter) extends Resource0 with AutoCloseable {
+  case class Resource5(roleComponent: Resource6, counter: XXX_ResourceEffectsRecorder) extends Resource0 with AutoCloseable {
     counter.startedCloseables += this
 
     override def close(): Unit = counter.closedCloseables += this
   }
 
-  case class Resource6(counter: InitCounter) extends Resource0
+  case class Resource6(counter: XXX_ResourceEffectsRecorder) extends Resource0
 
 }
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala
index b8475cc1e..8f296d5d6 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala
@@ -34,7 +34,7 @@ class ResourcesPluginBase extends ModuleDef {
 }
 
 class ResourcesPlugin extends ResourcesPluginBase with PluginDef {
-  make[InitCounter]
+  make[XXX_ResourceEffectsRecorder]
 
   make[Resource1]
   many[Resource0]
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
index 7f658049d..0a309ae71 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
@@ -2,7 +2,6 @@ package izumi.distage.roles.test.fixtures
 
 import cats.effect.IO
 import izumi.distage.model.definition.StandardAxis._
-import izumi.distage.model.monadic.{DIEffect, DIEffectAsync, DIEffectRunner}
 import izumi.distage.monadic.modules.CatsDIEffectModule
 import izumi.distage.plugins.PluginDef
 import izumi.distage.roles.internal.{ConfigWriter, Help}
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala
index ac4df70aa..2a7e2ebe3 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala
@@ -3,6 +3,7 @@ package izumi.distage.roles.test.fixtures
 import java.util.concurrent.ExecutorService
 
 import izumi.distage.config.annotations.ConfPath
+import izumi.distage.model.Locator.LocatorRef
 import izumi.distage.model.definition.DIResource
 import izumi.distage.model.monadic.DIEffect
 import izumi.distage.roles.model.{IntegrationCheck, RoleDescriptor, RoleService, RoleTask}
@@ -28,17 +29,21 @@ object TestTask00 extends RoleDescriptor {
   override final val id = "testtask00"
 }
 
+case class XXX_LocatorLeak(locatorRef: LocatorRef)
+
 class TestRole00[F[_]: DIEffect]
 (
   val conf: TestServiceConf@ConfPath("testservice"),
   val dummies: Set[Dummy],
-  val counter: InitCounter,
+  val counter: XXX_ResourceEffectsRecorder,
   val resource: TestRole00Resource[F],
   logger: IzLogger,
   notCloseable: NotCloseable,
   val resources: Set[Resource0],
   val conflict: Conflict,
   val es: ExecutorService,
+  val ref: XXX_LocatorLeak
+
 ) extends RoleService[F] {
   notCloseable.discard()
 
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/ContainerHealthCheck.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/ContainerHealthCheck.scala
index c416f6ad4..fe2fbba38 100644
--- a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/ContainerHealthCheck.scala
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/ContainerHealthCheck.scala
@@ -1,6 +1,6 @@
 package izumi.distage.testkit.integration.docker
 
-import izumi.distage.testkit.integration.docker.Docker.{AvailablePort, DockerPort, HealthCheckResult, ServicePort}
+import izumi.distage.testkit.integration.docker.Docker.{AvailablePort, DockerPort, HealthCheckResult}
 import izumi.fundamentals.platform.integration.{PortCheck, ResourceCheck}
 import izumi.logstage.api.IzLogger
 
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/IgnoreSupport.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/IgnoreSupport.scala
index 9a2b59885..1c231fbd1 100644
--- a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/IgnoreSupport.scala
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/IgnoreSupport.scala
@@ -1,6 +1,6 @@
 package izumi.distage.testkit.services.st.adapter
 
-import org.scalatest.exceptions.TestCanceledException
+import org.scalatest.TestCancellation
 
 @deprecated("Use dstest", "2019/Jul/18")
 private[testkit] trait IgnoreSupport {
@@ -17,8 +17,7 @@ private[testkit] trait IgnoreSupport {
   }
 
   protected final def ignoreThisTest(message: Option[String] = None, cause: Option[Throwable] = None): Nothing = {
-    throw new TestCanceledException(message, cause, failedCodeStackDepth = 0)
+    TestCancellation.cancel(message, cause)
   }
 
-
 }
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/dtest/DistageAbstractScalatestSpec.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/dtest/DistageAbstractScalatestSpec.scala
index bfa83fbb5..700256c81 100644
--- a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/dtest/DistageAbstractScalatestSpec.scala
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/dtest/DistageAbstractScalatestSpec.scala
@@ -12,13 +12,13 @@ import izumi.distage.testkit.services.{DISyntaxBIOBase, DISyntaxBase}
 import izumi.fundamentals.platform.language.{CodePosition, CodePositionMaterializer, Quirks}
 import izumi.logstage.api.{IzLogger, Log}
 import org.scalactic.source
-import org.scalatest.exceptions.TestCanceledException
+import org.scalatest.TestCancellation
 
 import scala.language.implicitConversions
 
 trait WithSingletonTestRegistration[F[_]] extends AbstractDistageSpec[F] {
   override def registerTest(function: ProviderMagnet[F[_]], env: TestEnvironment, pos: CodePosition, id: TestId): Unit = {
-    DistageTestsRegistrySingleton.register[F](DistageTest(function, env, TestMeta(id, pos, System.identityHashCode(function))))
+    DistageTestsRegistrySingleton.register[F](DistageTest(function, env, TestMeta(id, pos, System.identityHashCode(function).toLong)))
   }
 }
 
@@ -154,7 +154,7 @@ object DistageAbstractScalatestSpec {
     }
 
     private def cancelNow() = {
-      throw new TestCanceledException("test skipped!", 0)
+      TestCancellation.cancel(Some("test skipped!"), None, 1)
     }
   }
 
@@ -204,7 +204,7 @@ object DistageAbstractScalatestSpec {
     }
 
     private def cancelNow() = {
-      throw new TestCanceledException("test skipped!", 0)
+      TestCancellation.cancel(Some("test skipped!"), None, 1)
     }
   }
 
diff --git a/distage/distage-testkit/src/main/scala/org/scalatest/SafeTestReporter.scala b/distage/distage-testkit/src/main/scala/org/scalatest/SafeTestReporter.scala
index aae90aacc..41b57ce6e 100644
--- a/distage/distage-testkit/src/main/scala/org/scalatest/SafeTestReporter.scala
+++ b/distage/distage-testkit/src/main/scala/org/scalatest/SafeTestReporter.scala
@@ -64,6 +64,7 @@ class SafeTestReporter(underlying: TestReporter) extends TestReporter {
         }
         finish()
     }
+    ()
   }
 
   private def reportStatus(test: TestMeta, testStatus: TestStatus): Unit = {
diff --git a/distage/distage-testkit/src/main/scala/org/scalatest/ScalatestSuite.scala b/distage/distage-testkit/src/main/scala/org/scalatest/ScalatestSuite.scala
index 55c9893cf..b88994b2a 100644
--- a/distage/distage-testkit/src/main/scala/org/scalatest/ScalatestSuite.scala
+++ b/distage/distage-testkit/src/main/scala/org/scalatest/ScalatestSuite.scala
@@ -3,3 +3,4 @@ package org.scalatest
 object ScalatestSuite {
   def suiteToString(substitution: Option[(String, String)], theSuite: Suite): String = Suite.suiteToString(substitution, theSuite)
 }
+
diff --git a/distage/distage-testkit/src/main/scala/org/scalatest/TestCancellation.scala b/distage/distage-testkit/src/main/scala/org/scalatest/TestCancellation.scala
new file mode 100644
index 000000000..2b8e62d07
--- /dev/null
+++ b/distage/distage-testkit/src/main/scala/org/scalatest/TestCancellation.scala
@@ -0,0 +1,12 @@
+package org.scalatest
+
+import org.scalatest.exceptions.TestCanceledException
+
+object TestCancellation {
+  def cancel(message: Option[String], cause: Option[Throwable], delta: Int = 0): Nothing = {
+    val exception = new TestCanceledException(message, cause, failedCodeStackDepth = 1 + delta)
+    val trace = exception.getStackTrace.filterNot(c => c.getClassName.startsWith("org.scalatest.") || c.getClassName.startsWith("sbt."))
+    exception.setStackTrace(trace)
+    throw exception
+  }
+}
diff --git a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/catstest/CatsExtensionsTest.scala b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/catstest/CatsExtensionsTest.scala
index 8d8c46fe6..e195a7f2e 100644
--- a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/catstest/CatsExtensionsTest.scala
+++ b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/catstest/CatsExtensionsTest.scala
@@ -9,6 +9,7 @@ import izumi.distage.model.PlannerInput
 import izumi.distage.model.definition.ModuleDef
 import izumi.distage.model.plan.ExecutableOp.WiringOp.ReferenceInstance
 import distage._
+import izumi.distage.model.plan.ExecutableOp.SemiplanOp
 import org.scalatest.{GivenWhenThen, WordSpec}
 
 class CatsExtensionsTest extends WordSpec with GivenWhenThen {
@@ -39,7 +40,7 @@ class CatsExtensionsTest extends WordSpec with GivenWhenThen {
           make[TestDependency1].from(TestDependency1Eq(_: NotInContext): TestDependency1)
         })
       )
-      val testDependencyOp = testDependencyPlan.steps.last
+      val testDependencyOp = testDependencyPlan.steps.last.asInstanceOf[SemiplanOp]
 
       val plan2 = injector.finish(testDependencyPlan.traverse[Id] { _ => testDependencyOp })
 
diff --git a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
index e4e118725..8b1e5488e 100644
--- a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
+++ b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
@@ -5,7 +5,7 @@ import izumi.distage.model.definition.Id
 import izumi.distage.model.definition.StandardAxis.Env
 import izumi.distage.monadic.modules.{CatsDIEffectModule, ZIODIEffectModule}
 import izumi.distage.plugins.PluginDef
-import izumi.distage.testkit.integration.docker.Docker.{AvailablePort, ServicePort}
+import izumi.distage.testkit.integration.docker.Docker.AvailablePort
 import izumi.distage.testkit.integration.docker.examples.{DynamoDocker, PostgresDocker}
 import izumi.distage.testkit.integration.docker.modules.DockerContainerModule
 import zio.Task