From 6cb082bc519153c976fae8a9b443773e3b6d31a5 Mon Sep 17 00:00:00 2001 From: Rodolfo Hansen Date: Wed, 30 Sep 2026 22:11:11 +0200 Subject: [PATCH 1/2] =?UTF-8?q?refactor(core):=20retire=20MultiFocus.repre?= =?UTF-8?q?sentableAt=20=E2=80=94=20`repr0`=20was=20unobservable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `representableAt[F, A](F)(repr0)` discarded its index (`val _ = repr0`) and rebuilt pointwise via `F.tabulate`, so it built exactly the optic `MultiFocus.representable` builds, plus a parameter neither the factory body nor any operation on the built optic could reach: the Grate encoding's focus is the whole bundle `F.Representation => A` with `X = Unit`, and the write is positional. Two calls with different indices were the same optic. Decision — retire it (option b of the three considered): - (a) give `repr0` a runtime meaning: there is nowhere to put it. `X = Unit`, and the public `X` is existential, so a `.lead` accessor would need a field on the optic class plus a runtime witness match (the `Function1BroadcastOptic` shape) to recover it — sugar over `.at(repr0)`, which is the same read without optics-side state. The lead VALUE was already deleted as dead code: `MultiFocusLeadPosition` was dropped by 12b8279 for having zero readers, and the fold spike records "the lead value is empirically dead through `.modify`". A lead-sampling WRITE is not a missing feature but an unlawful one: `from` must tabulate positionally, and sampling one index rebuilds a constant container — the collapse the sibling `fix/grate-positional-composition` change set fixes. - (b) retire: position is a read-time argument, never a property of the optic, and `Representable` has no canonical index to hand a constructor (`Function1[Boolean, *]` has no privileged `Boolean`; `Function1[Nothing, *]` none at all). `.at(i)` subsumes the construction-time index, and works on composed optics too. - (c) keep it vestigial but pinned: that would freeze a lie in a published signature. The line's policy is to remove dead public surface (0.10's pruning, 0.13's `widenB`) — MiMa is off (`tlMimaPreviousVersions := Set.empty`) and each 0.x break is documented in `mima.sbt`. Source- and binary-breaking for direct callers; the migration is dropping the argument and reading a position with the `.at(i)` extension. Recorded in `mima.sbt` (newest break-list entry), `CHANGELOG.md` (Unreleased → Removed) and in both doc pages, with a tombstone on `representable`'s scaladoc. Specs: - `MultiFocusFunction1Spec`: the absorbed-Grate block is rewired onto `representable` and pinned against the instance's own `index` / `map` for two `Representable`s — the second permuted (index order ≠ field order), so "index 0" and "first field" are different questions, and `modify` / `replace` must stay pointwise (a lead-sampling write would stop matching the instance's `map`). - `UnlawfulFixturesSpec`: negative fixture — a hand-written lead-sampling twin of the factory fails `MultiFocusLaws.modifyIdentity`, while the shipped `representable` passes the same law on the same carrier and input; the twin also fails the pointwise-`map` pin the core spec holds `representable` to. The carrier is a structural container: the law compares with `==`, which is reference equality on a function, so a function-typed `S` would fail it whatever the rebuild shape. Verified on JDK 25 (Temurin 25.0.4.1): `sbt scalafmtAll test` green across the root aggregate (237 examples, 0 failures; 2 pre-existing skips), focused `core/testOnly dev.constructive.eo.MultiFocusFunction1Spec` (5 examples, 313 expectations) and `tests/testOnly dev.constructive.eo.UnlawfulFixturesSpec` (4 examples, 6 expectations), `sbt scalafmtCheckAll`, `sbt docs/mdoc` (0 errors) and `sbt docs/laikaSite`. --- CHANGELOG.md | 17 +++++ .../dev/constructive/eo/data/MultiFocus.scala | 24 +++---- .../eo/MultiFocusFunction1Spec.scala | 72 ++++++++++++++----- mima.sbt | 6 ++ site/docs/multifocus.md | 14 ++-- site/docs/optics.md | 9 +-- .../eo/UnlawfulFixturesSpec.scala | 62 +++++++++++++++- 7 files changed, 158 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40af65a6..db5da47e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- **core: `MultiFocus.representableAt`** — the `repr0` argument was unobservable by + construction. The Grate encoding's focus is the whole bundle `F.Representation => A` + (`X = Unit`) and the write is a pointwise `F.tabulate`, so the index never reached the + built optic and two calls with different indices were the same optic — the factory was + `MultiFocus.representable` plus a parameter nothing could read. It is removed rather + than deprecated, matching the 0.x line's dead-surface policy (see + [`mima.sbt`](./mima.sbt) for the break list): **source- and binary-breaking** for + direct callers. Migrate by dropping the parameter — `MultiFocus.representable[F, A]` + builds the same optic — and read a position with the `.at(i)` extension, which takes + the index per call (`g.at(i)(fa) == F.index(fa)(i)`) and so subsumes the + construction-time one. Pinned by `MultiFocusFunction1Spec` (read / write agreement with + the instance's own `index` and `map` on two `Representable`s, one of them permuted) and + the negative fixture in `UnlawfulFixturesSpec` (a lead-sampling rebuild — the + parameter's only conceivable real semantics — fails `MultiFocusLaws.modifyIdentity`). + ## [0.18.0] - 2026-09-29 ### Fixed diff --git a/core/src/main/scala/dev/constructive/eo/data/MultiFocus.scala b/core/src/main/scala/dev/constructive/eo/data/MultiFocus.scala index f7ec56ea..c1c25fc7 100644 --- a/core/src/main/scala/dev/constructive/eo/data/MultiFocus.scala +++ b/core/src/main/scala/dev/constructive/eo/data/MultiFocus.scala @@ -891,6 +891,15 @@ object MultiFocusK: * on `from((_, k))` materialise via `F.tabulate(k)`. The `.modify(f)` round-trip is exactly * `F.map(fa)(f)`. * + * Position is a read-time argument, never a property of the optic: the index is supplied per + * call by the `.at(i)` extension (`g.at(i)(fa) == F.index(fa)(i)`), and `Representable` has no + * canonical `Representation` to hand a constructor anyway — `Function1[Boolean, *]` has no + * privileged `Boolean`, and `Function1[Nothing, *]` has no index at all. So this factory takes + * no representative index, and the built optic carries none: `X = Unit`, and the whole + * index-parametric read lives in the focus bundle. (Pre-0.19 a second name, + * `representableAt(F)(repr0)`, took exactly such an index; it built this same optic — see the + * changelog for the removal.) + * * @group Constructors */ def representable[F[_], A](using @@ -930,21 +939,6 @@ object MultiFocusK: def zip[F[_], A, B](fa: F[A], fb: F[B])(using Representable[F]): F[(A, B)] = zipWith(fa, fb)((a, b) => (a, b)) - /** Representable-indexed variant with explicit representative index. The `repr0` argument is - * unused at runtime (rebuild operates pointwise via `F.tabulate`); preserved for API parity and - * to leave the door open for a future `.lead` accessor. - * - * @group Constructors - */ - def representableAt[F[_], A](F: Representable[F])( - repr0: F.Representation - ): Optic[F[A], F[A], A, A, MultiFocus[Function1[F.Representation, *]]] = - val _ = repr0 - new Optic[F[A], F[A], A, A, MultiFocus[Function1[F.Representation, *]]]: - type X = Unit - def to(fa: F[A]): (Unit, F.Representation => A) = ((), F.index(fa)) - def from(pair: (Unit, F.Representation => A)): F[A] = F.tabulate(pair._2) - /** Polymorphic homogeneous-tuple Function1-shaped factory. `to(t) = ((), i => t._i)`, * `from((_, k))` materialises via `Tuple.fromArray(Array.tabulate(size)(i => k(i)))`. * diff --git a/core/src/test/scala/dev/constructive/eo/MultiFocusFunction1Spec.scala b/core/src/test/scala/dev/constructive/eo/MultiFocusFunction1Spec.scala index e87950f5..9bcd69b1 100644 --- a/core/src/test/scala/dev/constructive/eo/MultiFocusFunction1Spec.scala +++ b/core/src/test/scala/dev/constructive/eo/MultiFocusFunction1Spec.scala @@ -2,7 +2,7 @@ package dev.constructive.eo import scala.language.implicitConversions -import cats.Representable +import cats.{Functor, Representable} import cats.instances.function.given import dev.constructive.eo.compose.* import org.scalacheck.Prop.forAll @@ -19,7 +19,8 @@ import optics.Optic.* * * - `MultiFocus.representable` over a Naperian Function1 (formerly * `Grate.apply[F: Representable]`) - * - `MultiFocus.representableAt` with explicit lead index (formerly `Grate.at`) + * - the `.at(i)` read surface standing in for the retired `representableAt` / `Grate.at` + * construction-time index, pinned against `F.index` on a permuted `Representable` * - `MultiFocus.tuple[T <: Tuple, A]` (formerly `Grate.tuple`) * - `forgetful2multifocusFunction1` Iso → MultiFocus[Function1] bridge (formerly * `forgetful2grate`) @@ -115,31 +116,66 @@ class MultiFocusFunction1Spec extends Specification with ScalaCheck: idOk && rotOk } - // covers: MultiFocus.representableAt — explicit-lead variant — modify-at-position and - // replace-broadcast both round-trip through the per-position read; - // MultiFocus[Function1].at(i) — typeclass-gated `.at(i: F.Representation)` read surface - // uses Representable[Function1[X0, *]] to read the focus at a chosen position (Q2 deliverable) - "MultiFocus.representableAt + .at(i): Representable-indexed factory + position read" >> { + // A second Grate `Representable`, over a container whose index ORDER is a permutation of its + // field order — so "index 0" and "first field" are different questions and a privileged / lead + // position has nowhere to hide. Lawful: `Representable` asks for `index` / `tabulate` to be + // mutually inverse, never for the index order to follow the declaration order. + enum Slot: + case S0, S1, S2 + + case class Tri[A](a: A, b: A, c: A) + + given triFunctor: Functor[Tri] with + def map[A, B](fa: Tri[A])(f: A => B): Tri[B] = Tri(f(fa.a), f(fa.b), f(fa.c)) + + given triRepresentable: Representable.Aux[Tri, Slot] = new Representable[Tri]: + type Representation = Slot + def F: Functor[Tri] = triFunctor + + def index[A](fa: Tri[A]): Slot => A = + case Slot.S0 => fa.c + case Slot.S1 => fa.a + case Slot.S2 => fa.b + + def tabulate[A](f: Slot => A): Tri[A] = Tri(f(Slot.S1), f(Slot.S2), f(Slot.S0)) + + // covers: MultiFocus.representable + .at(i) — position is a READ-time argument + // (`g.at(i)(fa) == F.index(fa)(i)`; the typeclass-gated read surface that replaced + // `representableAt`'s construction-time index), pinned against `F.index` / the instance's own + // `map` for two `Representable`s, the second permuted; modify / replace stay pointwise — the + // property a lead-sampling rebuild would break (witnessed negatively by UnlawfulFixturesSpec) + "MultiFocus.representable + .at(i): position read is index-parametric, modify stays pointwise" >> { val F = summon[Representable[[a] =>> Boolean => a]] - val gTrue: Optic[Boolean => Int, Boolean => Int, Int, Int, MultiFocus[Function1[Boolean, *]]] = - MultiFocus.representableAt[[a] =>> Boolean => a, Int](F)(true) - val gFalse: Optic[Boolean => Int, Boolean => Int, Int, Int, MultiFocus[Function1[Boolean, *]]] = - MultiFocus.representableAt[[a] =>> Boolean => a, Int](F)(false) + val g: Optic[Boolean => Int, Boolean => Int, Int, Int, MultiFocus[Function1[Boolean, *]]] = + MultiFocus.representable[[a] =>> Boolean => a, Int] val fn: Boolean => Int = b => if b then 42 else 7 - val doubled = gTrue.modify(_ * 2)(fn) + val doubled = g.modify(_ * 2)(fn) val modOk = (doubled(true) === 84).and(doubled(false) === 14) val fn2: Boolean => Int = b => if b then 1 else 2 - val flat = gFalse.replace(99)(fn2) + val flat = g.replace(99)(fn2) val replOk = (flat(true) === 99).and(flat(false) === 99) - val g: Optic[Boolean => Int, Boolean => Int, Int, Int, MultiFocus[Function1[Boolean, *]]] = - MultiFocus.representable[[a] =>> Boolean => a, Int] val readFn: Boolean => Int = b => if b then 100 else 200 - val readOk = (g.at(true)(readFn) === 100).and(g.at(false)(readFn) === 200) - - modOk.and(replOk).and(readOk) + val readOk = (g.at(true)(readFn) === F.index(readFn)(true)) + .and(g.at(false)(readFn) === F.index(readFn)(false)) + + // Permuted instance: the optic must follow the INSTANCE's index order (S0 is the THIRD field), + // and its write must stay pointwise — a rebuild that sampled one index would make every Slot + // equal, so `modify` would stop agreeing with the instance's own `map`. + val T = summon[Representable[Tri]] + val gt: Optic[Tri[Int], Tri[Int], Int, Int, MultiFocus[Function1[Slot, *]]] = + MultiFocus.representable[Tri, Int](using T) + val tri = Tri(1, 2, 3) // S0 -> 3, S1 -> 1, S2 -> 2 + val triRead = (gt.at(Slot.S0)(tri) === 3) + .and(gt.at(Slot.S1)(tri) === 1) + .and(gt.at(Slot.S2)(tri) === 2) + val triWrite = (gt.modify(_ * 10)(tri) === triFunctor.map(tri)(_ * 10)) + .and(gt.replace(0)(tri) === T.tabulate(_ => 0)) + val triOk = triRead.and(triWrite) + + modOk.and(replOk).and(readOk).and(triOk) } // covers: MultiFocus.tuple .andThen MultiFocus.tuple — same-carrier composition exercises diff --git a/mima.sbt b/mima.sbt index 0e89ad1a..e03eebc3 100644 --- a/mima.sbt +++ b/mima.sbt @@ -6,6 +6,12 @@ // cats-eo-avro has no published baseline anyway. Breaking-change // history, newest first: // +// 0.19: core `MultiFocus.representableAt` removed — the `repr0` argument +// never reached the built optic (the factory tabulates pointwise, so +// the index is a read-time argument, not a property of the optic), +// and `MultiFocus.representable` already built the identical carrier. +// Read a position with the `.at(i)` extension instead (source- and +// binary-breaking for direct callers; the replacement is one line). // 0.17: (a) jsoniter's string-path constructors keep the failure in the type // — `JsoniterPrism.fromPath` / `JsoniterTraversal.fromPath` return // `Either[String, _]`, the throwing `fromPath` is gone, and diff --git a/site/docs/multifocus.md b/site/docs/multifocus.md index 8effe917..23e6275b 100644 --- a/site/docs/multifocus.md +++ b/site/docs/multifocus.md @@ -28,7 +28,7 @@ multi-focus job: |-----------|-----|---------------| | **`AlgLens[F]`** | `F: Functor / Foldable / Traverse` | Algebraic ("classifier") lenses — a focus computed as a fold / classification over the structure, broadcast back on write. | | **Kaleidoscope** | `F: Apply` | Aggregating reads and batch-relative rewrites — `.collectWith` / `.collectMap` / `.collectList`. | -| **Grate** | `Function1[X0, *]` | Uniform rewrite across a fixed shape — homogeneous tuples and Naperian / representable containers (`MultiFocus.tuple` / `representable` / `representableAt`). | +| **Grate** | `Function1[X0, *]` | Uniform rewrite across a fixed shape — homogeneous tuples and Naperian / representable containers (`MultiFocus.tuple` / `representable`); reads land on a position with `.at(i)`. | | **PowerSeries** | `PSVec` | Element-wise traversal of a collection with downstream `.andThen` composition — the `Traversal.each` carrier; carries the hand-tuned `mfAssocPSVec` fast paths (`MultiFocusSingleton` for Lens morphs, `MultiFocusPSMaybeHit` for Prism / Optional). | | **`FixedTraversal[N]`** | `PSVec` | Fixed-arity traversal — the `Traversal.{two,three,four}` factories tabulate their known arity into the PowerSeries carrier, so they compose like `each`. | @@ -395,10 +395,15 @@ on the worktree branch that landed it: See [`docs/research/2026-04-28-multifocus-unification.md`](https://github.com/Constructive-Programming/eo/blob/main/docs/research/2026-04-28-multifocus-unification.md). - **Grate fold** — the lead-position field's empirical dead-code deletion; +20% perf on `Grate.modify`; absorbed factories ship - as `MultiFocus.representable` / `MultiFocus.representableAt` / - `MultiFocus.tuple`. The spike's research doc was lost in + as `MultiFocus.representable` / `MultiFocus.tuple`. The spike's research doc was lost in consolidation; the surviving evidence is the carrier-doc comment in `MultiFocus.scala` and the absorbed factory code. + The field's last remnant — the `repr0` parameter of the + `representable` variant that shipped alongside (v1's `Grate.at`, + later `MultiFocus.representableAt`) — was retired once + `.at(i)` made position a read-time argument: it never reached + the built optic, so two calls with different indices were the + same optic. See the changelog. - **PowerSeries fold** — `Snd[A]` match-type vestige eliminated; `mfAssocPSVec` preserves the parallel-array `AssocSndZ` representation and both `MultiFocusSingleton` / @@ -450,9 +455,6 @@ def fromOptionalF[F: MonoidK, S, T, A, B]( def representable[F: Representable, A] : Optic[F[A], F[A], A, A, MultiFocus[Function1[F.Representation, *]]] -def representableAt[F, A](F: Representable[F])(repr0: F.Representation) - : Optic[F[A], F[A], A, A, MultiFocus[Function1[F.Representation, *]]] - // Absorbed-Grate.tuple — F = Function1[Int, *] def tuple[T <: Tuple, A](using ValueOf[Tuple.Size[T]], Tuple.Union[T] <:< A) : Optic[T, T, A, A, MultiFocus[Function1[Int, *]]] diff --git a/site/docs/optics.md b/site/docs/optics.md index c83238ba..6054e655 100644 --- a/site/docs/optics.md +++ b/site/docs/optics.md @@ -398,10 +398,11 @@ ownerAllPhonesMobile.modify(!_)(Owner(List( `MultiFocus[Function1[X0, *]]` — a uniform rewrite across a fixed shape: homogeneous tuples and Naperian / representable containers, where every position is rebuilt the same way. The factories are -`MultiFocus.tuple[T <: Tuple, A]` (homogeneous-tuple uniform rewrite), -`MultiFocus.representable[F: Representable, A]` (arbitrary Naperian -rebuild), and `MultiFocus.representableAt` (representative-index -variant). See [MultiFocus reference](multifocus.md) and +`MultiFocus.tuple[T <: Tuple, A]` (homogeneous-tuple uniform rewrite) +and `MultiFocus.representable[F: Representable, A]` (arbitrary +Naperian rebuild); a read lands on a chosen position with the +`.at(i)` extension, which takes the `Representable` index per call. +See [MultiFocus reference](multifocus.md) and [Cookbook → Recipe A](cookbook.md) for a worked example. `MultiFocus.zipWith(fa, fb)(f)` (and its pairing form `zip`) is the diff --git a/tests/src/test/scala/dev/constructive/eo/UnlawfulFixturesSpec.scala b/tests/src/test/scala/dev/constructive/eo/UnlawfulFixturesSpec.scala index f7adb461..4f20c59c 100644 --- a/tests/src/test/scala/dev/constructive/eo/UnlawfulFixturesSpec.scala +++ b/tests/src/test/scala/dev/constructive/eo/UnlawfulFixturesSpec.scala @@ -1,11 +1,13 @@ package dev.constructive.eo +import cats.{Functor, Representable} +import cats.instances.function.given import org.specs2.mutable.Specification -import optics.{AffineFold, PickFold} -import data.ModifyF +import optics.{AffineFold, Optic, PickFold} +import data.{ModifyF, MultiFocus} import forgetful.ForgetfulFunctor -import laws.AffineFoldLaws +import laws.{AffineFoldLaws, MultiFocusLaws} import laws.data.ModifyFLaws /** Negative fixtures: deliberately UNLAWFUL instances asserted to FAIL specific laws. @@ -19,6 +21,11 @@ import laws.data.ModifyFLaws * fails this spec. * * See site/docs/quality-assurance.md ("Mutation testing" caveats) for the full story. + * + * The last fixture is a *semantic* one rather than a mutation-killing one: it pins the rebuild + * shape a retired API parameter could only have expressed (sample ONE written focus at a lead + * index, rebuild every position from it), so the surviving pointwise-tabulation property is known + * to discriminate that shape rather than merely to pass. */ class UnlawfulFixturesSpec extends Specification: @@ -66,3 +73,52 @@ class UnlawfulFixturesSpec extends Specification: val laws = new ModifyFLaws[(Int, String), Int] {} laws.functorComposition(1, _.length, _ + 1, _ + 1, "abc") must beFalse } + + "MultiFocusLaws.modifyIdentity rejects a lead-sampling rebuild" >> { + // covers: laws/MultiFocusLaws.scala modifyIdentity against a `from` that samples ONE written + // focus at a chosen "lead" index and rebuilds every position from it — the only semantics the + // retired `MultiFocus.representableAt(F)(repr0)` parameter could have had. A lead rebuild + // collapses the container to a constant, so the law must return false for it and true for + // the shipped pointwise `MultiFocus.representable` on the same carrier, same input. + // + // The carrier's `S` is a STRUCTURAL container here, not `Function1[X0, *]`: the law + // compares with `==`, which is reference equality on a function, so a function-typed `S` + // would fail this law whatever the rebuild shape (that is also why the MF discipline + // fixtures register List / ZipList / Const rather than the Grate carrier). + case class Dup[A](a: A, b: A) + given dupFunctor: Functor[Dup] with + def map[A, B](fa: Dup[A])(f: A => B): Dup[B] = Dup(f(fa.a), f(fa.b)) + given dupRepresentable: Representable.Aux[Dup, Boolean] = new Representable[Dup]: + type Representation = Boolean + def F: Functor[Dup] = dupFunctor + def index[A](fa: Dup[A]): Boolean => A = if _ then fa.a else fa.b + def tabulate[A](f: Boolean => A): Dup[A] = Dup(f(true), f(false)) + + val leadSampling: Optic[Dup[Int], Dup[Int], Int, Int, MultiFocus[Function1[Boolean, *]]] = + new Optic[Dup[Int], Dup[Int], Int, Int, MultiFocus[Function1[Boolean, *]]]: + type X = Unit + def to(fa: Dup[Int]): MultiFocus[Function1[Boolean, *]][X, Int] = + MultiFocus((), dupRepresentable.index(fa)) + def from(b: MultiFocus[Function1[Boolean, *]][X, Int]): Dup[Int] = + val lead: Boolean = true // the "representative index"; sampled for the whole rebuild + Dup(b.foci(lead), b.foci(lead)) + val lawful: Optic[Dup[Int], Dup[Int], Int, Int, MultiFocus[Function1[Boolean, *]]] = + MultiFocus.representable[Dup, Int] + + // Named outside the law body so the instance's own `functor` member cannot take part in its + // own initialisation. + val boolFnFunctor: Functor[Function1[Boolean, *]] = summon[Functor[Function1[Boolean, *]]] + def lawsFor( + o: Optic[Dup[Int], Dup[Int], Int, Int, MultiFocus[Function1[Boolean, *]]] + ) = new MultiFocusLaws[Dup[Int], Int, Function1[Boolean, *]]: + given functor: Functor[Function1[Boolean, *]] = boolFnFunctor + def multiFocus = o + + (lawsFor(leadSampling).modifyIdentity(Dup(1, 2)) must beFalse) + .and(lawsFor(lawful).modifyIdentity(Dup(1, 2)) must beTrue) + // The pin `MultiFocusFunction1Spec` holds the shipped factory to — that a write agrees + // with the carrier instance's own `map` — discriminates the same shape. + .and( + (leadSampling.modify(_ * 10)(Dup(1, 2)) == dupFunctor.map(Dup(1, 2))(_ * 10)) must beFalse + ) + } From 47f06960d363786dad3a25b4f543c738730157eb Mon Sep 17 00:00:00 2001 From: Rodolfo Hansen Date: Thu, 1 Oct 2026 00:28:14 +0200 Subject: [PATCH 2/2] style(test): order the new cats imports per scalafix OrganizeImports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's `sbt scalafixAll --check` step flagged both specs: with the brace form `cats.{Functor, Representable}` the ASCII sort puts `cats.instances.function.given` first (the pre-change `cats.Representable` sorted before it, which is why the single-name import read fine). Applied with `sbt scalafixAll`; no semantic change. Verified on JDK 25: the CI gate set locally — `scalafmtCheckAll scalafmtSbtCheck benchmarks/scalafmtCheck`, `scalafixAll --check`, `githubWorkflowCheck`, `mimaReportBinaryIssues` — plus the root-aggregate `sbt test` (all modules 0 failures) and the two focused specs (5 examples / 313 expectations, 4 examples / 6 expectations). --- .../scala/dev/constructive/eo/MultiFocusFunction1Spec.scala | 2 +- .../test/scala/dev/constructive/eo/UnlawfulFixturesSpec.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/test/scala/dev/constructive/eo/MultiFocusFunction1Spec.scala b/core/src/test/scala/dev/constructive/eo/MultiFocusFunction1Spec.scala index 9bcd69b1..bf6ef0b6 100644 --- a/core/src/test/scala/dev/constructive/eo/MultiFocusFunction1Spec.scala +++ b/core/src/test/scala/dev/constructive/eo/MultiFocusFunction1Spec.scala @@ -2,8 +2,8 @@ package dev.constructive.eo import scala.language.implicitConversions -import cats.{Functor, Representable} import cats.instances.function.given +import cats.{Functor, Representable} import dev.constructive.eo.compose.* import org.scalacheck.Prop.forAll import org.specs2.ScalaCheck diff --git a/tests/src/test/scala/dev/constructive/eo/UnlawfulFixturesSpec.scala b/tests/src/test/scala/dev/constructive/eo/UnlawfulFixturesSpec.scala index 4f20c59c..331ad664 100644 --- a/tests/src/test/scala/dev/constructive/eo/UnlawfulFixturesSpec.scala +++ b/tests/src/test/scala/dev/constructive/eo/UnlawfulFixturesSpec.scala @@ -1,7 +1,7 @@ package dev.constructive.eo -import cats.{Functor, Representable} import cats.instances.function.given +import cats.{Functor, Representable} import org.specs2.mutable.Specification import optics.{AffineFold, Optic, PickFold}