Skip to content

feat: effect 4 rewrite - #7

Merged
tbinna merged 1 commit into
mainfrom
feat-effect-4-rewrite
Aug 12, 2026
Merged

feat: effect 4 rewrite#7
tbinna merged 1 commit into
mainfrom
feat-effect-4-rewrite

Conversation

@tbinna

@tbinna tbinna commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added Effect 4-based APIs for JSON changesets, changelogs, stored-value evolution, and versioned schemas.
    • Added strict JSON validation, immutable migrations, unversioned initialization, and typed evolution errors.
    • Added support for JSON Patch and immutability-helper changesets.
  • Documentation

    • Added Effect 4 usage guidance, terminology, architecture decisions, and a v1-to-v2 migration guide.
  • Packaging

    • Updated the package for ESM-only usage and modern Node.js environments.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@tbinna, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 74ca340c-7c02-485e-9d6c-a3a3f78a890f

📥 Commits

Reviewing files that changed from the base of the PR and between 813d25f and dec7586.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • docs/migration-v2.md
  • src/Changelog.ts
  • test/Changelog.spec.ts
  • test/StoredValue.spec.ts
📝 Walkthrough

Walkthrough

The package is rewritten for Effect 4 and ESM-only publication. It adds validated changesets, changelogs, stored values, evolution, versioned schemas, typed errors, tests, documentation, package smoke tests, and updated CI and release workflows.

Changes

Effect 4 evolution API

Layer / File(s) Summary
Evolution domain contracts
src/Changeset.ts, src/Changelog.ts, src/StoredValue.ts, src/EvolutionError.ts, test/Changelog.spec.ts, test/StoredValue.spec.ts, test/EvolutionError.spec.ts
Adds runtime schemas and types for changesets, changelogs, strict stored values, and tagged evolution errors.
Evolution and versioned schemas
src/Evolution.ts, src/VersionedSchema.ts, src/index.ts, test/Evolution.spec.ts, test/VersionedSchema.spec.ts, test/Compatibility.spec.ts, test/fixtures/configuration.ts, test/type-contracts.ts
Adds Effect-based evolution, unversioned initialization, version-marker enforcement, versioned encoding and decoding, explicit exports, compatibility fixtures, and type contracts.

Packaging and project operations

Layer / File(s) Summary
ESM packaging and quality gates
package.json, scripts/build.mjs, scripts/package-smoke.mjs, tsconfig*.json, vitest.config.ts, .github/workflows/*, .releaserc
Switches the package to ESM and Vitest, adds build and package smoke tests, updates TypeScript settings, and limits releases to main.

Documentation and migration

Layer / File(s) Summary
Documentation and migration decisions
CONTEXT.md, README.md, docs/adr/*, docs/migration-v2.md, docs/plans/effect-v4-rewrite.md
Documents the Effect 4 API, stored-value protocol, versioning rules, adapter behavior, error model, ESM packaging, migration steps, and release decisions.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Consumer
  participant evolve
  participant ChangesetAdapters
  participant VersionedSchema
  Consumer->>evolve: Provide stored input
  evolve->>ChangesetAdapters: Apply pending changesets
  ChangesetAdapters-->>evolve: Return versioned stored value
  evolve->>VersionedSchema: Decode evolved value
  VersionedSchema-->>Consumer: Return application value
Loading

Possibly related PRs

  • toolsplus/json-evolutions#4: Adds earlier changelog validation and Node 24/toolchain modernization that this rewrite replaces with the Effect 4 implementation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request's main change: rewriting the package for Effect 4.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (4)
package.json (1)

35-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Lint Node scripts with Node globals.

Add scripts to both lint commands. Add a scripts/**/*.mjs ESLint block with globals.node. Limit globals.vitest to test files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 35 - 36, Update package.json scripts "fix:lint"
and "lint" to include the scripts directory. In eslint.config.mjs, add a
scripts/**/*.mjs configuration block with globals.node, and restrict
globals.vitest to test-file patterns only. Affected sites: package.json lines
35-36 require both command updates; eslint.config.mjs line 34 requires the
ESLint glob and global-scope changes.

Source: Linters/SAST tools

src/VersionedSchema.ts (2)

10-13: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

The encoded type is wider than the runtime schema.

VersionedEncoded declares _version: number, but the stored struct uses Schema.Literal(latest). A caller can build an encoded value with any version number and satisfy the type, then fail at decode time. If this widening is deliberate for stored-protocol compatibility, document the reason next to VersionedEncoded.

Also applies to: 48-51

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/VersionedSchema.ts` around lines 10 - 13, Align VersionedEncoded with the
runtime schema by constraining _version to the accepted Schema.Literal version
type instead of any number, so encoded values cannot claim unsupported versions;
if broad numeric compatibility is intentional, document that rationale directly
beside VersionedEncoded.

66-69: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

evolveAndDecode fails obscurely if the metadata symbol is missing.

Object.defineProperty attaches the changelog to one specific schema instance. Any later transformation or a cast produces a schema without the symbol. schema[versionedSchemaMetadata] is then undefined, and evolve calls latestVersion(undefined), which throws a TypeError outside the Effect error channel. Add an explicit check with a clear message.

🛡️ Proposed guard
     ): Effect.Effect<
         Schema.Struct.Type<Fields>,
         EvolutionError | Schema.SchemaError,
         R | Schema.Struct.DecodingServices<Fields>
-    > =>
-        evolve(
-            schema[versionedSchemaMetadata],
-            options,
-        )(input).pipe(Effect.flatMap(Schema.decodeUnknownEffect(schema)));
+    > => {
+        const changelog = schema[versionedSchemaMetadata];
+        if (changelog === undefined) {
+            throw new TypeError(
+                "evolveAndDecode requires a schema created with versioned(changelog).",
+            );
+        }
+        return evolve(changelog, options)(input).pipe(
+            Effect.flatMap(Schema.decodeUnknownEffect(schema)),
+        );
+    };

Also applies to: 85-88

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/VersionedSchema.ts` around lines 66 - 69, Add an explicit guard in
evolveAndDecode before using schema[versionedSchemaMetadata], and likewise in
the corresponding path around the later referenced lines, to detect missing
metadata and fail with a clear message through the existing Effect error
channel. Ensure latestVersion is never called with undefined, while preserving
normal behavior for schemas carrying the metadata.
src/StoredValue.ts (1)

59-63: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Array elements skip the data-property check that object properties receive.

The object branch verifies each own key is an enumerable data property through Object.getOwnPropertyDescriptor. The array branch reads elements with value.every(...), which invokes accessors. An array with an index defined as a getter therefore passes validation, but it is not a plain JSON array. Consider applying the same descriptor check to array indexes.

♻️ Proposed alignment of the array branch
     if (Array.isArray(value)) {
         valid =
             Object.getPrototypeOf(value) === Array.prototype &&
             hasOnlyArrayIndexes(value) &&
-            value.every((item) => isJsonValueAt(item, ancestors));
+            value.every((_item, index) => {
+                const descriptor = Object.getOwnPropertyDescriptor(
+                    value,
+                    index,
+                );
+                return (
+                    descriptor !== undefined &&
+                    "value" in descriptor &&
+                    descriptor.enumerable &&
+                    isJsonValueAt(descriptor.value, ancestors)
+                );
+            });
     } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/StoredValue.ts` around lines 59 - 63, Update the array validation branch
in isJsonValueAt to inspect each array index with its own property descriptor
before validating the element, matching the object-property data-descriptor
check and rejecting accessor-based indexes. Replace the value.every traversal
with descriptor-based validation while preserving the existing prototype,
index-only, and recursive isJsonValueAt checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 23-25: Add a workflow-level permissions configuration in the CI
workflow granting only read access to repository contents, such as contents:
read, so the typecheck, coverage, and smoke-package jobs cannot use a
write-capable GITHUB_TOKEN.

In `@CONTEXT.md`:
- Around line 7-8: Update the “Stored value” definition in CONTEXT.md to state
that _version is an own property whose value is a non-negative safe integer,
matching the guarantees documented in README.md and
docs/plans/effect-v4-rewrite.md.

In `@docs/adr/0005-reserve-the-version-marker.md`:
- Around line 1-3: Update the ADR’s root-marker contract to reserve only the
root-level _version property, explicitly allow nested business properties named
_version, and include test among the prohibited JSON Patch operations targeting
the root marker. Align the wording with the implementation and migration guide.

In `@docs/adr/0007-release-v2-without-a-prerelease-line.md`:
- Around line 1-3: Update the release documentation around the v2 baseline to
require creating the missing Git tag v1.0.3 on the commit that produced package
version 1.0.3 before publishing, ensuring semantic-release calculates the
breaking change as stable 2.0.0.

In `@docs/migration-v2.md`:
- Around line 27-35: Update the legacy example around Configuration and
createChangelog to define the v1 changeset before it is passed to
createChangelog, using the documented changeset declaration from the migration
API. Keep the example copy-pastable and leave the subsequent changelog,
versioned configuration, and decode flow unchanged.

In `@src/Changelog.ts`:
- Around line 83-85: Update src/Changelog.ts lines 83-85 in createChangelog to
defensively copy and recursively freeze the normalized changesets, including
each changeset’s _version and JSON Patch operations, before branding and
returning ValidChangelog. Add tests in test/Changelog.spec.ts lines 29-43
confirming mutations to the original changeset or patch after construction do
not alter the validated changelog.
- Around line 19-25: Update ownsRootVersionMarker and its use in
violatesMarkerOwnership so the empty JSON Pointer "" is treated as a
reserved-marker ownership violation for both operation.path and operation.from,
while preserving existing /_version handling. Add root-pointer test cases in
test/Changelog.spec.ts lines 95-110 covering path and from operations.

In `@test/StoredValue.spec.ts`:
- Around line 26-31: Update the parameter cases in the non-object-root test
around the it.each call so every input is wrapped in a single-element tuple,
including the empty and populated array roots. Preserve the existing rejects a
non-object root assertion while ensuring decodes receives each complete root
value.

---

Nitpick comments:
In `@package.json`:
- Around line 35-36: Update package.json scripts "fix:lint" and "lint" to
include the scripts directory. In eslint.config.mjs, add a scripts/**/*.mjs
configuration block with globals.node, and restrict globals.vitest to test-file
patterns only. Affected sites: package.json lines 35-36 require both command
updates; eslint.config.mjs line 34 requires the ESLint glob and global-scope
changes.

In `@src/StoredValue.ts`:
- Around line 59-63: Update the array validation branch in isJsonValueAt to
inspect each array index with its own property descriptor before validating the
element, matching the object-property data-descriptor check and rejecting
accessor-based indexes. Replace the value.every traversal with descriptor-based
validation while preserving the existing prototype, index-only, and recursive
isJsonValueAt checks.

In `@src/VersionedSchema.ts`:
- Around line 10-13: Align VersionedEncoded with the runtime schema by
constraining _version to the accepted Schema.Literal version type instead of any
number, so encoded values cannot claim unsupported versions; if broad numeric
compatibility is intentional, document that rationale directly beside
VersionedEncoded.
- Around line 66-69: Add an explicit guard in evolveAndDecode before using
schema[versionedSchemaMetadata], and likewise in the corresponding path around
the later referenced lines, to detect missing metadata and fail with a clear
message through the existing Effect error channel. Ensure latestVersion is never
called with undefined, while preserving normal behavior for schemas carrying the
metadata.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d21fdd15-d7d3-4ef9-871d-c9c6aada1a48

📥 Commits

Reviewing files that changed from the base of the PR and between 2edb1db and 813d25f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (50)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .releaserc
  • CONTEXT.md
  • README.md
  • docs/adr/0001-target-effect-4-exclusively.md
  • docs/adr/0002-preserve-the-stored-value-protocol.md
  • docs/adr/0003-retain-source-defined-immutability-helper-changesets.md
  • docs/adr/0004-separate-evolution-from-business-schema-decoding.md
  • docs/adr/0005-reserve-the-version-marker.md
  • docs/adr/0006-publish-esm-only.md
  • docs/adr/0007-release-v2-without-a-prerelease-line.md
  • docs/adr/0008-derive-schema-version-from-the-changelog.md
  • docs/adr/0009-advance-the-version-marker-after-each-changeset.md
  • docs/migration-v2.md
  • docs/plans/effect-v4-rewrite.md
  • eslint.config.mjs
  • jest.config.js
  • package.json
  • scripts/build.mjs
  • scripts/package-smoke.mjs
  • src/Changelog.ts
  • src/Changeset.ts
  • src/Evolution.ts
  • src/EvolutionError.ts
  • src/StoredValue.ts
  • src/VersionedSchema.ts
  • src/api/Changeset.ts
  • src/api/EvolutionError.ts
  • src/api/index.ts
  • src/core/Evolutions.ts
  • src/core/index.ts
  • src/index.ts
  • test/Changelog.spec.ts
  • test/Compatibility.spec.ts
  • test/Evolution.spec.ts
  • test/EvolutionError.spec.ts
  • test/StoredValue.spec.ts
  • test/VersionedSchema.spec.ts
  • test/core/Evolutions.spec.ts
  • test/core/__fixtures__/configuration/v0.ts
  • test/core/__fixtures__/configuration/v1.ts
  • test/core/__fixtures__/configuration/v2.ts
  • test/fixtures/configuration.ts
  • test/type-contracts.ts
  • tsconfig.eslint.json
  • tsconfig.json
  • tsconfig.module.json
  • tsconfig.test.json
  • vitest.config.ts
💤 Files with no reviewable changes (11)
  • tsconfig.module.json
  • test/core/fixtures/configuration/v0.ts
  • src/core/index.ts
  • test/core/Evolutions.spec.ts
  • jest.config.js
  • src/api/Changeset.ts
  • src/core/Evolutions.ts
  • test/core/fixtures/configuration/v2.ts
  • test/core/fixtures/configuration/v1.ts
  • src/api/EvolutionError.ts
  • src/api/index.ts

Comment thread .github/workflows/ci.yml
Comment thread CONTEXT.md
Comment thread docs/adr/0005-reserve-the-version-marker.md
Comment thread docs/adr/0007-release-v2-without-a-prerelease-line.md
Comment thread docs/migration-v2.md
Comment thread src/Changelog.ts
Comment thread src/Changelog.ts
Comment thread test/StoredValue.spec.ts Outdated
@tbinna
tbinna force-pushed the feat-effect-4-rewrite branch from 813d25f to 629088f Compare August 12, 2026 06:05
BREAKING CHANGE: replace the io-ts/fp-ts and CommonJS APIs with an Effect 4-native ESM interface while preserving the stored-value protocol.
@tbinna
tbinna force-pushed the feat-effect-4-rewrite branch from 629088f to dec7586 Compare August 12, 2026 06:14
@tbinna
tbinna merged commit 6273cd7 into main Aug 12, 2026
5 checks passed
@tbinna
tbinna deleted the feat-effect-4-rewrite branch August 12, 2026 06:58
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 3.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant