Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/bench-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ jobs:
distribution: temurin
java-version: 25 # kyoIntegration (KyoDiBench dep) targets JDK 25 since v0.15.0
- name: Cache coursier + ivy + sbt boot
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/bench-sweep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ jobs:
distribution: temurin
java-version: 25 # kyoIntegration (KyoDiBench dep) targets JDK 25 since v0.15.0
- name: Cache coursier + ivy + sbt boot
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
distribution: temurin
java-version: 21
- name: Cache coursier + ivy + sbt boot
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down
24 changes: 12 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-25
if: matrix.java == 'temurin@25'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand All @@ -76,7 +76,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-17
if: matrix.java == 'temurin@17'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand All @@ -100,7 +100,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-21
if: matrix.java == 'temurin@21'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down Expand Up @@ -177,7 +177,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-25
if: matrix.java == 'temurin@25'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand All @@ -201,7 +201,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-17
if: matrix.java == 'temurin@17'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand All @@ -225,7 +225,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-21
if: matrix.java == 'temurin@21'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down Expand Up @@ -299,7 +299,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-25
if: matrix.java == 'temurin@25'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand All @@ -323,7 +323,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-17
if: matrix.java == 'temurin@17'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand All @@ -347,7 +347,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-21
if: matrix.java == 'temurin@21'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down Expand Up @@ -393,7 +393,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-25
if: matrix.java == 'temurin@25'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand All @@ -417,7 +417,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-17
if: matrix.java == 'temurin@17'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand All @@ -441,7 +441,7 @@ jobs:
- name: Cache coursier + ivy + sbt boot
id: coursier-cache-temurin-21
if: matrix.java == 'temurin@21'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
distribution: temurin
java-version: 25
- name: Cache coursier + ivy + sbt boot
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
distribution: temurin
java-version: 25
- name: Cache coursier + ivy + sbt boot
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down Expand Up @@ -97,7 +97,7 @@ jobs:
distribution: temurin
java-version: 25
- name: Cache coursier + ivy + sbt boot
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/coursier
Expand Down
24 changes: 12 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
## Project

`cats-eo` — an Existential Optics library for Scala 3, built on top of
[cats](https://typelevel.org/cats/). Scala `3.8.3` via sbt `1.12.9`
(`project/build.properties`), runs on JDK 17, 21, or 25 — but the `kyo` module (and therefore the docs site) needs JDK 25; on older JVMs it drops out of the root aggregate.
[cats](https://typelevel.org/cats/). Scala `3.9.0` (the `scala3Version` pin in
`build.sbt`) via sbt `1.13.0` (`project/build.properties`), runs on JDK 17, 21, or 25 — but the `kyo` module (and therefore the docs site) needs JDK 25; on older JVMs it drops out of the root aggregate.

Human contributors: see [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the
day-one bootstrap. This file is the parallel guide for AI agents.
Expand Down Expand Up @@ -44,7 +44,7 @@ curl -fLo /usr/local/bin/cs \
https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz \
&& gunzip -f /usr/local/bin/cs && chmod +x /usr/local/bin/cs

# A recent JVM (sbt 1.12 supports JDK 17 and JDK 21)
# A recent JVM (the project runs on JDK 17, 21, or 25; `kyo` needs 25)
cs java --jvm temurin:21 --setup # writes JAVA_HOME into ~/.profile

# Scala dev tools
Expand All @@ -54,10 +54,10 @@ cs install --install-dir /usr/local/bin sbt scala scalafmt scalafix metals metal
After `cs java --setup`, open a fresh shell (or `source ~/.profile`) so
`JAVA_HOME` is on your PATH.

Installed versions in this environment: `sbt 1.12.9`, `scala-runner 1.12.4`
(Scala `3.8.3` by default, matching the project), `scalafmt 3.11.0` (honours
the `version` pin in `.scalafmt.conf`), `scalafix 0.14.6`, `metals 1.6.7`,
`metals-mcp 1.6.7`.
Installed versions in this environment: `sbt 1.12.9` (the launcher script — it
boots the sbt version pinned in `project/build.properties`), `scala-runner
1.12.4`, `scalafmt 3.11.0` (honours the `version` pin in `.scalafmt.conf`),
`scalafix 0.14.6`, `metals 1.6.7`, `metals-mcp 1.6.7`.

### Day-to-day commands

Expand Down Expand Up @@ -136,12 +136,12 @@ SBT_OPTS="-Xmx6g" sbt mutationAll

Key facts, all the hard-won kind:

- **Invoke as `project <m>; stryker`, NOT `<m>/stryker`.** The
module-scoped task form reads `loadedTestFrameworks` from the
aggregating root project (no test deps), so specs2 is invisible and
*every* mutant comes back `NoCoverage`. The `mutationAll` alias uses the
- **Invoke as `project <m>; stryker`** — the `mutationAll` alias uses the
project-switch form across core, laws, generics, schemes, schemesLaws,
circe, avro, jsoniter.
circe, avro, jsoniter, zio, kyo. The module-scoped `<m>/stryker` form works
again since 0.20.4; before that it resolved `loadedTestFrameworks` from the
aggregating root project (no test deps), so specs2 was invisible and
*every* mutant came back `NoCoverage`.
- **It's a report, not a gate** (`strykerThresholdsBreak := 0`): a low
score never fails the build.
- **`core` and `laws` are scored against the `tests/` suite via
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ canonical reference and stays current with what CI runs.

In short, you need:

- JDK 17 or JDK 21 (Temurin is fine).
- `sbt` 1.12.x.
- Scala 3.8.x (matches `project/build.properties`).
- JDK 17 or JDK 21 (Temurin is fine) — JDK 25 for the `kyo` module and the docs site.
- `sbt` 1.13.x (the launcher boots the version pinned in `project/build.properties`).
- Scala 3.9.x (pinned as `scala3Version` in `build.sbt`).
- `scalafmt` 3.11.x (honours the pin in `.scalafmt.conf`).

Clone the repo, then optionally enable the project git hooks so your
Expand Down
17 changes: 15 additions & 2 deletions avro/src/main/scala/dev/constructive/eo/avro/AvroWalk.scala
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,16 @@ private[avro] object AvroWalk:
out(i) = idx
loop(i + 1, t, index)
else
val here = if index != null then index else normalisedNameIndex(record)
// A `match` on the null sentinel rather than `if index != null`: stryker4s's
// `ConditionalExpression` mutant replaces an `if` condition with `true`/`false`, which
// would drop the flow narrowing and leave `index` typed `JMap[String, Integer] | Null`,
// so `here.get(...)` stops compiling and the mutant dies as a compile error instead of
// being exercised (issue #115). A match has no condition to replace. Writing the guard
// as `if … then index.nn` fixes the mutant too, but the guard already narrows `index`,
// so the compiler reports E216 "Unnecessary .nn" and `-Werror` rejects it.
val here: JMap[String, Integer] = index match
case null => normalisedNameIndex(record)
case m => m
val hit = here.get(normalisedName(n))
// A key present with `Ambiguous` is the same verdict the scan produced on its SECOND
// hit: more than one schema field normalises to this name, so the signal is no signal.
Expand Down Expand Up @@ -646,7 +655,11 @@ private[avro] object AvroWalk:
val resolved: Integer | Null =
if exact != null then Integer.valueOf(exact.pos)
else
val here = if index != null then index else normalisedNameIndex(record)
// Same explicit-nulls / stryker `ConditionalExpression` hazard as in [[totalNominalIndex]]:
// a `match` keeps the null-narrowing unmutatable and the non-null type explicit.
val here: JMap[String, Integer] = index match
case null => normalisedNameIndex(record)
case m => m
here.get(normalisedName(n))
resolved match
case null =>
Expand Down
12 changes: 6 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ ThisBuild / githubWorkflowJobSetup ~= { steps =>
case s: WorkflowStep.Use if s.id.exists(_.startsWith("setup-java-")) =>
val javaId = s.id.get.stripPrefix("setup-java-")
val cacheStep = WorkflowStep.Use(
UseRef.Public("actions", "cache", "v4"),
UseRef.Public("actions", "cache", "v6"),
params = Map(
"path" -> Seq(
"~/.cache/coursier",
Expand Down Expand Up @@ -356,7 +356,7 @@ lazy val scalacheck = ScalaCheckOrg %% "scalacheck" % "1.20.0"
lazy val monocle = Optics %% "monocle-core" % "3.3.0"
// droste — the recursion-scheme baseline for the schemes benchmarks (pattern
// functor + Fix encoding). Benchmark-only; never a published dependency.
lazy val drosteCore = "io.higherkindness" %% "droste-core" % "0.9.0-M3"
lazy val drosteCore = "io.higherkindness" %% "droste-core" % "0.10.0"
// kindlings 0.3.x (all three) ship a configurable macro-expansion timeout
// (`DerivationTimeout`, default 5s) and pull hearth 0.4.2 + kindlings-derivation-commons.
// We raise it to 30s via `-Xmacro-settings:{circe,cats,avro}Derivation.timeout=30s`
Expand Down Expand Up @@ -1176,10 +1176,10 @@ addCommandAlias(
)

// Mutation testing across the published modules (tests/benchmarks/docs
// aren't published). Uses the `project <m>; stryker` form, NOT
// `<m>/stryker`: see the plugins.sbt note — the module-scoped task reads
// `loadedTestFrameworks` from the empty root project and marks every
// mutant NoCoverage.
// aren't published). Uses the `project <m>; stryker` form — the alias
// switches projects anyway, and the borrowed-tests `set` lines below are
// expressed against those switches. (The `<m>/stryker` form works too
// since 0.20.4; see the plugins.sbt note.)
//
// The `set` lines borrow the `tests` module's compiled suite into core's
// and laws' Test scopes so their mutants get killed by the behavioural
Expand Down
23 changes: 15 additions & 8 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8")
// at release rather than as a per-PR gate (see `mutationAll` in
// build.sbt and site/docs/quality-assurance.md).
// Stryker runs each module's OWN `Test / test` against that module's
// mutants. NB: invoke it as `project <module>; stryker`, NOT
// `<module>/stryker` — the module-scoped task form resolves
// `loadedTestFrameworks` from the aggregating root project (which has no
// test deps), so every mutant comes back NoCoverage; switching the
// current project first makes specs2 visible. 0.20.x auto-derives the
// Scala 3 dialect from scalaVersion.
// 0.20.4 fixed multi-module invocation; the `project <m>; stryker` workaround still works.
addSbtPlugin("io.stryker-mutator" % "sbt-stryker4s" % "0.20.4")
// mutants. This build invokes it as `project <module>; stryker` (see
// `mutationAll` in build.sbt and the quality.yml release sweep).
// Upstream fixed the multi-module invocation in 0.20.4 by scoping the
// task to the invoking project, so `<module>/stryker` is correct too on
// 1.x; the current-project form is kept because the alias switches
// projects anyway and the borrowed-tests `set` lines are expressed with
// `project <m>;` as well. Before 0.20.4 the module-scoped form resolved
// `loadedTestFrameworks` from the aggregating root project (no test
// deps), so every mutant came back NoCoverage.
// The Scala 3 dialect is still auto-derived from scalaVersion. Bumped
// 0.20.4 → 1.1.1: v1 has no removed or renamed features, the sbt
// setting keys used in build.sbt (`strykerReporters`,
// `strykerExcludedMutations`, `strykerThresholdsBreak`) are unchanged,
// and 1.x requires sbt >= 1.11.2 (we run 1.13.0).
addSbtPlugin("io.stryker-mutator" % "sbt-stryker4s" % "1.1.1")

// Format check gate for CI (`sbt scalafmtCheckAll scalafmtSbtCheck`
// in the workflow). The project ships a `.scalafmt.conf` pinned to
Expand Down
7 changes: 4 additions & 3 deletions site/docs/quality-assurance.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ no change to the suite. Do not read such a delta as a regression.
```sh
# Statement / branch coverage (cross-module aggregate):
SBT_OPTS="-Xmx6g" sbt coverageAll
# → target/scala-3.8.3/scoverage-report/ (HTML + scoverage.xml)
# → target/scala-3.9.0/scoverage-report/ (HTML + scoverage.xml)

# Mutation testing across the runtime-logic modules:
SBT_OPTS="-Xmx6g" sbt mutationAll
Expand All @@ -400,8 +400,9 @@ python3 site/tools/gen-qa-report.py --check # CI: non-zero if stale
Both aliases relax the always-on `-Werror` (`tlFatalWarnings`) first, since
instrumented sources can surface `-Wunused` warnings; the larger heap is
because the `set` reapply re-evaluates the Laika docs settings. Mutation runs
with `project <m>; stryker` (not `<m>/stryker`) so specs2 is visible to the
test runner — see
use `project <m>; stryker`; the module-scoped `<m>/stryker` form works too
(upstream fixed it in 0.20.4 — before that it resolved `loadedTestFrameworks`
from the aggregating root project and marked every mutant `NoCoverage`) — see
[`project/plugins.sbt`](https://github.com/Constructive-Programming/eo/blob/main/project/plugins.sbt).

The [`quality.yml`](https://github.com/Constructive-Programming/eo/blob/main/.github/workflows/quality.yml)
Expand Down