@Override
  public boolean processChangedModule(DifferentiateContext context, Difference.Change<JvmModule, JvmModule.Diff> change, Utils future, Utils present) {
    JvmModule changedModule = change.getPast();
    JvmModule.Diff diff = change.getDiff();
    boolean affectSelf = false;
    boolean affectDeps = false;
    Set<String> constraintPackageNames = new SmartHashSet<>();

    if (diff.versionChanged()) {
      String version = changedModule.getVersion();
      String moduleName = changedModule.getName();
      affectDependentModules(
        context, present, changedModule, false,
        mod -> mod instanceof JvmModule && !isEmpty(filter(((JvmModule)mod).getRequires(), req -> Objects.equals(moduleName, req.getName()) && Objects.equals(version, req.getVersion())))
      );
    }

    Difference.Specifier<ModuleRequires, ModuleRequires.Diff> requiresDiff = diff.requires();
    for (ModuleRequires removedRequires : requiresDiff.removed()) {
      affectSelf = true;
      if (removedRequires.isTransitive()) {
        affectDeps = true;
        break;
      }
    }

    for (Difference.Change<ModuleRequires, ModuleRequires.Diff> rChange : requiresDiff.changed()) {
      affectSelf |= rChange.getDiff().versionChanged();
      if (rChange.getDiff().becameNonTransitive()) {
        affectDeps = true;
        // we could have created more precise constraint here: analyze if required module (recursively)
        // has only qualified exports that include given module's name. But this seems to be excessive since
        // in most cases module's exports are unqualified, so that any other module can access the exported API.
      }
    }

    Difference.Specifier<ModulePackage, ModulePackage.Diff> exportsDiff = diff.exports();
    if (!affectDeps) {
      if (!isEmpty(exportsDiff.removed())) {
        affectDeps = true;
        if (isEmpty(filter(exportsDiff.removed(), modPackage -> !modPackage.isQualified()))) {
          // all removed exports are qualified
          collect(flat(map(exportsDiff.removed(), modPackage -> modPackage.getModules())), constraintPackageNames);
        }
      }
    }

    if (!affectDeps || !constraintPackageNames.isEmpty()) {
      for (Difference.Change<ModulePackage, ModulePackage.Diff> exportChange : exportsDiff.changed()) {
        Iterable<String> removedModuleNames = exportChange.getDiff().targetModules().removed();
        affectDeps |= !isEmpty(removedModuleNames);
        if (affectDeps) {
          collect(removedModuleNames, constraintPackageNames);
        }
      }
    }

    if (affectSelf) {
      affectModule(context, present, changedModule);
    }

    if (affectDeps) {
      affectDependentModules(
        context, present, changedModule, true, constraintPackageNames.isEmpty()? null : node -> node instanceof JvmModule && constraintPackageNames.contains(((JvmModule)node).getName())
      );
    }
    return true;
  }
@Override
  public boolean processChangedModule(DifferentiateContext context, Difference.Change<JvmModule, JvmModule.Diff> change, Utils future, Utils present)