From 058c5f991225e96aeacc8d94b3baa91b7a435a15 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 22:32:15 +0000 Subject: [PATCH] release: v1.0.0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014bZeDEJcuNRdhY6SZ58mrZ --- CHANGELOG.md | 67 +++++++++++++++++++++++++++- README.md | 19 ++++---- build.sbt | 12 ++++- mima.sbt | 82 ++++++++++++++++++----------------- project/plugins.sbt | 4 +- site/docs/getting-started.md | 2 +- site/docs/integrations/kyo.md | 12 ++++- 7 files changed, 142 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6957ef51..8ad3033d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0] - 2026-10-25 + +**Binary-breaking: recompile against this release.** 1.0.0 changes public shapes that 0.18.x +call sites were compiled against: `Optional` is a sealed abstract class whose `getOrModify` / +`reverseGet` are methods (no longer constructor `val`s), `Optic.all` returns `List[A]`, and the +legacy MultiFocus Grate API is removed (all under Changed and Removed). eo's `inline` methods +splice their bodies into YOUR bytecode, so a jar built against 0.18.x keeps calling the old +members and fails at runtime (`NoSuchMethodError` and friends) against 1.0.0, even where the +source still compiles unchanged. Rebuild everything that depends on cats-eo. 1.0.0 is also built +with Scala 3.9.0, whose TASTy (28.9) older compilers cannot read: **downstream builds need Scala +3.9.x or later.** + +**What 1.0.0 means: binary compatibility from here on.** MiMa now checks every published module +on every build, so each later 1.x release stays binary-compatible with 1.0.0; breaking changes +wait for 2.0. One exception: **`cats-eo-kyo` is experimental.** It builds on kyo `1.0.0-RC7`, a +release candidate, so it is still published but excluded from the MiMa check and may change in +any 1.x release; the exclusion lifts once kyo 1.0.0 final ships (see the +[Kyo integration](https://eo.constructive.dev/integrations/kyo.html) page). The 0.x releases made +no compatibility promise. + ### Added - **iron: new `cats-eo-iron` module** — [Iron](https://github.com/Iltotore/iron) refined types as @@ -70,9 +90,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `JsonSlice(chunk.toByteBuffer)`, `Wire.detail` is `JsoniterPrism.inPlace[Issue].field(_.detail).raw` with `DetailJson` an opaque `JsonSlice`, the leaf prisms are in place too, and `joinFrames` takes `Chunk[Byte]`. +- **laws: multi-shot `ForgetfulTraverse` laws and D2 `modifyA[List]` (#145).** Every + `ForgetfulTraverse` / `modifyA` law used to run under a single-shot effect (`Id`, `Const`), so + none could notice a carrier that shares state across an effect's continuations (the PSVec bug + under Fixed). `ForgetfulTraverseLaws` gains sequential composition (`Nested[List, Option]`), + parallel composition (`Tuple2K[List, Vector]`) and `traverseMultiShotAgreesWithFoci` + (`traverse[List]` agrees with cats' `Traverse[List]` over the carrier's foci), in a new + `forgetfulTraverseMultiShot` rule set (its parent, `forgetfulTraverse`, is unchanged). + `ModifyAListLaws` / `ModifyAListTests` (D2) state `modifyA[List] ==` cats' cartesian traverse + over the foci. Multi-shot results grow as `2^n` in the foci, so their generators draw at + ScalaCheck size 8. Additive; downstream suites opt in by registering the new rule sets. +- **docs: [Streaming with fs2](https://eo.constructive.dev/cookbook/streaming-fs2.html) cookbook + page (#142).** Why there is no `cats-eo-fs2` module: the pipes it would ship are one-liners + over fs2's own combinators. Keeping prism matches (written against `CanGetOption`), effectful + modify with `evalMap` + `modifyA` (and why `Stream` as the `Applicative` gives a cartesian + product), routing on a prism's `Either` with `broadcastThrough`, and a `zoomPipe` that runs a + `Pipe` on one field (correct only for one-to-one, in-order pipes). No published module depends + on fs2. +- **demos: two worked programs built from eo optics, each with a cookbook page.** + [Blockchain smart contract compilation](https://eo.constructive.dev/cookbook/uplc-jvm.html) + (#132) compiles UPLC (Cardano Plutus Core) to JVM bytecode as a chain of lawful prisms and an + iso, checked differentially against the Scalus CEK machine. + [Allocation-free stream triage](https://eo.constructive.dev/cookbook/grpc-filter.html) (#133) + filters a gRPC stream of JSON messages read as raw bytes with eo-jsoniter prisms, without + decoding them. Demos are not published. ### Changed +- **`cats-eo-kyo` is experimental.** It is published as before, but outside the 1.x + binary-compatibility promise while it builds on kyo `1.0.0-RC7` (see the lead paragraph). +- **Toolchain and dependencies (#122, #149).** Built with Scala 3.9.0 (was 3.8.4; downstream + needs 3.9.x, see above), which kyo `1.0.0-RC7` requires. Runtime dependency bumps: + jsoniter-scala `2.38.17` → `2.41.2`, kyo-prelude / kyo-schema `1.0.0-RC6` → `1.0.0-RC7`, zio + `2.1.24` → `2.1.26`, zio-prelude `1.0.0-RC41` → `1.0.0-RC48`, vulcan (`Optional`) `1.13.0` → + `1.14.1`, and scalacheck `1.19.0` → `1.20.0` in `cats-eo-laws`. + - **core: `Optional` tears its source once per write** (breaking). `Optional` is now a sealed abstract class with an existential intermediate `type M` (`X = (T, M)`): a hit keeps what the read computed, and the write rebuilds from it (`rebuild(m, b)`) instead of re-running the @@ -123,6 +175,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **core: `PSVec`'s `Traverse` no longer shares one result array across `G`'s continuations + (#145).** `PSVec.pSVecTraverse.traverse` seeded `G.pure(new Array[Any](n))` and wrote into + that one array from every `G.map2` continuation. Any `Applicative` that resumes a continuation + more than once (`List`, `Vector`, `LazyList`, `Chain`, an fs2 `Stream`, a product effect such as + `Tuple2K`, a re-run `State` or `IO` value) handed every branch the same array, so the last + writer won: `Traversal.each[List, Int].modifyA[List](a => List(a, -a))(List(1, 2, 3))` gave 8 + copies of `List(-1, -2, -3)`. Results still land in one pre-sized array, now allocated inside + the first element's continuation (fresh per branch and per run) with a claim frontier: the + first writer at an index writes in place, any later one copies the prefix into its own buffer. + Single-shot effects cost the same as before; multi-shot effects pay one prefix copy per extra + branch. Every `modifyA` over a PSVec-backed traversal (`Traversal.each`, `MultiFocus[PSVec]`) + was affected. The new multi-shot laws under Added pin it. - **core: `MultiFocus[List].collectList` preserves observed context.** It passes a singleton focus vector to reconstruction instead of an invented leftover. This is not a guarantee of a singleton reconstructed @@ -1145,7 +1209,8 @@ JsonTraversal×Review corner). See: [`docs/research/2026-04-23-composition-gap-analysis.md`](docs/research/2026-04-23-composition-gap-analysis.md) §7 (and the per-cell ledger in §1.1 / §3 / §4). -[Unreleased]: https://github.com/Constructive-Programming/eo/compare/v0.18.0...HEAD +[Unreleased]: https://github.com/Constructive-Programming/eo/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/Constructive-Programming/eo/compare/v0.18.0...v1.0.0 [0.18.0]: https://github.com/Constructive-Programming/eo/compare/v0.17.0...v0.18.0 [0.17.0]: https://github.com/Constructive-Programming/eo/compare/v0.16.0...v0.17.0 [0.16.0]: https://github.com/Constructive-Programming/eo/compare/v0.15.1...v0.16.0 diff --git a/README.md b/README.md index 2b5c9d80..d94aa823 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,11 @@ libraryDependencies += "dev.constructive" %% "cats-eo-avro" % "0.18.0" libraryDependencies += "dev.constructive" %% "cats-eo-jsoniter" % "0.18.0" libraryDependencies += "dev.constructive" %% "cats-eo-iron" % "0.18.0" libraryDependencies += "dev.constructive" %% "cats-eo-zio" % "0.18.0" -libraryDependencies += "dev.constructive" %% "cats-eo-kyo" % "0.18.0" // JDK 25 +libraryDependencies += "dev.constructive" %% "cats-eo-kyo" % "0.18.0" // JDK 25; experimental libraryDependencies += "dev.constructive" %% "cats-eo-chimney" % "0.18.0" ``` -Requires Scala 3.8.x on JDK 17, 21, or 25 — except `cats-eo-kyo`, +Requires Scala 3.9.x on JDK 17, 21, or 25 — except `cats-eo-kyo`, which requires JDK 25 (kyo 1.0.0-RC5+ ships Java-25-only bytecode). ## 60-second tour @@ -139,7 +139,8 @@ personStreet.modify(_.toUpperCase)(alice) // address.street := "MAIN ST" `Record.iso` / `Record.lens` for kyo Records, and the optional kyo-schema bridge: `Focus` → optics, codec byte-face prisms, and optics over the untyped `Structure.Value` tree (`cats-eo-kyo`, - JDK 25). + JDK 25). **Experimental**: built on kyo 1.0.0-RC7, so it is outside + the 1.x binary-compatibility promise until kyo 1.0.0 final ships. - [chimney integration](https://eo.constructive.dev/integrations/chimney.html) — chimney's derived conversions as optics: `Transformer` → `Getter`, `Iso` → `Iso`, `Codec` → prism, `PartialTransformer` → a fold into @@ -174,11 +175,13 @@ for cats typeclasses. ## Project status -`0.1.0` is the first public release. Binary compatibility is guaranteed -within the `0.1.x` series via MiMa starting at `0.1.1` (the `0.1.0` -publish has no prior version to compare against). Pre-`1.0`, expect -surface refinements as we learn from real users; each non-bugfix release -will list breaking changes in [`CHANGELOG.md`](./CHANGELOG.md). +`1.0.0` is the first stable release. From here on, binary compatibility +is checked by MiMa across the whole `1.x` series: every `1.x` release is +binary-compatible with `1.0.0`. The exception is `cats-eo-kyo`, which is +**experimental** and outside that check while it builds on a kyo release +candidate; the exclusion lifts once kyo 1.0.0 final ships. The `0.x` +releases made no compatibility promise. Every release lists its changes +in [`CHANGELOG.md`](./CHANGELOG.md). ## License diff --git a/build.sbt b/build.sbt index a0ce74ad..3314c54a 100644 --- a/build.sbt +++ b/build.sbt @@ -7,8 +7,9 @@ val scala3Version = "3.9.0" // fields below are ThisBuild-scoped so every published sub-module // inherits them. -// Breaking-change history per 0.x minor lives in mima.sbt. -ThisBuild / tlBaseVersion := "0.18" +// 1.x is binary-compatible (MiMa-checked from 1.0.0; see mima.sbt, which +// also keeps the 0.x breaking-change history). +ThisBuild / tlBaseVersion := "1.0" ThisBuild / organization := "dev.constructive" ThisBuild / organizationName := "Constructive" ThisBuild / startYear := Some(2025) @@ -964,6 +965,13 @@ lazy val kyoIntegration: Project = project // kyo 1.0.0-RC5+ is Java-25-only bytecode, so this artifact honestly // targets 25 too (the ThisBuild floor stays 17 for every other module). tlJdkRelease := Some(25), + // EXPERIMENTAL: no binary-compatibility promise. cats-eo-kyo builds on + // kyo 1.0.0-RC7, a release candidate whose own API and bytecode still + // move between RCs, so this module is excluded from the 1.x MiMa check + // (every other published module is checked against 1.0.0). Lift this + // override — let sbt-typelevel compute the previous versions again — + // once kyo 1.0.0 final ships and the module is rebuilt on it. + tlMimaPreviousVersions := Set.empty, libraryDependencies += cats, libraryDependencies += kyoPrelude, libraryDependencies += kyoSchema % Optional, diff --git a/mima.sbt b/mima.sbt index bef332ed..bfced5b7 100644 --- a/mima.sbt +++ b/mima.sbt @@ -1,39 +1,54 @@ // MiMa setup — binary-compatibility checking, wired transitively // through sbt-typelevel-ci-release. // -// MiMa is currently DISABLED across the still-evolving 0.x line: -// every minor so far has been a deliberate breaking release, and -// cats-eo-avro has no published baseline anyway. Breaking-change -// history, newest first: +// MiMa is ON from 1.0.0. There is no `tlMimaPreviousVersions` override +// here: sbt-typelevel's default computes the previous versions from git +// tags — every earlier non-prerelease `vX.Y.Z` tag that early-semver says +// the current version must stay binary-compatible with (same major, and +// major > 0). So 1.0.0 itself compares against nothing (every earlier tag +// is 0.x), and every later 1.x release is checked against 1.0.0 and each +// 1.x release after it. `mimaReportBinaryIssues` runs in CI. // -// Unreleased: `Optional` becomes a sealed abstract class with an +// Exception: `cats-eo-kyo` is EXPERIMENTAL and opts out +// (`kyoIntegration / tlMimaPreviousVersions := Set.empty` in build.sbt): +// it builds on kyo 1.0.0-RC7, a release candidate. Lift that override once +// kyo 1.0.0 final ships. +// +// When a 1.x change trips MiMa, the default answer is: don't make it on +// 1.x. Accompany a genuinely internal change with a justified +// ProblemFilter, e.g. +// +// ThisBuild / mimaBinaryIssueFilters ++= Seq( +// ProblemFilters.exclude[MissingClassProblem]("dev.constructive.eo.internal.Foo"), +// ) +// +// Only exclude symbols that are genuinely internal (`private` or +// `private[pkg]`); exclusions on the public API are binary-compat +// breakage and belong in a major version bump instead. +// +// History. MiMa was disabled across the 0.x line: every 0.x minor was a +// deliberate breaking release, kept here for reference, newest first. +// +// 1.0.0 (breaking vs 0.18; the changes listed for 0.19 before 1.0 was +// chosen, which was never released, ship here): +// (a) `Optional` becomes a sealed abstract class with an // existential intermediate (`type M`, `X = (T, M)`) so every write // tears its source once; `new Optional(...)` is gone (use // `Optional.apply`) and `getOrModify` / `reverseGet` are methods // instead of constructor vals (source-compatible for callers, // binary-breaking). -// Unreleased: remove the legacy MultiFocus Function1 Grate kernel, tuple / -// representable factories, zip / zipWith, and Direct bridge, together -// with RepresentativeIndex. Indexed is the fixed-index replacement; -// composition addresses the full product grid, not the old diagonal. -// Source- and binary-breaking for callers of the removed APIs. Container -// MultiFocus and Traversal APIs are retained. -// -// 0.19: the `Iso → MultiFocus[Function1[X0, *]]` bridge -// (`forgetful2multifocusFunction1`) now requires -// `data.RepresentativeIndex[X0]` — the index its product's own `from` -// reads a bundle at, where a `null` sentinel used to stand in. `Int` / -// `Boolean` / `Unit` / singleton indices resolve off the companion -// (unchanged call sites), any other index type needs a local `given` -// or `RepresentativeIndex.at(v)`, and an uninhabited index type no -// longer bridges (source-breaking for direct references to the given, -// and for chains over an index type with no instance). -// 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). +// (b) the legacy MultiFocus Function1 Grate kernel is removed — +// tuple / representable / representableAt factories, zip / +// zipWith, the Direct bridge (`forgetful2multifocusFunction1`) and +// the short-lived `RepresentativeIndex` witness. Indexed is the +// fixed-index replacement; its composition addresses the full +// product grid, not the old diagonal. Source- and binary-breaking +// for callers of the removed APIs. Container MultiFocus and +// Traversal APIs are retained. +// (c) `Optic.all` returns `List[A]` (every focus) instead of +// `List[F[X, A]]` (binary-breaking descriptor change). +// (d) built with Scala 3.9.0: the TASTy is 28.9, so downstream +// compilers older than 3.9 cannot read it. // 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 @@ -61,16 +76,3 @@ // enclosing path (namespace) — a wire-format break vs 0.5.x. // Earlier: 0.2.0 JsonPrism → Affine `Optional` #31, 0.3.0 avro // field-naming #35, 0.4.0 additive #37/#38, 0.5.0 Confluent surface #41. -// -// Re-enable once the API is stable: set this to the published baseline -// line, and accompany every subsequent binary-breaking change with a -// justified ProblemFilter, e.g. -// -// ThisBuild / mimaBinaryIssueFilters ++= Seq( -// ProblemFilters.exclude[MissingClassProblem]("dev.constructive.eo.internal.Foo"), -// ) -// -// Only exclude symbols that are genuinely internal (`private` or -// `private[pkg]`); exclusions on the public API are binary-compat -// breakage and belong in a version bump instead. -ThisBuild / tlMimaPreviousVersions := Set.empty diff --git a/project/plugins.sbt b/project/plugins.sbt index 96afd0b6..b0e52905 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -35,8 +35,8 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") // signs artifacts with the configured GPG key, and publishes on // `v*` tag push. Brings MiMa in through its transitive // `sbt-typelevel-mima` dependency so binary-compat checks run on -// every CI build from 0.1.1 onward (0.1.0 has no previous version -// to compare against; see `mima.sbt`). +// every CI build across 1.x (1.0.0 has no previous 1.x version to +// compare against; 0.x was unchecked; see `mima.sbt`). addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % "0.8.7") // `sbt-typelevel-settings` contributes the curated scalac flag set diff --git a/site/docs/getting-started.md b/site/docs/getting-started.md index 62fbf0c3..de8b9db1 100644 --- a/site/docs/getting-started.md +++ b/site/docs/getting-started.md @@ -25,7 +25,7 @@ libraryDependencies ++= Seq( ) ``` -Requires Scala 3.8.x on JDK 17 or JDK 21. +Requires Scala 3.9.x on JDK 17, 21, or 25 (`cats-eo-kyo` needs JDK 25). ## Define diff --git a/site/docs/integrations/kyo.md b/site/docs/integrations/kyo.md index c8e1316f..d32219a9 100644 --- a/site/docs/integrations/kyo.md +++ b/site/docs/integrations/kyo.md @@ -29,6 +29,14 @@ applications alike. libraryDependencies += "dev.constructive" %% "cats-eo-kyo" % "@VERSION@" ``` +**Experimental.** `cats-eo-kyo` is published with every release but sits +outside eo's 1.x binary-compatibility promise: it builds on kyo +`1.0.0-RC7`, a release candidate, so its API and bytecode can change in +any 1.x release, and MiMa does not check it. Every other eo module is +MiMa-checked across 1.x. The exclusion lifts once kyo 1.0.0 final ships +and the module is rebuilt on it. It also needs JDK 25 (see +[JDK requirement](#jdk-requirement)). + ## Which direction are you integrating? The module is a two-way adapter; pick the row that matches what you @@ -253,8 +261,8 @@ pull kyo-schema transitively; add it (and a codec artifact, here json) yourself: ```scala -libraryDependencies += "io.getkyo" %% "kyo-schema" % "1.0.0-RC6" -libraryDependencies += "io.getkyo" %% "kyo-schema-json" % "1.0.0-RC6" +libraryDependencies += "io.getkyo" %% "kyo-schema" % "1.0.0-RC7" +libraryDependencies += "io.getkyo" %% "kyo-schema-json" % "1.0.0-RC7" ``` ### Focus bridge