Thanks for your interest in cats-eo. This guide is the day-one starting
point for human contributors. Companion guidance for AI-assisted
contributions lives in CLAUDE.md.
The toolchain — JDK, sbt, Scala, scalafmt, scalafix, metals — is
installed via Coursier. Full bootstrap (one
shell snippet to install all of it) is documented in
CLAUDE.md under "Toolchain"; that section is the
canonical reference and stays current with what CI runs.
In short, you need:
- JDK 17 or JDK 21 (Temurin is fine) — JDK 25 for the
kyomodule and the docs site. sbt1.13.x (the launcher boots the version pinned inproject/build.properties).- Scala 3.9.x (pinned as
scala3Versioninbuild.sbt). scalafmt3.11.x (honours the pin in.scalafmt.conf).
Clone the repo, then optionally enable the project git hooks so your local commits run the same gates CI does:
.githooks/install.shSee CLAUDE.md "Git hooks" for what each hook checks
and how to bypass them when you need to.
Before pushing, run the four commands CI gates on:
sbt scalafmtCheckAll # formatting
sbt compile # full project compile
sbt test # core + tests + generics + circe
sbt 'docs/mdoc; docs/laikaSite' # mdoc-verify every doc fence + render the siteIf you installed the git hooks (above), pre-commit runs the format
check and the docs build automatically when staged files touch
*.scala / *.sbt / *.md / .scalafmt.conf / site/ / build.sbt,
and pre-push runs the test sweep. CI runs the same set on every PR.
- Fork the repo on GitHub.
- Create a topic branch off
main. - Make your change. Keep commits focused; the project follows a
conventional-commits-style prefix (
feat(scope):,fix(scope):,docs(scope):, etc.) —git logshows the pattern. - Run the four local checks above.
- Push your branch and open a PR against
main.
CI gates on the same checks (scalafmtCheckAll, compile, test,
docs build). If any fail, push fixes to the same branch — no need to
re-open the PR.
For substantial design changes, open an issue first so we can sketch the approach before you spend time on it.
The benchmarks/ sub-project is not part of the root aggregate, so
sbt compile and sbt test skip it. Run benchmarks explicitly:
sbt "benchmarks/Jmh/run -i 5 -wi 3 -f 1 -t 1" # full sweep
sbt "benchmarks/Jmh/run -i 1 -wi 1 -f 1 LensBench.eoGet" # single benchFull instructions, the EO-only vs. paired-with-Monocle layout, and JMH
caveats live in CLAUDE.md "Benchmarks (JMH vs Monocle)"
and in benchmarks/README.md.
PRs that touch benchmark-mapped code (core/, avro/, circe/, jsoniter/,
schemes/, generics/, the harness, or the build) automatically get a
Benchmark A/B sticky comment after successful CI: the affected benchmarks
run on the merge-base and on your exact head, on the same VM, at a reduced
JMH profile. The benchmark workflow runs separately on the default branch,
so it adds no long-running check to your PR head and never gates merging.
Read it with the project doctrine in mind — the B/op (allocation) delta
is authoritative; the ns/op delta is directional. A perf:full label
escalates to the full suite; docs/site/tests-only PRs get no comment, and
PRs from forks are excluded (same caveat as the docs-preview workflow).
An ❌ comment means a side failed to build or run — it is never a pass.
Results for closed PRs or superseded heads are not posted. If you add
perf:full after CI completes, rerun with
gh workflow run bench-pr.yml --ref main -f pr=<number>.
Current numbers live in BENCHMARKS.md (generated by
the nightly sweep — never edit it by hand) with per-sweep history charts
on the project's GitHub Pages under /bench/, and each release carries
its jmh-results-<tag>.json as a downloadable asset. Operational details
(calibration, warning thresholds, mapping maintenance):
.github/bench/README.md.
sbt 'docs/mdoc; docs/laikaSite'
# Site output:
# site/target/docs/site/Serve that directory with any static HTTP server, e.g.:
python3 -m http.server -d site/target/docs/site/ 8080mdoc verifies every code fence in site/docs/*.md against the live
library, so a docs change that breaks a fence fails the build.
The project's primary quality signal is statement / branch coverage via
sbt-scoverage. The
canonical command, output paths, and the rationale for relying on local
runs (rather than a CI-uploaded badge) are documented in
CLAUDE.md under "Test-suite quality".
If you add new runtime code, please add the matching tests so coverage doesn't drift. Pure type-level machinery doesn't need coverage and is not counted against the target.
- Update
CHANGELOG.md: cut a## [X.Y.Z] - datesection out of[Unreleased](keep the[Unreleased]placeholder above it). This is load-bearing: the GitHub Release is created from this section, and the release workflow fails if it's missing. - Run
sbt githubWorkflowGenerateand commit anyci.ymldrift — a stale generated workflow has killed a tag build before. Never hand-editci.yml. - Commit with
release: vX.Y.Z. git tag vX.Y.Z && git push origin main vX.Y.Z.- The tag build runs the staged Sonatype Central Portal publish. On
success, two hand-maintained follow-up workflows fire on their own:
update-readme-version.ymlbumps the README's artifact versions onmain, andgithub-release.ymlpublishes the GitHub Release — the tag's CHANGELOG section as the curated headline, with GitHub's auto-generated PR list appended (categorised per.github/release.yml, so label PRs:breaking-change,enhancement,bug,performance,documentation). - Watch the workflow; on success the artifacts appear at
https://repo1.maven.org/maven2/dev/constructive/cats-eo_3/X.Y.Z/.
Release-notes conventions: a binary-breaking release leads its
changelog section with the recompile warning (inline-spliced call
sites fail at runtime against old jars — downstream users must see
this on the release page); link site pages
(https://eo.constructive.dev/…) rather than source files; when
performance is part of the story, include the CI benchmark numbers,
never local ones.
If the release workflow errors mid-staging — signing failure, namespace permission lapse, network blip during upload — recover as follows:
- Sign in at https://central.sonatype.com/publishing/deployments.
- Find the partially-uploaded deployment with status "Failed" or "Pending".
- Click "Drop" to discard it. This clears the staging area. Partially-uploaded artifacts in maven-central never become canonical because the deployment never transitioned to "Validated → Released".
- Resolve the underlying error (signing failure, namespace permission lapse, etc.). Bump the patch version and push a new tag — there is no concept of "rerun the failed publish"; you publish a new version.
- If the failed deployment was a snapshot, it is silently overwritten by the next snapshot publish to the same coords; no manual cleanup is needed.