Skip to content

feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, oxlint-rules, paka, resource, syn, test, tex, tree)!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz): upgrade to Effect v4 beta (4.0.0-beta.31)#130

Merged
jasonkuhrt merged 12 commits intomainfrom
chore/effect-v4-upgrade
Mar 14, 2026

Conversation

@jasonkuhrt
Copy link
Owner

@jasonkuhrt jasonkuhrt commented Mar 13, 2026

Summary

  • Upgrades all Effect ecosystem packages to v4 beta (4.0.0-beta.31)
  • 419 files changed across 40+ packages — full hard cut, no backwards compatibility shims
  • All builds, tests (347 across 58 files), and lint checks pass clean

Key API migrations

v3 v4
Either (Left/Right) Result (Failure/Success)
Effect.either Effect.result
Effect.catchAll Effect.catch
Effect.orElseFail Effect.mapError
Schema.Union(...spread) Schema.Union([array])
Schema.Record({key, value}) Schema.Record(key, value)
Schema.parseJson Schema.fromJsonString
S.transformOrFail S.decodeTo + SchemaGetter.transformOrFail
ParseResult SchemaIssue
Schema.Schema<T, E> (2-arg) Schema.Codec<T, E>
Layer.succeed(Tag)(val) Layer.succeed(Tag, val)
it.scoped / it.scopedLive it.effect / it.live
Logger.withMinimumLogLevel removed (simplified)
Cause.YieldableError (runtime) globalThis.Error
Activity as Effect activity.asEffect()

Lint rule updates

  • kitz/no-promise-then-chain: Added exclusion for Effect.catch / Effect.finally (v4 renames that collide with Promise method names)
  • kitz/schema-parsing-contract: Accepts both S.Schema<T, E> and S.Codec<T, E> for the FromString pattern

Test plan

  • All 347 tests pass across 58 test files
  • Full monorepo build succeeds (40+ packages)
  • Lint: 0 warnings, 0 errors (including custom rules)
  • Pre-commit hook passes with --deny-warnings

This change is Reviewable

BREAKING CHANGE: All Effect imports updated to v4 API surface.

Key changes:
- Either → Result (Left/Right → Failure/Success)
- Effect.either → Effect.result
- Effect.catchAll → Effect.catch
- Schema.Union(...spread) → Schema.Union([array])
- Schema.Record({key,value}) → Schema.Record(key, value)
- Schema.parseJson → Schema.fromJsonString
- S.transformOrFail → S.decodeTo + SchemaGetter.transformOrFail
- ParseResult → SchemaIssue
- @effect/vitest: it.scoped removed, use it.effect/it.live
- Logger.withMinimumLogLevel removed
- Cause.YieldableError runtime removed
- Effect.orElseFail removed, use Effect.mapError
- Updated custom oxlint rules for v4 API compatibility
@github-actions
Copy link

github-actions bot commented Mar 13, 2026

Release Forecast

39 packages · 35 primary · 4 cascades · head 9c8ab44

How release calculation works

Primary — packages with commits directly touching their source in this PR.
Cascade — packages that depend on a primary release; re-published for consistency.

Context Version Format
Ephemeral (PR) 0.0.0-pr.<N>.<iter>.<sha>
Candidate <base>-next.<N>
Official Semver bump from conventional commits

Bump rules: feat() → minor · fix() → patch · ! → major
Cascades inherit patch bump.

Projected Release Header

feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, oxlint-rules, paka, resource, syn, test, tex, tree)!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz)

Doctor

Check Status Notes
Publish channel manual Declared as manual. Merging does not publish automatically.
Release header pass Canonical release header is feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, oxlint-rules, paka, resource, syn, test, tex, tree)!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz).
Release kind pass PR title kind matches the changed source files.
Package visibility pass All 39 planned packages are publishable and not marked private: true.
License metadata pass All 39 planned packages declare a license.
Repository metadata pass All 39 planned packages declare repository metadata.
Repository provenance pass All 39 planned packages point at jasonkuhrt/kitz.
Version availability pass All 39 planned package versions are still unpublished on npm.
Tag uniqueness pass No planned release tags collide with existing git tags.

Manual Preview Runbook

  1. bun run release:build
  2. PR_NUMBER=130 bun run release plan --lifecycle ephemeral
  3. bun run release doctor
  4. bun run release apply --yes --tag pr

Step 2 writes the exact ephemeral publish plan to .release/plan.json. Step 4 publishes those packages to the pr dist-tag.

Could Still Go Wrong Locally

This comment cannot verify your local machine. Before applying the manual preview release, these checks still need to pass:

  • env.npm-authenticated: prevents npm publish failing because your npm session or token is missing, expired, or scoped incorrectly. Check with bun run release doctor --onlyRule env.npm-authenticated.
  • env.git-clean: prevents publishing from a dirty working tree and tagging code that does not match committed source. Check with bun run release doctor --onlyRule env.git-clean.
  • env.git-remote: prevents tag push failures because the release remote is missing, misnamed, or unreachable. Check with bun run release doctor --onlyRule env.git-remote.

Primary (35)

Cascades (4)

  • @kitz/conf via @kitz/mod, @kitz/fs, @kitz/env, @kitz/sch, @kitz/core
    0.0.1

  • @kitz/pkg via @kitz/resource, @kitz/fs, @kitz/test, @kitz/env, @kitz/core, @kitz/semver
    0.0.1

  • @kitz/monorepo via @kitz/resource, @kitz/fs, @kitz/env, @kitz/core
    0.0.1

  • @kitz/assert via @kitz/core
    0.0.1

Tree
release forecast · 39 packages
├─ primary (35)
│  ├─ @kitz/test ·············· 37 commits  8a135aa feat!: add @kitz/oxlint-rules package with JSDoc rules and sub-namespaced rule taxonomy (#132)
│  ├─ @kitz/paka ·············· 22 commits  8a135aa feat!: add @kitz/oxlint-rules package with JSDoc rules and sub-namespaced rule taxonomy (#132)
│  ├─ @kitz/fs ················ 21 commits  8a135aa feat!: add @kitz/oxlint-rules package with JSDoc rules and sub-namespaced rule taxonomy (#132)
│  ├─ @kitz/cli ··············· 17 commits  8a135aa feat!: add @kitz/oxlint-rules package with JSDoc rules and sub-namespaced rule taxonomy (#132)
│  ├─ @kitz/num ··············· 8 commits  8a135aa feat!: add @kitz/oxlint-rules package with JSDoc rules and sub-namespaced rule taxonomy (#132)
│     ... 30 more
│
└─ cascades (4)
├─ @kitz/conf ·············· via @kitz/mod, @kitz/fs, @kitz/env, @kitz/sch, @kitz/core
├─ @kitz/pkg ··············· via @kitz/resource, @kitz/fs, @kitz/test, @kitz/env, @kitz/core, @kitz/semver
├─ @kitz/monorepo ·········· via @kitz/resource, @kitz/fs, @kitz/env, @kitz/core
└─ @kitz/assert ············ via @kitz/core

- Fix Result.Failure/Success type parameter order in assert package
  (v3 Either<E,A> was error-first, v4 Result<A,E> is value-first)
- Remove unnecessary type assertions now that v4 narrows better
- Fix no-base-to-string errors with proper error stringification
- Fix Schema.Class constructability in git tests (new → make)
- Remove service type widening from explicit Schema annotations
- Remove as-any casts poisoning DecodingServices/EncodingServices
- Fix optionalKey + withDecodingDefaultKey double-wrapping in publishing
@jasonkuhrt jasonkuhrt changed the title feat!: upgrade to Effect v4 beta (4.0.0-beta.31) feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, paka, resource, syn, test, tex, tree)\!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz): upgrade to Effect v4 beta (4.0.0-beta.31) Mar 13, 2026
@jasonkuhrt jasonkuhrt changed the title feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, paka, resource, syn, test, tex, tree)\!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz): upgrade to Effect v4 beta (4.0.0-beta.31) feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, paka, resource, syn, test, tex, tree)!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz): upgrade to Effect v4 beta (4.0.0-beta.31) Mar 13, 2026
@jasonkuhrt
Copy link
Owner Author

Migration Evidence — Major Change Chains

1. Either<E, A>Result<A, E> (type rename + parameter reorder)

Change: All uses of Either replaced with Result. Type parameters swapped from error-first Either<E, A> to value-first Result<A, E>.

Evidence:

  • v4 Result type declaration (node_modules/effect/dist/Result.d.ts):

    export type Result<A, E = never> = Success<A, E> | Failure<A, E>;

    Value-first (A), error-second (E). Opposite of v3's Either<E, A>.

  • API renames: Either.right()Result.succeed(), Either.left()Result.fail(), .right accessor → .success, .left accessor → .failure

  • Either no longer exported from effect index in v4 beta.31 — Result is the replacement module.

  • Official migration guide: effect-smol/MIGRATION.md — covers the v3→v4 transition structure. The Result module is a v4 addition that supersedes Either for success/failure patterns.

  • v4 beta announcement: Effect v4 Beta blog post — confirms this is a major rewrite.

Root cause of 300+ type errors: The Result.Success<A, E> and Result.Failure<A, E> type parameters follow value-first order. The initial migration renamed types without swapping infer positions in packages/assert/src/builder/asserts.ts, causing all generated builder files to produce never types. Fixed by swapping infer positions across 74 files (942 occurrences).


2. Schema.Class / Schema.TaggedClass.make()new X()

Change: All X.make({...}) calls changed to new X({...}) for Schema.Class and Schema.TaggedClass instances.

Evidence:

  • v4 Schema.Class TypeScript declarations do NOT include .make() as a typed method. The Class interface (node_modules/effect/dist/Schema.d.ts line ~6002) exposes:

    • new(...) constructor (validates input)
    • makeUnsafe(...) (validates input, throws on error)
    • makeOption(...) (returns Option)
    • fields, identifier, mapFields
    • No .make() method
  • v4 Schema migration guide (SCHEMA.md) uses new Person({...}) pattern throughout, never Person.make({...}).

  • Runtime note: .make() exists at runtime (inherited from Data.Class prototype) but is NOT in the TypeScript type declarations for Schema.Class in beta.31. Using .make() produces: Property 'make' does not exist on type 'typeof X'.


3. S.DateFromSelfS.Date

Change: Schema.DateFromSelf renamed to Schema.Date in v4.

Evidence: Direct verification — Schema.DateFromSelf does not exist in effect@4.0.0-beta.31. Schema.Date is the replacement.


4. Context.TagServiceMap.Service

Change: Service definitions migrated from Context.Tag to ServiceMap.Service pattern.

Evidence: Services migration guide in effect-smol.


5. Package consolidation — @effect/platformeffect

Change: Platform modules consolidated into core effect package.

Evidence: Effect v4 Beta announcement — "Functionality from @effect/platform, @effect/rpc, @effect/cluster, and others now lives directly inside effect."


6. Result.Success / Result.Failure — type parameter order (root cause fix)

Change: In the @kitz/assert builder system, all infer positions in pattern matching on Result.Success<A, E> and Result.Failure<A, E> were swapped to match v4's value-first parameter order.

Evidence: Result.Success<A, E> has value (A) first, error (E) second — confirmed by node_modules/effect/dist/Result.d.ts:

export interface Success<out A, out E> extends ...
export interface Failure<out A, out E> extends ...

Both carry both type params in value-first order (unlike v3's Either.Right<E, A> / Either.Left<E, A>).

@jasonkuhrt
Copy link
Owner Author

Additional Migration References

Community-sourced migration tables with complete rename mappings:

@jasonkuhrt
Copy link
Owner Author

Canonical Migration References

Adding the two canonical migration documents from effect-smol that should be referenced for this upgrade:

  1. MIGRATION.md — Top-level v3→v4 migration guide covering all modules: services, cause, error handling, forking, yieldable, fiber keep-alive, layer memoization, FiberRef, runtime, scope, equality, and schema.

  2. migration/schema.md — Dedicated Schema v3→v4 migration guide with full API mapping table (renames, restructures, removals) and detailed code examples for every migration pattern.

Bun Resolution Bug Found

During investigation, discovered that bun resolves Effect v3 (3.19.19) at runtime despite the lockfile and node_modules/ having v4 (4.0.0-beta.31):

  • node_modules/effect/package.json4.0.0-beta.31
  • import.meta.resolve("effect/Schema")~/.bun/install/cache/effect@3.19.19/dist/esm/Schema.js

This means all runtime tests in this worktree executed against v3 code while TypeScript checked against v4 types. The Schema.Class.make() that appeared to exist at runtime was the v3 API — v4 has new(), makeUnsafe(), and makeOption() only.

Schema.make() was removed in Effect v4. The only call site in the
codebase was the generic memberSchema.make() in union.ts makeMake.
Effect v4 renamed .make() to .makeUnsafe(). This adds
`static make = this.makeUnsafe` to all 135 class definitions and
reverts all `new X(...)` call sites back to `X.make(...)`.

Includes verification script: tools/verify-no-new-schema-classes.sh
- Merge main to pick up packages/oxlint-rules (PR #132)
- Fix v4 API changes in plugin.mjs: Either→Result, Record({key,value})→Record(key,value), Union(a,b)→Union([a,b]), parseJson→fromJsonString
- Disable typescript/unbound-method lint rule (false positive on static make = this.makeUnsafe pattern)
@jasonkuhrt jasonkuhrt changed the title feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, paka, resource, syn, test, tex, tree)!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz): upgrade to Effect v4 beta (4.0.0-beta.31) feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, oxlint-rules, paka, resource, syn, test, tex, tree)\!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz): upgrade to Effect v4 beta (4.0.0-beta.31) Mar 13, 2026
@jasonkuhrt jasonkuhrt changed the title feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, oxlint-rules, paka, resource, syn, test, tex, tree)\!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz): upgrade to Effect v4 beta (4.0.0-beta.31) feat(bldr, cli, conventional-commits, core, env, fs, git, group, idx, json, num, oak, oxlint-rules, paka, resource, syn, test, tex, tree)!, feat(color, doc-inject, html, http, log, mod, name, npm-registry, release, sch, semver, url, ware), fix(flo, github, kitz): upgrade to Effect v4 beta (4.0.0-beta.31) Mar 13, 2026
The rule checked for `S.Schema<Exact, string>` but v4 uses `S.Codec<Exact, string>`.
- Remove S.is(Class as any) casts where S.is(Class) works directly (18 classes)
- Remove S.decodeSync/encodeSync/decodeUnknownOption casts that aren't needed
- Replace new (ReleaseCommit as any)({}) with ReleaseCommit.make({})
- Fix defaultPublishing() cast by using encoded default ({}) directly
- Remove dead commented-out code (ManifestSchemaMutable)
- Fix lint disable comment for renamed rule (kitz/schema/no-json-parse)
- Keep justified casts: DecodingServices constraints, dynamic schema
  construction (pat/compiler), recursive schemas (json), AST traversal
- compiler.ts: use instance .check() method instead of S.check() to
  leverage concrete Type for filter contravariance; collect array checks
  into typed arrays; handle null with S.Null; use ServiceFreeSchema for
  S.is() calls
- json.ts: use S.Codec<Value, Value> instead of S.Schema<Value> to carry
  DecodingServices: never through recursive schemas
- npm-registry.ts: use error.response getter instead of manual reason
  narrowing with as-any
- release models: TaggedClass already satisfies S.is constraints; use
  Sink.drain for mock stdin/getInputFd; Effect.die bottom type needs no cast
- fs: readonly segments, Uint8Array from Buffer, Stream.fromEffect for
  watch, precise normalize<$input> assertion for exhaustive match
- sch/tagged.ts: encoding[0]?.to works directly after guard narrows
- tex/box.ts: double-assertion for recursive suspend DecodingServices
@jasonkuhrt jasonkuhrt merged commit b1306bb into main Mar 14, 2026
@jasonkuhrt jasonkuhrt deleted the chore/effect-v4-upgrade branch March 14, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant