# v0.4.6 (7 June 2026) {#v046-7-june-2026}

[v0.4.6 on GitHub](https://github.com/higher-kinded-j/higher-kinded-j/releases/tag/v0.4.6)

**Optic-Driven Request Batching**

This release adds the user-facing surface around the `org.higherkindedj.optics.fetch` substrate so an optic traversal of N foci collapses to one batched backend call by swapping the strategy passed to `Optic.modifyF`. The optic core is untouched; the package is now exported and documented, the heterogeneous `Id -> Entity` case has a dedicated helper, and run-time failures land on the value channel as `Either` rather than as exceptions.

- [Optic-Driven Batching](../optics/optic_batching.md): New chapter under Optics / Integration and Recipes, with three diagrams (N+1 vs batched, the optic + applicative + runner pipeline, the applicative-versus-`flatMap` round cost) and a language-agnostic primer link for the DataLoader/Haxl idea. `org.higherkindedj.optics.fetch` is now exported from the core module; `Done`/`Blocked`/`PendingKeys` remain package-private so consumers interact with `Fetch` only through its static factories and runners
- `SafeFetch`: Total runner that captures resolver exceptions, missing-key reports, loader failures, and deadlines as `Either.left` values on the value channel instead of thrown exceptions; `SafeFetch.runCached`, `runAsync`, and `runAsyncWithTimeout` never throw and the safe-async future never completes exceptionally. `SafeFetch.partition` splits a per-key `Either<E, V>` result list into aligned successes and failures so a partial-success batch is preserved end-to-end
- `SourceRouter.routed`: Composes per-source `BatchLoader`s with a classifier into one loader the substrate can call; one round fans out to one concurrent dispatch per source, so a list mixing user ids and product skus produces exactly one call per backend, not one call per key
- `BatchLoaders.chunked(loader, maxSize)`: Caps a single dispatch's size for backends that enforce a per-request limit (`$in` clause cap, HTTP query-string ceiling, GraphQL batch limit); the substrate still sees one round and the loader splits the keyset behind the curtain
- `FetchOptics.fetchEach(source, rebuild)`: The type-changing list-traversal the codegen does not produce (codegen optics are type-preserving, so a `Traversal<Team, UserId>` cannot directly describe loading each `UserId` into a `User`); builds an `Optic<S, T, A, B>` from a list-reader and a rebuild function so heterogeneous fetch composes with the rest of the optic graph
- Tutorial 21: New tutorial journey (exercise + teaching-solution) covering the four pieces (same-type batching, heterogeneous fetch, multi-source routing, railway errors) with five exercises and tiered hints; the solution carries the *Why this is idiomatic / Alternative / Common wrong attempt* commentary per exercise
- Architecture-rule update: The `optics.fetch` package is exempted from the "no specific HKT type dependencies" rule alongside the existing `optics.util`, `optics.extensions`, and `optics.fluent` exemptions, on the same basis: `SafeFetch`'s railway runner is built around `Either` by design

**Plan Introspection and Guardrails for Optic Batching**

The audit and safety-rail layer on top of [Optic-Driven Batching](../optics/optic_batching.md): a way to fold a `Fetch` program into a structural plan without I/O, and a per-round guard that interposes between the program and its resolver to refuse runaway batches before they leave the JVM. Both compose with `SafeFetch` so refusal is a value, not a thrown exception.

- [Plan Introspection and Guardrails](../optics/optic_batching_guardrails.md): New chapter under Optics / Integration and Recipes covering the offline `Plans.preflight` walk, the per-round `Guard` family, and the railway-safe refusal pattern.
- `Plans.preflight`: Folds a `Fetch` program into a `Plan<K>` with zero I/O. Each round's keyset is recorded in dispatch order; round 1 is universally observable, and later rounds are walked on stub values when the program's combine logic tolerates `null` (a `Plan.truncated()` flag is the honest signal otherwise).
- `Guards.maxKeysPerRound` / `maxRounds` / `maxBackendCalls` / `audit` / `none`: Standard guards that pass or refuse a round at the runner boundary; compose with `Guard::and`. A refusal aborts the run with `GuardViolationException` carrying the offending `roundIndex` and `pendingKeys`.
- `Guards.runCached` / `runAsync`: Drop-in replacements for the substrate runners with the guard interposed; the resolver is never called for a refused round.
- `SafeFetch.runCachedWithGuard` / `runAsyncWithGuard`: Railway variants that capture a refusal as `Either.left(GuardViolationException)`; the run never throws and the safe-async future never completes exceptionally.
- Tutorial 22 (exercise + teaching solution) covering the five pieces (preflight, truncation, refusal, audit, railway capture).

**Test-suite consolidation (internal)**

Mostly an internal refactor of the hkj-core test suite The one user-visible piece is in `hkj-test`: new reusable law helpers (`FunctorLaws`, `ApplicativeLaws`, `MonadLaws`, `SelectiveLaws`) and a `KindEquivalence.byEqualsAfter` helper that downstream users can call to verify their own type-class instances. The Kind-accepting overloads on `EitherAssert` / `MaybeAssert` / `TryAssert` / `IOAssert` / `LazyAssert` / `ReaderAssert` / `ValidatedAssert` / `WriterAssert` / `VStreamAssert` / `VTaskAssert` now match the auto-narrowing pattern of `ListAssert` / `OptionalKindAssert` / `StreamAssert` / `IdAssert`.

**N-ary Coupled Lenses**

The arity ladder above `Lens.paired`: a record with three or more cross-field invariants can now be updated atomically through a single `coupled3`..`coupled9` call instead of nested `paired` workarounds.

- [Coupled Fields chapter](../optics/coupled_fields.md): "Three or More Coupled Fields" section rewritten to show the new ladder; the old "nest pairs / feature request" guidance is replaced.
- `CoupledLenses.coupled3` ... `coupled9` (in `org.higherkindedj.optics.util`): seven static factories, each with two overloads mirroring `Lens.paired` exactly (preserving form taking `(S, A, B, ...) -> S`; simple form taking the constructor reference `(A, B, ...) -> S`). Returns `Lens<S, TupleN<...>>` reconstructed atomically.
- `hkj-processor` adds `CoupledLensGenerator`, wired into the existing `@GenerateForComprehensions` trigger alongside the Tuple/For-step generators. Generation caps at arity 9 (cross-field invariants past that point are vanishing in practice); raising the cap is a one-line change to the generator.
- Tutorial 23 (exercise + teaching solution) demonstrating `coupled3` on a 3-field monotonic invariant and `coupled5` on the same shape at higher arity, including the canonical "chained set throws" failure mode that coupled lenses sidestep.

**API Deprecations Ahead of 0.5.0**

This release prepares users for a record-shape change to `StateT` that lands in 0.5.0. The single-argument runner methods and the explicit `monadF()` accessor are deprecated now so call sites can migrate ahead of time; the record component itself stays in place until 0.5.0.

- `StateT` runner methods accept an explicit `Monad<F>`: new overloads `StateT.evalStateT(state, monad)` and `StateT.execStateT(state, monad)` use the supplied monad rather than the one stored on the record. The matching helpers `StateTKindHelper.evalStateT(kind, state, monad)` and `execStateT(kind, state, monad)` are added on the same shape. New code should prefer these overloads; the single-argument forms are deprecated for removal in 0.5.0 ([#445](https://github.com/higher-kinded-j/higher-kinded-j/issues/445))
- `StateT.monadF()` deprecation: the explicit accessor is now `@Deprecated(forRemoval = true)`. In 0.5.0 the `monadF` record component itself is removed so that two `StateT` values with the same state function are considered equal regardless of which `Monad` instance they were constructed with. `equals`, `hashCode`, and `toString` therefore change in 0.5.0; until then the record-generated implementations still discriminate on the stored monad, which is the underlying defect the deprecation is staging
- Internal call sites in `MutableContext.evalWith` and `execWith` are migrated to the new two-argument overloads; library builds emit no deprecation warnings of their own

**`Try.fold` and `TryPath.fold` argument-order rename ahead of 0.5.0**

`Try.fold(successMapper, failureMapper)` and `TryPath.fold(successMapper, failureMapper)` are the two surfaces whose `fold` is *success-first*, against the error-first convention used by `Either.fold`, `Validated.fold`, `EitherF.fold`, `EitherPath.fold`, and `ValidationPath.fold`. A naked argument swap on `fold` would silently invert behaviour for the common case of parameter-ignoring lambdas (`v -> v`, `v -> null`, `Throwable::getMessage`) and for `Object`-typed method references, because both `Function` parameters have different generic bounds but lambdas often resolve to either. The fix is published under a distinct name so the change becomes a compile error rather than a runtime inversion.

- `Try.foldFailureFirst(failureMapper, successMapper)` and `TryPath.foldFailureFirst(failureMapper, successMapper)` are the canonical error-first replacements, added since 0.4.6. Their argument order matches `Either.fold` / `Validated.fold` / `EitherF.fold` / `EitherPath.fold` / `ValidationPath.fold`. The methods are named `foldFailureFirst` rather than overloading `match` (which would create a lambda-inference ambiguity with the existing `Try.match(Consumer, Consumer)`) or `fold` (which would silently invert behaviour for downstream call sites).
- `Try.fold(successMapper, failureMapper)` and `TryPath.fold(successMapper, failureMapper)` are now `@Deprecated(forRemoval = true)` and are removed in 0.5.0. Internal call sites across `hkj-core` (`TryApplicative`, `TryTraverse`, `TryPath`, `PathOps`, `VTaskContext`, `LensExtensions`, `Affines`), `hkj-test` (`VTaskPathAssert`, `VTaskContextAssert`), `hkj-spring` return-value handlers, `hkj-processor-plugins` (the `TryGenerator` annotation-processor template), and every runnable example, tutorial, and solution are migrated to `foldFailureFirst`; library builds emit no deprecation warnings of their own.
- The canonical name `fold` is planned to be reintroduced on both `Try` and `TryPath` with the error-first argument order in 0.6.0, once 0.5.0 has removed the success-first `fold` and every reachable call site has been forced through the renamed method. See [#452](https://github.com/higher-kinded-j/higher-kinded-j/issues/452) for the design rationale and the follow-up issue tracking the 0.6.0 reintroduction.
- OpenRewrite migration: a new `SwapTryFoldToFoldFailureFirstRecipe` is added to the existing `MigrateDeprecationsTo0_5_0` recipe group in `hkj-openrewrite`. The recipe matches `Try.fold(successMapper, failureMapper)` and `TryPath.fold(successMapper, failureMapper)` call sites and rewrites them to `foldFailureFirst(failureMapper, successMapper)`, atomically renaming the method and swapping the two arguments. It cannot be expressed as a stock `ChangeMethodName` invocation because the argument order changes; a naked rename would silently invert behaviour. Downstream consumers run `org.higherkindedj.openrewrite.MigrateDeprecationsTo0_5_0` to migrate `Try.fold`, `TryPath.fold`, `StateTKind.narrowK`, and `KindValidator.narrowWithPattern` in one pass.

---

**Previous:** [v0.4.7](v0_4_7.md)
**Next:** [v0.4.5](v0_4_5.md)
